feat: add reconciliation and quality reporting

This commit is contained in:
hectorzhao
2026-07-15 14:22:50 +08:00
parent 16311546af
commit 8c3336600e
32 changed files with 1730 additions and 200 deletions
+81 -5
View File
@@ -1,7 +1,7 @@
import { useEffect, useMemo, useState } from 'react';
import { Plus, Search, Trash2 } from 'lucide-react';
import { Breadcrumb, Button, Input, Modal, Select, Table, Textarea, Tag, type TableColumn } from '@/components/ui';
import { adminApi, type DictionaryItem } from '@/api/adminApi';
import { adminApi, type CommonReportField, type DictionaryItem } from '@/api/adminApi';
type DrainageField = DictionaryItem & {
code?: string;
@@ -10,6 +10,7 @@ type DrainageField = DictionaryItem & {
required?: boolean;
description?: string | null;
usageCount?: number;
commonUsageCount?: number;
};
type ReportFieldType = 'string' | 'image' | 'file';
@@ -25,6 +26,7 @@ const typeLabels: Record<string, string> = { string: '字符串', image: '图片
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');
@@ -36,12 +38,18 @@ export function AdminDrainageFieldsPage() {
const [description, setDescription] = useState('');
const [error, setError] = useState('');
const [deleteTarget, setDeleteTarget] = useState<DrainageField | null>(null);
const [configuringCommon, setConfiguringCommon] = 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() {
adminApi.listDrainageFields()
.then((items) => {
Promise.all([adminApi.listDrainageFields(), adminApi.listCommonReportFields()])
.then(([items, commonItems]) => {
setFields(items as DrainageField[]);
setCommonFields(commonItems);
setError('');
})
.catch((failure: Error) => setError(failure.message || '报备字段加载失败'));
@@ -74,7 +82,7 @@ export function AdminDrainageFieldsPage() {
}
function deleteField() {
if (!deleteTarget || (deleteTarget.usageCount ?? 0) > 0) return;
if (!deleteTarget || (deleteTarget.usageCount ?? 0) > 0 || (deleteTarget.commonUsageCount ?? 0) > 0) return;
adminApi.deleteDrainageField(deleteTarget.id)
.then(() => {
setDeleteTarget(null);
@@ -83,6 +91,29 @@ export function AdminDrainageFieldsPage() {
.catch((failure: Error) => setError(failure.message || '报备字段删除失败'));
}
function createCommonField() {
if (!commonFieldId) return;
adminApi.createCommonReportField({ drainageFieldId: commonFieldId, reportType: commonReportType, required: commonRequired })
.then(() => {
setCommonFieldId('');
setCommonReportType('signature');
setCommonRequired(false);
setConfiguringCommon(false);
loadData();
})
.catch((failure: Error) => setError(failure.message || '通用字段配置失败'));
}
function deleteCommonField() {
if (!commonDeleteTarget) return;
adminApi.deleteCommonReportField(commonDeleteTarget.id)
.then(() => {
setCommonDeleteTarget(null);
loadData();
})
.catch((failure: Error) => setError(failure.message || '通用字段删除失败'));
}
const columns = useMemo<Array<TableColumn<DrainageField>>>(() => [
{ key: 'code', title: '字段代码', width: '160px', render: (record) => <strong>{record.code}</strong> },
{ key: 'name', title: '字段名称', width: '190px', render: (record) => record.name ?? '-' },
@@ -90,7 +121,17 @@ export function AdminDrainageFieldsPage() {
{ key: 'description', title: '描述', render: (record) => record.description ?? '-' },
{ key: 'required', title: '是否必填', width: '120px', render: (record) => <Tag tone={record.required ? 'warning' : 'info'}>{record.required ? '必填' : '选填'}</Tag> },
{ key: 'usageCount', title: '使用通道数', width: '130px', render: (record) => <Tag tone={(record.usageCount ?? 0) > 0 ? 'warning' : 'neutral'}>{record.usageCount ?? 0}</Tag> },
{ key: 'actions', title: '操作', width: '120px', align: 'right', render: (record) => <Button disabled={(record.usageCount ?? 0) > 0} icon={<Trash2 size={14} />} onClick={() => setDeleteTarget(record)} size="sm" variant="danger"></Button> },
{ key: 'commonUsageCount', title: '通用配置数', width: '130px', render: (record) => <Tag tone={(record.commonUsageCount ?? 0) > 0 ? 'info' : 'neutral'}>{record.commonUsageCount ?? 0}</Tag> },
{ key: 'actions', title: '操作', width: '120px', align: 'right', render: (record) => <Button disabled={(record.usageCount ?? 0) > 0 || (record.commonUsageCount ?? 0) > 0} icon={<Trash2 size={14} />} onClick={() => setDeleteTarget(record)} size="sm" variant="danger"></Button> },
], []);
const commonColumns = useMemo<Array<TableColumn<CommonReportField>>>(() => [
{ key: 'name', title: '字段名称', render: (record) => <strong>{String(record.drainageField.name ?? record.drainageField.code ?? '-')}</strong> },
{ key: 'code', title: '字段代码', width: '170px', render: (record) => String(record.drainageField.code ?? '-') },
{ key: 'reportType', title: '资料用途', width: '180px', render: (record) => <Tag tone={record.reportType === 'signature' ? 'info' : 'warning'}>{record.reportType === 'signature' ? '签名报备资料' : '引流信息报备资料'}</Tag> },
{ key: 'fieldType', title: '字段类型', width: '130px', render: (record) => typeLabels[String(record.drainageField.fieldType ?? '')] ?? String(record.drainageField.fieldType ?? '-') },
{ key: 'required', title: '是否必填', width: '120px', render: (record) => <Tag tone={record.required ? 'warning' : 'neutral'}>{record.required ? '必填' : '选填'}</Tag> },
{ key: 'actions', title: '操作', width: '120px', align: 'right', render: (record) => <Button icon={<Trash2 size={14} />} onClick={() => setCommonDeleteTarget(record)} size="sm" variant="danger"></Button> },
], []);
return (
@@ -114,9 +155,34 @@ export function AdminDrainageFieldsPage() {
</div>
<div className="surface admin-system-table-card admin-drainage-table-card">
<div className="page-heading">
<div>
<h2></h2>
<p></p>
</div>
<Button icon={<Plus size={18} />} onClick={() => setConfiguringCommon(true)}></Button>
</div>
<Table columns={commonColumns} data={commonFields} emptyText="暂无通用字段配置" rowKey="id" />
</div>
<div className="surface admin-system-table-card admin-drainage-table-card">
<div className="page-heading"><div><h2></h2><p></p></div></div>
<Table columns={columns} data={filteredFields} emptyText="暂无字段" rowKey="id" />
</div>
<Modal
footer={<><Button onClick={() => setConfiguringCommon(false)} variant="ghost"></Button><Button disabled={!commonFieldId} onClick={createCommonField}></Button></>}
onClose={() => setConfiguringCommon(false)}
open={configuringCommon}
title="配置通用字段"
>
<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)} />
</div>
</Modal>
<Modal
footer={(
<>
@@ -156,6 +222,16 @@ export function AdminDrainageFieldsPage() {
<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>
);
}