From ad7c76e6e6007443b794e13054779161a7c1c9cc Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Fri, 24 Apr 2026 11:50:30 +0800 Subject: [PATCH] fix management dashboard user status --- .../__tests__/ManagementComponents.test.tsx | 134 ++++--- .../management/components/TrendChart.tsx | 126 +++--- .../components/UserGroupManagement.tsx | 370 ++++++++++-------- 3 files changed, 385 insertions(+), 245 deletions(-) diff --git a/src/modules/extensions/builtin/user-center/management/__tests__/ManagementComponents.test.tsx b/src/modules/extensions/builtin/user-center/management/__tests__/ManagementComponents.test.tsx index 2ecb14a..caabfa1 100644 --- a/src/modules/extensions/builtin/user-center/management/__tests__/ManagementComponents.test.tsx +++ b/src/modules/extensions/builtin/user-center/management/__tests__/ManagementComponents.test.tsx @@ -1,80 +1,122 @@ -import React from 'react' -import { fireEvent, render, screen } from '@testing-library/react' -import { describe, expect, it, vi } from 'vitest' +import React from "react"; +import { fireEvent, render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; -import OverviewCards from '../components/OverviewCards' -import TrendChart from '../components/TrendChart' -import PermissionMatrixEditor from '../components/PermissionMatrixEditor' -import UserGroupManagement from '../components/UserGroupManagement' +import OverviewCards from "../components/OverviewCards"; +import TrendChart from "../components/TrendChart"; +import PermissionMatrixEditor from "../components/PermissionMatrixEditor"; +import UserGroupManagement from "../components/UserGroupManagement"; -describe('Management dashboard components', () => { - it('renders loading state for overview cards', () => { - const { container } = render() - expect(container.querySelector('[aria-busy="true"]')).toBeInTheDocument() - }) +describe("Management dashboard components", () => { + it("renders loading state for overview cards", () => { + const { container } = render(); + expect(container.querySelector('[aria-busy="true"]')).toBeInTheDocument(); + }); - it('supports switching trend granularity', () => { + it("supports switching trend granularity", () => { const series = { daily: [ - { period: '2025-03-01', total: 120, active: 80, subscribed: 40 }, - { period: '2025-03-02', total: 140, active: 90, subscribed: 50 }, + { period: "2025-03-01", total: 120, active: 80, subscribed: 40 }, + { period: "2025-03-02", total: 140, active: 90, subscribed: 50 }, ], weekly: [ - { period: '2025-W09', total: 900, active: 600, subscribed: 320 }, + { period: "2025-W09", total: 900, active: 600, subscribed: 320 }, ], - } + }; - render() + render(); - expect(screen.getByText('2025-03-01')).toBeInTheDocument() + expect(screen.queryByText("2025-03-01")).not.toBeVisible(); - const weeklyButton = screen.getByRole('button', { name: '按周' }) - fireEvent.click(weeklyButton) + const detailsButton = screen.getByRole("button", { name: "展开明细" }); + expect(detailsButton).toHaveAttribute("aria-expanded", "false"); + fireEvent.click(detailsButton); - expect(screen.getByText('2025-W09')).toBeInTheDocument() - }) + expect(screen.getByText("2025-03-01")).toBeVisible(); + expect(detailsButton).toHaveAttribute("aria-expanded", "true"); - it('disables permission matrix editing when read only', () => { + const weeklyButton = screen.getByRole("button", { name: "按周" }); + fireEvent.click(weeklyButton); + + expect(screen.getByText("2025-W09")).toBeVisible(); + }); + + it("disables permission matrix editing when read only", () => { const matrix = { registration: { admin: true, operator: false, user: false }, - } + }; render( , - ) + ); - for (const checkbox of screen.getAllByRole('checkbox')) { - expect(checkbox).toBeDisabled() + for (const checkbox of screen.getAllByRole("checkbox")) { + expect(checkbox).toBeDisabled(); } - expect(screen.queryByRole('button', { name: /保存/ })).not.toBeInTheDocument() - }) + expect( + screen.queryByRole("button", { name: /保存/ }), + ).not.toBeInTheDocument(); + }); - it('flags pending role updates in user group management', () => { - const handleRoleChange = vi.fn() + it("flags pending role updates in user group management", () => { + const handleRoleChange = vi.fn(); const users = [ - { id: '1', email: 'admin@example.com', role: 'admin', active: true }, - { id: '2', email: 'operator@example.com', role: 'operator', active: false }, - ] + { + id: "1", + email: "admin@example.com", + username: "admin", + role: "admin", + active: true, + }, + { + id: "2", + email: "operator@example.com", + role: "operator", + active: false, + }, + ]; render( , - ) + ); - const pendingSelect = screen.getAllByRole('combobox')[0] - expect(pendingSelect).toBeDisabled() - expect(screen.getByText('更新中…')).toBeInTheDocument() + const pendingSelect = screen.getAllByRole("combobox")[0]; + expect(pendingSelect).toBeDisabled(); + expect(screen.getByText("更新中…")).toBeInTheDocument(); - const editableSelect = screen.getAllByRole('combobox')[1] - fireEvent.change(editableSelect, { target: { value: 'admin' } }) - expect(handleRoleChange).toHaveBeenCalledWith('2', 'admin') - }) -}) + const editableSelect = screen.getAllByRole("combobox")[1]; + fireEvent.change(editableSelect, { target: { value: "admin" } }); + expect(handleRoleChange).toHaveBeenCalledWith("2", "admin"); + }); + + it("shows usernames and treats missing active flags as enabled", () => { + render( + , + ); + + expect(screen.getByText("用户名")).toBeInTheDocument(); + expect(screen.getByText("defaultActive")).toBeInTheDocument(); + expect(screen.getByText("活跃")).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "暂停" })).toBeInTheDocument(); + expect(screen.queryByText("已暂停")).not.toBeInTheDocument(); + }); +}); diff --git a/src/modules/extensions/builtin/user-center/management/components/TrendChart.tsx b/src/modules/extensions/builtin/user-center/management/components/TrendChart.tsx index 284dcad..3724ba7 100644 --- a/src/modules/extensions/builtin/user-center/management/components/TrendChart.tsx +++ b/src/modules/extensions/builtin/user-center/management/components/TrendChart.tsx @@ -1,54 +1,55 @@ -'use client' +"use client"; -import { useMemo, useState } from 'react' -import Card from '../../components/Card' +import { useMemo, useState } from "react"; +import Card from "../../components/Card"; export type MetricsPoint = { - period: string - total: number - active: number - subscribed: number -} + period: string; + total: number; + active: number; + subscribed: number; +}; export type MetricsSeries = { - daily: MetricsPoint[] - weekly: MetricsPoint[] -} + daily: MetricsPoint[]; + weekly: MetricsPoint[]; +}; type TrendChartProps = { - series?: MetricsSeries - isLoading?: boolean -} + series?: MetricsSeries; + isLoading?: boolean; +}; -type Granularity = 'daily' | 'weekly' +type Granularity = "daily" | "weekly"; function buildSparkline(points: MetricsPoint[]) { if (!points || points.length === 0) { - return '' + return ""; } - const totals = points.map((point) => point.total) - const maxValue = Math.max(...totals, 1) - const lastIndex = totals.length - 1 || 1 + const totals = points.map((point) => point.total); + const maxValue = Math.max(...totals, 1); + const lastIndex = totals.length - 1 || 1; return totals .map((value, index) => { - const x = (index / lastIndex) * 100 - const y = 100 - (value / maxValue) * 100 - return `${index === 0 ? 'M' : 'L'}${x.toFixed(2)},${y.toFixed(2)}` + const x = (index / lastIndex) * 100; + const y = 100 - (value / maxValue) * 100; + return `${index === 0 ? "M" : "L"}${x.toFixed(2)},${y.toFixed(2)}`; }) - .join(' ') + .join(" "); } export function TrendChart({ series, isLoading = false }: TrendChartProps) { - const [granularity, setGranularity] = useState('daily') + const [granularity, setGranularity] = useState("daily"); + const [isDetailsOpen, setIsDetailsOpen] = useState(false); const points = useMemo(() => { if (!series) { - return [] as MetricsPoint[] + return [] as MetricsPoint[]; } - return granularity === 'daily' ? series.daily : series.weekly - }, [granularity, series]) + return granularity === "daily" ? series.daily : series.weekly; + }, [granularity, series]); - const sparklinePath = useMemo(() => buildSparkline(points), [points]) + const sparklinePath = useMemo(() => buildSparkline(points), [points]); return ( @@ -56,13 +57,15 @@ export function TrendChart({ series, isLoading = false }: TrendChartProps) {

趋势

-

按时间观察用户总量与活跃度的变化

+

+ 按时间观察用户总量与活跃度的变化 +

{( [ - { key: 'daily', label: '按日' }, - { key: 'weekly', label: '按周' }, + { key: "daily", label: "按日" }, + { key: "weekly", label: "按周" }, ] as Array<{ key: Granularity; label: string }> ).map((option) => (
-
+
{isLoading ? (
) : sparklinePath ? ( - + ) : ( -
暂无数据
+
+ 暂无数据 +
)}
-
+ + +
- ) + ); } -export default UserGroupManagement +export default UserGroupManagement;