import { Fragment, useCallback, useEffect, useMemo, useState } from "react";
import {
  Search, ChevronRight, ChevronDown, Download, Eye, Upload, Trash2, X,
  Edit2, Key, User, Activity, LogOut, Plus, RefreshCw, Check, Lock,
  FileText, Building, BarChart2, Users, Database,
  AlertCircle, Filter, FileCheck, Layers, FolderPlus, Paperclip, Library, Link2, Unlink
} from "lucide-react";
import {
  BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip,
  ResponsiveContainer, LineChart, Line, PieChart, Pie, Cell, Legend
} from "recharts";
import { HttpError } from "../shared/api/errors";
import { subscribeAuthExpired } from "../shared/auth/authEvents";
import { tokenStore } from "../shared/auth/tokenStore";
import {
  getFirstAllowedPage,
  getNavigationItems,
  isPageAllowed,
  loginSession,
  logoutSession,
  restoreSession,
  type AuthenticatedPage,
  type NavigationItem,
} from "../features/auth/authSession";
import type { LoginRequest, UserSummary } from "../features/auth/authTypes";
import { CategoryType, EnabledStatus } from "../shared/api/enums";
import { categoryApi } from "../features/categories/categoryApi";
import { useCategoryTree } from "../features/categories/categoryState";
import {
  findCategoryNode,
  getCategoryParentOptions,
  retainExistingCategoryIds,
} from "../features/categories/categoryTree";
import type {
  CategoryNode,
  CreateCategoryRequest,
  UpdateCategoryRequest,
} from "../features/categories/categoryTypes";
import {
  flattenOrganizations,
  useOrganizationTree,
  useUserDirectory,
} from "../features/directory/directoryState";
import type { OrganizationNode } from "../features/directory/directoryTypes";
import {
  AllowedAction,
  AttachmentType,
  DocumentType,
  RoleCode,
  type AttachmentType as AttachmentTypeCode,
  type DocumentStatus as DocumentStatusCode,
} from "../shared/api/enums";
import { documentApi } from "../features/documents/documentApi";
import {
  loadDetailOnce,
  useDocumentList,
  useDocumentTags,
  useSubPlanList,
} from "../features/documents/documentState";
import { TagPanel } from "../features/documents/TagPanel";
import type {
  DocumentDetail,
  DocumentSummary,
  MainPlanBrief,
} from "../features/documents/documentTypes";
import { DocumentSearchToolbar } from "../features/documents/DocumentSearchToolbar";
import { toUpdatedTimeRange } from "../features/documents/documentSearch";
import { attachmentApi } from "../features/attachments/attachmentApi";
import {
  useAttachmentList,
  useAttachmentMainPlans,
  useMountedAttachments,
} from "../features/attachments/attachmentState";
import type { AttachmentBindingSummary } from "../features/attachments/attachmentTypes";
import { FilePreviewModal } from "../features/files/FilePreviewModal";
import { DownloadButton } from "../features/files/DownloadButton";
import {
  BatchImportModal as F5BatchImportModal,
  DeleteDocumentModal,
  EditMetadataModal,
  SingleUploadModal,
} from "../features/mutations/MutationModals";
import { invalidateDetail } from "../features/documents/documentState";
import { notifyContentChanged } from "../shared/state/contentRefresh";
import { AttachmentBindingModal } from "../features/bindings/AttachmentBindingModal";
import { UnbindAttachmentModal } from "../features/bindings/UnbindAttachmentModal";
import { PermissionModal as F6PermissionModal } from "../features/permissions/PermissionModal";
import { AuditDashboard } from "../features/audit/AuditDashboard";
import { DocumentStatisticsCards } from "../features/statistics/DocumentStatisticsCards";
import { RecycleBinModal } from "../features/recycle-bin/RecycleBinModal";
import { useDictionaries } from "../features/dictionaries/dictionaryContext";

// ─── Color tokens ─────────────────────────────────────────────────────────────
// Header / Primary  #2876C1 — 青翠天蓝色 (sky cyan-blue)
// Text              #1A5299 — medium blue (no black elements)
// White             #FFFFFF — canvas, cards, inverse text
// Page bg           #EBF4FD — solid light blue, no gradients

const N = "#1A5299";   // medium blue — all text, never black
const B = "#3191eb";   // 青翠天蓝色 — header, buttons, accents
const W = "#FFFFFF";   // white — cards, canvas
const PBG = "#EBF4FD"; // page background (solid light blue)

// ─── Types ────────────────────────────────────────────────────────────────────
type Page = "login" | AuthenticatedPage;
type MainTab = "主案列表" | "全部方案" | "最近更新";
interface Doc {
  id: number; name: string; summary: string; level: string;
  tags: string[]; type: string; visibility: string; updatedAt: string;
  children?: Doc[];    // 子方案
  materials?: Doc[];   // 配套资料（如政策法规）
}
interface SharedAttachment extends Doc {
  attachmentType: string;
  fileFormat: string;
  uploader: string;
}
interface AuditLog {
  id: number; time: string; user: string; dept: string;
  action: string; target: string; result: string; ip: string;
}

// ─── Mock Data ────────────────────────────────────────────────────────────────
const DOCS: Doc[] = [
  {
    id: 1, name: "2024年度综合应急预案",
    summary: "本预案规定了组织机构在紧急情况下的响应流程、资源调配机制及协调联动程序，确保各部门高效协同，最大限度降低突发事件造成的损失。",
    level: "机密", tags: ["应急", "综合", "2024"], type: "主案", visibility: "全机关", updatedAt: "2024-03-15",
    children: [
      { id: 11, name: "人员组织子案", summary: "详细规定了应急响应各级人员的职责分工、通讯联络方式及人员调配程序，建立三级指挥体系，确保人员有序高效投入应急处置工作，实现快速响应和高效指挥。", level: "秘密", tags: ["人员", "组织"], type: "子案", visibility: "作战部", updatedAt: "2024-03-10", children: [] },
      { id: 12, name: "资源组织子案", summary: "明确了应急物资储备清单、调拨流程及保障措施，涵盖装备器材、后勤保障、运输配送等各环节，确保各类资源在第一时间准确到位，满足处置需要。", level: "秘密", tags: ["资源", "保障"], type: "子案", visibility: "后勤部", updatedAt: "2024-03-10", children: [] },
      { id: 13, name: "重点任务子案", summary: "针对核心关键任务制定专项处置方案，明确各阶段重点工作内容、时限要求及质量标准，确保关键节点任务圆满完成，全面提升整体任务完成质量。", level: "秘密", tags: ["任务", "重点"], type: "子案", visibility: "全机关", updatedAt: "2024-03-12", children: [] },
    ],
    materials: [
      { id: 101, name: "中华人民共和国突发事件应对法", summary: "全国突发事件应对工作的基础性法律，明确了各级政府、单位的应急职责、响应程序及法律责任，为应急预案制定与实施提供上位法依据。", level: "公开", tags: ["法律", "国家"], type: "配套", visibility: "全机关", updatedAt: "2007-08-30", children: [] },
      { id: 102, name: "国家突发公共事件总体应急预案", summary: "国务院发布的国家层面总体应急预案，规定了突发公共事件的分类分级、组织体系、运行机制及应急保障等核心内容。", level: "公开", tags: ["预案", "国家"], type: "配套", visibility: "全机关", updatedAt: "2006-01-08", children: [] },
      { id: 103, name: "军队应急处置工作规定", summary: "结合军队实际制定的应急处置工作规定，明确军队在突发事件中的职责、流程及与地方的协同机制。", level: "内部", tags: ["规定", "军队"], type: "配套", visibility: "全机关", updatedAt: "2020-05-12", children: [] },
    ],
  },
  {
    id: 2, name: "战备物资管理规程", summary: "本规程详细规定了战备物资的储存、维护、调拨及核查程序，建立全生命周期管理体系，确保战备物资始终处于良好可用状态，满足随时遂行任务需求。", level: "绝密", tags: ["战备", "物资", "管理"], type: "主案", visibility: "作战部", updatedAt: "2024-02-28",
    children: [
      { id: 21, name: "物资储备子案", summary: "细化战备物资储备分类、数量标准及储备布局，建立分级储备体系，确保关键物资足额到位、合理分布。", level: "机密", tags: ["储备", "物资"], type: "子案", visibility: "后勤部", updatedAt: "2024-02-20", children: [] },
      { id: 22, name: "物资调拨子案", summary: "规范战备物资调拨审批流程、运输组织及交接手续，确保物资调拨及时、准确、可追溯。", level: "机密", tags: ["调拨", "运输"], type: "子案", visibility: "作战部", updatedAt: "2024-02-22", children: [] },
    ],
    materials: [
      { id: 104, name: "军用物资管理办法", summary: "中央军委印发的物资管理办法，对军用物资的分类、采购、储备、使用等环节作出全面规范。", level: "秘密", tags: ["办法", "军队"], type: "配套", visibility: "作战部", updatedAt: "2019-11-15", children: [] },
      { id: 105, name: "战备物资储备管理规定", summary: "关于战备物资储备的管理规定，明确了储备原则、标准、轮换及动用审批程序。", level: "机密", tags: ["储备", "规定"], type: "配套", visibility: "作战部", updatedAt: "2021-07-08", children: [] },
    ],
  },
  {
    id: 3, name: "通信保障方案", summary: "规定了各类通信系统的部署架构、保障措施及故障应急处置预案，确保在复杂电磁环境下通信畅通，为指挥决策提供可靠信息支撑和技术保障。", level: "机密", tags: ["通信", "保障", "方案"], type: "主案", visibility: "通信部", updatedAt: "2024-01-20",
    children: [
      { id: 31, name: "固定通信保障子案", summary: "针对固定通信设施制定专项保障方案，明确日常维护、故障响应及扩容升级流程。", level: "秘密", tags: ["固定", "通信"], type: "子案", visibility: "通信部", updatedAt: "2024-01-15", children: [] },
      { id: 32, name: "机动通信保障子案", summary: "针对机动指挥、野外驻训等场景的机动通信保障方案，明确装备编配、链路建立与协同规则。", level: "秘密", tags: ["机动", "通信"], type: "子案", visibility: "通信部", updatedAt: "2024-01-18", children: [] },
    ],
    materials: [
      { id: 106, name: "中华人民共和国无线电管理条例", summary: "规范无线电频率使用、无线电台站设置及无线电波秩序的国家级行政法规。", level: "公开", tags: ["法规", "国家"], type: "配套", visibility: "通信部", updatedAt: "2016-11-11", children: [] },
      { id: 107, name: "军队通信保障条例", summary: "军队通信保障工作的基本法规，明确各级职责、保障原则及协同机制。", level: "秘密", tags: ["条例", "军队"], type: "配套", visibility: "通信部", updatedAt: "2018-04-22", children: [] },
    ],
  },
];

// 共享附件库：附件独立存在，通过挂载关系关联到一个或多个主案。
const SHARED_ATTACHMENTS: SharedAttachment[] = [
  { id: 101, name: "中华人民共和国突发事件应对法", summary: "全国突发事件应对工作的基础性法律，为应急预案制定与实施提供上位法依据。", level: "公开", tags: ["应急", "法规"], type: "附件", visibility: "全员共享", updatedAt: "2026-07-20", children: [], attachmentType: "政策法规", fileFormat: "DOCX", uploader: "系统管理员" },
  { id: 102, name: "国家突发公共事件总体应急预案", summary: "国家层面总体应急预案，规定突发公共事件的分类分级、组织体系和运行机制。", level: "公开", tags: ["国家", "预案"], type: "附件", visibility: "全员共享", updatedAt: "2026-07-18", children: [], attachmentType: "政策法规", fileFormat: "PDF", uploader: "张明" },
  { id: 103, name: "应急处置工作规定", summary: "明确突发事件处置中的职责、流程与协同机制。", level: "内部", tags: ["应急", "规定"], type: "附件", visibility: "全员共享", updatedAt: "2026-07-17", children: [], attachmentType: "工作规范", fileFormat: "DOCX", uploader: "刘洋" },
  { id: 104, name: "物资管理办法", summary: "对物资分类、采购、储备和使用等环节作出统一规范。", level: "内部", tags: ["物资", "办法"], type: "附件", visibility: "全员共享", updatedAt: "2026-07-16", children: [], attachmentType: "工作规范", fileFormat: "DOCX", uploader: "李伟" },
  { id: 105, name: "应急物资储备标准表", summary: "应急物资分类、数量和储备标准汇总表。", level: "内部", tags: ["物资", "标准"], type: "附件", visibility: "全员共享", updatedAt: "2026-07-15", children: [], attachmentType: "表格资料", fileFormat: "XLSX", uploader: "李伟" },
  { id: 106, name: "中华人民共和国无线电管理条例", summary: "规范无线电频率使用、台站设置及无线电波秩序的行政法规。", level: "公开", tags: ["通信", "法规"], type: "附件", visibility: "全员共享", updatedAt: "2026-07-14", children: [], attachmentType: "政策法规", fileFormat: "PDF", uploader: "王芳" },
  { id: 107, name: "通信保障工作规范", summary: "通信保障工作的职责、保障原则和协同机制。", level: "内部", tags: ["通信", "保障"], type: "附件", visibility: "全员共享", updatedAt: "2026-07-12", children: [], attachmentType: "工作规范", fileFormat: "DOCX", uploader: "王芳" },
  { id: 108, name: "应急处置工作流程图", summary: "突发事件应急处置关键环节和协同关系图示。", level: "公开", tags: ["应急", "流程"], type: "附件", visibility: "全员共享", updatedAt: "2026-07-10", children: [], attachmentType: "图示资料", fileFormat: "PDF", uploader: "系统管理员" },
];

const INITIAL_ATTACHMENT_BINDINGS: Record<number, number[]> = {
  1: [101, 102, 103, 105, 108],
  2: [102, 104, 105],
  3: [102, 106, 107, 108],
};

const AUDIT_LOGS: AuditLog[] = [
  { id: 1, time: "2024-03-20 09:15:23", user: "张明", dept: "作战部", action: "查看文档", target: "2024年度综合应急预案", result: "成功", ip: "192.168.1.101" },
  { id: 2, time: "2024-03-20 09:32:11", user: "李伟", dept: "后勤部", action: "下载文档", target: "资源组织子案", result: "成功", ip: "192.168.1.105" },
  { id: 3, time: "2024-03-20 10:01:45", user: "王芳", dept: "通信部", action: "上传文档", target: "通信保障方案_v2", result: "成功", ip: "192.168.1.108" },
  { id: 4, time: "2024-03-20 10:18:30", user: "赵强", dept: "总部机关", action: "编辑文档", target: "战备物资管理规程", result: "失败", ip: "192.168.1.102" },
  { id: 5, time: "2024-03-20 10:45:22", user: "刘洋", dept: "作战部", action: "查看文档", target: "人员组织子案", result: "成功", ip: "192.168.1.103" },
  { id: 6, time: "2024-03-20 11:02:18", user: "陈军", dept: "情报部", action: "权限配置", target: "重点任务子案", result: "成功", ip: "192.168.1.110" },
  { id: 7, time: "2024-03-20 11:23:55", user: "张明", dept: "作战部", action: "下载文档", target: "2024年度综合应急预案", result: "成功", ip: "192.168.1.101" },
  { id: 8, time: "2024-03-20 13:15:42", user: "孙莉", dept: "后勤部", action: "查看文档", target: "战备物资管理规程", result: "无权限", ip: "192.168.1.106" },
  { id: 9, time: "2024-03-20 13:45:08", user: "周鑫", dept: "通信部", action: "查看文档", target: "通信保障方案", result: "成功", ip: "192.168.1.109" },
  { id: 10, time: "2024-03-20 14:20:33", user: "吴刚", dept: "总部机关", action: "删除文档", target: "旧版应急预案_2023", result: "成功", ip: "192.168.1.104" },
  { id: 11, time: "2024-03-20 14:55:17", user: "李伟", dept: "后勤部", action: "批量导入", target: "物资清单文件包", result: "成功", ip: "192.168.1.105" },
  { id: 12, time: "2024-03-20 15:30:44", user: "赵强", dept: "总部机关", action: "登录系统", target: "—", result: "成功", ip: "192.168.1.102" },
];

const dailyData = [
  { day: "03-14", ops: 45 }, { day: "03-15", ops: 62 }, { day: "03-16", ops: 38 },
  { day: "03-17", ops: 71 }, { day: "03-18", ops: 55 }, { day: "03-19", ops: 48 }, { day: "03-20", ops: 83 },
];
const pieData = [
  { name: "查看文档", value: 45 }, { name: "下载文档", value: 22 },
  { name: "上传文档", value: 8 }, { name: "编辑文档", value: 12 },
  { name: "权限配置", value: 5 }, { name: "其他", value: 8 },
];
// Blue shades from the same hue family
const PIE_COLORS = ["#1A5299", "#2876C1", "#3D8FD4", "#5BA5E0", "#84BFEA", "#B3D8F4"];
const userBar = [
  { name: "张明", count: 18 }, { name: "李伟", count: 15 }, { name: "王芳", count: 12 },
  { name: "赵强", count: 9 }, { name: "刘洋", count: 8 }, { name: "陈军", count: 7 },
];

// ─── Level badge (navy / blue / white only) ────────────────────────────────────
function LevelBadge({ level }: { level: string }) {
  const styles: Record<string, { bg: string; text: string; border?: string }> = {
    "绝密": { bg: N, text: W },
    "机密": { bg: B, text: W },
    "秘密": { bg: W, text: B, border: B },
    "内部": { bg: W, text: N, border: N },
    "公开": { bg: `rgba(40,118,193,0.1)`, text: N, border: `rgba(26,82,153,0.3)` },
  };
  const s = styles[level] || { bg: N, text: W };
  return (
    <span style={{ background: s.bg, color: s.text, border: s.border ? `1px solid ${s.border}` : undefined }}
      className="inline-block px-2 py-0.5 text-xs font-black rounded-sm whitespace-nowrap">
      {level}
    </span>
  );
}

// ─── Highlight text ────────────────────────────────────────────────────────────
function HighlightText({ text, keyword }: { text: string; keyword: string }) {
  if (!keyword.trim()) return <span>{text}</span>;
  const parts = text.split(new RegExp(`(${keyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})`, "gi"));
  return (
    <span>
      {parts.map((part, i) =>
        part.toLowerCase() === keyword.toLowerCase()
          ? <mark key={i} style={{ background: "rgba(21,101,192,0.2)", color: N }} className="px-0.5 rounded-sm not-italic">{part}</mark>
          : <span key={i}>{part}</span>
      )}
    </span>
  );
}

// ─── Button primitives (consistent across all pages) ──────────────────────────
function BtnPrimary({ children, onClick, sm }: { children: React.ReactNode; onClick?: () => void; sm?: boolean }) {
  return (
    <button onClick={onClick}
      style={{ background: B, color: W }}
      className={`inline-flex items-center gap-1.5 font-black rounded-sm transition-opacity hover:opacity-80 active:opacity-60 ${sm ? "px-3 py-1 text-xs" : "px-4 py-2 text-sm"}`}>
      {children}
    </button>
  );
}
function BtnOutline({ children, onClick, sm, danger }: { children: React.ReactNode; onClick?: () => void; sm?: boolean; danger?: boolean }) {
  return (
    <button onClick={onClick}
      style={{ color: danger ? N : B, borderColor: danger ? N : B, background: W }}
      className={`inline-flex items-center gap-1.5 font-black rounded-sm border transition-colors hover:opacity-70 active:opacity-50 ${sm ? "px-3 py-1 text-xs" : "px-4 py-2 text-sm"}`}>
      {children}
    </button>
  );
}
function BtnGhost({ children, onClick, sm }: { children: React.ReactNode; onClick?: () => void; sm?: boolean }) {
  return (
    <button onClick={onClick}
      style={{ color: N, background: "transparent" }}
      className={`inline-flex items-center gap-1.5 font-black rounded-sm transition-colors hover:bg-[rgba(40,118,193,0.08)] active:bg-[rgba(40,118,193,0.18)] ${sm ? "px-2 py-1 text-xs" : "px-3 py-1.5 text-sm"}`}>
      {children}
    </button>
  );
}

// ─── Category tree ────────────────────────────────────────────────────────────
type CategoryModalState = {
  mode: "addRoot" | "addChild" | "edit";
  target?: CategoryNode;
};

interface CategoryFormValue {
  categoryCode: string;
  categoryName: string;
  categoryType: keyof typeof CategoryType;
  parentId: string | null;
  sortNo: number;
}

function generateInternalCategoryCode(): string {
  const timestamp = Date.now().toString(36);
  const random = Math.random().toString(36).slice(2, 14);
  return `CAT_${timestamp}_${random}`.toUpperCase();
}

function errorRequestSuffix(requestId: string): string {
  return requestId ? `（错误编号：${requestId}）` : "";
}

function categoryActionError(error: unknown): string {
  if (!(error instanceof HttpError)) return "分类操作失败，请稍后重试";
  if (error.code === "DATA_VERSION_CONFLICT") {
    return `分类已被其他用户修改，请刷新后重试。${errorRequestSuffix(error.requestId)}`;
  }
  if (error.code === "CATEGORY_IN_USE") {
    const details = error.details;
    if (details && typeof details === "object" && !Array.isArray(details)) {
      const values = details as Record<string, unknown>;
      const childCount = typeof values.childCategoryCount === "number" ? values.childCategoryCount : 0;
      const documentCount = typeof values.documentCount === "number" ? values.documentCount : 0;
      return `分类仍被占用：子分类 ${childCount} 个，关联方案 ${documentCount} 个。${errorRequestSuffix(error.requestId)}`;
    }
  }
  if (error.httpStatus === 403) {
    return `权限不足，无法执行分类管理操作。${errorRequestSuffix(error.requestId)}`;
  }
  return `${error.message}${errorRequestSuffix(error.requestId)}`;
}

function CategoryTreeNode({ node, depth, expanded, onToggle, selectedId, onSelect, onAddChild, onEdit, onDelete }: {
  node: CategoryNode; depth: number; expanded: Set<string>;
  onToggle: (id: string) => void; selectedId: string | null; onSelect: (id: string) => void;
  onAddChild?: (parent: CategoryNode) => void; onEdit?: (node: CategoryNode) => void; onDelete?: (node: CategoryNode) => void;
}) {
  const isOpen = expanded.has(node.id);
  const hasKids = node.children.length > 0;
  const isSelected = selectedId === node.id;
  const isAdmin = !!(onAddChild || onEdit || onDelete);
  return (
    <div>
      <div
        style={{
          paddingLeft: 8 + depth * 14,
          background: isSelected ? `rgba(21,101,192,0.1)` : undefined,
          color: isSelected ? B : N,
        }}
        className="group flex items-center gap-1 py-1.5 pr-1 cursor-pointer rounded-sm hover:bg-[rgba(21,101,192,0.07)] transition-colors"
        onClick={() => onSelect(node.id)}
      >
        <button
          className="w-4 h-4 flex items-center justify-center flex-shrink-0"
          style={{ color: B }}
          onClick={e => { e.stopPropagation(); if (hasKids) onToggle(node.id); }}
        >
          {hasKids ? (isOpen ? <ChevronDown size={13} /> : <ChevronRight size={13} />) : null}
        </button>
        <Layers size={12} style={{ color: B }} className="flex-shrink-0" />
        <span className="text-sm font-bold truncate flex-1" title={`${node.categoryCode} · ${node.categoryPath}`}>
          {node.categoryName}
          <span className="ml-1 text-[10px] opacity-45">({node.documentCount})</span>
        </span>
        {isAdmin && (
          <div className="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity" onClick={e => e.stopPropagation()}>
            {onAddChild && (
              <button title="添加子分类" onClick={() => onAddChild(node)}
                className="w-5 h-5 flex items-center justify-center rounded-sm hover:bg-[rgba(21,101,192,0.18)] transition-colors"
                style={{ color: B }}><Plus size={12} /></button>
            )}
            {onEdit && (
              <button title="编辑" onClick={() => onEdit(node)}
                className="w-5 h-5 flex items-center justify-center rounded-sm hover:bg-[rgba(21,101,192,0.18)] transition-colors"
                style={{ color: B }}><Edit2 size={11} /></button>
            )}
            {onDelete && (
              <button title="删除" onClick={() => onDelete(node)}
                className="w-5 h-5 flex items-center justify-center rounded-sm hover:bg-[rgba(21,101,192,0.18)] transition-colors"
                style={{ color: N }}><Trash2 size={11} /></button>
            )}
          </div>
        )}
      </div>
      {isOpen && hasKids && node.children.map(c => (
        <CategoryTreeNode key={c.id} node={c} depth={depth + 1} expanded={expanded} onToggle={onToggle}
          selectedId={selectedId} onSelect={onSelect}
          onAddChild={onAddChild} onEdit={onEdit} onDelete={onDelete} />
      ))}
    </div>
  );
}

function CategoryPanel({ admin, selectedCategoryId, onSelectCategory, allCount }: {
  admin?: boolean;
  selectedCategoryId?: string | null;
  onSelectCategory?: (id: string | null) => void;
  allCount?: number | null;
}) {
  const { keyword, setKeyword, tree, status, error, refresh } = useCategoryTree();
  const [expanded, setExpanded] = useState<Set<string>>(new Set());
  const [internalSelectedId, setInternalSelectedId] = useState<string | null>(null);
  const selectedId = selectedCategoryId === undefined ? internalSelectedId : selectedCategoryId;
  const setSelectedId = (id: string | null) => {
    if (selectedCategoryId === undefined) setInternalSelectedId(id);
    onSelectCategory?.(id);
  };
  const [editModal, setEditModal] = useState<CategoryModalState | null>(null);
  const [submitting, setSubmitting] = useState(false);
  const [actionMessage, setActionMessage] = useState("");
  const initialized = useMemo(() => ({ current: false }), []);

  useEffect(() => {
    setExpanded((current) => {
      const retained = retainExistingCategoryIds(current, tree);
      if (!initialized.current && tree[0]) {
        retained.add(tree[0].id);
        initialized.current = true;
      }
      return retained;
    });
    if (selectedId && !findCategoryNode(tree, selectedId)) setSelectedId(null);
  }, [initialized, selectedId, tree]);

  const toggle = (id: string) => setExpanded(p => {
    const next = new Set(p);
    next.has(id) ? next.delete(id) : next.add(id);
    return next;
  });

  const handleSubmitEdit = async (value: CategoryFormValue) => {
    if (!editModal || submitting) return;
    setSubmitting(true);
    setActionMessage("");
    try {
      if (editModal.mode === "edit" && editModal.target) {
        const request: UpdateCategoryRequest = {
          categoryName: value.categoryName,
          categoryType: value.categoryType,
          parentId: value.parentId,
          sortNo: value.sortNo,
          rowVersion: editModal.target.rowVersion,
        };
        await categoryApi.update(editModal.target.id, request);
        setSelectedId(editModal.target.id);
        setActionMessage("分类已更新。");
      } else {
        const request: CreateCategoryRequest = {
          categoryCode: value.categoryCode,
          categoryName: value.categoryName,
          categoryType: value.categoryType,
          parentId: editModal.mode === "addChild" ? editModal.target?.id ?? null : value.parentId,
          sortNo: value.sortNo,
        };
        const created = await categoryApi.create(request);
        setSelectedId(created.id);
        if (request.parentId) {
          setExpanded((current) => new Set(current).add(request.parentId as string));
        }
        setActionMessage("分类已新增。");
      }
      setEditModal(null);
      refresh();
    } catch (operationError) {
      setActionMessage(categoryActionError(operationError));
      if (operationError instanceof HttpError && operationError.code === "DATA_VERSION_CONFLICT") {
        refresh();
      }
    } finally {
      setSubmitting(false);
    }
  };

  const handleDelete = async (node: CategoryNode) => {
    if (submitting) return;
    if (!window.confirm(`确认删除分类「${node.categoryName}」？`)) return;
    setSubmitting(true);
    setActionMessage("");
    try {
      await categoryApi.remove(node.id, node.rowVersion);
      if (selectedId === node.id) setSelectedId(null);
      setActionMessage("分类已删除。");
      refresh();
    } catch (operationError) {
      setActionMessage(categoryActionError(operationError));
      if (operationError instanceof HttpError && operationError.code === "DATA_VERSION_CONFLICT") {
        refresh();
      }
    } finally {
      setSubmitting(false);
    }
  };

  return (
    <div className="flex flex-col h-full">
      <div className="flex items-center gap-1 mb-3">
        <div className="flex-1 flex items-center gap-2 rounded-sm border px-3 h-9 bg-white" style={{ borderColor: "rgba(26,82,153,0.2)" }}>
          <Search size={14} style={{ color: B }} />
          <input
            className="flex-1 text-sm bg-transparent outline-none font-bold"
            style={{ color: N }}
            placeholder="搜索分类名称或编码"
            value={keyword}
            onChange={e => setKeyword(e.target.value)}
          />
        </div>
        <button onClick={refresh} aria-label="重新加载分类树"
          style={{ background: B, color: W }}
          className="h-9 w-9 flex items-center justify-center rounded-sm hover:opacity-80 active:opacity-60 transition-opacity flex-shrink-0">
          <Search size={14} />
        </button>
      </div>
      {admin && (
        <button onClick={() => { setActionMessage(""); setEditModal({ mode: "addRoot" }); }}
          className="flex items-center justify-center gap-1.5 h-8 mb-2 rounded-sm text-xs font-black transition-opacity hover:opacity-80"
          style={{ background: B, color: W }}>
          <FolderPlus size={13} />添加顶级分类
        </button>
      )}
      {actionMessage && (
        <div className="mb-2 px-2 py-1.5 text-xs font-bold rounded-sm" style={{ color: N, background: "rgba(49,145,235,0.1)" }}>
          {actionMessage}
        </div>
      )}
      <div className="flex-1 overflow-y-auto">
        {!keyword && <button type="button" onClick={() => setSelectedId(null)} className="w-full flex items-center gap-2 py-2 px-2 mb-1 text-sm font-black rounded-sm" style={{ color: selectedId === null ? B : N, background: selectedId === null ? "rgba(21,101,192,.1)" : undefined }}><Layers size={13} /><span>全部分类</span><span className="ml-auto text-[10px] opacity-50">{allCount === null || allCount === undefined ? "加载中" : `(${allCount})`}</span></button>}
        {status === "loading" && <CategoryPanelState icon={<RefreshCw size={16} className="animate-spin" />} text="正在加载分类树" />}
        {status === "error" && (
          <CategoryPanelState
            icon={<AlertCircle size={16} />}
            text={`${error?.message ?? "分类树加载失败"}${errorRequestSuffix(error?.requestId ?? "")}`}
            action="重试"
            onAction={refresh}
          />
        )}
        {status === "empty" && <CategoryPanelState icon={<Layers size={16} />} text={keyword ? "未找到匹配分类" : "暂无分类"} />}
        {status === "success" && tree.map(n => (
          <CategoryTreeNode key={n.id} node={n} depth={0} expanded={expanded} onToggle={toggle}
            selectedId={selectedId} onSelect={setSelectedId}
            onAddChild={admin ? node => { setActionMessage(""); setEditModal({ mode: "addChild", target: node }); } : undefined}
            onEdit={admin ? node => { setActionMessage(""); setEditModal({ mode: "edit", target: node }); } : undefined}
            onDelete={admin ? handleDelete : undefined} />
        ))}
      </div>
      {editModal && (
        <CategoryEditModal
          title={editModal.mode === "edit" ? "编辑分类" : editModal.mode === "addChild" ? `为「${editModal.target?.categoryName}」添加子分类` : "添加顶级分类"}
          mode={editModal.mode}
          target={editModal.target}
          tree={tree}
          submitting={submitting}
          externalError={actionMessage}
          onSubmit={handleSubmitEdit}
          onClose={() => setEditModal(null)} />
      )}
    </div>
  );
}

// ─── 分类编辑 Modal（添加顶级 / 添加子分类 / 编辑） ────────────────────────────
function CategoryPanelState({ icon, text, action, onAction }: {
  icon: React.ReactNode; text: string; action?: string; onAction?: () => void;
}) {
  return (
    <div className="flex flex-col items-center justify-center gap-2 py-8 text-center text-xs font-bold" style={{ color: "rgba(26,82,153,0.55)" }}>
      {icon}<span>{text}</span>
      {action && onAction && <button onClick={onAction} className="px-3 py-1 rounded-sm" style={{ background: B, color: W }}>{action}</button>}
    </div>
  );
}

function CategoryEditModal({ title, mode, target, tree, submitting, externalError, onSubmit, onClose }: {
  title: string;
  mode: CategoryModalState["mode"];
  target?: CategoryNode;
  tree: CategoryNode[];
  submitting: boolean;
  externalError: string;
  onSubmit: (value: CategoryFormValue) => void;
  onClose: () => void;
}) {
  const [categoryCode] = useState(() => (
    mode === "edit" && target
      ? target.categoryCode
      : generateInternalCategoryCode()
  ));
  const [categoryName, setCategoryName] = useState(
    mode === "edit" ? target?.categoryName ?? "" : "",
  );
  const [categoryType] = useState<keyof typeof CategoryType>(
    mode === "edit" ? target?.categoryType ?? CategoryType.OTHER : CategoryType.OTHER,
  );
  const [parentId, setParentId] = useState<string | null>(
    mode === "addChild" ? target?.id ?? null : target?.parentId ?? null,
  );
  const [sortNo, setSortNo] = useState(String(mode === "edit" ? target?.sortNo ?? 10 : 10));
  const [formError, setFormError] = useState("");
  const parentOptions = getCategoryParentOptions(tree, mode === "edit" ? target : undefined);

  const submit = () => {
    const nextCode = categoryCode.trim();
    const nextName = categoryName.trim();
    const parsedSortNo = Number(sortNo);
    if (!nextName) {
      setFormError("分类名称不能为空");
      return;
    }
    if (!Number.isInteger(parsedSortNo) || parsedSortNo < 0) {
      setFormError("排序号必须是大于或等于0的整数");
      return;
    }
    setFormError("");
    onSubmit({
      categoryCode: nextCode,
      categoryName: nextName,
      categoryType,
      parentId,
      sortNo: parsedSortNo,
    });
  };

  return (
    <div className="fixed inset-0 z-50 flex items-center justify-center" style={{ background: "rgba(26,82,153,0.5)" }} onClick={onClose}>
      <div style={{ background: W, width: 420 }} className="rounded-sm shadow-2xl" onClick={e => e.stopPropagation()}>
        <div style={{ background: N, color: W }} className="h-10 flex items-center justify-between px-4 rounded-t-sm">
          <span className="text-sm font-black tracking-wide">{title}</span>
          <button onClick={onClose} className="opacity-70 hover:opacity-100 transition-opacity"><X size={16} /></button>
        </div>
        <div className="p-5 space-y-3">
          <CategoryFormField label="分类名称">
            <input autoFocus value={categoryName} onChange={e => setCategoryName(e.target.value)}
              className="w-full h-9 px-3 text-sm font-bold rounded-sm outline-none"
              style={{ color: N, background: `rgba(40,118,193,0.04)`, border: `1px solid rgba(40,118,193,0.3)` }} />
          </CategoryFormField>
          <CategoryFormField label="排序号">
            <input type="number" min={0} step={1} value={sortNo} onChange={e => setSortNo(e.target.value)}
              className="w-full h-9 px-3 text-sm font-bold rounded-sm outline-none" style={{ color: N, border: `1px solid rgba(40,118,193,0.3)` }} />
          </CategoryFormField>
          <CategoryFormField label="父分类">
            <select value={parentId ?? ""} disabled={mode === "addChild" || mode === "addRoot"} onChange={e => setParentId(e.target.value || null)}
              className="w-full h-9 px-2 text-sm font-bold rounded-sm outline-none disabled:opacity-60" style={{ color: N, border: `1px solid rgba(40,118,193,0.3)` }}>
              <option value="">顶级分类</option>
              {parentOptions.map(option => <option key={option.id} value={option.id} title={option.categoryPath}>{option.displayLabel}</option>)}
            </select>
          </CategoryFormField>
          {(formError || externalError) && <div className="text-xs font-bold" style={{ color: N }}>{formError || externalError}</div>}
          <div className="flex justify-end gap-2 mt-5">
            <BtnOutline onClick={onClose}>取消</BtnOutline>
            <button disabled={submitting} onClick={submit} className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-black rounded-sm disabled:opacity-60" style={{ background: B, color: W }}>
              {submitting ? <RefreshCw size={13} className="animate-spin" /> : <Check size={13} />}
              {submitting ? "提交中" : "确定"}
            </button>
          </div>
        </div>
      </div>
    </div>
  );
}

function CategoryFormField({ label, children }: { label: string; children: React.ReactNode }) {
  return (
    <label className="block">
      <span className="block text-xs font-black mb-1.5" style={{ color: `rgba(26,82,153,0.7)` }}>{label}</span>
      {children}
    </label>
  );
}

// ─── Shared left panel ────────────────────────────────────────────────────────
function LeftPanel({ children, admin, selectedCategoryId, onSelectCategory, allCount }: {
  children?: React.ReactNode;
  admin?: boolean;
  selectedCategoryId?: string | null;
  onSelectCategory?: (id: string | null) => void;
  allCount?: number | null;
}) {
  return (
    <div className="w-[272px] flex-shrink-0 m-3 flex flex-col" style={{ background: W, border: `1px solid rgba(40,118,193,0.2)` }}>
      <div className="px-4 py-3 flex items-center gap-2 border-b" style={{ borderColor: "rgba(40,118,193,0.14)", background: `rgba(40,118,193,0.05)` }}>
        <Layers size={15} style={{ color: B }} />
        <span className="text-sm font-black" style={{ color: N }}>方案分类</span>
      </div>
      <div className="flex-1 overflow-hidden flex flex-col p-3">
        <div className="flex-1 min-h-0 flex flex-col">
          <CategoryPanel
            admin={admin}
            selectedCategoryId={selectedCategoryId}
            onSelectCategory={onSelectCategory}
            allCount={allCount}
          />
        </div>
        {children}
      </div>
    </div>
  );
}

// ─── Word viewer modal ────────────────────────────────────────────────────────
function WordViewer({ doc, onClose }: { doc: Doc; onClose: () => void }) {
  return (
    <div className="fixed inset-0 z-50 flex items-center justify-center" style={{ background: "rgba(26,82,153,0.5)" }} onClick={onClose}>
      <div style={{ background: "#D6E8F6", width: 900, height: "88vh" }} className="flex flex-col" onClick={e => e.stopPropagation()}>
        {/* Title bar — government blue */}
        <div style={{ background: B, color: W }} className="h-10 flex items-center justify-between px-4 flex-shrink-0">
          <span className="text-sm font-black tracking-wide">{doc.name}.docx — 文档查看器</span>
          <button onClick={onClose} className="opacity-70 hover:opacity-100 transition-opacity"><X size={16} /></button>
        </div>
        {/* Ribbon — light blue */}
        <div style={{ background: "#EBF4FD", borderBottom: `1px solid rgba(40,118,193,0.2)` }} className="h-8 flex items-center px-4 gap-5 flex-shrink-0">
          {["文件", "开始", "插入", "视图"].map(m => (
            <span key={m} className="text-xs cursor-default px-1.5 py-0.5 rounded hover:bg-[rgba(40,118,193,0.12)] transition-colors font-bold" style={{ color: N }}>{m}</span>
          ))}
        </div>
        {/* Page canvas — light blue-gray surround */}
        <div className="flex-1 overflow-y-auto py-8 px-10" style={{ background: "#C8DCF0" }}>
          <div style={{ background: W, maxWidth: 700, margin: "0 auto", minHeight: 900, padding: "80px 72px", boxShadow: "0 4px 20px rgba(40,118,193,0.2)" }}>
            <h1 style={{ color: N, fontSize: 22, fontWeight: 900, textAlign: "center" }} className="mb-2">{doc.name}</h1>
            <div className="flex justify-center gap-6 mb-8" style={{ fontSize: 12, color: `rgba(26,82,153,0.6)` }}>
              <span>密级：<LevelBadge level={doc.level} /></span>
              <span>可见范围：{doc.visibility}</span>
              <span>更新日期：{doc.updatedAt}</span>
            </div>
            <div style={{ borderTop: `2px solid ${B}`, marginBottom: 24 }} />
            {[
              { title: "第一章　总则", body: `第一条　为切实做好应急处置工作，明确工作职责，规范工作程序，根据相关法规制度，结合实际情况，特制定本${doc.type === "子案" ? "子案" : "预案"}。\n\n${doc.summary}\n\n第二条　本${doc.type === "子案" ? "子案" : "预案"}适用范围为${doc.visibility}，涵盖各级指挥机构及保障单位，各单位须严格执行相关规定。` },
              { title: "第二章　组织机构与职责", body: "第三条　建立三级指挥体系。第一级为总指挥部，负责全面统筹协调；第二级为各专项工作组，负责具体组织实施；第三级为各基层执行单元，负责任务落实。\n\n第四条　各级机构职责明确，分工合理，既相互独立又协调配合，形成高效顺畅的工作运转机制。" },
              { title: "第三章　工作程序", body: "第五条　预警阶段。收到预警信息后，立即启动本预案，各级指挥机构进入临战状态，相关人员就位，装备器材做好准备。\n\n第六条　响应阶段。根据情况发展，按照指挥链逐级下达指令，各执行单元按分工迅速行动，及时反馈执行情况。" },
            ].map(s => (
              <div key={s.title} className="mb-6">
                <h2 style={{ color: N, fontSize: 15, fontWeight: 900 }} className="mb-3">{s.title}</h2>
                {s.body.split("\n\n").map((p, i) => (
                  <p key={i} style={{ fontSize: 13, color: N, lineHeight: 1.9, opacity: 0.85 }} className="mb-3 indent-8">{p}</p>
                ))}
              </div>
            ))}
            <div style={{ borderTop: `1px solid rgba(40,118,193,0.2)`, marginTop: 32, paddingTop: 12 }}>
              <p style={{ fontSize: 11, color: `rgba(26,82,153,0.5)`, textAlign: "right" }}>第 1 页，共 1 页 ｜ 密级：{doc.level} ｜ 严禁扩散</p>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

// ─── Upload modal ─────────────────────────────────────────────────────────────
function UploadModal({ onClose }: { onClose: () => void }) {
  const [dragging, setDragging] = useState(false);
  const [uploaded, setUploaded] = useState(false);
  const [form, setForm] = useState({ name: "", type: "主案", level: "内部", visibility: "全机关", summary: "", tags: "" });

  const inputCls = "w-full h-9 px-3 text-sm rounded-sm border outline-none transition-colors font-bold";
  const inputStyle = { borderColor: "rgba(26,82,153,0.2)", color: N };

  return (
    <div className="fixed inset-0 z-50 flex items-center justify-center" style={{ background: "rgba(26,82,153,0.5)" }} onClick={onClose}>
      <div style={{ background: W, width: 660, border: `1px solid rgba(11,37,69,0.15)` }} className="rounded-sm shadow-2xl" onClick={e => e.stopPropagation()}>
        <div style={{ background: N, color: W }} className="h-11 flex items-center justify-between px-5 rounded-t-sm">
          <span className="text-sm font-black tracking-widest">上传文档</span>
          <button onClick={onClose} className="opacity-60 hover:opacity-100 transition-opacity"><X size={16} /></button>
        </div>
        <div className="p-6 space-y-4">
          <div
            style={{ border: `2px dashed ${dragging ? B : "rgba(26,82,153,0.25)"}`, background: dragging ? `rgba(21,101,192,0.05)` : `rgba(40,118,193,0.04)` }}
            className="rounded-sm h-28 flex flex-col items-center justify-center gap-2 cursor-pointer transition-all"
            onDragOver={e => { e.preventDefault(); setDragging(true); }}
            onDragLeave={() => setDragging(false)}
            onDrop={e => { e.preventDefault(); setDragging(false); setUploaded(true); }}
            onClick={() => setUploaded(true)}
          >
            {uploaded
              ? <><Check size={22} style={{ color: B }} /><span className="text-sm font-black" style={{ color: B }}>文件已选择：综合应急预案.docx</span></>
              : <><Upload size={22} style={{ color: B }} /><span className="text-sm font-black" style={{ color: N }}>点击选择或拖拽文件至此处</span><span className="text-xs font-bold" style={{ color: `rgba(26,82,153,0.4)` }}>支持 .doc .docx .pdf .xls 格式</span></>
            }
          </div>
          <div className="grid grid-cols-2 gap-4">
            {[
              { label: "文档名称", key: "name", type: "input" },
              { label: "文档类型", key: "type", type: "select", opts: ["主案", "子案", "附件"] },
              { label: "密级", key: "level", type: "select", opts: ["内部", "秘密", "机密", "绝密"] },
              { label: "可见范围", key: "visibility", type: "select", opts: ["全机关", "作战部", "后勤部", "通信部", "情报部"] },
            ].map(f => (
              <div key={f.key}>
                <label className="text-xs font-black mb-1 block" style={{ color: N }}>{f.label}</label>
                {f.type === "input"
                  ? <input className={inputCls} style={inputStyle} value={form[f.key as keyof typeof form]} onChange={e => setForm(p => ({ ...p, [f.key]: e.target.value }))} />
                  : <select className={inputCls} style={{ ...inputStyle, background: W }} value={form[f.key as keyof typeof form]} onChange={e => setForm(p => ({ ...p, [f.key]: e.target.value }))}>
                    {f.opts!.map(o => <option key={o}>{o}</option>)}
                  </select>
                }
              </div>
            ))}
          </div>
          <div>
            <label className="text-xs font-black mb-1 block" style={{ color: N }}>文档概述</label>
            <textarea className="w-full h-20 px-3 py-2 text-sm rounded-sm border outline-none resize-none font-bold" style={inputStyle} placeholder="请输入文档内容概述…" value={form.summary} onChange={e => setForm(p => ({ ...p, summary: e.target.value }))} />
          </div>
          <div>
            <label className="text-xs font-black mb-1 block" style={{ color: N }}>标签（逗号分隔）</label>
            <input className={inputCls} style={inputStyle} placeholder="如：应急,综合,2024" value={form.tags} onChange={e => setForm(p => ({ ...p, tags: e.target.value }))} />
          </div>
          <div className="flex justify-end gap-2 pt-2">
            <BtnOutline onClick={onClose}>取消</BtnOutline>
            <BtnPrimary onClick={onClose}><Check size={14} />确认上传</BtnPrimary>
          </div>
        </div>
      </div>
    </div>
  );
}

// ─── Batch import modal (multi-file) ─────────────────────────────────────────
interface BatchFile { name: string; size: string; status: "ready" | "uploading" | "done" }

function BatchImportModal({ onClose }: { onClose: () => void }) {
  const [dragging, setDragging] = useState(false);
  const [files, setFiles] = useState<BatchFile[]>([]);

  const addFiles = (fileList: FileList | null) => {
    if (!fileList) return;
    const newFiles: BatchFile[] = Array.from(fileList).map(f => ({
      name: f.name,
      size: f.size > 1024 * 1024 ? `${(f.size / 1024 / 1024).toFixed(1)} MB` : `${(f.size / 1024).toFixed(0)} KB`,
      status: "ready",
    }));
    setFiles(prev => [...prev, ...newFiles]);
  };

  const handleDrop = (e: React.DragEvent) => {
    e.preventDefault();
    setDragging(false);
    addFiles(e.dataTransfer.files);
  };

  const removeFile = (i: number) => setFiles(prev => prev.filter((_, idx) => idx !== i));

  const handleImport = () => {
    setFiles(prev => prev.map(f => ({ ...f, status: "done" })));
    setTimeout(onClose, 1200);
  };

  return (
    <div className="fixed inset-0 z-50 flex items-center justify-center" style={{ background: "rgba(26,82,153,0.5)" }} onClick={onClose}>
      <div style={{ background: W, width: 660, border: `1px solid rgba(11,37,69,0.15)` }} className="rounded-sm shadow-2xl" onClick={e => e.stopPropagation()}>
        <div style={{ background: N, color: W }} className="h-11 flex items-center justify-between px-5 rounded-t-sm">
          <span className="text-sm font-black tracking-widest">批量导入文档</span>
          <button onClick={onClose} className="opacity-60 hover:opacity-100 transition-opacity"><X size={16} /></button>
        </div>
        <div className="p-6">
          {/* Drop zone */}
          <div
            style={{
              border: `2px dashed ${dragging ? B : "rgba(26,82,153,0.25)"}`,
              background: dragging ? `rgba(21,101,192,0.06)` : `rgba(40,118,193,0.04)`,
            }}
            className="rounded-sm h-32 flex flex-col items-center justify-center gap-2 cursor-pointer transition-all mb-4"
            onDragOver={e => { e.preventDefault(); setDragging(true); }}
            onDragLeave={() => setDragging(false)}
            onDrop={handleDrop}
            onClick={() => {
              const el = document.createElement("input");
              el.type = "file";
              el.multiple = true;
              el.accept = ".doc,.docx,.pdf,.xls,.xlsx";
              el.onchange = () => addFiles(el.files);
              el.click();
            }}
          >
            <Upload size={28} style={{ color: dragging ? B : `rgba(26,82,153,0.3)` }} />
            <span className="text-sm font-black" style={{ color: N }}>拖拽多个文件至此处，或点击选择</span>
            <span className="text-xs font-bold" style={{ color: `rgba(26,82,153,0.4)` }}>支持 .doc .docx .pdf .xls .xlsx，可同时导入多个</span>
          </div>

          {/* File list */}
          {files.length > 0 && (
            <div className="mb-4">
              <div className="flex items-center justify-between mb-2">
                <span className="text-sm font-black" style={{ color: N }}>已选文件（{files.length} 个）</span>
                <button onClick={() => setFiles([])} className="text-xs font-bold hover:opacity-60 transition-opacity" style={{ color: B }}>清空</button>
              </div>
              <div className="border rounded-sm overflow-hidden" style={{ borderColor: "rgba(11,37,69,0.15)", maxHeight: 200, overflowY: "auto" }}>
                {files.map((f, i) => (
                  <div key={i} className="flex items-center justify-between px-4 py-2.5 border-b last:border-0" style={{ borderColor: "rgba(40,118,193,0.1)", background: i % 2 === 0 ? W : `rgba(40,118,193,0.04)` }}>
                    <div className="flex items-center gap-2 flex-1 min-w-0">
                      <FileCheck size={14} style={{ color: f.status === "done" ? B : `rgba(26,82,153,0.4)` }} />
                      <span className="text-sm font-bold truncate" style={{ color: N }}>{f.name}</span>
                    </div>
                    <div className="flex items-center gap-3 flex-shrink-0 ml-4">
                      <span className="text-xs font-bold" style={{ color: `rgba(26,82,153,0.4)` }}>{f.size}</span>
                      {f.status === "done"
                        ? <Check size={14} style={{ color: B }} />
                        : <button onClick={() => removeFile(i)} className="hover:opacity-60 transition-opacity" style={{ color: N }}><X size={14} /></button>
                      }
                    </div>
                  </div>
                ))}
              </div>
            </div>
          )}

          <div className="flex justify-end gap-2">
            <BtnOutline onClick={onClose}>取消</BtnOutline>
            <BtnPrimary onClick={files.length ? handleImport : undefined}>
              <Upload size={14} />开始导入 {files.length > 0 ? `(${files.length})` : ""}
            </BtnPrimary>
          </div>
        </div>
      </div>
    </div>
  );
}

// ─── Permission modal ─────────────────────────────────────────────────────────
function PermissionOrganizationNode({ node, depth, expanded, checked, onToggleExpand, onToggleCheck }: {
  node: OrganizationNode;
  depth: number;
  expanded: Set<string>;
  checked: Set<string>;
  onToggleExpand: (id: string) => void;
  onToggleCheck: (id: string) => void;
}) {
  const hasChildren = node.children.length > 0;
  const isOpen = expanded.has(node.id);
  return (
    <div>
      <div className="flex items-center gap-1 py-1 rounded-sm hover:bg-[rgba(21,101,192,0.06)]" style={{ paddingLeft: depth * 16 + 4 }}>
        <button type="button" className="w-5 h-5 flex items-center justify-center" onClick={() => hasChildren && onToggleExpand(node.id)}>
          {hasChildren ? (isOpen ? <ChevronDown size={12} /> : <ChevronRight size={12} />) : null}
        </button>
        <label className="flex flex-1 items-center gap-2 cursor-pointer min-w-0">
          <input type="checkbox" checked={checked.has(node.id)} onChange={() => onToggleCheck(node.id)} className="accent-[#2876C1]" />
          <Building size={12} style={{ color: B }} />
          <span className="text-sm font-bold truncate" style={{ color: N }}>{node.orgName}</span>
        </label>
      </div>
      {hasChildren && isOpen && node.children.map(child => (
        <PermissionOrganizationNode key={child.id} node={child} depth={depth + 1} expanded={expanded} checked={checked}
          onToggleExpand={onToggleExpand} onToggleCheck={onToggleCheck} />
      ))}
    </div>
  );
}

function PermissionModal({ doc, onClose }: { doc: Doc; onClose: () => void }) {
  const [tab, setTab] = useState<"org" | "person">("org");
  const [orgChecked, setOrgChecked] = useState<Set<string>>(new Set());
  const [personChecked, setPersonChecked] = useState<Set<string>>(new Set());
  const [orgExpanded, setOrgExpanded] = useState<Set<string>>(new Set());
  const [orgKeyword, setOrgKeyword] = useState("");
  const [orgRetry, setOrgRetry] = useState(0);
  const [personKeyword, setPersonKeyword] = useState("");
  const [organizationId, setOrganizationId] = useState("");
  const [includeDescendants, setIncludeDescendants] = useState(false);
  const [personPage, setPersonPage] = useState(1);
  const [personRetry, setPersonRetry] = useState(0);
  const [notice, setNotice] = useState("");
  const organizationState = useOrganizationTree(orgKeyword, orgRetry);
  const userState = useUserDirectory({
    organizationId: organizationId || undefined,
    includeDescendants,
    keyword: personKeyword || undefined,
    status: EnabledStatus.ENABLED,
    page: personPage,
    pageSize: 20,
  }, personRetry);
  const organizationOptions = useMemo(
    () => flattenOrganizations(organizationState.tree),
    [organizationState.tree],
  );

  useEffect(() => {
    if (organizationState.tree[0]) {
      setOrgExpanded(current => current.size ? current : new Set([organizationState.tree[0].id]));
    }
  }, [organizationState.tree]);

  const toggleOrg = (id: string) => setOrgChecked(p => {
    const next = new Set(p);
    next.has(id) ? next.delete(id) : next.add(id);
    return next;
  });
  const togglePerson = (id: string) => setPersonChecked(p => {
    const next = new Set(p);
    next.has(id) ? next.delete(id) : next.add(id);
    return next;
  });
  const toggleOrgExpand = (id: string) => setOrgExpanded(p => {
    const next = new Set(p);
    next.has(id) ? next.delete(id) : next.add(id);
    return next;
  });

  return (
    <div className="fixed inset-0 z-50 flex items-center justify-center" style={{ background: "rgba(26,82,153,0.5)" }} onClick={onClose}>
      <div style={{ background: W, width: 540, border: `1px solid rgba(11,37,69,0.15)` }} className="rounded-sm shadow-2xl" onClick={e => e.stopPropagation()}>
        <div style={{ background: N, color: W }} className="h-11 flex items-center justify-between px-5 rounded-t-sm">
          <span className="text-sm font-black tracking-widest">权限配置 — {doc.name}</span>
          <button onClick={onClose} className="opacity-60 hover:opacity-100 transition-opacity"><X size={16} /></button>
        </div>
        <div className="flex border-b" style={{ borderColor: "rgba(40,118,193,0.14)" }}>
          {(["org", "person"] as const).map(t => (
            <button key={t} onClick={() => setTab(t)}
              style={{
                color: tab === t ? B : `rgba(26,82,153,0.5)`,
                borderBottom: tab === t ? `2px solid ${B}` : "2px solid transparent",
                background: "transparent",
              }}
              className="flex-1 h-10 text-sm font-black transition-colors">
              {t === "org" ? "组织机构权限" : "人员权限"}
            </button>
          ))}
        </div>
        <div className="p-4 h-[380px] flex flex-col min-h-0">
          {tab === "org" ? (
            <>
              <div className="flex items-center gap-2 h-9 px-3 mb-3 rounded-sm" style={{ border: `1px solid rgba(40,118,193,0.25)` }}>
                <Search size={13} style={{ color: B }} />
                <input value={orgKeyword} onChange={e => setOrgKeyword(e.target.value)} placeholder="搜索组织名称或编码"
                  className="flex-1 text-sm font-bold outline-none bg-transparent" style={{ color: N }} />
              </div>
              <div className="flex-1 overflow-y-auto">
                {organizationState.status === "loading" && <CategoryPanelState icon={<RefreshCw size={16} className="animate-spin" />} text="正在加载组织树" />}
                {organizationState.status === "empty" && <CategoryPanelState icon={<Building size={16} />} text="暂无匹配组织" />}
                {organizationState.status === "error" && (
                  <CategoryPanelState icon={<AlertCircle size={16} />}
                    text={`${organizationState.error?.httpStatus === 403 ? "权限不足：" : ""}${organizationState.error?.message ?? "组织树加载失败"}${errorRequestSuffix(organizationState.error?.requestId ?? "")}`}
                    action="重试" onAction={() => setOrgRetry(value => value + 1)} />
                )}
                {organizationState.status === "success" && organizationState.tree.map(node => (
                  <PermissionOrganizationNode key={node.id} node={node} depth={0} expanded={orgExpanded} checked={orgChecked}
                    onToggleExpand={toggleOrgExpand} onToggleCheck={toggleOrg} />
                ))}
              </div>
            </>
          ) : (
            <>
              <div className="grid grid-cols-2 gap-2 mb-2">
                <select value={organizationId} onChange={e => { setOrganizationId(e.target.value); setPersonPage(1); }}
                  className="h-9 px-2 text-sm font-bold rounded-sm outline-none" style={{ color: N, border: `1px solid rgba(40,118,193,0.25)` }}>
                  <option value="">全部组织</option>
                  {organizationOptions.map(org => <option key={org.id} value={org.id}>{org.orgPath}</option>)}
                </select>
                <div className="flex items-center gap-2 h-9 px-3 rounded-sm" style={{ border: `1px solid rgba(40,118,193,0.25)` }}>
                  <Search size={13} style={{ color: B }} />
                  <input value={personKeyword} onChange={e => { setPersonKeyword(e.target.value); setPersonPage(1); }}
                    placeholder="搜索用户名或姓名" className="flex-1 min-w-0 text-sm font-bold outline-none bg-transparent" style={{ color: N }} />
                </div>
              </div>
              <label className="flex items-center gap-2 mb-2 text-xs font-bold" style={{ color: N }}>
                <input type="checkbox" checked={includeDescendants} onChange={e => { setIncludeDescendants(e.target.checked); setPersonPage(1); }} className="accent-[#2876C1]" />
                包含下级组织
                <span className="ml-auto opacity-60">共 {userState.data.total} 人</span>
              </label>
              <div className="flex-1 overflow-y-auto">
                {userState.status === "loading" && <CategoryPanelState icon={<RefreshCw size={16} className="animate-spin" />} text="正在加载人员" />}
                {userState.status === "empty" && <CategoryPanelState icon={<User size={16} />} text="暂无匹配人员" />}
                {userState.status === "error" && (
                  <CategoryPanelState icon={<AlertCircle size={16} />}
                    text={`${userState.error?.httpStatus === 403 ? "权限不足：" : ""}${userState.error?.message ?? "人员列表加载失败"}${errorRequestSuffix(userState.error?.requestId ?? "")}`}
                    action="重试" onAction={() => setPersonRetry(value => value + 1)} />
                )}
                {userState.status === "success" && (
                  <div className="grid grid-cols-2 gap-1">
                    {userState.data.items.map(person => (
                      <label key={person.id} className="flex items-center gap-2 py-2 px-3 rounded-sm hover:bg-[rgba(21,101,192,0.06)] cursor-pointer">
                        <input type="checkbox" checked={personChecked.has(person.id)} onChange={() => togglePerson(person.id)} className="accent-[#2876C1]" />
                        <User size={12} style={{ color: B }} />
                        <span className="text-sm font-bold truncate" style={{ color: N }}>{person.realName} / {person.username}（{person.organizationName}）</span>
                      </label>
                    ))}
                  </div>
                )}
              </div>
              <div className="flex items-center justify-center gap-3 pt-2 text-xs font-bold" style={{ color: N }}>
                <button disabled={personPage <= 1 || userState.status === "loading"} onClick={() => setPersonPage(page => Math.max(1, page - 1))} className="px-2 py-1 disabled:opacity-30">上一页</button>
                <span>第 {userState.data.page} / {Math.max(userState.data.totalPages, 1)} 页</span>
                <button disabled={personPage >= userState.data.totalPages || userState.status === "loading"} onClick={() => setPersonPage(page => page + 1)} className="px-2 py-1 disabled:opacity-30">下一页</button>
              </div>
            </>
          )}
        </div>
        {notice && <div className="mx-5 mb-3 px-3 py-2 text-xs font-bold rounded-sm" style={{ color: N, background: "rgba(49,145,235,0.1)" }}>{notice}</div>}
        <div className="flex justify-end gap-2 px-5 pb-5">
          <BtnOutline onClick={() => { setOrgChecked(new Set()); setPersonChecked(new Set()); }} danger>
            <RefreshCw size={13} />重置
          </BtnOutline>
          <BtnPrimary onClick={() => setNotice("")}><Check size={13} />保存权限</BtnPrimary>
        </div>
      </div>
    </div>
  );
}

// ─── Page frame (shared header + background) ──────────────────────────────────
function PageFrame({ title, current, navigationItems, onNav, onLogout, logoutPending, currentUser, children }: {
  title: string;
  current: AuthenticatedPage;
  navigationItems: NavigationItem[];
  onNav: (p: AuthenticatedPage) => void;
  onLogout: () => void;
  logoutPending: boolean;
  currentUser: UserSummary;
  children: React.ReactNode;
}) {
  return (
    <div className="w-full h-screen flex flex-col overflow-hidden" style={{ fontFamily: "'Noto Sans SC', sans-serif", background: PBG }}>
      {/* Header — auto height from padding, fits menu naturally */}
      <div className="relative z-30 flex-shrink-0 flex items-center justify-between px-8 py-3" style={{ background: B }}>
        {/* Left spacer — mirrors nav width for true centering */}
        <div className="min-w-[250px] leading-tight" style={{ color: W }}>
          <div className="text-xs font-bold opacity-75 truncate">{currentUser.organizationName || "未设置单位"}</div>
          <div className="text-sm font-black truncate mt-1">{currentUser.realName}</div>
        </div>
        {/* Center: title */}
        <h1 className="absolute left-1/2 -translate-x-1/2 font-black tracking-[0.18em] whitespace-nowrap"
          style={{ color: W, fontSize: 22 }}>
          {title}
        </h1>
        {/* Right: nav — all items horizontal, never wrap */}
        <div className="flex items-center gap-0.5">
          {navigationItems.map(item => {
            const isActive = current === item.page;
            return (
              <button key={item.module} onClick={() => onNav(item.page)} className="whitespace-nowrap px-3 py-1.5 text-sm font-black rounded-sm transition-all hover:bg-white/20 active:bg-white/30"
                style={{ color: isActive ? W : `rgba(255,255,255,0.75)`, background: isActive ? `rgba(255,255,255,0.18)` : "transparent", textDecoration: isActive ? "underline" : "none", textUnderlineOffset: 3 }}>
                {item.label}
              </button>
            );
          })}
          <div className="w-px h-4 mx-1.5" style={{ background: `rgba(255,255,255,0.25)` }} />
          <button onClick={onLogout} disabled={logoutPending} className="flex items-center gap-1 whitespace-nowrap text-sm font-black px-2.5 py-1.5 rounded-sm hover:bg-white/20 active:bg-white/30 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
            style={{ color: `rgba(255,255,255,0.75)` }}>
            <LogOut size={13} />{logoutPending ? "退出中" : "退出"}
          </button>
        </div>
      </div>

      {/* Header → 主体 渐变过渡带（20px）：软化 header 下沿的硬分割线 */}
      <div aria-hidden className="relative z-20 flex-shrink-0"
        style={{ height: 20, background: `linear-gradient(to bottom, ${B} 0%, ${PBG} 100%)` }} />

      {/* Main content — sits below header */}
      <div className="relative z-40 flex flex-1 min-h-0">
        {children}
      </div>
    </div>
  );
}

// ─── P2: Main page ────────────────────────────────────────────────────────────
function DocTableRow({ doc, index, keyword, selected, onSelect, onView, onDownload }: {
  doc: Doc; index: string; keyword: string; selected: boolean;
  onSelect: (d: Doc) => void; onView: (d: Doc) => void; onDownload: (d: Doc) => void;
}) {
  return (
    <tr
      className="border-b transition-colors cursor-pointer"
      style={{
        borderColor: "rgba(40,118,193,0.1)",
        background: selected ? `rgba(21,101,192,0.10)` : W,
      }}
      onClick={() => onSelect(doc)}
      onMouseEnter={e => { if (!selected) e.currentTarget.style.background = `rgba(21,101,192,0.05)`; }}
      onMouseLeave={e => { if (!selected) e.currentTarget.style.background = W; }}>
      <td className="px-4 py-3 text-sm font-black whitespace-nowrap" style={{ color: B, width: 64, background: "inherit" }}>
        {index}
      </td>
      <td className="px-4 py-3" style={{ background: "inherit" }}>
        <div className="flex items-center gap-2">
          <FileText size={14} style={{ color: B }} className="flex-shrink-0" />
          <span className="text-sm font-black" style={{ color: N }}><HighlightText text={doc.name} keyword={keyword} /></span>
        </div>
      </td>
      <td className="px-4 py-3 max-w-[320px]" style={{ background: "inherit" }}>
        <p className="text-sm leading-relaxed line-clamp-2" style={{ color: `rgba(26,82,153,0.65)` }}>
          <HighlightText text={doc.summary} keyword={keyword} />
        </p>
      </td>
      <td className="px-4 py-3 whitespace-nowrap" style={{ background: "inherit" }}><LevelBadge level={doc.level} /></td>
      <td className="px-4 py-3" style={{ background: "inherit" }}>
        <div className="flex flex-wrap gap-1">
          {doc.tags.map(t => (
            <span key={t} style={{ color: B, background: `rgba(21,101,192,0.1)`, border: `1px solid rgba(21,101,192,0.2)` }} className="px-2 py-0.5 text-xs font-black rounded-sm">{t}</span>
          ))}
        </div>
      </td>
      <td className="px-4 py-3 whitespace-nowrap" style={{ background: "inherit" }}>
        <div className="flex items-center gap-2" onClick={e => e.stopPropagation()}>
          <BtnPrimary sm onClick={() => onView(doc)}><Eye size={12} />查看详情</BtnPrimary>
          <BtnOutline sm onClick={() => onDownload(doc)}><Download size={12} />下载</BtnOutline>
        </div>
      </td>
    </tr>
  );
}

// ─── Sub doc table (子方案 / 配套资料 共用) ───────────────────────────────────
function SubDocTable({ docs, emptyText }: { docs: Doc[]; emptyText: string }) {
  if (docs.length === 0) {
    return (
      <div className="flex flex-col items-center justify-center h-full py-16" style={{ color: `rgba(26,82,153,0.25)` }}>
        <FileText size={40} className="mb-3" />
        <span className="text-sm font-black">{emptyText}</span>
      </div>
    );
  }
  return (
    <table className="w-full border-collapse">
      <thead className="sticky top-0 z-10">
        <tr style={{ background: N }}>
          {["序号", "名称", "概述", "级别", "标签"].map(h => (
            <th key={h} className="px-4 py-2.5 text-left text-xs font-black whitespace-nowrap" style={{ color: W }}>{h}</th>
          ))}
        </tr>
      </thead>
      <tbody>
        {docs.map((d, i) => (
          <tr key={d.id} className="border-b transition-colors"
            style={{ borderColor: "rgba(40,118,193,0.08)", background: i % 2 === 0 ? W : `rgba(40,118,193,0.03)` }}
            onMouseEnter={e => (e.currentTarget.style.background = `rgba(21,101,192,0.07)`)}
            onMouseLeave={e => (e.currentTarget.style.background = i % 2 === 0 ? W : `rgba(40,118,193,0.03)`)}>
            <td className="px-4 py-2.5 text-sm font-black whitespace-nowrap" style={{ color: B }}>{i + 1}</td>
            <td className="px-4 py-2.5">
              <div className="flex items-center gap-2">
                <FileText size={13} style={{ color: B }} className="flex-shrink-0" />
                <span className="text-sm font-black" style={{ color: N }}>{d.name}</span>
              </div>
            </td>
            <td className="px-4 py-2.5 max-w-[360px]">
              <p className="text-xs leading-relaxed line-clamp-2" style={{ color: `rgba(26,82,153,0.6)` }}>{d.summary}</p>
            </td>
            <td className="px-4 py-2.5 whitespace-nowrap"><LevelBadge level={d.level} /></td>
            <td className="px-4 py-2.5">
              <div className="flex flex-wrap gap-1">
                {d.tags.map(t => (
                  <span key={t} style={{ color: B, background: `rgba(21,101,192,0.1)`, border: `1px solid rgba(21,101,192,0.2)` }} className="px-1.5 py-0.5 text-[10px] font-black rounded-sm">{t}</span>
                ))}
              </div>
            </td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

interface AuthenticatedPageProps {
  onNav: (p: AuthenticatedPage) => void;
  navigationItems: NavigationItem[];
  onLogout: () => void;
  logoutPending: boolean;
  currentUser: UserSummary;
}

function MainPage({ onNav, navigationItems, onLogout, logoutPending, currentUser }: AuthenticatedPageProps) {
  const [tab, setTab] = useState<MainTab>("主案列表");
  const [searchMode, setSearchMode] = useState(false);
  const [keyword, setKeyword] = useState("");
  const [viewDoc, setViewDoc] = useState<Doc | null>(null);
  const [toast, setToast] = useState("");
  const [selectedDoc, setSelectedDoc] = useState<Doc | null>(null);
  const [subTab, setSubTab] = useState<"子方案" | "附件资料">("子方案");

  const showToast = (msg: string) => { setToast(msg); setTimeout(() => setToast(""), 2500); };
  const handleSearch = (q: string) => {
    setKeyword(q);
    setSearchMode(!!q.trim());
    setSelectedDoc(null); // 切换搜索时清空选中，避免与列表不一致
  };

  const tabDocs = useMemo(() => {
    let base = DOCS;
    if (searchMode && keyword.trim()) {
      const k = keyword.toLowerCase();
      // 主案或其子方案/配套资料任一命中，则该主案进入结果
      base = DOCS.filter(d => {
        const self = d.name.toLowerCase().includes(k) || d.summary.toLowerCase().includes(k);
        const inKids = d.children?.some(c => c.name.toLowerCase().includes(k) || c.summary.toLowerCase().includes(k));
        const inMats = SHARED_ATTACHMENTS
          .filter(a => (INITIAL_ATTACHMENT_BINDINGS[d.id] ?? []).includes(a.id))
          .some(m => m.name.toLowerCase().includes(k) || m.summary.toLowerCase().includes(k));
        return self || inKids || inMats;
      });
    }
    if (!searchMode) {
      if (tab === "主案列表") return base.filter(d => d.type === "主案");
      if (tab === "最近更新") return [...base].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
    }
    return base;
  }, [tab, searchMode, keyword]);

  const TABS: MainTab[] = ["主案列表", "全部方案", "最近更新"];

  // 下方 Tab 当前应展示的列表
  const subDocs: Doc[] = selectedDoc
    ? (subTab === "子方案"
      ? (selectedDoc.children ?? [])
      : SHARED_ATTACHMENTS.filter(a => (INITIAL_ATTACHMENT_BINDINGS[selectedDoc.id] ?? []).includes(a.id)))
    : [];

  return (
    <PageFrame title="方案管理系统" current="main" navigationItems={navigationItems} onNav={onNav} onLogout={onLogout} logoutPending={logoutPending} currentUser={currentUser}>
      <LeftPanel />
      <div className="flex-1 flex flex-col m-3 ml-0 min-h-0">
        {/* 顶部 Tab 栏 */}
        <div className="flex items-center flex-shrink-0 mb-2" style={{ background: W, border: `1px solid rgba(40,118,193,0.18)` }}>
          {TABS.map(t => {
            const active = tab === t && !searchMode;
            return (
              <button key={t} onClick={() => { setTab(t); setSearchMode(false); setKeyword(""); }}
                style={{
                  color: active ? W : `rgba(26,82,153,0.55)`,
                  background: active ? N : "transparent",
                  borderRight: `1px solid rgba(40,118,193,0.14)`,
                }}
                className="px-7 py-3 text-sm font-black transition-all hover:bg-[rgba(40,118,193,0.07)] last:border-r-0">
                {t}
              </button>
            );
          })}
        {/* 右侧检索框：与左侧方案分类搜索共用 keyword，覆盖主案/子方案/配套资料 */}
          <div className="flex items-center gap-2 ml-auto mr-2 my-1.5 px-3 py-1.5 rounded-sm" style={{ background: `rgba(40,118,193,0.05)`, border: `1px solid rgba(40,118,193,0.25)` }}>
            <Search size={14} style={{ color: B }} className="flex-shrink-0" />
            <input
              type="text"
              value={keyword}
              onChange={e => handleSearch(e.target.value)}
              placeholder="检索方案 / 子方案 / 共享附件"
              className="text-sm font-bold outline-none bg-transparent w-60 placeholder:font-bold"
              style={{ color: N }}
            />
            {keyword && (
              <button onClick={() => handleSearch("")} style={{ color: `rgba(26,82,153,0.4)` }} className="flex-shrink-0 hover:opacity-60 transition-opacity" aria-label="清除检索">
                <X size={14} />
              </button>
            )}
          </div>
        </div>

        {/* 主案表格 */}
        <div className="flex-1 overflow-auto min-h-0 mb-2" style={{ background: W, border: `1px solid rgba(40,118,193,0.18)` }}>
          <table className="w-full border-collapse">
            <thead className="sticky top-0 z-10">
              <tr style={{ background: N }}>
                {["序号", "文档名称", searchMode ? "方案内容" : "文档概述", "级别", "标签", "操作"].map(h => (
                  <th key={h} className="px-4 py-3 text-left text-sm font-black whitespace-nowrap" style={{ color: W }}>{h}</th>
                ))}
              </tr>
            </thead>
            <tbody>
              {tabDocs.map((doc, i) => (
                <DocTableRow key={doc.id} doc={doc} index={String(i + 1)} keyword={keyword}
                  selected={!!selectedDoc && selectedDoc.id === doc.id}
                  onSelect={setSelectedDoc}
                  onView={setViewDoc}
                  onDownload={d => showToast(`正在下载：${d.name}.docx`)} />
              ))}
            </tbody>
          </table>
          {tabDocs.length === 0 && (
            <div className="flex flex-col items-center justify-center py-20" style={{ color: `rgba(26,82,153,0.25)` }}>
              <FileText size={44} className="mb-3" />
              <span className="text-base font-black">未找到相关文档</span>
            </div>
          )}
        </div>

        {/* 下方：子方案 / 附件资料 双 Tab 区域（固定高度） */}
        <div className="flex-shrink-0 flex flex-col" style={{ height: 280, background: W, border: `1px solid rgba(40,118,193,0.18)` }}>
          {/* Tab header */}
          <div className="flex items-center border-b flex-shrink-0" style={{ borderColor: "rgba(40,118,193,0.14)" }}>
            {(["子方案", "附件资料"] as const).map(t => {
              const active = subTab === t;
              const count = selectedDoc
                ? (t === "子方案"
                  ? (selectedDoc.children?.length ?? 0)
                  : (INITIAL_ATTACHMENT_BINDINGS[selectedDoc.id] ?? []).length)
                : 0;
              return (
                <button key={t} onClick={() => setSubTab(t)}
                  style={{
                    color: active ? W : `rgba(26,82,153,0.55)`,
                    background: active ? N : "transparent",
                    borderRight: `1px solid rgba(40,118,193,0.14)`,
                  }}
                  className="px-7 py-2.5 text-sm font-black transition-all hover:bg-[rgba(40,118,193,0.07)] last:border-r-0">
                  {t}{selectedDoc && <span className="ml-1 opacity-70">({count})</span>}
                </button>
              );
            })}
            <div className="ml-auto px-5 text-xs font-bold truncate" style={{ color: `rgba(26,82,153,0.55)` }}>
              {selectedDoc ? `当前主案：${selectedDoc.name}` : "请选择上方主案以查看详情"}
            </div>
          </div>
          {/* Tab body */}
          <div className="flex-1 overflow-auto">
            {!selectedDoc ? (
              <div className="flex flex-col items-center justify-center h-full" style={{ color: `rgba(26,82,153,0.25)` }}>
                <FileText size={40} className="mb-3" />
                <span className="text-sm font-black">请选择一个主案以查看其{subTab}</span>
              </div>
            ) : (
              <SubDocTable docs={subDocs} emptyText={`暂无${subTab}`} />
            )}
          </div>
        </div>
      </div>
      {toast && (
        <div className="fixed bottom-6 right-6 z-50 flex items-center gap-2 px-5 py-3 rounded-sm shadow-lg text-sm font-black" style={{ background: N, color: W }}>
          <Download size={14} />{toast}
        </div>
      )}
      {viewDoc && <WordViewer doc={viewDoc} onClose={() => setViewDoc(null)} />}
    </PageFrame>
  );
}

// ─── P3: Admin page ───────────────────────────────────────────────────────────
function AdminDocRow({ doc, index, selected, childCount, attachmentCount, onSelect, onView, onPerm, onDelete }: {
  doc: Doc; index: string; selected: boolean;
  childCount: number; attachmentCount: number;
  onSelect: (d: Doc) => void; onView: (d: Doc) => void; onPerm: (d: Doc) => void; onDelete: (d: Doc) => void;
}) {
  return (
    <tr className="border-b transition-colors cursor-pointer"
      style={{
        borderColor: "rgba(40,118,193,0.1)",
        background: selected ? `rgba(21,101,192,0.10)` : W,
      }}
      onClick={() => onSelect(doc)}
      onMouseEnter={e => { if (!selected) e.currentTarget.style.background = `rgba(21,101,192,0.05)`; }}
      onMouseLeave={e => { if (!selected) e.currentTarget.style.background = W; }}>
      <td className="px-4 py-3 text-sm font-black whitespace-nowrap" style={{ color: B, width: 64, background: "inherit" }}>{index}</td>
      <td className="px-4 py-3" style={{ background: "inherit" }}>
        <div className="flex items-center gap-2"><FileText size={14} style={{ color: B }} className="flex-shrink-0" /><span className="text-sm font-black" style={{ color: N }}>{doc.name}</span></div>
      </td>
      <td className="px-4 py-3" style={{ background: "inherit" }}>
        <span style={{ color: doc.type === "主案" ? N : B, background: doc.type === "主案" ? `rgba(40,118,193,0.1)` : `rgba(21,101,192,0.1)`, border: `1px solid ${doc.type === "主案" ? "rgba(26,82,153,0.2)" : "rgba(21,101,192,0.25)"}` }} className="text-xs font-black px-2 py-0.5 rounded-sm">{doc.type}</span>
      </td>
      <td className="px-4 py-3 whitespace-nowrap" style={{ background: "inherit" }}><LevelBadge level={doc.level} /></td>
      <td className="px-4 py-3 text-sm font-bold" style={{ color: `rgba(26,82,153,0.6)`, background: "inherit" }}>{doc.visibility}</td>
      <td className="px-4 py-3" style={{ background: "inherit" }}>
        <span className="inline-flex min-w-7 justify-center px-2 py-0.5 text-xs font-black rounded-sm" style={{ color: B, background: `rgba(21,101,192,0.1)` }}>{childCount}</span>
      </td>
      <td className="px-4 py-3" style={{ background: "inherit" }}>
        <span className="inline-flex min-w-7 justify-center px-2 py-0.5 text-xs font-black rounded-sm" style={{ color: B, background: `rgba(21,101,192,0.1)` }}>{attachmentCount}</span>
      </td>
      <td className="px-4 py-3 whitespace-nowrap" style={{ background: "inherit" }}>
        <div className="flex items-center gap-1.5" onClick={e => e.stopPropagation()}>
          <BtnPrimary sm onClick={() => onView(doc)}><Eye size={12} />查看</BtnPrimary>
          <BtnOutline sm onClick={() => { }}><Edit2 size={12} />编辑</BtnOutline>
          <BtnOutline sm onClick={() => onPerm(doc)}><Key size={12} />权限</BtnOutline>
          <BtnOutline sm danger onClick={() => onDelete(doc)}><Trash2 size={12} />删除</BtnOutline>
        </div>
      </td>
    </tr>
  );
}

// ─── Admin sub doc table ─────────────────────────────────────────────────────
function AdminSubDocTable({ docs, emptyText, onView, onPerm, onDelete }: {
  docs: Doc[]; emptyText: string;
  onView: (d: Doc) => void; onPerm: (d: Doc) => void; onDelete: (d: Doc) => void;
}) {
  if (docs.length === 0) {
    return (
      <div className="flex flex-col items-center justify-center h-full py-16" style={{ color: `rgba(26,82,153,0.25)` }}>
        <FileText size={40} className="mb-3" />
        <span className="text-sm font-black">{emptyText}</span>
      </div>
    );
  }
  return (
    <table className="w-full border-collapse">
      <thead className="sticky top-0 z-10">
        <tr style={{ background: N }}>
          {["序号", "名称", "级别", "标签", "操作"].map(h => (
            <th key={h} className="px-4 py-2.5 text-left text-xs font-black whitespace-nowrap" style={{ color: W }}>{h}</th>
          ))}
        </tr>
      </thead>
      <tbody>
        {docs.map((d, i) => (
          <tr key={d.id} className="border-b transition-colors"
            style={{ borderColor: "rgba(40,118,193,0.08)", background: i % 2 === 0 ? W : `rgba(40,118,193,0.03)` }}
            onMouseEnter={e => (e.currentTarget.style.background = `rgba(21,101,192,0.07)`)}
            onMouseLeave={e => (e.currentTarget.style.background = i % 2 === 0 ? W : `rgba(40,118,193,0.03)`)}>
            <td className="px-4 py-2.5 text-sm font-black whitespace-nowrap" style={{ color: B }}>{i + 1}</td>
            <td className="px-4 py-2.5">
              <div className="flex items-center gap-2">
                <FileText size={13} style={{ color: B }} className="flex-shrink-0" />
                <span className="text-sm font-black" style={{ color: N }}>{d.name}</span>
              </div>
            </td>
            <td className="px-4 py-2.5 whitespace-nowrap"><LevelBadge level={d.level} /></td>
            <td className="px-4 py-2.5">
              <div className="flex flex-wrap gap-1">
                {d.tags.map(t => (
                  <span key={t} style={{ color: B, background: `rgba(21,101,192,0.1)`, border: `1px solid rgba(21,101,192,0.2)` }} className="px-1.5 py-0.5 text-[10px] font-black rounded-sm">{t}</span>
                ))}
              </div>
            </td>
            <td className="px-4 py-2.5 whitespace-nowrap">
              <div className="flex items-center gap-1.5">
                <BtnPrimary sm onClick={() => onView(d)}><Eye size={12} />查看</BtnPrimary>
                <BtnOutline sm onClick={() => { }}><Edit2 size={12} />编辑</BtnOutline>
                <BtnOutline sm onClick={() => onPerm(d)}><Key size={12} />权限</BtnOutline>
                <BtnOutline sm danger onClick={() => onDelete(d)}><Trash2 size={12} />删除</BtnOutline>
              </div>
            </td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

function AttachmentTypePanel({ selected, onSelect }: { selected: string; onSelect: (type: string) => void }) {
  const types = [
    { name: "全部附件", count: SHARED_ATTACHMENTS.length },
    ...["政策法规", "工作规范", "表格资料", "图示资料", "其他资料"].map(name => ({
      name,
      count: SHARED_ATTACHMENTS.filter(a => a.attachmentType === name).length,
    })),
  ];
  return (
    <div className="w-[272px] flex-shrink-0 m-3 flex flex-col" style={{ background: W, border: `1px solid rgba(40,118,193,0.2)` }}>
      <div className="px-4 py-3 flex items-center gap-2 border-b" style={{ borderColor: "rgba(40,118,193,0.14)", background: `rgba(40,118,193,0.05)` }}>
        <Library size={15} style={{ color: B }} />
        <span className="text-sm font-black" style={{ color: N }}>附件类型</span>
      </div>
      <div className="p-3 border-b" style={{ borderColor: "rgba(40,118,193,0.1)" }}>
        <div className="flex items-center gap-2 rounded-sm border px-3 h-9 bg-white" style={{ borderColor: "rgba(26,82,153,0.2)" }}>
          <Search size={14} style={{ color: B }} />
          <input className="flex-1 text-sm bg-transparent outline-none font-bold" style={{ color: N }} placeholder="搜索附件类型" />
        </div>
      </div>
      <div className="p-3">
        {types.map(t => (
          <button key={t.name} onClick={() => onSelect(t.name)}
            className="w-full flex items-center justify-between px-3 py-2.5 mb-0.5 rounded-sm text-sm font-black transition-colors"
            style={{ color: selected === t.name ? W : N, background: selected === t.name ? N : "transparent" }}>
            <span className="flex items-center gap-2"><Paperclip size={13} />{t.name}</span>
            <span className="text-xs opacity-70">{t.count}</span>
          </button>
        ))}
      </div>
    </div>
  );
}

function MountedAttachmentTable({ attachments, bindingCounts, onView, onUnmount }: {
  attachments: SharedAttachment[];
  bindingCounts: Record<number, number>;
  onView: (d: Doc) => void;
  onUnmount: (d: SharedAttachment) => void;
}) {
  if (!attachments.length) {
    return (
      <div className="flex flex-col items-center justify-center h-full" style={{ color: `rgba(26,82,153,0.25)` }}>
        <Paperclip size={38} className="mb-2" />
        <span className="text-sm font-black">当前主案尚未挂载共享附件</span>
      </div>
    );
  }
  return (
    <table className="w-full border-collapse">
      <thead className="sticky top-0 z-10">
        <tr style={{ background: N }}>
          {["序号", "附件名称", "附件类型", "格式", "标签", "已挂载主案", "操作"].map(h => (
            <th key={h} className="px-4 py-2.5 text-left text-xs font-black whitespace-nowrap" style={{ color: W }}>{h}</th>
          ))}
        </tr>
      </thead>
      <tbody>
        {attachments.map((a, i) => (
          <tr key={a.id} className="border-b" style={{ borderColor: "rgba(40,118,193,0.08)", background: i % 2 ? `rgba(40,118,193,0.03)` : W }}>
            <td className="px-4 py-2.5 text-sm font-black" style={{ color: B }}>{i + 1}</td>
            <td className="px-4 py-2.5 text-sm font-black" style={{ color: N }}><span className="flex items-center gap-2"><Paperclip size={13} style={{ color: B }} />{a.name}</span></td>
            <td className="px-4 py-2.5 text-sm font-bold" style={{ color: N }}>{a.attachmentType}</td>
            <td className="px-4 py-2.5 text-sm font-bold" style={{ color: `rgba(26,82,153,0.6)` }}>{a.fileFormat}</td>
            <td className="px-4 py-2.5"><div className="flex gap-1">{a.tags.map(t => <span key={t} className="px-1.5 py-0.5 text-[10px] font-black" style={{ color: B, background: `rgba(21,101,192,0.1)` }}>{t}</span>)}</div></td>
            <td className="px-4 py-2.5 text-sm font-black" style={{ color: B }}>{bindingCounts[a.id] ?? 0} 个</td>
            <td className="px-4 py-2.5 whitespace-nowrap">
              <BtnPrimary sm onClick={() => onView(a)}><Eye size={12} />查看</BtnPrimary>
              <span className="ml-1.5"><BtnOutline sm onClick={() => onUnmount(a)}><Unlink size={12} />解除挂载</BtnOutline></span>
            </td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

function SharedAttachmentLibrary({ attachments, bindings, typeFilter, keyword, onKeywordChange, onView, onDelete }: {
  attachments: SharedAttachment[];
  bindings: Record<number, number[]>;
  typeFilter: string;
  keyword: string;
  onKeywordChange: (value: string) => void;
  onView: (d: Doc) => void;
  onDelete: (d: SharedAttachment, bindingCount: number) => void;
}) {
  const bindingCounts = Object.values(bindings).flat().reduce<Record<number, number>>((acc, id) => {
    acc[id] = (acc[id] ?? 0) + 1;
    return acc;
  }, {});
  const filtered = attachments.filter(a => {
    const matchesType = typeFilter === "全部附件" || a.attachmentType === typeFilter;
    const k = keyword.trim().toLowerCase();
    const matchesKeyword = !k || [a.name, a.summary, ...a.tags].some(v => v.toLowerCase().includes(k));
    return matchesType && matchesKeyword;
  });
  return (
    <div className="flex-1 flex flex-col min-h-0" style={{ background: W, border: `1px solid rgba(40,118,193,0.18)` }}>
      <div className="px-4 py-2 text-xs font-bold border-b" style={{ color: `rgba(26,82,153,0.58)`, background: `rgba(40,118,193,0.04)`, borderColor: "rgba(40,118,193,0.12)" }}>
        附件对所有已登录用户共享；删除已挂载附件时，系统需要提示其影响的主案范围。
      </div>
      <div className="px-4 py-3 flex items-center justify-between border-b" style={{ borderColor: "rgba(40,118,193,0.12)" }}>
        <div className="flex items-center gap-2">
          <div className="flex items-center gap-2 rounded-sm border px-3 h-9 bg-white w-72" style={{ borderColor: "rgba(26,82,153,0.2)" }}>
            <Search size={14} style={{ color: B }} />
            <input value={keyword} onChange={e => onKeywordChange(e.target.value)} className="flex-1 text-sm bg-transparent outline-none font-bold" style={{ color: N }} placeholder="搜索附件名称、标签、内容" />
          </div>
          <span className="text-xs font-bold" style={{ color: `rgba(26,82,153,0.45)` }}>共 {filtered.length} 个附件</span>
        </div>
      </div>
      <div className="flex-1 overflow-auto">
        <table className="w-full border-collapse">
          <thead className="sticky top-0 z-10"><tr style={{ background: N }}>
            {["序号", "附件名称", "附件类型", "格式", "标签", "上传人", "更新时间", "挂载主案", "操作"].map(h => <th key={h} className="px-3 py-3 text-left text-xs font-black whitespace-nowrap" style={{ color: W }}>{h}</th>)}
          </tr></thead>
          <tbody>{filtered.map((a, i) => (
            <tr key={a.id} className="border-b" style={{ borderColor: "rgba(40,118,193,0.08)", background: i % 2 ? `rgba(40,118,193,0.03)` : W }}>
              <td className="px-3 py-2.5 text-sm font-black" style={{ color: B }}>{i + 1}</td>
              <td className="px-3 py-2.5 text-sm font-black" style={{ color: N }}><span className="flex items-center gap-2"><Paperclip size={13} style={{ color: B }} />{a.name}</span></td>
              <td className="px-3 py-2.5 text-sm font-bold">{a.attachmentType}</td>
              <td className="px-3 py-2.5 text-sm font-bold" style={{ color: `rgba(26,82,153,0.6)` }}>{a.fileFormat}</td>
              <td className="px-3 py-2.5"><div className="flex gap-1">{a.tags.map(t => <span key={t} className="text-[10px] font-black px-1.5 py-0.5" style={{ color: B, background: `rgba(21,101,192,0.1)` }}>{t}</span>)}</div></td>
              <td className="px-3 py-2.5 text-sm font-bold">{a.uploader}</td>
              <td className="px-3 py-2.5 text-sm font-bold" style={{ color: `rgba(26,82,153,0.5)` }}>{a.updatedAt}</td>
              <td className="px-3 py-2.5 text-sm font-black" style={{ color: B }}>{bindingCounts[a.id] ?? 0} 个</td>
              <td className="px-3 py-2.5 whitespace-nowrap">
                <BtnPrimary sm onClick={() => onView(a)}><Eye size={12} />查看</BtnPrimary>
                <span className="ml-1"><BtnOutline sm onClick={() => { }}><Edit2 size={12} />编辑</BtnOutline></span>
                <span className="ml-1"><BtnOutline sm onClick={() => { }}><Link2 size={12} />挂载关系</BtnOutline></span>
                <span className="ml-1"><BtnOutline sm danger onClick={() => onDelete(a, bindingCounts[a.id] ?? 0)}><Trash2 size={12} />删除</BtnOutline></span>
              </td>
            </tr>
          ))}</tbody>
        </table>
      </div>
    </div>
  );
}

function MountAttachmentModal({ plan, attachments, mountedIds, onConfirm, onClose }: {
  plan: Doc;
  attachments: SharedAttachment[];
  mountedIds: number[];
  onConfirm: (ids: number[]) => void;
  onClose: () => void;
}) {
  const [keyword, setKeyword] = useState("");
  const [selected, setSelected] = useState<Set<number>>(new Set());
  const filtered = attachments.filter(a => !keyword.trim() || [a.name, a.summary, ...a.tags].some(v => v.toLowerCase().includes(keyword.trim().toLowerCase())));
  const toggle = (id: number) => setSelected(prev => {
    const next = new Set(prev);
    next.has(id) ? next.delete(id) : next.add(id);
    return next;
  });
  return (
    <div className="fixed inset-0 z-50 flex items-center justify-center" style={{ background: "rgba(26,82,153,0.5)" }} onClick={onClose}>
      <div className="w-[780px] max-h-[82vh] flex flex-col rounded-sm shadow-2xl" style={{ background: W }} onClick={e => e.stopPropagation()}>
        <div className="h-11 flex items-center justify-between px-5 flex-shrink-0" style={{ background: N, color: W }}>
          <span className="text-sm font-black">从共享附件库挂载到「{plan.name}」</span>
          <button onClick={onClose}><X size={16} /></button>
        </div>
        <div className="p-4 flex-shrink-0">
          <div className="flex items-center gap-2 rounded-sm border px-3 h-9 bg-white" style={{ borderColor: "rgba(26,82,153,0.2)" }}>
            <Search size={14} style={{ color: B }} />
            <input value={keyword} onChange={e => setKeyword(e.target.value)} className="flex-1 text-sm bg-transparent outline-none font-bold" style={{ color: N }} placeholder="搜索附件名称、标签、内容" />
          </div>
        </div>
        <div className="mx-4 border overflow-auto" style={{ borderColor: "rgba(40,118,193,0.16)", maxHeight: 360 }}>
          <table className="w-full border-collapse">
            <thead className="sticky top-0"><tr style={{ background: N }}>
              {["选择", "附件名称", "类型", "格式", "标签", "状态"].map(h => <th key={h} className="px-3 py-2.5 text-left text-xs font-black" style={{ color: W }}>{h}</th>)}
            </tr></thead>
            <tbody>{filtered.map((a, i) => {
              const mounted = mountedIds.includes(a.id);
              return (
                <tr key={a.id} className="border-b" style={{ borderColor: "rgba(40,118,193,0.08)", background: i % 2 ? `rgba(40,118,193,0.03)` : W }}>
                  <td className="px-3 py-2.5"><input type="checkbox" disabled={mounted} checked={mounted || selected.has(a.id)} onChange={() => toggle(a.id)} className="accent-[#2876C1]" /></td>
                  <td className="px-3 py-2.5 text-sm font-black" style={{ color: N }}>{a.name}</td>
                  <td className="px-3 py-2.5 text-sm font-bold">{a.attachmentType}</td>
                  <td className="px-3 py-2.5 text-sm font-bold">{a.fileFormat}</td>
                  <td className="px-3 py-2.5 text-xs font-bold" style={{ color: `rgba(26,82,153,0.6)` }}>{a.tags.join("、")}</td>
                  <td className="px-3 py-2.5 text-xs font-black" style={{ color: mounted ? B : `rgba(26,82,153,0.45)` }}>{mounted ? "已挂载" : "可挂载"}</td>
                </tr>
              );
            })}</tbody>
          </table>
        </div>
        <div className="flex items-center justify-between px-5 py-4 mt-4 border-t" style={{ borderColor: "rgba(40,118,193,0.12)" }}>
          <span className="text-sm font-bold" style={{ color: `rgba(26,82,153,0.6)` }}>已选择 {selected.size} 个新附件</span>
          <div className="flex gap-2"><BtnOutline onClick={onClose}>取消</BtnOutline><BtnPrimary onClick={() => onConfirm([...selected])}><Link2 size={13} />确认挂载</BtnPrimary></div>
        </div>
      </div>
    </div>
  );
}

function AdminPage({ onNav, navigationItems, onLogout, logoutPending, currentUser }: AuthenticatedPageProps) {
  const [viewDoc, setViewDoc] = useState<Doc | null>(null);
  const [permDoc, setPermDoc] = useState<Doc | null>(null);
  const [showUpload, setShowUpload] = useState(false);
  const [showBatch, setShowBatch] = useState(false);
  const [showMount, setShowMount] = useState(false);
  const [toast, setToast] = useState("");
  const [adminMode, setAdminMode] = useState<"plans" | "attachments">("plans");
  const [selectedDoc, setSelectedDoc] = useState<Doc | null>(DOCS[0]);
  const [subTab, setSubTab] = useState<"子方案" | "已挂载附件">("已挂载附件");
  const [attachments, setAttachments] = useState<SharedAttachment[]>(SHARED_ATTACHMENTS);
  const [attachmentBindings, setAttachmentBindings] = useState<Record<number, number[]>>(INITIAL_ATTACHMENT_BINDINGS);
  const [attachmentType, setAttachmentType] = useState("全部附件");
  const [attachmentKeyword, setAttachmentKeyword] = useState("");
  const showToast = (msg: string) => { setToast(msg); setTimeout(() => setToast(""), 2500); };

  const STATS = [
    { label: "已上传文档", value: "64", icon: <Database size={20} style={{ color: B }} /> },
    { label: "主案数目", value: "21", icon: <FileText size={20} style={{ color: B }} /> },
    { label: "子案数目", value: "43", icon: <FileText size={20} style={{ color: `rgba(21,101,192,0.6)` }} /> },
    { label: "共享附件", value: "18", icon: <Paperclip size={20} style={{ color: B }} /> },
    { label: "今日查看人数", value: "12", icon: <Users size={20} style={{ color: B }} /> },
  ];

  const mountedIds = selectedDoc ? (attachmentBindings[selectedDoc.id] ?? []) : [];
  const mountedAttachments = attachments.filter(a => mountedIds.includes(a.id));
  const attachmentBindingCounts = Object.values(attachmentBindings).flat().reduce<Record<number, number>>((acc, id) => {
    acc[id] = (acc[id] ?? 0) + 1;
    return acc;
  }, {});
  const mountAttachments = (ids: number[]) => {
    if (!selectedDoc || !ids.length) { setShowMount(false); return; }
    setAttachmentBindings(prev => ({ ...prev, [selectedDoc.id]: [...new Set([...(prev[selectedDoc.id] ?? []), ...ids])] }));
    setShowMount(false);
    showToast(`已挂载 ${ids.length} 个共享附件`);
  };
  const unmountAttachment = (attachment: SharedAttachment) => {
    if (!selectedDoc || !window.confirm(`确认解除「${attachment.name}」与当前主案的挂载关系？附件库中的文件不会被删除。`)) return;
    setAttachmentBindings(prev => ({ ...prev, [selectedDoc.id]: (prev[selectedDoc.id] ?? []).filter(id => id !== attachment.id) }));
    showToast(`已解除挂载：${attachment.name}`);
  };
  const deleteAttachment = (attachment: SharedAttachment, count: number) => {
    if (count > 0) { showToast(`该附件已挂载到 ${count} 个主案，请先处理挂载关系`); return; }
    if (!window.confirm(`确认删除共享附件「${attachment.name}」？`)) return;
    setAttachments(prev => prev.filter(a => a.id !== attachment.id));
    showToast(`已删除共享附件：${attachment.name}`);
  };

  return (
    <PageFrame title="方案管理系统后台" current="admin" navigationItems={navigationItems} onNav={onNav} onLogout={onLogout} logoutPending={logoutPending} currentUser={currentUser}>
      {adminMode === "plans"
        ? <LeftPanel admin />
        : <AttachmentTypePanel selected={attachmentType} onSelect={setAttachmentType} />}
      <div className="flex-1 flex flex-col m-3 ml-0 min-h-0">
        <div className="grid grid-cols-5 gap-3 mb-3 flex-shrink-0">
          {STATS.map(s => (
            <div key={s.label} style={{ background: W, border: `1px solid rgba(40,118,193,0.18)` }} className="flex items-center gap-3 px-4 py-3">
              <div style={{ background: `rgba(21,101,192,0.08)` }} className="p-2.5 rounded-sm">{s.icon}</div>
              <div>
                <div className="text-2xl font-black" style={{ color: N }}>{s.value}</div>
                <div className="text-xs font-bold" style={{ color: `rgba(26,82,153,0.45)` }}>{s.label}</div>
              </div>
            </div>
          ))}
        </div>
        <div className="flex items-center justify-between flex-shrink-0" style={{ background: W, border: `1px solid rgba(40,118,193,0.18)` }}>
          <div className="flex">
            {([{ id: "plans", label: "方案管理" }, { id: "attachments", label: "共享附件库" }] as const).map(m => (
              <button key={m.id} onClick={() => setAdminMode(m.id)}
                className="px-6 py-3 text-sm font-black border-r transition-colors"
                style={{ color: adminMode === m.id ? W : `rgba(26,82,153,0.55)`, background: adminMode === m.id ? N : W, borderColor: "rgba(40,118,193,0.14)" }}>
                {m.label}
              </button>
            ))}
          </div>
          <div className="flex gap-2 pr-4">
            <BtnPrimary onClick={() => setShowUpload(true)}><Upload size={14} />{adminMode === "plans" ? "上传主案" : "上传附件"}</BtnPrimary>
            <BtnOutline onClick={() => setShowBatch(true)}><Plus size={14} />{adminMode === "plans" ? "批量导入" : "批量导入附件"}</BtnOutline>
          </div>
        </div>

        {adminMode === "plans" ? (
          <>
            <div className="flex-1 flex flex-col min-h-0 mt-2 mb-2" style={{ background: W, border: `1px solid rgba(40,118,193,0.18)` }}>
              <div className="flex items-center justify-between px-5 py-3 border-b flex-shrink-0" style={{ borderColor: "rgba(40,118,193,0.14)", background: `rgba(40,118,193,0.04)` }}>
                <span className="text-base font-black" style={{ color: N }}>主案管理列表</span>
              </div>
              <div className="flex-1 overflow-auto min-h-0">
                <table className="w-full border-collapse">
                  <thead className="sticky top-0 z-10">
                    <tr style={{ background: N }}>
                      {["序号", "文档名称", "类型", "级别", "可见范围", "子方案", "附件", "操作"].map(h => (
                        <th key={h} className="px-4 py-3 text-left text-sm font-black whitespace-nowrap" style={{ color: W }}>{h}</th>
                      ))}
                    </tr>
                  </thead>
                  <tbody>
                    {DOCS.map((doc, i) => (
                      <AdminDocRow key={doc.id} doc={doc} index={String(i + 1)}
                        selected={!!selectedDoc && selectedDoc.id === doc.id}
                        childCount={doc.children?.length ?? 0}
                        attachmentCount={(attachmentBindings[doc.id] ?? []).length}
                        onSelect={setSelectedDoc}
                        onView={setViewDoc} onPerm={setPermDoc} onDelete={d => showToast(`已删除：${d.name}`)} />
                    ))}
                  </tbody>
                </table>
              </div>
            </div>

            <div className="flex-shrink-0 flex flex-col" style={{ height: 300, background: W, border: `1px solid rgba(40,118,193,0.18)` }}>
              <div className="flex items-center border-b flex-shrink-0" style={{ borderColor: "rgba(40,118,193,0.14)" }}>
                {(["子方案", "已挂载附件"] as const).map(t => {
                  const count = selectedDoc ? (t === "子方案" ? (selectedDoc.children?.length ?? 0) : mountedAttachments.length) : 0;
                  return (
                    <button key={t} onClick={() => setSubTab(t)}
                      style={{ color: subTab === t ? W : `rgba(26,82,153,0.55)`, background: subTab === t ? N : "transparent", borderRight: `1px solid rgba(40,118,193,0.14)` }}
                      className="px-7 py-2.5 text-sm font-black transition-all hover:bg-[rgba(40,118,193,0.07)]">
                      {t}{selectedDoc && <span className="ml-1 opacity-70">({count})</span>}
                    </button>
                  );
                })}
                <div className="ml-auto flex items-center gap-2 px-5">
                  <span className="text-xs font-bold truncate" style={{ color: `rgba(26,82,153,0.55)` }}>
                    {selectedDoc ? `当前主案：${selectedDoc.name}` : "请选择上方主案"}
                  </span>
                  {selectedDoc && subTab === "子方案" && (
                    <><BtnPrimary sm onClick={() => setShowUpload(true)}><Upload size={12} />上传子方案</BtnPrimary><BtnOutline sm onClick={() => setShowBatch(true)}><Plus size={12} />批量导入</BtnOutline></>
                  )}
                  {selectedDoc && subTab === "已挂载附件" && (
                    <><BtnPrimary sm onClick={() => setShowMount(true)}><Link2 size={12} />从共享附件库挂载</BtnPrimary><BtnOutline sm onClick={() => setShowUpload(true)}><Upload size={12} />上传新附件并挂载</BtnOutline></>
                  )}
                </div>
              </div>
              <div className="flex-1 overflow-auto">
                {!selectedDoc ? (
                  <div className="flex flex-col items-center justify-center h-full" style={{ color: `rgba(26,82,153,0.25)` }}><FileText size={40} className="mb-3" /><span className="text-sm font-black">请选择一个主案</span></div>
                ) : subTab === "子方案" ? (
                  <AdminSubDocTable docs={selectedDoc.children ?? []} emptyText="暂无子方案" onView={setViewDoc} onPerm={setPermDoc} onDelete={d => showToast(`已删除：${d.name}`)} />
                ) : (
                  <MountedAttachmentTable attachments={mountedAttachments} bindingCounts={attachmentBindingCounts} onView={setViewDoc} onUnmount={unmountAttachment} />
                )}
              </div>
            </div>
          </>
        ) : (
          <div className="flex-1 min-h-0 mt-2">
            <SharedAttachmentLibrary attachments={attachments} bindings={attachmentBindings} typeFilter={attachmentType} keyword={attachmentKeyword}
              onKeywordChange={setAttachmentKeyword} onView={setViewDoc} onDelete={deleteAttachment} />
          </div>
        )}
      </div>
      {toast && (
        <div className="fixed bottom-6 right-6 z-50 flex items-center gap-2 px-5 py-3 rounded-sm shadow-lg text-sm font-black" style={{ background: N, color: W }}>
          <Check size={14} />{toast}
        </div>
      )}
      {viewDoc && <WordViewer doc={viewDoc} onClose={() => setViewDoc(null)} />}
      {permDoc && <PermissionModal doc={permDoc} onClose={() => setPermDoc(null)} />}
      {showUpload && <UploadModal onClose={() => setShowUpload(false)} />}
      {showBatch && <BatchImportModal onClose={() => setShowBatch(false)} />}
      {showMount && selectedDoc && <MountAttachmentModal plan={selectedDoc} attachments={attachments} mountedIds={mountedIds} onConfirm={mountAttachments} onClose={() => setShowMount(false)} />}
    </PageFrame>
  );
}

// ─── P4: Audit page ───────────────────────────────────────────────────────────
function AuditPage({ onNav, navigationItems, onLogout, logoutPending, currentUser }: AuthenticatedPageProps) {
  const [keyword, setKeyword] = useState("");
  const [filterAction, setFilterAction] = useState("全部");

  const filtered = useMemo(() => AUDIT_LOGS.filter(l => {
    const matchK = !keyword.trim() || [l.user, l.dept, l.action, l.target, l.result].some(v => v.toLowerCase().includes(keyword.toLowerCase()));
    const matchA = filterAction === "全部" || l.action === filterAction;
    return matchK && matchA;
  }), [keyword, filterAction]);

  const resultStyle = (r: string) => {
    if (r === "成功") return { color: B, background: `rgba(21,101,192,0.1)`, border: `1px solid rgba(21,101,192,0.25)` };
    if (r === "失败") return { color: W, background: N, border: `1px solid ${N}` };
    return { color: N, background: `rgba(40,118,193,0.1)`, border: `1px solid rgba(26,82,153,0.2)` };
  };

  const ACTIONS = ["全部", "查看文档", "下载文档", "上传文档", "编辑文档", "权限配置", "删除文档", "登录系统"];

  return (
    <PageFrame title="操作日志审计" current="audit" navigationItems={navigationItems} onNav={onNav} onLogout={onLogout} logoutPending={logoutPending} currentUser={currentUser}>
      {/* Left panel with filter */}
      <div className="w-[272px] flex-shrink-0 m-3 flex flex-col" style={{ background: W, border: `1px solid rgba(40,118,193,0.2)` }}>
        <div className="px-4 py-3 flex items-center gap-2 border-b" style={{ borderColor: "rgba(40,118,193,0.14)", background: `rgba(40,118,193,0.05)` }}>
          <Activity size={15} style={{ color: B }} />
          <span className="text-sm font-black" style={{ color: N }}>日志筛选</span>
        </div>
        <div className="p-3 border-b" style={{ borderColor: "rgba(40,118,193,0.1)" }}>
          <div className="flex items-center gap-1">
            <div className="flex-1 flex items-center gap-2 rounded-sm border px-3 h-9 bg-white" style={{ borderColor: "rgba(26,82,153,0.2)" }}>
              <Search size={14} style={{ color: B }} />
              <input className="flex-1 text-sm bg-transparent outline-none font-bold" style={{ color: N }} placeholder="搜索名称、内容"
                value={keyword} onChange={e => setKeyword(e.target.value)} />
            </div>
            <button onClick={() => { }} style={{ background: B, color: W }} className="h-9 w-9 flex items-center justify-center rounded-sm hover:opacity-80 transition-opacity flex-shrink-0">
              <Search size={14} />
            </button>
          </div>
        </div>
        <div className="p-3 flex-1 overflow-y-auto">
          <div className="flex items-center gap-2 mb-2 px-1">
            <Filter size={12} style={{ color: B }} />
            <span className="text-xs font-black" style={{ color: N }}>操作类型</span>
          </div>
          {ACTIONS.map(a => (
            <button key={a} onClick={() => setFilterAction(a)}
              style={{
                background: filterAction === a ? N : "transparent",
                color: filterAction === a ? W : `rgba(26,82,153,0.5)`,
              }}
              className="block w-full text-left px-3 py-2 text-sm rounded-sm mb-0.5 font-bold transition-colors hover:bg-[rgba(40,118,193,0.08)]">
              {a}
            </button>
          ))}
        </div>
      </div>

      {/* Right: log + charts */}
      <div className="flex-1 flex gap-3 m-3 ml-0 min-h-0">
        {/* Log table */}
        <div className="flex-1 flex flex-col min-h-0" style={{ background: W, border: `1px solid rgba(40,118,193,0.18)` }}>
          <div className="flex items-center justify-between px-5 py-3 border-b flex-shrink-0" style={{ borderColor: "rgba(40,118,193,0.14)", background: `rgba(40,118,193,0.04)` }}>
            <div className="flex items-center gap-3">
              <span className="text-base font-black" style={{ color: N }}>操作日志</span>
              <span style={{ color: B, background: `rgba(21,101,192,0.1)` }} className="text-xs font-black px-2 py-0.5 rounded-sm">{filtered.length} 条</span>
            </div>
            <span className="text-sm font-bold" style={{ color: `rgba(26,82,153,0.4)` }}>2024-03-20</span>
          </div>
          <div className="flex-1 overflow-auto">
            <table className="w-full border-collapse">
              <thead className="sticky top-0 z-10">
                <tr style={{ background: N }}>
                  {["序号", "操作时间", "操作人员", "所属单位", "操作类型", "操作对象", "结果", "IP 地址"].map(h => (
                    <th key={h} className="px-3 py-3 text-left text-sm font-black whitespace-nowrap" style={{ color: W }}>{h}</th>
                  ))}
                </tr>
              </thead>
              <tbody>
                {filtered.map((log, i) => (
                  <tr key={log.id} className="border-b transition-colors" style={{ borderColor: "rgba(40,118,193,0.09)", background: i % 2 === 0 ? W : `rgba(40,118,193,0.04)` }}
                    onMouseEnter={e => (e.currentTarget.style.background = `rgba(21,101,192,0.05)`)}
                    onMouseLeave={e => (e.currentTarget.style.background = i % 2 === 0 ? W : `rgba(40,118,193,0.04)`)}>
                    <td className="px-3 py-2.5 text-sm font-black" style={{ color: B }}>{i + 1}</td>
                    <td className="px-3 py-2.5 text-sm font-bold whitespace-nowrap" style={{ color: N }}>{log.time}</td>
                    <td className="px-3 py-2.5">
                      <div className="flex items-center gap-1.5"><User size={12} style={{ color: B }} /><span className="text-sm font-black" style={{ color: N }}>{log.user}</span></div>
                    </td>
                    <td className="px-3 py-2.5 text-sm font-bold" style={{ color: `rgba(26,82,153,0.6)` }}>{log.dept}</td>
                    <td className="px-3 py-2.5">
                      <span style={{ color: N, background: `rgba(40,118,193,0.09)` }} className="text-xs font-black px-2 py-0.5 rounded-sm">{log.action}</span>
                    </td>
                    <td className="px-3 py-2.5 text-sm font-bold max-w-[150px] truncate" style={{ color: N }}>
                      <HighlightText text={log.target} keyword={keyword} />
                    </td>
                    <td className="px-3 py-2.5">
                      <span style={resultStyle(log.result)} className="text-xs font-black px-2 py-0.5 rounded-sm whitespace-nowrap">{log.result}</span>
                    </td>
                    <td className="px-3 py-2.5 text-sm font-bold" style={{ color: `rgba(26,82,153,0.45)` }}>{log.ip}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </div>

        {/* Stats charts */}
        <div className="w-[300px] flex-shrink-0 flex flex-col gap-3">
          {/* Trend */}
          <div style={{ background: W, border: `1px solid rgba(40,118,193,0.18)` }} className="flex-1 p-4">
            <div className="flex items-center gap-2 mb-3">
              <BarChart2 size={14} style={{ color: B }} />
              <span className="text-sm font-black" style={{ color: N }}>近7日操作趋势</span>
            </div>
            <ResponsiveContainer width="100%" height={120}>
              <LineChart data={dailyData} margin={{ top: 4, right: 8, bottom: 0, left: -20 }}>
                <CartesianGrid strokeDasharray="3 3" stroke="rgba(40,118,193,0.1)" />
                <XAxis dataKey="day" tick={{ fontSize: 11, fill: `rgba(26,82,153,0.5)`, fontWeight: 700 }} />
                <YAxis tick={{ fontSize: 11, fill: `rgba(26,82,153,0.5)`, fontWeight: 700 }} />
                <Tooltip contentStyle={{ fontSize: 12, border: `1px solid rgba(11,37,69,0.15)`, borderRadius: 2, fontWeight: 700 }} />
                <Line type="monotone" dataKey="ops" stroke={B} strokeWidth={2} dot={{ fill: B, r: 3 }} name="操作次数" />
              </LineChart>
            </ResponsiveContainer>
          </div>
          {/* Pie */}
          <div style={{ background: W, border: `1px solid rgba(40,118,193,0.18)` }} className="flex-1 p-4">
            <div className="flex items-center gap-2 mb-1">
              <Activity size={14} style={{ color: B }} />
              <span className="text-sm font-black" style={{ color: N }}>操作类型分布</span>
            </div>
            <ResponsiveContainer width="100%" height={140}>
              <PieChart>
                <Pie data={pieData} cx="50%" cy="50%" innerRadius={30} outerRadius={52} dataKey="value" nameKey="name">
                  {pieData.map((_, i) => <Cell key={i} fill={PIE_COLORS[i % PIE_COLORS.length]} />)}
                </Pie>
                <Tooltip contentStyle={{ fontSize: 11, border: `1px solid rgba(11,37,69,0.15)`, borderRadius: 2, fontWeight: 700 }} />
                <Legend iconSize={8} iconType="circle" wrapperStyle={{ fontSize: 11, fontWeight: 700 }} />
              </PieChart>
            </ResponsiveContainer>
          </div>
          {/* Bar */}
          <div style={{ background: W, border: `1px solid rgba(40,118,193,0.18)` }} className="flex-1 p-4">
            <div className="flex items-center gap-2 mb-3">
              <Users size={14} style={{ color: B }} />
              <span className="text-sm font-black" style={{ color: N }}>人员活跃度 TOP 6</span>
            </div>
            <ResponsiveContainer width="100%" height={110}>
              <BarChart data={userBar} layout="vertical" margin={{ top: 0, right: 8, bottom: 0, left: 8 }}>
                <CartesianGrid strokeDasharray="3 3" stroke="rgba(40,118,193,0.1)" horizontal={false} />
                <XAxis type="number" tick={{ fontSize: 11, fill: `rgba(26,82,153,0.5)`, fontWeight: 700 }} />
                <YAxis type="category" dataKey="name" tick={{ fontSize: 11, fill: N, fontWeight: 700 }} width={32} />
                <Tooltip contentStyle={{ fontSize: 11, border: `1px solid rgba(11,37,69,0.15)`, borderRadius: 2, fontWeight: 700 }} />
                <Bar dataKey="count" fill={B} radius={[0, 3, 3, 0]} name="操作次数" />
              </BarChart>
            </ResponsiveContainer>
          </div>
        </div>
      </div>
    </PageFrame>
  );
}

// ─── P1: Login page ───────────────────────────────────────────────────────────
function LoginPage({ onLogin, notice }: {
  onLogin: (request: LoginRequest) => Promise<void>;
  notice: string;
}) {
  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");
  const [keepSignedIn, setKeepSignedIn] = useState(false);
  const [error, setError] = useState("");
  const [errorRequestId, setErrorRequestId] = useState("");
  const [submitting, setSubmitting] = useState(false);

  const handleLogin = async () => {
    if (submitting) return;
    if (!username.trim() || !password.trim()) { setError("请输入用户名和密码"); return; }
    setSubmitting(true);
    setError("");
    setErrorRequestId("");
    try {
      await onLogin({ username: username.trim(), password, keepSignedIn });
    } catch (loginError) {
      if (loginError instanceof HttpError) {
        setError(loginError.message);
        setErrorRequestId(loginError.requestId);
      } else {
        setError("登录失败，请稍后重试");
      }
    } finally {
      setSubmitting(false);
    }
  };

  return (
    <div className="w-full h-screen flex flex-col overflow-hidden" style={{ fontFamily: "'Noto Sans SC', sans-serif", background: PBG }}>
      {/* Top bar — auto height, title only */}
      <div className="relative z-30 flex-shrink-0 flex items-center justify-center py-3" style={{ background: B }}>
        <span className="text-xl font-black tracking-[0.22em] whitespace-nowrap" style={{ color: W }}>方案管理系统</span>
      </div>

      {/* Header → 主体 渐变过渡带（20px） */}
      <div aria-hidden className="relative z-20 flex-shrink-0"
        style={{ height: 20, background: `linear-gradient(to bottom, ${B} 0%, ${PBG} 100%)` }} />

      {/* Center card */}
      <div className="flex-1 flex items-center justify-center relative z-10">
          <div style={{ background: W, width: 400, border: `1px solid rgba(40,118,193,0.2)` }} className="shadow-2xl rounded-sm overflow-hidden">
            {/* Top accent */}
            <div className="h-1" style={{ background: `linear-gradient(90deg, ${N}, ${B})` }} />
            <div className="px-10 py-10">
              <div className="flex flex-col items-center mb-8">
                <div className="w-14 h-14 rounded-full flex items-center justify-center mb-4" style={{ background: N }}>
                  <Lock size={22} style={{ color: W }} />
                </div>
                <h2 className="text-lg font-black tracking-widest" style={{ color: N }}>用户登录</h2>
                <p className="text-sm font-bold mt-1" style={{ color: `rgba(26,82,153,0.4)` }}>请使用授权账号登录系统</p>
              </div>
              {(error || notice) && (
                <div className="mb-4 flex items-center gap-2 px-3 py-2 rounded-sm" style={{ background: `rgba(40,118,193,0.08)`, border: `1px solid rgba(26,82,153,0.2)` }}>
                  <AlertCircle size={14} style={{ color: B }} />
                  <div>
                    <span className="text-sm font-black" style={{ color: N }}>{error || notice}</span>
                    {errorRequestId && <div className="mt-1 text-[11px] font-bold" style={{ color: `rgba(26,82,153,0.55)` }}>错误编号：{errorRequestId}</div>}
                  </div>
                </div>
              )}
              <div className="space-y-4">
                {[
                  { label: "用户名", key: "username", icon: <User size={14} style={{ color: B }} />, placeholder: "请输入用户名", type: "text" },
                  { label: "密码", key: "password", icon: <Lock size={14} style={{ color: B }} />, placeholder: "请输入密码", type: "password" },
                ].map(f => (
                  <div key={f.key}>
                    <label className="text-sm font-black mb-1.5 block" style={{ color: N }}>{f.label}</label>
                    <div className="flex items-center gap-3 rounded-sm border h-11 px-3 transition-colors" style={{ borderColor: "rgba(26,82,153,0.2)", background: `rgba(40,118,193,0.04)` }}>
                      {f.icon}
                      <input type={f.type} disabled={submitting} className="flex-1 text-sm bg-transparent outline-none font-bold disabled:cursor-not-allowed" style={{ color: N }}
                        placeholder={f.placeholder}
                        value={f.key === "username" ? username : password}
                        onChange={e => { f.key === "username" ? setUsername(e.target.value) : setPassword(e.target.value); setError(""); setErrorRequestId(""); }}
                        onKeyDown={e => { if (e.key === "Enter") void handleLogin(); }} />
                    </div>
                  </div>
                ))}
                <div className="flex items-center">
                  <label className="flex items-center gap-2 cursor-pointer">
                    <input type="checkbox" disabled={submitting} checked={keepSignedIn} onChange={e => setKeepSignedIn(e.target.checked)} className="accent-[#2876C1]" />
                    <span className="text-sm font-bold" style={{ color: `rgba(26,82,153,0.55)` }}>保持登录</span>
                  </label>
                </div>
                <button onClick={() => void handleLogin()} disabled={submitting} aria-busy={submitting}
                  className="w-full h-11 font-black text-base rounded-sm transition-opacity hover:opacity-80 active:opacity-60 disabled:opacity-60 disabled:cursor-not-allowed tracking-widest mt-2"
                  style={{ background: N, color: W }}>
                  {submitting ? "登 录 中…" : "登　录"}
                </button>
              </div>
              <p className="text-center text-xs font-bold mt-6" style={{ color: `rgba(26,82,153,0.3)` }}>本系统为涉密系统 · 严禁未授权访问</p>
            </div>
          </div>
      </div>
      <div className="relative z-10 pb-6 text-center text-xs font-bold" style={{ color: `rgba(26,82,153,0.3)` }}>
        © 方案管理系统 · 版权所有 · 严禁扩散
      </div>
    </div>
  );
}

function AuthStateScreen({ title, description, requestId, primaryLabel, onPrimary, secondaryLabel, onSecondary, busy }: {
  title: string;
  description: string;
  requestId?: string;
  primaryLabel: string;
  onPrimary: () => void;
  secondaryLabel?: string;
  onSecondary?: () => void;
  busy?: boolean;
}) {
  return (
    <div className="w-full h-screen flex flex-col" style={{ fontFamily: "'Noto Sans SC', sans-serif", background: PBG }}>
      <div className="relative z-30 flex-shrink-0 flex items-center justify-center py-3" style={{ background: B }}>
        <span className="text-xl font-black tracking-[0.22em]" style={{ color: W }}>方案管理系统</span>
      </div>
      <div aria-hidden style={{ height: 20, background: `linear-gradient(to bottom, ${B} 0%, ${PBG} 100%)` }} />
      <div className="flex-1 flex items-center justify-center">
        <div className="w-[420px] p-8 text-center shadow-xl rounded-sm" style={{ background: W, border: `1px solid rgba(40,118,193,0.2)` }}>
          <div className="w-12 h-12 mx-auto mb-4 rounded-full flex items-center justify-center" style={{ background: `rgba(40,118,193,0.1)`, color: B }}>
            {busy ? <RefreshCw size={21} className="animate-spin" /> : <AlertCircle size={21} />}
          </div>
          <h2 className="text-lg font-black mb-2" style={{ color: N }}>{title}</h2>
          <p className="text-sm font-bold leading-relaxed" style={{ color: `rgba(26,82,153,0.58)` }}>{description}</p>
          {requestId && <p className="text-xs font-bold mt-2" style={{ color: `rgba(26,82,153,0.45)` }}>错误编号：{requestId}</p>}
          {!busy && (
            <div className="flex justify-center gap-2 mt-6">
              {secondaryLabel && onSecondary && <BtnOutline onClick={onSecondary}>{secondaryLabel}</BtnOutline>}
              <BtnPrimary onClick={onPrimary}>{primaryLabel}</BtnPrimary>
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

// ─── Root ─────────────────────────────────────────────────────────────────────
function formatDateTime(value: string): string {
  return new Intl.DateTimeFormat("zh-CN", {
    year: "numeric",
    month: "2-digit",
    day: "2-digit",
    hour: "2-digit",
    minute: "2-digit",
  }).format(new Date(value));
}

function formatFileSize(value: number): string {
  if (value < 1024) return `${value} B`;
  if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KB`;
  return `${(value / 1024 / 1024).toFixed(1)} MB`;
}

function F4State({ loading, error, empty, onRetry }: {
  loading: boolean;
  error: HttpError | null;
  empty: boolean;
  onRetry?: () => void;
}) {
  if (loading) {
    return <div className="p-8 text-center text-sm font-bold" style={{ color: N }}><RefreshCw className="inline animate-spin mr-2" size={15} />正在加载数据…</div>;
  }
  if (error) {
    return (
      <div className="p-8 text-center text-sm font-bold" style={{ color: N }}>
        <AlertCircle className="inline mr-2" size={15} />
        {error.httpStatus === 403 ? "当前账号无权访问文档数据" : error.message}
        {error.requestId && <span className="block text-xs mt-1 opacity-60">错误编号：{error.requestId}</span>}
        {onRetry && error.httpStatus !== 401 && <button onClick={onRetry} className="block mx-auto mt-3 px-3 py-1.5 text-xs font-black border" style={{ color: N, borderColor: B }}>重新加载</button>}
      </div>
    );
  }
  if (empty) return <div className="p-8 text-center text-sm font-bold opacity-50" style={{ color: N }}>暂无符合条件的数据</div>;
  return null;
}

function F4Pager({ page, totalPages, onPage }: {
  page: number;
  totalPages: number;
  onPage: (page: number) => void;
}) {
  if (totalPages <= 1) return null;
  return (
    <div className="sticky bottom-0 p-2 flex justify-end gap-3 text-xs font-black" style={{ background: W, color: N }}>
      <button disabled={page <= 1} onClick={() => onPage(page - 1)}>上一页</button>
      <span>{page}/{totalPages}</span>
      <button disabled={page >= totalPages} onClick={() => onPage(page + 1)}>下一页</button>
    </div>
  );
}

function F4DocumentTable({ items, keyword, selectedId, onSelect, onDetail, bindingMode = false, onReverse, onView, onEdit, onDelete, onPermission, onUnbind, onFileError }: {
  items: (DocumentSummary | AttachmentBindingSummary)[];
  keyword?: string;
  selectedId?: string | null;
  onSelect?: (item: DocumentSummary) => void;
  onDetail: (item: DocumentSummary, attachment: boolean) => void;
  bindingMode?: boolean;
  onReverse?: (item: DocumentSummary) => void;
  onView?: (item: DocumentSummary) => void;
  onEdit?: (item: DocumentSummary) => void;
  onDelete?: (item: DocumentSummary) => void;
  onPermission?: (item: DocumentSummary) => void;
  onUnbind?: (item: AttachmentBindingSummary) => void;
  onFileError?: (error: HttpError) => void;
}) {
  const dictionaries = useDictionaries();
  return (
    <table className="w-full border-collapse">
      <thead className="sticky top-0 z-10">
        <tr style={{ background: N }}>
          {["序号", "名称", "类型/分类", "密级", "概述与标签", bindingMode ? "挂载顺序" : "更新时间", "操作"].map((heading) => (
            <th key={heading} className={`px-4 py-3 text-sm font-black whitespace-nowrap ${heading === "序号" ? "text-center" : "text-left"}`} style={{ color: W }}>{heading}</th>
          ))}
        </tr>
      </thead>
      <tbody>
        {items.map((item, index) => {
          const attachment = item.documentType === DocumentType.ATTACHMENT;
          const canView = item.allowedActions.includes(AllowedAction.VIEW);
          const canDownload = item.allowedActions.includes(AllowedAction.DOWNLOAD);
          const canEdit = item.allowedActions.includes(AllowedAction.EDIT);
          const canDelete = item.allowedActions.includes(AllowedAction.DELETE);
          const binding = bindingMode ? item as AttachmentBindingSummary : null;
          const kw = (keyword ?? "").trim();
          return (
            <Fragment key={binding?.bindingId ?? item.id}>
            <tr
              onClick={() => onSelect?.(item)}
              className="border-b cursor-pointer"
              style={{
                borderColor: "rgba(40,118,193,0.1)",
                background: selectedId === item.id ? "rgba(49,145,235,0.12)" : index % 2 ? "rgba(40,118,193,0.025)" : W,
              }}
            >
              <td className="px-4 py-3 text-sm font-bold text-center" style={{ color: N }}>{index + 1}</td>
              <td className="px-4 py-3">
                <div className="text-sm font-black" style={{ color: N }}>{kw ? <HighlightText text={item.documentName} keyword={kw} /> : item.documentName}</div>
                {attachment && <div className="text-[11px] font-bold opacity-55" style={{ color: N }}>
                  {item.fileExtension ? item.fileExtension.toUpperCase() : "无扩展名"}
                </div>}
              </td>
              <td className="px-4 py-3 text-xs font-bold" style={{ color: N }}>
                {attachment ? dictionaries.label("attachmentTypes", item.attachmentType) : item.categoryName ?? "未分类"}
              </td>
              <td className="px-4 py-3"><LevelBadge level={dictionaries.label("securityLevels", item.securityLevel)} /></td>
              <td className="px-4 py-3 max-w-[360px]">
                <div className="text-xs line-clamp-2" style={{ color: "rgba(26,82,153,.65)" }}>{kw ? <HighlightText text={item.summary || "暂无概述"} keyword={kw} /> : (item.summary || "暂无概述")}</div>
                <div className="flex flex-wrap gap-1 mt-1">
                  {item.tags.map((tag) => <span key={tag} className="px-1.5 py-0.5 text-[10px] font-black" style={{ color: B, background: "rgba(49,145,235,.1)" }}>{kw ? <HighlightText text={tag} keyword={kw} /> : tag}</span>)}
                </div>
              </td>
              <td className="px-4 py-3 text-xs font-bold whitespace-nowrap" style={{ color: N }}>
                {binding ? binding.bindingSortNo : formatDateTime(item.updatedAt)}
              </td>
              <td className="px-4 py-3 whitespace-nowrap">
                {canView && <button onClick={(event) => { event.stopPropagation(); onDetail(item, attachment); }} className="mr-2 text-xs font-black" style={{ color: B }}>详情</button>}
                {canView && onView && <button onClick={(event) => { event.stopPropagation(); onView(item); }} className="mr-2 text-xs font-black" style={{ color: B }}>查看</button>}
                {canDownload && onFileError && <DownloadButton item={item} onError={onFileError} onSuccess={() => { invalidateDetail(attachment ? "attachment" : "document", item.id); notifyContentChanged(attachment ? "attachments" : "documents"); }} />}
                {canEdit && onEdit && <button onClick={(event) => { event.stopPropagation(); onEdit(item); }} className="mr-2 text-xs font-black" style={{ color: B }}>编辑</button>}
                {canDelete && onDelete && <button onClick={(event) => { event.stopPropagation(); onDelete(item); }} className="mr-2 text-xs font-black" style={{ color: N }}>删除</button>}
                {onPermission && item.documentType === DocumentType.MAIN && item.allowedActions.includes(AllowedAction.CONFIG_PERMISSION) && <button onClick={(event) => { event.stopPropagation(); onPermission(item); }} className="mr-2 text-xs font-black" style={{ color: B }}>权限配置</button>}
                {onPermission && item.documentType === DocumentType.SUB_PLAN && <button onClick={(event) => { event.stopPropagation(); onPermission(item); }} className="mr-2 text-xs font-black" style={{ color: B }}>继承权限</button>}
                {binding && onUnbind && <button onClick={(event) => { event.stopPropagation(); onUnbind(binding); }} className="mr-2 text-xs font-black" style={{ color: N }}>解除挂载</button>}
                {attachment && onReverse && <button onClick={(event) => { event.stopPropagation(); onReverse(item); }} className="text-xs font-black" style={{ color: B }}>挂载主案({item.mountedPlanCount ?? 0})</button>}
              </td>
            </tr>
            {item.contentSnippet && (
              <tr style={{
                borderColor: "rgba(40,118,193,0.1)",
                background: selectedId === item.id ? "rgba(49,145,235,0.12)" : index % 2 ? "rgba(40,118,193,0.025)" : W,
              }}>
                <td className="px-4 py-3 text-center align-top" style={{ borderBottom: "1px solid rgba(40,118,193,0.1)" }}>
                  <span className="text-xs font-black opacity-50" style={{ color: N }}>正文</span>
                </td>
                <td colSpan={6} className="px-4 py-3" style={{ borderBottom: "1px solid rgba(40,118,193,0.1)" }}>
                  <div className="text-xs py-1" style={{ color: N }}>
                    {kw ? <HighlightText text={item.contentSnippet} keyword={kw} /> : item.contentSnippet}
                  </div>
                </td>
              </tr>
            )}
            </Fragment>
          );
        })}
      </tbody>
    </table>
  );
}

function F4DetailModal({ item, attachment, onClose }: {
  item: DocumentSummary;
  attachment: boolean;
  onClose: () => void;
}) {
  const dictionaries = useDictionaries();
  const [detail, setDetail] = useState<DocumentDetail | null>(null);
  const [error, setError] = useState<HttpError | null>(null);

  useEffect(() => {
    let active = true;
    const loader = attachment ? attachmentApi.detail : documentApi.detail;
    void loadDetailOnce(attachment ? "attachment" : "document", item.id, loader)
      .then((value) => { if (active) setDetail(value); })
      .catch((value: unknown) => { if (active) setError(value instanceof HttpError ? value : null); });
    return () => { active = false; };
  }, [attachment, item.id]);

  return (
    <div className="fixed inset-0 z-50 flex items-center justify-center" style={{ background: "rgba(26,82,153,.5)" }} onClick={onClose}>
      <div className="w-[680px] max-h-[82vh] overflow-auto shadow-2xl" style={{ background: W }} onClick={(event) => event.stopPropagation()}>
        <div className="h-11 px-5 flex items-center justify-between" style={{ background: N, color: W }}>
          <span className="font-black">详情 · {item.documentName}</span>
          <button onClick={onClose}><X size={17} /></button>
        </div>
        <div className="p-6">
          {!detail && !error && <F4State loading error={null} empty={false} />}
          {error && <F4State loading={false} error={error} empty={false} />}
          {detail && (
            <div className="space-y-4 text-sm" style={{ color: N }}>
              <p className="font-bold leading-7">{detail.summary || "暂无概述"}</p>
              <div className="grid grid-cols-2 gap-3 text-xs font-bold">
                <span>文档 ID：{detail.id}</span>
                <span>类型：{dictionaries.label("documentTypes", detail.documentType)}</span>
                <span>密级：{dictionaries.label("securityLevels", detail.securityLevel)}</span>
                <span>可见范围：{dictionaries.label("visibilityTypes", detail.visibilityType)}</span>
                <span>状态：{dictionaries.label("documentStatuses", detail.status)}</span>
                {attachment && <span>附件类型：{dictionaries.label("attachmentTypes", detail.attachmentType)}</span>}
                <span>原文件名：{detail.originalFileName}</span>
                <span>文件扩展名：{detail.fileExtension || "无"}</span>
                {attachment && <span>文件大小：{formatFileSize(detail.fileSize)}</span>}
                <span>更新时间：{formatDateTime(detail.updatedAt)}</span>
                <span>查看次数：{detail.viewCount}</span>
                <span>下载次数：{detail.downloadCount}</span>
              </div>
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

function F4ReverseModal({ attachment, onClose }: { attachment: DocumentSummary; onClose: () => void }) {
  const state = useAttachmentMainPlans(attachment.id);
  const dictionaries = useDictionaries();
  return (
    <div className="fixed inset-0 z-50 flex items-center justify-center" style={{ background: "rgba(26,82,153,.5)" }} onClick={onClose}>
      <div className="w-[560px] shadow-2xl" style={{ background: W }} onClick={(event) => event.stopPropagation()}>
        <div className="h-11 px-5 flex items-center justify-between" style={{ background: N, color: W }}>
          <span className="font-black">已挂载主案 · {attachment.documentName}</span>
          <button onClick={onClose}><X size={17} /></button>
        </div>
        <div className="p-5">
          <F4State loading={state.loading} error={state.error} empty={state.data?.length === 0} />
          {state.data?.map((plan: MainPlanBrief) => (
            <div key={plan.id} className="flex items-center justify-between px-3 py-3 border-b" style={{ borderColor: "rgba(40,118,193,.12)", color: N }}>
                      <div><div className="text-sm font-black">{plan.documentName}</div><div className="text-xs opacity-55">{plan.categoryName ?? "未分类"}</div></div>
              <LevelBadge level={dictionaries.label("securityLevels", plan.securityLevel)} />
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

function F4AttachmentLibrary({ compact = false, management = false, onPreview }: { compact?: boolean; management?: boolean; onPreview?: (item: DocumentSummary) => void }) {
  const dictionaries = useDictionaries();
  const [keyword, setKeyword] = useState("");
  const [attachmentType, setAttachmentType] = useState("");
  const [page, setPage] = useState(1);
  const [detail, setDetail] = useState<DocumentSummary | null>(null);
  const [reverse, setReverse] = useState<DocumentSummary | null>(null);
  const [localPreview, setLocalPreview] = useState<DocumentSummary | null>(null);
  const [fullView, setFullView] = useState<DocumentSummary | null>(null);
  const openPreview = onPreview ?? setLocalPreview;
  const [edit, setEdit] = useState<DocumentSummary | null>(null);
  const [remove, setRemove] = useState<DocumentSummary | null>(null);
  const [upload, setUpload] = useState(false);
  const [batch, setBatch] = useState(false);
  const [fileError, setFileError] = useState<HttpError | null>(null);
  const state = useAttachmentList({
    keyword: keyword.trim() || undefined,
    attachmentType: attachmentType ? attachmentType as keyof typeof AttachmentType : undefined,
    sortBy: "updatedAt",
    sortDirection: "desc",
    page,
    pageSize: compact ? 10 : 20,
  });
  return (
    <div className="h-full flex flex-col" style={{ background: W }}>
      <div className="flex items-center gap-2 p-3 border-b" style={{ borderColor: "rgba(40,118,193,.14)" }}>
        <Library size={16} style={{ color: B }} />
        <span className="text-sm font-black mr-2" style={{ color: N }}>独立共享附件库</span>
        <input value={keyword} onChange={(event) => { setKeyword(event.target.value); setPage(1); }} placeholder="搜索附件名称或概述"
          className="h-8 w-60 px-3 text-xs font-bold outline-none border" style={{ color: N, borderColor: "rgba(40,118,193,.25)" }} />
        <select value={attachmentType} onChange={(event) => { setAttachmentType(event.target.value); setPage(1); }}
          className="h-8 px-2 text-xs font-bold border" style={{ color: N, borderColor: "rgba(40,118,193,.25)" }}>
          <option value="">全部附件类型</option>
          {dictionaries.options("attachmentTypes").map((option) => <option key={option.code} value={option.code}>{option.label}</option>)}
        </select>
        <span className="ml-auto text-xs font-bold" style={{ color: N }}>共 {state.data?.total ?? 0} 项</span>
        {management && <><button onClick={() => setUpload(true)} className="px-3 py-1.5 text-xs font-black text-white" style={{ background: B }}>上传附件</button><button onClick={() => setBatch(true)} className="px-3 py-1.5 text-xs font-black border" style={{ color: N, borderColor: B }}>批量导入</button></>}
      </div>
      <div className="flex-1 overflow-auto">
        <F4State loading={state.loading} error={state.error} empty={state.data?.items.length === 0} />
        {state.data && state.data.items.length > 0 && (
          <F4DocumentTable
            items={state.data.items}
            onSelect={openPreview}
            onDetail={(item) => setDetail(item)}
            onReverse={setReverse}
            onView={setFullView}
            onEdit={management ? setEdit : undefined}
            onDelete={management ? setRemove : undefined}
            onFileError={setFileError}
          />
        )}
      </div>
      {state.data && state.data.totalPages > 1 && (
        <div className="p-2 flex justify-end gap-2 text-xs font-bold" style={{ color: N }}>
          <button disabled={page <= 1} onClick={() => setPage((value) => value - 1)}>上一页</button>
          <span>{page}/{state.data.totalPages}</span>
          <button disabled={page >= state.data.totalPages} onClick={() => setPage((value) => value + 1)}>下一页</button>
        </div>
      )}
      {detail && <F4DetailModal item={detail} attachment onClose={() => setDetail(null)} />}
      {reverse && <F4ReverseModal attachment={reverse} onClose={() => setReverse(null)} />}
      {!onPreview && localPreview && <FilePreviewModal item={localPreview} onClose={() => setLocalPreview(null)} />}
      {fullView && <FilePreviewModal item={fullView} mode="modal" onClose={() => setFullView(null)} />}
      {edit && <EditMetadataModal item={edit} onClose={() => setEdit(null)} />}
      {remove && <DeleteDocumentModal item={remove} onClose={() => setRemove(null)} onDeleted={() => { setDetail(null); setReverse(null); }} />}
      {upload && <SingleUploadModal kind="attachment" onClose={() => setUpload(false)} />}
      {batch && <F5BatchImportModal kind="attachment" onClose={() => setBatch(false)} />}
      {fileError && (
        <div className="fixed bottom-6 right-6 z-[70] max-w-md p-4 text-sm font-bold text-white shadow-xl" style={{ background: N }}>
          <button onClick={() => setFileError(null)} className="float-right ml-3"><X size={14} /></button>
          {fileError.message}{fileError.requestId ? `（错误编号：${fileError.requestId}）` : ""}
        </div>
      )}
    </div>
  );
}

function RealMainPage({ onNav, navigationItems, onLogout, logoutPending, currentUser }: AuthenticatedPageProps) {
  const [keyword, setKeyword] = useState("");
  const [updatedFrom, setUpdatedFrom] = useState("");
  const [updatedTo, setUpdatedTo] = useState("");
  const [categoryId, setCategoryId] = useState<string | null>(null);
  const [selectedTags, setSelectedTags] = useState<string[]>([]);
  const [allCategoryCount, setAllCategoryCount] = useState<number | null>(null);
  const [page, setPage] = useState(1);
  const [selected, setSelected] = useState<DocumentSummary | null>(null);
  const [lowerTab, setLowerTab] = useState<"sub" | "mounted">("sub");
  const [subKeyword, setSubKeyword] = useState("");
  const [subStatus, setSubStatus] = useState<DocumentStatusCode | "">("");
  const [subUpdatedFrom, setSubUpdatedFrom] = useState("");
  const [subUpdatedTo, setSubUpdatedTo] = useState("");
  const [subPage, setSubPage] = useState(1);
  const [mountedKeyword, setMountedKeyword] = useState("");
  const [mountedType, setMountedType] = useState<AttachmentTypeCode | "">("");
  const [mountedUpdatedFrom, setMountedUpdatedFrom] = useState("");
  const [mountedUpdatedTo, setMountedUpdatedTo] = useState("");
  const [mountedPage, setMountedPage] = useState(1);
  const [detail, setDetail] = useState<{ item: DocumentSummary; attachment: boolean } | null>(null);
  const [preview, setPreview] = useState<DocumentSummary | null>(null);
  const [fullView, setFullView] = useState<DocumentSummary | null>(null);
  const [fileError, setFileError] = useState<HttpError | null>(null);
  const mainRange = useMemo(() => toUpdatedTimeRange(updatedFrom, updatedTo), [updatedFrom, updatedTo]);
  const subRange = useMemo(() => toUpdatedTimeRange(subUpdatedFrom, subUpdatedTo), [subUpdatedFrom, subUpdatedTo]);
  const mountedRange = useMemo(() => toUpdatedTimeRange(mountedUpdatedFrom, mountedUpdatedTo), [mountedUpdatedFrom, mountedUpdatedTo]);
  const documents = useDocumentList({
    documentType: DocumentType.MAIN,
    categoryId: categoryId ?? undefined,
    includeDescendants: categoryId ? true : undefined,
    keyword: keyword.trim() || undefined,
    tags: selectedTags.length > 0 ? selectedTags : undefined,
    ...mainRange.query,
    sortBy: "documentName",
    sortDirection: "asc",
    page,
    pageSize: 20,
  }, !mainRange.error);
  const tagsState = useDocumentTags();
  const selectedMainId = selected?.documentType === DocumentType.MAIN ? selected.id : null;
  const subPlans = useSubPlanList(selectedMainId, {
    keyword: subKeyword.trim() || undefined,
    status: subStatus || undefined,
    ...subRange.query,
    page: subPage,
    pageSize: 20,
    sortBy: "documentName",
    sortDirection: "asc",
  }, !subRange.error);
  const mounted = useMountedAttachments(selectedMainId, {
    keyword: mountedKeyword.trim() || undefined,
    attachmentType: mountedType || undefined,
    ...mountedRange.query,
    page: mountedPage,
    pageSize: 20,
  }, !mountedRange.error);

  useEffect(() => {
    if (categoryId === null && documents.data) setAllCategoryCount(documents.data.total);
  }, [categoryId, documents.data]);

  const toggleTag = useCallback((tag: string) => {
    setSelectedTags((current) =>
      current.includes(tag)
        ? current.filter((item) => item !== tag)
        : [...current, tag],
    );
    setPage(1);
    setSelected(null);
  }, []);

  useEffect(() => {
    setSubKeyword("");
    setSubStatus("");
    setSubUpdatedFrom("");
    setSubUpdatedTo("");
    setSubPage(1);
    setMountedKeyword("");
    setMountedType("");
    setMountedUpdatedFrom("");
    setMountedUpdatedTo("");
    setMountedPage(1);
  }, [selectedMainId]);

  return (
    <PageFrame title="方案管理系统" current="main" navigationItems={navigationItems} onNav={onNav} onLogout={onLogout} logoutPending={logoutPending} currentUser={currentUser}>
      <LeftPanel allCount={allCategoryCount} selectedCategoryId={categoryId} onSelectCategory={(id) => { setCategoryId(id); setPage(1); setSelected(null); setPreview(null); }}>
        <TagPanel
          tags={tagsState.data}
          selectedTags={selectedTags}
          loading={tagsState.loading}
          error={tagsState.error?.message ?? null}
          onToggleTag={toggleTag}
          onClear={() => { setSelectedTags([]); setPage(1); setSelected(null); }}
          onRetry={tagsState.retry}
        />
      </LeftPanel>
      <div className="flex-1 flex flex-col m-3 ml-0 min-h-0">
        <DocumentSearchToolbar
          ariaLabel="方案检索"
          keyword={keyword}
          keywordPlaceholder="搜索方案名称、概述、标签或文档内容"
          updatedFrom={updatedFrom}
          updatedTo={updatedTo}
          error={mainRange.error}
          onKeywordChange={(value) => { setKeyword(value); setPage(1); setSelected(null); }}
          onUpdatedFromChange={(value) => { setUpdatedFrom(value); setPage(1); setSelected(null); }}
          onUpdatedToChange={(value) => { setUpdatedTo(value); setPage(1); setSelected(null); }}
          onClear={() => { setKeyword(""); setUpdatedFrom(""); setUpdatedTo(""); setPage(1); setSelected(null); }}
        />
        <div className="flex-1 overflow-auto min-h-0 mb-2" style={{ background: W, border: "1px solid rgba(40,118,193,.18)" }}>
          <F4State loading={documents.loading} error={documents.error} empty={documents.data?.items.length === 0} onRetry={documents.retry} />
          {documents.data && documents.data.items.length > 0 && <F4DocumentTable items={documents.data.items} keyword={keyword} selectedId={selected?.id} onSelect={(item) => { setSelected(item); setPreview(item); }} onDetail={(item, attachment) => setDetail({ item, attachment })} onView={setFullView} onFileError={setFileError} />}
          {documents.data && <F4Pager page={page} totalPages={documents.data.totalPages} onPage={(value) => { setPage(value); setSelected(null); }} />}
        </div>
        <div className="h-[390px] flex-shrink-0 flex flex-col" style={{ background: W, border: "1px solid rgba(40,118,193,.18)" }}>
          <div className="flex border-b" style={{ borderColor: "rgba(40,118,193,.14)" }}>
            {([{ id: "sub", label: "子方案" }, { id: "mounted", label: "配套资料" }] as const).map((item) => (
              <button key={item.id} onClick={() => setLowerTab(item.id)} className="px-6 py-2.5 text-sm font-black" style={{ color: lowerTab === item.id ? W : N, background: lowerTab === item.id ? N : W }}>{item.label}</button>
            ))}
            <span className="ml-auto px-4 py-3 text-xs font-bold" style={{ color: N }}>{selected ? `当前：${selected.documentName}` : "请选择一个主案"}</span>
          </div>
          <div className="flex-1 overflow-auto min-h-0">
            {!selected || selected.documentType !== DocumentType.MAIN ? (
              <div className="p-12 text-center text-sm font-bold opacity-50" style={{ color: N }}>请选择主案查看关联数据</div>
            ) : lowerTab === "sub" ? (
              <>
                <DocumentSearchToolbar ariaLabel="子方案检索" compact keyword={subKeyword} keywordPlaceholder="搜索方案名称、概述、标签或文档内容" updatedFrom={subUpdatedFrom} updatedTo={subUpdatedTo} error={subRange.error}
                  selectFilter={{ dictionary: "documentStatuses", value: subStatus, emptyLabel: "全部状态", ariaLabel: "子方案状态", onChange: (value) => { setSubStatus(value as DocumentStatusCode | ""); setSubPage(1); } }}
                  onKeywordChange={(value) => { setSubKeyword(value); setSubPage(1); }} onUpdatedFromChange={(value) => { setSubUpdatedFrom(value); setSubPage(1); }} onUpdatedToChange={(value) => { setSubUpdatedTo(value); setSubPage(1); }}
                  onClear={() => { setSubKeyword(""); setSubStatus(""); setSubUpdatedFrom(""); setSubUpdatedTo(""); setSubPage(1); }} />
                <F4State loading={subPlans.loading} error={subPlans.error} empty={subPlans.data?.items.length === 0} onRetry={subPlans.retry} />
                {subPlans.data?.items.length ? <F4DocumentTable items={subPlans.data.items} keyword={subKeyword} onSelect={setPreview} onDetail={(item, attachment) => setDetail({ item, attachment })} onView={setFullView} onFileError={setFileError} /> : null}
                {subPlans.data && <F4Pager page={subPage} totalPages={subPlans.data.totalPages} onPage={setSubPage} />}
              </>
            ) : (
              <>
                <DocumentSearchToolbar ariaLabel="已挂载附件检索" compact keyword={mountedKeyword} keywordPlaceholder="搜索附件名称或概述" updatedFrom={mountedUpdatedFrom} updatedTo={mountedUpdatedTo} error={mountedRange.error}
                  selectFilter={{ dictionary: "attachmentTypes", value: mountedType, emptyLabel: "全部附件类型", ariaLabel: "已挂载附件类型", onChange: (value) => { setMountedType(value as AttachmentTypeCode | ""); setMountedPage(1); } }}
                  onKeywordChange={(value) => { setMountedKeyword(value); setMountedPage(1); }} onUpdatedFromChange={(value) => { setMountedUpdatedFrom(value); setMountedPage(1); }} onUpdatedToChange={(value) => { setMountedUpdatedTo(value); setMountedPage(1); }}
                  onClear={() => { setMountedKeyword(""); setMountedType(""); setMountedUpdatedFrom(""); setMountedUpdatedTo(""); setMountedPage(1); }} />
                <F4State loading={mounted.loading} error={mounted.error} empty={mounted.data?.items.length === 0} onRetry={mounted.retry} />
                {mounted.data?.items.length ? <F4DocumentTable items={mounted.data.items} bindingMode onSelect={setPreview} onDetail={(item) => setDetail({ item, attachment: true })} onView={setFullView} onFileError={setFileError} /> : null}
                {mounted.data && <F4Pager page={mountedPage} totalPages={mounted.data.totalPages} onPage={setMountedPage} />}
              </>
            )}
          </div>
        </div>
      </div>
      {detail && <F4DetailModal {...detail} onClose={() => setDetail(null)} />}
      {preview && <FilePreviewModal item={preview} onClose={() => setPreview(null)} />}
      {fullView && <FilePreviewModal item={fullView} mode="modal" onClose={() => setFullView(null)} />}
      {fileError && <div className="fixed bottom-6 right-6 z-[70] max-w-md p-4 text-sm font-bold text-white shadow-xl" style={{ background: N }}><button onClick={() => setFileError(null)} className="float-right ml-3"><X size={14} /></button>{fileError.message}{fileError.requestId ? `（错误编号：${fileError.requestId}）` : ""}</div>}
    </PageFrame>
  );
}

function RealAdminPage({ onNav, navigationItems, onLogout, logoutPending, currentUser }: AuthenticatedPageProps) {
  const [mode, setMode] = useState<"plans" | "attachments">("plans");
  const [categoryId, setCategoryId] = useState<string | null>(null);
  const [allCategoryCount, setAllCategoryCount] = useState<number | null>(null);
  const [keyword, setKeyword] = useState("");
  const [updatedFrom, setUpdatedFrom] = useState("");
  const [updatedTo, setUpdatedTo] = useState("");
  const [planPage, setPlanPage] = useState(1);
  const [selected, setSelected] = useState<DocumentSummary | null>(null);
  const [relationTab, setRelationTab] = useState<"sub" | "mounted">("mounted");
  const [subKeyword, setSubKeyword] = useState("");
  const [subStatus, setSubStatus] = useState<DocumentStatusCode | "">("");
  const [subUpdatedFrom, setSubUpdatedFrom] = useState("");
  const [subUpdatedTo, setSubUpdatedTo] = useState("");
  const [subPage, setSubPage] = useState(1);
  const [mountedKeyword, setMountedKeyword] = useState("");
  const [mountedType, setMountedType] = useState<AttachmentTypeCode | "">("");
  const [mountedUpdatedFrom, setMountedUpdatedFrom] = useState("");
  const [mountedUpdatedTo, setMountedUpdatedTo] = useState("");
  const [mountedPage, setMountedPage] = useState(1);
  const [detail, setDetail] = useState<{ item: DocumentSummary; attachment: boolean } | null>(null);
  const [preview, setPreview] = useState<DocumentSummary | null>(null);
  const [fullView, setFullView] = useState<DocumentSummary | null>(null);
  const [edit, setEdit] = useState<DocumentSummary | null>(null);
  const [remove, setRemove] = useState<DocumentSummary | null>(null);
  const [uploadDocument, setUploadDocument] = useState(false);
  const [batchDocuments, setBatchDocuments] = useState(false);
  const [fileError, setFileError] = useState<HttpError | null>(null);
  const [bindingPlan, setBindingPlan] = useState<DocumentSummary | null>(null);
  const [permission, setPermission] = useState<DocumentSummary | null>(null);
  const [unbind, setUnbind] = useState<AttachmentBindingSummary | null>(null);
  const [recycleBinOpen, setRecycleBinOpen] = useState(false);
  const mainRange = useMemo(() => toUpdatedTimeRange(updatedFrom, updatedTo), [updatedFrom, updatedTo]);
  const subRange = useMemo(() => toUpdatedTimeRange(subUpdatedFrom, subUpdatedTo), [subUpdatedFrom, subUpdatedTo]);
  const mountedRange = useMemo(() => toUpdatedTimeRange(mountedUpdatedFrom, mountedUpdatedTo), [mountedUpdatedFrom, mountedUpdatedTo]);
  const plans = useDocumentList({ documentType: DocumentType.MAIN, categoryId: categoryId ?? undefined, includeDescendants: categoryId ? true : undefined, keyword: keyword.trim() || undefined, ...mainRange.query, sortBy: "updatedAt", sortDirection: "desc", page: planPage, pageSize: 20 }, !mainRange.error);
  const subPlans = useSubPlanList(selected?.id ?? null, { keyword: subKeyword.trim() || undefined, status: subStatus || undefined, ...subRange.query, page: subPage, pageSize: 20, sortBy: "documentName", sortDirection: "asc" }, !subRange.error);
  const mounted = useMountedAttachments(selected?.id ?? null, { keyword: mountedKeyword.trim() || undefined, attachmentType: mountedType || undefined, ...mountedRange.query, page: mountedPage, pageSize: 20 }, !mountedRange.error);

  useEffect(() => {
    if (categoryId === null && plans.data) setAllCategoryCount(plans.data.total);
  }, [categoryId, plans.data]);

  useEffect(() => {
    setSubKeyword("");
    setSubStatus("");
    setSubUpdatedFrom("");
    setSubUpdatedTo("");
    setSubPage(1);
    setMountedKeyword("");
    setMountedType("");
    setMountedUpdatedFrom("");
    setMountedUpdatedTo("");
    setMountedPage(1);
  }, [selected?.id]);

  useEffect(() => {
    if (!selected || !plans.data) return;
    const refreshed = plans.data.items.find((item) => item.id === selected.id);
    if (refreshed && refreshed !== selected) setSelected(refreshed);
  }, [plans.data, selected]);

  return (
    <PageFrame title="方案管理系统后台" current="admin" navigationItems={navigationItems} onNav={onNav} onLogout={onLogout} logoutPending={logoutPending} currentUser={currentUser}>
      {mode === "plans" ? <LeftPanel allCount={allCategoryCount} admin selectedCategoryId={categoryId} onSelectCategory={(id) => { setCategoryId(id); setPlanPage(1); setSelected(null); setPreview(null); }} /> : <div className="w-[272px] m-3 p-5 flex-shrink-0" style={{ background: W, color: N }}><Library size={22} className="mb-3" /><div className="font-black">共享附件</div></div>}
      <div className="flex-1 flex flex-col m-3 ml-0 min-h-0">
        <DocumentStatisticsCards />
        <div className="flex items-center mb-2" style={{ background: W, border: "1px solid rgba(40,118,193,.18)" }}>
          <button onClick={() => setMode("plans")} className="px-7 py-3 text-sm font-black" style={{ color: mode === "plans" ? W : N, background: mode === "plans" ? N : W }}>方案管理</button>
          <button onClick={() => setMode("attachments")} className="px-7 py-3 text-sm font-black" style={{ color: mode === "attachments" ? W : N, background: mode === "attachments" ? N : W }}>共享附件库</button>
          <div className="ml-auto flex gap-2 mr-3">
            <button onClick={() => setRecycleBinOpen(true)} className="px-3 py-1.5 text-xs font-black border" style={{ color: N, borderColor: B }}><Trash2 size={12} className="inline mr-1" />回收站</button>
            {mode === "plans" && <><button onClick={() => setUploadDocument(true)} className="px-3 py-1.5 text-xs font-black text-white" style={{ background: B }}>上传主案/子案</button><button onClick={() => setBatchDocuments(true)} className="px-3 py-1.5 text-xs font-black border" style={{ color: N, borderColor: B }}>批量导入方案</button></>}
          </div>
        </div>
        {mode === "attachments" ? <div className="flex-1 min-h-0"><F4AttachmentLibrary management onPreview={setPreview} /></div> : (
          <>
            <DocumentSearchToolbar ariaLabel="后台主案检索" keyword={keyword} keywordPlaceholder="搜索方案名称、概述、标签或文档内容" updatedFrom={updatedFrom} updatedTo={updatedTo} error={mainRange.error}
              onKeywordChange={(value) => { setKeyword(value); setPlanPage(1); setSelected(null); }} onUpdatedFromChange={(value) => { setUpdatedFrom(value); setPlanPage(1); setSelected(null); }} onUpdatedToChange={(value) => { setUpdatedTo(value); setPlanPage(1); setSelected(null); }}
              onClear={() => { setKeyword(""); setUpdatedFrom(""); setUpdatedTo(""); setPlanPage(1); setSelected(null); }} />
            <div className="flex-1 overflow-auto min-h-0 mb-2" style={{ background: W, border: "1px solid rgba(40,118,193,.18)" }}>
              <F4State loading={plans.loading} error={plans.error} empty={plans.data?.items.length === 0} onRetry={plans.retry} />
              {plans.data?.items.length ? <F4DocumentTable items={plans.data.items} keyword={keyword} selectedId={selected?.id} onSelect={(item) => { setSelected(item); setPreview(item); }} onDetail={(item, attachment) => setDetail({ item, attachment })} onView={setFullView} onEdit={setEdit} onDelete={setRemove} onPermission={setPermission} onFileError={setFileError} /> : null}
              {plans.data && <F4Pager page={planPage} totalPages={plans.data.totalPages} onPage={(value) => { setPlanPage(value); setSelected(null); }} />}
            </div>
            <div className="h-[400px] flex-shrink-0 flex flex-col" style={{ background: W, border: "1px solid rgba(40,118,193,.18)" }}>
              <div className="flex border-b" style={{ borderColor: "rgba(40,118,193,.14)" }}>
                <button onClick={() => setRelationTab("sub")} className="px-7 py-2.5 text-sm font-black" style={{ color: relationTab === "sub" ? W : N, background: relationTab === "sub" ? N : W }}>子方案</button>
                <button onClick={() => setRelationTab("mounted")} className="px-7 py-2.5 text-sm font-black" style={{ color: relationTab === "mounted" ? W : N, background: relationTab === "mounted" ? N : W }}>配套资料</button>
                {selected?.documentType === DocumentType.MAIN && selected.allowedActions.includes(AllowedAction.BIND_ATTACHMENT) && <button onClick={() => setBindingPlan(selected)} className="ml-3 my-1.5 px-3 text-xs font-black text-white" style={{ background: B }}>挂载资料</button>}
                {selected?.documentType === DocumentType.MAIN && selected.allowedActions.includes(AllowedAction.CONFIG_PERMISSION) && <button onClick={() => setPermission(selected)} className="my-1.5 ml-2 px-3 text-xs font-black border" style={{ color: N, borderColor: B }}>权限配置</button>}
                <span className="ml-auto px-4 py-3 text-xs font-bold" style={{ color: N }}>{selected ? `当前主案：${selected.documentName}` : "请选择主案"}</span>
              </div>
              <div className="flex-1 overflow-auto min-h-0">
                {!selected ? <div className="p-12 text-center text-sm font-bold opacity-50" style={{ color: N }}>请选择主案</div> : relationTab === "sub" ? (
                  <>
                    <DocumentSearchToolbar ariaLabel="后台子方案检索" compact keyword={subKeyword} keywordPlaceholder="搜索方案名称、概述、标签或文档内容" updatedFrom={subUpdatedFrom} updatedTo={subUpdatedTo} error={subRange.error}
                      selectFilter={{ dictionary: "documentStatuses", value: subStatus, emptyLabel: "全部状态", ariaLabel: "后台子方案状态", onChange: (value) => { setSubStatus(value as DocumentStatusCode | ""); setSubPage(1); } }}
                      onKeywordChange={(value) => { setSubKeyword(value); setSubPage(1); }} onUpdatedFromChange={(value) => { setSubUpdatedFrom(value); setSubPage(1); }} onUpdatedToChange={(value) => { setSubUpdatedTo(value); setSubPage(1); }}
                      onClear={() => { setSubKeyword(""); setSubStatus(""); setSubUpdatedFrom(""); setSubUpdatedTo(""); setSubPage(1); }} />
                    <F4State loading={subPlans.loading} error={subPlans.error} empty={subPlans.data?.items.length === 0} onRetry={subPlans.retry} />
                    <div className="px-4 py-2 text-xs font-bold" style={{ color: N }}>权限继承自主案：{selected.documentName}</div>
                    {subPlans.data?.items.length ? <F4DocumentTable items={subPlans.data.items} keyword={subKeyword} onSelect={setPreview} onDetail={(item, attachment) => setDetail({ item, attachment })} onView={setFullView} onEdit={setEdit} onDelete={setRemove} onFileError={setFileError} /> : null}
                    {subPlans.data && <F4Pager page={subPage} totalPages={subPlans.data.totalPages} onPage={setSubPage} />}
                  </>
                ) : (
                  <>
                    <DocumentSearchToolbar ariaLabel="后台已挂载附件检索" compact keyword={mountedKeyword} keywordPlaceholder="搜索附件名称或概述" updatedFrom={mountedUpdatedFrom} updatedTo={mountedUpdatedTo} error={mountedRange.error}
                      selectFilter={{ dictionary: "attachmentTypes", value: mountedType, emptyLabel: "全部附件类型", ariaLabel: "后台已挂载附件类型", onChange: (value) => { setMountedType(value as AttachmentTypeCode | ""); setMountedPage(1); } }}
                      onKeywordChange={(value) => { setMountedKeyword(value); setMountedPage(1); }} onUpdatedFromChange={(value) => { setMountedUpdatedFrom(value); setMountedPage(1); }} onUpdatedToChange={(value) => { setMountedUpdatedTo(value); setMountedPage(1); }}
                      onClear={() => { setMountedKeyword(""); setMountedType(""); setMountedUpdatedFrom(""); setMountedUpdatedTo(""); setMountedPage(1); }} />
                    <F4State loading={mounted.loading} error={mounted.error} empty={mounted.data?.items.length === 0} onRetry={mounted.retry} />
                    {mounted.data?.items.length ? <F4DocumentTable items={mounted.data.items} bindingMode onSelect={setPreview} onDetail={(item) => setDetail({ item, attachment: true })} onView={setFullView} onEdit={setEdit} onDelete={setRemove} onUnbind={selected.allowedActions.includes(AllowedAction.UNBIND_ATTACHMENT) ? setUnbind : undefined} onFileError={setFileError} /> : null}
                    {mounted.data && <F4Pager page={mountedPage} totalPages={mounted.data.totalPages} onPage={setMountedPage} />}
                  </>
                )}
              </div>
            </div>
          </>
        )}
      </div>
      {detail && <F4DetailModal {...detail} onClose={() => setDetail(null)} />}
      {preview && <FilePreviewModal item={preview} onClose={() => setPreview(null)} />}
      {fullView && <FilePreviewModal item={fullView} mode="modal" onClose={() => setFullView(null)} />}
      {edit && <EditMetadataModal item={edit} onClose={() => setEdit(null)} />}
      {remove && <DeleteDocumentModal item={remove} onClose={() => setRemove(null)} onDeleted={() => { if (selected?.id === remove.id) setSelected(null); setDetail(null); }} />}
      {uploadDocument && <SingleUploadModal kind="document" onClose={() => setUploadDocument(false)} />}
      {batchDocuments && <F5BatchImportModal kind="document" onClose={() => setBatchDocuments(false)} />}
      {bindingPlan && <AttachmentBindingModal mainPlan={bindingPlan} onClose={() => setBindingPlan(null)} onChanged={() => { setRelationTab("mounted"); }} />}
      {permission && <F6PermissionModal document={permission} onClose={() => setPermission(null)} onChanged={() => undefined} />}
      {selected && unbind && <UnbindAttachmentModal mainPlan={selected} attachment={unbind} onClose={() => setUnbind(null)} onChanged={() => { setRelationTab("mounted"); }} />}
      {recycleBinOpen && <RecycleBinModal onClose={() => setRecycleBinOpen(false)} />}
      {fileError && <div className="fixed bottom-6 right-6 z-[70] max-w-md p-4 text-sm font-bold text-white shadow-xl" style={{ background: N }}><button onClick={() => setFileError(null)} className="float-right ml-3"><X size={14} /></button>{fileError.message}{fileError.requestId ? `（错误编号：${fileError.requestId}）` : ""}</div>}
    </PageFrame>
  );
}

export default function App() {
  const dictionaries = useDictionaries();
  const [page, setPage] = useState<Page>("login");
  const [currentUser, setCurrentUser] = useState<UserSummary | null>(null);
  const [authStatus, setAuthStatus] = useState<"unauthenticated" | "restoring" | "authenticated" | "recoveryError">(
    tokenStore.getAccessToken() ? "restoring" : "unauthenticated",
  );
  const [authNotice, setAuthNotice] = useState("");
  const [recoveryError, setRecoveryError] = useState<{ message: string; requestId: string } | null>(null);
  const [logoutPending, setLogoutPending] = useState(false);

  const restoreCurrentSession = useCallback(async () => {
    if (!tokenStore.getAccessToken()) {
      setCurrentUser(null);
      setPage("login");
      setAuthStatus("unauthenticated");
      return;
    }

    setAuthStatus("restoring");
    setRecoveryError(null);
    try {
      const user = await restoreSession();
      if (!user) {
        setCurrentUser(null);
        setPage("login");
        setAuthStatus("unauthenticated");
        return;
      }
      setCurrentUser(user);
      setPage(getFirstAllowedPage(user.allowedModules) ?? "login");
      setAuthNotice("");
      setAuthStatus("authenticated");
    } catch (error) {
      if (error instanceof HttpError && error.httpStatus === 401) return;
      setRecoveryError({
        message: error instanceof HttpError ? error.message : "无法连接认证服务",
        requestId: error instanceof HttpError ? error.requestId : "",
      });
      setAuthStatus("recoveryError");
    }
  }, []);

  useEffect(() => {
    const unsubscribe = subscribeAuthExpired(({ requestId }) => {
      dictionaries.clear();
      setCurrentUser(null);
      setPage("login");
      setRecoveryError(null);
      setAuthNotice(`登录状态已失效，请重新登录${requestId ? `（错误编号：${requestId}）` : ""}`);
      setAuthStatus("unauthenticated");
    });
    if (tokenStore.getAccessToken()) void restoreCurrentSession();
    return unsubscribe;
  }, [restoreCurrentSession]);

  useEffect(() => {
    if (authStatus === "authenticated" && currentUser) {
      void dictionaries.load(currentUser.id).catch(() => undefined);
    }
  }, [authStatus, currentUser?.id]);

  const handleLogin = async (request: LoginRequest) => {
    const user = await loginSession(request);
    setCurrentUser(user);
    setPage(getFirstAllowedPage(user.allowedModules) ?? "login");
    setAuthNotice("");
    setRecoveryError(null);
    setAuthStatus("authenticated");
  };

  const handleLogout = async () => {
    if (logoutPending) return;
    setLogoutPending(true);
    const result = await logoutSession();
    dictionaries.clear();
    setCurrentUser(null);
    setPage("login");
    setRecoveryError(null);
    setAuthStatus("unauthenticated");
    if (result.serverConfirmed) {
      setAuthNotice("");
    } else {
      const requestId = result.error instanceof HttpError ? result.error.requestId : "";
      setAuthNotice(`已退出本机，服务端退出未确认${requestId ? `（错误编号：${requestId}）` : ""}`);
    }
    setLogoutPending(false);
  };

  const navigationItems =
    currentUser && dictionaries.status === "success"
      ? getNavigationItems(
          currentUser.allowedModules,
          (module) => dictionaries.label("allowedModules", module),
        )
      : [];
  const handleNavigation = (target: AuthenticatedPage) => {
    if (!currentUser) return;
    if (isPageAllowed(target, currentUser.allowedModules)) {
      setPage(target);
      return;
    }
    setPage(getFirstAllowedPage(currentUser.allowedModules) ?? "login");
  };

  if (authStatus === "restoring") {
    return <AuthStateScreen title="正在恢复登录状态" description="正在验证现有登录凭证，请稍候。" primaryLabel="" onPrimary={() => { }} busy />;
  }

  if (authStatus === "recoveryError") {
    return (
      <AuthStateScreen
        title="登录状态恢复失败"
        description={recoveryError?.message ?? "认证服务暂时不可用，本地Token尚未清除。"}
        requestId={recoveryError?.requestId}
        primaryLabel="重试"
        onPrimary={() => void restoreCurrentSession()}
        secondaryLabel="返回登录页"
        onSecondary={() => {
          setCurrentUser(null);
          setPage("login");
          setAuthNotice("会话尚未恢复，您可以重新登录；本地Token未因网络错误被自动清除。");
          setAuthStatus("unauthenticated");
        }}
      />
    );
  }

  if (authStatus === "unauthenticated" || !currentUser) {
    return <LoginPage onLogin={handleLogin} notice={authNotice} />;
  }

  if (dictionaries.status === "idle" || dictionaries.status === "loading") {
    return <AuthStateScreen title="正在加载系统字典" description="正在加载界面中文标签，请稍候。" primaryLabel="" onPrimary={() => { }} busy />;
  }

  if (dictionaries.status === "error") {
    return (
      <AuthStateScreen
        title="系统字典加载失败"
        description={dictionaries.error?.message ?? "系统字典暂时不可用，关键表单已禁用。"}
        requestId={dictionaries.error?.requestId}
        primaryLabel="重试"
        onPrimary={() => void dictionaries.retry(currentUser.id).catch(() => undefined)}
        secondaryLabel="退出登录"
        onSecondary={() => void handleLogout()}
      />
    );
  }

  if (navigationItems.length === 0) {
    return (
      <AuthStateScreen
        title="暂无可用模块"
        description="当前账号已通过认证，但后端未授予任何可访问模块。"
        primaryLabel={logoutPending ? "退出中" : "退出登录"}
        onPrimary={() => void handleLogout()}
        busy={logoutPending}
      />
    );
  }

  const pageProps: AuthenticatedPageProps = {
    onNav: handleNavigation,
    navigationItems,
    onLogout: () => void handleLogout(),
    logoutPending,
    currentUser,
  };

  return (
    <div style={{ fontFamily: "'Noto Sans SC', sans-serif" }}>
      {page === "main" && <RealMainPage {...pageProps} />}
      {page === "admin" && currentUser.roleCode === RoleCode.ADMIN && (
        <RealAdminPage {...pageProps} />
      )}
      {page === "audit" && currentUser.roleCode === RoleCode.ADMIN && (
        <PageFrame title="操作日志审计" current="audit" {...pageProps}>
          <AuditDashboard />
        </PageFrame>
      )}
      {((page === "admin" && currentUser.roleCode !== RoleCode.ADMIN) ||
        (page === "audit" && currentUser.roleCode !== RoleCode.ADMIN)) && (
        <PageFrame
          title="模块权限异常"
          current={page}
          {...pageProps}
        >
          <div
            className="m-3 p-8 flex-1 text-sm font-bold"
            style={{ background: W, color: N }}
          >
            当前账号无法访问该模块，请联系管理员。
          </div>
        </PageFrame>
      )}
    </div>
  );
}
