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

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, 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