17 changed files with 1203 additions and 607 deletions
			
			
		- 
					8src/global.d.ts
 - 
					476src/pages/message/my/index.tsx
 - 
					0src/pages/message/my/style.ts
 - 
					78src/pages/message/template/index.tsx
 - 
					26src/pages/message/template/style.ts
 - 
					17src/service/message/message.ts
 - 
					13src/service/message/my.ts
 - 
					17src/service/message/template.ts
 - 
					68src/store/message/my.ts
 - 
					25src/store/message/template.ts
 - 
					11src/types/message/my.ts
 - 
					25src/types/message/template.ts
 - 
					25src/types/system/message.ts
 - 
					1vite.config.ts
 
@ -0,0 +1,476 @@ | 
			
		|||||
 | 
				import {useTranslation} from '@/i18n.ts' | 
			
		||||
 | 
				import {Badge, Button, Divider, Form, Input, Select, Space, Tooltip} from 'antd' | 
			
		||||
 | 
				import {useAtom, useAtomValue} from 'jotai' | 
			
		||||
 | 
				import React, {useEffect, useMemo, useState} from 'react' | 
			
		||||
 | 
				import {BetaSchemaForm, ProColumns, ProFormColumnsType,} from '@ant-design/pro-components' | 
			
		||||
 | 
				import ListPageLayout from '@/layout/ListPageLayout.tsx' | 
			
		||||
 | 
				import {useStyle} from './style.ts' | 
			
		||||
 | 
				import {FilterOutlined} from '@ant-design/icons' | 
			
		||||
 | 
				import {getValueCount, unSetColumnRules} from '@/utils' | 
			
		||||
 | 
				import {Table as ProTable} from '@/components/table' | 
			
		||||
 | 
				import {msgListAtom, msgSearchAtom, saveMsgAtom} from "@/store/message/my.ts"; | 
			
		||||
 | 
				import {templateAllListAtom} from "@/store/message/template.ts"; | 
			
		||||
 | 
				import {coverType, IMsgTemplate} from "@/types/message/template.ts"; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				const i18nPrefix = 'msgMy.list' | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				const MdwMessage = () => { | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  const {styles, cx} = useStyle() | 
			
		||||
 | 
				  const {t} = useTranslation() | 
			
		||||
 | 
				  const [form] = Form.useForm() | 
			
		||||
 | 
				  const [filterForm] = Form.useForm() | 
			
		||||
 | 
				  const {mutate: saveOrUpdate, isPending: isSubmitting, isSuccess} = useAtomValue(saveMsgAtom) | 
			
		||||
 | 
				  const [search, setSearch] = useAtom(msgSearchAtom) | 
			
		||||
 | 
				  const {data, isFetching, isLoading, refetch} = useAtomValue(msgListAtom) | 
			
		||||
 | 
				  const {data: templateList} = useAtomValue(templateAllListAtom) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  const [open, setOpen] = useState(false) | 
			
		||||
 | 
				  const [openFilter, setFilterOpen] = useState(false) | 
			
		||||
 | 
				  const [currentTemplate, setCurrentTemplate] = useState<IMsgTemplate>() | 
			
		||||
 | 
				  const [searchKey, setSearchKey] = useState(search?.title) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  const [templateType, setTemplateType] = useState('') | 
			
		||||
 | 
				  const typeHandlerChange = (value: string) => { | 
			
		||||
 | 
				    if (value !== 'EMAIL') { | 
			
		||||
 | 
				      setTemplateTitle('') | 
			
		||||
 | 
				      form.setFieldsValue({'title': undefined}) | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				    setTemplateType(value) | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  const templateChange = (index: number) => { | 
			
		||||
 | 
				    if (templateList && index !== undefined) { | 
			
		||||
 | 
				      // key转换
 | 
			
		||||
 | 
				      const result = templateList[index].fields.split(',').map(item => { | 
			
		||||
 | 
				        return { | 
			
		||||
 | 
				          field_key: item, | 
			
		||||
 | 
				          field_value: '' | 
			
		||||
 | 
				        }; | 
			
		||||
 | 
				      }); | 
			
		||||
 | 
				      setCurrentTemplate(templateList[index]) | 
			
		||||
 | 
				      form.setFieldsValue({...templateList[index], 'fieldList': result}) | 
			
		||||
 | 
				      setTemplateType(templateList[index].type) | 
			
		||||
 | 
				      setTemplateTitle(templateList[index].title) | 
			
		||||
 | 
				      setTemplateContent(templateList[index].content) | 
			
		||||
 | 
				    } else { | 
			
		||||
 | 
				      form.resetFields() | 
			
		||||
 | 
				      setTemplateType('') | 
			
		||||
 | 
				      setCurrentTemplate(undefined) | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  const [templateTitle, setTemplateTitle] = useState(''); | 
			
		||||
 | 
				  const [templateContent, setTemplateContent] = useState(''); | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  useEffect(() => { | 
			
		||||
 | 
				    handleChange() | 
			
		||||
 | 
				  }, [templateTitle, templateContent]); | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  const handleContentChange = (e) => { | 
			
		||||
 | 
				    const value = e.target.value; | 
			
		||||
 | 
				    setTemplateContent(value) | 
			
		||||
 | 
				  }; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  const titheHandleContentChange = (e) => { | 
			
		||||
 | 
				    const value = e.target.value; | 
			
		||||
 | 
				    setTemplateTitle(value) | 
			
		||||
 | 
				  }; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  const handleChange = () => { | 
			
		||||
 | 
				    // 使用正则表达式匹配 ${var} 格式的变量
 | 
			
		||||
 | 
				    const regex = /\${\.([a-zA-Z0-9_]+)}/g; | 
			
		||||
 | 
				    const matches = [...(templateTitle + templateContent).matchAll(regex)]; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    // 提取变量名
 | 
			
		||||
 | 
				    const variables = Array.from(new Set(matches.map(match => match[1]))); | 
			
		||||
 | 
				    const result = variables.map(item => { | 
			
		||||
 | 
				      return { | 
			
		||||
 | 
				        field_key: item, | 
			
		||||
 | 
				        field_value: '' | 
			
		||||
 | 
				      }; | 
			
		||||
 | 
				    }); | 
			
		||||
 | 
				    form.setFieldsValue({'fieldList': result}) | 
			
		||||
 | 
				  }; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  const handleInputChange = (index, e) => { | 
			
		||||
 | 
				    form.getFieldValue("fieldList")[index].field_value = e.target.value | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  const drawerColumns = useMemo(() => { | 
			
		||||
 | 
				    return [ | 
			
		||||
 | 
				      { | 
			
		||||
 | 
				        title: 'ID', | 
			
		||||
 | 
				        dataIndex: 'id', | 
			
		||||
 | 
				        hideInTable: true, | 
			
		||||
 | 
				        hideInSearch: true, | 
			
		||||
 | 
				        formItemProps: {hidden: true} | 
			
		||||
 | 
				      }, | 
			
		||||
 | 
				      { | 
			
		||||
 | 
				        title: t(`${i18nPrefix}.columns.template`, '选择模板'), | 
			
		||||
 | 
				        valueType: 'select', | 
			
		||||
 | 
				        fieldProps: { | 
			
		||||
 | 
				          allowClear: true, | 
			
		||||
 | 
				        }, | 
			
		||||
 | 
				        renderFormItem: () => { | 
			
		||||
 | 
				          return <Select onChange={templateChange}> | 
			
		||||
 | 
				            { | 
			
		||||
 | 
				              templateList?.map((template, index) => ( | 
			
		||||
 | 
				                      <Select.Option key={index} value={index}> | 
			
		||||
 | 
				                        {template.name} | 
			
		||||
 | 
				                      </Select.Option> | 
			
		||||
 | 
				              )) | 
			
		||||
 | 
				            } | 
			
		||||
 | 
				          </Select> | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				      }, | 
			
		||||
 | 
				      { | 
			
		||||
 | 
				        title: t(`${i18nPrefix}.columns.type`, '消息类型'), | 
			
		||||
 | 
				        dataIndex: 'type', | 
			
		||||
 | 
				        valueType: 'select', | 
			
		||||
 | 
				        fieldProps: { | 
			
		||||
 | 
				          options: [ | 
			
		||||
 | 
				            {label: '短信', value: 'SMS'}, | 
			
		||||
 | 
				            {label: '邮件', value: 'EMAIL'}, | 
			
		||||
 | 
				            {label: 'Telegram', value: 'TG'} | 
			
		||||
 | 
				          ], | 
			
		||||
 | 
				          allowClear: false, | 
			
		||||
 | 
				          onChange: typeHandlerChange | 
			
		||||
 | 
				        }, | 
			
		||||
 | 
				        formItemProps: { | 
			
		||||
 | 
				          rules: [ | 
			
		||||
 | 
				            { | 
			
		||||
 | 
				              required: true, | 
			
		||||
 | 
				            } | 
			
		||||
 | 
				          ] | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				      }, | 
			
		||||
 | 
				      { | 
			
		||||
 | 
				        title: t(`${i18nPrefix}.columns.title`, '标题'), | 
			
		||||
 | 
				        dataIndex: 'title', | 
			
		||||
 | 
				        valueType: 'text', | 
			
		||||
 | 
				        fieldProps: { | 
			
		||||
 | 
				          maxLength: 100, | 
			
		||||
 | 
				          showCount: true, | 
			
		||||
 | 
				          onChange: titheHandleContentChange, | 
			
		||||
 | 
				        }, | 
			
		||||
 | 
				        formItemProps: { | 
			
		||||
 | 
				          tooltip: '支持邮件类型', | 
			
		||||
 | 
				          hidden: templateType != 'EMAIL', | 
			
		||||
 | 
				          rules: [ | 
			
		||||
 | 
				            { | 
			
		||||
 | 
				              required: templateType == 'EMAIL', | 
			
		||||
 | 
				              message: t('message.required', '模板内容必填') | 
			
		||||
 | 
				            } | 
			
		||||
 | 
				          ] | 
			
		||||
 | 
				        }, | 
			
		||||
 | 
				      }, | 
			
		||||
 | 
				      { | 
			
		||||
 | 
				        title: t(`${i18nPrefix}.columns.content`, '正文'), | 
			
		||||
 | 
				        dataIndex: 'content', | 
			
		||||
 | 
				        valueType: 'textarea', | 
			
		||||
 | 
				        fieldProps: { | 
			
		||||
 | 
				          maxLength: 1000, | 
			
		||||
 | 
				          showCount: true, | 
			
		||||
 | 
				          rows: 15, | 
			
		||||
 | 
				          onChange: handleContentChange, | 
			
		||||
 | 
				        }, | 
			
		||||
 | 
				        formItemProps: { | 
			
		||||
 | 
				          rules: [ | 
			
		||||
 | 
				            { | 
			
		||||
 | 
				              required: true, | 
			
		||||
 | 
				              message: t('message.required', '内容必填') | 
			
		||||
 | 
				            } | 
			
		||||
 | 
				          ] | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				      }, | 
			
		||||
 | 
				      { | 
			
		||||
 | 
				        title: t(`${i18nPrefix}.columns.fieldList`, '填充变量'), | 
			
		||||
 | 
				        dataIndex: 'fieldList', | 
			
		||||
 | 
				        formItemProps: { | 
			
		||||
 | 
				          hidden: currentTemplate === undefined, | 
			
		||||
 | 
				        }, | 
			
		||||
 | 
				        renderFormItem: (_, config) => { | 
			
		||||
 | 
				          return ( | 
			
		||||
 | 
				                  <> | 
			
		||||
 | 
				                    { | 
			
		||||
 | 
				                      config.value?.map((variable, index) => ( | 
			
		||||
 | 
				                              <div key={index} style={{marginBottom: 8}}> | 
			
		||||
 | 
				                                <Input | 
			
		||||
 | 
				                                        addonBefore={variable.field_key} | 
			
		||||
 | 
				                                        onChange={(e) => handleInputChange(index, e)} | 
			
		||||
 | 
				                                /> | 
			
		||||
 | 
				                              </div> | 
			
		||||
 | 
				                      )) | 
			
		||||
 | 
				                    } | 
			
		||||
 | 
				                  </> | 
			
		||||
 | 
				          ); | 
			
		||||
 | 
				        }, | 
			
		||||
 | 
				      }, | 
			
		||||
 | 
				      { | 
			
		||||
 | 
				        title: t(`${i18nPrefix}.columns.dest`, '收件人(多个收件人用英文逗号隔开,如果类型是TG,则填token)'), | 
			
		||||
 | 
				        dataIndex: 'dest', | 
			
		||||
 | 
				        valueType: 'textarea', | 
			
		||||
 | 
				        fieldProps: { | 
			
		||||
 | 
				          maxLength: 1000, | 
			
		||||
 | 
				          showCount: true, | 
			
		||||
 | 
				          rows: 5, | 
			
		||||
 | 
				          placeholder: 'aaa@qq.com,bbb@gmail.com', | 
			
		||||
 | 
				        }, | 
			
		||||
 | 
				        formItemProps: { | 
			
		||||
 | 
				          rules: [ | 
			
		||||
 | 
				            { | 
			
		||||
 | 
				              required: true, | 
			
		||||
 | 
				              message: t('message.required', '内容必填') | 
			
		||||
 | 
				            } | 
			
		||||
 | 
				          ] | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				      }, | 
			
		||||
 | 
				    ] as ProColumns[] | 
			
		||||
 | 
				  }, [search, templateType, templateList]) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  const columns = useMemo(() => { | 
			
		||||
 | 
				    return [ | 
			
		||||
 | 
				      { | 
			
		||||
 | 
				        title: 'ID', | 
			
		||||
 | 
				        dataIndex: 'id', | 
			
		||||
 | 
				        hideInTable: true, | 
			
		||||
 | 
				        hideInSearch: true, | 
			
		||||
 | 
				        formItemProps: {hidden: true} | 
			
		||||
 | 
				      }, | 
			
		||||
 | 
				      { | 
			
		||||
 | 
				        title: t(`${i18nPrefix}.columns.type`, '类型'), | 
			
		||||
 | 
				        dataIndex: 'type', | 
			
		||||
 | 
				        render: (_, record) => { | 
			
		||||
 | 
				          return <div>{coverType(record.type)}</div> | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				      }, | 
			
		||||
 | 
				      { | 
			
		||||
 | 
				        title: t(`${i18nPrefix}.columns.title`, '标题'), | 
			
		||||
 | 
				        dataIndex: 'title', | 
			
		||||
 | 
				      }, | 
			
		||||
 | 
				      { | 
			
		||||
 | 
				        title: t(`${i18nPrefix}.columns.content`, '正文'), | 
			
		||||
 | 
				        dataIndex: 'content', | 
			
		||||
 | 
				      }, | 
			
		||||
 | 
				      { | 
			
		||||
 | 
				        title: t(`${i18nPrefix}.columns.dest`, '收件人'), | 
			
		||||
 | 
				        dataIndex: 'dest', | 
			
		||||
 | 
				      }, | 
			
		||||
 | 
				      { | 
			
		||||
 | 
				        title: t(`${i18nPrefix}.columns.status`, '状态'), | 
			
		||||
 | 
				        dataIndex: 'status', | 
			
		||||
 | 
				        render: (_text, record) => { | 
			
		||||
 | 
				          return <Badge | 
			
		||||
 | 
				                  status={['default', 'processing', 'success', 'error'][record.status] as any} | 
			
		||||
 | 
				                  text={['未处理', '发送中', '发送成功', '发送失败'][record.status]}/> | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				      }, | 
			
		||||
 | 
				      { | 
			
		||||
 | 
				        title: t(`${i18nPrefix}.columns.send_at`, '发送时间'), | 
			
		||||
 | 
				        dataIndex: 'send_at', | 
			
		||||
 | 
				      }, | 
			
		||||
 | 
				      // {
 | 
			
		||||
 | 
				      //   title: t(`${i18nPrefix}.columns.option`, '操作'),
 | 
			
		||||
 | 
				      //   key: 'option',
 | 
			
		||||
 | 
				      //   valueType: 'option',
 | 
			
		||||
 | 
				      //   fixed: 'right',
 | 
			
		||||
 | 
				      //   render: (_, record) => [
 | 
			
		||||
 | 
				      //     <Popconfirm
 | 
			
		||||
 | 
				      //             key={'del_confirm'}
 | 
			
		||||
 | 
				      //             disabled={isDeleting}
 | 
			
		||||
 | 
				      //             onConfirm={() => {
 | 
			
		||||
 | 
				      //               deleteAppPackage(record.id)
 | 
			
		||||
 | 
				      //             }}
 | 
			
		||||
 | 
				      //             title={t('message.deleteConfirm')}>
 | 
			
		||||
 | 
				      //       <a key="del">
 | 
			
		||||
 | 
				      //         {t('actions.delete', '删除')}
 | 
			
		||||
 | 
				      //       </a>
 | 
			
		||||
 | 
				      //     </Popconfirm>,
 | 
			
		||||
 | 
				      //   ]
 | 
			
		||||
 | 
				      // }
 | 
			
		||||
 | 
				    ] as ProColumns[] | 
			
		||||
 | 
				  }, [search, currentTemplate]) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  useEffect(() => { | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    setSearchKey(search?.title) | 
			
		||||
 | 
				    filterForm.setFieldsValue(search) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  }, [search]) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  useEffect(() => { | 
			
		||||
 | 
				    if (isSuccess) { | 
			
		||||
 | 
				      setOpen(false) | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				  }, [isSuccess]) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  return ( | 
			
		||||
 | 
				          <ListPageLayout className={styles.container}> | 
			
		||||
 | 
				            <ProTable | 
			
		||||
 | 
				                    rowKey="id" | 
			
		||||
 | 
				                    headerTitle={t(`${i18nPrefix}.title`, '消息管理')} | 
			
		||||
 | 
				                    toolbar={{ | 
			
		||||
 | 
				                      search: { | 
			
		||||
 | 
				                        loading: isFetching && !!search?.title, | 
			
		||||
 | 
				                        onSearch: (value: string) => { | 
			
		||||
 | 
				                          setSearch(prev => ({ | 
			
		||||
 | 
				                            ...prev, | 
			
		||||
 | 
				                            title: value | 
			
		||||
 | 
				                          })) | 
			
		||||
 | 
				                        }, | 
			
		||||
 | 
				                        allowClear: true, | 
			
		||||
 | 
				                        onChange: (e) => { | 
			
		||||
 | 
				                          setSearchKey(e.target?.value) | 
			
		||||
 | 
				                        }, | 
			
		||||
 | 
				                        value: searchKey, | 
			
		||||
 | 
				                        placeholder: t(`${i18nPrefix}.placeholder`, '输入模板名称') | 
			
		||||
 | 
				                      }, | 
			
		||||
 | 
				                      actions: [ | 
			
		||||
 | 
				                        <Tooltip key={'filter'} title={t(`${i18nPrefix}.filter.tooltip`, '高级查询')}> | 
			
		||||
 | 
				                          <Badge count={getValueCount(search)}> | 
			
		||||
 | 
				                            <Button | 
			
		||||
 | 
				                                    onClick={() => { | 
			
		||||
 | 
				                                      setFilterOpen(true) | 
			
		||||
 | 
				                                    }} | 
			
		||||
 | 
				                                    icon={<FilterOutlined/>} shape={'circle'} size={'small'}/> | 
			
		||||
 | 
				                          </Badge> | 
			
		||||
 | 
				                        </Tooltip>, | 
			
		||||
 | 
				                        <Divider type={'vertical'} key={'divider'}/>, | 
			
		||||
 | 
				                        <Button key={'add'} | 
			
		||||
 | 
				                                onClick={() => { | 
			
		||||
 | 
				                                  form.resetFields() | 
			
		||||
 | 
				                                  form.setFieldsValue({ | 
			
		||||
 | 
				                                    id: 0, | 
			
		||||
 | 
				                                  }) | 
			
		||||
 | 
				                                  setOpen(true) | 
			
		||||
 | 
				                                }} | 
			
		||||
 | 
				                                type={'primary'}>{t(`${i18nPrefix}.add`, '发送消息')}</Button> | 
			
		||||
 | 
				                      ] | 
			
		||||
 | 
				                    }} | 
			
		||||
 | 
				                    scroll={{ | 
			
		||||
 | 
				                      x: columns.length * 200, | 
			
		||||
 | 
				                      y: 'calc(100vh - 290px)' | 
			
		||||
 | 
				                    }} | 
			
		||||
 | 
				                    search={false} | 
			
		||||
 | 
				                    dateFormatter="string" | 
			
		||||
 | 
				                    loading={isLoading || isFetching} | 
			
		||||
 | 
				                    dataSource={data?.rows ?? []} | 
			
		||||
 | 
				                    columns={columns} | 
			
		||||
 | 
				                    options={{ | 
			
		||||
 | 
				                      reload: () => { | 
			
		||||
 | 
				                        refetch() | 
			
		||||
 | 
				                      }, | 
			
		||||
 | 
				                    }} | 
			
		||||
 | 
				                    pagination={{ | 
			
		||||
 | 
				                      total: data?.total, | 
			
		||||
 | 
				                      pageSize: search.pageSize, | 
			
		||||
 | 
				                      current: search.page, | 
			
		||||
 | 
				                      onShowSizeChange: (current: number, size: number) => { | 
			
		||||
 | 
				                        setSearch({ | 
			
		||||
 | 
				                          ...search, | 
			
		||||
 | 
				                          pageSize: size, | 
			
		||||
 | 
				                          page: current | 
			
		||||
 | 
				                        }) | 
			
		||||
 | 
				                      }, | 
			
		||||
 | 
				                      onChange: (current, pageSize) => { | 
			
		||||
 | 
				                        setSearch(prev => { | 
			
		||||
 | 
				                          return { | 
			
		||||
 | 
				                            ...prev, | 
			
		||||
 | 
				                            page: current, | 
			
		||||
 | 
				                            pageSize: pageSize, | 
			
		||||
 | 
				                          } | 
			
		||||
 | 
				                        }) | 
			
		||||
 | 
				                      }, | 
			
		||||
 | 
				                    }} | 
			
		||||
 | 
				            /> | 
			
		||||
 | 
				            <BetaSchemaForm | 
			
		||||
 | 
				                    grid={true} | 
			
		||||
 | 
				                    shouldUpdate={false} | 
			
		||||
 | 
				                    width={1000} | 
			
		||||
 | 
				                    form={form} | 
			
		||||
 | 
				                    layout={'vertical'} | 
			
		||||
 | 
				                    scrollToFirstError={true} | 
			
		||||
 | 
				                    title={t(`${i18nPrefix}.title_add}`, '发送消息')} | 
			
		||||
 | 
				                    layoutType={'DrawerForm'} | 
			
		||||
 | 
				                    open={open} | 
			
		||||
 | 
				                    drawerProps={{ | 
			
		||||
 | 
				                      maskClosable: false, | 
			
		||||
 | 
				                    }} | 
			
		||||
 | 
				                    onOpenChange={(open) => { | 
			
		||||
 | 
				                      setOpen(open) | 
			
		||||
 | 
				                    }} | 
			
		||||
 | 
				                    loading={isSubmitting} | 
			
		||||
 | 
				                    onValuesChange={() => { | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				                    }} | 
			
		||||
 | 
				                    onFinish={async (values) => { | 
			
		||||
 | 
				                      saveOrUpdate({...values, "template_id": currentTemplate?.id}) | 
			
		||||
 | 
				                    }} | 
			
		||||
 | 
				                    columns={drawerColumns as ProFormColumnsType[]}/> | 
			
		||||
 | 
				            <BetaSchemaForm | 
			
		||||
 | 
				                    title={t(`${i18nPrefix}.filter.title`, '模板高级查询')} | 
			
		||||
 | 
				                    grid={true} | 
			
		||||
 | 
				                    shouldUpdate={false} | 
			
		||||
 | 
				                    width={500} | 
			
		||||
 | 
				                    form={filterForm} | 
			
		||||
 | 
				                    open={openFilter} | 
			
		||||
 | 
				                    onOpenChange={open => { | 
			
		||||
 | 
				                      setFilterOpen(open) | 
			
		||||
 | 
				                    }} | 
			
		||||
 | 
				                    layout={'vertical'} | 
			
		||||
 | 
				                    scrollToFirstError={true} | 
			
		||||
 | 
				                    layoutType={'DrawerForm'} | 
			
		||||
 | 
				                    drawerProps={{ | 
			
		||||
 | 
				                      maskClosable: false, | 
			
		||||
 | 
				                      mask: false, | 
			
		||||
 | 
				                    }} | 
			
		||||
 | 
				                    submitter={{ | 
			
		||||
 | 
				                      searchConfig: { | 
			
		||||
 | 
				                        resetText: t(`${i18nPrefix}.filter.reset`, '清空'), | 
			
		||||
 | 
				                        submitText: t(`${i18nPrefix}.filter.submit`, '查询'), | 
			
		||||
 | 
				                      }, | 
			
		||||
 | 
				                      onReset: () => { | 
			
		||||
 | 
				                        filterForm.resetFields() | 
			
		||||
 | 
				                      }, | 
			
		||||
 | 
				                      render: (props,) => { | 
			
		||||
 | 
				                        return ( | 
			
		||||
 | 
				                                <div style={{textAlign: 'right'}}> | 
			
		||||
 | 
				                                  <Space> | 
			
		||||
 | 
				                                    <Button onClick={() => { | 
			
		||||
 | 
				                                      props.reset() | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				                                    }}>{props.searchConfig?.resetText}</Button> | 
			
		||||
 | 
				                                    <Button type="primary" | 
			
		||||
 | 
				                                            onClick={() => { | 
			
		||||
 | 
				                                              props.submit() | 
			
		||||
 | 
				                                            }} | 
			
		||||
 | 
				                                    >{props.searchConfig?.submitText}</Button> | 
			
		||||
 | 
				                                  </Space> | 
			
		||||
 | 
				                                </div> | 
			
		||||
 | 
				                        ) | 
			
		||||
 | 
				                      }, | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				                    }} | 
			
		||||
 | 
				                    onValuesChange={() => { | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				                    }} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				                    onFinish={async (values) => { | 
			
		||||
 | 
				                      //处理,变成数组
 | 
			
		||||
 | 
				                      Object.keys(values).forEach(key => { | 
			
		||||
 | 
				                        if (typeof values[key] === 'string' && values[key].includes(',')) { | 
			
		||||
 | 
				                          values[key] = values[key].split(',') | 
			
		||||
 | 
				                        } | 
			
		||||
 | 
				                      }) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				                      setSearch(values) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				                    }} | 
			
		||||
 | 
				                    columns={unSetColumnRules(columns.filter(item => !item.hideInSearch)) as ProFormColumnsType[]}/> | 
			
		||||
 | 
				          </ListPageLayout> | 
			
		||||
 | 
				  ) | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export default MdwMessage | 
			
		||||
@ -0,0 +1,26 @@ | 
			
		|||||
 | 
				import { createStyles } from '@/theme' | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export const useStyle = createStyles(({ token, css, cx, prefixCls }, props: any) => { | 
			
		||||
 | 
				    const prefix = `${prefixCls}-${token?.proPrefix}-appPackage-list-page` | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    const container = css`
 | 
			
		||||
 | 
				        .ant-table-cell{ | 
			
		||||
 | 
				          .ant-tag{ | 
			
		||||
 | 
				            padding-inline: 3px; | 
			
		||||
 | 
				            margin-inline-end: 3px; | 
			
		||||
 | 
				          } | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				        .ant-table-empty { | 
			
		||||
 | 
				          .ant-table-body{ | 
			
		||||
 | 
				            height: calc(100vh - 350px) | 
			
		||||
 | 
				          } | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				      .ant-pro-table-highlight{ | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				      } | 
			
		||||
 | 
				    `
 | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    return { | 
			
		||||
 | 
				        container: cx(prefix, props?.className, container), | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				}) | 
			
		||||
@ -1,17 +0,0 @@ | 
			
		|||||
import request from '@/request.ts' | 
				 | 
			
		||||
import { IApiResult, IPageResult } from '@/global' | 
				 | 
			
		||||
import { IMsgFieldRes, IMsgTemplate } from '@/types/system/message.ts' | 
				 | 
			
		||||
import { createCURD } from '@/service/base.ts' | 
				 | 
			
		||||
 | 
				 | 
			
		||||
const mdwMessage = { | 
				 | 
			
		||||
  ...createCURD<any, IMsgTemplate>('/mdw-msg/template'), | 
				 | 
			
		||||
  list: async (params: any) => { | 
				 | 
			
		||||
    return await request.get<IPageResult<IMsgTemplate>>(`mdw-msg/template/list`, { ...params }) | 
				 | 
			
		||||
  }, | 
				 | 
			
		||||
 | 
				 | 
			
		||||
  fieldList: async (params: any) => { | 
				 | 
			
		||||
    return await request.get<IApiResult<IMsgFieldRes>>(`mdw-msg/template/fieldList`, { ...params }) | 
				 | 
			
		||||
  }, | 
				 | 
			
		||||
} | 
				 | 
			
		||||
 | 
				 | 
			
		||||
export default mdwMessage | 
				 | 
			
		||||
@ -0,0 +1,13 @@ | 
			
		|||||
 | 
				import request from '@/request.ts' | 
			
		||||
 | 
				import { IPageResult } from '@/global' | 
			
		||||
 | 
				import { createCURD } from '@/service/base.ts' | 
			
		||||
 | 
				import { IMsgMy } from '@/types/message/my.ts' | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				const messageMy = { | 
			
		||||
 | 
				  ...createCURD<any, IMsgMy>('/mdw-msg/msg'), | 
			
		||||
 | 
				  list: async (params: any) => { | 
			
		||||
 | 
				    return await request.get<IPageResult<IMsgMy>>(`mdw-msg/msg/list`, { ...params }) | 
			
		||||
 | 
				  }, | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export default messageMy | 
			
		||||
@ -0,0 +1,17 @@ | 
			
		|||||
 | 
				import request from '@/request.ts' | 
			
		||||
 | 
				import { IListAllResult, IPageResult } from '@/global' | 
			
		||||
 | 
				import { IMsgTemplate } from '@/types/message/template.ts' | 
			
		||||
 | 
				import { createCURD } from '@/service/base.ts' | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				const messageTemplate = { | 
			
		||||
 | 
				  ...createCURD<any, IMsgTemplate>('/mdw-msg/template'), | 
			
		||||
 | 
				  list: async (params: any) => { | 
			
		||||
 | 
				    return await request.get<IPageResult<IMsgTemplate>>(`mdw-msg/template/list`, { ...params }) | 
			
		||||
 | 
				  }, | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  listAll: async () => { | 
			
		||||
 | 
				    return await request.get<IListAllResult<IMsgTemplate>>(`mdw-msg/template/listAll`) | 
			
		||||
 | 
				  }, | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export default messageTemplate | 
			
		||||
@ -0,0 +1,68 @@ | 
			
		|||||
 | 
				import { atom } from 'jotai/index' | 
			
		||||
 | 
				import { IApiResult, IPage } from '@/global' | 
			
		||||
 | 
				import { atomWithMutation, atomWithQuery, queryClientAtom } from 'jotai-tanstack-query' | 
			
		||||
 | 
				import { message } from 'antd' | 
			
		||||
 | 
				import { t } from 'i18next' | 
			
		||||
 | 
				import { IMsgMy } from '@/types/message/my.ts' | 
			
		||||
 | 
				import messageMy from '@/service/message/my.ts' | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				type SearchParams = IPage & { | 
			
		||||
 | 
				  key?: string | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  [key: string]: any | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export const msgIdsAtom = atom<number>(0) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export const msgSearchAtom = atom<SearchParams>({ | 
			
		||||
 | 
				  key: '', | 
			
		||||
 | 
				  pageSize: 10, | 
			
		||||
 | 
				  page: 1, | 
			
		||||
 | 
				} as SearchParams) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export const msgPageAtom = atom<IPage>({ | 
			
		||||
 | 
				  pageSize: 10, | 
			
		||||
 | 
				  page: 1, | 
			
		||||
 | 
				}) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export const msgListAtom = atomWithQuery((get) => { | 
			
		||||
 | 
				  return { | 
			
		||||
 | 
				    queryKey: [ 'msgList', get(msgSearchAtom) ], | 
			
		||||
 | 
				    queryFn: async ({ queryKey: [ , params ] }) => { | 
			
		||||
 | 
				      const list = await messageMy.list(params as SearchParams) | 
			
		||||
 | 
				      return list.data | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				}) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export const deleteMsgAtom = atomWithMutation((get) => { | 
			
		||||
 | 
				  return { | 
			
		||||
 | 
				    mutationKey: [ 'deleteMsg' ], | 
			
		||||
 | 
				    mutationFn: async (ids: number) => { | 
			
		||||
 | 
				      return await messageMy.delete(ids ?? get(msgIdsAtom) as number) | 
			
		||||
 | 
				    }, | 
			
		||||
 | 
				    onSuccess: (res) => { | 
			
		||||
 | 
				      message.success('message.deleteSuccess') | 
			
		||||
 | 
				      //更新列表
 | 
			
		||||
 | 
				      get(queryClientAtom).invalidateQueries({ queryKey: [ 'msgList', get(msgSearchAtom) ] }) | 
			
		||||
 | 
				      return res | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				}) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export const saveMsgAtom = atomWithMutation<IApiResult, IMsgMy>((get) => { | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  return { | 
			
		||||
 | 
				    mutationKey: [ 'saveMsg' ], | 
			
		||||
 | 
				    mutationFn: async (data) => { | 
			
		||||
 | 
				      return await messageMy.add(data) | 
			
		||||
 | 
				    }, | 
			
		||||
 | 
				    onSuccess: (res) => { | 
			
		||||
 | 
				      message.success(t('message.saveSuccess', '保存成功')) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				      get(queryClientAtom).invalidateQueries({ queryKey: [ 'msgList', get(msgSearchAtom) ] }) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				      return res | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				}) | 
			
		||||
@ -0,0 +1,11 @@ | 
			
		|||||
 | 
				export interface IMsgMy { | 
			
		||||
 | 
				  id: number | 
			
		||||
 | 
				  template_id: number | 
			
		||||
 | 
				  title: string | 
			
		||||
 | 
				  send_content: string | 
			
		||||
 | 
				  dest: string | 
			
		||||
 | 
				  type: string | 
			
		||||
 | 
				  status: number | 
			
		||||
 | 
				  error_message: string | 
			
		||||
 | 
				  send_at: number | 
			
		||||
 | 
				} | 
			
		||||
@ -0,0 +1,25 @@ | 
			
		|||||
 | 
				export interface IMsgTemplate { | 
			
		||||
 | 
				  id: number | 
			
		||||
 | 
				  name: string | 
			
		||||
 | 
				  content: string | 
			
		||||
 | 
				  title: string | 
			
		||||
 | 
				  dest: string | 
			
		||||
 | 
				  type: string | 
			
		||||
 | 
				  fields: string | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export const coverType = (type: string) => { | 
			
		||||
 | 
				  let typeText = '' | 
			
		||||
 | 
				  switch (type) { | 
			
		||||
 | 
				    case 'SMS': | 
			
		||||
 | 
				      typeText = '短信' | 
			
		||||
 | 
				      break | 
			
		||||
 | 
				    case 'EMAIL': | 
			
		||||
 | 
				      typeText = '邮件' | 
			
		||||
 | 
				      break | 
			
		||||
 | 
				    case 'TG': | 
			
		||||
 | 
				      typeText = 'Telegram' | 
			
		||||
 | 
				      break | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				  return typeText | 
			
		||||
 | 
				} | 
			
		||||
@ -1,25 +0,0 @@ | 
			
		|||||
export interface IMsgTemplate { | 
				 | 
			
		||||
  id: number | 
				 | 
			
		||||
  name: string | 
				 | 
			
		||||
  content: string | 
				 | 
			
		||||
  title: string | 
				 | 
			
		||||
  dest: string | 
				 | 
			
		||||
  type: string | 
				 | 
			
		||||
} | 
				 | 
			
		||||
 | 
				 | 
			
		||||
export interface IMsgFieldRes { | 
				 | 
			
		||||
  list: IMsgField[] | 
				 | 
			
		||||
} | 
				 | 
			
		||||
 | 
				 | 
			
		||||
 | 
				 | 
			
		||||
export interface IMsgField { | 
				 | 
			
		||||
  template_id: string | 
				 | 
			
		||||
  field_key: string | 
				 | 
			
		||||
  field_value: string | 
				 | 
			
		||||
} | 
				 | 
			
		||||
 | 
				 | 
			
		||||
export interface IMdwMsgReq { | 
				 | 
			
		||||
  id: string | 
				 | 
			
		||||
  name: string | 
				 | 
			
		||||
  content: string | 
				 | 
			
		||||
} | 
				 | 
			
		||||
						Write
						Preview
					
					
					Loading…
					
					Cancel
						Save
					
		Reference in new issue