diff --git a/apps/admin-web/src/pages/AdminDashboardPage.test.tsx b/apps/admin-web/src/pages/AdminDashboardPage.test.tsx index 46c47f20e..781a2862f 100644 --- a/apps/admin-web/src/pages/AdminDashboardPage.test.tsx +++ b/apps/admin-web/src/pages/AdminDashboardPage.test.tsx @@ -71,11 +71,14 @@ test('Dashboard 默认加载今日指标并支持运营汇总页签', async () = const user = userEvent.setup(); render(); - expect(await screen.findByText('本日总生产素材数')).toBeTruthy(); + expect(await screen.findByText('总计数据')).toBeTruthy(); + expect(screen.getByText('时段数据')).toBeTruthy(); + expect(screen.getByText('本日生产素材数')).toBeTruthy(); expect(screen.getByText('总注册用户')).toBeTruthy(); expect(screen.getByText('本日新增用户数')).toBeTruthy(); expect(screen.getByText('当前使用人数(五分钟统计一次)')).toBeTruthy(); expect(screen.getByText('生产素材')).toBeTruthy(); + expect(screen.queryByRole('button', { name: '本时段' })).toBeNull(); await user.click(screen.getByRole('button', { name: '运营汇总' })); @@ -88,7 +91,7 @@ test('Dashboard 选择本周时按当前终止日期填充整周范围', async ( const user = userEvent.setup(); render(); - await screen.findByText('本日总生产素材数'); + await screen.findByText('本日生产素材数'); fireEvent.change(screen.getByLabelText('终止日期'), { target: { value: '2026-07-02' }, }); @@ -96,8 +99,8 @@ test('Dashboard 选择本周时按当前终止日期填充整周范围', async ( await waitFor(() => { expect(getAdminDashboard).toHaveBeenLastCalledWith('admin-token', { - granularity: 'week', - anchor: '2026-07-05', + granularity: 'period', + anchor: undefined, startDate: '2026-06-29', endDate: '2026-07-05', }); @@ -108,7 +111,7 @@ test('Dashboard 选择本月时按当前终止日期填充整月范围', async ( const user = userEvent.setup(); render(); - await screen.findByText('本日总生产素材数'); + await screen.findByText('本日生产素材数'); fireEvent.change(screen.getByLabelText('终止日期'), { target: { value: '2026-07-12' }, }); @@ -116,8 +119,8 @@ test('Dashboard 选择本月时按当前终止日期填充整月范围', async ( await waitFor(() => { expect(getAdminDashboard).toHaveBeenLastCalledWith('admin-token', { - granularity: 'month', - anchor: '2026-07-31', + granularity: 'period', + anchor: undefined, startDate: '2026-07-01', endDate: '2026-07-31', }); @@ -127,7 +130,7 @@ test('Dashboard 选择本月时按当前终止日期填充整月范围', async ( test('Dashboard 手动选择起止日期时使用本时段查询', async () => { render(); - await screen.findByText('本日总生产素材数'); + await screen.findByText('本日生产素材数'); fireEvent.change(screen.getByLabelText('起始日期'), { target: { value: '2026-06-01' }, }); diff --git a/apps/admin-web/src/pages/AdminDashboardPage.tsx b/apps/admin-web/src/pages/AdminDashboardPage.tsx index e3277a236..7c942ea54 100644 --- a/apps/admin-web/src/pages/AdminDashboardPage.tsx +++ b/apps/admin-web/src/pages/AdminDashboardPage.tsx @@ -18,13 +18,12 @@ interface AdminDashboardPageProps { type AdminDashboardTab = 'metrics' | 'operations'; const granularityOptions: Array<{ - id: AdminDashboardGranularity; + id: Exclude; label: string; }> = [ { id: 'day', label: '本日' }, { id: 'week', label: '本周' }, { id: 'month', label: '本月' }, - { id: 'period', label: '本时段' }, ]; export function AdminDashboardPage({ @@ -44,15 +43,15 @@ export function AdminDashboardPage({ const [errorMessage, setErrorMessage] = useState(''); const [isLoading, setIsLoading] = useState(false); - const loadDashboard = useCallback(async () => { + const loadDashboard = useCallback(async (range = dateRange) => { setIsLoading(true); setErrorMessage(''); try { const response = await getAdminDashboard(token, { - granularity, - anchor: granularity === 'period' ? undefined : dateRange.endDate, - startDate: dateRange.startDate, - endDate: dateRange.endDate, + granularity: 'period', + anchor: undefined, + startDate: range.startDate, + endDate: range.endDate, }); setDashboard(response); } catch (error: unknown) { @@ -60,13 +59,7 @@ export function AdminDashboardPage({ } finally { setIsLoading(false); } - }, [ - dateRange.endDate, - dateRange.startDate, - granularity, - onUnauthorized, - token, - ]); + }, [dateRange, onUnauthorized, token]); useEffect(() => { void loadDashboard(); @@ -83,26 +76,49 @@ export function AdminDashboardPage({ }, [loadDashboard]); const metrics = dashboard?.metrics; - const metricCards = useMemo( + const totalMetricCards = useMemo( () => [ - { - id: 'generated-assets', - label: `${rangePrefix(granularity)}总生产素材数`, - value: metrics?.generatedAssets ?? 0, - unit: '个', - }, - { - id: 'consumed-mud-points', - label: `${rangePrefix(granularity)}总消耗泥点数`, - value: metrics?.consumedMudPoints ?? 0, - unit: '泥点', - }, { id: 'total-registered-users', label: '总注册用户', value: metrics?.totalRegisteredUsers ?? 0, unit: '人', }, + { + id: 'total-visit-users', + label: '总访问人数', + value: metrics?.totalVisitUsers ?? 0, + unit: '人', + }, + { + id: 'total-visit-count', + label: '总访问次数', + value: metrics?.totalVisitCount ?? 0, + unit: '次', + }, + { + id: 'current-users', + label: '当前使用人数(五分钟统计一次)', + value: metrics?.currentUsers ?? 0, + unit: '人', + }, + ], + [metrics], + ); + const periodMetricCards = useMemo( + () => [ + { + id: 'generated-assets', + label: `${rangePrefix(granularity)}生产素材数`, + value: metrics?.generatedAssets ?? 0, + unit: '个', + }, + { + id: 'consumed-mud-points', + label: `${rangePrefix(granularity)}消耗泥点数`, + value: metrics?.consumedMudPoints ?? 0, + unit: '泥点', + }, { id: 'new-registered-users', label: `${rangePrefix(granularity)}新增用户数`, @@ -115,30 +131,12 @@ export function AdminDashboardPage({ value: metrics?.visitUsers ?? 0, unit: '人', }, - { - id: 'total-visit-users', - label: '总访问人数', - value: metrics?.totalVisitUsers ?? 0, - unit: '人', - }, { id: 'visit-count', label: `${rangePrefix(granularity)}访问次数`, value: metrics?.visitCount ?? 0, unit: '次', }, - { - id: 'total-visit-count', - label: '总访问次数', - value: metrics?.totalVisitCount ?? 0, - unit: '次', - }, - { - id: 'current-users', - label: '当前使用人数(五分钟统计一次)', - value: metrics?.currentUsers ?? 0, - unit: '人', - }, ], [granularity, metrics], ); @@ -244,16 +242,41 @@ export function AdminDashboardPage({ {activeTab === 'metrics' ? ( <> -
- {metricCards.map((metric) => ( - - ))} -
+
+
+

总计数据

+ 全站累计 +
+
+ {totalMetricCards.map((metric) => ( + + ))} +
+
+ +
+
+

时段数据

+ {dashboard?.range.periodLabel ?? '-'} +
+
+ {periodMetricCards.map((metric) => ( + + ))} +
+
{(dashboard?.charts ?? []).map((chart) => ( @@ -299,13 +322,19 @@ export function AdminDashboardPage({ ); - function handleGranularityChange(nextGranularity: AdminDashboardGranularity) { + function handleGranularityChange( + nextGranularity: Exclude, + ) { + const nextRange = buildPresetDateRange(nextGranularity, dateRange.endDate); + const rangeChanged = + nextRange.startDate !== dateRange.startDate || + nextRange.endDate !== dateRange.endDate; setGranularity(nextGranularity); - setDateRange((current) => - nextGranularity === 'period' - ? normalizeDateRange(current) - : buildPresetDateRange(nextGranularity, current.endDate), - ); + if (rangeChanged) { + setDateRange(nextRange); + } else { + void loadDashboard(nextRange); + } } function handleDateRangeChange( diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index c23510bf6..73d8e5f26 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -3722,7 +3722,7 @@ - 背景:后台需要默认进入运营数据面板,而不是服务 / 数据库状态页;看板要同时支持日 / 周 / 月筛选,并展示生产素材、泥点消耗、注册、访问和当前使用人数。 - 决策:`apps/admin-web` 默认路由改为 `#dashboard`,原 `#overview` 保留为“服务总览”。Dashboard 统一通过 `GET /admin/api/dashboard` 读取 api-server 后端投影,不让前端绕过 BFF 直接访问 SpacetimeDB。后端不新增 SpacetimeDB schema,聚合现有 `editor_project_resource`、`profile_wallet_ledger`、`profile_dashboard_state`、`tracking_daily_stat` 和 `tracking_event`。 -- 2026-06-24 补充:Dashboard 日期控件改为起始日期 / 终止日期;`granularity=period` 使用 `startDate` / `endDate` 自定义闭区间,`day` / `week` / `month` 保留 `anchor` 兼容并由前端自动填充对应范围,页面周期切换展示为“本日 / 本周 / 本月 / 本时段”。 +- 2026-06-24 补充:Dashboard 日期控件改为起始日期 / 终止日期;`granularity=period` 使用 `startDate` / `endDate` 自定义闭区间,`day` / `week` / `month` 保留 `anchor` 兼容;前端展示“本日 / 本周 / 本月”快捷按钮,只修改起止日期并刷新,页面总计数据和时段数据分区展示。 - 指标口径:生产素材数统计 `editor_project_resource.source_type = generated`;消耗泥点数统计 `profile_wallet_ledger.source_type = asset_operation_consume` 的负向流水绝对值;总注册用户和新增用户数均来自 `profile_dashboard_state`,其中新增用户数按 `created_at` 落入当前时间窗统计;访问次数只统计 `tracking_daily_stat.scope_kind = site`;访问人数和当前使用人数按登录用户去重,匿名访问人数需要未来补 visitor id 后才能统计。 - 影响范围:`/admin/api/dashboard`、`shared-contracts` admin DTO、`apps/admin-web` 默认路由和 Dashboard 页面、后台运营文档。 - 验证方式:`cargo test -p api-server --manifest-path server-rs/Cargo.toml admin`、`npm run admin-web:typecheck`、`npx vitest run apps/admin-web/src/pages/AdminDashboardPage.test.tsx apps/admin-web/src/app/adminRoutes.test.ts --reporter verbose`、`npm run check:encoding`、`git diff --check`。 diff --git a/docs/technical/【后台管理】Dashboard运营看板方案-2026-06-23.md b/docs/technical/【后台管理】Dashboard运营看板方案-2026-06-23.md index 79ef1e63d..506e6a04d 100644 --- a/docs/technical/【后台管理】Dashboard运营看板方案-2026-06-23.md +++ b/docs/technical/【后台管理】Dashboard运营看板方案-2026-06-23.md @@ -13,7 +13,7 @@ - `granularity` 默认为 `day`。 - `anchor` 使用北京时间日历日期,保留给 `day` / `week` / `month` 兼容旧查询口径。`week` 和 `month` 选择包含该日期的自然周 / 自然月。 - `period` 使用 `startDate` / `endDate` 作为闭区间自定义时段,最多 366 天。 -- 前端统一展示起始日期和终止日期两个日期选择器;点击“本日 / 本周 / 本月”会按当前终止日期自动填充对应自然日 / 自然周 / 自然月范围,手动修改起止日期或点击“本时段”后按当前日期范围查询。 +- 前端统一展示起始日期和终止日期两个日期选择器;点击“本日 / 本周 / 本月”只按当前终止日期自动填充对应自然日 / 自然周 / 自然月范围并刷新,手动修改起止日期后按当前日期范围查询。 - 前端每 5 分钟自动刷新一次,同时保留手动刷新按钮。 ## 指标口径 @@ -21,7 +21,7 @@ - 生产素材数:`editor_project_resource` 中 `source_type = 'generated'` 的资源,按 `created_at` 映射到北京时间业务日。 - 消耗泥点数:`profile_wallet_ledger` 中 `source_type = asset_operation_consume` 且 `amount_delta < 0` 的流水绝对值,按 `created_at` 映射到北京时间业务日。 - 总注册用户:`profile_dashboard_state` 行数。 -- 新增用户数:`profile_dashboard_state` 中 `created_at` 落在当前筛选时间窗内的账号数,按北京时间业务日归属,支持本日 / 本周 / 本月 / 本时段切换。 +- 新增用户数:`profile_dashboard_state` 中 `created_at` 落在当前筛选时间窗内的账号数,按北京时间业务日归属,支持本日 / 本周 / 本月快捷日期范围。 - 访问次数:`tracking_daily_stat` 中 `scope_kind = site` 的日聚合次数。它表示站点级成功路由 / 站点级事件,不把用户级钱包、任务、生成等业务操作混入访问次数。 - 访问人数:当前数据只具备登录用户维度,按 `tracking_daily_stat` 中 `scope_kind = user` 的 `scope_id` 去重;匿名访问人数需要未来补充稳定 visitor id 后才能统计。 - 当前使用人数:最近 5 分钟内 `tracking_event` 中有 `user_id` 的登录用户去重;不跟随页面选择的历史日 / 周 / 月。