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

9 months ago
8 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
8 months ago
9 months ago
9 months ago
9 months ago
8 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
8 months ago
9 months ago
8 months ago
9 months ago
8 months ago
9 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
9 months ago
9 months ago
8 months ago
9 months ago
9 months ago
9 months ago
9 months ago
8 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
8 months ago
9 months ago
9 months ago
8 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
8 months ago
8 months ago
8 months ago
9 months ago
8 months ago
9 months ago
8 months ago
8 months ago
8 months ago
9 months ago
8 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
8 months ago
9 months ago
9 months ago
9 months ago
8 months ago
9 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