|
|
@@ -129,7 +129,7 @@ function ErrorBlock({ error }: { error: HttpError | null }) {
|
|
|
: null;
|
|
|
const plans = Array.isArray(details?.mainPlans) ? details.mainPlans : [];
|
|
|
return (
|
|
|
- <div className="p-3 text-xs font-bold leading-5" style={{ background: "rgba(26,82,153,.08)", color: NAVY }}>
|
|
|
+ <div className="p-3 text-base font-bold leading-5" style={{ background: "rgba(26,82,153,.08)", color: NAVY }}>
|
|
|
<div>{mutationMessage(error)}</div>
|
|
|
{typeof details?.mountedPlanCount === "number" && <div>当前挂载主案数:{details.mountedPlanCount}</div>}
|
|
|
{typeof details?.childCategoryCount === "number" && <div>下级分类数:{details.childCategoryCount}</div>}
|
|
|
@@ -162,10 +162,10 @@ function Modal({ title, children, onClose, wide = false }: {
|
|
|
}
|
|
|
|
|
|
function Field({ label, children }: { label: string; children: React.ReactNode }) {
|
|
|
- return <label className="block"><span className="block text-xs font-black mb-1" style={{ color: NAVY }}>{label}</span>{children}</label>;
|
|
|
+ return <label className="block"><span className="block text-base font-black mb-1" style={{ color: NAVY }}>{label}</span>{children}</label>;
|
|
|
}
|
|
|
|
|
|
-const inputClass = "w-full h-9 px-3 text-sm font-bold border outline-none";
|
|
|
+const inputClass = "w-full h-9 px-3 text-base font-bold border outline-none";
|
|
|
const selectStyle = { color: NAVY, borderColor: "rgba(26,82,153,.25)" };
|
|
|
|
|
|
function useReferenceData() {
|
|
|
@@ -200,10 +200,12 @@ function defaultDocumentMetadata(
|
|
|
};
|
|
|
}
|
|
|
|
|
|
-export function SingleUploadModal({ kind, documentType: fixedDocumentType = "MAIN", initialParentId, initialAttachmentType, mainOnly = false, onUploaded, onClose }: {
|
|
|
+export function SingleUploadModal({ kind, documentType: fixedDocumentType = "MAIN", initialParentId, initialCategoryId, initialAttachmentType, mainOnly = false, onUploaded, onClose }: {
|
|
|
kind: "document" | "attachment";
|
|
|
documentType?: "MAIN" | "SUB_PLAN";
|
|
|
initialParentId?: string;
|
|
|
+ /** 上传主体方案时的默认分类:传入当前页面选中分类,未选分类则不指定由用户选择。 */
|
|
|
+ initialCategoryId?: string;
|
|
|
/** 附件类型预置值(管理页 Tab 页"上传"入口按当前页签类型上传共享资料)。 */
|
|
|
initialAttachmentType?: AttachmentTypeCode;
|
|
|
/** 仅允许上传主案:隐藏方案类型切换,documentType 锁定为 MAIN。 */
|
|
|
@@ -220,7 +222,7 @@ export function SingleUploadModal({ kind, documentType: fixedDocumentType = "MAI
|
|
|
initialParentId ? "SUB_PLAN" : fixedDocumentType,
|
|
|
);
|
|
|
const [parentDocumentId, setParentDocumentId] = useState(initialParentId ?? "");
|
|
|
- const [categoryId, setCategoryId] = useState("");
|
|
|
+ const [categoryId, setCategoryId] = useState(initialCategoryId ?? "");
|
|
|
const [securityLevel, setSecurityLevel] = useState(SecurityLevel.INTERNAL);
|
|
|
const [attachmentType, setAttachmentType] = useState<AttachmentTypeCode>(initialAttachmentType ?? AttachmentType.SUPPORT_ACHIEVEMENT);
|
|
|
const [submitting, setSubmitting] = useState(false);
|
|
|
@@ -334,13 +336,13 @@ export function SingleUploadModal({ kind, documentType: fixedDocumentType = "MAI
|
|
|
<Field label="附件类型 *"><select value={attachmentType} disabled={submitting} onChange={(event) => setAttachmentType(event.target.value as typeof attachmentType)} className={inputClass} style={selectStyle}>{dictionaries.options("attachmentTypes").map((option) => <option key={option.code} value={option.code}>{option.label}</option>)}</select></Field>
|
|
|
) : (
|
|
|
<>
|
|
|
- {documentType === "MAIN" ? <Field label="方案分类 *"><select value={categoryId} disabled={submitting || references.categoryLoading} onChange={(event) => { setCategoryId(event.target.value); setOverwrite(null); }} className={inputClass} style={selectStyle}><option value="">请选择最末级分类</option>{references.categories.map((category) => <option key={category.id} value={category.id} disabled={!category.isLeaf} title={category.categoryPath}>{category.displayLabel}{category.isLeaf ? "" : "(请选择下级分类)"}</option>)}</select></Field> : <div className="px-3 py-2 text-xs font-bold" style={{ color: NAVY, background: "#EBF4FD" }}>方案分类继承自主案:{references.mainPlans.find((plan) => plan.id === parentDocumentId)?.categoryPath ?? references.mainPlans.find((plan) => plan.id === parentDocumentId)?.categoryName ?? "请选择所属主案"}</div>}
|
|
|
+ {documentType === "MAIN" ? <Field label="方案分类 *"><select value={categoryId} disabled={submitting || references.categoryLoading} onChange={(event) => { setCategoryId(event.target.value); setOverwrite(null); }} className={inputClass} style={selectStyle}><option value="">请选择最末级分类</option>{references.categories.map((category) => <option key={category.id} value={category.id} disabled={!category.isLeaf} title={category.categoryPath}>{category.displayLabel}{category.isLeaf ? "" : "(请选择下级分类)"}</option>)}</select></Field> : <div className="px-3 py-2 text-base font-bold" style={{ color: NAVY, background: "#EBF4FD" }}>方案分类继承自主案:{references.mainPlans.find((plan) => plan.id === parentDocumentId)?.categoryPath ?? references.mainPlans.find((plan) => plan.id === parentDocumentId)?.categoryName ?? "请选择所属主案"}</div>}
|
|
|
<div className="grid grid-cols-1 gap-3">
|
|
|
<Field label="密级 *"><select value={securityLevel} onChange={(event) => setSecurityLevel(event.target.value as typeof securityLevel)} className={inputClass} style={selectStyle}>{dictionaries.options("securityLevels").map((option) => <option key={option.code} value={option.code}>{option.label}</option>)}</select></Field>
|
|
|
</div>
|
|
|
{documentType === "MAIN" && !overwrite && (
|
|
|
<div className="border" style={{ borderColor: "rgba(26,82,153,.18)" }}>
|
|
|
- <button type="button" disabled={submitting} onClick={() => setAdvancedOpen((open) => !open)} className="w-full px-3 h-9 flex items-center justify-between text-xs font-black" style={{ color: NAVY }}>
|
|
|
+ <button type="button" disabled={submitting} onClick={() => setAdvancedOpen((open) => !open)} className="w-full px-3 h-9 flex items-center justify-between text-base font-black" style={{ color: NAVY }}>
|
|
|
<span className="flex items-center gap-1"><Lock size={13} />高级设置 · 方案密码锁(可选)</span>
|
|
|
<span className="opacity-70">{advancedOpen ? "收起" : "展开"}</span>
|
|
|
</button>
|
|
|
@@ -350,17 +352,17 @@ export function SingleUploadModal({ kind, documentType: fixedDocumentType = "MAI
|
|
|
<input type="password" value={password} disabled={submitting} autoComplete="new-password" onChange={(event) => setPassword(event.target.value)} maxLength={128} placeholder="6位以上,设置后受保护操作需输入该密码" className={inputClass} style={selectStyle} />
|
|
|
</Field>
|
|
|
<div>
|
|
|
- <span className="block text-xs font-black mb-1" style={{ color: NAVY }}>需密码保护的操作 *(密码非空时至少勾选一项)</span>
|
|
|
+ <span className="block text-base font-black mb-1" style={{ color: NAVY }}>需密码保护的操作 *(密码非空时至少勾选一项)</span>
|
|
|
<div className="grid grid-cols-4 gap-1.5">
|
|
|
{ALL_PASSWORD_ACTIONS.map((code) => (
|
|
|
- <label key={code} className="flex items-center gap-1.5 text-xs font-bold" style={{ color: NAVY }}>
|
|
|
+ <label key={code} className="flex items-center gap-1.5 text-base font-bold" style={{ color: NAVY }}>
|
|
|
<input type="checkbox" checked={protectedActions.includes(code)} disabled={submitting} onChange={() => setProtectedActions((current) => current.includes(code) ? current.filter((item) => item !== code) : [...current, code])} />
|
|
|
{PASSWORD_ACTION_LABELS[code]}
|
|
|
</label>
|
|
|
))}
|
|
|
</div>
|
|
|
</div>
|
|
|
- <div className="text-xs opacity-70" style={{ color: NAVY }}>管理员对设密主案的查看、下载、编辑等操作也需输入密码(删除除外);修改或清除密码需提供原密码。</div>
|
|
|
+ <div className="text-base opacity-70" style={{ color: NAVY }}>管理员对设密主案的查看、下载、编辑等操作也需输入密码(删除除外);修改或清除密码需提供原密码。</div>
|
|
|
</div>
|
|
|
)}
|
|
|
</div>
|
|
|
@@ -368,10 +370,10 @@ export function SingleUploadModal({ kind, documentType: fixedDocumentType = "MAI
|
|
|
</>
|
|
|
)}
|
|
|
{/* 概述/标签输入框已按要求隐藏:创建时提交空值,后续可在编辑弹窗维护 */}
|
|
|
- {formError && <div className="text-xs font-bold" style={{ color: NAVY }}>{formError}</div>}
|
|
|
+ {formError && <div className="text-base font-bold" style={{ color: NAVY }}>{formError}</div>}
|
|
|
<ErrorBlock error={error} />
|
|
|
- {overwrite && <div role="alertdialog" className="p-4 text-sm font-bold border" style={{ color: NAVY, borderColor: BLUE, background: "#EBF4FD" }}><div>已存在同名{documentType === "MAIN" ? "主案" : "子案"}“{overwrite.existingDocumentName}”,是否覆盖?</div><div className="mt-1 text-xs opacity-70">覆盖将保留原记录、权限及关联关系,仅替换文件和本次填写的元数据。</div><div className="flex justify-end gap-2 mt-3"><button type="button" onClick={() => setOverwrite(null)} className="px-3 py-1.5 border" style={selectStyle}>取消覆盖</button><button type="button" disabled={submitting} onClick={() => void submit(true)} className="px-3 py-1.5 text-white" style={{ background: BLUE }}>确认覆盖</button></div></div>}
|
|
|
- <div className="flex justify-end gap-2"><button onClick={onClose} disabled={submitting} className="px-4 py-2 text-sm font-black border" style={selectStyle}>取消</button><button onClick={() => void submit()} disabled={submitting || dictionaries.status !== "success"} className="px-4 py-2 text-sm font-black text-white disabled:opacity-60" style={{ background: BLUE }}>{submitting ? <><RefreshCw size={13} className="inline animate-spin mr-1" />正在上传/处理中</> : <><Upload size={13} className="inline mr-1" />确认上传</>}</button></div>
|
|
|
+ {overwrite && <div role="alertdialog" className="p-4 text-base font-bold border" style={{ color: NAVY, borderColor: BLUE, background: "#EBF4FD" }}><div>已存在同名{documentType === "MAIN" ? "主案" : "子案"}“{overwrite.existingDocumentName}”,是否覆盖?</div><div className="mt-1 text-base opacity-70">覆盖将保留原记录、权限及关联关系,仅替换文件和本次填写的元数据。</div><div className="flex justify-end gap-2 mt-3"><button type="button" onClick={() => setOverwrite(null)} className="px-3 py-1.5 border" style={selectStyle}>取消覆盖</button><button type="button" disabled={submitting} onClick={() => void submit(true)} className="px-3 py-1.5 text-white" style={{ background: BLUE }}>确认覆盖</button></div></div>}
|
|
|
+ <div className="flex justify-end gap-2"><button onClick={onClose} disabled={submitting} className="px-4 py-2 text-base font-black border" style={selectStyle}>取消</button><button onClick={() => void submit()} disabled={submitting || dictionaries.status !== "success"} className="px-4 py-2 text-base font-black text-white disabled:opacity-60" style={{ background: BLUE }}>{submitting ? <><RefreshCw size={13} className="inline animate-spin mr-1" />正在上传/处理中</> : <><Upload size={13} className="inline mr-1" />确认上传</>}</button></div>
|
|
|
</div>
|
|
|
</Modal>
|
|
|
);
|
|
|
@@ -475,7 +477,7 @@ export function BatchImportModal({ kind, onClose }: { kind: BatchKind; onClose:
|
|
|
<Modal title={kind === "attachment" ? "批量导入配套资料" : "批量导入方案文档"} onClose={onClose} wide>
|
|
|
<div className="p-5 space-y-3">
|
|
|
<div onDragOver={(event) => event.preventDefault()} onDrop={(event) => { event.preventDefault(); addFiles(Array.from(event.dataTransfer.files)); }}
|
|
|
- className="p-5 text-center border-2 border-dashed text-sm font-black" style={{ color: NAVY, borderColor: "rgba(49,145,235,.45)", background: "#EBF4FD" }}>
|
|
|
+ className="p-5 text-center border-2 border-dashed text-base font-black" style={{ color: NAVY, borderColor: "rgba(49,145,235,.45)", background: "#EBF4FD" }}>
|
|
|
拖入多个文件,或 <label className="underline cursor-pointer"><input type="file" multiple accept={ACCEPT} className="hidden" onChange={(event) => addFiles(Array.from(event.target.files ?? []))} />点击选择</label>
|
|
|
</div>
|
|
|
{drafts.map((draft, index) => {
|
|
|
@@ -484,31 +486,31 @@ export function BatchImportModal({ kind, onClose }: { kind: BatchKind; onClose:
|
|
|
const attachmentMetadata = kind === "attachment" ? metadata as AttachmentCreateMetadata : null;
|
|
|
return (
|
|
|
<div key={draft.key} className="p-3 border grid grid-cols-12 gap-2 items-end" style={{ borderColor: "rgba(26,82,153,.18)" }}>
|
|
|
- <div className="col-span-2 text-xs font-black truncate" {...tipProps(draft.file.name)}>{index + 1}. {draft.file.name}</div>
|
|
|
+ <div className="col-span-2 text-base font-black truncate" {...tipProps(draft.file.name)}>{index + 1}. {draft.file.name}</div>
|
|
|
<div className="col-span-2"><input value={metadata.documentName} onChange={(event) => updateMetadata(draft.key, { documentName: event.target.value })} className={inputClass} style={selectStyle} /></div>
|
|
|
{kind === "document" ? (
|
|
|
<>
|
|
|
<div className="col-span-1"><select value={documentMetadata!.documentType} onChange={(event) => changeDocumentType(draft.key, event.target.value as "MAIN" | "SUB_PLAN")} className={inputClass} style={selectStyle}>{dictionaries.options("documentTypes").filter((option) => option.code !== "ATTACHMENT").map((option) => <option key={option.code} value={option.code}>{option.label}</option>)}</select></div>
|
|
|
- <div className="col-span-2">{documentMetadata!.documentType === "MAIN" ? <select value={documentMetadata!.categoryId} onChange={(event) => updateMetadata(draft.key, { categoryId: event.target.value } as Partial<BatchMetadata>)} className={inputClass} style={selectStyle}>{references.categories.map((category) => <option key={category.id} value={category.id} disabled={!category.isLeaf} title={category.categoryPath}>{category.displayLabel}</option>)}</select> : <div className="h-9 px-2 flex items-center text-xs font-bold" style={{ background: "#EBF4FD", color: NAVY }}>分类继承自主案</div>}</div>
|
|
|
+ <div className="col-span-2">{documentMetadata!.documentType === "MAIN" ? <select value={documentMetadata!.categoryId} onChange={(event) => updateMetadata(draft.key, { categoryId: event.target.value } as Partial<BatchMetadata>)} className={inputClass} style={selectStyle}>{references.categories.map((category) => <option key={category.id} value={category.id} disabled={!category.isLeaf} title={category.categoryPath}>{category.displayLabel}</option>)}</select> : <div className="h-9 px-2 flex items-center text-base font-bold" style={{ background: "#EBF4FD", color: NAVY }}>分类继承自主案</div>}</div>
|
|
|
<div className="col-span-2"><select disabled={documentMetadata!.documentType === "MAIN"} value={documentMetadata!.documentType === "SUB_PLAN" ? documentMetadata!.parentDocumentId : ""} onChange={(event) => updateMetadata(draft.key, { parentDocumentId: event.target.value } as Partial<BatchMetadata>)} className={inputClass} style={selectStyle}><option value="">父主案</option>{references.mainPlans.map((plan) => <option key={plan.id} value={plan.id}>{plan.documentName}</option>)}</select></div>
|
|
|
</>
|
|
|
) : (
|
|
|
<div className="col-span-2"><select value={attachmentMetadata!.attachmentType} onChange={(event) => updateMetadata(draft.key, { attachmentType: event.target.value as AttachmentCreateMetadata["attachmentType"] })} className={inputClass} style={selectStyle}>{dictionaries.options("attachmentTypes").map((option) => <option key={option.code} value={option.code}>{option.label}</option>)}</select></div>
|
|
|
)}
|
|
|
- <button disabled={submitting} onClick={() => setDrafts((current) => current.filter((item) => item.key !== draft.key))} className="col-span-1 h-9 text-xs font-black" style={{ color: NAVY }}><Trash2 size={13} className="inline" />移除</button>
|
|
|
+ <button disabled={submitting} onClick={() => setDrafts((current) => current.filter((item) => item.key !== draft.key))} className="col-span-1 h-9 text-base font-black" style={{ color: NAVY }}><Trash2 size={13} className="inline" />移除</button>
|
|
|
{documentMetadata && <>
|
|
|
<div className="col-span-2"><select value={documentMetadata.securityLevel} onChange={(event) => updateMetadata(draft.key, { securityLevel: event.target.value as DocumentCreateMetadata["securityLevel"] } as Partial<BatchMetadata>)} className={inputClass} style={selectStyle}>{dictionaries.options("securityLevels").map((option) => <option key={option.code} value={option.code}>{option.label}</option>)}</select></div>
|
|
|
</>}
|
|
|
- {draft.result && <div className="col-span-12 text-xs font-bold" style={{ color: draft.result.success ? BLUE : NAVY }}>{draft.result.success ? `成功:${draft.result.document.documentName}` : `失败:${draft.result.errorCode} · ${draft.result.errorMessage}`}</div>}
|
|
|
+ {draft.result && <div className="col-span-12 text-base font-bold" style={{ color: draft.result.success ? BLUE : NAVY }}>{draft.result.success ? `成功:${draft.result.document.documentName}` : `失败:${draft.result.errorCode} · ${draft.result.errorMessage}`}</div>}
|
|
|
</div>
|
|
|
);
|
|
|
})}
|
|
|
- {summary && <div className="text-sm font-black" style={{ color: NAVY }}>总数 {summary.total} · 成功 {summary.successCount} · 失败 {summary.failureCount}</div>}
|
|
|
+ {summary && <div className="text-base font-black" style={{ color: NAVY }}>总数 {summary.total} · 成功 {summary.successCount} · 失败 {summary.failureCount}</div>}
|
|
|
<ErrorBlock error={error} />
|
|
|
<div className="flex justify-end gap-2">
|
|
|
- {summary?.failureCount ? <button onClick={() => { setDrafts((current) => retainFailedDrafts(current)); setSummary(null); }} className="px-4 py-2 text-sm font-black border" style={selectStyle}>仅保留失败项重试</button> : null}
|
|
|
- <button onClick={onClose} disabled={submitting} className="px-4 py-2 text-sm font-black border" style={selectStyle}>关闭</button>
|
|
|
- <button onClick={() => void submit()} disabled={submitting || dictionaries.status !== "success" || drafts.length === 0 || drafts.every((draft) => draft.result?.success)} className="px-4 py-2 text-sm font-black text-white disabled:opacity-50" style={{ background: BLUE }}><FilePlus2 size={13} className="inline mr-1" />{submitting ? "正在上传/处理中" : "提交当前批次"}</button>
|
|
|
+ {summary?.failureCount ? <button onClick={() => { setDrafts((current) => retainFailedDrafts(current)); setSummary(null); }} className="px-4 py-2 text-base font-black border" style={selectStyle}>仅保留失败项重试</button> : null}
|
|
|
+ <button onClick={onClose} disabled={submitting} className="px-4 py-2 text-base font-black border" style={selectStyle}>关闭</button>
|
|
|
+ <button onClick={() => void submit()} disabled={submitting || dictionaries.status !== "success" || drafts.length === 0 || drafts.every((draft) => draft.result?.success)} className="px-4 py-2 text-base font-black text-white disabled:opacity-50" style={{ background: BLUE }}><FilePlus2 size={13} className="inline mr-1" />{submitting ? "正在上传/处理中" : "提交当前批次"}</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
</Modal>
|
|
|
@@ -849,17 +851,17 @@ export function EditMetadataModal({ item, canManagePassword = false, canManageRe
|
|
|
return (
|
|
|
<Modal title={`编辑 · ${item.documentName}`} onClose={onClose}>
|
|
|
<div className="p-6 space-y-3">
|
|
|
- {loading && <div className="text-sm font-bold" style={{ color: NAVY }}><RefreshCw size={14} className="inline animate-spin mr-2" />正在加载最新详情…</div>}
|
|
|
+ {loading && <div className="text-base font-bold" style={{ color: NAVY }}><RefreshCw size={14} className="inline animate-spin mr-2" />正在加载最新详情…</div>}
|
|
|
{detail && <>
|
|
|
<Field label="名称 *"><input value={documentName} onChange={(event) => setDocumentName(event.target.value)} className={inputClass} style={selectStyle} /></Field>
|
|
|
{attachment ? <Field label="附件类型"><select value={attachmentType} onChange={(event) => setAttachmentType(event.target.value as typeof attachmentType)} className={inputClass} style={selectStyle}>{dictionaries.options("attachmentTypes").map((option) => <option key={option.code} value={option.code}>{option.label}</option>)}</select></Field> : <>
|
|
|
- {item.documentType === DocumentType.MAIN ? <Field label="分类"><select value={categoryId} onChange={(event) => setCategoryId(event.target.value)} className={inputClass} style={selectStyle}>{references.categories.map((category) => <option key={category.id} value={category.id} disabled={!category.isLeaf} title={category.categoryPath}>{category.displayLabel}{category.isLeaf ? "" : "(请选择下级分类)"}</option>)}</select></Field> : <div className="px-3 py-2 text-xs font-bold" style={{ background: "#EBF4FD", color: NAVY }}>方案分类继承自主案:{detail.categoryPath ?? detail.categoryName ?? "未分类"}</div>}
|
|
|
+ {item.documentType === DocumentType.MAIN ? <Field label="分类"><select value={categoryId} onChange={(event) => setCategoryId(event.target.value)} className={inputClass} style={selectStyle}>{references.categories.map((category) => <option key={category.id} value={category.id} disabled={!category.isLeaf} title={category.categoryPath}>{category.displayLabel}{category.isLeaf ? "" : "(请选择下级分类)"}</option>)}</select></Field> : <div className="px-3 py-2 text-base font-bold" style={{ background: "#EBF4FD", color: NAVY }}>方案分类继承自主案:{detail.categoryPath ?? detail.categoryName ?? "未分类"}</div>}
|
|
|
<div className="grid grid-cols-1 gap-3">
|
|
|
<Field label="密级"><select value={securityLevel} onChange={(event) => setSecurityLevel(event.target.value as typeof securityLevel)} className={inputClass} style={selectStyle}>{dictionaries.options("securityLevels").map((option) => <option key={option.code} value={option.code}>{option.label}</option>)}</select></Field>
|
|
|
</div>
|
|
|
{mainPlan && canManagePassword && (
|
|
|
<div className="border p-3 space-y-3" style={{ borderColor: "rgba(26,82,153,.18)", background: "#F5FAFF" }}>
|
|
|
- <div className="text-xs font-black flex items-center gap-1" style={{ color: NAVY }}>
|
|
|
+ <div className="text-base font-black flex items-center gap-1" style={{ color: NAVY }}>
|
|
|
<Lock size={13} />方案密码锁:
|
|
|
{detail.hasPassword
|
|
|
? `已设置(当前保护:${(detail.passwordProtectedActions ?? []).map((code) => PASSWORD_ACTION_LABELS[code]).join("、") || "无"})`
|
|
|
@@ -874,10 +876,10 @@ export function EditMetadataModal({ item, canManagePassword = false, canManageRe
|
|
|
<input type="password" value={pwNew} disabled={pwSubmitting} autoComplete="new-password" onChange={(event) => setPwNew(event.target.value)} maxLength={128} placeholder="留空且不点击保存则不修改" className={inputClass} style={selectStyle} />
|
|
|
</Field>
|
|
|
<div>
|
|
|
- <span className="block text-xs font-black mb-1" style={{ color: NAVY }}>需密码保护的操作 *</span>
|
|
|
+ <span className="block text-base font-black mb-1" style={{ color: NAVY }}>需密码保护的操作 *</span>
|
|
|
<div className="grid grid-cols-4 gap-1.5">
|
|
|
{ALL_PASSWORD_ACTIONS.map((code) => (
|
|
|
- <label key={code} className="flex items-center gap-1.5 text-xs font-bold" style={{ color: NAVY }}>
|
|
|
+ <label key={code} className="flex items-center gap-1.5 text-base font-bold" style={{ color: NAVY }}>
|
|
|
<input type="checkbox" checked={pwActions.includes(code)} disabled={pwSubmitting} onChange={() => setPwActions((current) => current.includes(code) ? current.filter((item) => item !== code) : [...current, code])} />
|
|
|
{PASSWORD_ACTION_LABELS[code]}
|
|
|
</label>
|
|
|
@@ -886,18 +888,18 @@ export function EditMetadataModal({ item, canManagePassword = false, canManageRe
|
|
|
</div>
|
|
|
<ErrorBlock error={pwError} />
|
|
|
<div className="flex justify-end gap-2">
|
|
|
- {detail.hasPassword && <button type="button" onClick={() => void removePasswordLock()} disabled={pwSubmitting || !pwOld} className="px-3 py-1.5 text-xs font-black border" style={selectStyle}>清除密码</button>}
|
|
|
- <button type="button" onClick={() => void savePasswordLock()} disabled={pwSubmitting || !pwNew || pwActions.length === 0} className="px-3 py-1.5 text-xs font-black text-white disabled:opacity-50" style={{ background: BLUE }}>{pwSubmitting ? "保存中" : "保存密码设置"}</button>
|
|
|
+ {detail.hasPassword && <button type="button" onClick={() => void removePasswordLock()} disabled={pwSubmitting || !pwOld} className="px-3 py-1.5 text-base font-black border" style={selectStyle}>清除密码</button>}
|
|
|
+ <button type="button" onClick={() => void savePasswordLock()} disabled={pwSubmitting || !pwNew || pwActions.length === 0} className="px-3 py-1.5 text-base font-black text-white disabled:opacity-50" style={{ background: BLUE }}>{pwSubmitting ? "保存中" : "保存密码设置"}</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
)}
|
|
|
{canManageRelations ? (
|
|
|
<div className="border p-3 space-y-2" style={{ borderColor: "rgba(26,82,153,.18)", background: "#F5FAFF" }} data-testid="document-relations-editor">
|
|
|
- <div className="text-xs font-black flex items-center gap-1" style={{ color: NAVY }}>
|
|
|
+ <div className="text-base font-black flex items-center gap-1" style={{ color: NAVY }}>
|
|
|
<Link2 size={13} />关联方案(仅可关联主案,保存后整体替换)
|
|
|
</div>
|
|
|
{relationsLoading && relationRows.length === 0 && (
|
|
|
- <div className="text-xs font-bold" style={{ color: NAVY }}><RefreshCw size={12} className="inline animate-spin mr-1" />正在加载关联…</div>
|
|
|
+ <div className="text-base font-bold" style={{ color: NAVY }}><RefreshCw size={12} className="inline animate-spin mr-1" />正在加载关联…</div>
|
|
|
)}
|
|
|
{relationRows.map((row, index) => {
|
|
|
const presetCode = relationPresets ? matchPresetCode(row.relationType, relationPresets) : null;
|
|
|
@@ -936,23 +938,23 @@ export function EditMetadataModal({ item, canManagePassword = false, canManageRe
|
|
|
{missingTarget ? <option value={row.relatedDocumentId}>{relationNames[row.relatedDocumentId] ?? `已关联主案 ${row.relatedDocumentId}`}</option> : null}
|
|
|
</select>
|
|
|
</div>
|
|
|
- <button type="button" disabled={relationSubmitting} onClick={() => setRelationRows((current) => current.filter((_, rowIndex) => rowIndex !== index))} className="col-span-1 h-9 text-xs font-black" style={{ color: NAVY }}><Trash2 size={13} className="inline" /></button>
|
|
|
+ <button type="button" disabled={relationSubmitting} onClick={() => setRelationRows((current) => current.filter((_, rowIndex) => rowIndex !== index))} className="col-span-1 h-9 text-base font-black" style={{ color: NAVY }}><Trash2 size={13} className="inline" /></button>
|
|
|
</div>
|
|
|
);
|
|
|
})}
|
|
|
- <button type="button" disabled={relationSubmitting || relationPresets === null || relationRows.length >= 50} onClick={() => setRelationRows((current) => [...current, { relatedDocumentId: "", relationType: relationPresets?.[0]?.label ?? "" }])} className="px-3 py-1.5 text-xs font-black border disabled:opacity-50" style={selectStyle}>添加关联</button>
|
|
|
- {relationFormError && <div className="text-xs font-bold" style={{ color: NAVY }}>{relationFormError}</div>}
|
|
|
+ <button type="button" disabled={relationSubmitting || relationPresets === null || relationRows.length >= 50} onClick={() => setRelationRows((current) => [...current, { relatedDocumentId: "", relationType: relationPresets?.[0]?.label ?? "" }])} className="px-3 py-1.5 text-base font-black border disabled:opacity-50" style={selectStyle}>添加关联</button>
|
|
|
+ {relationFormError && <div className="text-base font-bold" style={{ color: NAVY }}>{relationFormError}</div>}
|
|
|
<ErrorBlock error={relationError} />
|
|
|
<div className="flex justify-end">
|
|
|
- <button type="button" onClick={() => void saveRelations()} disabled={relationSubmitting || !detail || relationPresets === null} className="px-3 py-1.5 text-xs font-black text-white disabled:opacity-50" style={{ background: BLUE }}>{relationSubmitting ? "保存中" : "保存关联"}</button>
|
|
|
+ <button type="button" onClick={() => void saveRelations()} disabled={relationSubmitting || !detail || relationPresets === null} className="px-3 py-1.5 text-base font-black text-white disabled:opacity-50" style={{ background: BLUE }}>{relationSubmitting ? "保存中" : "保存关联"}</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
) : (
|
|
|
relationRows.length > 0 && (
|
|
|
<div className="border p-3 space-y-1" style={{ borderColor: "rgba(26,82,153,.18)" }} data-testid="document-relations-readonly">
|
|
|
- <div className="text-xs font-black flex items-center gap-1" style={{ color: NAVY }}><Link2 size={13} />关联方案</div>
|
|
|
+ <div className="text-base font-black flex items-center gap-1" style={{ color: NAVY }}><Link2 size={13} />关联方案</div>
|
|
|
{relationRows.map((row) => (
|
|
|
- <div key={row.relatedDocumentId} className="text-xs font-bold" style={{ color: NAVY }}>· {row.relationType} · {relationNames[row.relatedDocumentId] ?? references.mainPlans.find((plan) => plan.id === row.relatedDocumentId)?.documentName ?? row.relatedDocumentId}</div>
|
|
|
+ <div key={row.relatedDocumentId} className="text-base font-bold" style={{ color: NAVY }}>· {row.relationType} · {relationNames[row.relatedDocumentId] ?? references.mainPlans.find((plan) => plan.id === row.relatedDocumentId)?.documentName ?? row.relatedDocumentId}</div>
|
|
|
))}
|
|
|
</div>
|
|
|
)
|
|
|
@@ -960,21 +962,21 @@ export function EditMetadataModal({ item, canManagePassword = false, canManageRe
|
|
|
</>}
|
|
|
{detail.contentExtractStatus !== undefined && (
|
|
|
<div className="border p-3 space-y-2" style={{ borderColor: detail.contentExtractStatus === ContentExtractStatus.FAILED ? "rgba(200,30,30,.35)" : "rgba(26,82,153,.18)", background: detail.contentExtractStatus === ContentExtractStatus.FAILED ? "#FFF5F5" : "#F5FAFF" }} data-testid="content-extract-panel">
|
|
|
- <div className="text-xs font-black flex items-center gap-1" style={{ color: NAVY }}>
|
|
|
+ <div className="text-base font-black flex items-center gap-1" style={{ color: NAVY }}>
|
|
|
<FileSearch size={13} />正文解析:{EXTRACT_STATUS_LABELS[detail.contentExtractStatus]}
|
|
|
{detail.contentExtractedAt ? <span className="font-bold opacity-70">({detail.contentExtractedAt.replace("T", " ").replace("Z", " UTC")})</span> : null}
|
|
|
</div>
|
|
|
{detail.contentExtractStatus === ContentExtractStatus.FAILED && (
|
|
|
- <div className="text-xs font-bold" style={{ color: "#B91C1C" }} data-testid="content-extract-error">
|
|
|
+ <div className="text-base font-bold" style={{ color: "#B91C1C" }} data-testid="content-extract-error">
|
|
|
失败原因:{detail.contentExtractError ?? "未知错误"}。可点击"重新解析"重试,若持续失败请检查文件是否损坏或加密。
|
|
|
</div>
|
|
|
)}
|
|
|
{detail.contentExtractStatus === ContentExtractStatus.EMPTY && (
|
|
|
- <div className="text-xs font-bold" style={{ color: NAVY }}>未提取到正文文本,全文检索与正文预览不可用;若为扫描件属预期行为。</div>
|
|
|
+ <div className="text-sm font-bold" style={{ color: NAVY }}>未提取到正文文本,全文检索与正文预览不可用;若为扫描件属预期行为。</div>
|
|
|
)}
|
|
|
<ErrorBlock error={reExtractError} />
|
|
|
<div className="flex justify-end">
|
|
|
- <button type="button" onClick={() => void reExtract()} disabled={reExtracting} className="px-3 py-1.5 text-xs font-black text-white disabled:opacity-50" style={{ background: BLUE }} data-testid="re-extract-button">
|
|
|
+ <button type="button" onClick={() => void reExtract()} disabled={reExtracting} className="px-3 py-1.5 text-base font-black text-white disabled:opacity-50" style={{ background: BLUE }} data-testid="re-extract-button">
|
|
|
<RefreshCw size={12} className={reExtracting ? "inline animate-spin mr-1" : "inline mr-1"} />{reExtracting ? "解析中…" : "重新解析"}
|
|
|
</button>
|
|
|
</div>
|
|
|
@@ -992,12 +994,12 @@ export function EditMetadataModal({ item, canManagePassword = false, canManageRe
|
|
|
{organizationId === "" && <option value="">未设置单位</option>}
|
|
|
{organizationOptions.map((option) => <option key={option.id} value={option.id} title={option.label}>{option.label}</option>)}
|
|
|
</select>
|
|
|
- {!canManageOrganization && <span className="block mt-1 text-xs opacity-70" style={{ color: NAVY }}>调整所属单位需管理员权限</span>}
|
|
|
+ {!canManageOrganization && <span className="block mt-1 text-base opacity-70" style={{ color: NAVY }}>调整所属单位需管理员权限</span>}
|
|
|
</Field>
|
|
|
{/* 概述/标签栏已按要求隐藏:state 保留服务端原值,保存时原样提交,不改动既有数据 */}
|
|
|
</>}
|
|
|
<ErrorBlock error={error} />
|
|
|
- <div className="flex justify-end gap-2"><button onClick={onClose} disabled={submitting} className="px-4 py-2 text-sm font-black border" style={selectStyle}>取消</button><button onClick={() => void submit()} disabled={!detail || submitting || dictionaries.status !== "success"} className="px-4 py-2 text-sm font-black text-white disabled:opacity-50" style={{ background: BLUE }}><Check size={13} className="inline mr-1" />{submitting ? "提交中" : "保存元数据"}</button></div>
|
|
|
+ <div className="flex justify-end gap-2"><button onClick={onClose} disabled={submitting} className="px-4 py-2 text-base font-black border" style={selectStyle}>取消</button><button onClick={() => void submit()} disabled={!detail || submitting || dictionaries.status !== "success"} className="px-4 py-2 text-base font-black text-white disabled:opacity-50" style={{ background: BLUE }}><Check size={13} className="inline mr-1" />{submitting ? "提交中" : "保存元数据"}</button></div>
|
|
|
</div>
|
|
|
</Modal>
|
|
|
);
|
|
|
@@ -1055,9 +1057,9 @@ export function DeleteDocumentModal({ item, onClose, onDeleted }: {
|
|
|
return (
|
|
|
<Modal title={`删除 · ${item.documentName}`} onClose={onClose}>
|
|
|
<div className="p-6 space-y-4" style={{ color: NAVY }}>
|
|
|
- {loading ? <div className="text-sm font-bold"><RefreshCw size={14} className="inline animate-spin mr-2" />正在加载…</div> : <div className="text-sm font-bold leading-7">确认删除该{attachment ? "配套资料" : item.documentType === "MAIN" ? "主案" : "子方案"}?</div>}
|
|
|
+ {loading ? <div className="text-base font-bold"><RefreshCw size={14} className="inline animate-spin mr-2" />正在加载…</div> : <div className="text-base font-bold leading-7">确认删除该{attachment ? "配套资料" : item.documentType === "MAIN" ? "主案" : "子方案"}?</div>}
|
|
|
<ErrorBlock error={error} />
|
|
|
- <div className="flex justify-end gap-2"><button onClick={onClose} disabled={submitting} className="px-4 py-2 text-sm font-black border" style={selectStyle}>取消</button><button onClick={() => void remove()} disabled={!detail || submitting} className="px-4 py-2 text-sm font-black text-white disabled:opacity-50" style={{ background: NAVY }}><Trash2 size={13} className="inline mr-1" />{submitting ? "删除中" : "确认删除"}</button></div>
|
|
|
+ <div className="flex justify-end gap-2"><button onClick={onClose} disabled={submitting} className="px-4 py-2 text-base font-black border" style={selectStyle}>取消</button><button onClick={() => void remove()} disabled={!detail || submitting} className="px-4 py-2 text-base font-black text-white disabled:opacity-50" style={{ background: NAVY }}><Trash2 size={13} className="inline mr-1" />{submitting ? "删除中" : "确认删除"}</button></div>
|
|
|
</div>
|
|
|
</Modal>
|
|
|
);
|