Commit f99189d9 authored by drigle's avatar drigle

fix: 完善引用选择与表格列分组显示

parent b08715fd
{
"name": "integrity_practice_management_new",
"version": "1.0.11",
"version": "1.0.13",
"build": "abc",
"description": "廉洁从业管理",
"date": "2025-07-14 19:27:20",
......
import XinyuanAutocomplete from '@/components/XinyuanAutocomplete';
import { ChevronDown, Close, Search } from '@/components/AppIcons';
import useFeedbackState from '@/hooks/useFeedbackState';
import { queryRecords } from '@/services/DataService';
import { Label } from '@heroui/react';
import { Button, Label, ListBox, Popover, SearchField, Spinner } from '@heroui/react';
import { useEffect, useMemo, useRef, useState } from 'react';
const DEFAULT_ID_LOOKUP_CHUNK_SIZE = 50;
......@@ -541,26 +541,16 @@ export default function ReferenceSearchSelect({
.join('、')} 等 ${selectedOptions.length} 项`
: selectedOptions[0]?.label || placeholder;
const selectionProps = multiple
? {
selectedKeys: new Set(selectedIds),
onSelectionChange: (keys) => {
const nextIds =
keys === 'all' ? menuOptions.map((item) => item.id) : [...keys];
commitChange(nextIds);
},
}
: {
selectedKey: selectedIds[0] || null,
onSelectionChange: (key) => {
const nextKey = key ? String(key) : '';
commitChange(nextKey ? [String(nextKey)] : []);
setActive(false);
},
};
const handleSelectionChange = (keys) => {
const nextIds = keys === 'all' ? menuOptions.map((item) => item.id) : Array.from(keys || []).map(String);
commitChange(nextIds);
if (!multiple) handleOpenChange(false);
};
const handleOpenChange = (isOpen) => {
if (disabled) return;
setActive(isOpen);
if (isOpen) setLoading(true);
if (!isOpen) {
setQuery('');
setError('');
......@@ -568,42 +558,62 @@ export default function ReferenceSearchSelect({
};
return (
<XinyuanAutocomplete
{...selectionProps}
aria-label={placeholder}
emptyText="无匹配结果"
errorText={error}
inputValue={query}
isDisabled={disabled}
isClearable={allowClear}
isLoading={loading}
isOpen={active}
options={menuOptions}
placement={optionsAnchor || 'bottom start'}
placeholder={placeholder}
searchPlaceholder={placeholder}
selectionMode={multiple ? 'multiple' : 'single'}
shouldFilter={false}
valueText={displayText}
variant={variant}
onInputChange={setQuery}
onClear={clearSelection}
onOpenChange={handleOpenChange}
renderOption={(option) => (
<div className="min-w-0 flex-1 overflow-hidden">
<Label className="block truncate" title={option.label}>
{option.label}
</Label>
{option.description ? (
<div
className="truncate text-xs text-muted"
title={option.description}
<Popover.Root isOpen={active} onOpenChange={handleOpenChange}>
<Button
aria-label={placeholder}
className="rc-reference-select-trigger"
fullWidth
isDisabled={disabled}
variant={variant || 'secondary'}
>
<span className="rc-reference-select-trigger__value" title={selectedOptions.map((option) => option.label).join('、') || placeholder}>
{displayText}
</span>
<span className="rc-reference-select-trigger__indicator"><ChevronDown size={16} /></span>
</Button>
<Popover.Content className="rc-reference-select-popover" placement={optionsAnchor || 'bottom start'}>
<Popover.Dialog aria-label={placeholder} className="min-w-0 p-0">
<div className="flex items-center justify-between gap-3 px-3 pb-2 pt-3">
<span className="text-sm text-muted">已选择 {selectedIds.length} 项</span>
{allowClear ? <Button size="sm" variant="tertiary" onPress={clearSelection}>清空</Button> : null}
</div>
<div className="px-2 pb-2">
<SearchField aria-label="搜索引用记录" fullWidth value={query} onChange={setQuery} variant={variant || 'secondary'}>
<SearchField.Group>
<SearchField.SearchIcon><Search size={16} /></SearchField.SearchIcon>
<SearchField.Input autoFocus placeholder={placeholder} />
<SearchField.ClearButton aria-label="清空搜索"><Close size={14} /></SearchField.ClearButton>
</SearchField.Group>
</SearchField>
</div>
{loading ? (
<div className="grid min-h-24 place-items-center" role="status"><Spinner size="sm" aria-label="搜索中" /></div>
) : error ? (
<div className="px-3 py-4 text-sm text-danger" role="alert">{error}</div>
) : menuOptions.length === 0 ? (
<div className="px-3 py-4 text-center text-sm text-muted" role="status">无匹配结果</div>
) : (
<ListBox
aria-label={placeholder}
className="rc-reference-select-options"
items={menuOptions}
selectedKeys={new Set(selectedIds)}
selectionMode={multiple ? 'multiple' : 'single'}
onSelectionChange={handleSelectionChange}
>
{option.description}
</div>
) : null}
</div>
)}
/>
{(option) => (
<ListBox.Item className="rc-reference-select-option" id={option.id} textValue={option.label}>
<div className="min-w-0 flex-1 overflow-hidden">
<Label className="block truncate" title={option.label}>{option.label}</Label>
{option.description ? <div className="truncate text-xs text-muted" title={option.description}>{option.description}</div> : null}
</div>
<ListBox.ItemIndicator />
</ListBox.Item>
)}
</ListBox>
)}
</Popover.Dialog>
</Popover.Content>
</Popover.Root>
);
}
This diff is collapsed.
......@@ -1716,3 +1716,48 @@
outline: 2px solid var(--rc-sidebar-focus);
outline-offset: 2px;
}
.rc-reference-select-trigger {
display: flex;
justify-content: space-between;
width: 100%;
min-width: 0;
height: 40px;
padding: 0 12px;
border: 1px solid var(--rc-field-border, var(--rc-border-default));
border-radius: 8px;
background: var(--rc-field-background);
color: var(--rc-text-primary);
font-weight: 400;
}
.rc-reference-select-trigger__value {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.rc-reference-select-trigger__indicator {
display: inline-grid;
place-items: center;
flex: 0 0 16px;
color: var(--rc-text-muted);
}
.rc-reference-select-popover {
width: var(--trigger-width);
max-width: min(24rem, calc(100vw - 24px));
border: 1px solid var(--rc-border-default);
border-radius: 12px;
background: var(--rc-bg-surface);
}
.rc-reference-select-options {
max-height: 288px;
overflow-y: auto;
}
.rc-reference-select-option[data-selected] {
background: var(--rc-brand-primary-soft, var(--accent-soft));
color: var(--rc-brand-primary);
}
......@@ -10,6 +10,7 @@ import { useLocation } from 'umi';
import UserAssignmentDialog from './UserAssignmentDialog';
import XinyuanAutocomplete from '@/components/XinyuanAutocomplete';
import FormDatePicker from '@/components/FormDatePicker';
import ReferenceSearchSelect from '@/components/ReferenceSearchSelect';
import {
getSubjectNameFromPath,
INTEGRITY_PRACTICE_SUBJECT_CONFIG,
......@@ -27,7 +28,7 @@ import {
buildIntegrityCreatePayload,
} from './integrityPracticeWorkbenchService';
import { createRecord } from '@/services/DataService';
import { getViewRestrictionDefaults, getSelectOptions, emptyValueForField, toArray } from '@/utils/subjectView';
import { getViewRestrictionDefaults, getSelectOptions, getReferenceSubjectName, isReferenceFieldType, emptyValueForField, toArray } from '@/utils/subjectView';
const EMPTY_ARRAY = [];
const MUTATING_ACTION_PATTERN = /delete|archive|create|push/i;
......@@ -347,6 +348,8 @@ export default function SubjectWorkbench() {
: '字段、列顺序和视图均来自当前 Subject 配置。'
}
columns={columns}
columnGroups={activeView?.settings?.groups || EMPTY_ARRAY}
columnGroupScope={`${subjectName}:${activeView?.name || ''}`}
records={records}
selectedIds={selectedIds}
onSelectionChange={hasAccessibleView ? setSelectedIds : undefined}
......@@ -446,7 +449,18 @@ export default function SubjectWorkbench() {
key={field.field}
>
<Label>{fieldLabel}</Label>
{options.length ? (
{isReferenceFieldType(fieldType) ? (
<ReferenceSearchSelect
subject={getReferenceSubjectName(field.fieldDef)}
value={value}
multiple={isMultiple}
disabled={isFieldDisabled || actionLoading}
placeholder={`搜索并选择${fieldLabel}`}
optionsAnchor="bottom start"
selectedSummaryMode={isMultiple ? 'count' : 'labels'}
onChange={(nextValue) => setCreateValues((current) => ({ ...current, [name]: nextValue }))}
/>
) : options.length ? (
<XinyuanAutocomplete
aria-label={fieldLabel}
isClearable
......
......@@ -33,6 +33,7 @@
9. 当前 Detail 有有效 `settings.auth` 或 `settings.disableColumns` 时,字段权限整套使用 Detail;两项都未配置或没有实际 Detail 时,整体回退打开详情的当前 Grid。两套权限不合并、不逐字段回退;`disableColumns` 对管理员也始终只读,普通用户在选定权限来源中缺少字段 `auth` 时只读。切换 Detail 后立即重算字段状态和编辑入口,编辑入口只检查当前 Detail 展示字段。Grid `viewAuth` 只控制 Tab 可见性,不隐藏详情字段。
10. 编辑态远程引用字段通过 HeroUI 弹层搜索选择。多值字段在触发器中显示已选数量,可在弹层内连续选择、取消或清空;单值字段选中后关闭弹层。候选标题过长时单行省略但保留全文提示,写入值仍严格为 `_id` 或 `_id[]`,不改变保存时机和 `metadata` 协议。
11. 编辑态的单选和多选属性使用 HeroUI Select/Autocomplete,日期使用 DatePicker,附件使用 HeroUI Pro DropZone;选择后立即更新当前弹窗草稿,最终仍由弹窗底部统一“保存”提交。
12. 自查底稿填写表的“模板”和“项目结果”等引用字段使用独立受控的 HeroUI Popover + ListBox,点击即打开,候选尚未加载或当前值为空也可展开。打开后显示加载状态并查询目标对象,支持远程搜索、单选/多选与清空;失败时显示错误并可重新搜索或关闭后重开。修改只更新弹窗草稿,取消不写入,保存仍通过 `PUT /record/:name` 提交引用 `_id` / `_id[]`。
异常处理:
......@@ -40,8 +41,18 @@
- 没有可访问 `grid` 视图时不创建兜底 View、不查询记录,并隐藏新增、批量和行操作;只有一个可访问 View 时隐藏 Tabs 轨道。
- 配置动作执行期间禁用重复点击;成功后使用顶部 Toast,失败时保留当前列表并显示服务端错误。
### 2.0 字段分组表头与显示控制
- 各业务列表读取当前 Grid View 的 `settings.groups`;兼容 `children` / `fields` / `columns` 字段列表及带或不带 `metadata.` 的字段路径,仅显示当前视图中有效的列,空分组忽略。
- 同组字段按配置顺序相邻展示,并保留固定列区域。表头采用与廉洁风险防控一致的上下两行:上方为弱化组名,下方为字段名称;未分组字段保留普通表头。
- 表格上方提供分组开关和列数,默认全部展开。点击可隐藏整组字段,再次点击恢复;悬停或键盘聚焦显示字段清单,并突出对应表头。隐藏后保留分组开关,可随时恢复。
- 分组切换只改变当前表格展示,保留勾选、筛选、分页和记录数据,并按可见列宽重新计算横向滚动与固定列;切换 Subject 或视图后恢复默认展开。分组字段全部隐藏时保留选择及操作列。
- 本交互不新增请求、不修改 Subject 配置,也不改变 `metadata`、审批或导出协议。
### 2.1 新建记录与 View 限制
- 自查底稿模板列表点击“新增记录”后,“填写项目”(`entry_item`)按字段配置远程查询 `self_check_the_working_draft`,使用共享引用选择器搜索、多选、取消选择或清空,草稿和最终 `metadata.entry_item` 只保存 `_id[]`。其它引用字段同样读取目标 Subject 和 `multiple` 配置,单值保存 `_id`,选择后关闭弹层;多值可连续选择。
- 引用候选查询通过 `POST /record/query`,搜索期间显示加载状态,无结果或失败时显示对应提示,可重新搜索。选择仅更新新建草稿,点击弹窗保存才调用创建接口;视图限制字段及提交期间禁用选择器。
- 新建记录默认继承当前 Grid View 的 `settings.restrictions`。`$eq` 或单值 `$in` 条件会将唯一值写入 `metadata`,对应控件禁用;即使字段未列入弹窗,也会随请求写入。
- 多值集合、否定条件或同一字段互相冲突时无法确定唯一值,禁止创建并提示“请先调整视图配置”。
- 新建请求使用 `POST /record`,所有业务字段放在 `metadata`;编辑已有记录只提交用户修改字段,不使用 Grid View 限制覆盖原值。
......@@ -50,6 +61,8 @@
入口为 streams-admin 的应用布局配置或 Subject 的 Grid 视图配置,作用于本模块七个业务列表的共享 RecordWorkbench 表格中的引用、选择、子对象及 form 字段。页面加载应用及视图配置后按列渲染,切换视图时使用该视图的配置。
- 视图中配置的 `metadata.*` 业务列必须存在于当前 Subject 字段定义中;残留的无效列不展示,也不作为默认筛选项。Record 根级标题、创建时间等系统列保留原规则。“附件1:廉洁从业自查底稿”对应 `self_inspection_submission_form`,不包含 `archive_status`,因此不显示“归档状态”列;自查底稿填写表中真实定义的归档状态字段继续显示。
- 应用默认值读取 `layouts.default.subjects.<subjectName>.gridRecordConfig.displayConfig`,兼容 streams-admin 的 `displayConfig.maxTagCount`。
- 视图默认值读取 `settings.displayConfig`,同名属性覆盖应用默认值;`settings.columns[]` 中的单列 `displayConfig` 再覆盖视图默认值,列上的 `displayMode`、`maxTagCount`、`titleField` 优先级最高。
- `displayMode: "labels"` 显示已选选项或关联标题;选项文案来自字段 `settings.options`,仅展示当前记录实际选中的值。
......
......@@ -286,6 +286,7 @@ export function buildViewColumns(view, subject, { includeHidden = false, display
return configuredColumns
.filter((column) =>
(includeHidden || !column.hide) && (!hideTitleColumn || column.field !== 'title'))
.filter((column) => !column.field?.startsWith('metadata.') || getFieldDefinition(column.field, subject))
.map((column) => {
const fieldDef = getFieldDefinition(column.field, subject);
const label = getFieldLabel(column.field, fieldDef, column.label);
......
......@@ -35,6 +35,21 @@ const { getTableFieldDisplay, getTableFieldDisplayConfig, formatTableFieldValue
const { buildViewColumns } = loadModule('utils/subjectView.js');
const column = (type, config = {}) => ({ field: 'metadata.values', fieldDef: { type }, ...config });
test('view columns omit stale metadata fields while retaining real archive and system fields', () => {
const view = { settings: { columns: [
{ field: 'title' },
{ field: 'metadata.inspection_item' },
{ field: 'metadata.archive_status' },
{ field: 'created_at' },
] } };
const subject = { fields: [{ name: 'inspection_item', type: 'select' }] };
assert.deepEqual(buildViewColumns(view, subject).map(({ field }) => field), [
'title', 'metadata.inspection_item', 'created_at',
]);
subject.fields.push({ name: 'archive_status', type: 'select' });
assert.equal(buildViewColumns(view, subject)[2].field, 'metadata.archive_status');
});
test('count mode counts selected values, including zero, and excludes empty entries', () => {
const config = column('select', { displayMode: 'count' });
assert.equal(formatTableFieldValue([0, '选中', null, undefined, '', ' '], config), '2');
......
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