index.jsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620
  1. import { Input, Button, message, Tooltip } from 'antd'
  2. import { connect, useModel } from 'umi'
  3. import React, { useEffect, useState, useRef, useMemo } from 'react'
  4. import consts from '@/consts'
  5. import LazyCascader, { formatValues } from '@/components/LazyCascader'
  6. import PersonLabel from '../Company/components/PersonLabel'
  7. import {
  8. TagTypeEnum,
  9. TagDataTypeEnum,
  10. LabelModeType
  11. } from '../Company/components/PersonLabel/const'
  12. import { Add, Down, Check } from '@icon-park/react'
  13. import { apiAddClient, queryClient } from '@/services/customer'
  14. import { apiBatchLink } from '@/services/customer'
  15. import styles from './index.less'
  16. import { generateFilterField, generateSortField } from '@/utils/utils'
  17. import { PermDataTypeEunm } from '../Company/components/PermSelect'
  18. import BasicTable from '@/components/Table'
  19. import { getPermAuthCache } from '@/utils/auth'
  20. import { useModal } from '@/components/ModalStore'
  21. import { Plus } from '@icon-park/react'
  22. const Client = props => {
  23. const dispatchModal = useModal()
  24. const {
  25. personTagColorMap,
  26. teamTagColorMap,
  27. personTags,
  28. teamTags,
  29. dispatch,
  30. shouldUpdate,
  31. loading,
  32. sourceList
  33. } = props
  34. const { initialState: { permData } = { permData: {} } } = useModel('@@initialState')
  35. const tRef = useRef()
  36. const [tagState, setTagState] = useState({
  37. tagIds: [],
  38. tagCooperationIds: []
  39. })
  40. const [state, setState] = useState({
  41. params: {},
  42. orderIds: []
  43. })
  44. const tag = useMemo(() => {
  45. const personTagMap = {}
  46. const teamTagMap = {}
  47. const personOptions = []
  48. const teamOptions = []
  49. personTags.forEach(item => {
  50. // personOptions.push({ label: item.name, value: item.id })
  51. personTagMap[item.id] = {
  52. text: (
  53. <>
  54. <span
  55. className="w-12px h-12px inline-block rounded-1/2 mr-3px"
  56. style={{ backgroundColor: personTagColorMap[item.id] }}
  57. />
  58. <span>{item.name}</span>
  59. </>
  60. )
  61. }
  62. })
  63. teamTags.forEach(item => {
  64. // teamOptions.push({ label: item.name, value: item.id })
  65. teamTagMap[item.id] = {
  66. text: (
  67. <>
  68. <span
  69. className="w-12px h-12px inline-block mr-3px"
  70. style={{ backgroundColor: teamTagColorMap[item.id] }}
  71. />
  72. <span>{item.name}</span>
  73. </>
  74. )
  75. }
  76. })
  77. return { personTagMap, teamTagMap, personOptions, teamOptions }
  78. }, [loading, shouldUpdate])
  79. // 默认刷新一次列表请求数据
  80. useEffect(() => {
  81. if (!sourceList?.length) {
  82. dispatch({
  83. type: 'client/fetchSourceList'
  84. })
  85. }
  86. if (!personTags.length) {
  87. dispatch({
  88. type: 'client/fetchPersonTags',
  89. payload: { tagType: TagTypeEnum.PERSONTAG, tagColumn: TagDataTypeEnum.CLIENT }
  90. })
  91. }
  92. if (!teamTags.length) {
  93. dispatch({
  94. type: 'client/fetchTeamTags',
  95. payload: { tagType: TagTypeEnum.TEAMTAG, tagColumn: TagDataTypeEnum.CLIENT }
  96. })
  97. }
  98. if (shouldUpdate) {
  99. tRef.current.reload()
  100. }
  101. }, [shouldUpdate])
  102. const toolbarOptionsChange = (type, ids) => {
  103. setTagState({ ...tagState, [type]: ids })
  104. }
  105. const [defaultPermAuthCache] = getPermAuthCache(PermDataTypeEunm.CUSTOMER)
  106. // 重置到默认值,包括表单
  107. const initData = () => {
  108. tRef.current?.reset()
  109. }
  110. // 刷新
  111. const refreshData = () => {
  112. tRef.current?.reload()
  113. }
  114. const handleAddClient = async values => {
  115. const payload = formatValues(values)
  116. const { code = -1, msg = '新增失败' } = await apiAddClient(payload)
  117. if (code === consts.RET_CODE.SUCCESS) {
  118. message.success(msg)
  119. initData()
  120. }
  121. return code === consts.RET_CODE.SUCCESS
  122. }
  123. const columns = [
  124. {
  125. title: '客户',
  126. dataIndex: 'clientName',
  127. key: 'clientName',
  128. width: 80,
  129. fixed: 'left',
  130. render: (clientName, record) => (
  131. <span
  132. onClick={() =>
  133. dispatchModal('D_CLIENT_DETAIL', { dataId: record.id, orderIds: state.orderIds })
  134. }
  135. className="text-primary cursor-pointer hover:text-[#967bbd]"
  136. >
  137. {clientName}
  138. </span>
  139. ),
  140. sorter: true,
  141. search: false
  142. },
  143. {
  144. title: '单位名称',
  145. dataIndex: 'companyName',
  146. key: 'companyName',
  147. width: 200,
  148. // ellipsis: true,
  149. render: (companyName, record) =>
  150. record.companyId !== consts.ENCRYPTION_ZERO ? (
  151. <span
  152. onClick={() =>
  153. dispatchModal('D_COMPANY_DETAIL', {
  154. dataId: record.companyId,
  155. orderIds: state.orderIds
  156. })
  157. }
  158. className="text-primary cursor-pointer hover:text-[#967bbd]"
  159. >
  160. {companyName}
  161. </span>
  162. ) : (
  163. '-'
  164. ),
  165. sorter: true,
  166. search: false
  167. },
  168. {
  169. title: '部门',
  170. dataIndex: 'department',
  171. key: 'department',
  172. ellipsis: true,
  173. width: 70,
  174. search: false
  175. },
  176. {
  177. title: '职务',
  178. dataIndex: 'position',
  179. key: 'position',
  180. width: 75,
  181. search: false
  182. },
  183. {
  184. title: '办公室',
  185. dataIndex: 'office',
  186. key: 'office',
  187. width: 90,
  188. ellipsis: true,
  189. search: false
  190. },
  191. {
  192. title: '电话',
  193. dataIndex: 'phone',
  194. key: 'phone',
  195. width: 120,
  196. search: false,
  197. sorter: true
  198. },
  199. {
  200. title: '手机',
  201. dataIndex: 'telephone',
  202. key: 'telephone',
  203. width: 120,
  204. sorter: true,
  205. search: false
  206. },
  207. {
  208. title: 'QQ',
  209. dataIndex: 'qq',
  210. key: 'qq',
  211. width: 120,
  212. sorter: true,
  213. search: false
  214. },
  215. {
  216. title: '邮箱',
  217. dataIndex: 'email',
  218. key: 'email',
  219. width: 165,
  220. sorter: true,
  221. // ellipsis: true,
  222. search: false
  223. },
  224. {
  225. title: '地区',
  226. dataIndex: 'districtName',
  227. key: 'districtName',
  228. width: 120,
  229. search: false,
  230. // ellipsis: true,
  231. // filterDropdown: filterDropdownProps => <FilterCascader {...filterDropdownProps} />,
  232. renderText: text => text?.replaceAll(' / ', ' , ')
  233. },
  234. {
  235. title: '个人标签',
  236. dataIndex: 'tagIds',
  237. key: 'tagIds',
  238. width: 160,
  239. filters: true,
  240. search: false,
  241. // onFilter: true,
  242. valueEnum: tag.personTagMap,
  243. // valueType: 'select',
  244. // fieldProps: {
  245. // mode: 'multiple',
  246. // options: tag.personOptions,
  247. // showArrow: true
  248. // },
  249. render: (_, record) => (
  250. <div className="flex items-center flex-wrap">
  251. {record.tagIds.map(item => (
  252. <Tooltip title={personTags.find(i => i.id === item)?.name} key={record.id + item}>
  253. <span
  254. className="inline-block w-12px h-12px rounded-1/2 mr-3px mb-3px"
  255. style={{ backgroundColor: personTagColorMap[item] }}
  256. />
  257. </Tooltip>
  258. ))}
  259. <PersonLabel
  260. dataId={record.id}
  261. tagIds={record.tagIds}
  262. tagType={TagTypeEnum.PERSONTAG}
  263. tagColumn={TagDataTypeEnum.CLIENT}
  264. checkCallBack={refreshData}
  265. modeType={LabelModeType.column}
  266. >
  267. <Add theme="filled" size="20" fill="#868e96" className="cursor-pointer" />
  268. </PersonLabel>
  269. </div>
  270. )
  271. },
  272. {
  273. title: '协作标签',
  274. dataIndex: 'tagCooperationIds',
  275. key: 'tagCooperationIds',
  276. width: 160,
  277. filters: true,
  278. search: false,
  279. valueEnum: tag.teamTagMap,
  280. // valueType: 'select',
  281. // fieldProps: {
  282. // mode: 'multiple',
  283. // options: tag.teamOptions,
  284. // showArrow: true
  285. // },
  286. render: (_, record) => (
  287. <div className="flex items-center flex-wrap">
  288. {record.tagCooperationIds.map(item => (
  289. <Tooltip title={teamTags.find(i => i.id === item)?.name} key={record.id + item}>
  290. <span
  291. key={record.id + item}
  292. className="inline-block w-12px h-12px mr-3px mb-3px"
  293. style={{ backgroundColor: teamTagColorMap[item] }}
  294. />
  295. </Tooltip>
  296. ))}
  297. <PersonLabel
  298. dataId={record.id}
  299. tagIds={record.tagCooperationIds}
  300. tagType={TagTypeEnum.TEAMTAG}
  301. tagColumn={TagDataTypeEnum.CLIENT}
  302. checkCallBack={refreshData}
  303. modeType={LabelModeType.column}
  304. >
  305. <Add theme="filled" size="20" fill="#868e96" className="cursor-pointer" />
  306. </PersonLabel>
  307. </div>
  308. )
  309. },
  310. {
  311. title: '地址',
  312. dataIndex: 'address',
  313. key: 'address',
  314. ellipsis: true,
  315. width: 150,
  316. sorter: true,
  317. search: false
  318. },
  319. {
  320. title: '乘车',
  321. dataIndex: 'ride',
  322. key: 'ride',
  323. ellipsis: true,
  324. width: 80,
  325. search: false
  326. },
  327. {
  328. title: '地标',
  329. dataIndex: 'landmarks',
  330. key: 'landmarks',
  331. ellipsis: true,
  332. width: 80,
  333. search: false
  334. },
  335. {
  336. title: '住宿',
  337. dataIndex: 'stay',
  338. key: 'stay',
  339. ellipsis: true,
  340. width: 80,
  341. search: false
  342. },
  343. {
  344. title: '来源',
  345. dataIndex: 'sourceName',
  346. key: 'sourceName',
  347. width: 80,
  348. search: false,
  349. filters: true,
  350. filterMultiple: false,
  351. valueEnum: sourceList.reduce((prev, curr) => {
  352. prev[curr.value] = { text: curr.label }
  353. return prev
  354. }, {})
  355. },
  356. {
  357. title: '备注',
  358. dataIndex: 'mark',
  359. key: 'mark',
  360. ellipsis: true,
  361. width: 80,
  362. search: false
  363. },
  364. // {
  365. // title: '软件锁',
  366. // dataIndex: 'keynum',
  367. // key: 'keynum',
  368. // width: 50,
  369. // render: keynum => <a>{keynum}</a>,
  370. // search: false
  371. // },
  372. {
  373. title: '创建人',
  374. dataIndex: 'staffName',
  375. key: 'staffName',
  376. width: 80,
  377. search: false
  378. }
  379. ]
  380. const columnsStateMap = {
  381. phone: {
  382. show: false
  383. },
  384. qq: {
  385. show: false
  386. },
  387. position: {
  388. show: false
  389. },
  390. office: {
  391. show: false
  392. },
  393. local: {
  394. show: false
  395. },
  396. stay: {
  397. show: false
  398. },
  399. keynum: {
  400. show: false
  401. },
  402. mark: {
  403. show: false
  404. },
  405. ride: {
  406. show: false
  407. },
  408. landmarks: {
  409. show: false
  410. },
  411. address: {
  412. show: false
  413. },
  414. staffName: {
  415. show: false
  416. },
  417. sourceName: {
  418. show: false
  419. }
  420. }
  421. const handleSearch = value => {
  422. tRef.current.reloadAndRest()
  423. setState({ ...state, params: { ...state.params, search: value } })
  424. }
  425. const onDistrictChange = value => {
  426. if (!value) return
  427. tRef.current.reloadAndRest()
  428. const [province = '', city = '', area = ''] = value || []
  429. setState({ ...state, params: { ...state.params, province, city, area } })
  430. }
  431. // 批量设置-确认触发
  432. const tagsSettingConfirm = async clientIds => {
  433. let requestSuccess = true
  434. if (tagState.tagIds.length) {
  435. const { code = -1 } = await apiBatchLink({
  436. tagIds: tagState.tagIds,
  437. dataIds: clientIds,
  438. dataType: TagDataTypeEnum.CLIENT,
  439. tagType: TagTypeEnum.PERSONTAG
  440. })
  441. if (code !== consts.RET_CODE.SUCCESS) {
  442. requestSuccess = false
  443. }
  444. }
  445. if (tagState.tagCooperationIds.length) {
  446. const { code = -1 } = await apiBatchLink({
  447. tagIds: tagState.tagCooperationIds,
  448. dataIds: clientIds,
  449. dataType: TagDataTypeEnum.CLIENT,
  450. tagType: TagTypeEnum.TEAMTAG
  451. })
  452. if (code !== consts.RET_CODE.SUCCESS) {
  453. requestSuccess = false
  454. }
  455. }
  456. requestSuccess && refreshData()
  457. }
  458. const renderTableAlert = ({ selectedRowKeys }) => {
  459. return (
  460. <div className="flex flex-row">
  461. <div className="mr-5px">
  462. <PersonLabel
  463. tagType={TagTypeEnum.PERSONTAG}
  464. tagColumn={TagDataTypeEnum.CLIENT}
  465. checkCallBack={toolbarOptionsChange}
  466. modeType={LabelModeType.toolbar}
  467. >
  468. {tagState.tagIds.length ? (
  469. <Button>
  470. {tagState.tagIds.map(item => (
  471. <span
  472. key={item}
  473. className="w-12px h-12px inline-block rounded-1/2 mr-3px"
  474. style={{ backgroundColor: personTagColorMap[item] }}
  475. />
  476. ))}
  477. <Down />
  478. </Button>
  479. ) : (
  480. <Button>
  481. 个人标签
  482. <Down />
  483. </Button>
  484. )}
  485. </PersonLabel>
  486. </div>
  487. <div className="mr-5">
  488. <PersonLabel
  489. tagType={TagTypeEnum.TEAMTAG}
  490. tagColumn={TagDataTypeEnum.CLIENT}
  491. checkCallBack={toolbarOptionsChange}
  492. modeType={LabelModeType.toolbar}
  493. >
  494. {tagState.tagCooperationIds.length ? (
  495. <Button>
  496. {tagState.tagCooperationIds.map(item => (
  497. <span
  498. key={item}
  499. className="w-12px h-12px inline-block mr-3px"
  500. style={{ backgroundColor: teamTagColorMap[item] }}
  501. />
  502. ))}
  503. <Down />
  504. </Button>
  505. ) : (
  506. <Button>
  507. 协作标签
  508. <Down />
  509. </Button>
  510. )}
  511. </PersonLabel>
  512. </div>
  513. <Button onClick={() => tagsSettingConfirm(selectedRowKeys)}>
  514. <Check size={14} />
  515. 确认
  516. </Button>
  517. </div>
  518. )
  519. }
  520. return (
  521. <div className={styles.contactContent}>
  522. <BasicTable
  523. mainMenuType="customer"
  524. columnStateType="CLIENT"
  525. params={state.params}
  526. actionRef={tRef}
  527. rowKey={record => record.id}
  528. columns={columns}
  529. request={async (params, sort, filter) => {
  530. const srotOrder = generateSortField(sort)
  531. const nFilter = generateFilterField(filter)
  532. const {
  533. code = -1,
  534. data: { client = [], total = 0 }
  535. } = await queryClient({ ...params, ...srotOrder, ...nFilter })
  536. setState({ ...state, orderIds: client.map(item => item.id) })
  537. return {
  538. data: client,
  539. success: code === consts.RET_CODE.SUCCESS,
  540. total
  541. }
  542. }}
  543. tableAlertRender={renderTableAlert}
  544. rowSelection={{
  545. type: 'checkbox',
  546. columnWidth: 25
  547. }}
  548. search={{ filterType: 'light' }}
  549. toolbar={{
  550. search: (
  551. <Tooltip placement="bottom" title="输入客户、单位名称、电话邮箱等">
  552. <Input.Search
  553. style={{ width: '250px' }}
  554. allowClear={true}
  555. placeholder={`在${
  556. permData[PermDataTypeEunm.CUSTOMER]?.find(
  557. item => item.value === defaultPermAuthCache
  558. )?.label
  559. }下搜索`}
  560. onSearch={handleSearch}
  561. />
  562. </Tooltip>
  563. ),
  564. actions: [
  565. <LazyCascader
  566. key="lazyCascader"
  567. onChange={onDistrictChange}
  568. changeOnSelect={true}
  569. bordered={false}
  570. showFooter={true}
  571. className="hover:bg-[rgba(0,0,0,0.1)]"
  572. />,
  573. <Button
  574. type="primary"
  575. key="addClientBtn"
  576. onClick={() =>
  577. dispatchModal('M_CLIENT_ADD', {
  578. onConfirm: handleAddClient,
  579. reload: () => tRef.current?.reload()
  580. })
  581. }
  582. >
  583. <Plus className="mr-1" />
  584. 客户
  585. </Button>
  586. ]
  587. }}
  588. columnsStateMap={columnsStateMap}
  589. />
  590. </div>
  591. )
  592. }
  593. export default connect(({ client, refresh, loading }) => ({
  594. personTagColorMap: client.personTagColorMap,
  595. personTags: client.personTags,
  596. sourceList: client.sourceList,
  597. teamTagColorMap: client.teamTagColorMap,
  598. teamTags: client.teamTags,
  599. loading: loading.models.client,
  600. shouldUpdate: refresh.client
  601. }))(Client)