Commit 394be253 authored by Andy-bubu's avatar Andy-bubu

feat: align grouped list headers and interactions

parent cb8971aa
......@@ -25,6 +25,7 @@
- Modal 是独立 Overlay/L1 平面;查看态字段组使用 `Surface secondary`/L2。详情中的关联记录是 L2 内的静态 L3 子区域,使用 HeroUI `Surface tertiary` 包裹共享 `DataTable`,并通过 HeroUI Pro DataGrid 官方 `secondary` variant 区分表头和 L3 背景;页面不得覆盖 DataGrid 的 BEM、slot、背景或圆角。
- 业务列表页面标题由 Shell 的 HeroUI Pro Navbar 统一呈现,`RecordWorkbench` 内容区不再重复渲染同名 `<h2>`,以避免标题重复并让搜索/工具栏保持首屏优先级。
- `DataTable` 是唯一业务表格边界:它负责列宽计算、真实宽表检测、内部滚动、状态类、原生选择、ActionBar、空/加载状态、Pagination 和统一排版 Token;页面只提供业务列与回调。`src/design-system/data-grid.css` 只负责共享几何与排版,不绘制 DataGrid 背景、描边、圆角、阴影或状态。
- `RecordWorkbench` 从当前 Grid View 的 `settings.groups` 派生列分组、列数、字段摘要和两层表头;分组按钮的 Hover/Focus 预览与点击显隐只影响共享 DataGrid 的呈现列,不修改 View 配置、查询字段或业务记录。表头预览使用 HeroUI 语义 Token,不由页面 CSS 重画。
- `XinyuanModal` 与直接 HeroUI Modal 页面均使用 `rc-dialog-panel--{size}` 共享宽度阶梯;不得在页面里通过颜色、阴影、圆角或旧 BEM class 重画弹窗。
- 弹窗头部说明由 HeroUI `Description` 负责;字段标签由共享 `getFieldDisplayLabel` 从字段定义和中文别名表解析,`metadata.*` 只存在于数据层。文本、选择、日期、人员/引用和附件的提示均由对应 HeroUI 控件在内部展示,禁止再在控件下方复制同一句自动提示。
- 4xl 动态表单由共享 `DynamicRecordDialog` 负责等宽双列编排,字段类型决定是否跨列;独立流程页没有 AppShell 时保留 `RecordWorkbench` 内容标题,主应用列表通过 `showContentTitle={false}` 只使用 Navbar 标题。
......
......@@ -31,6 +31,7 @@
- 全局 `DataTable` 必须向 HeroUI Pro `DataGrid` 传递 populated/loading/empty 状态类;populated 状态的 root、scroll、resizable、content 链使用 `min-height: 0` 并按真实表格高度收缩,不能保留空状态的最小高度。
- DataGrid 普通字段单行截断,详情关联表的长文本最多两行;完整值通过共享 HeroUI `Tooltip` 提供。页面不得使用 `whitespace-normal`、`font-mono` 或原生 `title` 来替代表格文本契约。
- DataGrid 表头、普通字段、主字段、次字段和 Chip 统一使用 `--rc-data-grid-*` Token 与 `pm-data-grid-*` 共享排版类,页面不得单独声明表格字重或颜色。
- View `settings.groups` 驱动列分组:工具栏分组按钮显示真实分组名与列数,Tooltip 列出所含字段;Hover/Focus 时仅通过语义 Token 联动高亮该组表头,点击切换整组列显隐。分组字段的 DataGrid 表头采用“分组名 + 字段名”两层结构,分组名称按 `label`、`title`、`name` 的顺序解析,不得硬编码业务组名或另建页面状态。
- DataGrid 的最小列宽总和超过 viewport 时,只允许共享 DataGrid 内部横向滚动;页面和 Modal 不得被宽表撑出。宽度链必须保持 `width: 100%` 与实际内容 `min-width: 100%`。
## 验收补充
......
......@@ -21,7 +21,7 @@ import {
} from '@/components/AppIcons';
import FilterSelect from '@/components/FilterSelect';
import FormDatePicker from '@/components/FormDatePicker';
import { formatFieldValue, getFieldValue } from '@/utils/subjectView';
import { formatFieldValue, getFieldValue, normalizeText } from '@/utils/subjectView';
import {
Alert,
Button,
......@@ -156,8 +156,16 @@ function buildColumnGroupMeta(columnGroups = [], columns = []) {
return {
...group,
id,
name: group?.name || id,
name:
normalizeText(group?.label) ||
normalizeText(group?.title) ||
normalizeText(group?.name) ||
id,
children,
columnLabels: children.map((field) => {
const column = columns.find((item) => item.field === field);
return String(column?.label || field).trim();
}),
};
})
.filter((group) => group.children.length > 0);
......@@ -332,28 +340,65 @@ function getWorkbenchColumnWidth(column, stickyMeta) {
return Math.min(readableWidth, 280);
}
function ColumnGroupControl({ group, collapsed, index, onToggle }) {
function ColumnGroupControl({
group,
collapsed,
index,
onPreviewChange,
onToggle,
}) {
const Icon = index % 2 === 0 ? Group : ChartBarSquareIcon;
const actionLabel = `${collapsed ? '显示' : '隐藏'}${group.name}列`;
const columnSummary = group.columnLabels.join('、');
return (
<Tooltip delay={300}>
<Tooltip closeDelay={80} delay={150}>
<ToggleButton
size="sm"
isSelected={collapsed}
onChange={onToggle}
onBlur={() => onPreviewChange(null)}
onFocus={() => onPreviewChange(group.id)}
onHoverEnd={() => onPreviewChange(null)}
onHoverStart={() => onPreviewChange(group.id)}
aria-label={actionLabel}
>
<Icon size={16} />
<span>{group.name}</span>
<span aria-hidden="true" className="text-xs opacity-70">
{group.children.length} 列
</span>
</ToggleButton>
<Tooltip.Content>
{actionLabel}(共 {group.children.length} 个字段)
<Tooltip.Content className="max-w-80" placement="bottom">
<div className="space-y-1">
<div className="font-medium">{actionLabel}</div>
<div className="text-xs opacity-80">包含:{columnSummary}</div>
</div>
</Tooltip.Content>
</Tooltip>
);
}
function ColumnHeaderLabel({ label, groupName }) {
if (!groupName) {
return label;
}
return (
<span className="flex min-w-0 flex-col items-start gap-0.5 py-0.5">
<span
aria-hidden="true"
className="text-[11px] font-normal leading-4 text-muted"
>
{groupName}
</span>
<span className="text-xs font-medium leading-4 text-foreground">
{label}
</span>
</span>
);
}
function renderRowActionButton({ action, isDisabled, onPress }) {
const Icon = action.icon || DEFAULT_ROW_ACTION_ICONS[action.key];
......@@ -450,6 +495,7 @@ export default function RecordWorkbench({
const [drawerDirty, setDrawerDirty] = useState(false);
const [filtersExpanded, setFiltersExpanded] = useState(false);
const [collapsedColumnGroups, setCollapsedColumnGroups] = useState({});
const [previewColumnGroupId, setPreviewColumnGroupId] = useState(null);
const [columnGroupAnnouncement, setColumnGroupAnnouncement] = useState('');
const [expandedRowIds, setExpandedRowIds] = useState({});
const filterFieldVariant = 'secondary';
......@@ -745,9 +791,23 @@ export default function RecordWorkbench({
: []),
...stickyColumns.orderedColumns.map((column, columnIndex) => {
const stickyMeta = stickyColumns.stickyMeta[column.field];
const columnGroupId = columnGroupMeta.fieldToGroup[column.field];
const columnGroup = columnGroupId
? columnGroupMeta.groupById[columnGroupId]
: null;
const isPreviewedGroupColumn = columnGroupId === previewColumnGroupId;
return {
key: column.field,
title: column.label,
title: (
<ColumnHeaderLabel
groupName={columnGroup?.name}
label={column.label}
/>
),
headerClassName: clsx(
column.headerClassName,
isPreviewedGroupColumn && 'bg-accent-soft text-accent-soft-foreground',
),
width: getWorkbenchColumnWidth(column, stickyMeta),
minWidth: getWorkbenchColumnWidth(column, stickyMeta),
pin:
......@@ -1064,6 +1124,7 @@ export default function RecordWorkbench({
group={group}
collapsed={Boolean(collapsedColumnGroups[group.id])}
index={index}
onPreviewChange={setPreviewColumnGroupId}
onToggle={() => toggleColumnGroup(group.id)}
/>
))}
......
......@@ -21,6 +21,8 @@
1. 四个列表页继续使用当前项目的 `SubjectRecordWorkbench` 和 `RecordWorkbench`。
2. 表格列、分组、排序、筛选、视图限制和字段选项由线上 Subject/View 配置驱动。
`settings.groups` 的分组入口显示真实组名、列数和字段清单;Hover/Focus 会预览对应表头,
点击只切换该组列的显示状态。分组字段表头同时展示分组名和字段名,不改变查询或记录数据。
3. 详情继续使用当前 `RecordDetailDrawer`,列表页不复制或改写详情抽屉逻辑;可编辑页面
的字段维护统一从详情侧栏进入。
4. 新建、复制和软删除沿用工作台既有行为;列表行内不提供独立编辑按钮,业务字段写入
......
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