This commit is contained in:
2026-05-25 17:08:18 +08:00
parent df501f6151
commit f1259b71b5
9178 changed files with 1626125 additions and 0 deletions
@@ -0,0 +1,250 @@
import React, { useEffect, useState } from 'react';
import {
Button, Card, Form, Input, Modal, Popconfirm, Select, Space, Table, Tag, Typography, message, Empty,
} from 'antd';
import {
BellOutlined, PlusOutlined, DeleteOutlined, SendOutlined, EyeOutlined, TeamOutlined,
} from '@ant-design/icons';
import { getAdminNotifications, createAdminNotification, deleteAdminNotification, getAdminUsers, getNotificationReadUsers } from '../api';
import { formatDate } from '../utils/formatDate';
interface NotificationRecord {
id: string;
title: string;
content: string;
type: string;
target: string;
targetUserId: string | null;
createdAt: string;
}
interface UserOption {
id: string;
username: string;
}
interface ReadUser {
userId: string;
username: string;
readAt: string;
}
const AdminNotificationManager: React.FC = () => {
const [notifications, setNotifications] = useState<NotificationRecord[]>([]);
const [loading, setLoading] = useState(false);
const [users, setUsers] = useState<UserOption[]>([]);
const [modalOpen, setModalOpen] = useState(false);
const [form] = Form.useForm();
const [readModal, setReadModal] = useState<{ open: boolean; notifId: string; title: string }>({ open: false, notifId: '', title: '' });
const [readUsers, setReadUsers] = useState<ReadUser[]>([]);
const [readLoading, setReadLoading] = useState(false);
const load = async () => {
setLoading(true);
try {
const res = await getAdminNotifications();
const userList = await getAdminUsers();
const userMap: Record<string, string> = {};
userList.forEach((u: any) => { userMap[u.id] = u.username; });
const items = (res.items || []).map((n: any) => ({
id: n.id,
title: n.title,
content: n.content,
type: n.type,
targetUserId: n.userId || null,
target: n.userId ? (userMap[n.userId] || n.userId) : '全部用户',
createdAt: n.createdAt,
}));
setNotifications(items);
setUsers(userList.map((u: any) => ({ id: u.id, username: u.username })));
} catch {
message.error('加载通知列表失败');
} finally {
setLoading(false);
}
};
useEffect(() => { load(); }, []);
const handleSend = async () => {
try {
const values = await form.validateFields();
await createAdminNotification({
title: values.title,
content: values.content,
type: values.type || 'system',
target_user_id: values.target_user_id || undefined,
});
message.success('消息已发送');
setModalOpen(false);
form.resetFields();
load();
} catch { /* validation */ }
};
const handleDelete = async (id: string) => {
try {
await deleteAdminNotification(id);
message.success('已删除');
load();
} catch (e: any) {
message.error(e?.message || '删除失败');
}
};
const handleViewRead = async (notifId: string, title: string) => {
setReadModal({ open: true, notifId, title });
setReadLoading(true);
try {
const res = await getNotificationReadUsers(notifId);
setReadUsers(res.items || []);
} catch {
message.error('加载已读列表失败');
} finally {
setReadLoading(false);
}
};
const getTypeColor = (type: string) => {
switch (type) {
case 'system': return 'blue';
case 'credit': return 'orange';
case 'promo': return 'purple';
default: return 'default';
}
};
const columns = [
{
title: '标题', dataIndex: 'title', width: 200,
render: (v: string) => <Typography.Text strong>{v}</Typography.Text>,
},
{
title: '内容', dataIndex: 'content', ellipsis: true,
},
{
title: '类型', dataIndex: 'type', width: 80,
render: (v: string) => {
const labels: Record<string, string> = { system: '系统', credit: '积分', promo: '活动' };
return <Tag color={getTypeColor(v)}>{labels[v] || v}</Tag>;
},
},
{
title: '发送目标', dataIndex: 'target', width: 120,
render: (v: string) => (
<Tag color={v === '全部用户' ? 'green' : 'blue'}>{v}</Tag>
),
},
{
title: '发送时间', dataIndex: 'createdAt', width: 160,
render: (v: string) => formatDate(v),
},
{
title: '操作', key: 'action', width: 180,
render: (_: any, r: NotificationRecord) => (
<Space size={4}>
<Button type="link" size="small" icon={<EyeOutlined />} onClick={() => handleViewRead(r.id, r.title)}>
</Button>
<Popconfirm title="确定删除该消息?" onConfirm={() => handleDelete(r.id)}>
<Button type="link" danger size="small" icon={<DeleteOutlined />}></Button>
</Popconfirm>
</Space>
),
},
];
return (
<div>
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
<Space>
<BellOutlined style={{ fontSize: 18, color: '#6366f1' }} />
<Typography.Text strong style={{ fontSize: 16 }}></Typography.Text>
<Tag color="purple">{notifications.length} </Tag>
</Space>
<Button type="primary" icon={<PlusOutlined />} onClick={() => setModalOpen(true)}
style={{ borderRadius: 8 }}>
</Button>
</div>
<Table
columns={columns}
dataSource={notifications}
rowKey="id"
loading={loading}
pagination={{ pageSize: 10, showTotal: (t) => `${t} 条消息` }}
scroll={{ x: 900 }}
/>
</Card>
{/* Send Notification Modal */}
<Modal
title={<Space><SendOutlined /></Space>}
open={modalOpen}
onOk={handleSend}
onCancel={() => { setModalOpen(false); form.resetFields(); }}
okText="发送" cancelText="取消" width={520}
>
<Form form={form} layout="vertical" style={{ marginTop: 16 }}>
<Form.Item name="title" label="消息标题"
rules={[{ required: true, message: '请输入标题' }]}>
<Input placeholder="请输入消息标题" size="large" />
</Form.Item>
<Form.Item name="content" label="消息内容"
rules={[{ required: true, message: '请输入内容' }]}>
<Input.TextArea rows={4} placeholder="请输入消息内容" size="large" />
</Form.Item>
<div style={{ display: 'flex', gap: 16 }}>
<Form.Item name="type" label="消息类型" style={{ flex: 1 }}
initialValue="system" rules={[{ required: true }]}>
<Select size="large" options={[
{ value: 'system', label: '系统通知' },
{ value: 'credit', label: '积分通知' },
{ value: 'promo', label: '活动通知' },
]} />
</Form.Item>
<Form.Item name="target_user_id" label="发送目标" style={{ flex: 1 }}
extra="留空则发送给全部用户">
<Select size="large" allowClear placeholder="全部用户"
options={users.map(u => ({ value: u.id, label: u.username }))} />
</Form.Item>
</div>
</Form>
</Modal>
{/* Read Users Modal */}
<Modal
title={<Space><TeamOutlined /> {readModal.title}</Space>}
open={readModal.open}
onCancel={() => { setReadModal({ open: false, notifId: '', title: '' }); setReadUsers([]); }}
footer={null} width={480}
>
{readLoading ? (
<div style={{ textAlign: 'center', padding: 40 }}>...</div>
) : readUsers.length === 0 ? (
<Empty description="暂无用户已读" style={{ padding: '40px 0' }} />
) : (
<div>
<div style={{ marginBottom: 12, color: '#64748b', fontSize: 13 }}>
{readUsers.length}
</div>
<Table
dataSource={readUsers}
rowKey="userId"
pagination={false}
size="small"
columns={[
{ title: '用户名', dataIndex: 'username', render: (v: string) => <Typography.Text strong>{v}</Typography.Text> },
{ title: '已读时间', dataIndex: 'readAt', width: 180, render: (v: string) => formatDate(v) },
]}
/>
</div>
)}
</Modal>
</div>
);
};
export default AdminNotificationManager;