731d93843d
Wire admin UI and API routes to restored Memind personal memory and skill runtime modules, including candidate review and runtime status display. Co-authored-by: Cursor <cursoragent@cursor.com>
296 lines
11 KiB
TypeScript
296 lines
11 KiB
TypeScript
import { Link } from 'react-router-dom';
|
||
import { useCallback, useEffect, useState } from 'react';
|
||
import { getAdminDashboardSummary, restartAdminService } from '../../api/client';
|
||
import type { AdminServiceRestartAction } from '../../types';
|
||
import type { AdminDashboardSummary } from '../../types';
|
||
import { formatTime, formatYuan } from '../utils/format';
|
||
|
||
const QUICK_LINKS = [
|
||
{ to: '/users', label: '用户管理', desc: '创建账号、启用禁用' },
|
||
{ to: '/billing/recharge', label: '充值', desc: '为用户账户充值' },
|
||
{ to: '/providers', label: '统一模型中心', desc: 'Provider、执行器模型与启动控制' },
|
||
{ to: '/asset-gateway', label: '资产能力', desc: '可插拔素材插件、Provider 与专属 LLM 选择' },
|
||
{ to: '/mindspace', label: 'MindSpace 配置', desc: '公开页上限与空间发布参数' },
|
||
{ to: '/memory-v2', label: 'Memory V2', desc: '长期记忆 backend 与前置路由开关' },
|
||
{ to: '/skill-runtime', label: 'Skill Runtime', desc: 'H5 manifest 关键词路由开关' },
|
||
{ to: '/system-tests', label: '系统测试验证', desc: '选择测试账号执行联调并汇总问题反馈' },
|
||
{ to: '/capabilities', label: '能力权限', desc: '扩展与工具开关' },
|
||
] as const;
|
||
|
||
export function DashboardPage() {
|
||
const [summary, setSummary] = useState<AdminDashboardSummary | null>(null);
|
||
const [loading, setLoading] = useState(true);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [serviceBusy, setServiceBusy] = useState<AdminServiceRestartAction | null>(null);
|
||
const [serviceMessage, setServiceMessage] = useState<string | null>(null);
|
||
const [serviceError, setServiceError] = useState<string | null>(null);
|
||
|
||
const load = useCallback(async () => {
|
||
setLoading(true);
|
||
setError(null);
|
||
try {
|
||
setSummary(await getAdminDashboardSummary());
|
||
} catch (err) {
|
||
setError(err instanceof Error ? err.message : '加载概览失败');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
void load();
|
||
}, [load]);
|
||
|
||
const handleServiceRestart = useCallback(async (action: AdminServiceRestartAction) => {
|
||
setServiceBusy(action);
|
||
setServiceMessage(null);
|
||
setServiceError(null);
|
||
try {
|
||
const result = await restartAdminService(action);
|
||
const suffix = result.logFile ? ` 日志: ${result.logFile}` : '';
|
||
setServiceMessage(`${result.message}${suffix}`);
|
||
if (action === 'local_restart') {
|
||
window.setTimeout(() => window.location.reload(), 6000);
|
||
}
|
||
} catch (err) {
|
||
setServiceError(err instanceof Error ? err.message : `${action} 失败`);
|
||
} finally {
|
||
setServiceBusy(null);
|
||
}
|
||
}, []);
|
||
|
||
const safeSummary: AdminDashboardSummary = summary ?? {
|
||
users: { total: 0, active: 0, lowBalance: 0, totalBalanceCents: 0 },
|
||
usage24h: { count: 0, costCents: 0 },
|
||
lowBalanceUsers: [],
|
||
recentUsage: [],
|
||
recentLedger: [],
|
||
llm: null,
|
||
};
|
||
|
||
return (
|
||
<div className="admin-page">
|
||
<div className="admin-page-head">
|
||
<h2>概览</h2>
|
||
<p className="muted">平台运行摘要与快捷入口</p>
|
||
</div>
|
||
|
||
{error && <p className="banner banner-error">{error}</p>}
|
||
{serviceMessage && <p className="banner banner-info">{serviceMessage}</p>}
|
||
{serviceError && <p className="banner banner-error">{serviceError}</p>}
|
||
|
||
<div className="admin-stat-grid">
|
||
<div className="admin-stat-card">
|
||
<div className="admin-stat-label">用户总数</div>
|
||
<div className="admin-stat-value">{loading ? '—' : safeSummary.users.total}</div>
|
||
<div className="muted">活跃 {loading ? '—' : safeSummary.users.active}</div>
|
||
</div>
|
||
<div className="admin-stat-card">
|
||
<div className="admin-stat-label">余额不足</div>
|
||
<div className="admin-stat-value">{loading ? '—' : safeSummary.users.lowBalance}</div>
|
||
<div className="muted">普通用户余额 ≤ ¥0</div>
|
||
</div>
|
||
<div className="admin-stat-card">
|
||
<div className="admin-stat-label">账户总余额</div>
|
||
<div className="admin-stat-value">
|
||
{loading ? '—' : `¥${formatYuan(safeSummary.users.totalBalanceCents)}`}
|
||
</div>
|
||
</div>
|
||
<div className="admin-stat-card">
|
||
<div className="admin-stat-label">24h 用量</div>
|
||
<div className="admin-stat-value">{loading ? '—' : safeSummary.usage24h.count}</div>
|
||
<div className="muted">
|
||
扣费 ¥{loading ? '—' : formatYuan(safeSummary.usage24h.costCents)}
|
||
</div>
|
||
</div>
|
||
<div className="admin-stat-card">
|
||
<div className="admin-stat-label">统一模型中心</div>
|
||
<div className="admin-stat-value">{loading ? '—' : (safeSummary.llm?.keyCount ?? '—')}</div>
|
||
<div className="muted">
|
||
{loading
|
||
? '—'
|
||
: safeSummary.llm?.selectedKeyName
|
||
? `当前 ${safeSummary.llm.selectedKeyName}`
|
||
: '未配置'}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<section className="admin-card">
|
||
<div className="admin-card-head">
|
||
<div>
|
||
<h2>服务重启</h2>
|
||
<p className="muted">`local_restart` = 本机开发重启;`pro_restart` = 本机生产脚本重启。</p>
|
||
</div>
|
||
</div>
|
||
<div className="admin-service-grid">
|
||
<article className="admin-service-card">
|
||
<h3>本机开发重启</h3>
|
||
<p className="muted">本机构建并重启 `127.0.0.1:8085` 和 `127.0.0.1:5174`。页面会短暂断开。</p>
|
||
<button
|
||
type="button"
|
||
className="send-btn"
|
||
disabled={serviceBusy !== null}
|
||
onClick={() => void handleServiceRestart('local_restart')}
|
||
>
|
||
{serviceBusy === 'local_restart' ? '重启中...' : '执行本机重启'}
|
||
</button>
|
||
</article>
|
||
<article className="admin-service-card">
|
||
<h3>生产脚本重启</h3>
|
||
<p className="muted">本机直接执行 `remote_restart.sh`。部署在生产机时,用它重启当前生产机服务。</p>
|
||
<button
|
||
type="button"
|
||
className="ghost-btn"
|
||
disabled={serviceBusy !== null}
|
||
onClick={() => void handleServiceRestart('pro_restart')}
|
||
>
|
||
{serviceBusy === 'pro_restart' ? '重启中...' : '执行生产重启'}
|
||
</button>
|
||
</article>
|
||
</div>
|
||
</section>
|
||
|
||
{!loading && safeSummary.lowBalanceUsers.length > 0 && (
|
||
<section className="admin-card">
|
||
<div className="admin-card-head">
|
||
<h2>余额告警</h2>
|
||
<Link to="/billing/recharge" className="ghost-btn admin-inline-link">
|
||
去充值
|
||
</Link>
|
||
</div>
|
||
<div className="admin-table-wrap">
|
||
<table className="admin-table">
|
||
<thead>
|
||
<tr>
|
||
<th>用户</th>
|
||
<th>余额</th>
|
||
<th>操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{safeSummary.lowBalanceUsers.map((user) => (
|
||
<tr key={user.id}>
|
||
<td>
|
||
<div>{user.displayName}</div>
|
||
<div className="muted">@{user.username}</div>
|
||
</td>
|
||
<td>¥{formatYuan(user.balanceCents)}</td>
|
||
<td>
|
||
<Link
|
||
to={`/users/${user.id}`}
|
||
className="ghost-btn admin-inline-link"
|
||
>
|
||
用户详情
|
||
</Link>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
)}
|
||
|
||
<section className="admin-card">
|
||
<h2>快捷入口</h2>
|
||
<div className="admin-link-grid">
|
||
{QUICK_LINKS.map((item) => (
|
||
<Link key={item.to} to={item.to} className="admin-link-card">
|
||
<span className="admin-link-card-title">{item.label}</span>
|
||
<span className="muted">{item.desc}</span>
|
||
</Link>
|
||
))}
|
||
</div>
|
||
</section>
|
||
|
||
{!loading && (
|
||
<>
|
||
<section className="admin-card">
|
||
<div className="admin-card-head">
|
||
<h2>最近用量</h2>
|
||
<Link to="/billing/usage" className="ghost-btn admin-inline-link">
|
||
查看全部
|
||
</Link>
|
||
</div>
|
||
<div className="admin-table-wrap">
|
||
<table className="admin-table">
|
||
<thead>
|
||
<tr>
|
||
<th>时间</th>
|
||
<th>用户</th>
|
||
<th>Token</th>
|
||
<th>扣费</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{safeSummary.recentUsage.length === 0 ? (
|
||
<tr>
|
||
<td colSpan={4} className="muted">
|
||
暂无记录
|
||
</td>
|
||
</tr>
|
||
) : (
|
||
safeSummary.recentUsage.map((row) => (
|
||
<tr key={row.id}>
|
||
<td>{formatTime(row.createdAt)}</td>
|
||
<td>@{row.username}</td>
|
||
<td>
|
||
in {row.inputTokens} / out {row.outputTokens}
|
||
</td>
|
||
<td>¥{formatYuan(row.costCents)}</td>
|
||
</tr>
|
||
))
|
||
)}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
|
||
<section className="admin-card">
|
||
<div className="admin-card-head">
|
||
<h2>最近流水</h2>
|
||
<Link to="/billing/ledger" className="ghost-btn admin-inline-link">
|
||
查看全部
|
||
</Link>
|
||
</div>
|
||
<div className="admin-table-wrap">
|
||
<table className="admin-table">
|
||
<thead>
|
||
<tr>
|
||
<th>时间</th>
|
||
<th>用户</th>
|
||
<th>类型</th>
|
||
<th>金额</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{safeSummary.recentLedger.length === 0 ? (
|
||
<tr>
|
||
<td colSpan={4} className="muted">
|
||
暂无记录
|
||
</td>
|
||
</tr>
|
||
) : (
|
||
safeSummary.recentLedger.map((row) => (
|
||
<tr key={row.id}>
|
||
<td>{formatTime(row.createdAt)}</td>
|
||
<td>@{row.username}</td>
|
||
<td>{row.type}</td>
|
||
<td className={row.amountCents < 0 ? 'text-error' : ''}>
|
||
{row.amountCents >= 0 ? '+' : ''}¥
|
||
{formatYuan(Math.abs(row.amountCents))}
|
||
</td>
|
||
</tr>
|
||
))
|
||
)}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
</>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|