fix: connect operations pages to real backend
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
BadgeCheck,
|
||||
BellRing,
|
||||
@@ -11,61 +11,76 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Button, Chart, Table, Tag, type TableColumn } from '@/components/ui';
|
||||
import { channelShare, hourlySendTrend } from '@/mock/chartData';
|
||||
import { clientService, type RecentMessage, type TemplateStatus } from '@/mock';
|
||||
import { clientApi, type DashboardResponse } from '@/api/adminApi';
|
||||
import { createLineOption, createPieOption } from '@/theme/chartOptions';
|
||||
|
||||
const statusLabelMap: Record<RecentMessage['status'], string> = {
|
||||
success: '发送完成',
|
||||
warning: '排队中',
|
||||
info: '发送中',
|
||||
danger: '发送失败',
|
||||
type RecentTaskRow = {
|
||||
id: string;
|
||||
taskNo: string;
|
||||
scene: string;
|
||||
count: number;
|
||||
channel: string;
|
||||
createdAt: string;
|
||||
status: string;
|
||||
};
|
||||
|
||||
const templateStatusLabelMap: Record<TemplateStatus, string> = {
|
||||
draft: '草稿',
|
||||
pending: '待审核',
|
||||
approved: '已通过',
|
||||
rejected: '已驳回',
|
||||
};
|
||||
|
||||
const columns: Array<TableColumn<RecentMessage>> = [
|
||||
{ key: 'id', title: '批次编号', render: (record) => record.id },
|
||||
const columns: Array<TableColumn<RecentTaskRow>> = [
|
||||
{ key: 'taskNo', title: '批次编号', render: (record) => record.taskNo },
|
||||
{ key: 'scene', title: '发送场景', render: (record) => record.scene },
|
||||
{ key: 'count', title: '发送量', render: (record) => `${record.count.toLocaleString('zh-CN')} 条` },
|
||||
{ key: 'channel', title: '通道', render: (record) => record.channel },
|
||||
{ key: 'createdAt', title: '创建时间', render: (record) => record.createdAt },
|
||||
{ key: 'status', title: '状态', render: (record) => <Tag tone={record.status}>{statusLabelMap[record.status]}</Tag> },
|
||||
{ key: 'status', title: '状态', render: (record) => <Tag tone={record.status === 'completed' ? 'success' : record.status === 'failed' ? 'danger' : 'info'}>{record.status}</Tag> },
|
||||
];
|
||||
|
||||
export function ClientHome() {
|
||||
const navigate = useNavigate();
|
||||
const overview = clientService.getOverview();
|
||||
const recentMessages = clientService.getRecentMessages();
|
||||
const templates = clientService.getTemplates();
|
||||
const signatures = clientService.getSignatures();
|
||||
const invoices = clientService.getInvoices();
|
||||
const [dashboard, setDashboard] = useState<DashboardResponse | null>(null);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const approvedTemplates = templates.filter((item) => item.status === 'approved').length;
|
||||
const approvedSignatures = signatures.filter((item) => item.status === 'approved').length;
|
||||
const pendingTemplates = templates.filter((item) => item.status === 'pending').length;
|
||||
const pendingSignatures = signatures.filter((item) => item.status === 'pending').length;
|
||||
const latestInvoice = invoices[0];
|
||||
const balanceBaseline = overview.availableBalance + overview.todaySpend - overview.todayRefund;
|
||||
const balancePercent = Math.min(100, Math.round((overview.availableBalance / balanceBaseline) * 100));
|
||||
useEffect(() => {
|
||||
clientApi.getDashboard()
|
||||
.then(setDashboard)
|
||||
.catch((err) => {
|
||||
setError(err instanceof Error ? err.message : '客户端工作台加载失败');
|
||||
setDashboard(null);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const account = dashboard?.accounts[0];
|
||||
const availableBalance = ((account?.balanceCents ?? 0) + (account?.creditCents ?? 0)) / 100;
|
||||
const todaySpend = (dashboard?.today.spendCents ?? 0) / 100;
|
||||
const todayRefund = Math.abs((dashboard?.transactions._sum.amountCents ?? 0) < 0 ? 0 : dashboard?.transactions._sum.amountCents ?? 0) / 100;
|
||||
const balanceBaseline = Math.max(availableBalance + todaySpend - todayRefund, availableBalance, 1);
|
||||
const balancePercent = Math.min(100, Math.round((availableBalance / balanceBaseline) * 100));
|
||||
const recentMessages = useMemo<RecentTaskRow[]>(() => (dashboard?.recentTasks ?? []).map((task) => ({
|
||||
id: String(task.id ?? task.taskNo),
|
||||
taskNo: String(task.taskNo ?? task.id),
|
||||
scene: String(task.category ?? task.content ?? '短信发送'),
|
||||
count: Number(task.phoneTotal ?? task.progressTotal ?? 0),
|
||||
channel: Array.isArray(task.messages) && task.messages[0]?.channel?.name ? String(task.messages[0].channel.name) : '待路由',
|
||||
createdAt: task.createdAt ? new Date(String(task.createdAt)).toLocaleString('zh-CN') : '',
|
||||
status: String(task.status ?? 'unknown'),
|
||||
})), [dashboard]);
|
||||
const latestRecharge = dashboard?.recentRecharges[0];
|
||||
|
||||
const sendTrendOption = useMemo(
|
||||
() => createLineOption({
|
||||
labels: hourlySendTrend.map((item) => item.time),
|
||||
labels: ['今日'],
|
||||
series: [
|
||||
{ name: '提交量', data: hourlySendTrend.map((item) => item.sent) },
|
||||
{ name: '成功量', data: hourlySendTrend.map((item) => item.success) },
|
||||
{ name: '提交量', data: [dashboard?.today.sent ?? 0] },
|
||||
{ name: '成功量', data: [dashboard?.today.delivered ?? 0] },
|
||||
],
|
||||
}),
|
||||
[],
|
||||
[dashboard],
|
||||
);
|
||||
|
||||
const channelShareOption = useMemo(() => createPieOption({ data: channelShare }), []);
|
||||
const channelShareOption = useMemo(() => createPieOption({
|
||||
data: (dashboard?.gatewayConnections ?? []).map((item) => ({
|
||||
name: item.status,
|
||||
value: item._sum.currentConnections ?? item._count._all,
|
||||
})),
|
||||
}), [dashboard]);
|
||||
|
||||
return (
|
||||
<section className="page-stack">
|
||||
@@ -86,20 +101,21 @@ export function ClientHome() {
|
||||
<div className="dashboard-grid">
|
||||
<div className="surface metric-card metric-card--featured">
|
||||
<span>账户剩余余额</span>
|
||||
<strong>¥{overview.availableBalance.toLocaleString('zh-CN', { minimumFractionDigits: 2 })}</strong>
|
||||
<small>今日消费 ¥{overview.todaySpend.toLocaleString('zh-CN', { minimumFractionDigits: 2 })}</small>
|
||||
<strong>¥{availableBalance.toLocaleString('zh-CN', { minimumFractionDigits: 2 })}</strong>
|
||||
<small>今日消费 ¥{todaySpend.toLocaleString('zh-CN', { minimumFractionDigits: 2 })}</small>
|
||||
</div>
|
||||
<div className="surface metric-card">
|
||||
<span>今日发送</span>
|
||||
<strong>{overview.todaySent.toLocaleString('zh-CN')}</strong>
|
||||
<small>成功率 {overview.todaySuccessRate}%</small>
|
||||
<strong>{(dashboard?.today.sent ?? 0).toLocaleString('zh-CN')}</strong>
|
||||
<small>成功率 {dashboard?.today.successRate ?? 0}%</small>
|
||||
</div>
|
||||
<div className="surface metric-card">
|
||||
<span>今日返还金额</span>
|
||||
<strong>¥{overview.todayRefund.toLocaleString('zh-CN', { minimumFractionDigits: 2 })}</strong>
|
||||
<strong>¥{todayRefund.toLocaleString('zh-CN', { minimumFractionDigits: 2 })}</strong>
|
||||
<small>异常回执与退费返还</small>
|
||||
</div>
|
||||
</div>
|
||||
{error ? <div className="surface ui-table__empty">{error}</div> : null}
|
||||
|
||||
<div className="overview-grid">
|
||||
<div className="surface section-stack">
|
||||
@@ -118,12 +134,12 @@ export function ClientHome() {
|
||||
<button className="quick-action" onClick={() => navigate('/client/templates')} type="button">
|
||||
<FileText size={20} />
|
||||
<span>模板管理</span>
|
||||
<small>{approvedTemplates} 个可用模板</small>
|
||||
<small>进入真实模板列表</small>
|
||||
</button>
|
||||
<button className="quick-action" onClick={() => navigate('/client/signatures')} type="button">
|
||||
<PenLine size={20} />
|
||||
<span>签名管理</span>
|
||||
<small>{approvedSignatures} 个可用签名</small>
|
||||
<small>进入真实签名列表</small>
|
||||
</button>
|
||||
<button className="quick-action" onClick={() => navigate('/client/billing')} type="button">
|
||||
<WalletCards size={20} />
|
||||
@@ -139,20 +155,20 @@ export function ClientHome() {
|
||||
<h2>账户状态</h2>
|
||||
<p className="muted">企业认证与资源用量。</p>
|
||||
</div>
|
||||
<Tag tone="success">已认证</Tag>
|
||||
<Tag tone={account?.status === 'active' ? 'success' : 'warning'}>{account?.status ?? '未知'}</Tag>
|
||||
</div>
|
||||
<div className="summary-list">
|
||||
<div>
|
||||
<span>企业主体</span>
|
||||
<strong>上海云舟科技有限公司</strong>
|
||||
<strong>{account?.tenant?.name ?? account?.tenantId ?? '当前租户'}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>默认签名</span>
|
||||
<strong>【云舟科技】</strong>
|
||||
<strong>由发送资源 API 管理</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>最近充值</span>
|
||||
<strong>{latestInvoice.title}</strong>
|
||||
<strong>{latestRecharge ? `¥${(latestRecharge.amountCents / 100).toFixed(2)}` : '暂无充值'}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
@@ -172,16 +188,16 @@ export function ClientHome() {
|
||||
<BadgeCheck size={22} />
|
||||
<div>
|
||||
<span>模板状态</span>
|
||||
<strong>{approvedTemplates} 已通过</strong>
|
||||
<small>{templates.map((item) => templateStatusLabelMap[item.status]).join(' / ')}</small>
|
||||
<strong>{dashboard?.pendingAuditCount ?? 0} 待处理</strong>
|
||||
<small>点击进入模板明细</small>
|
||||
</div>
|
||||
</div>
|
||||
<div className="surface mini-status-card">
|
||||
<PenLine size={22} />
|
||||
<div>
|
||||
<span>签名状态</span>
|
||||
<strong>{approvedSignatures} 已通过</strong>
|
||||
<small>待审核 {pendingSignatures},需处理 {signatures.filter((item) => item.status === 'rejected').length}</small>
|
||||
<strong>真实 API</strong>
|
||||
<small>点击进入签名明细</small>
|
||||
</div>
|
||||
</div>
|
||||
<div className="surface mini-status-card">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { CalendarDays, Download, FileText, Search } from 'lucide-react';
|
||||
import {
|
||||
Button,
|
||||
@@ -9,20 +9,10 @@ import {
|
||||
Tag,
|
||||
type TableColumn,
|
||||
} from '@/components/ui';
|
||||
import { clientApi, type OperationLogItem } from '@/api/adminApi';
|
||||
|
||||
type LogLevel = 'info' | 'success' | 'warning' | 'error';
|
||||
|
||||
type SystemLog = {
|
||||
id: string;
|
||||
time: string;
|
||||
level: LogLevel;
|
||||
module: string;
|
||||
operator: string;
|
||||
action: string;
|
||||
detail: string;
|
||||
ip: string;
|
||||
};
|
||||
|
||||
const levelLabelMap: Record<LogLevel, string> = {
|
||||
info: '信息',
|
||||
success: '成功',
|
||||
@@ -37,18 +27,6 @@ const levelToneMap: Record<LogLevel, 'info' | 'success' | 'warning' | 'danger'>
|
||||
error: 'danger',
|
||||
};
|
||||
|
||||
const logsSeed: SystemLog[] = [
|
||||
{ id: 'LOG001', time: '2026-03-17 14:35:22', level: 'info', module: '用户管理', operator: '张三', action: '创建用户', detail: '创建用户账号:李四(lisi@example.com)', ip: '192.168.1.100' },
|
||||
{ id: 'LOG002', time: '2026-03-17 14:20:15', level: 'success', module: '短信服务', operator: '李四', action: '发送短信', detail: '批量发送短信至500个号码,发送成功', ip: '192.168.1.101' },
|
||||
{ id: 'LOG003', time: '2026-03-17 13:45:33', level: 'warning', module: '彩信服务', operator: '王五', action: '模板审核', detail: '彩信模板“春节祝福”审核未通过,原因:内容包含敏感词', ip: '192.168.1.102' },
|
||||
{ id: 'LOG004', time: '2026-03-17 12:10:08', level: 'error', module: '系统管理', operator: '赵六', action: '登录失败', detail: '用户登录失败,错误:密码错误(连续3次)', ip: '192.168.1.103' },
|
||||
{ id: 'LOG005', time: '2026-03-17 11:30:45', level: 'info', module: '用户管理', operator: '张三', action: '修改权限', detail: '修改用户“孙七”的角色:普通用户 → 管理员', ip: '192.168.1.100' },
|
||||
{ id: 'LOG006', time: '2026-03-17 10:15:20', level: 'success', module: '短信服务', operator: '李四', action: '签名审核', detail: '短信签名“优品商城”审核通过', ip: '192.168.1.101' },
|
||||
{ id: 'LOG007', time: '2026-03-17 09:50:12', level: 'info', module: '彩信服务', operator: '王五', action: '创建模板', detail: '创建彩信模板“新品发布”(模板ID:MMS_1a2b3c4d)', ip: '192.168.1.102' },
|
||||
{ id: 'LOG008', time: '2026-03-17 09:05:33', level: 'error', module: '短信服务', operator: '李四', action: '发送失败', detail: '短信发送失败,错误:余额不足', ip: '192.168.1.101' },
|
||||
{ id: 'LOG009', time: '2026-03-17 08:40:18', level: 'warning', module: '系统管理', operator: 'system', action: '系统告警', detail: '系统磁盘使用率超过80%,当前使用率:85%', ip: '127.0.0.1' },
|
||||
];
|
||||
|
||||
export function ClientSystemLogsPage() {
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [level, setLevel] = useState('all');
|
||||
@@ -56,30 +34,39 @@ export function ClientSystemLogsPage() {
|
||||
const [range, setRange] = useState('today');
|
||||
const [page, setPage] = useState(1);
|
||||
const pageSize = 5;
|
||||
const [logs, setLogs] = useState<OperationLogItem[]>([]);
|
||||
const [modules, setModules] = useState<string[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
clientApi.listSystemLogs({ keyword, level, module, range, page, pageSize })
|
||||
.then((data) => {
|
||||
setLogs(data.items);
|
||||
setModules(data.modules);
|
||||
setTotal(data.total);
|
||||
setError('');
|
||||
})
|
||||
.catch((err) => {
|
||||
setLogs([]);
|
||||
setTotal(0);
|
||||
setError(err instanceof Error ? err.message : '系统日志加载失败');
|
||||
});
|
||||
}, [keyword, level, module, range, page]);
|
||||
|
||||
const moduleOptions = useMemo(() => {
|
||||
const modules = Array.from(new Set(logsSeed.map((item) => item.module)));
|
||||
return [{ label: '全部模块', value: 'all' }, ...modules.map((item) => ({ label: item, value: item }))];
|
||||
}, []);
|
||||
|
||||
const filteredLogs = logsSeed.filter((item) => {
|
||||
const target = `${item.operator} ${item.action} ${item.detail}`;
|
||||
const matchesKeyword = !keyword || target.toLowerCase().includes(keyword.toLowerCase());
|
||||
const matchesLevel = level === 'all' || item.level === level;
|
||||
const matchesModule = module === 'all' || item.module === module;
|
||||
return matchesKeyword && matchesLevel && matchesModule;
|
||||
});
|
||||
const totalPages = Math.max(1, Math.ceil(filteredLogs.length / pageSize));
|
||||
}, [modules]);
|
||||
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
||||
const currentPage = Math.min(page, totalPages);
|
||||
const pagedLogs = filteredLogs.slice((currentPage - 1) * pageSize, currentPage * pageSize);
|
||||
|
||||
const columns = useMemo<Array<TableColumn<SystemLog>>>(() => [
|
||||
{ key: 'time', title: '时间', width: '190px', render: (record) => <span className="muted">{record.time}</span> },
|
||||
const columns = useMemo<Array<TableColumn<OperationLogItem>>>(() => [
|
||||
{ key: 'time', title: '时间', width: '190px', render: (record) => <span className="muted">{new Date(record.time).toLocaleString('zh-CN')}</span> },
|
||||
{ key: 'level', title: '级别', width: '110px', render: (record) => <Tag tone={levelToneMap[record.level]}>{levelLabelMap[record.level]}</Tag> },
|
||||
{ key: 'module', title: '模块', width: '150px', render: (record) => <strong>{record.module}</strong> },
|
||||
{ key: 'operator', title: '操作人', width: '130px', render: (record) => <strong>{record.operator}</strong> },
|
||||
{ key: 'action', title: '操作', width: '160px', render: (record) => <strong>{record.action}</strong> },
|
||||
{ key: 'detail', title: '详情', render: (record) => <span className="system-log-detail">{record.detail}</span> },
|
||||
{ key: 'detail', title: '详情', render: (record) => <span className="system-log-detail">{JSON.stringify(record.detail)}</span> },
|
||||
{ key: 'ip', title: 'IP地址', width: '150px', render: (record) => <span className="muted">{record.ip}</span> },
|
||||
], []);
|
||||
|
||||
@@ -134,14 +121,14 @@ export function ClientSystemLogsPage() {
|
||||
</div>
|
||||
|
||||
<div className="surface system-table-card">
|
||||
<Table columns={columns} data={pagedLogs} emptyText="暂无系统日志" rowKey="id" />
|
||||
<Table columns={columns} data={logs} emptyText={error || '暂无系统日志'} rowKey="id" />
|
||||
<Pagination
|
||||
nextDisabled={currentPage >= totalPages}
|
||||
onNext={() => setPage((value) => Math.min(totalPages, value + 1))}
|
||||
onPrevious={() => setPage((value) => Math.max(1, value - 1))}
|
||||
page={currentPage}
|
||||
previousDisabled={currentPage <= 1}
|
||||
total={filteredLogs.length}
|
||||
total={total}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user