addGroupObject.tsx 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529
  1. import { App, Badge, Button, Card, Empty, Form, Input, InputNumber, Modal, Popover, Radio, Select, Space, Table, Typography } from 'antd';
  2. import React, { useEffect, useState } from 'react';
  3. import { PlusOutlined, QuestionCircleFilled } from '@ant-design/icons';
  4. import FilterUser from '@/pages/weComTask/components/filterUser';
  5. import MindTags from '@/pages/weComTask/components/mindTags';
  6. import { getBindMpListApi } from '@/pages/weComTask/API/corpUserAssign';
  7. import { useAjax } from '@/Hook/useAjax';
  8. import FilterUserTooltip from '@/pages/weComTask/components/filterUser/filterUserTooltip';
  9. import { businessPlanData } from '@/pages/weComTask/page/businessPlan/create/const';
  10. import { ColumnsType } from 'antd/es/table';
  11. const { Text, Paragraph } = Typography;
  12. /**
  13. * 进群对象新建组件
  14. * @param param0
  15. * @returns
  16. */
  17. const AddGroupObject: React.FC<GROUP_CHAT_CREATE.AddGroupObjectProps> = ({ value = [], onChange, onCopy, bookPlatForm, index, strategyList, bookList }) => {
  18. /******************************************/
  19. const { message } = App.useApp();
  20. const [visible, setVisible] = useState<boolean>(false)
  21. const [initialValues, setInitialValues] = useState<any>()
  22. const [copyData, setCopyData] = useState<{ visible?: boolean, data?: any }>()
  23. const [copyIndex, setCopyIndex] = useState<number>()
  24. /******************************************/
  25. return <>
  26. {value?.length > 0 ? <ShowGroupUserTable
  27. value={value}
  28. bookList={bookList}
  29. bookPlatForm={bookPlatForm}
  30. handleDelete={(id) => {
  31. onChange?.(value.filter(item => item.id !== id))
  32. }}
  33. handleEdit={(record) => {
  34. setInitialValues(record)
  35. setVisible(true)
  36. }}
  37. handleCopy={(record) => {
  38. const newValue = { ...record, id: Date.now(), groupObjectName: `copy策略${index + 1}_` + record.groupObjectName }
  39. setCopyData({ visible: true, data: newValue })
  40. }}
  41. /> : <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description='暂无群配置' />}
  42. <Button type="dashed" style={{ width: '100%' }} block icon={<PlusOutlined />} onClick={() => setVisible(true)}>新建群配置</Button>
  43. {visible && <AddGroupObjectModal
  44. visible={visible}
  45. initialValues={initialValues}
  46. onChange={(values) => {
  47. let newValue = JSON.parse(JSON.stringify(value))
  48. if (initialValues?.id) {
  49. newValue = newValue.map(item => {
  50. if (item.id === initialValues.id)
  51. return values
  52. return item
  53. })
  54. } else {
  55. newValue.push(values)
  56. }
  57. onChange?.(newValue)
  58. setVisible(false)
  59. setInitialValues(undefined)
  60. }}
  61. onClose={() => {
  62. setVisible(false)
  63. setInitialValues(undefined)
  64. }}
  65. />}
  66. {copyData?.visible && <Modal
  67. title={<strong>复制至</strong>}
  68. open={copyData?.visible}
  69. onCancel={() => setCopyData(undefined)}
  70. onOk={() => {
  71. if (copyIndex) {
  72. onCopy?.(copyData.data, copyIndex)
  73. setCopyData(undefined)
  74. setCopyIndex(undefined)
  75. setVisible(false)
  76. } else {
  77. message.error('清选择复制到哪')
  78. }
  79. }}
  80. >
  81. <Select
  82. style={{ width: '100%' }}
  83. onChange={(e) => {
  84. setCopyIndex(e)
  85. }}
  86. placeholder="请选择复制到哪"
  87. value={copyIndex}
  88. options={strategyList.map((item, index) => ({ value: index + 1, label: item.strategyName }))}
  89. />
  90. </Modal>}
  91. </>
  92. };
  93. export const ShowGroupUserTable: React.FC<GROUP_CHAT_CREATE.ShowGroupUserTableProps> = React.memo(({ bookList, bookPlatForm, value, handleEdit, handleDelete, handleCopy, isPreview }) => {
  94. const columns: ColumnsType<any> = [
  95. {
  96. title: '群名称',
  97. dataIndex: 'groupObjectName',
  98. key: 'groupObjectName',
  99. width: 120,
  100. ellipsis: true,
  101. fixed: 'left'
  102. },
  103. {
  104. title: '是否开启已有旧群聊补缺',
  105. dataIndex: 'isRepair',
  106. key: 'isRepair',
  107. width: 150,
  108. ellipsis: true,
  109. render(value, record, index) {
  110. return value ? <Space>
  111. <Badge status="success" text="开启" />
  112. <span>近{record.repairTimes}天</span>
  113. </Space> : <Badge status="error" text="关闭" />
  114. },
  115. },
  116. {
  117. title: '进群对象',
  118. dataIndex: 'externalUserFilter',
  119. key: 'externalUserFilter',
  120. width: 90,
  121. align: 'center',
  122. render(value) {
  123. return value ? <div style={{ display: 'flex', alignItems: 'center' }}>
  124. <div style={{ flex: '1 0', overflow: 'hidden' }}>
  125. <Text ellipsis>指定</Text>
  126. </div>
  127. <Popover
  128. placement="right"
  129. styles={{ body: { maxWidth: 350, maxHeight: 350, overflow: 'hidden', overflowY: 'auto' } }}
  130. mouseEnterDelay={0.5}
  131. content={<FilterUserTooltip
  132. bookCityList={bookPlatForm?.map(item => ({ label: item.platformName, value: item.platformKey }))}
  133. configName={value?.configName}
  134. data={value?.configContent}
  135. />}
  136. >
  137. <a style={{ color: '#000' }}><QuestionCircleFilled /></a>
  138. </Popover>
  139. </div> : '全部'
  140. },
  141. },
  142. {
  143. title: '群递增起始编号',
  144. dataIndex: 'groupIndex',
  145. key: 'groupIndex',
  146. width: 60,
  147. align: 'center'
  148. },
  149. {
  150. title: '群固定人数',
  151. dataIndex: 'groupUserCount',
  152. key: 'groupUserCount',
  153. width: 50,
  154. align: 'center'
  155. },
  156. {
  157. title: '邀请客户进群完毕后客服号是否退群',
  158. dataIndex: 'autoOutGroup',
  159. key: 'autoOutGroup',
  160. width: 85,
  161. align: 'center',
  162. render(value) {
  163. return value ? <span style={{ color: '#1890ff' }}>是</span> : <span style={{ color: 'red' }}>否</span>
  164. },
  165. },
  166. {
  167. title: '是否排除已在群的客户',
  168. dataIndex: 'excludeInGroup',
  169. key: 'excludeInGroup',
  170. width: 60,
  171. align: 'center',
  172. render(value) {
  173. return value ? <span style={{ color: '#1890ff' }}>是</span> : <span style={{ color: 'red' }}>否</span>
  174. },
  175. },
  176. {
  177. title: '拉群完成后自动删除拉群标签',
  178. dataIndex: 'excludeInGroup',
  179. key: 'excludeInGroup',
  180. width: 70,
  181. align: 'center',
  182. render(value) {
  183. return value ? <span style={{ color: '#1890ff' }}>是</span> : <span style={{ color: 'red' }}>否</span>
  184. },
  185. },
  186. {
  187. title: '是否开启失败人群重试',
  188. dataIndex: 'isRetry',
  189. key: 'isRetry',
  190. width: 70,
  191. align: 'center',
  192. render(value) {
  193. return value ? <span style={{ color: '#1890ff' }}>是</span> : <span style={{ color: 'red' }}>否</span>
  194. },
  195. },
  196. {
  197. title: '是否根据所选主体相同客户去重',
  198. dataIndex: 'excludeSameCorpUser',
  199. key: 'excludeSameCorpUser',
  200. width: 70,
  201. align: 'center',
  202. render(value) {
  203. return value ? <span style={{ color: '#1890ff' }}>是</span> : <span style={{ color: 'red' }}>否</span>
  204. },
  205. },
  206. {
  207. title: '群聊关联公众号',
  208. dataIndex: 'weChatAppid',
  209. key: 'weChatAppid',
  210. width: 100,
  211. align: 'center',
  212. ellipsis: true,
  213. render(value) {
  214. return value || '--'
  215. },
  216. },
  217. {
  218. title: '建群模板名称',
  219. dataIndex: 'templateName',
  220. key: 'templateName',
  221. width: 100,
  222. ellipsis: true,
  223. render(value) {
  224. return value || '--'
  225. },
  226. },
  227. {
  228. title: '拉群完成后群聊备注',
  229. dataIndex: 'remark',
  230. key: 'remark',
  231. width: 140,
  232. ellipsis: true,
  233. render(value) {
  234. return value || '--'
  235. },
  236. },
  237. {
  238. title: '拉群完成后群聊智能标签',
  239. dataIndex: 'tagDTO',
  240. key: 'tagDTO',
  241. width: 200,
  242. ellipsis: true,
  243. render(value) {
  244. return value && Object.keys(value)?.length > 0 ?
  245. <Paragraph ellipsis style={{ margin: 0 }}>{Object.keys(value).map(key => {
  246. if (key === 'business' && value[key]) {
  247. return `业务(来源渠道):${businessPlanData.find(i => i.value === value.business)?.label || '<空>'}`
  248. } else if (key === 'bookCity' && value[key]) {
  249. return `书城(来源渠道):${bookPlatForm.find(i => i.id === value.bookCity)?.platformName || '<空>'}`
  250. } else if (key === 'product' && value[key]) {
  251. return `产品(来源渠道):${bookList.find(i => i.id === value.product)?.bookName || '<空>'}`
  252. }
  253. return ''
  254. }).join('、')}</Paragraph> : '--'
  255. },
  256. },
  257. {
  258. title: '客服消息',
  259. dataIndex: 'groupSendMsg',
  260. key: 'groupSendMsg',
  261. width: 140,
  262. ellipsis: true,
  263. render(value) {
  264. return value ? value : '--'
  265. }
  266. },
  267. {
  268. title: '操作',
  269. dataIndex: 'cz',
  270. key: 'cz',
  271. width: 100,
  272. fixed: 'right',
  273. align: 'center',
  274. render(_, record) {
  275. return <Space>
  276. <a onClick={() => {
  277. handleCopy?.(record)
  278. }}>复制</a>
  279. <a onClick={() => {
  280. handleEdit?.(record)
  281. }}>修改</a>
  282. <a style={{ color: 'red' }} onClick={() => {
  283. handleDelete?.(record.id)
  284. }}>删除</a>
  285. </Space>
  286. },
  287. }
  288. ]
  289. if (isPreview) {
  290. columns.pop()
  291. }
  292. return <Table
  293. size='small'
  294. bordered
  295. rowKey={'id'}
  296. dataSource={value}
  297. scroll={{ y: 1000 }}
  298. columns={columns}
  299. />
  300. })
  301. const AddGroupObjectModal: React.FC<GROUP_CHAT_CREATE.AddGroupObjectModalProps> = React.memo(({ visible, onChange, onClose, initialValues }) => {
  302. /******************************************/
  303. const { message } = App.useApp();
  304. const [form] = Form.useForm();
  305. const externalUserType = Form.useWatch('externalUserType', form);
  306. const isRepair = Form.useWatch('isRepair', form);
  307. const getBindMpList = useAjax(() => getBindMpListApi())
  308. /******************************************/
  309. useEffect(() => {
  310. getBindMpList.run()
  311. }, [])
  312. const handleOk = () => {
  313. form.validateFields().then((values) => {
  314. if (initialValues?.id) {
  315. values.id = initialValues.id
  316. } else {
  317. values.id = Date.now();
  318. }
  319. console.log(values)
  320. onChange?.(values)
  321. }).catch(() => {
  322. form.submit()
  323. });
  324. }
  325. return <Modal
  326. title={<strong>{(initialValues && Object.keys(initialValues).length > 0) ? '修改' : '新建'}群配置</strong>}
  327. open={visible}
  328. onCancel={onClose}
  329. width={600}
  330. onOk={handleOk}
  331. styles={{ body: { maxHeight: 550, overflowY: 'auto' } }}
  332. >
  333. <Form
  334. form={form}
  335. name="addGroupUser"
  336. labelAlign='left'
  337. labelCol={{ span: 9 }}
  338. colon={false}
  339. labelWrap
  340. scrollToFirstError={{
  341. behavior: 'smooth',
  342. block: 'center'
  343. }}
  344. onFinishFailed={({ errorFields }) => {
  345. message.error(errorFields?.[0]?.errors?.[0])
  346. }}
  347. onFinish={handleOk}
  348. initialValues={(initialValues && Object.keys(initialValues).length > 0) ? initialValues : { externalUserType: 'specify', isRepair: false, groupIndex: 1, groupUserCount: 37, autoOutGroup: false, excludeInGroup: true, deleteGroupTag: true, isRetry: false, excludeSameCorpUser: false }}
  349. preserve={true}
  350. >
  351. <Form.Item
  352. name={'groupObjectName'}
  353. label={<strong>群名称</strong>}
  354. rules={[{ required: true, message: '请输入群名称!' }]}
  355. >
  356. <Input placeholder='请输入群名称' allowClear />
  357. </Form.Item>
  358. <Form.Item
  359. name={'isRepair'}
  360. label={<strong>是否开启已有旧群聊补缺</strong>}
  361. tooltip={{ title: '优先将客户补充进已有旧群聊的空缺位置,待旧群满员后再创建新群容纳剩余客户。', placement: 'top' }}
  362. rules={[{ required: true, message: '请选择是否开启群补人!' }]}
  363. >
  364. <Radio.Group options={[{ label: '开启', value: true }, { label: '关闭', value: false }]} />
  365. </Form.Item>
  366. {isRepair && <>
  367. <Form.Item
  368. name={'repairTimes'}
  369. label={<strong>补缺群聊范围</strong>}
  370. rules={[{ required: true, message: '请选择补缺群聊范围!' }]}
  371. tooltip={{ title: '可补缺的群聊范围:在本建群策略内创建的群聊', placement: 'top' }}
  372. >
  373. <Select
  374. showSearch
  375. allowClear
  376. placeholder="请选择补缺群聊范围"
  377. filterOption={(input, option) =>
  378. (option?.label as any)?.toLowerCase().indexOf(input.toLowerCase()) >= 0
  379. }
  380. options={Array(30).fill('').map((_, index) => ({ label: `近${index + 1}天`, value: index + 1 }))}
  381. />
  382. </Form.Item>
  383. <Form.Item
  384. name={'groupSendMsg'}
  385. label={<strong>客服消息</strong>}
  386. >
  387. <Input.TextArea placeholder="请输入建群成功发送内容" />
  388. </Form.Item>
  389. </>}
  390. <Form.Item
  391. label={<strong>进群对象配置</strong>}
  392. required
  393. >
  394. <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
  395. <Form.Item
  396. name={'externalUserType'}
  397. rules={[{ required: true, message: '请选择转移对象!' }]}
  398. noStyle
  399. >
  400. <Radio.Group options={[{ label: '全部', value: 'all' }, { label: '指定', value: 'specify' }]} />
  401. </Form.Item>
  402. {externalUserType === 'specify' && <div style={{ marginTop: 8, width: '100%' }}>
  403. <Form.Item
  404. name={'externalUserFilter'}
  405. rules={[{ required: true, message: '请选择人群包!' }]}
  406. noStyle
  407. >
  408. <FilterUser configType={'USER_GROUP'} />
  409. </Form.Item>
  410. </div>}
  411. </div>
  412. </Form.Item>
  413. <Form.Item
  414. name={'groupIndex'}
  415. tooltip={{ title: `例如:起始编号为'1'群名为'测试群',再建群后群名为'测试群1'超出群人数的用户将分配到'测试群2'依次递增`, placement: 'top' }}
  416. label={<strong>群递增起始编号</strong>}
  417. rules={[{ required: true, message: '请输入群递增起始编号!' }]}
  418. >
  419. <InputNumber placeholder='请输入群递增起始编号' min={1} style={{ width: '100%' }} />
  420. </Form.Item>
  421. <Form.Item
  422. name={'groupUserCount'}
  423. tooltip={{ title: `不包括建群人和固定企微号,最大上限数量38`, placement: 'top' }}
  424. label={<strong>群固定人数</strong>}
  425. rules={[{ required: true, message: '请输入群固定人数!' }]}
  426. >
  427. <InputNumber placeholder='请输入群固定人数' max={38} min={1} style={{ width: '100%' }} />
  428. </Form.Item>
  429. <Form.Item
  430. name={'autoOutGroup'}
  431. label={<strong>邀请客户进群完毕后客服号是否退群</strong>}
  432. rules={[{ required: true, message: '请选择是否退群!' }]}
  433. >
  434. <Radio.Group>
  435. <Radio value={true}>是</Radio>
  436. <Radio value={false}>否</Radio>
  437. </Radio.Group>
  438. </Form.Item>
  439. <Form.Item
  440. name={'excludeInGroup'}
  441. label={<strong>是否排除已在群的客户</strong>}
  442. rules={[{ required: true, message: '请选择是否排除已在群的客户!' }]}
  443. >
  444. <Radio.Group>
  445. <Radio value={true}>是</Radio>
  446. </Radio.Group>
  447. </Form.Item>
  448. <Form.Item
  449. name={'deleteGroupTag'}
  450. label={<strong>拉群完成后自动删除拉群标签</strong>}
  451. rules={[{ required: true, message: '请选择拉群完成后自动删除拉群标签!' }]}
  452. >
  453. <Radio.Group>
  454. <Radio value={true}>是</Radio>
  455. </Radio.Group>
  456. </Form.Item>
  457. <Form.Item
  458. name={'isRetry'}
  459. label={<strong>是否开启失败人群重试</strong>}
  460. rules={[{ required: true, message: '请选择是否开启失败人群重试!' }]}
  461. >
  462. <Radio.Group>
  463. <Radio value={true}>是</Radio>
  464. <Radio value={false}>否</Radio>
  465. </Radio.Group>
  466. </Form.Item>
  467. <Form.Item
  468. name={'excludeSameCorpUser'}
  469. label={<strong>是否根据所选主体相同客户去重</strong>}
  470. rules={[{ required: true, message: '请选择是否开启失败人群重试!' }]}
  471. >
  472. <Radio.Group>
  473. <Radio value={true}>是</Radio>
  474. <Radio value={false}>否</Radio>
  475. </Radio.Group>
  476. </Form.Item>
  477. <Form.Item
  478. name={'weChatAppid'}
  479. label={<strong>群聊关联公众号</strong>}
  480. >
  481. <Select
  482. showSearch
  483. allowClear
  484. placeholder="选择公众号"
  485. filterOption={(input, option) =>
  486. (option?.label as any)?.toLowerCase().indexOf(input.toLowerCase()) >= 0
  487. }
  488. options={getBindMpList?.data?.data?.map(item => ({ label: item.name, value: item.name + '_' + item.id }))}
  489. />
  490. </Form.Item>
  491. <Form.Item
  492. name={'templateName'}
  493. label={<strong>建群模板名称</strong>}
  494. >
  495. <Input placeholder="请输入建群模板名称" />
  496. </Form.Item>
  497. <Form.Item
  498. name={'remark'}
  499. label={<strong>拉群完成后群聊备注</strong>}
  500. >
  501. <Input.TextArea placeholder="请输入群聊备注" />
  502. </Form.Item>
  503. <Card title={<strong>拉群完成后群聊智能标签</strong>} style={{ background: '#fff', marginBottom: 10 }} styles={{ body: { padding: '6px 0 6px 16px' } }}>
  504. <Form.Item
  505. name={'tagDTO'}
  506. style={{ marginBottom: 0 }}
  507. >
  508. <MindTags />
  509. </Form.Item>
  510. </Card>
  511. </Form>
  512. </Modal>
  513. })
  514. export default React.memo(AddGroupObject);