index.jsx 16 KB

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