feat: adopt kumo green ui system
This commit is contained in:
@@ -9,6 +9,7 @@ import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select } from "@/components/ui/select";
|
||||
import type { ApiResult } from "@/modules/core/server/api";
|
||||
import type { Organization } from "@/modules/core/types";
|
||||
|
||||
@@ -206,17 +207,14 @@ function AuthPanelContent({ mode }: AuthPanelProps): React.ReactElement {
|
||||
{mode === "register" && !invitationToken && organizations.length > 0 ? (
|
||||
<label className="block space-y-2">
|
||||
<span className="text-sm font-medium">申请加入机构</span>
|
||||
<select
|
||||
className="h-11 w-full rounded-md border bg-background px-3 text-sm outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring"
|
||||
<Select
|
||||
aria-label="申请加入机构"
|
||||
name="organizationId"
|
||||
>
|
||||
<option value="">暂不加入机构</option>
|
||||
{organizations.map((organization) => (
|
||||
<option key={organization.id} value={organization.id}>
|
||||
{organization.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
options={[
|
||||
{ value: "", label: "暂不加入机构" },
|
||||
...organizations.map((organization) => ({ value: organization.id, label: organization.name })),
|
||||
]}
|
||||
/>
|
||||
</label>
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import Link from "next/link";
|
||||
import { Clock3, MailCheck, ShieldCheck } from "lucide-react";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { LinkButton } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { SignOutButton } from "@/modules/auth/components/SignOutButton";
|
||||
import type { PublicAccount } from "@/modules/core/types";
|
||||
@@ -52,9 +51,9 @@ export function PendingAssignment({ account }: PendingAssignmentProps): React.Re
|
||||
<p className="mt-1 text-xs text-muted-foreground">分配角色后重新进入即可使用。</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button asChild className="w-fit" variant="outline">
|
||||
<Link href="/app/dashboard">刷新状态</Link>
|
||||
</Button>
|
||||
<LinkButton className="w-fit" href="/app/dashboard" variant="outline">
|
||||
刷新状态
|
||||
</LinkButton>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
@@ -13,6 +13,7 @@ import type { LucideIcon } from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Table } from "@/components/ui/table";
|
||||
|
||||
type Metric = {
|
||||
label: string;
|
||||
@@ -112,30 +113,30 @@ export function DashboardHome(): React.ReactElement {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="overflow-hidden rounded-md border">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-secondary text-left text-xs text-muted-foreground">
|
||||
<tr>
|
||||
<th className="px-4 py-3 font-medium">任务</th>
|
||||
<th className="px-4 py-3 font-medium">负责</th>
|
||||
<th className="px-4 py-3 font-medium">状态</th>
|
||||
<th className="px-4 py-3 text-right font-medium">截止</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y bg-card">
|
||||
<Table className="w-full 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 text-right font-medium">截止</Table.Head>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body className="divide-y bg-card">
|
||||
{taskRows.map((task) => (
|
||||
<tr key={task.name}>
|
||||
<td className="px-4 py-3 font-medium">{task.name}</td>
|
||||
<td className="px-4 py-3 text-muted-foreground">{task.owner}</td>
|
||||
<td className="px-4 py-3">
|
||||
<Table.Row key={task.name}>
|
||||
<Table.Cell className="px-4 py-3 font-medium">{task.name}</Table.Cell>
|
||||
<Table.Cell className="px-4 py-3 text-muted-foreground">{task.owner}</Table.Cell>
|
||||
<Table.Cell className="px-4 py-3">
|
||||
<Badge variant={task.status === "需复核" ? "warning" : "secondary"}>
|
||||
{task.status}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right text-muted-foreground">{task.due}</td>
|
||||
</tr>
|
||||
</Table.Cell>
|
||||
<Table.Cell className="px-4 py-3 text-right text-muted-foreground">{task.due}</Table.Cell>
|
||||
</Table.Row>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</Table.Body>
|
||||
</Table>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -7,7 +7,9 @@ import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Dialog } from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Input, Textarea } from "@/components/ui/input";
|
||||
import { Select } from "@/components/ui/select";
|
||||
import { Table } from "@/components/ui/table";
|
||||
import type { ApiResult } from "@/modules/core/server/api";
|
||||
import type { Permission } from "@/modules/core/types";
|
||||
import type { CareLevel, Elder, ElderInput, ElderStatus } from "@/modules/elders/types";
|
||||
@@ -66,6 +68,20 @@ function getRiskLabel(elder: Elder): { label: string; variant: "danger" | "warni
|
||||
return { label: "绿色稳定", variant: "success" };
|
||||
}
|
||||
|
||||
const statusOptions = [
|
||||
{ value: ALL_STATUS, label: "全部状态" },
|
||||
...Object.entries(ELDER_STATUS_LABELS).map(([value, label]) => ({ value, label })),
|
||||
];
|
||||
|
||||
const careLevelOptions = [
|
||||
{ value: ALL_CARE_LEVELS, label: "全部照护" },
|
||||
...Object.entries(CARE_LEVEL_LABELS).map(([value, label]) => ({ value, label })),
|
||||
];
|
||||
|
||||
const genderOptions = Object.entries(GENDER_LABELS).map(([value, label]) => ({ value, label }));
|
||||
const formCareLevelOptions = Object.entries(CARE_LEVEL_LABELS).map(([value, label]) => ({ value, label }));
|
||||
const formStatusOptions = Object.entries(ELDER_STATUS_LABELS).map(([value, label]) => ({ value, label }));
|
||||
|
||||
function matchesFilters(
|
||||
elder: Elder,
|
||||
query: string,
|
||||
@@ -291,33 +307,23 @@ export function EldersClient({ initialElders, permissions }: EldersClientProps):
|
||||
value={query}
|
||||
/>
|
||||
</label>
|
||||
<label className="relative block">
|
||||
<div className="relative block">
|
||||
<Filter className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<select
|
||||
className="h-11 w-full rounded-md border bg-background px-9 text-sm outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring"
|
||||
onChange={(event) => updateStatusFilter(event.target.value)}
|
||||
<Select
|
||||
aria-label="按老人状态筛选"
|
||||
className="w-full pl-9"
|
||||
onValueChange={updateStatusFilter}
|
||||
options={statusOptions}
|
||||
value={statusFilter}
|
||||
>
|
||||
<option value={ALL_STATUS}>全部状态</option>
|
||||
{Object.entries(ELDER_STATUS_LABELS).map(([value, label]) => (
|
||||
<option key={value} value={value}>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<select
|
||||
className="h-11 w-full rounded-md border bg-background px-3 text-sm outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring"
|
||||
onChange={(event) => updateCareLevelFilter(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Select
|
||||
aria-label="按照护等级筛选"
|
||||
className="w-full"
|
||||
onValueChange={updateCareLevelFilter}
|
||||
options={careLevelOptions}
|
||||
value={careLevelFilter}
|
||||
>
|
||||
<option value={ALL_CARE_LEVELS}>全部照护</option>
|
||||
{Object.entries(CARE_LEVEL_LABELS).map(([value, label]) => (
|
||||
<option key={value} value={value}>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{message ? (
|
||||
@@ -328,46 +334,46 @@ export function EldersClient({ initialElders, permissions }: EldersClientProps):
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="overflow-x-auto rounded-md border">
|
||||
<table className="w-full min-w-[980px] text-sm">
|
||||
<thead className="bg-secondary text-left text-xs text-muted-foreground">
|
||||
<tr>
|
||||
<th className="px-4 py-3 font-medium">姓名</th>
|
||||
<th className="px-4 py-3 font-medium">照护</th>
|
||||
<th className="px-4 py-3 font-medium">床位</th>
|
||||
<th className="px-4 py-3 font-medium">联系人</th>
|
||||
<th className="px-4 py-3 font-medium">状态</th>
|
||||
<th className="px-4 py-3 font-medium">标记</th>
|
||||
<th className="px-4 py-3 text-right font-medium">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y bg-card">
|
||||
<Table className="w-full min-w-[980px] 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 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.Row>
|
||||
</Table.Header>
|
||||
<Table.Body className="divide-y bg-card">
|
||||
{visibleElders.map((elder) => {
|
||||
const risk = getRiskLabel(elder);
|
||||
|
||||
return (
|
||||
<tr key={elder.id}>
|
||||
<td className="px-4 py-3">
|
||||
<Table.Row key={elder.id}>
|
||||
<Table.Cell className="px-4 py-3">
|
||||
<p className="font-medium">{elder.name}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{GENDER_LABELS[elder.gender]} · {elder.age} 岁
|
||||
</p>
|
||||
</td>
|
||||
<td className="px-4 py-3">{CARE_LEVEL_LABELS[elder.careLevel]}</td>
|
||||
<td className="px-4 py-3 text-muted-foreground">
|
||||
</Table.Cell>
|
||||
<Table.Cell className="px-4 py-3">{CARE_LEVEL_LABELS[elder.careLevel]}</Table.Cell>
|
||||
<Table.Cell className="px-4 py-3 text-muted-foreground">
|
||||
{elder.room}-{elder.bed}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-muted-foreground">
|
||||
</Table.Cell>
|
||||
<Table.Cell className="px-4 py-3 text-muted-foreground">
|
||||
{elder.primaryContact} / {elder.phone}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
</Table.Cell>
|
||||
<Table.Cell className="px-4 py-3">
|
||||
<Badge variant={elder.status === "active" ? "success" : "secondary"}>
|
||||
{ELDER_STATUS_LABELS[elder.status]}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
</Table.Cell>
|
||||
<Table.Cell className="px-4 py-3">
|
||||
<Badge variant={risk.variant}>{risk.label}</Badge>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
</Table.Cell>
|
||||
<Table.Cell className="px-4 py-3">
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
disabled={!canUpdate}
|
||||
@@ -390,19 +396,19 @@ export function EldersClient({ initialElders, permissions }: EldersClientProps):
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
);
|
||||
})}
|
||||
{visibleElders.length === 0 ? (
|
||||
<tr>
|
||||
<td className="px-4 py-8 text-center text-muted-foreground" colSpan={7}>
|
||||
<Table.Row>
|
||||
<Table.Cell className="px-4 py-8 text-center text-muted-foreground" colSpan={7}>
|
||||
暂无匹配老人档案
|
||||
</td>
|
||||
</tr>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
) : null}
|
||||
</tbody>
|
||||
</table>
|
||||
</Table.Body>
|
||||
</Table>
|
||||
</div>
|
||||
<div className="mt-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
@@ -444,20 +450,15 @@ export function EldersClient({ initialElders, permissions }: EldersClientProps):
|
||||
<Input value={form.name} onChange={(event) => updateField("name", event.target.value)} />
|
||||
</label>
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<label className="grid gap-1.5 text-sm font-medium">
|
||||
<div className="grid gap-1.5 text-sm font-medium">
|
||||
性别
|
||||
<select
|
||||
className="h-11 rounded-md border bg-background px-3 text-sm"
|
||||
onChange={(event) => updateField("gender", event.target.value as ElderInput["gender"])}
|
||||
<Select
|
||||
aria-label="性别"
|
||||
onValueChange={(value) => updateField("gender", value as ElderInput["gender"])}
|
||||
options={genderOptions}
|
||||
value={form.gender}
|
||||
>
|
||||
{Object.entries(GENDER_LABELS).map(([value, label]) => (
|
||||
<option key={value} value={value}>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
/>
|
||||
</div>
|
||||
<label className="grid gap-1.5 text-sm font-medium">
|
||||
年龄
|
||||
<Input
|
||||
@@ -470,34 +471,24 @@ export function EldersClient({ initialElders, permissions }: EldersClientProps):
|
||||
</label>
|
||||
</div>
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<label className="grid gap-1.5 text-sm font-medium">
|
||||
<div className="grid gap-1.5 text-sm font-medium">
|
||||
护理等级
|
||||
<select
|
||||
className="h-11 rounded-md border bg-background px-3 text-sm"
|
||||
onChange={(event) => updateField("careLevel", event.target.value as ElderInput["careLevel"])}
|
||||
<Select
|
||||
aria-label="护理等级"
|
||||
onValueChange={(value) => updateField("careLevel", value as ElderInput["careLevel"])}
|
||||
options={formCareLevelOptions}
|
||||
value={form.careLevel}
|
||||
>
|
||||
{Object.entries(CARE_LEVEL_LABELS).map(([value, label]) => (
|
||||
<option key={value} value={value}>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="grid gap-1.5 text-sm font-medium">
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-1.5 text-sm font-medium">
|
||||
状态
|
||||
<select
|
||||
className="h-11 rounded-md border bg-background px-3 text-sm"
|
||||
onChange={(event) => updateField("status", event.target.value as ElderInput["status"])}
|
||||
<Select
|
||||
aria-label="状态"
|
||||
onValueChange={(value) => updateField("status", value as ElderInput["status"])}
|
||||
options={formStatusOptions}
|
||||
value={form.status}
|
||||
>
|
||||
{Object.entries(ELDER_STATUS_LABELS).map(([value, label]) => (
|
||||
<option key={value} value={value}>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<label className="grid gap-1.5 text-sm font-medium">
|
||||
@@ -519,8 +510,8 @@ export function EldersClient({ initialElders, permissions }: EldersClientProps):
|
||||
</label>
|
||||
<label className="grid gap-1.5 text-sm font-medium">
|
||||
健康备注
|
||||
<textarea
|
||||
className="min-h-24 rounded-md border bg-background px-3 py-2 text-sm outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring"
|
||||
<Textarea
|
||||
className="min-h-24"
|
||||
onChange={(event) => updateField("medicalNotes", event.target.value)}
|
||||
value={form.medicalNotes}
|
||||
/>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useRouter } from "next/navigation";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import type { ApiResult } from "@/modules/core/server/api";
|
||||
import type { SystemSettings } from "@/modules/core/types";
|
||||
@@ -69,7 +70,7 @@ export function GlobalSettingsClient({ settings }: GlobalSettingsClientProps): R
|
||||
<span className="block font-medium">开放公开注册</span>
|
||||
<span className="block text-xs text-muted-foreground">关闭后普通注册不可用;邀请链接仍可使用。</span>
|
||||
</span>
|
||||
<input defaultChecked={settings.registrationEnabled} name="registrationEnabled" type="checkbox" />
|
||||
<Checkbox aria-label="开放公开注册" defaultChecked={settings.registrationEnabled} name="registrationEnabled" />
|
||||
</label>
|
||||
|
||||
<label className="flex items-center justify-between gap-4 rounded-md border p-3 text-sm">
|
||||
@@ -77,7 +78,7 @@ export function GlobalSettingsClient({ settings }: GlobalSettingsClientProps): R
|
||||
<span className="block font-medium">启用 OIDC 登录</span>
|
||||
<span className="block text-xs text-muted-foreground">作为平台级总开关,机构级 OIDC 配置仍可单独管理。</span>
|
||||
</span>
|
||||
<input defaultChecked={settings.oidcEnabled} name="oidcEnabled" type="checkbox" />
|
||||
<Checkbox aria-label="启用 OIDC 登录" defaultChecked={settings.oidcEnabled} name="oidcEnabled" />
|
||||
</label>
|
||||
|
||||
<label className="flex items-center justify-between gap-4 rounded-md border p-3 text-sm">
|
||||
@@ -85,7 +86,7 @@ export function GlobalSettingsClient({ settings }: GlobalSettingsClientProps): R
|
||||
<span className="block font-medium">OIDC 自动开户</span>
|
||||
<span className="block text-xs text-muted-foreground">首次 OIDC 登录时按声明信息创建账号,默认进入等待分配。</span>
|
||||
</span>
|
||||
<input defaultChecked={settings.oidcAutoProvision} name="oidcAutoProvision" type="checkbox" />
|
||||
<Checkbox aria-label="OIDC 自动开户" defaultChecked={settings.oidcAutoProvision} name="oidcAutoProvision" />
|
||||
</label>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -7,7 +7,9 @@ import { useRouter } from "next/navigation";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Table } from "@/components/ui/table";
|
||||
import type { ApiResult } from "@/modules/core/server/api";
|
||||
import type { Organization, OrganizationInvitation, RoleDefinition } from "@/modules/core/types";
|
||||
import { getInviteHref, OrganizationInviteDialog } from "@/modules/settings/components/OrganizationInviteDialog";
|
||||
@@ -74,7 +76,11 @@ export function OrganizationDetailClient({
|
||||
<span className="block font-medium">开放公开注册</span>
|
||||
<span className="block text-xs text-muted-foreground">关闭后,注册页不再展示该机构;邀请链接仍可使用。</span>
|
||||
</span>
|
||||
<input defaultChecked={organization.registrationEnabled} name="registrationEnabled" type="checkbox" />
|
||||
<Checkbox
|
||||
aria-label="开放公开注册"
|
||||
defaultChecked={organization.registrationEnabled}
|
||||
name="registrationEnabled"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center justify-between gap-4 rounded-md border p-3 text-sm">
|
||||
@@ -82,7 +88,7 @@ export function OrganizationDetailClient({
|
||||
<span className="block font-medium">启用 OIDC 登录</span>
|
||||
<span className="block text-xs text-muted-foreground">保存标准 OIDC 配置,后续登录入口按该开关展示。</span>
|
||||
</span>
|
||||
<input defaultChecked={organization.oidcEnabled} name="oidcEnabled" type="checkbox" />
|
||||
<Checkbox aria-label="启用 OIDC 登录" defaultChecked={organization.oidcEnabled} name="oidcEnabled" />
|
||||
</label>
|
||||
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
@@ -126,7 +132,11 @@ export function OrganizationDetailClient({
|
||||
<span className="block font-medium">OIDC 自动开户</span>
|
||||
<span className="block text-xs text-muted-foreground">首次 OIDC 登录时自动创建账号并进入本机构。</span>
|
||||
</span>
|
||||
<input defaultChecked={organization.oidcAutoProvision} name="oidcAutoProvision" type="checkbox" />
|
||||
<Checkbox
|
||||
aria-label="OIDC 自动开户"
|
||||
defaultChecked={organization.oidcAutoProvision}
|
||||
name="oidcAutoProvision"
|
||||
/>
|
||||
</label>
|
||||
|
||||
{settingsMessage ? (
|
||||
@@ -156,28 +166,28 @@ export function OrganizationDetailClient({
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-4">
|
||||
<div className="overflow-x-auto rounded-md border">
|
||||
<table className="w-full min-w-[720px] text-sm">
|
||||
<thead className="bg-secondary text-left text-xs text-muted-foreground">
|
||||
<tr>
|
||||
<th className="px-4 py-3 font-medium">对象</th>
|
||||
<th className="px-4 py-3 font-medium">角色</th>
|
||||
<th className="px-4 py-3 font-medium">状态</th>
|
||||
<th className="px-4 py-3 font-medium">有效期</th>
|
||||
<th className="px-4 py-3 text-right font-medium">链接</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y bg-card">
|
||||
<Table className="w-full min-w-[720px] 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.Row>
|
||||
</Table.Header>
|
||||
<Table.Body className="divide-y bg-card">
|
||||
{invitations.map((invitation) => (
|
||||
<tr className="hover:bg-secondary/45" key={invitation.id}>
|
||||
<td className="px-4 py-3">{invitation.email || "通用邀请"}</td>
|
||||
<td className="px-4 py-3">{invitation.roleLabel}</td>
|
||||
<td className="px-4 py-3">
|
||||
<Table.Row className="hover:bg-secondary/45" key={invitation.id}>
|
||||
<Table.Cell className="px-4 py-3">{invitation.email || "通用邀请"}</Table.Cell>
|
||||
<Table.Cell className="px-4 py-3">{invitation.roleLabel}</Table.Cell>
|
||||
<Table.Cell className="px-4 py-3">
|
||||
<Badge variant={invitation.status === "active" ? "success" : "secondary"}>{invitation.status}</Badge>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-muted-foreground">
|
||||
</Table.Cell>
|
||||
<Table.Cell className="px-4 py-3 text-muted-foreground">
|
||||
{new Date(invitation.expiresAt).toLocaleString("zh-CN")}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right">
|
||||
</Table.Cell>
|
||||
<Table.Cell className="px-4 py-3 text-right">
|
||||
<Button
|
||||
onClick={() => void navigator.clipboard.writeText(getInviteHref(invitation.token))}
|
||||
size="sm"
|
||||
@@ -187,18 +197,18 @@ export function OrganizationDetailClient({
|
||||
<Copy className="size-4" aria-hidden="true" />
|
||||
复制
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
))}
|
||||
{invitations.length === 0 ? (
|
||||
<tr>
|
||||
<td className="px-4 py-8 text-center text-muted-foreground" colSpan={5}>
|
||||
<Table.Row>
|
||||
<Table.Cell className="px-4 py-8 text-center text-muted-foreground" colSpan={5}>
|
||||
暂无邀请
|
||||
</td>
|
||||
</tr>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
) : null}
|
||||
</tbody>
|
||||
</table>
|
||||
</Table.Body>
|
||||
</Table>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<LinkIcon className="size-4" aria-hidden="true" />
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useRouter } from "next/navigation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog } from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select } from "@/components/ui/select";
|
||||
import type { ApiResult } from "@/modules/core/server/api";
|
||||
import type { Organization, OrganizationInvitation, RoleDefinition } from "@/modules/core/types";
|
||||
|
||||
@@ -91,20 +92,15 @@ export function OrganizationInviteDialog({
|
||||
</label>
|
||||
<label className="grid gap-2 text-sm">
|
||||
<span className="font-medium">角色</span>
|
||||
<select
|
||||
className="h-11 w-full rounded-md border bg-background px-3 text-sm outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring"
|
||||
<Select
|
||||
aria-label="邀请角色"
|
||||
defaultValue={defaultRoleId}
|
||||
disabled={organizationRoles.length === 0}
|
||||
key={organization.id}
|
||||
name="roleId"
|
||||
options={organizationRoles.map((role) => ({ value: role.id, label: role.label }))}
|
||||
required
|
||||
>
|
||||
{organizationRoles.map((role) => (
|
||||
<option key={role.id} value={role.id}>
|
||||
{role.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
/>
|
||||
</label>
|
||||
{message ? (
|
||||
<p className="rounded-md bg-secondary px-3 py-2 text-sm text-secondary-foreground" role="status">
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useRouter } from "next/navigation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog } from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select } from "@/components/ui/select";
|
||||
import type { ApiResult } from "@/modules/core/server/api";
|
||||
import type { Organization, RoleDefinition } from "@/modules/core/types";
|
||||
import { OrganizationInviteDialog } from "@/modules/settings/components/OrganizationInviteDialog";
|
||||
@@ -204,14 +205,15 @@ export function OrganizationRowActions({
|
||||
</label>
|
||||
<label className="grid gap-2 text-sm">
|
||||
<span className="font-medium">状态</span>
|
||||
<select
|
||||
className="h-11 w-full rounded-md border bg-background px-3 text-sm outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring"
|
||||
<Select
|
||||
aria-label="机构状态"
|
||||
defaultValue={organization.status}
|
||||
name="status"
|
||||
>
|
||||
<option value="active">启用</option>
|
||||
<option value="disabled">停用</option>
|
||||
</select>
|
||||
options={[
|
||||
{ value: "active", label: "启用" },
|
||||
{ value: "disabled", label: "停用" },
|
||||
]}
|
||||
/>
|
||||
</label>
|
||||
{message ? (
|
||||
<p className="rounded-md bg-secondary px-3 py-2 text-sm text-secondary-foreground" role="status">
|
||||
|
||||
@@ -6,8 +6,10 @@ import { useRouter } from "next/navigation";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Dialog } from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select } from "@/components/ui/select";
|
||||
import type { ApiResult } from "@/modules/core/server/api";
|
||||
import type { Permission, RoleDefinition } from "@/modules/core/types";
|
||||
|
||||
@@ -101,11 +103,10 @@ function PermissionTree({
|
||||
<div className="rounded-md border bg-card" key={group.category}>
|
||||
<label className="flex items-center justify-between gap-3 border-b px-3 py-3 text-sm">
|
||||
<span className="flex min-w-0 items-center gap-3">
|
||||
<input
|
||||
<Checkbox
|
||||
aria-label={`切换 ${group.category} 权限`}
|
||||
checked={isAllSelected}
|
||||
className="size-4 rounded border-input"
|
||||
onChange={(event) => toggleCategory(group.permissions, event.target.checked)}
|
||||
type="checkbox"
|
||||
onCheckedChange={(checked) => toggleCategory(group.permissions, checked)}
|
||||
/>
|
||||
<span>
|
||||
<span className="block font-medium">{group.category}</span>
|
||||
@@ -122,11 +123,11 @@ function PermissionTree({
|
||||
className="flex min-h-16 items-start gap-3 rounded-md border bg-background p-3 text-sm transition-colors hover:bg-secondary/50"
|
||||
key={permission.id}
|
||||
>
|
||||
<input
|
||||
<Checkbox
|
||||
aria-label={`切换 ${permission.label}`}
|
||||
checked={selectedSet.has(permission.id)}
|
||||
className="mt-1 size-4 rounded border-input"
|
||||
onChange={(event) => togglePermission(permission.id, event.target.checked)}
|
||||
type="checkbox"
|
||||
className="mt-1"
|
||||
onCheckedChange={(checked) => togglePermission(permission.id, checked)}
|
||||
/>
|
||||
<span className="min-w-0">
|
||||
<span className="block font-medium">{permission.label}</span>
|
||||
@@ -366,14 +367,15 @@ export function RoleRowActions({ permissions, role }: RoleRowActionsProps): Reac
|
||||
</label>
|
||||
<label className="grid gap-2 text-sm">
|
||||
<span className="font-medium">状态</span>
|
||||
<select
|
||||
className="h-11 w-full rounded-md border bg-background px-3 text-sm outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring"
|
||||
<Select
|
||||
aria-label="角色状态"
|
||||
defaultValue={String(role.isEnabled)}
|
||||
name="isEnabled"
|
||||
>
|
||||
<option value="true">启用</option>
|
||||
<option value="false">停用</option>
|
||||
</select>
|
||||
options={[
|
||||
{ value: "true", label: "启用" },
|
||||
{ value: "false", label: "停用" },
|
||||
]}
|
||||
/>
|
||||
</label>
|
||||
<PermissionTree
|
||||
onChange={setSelectedPermissions}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Activity, Building2, Server, ShieldCheck, Users } from "lucide-react";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Table } from "@/components/ui/table";
|
||||
import type {
|
||||
AuditLog,
|
||||
JoinRequest,
|
||||
@@ -191,39 +192,39 @@ export function SettingsOverview({
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="overflow-x-auto rounded-md border">
|
||||
<table className="w-full min-w-[560px] text-sm">
|
||||
<thead className="bg-secondary text-left text-xs text-muted-foreground">
|
||||
<tr>
|
||||
<th className="px-4 py-3 font-medium">机构</th>
|
||||
<th className="px-4 py-3 font-medium">Slug</th>
|
||||
<th className="px-4 py-3 font-medium">状态</th>
|
||||
<th className="px-4 py-3 text-right font-medium">创建时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y bg-card">
|
||||
<Table className="w-full min-w-[560px] 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">Slug</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.Row>
|
||||
</Table.Header>
|
||||
<Table.Body className="divide-y bg-card">
|
||||
{organizations.map((organization) => (
|
||||
<tr key={organization.id}>
|
||||
<td className="px-4 py-3 font-medium">{organization.name}</td>
|
||||
<td className="px-4 py-3 text-muted-foreground">{organization.slug}</td>
|
||||
<td className="px-4 py-3">
|
||||
<Table.Row key={organization.id}>
|
||||
<Table.Cell className="px-4 py-3 font-medium">{organization.name}</Table.Cell>
|
||||
<Table.Cell className="px-4 py-3 text-muted-foreground">{organization.slug}</Table.Cell>
|
||||
<Table.Cell className="px-4 py-3">
|
||||
<Badge variant={organization.status === "active" ? "success" : "danger"}>
|
||||
{organization.status === "active" ? "启用" : "停用"}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right text-muted-foreground">
|
||||
</Table.Cell>
|
||||
<Table.Cell className="px-4 py-3 text-right text-muted-foreground">
|
||||
{new Date(organization.createdAt).toLocaleString("zh-CN")}
|
||||
</td>
|
||||
</tr>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
))}
|
||||
{organizations.length === 0 ? (
|
||||
<tr>
|
||||
<td className="px-4 py-8 text-center text-muted-foreground" colSpan={4}>
|
||||
<Table.Row>
|
||||
<Table.Cell className="px-4 py-8 text-center text-muted-foreground" colSpan={4}>
|
||||
暂无机构
|
||||
</td>
|
||||
</tr>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
) : null}
|
||||
</tbody>
|
||||
</table>
|
||||
</Table.Body>
|
||||
</Table>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -285,35 +286,35 @@ export function SettingsOverview({
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="overflow-x-auto rounded-md border">
|
||||
<table className="w-full min-w-[620px] text-sm">
|
||||
<thead className="bg-secondary text-left text-xs text-muted-foreground">
|
||||
<tr>
|
||||
<th className="px-4 py-3 font-medium">账号</th>
|
||||
<th className="px-4 py-3 font-medium">角色</th>
|
||||
<th className="px-4 py-3 font-medium">状态</th>
|
||||
<th className="px-4 py-3 text-right font-medium">创建时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y bg-card">
|
||||
<Table className="w-full min-w-[620px] 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 text-right font-medium">创建时间</Table.Head>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body className="divide-y bg-card">
|
||||
{accounts.map((account) => (
|
||||
<tr key={account.id}>
|
||||
<td className="px-4 py-3">
|
||||
<Table.Row key={account.id}>
|
||||
<Table.Cell className="px-4 py-3">
|
||||
<p className="font-medium">{account.name}</p>
|
||||
<p className="text-xs text-muted-foreground">{account.email}</p>
|
||||
</td>
|
||||
<td className="px-4 py-3">{formatRoleLabel(account.role)}</td>
|
||||
<td className="px-4 py-3">
|
||||
</Table.Cell>
|
||||
<Table.Cell className="px-4 py-3">{formatRoleLabel(account.role)}</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>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right text-muted-foreground">
|
||||
</Table.Cell>
|
||||
<Table.Cell className="px-4 py-3 text-right text-muted-foreground">
|
||||
{new Date(account.createdAt).toLocaleString("zh-CN")}
|
||||
</td>
|
||||
</tr>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</Table.Body>
|
||||
</Table>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -325,41 +326,41 @@ export function SettingsOverview({
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="overflow-x-auto rounded-md border">
|
||||
<table className="w-full min-w-[720px] text-sm">
|
||||
<thead className="bg-secondary text-left text-xs text-muted-foreground">
|
||||
<tr>
|
||||
<th className="px-4 py-3 font-medium">账号 ID</th>
|
||||
<th className="px-4 py-3 font-medium">机构 ID</th>
|
||||
<th className="px-4 py-3 font-medium">角色</th>
|
||||
<th className="px-4 py-3 font-medium">状态</th>
|
||||
<th className="px-4 py-3 text-right font-medium">创建时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y bg-card">
|
||||
<Table className="w-full min-w-[720px] 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">账号 ID</Table.Head>
|
||||
<Table.Head className="px-4 py-3 font-medium">机构 ID</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.Row>
|
||||
</Table.Header>
|
||||
<Table.Body className="divide-y bg-card">
|
||||
{memberships.map((membership) => (
|
||||
<tr key={membership.id}>
|
||||
<td className="px-4 py-3 text-muted-foreground">{membership.accountId.slice(0, 8)}</td>
|
||||
<td className="px-4 py-3 text-muted-foreground">{membership.organizationId.slice(0, 8)}</td>
|
||||
<td className="px-4 py-3">{membership.roleLabel}</td>
|
||||
<td className="px-4 py-3">
|
||||
<Table.Row key={membership.id}>
|
||||
<Table.Cell className="px-4 py-3 text-muted-foreground">{membership.accountId.slice(0, 8)}</Table.Cell>
|
||||
<Table.Cell className="px-4 py-3 text-muted-foreground">{membership.organizationId.slice(0, 8)}</Table.Cell>
|
||||
<Table.Cell className="px-4 py-3">{membership.roleLabel}</Table.Cell>
|
||||
<Table.Cell className="px-4 py-3">
|
||||
<Badge variant={membership.status === "active" ? "success" : membership.status === "pending" ? "warning" : "danger"}>
|
||||
{membership.status}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right text-muted-foreground">
|
||||
</Table.Cell>
|
||||
<Table.Cell className="px-4 py-3 text-right text-muted-foreground">
|
||||
{new Date(membership.createdAt).toLocaleString("zh-CN")}
|
||||
</td>
|
||||
</tr>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
))}
|
||||
{memberships.length === 0 ? (
|
||||
<tr>
|
||||
<td className="px-4 py-8 text-center text-muted-foreground" colSpan={5}>
|
||||
<Table.Row>
|
||||
<Table.Cell className="px-4 py-8 text-center text-muted-foreground" colSpan={5}>
|
||||
暂无成员关系
|
||||
</td>
|
||||
</tr>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
) : null}
|
||||
</tbody>
|
||||
</table>
|
||||
</Table.Body>
|
||||
</Table>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -370,30 +371,30 @@ export function SettingsOverview({
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="overflow-x-auto rounded-md border">
|
||||
<table className="w-full min-w-[860px] text-sm">
|
||||
<thead className="bg-secondary text-left text-xs text-muted-foreground">
|
||||
<tr>
|
||||
<th className="px-4 py-3 font-medium">时间</th>
|
||||
<th className="px-4 py-3 font-medium">操作者</th>
|
||||
<th className="px-4 py-3 font-medium">动作</th>
|
||||
<th className="px-4 py-3 font-medium">对象</th>
|
||||
<th className="px-4 py-3 font-medium">结果</th>
|
||||
<th className="px-4 py-3 font-medium">原因</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y bg-card">
|
||||
<Table className="w-full min-w-[860px] 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 font-medium">结果</Table.Head>
|
||||
<Table.Head className="px-4 py-3 font-medium">原因</Table.Head>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body className="divide-y bg-card">
|
||||
{auditLogs.map((log) => (
|
||||
<tr key={log.id}>
|
||||
<td className="px-4 py-3 text-muted-foreground">
|
||||
<Table.Row key={log.id}>
|
||||
<Table.Cell className="px-4 py-3 text-muted-foreground">
|
||||
{new Date(log.timestamp).toLocaleString("zh-CN")}
|
||||
</td>
|
||||
<td className="px-4 py-3">{log.actorEmail ?? "系统"}</td>
|
||||
<td className="px-4 py-3">{log.action}</td>
|
||||
<td className="px-4 py-3 text-muted-foreground">
|
||||
</Table.Cell>
|
||||
<Table.Cell className="px-4 py-3">{log.actorEmail ?? "系统"}</Table.Cell>
|
||||
<Table.Cell className="px-4 py-3">{log.action}</Table.Cell>
|
||||
<Table.Cell className="px-4 py-3 text-muted-foreground">
|
||||
{log.targetType}
|
||||
{log.targetId ? ` / ${log.targetId.slice(0, 8)}` : ""}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
</Table.Cell>
|
||||
<Table.Cell className="px-4 py-3">
|
||||
<Badge
|
||||
variant={
|
||||
log.result === "success" ? "success" : log.result === "denied" ? "warning" : "danger"
|
||||
@@ -401,19 +402,19 @@ export function SettingsOverview({
|
||||
>
|
||||
{log.result}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-muted-foreground">{log.reason}</td>
|
||||
</tr>
|
||||
</Table.Cell>
|
||||
<Table.Cell className="px-4 py-3 text-muted-foreground">{log.reason}</Table.Cell>
|
||||
</Table.Row>
|
||||
))}
|
||||
{auditLogs.length === 0 ? (
|
||||
<tr>
|
||||
<td className="px-4 py-8 text-center text-muted-foreground" colSpan={6}>
|
||||
<Table.Row>
|
||||
<Table.Cell className="px-4 py-8 text-center text-muted-foreground" colSpan={6}>
|
||||
暂无审计日志
|
||||
</td>
|
||||
</tr>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
) : null}
|
||||
</tbody>
|
||||
</table>
|
||||
</Table.Body>
|
||||
</Table>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import Link from "next/link";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Button, LinkButton } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { buildPageHref } from "@/modules/settings/lib/pagination";
|
||||
|
||||
@@ -39,9 +37,9 @@ export function TableToolbar({
|
||||
上一页
|
||||
</Button>
|
||||
) : (
|
||||
<Button asChild size="sm" variant="outline">
|
||||
<Link href={buildPageHref(pathname, query, page - 1)}>上一页</Link>
|
||||
</Button>
|
||||
<LinkButton href={buildPageHref(pathname, query, page - 1)} size="sm" variant="outline">
|
||||
上一页
|
||||
</LinkButton>
|
||||
)}
|
||||
<span className="min-w-16 text-center">
|
||||
{page}/{pageCount}
|
||||
@@ -51,9 +49,9 @@ export function TableToolbar({
|
||||
下一页
|
||||
</Button>
|
||||
) : (
|
||||
<Button asChild size="sm" variant="outline">
|
||||
<Link href={buildPageHref(pathname, query, page + 1)}>下一页</Link>
|
||||
</Button>
|
||||
<LinkButton href={buildPageHref(pathname, query, page + 1)} size="sm" variant="outline">
|
||||
下一页
|
||||
</LinkButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Dialog } from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select } from "@/components/ui/select";
|
||||
import type { ApiResult } from "@/modules/core/server/api";
|
||||
import type { JoinRequest, Organization, PublicAccount, RoleDefinition } from "@/modules/core/types";
|
||||
|
||||
@@ -36,6 +37,7 @@ export function UserManagementClient({
|
||||
const router = useRouter();
|
||||
const [isCreateOpen, setIsCreateOpen] = useState(false);
|
||||
const [selectedOrganizationId, setSelectedOrganizationId] = useState(() => getDefaultOrganizationId(organizations));
|
||||
const [reviewRoleByRequestId, setReviewRoleByRequestId] = useState<Record<string, string>>({});
|
||||
const [message, setMessage] = useState("");
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
|
||||
@@ -169,6 +171,7 @@ export function UserManagementClient({
|
||||
{pendingJoinRequests.map((request) => {
|
||||
const requestRoles = getOrganizationRoles(roles, request.organizationId);
|
||||
const defaultRoleId = requestRoles.find((role) => role.key === "caregiver")?.id ?? requestRoles[0]?.id ?? "";
|
||||
const selectedRoleId = reviewRoleByRequestId[request.id] ?? defaultRoleId;
|
||||
|
||||
return (
|
||||
<div key={request.id} className="rounded-md border p-3">
|
||||
@@ -181,24 +184,18 @@ export function UserManagementClient({
|
||||
<Badge variant="warning">待审批</Badge>
|
||||
</div>
|
||||
<div className="mt-3 grid gap-2 sm:grid-cols-[1fr_auto_auto]">
|
||||
<select
|
||||
className="h-10 w-full rounded-md border bg-background px-3 text-sm outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring"
|
||||
defaultValue={defaultRoleId}
|
||||
id={`role-${request.id}`}
|
||||
>
|
||||
{requestRoles.map((role) => (
|
||||
<option key={role.id} value={role.id}>
|
||||
{role.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Select
|
||||
aria-label={`${request.accountName} 审批角色`}
|
||||
disabled={requestRoles.length === 0}
|
||||
onValueChange={(roleId) =>
|
||||
setReviewRoleByRequestId((current) => ({ ...current, [request.id]: roleId }))
|
||||
}
|
||||
options={requestRoles.map((role) => ({ value: role.id, label: role.label }))}
|
||||
value={selectedRoleId}
|
||||
/>
|
||||
<Button
|
||||
disabled={isPending || !defaultRoleId || !canManageAccounts}
|
||||
onClick={() => {
|
||||
const element = document.getElementById(`role-${request.id}`);
|
||||
const roleId = element instanceof HTMLSelectElement ? element.value : defaultRoleId;
|
||||
void reviewJoinRequest(request.id, "approved", roleId);
|
||||
}}
|
||||
disabled={isPending || !selectedRoleId || !canManageAccounts}
|
||||
onClick={() => void reviewJoinRequest(request.id, "approved", selectedRoleId)}
|
||||
size="sm"
|
||||
type="button"
|
||||
>
|
||||
@@ -232,35 +229,26 @@ export function UserManagementClient({
|
||||
<form className="grid gap-3" onSubmit={handleCreateAccount}>
|
||||
<label className="grid gap-2 text-sm">
|
||||
<span className="font-medium">机构</span>
|
||||
<select
|
||||
className="h-11 w-full rounded-md border bg-background px-3 text-sm outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring"
|
||||
<Select
|
||||
aria-label="机构"
|
||||
name="organizationId"
|
||||
onChange={(event) => setSelectedOrganizationId(event.target.value)}
|
||||
onValueChange={setSelectedOrganizationId}
|
||||
options={organizations.map((organization) => ({ value: organization.id, label: organization.name }))}
|
||||
value={selectedOrganizationId}
|
||||
>
|
||||
{organizations.map((organization) => (
|
||||
<option key={organization.id} value={organization.id}>
|
||||
{organization.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="grid gap-2 text-sm">
|
||||
<span className="font-medium">角色</span>
|
||||
<select
|
||||
className="h-11 w-full rounded-md border bg-background px-3 text-sm outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring"
|
||||
<Select
|
||||
aria-label="角色"
|
||||
defaultValue={organizationRoles[0]?.id ?? ""}
|
||||
disabled={organizationRoles.length === 0}
|
||||
key={selectedOrganizationId}
|
||||
name="roleId"
|
||||
options={organizationRoles.map((role) => ({ value: role.id, label: role.label }))}
|
||||
required
|
||||
>
|
||||
{organizationRoles.map((role) => (
|
||||
<option key={role.id} value={role.id}>
|
||||
{role.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="grid gap-2 text-sm">
|
||||
@@ -424,47 +412,38 @@ export function UserAccountActions({
|
||||
</label>
|
||||
<label className="grid gap-2 text-sm">
|
||||
<span className="font-medium">状态</span>
|
||||
<select
|
||||
className="h-11 w-full rounded-md border bg-background px-3 text-sm outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring"
|
||||
<Select
|
||||
aria-label="账号状态"
|
||||
defaultValue={account.status}
|
||||
disabled={isCurrentAccount}
|
||||
name="status"
|
||||
>
|
||||
<option value="active">启用</option>
|
||||
<option value="disabled">停用</option>
|
||||
</select>
|
||||
options={[
|
||||
{ value: "active", label: "启用" },
|
||||
{ value: "disabled", label: "停用" },
|
||||
]}
|
||||
/>
|
||||
</label>
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<label className="grid gap-2 text-sm">
|
||||
<span className="font-medium">机构</span>
|
||||
<select
|
||||
className="h-11 w-full rounded-md border bg-background px-3 text-sm outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring"
|
||||
<Select
|
||||
aria-label="账号机构"
|
||||
name="organizationId"
|
||||
onChange={(event) => setSelectedOrganizationId(event.target.value)}
|
||||
onValueChange={setSelectedOrganizationId}
|
||||
options={organizations.map((organization) => ({ value: organization.id, label: organization.name }))}
|
||||
value={selectedOrganizationId}
|
||||
>
|
||||
{organizations.map((organization) => (
|
||||
<option key={organization.id} value={organization.id}>
|
||||
{organization.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
/>
|
||||
</label>
|
||||
<label className="grid gap-2 text-sm">
|
||||
<span className="font-medium">角色</span>
|
||||
<select
|
||||
className="h-11 w-full rounded-md border bg-background px-3 text-sm outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring"
|
||||
<Select
|
||||
aria-label="账号角色"
|
||||
defaultValue={defaultRoleId}
|
||||
disabled={organizationRoles.length === 0}
|
||||
key={selectedOrganizationId}
|
||||
name="roleId"
|
||||
>
|
||||
{organizationRoles.map((role) => (
|
||||
<option key={role.id} value={role.id}>
|
||||
{role.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
options={organizationRoles.map((role) => ({ value: role.id, label: role.label }))}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
{message ? (
|
||||
|
||||
@@ -7,6 +7,8 @@ import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog } from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select } from "@/components/ui/select";
|
||||
import { Table } from "@/components/ui/table";
|
||||
|
||||
export type CmsRecordTone = "success" | "warning" | "danger" | "secondary";
|
||||
|
||||
@@ -92,71 +94,67 @@ export function CmsModuleClient({ records }: CmsModuleClientProps): React.ReactE
|
||||
value={query}
|
||||
/>
|
||||
</label>
|
||||
<label className="relative block">
|
||||
<div className="relative block">
|
||||
<Filter className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<select
|
||||
className="h-11 w-full rounded-md border bg-background px-9 text-sm outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring"
|
||||
onChange={(event) => updateStatus(event.target.value)}
|
||||
<Select
|
||||
aria-label="按状态筛选"
|
||||
className="w-full pl-9"
|
||||
onValueChange={updateStatus}
|
||||
options={statusOptions.map((option) => ({ value: option, label: option }))}
|
||||
value={status}
|
||||
>
|
||||
{statusOptions.map((option) => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full min-w-[860px] text-sm">
|
||||
<thead className="bg-secondary text-left text-xs text-muted-foreground">
|
||||
<tr>
|
||||
<th className="px-4 py-3 font-medium">记录</th>
|
||||
<th className="px-4 py-3 font-medium">分类</th>
|
||||
<th className="px-4 py-3 font-medium">负责人</th>
|
||||
<th className="px-4 py-3 font-medium">状态</th>
|
||||
<th className="px-4 py-3 font-medium">标记</th>
|
||||
<th className="px-4 py-3 text-right font-medium">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y bg-card">
|
||||
<Table className="w-full min-w-[860px] 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 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">
|
||||
{visibleRecords.map((record) => (
|
||||
<tr key={record.id}>
|
||||
<td className="px-4 py-3">
|
||||
<Table.Row key={record.id}>
|
||||
<Table.Cell className="px-4 py-3">
|
||||
<p className="font-medium">{record.title}</p>
|
||||
<p className="mt-1 max-w-md truncate text-xs text-muted-foreground">{record.summary}</p>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-muted-foreground">{record.category}</td>
|
||||
<td className="px-4 py-3">{record.owner}</td>
|
||||
<td className="px-4 py-3">
|
||||
</Table.Cell>
|
||||
<Table.Cell className="px-4 py-3 text-muted-foreground">{record.category}</Table.Cell>
|
||||
<Table.Cell className="px-4 py-3">{record.owner}</Table.Cell>
|
||||
<Table.Cell className="px-4 py-3">
|
||||
<Badge variant={record.statusTone}>{record.status}</Badge>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
</Table.Cell>
|
||||
<Table.Cell className="px-4 py-3">
|
||||
<Badge variant={record.priority === "高" ? "danger" : record.priority === "中" ? "warning" : "secondary"}>
|
||||
{record.priority}优先级
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
</Table.Cell>
|
||||
<Table.Cell className="px-4 py-3">
|
||||
<div className="flex justify-end">
|
||||
<Button onClick={() => setSelectedRecord(record)} size="sm" type="button" variant="outline">
|
||||
<Eye className="size-4" aria-hidden="true" />
|
||||
详情
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
))}
|
||||
{visibleRecords.length === 0 ? (
|
||||
<tr>
|
||||
<td className="px-4 py-8 text-center text-muted-foreground" colSpan={6}>
|
||||
<Table.Row>
|
||||
<Table.Cell className="px-4 py-8 text-center text-muted-foreground" colSpan={6}>
|
||||
暂无匹配记录
|
||||
</td>
|
||||
</tr>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
) : null}
|
||||
</tbody>
|
||||
</table>
|
||||
</Table.Body>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3 border-t p-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
|
||||
Reference in New Issue
Block a user