feat: enhance operations dashboard and reporting controls
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Database, Edit3, FileCheck2, Link2, Plus, Search, Trash2 } from 'lucide-react';
|
||||
import { ArrowDown, ArrowUp, 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';
|
||||
|
||||
@@ -32,6 +32,8 @@ export function AdminDrainageFieldsPage() {
|
||||
const [type, setType] = useState('all');
|
||||
const [appliedType, setAppliedType] = useState('all');
|
||||
const [creating, setCreating] = useState(false);
|
||||
const [editingField, setEditingField] = useState<DrainageField | null>(null);
|
||||
const [fieldSaving, setFieldSaving] = useState(false);
|
||||
const [code, setCode] = useState('');
|
||||
const [name, setName] = useState('');
|
||||
const [fieldType, setFieldType] = useState<ReportFieldType>('string');
|
||||
@@ -45,6 +47,7 @@ export function AdminDrainageFieldsPage() {
|
||||
const [commonReportType, setCommonReportType] = useState<'signature' | 'drainage'>('signature');
|
||||
const [commonRequired, setCommonRequired] = useState(false);
|
||||
const [commonDeleteTarget, setCommonDeleteTarget] = useState<CommonReportField | null>(null);
|
||||
const [commonOrderingId, setCommonOrderingId] = useState<string>();
|
||||
const codeError = code && !/^[A-Za-z0-9]+$/.test(code) ? '字段代码只能包含阿拉伯数字和英文大小写字母' : '';
|
||||
|
||||
function loadData() {
|
||||
@@ -70,17 +73,38 @@ export function AdminDrainageFieldsPage() {
|
||||
[appliedKeyword, appliedType, fields],
|
||||
);
|
||||
|
||||
function createField() {
|
||||
adminApi.createDrainageField({ code, name, fieldType, description, status: 'active' })
|
||||
function closeFieldModal() {
|
||||
setCreating(false);
|
||||
setEditingField(null);
|
||||
setCode('');
|
||||
setName('');
|
||||
setFieldType('string');
|
||||
setDescription('');
|
||||
}
|
||||
|
||||
function openFieldModal(field?: DrainageField) {
|
||||
setEditingField(field ?? null);
|
||||
setCode(field?.code ?? '');
|
||||
setName(field?.name ?? '');
|
||||
setFieldType((field?.fieldType as ReportFieldType | undefined) ?? 'string');
|
||||
setDescription(field?.description ?? '');
|
||||
setError('');
|
||||
setCreating(true);
|
||||
}
|
||||
|
||||
function saveField() {
|
||||
if (fieldSaving) return;
|
||||
setFieldSaving(true);
|
||||
const request = editingField
|
||||
? adminApi.updateDrainageField(editingField.id, { code, name, fieldType, description })
|
||||
: adminApi.createDrainageField({ code, name, fieldType, description, status: 'active' });
|
||||
request
|
||||
.then(() => {
|
||||
setCode('');
|
||||
setName('');
|
||||
setFieldType('string');
|
||||
setDescription('');
|
||||
setCreating(false);
|
||||
closeFieldModal();
|
||||
loadData();
|
||||
})
|
||||
.catch((failure: Error) => setError(failure.message || '报备字段新增失败'));
|
||||
.catch((failure: Error) => setError(failure.message || '报备字段保存失败'))
|
||||
.finally(() => setFieldSaving(false));
|
||||
}
|
||||
|
||||
function deleteField() {
|
||||
@@ -130,6 +154,26 @@ export function AdminDrainageFieldsPage() {
|
||||
.catch((failure: Error) => setError(failure.message || '通用字段删除失败'));
|
||||
}
|
||||
|
||||
async function moveCommonField(field: CommonReportField, direction: -1 | 1) {
|
||||
if (commonOrderingId) return;
|
||||
const group = commonFields.filter((item) => item.reportType === field.reportType);
|
||||
const currentIndex = group.findIndex((item) => item.id === field.id);
|
||||
const targetIndex = currentIndex + direction;
|
||||
if (currentIndex < 0 || targetIndex < 0 || targetIndex >= group.length) return;
|
||||
const ids = group.map((item) => item.id);
|
||||
[ids[currentIndex], ids[targetIndex]] = [ids[targetIndex], ids[currentIndex]];
|
||||
setCommonOrderingId(field.id);
|
||||
setError('');
|
||||
try {
|
||||
await adminApi.reorderCommonReportFields({ reportType: field.reportType, ids });
|
||||
loadData();
|
||||
} catch (failure) {
|
||||
setError(failure instanceof Error ? failure.message : '通用字段顺序调整失败');
|
||||
} finally {
|
||||
setCommonOrderingId(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -142,7 +186,7 @@ export function AdminDrainageFieldsPage() {
|
||||
<h1>报备字段库</h1>
|
||||
<p>统一维护签名和引流信息的资料字段,并配置全平台通用报备要求。</p>
|
||||
</div>
|
||||
<Button icon={<Plus size={16} />} onClick={() => setCreating(true)} size="sm">添加字段</Button>
|
||||
<Button icon={<Plus size={16} />} onClick={() => openFieldModal()} size="sm">添加字段</Button>
|
||||
</div>
|
||||
{error ? <p className="form-error">{error}</p> : null}
|
||||
|
||||
@@ -170,8 +214,8 @@ export function AdminDrainageFieldsPage() {
|
||||
<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" />
|
||||
<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>
|
||||
|
||||
@@ -180,7 +224,7 @@ export function AdminDrainageFieldsPage() {
|
||||
{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>
|
||||
<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>;
|
||||
@@ -205,23 +249,25 @@ export function AdminDrainageFieldsPage() {
|
||||
<Modal
|
||||
footer={(
|
||||
<>
|
||||
<Button onClick={() => setCreating(false)} variant="ghost">取消</Button>
|
||||
<Button disabled={!code || !name || Boolean(codeError)} onClick={createField}>保存</Button>
|
||||
<Button disabled={fieldSaving} onClick={closeFieldModal} variant="ghost">取消</Button>
|
||||
<Button disabled={!code || !name || Boolean(codeError) || fieldSaving} onClick={saveField}>{fieldSaving ? '保存中...' : '保存'}</Button>
|
||||
</>
|
||||
)}
|
||||
onClose={() => setCreating(false)}
|
||||
onClose={closeFieldModal}
|
||||
open={creating}
|
||||
title="添加报备字段"
|
||||
title={editingField ? '编辑报备字段' : '添加报备字段'}
|
||||
>
|
||||
<div className="admin-system-modal-form">
|
||||
<Input 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))}
|
||||
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}
|
||||
<Textarea
|
||||
className="admin-system-modal-form__wide"
|
||||
label="描述"
|
||||
@@ -255,6 +301,6 @@ export function AdminDrainageFieldsPage() {
|
||||
);
|
||||
}
|
||||
|
||||
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>;
|
||||
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>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user