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}
+
+
+ {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
-
+