diff --git a/apps/admin-web/src/api/adminApiClient.ts b/apps/admin-web/src/api/adminApiClient.ts index 81893939c..5df358f76 100644 --- a/apps/admin-web/src/api/adminApiClient.ts +++ b/apps/admin-web/src/api/adminApiClient.ts @@ -441,6 +441,8 @@ function buildDashboardQuery(query: AdminDashboardQuery) { const params = new URLSearchParams(); appendQueryParam(params, 'granularity', query.granularity); appendQueryParam(params, 'anchor', query.anchor); + appendQueryParam(params, 'startDate', query.startDate); + appendQueryParam(params, 'endDate', query.endDate); const queryString = params.toString(); return queryString ? `?${queryString}` : ''; } diff --git a/apps/admin-web/src/api/adminApiTypes.ts b/apps/admin-web/src/api/adminApiTypes.ts index 7f550de16..7e4617954 100644 --- a/apps/admin-web/src/api/adminApiTypes.ts +++ b/apps/admin-web/src/api/adminApiTypes.ts @@ -54,11 +54,13 @@ export interface AdminOverviewResponse { database: AdminDatabaseOverviewPayload; } -export type AdminDashboardGranularity = 'day' | 'week' | 'month'; +export type AdminDashboardGranularity = 'day' | 'week' | 'month' | 'period'; export interface AdminDashboardQuery { granularity?: AdminDashboardGranularity; anchor?: string; + startDate?: string; + endDate?: string; } export interface AdminDashboardResponse { @@ -82,6 +84,7 @@ export interface AdminDashboardMetricsPayload { generatedAssets: number; consumedMudPoints: number; totalRegisteredUsers: number; + newRegisteredUsers: number; visitUsers: number; totalVisitUsers: number; visitCount: number; diff --git a/apps/admin-web/src/pages/AdminDashboardPage.test.tsx b/apps/admin-web/src/pages/AdminDashboardPage.test.tsx index 64f5412ac..46c47f20e 100644 --- a/apps/admin-web/src/pages/AdminDashboardPage.test.tsx +++ b/apps/admin-web/src/pages/AdminDashboardPage.test.tsx @@ -1,12 +1,12 @@ /* @vitest-environment jsdom */ -import {fireEvent, render, screen, waitFor} from '@testing-library/react'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import {beforeEach, expect, test, vi} from 'vitest'; +import { beforeEach, expect, test, vi } from 'vitest'; -import {getAdminDashboard} from '../api/adminApiClient'; -import type {AdminDashboardResponse} from '../api/adminApiTypes'; -import {AdminDashboardPage} from './AdminDashboardPage'; +import { getAdminDashboard } from '../api/adminApiClient'; +import type { AdminDashboardResponse } from '../api/adminApiTypes'; +import { AdminDashboardPage } from './AdminDashboardPage'; vi.mock('../api/adminApiClient', () => ({ formatAdminApiError: vi.fn((error: unknown) => @@ -28,6 +28,7 @@ const dashboardResponse: AdminDashboardResponse = { generatedAssets: 12, consumedMudPoints: 88, totalRegisteredUsers: 1200, + newRegisteredUsers: 16, visitUsers: 34, totalVisitUsers: 456, visitCount: 98, @@ -40,7 +41,7 @@ const dashboardResponse: AdminDashboardResponse = { title: '生产素材', unit: '个', total: 12, - buckets: [{key: '2026-06-23', label: '2026-06-23', value: 12}], + buckets: [{ key: '2026-06-23', label: '2026-06-23', value: 12 }], }, ], operations: { @@ -53,9 +54,9 @@ const dashboardResponse: AdminDashboardResponse = { }, ], assetKindBreakdown: [ - {key: 'editor_generated_image', label: '画板图片', value: 9}, + { key: 'editor_generated_image', label: '画板图片', value: 9 }, ], - moduleVisitBreakdown: [{key: 'auth', label: '认证', value: 30}], + moduleVisitBreakdown: [{ key: 'auth', label: '认证', value: 30 }], }, warnings: [], generatedAt: '2026-06-23T12:00:00Z', @@ -70,8 +71,9 @@ 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(); @@ -82,38 +84,64 @@ test('Dashboard 默认加载今日指标并支持运营汇总页签', async () = expect(screen.getByText('访问模块分布')).toBeTruthy(); }); -test('Dashboard 选择周时带上 week granularity 和周一 anchor', async () => { +test('Dashboard 选择本周时按当前终止日期填充整周范围', async () => { const user = userEvent.setup(); render(); - await screen.findByText('今日总生产素材数'); - await user.click(screen.getByRole('button', { name: '周' })); - fireEvent.change(screen.getByLabelText('周'), { - target: {value: '2026-W27'}, + await screen.findByText('本日总生产素材数'); + fireEvent.change(screen.getByLabelText('终止日期'), { + target: { value: '2026-07-02' }, }); + await user.click(screen.getByRole('button', { name: '本周' })); await waitFor(() => { expect(getAdminDashboard).toHaveBeenLastCalledWith('admin-token', { granularity: 'week', - anchor: '2026-06-29', + anchor: '2026-07-05', + startDate: '2026-06-29', + endDate: '2026-07-05', }); }); }); -test('Dashboard 选择月份时带上 month granularity 和月初 anchor', async () => { +test('Dashboard 选择本月时按当前终止日期填充整月范围', async () => { const user = userEvent.setup(); render(); - await screen.findByText('今日总生产素材数'); - await user.click(screen.getByRole('button', { name: '月' })); - fireEvent.change(screen.getByLabelText('月份'), { - target: {value: '2026-07'}, + await screen.findByText('本日总生产素材数'); + fireEvent.change(screen.getByLabelText('终止日期'), { + target: { value: '2026-07-12' }, }); + await user.click(screen.getByRole('button', { name: '本月' })); await waitFor(() => { expect(getAdminDashboard).toHaveBeenLastCalledWith('admin-token', { granularity: 'month', - anchor: '2026-07-01', + anchor: '2026-07-31', + startDate: '2026-07-01', + endDate: '2026-07-31', }); }); }); + +test('Dashboard 手动选择起止日期时使用本时段查询', async () => { + render(); + + await screen.findByText('本日总生产素材数'); + fireEvent.change(screen.getByLabelText('起始日期'), { + target: { value: '2026-06-01' }, + }); + fireEvent.change(screen.getByLabelText('终止日期'), { + target: { value: '2026-06-12' }, + }); + + await waitFor(() => { + expect(getAdminDashboard).toHaveBeenLastCalledWith('admin-token', { + granularity: 'period', + anchor: undefined, + startDate: '2026-06-01', + endDate: '2026-06-12', + }); + }); + expect(screen.getByText('本时段新增用户数')).toBeTruthy(); +}); diff --git a/apps/admin-web/src/pages/AdminDashboardPage.tsx b/apps/admin-web/src/pages/AdminDashboardPage.tsx index ce76add07..e3277a236 100644 --- a/apps/admin-web/src/pages/AdminDashboardPage.tsx +++ b/apps/admin-web/src/pages/AdminDashboardPage.tsx @@ -1,14 +1,14 @@ -import {RefreshCw} from 'lucide-react'; -import {useCallback, useEffect, useMemo, useState} from 'react'; +import { RefreshCw } from 'lucide-react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; -import {getAdminDashboard} from '../api/adminApiClient'; +import { getAdminDashboard } from '../api/adminApiClient'; import type { AdminDashboardBreakdownRowPayload, AdminDashboardChartPayload, AdminDashboardGranularity, AdminDashboardResponse, } from '../api/adminApiTypes'; -import {handlePageError} from './pageUtils'; +import { handlePageError } from './pageUtils'; interface AdminDashboardPageProps { token: string; @@ -21,9 +21,10 @@ const granularityOptions: Array<{ id: AdminDashboardGranularity; label: string; }> = [ - {id: 'day', label: '日'}, - {id: 'week', label: '周'}, - {id: 'month', label: '月'}, + { id: 'day', label: '本日' }, + { id: 'week', label: '本周' }, + { id: 'month', label: '本月' }, + { id: 'period', label: '本时段' }, ]; export function AdminDashboardPage({ @@ -35,11 +36,13 @@ export function AdminDashboardPage({ ); const [granularity, setGranularity] = useState('day'); - const [anchor, setAnchor] = useState(() => formatDateInput(new Date())); + const [dateRange, setDateRange] = useState(() => { + const today = formatDateInput(new Date()); + return { startDate: today, endDate: today }; + }); const [activeTab, setActiveTab] = useState('metrics'); const [errorMessage, setErrorMessage] = useState(''); const [isLoading, setIsLoading] = useState(false); - const anchorInput = buildAnchorInput(granularity, anchor); const loadDashboard = useCallback(async () => { setIsLoading(true); @@ -47,7 +50,9 @@ export function AdminDashboardPage({ try { const response = await getAdminDashboard(token, { granularity, - anchor, + anchor: granularity === 'period' ? undefined : dateRange.endDate, + startDate: dateRange.startDate, + endDate: dateRange.endDate, }); setDashboard(response); } catch (error: unknown) { @@ -55,7 +60,13 @@ export function AdminDashboardPage({ } finally { setIsLoading(false); } - }, [anchor, granularity, onUnauthorized, token]); + }, [ + dateRange.endDate, + dateRange.startDate, + granularity, + onUnauthorized, + token, + ]); useEffect(() => { void loadDashboard(); @@ -92,6 +103,12 @@ export function AdminDashboardPage({ value: metrics?.totalRegisteredUsers ?? 0, unit: '人', }, + { + id: 'new-registered-users', + label: `${rangePrefix(granularity)}新增用户数`, + value: metrics?.newRegisteredUsers ?? 0, + unit: '人', + }, { id: 'visit-users', label: `${rangePrefix(granularity)}访问人数`, @@ -134,6 +151,28 @@ export function AdminDashboardPage({

{dashboard?.range.periodLabel ?? '运营指标'}

+
+ + +
setGranularity(option.id)} + onClick={() => handleGranularityChange(option.id)} > {option.label} ))}
-