Files
lislgosms/src/apps/admin/AdminDrainageFieldsPage.tsx
T

261 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>;
}