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.

466 lines
16 KiB

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