index.jsx 16 KB

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