You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

473 lines
16 KiB

1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
  1. import {useTranslation} from '@/i18n.ts'
  2. import {Badge, Button, Divider, Form, Input, Select, Space, Table, TableColumnsType, Tooltip} from 'antd'
  3. import {useAtom, useAtomValue} from 'jotai'
  4. import React, {useEffect, useMemo, useState} from 'react'
  5. import {BetaSchemaForm, ProColumns, ProFormColumnsType,} from '@ant-design/pro-components'
  6. import ListPageLayout from '@/layout/ListPageLayout.tsx'
  7. import {useStyle} from './style.ts'
  8. import {FilterOutlined} from '@ant-design/icons'
  9. import {getValueCount, unSetColumnRules} from '@/utils'
  10. import {Table as ProTable} from '@/components/table'
  11. import {msgListAtom, msgSearchAtom, saveMsgAtom} from "@/store/message/my.ts";
  12. import {templateAllListAtom} from "@/store/message/template.ts";
  13. import {coverType, IMsgTemplate} from "@/types/message/template.ts";
  14. import dayjs from "dayjs";
  15. const i18nPrefix = 'msgMy.list'
  16. const MdwMessage = () => {
  17. const {styles} = useStyle()
  18. const {t} = useTranslation()
  19. const [form] = Form.useForm()
  20. const [filterForm] = Form.useForm()
  21. const {mutate: saveOrUpdate, isPending: isSubmitting, isSuccess} = useAtomValue(saveMsgAtom)
  22. const [search, setSearch] = useAtom(msgSearchAtom)
  23. const {data, isFetching, isLoading, refetch} = useAtomValue(msgListAtom)
  24. const {data: templateList} = useAtomValue(templateAllListAtom)
  25. const [open, setOpen] = useState(false)
  26. const [openFilter, setFilterOpen] = useState(false)
  27. const [currentTemplate, setCurrentTemplate] = useState<IMsgTemplate>()
  28. const [searchKey, setSearchKey] = useState(search?.title)
  29. const [templateType, setTemplateType] = useState('')
  30. const templateChange = (index: number) => {
  31. if (templateList && index !== undefined) {
  32. // key转换
  33. const result = templateList[index].fields.split(',').map(item => {
  34. return {
  35. field_key: item,
  36. field_value: ''
  37. };
  38. });
  39. setCurrentTemplate(templateList[index])
  40. form.setFieldsValue({...templateList[index], 'fieldList': result})
  41. setTemplateType(templateList[index].type)
  42. } else {
  43. form.resetFields()
  44. setTemplateType('')
  45. setCurrentTemplate(undefined)
  46. }
  47. }
  48. const handleInputChange = (index, e) => {
  49. form.getFieldValue("fieldList")[index].field_value = e.target.value
  50. }
  51. const drawerColumns = useMemo(() => {
  52. return [
  53. {
  54. title: 'ID',
  55. dataIndex: 'id',
  56. hideInTable: true,
  57. hideInSearch: true,
  58. formItemProps: {hidden: true}
  59. },
  60. {
  61. title: t(`${i18nPrefix}.columns.template`, '选择模板'),
  62. valueType: 'select',
  63. fieldProps: {
  64. allowClear: true,
  65. },
  66. renderFormItem: () => {
  67. return <Select onChange={templateChange}>
  68. {
  69. templateList?.map((template, index) => (
  70. <Select.Option key={index} value={index}>
  71. {template.name}
  72. </Select.Option>
  73. ))
  74. }
  75. </Select>
  76. },
  77. formItemProps: {
  78. rules: [
  79. {
  80. required: true,
  81. }
  82. ]
  83. },
  84. },
  85. {
  86. title: t(`${i18nPrefix}.columns.type`, '通道类型'),
  87. dataIndex: 'type',
  88. valueType: 'select',
  89. fieldProps: {
  90. options: [
  91. {label: '短信', value: 'SMS'},
  92. {label: '邮件', value: 'EMAIL'},
  93. {label: 'Telegram', value: 'TG'}
  94. ],
  95. allowClear: false,
  96. disabled: true,
  97. },
  98. formItemProps: {
  99. rules: [
  100. {
  101. required: true,
  102. }
  103. ]
  104. }
  105. },
  106. {
  107. title: t(`${i18nPrefix}.columns.token`, 'Telegram BOT API Token'),
  108. dataIndex: 'token',
  109. valueType: 'text',
  110. fieldProps: {
  111. placeholder: "请输入 @BotFather 获取到的BOT的API Token"
  112. },
  113. formItemProps: {
  114. hidden: templateType != 'TG',
  115. rules: [
  116. {
  117. required: templateType == 'TG',
  118. }
  119. ]
  120. },
  121. },
  122. {
  123. title: t(`${i18nPrefix}.columns.email_from`, '发件人邮箱'),
  124. dataIndex: 'email_from',
  125. valueType: 'text',
  126. fieldProps: {
  127. maxLength: 100,
  128. showCount: true,
  129. },
  130. formItemProps: {
  131. hidden: templateType != 'EMAIL',
  132. rules: [
  133. {
  134. required: templateType == 'EMAIL',
  135. message: t('message.required', '模板内容必填')
  136. }
  137. ]
  138. },
  139. },
  140. {
  141. title: t(`${i18nPrefix}.columns.title`, '标题'),
  142. dataIndex: 'title',
  143. valueType: 'text',
  144. fieldProps: {
  145. maxLength: 100,
  146. showCount: true,
  147. disabled: true,
  148. },
  149. formItemProps: {
  150. hidden: templateType != 'EMAIL',
  151. rules: [
  152. {
  153. required: templateType == 'EMAIL',
  154. message: t('message.required', '模板内容必填')
  155. }
  156. ]
  157. },
  158. },
  159. {
  160. title: t(`${i18nPrefix}.columns.content`, '正文'),
  161. dataIndex: 'content',
  162. valueType: 'textarea',
  163. fieldProps: {
  164. maxLength: 1000,
  165. showCount: true,
  166. rows: 15,
  167. disabled: true,
  168. },
  169. formItemProps: {
  170. rules: [
  171. {
  172. required: true,
  173. message: t('message.required', '内容必填')
  174. }
  175. ]
  176. }
  177. },
  178. {
  179. title: t(`${i18nPrefix}.columns.fieldList`, '填充变量'),
  180. dataIndex: 'fieldList',
  181. formItemProps: {
  182. hidden: currentTemplate === undefined,
  183. },
  184. renderFormItem: (_, config) => {
  185. return (
  186. <>
  187. {
  188. config.value?.map((variable, index) => (
  189. <div key={index} style={{marginBottom: 8}}>
  190. <Input
  191. addonBefore={variable.field_key}
  192. onChange={(e) => handleInputChange(index, e)}
  193. />
  194. </div>
  195. ))
  196. }
  197. </>
  198. );
  199. },
  200. },
  201. {
  202. title: t(`${i18nPrefix}.columns.dest`, '收件人(多个收件人用英文逗号隔开;如果类型是Telegram,请填写User ID或Chat ID)'),
  203. dataIndex: 'dest',
  204. valueType: 'textarea',
  205. fieldProps: {
  206. maxLength: 1000,
  207. showCount: true,
  208. rows: 5,
  209. placeholder: 'aaa@qq.com,bbb@gmail.com,-1001953214222,-1001953214333',
  210. },
  211. formItemProps: {
  212. rules: [
  213. {
  214. required: true,
  215. message: t('message.required', '内容必填')
  216. }
  217. ]
  218. }
  219. },
  220. ] as ProColumns[]
  221. }, [search, templateType, templateList])
  222. const columns = useMemo(() => {
  223. return [
  224. {
  225. title: 'ID',
  226. dataIndex: 'id',
  227. hideInTable: true,
  228. hideInSearch: true,
  229. formItemProps: {hidden: true}
  230. },
  231. {
  232. title: t(`${i18nPrefix}.columns.type`, '类型'),
  233. dataIndex: 'type',
  234. render: (_, record) => {
  235. return <div>{coverType(record.type)}</div>
  236. }
  237. },
  238. {
  239. title: t(`${i18nPrefix}.columns.title`, '标题'),
  240. dataIndex: 'title',
  241. },
  242. {
  243. title: t(`${i18nPrefix}.columns.content`, '正文'),
  244. dataIndex: 'content',
  245. },
  246. {
  247. title: t(`${i18nPrefix}.columns.send_at`, '预计发送时间'),
  248. dataIndex: 'send_at',
  249. render: (_, record) => {
  250. return <div>{record.send_at == 0 ? "立即" : dayjs(record.send_at).format('YYYY-MM-DD HH:mm:ss')}</div>
  251. }
  252. },
  253. ] as ProColumns[]
  254. }, [search, currentTemplate])
  255. const expandedRowRender = (record) => {
  256. const expandedColumns: TableColumnsType = [
  257. {
  258. title: "收件人",
  259. dataIndex: 'dest',
  260. },
  261. {
  262. title: t(`${i18nPrefix}.columns.status`, '状态'),
  263. dataIndex: 'status',
  264. render: (_text, record) => {
  265. return <Badge
  266. status={['default', 'processing', 'success', 'error'][record.status] as any}
  267. text={['未处理', '发送中', '发送成功', '发送失败'][record.status]}/>
  268. }
  269. },
  270. {
  271. title: t(`${i18nPrefix}.columns.send_at`, '发送时间'),
  272. dataIndex: 'send_at',
  273. render: (_, record) => {
  274. return <div>{record.send_at == 0 ? 0 : dayjs(record.send_at*1000).format('YYYY-MM-DD HH:mm:ss')}</div>
  275. }
  276. },
  277. ];
  278. return <Table columns={expandedColumns} dataSource={record.dest} pagination={false}/>;
  279. }
  280. useEffect(() => {
  281. setSearchKey(search?.title)
  282. filterForm.setFieldsValue(search)
  283. }, [search])
  284. useEffect(() => {
  285. if (isSuccess) {
  286. setOpen(false)
  287. }
  288. }, [isSuccess])
  289. return (
  290. <ListPageLayout className={styles.container}>
  291. <ProTable
  292. rowKey="id"
  293. headerTitle={t(`${i18nPrefix}.title`, '消息管理')}
  294. toolbar={{
  295. search: {
  296. loading: isFetching && !!search?.title,
  297. onSearch: (value: string) => {
  298. setSearch(prev => ({
  299. ...prev,
  300. title: value
  301. }))
  302. },
  303. allowClear: true,
  304. onChange: (e) => {
  305. setSearchKey(e.target?.value)
  306. },
  307. value: searchKey,
  308. placeholder: t(`${i18nPrefix}.placeholder`, '输入模板名称')
  309. },
  310. actions: [
  311. <Tooltip key={'filter'} title={t(`${i18nPrefix}.filter.tooltip`, '高级查询')}>
  312. <Badge count={getValueCount(search)}>
  313. <Button
  314. onClick={() => {
  315. setFilterOpen(true)
  316. }}
  317. icon={<FilterOutlined/>} shape={'circle'} size={'small'}/>
  318. </Badge>
  319. </Tooltip>,
  320. <Divider type={'vertical'} key={'divider'}/>,
  321. <Button key={'add'}
  322. onClick={() => {
  323. form.resetFields()
  324. form.setFieldsValue({
  325. id: 0,
  326. })
  327. setOpen(true)
  328. }}
  329. type={'primary'}>{t(`${i18nPrefix}.add`, '发送消息')}</Button>
  330. ]
  331. }}
  332. scroll={{
  333. x: columns.length * 200,
  334. y: 'calc(100vh - 290px)'
  335. }}
  336. search={false}
  337. dateFormatter="string"
  338. loading={isLoading || isFetching}
  339. dataSource={data?.rows ?? []}
  340. columns={columns}
  341. options={{
  342. reload: () => {
  343. refetch()
  344. },
  345. }}
  346. pagination={{
  347. total: data?.total,
  348. pageSize: search.pageSize,
  349. current: search.page,
  350. onShowSizeChange: (current: number, size: number) => {
  351. setSearch({
  352. ...search,
  353. pageSize: size,
  354. page: current
  355. })
  356. },
  357. onChange: (current, pageSize) => {
  358. setSearch(prev => {
  359. return {
  360. ...prev,
  361. page: current,
  362. pageSize: pageSize,
  363. }
  364. })
  365. },
  366. }}
  367. expandable={{
  368. expandedRowRender,
  369. }}
  370. />
  371. <BetaSchemaForm
  372. grid={true}
  373. shouldUpdate={false}
  374. width={1000}
  375. form={form}
  376. layout={'vertical'}
  377. scrollToFirstError={true}
  378. title={t(`${i18nPrefix}.title_add}`, '发送消息')}
  379. layoutType={'DrawerForm'}
  380. open={open}
  381. drawerProps={{
  382. maskClosable: false,
  383. }}
  384. onOpenChange={(open) => {
  385. setOpen(open)
  386. }}
  387. loading={isSubmitting}
  388. onValuesChange={() => {
  389. }}
  390. onFinish={async (values) => {
  391. saveOrUpdate({...values, "code": currentTemplate?.code})
  392. }}
  393. columns={drawerColumns as ProFormColumnsType[]}/>
  394. <BetaSchemaForm
  395. title={t(`${i18nPrefix}.filter.title`, '模板高级查询')}
  396. grid={true}
  397. shouldUpdate={false}
  398. width={500}
  399. form={filterForm}
  400. open={openFilter}
  401. onOpenChange={open => {
  402. setFilterOpen(open)
  403. }}
  404. layout={'vertical'}
  405. scrollToFirstError={true}
  406. layoutType={'DrawerForm'}
  407. drawerProps={{
  408. maskClosable: false,
  409. mask: false,
  410. }}
  411. submitter={{
  412. searchConfig: {
  413. resetText: t(`${i18nPrefix}.filter.reset`, '清空'),
  414. submitText: t(`${i18nPrefix}.filter.submit`, '查询'),
  415. },
  416. onReset: () => {
  417. filterForm.resetFields()
  418. },
  419. render: (props,) => {
  420. return (
  421. <div style={{textAlign: 'right'}}>
  422. <Space>
  423. <Button onClick={() => {
  424. props.reset()
  425. }}>{props.searchConfig?.resetText}</Button>
  426. <Button type="primary"
  427. onClick={() => {
  428. props.submit()
  429. }}
  430. >{props.searchConfig?.submitText}</Button>
  431. </Space>
  432. </div>
  433. )
  434. },
  435. }}
  436. onValuesChange={() => {
  437. }}
  438. onFinish={async (values) => {
  439. //处理,变成数组
  440. Object.keys(values).forEach(key => {
  441. if (typeof values[key] === 'string' && values[key].includes(',')) {
  442. values[key] = values[key].split(',')
  443. }
  444. })
  445. setSearch(values)
  446. }}
  447. columns={unSetColumnRules(columns.filter(item => !item.hideInSearch)) as ProFormColumnsType[]}/>
  448. </ListPageLayout>
  449. )
  450. }
  451. export default MdwMessage