feat: support WPS report material workbooks

This commit is contained in:
hectorzhao
2026-09-04 17:10:04 +08:00
parent 48d0363920
commit fb39c8b606
29 changed files with 2737 additions and 734 deletions
+328 -44
View File
@@ -65,11 +65,16 @@ export function AdminDrainageFieldsPage() {
}, []);
const filteredFields = useMemo(
() => fields.filter((field) => {
const matchesKeyword = !appliedKeyword || [field.code, field.name, field.fieldType, field.description].some((value) => String(value ?? '').includes(appliedKeyword));
const matchesType = appliedType === 'all' || field.fieldType === appliedType;
return matchesKeyword && matchesType;
}),
() =>
fields.filter((field) => {
const matchesKeyword =
!appliedKeyword ||
[field.code, field.name, field.fieldType, field.description].some((value) =>
String(value ?? '').includes(appliedKeyword),
);
const matchesType = appliedType === 'all' || field.fieldType === appliedType;
return matchesKeyword && matchesType;
}),
[appliedKeyword, appliedType, fields],
);
@@ -109,7 +114,8 @@ export function AdminDrainageFieldsPage() {
function deleteField() {
if (!deleteTarget || (deleteTarget.usageCount ?? 0) > 0 || (deleteTarget.commonUsageCount ?? 0) > 0) return;
adminApi.deleteDrainageField(deleteTarget.id)
adminApi
.deleteDrainageField(deleteTarget.id)
.then(() => {
setDeleteTarget(null);
loadData();
@@ -122,7 +128,9 @@ export function AdminDrainageFieldsPage() {
setCommonSaving(true);
setError('');
const body = { drainageFieldId: commonFieldId, reportType: commonReportType, required: commonRequired };
const request = editingCommonId ? adminApi.updateCommonReportField(editingCommonId, body) : adminApi.createCommonReportField(body);
const request = editingCommonId
? adminApi.updateCommonReportField(editingCommonId, body)
: adminApi.createCommonReportField(body);
request
.then(() => {
setCommonFieldId('');
@@ -146,7 +154,8 @@ export function AdminDrainageFieldsPage() {
function deleteCommonField() {
if (!commonDeleteTarget) return;
adminApi.deleteCommonReportField(commonDeleteTarget.id)
adminApi
.deleteCommonReportField(commonDeleteTarget.id)
.then(() => {
setCommonDeleteTarget(null);
loadData();
@@ -176,7 +185,9 @@ export function AdminDrainageFieldsPage() {
const signatureCommon = commonFields.filter((field) => field.reportType === 'signature');
const drainageCommon = commonFields.filter((field) => field.reportType === 'drainage');
const referencedCount = fields.filter((field) => (field.usageCount ?? 0) > 0 || (field.commonUsageCount ?? 0) > 0).length;
const referencedCount = fields.filter(
(field) => (field.usageCount ?? 0) > 0 || (field.commonUsageCount ?? 0) > 0,
).length;
return (
<section className="page-stack admin-system-page admin-drainage-page">
@@ -186,22 +197,68 @@ export function AdminDrainageFieldsPage() {
<h1></h1>
<p></p>
</div>
<Button icon={<Plus size={16} />} onClick={() => openFieldModal()} size="sm"></Button>
</div>
{error ? <p className="form-error">{error}</p> : null}
<div className="admin-drainage-summary">
<article><span><Database size={18} /></span><div><strong>{fields.length}</strong><p></p></div></article>
<article><span><FileCheck2 size={18} /></span><div><strong>{commonFields.length}</strong><p></p></div></article>
<article><span><Link2 size={18} /></span><div><strong>{referencedCount}</strong><p></p></div></article>
<article>
<span>
<Database size={18} />
</span>
<div>
<strong>{fields.length}</strong>
<p></p>
</div>
</article>
<article>
<span>
<FileCheck2 size={18} />
</span>
<div>
<strong>{commonFields.length}</strong>
<p></p>
</div>
</article>
<article>
<span>
<Link2 size={18} />
</span>
<div>
<strong>{referencedCount}</strong>
<p></p>
</div>
</article>
</div>
<div className="surface admin-drainage-toolbar">
<Input onChange={(event) => setKeyword(event.target.value)} placeholder="搜索字段名称、代码或描述..." prefix={<Search size={16} />} value={keyword} />
<Input
onChange={(event) => setKeyword(event.target.value)}
placeholder="搜索字段名称、代码或描述..."
prefix={<Search size={16} />}
value={keyword}
/>
<Select onChange={(event) => setType(event.target.value)} options={typeOptions} value={type} />
<div className="admin-system-toolbar__actions">
<Button icon={<Search size={16} />} onClick={() => { setAppliedKeyword(keyword.trim()); setAppliedType(type); }}></Button>
<Button onClick={() => { setKeyword(''); setType('all'); setAppliedKeyword(''); setAppliedType('all'); }} variant="ghost"></Button>
<Button
icon={<Search size={16} />}
onClick={() => {
setAppliedKeyword(keyword.trim());
setAppliedType(type);
}}
>
</Button>
<Button
onClick={() => {
setKeyword('');
setType('all');
setAppliedKeyword('');
setAppliedType('all');
}}
variant="ghost"
>
</Button>
</div>
</div>
@@ -211,63 +268,185 @@ export function AdminDrainageFieldsPage() {
<h2></h2>
<p></p>
</div>
<Button icon={<Plus size={16} />} onClick={() => openCommonField()} size="sm" variant="secondary"></Button>
<Button icon={<Plus size={16} />} onClick={() => openCommonField()} size="sm" variant="secondary">
</Button>
</div>
<div className="admin-drainage-common-grid">
<CommonFieldGroup fields={signatureCommon} label="签名报备资料" onEdit={openCommonField} onDelete={setCommonDeleteTarget} onMove={moveCommonField} orderingId={commonOrderingId} tone="info" />
<CommonFieldGroup fields={drainageCommon} label="引流信息报备资料" onEdit={openCommonField} onDelete={setCommonDeleteTarget} onMove={moveCommonField} orderingId={commonOrderingId} tone="warning" />
<CommonFieldGroup
fields={signatureCommon}
label="签名报备资料"
onEdit={openCommonField}
onDelete={setCommonDeleteTarget}
onMove={moveCommonField}
orderingId={commonOrderingId}
tone="info"
/>
<CommonFieldGroup
fields={drainageCommon}
label="引流信息报备资料"
onEdit={openCommonField}
onDelete={setCommonDeleteTarget}
onMove={moveCommonField}
orderingId={commonOrderingId}
tone="warning"
/>
</div>
</div>
<div className="surface admin-drainage-section">
<div className="admin-drainage-section__heading"><div><h2></h2><p> {filteredFields.length} </p></div></div>
{filteredFields.length ? <div className="admin-drainage-field-grid">{filteredFields.map((field) => {
const locked = (field.usageCount ?? 0) > 0 || (field.commonUsageCount ?? 0) > 0;
return <article className="admin-drainage-field-card" key={field.id}>
<div className="admin-drainage-field-card__top"><span className="admin-drainage-type">{typeLabels[field.fieldType ?? ''] ?? field.fieldType}</span><div className="admin-drainage-field-card__actions"><Button aria-label={`编辑${field.name}`} icon={<Edit3 size={14} />} iconOnly onClick={() => openFieldModal(field)} size="sm" variant="ghost"></Button><Button aria-label={`删除${field.name}`} disabled={locked} icon={<Trash2 size={14} />} iconOnly onClick={() => setDeleteTarget(field)} size="sm" variant="ghost"></Button></div></div>
<h3>{field.name ?? '-'}</h3><code>{field.code}</code><p>{field.description || '暂无字段说明'}</p>
<div className="admin-drainage-field-card__meta"><span> <strong>{field.usageCount ?? 0}</strong></span><span> <strong>{field.commonUsageCount ?? 0}</strong></span></div>
</article>;
})}</div> : <div className="admin-drainage-empty"></div>}
<div className="admin-drainage-section__heading">
<div>
<h2></h2>
<p> {filteredFields.length} </p>
</div>
<Button icon={<Plus size={16} />} onClick={() => openFieldModal()} size="sm">
</Button>
</div>
{filteredFields.length ? (
<div className="admin-drainage-field-grid">
{filteredFields.map((field) => {
const locked = (field.usageCount ?? 0) > 0 || (field.commonUsageCount ?? 0) > 0;
return (
<article className="admin-drainage-field-card" key={field.id}>
<div className="admin-drainage-field-card__top">
<span className="admin-drainage-type">{typeLabels[field.fieldType ?? ''] ?? field.fieldType}</span>
<div className="admin-drainage-field-card__actions">
<Button
aria-label={`编辑${field.name}`}
icon={<Edit3 size={14} />}
iconOnly
onClick={() => openFieldModal(field)}
size="sm"
variant="ghost"
>
</Button>
<Button
aria-label={`删除${field.name}`}
disabled={locked}
icon={<Trash2 size={14} />}
iconOnly
onClick={() => setDeleteTarget(field)}
size="sm"
variant="ghost"
>
</Button>
</div>
</div>
<h3>{field.name ?? '-'}</h3>
<code>{field.code}</code>
<p>{field.description || '暂无字段说明'}</p>
<div className="admin-drainage-field-card__meta">
<span>
<strong>{field.usageCount ?? 0}</strong>
</span>
<span>
<strong>{field.commonUsageCount ?? 0}</strong>
</span>
</div>
</article>
);
})}
</div>
) : (
<div className="admin-drainage-empty"></div>
)}
</div>
<Modal
footer={<><Button disabled={commonSaving} onClick={() => setConfiguringCommon(false)} variant="ghost"></Button><Button disabled={!commonFieldId || commonSaving} onClick={createCommonField}>{commonSaving ? '保存中...' : '保存'}</Button></>}
footer={
<>
<Button disabled={commonSaving} onClick={() => setConfiguringCommon(false)} variant="ghost">
</Button>
<Button disabled={!commonFieldId || commonSaving} onClick={createCommonField}>
{commonSaving ? '保存中...' : '保存'}
</Button>
</>
}
onClose={() => setConfiguringCommon(false)}
open={configuringCommon}
title={editingCommonId ? '修改通用字段配置' : '配置通用字段'}
>
<div className="admin-system-modal-form">
<Select label="报备字段" onChange={(event) => setCommonFieldId(event.target.value)} options={[{ label: '请选择字段', value: '' }, ...fields.filter((field) => field.status !== 'deleted').map((field) => ({ label: `${field.name}${field.code}`, value: field.id }))]} value={commonFieldId} />
<Select label="资料用途" onChange={(event) => setCommonReportType(event.target.value as 'signature' | 'drainage')} options={[{ label: '签名报备资料', value: 'signature' }, { label: '引流信息报备资料', value: 'drainage' }]} value={commonReportType} />
<Select label="是否必填" onChange={(event) => setCommonRequired(event.target.value === 'true')} options={[{ label: '选填', value: 'false' }, { label: '必填', value: 'true' }]} value={String(commonRequired)} />
<Select
label="报备字段"
onChange={(event) => setCommonFieldId(event.target.value)}
options={[
{ label: '请选择字段', value: '' },
...fields
.filter((field) => field.status !== 'deleted')
.map((field) => ({ label: `${field.name}${field.code}`, value: field.id })),
]}
value={commonFieldId}
/>
<Select
label="资料用途"
onChange={(event) => setCommonReportType(event.target.value as 'signature' | 'drainage')}
options={[
{ label: '签名报备资料', value: 'signature' },
{ label: '引流信息报备资料', value: 'drainage' },
]}
value={commonReportType}
/>
<Select
label="是否必填"
onChange={(event) => setCommonRequired(event.target.value === 'true')}
options={[
{ label: '选填', value: 'false' },
{ label: '必填', value: 'true' },
]}
value={String(commonRequired)}
/>
<p></p>
{error ? <p className="form-error">{error}</p> : null}
</div>
</Modal>
<Modal
footer={(
footer={
<>
<Button disabled={fieldSaving} onClick={closeFieldModal} variant="ghost"></Button>
<Button disabled={!code || !name || Boolean(codeError) || fieldSaving} onClick={saveField}>{fieldSaving ? '保存中...' : '保存'}</Button>
<Button disabled={fieldSaving} onClick={closeFieldModal} variant="ghost">
</Button>
<Button disabled={!code || !name || Boolean(codeError) || fieldSaving} onClick={saveField}>
{fieldSaving ? '保存中...' : '保存'}
</Button>
</>
)}
}
onClose={closeFieldModal}
open={creating}
title={editingField ? '编辑报备字段' : '添加报备字段'}
>
<div className="admin-system-modal-form">
<Input disabled={Boolean(editingField && ((editingField.usageCount ?? 0) > 0 || (editingField.commonUsageCount ?? 0) > 0))} error={codeError} label="字段代码" onChange={(event) => setCode(event.target.value)} placeholder="仅允许数字和英文字母" value={code} />
<Input
disabled={Boolean(
editingField && ((editingField.usageCount ?? 0) > 0 || (editingField.commonUsageCount ?? 0) > 0),
)}
error={codeError}
label="字段代码"
onChange={(event) => setCode(event.target.value)}
placeholder="仅允许数字和英文字母"
value={code}
/>
<Input label="字段名称" onChange={(event) => setName(event.target.value)} value={name} />
<Select
disabled={Boolean(editingField && ((editingField.usageCount ?? 0) > 0 || (editingField.commonUsageCount ?? 0) > 0))}
disabled={Boolean(
editingField && ((editingField.usageCount ?? 0) > 0 || (editingField.commonUsageCount ?? 0) > 0),
)}
label="字段类型"
onChange={(event) => setFieldType(event.target.value as ReportFieldType)}
options={typeOptions.filter((option) => option.value !== 'all')}
value={fieldType}
/>
{editingField && ((editingField.usageCount ?? 0) > 0 || (editingField.commonUsageCount ?? 0) > 0) ? <p className="admin-system-modal-form__wide"></p> : null}
{editingField && ((editingField.usageCount ?? 0) > 0 || (editingField.commonUsageCount ?? 0) > 0) ? (
<p className="admin-system-modal-form__wide">
</p>
) : null}
<Textarea
className="admin-system-modal-form__wide"
label="描述"
@@ -279,7 +458,16 @@ export function AdminDrainageFieldsPage() {
</Modal>
{deleteTarget ? (
<Modal
footer={<><Button onClick={() => setDeleteTarget(null)} variant="ghost"></Button><Button onClick={deleteField} variant="danger"></Button></>}
footer={
<>
<Button onClick={() => setDeleteTarget(null)} variant="ghost">
</Button>
<Button onClick={deleteField} variant="danger">
</Button>
</>
}
onClose={() => setDeleteTarget(null)}
open
title="删除报备字段"
@@ -289,18 +477,114 @@ export function AdminDrainageFieldsPage() {
) : null}
{commonDeleteTarget ? (
<Modal
footer={<><Button onClick={() => setCommonDeleteTarget(null)} variant="ghost"></Button><Button onClick={deleteCommonField} variant="danger"></Button></>}
footer={
<>
<Button onClick={() => setCommonDeleteTarget(null)} variant="ghost">
</Button>
<Button onClick={deleteCommonField} variant="danger">
</Button>
</>
}
onClose={() => setCommonDeleteTarget(null)}
open
title="删除通用字段配置"
>
<p>{String(commonDeleteTarget.drainageField.name ?? commonDeleteTarget.drainageField.code)}{commonDeleteTarget.reportType === 'signature' ? '签名报备' : '引流信息报备'}</p>
<p>
{String(commonDeleteTarget.drainageField.name ?? commonDeleteTarget.drainageField.code)}
{commonDeleteTarget.reportType === 'signature' ? '签名报备' : '引流信息报备'}
</p>
</Modal>
) : null}
</section>
);
}
function CommonFieldGroup({ fields, label, onEdit, onDelete, onMove, orderingId, tone }: { fields: CommonReportField[]; label: string; onEdit: (field: CommonReportField) => void; onDelete: (field: CommonReportField) => void; onMove: (field: CommonReportField, direction: -1 | 1) => void; orderingId?: string; tone: 'info' | 'warning' }) {
return <section className="admin-drainage-common-group"><div className="admin-drainage-common-group__title"><Tag tone={tone}>{label}</Tag><span>{fields.length} </span></div>{fields.length ? <div className="admin-drainage-common-list">{fields.map((field, index) => <div key={field.id}><div><strong>{String(field.drainageField.name ?? field.drainageField.code)}</strong><span>{String(field.drainageField.code)} · {typeLabels[String(field.drainageField.fieldType ?? '')] ?? '-'}</span></div><Tag tone={field.required ? 'warning' : 'neutral'}>{field.required ? '必填' : '选填'}</Tag><div className="admin-drainage-common-order"><Button aria-label={`上移通用字段${field.drainageField.name}`} disabled={index === 0 || Boolean(orderingId)} icon={<ArrowUp size={14} />} iconOnly onClick={() => onMove(field, -1)} size="sm" variant="ghost"></Button><Button aria-label={`下移通用字段${field.drainageField.name}`} disabled={index === fields.length - 1 || Boolean(orderingId)} icon={<ArrowDown size={14} />} iconOnly onClick={() => onMove(field, 1)} size="sm" variant="ghost"></Button></div><Button aria-label={`修改通用字段${field.drainageField.name}`} icon={<Edit3 size={14} />} onClick={() => onEdit(field)} size="sm" variant="ghost"></Button><Button aria-label="删除通用字段" icon={<Trash2 size={14} />} iconOnly onClick={() => onDelete(field)} size="sm" variant="ghost"></Button></div>)}</div> : <p className="admin-drainage-common-empty"></p>}</section>;
function CommonFieldGroup({
fields,
label,
onEdit,
onDelete,
onMove,
orderingId,
tone,
}: {
fields: CommonReportField[];
label: string;
onEdit: (field: CommonReportField) => void;
onDelete: (field: CommonReportField) => void;
onMove: (field: CommonReportField, direction: -1 | 1) => void;
orderingId?: string;
tone: 'info' | 'warning';
}) {
return (
<section className="admin-drainage-common-group">
<div className="admin-drainage-common-group__title">
<Tag tone={tone}>{label}</Tag>
<span>{fields.length} </span>
</div>
{fields.length ? (
<div className="admin-drainage-common-list">
{fields.map((field, index) => (
<div key={field.id}>
<div>
<strong>{String(field.drainageField.name ?? field.drainageField.code)}</strong>
<span>
{String(field.drainageField.code)} · {typeLabels[String(field.drainageField.fieldType ?? '')] ?? '-'}
</span>
</div>
<Tag tone={field.required ? 'warning' : 'neutral'}>{field.required ? '必填' : '选填'}</Tag>
<div className="admin-drainage-common-order">
<Button
aria-label={`上移通用字段${field.drainageField.name}`}
disabled={index === 0 || Boolean(orderingId)}
icon={<ArrowUp size={14} />}
iconOnly
onClick={() => onMove(field, -1)}
size="sm"
variant="ghost"
>
</Button>
<Button
aria-label={`下移通用字段${field.drainageField.name}`}
disabled={index === fields.length - 1 || Boolean(orderingId)}
icon={<ArrowDown size={14} />}
iconOnly
onClick={() => onMove(field, 1)}
size="sm"
variant="ghost"
>
</Button>
</div>
<Button
aria-label={`修改通用字段${field.drainageField.name}`}
icon={<Edit3 size={14} />}
onClick={() => onEdit(field)}
size="sm"
variant="ghost"
>
</Button>
<Button
aria-label="删除通用字段"
icon={<Trash2 size={14} />}
iconOnly
onClick={() => onDelete(field)}
size="sm"
variant="ghost"
>
</Button>
</div>
))}
</div>
) : (
<p className="admin-drainage-common-empty"></p>
)}
</section>
);
}