fix management dashboard user status

This commit is contained in:
Haitao Pan 2026-04-24 11:50:30 +08:00
parent 081bedd637
commit ad7c76e6e6
3 changed files with 385 additions and 245 deletions

View File

@ -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(<OverviewCards isLoading />)
expect(container.querySelector('[aria-busy="true"]')).toBeInTheDocument()
})
describe("Management dashboard components", () => {
it("renders loading state for overview cards", () => {
const { container } = render(<OverviewCards isLoading />);
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(<TrendChart series={series} />)
render(<TrendChart series={series} />);
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(
<PermissionMatrixEditor
matrix={matrix}
roles={['admin', 'operator', 'user']}
roles={["admin", "operator", "user"]}
canEdit={false}
/>,
)
);
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(
<UserGroupManagement
users={users}
canEditRoles
pendingUserIds={new Set(['1'])}
pendingUserIds={new Set(["1"])}
onRoleChange={handleRoleChange}
/>,
)
);
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(
<UserGroupManagement
users={[
{
id: "1",
email: "default-active@example.com",
username: "defaultActive",
role: "user",
},
]}
canEditRoles
/>,
);
expect(screen.getByText("用户名")).toBeInTheDocument();
expect(screen.getByText("defaultActive")).toBeInTheDocument();
expect(screen.getByText("活跃")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "暂停" })).toBeInTheDocument();
expect(screen.queryByText("已暂停")).not.toBeInTheDocument();
});
});

View File

@ -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<Granularity>('daily')
const [granularity, setGranularity] = useState<Granularity>("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 (
<Card>
@ -56,13 +57,15 @@ export function TrendChart({ series, isLoading = false }: TrendChartProps) {
<header className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div>
<h2 className="text-lg font-semibold text-gray-900"></h2>
<p className="text-sm text-gray-500"></p>
<p className="text-sm text-gray-500">
</p>
</div>
<div className="inline-flex items-center gap-2 rounded-full border border-gray-200 bg-white/80 p-1 text-xs shadow-sm">
{(
[
{ key: 'daily', label: '按日' },
{ key: 'weekly', label: '按周' },
{ key: "daily", label: "按日" },
{ key: "weekly", label: "按周" },
] as Array<{ key: Granularity; label: string }>
).map((option) => (
<button
@ -70,8 +73,8 @@ export function TrendChart({ series, isLoading = false }: TrendChartProps) {
type="button"
className={`rounded-full px-3 py-1 font-medium transition ${
granularity === option.key
? 'bg-purple-600 text-white shadow'
: 'text-gray-600 hover:bg-purple-50'
? "bg-purple-600 text-white shadow"
: "text-gray-600 hover:bg-purple-50"
}`}
onClick={() => setGranularity(option.key)}
aria-pressed={granularity === option.key}
@ -82,12 +85,20 @@ export function TrendChart({ series, isLoading = false }: TrendChartProps) {
</div>
</header>
<div className="flex flex-col gap-4" aria-busy={isLoading} aria-live="polite">
<div
className="flex flex-col gap-4"
aria-busy={isLoading}
aria-live="polite"
>
<div className="relative h-32 w-full overflow-hidden rounded-xl border border-purple-100 bg-gradient-to-br from-purple-50 via-white to-indigo-50">
{isLoading ? (
<div className="absolute inset-0 animate-pulse bg-gradient-to-r from-transparent via-purple-100/60 to-transparent" />
) : sparklinePath ? (
<svg viewBox="0 0 100 100" preserveAspectRatio="none" className="h-full w-full text-purple-500">
<svg
viewBox="0 0 100 100"
preserveAspectRatio="none"
className="h-full w-full text-purple-500"
>
<path
d={`${sparklinePath}`}
fill="none"
@ -98,11 +109,27 @@ export function TrendChart({ series, isLoading = false }: TrendChartProps) {
/>
</svg>
) : (
<div className="flex h-full items-center justify-center text-sm text-gray-400"></div>
<div className="flex h-full items-center justify-center text-sm text-gray-400">
</div>
)}
</div>
<div className="overflow-x-auto">
<button
type="button"
className="inline-flex w-fit items-center rounded-full border border-gray-200 bg-white px-3 py-1.5 text-xs font-medium text-gray-600 shadow-sm transition hover:border-purple-200 hover:bg-purple-50 hover:text-purple-700"
onClick={() => setIsDetailsOpen((current) => !current)}
aria-expanded={isDetailsOpen}
aria-controls="management-trend-details"
>
{isDetailsOpen ? "收起明细" : "展开明细"}
</button>
<div
id="management-trend-details"
className="overflow-x-auto"
hidden={!isDetailsOpen}
>
<table className="min-w-full table-fixed divide-y divide-gray-200 text-left text-sm">
<thead className="bg-gray-50/80 text-xs uppercase tracking-wide text-gray-500">
<tr>
@ -131,11 +158,22 @@ export function TrendChart({ series, isLoading = false }: TrendChartProps) {
</tr>
))
: points.map((point) => (
<tr key={`${granularity}-${point.period}`} className="transition hover:bg-purple-50/50">
<td className="px-3 py-2 font-medium text-gray-700">{point.period}</td>
<td className="px-3 py-2 text-gray-900">{point.total}</td>
<td className="px-3 py-2 text-gray-900">{point.active}</td>
<td className="px-3 py-2 text-gray-900">{point.subscribed}</td>
<tr
key={`${granularity}-${point.period}`}
className="transition hover:bg-purple-50/50"
>
<td className="px-3 py-2 font-medium text-gray-700">
{point.period}
</td>
<td className="px-3 py-2 text-gray-900">
{point.total}
</td>
<td className="px-3 py-2 text-gray-900">
{point.active}
</td>
<td className="px-3 py-2 text-gray-900">
{point.subscribed}
</td>
</tr>
))}
</tbody>
@ -144,7 +182,7 @@ export function TrendChart({ series, isLoading = false }: TrendChartProps) {
</div>
</div>
</Card>
)
);
}
export default TrendChart
export default TrendChart;

View File

@ -1,55 +1,70 @@
'use client'
"use client";
import { type FormEvent, useMemo, useState } from 'react'
import Card from '../../components/Card'
import { type FormEvent, useMemo, useState } from "react";
import Card from "../../components/Card";
export type ManagedUser = {
id: string
email: string
role?: string
groups?: string[]
active?: boolean
created_at?: string
}
id: string;
email: string;
username?: string;
name?: string;
role?: string;
groups?: string[];
active?: boolean;
created_at?: string;
};
export type CreateManagedUserInput = {
email: string
uuid: string
groups: string[]
}
email: string;
uuid: string;
groups: string[];
};
type UserGroupManagementProps = {
users?: ManagedUser[]
isLoading?: boolean
pendingUserIds?: Set<string>
canEditRoles: boolean
canCreateCustomUser?: boolean
onRoleChange?: (userId: string, role: string) => void
onInvite?: () => void
onImport?: () => void
onPauseUser?: (userId: string) => void
onResumeUser?: (userId: string) => void
onDeleteUser?: (userId: string) => void
onRenewUuid?: (userId: string) => void
onManageBlacklist?: () => void
onCreateCustomUser?: (input: CreateManagedUserInput) => Promise<void> | void
}
users?: ManagedUser[];
isLoading?: boolean;
pendingUserIds?: Set<string>;
canEditRoles: boolean;
canCreateCustomUser?: boolean;
onRoleChange?: (userId: string, role: string) => void;
onInvite?: () => void;
onImport?: () => void;
onPauseUser?: (userId: string) => void;
onResumeUser?: (userId: string) => void;
onDeleteUser?: (userId: string) => void;
onRenewUuid?: (userId: string) => void;
onManageBlacklist?: () => void;
onCreateCustomUser?: (input: CreateManagedUserInput) => Promise<void> | void;
};
const ROLE_OPTIONS = [
{ value: 'admin', label: '管理员' },
{ value: 'operator', label: '运营者' },
{ value: 'user', label: '用户' },
]
{ value: "admin", label: "管理员" },
{ value: "operator", label: "运营者" },
{ value: "user", label: "用户" },
];
function parseGroupList(input: string): string[] {
const values = input
.split(/[\n,]/)
.map((entry) => entry.trim())
.filter((entry) => entry.length > 0)
.filter((entry) => entry.length > 0);
return Array.from(new Set(values))
return Array.from(new Set(values));
}
function getUserDisplayName(user: ManagedUser): string {
const username = user.username?.trim();
if (username) {
return username;
}
const name = user.name?.trim();
if (name) {
return name;
}
return "—";
}
export function UserGroupManagement({
users,
@ -67,53 +82,56 @@ export function UserGroupManagement({
onManageBlacklist,
onCreateCustomUser,
}: UserGroupManagementProps) {
const data = useMemo(() => users ?? [], [users])
const pendingSet = pendingUserIds ?? new Set<string>()
const data = useMemo(() => users ?? [], [users]);
const pendingSet = pendingUserIds ?? new Set<string>();
const [customEmail, setCustomEmail] = useState('')
const [customUuid, setCustomUuid] = useState('')
const [customGroups, setCustomGroups] = useState('')
const [isCreating, setIsCreating] = useState(false)
const [createMessage, setCreateMessage] = useState<string | undefined>()
const [createError, setCreateError] = useState<string | undefined>()
const [customEmail, setCustomEmail] = useState("");
const [customUuid, setCustomUuid] = useState("");
const [customGroups, setCustomGroups] = useState("");
const [isCreating, setIsCreating] = useState(false);
const [createMessage, setCreateMessage] = useState<string | undefined>();
const [createError, setCreateError] = useState<string | undefined>();
const parsedCustomGroups = useMemo(() => parseGroupList(customGroups), [customGroups])
const parsedCustomGroups = useMemo(
() => parseGroupList(customGroups),
[customGroups],
);
const handleCreateCustomUser = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault()
event.preventDefault();
if (!onCreateCustomUser) {
return
return;
}
const email = customEmail.trim()
const uuid = customUuid.trim()
const email = customEmail.trim();
const uuid = customUuid.trim();
if (!email || !uuid) {
setCreateError('请填写邮箱与 UUID')
return
setCreateError("请填写邮箱与 UUID");
return;
}
const groups = parsedCustomGroups
const groups = parsedCustomGroups;
if (groups.length === 0) {
setCreateError('请至少填写一个用户组')
return
setCreateError("请至少填写一个用户组");
return;
}
setIsCreating(true)
setCreateError(undefined)
setCreateMessage(undefined)
setIsCreating(true);
setCreateError(undefined);
setCreateMessage(undefined);
try {
await onCreateCustomUser({ email, uuid, groups })
setCreateMessage('用户创建成功')
setCustomEmail('')
setCustomUuid('')
setCustomGroups('')
await onCreateCustomUser({ email, uuid, groups });
setCreateMessage("用户创建成功");
setCustomEmail("");
setCustomUuid("");
setCustomGroups("");
} catch (error) {
setCreateError(error instanceof Error ? error.message : '创建失败')
setCreateError(error instanceof Error ? error.message : "创建失败");
} finally {
setIsCreating(false)
setIsCreating(false);
}
}
};
return (
<Card>
@ -121,7 +139,9 @@ export function UserGroupManagement({
<header className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div>
<h2 className="text-lg font-semibold text-gray-900"></h2>
<p className="text-sm text-gray-500"></p>
<p className="text-sm text-gray-500">
</p>
</div>
<div className="flex flex-wrap gap-2">
<button
@ -149,9 +169,16 @@ export function UserGroupManagement({
</header>
{canCreateCustomUser ? (
<form onSubmit={handleCreateCustomUser} className="rounded-xl border border-purple-100 bg-purple-50/60 p-4">
<h3 className="text-sm font-semibold text-purple-800">Root UUID </h3>
<p className="mt-1 text-xs text-purple-700"></p>
<form
onSubmit={handleCreateCustomUser}
className="rounded-xl border border-purple-100 bg-purple-50/60 p-4"
>
<h3 className="text-sm font-semibold text-purple-800">
Root UUID
</h3>
<p className="mt-1 text-xs text-purple-700">
</p>
<div className="mt-3 grid grid-cols-1 gap-3 md:grid-cols-2">
<label className="flex flex-col gap-1 text-xs text-gray-600">
@ -190,19 +217,28 @@ export function UserGroupManagement({
disabled={isCreating}
className="inline-flex items-center rounded-full bg-purple-600 px-4 py-2 text-sm font-medium text-white transition hover:bg-purple-700 disabled:cursor-not-allowed disabled:opacity-60"
>
{isCreating ? '创建中…' : '创建用户'}
{isCreating ? "创建中…" : "创建用户"}
</button>
{createMessage ? <span className="text-xs text-green-700">{createMessage}</span> : null}
{createError ? <span className="text-xs text-red-600">{createError}</span> : null}
{createMessage ? (
<span className="text-xs text-green-700">{createMessage}</span>
) : null}
{createError ? (
<span className="text-xs text-red-600">{createError}</span>
) : null}
</div>
</form>
) : null}
<div className="overflow-x-auto" aria-busy={isLoading} aria-live="polite">
<div
className="overflow-x-auto"
aria-busy={isLoading}
aria-live="polite"
>
<table className="min-w-full divide-y divide-gray-200 text-left text-sm">
<thead className="bg-gray-50/80 text-xs uppercase tracking-wide text-gray-500">
<tr>
<th className="px-4 py-2 font-medium"></th>
<th className="px-4 py-2 font-medium"></th>
<th className="px-4 py-2 font-medium"></th>
<th className="px-4 py-2 font-medium"></th>
<th className="px-4 py-2 font-medium"></th>
@ -212,99 +248,123 @@ export function UserGroupManagement({
<tbody className="divide-y divide-gray-100 bg-white/80">
{isLoading
? Array.from({ length: 5 }).map((_, index) => (
<tr key={index} className="animate-pulse">
<td className="px-4 py-3">
<span className="inline-block h-4 w-48 rounded bg-gray-200" />
</td>
<td className="px-4 py-3">
<span className="inline-block h-4 w-24 rounded bg-gray-200" />
</td>
<td className="px-4 py-3">
<span className="inline-block h-4 w-32 rounded bg-gray-200" />
</td>
<td className="px-4 py-3">
<span className="inline-block h-4 w-16 rounded bg-gray-200" />
</td>
<td className="px-4 py-3">
<span className="inline-block h-4 w-24 rounded bg-gray-200" />
</td>
</tr>
))
: data.map((user) => {
const role = user.role ?? 'user'
const isPending = pendingSet.has(user.id)
return (
<tr key={user.id} className="transition hover:bg-purple-50/50">
<td className="px-4 py-3 text-sm font-medium text-gray-800">{user.email}</td>
<tr key={index} className="animate-pulse">
<td className="px-4 py-3">
<select
className="w-40 rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm text-gray-700 focus:border-purple-400 focus:outline-none focus:ring-2 focus:ring-purple-200"
value={role}
disabled={!canEditRoles || isPending}
onChange={(event) => onRoleChange?.(user.id, event.target.value)}
>
{ROLE_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
{isPending ? <p className="mt-1 text-xs text-purple-500"></p> : null}
</td>
<td className="px-4 py-3 text-gray-600">{user.groups?.join('、') || '—'}</td>
<td className="px-4 py-3">
<span
className={`inline-flex items-center rounded-full px-2 py-1 text-xs font-medium ${user.active ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'}`}
>
{user.active ? '活跃' : '已暂停'}
</span>
<span className="inline-block h-4 w-48 rounded bg-gray-200" />
</td>
<td className="px-4 py-3">
<div className="flex gap-2">
{user.active ? (
<button
onClick={() => onPauseUser?.(user.id)}
className="text-xs text-orange-600 hover:text-orange-700"
>
</button>
) : (
<button
onClick={() => onResumeUser?.(user.id)}
className="text-xs text-green-600 hover:text-green-700"
>
</button>
)}
<button
onClick={() => onRenewUuid?.(user.id)}
className="text-xs text-blue-600 hover:text-blue-700"
>
UUID
</button>
<button
onClick={() => {
if (confirm('确定要删除该用户吗?此操作不可逆。')) {
onDeleteUser?.(user.id)
}
}}
className="text-xs text-red-600 hover:text-red-700"
>
</button>
</div>
<span className="inline-block h-4 w-28 rounded bg-gray-200" />
</td>
<td className="px-4 py-3">
<span className="inline-block h-4 w-24 rounded bg-gray-200" />
</td>
<td className="px-4 py-3">
<span className="inline-block h-4 w-32 rounded bg-gray-200" />
</td>
<td className="px-4 py-3">
<span className="inline-block h-4 w-16 rounded bg-gray-200" />
</td>
<td className="px-4 py-3">
<span className="inline-block h-4 w-24 rounded bg-gray-200" />
</td>
</tr>
)
})}
))
: data.map((user) => {
const role = user.role ?? "user";
const isPending = pendingSet.has(user.id);
const isActive = user.active !== false;
return (
<tr
key={user.id}
className="transition hover:bg-purple-50/50"
>
<td className="px-4 py-3 text-sm font-medium text-gray-800">
{user.email}
</td>
<td className="px-4 py-3 text-gray-700">
{getUserDisplayName(user)}
</td>
<td className="px-4 py-3">
<select
className="w-40 rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm text-gray-700 focus:border-purple-400 focus:outline-none focus:ring-2 focus:ring-purple-200"
value={role}
disabled={!canEditRoles || isPending}
onChange={(event) =>
onRoleChange?.(user.id, event.target.value)
}
>
{ROLE_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
{isPending ? (
<p className="mt-1 text-xs text-purple-500">
</p>
) : null}
</td>
<td className="px-4 py-3 text-gray-600">
{user.groups?.join("、") || "—"}
</td>
<td className="px-4 py-3">
<span
className={`inline-flex items-center rounded-full px-2 py-1 text-xs font-medium ${isActive ? "bg-green-100 text-green-700" : "bg-red-100 text-red-700"}`}
>
{isActive ? "活跃" : "已暂停"}
</span>
</td>
<td className="px-4 py-3">
<div className="flex gap-2">
{isActive ? (
<button
onClick={() => onPauseUser?.(user.id)}
className="text-xs text-orange-600 hover:text-orange-700"
>
</button>
) : (
<button
onClick={() => onResumeUser?.(user.id)}
className="text-xs text-green-600 hover:text-green-700"
>
</button>
)}
<button
onClick={() => onRenewUuid?.(user.id)}
className="text-xs text-blue-600 hover:text-blue-700"
>
UUID
</button>
<button
onClick={() => {
if (
confirm("确定要删除该用户吗?此操作不可逆。")
) {
onDeleteUser?.(user.id);
}
}}
className="text-xs text-red-600 hover:text-red-700"
>
</button>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
{!isLoading && data.length === 0 ? (
<div className="py-6 text-center text-sm text-gray-500"></div>
<div className="py-6 text-center text-sm text-gray-500">
</div>
) : null}
</div>
</div>
</Card>
)
);
}
export default UserGroupManagement
export default UserGroupManagement;