|
|
@@ -28,7 +28,6 @@ import { categoryApi } from "../features/categories/categoryApi";
|
|
|
import { useCategoryTree } from "../features/categories/categoryState";
|
|
|
import {
|
|
|
findCategoryNode,
|
|
|
- getCategoryBranchMainPlanCount,
|
|
|
getCategoryParentOptions,
|
|
|
retainExistingCategoryIds,
|
|
|
} from "../features/categories/categoryTree";
|
|
|
@@ -345,7 +344,7 @@ function CategoryTreeNode({ node, depth, expanded, onToggle, selectedId, onSelec
|
|
|
<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">({getCategoryBranchMainPlanCount(node)})</span>
|
|
|
+ <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()}>
|
|
|
@@ -376,10 +375,11 @@ function CategoryTreeNode({ node, depth, expanded, onToggle, selectedId, onSelec
|
|
|
);
|
|
|
}
|
|
|
|
|
|
-function CategoryPanel({ admin, selectedCategoryId, onSelectCategory }: {
|
|
|
+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());
|
|
|
@@ -507,6 +507,7 @@ function CategoryPanel({ admin, selectedCategoryId, onSelectCategory }: {
|
|
|
</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
|
|
|
@@ -650,11 +651,12 @@ function CategoryFormField({ label, children }: { label: string; children: React
|
|
|
}
|
|
|
|
|
|
// ─── Shared left panel ────────────────────────────────────────────────────────
|
|
|
-function LeftPanel({ children, admin, selectedCategoryId, onSelectCategory }: {
|
|
|
+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)` }}>
|
|
|
@@ -667,6 +669,7 @@ function LeftPanel({ children, admin, selectedCategoryId, onSelectCategory }: {
|
|
|
admin={admin}
|
|
|
selectedCategoryId={selectedCategoryId}
|
|
|
onSelectCategory={onSelectCategory}
|
|
|
+ allCount={allCount}
|
|
|
/>
|
|
|
{children}
|
|
|
</div>
|
|
|
@@ -1071,13 +1074,14 @@ function PermissionModal({ doc, onClose }: { doc: Doc; onClose: () => void }) {
|
|
|
}
|
|
|
|
|
|
// ─── Page frame (shared header + background) ──────────────────────────────────
|
|
|
-function PageFrame({ title, current, navigationItems, onNav, onLogout, logoutPending, children }: {
|
|
|
+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 (
|
|
|
@@ -1085,11 +1089,9 @@ function PageFrame({ title, current, navigationItems, onNav, onLogout, logoutPen
|
|
|
{/* 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="flex items-center gap-1 opacity-0 pointer-events-none" aria-hidden>
|
|
|
- <span className="px-3 py-1.5 text-sm font-black whitespace-nowrap">文档浏览</span>
|
|
|
- <span className="px-3 py-1.5 text-sm font-black whitespace-nowrap">后台管理</span>
|
|
|
- <span className="px-3 py-1.5 text-sm font-black whitespace-nowrap">日志审计</span>
|
|
|
- <span className="px-2 py-1.5 text-sm font-black whitespace-nowrap">退出</span>
|
|
|
+ <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"
|
|
|
@@ -1120,7 +1122,7 @@ function PageFrame({ title, current, navigationItems, onNav, onLogout, logoutPen
|
|
|
style={{ height: 20, background: `linear-gradient(to bottom, ${B} 0%, ${PBG} 100%)` }} />
|
|
|
|
|
|
{/* Main content — sits below header */}
|
|
|
- <div className="relative z-10 flex flex-1 min-h-0">
|
|
|
+ <div className="relative z-40 flex flex-1 min-h-0">
|
|
|
{children}
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -1229,9 +1231,10 @@ interface AuthenticatedPageProps {
|
|
|
navigationItems: NavigationItem[];
|
|
|
onLogout: () => void;
|
|
|
logoutPending: boolean;
|
|
|
+ currentUser: UserSummary;
|
|
|
}
|
|
|
|
|
|
-function MainPage({ onNav, navigationItems, onLogout, logoutPending }: AuthenticatedPageProps) {
|
|
|
+function MainPage({ onNav, navigationItems, onLogout, logoutPending, currentUser }: AuthenticatedPageProps) {
|
|
|
const [tab, setTab] = useState<MainTab>("主案列表");
|
|
|
const [searchMode, setSearchMode] = useState(false);
|
|
|
const [keyword, setKeyword] = useState("");
|
|
|
@@ -1278,7 +1281,7 @@ function MainPage({ onNav, navigationItems, onLogout, logoutPending }: Authentic
|
|
|
: [];
|
|
|
|
|
|
return (
|
|
|
- <PageFrame title="方案管理系统" current="main" navigationItems={navigationItems} onNav={onNav} onLogout={onLogout} logoutPending={logoutPending}>
|
|
|
+ <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 栏 */}
|
|
|
@@ -1690,7 +1693,7 @@ function MountAttachmentModal({ plan, attachments, mountedIds, onConfirm, onClos
|
|
|
);
|
|
|
}
|
|
|
|
|
|
-function AdminPage({ onNav, navigationItems, onLogout, logoutPending }: AuthenticatedPageProps) {
|
|
|
+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);
|
|
|
@@ -1739,7 +1742,7 @@ function AdminPage({ onNav, navigationItems, onLogout, logoutPending }: Authenti
|
|
|
};
|
|
|
|
|
|
return (
|
|
|
- <PageFrame title="方案管理系统后台" current="admin" navigationItems={navigationItems} onNav={onNav} onLogout={onLogout} logoutPending={logoutPending}>
|
|
|
+ <PageFrame title="方案管理系统后台" current="admin" navigationItems={navigationItems} onNav={onNav} onLogout={onLogout} logoutPending={logoutPending} currentUser={currentUser}>
|
|
|
{adminMode === "plans"
|
|
|
? <LeftPanel admin />
|
|
|
: <AttachmentTypePanel selected={attachmentType} onSelect={setAttachmentType} />}
|
|
|
@@ -1857,7 +1860,7 @@ function AdminPage({ onNav, navigationItems, onLogout, logoutPending }: Authenti
|
|
|
}
|
|
|
|
|
|
// ─── P4: Audit page ───────────────────────────────────────────────────────────
|
|
|
-function AuditPage({ onNav, navigationItems, onLogout, logoutPending }: AuthenticatedPageProps) {
|
|
|
+function AuditPage({ onNav, navigationItems, onLogout, logoutPending, currentUser }: AuthenticatedPageProps) {
|
|
|
const [keyword, setKeyword] = useState("");
|
|
|
const [filterAction, setFilterAction] = useState("全部");
|
|
|
|
|
|
@@ -1876,7 +1879,7 @@ function AuditPage({ onNav, navigationItems, onLogout, logoutPending }: Authenti
|
|
|
const ACTIONS = ["全部", "查看文档", "下载文档", "上传文档", "编辑文档", "权限配置", "删除文档", "登录系统"];
|
|
|
|
|
|
return (
|
|
|
- <PageFrame title="操作日志审计" current="audit" navigationItems={navigationItems} onNav={onNav} onLogout={onLogout} logoutPending={logoutPending}>
|
|
|
+ <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)` }}>
|
|
|
@@ -2099,12 +2102,11 @@ function LoginPage({ onLogin, notice }: {
|
|
|
</div>
|
|
|
</div>
|
|
|
))}
|
|
|
- <div className="flex items-center justify-between">
|
|
|
+ <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>
|
|
|
- <button className="text-sm font-black transition-opacity hover:opacity-60" style={{ color: B }}>忘记密码?</button>
|
|
|
</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"
|
|
|
@@ -2117,7 +2119,7 @@ function LoginPage({ onLogin, notice }: {
|
|
|
</div>
|
|
|
</div>
|
|
|
<div className="relative z-10 pb-6 text-center text-xs font-bold" style={{ color: `rgba(26,82,153,0.3)` }}>
|
|
|
- © 2024 方案管理系统 · 版权所有 · 严禁扩散
|
|
|
+ © 方案管理系统 · 版权所有 · 严禁扩散
|
|
|
</div>
|
|
|
</div>
|
|
|
);
|
|
|
@@ -2214,14 +2216,14 @@ function F4Pager({ page, totalPages, onPage }: {
|
|
|
);
|
|
|
}
|
|
|
|
|
|
-function F4DocumentTable({ items, selectedId, onSelect, onDetail, bindingMode = false, onReverse, onPreview, onEdit, onDelete, onPermission, onUnbind, onFileError }: {
|
|
|
+function F4DocumentTable({ items, selectedId, onSelect, onDetail, bindingMode = false, onReverse, onView, onEdit, onDelete, onPermission, onUnbind, onFileError }: {
|
|
|
items: (DocumentSummary | AttachmentBindingSummary)[];
|
|
|
selectedId?: string | null;
|
|
|
onSelect?: (item: DocumentSummary) => void;
|
|
|
onDetail: (item: DocumentSummary, attachment: boolean) => void;
|
|
|
bindingMode?: boolean;
|
|
|
onReverse?: (item: DocumentSummary) => void;
|
|
|
- onPreview?: (item: DocumentSummary) => void;
|
|
|
+ onView?: (item: DocumentSummary) => void;
|
|
|
onEdit?: (item: DocumentSummary) => void;
|
|
|
onDelete?: (item: DocumentSummary) => void;
|
|
|
onPermission?: (item: DocumentSummary) => void;
|
|
|
@@ -2278,7 +2280,7 @@ function F4DocumentTable({ items, selectedId, onSelect, onDetail, bindingMode =
|
|
|
</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 && onPreview && <button onClick={(event) => { event.stopPropagation(); onPreview(item); }} 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>}
|
|
|
@@ -2372,14 +2374,16 @@ function F4ReverseModal({ attachment, onClose }: { attachment: DocumentSummary;
|
|
|
);
|
|
|
}
|
|
|
|
|
|
-function F4AttachmentLibrary({ compact = false, management = false }: { compact?: boolean; management?: boolean }) {
|
|
|
+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 [preview, setPreview] = 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);
|
|
|
@@ -2413,9 +2417,10 @@ function F4AttachmentLibrary({ compact = false, management = false }: { compact?
|
|
|
{state.data && state.data.items.length > 0 && (
|
|
|
<F4DocumentTable
|
|
|
items={state.data.items}
|
|
|
+ onSelect={openPreview}
|
|
|
onDetail={(item) => setDetail(item)}
|
|
|
onReverse={setReverse}
|
|
|
- onPreview={setPreview}
|
|
|
+ onView={setFullView}
|
|
|
onEdit={management ? setEdit : undefined}
|
|
|
onDelete={management ? setRemove : undefined}
|
|
|
onFileError={setFileError}
|
|
|
@@ -2431,7 +2436,8 @@ function F4AttachmentLibrary({ compact = false, management = false }: { compact?
|
|
|
)}
|
|
|
{detail && <F4DetailModal item={detail} attachment onClose={() => setDetail(null)} />}
|
|
|
{reverse && <F4ReverseModal attachment={reverse} onClose={() => setReverse(null)} />}
|
|
|
- {preview && <FilePreviewModal item={preview} onClose={() => setPreview(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)} />}
|
|
|
@@ -2446,15 +2452,15 @@ function F4AttachmentLibrary({ compact = false, management = false }: { compact?
|
|
|
);
|
|
|
}
|
|
|
|
|
|
-function RealMainPage({ onNav, navigationItems, onLogout, logoutPending }: AuthenticatedPageProps) {
|
|
|
- const [tab, setTab] = useState<"main" | "all" | "recent">("main");
|
|
|
+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 [allCategoryCount, setAllCategoryCount] = useState<number | null>(null);
|
|
|
const [page, setPage] = useState(1);
|
|
|
const [selected, setSelected] = useState<DocumentSummary | null>(null);
|
|
|
- const [lowerTab, setLowerTab] = useState<"sub" | "mounted" | "library">("sub");
|
|
|
+ const [lowerTab, setLowerTab] = useState<"sub" | "mounted">("sub");
|
|
|
const [subKeyword, setSubKeyword] = useState("");
|
|
|
const [subStatus, setSubStatus] = useState<DocumentStatusCode | "">("");
|
|
|
const [subUpdatedFrom, setSubUpdatedFrom] = useState("");
|
|
|
@@ -2467,20 +2473,19 @@ function RealMainPage({ onNav, navigationItems, onLogout, logoutPending }: Authe
|
|
|
const [mountedPage, setMountedPage] = useState(1);
|
|
|
const [detail, setDetail] = useState<{ item: DocumentSummary; attachment: boolean } | null>(null);
|
|
|
const [preview, setPreview] = useState<DocumentSummary | null>(null);
|
|
|
- const [edit, setEdit] = useState<DocumentSummary | null>(null);
|
|
|
- const [remove, setRemove] = useState<DocumentSummary | null>(null);
|
|
|
+ const [fullView, setFullView] = useState<DocumentSummary | null>(null);
|
|
|
const [fileError, setFileError] = useState<HttpError | null>(null);
|
|
|
- const [permission, setPermission] = useState<DocumentSummary | 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: tab === "main" ? DocumentType.MAIN : `${DocumentType.MAIN},${DocumentType.SUB_PLAN}`,
|
|
|
+ documentType: DocumentType.MAIN,
|
|
|
categoryId: categoryId ?? undefined,
|
|
|
+ includeDescendants: categoryId ? true : undefined,
|
|
|
keyword: keyword.trim() || undefined,
|
|
|
...mainRange.query,
|
|
|
- sortBy: tab === "recent" ? "updatedAt" : "documentName",
|
|
|
- sortDirection: tab === "recent" ? "desc" : "asc",
|
|
|
+ sortBy: "documentName",
|
|
|
+ sortDirection: "asc",
|
|
|
page,
|
|
|
pageSize: 20,
|
|
|
}, !mainRange.error);
|
|
|
@@ -2502,6 +2507,10 @@ function RealMainPage({ onNav, navigationItems, onLogout, logoutPending }: Authe
|
|
|
pageSize: 20,
|
|
|
}, !mountedRange.error);
|
|
|
|
|
|
+ useEffect(() => {
|
|
|
+ if (categoryId === null && documents.data) setAllCategoryCount(documents.data.total);
|
|
|
+ }, [categoryId, documents.data]);
|
|
|
+
|
|
|
useEffect(() => {
|
|
|
setSubKeyword("");
|
|
|
setSubStatus("");
|
|
|
@@ -2516,19 +2525,13 @@ function RealMainPage({ onNav, navigationItems, onLogout, logoutPending }: Authe
|
|
|
}, [selectedMainId]);
|
|
|
|
|
|
return (
|
|
|
- <PageFrame title="方案管理系统" current="main" navigationItems={navigationItems} onNav={onNav} onLogout={onLogout} logoutPending={logoutPending}>
|
|
|
- <LeftPanel selectedCategoryId={categoryId} onSelectCategory={(id) => { setCategoryId(id); setPage(1); setSelected(null); }} />
|
|
|
+ <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); }} />
|
|
|
<div className="flex-1 flex flex-col m-3 ml-0 min-h-0">
|
|
|
- <div className="flex items-center mb-2" style={{ background: W, border: "1px solid rgba(40,118,193,.18)" }}>
|
|
|
- {([{ id: "main", label: "主案列表" }, { id: "all", label: "全部方案" }, { id: "recent", label: "最近更新" }] as const).map((item) => (
|
|
|
- <button key={item.id} onClick={() => { setTab(item.id); setPage(1); setSelected(null); }} className="px-7 py-3 text-sm font-black"
|
|
|
- style={{ color: tab === item.id ? W : N, background: tab === item.id ? N : W }}>{item.label}</button>
|
|
|
- ))}
|
|
|
- </div>
|
|
|
<DocumentSearchToolbar
|
|
|
ariaLabel="方案检索"
|
|
|
keyword={keyword}
|
|
|
- keywordPlaceholder="搜索方案名称或概述"
|
|
|
+ keywordPlaceholder="搜索方案名称、概述、标签或文档内容"
|
|
|
updatedFrom={updatedFrom}
|
|
|
updatedTo={updatedTo}
|
|
|
error={mainRange.error}
|
|
|
@@ -2539,27 +2542,27 @@ function RealMainPage({ onNav, navigationItems, onLogout, logoutPending }: Authe
|
|
|
/>
|
|
|
<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} selectedId={selected?.id} onSelect={setSelected} onDetail={(item, attachment) => setDetail({ item, attachment })} onPreview={setPreview} onEdit={setEdit} onDelete={setRemove} onPermission={setPermission} onFileError={setFileError} />}
|
|
|
+ {documents.data && documents.data.items.length > 0 && <F4DocumentTable items={documents.data.items} 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: "已挂载共享附件" }, { id: "library", label: "共享附件库" }] as const).map((item) => (
|
|
|
+ {([{ 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">
|
|
|
- {lowerTab === "library" ? <F4AttachmentLibrary compact /> : !selected || selected.documentType !== DocumentType.MAIN ? (
|
|
|
+ {!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}
|
|
|
+ <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} onDetail={(item, attachment) => setDetail({ item, attachment })} onPreview={setPreview} onEdit={setEdit} onDelete={setRemove} onPermission={setPermission} onFileError={setFileError} /> : null}
|
|
|
+ {subPlans.data?.items.length ? <F4DocumentTable items={subPlans.data.items} onSelect={setPreview} onDetail={(item, attachment) => setDetail({ item, attachment })} onView={setFullView} onFileError={setFileError} /> : null}
|
|
|
{subPlans.data && <F4Pager page={subPage} totalPages={subPlans.data.totalPages} onPage={setSubPage} />}
|
|
|
</>
|
|
|
) : (
|
|
|
@@ -2569,7 +2572,7 @@ function RealMainPage({ onNav, navigationItems, onLogout, logoutPending }: Authe
|
|
|
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 onDetail={(item) => setDetail({ item, attachment: true })} onPreview={setPreview} onEdit={setEdit} onDelete={setRemove} onFileError={setFileError} /> : null}
|
|
|
+ {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} />}
|
|
|
</>
|
|
|
)}
|
|
|
@@ -2578,17 +2581,16 @@ function RealMainPage({ onNav, navigationItems, onLogout, logoutPending }: Authe
|
|
|
</div>
|
|
|
{detail && <F4DetailModal {...detail} onClose={() => setDetail(null)} />}
|
|
|
{preview && <FilePreviewModal item={preview} onClose={() => setPreview(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); }} />}
|
|
|
- {permission && <F6PermissionModal document={permission} onClose={() => setPermission(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 }: AuthenticatedPageProps) {
|
|
|
+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("");
|
|
|
@@ -2607,6 +2609,7 @@ function RealAdminPage({ onNav, navigationItems, onLogout, logoutPending }: Auth
|
|
|
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);
|
|
|
@@ -2619,10 +2622,14 @@ function RealAdminPage({ onNav, navigationItems, onLogout, logoutPending }: Auth
|
|
|
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, keyword: keyword.trim() || undefined, ...mainRange.query, sortBy: "updatedAt", sortDirection: "desc", page: planPage, pageSize: 20 }, !mainRange.error);
|
|
|
+ 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("");
|
|
|
@@ -2643,8 +2650,8 @@ function RealAdminPage({ onNav, navigationItems, onLogout, logoutPending }: Auth
|
|
|
}, [plans.data, selected]);
|
|
|
|
|
|
return (
|
|
|
- <PageFrame title="方案管理系统后台" current="admin" navigationItems={navigationItems} onNav={onNav} onLogout={onLogout} logoutPending={logoutPending}>
|
|
|
- {mode === "plans" ? <LeftPanel admin selectedCategoryId={categoryId} onSelectCategory={(id) => { setCategoryId(id); setPlanPage(1); setSelected(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>}
|
|
|
+ <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)" }}>
|
|
|
@@ -2652,17 +2659,17 @@ function RealAdminPage({ onNav, navigationItems, onLogout, logoutPending }: Auth
|
|
|
<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></>}
|
|
|
+ {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 /></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}
|
|
|
+ <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} selectedId={selected?.id} onSelect={setSelected} onDetail={(item, attachment) => setDetail({ item, attachment })} onPreview={setPreview} onEdit={setEdit} onDelete={setRemove} onPermission={setPermission} onFileError={setFileError} /> : null}
|
|
|
+ {plans.data?.items.length ? <F4DocumentTable items={plans.data.items} 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)" }}>
|
|
|
@@ -2676,12 +2683,13 @@ function RealAdminPage({ onNav, navigationItems, onLogout, logoutPending }: Auth
|
|
|
<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}
|
|
|
+ <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} onDetail={(item, attachment) => setDetail({ item, attachment })} onPreview={setPreview} onEdit={setEdit} onDelete={setRemove} onPermission={setPermission} onFileError={setFileError} /> : null}
|
|
|
+ <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} 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} />}
|
|
|
</>
|
|
|
) : (
|
|
|
@@ -2691,7 +2699,7 @@ function RealAdminPage({ onNav, navigationItems, onLogout, logoutPending }: Auth
|
|
|
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 onDetail={(item) => setDetail({ item, attachment: true })} onPreview={setPreview} onEdit={setEdit} onDelete={setRemove} onUnbind={selected.allowedActions.includes(AllowedAction.UNBIND_ATTACHMENT) ? setUnbind : undefined} onFileError={setFileError} /> : null}
|
|
|
+ {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} />}
|
|
|
</>
|
|
|
)}
|
|
|
@@ -2702,6 +2710,7 @@ function RealAdminPage({ onNav, navigationItems, onLogout, logoutPending }: Auth
|
|
|
</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)} />}
|
|
|
@@ -2882,6 +2891,7 @@ export default function App() {
|
|
|
navigationItems,
|
|
|
onLogout: () => void handleLogout(),
|
|
|
logoutPending,
|
|
|
+ currentUser,
|
|
|
};
|
|
|
|
|
|
return (
|
|
|
@@ -2890,13 +2900,13 @@ export default function App() {
|
|
|
{page === "admin" && currentUser.roleCode === RoleCode.ADMIN && (
|
|
|
<RealAdminPage {...pageProps} />
|
|
|
)}
|
|
|
- {page === "audit" && currentUser.roleCode === RoleCode.AUDITOR && (
|
|
|
+ {page === "audit" && currentUser.roleCode === RoleCode.ADMIN && (
|
|
|
<PageFrame title="操作日志审计" current="audit" {...pageProps}>
|
|
|
<AuditDashboard />
|
|
|
</PageFrame>
|
|
|
)}
|
|
|
{((page === "admin" && currentUser.roleCode !== RoleCode.ADMIN) ||
|
|
|
- (page === "audit" && currentUser.roleCode !== RoleCode.AUDITOR)) && (
|
|
|
+ (page === "audit" && currentUser.roleCode !== RoleCode.ADMIN)) && (
|
|
|
<PageFrame
|
|
|
title="模块权限异常"
|
|
|
current={page}
|