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