Browse Source

消息模板

main
cs 3 months ago
parent
commit
87cc601a21
  1. 8
      src/global.d.ts
  2. 345
      src/pages/message/my/index.tsx
  3. 0
      src/pages/message/my/style.ts
  4. 74
      src/pages/message/template/index.tsx
  5. 26
      src/pages/message/template/style.ts
  6. 17
      src/service/message/message.ts
  7. 13
      src/service/message/my.ts
  8. 17
      src/service/message/template.ts
  9. 71
      src/store/message/my.ts
  10. 25
      src/store/message/template.ts
  11. 11
      src/types/message/my.ts
  12. 9
      src/types/message/template.ts
  13. 25
      src/types/system/message.ts

8
src/global.d.ts

@ -33,6 +33,14 @@ export type IPageResult<T = any> = IPage & {
total: number total: number
} }
export type IListAllResult<T = any> = {
list: T[]
}
export type IBoolResult = {
success: boolean
}
export type IApiResult<T = any> = { export type IApiResult<T = any> = {
code: number; code: number;
data: T; data: T;

345
src/pages/message/my/index.tsx

@ -0,0 +1,345 @@
import {useTranslation} from '@/i18n.ts'
import {Badge, Button, Divider, Form, Input, 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";
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 [searchKey, setSearchKey] = useState(search?.title)
const drawerColumns = useMemo(() => {
return [
{
title: 'ID',
dataIndex: 'id',
hideInTable: true,
hideInSearch: true,
formItemProps: {hidden: true}
},
{
title: t(`${i18nPrefix}.columns.type`, '选择模板'),
dataIndex: 'type',
valueType: 'select',
fieldProps: {
options: [
{label: '短信', value: 'SMS'},
{label: '邮件', value: 'EMAIL'},
{label: 'Telegram', value: 'TG'}
],
allowClear: false,
},
formItemProps: {
rules: [
{
required: true,
}
]
}
},
{
title: t(`${i18nPrefix}.columns.title`, '标题'),
dataIndex: 'title',
valueType: 'text',
fieldProps: {
maxLength: 100,
showCount: true,
},
},
{
title: t(`${i18nPrefix}.columns.content`, '模板内容'),
dataIndex: 'content',
valueType: 'textarea',
fieldProps: {
defaultValue: "你好,我叫${.name}",
maxLength: 1000,
showCount: true,
rows: 15,
},
formItemProps: {
rules: [
{
required: true,
message: t('message.required', '模板内容必填')
}
]
}
},
{
title: t(`${i18nPrefix}.columns.field`, '识别到的变量'),
dataIndex: 'field',
renderFormItem: () => {
return (
<>
{
templateList?.map((variable, index) => (
<div key={index} style={{marginBottom: 8}}>
<Input
addonBefore={`${variable.name}:`}
defaultValue=""
placeholder={`请输入${variable}`}
/>
</div>
))}
</>
);
}
},
{
title: t(`${i18nPrefix}.columns.dest`, '收件人(多个收件人用英文逗号隔开,如果类型是TG,则填token)'),
dataIndex: 'dest',
valueType: 'textarea',
fieldProps: {
maxLength: 1000,
showCount: true,
rows: 15,
placeholder: 'aaa@qq.com,bbb@gmail.com',
},
},
] as ProColumns[]
}, [search])
const columns = useMemo(() => {
return [
{
title: 'ID',
dataIndex: 'id',
hideInTable: true,
hideInSearch: true,
formItemProps: {hidden: true}
},
{
title: t(`${i18nPrefix}.columns.name`, '模板名称'),
dataIndex: 'name',
},
{
title: t(`${i18nPrefix}.columns.title`, '模板标题'),
dataIndex: 'title',
},
{
title: t(`${i18nPrefix}.columns.content`, '模板内容'),
dataIndex: 'content',
},
// {
// 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])
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_${form.getFieldValue('id') !== 0 ? 'edit' : 'add'}`, form.getFieldValue('id') !== 0 ? '模板编辑' : '模板添加')}
layoutType={'DrawerForm'}
open={open}
drawerProps={{
maskClosable: false,
}}
onOpenChange={(open) => {
setOpen(open)
}}
loading={isSubmitting}
onValuesChange={() => {
}}
onFinish={async (values) => {
saveOrUpdate(values)
}}
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
src/pages/system/message/style.ts → src/pages/message/my/style.ts

74
src/pages/system/message/index.tsx → src/pages/message/template/index.tsx

@ -5,7 +5,7 @@ import React, {useEffect, useMemo, useState} from 'react'
import Action from '@/components/action/Action.tsx' import Action from '@/components/action/Action.tsx'
import {BetaSchemaForm, ProColumns, ProFormColumnsType,} from '@ant-design/pro-components' import {BetaSchemaForm, ProColumns, ProFormColumnsType,} from '@ant-design/pro-components'
import ListPageLayout from '@/layout/ListPageLayout.tsx' import ListPageLayout from '@/layout/ListPageLayout.tsx'
import {useStyle} from './style'
import {useStyle} from './style.ts'
import {FilterOutlined} from '@ant-design/icons' import {FilterOutlined} from '@ant-design/icons'
import {getValueCount, unSetColumnRules} from '@/utils' import {getValueCount, unSetColumnRules} from '@/utils'
import {Table as ProTable} from '@/components/table' import {Table as ProTable} from '@/components/table'
@ -15,7 +15,7 @@ import {
templateAtom, templateAtom,
templateListAtom, templateListAtom,
templateSearchAtom templateSearchAtom
} from "@/store/system/message.ts";
} from "@/store/message/template.ts";
const i18nPrefix = 'mdwMessage.list' const i18nPrefix = 'mdwMessage.list'
@ -35,49 +35,42 @@ const MdwMessage = () => {
const [openFilter, setFilterOpen] = useState(false) const [openFilter, setFilterOpen] = useState(false)
const [searchKey, setSearchKey] = useState(search?.title) const [searchKey, setSearchKey] = useState(search?.title)
const [templateField, setTemplateField] = useState<string[]>(['name']);
const [titleTemplateField, setTitleTemplateField] = useState<string[]>([]);
const [templateField, setTemplateField] = useState<string[]>([]);
const [allTemplateField, setAllTemplateField] = useState<string[]>([]);
useEffect(() => {
const combinedFields = [...templateField, ...titleTemplateField];
const uniqueFields = Array.from(new Set(combinedFields));
setAllTemplateField(uniqueFields);
}, [templateField, titleTemplateField]);
const [templateTitle, setTemplateTitle] = useState('');
const [templateContent, setTemplateContent] = useState('');
const [templateType, setTemplateType] = useState('') const [templateType, setTemplateType] = useState('')
useEffect(() => { useEffect(() => {
setTemplateType(currentTemplate?.type) setTemplateType(currentTemplate?.type)
}, [open]);
const handleContentChange = (e) => {
const value = e.target.value;
if (form.getFieldValue('id') === 0) {
setTemplateField(['name']);
} else {
setTemplateField(currentTemplate.fields.split(","));
}
}, [open]);
const handleChange = (title: string, content: string) => {
// 使用正则表达式匹配 ${var} 格式的变量 // 使用正则表达式匹配 ${var} 格式的变量
const regex = /\${\.([a-zA-Z0-9_]+)}/g; const regex = /\${\.([a-zA-Z0-9_]+)}/g;
const matches = [...value.matchAll(regex)];
const matches = [...(title + content).matchAll(regex)];
// 提取变量名 // 提取变量名
const variables = Array.from(new Set(matches.map(match => match[1]))); const variables = Array.from(new Set(matches.map(match => match[1])));
// 更新变量状态
setTemplateField(variables); setTemplateField(variables);
}; };
const titheHandleContentChange = (e) => {
const handleContentChange = (e) => {
const value = e.target.value; const value = e.target.value;
setTemplateContent(value)
handleChange(templateTitle, value)
};
// 使用正则表达式匹配 ${var} 格式的变量
const regex = /\${\.([a-zA-Z0-9_]+)}/g;
const matches = [...value.matchAll(regex)];
// 提取变量名
const variables = Array.from(new Set(matches.map(match => match[1])));
// 更新变量状态
setTitleTemplateField(variables);
const titheHandleContentChange = (e) => {
const value = e.target.value;
setTemplateTitle(value)
handleChange(value, templateContent)
}; };
const typeHandlerChange = (value) => { const typeHandlerChange = (value) => {
@ -85,7 +78,6 @@ const MdwMessage = () => {
setTemplateType(value) setTemplateType(value)
} }
const drawerColumns = useMemo(() => { const drawerColumns = useMemo(() => {
return [ return [
{ {
@ -174,13 +166,13 @@ const MdwMessage = () => {
} }
}, },
{ {
title: t(`${i18nPrefix}.columns.field`, '识别到的变量'),
dataIndex: 'field',
title: t(`${i18nPrefix}.columns.fields`, '识别到的变量'),
dataIndex: 'fields',
renderFormItem: () => { renderFormItem: () => {
return ( return (
<> <>
{ {
allTemplateField.map((variable, index) => (
templateField.map((variable, index) => (
<Tag key={index} color="blue" style={{marginRight: 8}}> <Tag key={index} color="blue" style={{marginRight: 8}}>
{variable} {variable}
</Tag> </Tag>
@ -202,7 +194,7 @@ const MdwMessage = () => {
}, },
}, },
] as ProColumns[] ] as ProColumns[]
}, [isDeleting, currentTemplate, search, allTemplateField, templateType])
}, [isDeleting, currentTemplate, search, templateField, templateType])
const columns = useMemo(() => { const columns = useMemo(() => {
return [ return [
@ -218,6 +210,13 @@ const MdwMessage = () => {
dataIndex: 'name', dataIndex: 'name',
}, },
{ {
title: t(`${i18nPrefix}.columns.type`, '模板类型'),
dataIndex: 'type',
render: (_, record) => {
return <div>{record.type}</div>
}
},
{
title: t(`${i18nPrefix}.columns.title`, '模板标题'), title: t(`${i18nPrefix}.columns.title`, '模板标题'),
dataIndex: 'title', dataIndex: 'title',
}, },
@ -249,13 +248,6 @@ const MdwMessage = () => {
{t('actions.delete', '删除')} {t('actions.delete', '删除')}
</a> </a>
</Popconfirm>, </Popconfirm>,
<Divider type={'vertical'}/>,
<Action key="send"
as={'a'}
onClick={() => {
form.setFieldsValue(record)
setOpen(true)
}}>{t('actions.sendMsg', '发送消息')}</Action>,
] ]
} }
] as ProColumns[] ] as ProColumns[]
@ -384,7 +376,7 @@ const MdwMessage = () => {
}} }}
onFinish={async (values) => { onFinish={async (values) => {
saveOrUpdate({...values, 'fields': allTemplateField})
saveOrUpdate({...values, 'fields': templateField.join()})
}} }}
columns={drawerColumns as ProFormColumnsType[]}/> columns={drawerColumns as ProFormColumnsType[]}/>
<BetaSchemaForm <BetaSchemaForm

26
src/pages/message/template/style.ts

@ -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),
}
})

17
src/service/message/message.ts

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

13
src/service/message/my.ts

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

17
src/service/message/template.ts

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

71
src/store/message/my.ts

@ -0,0 +1,71 @@
import { atom } from 'jotai/index'
import { IApiResult, IPage } from '@/global'
import { atomWithMutation, atomWithQuery, queryClientAtom } from 'jotai-tanstack-query'
import messageTemplate from '@/service/message/template.ts'
import { message } from 'antd'
import { t } from 'i18next'
import { IMsgMy } from '@/types/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 messageTemplate.list(params as SearchParams)
return list.data
}
}
})
export const deleteMsgAtom = atomWithMutation((get) => {
return {
mutationKey: [ 'deleteMsg' ],
mutationFn: async (ids: number) => {
return await messageTemplate.delete(ids ?? get(msgIdsAtom) as number)
},
onSuccess: (res) => {
message.success('message.deleteSuccess')
//更新列表
get(queryClientAtom).invalidateQueries({ queryKey: [ 'templateList', get(msgSearchAtom) ] })
return res
}
}
})
export const saveMsgAtom = atomWithMutation<IApiResult, IMsgMy>((get) => {
return {
mutationKey: [ 'saveMsg' ],
mutationFn: async (data) => {
if (data.id === 0) {
return await messageTemplate.add(data)
}
return await messageTemplate.update(data)
},
onSuccess: (res) => {
message.success(t('message.saveSuccess', '保存成功'))
get(queryClientAtom).invalidateQueries({ queryKey: [ 'templateList', get(msgSearchAtom) ] })
return res
}
}
})

25
src/store/system/message.ts → src/store/message/template.ts

@ -1,8 +1,8 @@
import { atom } from 'jotai/index' import { atom } from 'jotai/index'
import { IApiResult, IPage } from '@/global' import { IApiResult, IPage } from '@/global'
import { atomWithMutation, atomWithQuery, queryClientAtom } from 'jotai-tanstack-query' import { atomWithMutation, atomWithQuery, queryClientAtom } from 'jotai-tanstack-query'
import { IMsgTemplate } from '@/types/system/message.ts'
import mdwMessage from '@/service/message/message.ts'
import { IMsgTemplate } from '@/types/message/template.ts'
import messageTemplate from '@/service/message/template.ts'
import { message } from 'antd' import { message } from 'antd'
import { t } from 'i18next' import { t } from 'i18next'
@ -12,7 +12,7 @@ type SearchParams = IPage & {
[key: string]: any [key: string]: any
} }
export const templateAIdsAtom = atom<number>(0)
export const templateIdsAtom = atom<number>(0)
export const templateAtom = atom<IMsgTemplate>(undefined as unknown as IMsgTemplate) export const templateAtom = atom<IMsgTemplate>(undefined as unknown as IMsgTemplate)
@ -31,18 +31,18 @@ export const templateListAtom = atomWithQuery((get) => {
return { return {
queryKey: [ 'templateList', get(templateSearchAtom) ], queryKey: [ 'templateList', get(templateSearchAtom) ],
queryFn: async ({ queryKey: [ , params ] }) => { queryFn: async ({ queryKey: [ , params ] }) => {
const list = await mdwMessage.list(params as SearchParams)
const list = await messageTemplate.list(params as SearchParams)
return list.data return list.data
} }
} }
}) })
export const templateFieldAtom = atomWithQuery(() => {
export const templateAllListAtom = atomWithQuery(() => {
return { return {
queryKey: [ 'templateField' ],
queryFn: async ({ queryKey: [ , params ] }) => {
const list = await mdwMessage.fieldList(params)
return list.data
queryKey: [ 'templateAllList'],
queryFn: async ({ queryKey: [ , ] }) => {
const list = await messageTemplate.listAll()
return list.data.list
} }
} }
}) })
@ -51,7 +51,7 @@ export const deleteTemplateAtom = atomWithMutation((get) => {
return { return {
mutationKey: [ 'deleteTemplate' ], mutationKey: [ 'deleteTemplate' ],
mutationFn: async (ids: number) => { mutationFn: async (ids: number) => {
return await mdwMessage.delete(ids ?? get(templateAIdsAtom) as number)
return await messageTemplate.delete(ids ?? get(templateIdsAtom) as number)
}, },
onSuccess: (res) => { onSuccess: (res) => {
message.success('message.deleteSuccess') message.success('message.deleteSuccess')
@ -67,11 +67,10 @@ export const saveOrUpdateTemplateAtom = atomWithMutation<IApiResult, IMsgTemplat
return { return {
mutationKey: [ 'updateTemplate' ], mutationKey: [ 'updateTemplate' ],
mutationFn: async (data) => { mutationFn: async (data) => {
//data.status = data.status ? '1' : '0'
if (data.id === 0) { if (data.id === 0) {
return await mdwMessage.add(data)
return await messageTemplate.add(data)
} }
return await mdwMessage.update(data)
return await messageTemplate.update(data)
}, },
onSuccess: (res) => { onSuccess: (res) => {
const isAdd = !!res.data?.id const isAdd = !!res.data?.id

11
src/types/message/my.ts

@ -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
}

9
src/types/message/template.ts

@ -0,0 +1,9 @@
export interface IMsgTemplate {
id: number
name: string
content: string
title: string
dest: string
type: string
fields: string
}

25
src/types/system/message.ts

@ -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
}
Loading…
Cancel
Save