index.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620
  1. import { Input, Button, message, Tooltip } from 'antd'
  2. import { connect, useAccess, useModel } from '@umijs/max'
  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 { TagType, TagDataType, 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 { PermDataTypeEunm } from '../Company/components/PermSelect'
  13. import BasicTable from '@/components/Table'
  14. import { getPermAuthCache } from '@/utils/auth'
  15. import { useModal } from '@/components/ModalStore'
  16. import { Plus } from '@icon-park/react'
  17. import { PageContainer } from '@ant-design/pro-layout'
  18. import { LoadingOutlined } from '@ant-design/icons'
  19. const Client = props => {
  20. const dispatchModal = useModal()
  21. const {
  22. personTagColorMap,
  23. teamTagColorMap,
  24. personTags,
  25. teamTags,
  26. dispatch,
  27. shouldUpdate,
  28. loading,
  29. sourceList
  30. } = props
  31. const { initialState: { permData } = { permData: {} } } = useModel('@@initialState')
  32. const hasAddPerm = useAccess()?.validatePermByType('client_add') || false
  33. const tRef = useRef()
  34. const [tagState, setTagState] = useState({
  35. tagIds: [],
  36. tagCooperationIds: []
  37. })
  38. const [state, setState] = useState({
  39. loading: false,
  40. params: {},
  41. orderIds: []
  42. })
  43. const tag = useMemo(() => {
  44. const personTagMap = {}
  45. const teamTagMap = {}
  46. const personOptions = []
  47. const teamOptions = []
  48. personTags.forEach(item => {
  49. // personOptions.push({ label: item.name, value: item.id })
  50. personTagMap[item.id] = {
  51. text: (
  52. <>
  53. <span
  54. className="w-12px h-12px inline-block rounded-1/2 mr-3px"
  55. style={{ backgroundColor: personTagColorMap[item.id] }}
  56. />
  57. <span>{item.name}</span>
  58. </>
  59. )
  60. }
  61. })
  62. teamTags.forEach(item => {
  63. // teamOptions.push({ label: item.name, value: item.id })
  64. teamTagMap[item.id] = {
  65. text: (
  66. <>
  67. <span
  68. className="w-12px h-12px inline-block mr-3px"
  69. style={{ backgroundColor: teamTagColorMap[item.id] }}
  70. />
  71. <span>{item.name}</span>
  72. </>
  73. )
  74. }
  75. })
  76. return { personTagMap, teamTagMap, personOptions, teamOptions }
  77. }, [loading, shouldUpdate])
  78. // 默认刷新一次列表请求数据
  79. useEffect(() => {
  80. if (!sourceList?.length) {
  81. dispatch({
  82. type: 'client/fetchSourceList'
  83. })
  84. }
  85. if (!personTags.length) {
  86. dispatch({
  87. type: 'client/fetchPersonTags',
  88. payload: { tagType: TagType.PERSONTAG, tagColumn: TagDataType.CLIENT }
  89. })
  90. }
  91. if (!teamTags.length) {
  92. dispatch({
  93. type: 'client/fetchTeamTags',
  94. payload: { tagType: TagType.TEAMTAG, tagColumn: TagDataType.CLIENT }
  95. })
  96. }
  97. if (shouldUpdate) {
  98. tRef.current.reload()
  99. }
  100. }, [shouldUpdate])
  101. const toolbarOptionsChange = (type, ids) => {
  102. setTagState({ ...tagState, [type]: ids })
  103. }
  104. const [defaultPermAuthCache] = getPermAuthCache(PermDataTypeEunm.CUSTOMER)
  105. // 重置到默认值,包括表单
  106. const initData = () => {
  107. tRef.current?.reset()
  108. }
  109. // 刷新
  110. const refreshData = () => {
  111. tRef.current?.reload()
  112. }
  113. const handleAddClient = async values => {
  114. const payload = formatValues(values)
  115. const { code = -1, msg = '新增失败' } = await apiAddClient(payload)
  116. if (code === consts.RET_CODE.SUCCESS) {
  117. message.success(msg)
  118. initData()
  119. }
  120. return code === consts.RET_CODE.SUCCESS
  121. }
  122. const columns = [
  123. {
  124. title: '客户',
  125. dataIndex: 'clientName',
  126. key: 'clientName',
  127. width: 80,
  128. fixed: 'left',
  129. render: (clientName, record) => (
  130. <span
  131. onClick={() => dispatchModal('D_CLIENT_DETAIL', { dataId: record.id, orderIds: state.orderIds })}
  132. className="text-primary cursor-pointer hover:text-hex-967bbd">
  133. {clientName}
  134. </span>
  135. ),
  136. sorter: true,
  137. search: false
  138. },
  139. {
  140. title: '单位名称',
  141. dataIndex: 'companyName',
  142. key: 'companyName',
  143. width: 200,
  144. // ellipsis: true,
  145. render: (companyName, record) =>
  146. record.companyId ? (
  147. <span
  148. onClick={() => {
  149. dispatchModal('D_COMPANY_DETAIL', {
  150. updateKey: 'client',
  151. dataId: record.companyId,
  152. orderIds: state.orderIds
  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: '微信号',
  205. dataIndex: 'wechat',
  206. key: 'wechat',
  207. width: 120,
  208. search: false
  209. },
  210. {
  211. title: 'QQ',
  212. dataIndex: 'qq',
  213. key: 'qq',
  214. width: 120,
  215. sorter: true,
  216. search: false
  217. },
  218. {
  219. title: '邮箱',
  220. dataIndex: 'email',
  221. key: 'email',
  222. width: 165,
  223. sorter: true,
  224. // ellipsis: true,
  225. search: false
  226. },
  227. {
  228. title: '地区',
  229. dataIndex: 'districtName',
  230. key: 'districtName',
  231. width: 120,
  232. search: false,
  233. // ellipsis: true,
  234. // filterDropdown: filterDropdownProps => <FilterCascader {...filterDropdownProps} />,
  235. renderText: text => text?.replaceAll(' / ', ' , ')
  236. },
  237. {
  238. title: '个人标签',
  239. dataIndex: 'tagIds',
  240. key: 'tagIds',
  241. width: 160,
  242. filters: true,
  243. search: false,
  244. // onFilter: true,
  245. valueEnum: tag.personTagMap,
  246. // valueType: 'select',
  247. // fieldProps: {
  248. // mode: 'multiple',
  249. // options: tag.personOptions,
  250. // showArrow: true
  251. // },
  252. render: (_, record) => (
  253. <div className="flex items-center flex-wrap">
  254. {record.tagIds.map(item => (
  255. <Tooltip title={personTags.find(i => i.id === item)?.name} key={record.id + item}>
  256. <span
  257. className="inline-block w-12px h-12px rounded-1/2 mr-3px mb-3px"
  258. style={{ backgroundColor: personTagColorMap[item] }}
  259. />
  260. </Tooltip>
  261. ))}
  262. <PersonLabel
  263. dataId={record.id}
  264. tagIds={record.tagIds}
  265. tagType={TagType.PERSONTAG}
  266. tagColumn={TagDataType.CLIENT}
  267. checkCallBack={refreshData}
  268. modeType={LabelModeType.column}>
  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={TagType.TEAMTAG}
  303. tagColumn={TagDataType.CLIENT}
  304. checkCallBack={refreshData}
  305. modeType={LabelModeType.column}>
  306. <Add theme="filled" size="20" fill="#868e96" className="cursor-pointer" />
  307. </PersonLabel>
  308. </div>
  309. )
  310. },
  311. {
  312. title: '地址',
  313. dataIndex: 'address',
  314. key: 'address',
  315. ellipsis: true,
  316. width: 150,
  317. sorter: true,
  318. search: false
  319. },
  320. {
  321. title: '乘车',
  322. dataIndex: 'ride',
  323. key: 'ride',
  324. ellipsis: true,
  325. width: 80,
  326. search: false
  327. },
  328. {
  329. title: '地标',
  330. dataIndex: 'landmarks',
  331. key: 'landmarks',
  332. ellipsis: true,
  333. width: 80,
  334. search: false
  335. },
  336. {
  337. title: '住宿',
  338. dataIndex: 'stay',
  339. key: 'stay',
  340. ellipsis: true,
  341. width: 80,
  342. search: false
  343. },
  344. {
  345. title: '来源',
  346. dataIndex: 'sourceName',
  347. key: 'sourceName',
  348. width: 80,
  349. search: false,
  350. filters: true,
  351. filterMultiple: false,
  352. valueEnum: sourceList.reduce((prev, curr) => {
  353. prev[curr.value] = { text: curr.label }
  354. return prev
  355. }, {})
  356. },
  357. {
  358. title: '备注',
  359. dataIndex: 'mark',
  360. key: 'mark',
  361. ellipsis: true,
  362. width: 80,
  363. search: false
  364. },
  365. // {
  366. // title: '软件锁',
  367. // dataIndex: 'keynum',
  368. // key: 'keynum',
  369. // width: 50,
  370. // render: keynum => <a>{keynum}</a>,
  371. // search: false
  372. // },
  373. {
  374. title: '创建人',
  375. dataIndex: 'staffName',
  376. key: 'staffName',
  377. width: 80,
  378. search: false
  379. }
  380. ]
  381. const columnsStateMap = {
  382. phone: {
  383. show: false
  384. },
  385. qq: {
  386. show: false
  387. },
  388. position: {
  389. show: false
  390. },
  391. office: {
  392. show: false
  393. },
  394. local: {
  395. show: false
  396. },
  397. stay: {
  398. show: false
  399. },
  400. keynum: {
  401. show: false
  402. },
  403. mark: {
  404. show: false
  405. },
  406. ride: {
  407. show: false
  408. },
  409. landmarks: {
  410. show: false
  411. },
  412. address: {
  413. show: false
  414. },
  415. staffName: {
  416. show: false
  417. },
  418. sourceName: {
  419. show: false
  420. }
  421. }
  422. const handleSearch = value => {
  423. tRef.current.reloadAndRest()
  424. setState({ ...state, params: { ...state.params, search: value } })
  425. }
  426. const onDistrictChange = value => {
  427. if (!value) return
  428. tRef.current.reloadAndRest()
  429. const [province = '', city = '', area = ''] = value || []
  430. setState({ ...state, params: { ...state.params, province, city, area } })
  431. }
  432. // 批量设置-确认触发
  433. const tagsSettingConfirm = async clientIds => {
  434. setState({ ...state, loading: true })
  435. let requestSuccess = true
  436. if (tagState.tagIds.length) {
  437. const { code = -1 } = await apiBatchLink({
  438. tagIds: tagState.tagIds,
  439. dataIds: clientIds,
  440. dataType: TagDataType.CLIENT,
  441. tagType: TagType.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: TagDataType.CLIENT,
  452. tagType: TagType.TEAMTAG
  453. })
  454. if (code !== consts.RET_CODE.SUCCESS) {
  455. requestSuccess = false
  456. }
  457. }
  458. setState({ ...state, loading: false })
  459. requestSuccess && refreshData()
  460. }
  461. const renderTableAlert = ({ selectedRowKeys }) => {
  462. return (
  463. <div className="flex flex-row">
  464. <div className="mr-5px">
  465. <PersonLabel
  466. tagType={TagType.PERSONTAG}
  467. tagColumn={TagDataType.CLIENT}
  468. checkCallBack={toolbarOptionsChange}
  469. modeType={LabelModeType.toolbar}>
  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={TagType.TEAMTAG}
  492. tagColumn={TagDataType.CLIENT}
  493. checkCallBack={toolbarOptionsChange}
  494. modeType={LabelModeType.toolbar}>
  495. {tagState.tagCooperationIds.length ? (
  496. <Button>
  497. {tagState.tagCooperationIds.map(item => (
  498. <span
  499. key={item}
  500. className="w-12px h-12px inline-block mr-3px"
  501. style={{ backgroundColor: teamTagColorMap[item] }}
  502. />
  503. ))}
  504. <Down />
  505. </Button>
  506. ) : (
  507. <Button>
  508. 协作标签
  509. <Down />
  510. </Button>
  511. )}
  512. </PersonLabel>
  513. </div>
  514. <Button
  515. onClick={() => tagsSettingConfirm(selectedRowKeys)}
  516. icon={state.loading ? <LoadingOutlined /> : <Check size={14} />}
  517. />
  518. </div>
  519. )
  520. }
  521. return (
  522. <PageContainer title={false} breadcrumb={false}>
  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 {
  533. code = -1,
  534. data: { client = [], total = 0 }
  535. } = await queryClient({ ...params, ...sort, ...filter })
  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(item => item.value === defaultPermAuthCache)
  557. ?.label
  558. }下搜索`}
  559. onSearch={handleSearch}
  560. />
  561. </Tooltip>
  562. ),
  563. actions: [
  564. <LazyCascader
  565. key="lazyCascader"
  566. onChange={onDistrictChange}
  567. changeOnSelect={true}
  568. bordered={false}
  569. showFooter={true}
  570. className="hover:bg-[rgba(0,0,0,0.1)]"
  571. />,
  572. <Button
  573. type="primary"
  574. key="addClientBtn"
  575. disabled={!hasAddPerm}
  576. onClick={() =>
  577. dispatchModal('M_CLIENT_ADD', {
  578. onConfirm: handleAddClient,
  579. reload: () => tRef.current?.reload()
  580. })
  581. }>
  582. <Plus className="mr-1" />
  583. 客户
  584. </Button>
  585. ]
  586. }}
  587. columnsStateMap={columnsStateMap}
  588. />
  589. </div>
  590. </PageContainer>
  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)