lk
2 months ago
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: '[email protected],[email protected]', |
|||
}, |
|||
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