feat(message): add message center module (Phase 5)
Implement the complete message center with: - Database migrations for message_templates, messages, message_subscriptions tables - erp-message crate with entities, DTOs, services, handlers - Message CRUD, send, read/unread tracking, soft delete - Template management with variable interpolation - Subscription preferences with DND support - Frontend: messages page, notification panel, unread count badge - Server integration with module registration and routing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
117
apps/web/src/pages/messages/MessageTemplates.tsx
Normal file
117
apps/web/src/pages/messages/MessageTemplates.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Table, Button, Modal, Form, Input, Select, message } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { listTemplates, createTemplate, type MessageTemplateInfo } from '../../api/messageTemplates';
|
||||
|
||||
export default function MessageTemplates() {
|
||||
const [data, setData] = useState<MessageTemplateInfo[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const fetchData = async (p = page) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await listTemplates(p, 20);
|
||||
setData(result.data);
|
||||
setTotal(result.total);
|
||||
} catch {
|
||||
message.error('加载模板列表失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData(1);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const handleCreate = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
await createTemplate(values);
|
||||
message.success('模板创建成功');
|
||||
setModalOpen(false);
|
||||
form.resetFields();
|
||||
fetchData();
|
||||
} catch {
|
||||
message.error('创建失败');
|
||||
}
|
||||
};
|
||||
|
||||
const columns: ColumnsType<MessageTemplateInfo> = [
|
||||
{ title: '名称', dataIndex: 'name', key: 'name' },
|
||||
{ title: '编码', dataIndex: 'code', key: 'code' },
|
||||
{
|
||||
title: '通道',
|
||||
dataIndex: 'channel',
|
||||
key: 'channel',
|
||||
render: (c: string) => {
|
||||
const map: Record<string, string> = { in_app: '站内', email: '邮件', sms: '短信', wechat: '微信' };
|
||||
return map[c] || c;
|
||||
},
|
||||
},
|
||||
{ title: '标题模板', dataIndex: 'title_template', key: 'title_template', ellipsis: true },
|
||||
{ title: '语言', dataIndex: 'language', key: 'language', width: 80 },
|
||||
{ title: '创建时间', dataIndex: 'created_at', key: 'created_at', width: 180 },
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Button type="primary" onClick={() => setModalOpen(true)}>新建模板</Button>
|
||||
</div>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
current: page,
|
||||
total,
|
||||
pageSize: 20,
|
||||
onChange: (p) => { setPage(p); fetchData(p); },
|
||||
}}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title="新建消息模板"
|
||||
open={modalOpen}
|
||||
onOk={handleCreate}
|
||||
onCancel={() => { setModalOpen(false); form.resetFields(); }}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item name="name" label="名称" rules={[{ required: true, message: '请输入名称' }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="code" label="编码" rules={[{ required: true, message: '请输入编码' }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="channel" label="通道" initialValue="in_app">
|
||||
<Select options={[
|
||||
{ value: 'in_app', label: '站内' },
|
||||
{ value: 'email', label: '邮件' },
|
||||
{ value: 'sms', label: '短信' },
|
||||
{ value: 'wechat', label: '微信' },
|
||||
]} />
|
||||
</Form.Item>
|
||||
<Form.Item name="title_template" label="标题模板" rules={[{ required: true, message: '请输入标题模板' }]}>
|
||||
<Input placeholder="支持 {{variable}} 变量插值" />
|
||||
</Form.Item>
|
||||
<Form.Item name="body_template" label="内容模板" rules={[{ required: true, message: '请输入内容模板' }]}>
|
||||
<Input.TextArea rows={4} placeholder="支持 {{variable}} 变量插值" />
|
||||
</Form.Item>
|
||||
<Form.Item name="language" label="语言" initialValue="zh-CN">
|
||||
<Select options={[
|
||||
{ value: 'zh-CN', label: '中文' },
|
||||
{ value: 'en-US', label: '英文' },
|
||||
]} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user