153 lines
6.9 KiB
TypeScript
153 lines
6.9 KiB
TypeScript
import { redirect } from "next/navigation";
|
|
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Table } from "@/components/ui/table";
|
|
import { getCurrentAuthContext, toPublicAccount } from "@/modules/core/server/auth";
|
|
import { getRoleDefinitions, hasPermission } from "@/modules/core/server/permissions";
|
|
import { readData } from "@/modules/core/server/store";
|
|
import { ROLE_LABELS } from "@/modules/core/types";
|
|
import type { RoleId } from "@/modules/core/types";
|
|
import { TableToolbar } from "@/modules/settings/components/TableToolbar";
|
|
import { UserAccountActions, UserManagementClient } from "@/modules/settings/components/UserManagementClient";
|
|
import { getPage, getPageCount, getSearchParam, paginate, type SearchParams } from "@/modules/settings/lib/pagination";
|
|
import { UserAvatar } from "@/modules/shared/components/UserAvatar";
|
|
|
|
type UsersPageProps = {
|
|
searchParams?: Promise<SearchParams>;
|
|
};
|
|
|
|
function formatRoleLabel(role: string): string {
|
|
if (role in ROLE_LABELS) {
|
|
return ROLE_LABELS[role as RoleId];
|
|
}
|
|
|
|
return role;
|
|
}
|
|
|
|
export default async function UsersPage({ searchParams }: UsersPageProps): Promise<React.ReactElement> {
|
|
const context = await getCurrentAuthContext();
|
|
if (!context) {
|
|
redirect("/login");
|
|
}
|
|
|
|
if (!hasPermission(context.permissions, "account:read")) {
|
|
redirect("/app/dashboard");
|
|
}
|
|
|
|
const params = (await searchParams) ?? {};
|
|
const query = getSearchParam(params, "q");
|
|
const page = getPage(params);
|
|
const data = await readData();
|
|
const roles = await getRoleDefinitions(context.organization?.id);
|
|
const roleById = new Map(roles.map((role) => [role.id, role]));
|
|
const organizationById = new Map(data.organizations.map((organization) => [organization.id, organization]));
|
|
const accounts = data.accounts.map((account) => {
|
|
const membership = context.organization?.id
|
|
? data.memberships.find(
|
|
(item) => item.accountId === account.id && item.organizationId === context.organization?.id && item.status === "active",
|
|
)
|
|
: data.memberships.find((item) => item.accountId === account.id && item.status === "active");
|
|
const organization = membership ? organizationById.get(membership.organizationId) : undefined;
|
|
const platformRole = account.platformRoleId ? roleById.get(account.platformRoleId) : undefined;
|
|
|
|
return toPublicAccount(account, platformRole?.key ?? membership?.roleKey, organization);
|
|
});
|
|
const normalizedQuery = query.trim().toLowerCase();
|
|
const filteredAccounts = accounts.filter((account) =>
|
|
[account.name, account.email, account.status, String(account.role), account.organization ?? ""]
|
|
.join(" ")
|
|
.toLowerCase()
|
|
.includes(normalizedQuery),
|
|
);
|
|
const pageCount = getPageCount(filteredAccounts.length);
|
|
const visibleAccounts = paginate(filteredAccounts, page);
|
|
|
|
return (
|
|
<div className="flex flex-col gap-5">
|
|
<section>
|
|
<Badge variant="success">Accounts</Badge>
|
|
<h2 className="mt-3 text-xl font-semibold">用户管理</h2>
|
|
<p className="mt-1 text-sm text-muted-foreground">账号创建、机构加入审批、角色分配和账号列表。</p>
|
|
</section>
|
|
|
|
<UserManagementClient
|
|
canManageAccounts={hasPermission(context.permissions, "account:manage")}
|
|
organizations={data.organizations}
|
|
roles={roles}
|
|
joinRequests={data.joinRequests}
|
|
/>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>账号列表</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="p-0">
|
|
<TableToolbar
|
|
page={Math.min(page, pageCount)}
|
|
pageCount={pageCount}
|
|
pathname="/app/settings/users"
|
|
query={query}
|
|
searchPlaceholder="搜索姓名、邮箱、角色、状态"
|
|
total={filteredAccounts.length}
|
|
/>
|
|
<div className="overflow-x-auto">
|
|
<Table className="w-full min-w-[920px] text-sm">
|
|
<Table.Header className="bg-secondary text-left text-xs text-muted-foreground">
|
|
<Table.Row>
|
|
<Table.Head className="px-4 py-3 font-medium">账号</Table.Head>
|
|
<Table.Head className="px-4 py-3 font-medium">角色</Table.Head>
|
|
<Table.Head className="px-4 py-3 font-medium">机构</Table.Head>
|
|
<Table.Head className="px-4 py-3 font-medium">状态</Table.Head>
|
|
<Table.Head className="px-4 py-3 text-right font-medium">创建时间</Table.Head>
|
|
<Table.Head className="px-4 py-3 text-right font-medium">操作</Table.Head>
|
|
</Table.Row>
|
|
</Table.Header>
|
|
<Table.Body className="divide-y bg-card">
|
|
{visibleAccounts.map((account) => (
|
|
<Table.Row className="table-row-enter hover:bg-secondary/45" key={account.id}>
|
|
<Table.Cell className="px-4 py-3">
|
|
<div className="flex items-center gap-3">
|
|
<UserAvatar avatarUrl={account.avatarUrl} name={account.name} size="sm" />
|
|
<div className="min-w-0">
|
|
<p className="truncate font-medium">{account.name}</p>
|
|
<p className="truncate text-xs text-muted-foreground">{account.email}</p>
|
|
</div>
|
|
</div>
|
|
</Table.Cell>
|
|
<Table.Cell className="px-4 py-3">{formatRoleLabel(account.role)}</Table.Cell>
|
|
<Table.Cell className="px-4 py-3 text-muted-foreground">{account.organization ?? "-"}</Table.Cell>
|
|
<Table.Cell className="px-4 py-3">
|
|
<Badge variant={account.status === "active" ? "success" : account.status === "pending" ? "warning" : "danger"}>
|
|
{account.status === "active" ? "启用" : account.status === "pending" ? "待审批" : "停用"}
|
|
</Badge>
|
|
</Table.Cell>
|
|
<Table.Cell className="px-4 py-3 text-right text-muted-foreground">
|
|
{new Date(account.createdAt).toLocaleString("zh-CN")}
|
|
</Table.Cell>
|
|
<Table.Cell className="px-4 py-3">
|
|
<UserAccountActions
|
|
account={account}
|
|
currentAccountId={context.account.id}
|
|
organizations={data.organizations}
|
|
roles={roles}
|
|
/>
|
|
</Table.Cell>
|
|
</Table.Row>
|
|
))}
|
|
{visibleAccounts.length === 0 ? (
|
|
<Table.Row>
|
|
<Table.Cell className="px-4 py-8 text-center text-muted-foreground" colSpan={6}>
|
|
暂无匹配账号
|
|
</Table.Cell>
|
|
</Table.Row>
|
|
) : null}
|
|
</Table.Body>
|
|
</Table>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|