261 lines
14 KiB
TypeScript
261 lines
14 KiB
TypeScript
import { useEffect, useMemo, useState } from 'react';
|
||
import { Database, Edit3, FileCheck2, Link2, Plus, Search, Trash2 } from 'lucide-react';
|
||
import { Breadcrumb, Button, Input, Modal, Select, Textarea, Tag } from '@/components/ui';
|
||
import { adminApi, type CommonReportField, type DictionaryItem } from '@/api/adminApi';
|
||
|
||
type DrainageField = DictionaryItem & {
|
||
code?: string;
|
||
name?: string;
|
||
fieldType?: string;
|
||
required?: boolean;
|
||
description?: string | null;
|
||
usageCount?: number;
|
||
commonUsageCount?: number;
|
||
};
|
||
|
||
type ReportFieldType = 'string' | 'image' | 'file';
|
||
|
||
const typeOptions = [
|
||
{ label: '全部类型', value: 'all' },
|
||
{ label: '字符串', value: 'string' },
|
||
{ label: '图片', value: 'image' },
|
||
{ label: '文件', value: 'file' },
|
||
];
|
||
|
||
const typeLabels: Record<string, string> = { string: '字符串', image: '图片', file: '文件' };
|
||
|
||
export function AdminDrainageFieldsPage() {
|
||
const [fields, setFields] = useState<DrainageField[]>([]);
|
||
const [commonFields, setCommonFields] = useState<CommonReportField[]>([]);
|
||
const [keyword, setKeyword] = useState('');
|
||
const [appliedKeyword, setAppliedKeyword] = useState('');
|
||
const [type, setType] = useState('all');
|
||
const [appliedType, setAppliedType] = useState('all');
|
||
const [creating, setCreating] = useState(false);
|
||
const [code, setCode] = useState('');
|
||
const [name, setName] = useState('');
|
||
const [fieldType, setFieldType] = useState<ReportFieldType>('string');
|
||
const [description, setDescription] = useState('');
|
||
const [error, setError] = useState('');
|
||
const [deleteTarget, setDeleteTarget] = useState<DrainageField | null>(null);
|
||
const [configuringCommon, setConfiguringCommon] = useState(false);
|
||
const [editingCommonId, setEditingCommonId] = useState<string>();
|
||
const [commonSaving, setCommonSaving] = useState(false);
|
||
const [commonFieldId, setCommonFieldId] = useState('');
|
||
const [commonReportType, setCommonReportType] = useState<'signature' | 'drainage'>('signature');
|
||
const [commonRequired, setCommonRequired] = useState(false);
|
||
const [commonDeleteTarget, setCommonDeleteTarget] = useState<CommonReportField | null>(null);
|
||
const codeError = code && !/^[A-Za-z0-9]+$/.test(code) ? '字段代码只能包含阿拉伯数字和英文大小写字母' : '';
|
||
|
||
function loadData() {
|
||
Promise.all([adminApi.listDrainageFields(), adminApi.listCommonReportFields()])
|
||
.then(([items, commonItems]) => {
|
||
setFields(items as DrainageField[]);
|
||
setCommonFields(commonItems);
|
||
setError('');
|
||
})
|
||
.catch((failure: Error) => setError(failure.message || '报备字段加载失败'));
|
||
}
|
||
|
||
useEffect(() => {
|
||
loadData();
|
||
}, []);
|
||
|
||
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;
|
||
}),
|
||
[appliedKeyword, appliedType, fields],
|
||
);
|
||
|
||
function createField() {
|
||
adminApi.createDrainageField({ code, name, fieldType, description, status: 'active' })
|
||
.then(() => {
|
||
setCode('');
|
||
setName('');
|
||
setFieldType('string');
|
||
setDescription('');
|
||
setCreating(false);
|
||
loadData();
|
||
})
|
||
.catch((failure: Error) => setError(failure.message || '报备字段新增失败'));
|
||
}
|
||
|
||
function deleteField() {
|
||
if (!deleteTarget || (deleteTarget.usageCount ?? 0) > 0 || (deleteTarget.commonUsageCount ?? 0) > 0) return;
|
||
adminApi.deleteDrainageField(deleteTarget.id)
|
||
.then(() => {
|
||
setDeleteTarget(null);
|
||
loadData();
|
||
})
|
||
.catch((failure: Error) => setError(failure.message || '报备字段删除失败'));
|
||
}
|
||
|
||
function createCommonField() {
|
||
if (!commonFieldId || commonSaving) return;
|
||
setCommonSaving(true);
|
||
setError('');
|
||
const body = { drainageFieldId: commonFieldId, reportType: commonReportType, required: commonRequired };
|
||
const request = editingCommonId ? adminApi.updateCommonReportField(editingCommonId, body) : adminApi.createCommonReportField(body);
|
||
request
|
||
.then(() => {
|
||
setCommonFieldId('');
|
||
setCommonReportType('signature');
|
||
setCommonRequired(false);
|
||
setConfiguringCommon(false);
|
||
loadData();
|
||
})
|
||
.catch((failure: Error) => setError(failure.message || '通用字段配置失败'))
|
||
.finally(() => setCommonSaving(false));
|
||
}
|
||
|
||
function openCommonField(field?: CommonReportField) {
|
||
setEditingCommonId(field?.id);
|
||
setCommonFieldId(field?.drainageFieldId ?? '');
|
||
setCommonReportType(field?.reportType ?? 'signature');
|
||
setCommonRequired(field?.required ?? false);
|
||
setError('');
|
||
setConfiguringCommon(true);
|
||
}
|
||
|
||
function deleteCommonField() {
|
||
if (!commonDeleteTarget) return;
|
||
adminApi.deleteCommonReportField(commonDeleteTarget.id)
|
||
.then(() => {
|
||
setCommonDeleteTarget(null);
|
||
loadData();
|
||
})
|
||
.catch((failure: Error) => setError(failure.message || '通用字段删除失败'));
|
||
}
|
||
|
||
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;
|
||
|
||
return (
|
||
<section className="page-stack admin-system-page admin-drainage-page">
|
||
<div className="page-heading">
|
||
<div>
|
||
<Breadcrumb items={['基础配置', '报备字段库']} />
|
||
<h1>报备字段库</h1>
|
||
<p>统一维护签名和引流信息的资料字段,并配置全平台通用报备要求。</p>
|
||
</div>
|
||
<Button icon={<Plus size={16} />} onClick={() => setCreating(true)} 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>
|
||
</div>
|
||
|
||
<div className="surface admin-drainage-toolbar">
|
||
<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>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="surface admin-drainage-section">
|
||
<div className="admin-drainage-section__heading">
|
||
<div>
|
||
<h2>通用字段配置</h2>
|
||
<p>企业新增或编辑签名、引流信息时必须按这里的配置填写,通道字段也可以直接引用。</p>
|
||
</div>
|
||
<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} tone="info" />
|
||
<CommonFieldGroup fields={drainageCommon} label="引流信息报备资料" onEdit={openCommonField} onDelete={setCommonDeleteTarget} 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><Button aria-label={`删除${field.name}`} disabled={locked} icon={<Trash2 size={14} />} iconOnly onClick={() => setDeleteTarget(field)} size="sm" variant="ghost">删除</Button></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></>}
|
||
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)} />
|
||
<p>修改后用于后续新增、编辑时的资料要求;历史报备资料和要求快照保持不变。</p>
|
||
{error ? <p className="form-error">{error}</p> : null}
|
||
</div>
|
||
</Modal>
|
||
|
||
<Modal
|
||
footer={(
|
||
<>
|
||
<Button onClick={() => setCreating(false)} variant="ghost">取消</Button>
|
||
<Button disabled={!code || !name || Boolean(codeError)} onClick={createField}>保存</Button>
|
||
</>
|
||
)}
|
||
onClose={() => setCreating(false)}
|
||
open={creating}
|
||
title="添加报备字段"
|
||
>
|
||
<div className="admin-system-modal-form">
|
||
<Input error={codeError} label="字段代码" onChange={(event) => setCode(event.target.value)} placeholder="仅允许数字和英文字母" value={code} />
|
||
<Input label="字段名称" onChange={(event) => setName(event.target.value)} value={name} />
|
||
<Select
|
||
label="字段类型"
|
||
onChange={(event) => setFieldType(event.target.value as ReportFieldType)}
|
||
options={typeOptions.filter((option) => option.value !== 'all')}
|
||
value={fieldType}
|
||
/>
|
||
<Textarea
|
||
className="admin-system-modal-form__wide"
|
||
label="描述"
|
||
onChange={(event) => setDescription(event.target.value)}
|
||
rows={4}
|
||
value={description}
|
||
/>
|
||
</div>
|
||
</Modal>
|
||
{deleteTarget ? (
|
||
<Modal
|
||
footer={<><Button onClick={() => setDeleteTarget(null)} variant="ghost">取消</Button><Button onClick={deleteField} variant="danger">确认删除</Button></>}
|
||
onClose={() => setDeleteTarget(null)}
|
||
open
|
||
title="删除报备字段"
|
||
>
|
||
<p>确认删除“{deleteTarget.name}”吗?未被通道使用的字段将从数据库中永久删除。</p>
|
||
</Modal>
|
||
) : null}
|
||
{commonDeleteTarget ? (
|
||
<Modal
|
||
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>
|
||
</Modal>
|
||
) : null}
|
||
</section>
|
||
);
|
||
}
|
||
|
||
function CommonFieldGroup({ fields, label, onEdit, onDelete, tone }: { fields: CommonReportField[]; label: string; onEdit: (field: CommonReportField) => void; onDelete: (field: CommonReportField) => void; 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) => <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><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>;
|
||
}
|