Commit 6c54c130 authored by David Yang's avatar David Yang

fix: 恢复廉洁风险配置表格交互

parent e446f89a
......@@ -40,6 +40,7 @@ import {
} from '@/catalyst-ui-kit/javascript/table';
import { Textarea } from '@/catalyst-ui-kit/javascript/textarea';
import { UserContext } from '@/wrapper/Auth';
import * as Headless from '@headlessui/react';
import {
ArchiveBoxArrowDownIcon,
ArrowDownTrayIcon,
......@@ -64,6 +65,7 @@ import dayjs from 'dayjs';
import {
useContext,
useEffect,
Fragment,
useMemo,
useRef,
useState,
......@@ -77,10 +79,13 @@ import {
exportRiskWorkbook,
formatCellValue,
getColumnLabel,
getColumnFieldName,
getConfiguredColumns,
getFieldLabel,
getFieldName,
getFieldOptions,
getFieldType,
getTableHeaderGroups,
importDisplayWorkbook,
importMeasureWorkbook,
isFieldMultiple,
......@@ -146,8 +151,9 @@ function getRecordTone(value) {
return 'zinc';
}
function CellValue({ record, name }) {
const value = formatCellValue(record, name);
function CellValue({ record, column }) {
const name = getColumnFieldName(column);
const value = formatCellValue(record, column);
const isStatus = [
'probability',
'hazard_degree',
......@@ -339,18 +345,76 @@ function DynamicField({ field, value, onChange, disabled, error }) {
);
}
export function RiskRecordDialog({ open, onClose, config, subject, record, onSaved }) {
function getDrawerFieldGroups(fields, view) {
const groups = view?.settings?.groups || view?.groups || [];
if (!Array.isArray(groups) || groups.length === 0) return [];
const groupByField = new Map();
groups.forEach((group) => {
const id = group.id || group.name;
(group.children || []).forEach((child) => {
groupByField.set(String(child).replace(/^metadata\./, ''), {
id,
label: group.name || group.label || id,
});
});
});
const renderedGroups = new Set();
const renderedFields = new Set();
const sections = [];
fields.forEach((field) => {
const name = getFieldName(field);
if (renderedFields.has(name)) return;
const group = groupByField.get(name);
if (!group || renderedGroups.has(group.id)) {
sections.push({ id: name, label: '', fields: [field] });
renderedFields.add(name);
return;
}
const groupFields = fields.filter((item) => groupByField.get(getFieldName(item))?.id === group.id);
groupFields.forEach((item) => renderedFields.add(getFieldName(item)));
renderedGroups.add(group.id);
sections.push({ id: group.id, label: group.label, fields: groupFields });
});
return sections;
}
export function RiskRecordDialog({ open, onClose, config, subject, view, record, onSaved }) {
const [editing, setEditing] = useState(!record);
const [metadata, setMetadata] = useState({});
const [errors, setErrors] = useState({});
const [saving, setSaving] = useState(false);
const fields = useMemo(
const displayColumns = useMemo(
() =>
fieldsOf(subject).filter((field) => {
getConfiguredColumns({
subject,
view,
fallbackColumns: config.columns,
sampleRecord: record,
hideRecordTitle: config.drawerConfig?.hideRecordTitle,
}),
[subject, view, config, record],
);
const fields = useMemo(
() => {
const subjectFields = fieldsOf(subject);
const configuredFields = displayColumns
.map((column) => column.fieldDef)
.filter((field) =>
subjectFields.some((subjectField) => getFieldName(subjectField) === getFieldName(field)),
);
const source = configuredFields.length > 0 ? configuredFields : subjectFields;
return source.filter((field) => {
const type = getFieldType(field);
return !['formula', 'computed'].includes(type) && !field?.settings?.hidden && !field?.hidden;
}),
[subject],
});
},
[subject, displayColumns],
);
const drawerFieldGroups = useMemo(
() => getDrawerFieldGroups(fields, view),
[fields, view],
);
useEffect(() => {
......@@ -397,72 +461,113 @@ export function RiskRecordDialog({ open, onClose, config, subject, record, onSav
}
};
const renderField = (field) => {
const name = getFieldName(field);
return (
<Field
key={name}
className={clsx(
['long_text', 'rich_text', 'textarea', 'json', 'object'].includes(getFieldType(field)) && 'md:col-span-2',
)}
>
<Label>
{getFieldLabel(field)}
{isFieldRequired(field) ? <span className="ml-1 text-red-600">*</span> : null}
</Label>
<DynamicField
field={field}
value={metadata[name]}
onChange={(value) => setMetadata((current) => ({ ...current, [name]: value }))}
disabled={!editing || (Boolean(record) && isFieldPrimary(field))}
error={errors[name]}
/>
{errors[name] ? <ErrorMessage>{errors[name]}</ErrorMessage> : null}
</Field>
);
};
return (
<Dialog open={open} onClose={onClose} size="5xl">
<DialogTitle>{record ? '记录详情' : `新增${config.title}`}</DialogTitle>
<DialogDescription>
{editing ? '业务字段将统一保存到 metadata 对象中。' : '查看完整字段信息。'}
</DialogDescription>
<DialogBody>
{fields.length > 0 ? (
<div className="grid gap-x-6 gap-y-5 md:grid-cols-2">
{fields.map((field) => {
const name = getFieldName(field);
return (
<Field
key={name}
className={clsx(
['long_text', 'rich_text', 'textarea', 'json', 'object'].includes(getFieldType(field)) && 'md:col-span-2',
)}
>
<Label>
{getFieldLabel(field)}
{isFieldRequired(field) ? <span className="ml-1 text-red-600">*</span> : null}
</Label>
<DynamicField
field={field}
value={metadata[name]}
onChange={(value) => setMetadata((current) => ({ ...current, [name]: value }))}
disabled={!editing || (Boolean(record) && isFieldPrimary(field))}
error={errors[name]}
/>
{errors[name] ? <ErrorMessage>{errors[name]}</ErrorMessage> : null}
</Field>
);
})}
</div>
) : (
<div className="rounded-lg border border-amber-200 bg-amber-50 p-4 text-sm text-amber-800">
当前 Subject 未返回可编辑字段,请检查线上字段配置。
<Headless.Dialog open={open} onClose={onClose} className="relative z-50">
<Headless.DialogBackdrop
transition
className="fixed inset-0 bg-zinc-950/25 transition duration-150 data-closed:opacity-0"
/>
<div className="fixed inset-0 overflow-hidden">
<div className="absolute inset-0 overflow-hidden">
<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>
)}
</DialogBody>
<DialogActions>
<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}
</DialogActions>
</Dialog>
</div>
</div>
</Headless.Dialog>
);
}
......@@ -547,6 +652,7 @@ export default function RiskDataWorkspace({ workspaceKey }) {
const fileInputRef = useRef(null);
const [records, setRecords] = useState([]);
const [subject, setSubject] = useState(null);
const [view, setView] = useState(null);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(0);
const [pageSize, setPageSize] = useState(20);
......@@ -572,6 +678,47 @@ export default function RiskDataWorkspace({ workspaceKey }) {
() => new Map(fieldsOf(subject).map((field) => [getFieldName(field), field])),
[subject],
);
const tableColumns = useMemo(
() =>
getConfiguredColumns({
subject,
view,
fallbackColumns: config.columns,
sampleRecord: records[0],
hideRecordTitle: config.drawerConfig?.hideRecordTitle,
}),
[subject, view, config, records],
);
const tableHeaderGroups = useMemo(
() => getTableHeaderGroups(tableColumns, view, config.subject),
[tableColumns, view, config.subject],
);
const hasGroupedHeaders = tableHeaderGroups.some((item) => item.type === 'group');
const rowGroupFields = useMemo(
() =>
(view?.settings?.groupCols || [])
.map((item) => item?.field)
.filter(Boolean),
[view],
);
const groupedRecordSections = useMemo(() => {
if (!rowGroupFields.length) return [{ key: '__all__', label: '', records }];
const sectionMap = new Map();
records.forEach((record) => {
const labels = rowGroupFields.map((field) => {
const value = formatCellValue(record, field);
return `${getColumnLabel(field, subject)}:${value === '-' ? '空' : value}`;
});
const key = labels.join(' / ');
if (!sectionMap.has(key)) sectionMap.set(key, []);
sectionMap.get(key).push(record);
});
return Array.from(sectionMap.entries()).map(([key, items]) => ({
key,
label: key,
records: items,
}));
}, [records, rowGroupFields, subject]);
const totalPages = Math.max(1, Math.ceil(total / pageSize));
const load = async () => {
......@@ -585,6 +732,7 @@ export default function RiskDataWorkspace({ workspaceKey }) {
});
setRecords(result.records);
setSubject(result.subject);
setView(result.view);
setTotal(result.total);
setSelectedIds(new Set());
} catch (loadError) {
......@@ -819,7 +967,7 @@ export default function RiskDataWorkspace({ workspaceKey }) {
<Table dense striped bleed>
<TableHead>
<TableRow>
<TableHeader className="w-12">
<TableHeader className="w-12" rowSpan={hasGroupedHeaders ? 2 : undefined}>
<Checkbox
color="sky"
checked={allCurrentSelected}
......@@ -830,60 +978,91 @@ export default function RiskDataWorkspace({ workspaceKey }) {
aria-label="全选当前页"
/>
</TableHeader>
{config.columns.map((name) => (
<TableHeader key={name}>{getColumnLabel(name, subject)}</TableHeader>
{(hasGroupedHeaders ? tableHeaderGroups : tableColumns.map((column) => ({ type: 'column', column }))).map((item) => (
item.type === 'group' ? (
<TableHeader key={item.id} colSpan={item.columns.length} className="border-x border-zinc-950/5 text-center text-zinc-700">
{item.label}
</TableHeader>
) : (
<TableHeader key={item.column.field} rowSpan={hasGroupedHeaders ? 2 : undefined}>
{getColumnLabel(item.column, subject)}
</TableHeader>
)
))}
<TableHeader className="w-16"><span className="sr-only">操作</span></TableHeader>
<TableHeader className="w-16" rowSpan={hasGroupedHeaders ? 2 : undefined}>
<span className="sr-only">操作</span>
</TableHeader>
</TableRow>
{hasGroupedHeaders ? (
<TableRow>
{tableHeaderGroups
.filter((item) => item.type === 'group')
.flatMap((item) => item.columns)
.map((column) => (
<TableHeader key={column.field}>{getColumnLabel(column, subject)}</TableHeader>
))}
</TableRow>
) : null}
</TableHead>
<TableBody>
{records.map((record) => (
<TableRow key={recordKey(record)} className="hover:bg-zinc-50">
<TableCell>
<Checkbox
color="sky"
checked={selectedIds.has(recordKey(record))}
onChange={(checked) => toggleRecord(record, checked)}
aria-label={`选择记录 ${record.title || recordKey(record)}`}
/>
</TableCell>
{config.columns.map((name) => (
<TableCell key={name} onClick={() => openRecord(record)} className="cursor-pointer">
<CellValue record={record} name={name} />
</TableCell>
{groupedRecordSections.map((section) => (
<Fragment key={section.key}>
{section.label ? (
<TableRow>
<TableCell colSpan={tableColumns.length + 2} className="bg-zinc-50 py-2 text-xs font-medium text-zinc-600">
{section.label}({section.records.length})
</TableCell>
</TableRow>
) : null}
{section.records.map((record) => (
<TableRow key={recordKey(record)} className="hover:bg-zinc-50">
<TableCell>
<Checkbox
color="sky"
checked={selectedIds.has(recordKey(record))}
onChange={(checked) => toggleRecord(record, checked)}
aria-label={`选择记录 ${record.title || recordKey(record)}`}
/>
</TableCell>
{tableColumns.map((column) => (
<TableCell key={column.field} onClick={() => openRecord(record)} className="cursor-pointer">
<CellValue record={record} column={column} />
</TableCell>
))}
<TableCell>
<Dropdown>
<DropdownButton plain aria-label="记录操作" title="记录操作">
<EllipsisHorizontalIcon />
</DropdownButton>
<DropdownMenu anchor="bottom end">
<DropdownItem onClick={() => openRecord(record)}><PencilSquareIcon />查看详情</DropdownItem>
{config.canCopy ? (
<DropdownItem onClick={() => runAction(() => copyRiskRecords(config.subject, [record]), '复制完成')}>
<DocumentDuplicateIcon />复制整行
</DropdownItem>
) : null}
{config.canArchive ? (
<DropdownItem onClick={() => requestConfirmation(
'确认归档该记录?',
'系统将复制当前记录到历史 Subject,并创建归档批次。',
() => runAction(() => archiveRiskRecords(config.subject, [record]), '归档完成'),
)}><ArchiveBoxArrowDownIcon />一键归档</DropdownItem>
) : null}
{config.canEdit ? <DropdownDivider /> : null}
{config.canEdit ? (
<DropdownItem onClick={() => requestConfirmation(
'确认删除该记录?',
'记录将被软删除,可在回收站恢复。',
() => runAction(() => deleteRiskRecords(config.subject, targetRecords(record)), '删除完成'),
true,
)}><TrashIcon />删除数据</DropdownItem>
) : null}
</DropdownMenu>
</Dropdown>
</TableCell>
</TableRow>
))}
<TableCell>
<Dropdown>
<DropdownButton plain aria-label="记录操作" title="记录操作">
<EllipsisHorizontalIcon />
</DropdownButton>
<DropdownMenu anchor="bottom end">
<DropdownItem onClick={() => openRecord(record)}><PencilSquareIcon />查看详情</DropdownItem>
{config.canCopy ? (
<DropdownItem onClick={() => runAction(() => copyRiskRecords(config.subject, [record]), '复制完成')}>
<DocumentDuplicateIcon />复制整行
</DropdownItem>
) : null}
{config.canArchive ? (
<DropdownItem onClick={() => requestConfirmation(
'确认归档该记录?',
'系统将复制当前记录到历史 Subject,并创建归档批次。',
() => runAction(() => archiveRiskRecords(config.subject, [record]), '归档完成'),
)}><ArchiveBoxArrowDownIcon />一键归档</DropdownItem>
) : null}
{config.canEdit ? <DropdownDivider /> : null}
{config.canEdit ? (
<DropdownItem onClick={() => requestConfirmation(
'确认删除该记录?',
'记录将被软删除,可在回收站恢复。',
() => runAction(() => deleteRiskRecords(config.subject, targetRecords(record)), '删除完成'),
true,
)}><TrashIcon />删除数据</DropdownItem>
) : null}
</DropdownMenu>
</Dropdown>
</TableCell>
</TableRow>
</Fragment>
))}
</TableBody>
</Table>
......@@ -901,15 +1080,20 @@ export default function RiskDataWorkspace({ workspaceKey }) {
/>
<button type="button" onClick={() => openRecord(record)} className="min-w-0 flex-1 text-left">
<div className="font-medium text-zinc-950">
{formatCellValue(record, config.columns[0])}
{formatCellValue(record, tableColumns[0] || config.columns[0])}
</div>
<div className="mt-2 line-clamp-3 text-sm text-zinc-600">
{formatCellValue(record, config.columns.find((name) => ['integrity_risk_points', 'description', 'title'].includes(name)) || config.columns[1])}
{formatCellValue(
record,
tableColumns.find((column) =>
['integrity_risk_points', 'description', 'title'].includes(getColumnFieldName(column)),
) || tableColumns[1] || config.columns[1],
)}
</div>
<div className="mt-3 flex flex-wrap gap-2">
{config.columns.slice(1, 5).map((name) => {
const value = formatCellValue(record, name);
return value === '-' ? null : <Badge key={name} color={getRecordTone(value)}>{value}</Badge>;
{tableColumns.slice(1, 5).map((column) => {
const value = formatCellValue(record, column);
return value === '-' ? null : <Badge key={column.field} color={getRecordTone(value)}>{value}</Badge>;
})}
</div>
</button>
......@@ -949,6 +1133,7 @@ export default function RiskDataWorkspace({ workspaceKey }) {
onClose={() => setRecordDialogOpen(false)}
config={config}
subject={subject}
view={view}
record={activeRecord}
onSaved={async (message) => {
setNotice(message);
......
......@@ -14,11 +14,11 @@
## 3. 通用交互流程
1. 页面进入时调用 `GET /subject/:name` 加载字段定义,并调用 `POST /record/query` 与 `POST /record/count` 分页加载记录。
1. 页面进入时调用 `GET /subject/:name` 加载字段定义;若模块配置了 `filterView`,同时调用 `GET /subject/:name/view/:viewName` 读取视图列、列分组和行分组配置;随后调用 `POST /record/query` 与 `POST /record/count` 分页加载记录。
2. 关键词可搜索标题、廉洁风险点、主要防控措施或措施描述。
3. 分类、概率、危害程度、审批状态和类别筛选只在对应字段存在 `settings.options` 时展示,筛选值不在前端硬编码。
4. 桌面端使用 Catalyst `Table`,移动端使用可点击的记录列表;两端都支持当前页勾选。
5. 点击记录打开 Headless UI/Catalyst 详情弹框。具备编辑权限的页面可切换到编辑态。
4. 桌面端使用 Catalyst `Table`,表头优先按视图 `settings.columns` 生成,并兼容 `settings.groups` 的两层分组表头;若视图配置 `settings.groupCols`,当前页记录按配置字段分块展示。移动端使用同一列配置生成可点击记录列表;两端都支持当前页勾选。
5. 点击记录打开 Headless UI 右侧详情抽屉。详情字段优先沿用当前视图列顺序和分组配置,具备编辑权限的页面可切换到编辑态。
6. 新增和编辑表单根据 Subject 字段类型动态生成;业务字段统一写入 `metadata`。
7. 删除前必须二次确认,默认执行软删除。接口异常保留当前页面状态并显示可恢复的错误提示。
......
......@@ -9,6 +9,7 @@ import {
importRecords,
loadRecord,
loadSubject,
getSubjectViewDetail,
loadUser,
queryRecords,
queryUsers,
......@@ -54,6 +55,7 @@ export const WORKSPACE_CONFIG = {
canArchive: true,
canCopy: true,
exportKind: 'process',
filterView: ['view_RAGm05'],
},
display: {
subject: RISK_SUBJECTS.display,
......@@ -152,6 +154,45 @@ const EXPORT_COLUMNS = [
['类别', 'type'],
];
const RISK_TABLE_GROUPS = [
{
id: 'risk_identification',
name: '风险识别',
children: [
'original_index',
'classification',
'involving_major_aspects_business_transaction_processes',
'involving_major_aspects_business_transaction_processes_add',
'integrity_risk_points',
'involving_positions_or_personnel',
'involving_positions_or_personnel_add',
],
},
{
id: 'risk_assessment',
name: '风险评估',
children: ['probability', 'hazard_degree', '__risk_level'],
},
{
id: 'prevention_measures',
name: '防控措施',
children: [
'description_of_main_prevention_and_control_measures',
'file_name_and_document_number',
'file_name_other',
'subsidiary_system',
'subsidiary_system_add',
'business_unit',
'business_unit_add',
],
},
{
id: 'approval_flow',
name: '填报审批',
children: ['filled_by', 'approved_by', 'approval_status', 'final_approval_status', 'type'],
},
];
function extractRecords(response) {
if (Array.isArray(response)) return response;
if (Array.isArray(response?.records)) return response.records;
......@@ -172,6 +213,70 @@ function extractCount(response) {
return values.find((value) => Number.isFinite(value)) || 0;
}
function fieldsOf(subject) {
if (Array.isArray(subject?.fields)) return subject.fields;
if (subject?.fields && typeof subject.fields === 'object') return Object.values(subject.fields);
return [];
}
function stripMetadataPrefix(fieldPath = '') {
const normalized = String(fieldPath || '');
if (normalized.startsWith('metadata.')) return normalized.replace('metadata.', '');
return normalized.split('.').pop() || normalized;
}
function getNestedValue(source, path) {
if (!source || !path) return undefined;
return String(path)
.split('.')
.reduce((value, key) => (value === undefined || value === null ? undefined : value[key]), source);
}
function getFieldDefinition(subject, columnField) {
const name = stripMetadataPrefix(columnField);
return fieldsOf(subject).find((field) => getFieldName(field) === name) || null;
}
function isHiddenField(field = {}) {
const type = getFieldType(field);
return Boolean(field.hidden || field?.settings?.hidden || ['formula', 'computed'].includes(type));
}
function normalizeColumnConfig(column, subject) {
const field = typeof column === 'string' ? column : column?.field || column?.name;
if (!field) return null;
const name = stripMetadataPrefix(field);
const fieldDef = getFieldDefinition(subject, field);
return {
field,
name,
width: typeof column === 'object' ? column.width : undefined,
pinned: typeof column === 'object' ? column.pinned : undefined,
fieldDef: fieldDef || { name, label: RISK_FIELD_LABELS[name], type: 'text' },
label: typeof column === 'object'
? column.label || column.headerName || column.title || getColumnLabel(name, subject)
: getColumnLabel(name, subject),
};
}
function workspaceConfigBySubject(subjectName) {
return Object.values(WORKSPACE_CONFIG).find((item) => item.subject === subjectName) || {};
}
async function loadConfiguredView(subjectName) {
const config = workspaceConfigBySubject(subjectName);
const viewNames = config.filterView || [];
for (const viewName of viewNames) {
try {
const view = await getSubjectViewDetail(subjectName, viewName);
if (view) return view;
} catch {
// View configuration is optional. The table falls back to Subject fields.
}
}
return null;
}
export function getFieldName(field = {}) {
return field.name || field.alias || field.key || '';
}
......@@ -212,8 +317,12 @@ export function isFieldMultiple(field = {}) {
}
export function getColumnLabel(name, subject) {
if (RISK_FIELD_LABELS[name]) return RISK_FIELD_LABELS[name];
return getFieldLabel(subject?.fields?.find((field) => getFieldName(field) === name));
const fieldName = stripMetadataPrefix(typeof name === 'object' ? name.field || name.name : name);
if (typeof name === 'object' && (name.label || name.headerName || name.title)) {
return name.label || name.headerName || name.title;
}
if (RISK_FIELD_LABELS[fieldName]) return RISK_FIELD_LABELS[fieldName];
return getFieldLabel(fieldsOf(subject).find((field) => getFieldName(field) === fieldName));
}
export function calculateRiskLevel(probability, hazardDegree) {
......@@ -231,7 +340,13 @@ function unwrapReference(value) {
return value ?? '';
}
export function formatCellValue(record, name) {
export function getColumnFieldName(column) {
return stripMetadataPrefix(typeof column === 'object' ? column.field || column.name : column);
}
export function formatCellValue(record, column) {
const fieldPath = typeof column === 'object' ? column.field || column.name : column;
const name = stripMetadataPrefix(fieldPath);
if (name === 'title') return record?.title || '-';
if (name === 'created_at' || name === 'updated_at') {
const value = record?.[name];
......@@ -249,7 +364,9 @@ export function formatCellValue(record, name) {
].reduce((total, value) => total + (Array.isArray(value) ? value.length : value ? 1 : 0), 0);
}
const value = record?.metadata?.[name];
const value = String(fieldPath || '').includes('.')
? getNestedValue(record, fieldPath)
: record?.[name] ?? record?.metadata?.[name];
if (Array.isArray(value)) {
return value.map(unwrapReference).filter(Boolean).join('、') || '-';
}
......@@ -261,6 +378,108 @@ export function formatCellValue(record, name) {
return value === 0 ? '0' : value || '-';
}
export function getConfiguredColumns({ subject, view, fallbackColumns = [], sampleRecord, hideRecordTitle = false }) {
const viewColumns = view?.settings?.columns || view?.columns || [];
const hasViewColumns = Array.isArray(viewColumns) && viewColumns.length > 0;
const configuredNames = new Set();
const columns = [];
if (hasViewColumns) {
viewColumns.forEach((column) => {
const field = column?.field || column?.name;
if (!field) return;
const name = stripMetadataPrefix(field);
configuredNames.add(name);
if (column.hide || (name === 'title' && hideRecordTitle)) return;
const normalized = normalizeColumnConfig(column, subject);
if (normalized) columns.push(normalized);
});
}
const sourceFields = fieldsOf(subject).filter((field) => {
const name = getFieldName(field);
return name && !configuredNames.has(name) && !isHiddenField(field);
});
if (hasViewColumns) {
sourceFields.forEach((field) => {
const name = getFieldName(field);
const fieldPath = sampleRecord?.metadata && Object.prototype.hasOwnProperty.call(sampleRecord.metadata, name)
? `metadata.${name}`
: name;
columns.push({
field: fieldPath,
name,
label: getFieldLabel(field),
fieldDef: field,
});
});
}
if (!hasViewColumns && sourceFields.length > 0) {
if (!hideRecordTitle && (sampleRecord?.title || fallbackColumns.includes('title'))) {
columns.push(normalizeColumnConfig('title', subject));
}
sourceFields.forEach((field) => {
const name = getFieldName(field);
columns.push({
field: `metadata.${name}`,
name,
label: getFieldLabel(field),
fieldDef: field,
});
});
}
fallbackColumns.forEach((field) => {
const name = stripMetadataPrefix(field);
if (!columns.some((column) => column.name === name) && (name.startsWith('__') || columns.length === 0)) {
const normalized = normalizeColumnConfig(field, subject);
if (normalized) columns.push(normalized);
}
});
return columns.filter(Boolean);
}
export function getTableHeaderGroups(columns, view, subjectName) {
const configuredGroups = view?.settings?.groups || view?.groups || [];
const groups = Array.isArray(configuredGroups) && configuredGroups.length > 0
? configuredGroups
: [RISK_SUBJECTS.process, RISK_SUBJECTS.display].includes(subjectName)
? RISK_TABLE_GROUPS
: [];
if (!groups.length) return [];
const groupByField = new Map();
groups.forEach((group) => {
const id = group.id || group.name;
(group.children || []).forEach((child) => {
groupByField.set(stripMetadataPrefix(child), { id, name: group.name || group.label || id });
});
});
const renderedGroups = new Set();
const renderedColumns = new Set();
const headerGroups = [];
columns.forEach((column) => {
if (renderedColumns.has(column.name)) return;
const group = groupByField.get(column.name);
if (!group || renderedGroups.has(group.id)) {
headerGroups.push({ type: 'column', column });
renderedColumns.add(column.name);
return;
}
const groupedColumns = columns.filter((item) => groupByField.get(item.name)?.id === group.id);
groupedColumns.forEach((item) => renderedColumns.add(item.name));
renderedGroups.add(group.id);
headerGroups.push({ type: 'group', id: group.id, label: group.name, columns: groupedColumns });
});
return headerGroups;
}
function buildRecordFilter(filters = {}) {
const filter = {};
const keyword = String(filters.keyword || '').trim();
......@@ -291,8 +510,9 @@ export async function loadWorkspaceData(subjectName, filters = {}) {
const filter = buildRecordFilter(filters);
const page = Number(filters.page || 0);
const pageSize = Number(filters.pageSize || 20);
const [subject, response, countResponse] = await Promise.all([
const [subject, view, response, countResponse] = await Promise.all([
loadSubject(subjectName),
loadConfiguredView(subjectName),
queryRecords({
subject: subjectName,
page,
......@@ -308,6 +528,7 @@ export async function loadWorkspaceData(subjectName, filters = {}) {
return {
subject,
view,
records: extractRecords(response),
total: extractCount(countResponse),
};
......
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