Commit c1d89af9 authored by David Yang's avatar David Yang

fix: 对齐廉洁风险详情弹窗交互

parent 6c54c130
...@@ -40,7 +40,6 @@ import { ...@@ -40,7 +40,6 @@ import {
} from '@/catalyst-ui-kit/javascript/table'; } from '@/catalyst-ui-kit/javascript/table';
import { Textarea } from '@/catalyst-ui-kit/javascript/textarea'; import { Textarea } from '@/catalyst-ui-kit/javascript/textarea';
import { UserContext } from '@/wrapper/Auth'; import { UserContext } from '@/wrapper/Auth';
import * as Headless from '@headlessui/react';
import { import {
ArchiveBoxArrowDownIcon, ArchiveBoxArrowDownIcon,
ArrowDownTrayIcon, ArrowDownTrayIcon,
...@@ -345,9 +344,9 @@ function DynamicField({ field, value, onChange, disabled, error }) { ...@@ -345,9 +344,9 @@ function DynamicField({ field, value, onChange, disabled, error }) {
); );
} }
function getDrawerFieldGroups(fields, view) { function getDialogColumnSections(columns, view) {
const groups = view?.settings?.groups || view?.groups || []; const groups = view?.settings?.groups || view?.groups || [];
if (!Array.isArray(groups) || groups.length === 0) return []; if (!Array.isArray(groups) || groups.length === 0) return [{ id: 'base', label: '', columns }];
const groupByField = new Map(); const groupByField = new Map();
groups.forEach((group) => { groups.forEach((group) => {
...@@ -361,21 +360,21 @@ function getDrawerFieldGroups(fields, view) { ...@@ -361,21 +360,21 @@ function getDrawerFieldGroups(fields, view) {
}); });
const renderedGroups = new Set(); const renderedGroups = new Set();
const renderedFields = new Set(); const renderedColumns = new Set();
const sections = []; const sections = [];
fields.forEach((field) => { columns.forEach((column) => {
const name = getFieldName(field); const name = getColumnFieldName(column);
if (renderedFields.has(name)) return; if (renderedColumns.has(name)) return;
const group = groupByField.get(name); const group = groupByField.get(name);
if (!group || renderedGroups.has(group.id)) { if (!group || renderedGroups.has(group.id)) {
sections.push({ id: name, label: '', fields: [field] }); sections.push({ id: column.field, label: '', columns: [column] });
renderedFields.add(name); renderedColumns.add(name);
return; return;
} }
const groupFields = fields.filter((item) => groupByField.get(getFieldName(item))?.id === group.id); const groupColumns = columns.filter((item) => groupByField.get(getColumnFieldName(item))?.id === group.id);
groupFields.forEach((item) => renderedFields.add(getFieldName(item))); groupColumns.forEach((item) => renderedColumns.add(getColumnFieldName(item)));
renderedGroups.add(group.id); renderedGroups.add(group.id);
sections.push({ id: group.id, label: group.label, fields: groupFields }); sections.push({ id: group.id, label: group.label, columns: groupColumns });
}); });
return sections; return sections;
} }
...@@ -412,9 +411,9 @@ export function RiskRecordDialog({ open, onClose, config, subject, view, record, ...@@ -412,9 +411,9 @@ export function RiskRecordDialog({ open, onClose, config, subject, view, record,
}, },
[subject, displayColumns], [subject, displayColumns],
); );
const drawerFieldGroups = useMemo( const dialogSections = useMemo(
() => getDrawerFieldGroups(fields, view), () => getDialogColumnSections(displayColumns, view),
[fields, view], [displayColumns, view],
); );
useEffect(() => { useEffect(() => {
...@@ -486,88 +485,109 @@ export function RiskRecordDialog({ open, onClose, config, subject, view, record, ...@@ -486,88 +485,109 @@ export function RiskRecordDialog({ open, onClose, config, subject, view, record,
); );
}; };
const renderDetailColumn = (column) => {
const field = column.fieldDef;
return (
<div
key={column.field}
className={clsx(
'grid gap-2 border-b border-zinc-100 pb-4 md:grid-cols-[180px_minmax(0,1fr)]',
['long_text', 'rich_text', 'textarea', 'json', 'object'].includes(getFieldType(field)) && 'md:col-span-2',
)}
>
<div className="text-sm font-medium text-zinc-500">{getColumnLabel(column, subject)}</div>
<div className="min-w-0 whitespace-pre-wrap break-words text-sm text-zinc-900">
{formatCellValue(record, column)}
</div>
</div>
);
};
const renderSection = (section) => (
<section
key={section.id}
className={section.label ? 'border-t border-zinc-200 pt-5 first:border-t-0 first:pt-0' : ''}
>
{section.label ? (
<h3 className="mb-4 text-sm font-semibold text-zinc-900">{section.label}</h3>
) : null}
{editing ? (
<div className="grid gap-x-6 gap-y-5 md:grid-cols-2">
{section.columns
.map((column) => fields.find((field) => getFieldName(field) === getColumnFieldName(column)))
.filter(Boolean)
.map(renderField)}
</div>
) : (
<div className="grid gap-x-6 gap-y-4 md:grid-cols-2">
{section.columns.map(renderDetailColumn)}
</div>
)}
</section>
);
const handleClose = () => {
if (!saving) onClose();
};
return ( return (
<Headless.Dialog open={open} onClose={onClose} className="relative z-50"> <Dialog open={open} onClose={handleClose} size="4xl">
<Headless.DialogBackdrop <div className="flex items-start justify-between gap-4">
transition <div>
className="fixed inset-0 bg-zinc-950/25 transition duration-150 data-closed:opacity-0" <DialogTitle>{record ? '记录详情' : `新增${config.title}`}</DialogTitle>
/> <DialogDescription>
<div className="fixed inset-0 overflow-hidden"> {editing ? '业务字段将统一保存到 metadata 对象中,保存成功后生效。' : '默认以只读方式查看记录字段;点击编辑后再修改。'}
<div className="absolute inset-0 overflow-hidden"> </DialogDescription>
<div className="pointer-events-none fixed inset-y-0 right-0 flex max-w-full pl-8 sm:pl-16">
<Headless.DialogPanel
transition
className="pointer-events-auto flex h-full w-screen max-w-5xl flex-col bg-white shadow-2xl ring-1 ring-zinc-950/10 transition duration-150 data-closed:translate-x-full"
>
<div className="flex items-start justify-between gap-4 border-b border-zinc-200 px-5 py-4 sm:px-6">
<div>
<Headless.DialogTitle className="text-base font-semibold text-zinc-950">
{record ? '记录详情' : `新增${config.title}`}
</Headless.DialogTitle>
<p className="mt-1 text-sm text-zinc-500">
{editing ? '业务字段将统一保存到 metadata 对象中。' : '查看完整字段信息。'}
</p>
</div>
<Button plain onClick={onClose} aria-label="关闭详情" title="关闭详情">
<XMarkIcon />
</Button>
</div>
<div className="flex-1 overflow-y-auto px-5 py-5 sm:px-6">
{fields.length > 0 ? (
drawerFieldGroups.length > 0 ? (
<div className="space-y-6">
{drawerFieldGroups.map((section) => (
<section key={section.id} className={section.label ? 'border-t border-zinc-200 pt-5 first:border-t-0 first:pt-0' : ''}>
{section.label ? (
<h3 className="mb-4 text-sm font-semibold text-zinc-900">{section.label}</h3>
) : null}
<div className="grid gap-x-6 gap-y-5 md:grid-cols-2">
{section.fields.map(renderField)}
</div>
</section>
))}
</div>
) : (
<div className="grid gap-x-6 gap-y-5 md:grid-cols-2">
{fields.map(renderField)}
</div>
)
) : (
<div className="rounded-lg border border-amber-200 bg-amber-50 p-4 text-sm text-amber-800">
当前 Subject 未返回可编辑字段,请检查线上字段配置。
</div>
)}
</div>
<div className="flex flex-col-reverse gap-3 border-t border-zinc-200 px-5 py-4 sm:flex-row sm:justify-end sm:px-6">
<Button plain onClick={onClose}>关闭</Button>
{record && config.canEdit && !editing ? (
<Button color="blue" onClick={() => setEditing(true)}>
<PencilSquareIcon />编辑
</Button>
) : null}
{editing ? (
<>
{record ? (
<Button outline onClick={() => {
setMetadata(buildInitialMetadata(subject, record));
setErrors({});
setEditing(false);
}}>
取消编辑
</Button>
) : null}
<Button color="blue" onClick={handleSave} disabled={saving || fields.length === 0}>
{saving ? <ArrowPathIcon className="animate-spin" /> : <PencilSquareIcon />}
{saving ? '保存中' : '保存'}
</Button>
</>
) : null}
</div>
</Headless.DialogPanel>
</div>
</div> </div>
<button
type="button"
onClick={handleClose}
disabled={saving}
aria-label="关闭详情"
className="inline-flex size-10 shrink-0 items-center justify-center rounded-full border border-zinc-200 bg-white text-zinc-500 shadow-sm transition hover:border-zinc-300 hover:bg-zinc-50 hover:text-zinc-700 disabled:cursor-not-allowed disabled:opacity-50"
>
<XMarkIcon className="size-5" />
</button>
</div> </div>
</Headless.Dialog>
<DialogBody className="max-h-[70vh] overflow-y-auto pr-1">
{displayColumns.length > 0 ? (
<div className="space-y-6">
{dialogSections.map(renderSection)}
</div>
) : (
<div className="rounded-lg border border-amber-200 bg-amber-50 p-4 text-sm text-amber-800">
当前 Subject 未返回可显示字段,请检查线上字段配置。
</div>
)}
</DialogBody>
<DialogActions>
<Button plain onClick={handleClose} disabled={saving}>关闭</Button>
{record && config.canEdit && !editing ? (
<Button color="blue" onClick={() => setEditing(true)}>
<PencilSquareIcon />编辑
</Button>
) : null}
{editing ? (
<>
{record ? (
<Button outline disabled={saving} onClick={() => {
setMetadata(buildInitialMetadata(subject, record));
setErrors({});
setEditing(false);
}}>
取消编辑
</Button>
) : null}
<Button color="blue" onClick={handleSave} disabled={saving || fields.length === 0}>
{saving ? <ArrowPathIcon className="animate-spin" /> : <PencilSquareIcon />}
{saving ? '保存中' : '保存'}
</Button>
</>
) : null}
</DialogActions>
</Dialog>
); );
} }
......
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
2. 关键词可搜索标题、廉洁风险点、主要防控措施或措施描述。 2. 关键词可搜索标题、廉洁风险点、主要防控措施或措施描述。
3. 分类、概率、危害程度、审批状态和类别筛选只在对应字段存在 `settings.options` 时展示,筛选值不在前端硬编码。 3. 分类、概率、危害程度、审批状态和类别筛选只在对应字段存在 `settings.options` 时展示,筛选值不在前端硬编码。
4. 桌面端使用 Catalyst `Table`,表头优先按视图 `settings.columns` 生成,并兼容 `settings.groups` 的两层分组表头;若视图配置 `settings.groupCols`,当前页记录按配置字段分块展示。移动端使用同一列配置生成可点击记录列表;两端都支持当前页勾选。 4. 桌面端使用 Catalyst `Table`,表头优先按视图 `settings.columns` 生成,并兼容 `settings.groups` 的两层分组表头;若视图配置 `settings.groupCols`,当前页记录按配置字段分块展示。移动端使用同一列配置生成可点击记录列表;两端都支持当前页勾选。
5. 点击记录打开 Headless UI 右侧详情抽屉。详情字段优先沿用当前视图列顺序和分组配置,具备编辑权限的页面可切换到编辑态。 5. 点击记录打开 Catalyst `Dialog` 居中详情弹窗。详情字段优先沿用当前视图列顺序和分组配置,查看态只展示配置字段值;具备编辑权限的页面可切换到编辑态,编辑控件继续按 Subject 字段类型动态生成。
6. 新增和编辑表单根据 Subject 字段类型动态生成;业务字段统一写入 `metadata`。 6. 新增和编辑表单根据 Subject 字段类型动态生成;业务字段统一写入 `metadata`。
7. 删除前必须二次确认,默认执行软删除。接口异常保留当前页面状态并显示可恢复的错误提示。 7. 删除前必须二次确认,默认执行软删除。接口异常保留当前页面状态并显示可恢复的错误提示。
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment