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.

440 lines
15 KiB

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
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
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
  1. import {useTranslation} from '@/i18n.ts'
  2. import {Badge, Button, Divider, Form, Popconfirm, Space, Tag, Tooltip} from 'antd'
  3. import {useAtom, useAtomValue} from 'jotai'
  4. import React, {useEffect, useMemo, useState} from 'react'
  5. import Action from '@/components/action/Action.tsx'
  6. import {BetaSchemaForm, ProColumns, ProFormColumnsType,} from '@ant-design/pro-components'
  7. import ListPageLayout from '@/layout/ListPageLayout.tsx'
  8. import {useStyle} from './style.ts'
  9. import {FilterOutlined} from '@ant-design/icons'
  10. import {getValueCount, unSetColumnRules} from '@/utils'
  11. import {Table as ProTable} from '@/components/table'
  12. import {
  13. deleteTemplateAtom,
  14. saveOrUpdateTemplateAtom,
  15. templateAtom,
  16. templateListAtom,
  17. templateSearchAtom
  18. } from "@/store/message/template.ts";
  19. import {coverType} from "@/types/message/template.ts";
  20. const i18nPrefix = 'mdwMessage.list'
  21. const MdwMessage = () => {
  22. const {styles, cx} = useStyle()
  23. const {t} = useTranslation()
  24. const [form] = Form.useForm()
  25. const [filterForm] = Form.useForm()
  26. const {mutate: saveOrUpdate, isPending: isSubmitting, isSuccess} = useAtomValue(saveOrUpdateTemplateAtom)
  27. const [search, setSearch] = useAtom(templateSearchAtom)
  28. const [currentTemplate, setAppPackage] = useAtom(templateAtom)
  29. const {data, isFetching, isLoading, refetch} = useAtomValue(templateListAtom)
  30. const {mutate: deleteAppPackage, isPending: isDeleting} = useAtomValue(deleteTemplateAtom)
  31. const [open, setOpen] = useState(false)
  32. const [openFilter, setFilterOpen] = useState(false)
  33. const [searchKey, setSearchKey] = useState(search?.title)
  34. const [templateField, setTemplateField] = useState<string[]>([]);
  35. const [templateTitle, setTemplateTitle] = useState('');
  36. const [templateContent, setTemplateContent] = useState('');
  37. const [templateType, setTemplateType] = useState('')
  38. useEffect(() => {
  39. setTemplateType(currentTemplate?.type)
  40. if (form.getFieldValue('id') === 0) {
  41. setTemplateField(['name']);
  42. } else {
  43. setTemplateField(currentTemplate?.fields.split(","));
  44. }
  45. }, [open]);
  46. const handleChange = () => {
  47. // 使用正则表达式匹配 ${var} 格式的变量
  48. const regex = /\${\.([a-zA-Z0-9_]+)}/g;
  49. const matches = [...(templateTitle + templateContent).matchAll(regex)];
  50. // 提取变量名
  51. const variables = Array.from(new Set(matches.map(match => match[1])));
  52. setTemplateField(variables);
  53. };
  54. useEffect(() => {
  55. handleChange()
  56. }, [templateTitle, templateContent]);
  57. const handleContentChange = (e) => {
  58. const value = e.target.value;
  59. setTemplateContent(value)
  60. };
  61. const titheHandleContentChange = (e) => {
  62. const value = e.target.value;
  63. setTemplateTitle(value)
  64. };
  65. const typeHandlerChange = (value) => {
  66. if (value !== 'EMAIL') {
  67. setTemplateTitle('')
  68. form.setFieldsValue({'title': undefined})
  69. }
  70. setTemplateType(value)
  71. }
  72. const drawerColumns = useMemo(() => {
  73. return [
  74. {
  75. title: 'ID',
  76. dataIndex: 'id',
  77. hideInTable: true,
  78. hideInSearch: true,
  79. formItemProps: {hidden: true}
  80. },
  81. {
  82. title: t(`${i18nPrefix}.columns.name`, '模板名称'),
  83. dataIndex: 'name',
  84. valueType: 'text',
  85. fieldProps: {
  86. maxLength: 50,
  87. showCount: true,
  88. },
  89. formItemProps: {
  90. rules: [
  91. {
  92. required: true,
  93. message: t('message.required', '模板名称必填')
  94. }
  95. ]
  96. }
  97. },
  98. {
  99. title: t(`${i18nPrefix}.columns.type`, '模板类型'),
  100. dataIndex: 'type',
  101. valueType: 'select',
  102. fieldProps: {
  103. options: [
  104. {label: '短信', value: 'SMS'},
  105. {label: '邮件', value: 'EMAIL'},
  106. {label: 'Telegram', value: 'TG'}
  107. ],
  108. allowClear: false,
  109. onChange: typeHandlerChange
  110. },
  111. formItemProps: {
  112. rules: [
  113. {
  114. required: true,
  115. }
  116. ]
  117. }
  118. },
  119. {
  120. title: t(`${i18nPrefix}.columns.title`, '模板标题'),
  121. dataIndex: 'title',
  122. valueType: 'text',
  123. fieldProps: {
  124. maxLength: 100,
  125. showCount: true,
  126. onChange: titheHandleContentChange, // 监听输入事件
  127. },
  128. formItemProps: {
  129. tooltip: '支持邮件类型',
  130. hidden: templateType != 'EMAIL',
  131. rules: [
  132. {
  133. required: templateType == 'EMAIL',
  134. message: t('message.required', '模板内容必填')
  135. }
  136. ]
  137. },
  138. },
  139. {
  140. title: t(`${i18nPrefix}.columns.content`, '模板内容'),
  141. dataIndex: 'content',
  142. valueType: 'textarea',
  143. fieldProps: {
  144. defaultValue: "你好,我叫${.name}",
  145. maxLength: 1000,
  146. showCount: true,
  147. rows: 15,
  148. onChange: handleContentChange, // 监听输入事件
  149. },
  150. formItemProps: {
  151. rules: [
  152. {
  153. required: true,
  154. message: t('message.required', '模板内容必填')
  155. }
  156. ]
  157. }
  158. },
  159. {
  160. title: t(`${i18nPrefix}.columns.fields`, '识别到的变量'),
  161. dataIndex: 'fields',
  162. renderFormItem: () => {
  163. return (
  164. <>
  165. {
  166. templateField.map((variable, index) => (
  167. <Tag key={index} color="blue" style={{marginRight: 8}}>
  168. {variable}
  169. </Tag>
  170. ))
  171. }
  172. </>
  173. );
  174. }
  175. },
  176. ] as ProColumns[]
  177. }, [isDeleting, currentTemplate, search, templateField, templateType])
  178. const columns = useMemo(() => {
  179. return [
  180. {
  181. title: 'ID',
  182. dataIndex: 'id',
  183. hideInTable: true,
  184. hideInSearch: true,
  185. formItemProps: {hidden: true}
  186. },
  187. {
  188. title: t(`${i18nPrefix}.columns.name`, '模板名称'),
  189. dataIndex: 'name',
  190. },
  191. {
  192. title: t(`${i18nPrefix}.columns.type`, '模板类型'),
  193. dataIndex: 'type',
  194. render: (_, record) => {
  195. return <div>{coverType(record.type)}</div>
  196. }
  197. },
  198. {
  199. title: t(`${i18nPrefix}.columns.title`, '模板标题'),
  200. dataIndex: 'title',
  201. },
  202. {
  203. title: t(`${i18nPrefix}.columns.content`, '模板内容'),
  204. dataIndex: 'content',
  205. },
  206. {
  207. title: t(`${i18nPrefix}.columns.option`, '操作'),
  208. key: 'option',
  209. valueType: 'option',
  210. fixed: 'right',
  211. render: (_, record) => [
  212. <Action key="edit"
  213. as={'a'}
  214. onClick={() => {
  215. form.setFieldsValue(record)
  216. setOpen(true)
  217. }}>{t('actions.edit')}</Action>,
  218. <Divider type={'vertical'}/>,
  219. <Popconfirm
  220. key={'del_confirm'}
  221. disabled={isDeleting}
  222. onConfirm={() => {
  223. deleteAppPackage(record.id)
  224. }}
  225. title={t('message.deleteConfirm')}>
  226. <a key="del">
  227. {t('actions.delete', '删除')}
  228. </a>
  229. </Popconfirm>,
  230. ]
  231. }
  232. ] as ProColumns[]
  233. }, [isDeleting, currentTemplate, search])
  234. useEffect(() => {
  235. setSearchKey(search?.title)
  236. filterForm.setFieldsValue(search)
  237. }, [search])
  238. useEffect(() => {
  239. if (isSuccess) {
  240. setOpen(false)
  241. }
  242. }, [isSuccess])
  243. return (
  244. <ListPageLayout className={styles.container}>
  245. <ProTable
  246. rowKey="id"
  247. headerTitle={t(`${i18nPrefix}.title`, '消息模板管理')}
  248. toolbar={{
  249. search: {
  250. loading: isFetching && !!search?.title,
  251. onSearch: (value: string) => {
  252. setSearch(prev => ({
  253. ...prev,
  254. title: value
  255. }))
  256. },
  257. allowClear: true,
  258. onChange: (e) => {
  259. setSearchKey(e.target?.value)
  260. },
  261. value: searchKey,
  262. placeholder: t(`${i18nPrefix}.placeholder`, '输入模板名称')
  263. },
  264. actions: [
  265. <Tooltip key={'filter'} title={t(`${i18nPrefix}.filter.tooltip`, '高级查询')}>
  266. <Badge count={getValueCount(search)}>
  267. <Button
  268. onClick={() => {
  269. setFilterOpen(true)
  270. }}
  271. icon={<FilterOutlined/>} shape={'circle'} size={'small'}/>
  272. </Badge>
  273. </Tooltip>,
  274. <Divider type={'vertical'} key={'divider'}/>,
  275. <Button key={'add'}
  276. onClick={() => {
  277. form.resetFields()
  278. form.setFieldsValue({
  279. id: 0,
  280. })
  281. setOpen(true)
  282. }}
  283. type={'primary'}>{t(`${i18nPrefix}.add`, '添加模板')}</Button>
  284. ]
  285. }}
  286. scroll={{
  287. x: columns.length * 200,
  288. y: 'calc(100vh - 290px)'
  289. }}
  290. search={false}
  291. onRow={(record) => {
  292. return {
  293. className: cx({
  294. // 'ant-table-row-selected': currentAppPackage?.id === record.id
  295. }),
  296. onClick: () => {
  297. setAppPackage(record)
  298. }
  299. }
  300. }}
  301. dateFormatter="string"
  302. loading={isLoading || isFetching}
  303. dataSource={data?.rows ?? []}
  304. columns={columns}
  305. options={{
  306. reload: () => {
  307. refetch()
  308. },
  309. }}
  310. pagination={{
  311. total: data?.total,
  312. pageSize: search.pageSize,
  313. current: search.page,
  314. onShowSizeChange: (current: number, size: number) => {
  315. setSearch({
  316. ...search,
  317. pageSize: size,
  318. page: current
  319. })
  320. },
  321. onChange: (current, pageSize) => {
  322. setSearch(prev => {
  323. return {
  324. ...prev,
  325. page: current,
  326. pageSize: pageSize,
  327. }
  328. })
  329. },
  330. }}
  331. />
  332. <BetaSchemaForm
  333. grid={true}
  334. shouldUpdate={false}
  335. width={1000}
  336. form={form}
  337. layout={'vertical'}
  338. scrollToFirstError={true}
  339. title={t(`${i18nPrefix}.title_${form.getFieldValue('id') !== 0 ? 'edit' : 'add'}`, form.getFieldValue('id') !== 0 ? '模板编辑' : '模板添加')}
  340. layoutType={'DrawerForm'}
  341. open={open}
  342. drawerProps={{
  343. maskClosable: false,
  344. }}
  345. onOpenChange={(open) => {
  346. setOpen(open)
  347. }}
  348. loading={isSubmitting}
  349. onValuesChange={() => {
  350. }}
  351. onFinish={async (values) => {
  352. saveOrUpdate({...values, 'fields': templateField.join()})
  353. }}
  354. columns={drawerColumns as ProFormColumnsType[]}/>
  355. <BetaSchemaForm
  356. title={t(`${i18nPrefix}.filter.title`, '模板高级查询')}
  357. grid={true}
  358. shouldUpdate={false}
  359. width={500}
  360. form={filterForm}
  361. open={openFilter}
  362. onOpenChange={open => {
  363. setFilterOpen(open)
  364. }}
  365. layout={'vertical'}
  366. scrollToFirstError={true}
  367. layoutType={'DrawerForm'}
  368. drawerProps={{
  369. maskClosable: false,
  370. mask: false,
  371. }}
  372. submitter={{
  373. searchConfig: {
  374. resetText: t(`${i18nPrefix}.filter.reset`, '清空'),
  375. submitText: t(`${i18nPrefix}.filter.submit`, '查询'),
  376. },
  377. onReset: () => {
  378. filterForm.resetFields()
  379. },
  380. render: (props,) => {
  381. return (
  382. <div style={{textAlign: 'right'}}>
  383. <Space>
  384. <Button onClick={() => {
  385. props.reset()
  386. }}>{props.searchConfig?.resetText}</Button>
  387. <Button type="primary"
  388. onClick={() => {
  389. props.submit()
  390. }}
  391. >{props.searchConfig?.submitText}</Button>
  392. </Space>
  393. </div>
  394. )
  395. },
  396. }}
  397. onValuesChange={() => {
  398. }}
  399. onFinish={async (values) => {
  400. //处理,变成数组
  401. Object.keys(values).forEach(key => {
  402. if (typeof values[key] === 'string' && values[key].includes(',')) {
  403. values[key] = values[key].split(',')
  404. }
  405. })
  406. setSearch(values)
  407. }}
  408. columns={unSetColumnRules(columns.filter(item => !item.hideInSearch)) as ProFormColumnsType[]}/>
  409. </ListPageLayout>
  410. )
  411. }
  412. export default MdwMessage