feat(admin-web): 添加通知管理功能
- 创建通知管理 API 服务 (notificationService.ts) - 添加通知列表页面,支持创建/编辑/删除/启用禁用 - 添加侧边栏"通知管理"菜单入口 - 支持按类型筛选通知 - 表单支持设置发布时间和过期时间 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
647f86ec89
commit
5d0264db92
|
|
@ -0,0 +1,268 @@
|
||||||
|
@use '@/styles/variables' as *;
|
||||||
|
|
||||||
|
.notifications {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__card {
|
||||||
|
background: $bg-card;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
box-shadow: $shadow-card;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__filters {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__select {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid $border-color;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
background: white;
|
||||||
|
min-width: 140px;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: $primary-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__loading,
|
||||||
|
&__empty,
|
||||||
|
&__error {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 60px 20px;
|
||||||
|
color: $text-secondary;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__error {
|
||||||
|
color: $error-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__item {
|
||||||
|
background: white;
|
||||||
|
border: 1px solid $border-color;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__itemHeader {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__itemMeta {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__tag {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&--blue {
|
||||||
|
background: #e6f4ff;
|
||||||
|
color: #1677ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--green {
|
||||||
|
background: #f6ffed;
|
||||||
|
color: #52c41a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--yellow {
|
||||||
|
background: #fffbe6;
|
||||||
|
color: #faad14;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--purple {
|
||||||
|
background: #f9f0ff;
|
||||||
|
color: #722ed1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--orange {
|
||||||
|
background: #fff7e6;
|
||||||
|
color: #fa8c16;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--gray {
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #8c8c8c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__priority,
|
||||||
|
&__target {
|
||||||
|
font-size: 12px;
|
||||||
|
color: $text-secondary;
|
||||||
|
padding: 2px 6px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__disabled {
|
||||||
|
font-size: 12px;
|
||||||
|
color: $error-color;
|
||||||
|
padding: 2px 6px;
|
||||||
|
background: #fff1f0;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__itemActions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__actionBtn {
|
||||||
|
padding: 4px 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: $text-secondary;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid $border-color;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $primary-color;
|
||||||
|
border-color: $primary-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--danger {
|
||||||
|
&:hover {
|
||||||
|
color: $error-color;
|
||||||
|
border-color: $error-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__itemTitle {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $text-primary;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__itemContent {
|
||||||
|
font-size: 14px;
|
||||||
|
color: $text-secondary;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__itemFooter {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: $text-tertiary;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表单样式
|
||||||
|
&__form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__formRow {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__formGroup {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: $text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid $border-color;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: $primary-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: $text-tertiary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__formHint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: $text-tertiary;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__modalFooter {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,457 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useCallback, useEffect } from 'react';
|
||||||
|
import { Modal, toast, Button } from '@/components/common';
|
||||||
|
import { PageContainer } from '@/components/layout';
|
||||||
|
import { cn } from '@/utils/helpers';
|
||||||
|
import { formatDateTime } from '@/utils/formatters';
|
||||||
|
import {
|
||||||
|
notificationService,
|
||||||
|
NotificationItem,
|
||||||
|
NotificationType,
|
||||||
|
NOTIFICATION_TYPE_OPTIONS,
|
||||||
|
NOTIFICATION_PRIORITY_OPTIONS,
|
||||||
|
TARGET_TYPE_OPTIONS,
|
||||||
|
} from '@/services/notificationService';
|
||||||
|
import styles from './notifications.module.scss';
|
||||||
|
|
||||||
|
// 获取类型标签样式
|
||||||
|
const getTypeStyle = (type: NotificationType) => {
|
||||||
|
const option = NOTIFICATION_TYPE_OPTIONS.find((o) => o.value === type);
|
||||||
|
return option?.color || 'gray';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取类型标签文本
|
||||||
|
const getTypeLabel = (type: NotificationType) => {
|
||||||
|
const option = NOTIFICATION_TYPE_OPTIONS.find((o) => o.value === type);
|
||||||
|
return option?.label || type;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取优先级标签
|
||||||
|
const getPriorityLabel = (priority: string) => {
|
||||||
|
const option = NOTIFICATION_PRIORITY_OPTIONS.find((o) => o.value === priority);
|
||||||
|
return option?.label || priority;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取目标用户标签
|
||||||
|
const getTargetLabel = (target: string) => {
|
||||||
|
const option = TARGET_TYPE_OPTIONS.find((o) => o.value === target);
|
||||||
|
return option?.label || target;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知管理页面
|
||||||
|
*/
|
||||||
|
export default function NotificationsPage() {
|
||||||
|
const [notifications, setNotifications] = useState<NotificationItem[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [typeFilter, setTypeFilter] = useState<NotificationType | ''>('');
|
||||||
|
|
||||||
|
// 弹窗状态
|
||||||
|
const [showModal, setShowModal] = useState(false);
|
||||||
|
const [editingNotification, setEditingNotification] = useState<NotificationItem | null>(null);
|
||||||
|
const [deleteConfirm, setDeleteConfirm] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// 表单状态
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
title: '',
|
||||||
|
content: '',
|
||||||
|
type: 'SYSTEM' as NotificationType,
|
||||||
|
priority: 'NORMAL',
|
||||||
|
targetType: 'ALL',
|
||||||
|
imageUrl: '',
|
||||||
|
linkUrl: '',
|
||||||
|
publishedAt: '',
|
||||||
|
expiresAt: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 加载通知列表
|
||||||
|
const loadNotifications = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
const data = await notificationService.getNotifications({
|
||||||
|
type: typeFilter || undefined,
|
||||||
|
limit: 100,
|
||||||
|
});
|
||||||
|
setNotifications(data);
|
||||||
|
} catch (err) {
|
||||||
|
setError((err as Error).message || '加载失败');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [typeFilter]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadNotifications();
|
||||||
|
}, [loadNotifications]);
|
||||||
|
|
||||||
|
// 打开创建弹窗
|
||||||
|
const handleCreate = () => {
|
||||||
|
setEditingNotification(null);
|
||||||
|
setFormData({
|
||||||
|
title: '',
|
||||||
|
content: '',
|
||||||
|
type: 'SYSTEM',
|
||||||
|
priority: 'NORMAL',
|
||||||
|
targetType: 'ALL',
|
||||||
|
imageUrl: '',
|
||||||
|
linkUrl: '',
|
||||||
|
publishedAt: '',
|
||||||
|
expiresAt: '',
|
||||||
|
});
|
||||||
|
setShowModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 打开编辑弹窗
|
||||||
|
const handleEdit = (notification: NotificationItem) => {
|
||||||
|
setEditingNotification(notification);
|
||||||
|
setFormData({
|
||||||
|
title: notification.title,
|
||||||
|
content: notification.content,
|
||||||
|
type: notification.type,
|
||||||
|
priority: notification.priority,
|
||||||
|
targetType: notification.targetType,
|
||||||
|
imageUrl: notification.imageUrl || '',
|
||||||
|
linkUrl: notification.linkUrl || '',
|
||||||
|
publishedAt: notification.publishedAt ? notification.publishedAt.slice(0, 16) : '',
|
||||||
|
expiresAt: notification.expiresAt ? notification.expiresAt.slice(0, 16) : '',
|
||||||
|
});
|
||||||
|
setShowModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 关闭弹窗
|
||||||
|
const handleCloseModal = () => {
|
||||||
|
setShowModal(false);
|
||||||
|
setEditingNotification(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 提交表单
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!formData.title.trim()) {
|
||||||
|
toast.error('请输入通知标题');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!formData.content.trim()) {
|
||||||
|
toast.error('请输入通知内容');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
title: formData.title.trim(),
|
||||||
|
content: formData.content.trim(),
|
||||||
|
type: formData.type,
|
||||||
|
priority: formData.priority,
|
||||||
|
targetType: formData.targetType,
|
||||||
|
imageUrl: formData.imageUrl.trim() || undefined,
|
||||||
|
linkUrl: formData.linkUrl.trim() || undefined,
|
||||||
|
publishedAt: formData.publishedAt ? new Date(formData.publishedAt).toISOString() : undefined,
|
||||||
|
expiresAt: formData.expiresAt ? new Date(formData.expiresAt).toISOString() : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (editingNotification) {
|
||||||
|
await notificationService.updateNotification(editingNotification.id, payload);
|
||||||
|
toast.success('通知已更新');
|
||||||
|
} else {
|
||||||
|
await notificationService.createNotification(payload);
|
||||||
|
toast.success('通知已创建');
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCloseModal();
|
||||||
|
loadNotifications();
|
||||||
|
} catch (err) {
|
||||||
|
toast.error((err as Error).message || '操作失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 切换启用状态
|
||||||
|
const handleToggle = async (notification: NotificationItem) => {
|
||||||
|
try {
|
||||||
|
await notificationService.toggleNotification(notification.id, !notification.isEnabled);
|
||||||
|
toast.success(notification.isEnabled ? '已禁用' : '已启用');
|
||||||
|
loadNotifications();
|
||||||
|
} catch (err) {
|
||||||
|
toast.error((err as Error).message || '操作失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除通知
|
||||||
|
const handleDelete = async (id: string) => {
|
||||||
|
try {
|
||||||
|
await notificationService.deleteNotification(id);
|
||||||
|
toast.success('通知已删除');
|
||||||
|
setDeleteConfirm(null);
|
||||||
|
loadNotifications();
|
||||||
|
} catch (err) {
|
||||||
|
toast.error((err as Error).message || '删除失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer title="通知管理">
|
||||||
|
<div className={styles.notifications}>
|
||||||
|
{/* 页面标题和操作按钮 */}
|
||||||
|
<div className={styles.notifications__header}>
|
||||||
|
<h1 className={styles.notifications__title}>通知管理</h1>
|
||||||
|
<div className={styles.notifications__actions}>
|
||||||
|
<Button variant="primary" onClick={handleCreate}>
|
||||||
|
+ 新建通知
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 主内容卡片 */}
|
||||||
|
<div className={styles.notifications__card}>
|
||||||
|
{/* 筛选区域 */}
|
||||||
|
<div className={styles.notifications__filters}>
|
||||||
|
<select
|
||||||
|
className={styles.notifications__select}
|
||||||
|
value={typeFilter}
|
||||||
|
onChange={(e) => setTypeFilter(e.target.value as NotificationType | '')}
|
||||||
|
>
|
||||||
|
<option value="">全部类型</option>
|
||||||
|
{NOTIFICATION_TYPE_OPTIONS.map((opt) => (
|
||||||
|
<option key={opt.value} value={opt.value}>
|
||||||
|
{opt.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<Button variant="outline" size="sm" onClick={loadNotifications}>
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 通知列表 */}
|
||||||
|
<div className={styles.notifications__list}>
|
||||||
|
{loading ? (
|
||||||
|
<div className={styles.notifications__loading}>加载中...</div>
|
||||||
|
) : error ? (
|
||||||
|
<div className={styles.notifications__error}>
|
||||||
|
<span>{error}</span>
|
||||||
|
<Button variant="outline" size="sm" onClick={loadNotifications}>
|
||||||
|
重试
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : notifications.length === 0 ? (
|
||||||
|
<div className={styles.notifications__empty}>
|
||||||
|
暂无通知,点击"新建通知"创建第一条通知
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
notifications.map((notification) => (
|
||||||
|
<div
|
||||||
|
key={notification.id}
|
||||||
|
className={cn(
|
||||||
|
styles.notifications__item,
|
||||||
|
!notification.isEnabled && styles['notifications__item--disabled']
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className={styles.notifications__itemHeader}>
|
||||||
|
<div className={styles.notifications__itemMeta}>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
styles.notifications__tag,
|
||||||
|
styles[`notifications__tag--${getTypeStyle(notification.type)}`]
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{getTypeLabel(notification.type)}
|
||||||
|
</span>
|
||||||
|
<span className={styles.notifications__priority}>
|
||||||
|
{getPriorityLabel(notification.priority)}
|
||||||
|
</span>
|
||||||
|
<span className={styles.notifications__target}>
|
||||||
|
{getTargetLabel(notification.targetType)}
|
||||||
|
</span>
|
||||||
|
{!notification.isEnabled && (
|
||||||
|
<span className={styles.notifications__disabled}>已禁用</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={styles.notifications__itemActions}>
|
||||||
|
<button
|
||||||
|
className={styles.notifications__actionBtn}
|
||||||
|
onClick={() => handleToggle(notification)}
|
||||||
|
>
|
||||||
|
{notification.isEnabled ? '禁用' : '启用'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={styles.notifications__actionBtn}
|
||||||
|
onClick={() => handleEdit(notification)}
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={cn(styles.notifications__actionBtn, styles['notifications__actionBtn--danger'])}
|
||||||
|
onClick={() => setDeleteConfirm(notification.id)}
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h3 className={styles.notifications__itemTitle}>{notification.title}</h3>
|
||||||
|
<p className={styles.notifications__itemContent}>{notification.content}</p>
|
||||||
|
<div className={styles.notifications__itemFooter}>
|
||||||
|
<span>创建时间: {formatDateTime(notification.createdAt)}</span>
|
||||||
|
{notification.publishedAt && (
|
||||||
|
<span>发布时间: {formatDateTime(notification.publishedAt)}</span>
|
||||||
|
)}
|
||||||
|
{notification.expiresAt && (
|
||||||
|
<span>过期时间: {formatDateTime(notification.expiresAt)}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 创建/编辑弹窗 */}
|
||||||
|
<Modal
|
||||||
|
visible={showModal}
|
||||||
|
title={editingNotification ? '编辑通知' : '新建通知'}
|
||||||
|
onClose={handleCloseModal}
|
||||||
|
footer={
|
||||||
|
<div className={styles.notifications__modalFooter}>
|
||||||
|
<Button variant="outline" onClick={handleCloseModal}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button variant="primary" onClick={handleSubmit}>
|
||||||
|
{editingNotification ? '保存' : '创建'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
width={640}
|
||||||
|
>
|
||||||
|
<div className={styles.notifications__form}>
|
||||||
|
<div className={styles.notifications__formGroup}>
|
||||||
|
<label>通知标题 *</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formData.title}
|
||||||
|
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
|
||||||
|
placeholder="请输入通知标题"
|
||||||
|
maxLength={100}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.notifications__formGroup}>
|
||||||
|
<label>通知内容 *</label>
|
||||||
|
<textarea
|
||||||
|
value={formData.content}
|
||||||
|
onChange={(e) => setFormData({ ...formData, content: e.target.value })}
|
||||||
|
placeholder="请输入通知内容"
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.notifications__formRow}>
|
||||||
|
<div className={styles.notifications__formGroup}>
|
||||||
|
<label>通知类型</label>
|
||||||
|
<select
|
||||||
|
value={formData.type}
|
||||||
|
onChange={(e) => setFormData({ ...formData, type: e.target.value as NotificationType })}
|
||||||
|
>
|
||||||
|
{NOTIFICATION_TYPE_OPTIONS.map((opt) => (
|
||||||
|
<option key={opt.value} value={opt.value}>
|
||||||
|
{opt.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.notifications__formGroup}>
|
||||||
|
<label>优先级</label>
|
||||||
|
<select
|
||||||
|
value={formData.priority}
|
||||||
|
onChange={(e) => setFormData({ ...formData, priority: e.target.value })}
|
||||||
|
>
|
||||||
|
{NOTIFICATION_PRIORITY_OPTIONS.map((opt) => (
|
||||||
|
<option key={opt.value} value={opt.value}>
|
||||||
|
{opt.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.notifications__formGroup}>
|
||||||
|
<label>目标用户</label>
|
||||||
|
<select
|
||||||
|
value={formData.targetType}
|
||||||
|
onChange={(e) => setFormData({ ...formData, targetType: e.target.value })}
|
||||||
|
>
|
||||||
|
{TARGET_TYPE_OPTIONS.map((opt) => (
|
||||||
|
<option key={opt.value} value={opt.value}>
|
||||||
|
{opt.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.notifications__formRow}>
|
||||||
|
<div className={styles.notifications__formGroup}>
|
||||||
|
<label>发布时间</label>
|
||||||
|
<input
|
||||||
|
type="datetime-local"
|
||||||
|
value={formData.publishedAt}
|
||||||
|
onChange={(e) => setFormData({ ...formData, publishedAt: e.target.value })}
|
||||||
|
/>
|
||||||
|
<span className={styles.notifications__formHint}>留空表示立即发布</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.notifications__formGroup}>
|
||||||
|
<label>过期时间</label>
|
||||||
|
<input
|
||||||
|
type="datetime-local"
|
||||||
|
value={formData.expiresAt}
|
||||||
|
onChange={(e) => setFormData({ ...formData, expiresAt: e.target.value })}
|
||||||
|
/>
|
||||||
|
<span className={styles.notifications__formHint}>留空表示永不过期</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.notifications__formGroup}>
|
||||||
|
<label>图片链接 (可选)</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formData.imageUrl}
|
||||||
|
onChange={(e) => setFormData({ ...formData, imageUrl: e.target.value })}
|
||||||
|
placeholder="https://..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.notifications__formGroup}>
|
||||||
|
<label>跳转链接 (可选)</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formData.linkUrl}
|
||||||
|
onChange={(e) => setFormData({ ...formData, linkUrl: e.target.value })}
|
||||||
|
placeholder="https://..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* 删除确认弹窗 */}
|
||||||
|
<Modal
|
||||||
|
visible={!!deleteConfirm}
|
||||||
|
title="确认删除"
|
||||||
|
onClose={() => setDeleteConfirm(null)}
|
||||||
|
footer={
|
||||||
|
<div className={styles.notifications__modalFooter}>
|
||||||
|
<Button variant="outline" onClick={() => setDeleteConfirm(null)}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button variant="danger" onClick={() => deleteConfirm && handleDelete(deleteConfirm)}>
|
||||||
|
确认删除
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
width={400}
|
||||||
|
>
|
||||||
|
<p>确定要删除这条通知吗?此操作无法撤销。</p>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -27,6 +27,7 @@ const topMenuItems: MenuItem[] = [
|
||||||
{ key: 'users', icon: '/images/Container2.svg', label: '用户管理', path: '/users' },
|
{ key: 'users', icon: '/images/Container2.svg', label: '用户管理', path: '/users' },
|
||||||
{ key: 'leaderboard', icon: '/images/Container3.svg', label: '龙虎榜', path: '/leaderboard' },
|
{ key: 'leaderboard', icon: '/images/Container3.svg', label: '龙虎榜', path: '/leaderboard' },
|
||||||
{ key: 'authorization', icon: '/images/Container4.svg', label: '授权管理', path: '/authorization' },
|
{ key: 'authorization', icon: '/images/Container4.svg', label: '授权管理', path: '/authorization' },
|
||||||
|
{ key: 'notifications', icon: '/images/Container3.svg', label: '通知管理', path: '/notifications' },
|
||||||
{ key: 'statistics', icon: '/images/Container5.svg', label: '数据统计', path: '/statistics' },
|
{ key: 'statistics', icon: '/images/Container5.svg', label: '数据统计', path: '/statistics' },
|
||||||
{ key: 'settings', icon: '/images/Container6.svg', label: '系统设置', path: '/settings' },
|
{ key: 'settings', icon: '/images/Container6.svg', label: '系统设置', path: '/settings' },
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -96,4 +96,13 @@ export const API_ENDPOINTS = {
|
||||||
CHARTS: '/v1/dashboard/charts',
|
CHARTS: '/v1/dashboard/charts',
|
||||||
REGION: '/v1/dashboard/region',
|
REGION: '/v1/dashboard/region',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 通知管理 (admin-service)
|
||||||
|
NOTIFICATIONS: {
|
||||||
|
LIST: '/v1/admin/notifications',
|
||||||
|
CREATE: '/v1/admin/notifications',
|
||||||
|
DETAIL: (id: string) => `/v1/admin/notifications/${id}`,
|
||||||
|
UPDATE: (id: string) => `/v1/admin/notifications/${id}`,
|
||||||
|
DELETE: (id: string) => `/v1/admin/notifications/${id}`,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,141 @@
|
||||||
|
/**
|
||||||
|
* 通知管理服务
|
||||||
|
* 负责通知数据的API调用
|
||||||
|
*/
|
||||||
|
|
||||||
|
import apiClient from '@/infrastructure/api/client';
|
||||||
|
import { API_ENDPOINTS } from '@/infrastructure/api/endpoints';
|
||||||
|
|
||||||
|
/** 通知类型 */
|
||||||
|
export type NotificationType = 'SYSTEM' | 'ACTIVITY' | 'REWARD' | 'UPGRADE' | 'ANNOUNCEMENT';
|
||||||
|
|
||||||
|
/** 通知优先级 */
|
||||||
|
export type NotificationPriority = 'LOW' | 'NORMAL' | 'HIGH' | 'URGENT';
|
||||||
|
|
||||||
|
/** 目标用户类型 */
|
||||||
|
export type TargetType = 'ALL' | 'NEW_USER' | 'VIP';
|
||||||
|
|
||||||
|
/** 通知项 */
|
||||||
|
export interface NotificationItem {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
type: NotificationType;
|
||||||
|
priority: NotificationPriority;
|
||||||
|
targetType: TargetType;
|
||||||
|
imageUrl: string | null;
|
||||||
|
linkUrl: string | null;
|
||||||
|
isEnabled: boolean;
|
||||||
|
publishedAt: string | null;
|
||||||
|
expiresAt: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
createdBy: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 创建通知请求 */
|
||||||
|
export interface CreateNotificationRequest {
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
type: NotificationType;
|
||||||
|
priority?: NotificationPriority;
|
||||||
|
targetType?: TargetType;
|
||||||
|
imageUrl?: string;
|
||||||
|
linkUrl?: string;
|
||||||
|
publishedAt?: string;
|
||||||
|
expiresAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 更新通知请求 */
|
||||||
|
export interface UpdateNotificationRequest {
|
||||||
|
title?: string;
|
||||||
|
content?: string;
|
||||||
|
type?: NotificationType;
|
||||||
|
priority?: NotificationPriority;
|
||||||
|
targetType?: TargetType;
|
||||||
|
imageUrl?: string | null;
|
||||||
|
linkUrl?: string | null;
|
||||||
|
isEnabled?: boolean;
|
||||||
|
publishedAt?: string | null;
|
||||||
|
expiresAt?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查询通知列表参数 */
|
||||||
|
export interface ListNotificationsParams {
|
||||||
|
type?: NotificationType;
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 通知类型选项 */
|
||||||
|
export const NOTIFICATION_TYPE_OPTIONS = [
|
||||||
|
{ value: 'SYSTEM', label: '系统通知', color: 'blue' },
|
||||||
|
{ value: 'ACTIVITY', label: '活动通知', color: 'green' },
|
||||||
|
{ value: 'REWARD', label: '收益通知', color: 'yellow' },
|
||||||
|
{ value: 'UPGRADE', label: '升级通知', color: 'purple' },
|
||||||
|
{ value: 'ANNOUNCEMENT', label: '公告', color: 'orange' },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
/** 通知优先级选项 */
|
||||||
|
export const NOTIFICATION_PRIORITY_OPTIONS = [
|
||||||
|
{ value: 'LOW', label: '低', color: 'gray' },
|
||||||
|
{ value: 'NORMAL', label: '普通', color: 'blue' },
|
||||||
|
{ value: 'HIGH', label: '高', color: 'orange' },
|
||||||
|
{ value: 'URGENT', label: '紧急', color: 'red' },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
/** 目标用户类型选项 */
|
||||||
|
export const TARGET_TYPE_OPTIONS = [
|
||||||
|
{ value: 'ALL', label: '全部用户' },
|
||||||
|
{ value: 'NEW_USER', label: '新用户' },
|
||||||
|
{ value: 'VIP', label: 'VIP用户' },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知管理服务
|
||||||
|
*/
|
||||||
|
export const notificationService = {
|
||||||
|
/**
|
||||||
|
* 获取通知列表
|
||||||
|
*/
|
||||||
|
async getNotifications(params: ListNotificationsParams = {}): Promise<NotificationItem[]> {
|
||||||
|
return apiClient.get(API_ENDPOINTS.NOTIFICATIONS.LIST, { params });
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取通知详情
|
||||||
|
*/
|
||||||
|
async getNotification(id: string): Promise<NotificationItem> {
|
||||||
|
return apiClient.get(API_ENDPOINTS.NOTIFICATIONS.DETAIL(id));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建通知
|
||||||
|
*/
|
||||||
|
async createNotification(data: CreateNotificationRequest): Promise<NotificationItem> {
|
||||||
|
return apiClient.post(API_ENDPOINTS.NOTIFICATIONS.CREATE, data);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新通知
|
||||||
|
*/
|
||||||
|
async updateNotification(id: string, data: UpdateNotificationRequest): Promise<NotificationItem> {
|
||||||
|
return apiClient.put(API_ENDPOINTS.NOTIFICATIONS.UPDATE(id), data);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除通知
|
||||||
|
*/
|
||||||
|
async deleteNotification(id: string): Promise<void> {
|
||||||
|
return apiClient.delete(API_ENDPOINTS.NOTIFICATIONS.DELETE(id));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换通知启用状态
|
||||||
|
*/
|
||||||
|
async toggleNotification(id: string, isEnabled: boolean): Promise<NotificationItem> {
|
||||||
|
return apiClient.put(API_ENDPOINTS.NOTIFICATIONS.UPDATE(id), { isEnabled });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default notificationService;
|
||||||
Loading…
Reference in New Issue