Fix admin dashboard summary nulls
This commit is contained in:
+19
-2
@@ -229,9 +229,26 @@ export function createLocalUserAuth(pool) {
|
||||
|
||||
async function getAdminSummary() {
|
||||
const [[row]] = await pool.execute(
|
||||
`SELECT COUNT(*) AS total, SUM(CASE WHEN balance_cents < 0 THEN 1 ELSE 0 END) AS lowBalance FROM auth_users`,
|
||||
`SELECT
|
||||
COUNT(*) AS total,
|
||||
SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) AS active,
|
||||
SUM(CASE WHEN balance_cents < 0 THEN 1 ELSE 0 END) AS lowBalance,
|
||||
SUM(balance_cents) AS totalBalanceCents
|
||||
FROM auth_users`,
|
||||
);
|
||||
return { users: { total: Number(row.total ?? 0), lowBalance: Number(row.lowBalance ?? 0) }, routes: { total: 0, active: 0 } };
|
||||
return {
|
||||
users: {
|
||||
total: Number(row.total ?? 0),
|
||||
active: Number(row.active ?? 0),
|
||||
lowBalance: Number(row.lowBalance ?? 0),
|
||||
totalBalanceCents: Number(row.totalBalanceCents ?? 0),
|
||||
},
|
||||
usage24h: { count: 0, costCents: 0 },
|
||||
lowBalanceUsers: [],
|
||||
recentUsage: [],
|
||||
recentLedger: [],
|
||||
llm: { keyCount: 0, selectedKeyName: null, globalModel: null },
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -5,10 +5,10 @@ import type { AdminDashboardSummary } from '../../types';
|
||||
import { formatTime, formatYuan } from '../utils/format';
|
||||
|
||||
const QUICK_LINKS = [
|
||||
{ to: '/admin/users', label: '用户管理', desc: '创建账号、启用禁用' },
|
||||
{ to: '/admin/billing/recharge', label: '充值', desc: '为用户账户充值' },
|
||||
{ to: '/admin/providers', label: 'LLM Provider', desc: '模型 Key 与全局配置' },
|
||||
{ to: '/admin/capabilities', label: '能力权限', desc: '扩展与工具开关' },
|
||||
{ to: '/users', label: '用户管理', desc: '创建账号、启用禁用' },
|
||||
{ to: '/billing/recharge', label: '充值', desc: '为用户账户充值' },
|
||||
{ to: '/providers', label: 'LLM Provider', desc: '模型 Key 与全局配置' },
|
||||
{ to: '/capabilities', label: '能力权限', desc: '扩展与工具开关' },
|
||||
] as const;
|
||||
|
||||
export function DashboardPage() {
|
||||
@@ -32,6 +32,15 @@ export function DashboardPage() {
|
||||
void load();
|
||||
}, [load]);
|
||||
|
||||
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">
|
||||
@@ -44,45 +53,45 @@ export function DashboardPage() {
|
||||
<div className="admin-stat-grid">
|
||||
<div className="admin-stat-card">
|
||||
<div className="admin-stat-label">用户总数</div>
|
||||
<div className="admin-stat-value">{loading ? '—' : summary?.users.total}</div>
|
||||
<div className="muted">活跃 {loading ? '—' : summary?.users.active}</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 ? '—' : summary?.users.lowBalance}</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(summary?.users.totalBalanceCents ?? 0)}`}
|
||||
{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 ? '—' : summary?.usage24h.count}</div>
|
||||
<div className="admin-stat-value">{loading ? '—' : safeSummary.usage24h.count}</div>
|
||||
<div className="muted">
|
||||
扣费 ¥{loading ? '—' : formatYuan(summary?.usage24h.costCents ?? 0)}
|
||||
扣费 ¥{loading ? '—' : formatYuan(safeSummary.usage24h.costCents)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="admin-stat-card">
|
||||
<div className="admin-stat-label">LLM Provider</div>
|
||||
<div className="admin-stat-value">{loading ? '—' : (summary?.llm?.keyCount ?? '—')}</div>
|
||||
<div className="admin-stat-value">{loading ? '—' : (safeSummary.llm?.keyCount ?? '—')}</div>
|
||||
<div className="muted">
|
||||
{loading
|
||||
? '—'
|
||||
: summary?.llm?.selectedKeyName
|
||||
? `当前 ${summary.llm.selectedKeyName}`
|
||||
: safeSummary.llm?.selectedKeyName
|
||||
? `当前 ${safeSummary.llm.selectedKeyName}`
|
||||
: '未配置'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!loading && summary && summary.lowBalanceUsers.length > 0 && (
|
||||
{!loading && safeSummary.lowBalanceUsers.length > 0 && (
|
||||
<section className="admin-card">
|
||||
<div className="admin-card-head">
|
||||
<h2>余额告警</h2>
|
||||
<Link to="/admin/billing/recharge" className="ghost-btn admin-inline-link">
|
||||
<Link to="/billing/recharge" className="ghost-btn admin-inline-link">
|
||||
去充值
|
||||
</Link>
|
||||
</div>
|
||||
@@ -96,7 +105,7 @@ export function DashboardPage() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{summary.lowBalanceUsers.map((user) => (
|
||||
{safeSummary.lowBalanceUsers.map((user) => (
|
||||
<tr key={user.id}>
|
||||
<td>
|
||||
<div>{user.displayName}</div>
|
||||
@@ -105,7 +114,7 @@ export function DashboardPage() {
|
||||
<td>¥{formatYuan(user.balanceCents)}</td>
|
||||
<td>
|
||||
<Link
|
||||
to={`/admin/users/${user.id}`}
|
||||
to={`/users/${user.id}`}
|
||||
className="ghost-btn admin-inline-link"
|
||||
>
|
||||
用户详情
|
||||
@@ -131,12 +140,12 @@ export function DashboardPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{!loading && summary && (
|
||||
{!loading && (
|
||||
<>
|
||||
<section className="admin-card">
|
||||
<div className="admin-card-head">
|
||||
<h2>最近用量</h2>
|
||||
<Link to="/admin/billing/usage" className="ghost-btn admin-inline-link">
|
||||
<Link to="/billing/usage" className="ghost-btn admin-inline-link">
|
||||
查看全部
|
||||
</Link>
|
||||
</div>
|
||||
@@ -151,14 +160,14 @@ export function DashboardPage() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{summary.recentUsage.length === 0 ? (
|
||||
{safeSummary.recentUsage.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={4} className="muted">
|
||||
暂无记录
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
summary.recentUsage.map((row) => (
|
||||
safeSummary.recentUsage.map((row) => (
|
||||
<tr key={row.id}>
|
||||
<td>{formatTime(row.createdAt)}</td>
|
||||
<td>@{row.username}</td>
|
||||
@@ -177,7 +186,7 @@ export function DashboardPage() {
|
||||
<section className="admin-card">
|
||||
<div className="admin-card-head">
|
||||
<h2>最近流水</h2>
|
||||
<Link to="/admin/billing/ledger" className="ghost-btn admin-inline-link">
|
||||
<Link to="/billing/ledger" className="ghost-btn admin-inline-link">
|
||||
查看全部
|
||||
</Link>
|
||||
</div>
|
||||
@@ -192,14 +201,14 @@ export function DashboardPage() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{summary.recentLedger.length === 0 ? (
|
||||
{safeSummary.recentLedger.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={4} className="muted">
|
||||
暂无记录
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
summary.recentLedger.map((row) => (
|
||||
safeSummary.recentLedger.map((row) => (
|
||||
<tr key={row.id}>
|
||||
<td>{formatTime(row.createdAt)}</td>
|
||||
<td>@{row.username}</td>
|
||||
|
||||
Reference in New Issue
Block a user