|
|
@@ -28,6 +28,7 @@ import { categoryApi } from "../features/categories/categoryApi";
|
|
|
import { useCategoryTree } from "../features/categories/categoryState";
|
|
|
import {
|
|
|
findCategoryNode,
|
|
|
+ getCategoryBranchMainPlanCount,
|
|
|
getCategoryParentOptions,
|
|
|
retainExistingCategoryIds,
|
|
|
} from "../features/categories/categoryTree";
|
|
|
@@ -42,7 +43,14 @@ import {
|
|
|
useUserDirectory,
|
|
|
} from "../features/directory/directoryState";
|
|
|
import type { OrganizationNode } from "../features/directory/directoryTypes";
|
|
|
-import { AllowedAction, AttachmentType, DocumentType, RoleCode } from "../shared/api/enums";
|
|
|
+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,
|
|
|
@@ -54,6 +62,8 @@ import type {
|
|
|
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,
|
|
|
@@ -275,6 +285,12 @@ interface CategoryFormValue {
|
|
|
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})` : "";
|
|
|
}
|
|
|
@@ -329,7 +345,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">({node.documentCount})</span>
|
|
|
+ <span className="ml-1 text-[10px] opacity-45">({getCategoryBranchMainPlanCount(node)})</span>
|
|
|
</span>
|
|
|
{isAdmin && (
|
|
|
<div className="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity" onClick={e => e.stopPropagation()}>
|
|
|
@@ -411,7 +427,7 @@ function CategoryPanel({ admin, selectedCategoryId, onSelectCategory }: {
|
|
|
};
|
|
|
await categoryApi.update(editModal.target.id, request);
|
|
|
setSelectedId(editModal.target.id);
|
|
|
- setActionMessage("分类已更新,当前树已从服务器重新加载。");
|
|
|
+ setActionMessage("分类已更新。");
|
|
|
} else {
|
|
|
const request: CreateCategoryRequest = {
|
|
|
categoryCode: value.categoryCode,
|
|
|
@@ -425,7 +441,7 @@ function CategoryPanel({ admin, selectedCategoryId, onSelectCategory }: {
|
|
|
if (request.parentId) {
|
|
|
setExpanded((current) => new Set(current).add(request.parentId as string));
|
|
|
}
|
|
|
- setActionMessage("分类已新增,当前树已从服务器重新加载。");
|
|
|
+ setActionMessage("分类已新增。");
|
|
|
}
|
|
|
setEditModal(null);
|
|
|
refresh();
|
|
|
@@ -441,13 +457,13 @@ function CategoryPanel({ admin, selectedCategoryId, onSelectCategory }: {
|
|
|
|
|
|
const handleDelete = async (node: CategoryNode) => {
|
|
|
if (submitting) return;
|
|
|
- if (!window.confirm(`确认逻辑删除分类「${node.categoryName}」?不会递归删除子分类,不会迁移关联方案,且本阶段不提供恢复。`)) 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("分类已逻辑删除,当前树已从服务器重新加载。");
|
|
|
+ setActionMessage("分类已删除。");
|
|
|
refresh();
|
|
|
} catch (operationError) {
|
|
|
setActionMessage(categoryActionError(operationError));
|
|
|
@@ -546,14 +562,21 @@ function CategoryEditModal({ title, mode, target, tree, submitting, externalErro
|
|
|
onSubmit: (value: CategoryFormValue) => void;
|
|
|
onClose: () => void;
|
|
|
}) {
|
|
|
- const dictionaries = useDictionaries();
|
|
|
- const [categoryCode, setCategoryCode] = useState(target?.categoryCode ?? "");
|
|
|
- const [categoryName, setCategoryName] = useState(target?.categoryName ?? "");
|
|
|
- const [categoryType, setCategoryType] = useState<keyof typeof CategoryType>(target?.categoryType ?? CategoryType.SCENE);
|
|
|
+ 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(target?.sortNo ?? 10));
|
|
|
+ const [sortNo, setSortNo] = useState(String(mode === "edit" ? target?.sortNo ?? 10 : 10));
|
|
|
const [formError, setFormError] = useState("");
|
|
|
const parentOptions = getCategoryParentOptions(tree, mode === "edit" ? target : undefined);
|
|
|
|
|
|
@@ -561,8 +584,8 @@ function CategoryEditModal({ title, mode, target, tree, submitting, externalErro
|
|
|
const nextCode = categoryCode.trim();
|
|
|
const nextName = categoryName.trim();
|
|
|
const parsedSortNo = Number(sortNo);
|
|
|
- if (!nextCode || !nextName) {
|
|
|
- setFormError("分类编码和分类名称不能为空");
|
|
|
+ if (!nextName) {
|
|
|
+ setFormError("分类名称不能为空");
|
|
|
return;
|
|
|
}
|
|
|
if (!Number.isInteger(parsedSortNo) || parsedSortNo < 0) {
|
|
|
@@ -587,28 +610,15 @@ function CategoryEditModal({ title, mode, target, tree, submitting, externalErro
|
|
|
<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={mode !== "edit"} value={categoryCode} readOnly={mode === "edit"} onChange={e => setCategoryCode(e.target.value)}
|
|
|
- className="w-full h-9 px-3 text-sm font-bold rounded-sm outline-none read-only:opacity-60"
|
|
|
- style={{ color: N, background: `rgba(40,118,193,0.04)`, border: `1px solid rgba(40,118,193,0.3)` }} />
|
|
|
- </CategoryFormField>
|
|
|
<CategoryFormField label="分类名称">
|
|
|
- <input autoFocus={mode === "edit"} value={categoryName} onChange={e => setCategoryName(e.target.value)}
|
|
|
+ <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>
|
|
|
- <div className="grid grid-cols-2 gap-3">
|
|
|
- <CategoryFormField label="分类类型">
|
|
|
- <select value={categoryType} onChange={e => setCategoryType(e.target.value as keyof typeof CategoryType)}
|
|
|
- className="w-full h-9 px-2 text-sm font-bold rounded-sm outline-none" style={{ color: N, border: `1px solid rgba(40,118,193,0.3)` }}>
|
|
|
- {dictionaries.options("categoryTypes").map(option => <option key={option.code} value={option.code}>{option.label}</option>)}
|
|
|
- </select>
|
|
|
- </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>
|
|
|
- </div>
|
|
|
+ <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)` }}>
|
|
|
@@ -650,7 +660,7 @@ function LeftPanel({ children, admin, selectedCategoryId, onSelectCategory }: {
|
|
|
<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>
|
|
|
+ <span className="text-sm font-black" style={{ color: N }}>方案分类</span>
|
|
|
</div>
|
|
|
<div className="flex-1 overflow-hidden flex flex-col p-3">
|
|
|
<CategoryPanel
|
|
|
@@ -1053,7 +1063,7 @@ function PermissionModal({ doc, onClose }: { doc: Doc; onClose: () => void }) {
|
|
|
<BtnOutline onClick={() => { setOrgChecked(new Set()); setPersonChecked(new Set()); }} danger>
|
|
|
<RefreshCw size={13} />重置
|
|
|
</BtnOutline>
|
|
|
- <BtnPrimary onClick={() => setNotice("组织和人员数据已接入,权限持久化将在后续阶段完成。")}><Check size={13} />保存权限</BtnPrimary>
|
|
|
+ <BtnPrimary onClick={() => setNotice("")}><Check size={13} />保存权限</BtnPrimary>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -1268,7 +1278,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}>
|
|
|
<LeftPanel />
|
|
|
<div className="flex-1 flex flex-col m-3 ml-0 min-h-0">
|
|
|
{/* 顶部 Tab 栏 */}
|
|
|
@@ -1287,7 +1297,7 @@ function MainPage({ onNav, navigationItems, onLogout, logoutPending }: Authentic
|
|
|
</button>
|
|
|
);
|
|
|
})}
|
|
|
- {/* 右侧检索框:与左侧方案计划分类搜索共用 keyword,覆盖主案/子方案/配套资料 */}
|
|
|
+ {/* 右侧检索框:与左侧方案分类搜索共用 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
|
|
|
@@ -1729,7 +1739,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}>
|
|
|
{adminMode === "plans"
|
|
|
? <LeftPanel admin />
|
|
|
: <AttachmentTypePanel selected={attachmentType} onSelect={setAttachmentType} />}
|
|
|
@@ -2043,7 +2053,7 @@ function LoginPage({ onLogin, notice }: {
|
|
|
<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>
|
|
|
+ <span className="text-xl font-black tracking-[0.22em] whitespace-nowrap" style={{ color: W }}>方案管理系统</span>
|
|
|
</div>
|
|
|
|
|
|
{/* Header → 主体 渐变过渡带(20px) */}
|
|
|
@@ -2107,7 +2117,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 方案计划系统 · 版权所有 · 严禁扩散
|
|
|
+ © 2024 方案管理系统 · 版权所有 · 严禁扩散
|
|
|
</div>
|
|
|
</div>
|
|
|
);
|
|
|
@@ -2126,7 +2136,7 @@ function AuthStateScreen({ title, description, requestId, primaryLabel, onPrimar
|
|
|
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>
|
|
|
+ <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">
|
|
|
@@ -2166,13 +2176,14 @@ function formatFileSize(value: number): string {
|
|
|
return `${(value / 1024 / 1024).toFixed(1)} MB`;
|
|
|
}
|
|
|
|
|
|
-function F4State({ loading, error, empty }: {
|
|
|
+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>;
|
|
|
+ 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 (
|
|
|
@@ -2180,6 +2191,7 @@ function F4State({ loading, error, empty }: {
|
|
|
<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>
|
|
|
);
|
|
|
}
|
|
|
@@ -2247,9 +2259,9 @@ function F4DocumentTable({ items, selectedId, onSelect, onDetail, bindingMode =
|
|
|
<td className="px-4 py-3 text-sm font-bold" style={{ color: N }}>{index + 1}</td>
|
|
|
<td className="px-4 py-3">
|
|
|
<div className="text-sm font-black" style={{ color: N }}>{item.documentName}</div>
|
|
|
- <div className="text-[11px] font-bold opacity-55" style={{ color: N }}>
|
|
|
- ID {item.id}{attachment ? ` · ${item.fileExtension || "无扩展名"}` : ""}
|
|
|
- </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 ?? "未分类"}
|
|
|
@@ -2266,7 +2278,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 && onPreview && <button onClick={(event) => { event.stopPropagation(); onPreview(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>}
|
|
|
@@ -2305,7 +2317,7 @@ function F4DetailModal({ item, attachment, onClose }: {
|
|
|
<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>
|
|
|
+ <span className="font-black">详情 · {item.documentName}</span>
|
|
|
<button onClick={onClose}><X size={17} /></button>
|
|
|
</div>
|
|
|
<div className="p-6">
|
|
|
@@ -2328,7 +2340,6 @@ function F4DetailModal({ item, attachment, onClose }: {
|
|
|
<span>查看次数:{detail.viewCount}</span>
|
|
|
<span>下载次数:{detail.downloadCount}</span>
|
|
|
</div>
|
|
|
- <div className="p-4 text-center font-black" style={{ background: PBG }}>请使用列表中的“预览”或“下载”入口读取真实文件。</div>
|
|
|
</div>
|
|
|
)}
|
|
|
</div>
|
|
|
@@ -2351,7 +2362,7 @@ function F4ReverseModal({ attachment, onClose }: { attachment: DocumentSummary;
|
|
|
<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">主案 ID {plan.id} · {plan.categoryName ?? "未分类"}</div></div>
|
|
|
+ <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>
|
|
|
))}
|
|
|
@@ -2394,7 +2405,7 @@ function F4AttachmentLibrary({ compact = false, management = false }: { compact?
|
|
|
<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>
|
|
|
+ <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">
|
|
|
@@ -2438,30 +2449,74 @@ function F4AttachmentLibrary({ compact = false, management = false }: { compact?
|
|
|
function RealMainPage({ onNav, navigationItems, onLogout, logoutPending }: AuthenticatedPageProps) {
|
|
|
const [tab, setTab] = useState<"main" | "all" | "recent">("main");
|
|
|
const [keyword, setKeyword] = useState("");
|
|
|
+ const [updatedFrom, setUpdatedFrom] = useState("");
|
|
|
+ const [updatedTo, setUpdatedTo] = useState("");
|
|
|
const [categoryId, setCategoryId] = useState<string | null>(null);
|
|
|
const [page, setPage] = useState(1);
|
|
|
const [selected, setSelected] = useState<DocumentSummary | null>(null);
|
|
|
const [lowerTab, setLowerTab] = useState<"sub" | "mounted" | "library">("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 [edit, setEdit] = useState<DocumentSummary | null>(null);
|
|
|
const [remove, setRemove] = 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}`,
|
|
|
categoryId: categoryId ?? undefined,
|
|
|
keyword: keyword.trim() || undefined,
|
|
|
+ ...mainRange.query,
|
|
|
sortBy: tab === "recent" ? "updatedAt" : "documentName",
|
|
|
sortDirection: tab === "recent" ? "desc" : "asc",
|
|
|
page,
|
|
|
pageSize: 20,
|
|
|
- });
|
|
|
- const subPlans = useSubPlanList(selected?.documentType === DocumentType.MAIN ? selected.id : null, { page: 1, pageSize: 20, sortBy: "documentName", sortDirection: "asc" });
|
|
|
- const mounted = useMountedAttachments(selected?.documentType === DocumentType.MAIN ? selected.id : null, { page: 1, pageSize: 20 });
|
|
|
+ }, !mainRange.error);
|
|
|
+ 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(() => {
|
|
|
+ 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}>
|
|
|
+ <PageFrame title="方案管理系统" current="main" navigationItems={navigationItems} onNav={onNav} onLogout={onLogout} logoutPending={logoutPending}>
|
|
|
<LeftPanel selectedCategoryId={categoryId} onSelectCategory={(id) => { setCategoryId(id); setPage(1); setSelected(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)" }}>
|
|
|
@@ -2469,27 +2524,54 @@ function RealMainPage({ onNav, navigationItems, onLogout, logoutPending }: Authe
|
|
|
<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 className="ml-auto mr-3 flex items-center gap-2"><Search size={14} style={{ color: B }} /><input value={keyword} onChange={(event) => { setKeyword(event.target.value); setPage(1); }} placeholder="搜索方案名称或概述" className="w-64 h-8 px-2 text-sm font-bold outline-none border" style={{ color: N }} /></div>
|
|
|
</div>
|
|
|
+ <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} />
|
|
|
+ <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 && <F4Pager page={page} totalPages={documents.data.totalPages} onPage={(value) => { setPage(value); setSelected(null); }} />}
|
|
|
</div>
|
|
|
- <div className="h-[290px] flex-shrink-0 flex flex-col" style={{ background: W, border: "1px solid rgba(40,118,193,.18)" }}>
|
|
|
+ <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) => (
|
|
|
<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">
|
|
|
+ <div className="flex-1 overflow-auto min-h-0">
|
|
|
{lowerTab === "library" ? <F4AttachmentLibrary compact /> : !selected || selected.documentType !== DocumentType.MAIN ? (
|
|
|
- <div className="p-12 text-center text-sm font-bold opacity-50" style={{ color: N }}>请选择主案查看真实关联数据</div>
|
|
|
+ <div className="p-12 text-center text-sm font-bold opacity-50" style={{ color: N }}>请选择主案查看关联数据</div>
|
|
|
) : lowerTab === "sub" ? (
|
|
|
- <><F4State loading={subPlans.loading} error={subPlans.error} empty={subPlans.data?.items.length === 0} />{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}</>
|
|
|
+ <>
|
|
|
+ <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 && <F4Pager page={subPage} totalPages={subPlans.data.totalPages} onPage={setSubPage} />}
|
|
|
+ </>
|
|
|
) : (
|
|
|
- <><F4State loading={mounted.loading} error={mounted.error} empty={mounted.data?.items.length === 0} />{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}</>
|
|
|
+ <>
|
|
|
+ <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 onDetail={(item) => setDetail({ item, attachment: true })} onPreview={setPreview} onEdit={setEdit} onDelete={setRemove} onFileError={setFileError} /> : null}
|
|
|
+ {mounted.data && <F4Pager page={mountedPage} totalPages={mounted.data.totalPages} onPage={setMountedPage} />}
|
|
|
+ </>
|
|
|
)}
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -2508,9 +2590,21 @@ function RealAdminPage({ onNav, navigationItems, onLogout, logoutPending }: Auth
|
|
|
const [mode, setMode] = useState<"plans" | "attachments">("plans");
|
|
|
const [categoryId, setCategoryId] = useState<string | 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 [edit, setEdit] = useState<DocumentSummary | null>(null);
|
|
|
@@ -2522,9 +2616,25 @@ function RealAdminPage({ onNav, navigationItems, onLogout, logoutPending }: Auth
|
|
|
const [permission, setPermission] = useState<DocumentSummary | null>(null);
|
|
|
const [unbind, setUnbind] = useState<AttachmentBindingSummary | null>(null);
|
|
|
const [recycleBinOpen, setRecycleBinOpen] = useState(false);
|
|
|
- const plans = useDocumentList({ documentType: DocumentType.MAIN, categoryId: categoryId ?? undefined, keyword: keyword.trim() || undefined, sortBy: "updatedAt", sortDirection: "desc", page: planPage, pageSize: 20 });
|
|
|
- const subPlans = useSubPlanList(selected?.id ?? null, { page: 1, pageSize: 50, sortBy: "documentName", sortDirection: "asc" });
|
|
|
- const mounted = useMountedAttachments(selected?.id ?? null, { page: 1, pageSize: 50 });
|
|
|
+ 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 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(() => {
|
|
|
+ setSubKeyword("");
|
|
|
+ setSubStatus("");
|
|
|
+ setSubUpdatedFrom("");
|
|
|
+ setSubUpdatedTo("");
|
|
|
+ setSubPage(1);
|
|
|
+ setMountedKeyword("");
|
|
|
+ setMountedType("");
|
|
|
+ setMountedUpdatedFrom("");
|
|
|
+ setMountedUpdatedTo("");
|
|
|
+ setMountedPage(1);
|
|
|
+ }, [selected?.id]);
|
|
|
|
|
|
useEffect(() => {
|
|
|
if (!selected || !plans.data) return;
|
|
|
@@ -2533,30 +2643,29 @@ 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><p className="text-xs font-bold opacity-60 mt-2 leading-6">附件独立存在,可上传、批量导入、编辑、预览、下载和逻辑删除。共享附件面向全部有效登录用户,不配置组织或人员 ACL。</p></div>}
|
|
|
+ <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>}
|
|
|
<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>
|
|
|
- <span className="ml-auto mr-3 text-xs font-black" style={{ color: N }}>F5 文件管理基线 · F6 挂载关系与主案权限已接入</span>
|
|
|
- <div className="flex gap-2 mr-3">
|
|
|
+ <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 /></div> : (
|
|
|
<>
|
|
|
- <div className="flex items-center gap-2 p-2 mb-2" style={{ background: W }}>
|
|
|
- <Search size={14} style={{ color: B }} /><input value={keyword} onChange={(event) => { setKeyword(event.target.value); setPlanPage(1); }} placeholder="搜索主案" className="h-8 w-64 px-2 text-sm font-bold border outline-none" style={{ color: N }} />
|
|
|
- </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} />
|
|
|
+ <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 && <F4Pager page={planPage} totalPages={plans.data.totalPages} onPage={(value) => { setPlanPage(value); setSelected(null); }} />}
|
|
|
</div>
|
|
|
- <div className="h-[300px] flex-shrink-0 flex flex-col" style={{ background: W, border: "1px solid rgba(40,118,193,.18)" }}>
|
|
|
+ <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>
|
|
|
@@ -2564,11 +2673,27 @@ function RealAdminPage({ onNav, navigationItems, onLogout, logoutPending }: Auth
|
|
|
{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">
|
|
|
+ <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" ? (
|
|
|
- <><F4State loading={subPlans.loading} error={subPlans.error} empty={subPlans.data?.items.length === 0} />{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}</>
|
|
|
+ <>
|
|
|
+ <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 && <F4Pager page={subPage} totalPages={subPlans.data.totalPages} onPage={setSubPage} />}
|
|
|
+ </>
|
|
|
) : (
|
|
|
- <><F4State loading={mounted.loading} error={mounted.error} empty={mounted.data?.items.length === 0} />{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}</>
|
|
|
+ <>
|
|
|
+ <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 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 && <F4Pager page={mountedPage} totalPages={mounted.data.totalPages} onPage={setMountedPage} />}
|
|
|
+ </>
|
|
|
)}
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -2781,7 +2906,7 @@ export default function App() {
|
|
|
className="m-3 p-8 flex-1 text-sm font-bold"
|
|
|
style={{ background: W, color: N }}
|
|
|
>
|
|
|
- 后端返回的模块授权与该接口的角色权限不一致,本页面未发起任何受限数据请求。请联系管理员并提供当前账号信息。
|
|
|
+ 当前账号无法访问该模块,请联系管理员。
|
|
|
</div>
|
|
|
</PageFrame>
|
|
|
)}
|