diff --git a/app/(app)/app/settings/audit/page.tsx b/app/(app)/app/settings/audit/page.tsx index eb6e5b4..3f62238 100644 --- a/app/(app)/app/settings/audit/page.tsx +++ b/app/(app)/app/settings/audit/page.tsx @@ -1,12 +1,11 @@ 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 } from "@/modules/core/server/auth"; import { hasPermission } from "@/modules/core/server/permissions"; import { readData } from "@/modules/core/server/store"; -import { TableToolbar } from "@/modules/settings/components/TableToolbar"; +import { TablePagination, TableToolbar } from "@/modules/settings/components/TableToolbar"; import { getPage, getPageCount, getSearchParam, paginate, type SearchParams } from "@/modules/settings/lib/pagination"; type AuditPageProps = { @@ -39,10 +38,8 @@ export default async function AuditPage({ searchParams }: AuditPageProps): Promi return (
-
- Audit -

审计日志

-

关键操作、拒绝访问和故障处理留痕。

+
+

审计日志

@@ -51,8 +48,6 @@ export default async function AuditPage({ searchParams }: AuditPageProps): Promi
+ diff --git a/app/(app)/app/settings/organizations/page.tsx b/app/(app)/app/settings/organizations/page.tsx index 14b4156..d58ba66 100644 --- a/app/(app)/app/settings/organizations/page.tsx +++ b/app/(app)/app/settings/organizations/page.tsx @@ -1,7 +1,6 @@ import { redirect } from "next/navigation"; import Link from "next/link"; -import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Table } from "@/components/ui/table"; import { getCurrentAuthContext } from "@/modules/core/server/auth"; @@ -11,7 +10,7 @@ import { OrganizationManagementClient, OrganizationRowActions, } from "@/modules/settings/components/OrganizationManagementClient"; -import { TableToolbar } from "@/modules/settings/components/TableToolbar"; +import { TablePagination, TableToolbar } from "@/modules/settings/components/TableToolbar"; import { getPage, getPageCount, getSearchParam, paginate, type SearchParams } from "@/modules/settings/lib/pagination"; type OrganizationsPageProps = { @@ -52,12 +51,8 @@ export default async function OrganizationsPage({ searchParams }: OrganizationsP return (
-
-
- Organizations -

机构管理

-

多机构租户、启停状态、成员邀请和机构角色初始化。

-
+
+

机构管理

{canManageOrganizations ? : null}
@@ -67,8 +62,6 @@ export default async function OrganizationsPage({ searchParams }: OrganizationsP
+ diff --git a/app/(app)/app/settings/roles/page.tsx b/app/(app)/app/settings/roles/page.tsx index 8a49798..538f685 100644 --- a/app/(app)/app/settings/roles/page.tsx +++ b/app/(app)/app/settings/roles/page.tsx @@ -1,12 +1,11 @@ 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 } from "@/modules/core/server/auth"; import { getRoleDefinitions, hasPermission, PERMISSION_DEFINITIONS } from "@/modules/core/server/permissions"; import { RoleManagementClient, RoleRowActions } from "@/modules/settings/components/RoleManagementClient"; -import { TableToolbar } from "@/modules/settings/components/TableToolbar"; +import { TablePagination, TableToolbar } from "@/modules/settings/components/TableToolbar"; import { getPage, getPageCount, getSearchParam, paginate, type SearchParams } from "@/modules/settings/lib/pagination"; type RolesPageProps = { @@ -37,12 +36,8 @@ export default async function RolesPage({ searchParams }: RolesPageProps): Promi return (
-
-
- RBAC -

角色权限

-

系统权限点、内置角色和机构自定义角色。

-
+
+

角色权限

{canManageRoles ? : null}
@@ -52,8 +47,6 @@ export default async function RolesPage({ searchParams }: RolesPageProps): Promi
+ diff --git a/app/(app)/app/settings/status/page.tsx b/app/(app)/app/settings/status/page.tsx index b4a8c86..7d95aac 100644 --- a/app/(app)/app/settings/status/page.tsx +++ b/app/(app)/app/settings/status/page.tsx @@ -8,7 +8,7 @@ import { getCurrentAuthContext } from "@/modules/core/server/auth"; import { hasPermission } from "@/modules/core/server/permissions"; import { readData } from "@/modules/core/server/store"; import { IncidentStatusActions } from "@/modules/settings/components/IncidentStatusActions"; -import { TableToolbar } from "@/modules/settings/components/TableToolbar"; +import { TablePagination, TableToolbar } from "@/modules/settings/components/TableToolbar"; import { getPage, getPageCount, getSearchParam, paginate, type SearchParams } from "@/modules/settings/lib/pagination"; type StatusPageProps = { @@ -42,10 +42,12 @@ export default async function StatusPage({ searchParams }: StatusPageProps): Pro return (
-
+
+
+

运行状态

+

{databaseHealth.reason}

+
{databaseHealth.ok ? "Healthy" : "Degraded"} -

运行状态

-

{databaseHealth.reason}

@@ -81,8 +83,6 @@ export default async function StatusPage({ searchParams }: StatusPageProps): Pro
+ diff --git a/app/(app)/app/settings/users/page.tsx b/app/(app)/app/settings/users/page.tsx index 072fe9f..b3e047f 100644 --- a/app/(app)/app/settings/users/page.tsx +++ b/app/(app)/app/settings/users/page.tsx @@ -8,7 +8,7 @@ import { getRoleDefinitions, hasPermission } from "@/modules/core/server/permiss 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 { TablePagination, 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"; @@ -65,10 +65,8 @@ export default async function UsersPage({ searchParams }: UsersPageProps): Promi return (
-
- Accounts -

用户管理

-

账号创建、机构加入审批、角色分配和账号列表。

+
+

用户管理

+ diff --git a/modules/settings/components/TableToolbar.tsx b/modules/settings/components/TableToolbar.tsx index 3fa416c..3907a76 100644 --- a/modules/settings/components/TableToolbar.tsx +++ b/modules/settings/components/TableToolbar.tsx @@ -4,8 +4,6 @@ import { buildPageHref } from "@/modules/settings/lib/pagination"; type TableToolbarProps = { actionLabel?: string; - page: number; - pageCount: number; pathname: string; query: string; searchPlaceholder: string; @@ -14,8 +12,6 @@ type TableToolbarProps = { export function TableToolbar({ actionLabel, - page, - pageCount, pathname, query, searchPlaceholder, @@ -29,31 +25,88 @@ export function TableToolbar({ 搜索 -
- {actionLabel ?? `共 ${total} 条`} -
- {page <= 1 ? ( - - ) : ( - - 上一页 - - )} - - {page}/{pageCount} - - {page >= pageCount ? ( - - ) : ( - - 下一页 - - )} -
+ {actionLabel ?? `共 ${total} 条`} +
+ ); +} + +type TablePaginationProps = { + page: number; + pageCount: number; + pathname: string; + query: string; + total: number; +}; + +export function TablePagination({ + page, + pageCount, + pathname, + query, + total, +}: TablePaginationProps): React.ReactElement { + const safePage = Math.min(page, pageCount); + const isFirstPage = safePage <= 1; + const isLastPage = safePage >= pageCount; + + return ( +
+ 共 {total} 条 +
+ {isFirstPage ? ( + + ) : ( + + 首页 + + )} + {isFirstPage ? ( + + ) : ( + + 上一页 + + )} + + {safePage}/{pageCount} + +
+ {query.trim() ? : null} + + +
+ {isLastPage ? ( + + ) : ( + + 下一页 + + )} + {isLastPage ? ( + + ) : ( + + 末页 + + )}
); diff --git a/modules/shared/components/AccountMenu.tsx b/modules/shared/components/AccountMenu.tsx new file mode 100644 index 0000000..b65aa0a --- /dev/null +++ b/modules/shared/components/AccountMenu.tsx @@ -0,0 +1,109 @@ +"use client"; + +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { Ellipsis, LogOut, Settings } from "lucide-react"; +import { useEffect, useRef, useState } from "react"; + +import { Button } from "@/components/ui/button"; +import type { Permission, PublicAccount } from "@/modules/core/types"; +import { UserAvatar } from "@/modules/shared/components/UserAvatar"; + +type AccountMenuProps = { + account: PublicAccount; + permissions: Permission[]; +}; + +export function AccountMenu({ account, permissions }: AccountMenuProps): React.ReactElement { + const router = useRouter(); + const menuRef = useRef(null); + const [isOpen, setIsOpen] = useState(false); + const [isPending, setIsPending] = useState(false); + const canReadAccounts = permissions.includes("account:read"); + const settingsHref = canReadAccounts ? `/app/settings/users?q=${encodeURIComponent(account.email)}` : "/app/settings/global"; + + useEffect(() => { + if (!isOpen) { + return; + } + + function handlePointerDown(event: PointerEvent): void { + const target = event.target; + if (target instanceof Node && !menuRef.current?.contains(target)) { + setIsOpen(false); + } + } + + function handleKeyDown(event: KeyboardEvent): void { + if (event.key === "Escape") { + setIsOpen(false); + } + } + + document.addEventListener("pointerdown", handlePointerDown); + document.addEventListener("keydown", handleKeyDown); + + return () => { + document.removeEventListener("pointerdown", handlePointerDown); + document.removeEventListener("keydown", handleKeyDown); + }; + }, [isOpen]); + + async function handleSignOut(): Promise { + setIsPending(true); + await fetch("/api/auth/logout", { method: "POST" }); + router.refresh(); + router.replace("/login"); + } + + return ( +
+
+ +
+

{account.name}

+

{account.email}

+
+ +
+ + {isOpen ? ( +
+ setIsOpen(false)} + role="menuitem" + > +
+ ) : null} +
+ ); +} diff --git a/modules/shared/components/AppBreadcrumbs.tsx b/modules/shared/components/AppBreadcrumbs.tsx new file mode 100644 index 0000000..49da7eb --- /dev/null +++ b/modules/shared/components/AppBreadcrumbs.tsx @@ -0,0 +1,83 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { ChevronRight } from "lucide-react"; + +type BreadcrumbItem = { + href?: string; + label: string; +}; + +const routeLabels: Record = { + "/app/dashboard": "运营看板", + "/app/elders": "老人档案", + "/app/beds": "床位房间", + "/app/health": "健康照护", + "/app/care": "护理服务", + "/app/emergency": "安全应急", + "/app/devices": "设备运维", + "/app/notices": "公告通知", + "/app/alerts": "规则预警", + "/app/family": "家属服务", + "/app/settings/global": "全局配置", + "/app/settings/organizations": "机构管理", + "/app/settings/users": "用户管理", + "/app/settings/roles": "角色权限", + "/app/settings/audit": "审计日志", + "/app/settings/status": "运行状态", +}; + +function getBreadcrumbs(pathname: string): BreadcrumbItem[] { + if (pathname === "/app" || pathname === "/app/dashboard") { + return [{ label: "工作台" }]; + } + + if (pathname.startsWith("/app/settings/organizations/")) { + return [ + { href: "/app/dashboard", label: "工作台" }, + { href: "/app/settings/users", label: "系统设置" }, + { href: "/app/settings/organizations", label: "机构管理" }, + { label: "机构详情" }, + ]; + } + + if (pathname.startsWith("/app/settings")) { + return [ + { href: "/app/dashboard", label: "工作台" }, + { href: "/app/settings/users", label: "系统设置" }, + { label: routeLabels[pathname] ?? "设置" }, + ]; + } + + return [ + { href: "/app/dashboard", label: "工作台" }, + { label: routeLabels[pathname] ?? "工作区" }, + ]; +} + +export function AppBreadcrumbs(): React.ReactElement { + const pathname = usePathname(); + const breadcrumbs = getBreadcrumbs(pathname); + + return ( + + ); +} diff --git a/modules/shared/components/AppShell.tsx b/modules/shared/components/AppShell.tsx index c04d4da..609b219 100644 --- a/modules/shared/components/AppShell.tsx +++ b/modules/shared/components/AppShell.tsx @@ -20,6 +20,8 @@ import type { LucideIcon } from "lucide-react"; import type { Permission, PublicAccount } from "@/modules/core/types"; import { SignOutButton } from "@/modules/auth/components/SignOutButton"; +import { AccountMenu } from "@/modules/shared/components/AccountMenu"; +import { AppBreadcrumbs } from "@/modules/shared/components/AppBreadcrumbs"; import { UserAvatar } from "@/modules/shared/components/UserAvatar"; type NavItem = { @@ -194,14 +196,7 @@ export function AppShell({ children, account, permissions }: AppShellProps): Rea
-
- -
-

{account.name}

-

{account.email}

-
-
- +
@@ -212,9 +207,7 @@ export function AppShell({ children, account, permissions }: AppShellProps): Rea -
-

工作台

-
+