Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in / Register
Toggle navigation
P
problem_rectification
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
David Yang
problem_rectification
Commits
f53d7e42
Commit
f53d7e42
authored
Aug 19, 2026
by
Andy-bubu
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: finalize Xinyuan HeroUI design standards
parent
837b6947
Show whitespace changes
Inline
Side-by-side
Showing
23 changed files
with
923 additions
and
295 deletions
+923
-295
ui-architecture.md
docs/ui-architecture.md
+15
-4
ui-migration-checklist.md
docs/ui-migration-checklist.md
+20
-5
xinyuan-design-system.md
docs/xinyuan-design-system.md
+54
-12
xinyuan-ui-audit.md
docs/xinyuan-ui-audit.md
+5
-3
AppIcons.jsx
src/components/AppIcons.jsx
+6
-0
DashboardSidebar.jsx
src/components/DashboardSidebar.jsx
+2
-5
FormDatePicker.jsx
src/components/FormDatePicker.jsx
+33
-6
ReferenceSearchSelect.jsx
src/components/ReferenceSearchSelect.jsx
+0
-2
TableActionButton.jsx
src/components/TableActionButton.jsx
+17
-7
XinyuanAutocomplete.jsx
src/components/XinyuanAutocomplete.jsx
+13
-5
index.jsx
src/components/record-workbench/index.jsx
+40
-75
chartPalette.js
src/design-system/chartPalette.js
+110
-0
shell.css
src/design-system/shell.css
+205
-8
theme.css
src/design-system/theme.css
+50
-48
tokens.css
src/design-system/tokens.css
+79
-1
SubjectViewContent.jsx
...ages/RecordDetailDrawer/components/SubjectViewContent.jsx
+16
-4
TailwindEditableField.jsx
...s/RecordDetailDrawer/components/TailwindEditableField.jsx
+37
-7
ApplicationSwitcherDialog.jsx
...pages/problem_rectification/ApplicationSwitcherDialog.jsx
+18
-5
index.jsx
src/pages/problem_rectification/archive/index.jsx
+3
-3
index.jsx
src/pages/problem_rectification/dashboard/index.jsx
+152
-66
index.jsx
src/pages/problem_rectification/flow-table/index.jsx
+3
-3
index.jsx
src/pages/problem_rectification/form/index.jsx
+36
-15
index.jsx
src/pages/problem_rectification/list/index.jsx
+9
-11
No files found.
docs/ui-architecture.md
View file @
f53d7e42
...
...
@@ -10,21 +10,32 @@
## 标准实现
-
Surface:L0
`background`
是页面画布,L1
`surface`
是第一内容层,L2
`surface-secondary`
是次要内容和次要控件层,L3
`surface-tertiary`
只用于 L2 内确有必要的第三层静态内容。Portal 浮层不参与 L0-L3 编号,统一使用 HeroUI
`overlay`
。
`theme.css`
固定映射
`field-background = surface`
、
`default = surface-secondary`
、
`overlay = surface`
;页面只选择 HeroUI
`primary/secondary`
,不得写背景色或按深浅主题切换 variant。
-
DataGrid:HeroUI Pro
`DataGrid`
原生
`selectedKeys`
、
`onSelectionChange`
、
`selectionMode`
、
`showSelectionCheckboxes`
,配合原生固定列、Pagination 和 ActionBar。
-
Pagination:直接使用 HeroUI compound API,不增加页容量 Select/Autocomplete。HeroUI 不定义每页条数,项目数据层统一使用 10 条/页并继续传递
`page_size=10`
。
-
DataGrid 宽度:共享层直接使用 HeroUI Pro
`ColumnSize`
。非宽表将第一业务主列设为
`1fr`
,选择/操作列固定并以
`min-w-full`
保证完整宽度链;宽表保留业务最小像素宽度,仅在 DataGrid 内横向滚动。页面不得用背景、伪元素或空列填补右侧空白。
-
DataGrid 行操作:共享
`TableActionButton`
直接组合 HeroUI
`Button`
、
`Dropdown`
、
`Tooltip`
与 Ant Design Icons,不兼容旧按钮 API。普通操作使用
`secondary`
正圆,危险操作使用
`danger-soft`
正圆;32px 按钮、16px 图标、8px 间距并与表头左对齐,列宽按可见按钮数动态计算。外观状态由 HeroUI variant 与
`theme.css`
管理,布局类只保证圆心和 SVG 中心重合。
-
Modal:HeroUI
`Modal`
/
`AlertDialog`
compound API,标题、正文和页脚上下结构,Portal 继承根主题。内容型新增表单与详情统一使用
`4xl`
(1088px);同一表单的 Input、TextArea、DateField.Group、Autocomplete 和多选控件统一使用浅色
`primary`
、深色
`secondary`
的 HeroUI field variant。提交约束仅在用户点击确认且校验失败后通过受控
`AlertDialog`
展示,不在表单中常驻。可编辑详情默认是查看态,Footer 右下的单一“编辑”主操作切换到整窗编辑,查看态仅通过右上关闭图标退出;草稿由弹窗统一持有,Footer 原位切换取消/保存并通过原更新服务一次提交,不允许字段级即时保存。
-
DataGrid 行操作:共享
`TableActionButton`
直接组合 HeroUI
`Button`
、
`Dropdown`
、
`Tooltip`
与 Ant Design Icons,不兼容旧按钮 API。普通查看/编辑/日期/复制/锁定/更多使用
`tertiary`
中性正圆,明确关键的审批/提交使用
`secondary`
强调正圆,删除/驳回使用
`danger-soft`
危险正圆;32px 按钮、16px 图标、8px 间距并与表头左对齐,列宽按可见按钮数动态计算。外观状态由 HeroUI variant 与
`theme.css`
管理,布局类只保证圆心和 SVG 中心重合。
-
HeroUI 危险语义统一由
`theme.css`
的鑫元红系
`--danger`
驱动:浅色为主红
`#9E1B1B`
,深色为同色相增亮红;
`danger-soft`
、Hover 和前景色保留 HeroUI 官方派生公式。删除、驳回、退出和失败状态不得在页面层写粉色、洋红或私有红色。
-
Modal:HeroUI
`Modal`
/
`AlertDialog`
compound API,标题、正文和页脚上下结构,Portal 继承根主题。层级固定为
`Backdrop -> Modal.Dialog overlay -> nested secondary -> rare static tertiary`
:Header/Body/Footer 透明承载;普通详情查看态用一个
`Surface secondary`
承载整组只读标签/值,字段本身不重复套 Card;对象预览或 Logo well 等真实第三层静态内容才使用
`tertiary`
。进入编辑态后不保留整组查看 Surface,由
`secondary`
表单控件直接位于 Overlay。禁止用默认 L1 Card 在 Overlay 内表达嵌套边界,也禁止给每个只读字段套 Card 或用页面 CSS 补背景。内容型新增表单与详情统一使用
`4xl`
(1088px);Modal 内 Input、TextArea、DateField.Group、Autocomplete 和多选控件在两种主题下统一使用 HeroUI
`secondary`
。提交约束仅在用户点击确认且校验失败后通过受控
`AlertDialog`
展示,不在表单中常驻。可编辑详情默认是查看态,Footer 右下的单一“编辑”主操作切换到整窗编辑,查看态仅通过右上关闭图标退出;草稿由弹窗统一持有,Footer 原位切换取消/保存并通过原更新服务一次提交,不允许字段级即时保存。
-
详情查看 Surface 的跨应用固定几何为
`rounded-2xl px-5 py-2 sm:px-6 sm:py-3`
;内部字段网格为单列到
`md`
双列、
`gap-x-8 gap-y-2`
,普通字段
`py-3`
,标签与值间距 6px,长内容跨两列。此结构只控制视觉包含关系,不能拥有或重置业务状态。
-
详情字段的空值展示属于纯表现层:
`null`
、空字符串、仅空格字符串和空数组在查看态统一显示弱化的“暂无内容”,字段本身保持无框;编辑态的 HeroUI
`Input`
/
`TextArea`
使用“请输入{可见字段名}”作为
`placeholder`
。任何提示文案都不得进入受控
`value`
、业务校验或保存 payload。
-
表单:HeroUI TextField、Input、TextArea、Autocomplete、SearchField、Checkbox、DatePicker;下拉型枚举、用户、引用、单选、多选、年份与页容量通过
`XinyuanAutocomplete.jsx`
组合官方 Autocomplete compound API,上传使用 HeroUI Pro DropZone。
-
日期字段:共享
`FormDatePicker.jsx`
直接组合 HeroUI
`DatePicker + DateField + Calendar + CloseButton`
。日历触发器与 Autocomplete 指示器使用同一 16px Ant Design 图标几何、共享
`FIELD_SUFFIX_ICON_STYLE`
和尾部光学中心;该样式只引用 HeroUI
`--field-placeholder`
/
`--muted`
Token,并直接作用于 Ant 图标根节点,避免不同图标组件的继承链产生色差。可选字段有值时允许清空,并通过页面原有变更回调传出
`''`
;组件不得修改日期协议、请求节奏、校验或业务状态。
-
表单布局:
`4xl`
新增/编辑弹窗桌面使用等宽双列,普通输入、日期、单选和多选各占一列,只有长文本 TextArea 跨两列;移动端回退单列。跨列只由内容密度决定,不由
`multiple`
数据属性决定。
-
Autocomplete:选择状态只使用 HeroUI 当前
`selectedKey`
/
`selectedKeys`
、
`selectionMode`
和
`onSelectionChange`
,搜索状态只使用
`inputValue`
/
`onInputChange`
。远程字段继续在业务层拥有防抖、请求和 ID-only 映射;共享组件不合成旧 DOM 事件、不复刻旧 Select API。
-
Tabs:直接使用 HeroUI Tabs compound API。页面级、模块级和详情级都按文字内容确定各项宽度,只在 compound slot 的
`className`
上设置
`Tabs/ListContainer: w-fit max-w-full min-w-0`
、
`List: w-max min-w-max`
、
`Tab: w-auto min-w-max flex-none whitespace-nowrap`
。标签始终单行完整展示,项目不得均分或固定 Tab 宽度,溢出只由
`Tabs.ListContainer`
的官方
`ScrollShadow`
处理。选中态、尺寸、圆角、颜色、阴影和动效采用官方默认,仅由
`theme.css`
提供主题值;禁止全局选择
`.tabs__*`
重画外观。
-
图表:HeroUI Pro PieChart、BarChart、ComposedChart、KPI 和 ChartTooltip;保留原统计数据与业务回调,不保留 ECharts 私有主题。
-
图表:HeroUI Pro PieChart、BarChart、ComposedChart、KPI 和 ChartTooltip;共享
`chartPalette.js`
导出
`tokens.css`
中的 8 个分类色、语义图表色引用及同图唯一分配器。分类图表必须一次传入完整系列 key,在 8 个系列内确定性消除哈希撞色;页面不得直接用
`success/danger/warning/foreground`
组装色板。图表 Card 主标题在 HeroUI
`Card.Header`
内统一左对齐并与正文左边缘一致;分类层级、范围和统计口径参数以紧凑
`secondary`
Autocomplete 位于 Header 右侧同一行,真正的并列视图切换才使用 HeroUI Tabs。禁止按图表类型切换为居中标题。图例文字通过公开 label slot 使用
`--rc-chart-legend-font-size`
/
`--rc-chart-legend-line-height`
,不得覆盖第三方图表内部类。保留原统计数据与业务回调,不保留 ECharts 私有主题。
-
图表 Tooltip:共享
`colorizeXinyuanChartTooltipPayload`
在进入 HeroUI Pro
`ChartTooltipContent`
前把 Tooltip indicator 颜色归一化为当前图形 mark/图例的同一 Token,并排除
`none`
/
`transparent`
。环图、单系列柱图和多系列组合图均显示官方 8px 圆点;页面不复制 Tooltip DOM 或样式。
-
图表焦点:HeroUI Pro 图表根统一使用
`rc-chart-interaction`
。共享层通过
`:focus:not(:focus-visible)`
隐藏鼠标点击产生的浏览器原生蓝框,通过
`:focus-visible`
和主题
`--focus`
保留键盘焦点;不得禁用 Recharts
`accessibilityLayer`
,也不得修改图表点击回调。
-
KPI:页面直接组合 HeroUI Pro
`KPI`
compound API,Card 根使用
`rc-card-surface rc-dashboard-card`
,
`KPI.Icon`
使用共享
`rc-metric-icon-surface`
。共享九色 Token 解决 Pro 原生
`status`
只有三种时的无背景回退,浅底统一为 9% tone(neutral 8%)。
`.rc-kpi-grid data-count`
负责 1–9 项栅格和无显式
`data-tone`
时的顺序唯一配色;业务可覆盖语义 tone,但同组不得重复图标或颜色。超过九项拆分模块,页面不得自定义色值、尺寸和背景。
-
图表栅格:共享
`.rc-chart-grid`
在 1280px 及以上使用等宽
`1fr / 1fr`
,以下单列。计数边界是同一当前可见业务 Section/Tab Panel 内的同级图表,不是整个路由页面;可配对图表保持双列,
`:only-child`
自动跨
`1 / -1`
通铺。隐藏区域和其他 Section 的图表不得让当前单图模块退化为左半宽。
-
页面布局:
`rc-app-page`
统一承担响应式页边距;页面根、模块、网格、工具栏和列表分别使用
`rc-page-stack`
、
`rc-page-section`
、
`rc-page-grid`
、
`rc-page-toolbar`
、
`rc-list-stack`
。这些类只管理布局几何,不选择或覆盖 HeroUI 内部 DOM。
-
列表工具栏:数据集标题单独位于透明列表区首行,下一行才是命令工具栏。搜索与列分组并存时,左侧顺序固定为 HeroUI
`SearchField primary`
在前、HeroUI
`ToggleButton`
分组控件在后,右侧为业务操作;主操作使用
`Button primary`
并最右,次操作使用
`Button secondary`
。移动端搜索先独占整行,分组控件在下一行自然换行。列分组 ToggleButton 直接使用官方状态和 Ant Design 16px 图标,不重画胶囊背景:列正常展示时是默认灰色未选中态,列被收起时才是主题选中态;Tooltip/aria-label 根据当前状态只描述下一步“显示”或“隐藏”动作。只有 Surface/overlay 内的嵌套字段使用
`secondary`
SearchField;
`outline`
不作为次按钮默认映射。
-
模块 Card 控件:Card Header 使用单行“左侧标题 + 右侧控件”。并列内容视图用 HeroUI Tabs;分类层级、日期范围、统计口径等同一内容的参数用直接 HeroUI Autocomplete 或 HeroUI-native
`XinyuanAutocomplete`
,使用
`secondary`
并按内容紧凑定宽。参数变化只连接原状态和回调,不改变统计口径、请求或数据结构。
## 主题
`src/design-system/theme.css`
是
权威主题,必须与
`xinyuan-design-system`
基线逐字节一致。加载顺序为 Tailwind、HeroUI、HeroUI Pro、
`theme.css`
、
`tokens.css`
、Shell/Login 样式。
`tokens.css`
只提供 Shell/Login 所需的
`--rc-*`
别名,不覆盖 HeroUI 核心或组件 Token
。内容页面禁止用 CSS 选择 HeroUI 内部 BEM 类、slot 状态或选中属性。
`src/design-system/theme.css`
是
以 HeroUI 官方主题工具导出值为基础并加入已批准表面映射的鑫元权威主题,必须与
`/Users/liuchenlong/Desktop/theme.css`
及
`xinyuan-design-system`
基线逐字节一致。加载顺序为 Tailwind、HeroUI、HeroUI Pro、
`theme.css`
、
`tokens.css`
、Shell/Login 样式。
`tokens.css`
只提供 Shell/Login、页面几何和批准的数据可视化扩展 Token,不覆盖 HeroUI 核心或组件 Token。图表系列 Token 在浅色/深色中固定不变;HeroUI Pro 继续拥有坐标轴、网格、文字、Tooltip 和表面的主题适配
。内容页面禁止用 CSS 选择 HeroUI 内部 BEM 类、slot 状态或选中属性。
页面几何 Token 是允许的扩展:Navbar Header、HeroUI Pro 折叠按钮、主题按钮和页面内容共用
`--rc-shell-inline-gutter`
,桌面为 48px、平板为 32px、手机为 16px,左右完全一致。页面一级模块、模块网格和列表内部节奏为 32/24/16px;手机端一级模块和网格收敛为 24/16px。页面不得新增另一套私有 gutter 或顶层
`space-y-*`
。
...
...
docs/ui-migration-checklist.md
View file @
f53d7e42
...
...
@@ -36,22 +36,37 @@
-
当前桌面会话没有可用浏览器连接,因此表格勾选、Portal、暗色切换和响应式仍保持“待浏览器验收”,不以构建结果替代。
-
本地枚举、表单单选/多选、远程用户/引用、统计年份和分页条数已统一为 HeroUI Autocomplete;活动业务代码的 Select、原生 select 与页面自建选择 Popover 扫描为零。
-
Autocomplete 编译和静态 selection 契约检查已通过;打开态搜索、单选关闭、多选保留、清空、键盘焦点及深浅色 Portal 仍待真实浏览器验收。
-
日期/月份字段的日历触发器与 Autocomplete 箭头已统一为 16px Ant Design Icons、HeroUI 字段占位前景和同一右侧光学中心;可选日期类字段有值时显示 HeroUI
`CloseButton`
,通过既有
`onChange`
回写
`''`
。检查报告出具月份的清空、自动刷新和深浅色真实像素对齐仍待浏览器验收。
-
页面布局已统一到流式 HeroUI 示例节奏:Navbar 操作与页面内容共用对称 gutter,桌面左右 48px、平板左右 32px、手机左右 16px;一级模块 32px、模块网格 24px、列表内部 16px。驾驶舱、整改分析、问题清单、归档、整改处理、整改流程和空状态页已接入共享语义布局类。
-
Tabs 已统一为 HeroUI compound API 的内容宽度模式:每项宽度由自身文字和 HeroUI 默认内边距决定,不均分、不固定;所有标签
`flex-none + min-w-max + whitespace-nowrap`
,不足时仅由
`Tabs.ListContainer`
内部滚动。没有覆盖 HeroUI 的颜色、圆角、指示器、阴影或选中态,选中 key 与切换回调保持原样。
-
DataGrid 行操作已统一为 HeroUI
`secondary`
/
`danger-soft`
正圆按钮,Ant Design 16px 图标采用三层网格居中,操作组左对齐且间距 8px;操作列宽度随 1/2/3 个可见入口计算为 80/112/144px,4 个及以上折叠为“前两个 + 更多”。业务回调、权限、禁用条件和操作排序未改,真实像素居中与 Tooltip/Dropdown 打开态仍待浏览器验收。
-
DataGrid 行操作已按语义分级为 HeroUI
`tertiary`
/
`secondary`
/
`danger-soft`
正圆按钮:查看、编辑、日期、复制、锁定和更多使用中性
`tertiary`
,审批/提交等关键操作使用强调
`secondary`
,删除/驳回使用
`danger-soft`
。Ant Design 16px 图标采用三层网格居中,操作组左对齐且间距 8px;操作列宽度随 1/2/3 个可见入口计算为 80/112/144px,4 个及以上折叠为“前两个 + 更多”。业务回调、权限、禁用条件和操作排序未改,真实像素居中与 Tooltip/Dropdown 打开态仍待浏览器验收。
-
危险操作已从 HeroUI 默认洋红系统一为鑫元红系 Token:浅色
`--danger`
为
`#9E1B1B`
,深色为同色相增亮红;删除行图标、删除选中、确认删除、驳回与退出登录继续直接使用 HeroUI
`danger`
/
`danger-soft`
,不存在页面私有颜色。
-
短列表宽度已改为 HeroUI Pro 原生
`1fr`
弹性列:第一业务主列吸收剩余空间,选择列、操作列和其他字段保留最小宽度,表格内容至少铺满 DataGrid。归档等少列列表不再出现右侧大块空白;真实宽表仍保留内部横向滚动。各视口的实际列分配仍待浏览器验收。
-
页容量选择器已从共享分页、问题清单和归档列表移除。分页栏只保留 HeroUI compound Pagination 内容,数据层默认并固定为 10 条/页;翻页与服务端
`page_size`
逻辑未改。
-
归档页的数字指标卡已从页面私有
`Card + Chip`
排版替换为 HeroUI Pro
`KPI`
compound API;标题在 Header,数值和语义图标由 Content 原生两列布局承载,不再显示右上角说明图标。统计口径与数值未改。
-
驾驶舱与归档页的 KPI 组已改用 16px 关联项间距;一级模块仍保持 32px,普通图表/模块网格仍保持 24px。HeroUI Pro KPI 内部 padding 和组件外观未覆盖。
-
列表筛选已改为条件数量自适应的 HeroUI 布局:1–2 个条件直接位于列表标题上方,3 个以上保留独立 Card;问题清单的 10 个条件首行展示 3 个等宽高频项,其余 7 项使用原生 HeroUI Disclosure 展开。筛选组采用 12px 间距、标签与控件 6px 间距,宽屏展开区为 4 列;该页原本已随筛选状态自动请求,已移除重复查询和清空图标入口,筛选值、请求参数与自动刷新时机未改。
-
驾驶舱、归档和整改流程的数值图标已统一到共享
`.rc-metric-icon-surface`
:所有图标都有 48px 浅色语义背景和 20px Ant Design 图标;驾驶舱五项分别使用
`info / success / teal / danger / warning`
,组内图标和颜色均不重复。统计值、接口和回调未改,浅色/深色真实像素居中仍待浏览器验收。
-
KPI 图标浅底混色已从 16% 降为 9%(neutral 8%),并补齐九组共享 tone;
`.rc-kpi-grid data-count`
已覆盖 2、5、6、7、8、9 项布局,1–9 项无显式语义 tone 时自动按顺序使用不同颜色。超过九项须拆分模块,不允许循环撞色。
-
驾驶舱与整改分析图表已从
`accent/warning/success/danger/muted/foreground`
状态色拼接改为鑫元 8 色清爽冷调分类色板,并增加主系列、风险、提醒、完成率、对比和中性语义色。环图、堆叠柱图、重点问题、逾期排行、责任矩阵和质效评级均只引用共享 Token;分类系列通过共享分配器做确定性冲突消解,8 个以内不再出现审计部/风险管理部同色。系列色在浅色/深色下固定不变,HeroUI Pro 仍负责坐标轴、网格、文字、Tooltip 与表面的模式切换;统计数据和交互回调未改。浅色/深色实际对比度、Tooltip 和图例一致性仍待真实浏览器验收。
-
整改分析页图表按当前可见业务分组接入
`.rc-chart-grid`
:业务领域/根本原因、来源对比/重点问题、逾期排行/责任矩阵在各自 Section 内保持宽桌面等宽双列,不再使用
`1.05/0.95`
或
`0.9/1.1`
;“审计整改质效”分组当前只有综合整改质效结果一个图表,因此通过共享
`:only-child`
规则通铺,与普通驾驶舱的单图整改进度一致。1280px 以下统一回退单列;真实图例换行和各断点高度仍待浏览器验收。
-
驾驶舱与整改分析的图表 Card 主标题已统一为 HeroUI
`Card.Header`
左对齐,与
`Card.Content`
左边缘一致;分类层级等当前图表参数使用紧凑 HeroUI Autocomplete 位于 Header 右侧同一行,真正并列视图才使用 Tabs。不得因图表类型、单双列、空状态或通铺布局切换为居中标题;真实像素边线仍待浏览器验收。
-
图表图例说明文字已统一为扩展 Token 控制的 12px/16px,通过 Recharts 公开
`labelStyle`
传入;8px 圆点、系列颜色、数据顺序和图表交互未改。真实宽屏换行和暗色模式仍待浏览器验收。
-
列表筛选已改为条件数量自适应的 HeroUI 布局:1–2 个条件直接位于列表标题上方,3 个以上使用直接 HeroUI
`Surface`
,不使用 Card 或
`Card.Content`
;问题清单的 9 个结构化条件首行展示 3 个等宽高频项,其余 6 项使用原生 HeroUI Disclosure 展开。筛选组采用 12px 间距、标签与控件 6px 间距,宽屏展开区为 3 列;该页原本已随筛选状态自动请求,已移除重复查询和清空图标入口,筛选值、请求参数与自动刷新时机未改。
-
筛选折叠入口不再使用手写条件渲染和箭头状态,已切换为
`Disclosure.Trigger + Disclosure.Indicator + Disclosure.Content`
;真实展开动画、键盘操作、焦点状态、暗色表面和各视口布局仍待浏览器验收。
-
展开筛选区已限制在首行字段区域内并复用同一三列模板,6 个附加条件形成 3×2 对齐网格;右侧“收起”操作列不再参与展开字段的宽度分配。平板回退 2 列、手机回退 1 列。
-
筛选 Card 内的 Autocomplete、DateField.Group 和普通 Input 已改为根主题驱动的 HeroUI 官方 variant:浅色
`primary`
避免 secondary 灰度过深,暗色
`secondary`
避免字段表面与 Card 融为一体;未增加私有颜色、描边或主题覆盖。
-
筛选页面内层级已统一为页面
`background`
→ HeroUI
`Surface default`
→ HeroUI
`secondary`
筛选框。权威主题已将
`--default`
映射到
`--surface-secondary`
,修正原值与
`surface-tertiary`
过近造成的深灰错觉;外层 Surface 直接使用
`rounded-3xl p-6`
,不叠加描边或 Card 阴影,内部 Autocomplete、DateField.Group 和普通 Input 在浅色/暗色中都使用
`secondary`
。筛选 Autocomplete 和日期选择的真实 Popover 已取消
`surface-tertiary`
覆盖,与 Modal、Dropdown、Tooltip 等全部浮层统一使用 HeroUI 默认
`overlay`
;Popover 内 SearchField 继续使用
`secondary`
作为浮层内控件。
-
Surface 层级已固化到项目设计规范、UI 架构和共享
`xinyuan-design-system`
skill:
`background -> surface/default -> surface-secondary -> surface-tertiary`
只描述页面内嵌套,
`overlay`
是独立的浮动/阻断维度,不属于第四层 Surface。旧“白色筛选 Card + 描边/阴影”和筛选弹层私有表面规则已移除。
-
问题清单“整改情况”列已提高到 420px(固定时 360px),使用两行换行和 HeroUI Tooltip 展示全文,避免内容越过单元格;所有 user 类型列表字段已改为 HeroUI
`Avatar`
+ 姓名的紧凑身份展示,人员列至少 220px。
-
驾驶舱与整改分析的年度范围控件已去除重复“统计年份”可见标签,保留
`aria-label`
,使用 144px 宽的 HeroUI Autocomplete
`primary`
触发器;年份来源、默认值和切换回调未改变。
-
问题整改详情已调整为 HeroUI Modal 的 1088px 内容型宽度,28px inset、内部滚动和右上角全屏/关闭操作组;全屏图标不再落到标题下方,字段内容移除逐字段阴影 Card,改为紧凑两列标签/值网格。
-
问题整改详情已改为弹窗级查看/编辑状态:默认字段只读,单一“编辑”入口进入整窗编辑,Footer 统一“取消 / 保存”;取消恢复原记录,保存沿用原
`updateRecord`
和嵌套 payload 一次提交变更,视图禁用、锁定和只读字段不进入编辑。生产构建与静态状态检查已通过,真实点击与接口请求仍待浏览器验收。
-
新增问题整改记录弹窗已与详情统一为 HeroUI
`4xl`
(1088px);Input、TextArea、DateField.Group、Autocomplete 和多选控件
共用浅色
`primary`
/ 深色
`secondary`
variant 并铺满字段列。原“检查项目名称、问题简述至少填写一项”不再常驻,只有点击“确认新增”且原校验失败时才打开 HeroUI
`AlertDialog`
,关闭后保留草稿且不发送新增请求。已在真实浏览器验证 1280x720 浅色/深色和 390x844:桌面弹窗宽 1088px,移动端宽 358px且保留 16px安全边距;三类字段均为 36px高、同列等宽、同主题背景,校验失败期间未出现新增接口请求
。
-
新增问题整改记录弹窗已与详情统一为 HeroUI
`4xl`
(1088px);Input、TextArea、DateField.Group、Autocomplete 和多选控件
在浅色、深色中统一使用
`secondary`
并铺满字段列,不再监听主题切换 variant。原“检查项目名称、问题简述至少填写一项”不再常驻,只有点击“确认新增”且原校验失败时才打开 HeroUI
`AlertDialog`
,关闭后保留草稿且不发送新增请求。此前已验证弹窗宽度、移动端安全边距和校验行为;本次统一后的两种主题字段层级仍需真实浏览器复验
。
-
新增表单的短字段已统一为桌面等宽双列,责任部门等多选字段不再单独占满整行,并会与下一项整改进度配对;问题简述、整改情况和备注等长文本仍跨两列,移动端全部回退单列。真实浏览器已验证责任部门/整改进度同排各 508px,390px 视口下各字段依次回退为 302px 单列。
-
问题清单与归档列表的单一关键词字段已从筛选 Card 拆出,改为列表标题右侧的 HeroUI
`SearchField secondary`
;桌面保持 320–384px,手机换行全宽,原关键词值、自动请求和清空回调保持不变。
-
弹窗内部层级已统一为
`Backdrop -> Overlay -> secondary -> rare tertiary`
:应用切换项与骨架、详情附件编辑器、流程详情摘要对象使用 HeroUI
`secondary`
;Header/Body/Footer 保持透明,普通详情查看态通过一个
`Surface secondary`
承载整组无框标签/值。已移除需要边界的默认 L1 Card,未新增页面私有背景、描边或阴影;真实深浅色层级仍待浏览器验收。
-
应用切换 Logo 已统一为 L2 应用 Card 内的 HeroUI
`Surface tertiary + Avatar soft`
:固定 36px 区域、真实图片等比包含、无图/加载失败使用同尺寸 accent 首字回退,解决透明 Logo 与裸文字缺少容器的问题。问题详情编辑态不保留整组查看 Surface,避免表单控件发生 L2 套 L2;业务字段、权限、编辑入口和保存回调未改。
-
问题清单与归档列表的单一关键词字段已从结构化筛选 Surface 拆出。数据集标题独占透明首行,下一行命令工具栏使用 HeroUI
`SearchField primary`
;存在列分组时,搜索位于左侧最前,分组 ToggleButton 紧随其后。搜索桌面保持 320–384px,手机先独占整行,原关键词值、自动请求和清空回调保持不变。问题清单和归档页的次工具栏操作已从
`outline`
统一为 HeroUI
`secondary`
;新增记录继续使用
`primary`
,按钮回调与禁用条件未改。
-
问题清单标题已独立为列表区首行,搜索、列分组和业务按钮整体下移到第二行;第二行左侧顺序固定为搜索在前、列分组在后,右侧操作顺序不变。列分组使用带 16px Ant Design 图标的 HeroUI
`ToggleButton size="sm"`
:默认可见时使用官方灰色未选中态,收起后才使用主题选中态;Hover/读屏文案按当前状态分别为“隐藏{分组名}列”或“显示{分组名}列”。列显隐回调、滚动恢复、搜索请求及播报逻辑未改;真实桌面/移动端对齐仍待浏览器复验。
-
整改分析“问题类型 / 业务领域分布”的分类层级已从 Header 下方整行 Tabs 改为标题右侧 HeroUI-native
`XinyuanAutocomplete secondary`
;一级至四级选项、
`activeCategoryLevel`
状态和原数据切换回调保持不变。该控件按模块参数而非并列视图处理,其他真实视图切换 Tabs 未受影响。
-
驾驶舱与整改分析的 Pie/Bar/Composed Tooltip 已统一通过共享 payload 颜色归一化后进入 HeroUI Pro
`ChartTooltipContent`
;环图和单系列 Tooltip 不再因不可见 stroke 丢失颜色圆点,多系列圆点继续与柱/线/图例同色。统计值、Hover 触发和数据口径未改,真实浏览器仍需复验全部图表。
-
驾驶舱与整改分析的 Pie/Bar/Composed 根已统一挂载
`rc-chart-interaction`
;鼠标点击图形区域不再显示浏览器蓝色 Focus 方框,键盘
`focus-visible`
仍使用鑫元主题焦点环。Recharts
`accessibilityLayer`
、图表点击与 Tooltip 逻辑均未关闭。
-
仍需在 1440x900、1024x768、768x1024 与 390x844 浏览器视口核验对齐线、换行、DataGrid 内部横向滚动和移动端无页面级横向溢出。
docs/xinyuan-design-system.md
View file @
f53d7e42
...
...
@@ -13,9 +13,10 @@
## 2. 主题权威
-
`src/design-system/theme.css`
是浅色、暗色、品牌色、语义色、圆角和字体的唯一主题权威,
必须与批准的鑫元
`theme.css`
逐字节一致
。
-
`src/design-system/tokens.css`
仅保留 Login/AppShell
的
`--rc-*`
别名,不得覆盖 HeroUI 的组件 Token、阴影
、控件高度、DataGrid、Tabs、Modal、Card、Checkbox 或 Pagination 外观。
-
`src/design-system/theme.css`
是浅色、暗色、品牌色、语义色、圆角和字体的唯一主题权威,
以 HeroUI 官方主题工具导出值为基础,并在文件内固化批准的 L1/L2/Overlay 映射。它必须与
`/Users/liuchenlong/Desktop/theme.css`
及共享 Skill 基线逐字节一致;升级 HeroUI 时重新导出基础值、复核三项语义映射并整体批准新基线
。
-
`src/design-system/tokens.css`
仅保留 Login/AppShell
别名、页面几何和批准的数据可视化扩展 Token,不得覆盖 HeroUI 的组件 Token
、控件高度、DataGrid、Tabs、Modal、Card、Checkbox 或 Pagination 外观。
-
内容页面使用 HeroUI 的
`background`
、
`surface`
、
`foreground`
、
`muted`
、
`accent`
、
`success`
、
`warning`
、
`danger`
等语义 Token,不新增 hex、zinc/slate/sky/rose 等私有调色板。
-
HeroUI
`danger`
固定为鑫元红系:浅色
`oklch(45.32% 0.1661 27.03)`
(鑫元主红
`#9E1B1B`
),深色
`oklch(62% 0.16 27.03)`
(同色相增亮)。删除、驳回、退出和失败状态继续使用 HeroUI
`danger`
/
`danger-soft`
,软色由 HeroUI 公式自动生成;禁止恢复洋红、亮粉或页面私有危险色。
-
主题挂载在
`document.documentElement`
,Portal、Popover、Tooltip、Dropdown 和 Modal 必须继承同一主题。
-
禁止在页面 CSS 中选择 HeroUI 内部类或状态属性,例如
`.data-grid*`
、
`.tabs*`
、
`.modal*`
、
`.checkbox*`
、
`[data-selected]`
、
`[data-state]`
。
...
...
@@ -27,6 +28,28 @@
-
页面不得用普通
`div`
模拟 Card、Alert、Chip、EmptyState、ListBox、Table 或 Modal。
-
不嵌套 Card;同层内容优先使用间距和 HeroUI 原生分隔结构。
### 3.1 Surface 层级硬规范
Surface 用于表达页面内的包含关系,Overlay 用于表达浮动关系。页面只判断使用场景,不直接写颜色。以下十六进制仅便于人工识别,代码以
`theme.css`
中的 OKLCH 值为准。
| 层级 | Token 与固定值 | 什么时候使用 | 页面写法 |
| --- | --- | --- | --- |
| L0 页面画布 |
`--background`
:浅色
`oklch(97.02% 0.0045 27.03)`
,约
`#F8F4F3`
;深色
`oklch(12% 0.0045 27.03)`
,约
`#070505`
| AppShell、页面底色、无框工具栏所在画布 | 不新建表面;使用页面容器 |
| L1 主内容 |
`--surface`
:浅色
`oklch(100% 0.0022 27.03)`
,约
`#FFFFFE`
;深色
`oklch(21.03% 0.009 27.03)`
,约
`#1C1716`
| 第一层内容面板、筛选 Surface、Card、直接位于页面画布上的搜索框 |
`Surface default`
、Card 默认、字段
`primary`
|
| L2 次要层 |
`--surface-secondary`
:浅色
`oklch(95.24% 0.0036 27.03)`
,约
`#F2EEEE`
;深色
`oklch(25.70% 0.0067 27.03)`
,约
`#262222`
| L1 内的筛选框、表单控件、次按钮、真正的次级内容区 | 字段或按钮
`secondary`
;结构区使用
`surface-secondary`
|
| L3 第三层 |
`--surface-tertiary`
:浅色
`oklch(93.73% 0.0036 27.03)`
,约
`#EDE9E9`
;深色
`oklch(27.21% 0.0067 27.03)`
,约
`#2A2625`
| 仅限 L2 内仍需嵌套的静态内容区;极少使用 | 只使用语义 Surface Token,不作为字段 variant |
| Overlay 浮层 |
`--overlay = --surface`
:浅色约
`#FFFFFE`
;深色约
`#1C1716`
,通过官方 overlay shadow 提升 | Modal、AlertDialog、Autocomplete/DatePicker Popover、Dropdown、Tooltip | 保留 HeroUI 默认 Overlay,不传背景类 |
`theme.css`
的唯一组件映射为:HeroUI
`primary`
字段读取
`--field-background = --surface`
(L1);HeroUI
`secondary`
字段和次按钮读取
`--default = --surface-secondary`
(L2);所有 Portal 浮层读取
`--overlay = --surface`
。因此页面不得设置
`bg-*`
、hex、OKLCH 或组件内部 CSS。
选择规则只有三条:直接位于页面画布上的搜索或输入使用
`primary`
;位于 Surface、Card、Modal 或 Overlay 内容中的输入、日期和 Autocomplete 使用
`secondary`
;所有弹出面板保持组件默认 Overlay。L3 绝不用于 Input、Button、Popover 或 Modal。
-
3 个及以上结构化筛选固定使用
`<Surface variant="default" className="rounded-3xl p-6">`
;不加描边、不加 Card 阴影、不再套
`Card`
/
`Card.Content`
。1–2 个筛选条件保持无框。
-
筛选、Modal 和 Overlay 内容内的 Input、Autocomplete 和真实
`DateField.Group`
在浅色、暗色中都使用
`secondary`
,不得根据主题切换 variant。
-
页面 Surface 层级不延伸到 Portal。筛选 Autocomplete/DatePicker 的真实 Popover 与普通表单、详情、Dropdown、Tooltip、Modal 一样,统一使用 HeroUI 官方
`overlay`
表面、前景色、圆角和阴影,不传入背景类覆盖默认浮层。
-
Popover 内 SearchField 可继续使用
`secondary`
作为 overlay 内部控件层,但不得改变 Popover 根表面,也不得给筛选浮层建立私有 light/dark 分支。
-
父子层级需要区分时不得解析成相同背景;没有嵌套语义时也不得为制造色差凭空增加一层 Surface。
-
禁止以
`bg-white`
、zinc/slate/gray、透明叠色、渐变、模糊、页面私有 light/dark selector 代替主题 Surface。透明仅用于已由父层提供边界的工具栏、列表标题、行或纯布局组。
## 4. Login 与 AppShell
-
应用名称统一为“问题整改”,供 Login、Sidebar、Navbar、应用切换和浏览器标题共用。
...
...
@@ -34,7 +57,8 @@
-
Sidebar、Navbar 使用 HeroUI Pro,Logo 浅色使用
`xy-logo-red.png`
、暗色使用
`xy-logo-white.png`
。
-
`src/design-system/shell.css`
只负责应用壳布局、导航定位、折叠、Logo 和响应式,不得延伸到列表、筛选、Tabs、Card、DataGrid 或弹窗正文。
-
侧边栏只有一个导航组时,不在菜单列表上方重复显示应用名称;组名继续作为
`Sidebar.Menu`
的可访问性标签。只有存在两个及以上确需区分的业务分组时才显示
`Sidebar.GroupLabel`
。
-
应用切换使用 HeroUI Modal、Card、Avatar、Chip、Skeleton、Alert 和 EmptyState 的默认外观。
-
应用切换使用 HeroUI Modal、Card、Surface、Avatar、Chip、Skeleton、Alert 和 EmptyState。应用 Card 位于 Overlay 内,使用
`secondary`
;36px Logo 区域是 Card 内的静态第三层,使用
`Surface tertiary`
承载 HeroUI
`Avatar soft`
。真实图片使用
`object-contain`
,无图或加载失败时使用同尺寸的
`accent`
首字回退,不允许裸文字、拉伸图片或尺寸跳变。
-
该 Logo well 是跨应用固定规范:
`size-9`
、
`shrink-0`
、
`rounded-[var(--rc-radius-control)]`
、居中且裁切稳定,内部 Avatar 铺满、使用相同圆角并保持透明,图片使用
`object-contain p-1.5`
,Skeleton 也使用
`size-9`
。浅色、深色下必须分别验证真实不透明图、透明/emoji-like 图和加载失败回退,三者不得改变标题起点。
### 4.1 页面边距与布局节奏
...
...
@@ -54,16 +78,19 @@
-
所有 Tab 宽度跟随各自文字内容,不均分剩余空间、不设置统一宽度。统一 slot class 为
`Tabs/ListContainer: w-fit max-w-full min-w-0`
、
`List: w-max min-w-max`
、
`Tab: w-auto min-w-max flex-none whitespace-nowrap`
;
`全部`
等短标签保持紧凑,
`风险管理部`
等长标签自然更宽。
-
Tab 文字必须完整单行显示,不允许中文部门名、分类名或视图名换成两行,也不通过截断隐藏标签。空间不足时只允许 HeroUI
`Tabs.ListContainer`
内置
`ScrollShadow`
横向滚动,外层不得再加
`overflow-x-auto`
形成双滚动。
-
上述 class 只约束宽度、flex 收缩和换行;不得覆盖
`.tabs__list-container`
、
`.tabs__list`
、
`.tabs__tab`
、
`.tabs__indicator`
的背景、边框、圆角、颜色、阴影、尺寸、Hover 或选中态,也不得把文档示例 CSS 复制为项目私有 Tabs 皮肤。
-
筛选区使用 HeroUI Card、Autocomplete、SearchField、Button;
关键词搜索和选择器弹层搜索的图标都放在官方
`SearchField.SearchIcon`
slot。
-
Card 内的结构化筛选字段按根主题切换 HeroUI 官方 variant:浅色使用
`primary`
的
`--field-background`
白色表面,暗色使用
`secondary`
的
`--default`
表面与 Card 拉开层级。Autocomplete、普通 Input 使用同一 variant,日期将 variant 传给真实的
`DateField.Group`
;不修改
`theme.css`
,不新增页面私有背景、描边
或阴影。
-
结构化筛选区直接使用 HeroUI
`Surface`
、Autocomplete、SearchField 和 Button;不用 Card 代替 Surface,不再套
`Card.Content`
。
关键词搜索和选择器弹层搜索的图标都放在官方
`SearchField.SearchIcon`
slot。
-
筛选页面内层级固定为页面
`background`
→
`Surface default / bg-surface`
→
`secondary / --default = --surface-secondary`
筛选框。外层 Surface 使用官方基础示例的
`rounded-3xl p-6`
,不额外添加描边、Card 阴影或页面私有背景;Surface 内的 Autocomplete、普通 Input 和真实
`DateField.Group`
在浅色/暗色中统一使用 HeroUI
`secondary`
。筛选 Autocomplete 与 DatePicker 的真实 Popover 脱离页面层级,直接使用 HeroUI 默认
`overlay`
;其内部官方 SearchField 继续继承
`secondary`
variant。不得为筛选弹层覆盖背景、前景色、圆角
或阴影。
-
DataGrid 长文本先按业务重要性扩宽,再允许最多两行自然换行,超出内容通过 HeroUI Tooltip 展示全文;“整改情况”基准宽度为 420px,作为左侧固定列时限制为 360px,任何文字都不得跨越单元格遮挡相邻字段。
-
DataGrid 的用户类型字段直接使用 HeroUI
`Avatar`
:列表使用
`size="sm"`
、
`variant="soft"`
,有头像资源时显示图片,无资源时显示姓名首字回退;头像与姓名水平居中,人员列基准宽度不小于 220px,不使用页面私有渐变、边框或旧 Avatar 组件。
-
驾驶舱、分析页的单一年度范围使用紧凑 HeroUI Autocomplete,选择值本身表达“全部年份”或具体年份时不再重复显示外置“统计年份”标签;触发器使用官方
`primary`
外观和内容宽度,不添加页面私有胶囊、阴影或边框。
-
内容型详情弹窗使用 HeroUI Modal
`4xl`
基准宽度 1088px、内部滚动和 28px 内容 inset;全屏与关闭图标在标题栏右上组成同一 24px 操作组并垂直居中。详情字段使用无框标签/值网格,不为每个字段重复创建带阴影 Card。
-
可编辑详情弹窗默认进入查看态,字段值不可点击编辑;Footer 右下只提供一个“编辑”主操作,不再重复显示“关闭”文字按钮,查看态通过右上角关闭图标退出。进入编辑态后一次性展示全部有权限字段的 HeroUI 控件,Footer 原位切换为“取消 / 保存”;取消丢弃整次草稿,保存按原接口和嵌套 payload 一次提交全部变更。禁止每个字段各自显示编辑、清空、取消或保存按钮,锁定、只读和视图禁用字段在编辑态仍保持查看表现。
-
详情编辑态的 Input、TextArea、DatePicker、Autocomplete 和引用选择必须共用同一 HeroUI field variant:浅色
`primary`
、深色
`secondary`
。不得让选择框呈白底而文本字段呈深灰底,也不得用页面私有背景或描边强行对齐。
-
列表的单一关键词搜索不进入结构化筛选 Card,也不显示字段标签;使用 HeroUI
`SearchField`
的
`secondary`
、
`fullWidth`
compound API,放在列表标题右侧。桌面宽度为 320–384px,手机端换到标题下一行并占满可用宽度;搜索和清空图标使用 Ant Design Icons。
-
筛选区按条件数量自适应:1–2 个条件直接放在列表模块标题上方,不额外套 Card;3 个及以上条件使用独立 HeroUI Card。独立筛选卡首行最多保留 3 个高频条件,其余条件使用 HeroUI
`Disclosure`
的
`Trigger`
、
`Indicator`
和
`Content`
展开,不手写高度动画、旋转箭头或折叠状态 DOM。
-
详情编辑态位于 Modal 内,Input、TextArea、DatePicker、Autocomplete 和引用选择在浅色、深色中统一使用 HeroUI
`secondary`
。不得根据主题切换 variant,也不得用页面私有背景或描边强行对齐。
-
列表的单一关键词搜索不进入结构化筛选 Surface,也不显示字段标签;透明列表区先单独显示数据集标题,再在标题下方放命令工具栏。搜索与列分组并存时,工具栏左侧先放 HeroUI
`SearchField`
的默认
`primary`
、
`fullWidth`
compound API,再放 HeroUI
`ToggleButton`
列分组控件,业务操作位于最右。桌面端搜索与分组同排水平居中;手机端搜索先独占整行,分组控件在下一行自然换行。只有位于
`Surface`
或
`overlay`
内的嵌套搜索控件才使用
`secondary`
。搜索桌面宽度为 320–384px,手机端占满可用宽度;搜索、清空和分组图标使用 Ant Design Icons。所有列表关键词搜索统一使用占位文案“请输入关键词”,不按数据对象变化、不枚举可搜索字段、不加省略号;精确的人员、引用或下拉搜索仍使用明确的目标对象文案。
-
列分组是显示/隐藏字段的 UI 状态,不是数据筛选。每个分组直接使用 HeroUI
`ToggleButton size="sm"`
,采用 16px Ant Design 语义图标与文字;列正常展示时保持 HeroUI 默认灰色未选中态,列被收起时才使用官方主题选中态,不得继续使用 Switch、私有 segmented control、页面背景色或手写 selected 样式。Tooltip 与
`aria-label`
必须说明点击后的唯一动作:当前展示时写“隐藏{分组名}列”,当前收起时写“显示{分组名}列”,不得使用含糊的“显示或隐藏”。列显隐、滚动恢复和可访问播报必须保持原回调。
-
模块 Card 内先区分“并列视图”和“当前内容参数”:真正替换模块内容的视图、模式或指标切换使用内容宽度 HeroUI Tabs;分类层级、时间范围、统计口径等只改变同一图表/内容呈现方式的参数使用 HeroUI Autocomplete。模块参数选择器位于 Card Header 右侧并与左侧标题同一行水平居中,使用
`secondary`
表面和紧凑内容宽度;不得在标题下方再占一整行 Tabs,也不得使用页面私有 Select。窄屏可让标题换行,但选择器不压缩且不能造成 Card 横向溢出。
-
列表工具栏主操作使用 HeroUI
`primary`
并置于最右,导出、归档、对比等次操作直接使用安装版本 HeroUI
`secondary`
。
`outline`
是独立的强调边界 variant,不再作为次按钮的默认同义词;不得用页面 CSS 重画 secondary 或 outline。
-
筛选区按条件数量自适应:1–2 个条件直接放在列表模块标题上方,不额外套容器;3 个及以上条件使用独立 HeroUI
`Surface`
。首行最多保留 3 个高频条件,其余条件使用 HeroUI
`Disclosure`
的
`Trigger`
、
`Indicator`
和
`Content`
展开,不手写高度动画、旋转箭头或折叠状态 DOM。
-
同一筛选组内字段间距统一为 12px,字段标签与控件间距为 6px;展开内容与首行间距为 12px。展开区必须复用首行字段区域的宽度和列模板,不跨入右侧展开操作列;本项目宽屏为 3 列、平板 2 列、手机 1 列,使上下字段左右边线完全对齐。
-
展开入口使用 HeroUI
`sm`
、
`ghost`
按钮与
`Disclosure.Indicator`
,静止态透明,文案为“展开”/“收起”。自动筛选页面不显示查询或刷新按钮;只有业务明确采用手动提交时才保留查询按钮。按钮外观、动画、焦点和暗色状态由 HeroUI 与
`theme.css`
管理,页面不得绘制灰色胶囊、分割线或独立箭头。
-
筛选字段、展开/收起、刷新、查询和重置的业务回调保持原样;不得因视觉迁移改变触发时机和请求参数。
...
...
@@ -72,6 +99,7 @@
-
Autocomplete、SearchField、ListBox、Checkbox 遵守 React Aria compound/slot 结构,不用旧 DOM 模拟;箭头与搜索/清除图标使用
`AppIcons.jsx`
的 Ant Design Icons。
-
`@heroui/styles`
自带的
`autocomplete.css`
是外观权威,不复制用户文档示例 CSS,不在页面中覆盖
`.autocomplete*`
内部类。
-
日期使用共享 HeroUI DatePicker,上传使用 HeroUI Pro DropZone;禁止可见原生 date、select、file 控件。
-
日期、月份和日期时间字段的日历图标与 Autocomplete 下拉箭头统一使用 16px Ant Design Icons,并通过共享
`FIELD_SUFFIX_ICON_STYLE`
让 Ant 图标根节点直接解析 HeroUI
`var(--field-placeholder, var(--muted))`
;不能只依赖跨组件继承,也不得写黑色、灰色常量或页面私有颜色。两者尾部图标的右侧光学中心必须一致,不允许用页面私有偏移修正。可选日期类字段在有值时显示 HeroUI
`CloseButton`
清空入口,清空必须沿用原
`onChange`
通道写回业务既有空值
`''`
,不得改变日期格式、查询参数或自动请求时机;必填、只读和禁用字段不显示清空入口。
## 6. DataGrid 与分页
...
...
@@ -81,19 +109,25 @@
-
DataGrid 无论列数多少都占满列表可用宽度。非宽表使用 HeroUI Pro 原生列尺寸:选择列和操作列保持固定,第一业务主列使用
`1fr`
吸收剩余空间,表格内容保持
`min-width: 100%`
;只有真实最小列宽超过视口时才使用像素列宽和内部横向滚动,不允许出现表面全宽但正文只停在左侧的空白断层。
-
批量选择后使用 HeroUI Pro ActionBar;清除选择和业务操作保留原回调。
-
分页直接使用 HeroUI compound Pagination,只展示总数、当前/总页数、上一页、页码和下一页;不展示页容量选择器。HeroUI 不规定每页条数,本系统在数据层固定为 10 条/页,不得手写页码按钮或 CSS 改写当前页样式。
-
行操作使用 HeroUI
`Button isIconOnly size="sm"`
、Dropdown、Tooltip 和 Ant Design Icons。
普通操作统一
`variant="secondary"`
,删除/驳回统一
`variant="danger-soft"`
;
按钮为 HeroUI 自带的 32px 桌面/36px 触屏正圆,图标固定 16px 并在按钮中水平、垂直居中。
-
行操作使用 HeroUI
`Button isIconOnly size="sm"`
、Dropdown、Tooltip 和 Ant Design Icons。
查看、编辑、日期、复制、锁定和更多等普通操作使用官方
`tertiary`
,保留灰色圆底和默认中性前景;审批通过、提交等明确关键操作才使用
`secondary`
的鑫元强调前景;删除/驳回统一
`danger-soft`
。
按钮为 HeroUI 自带的 32px 桌面/36px 触屏正圆,图标固定 16px 并在按钮中水平、垂直居中。
-
操作组与“操作”表头左边缘对齐,按钮间距固定 8px。1-3 个操作全部展示,4 个及以上展示前两个和一个同规格的“更多”圆形入口;操作列按实际可见圆形按钮数量计算宽度,不得将图标挤到单元格右侧。
-
不得用页面 CSS 重画操作按钮背景、边框、Hover 或危险色,也不得使用
`ghost`
作为普通行操作静止态;操作顺序、权限、禁用条件及回调保持不变。
## 7. Card、KPI、Modal 与状态
-
数字指标卡直接使用 HeroUI Pro
`KPI`
compound API:
`KPI.Header + KPI.Title`
、可选
`KPI.Actions`
、
`KPI.Content + KPI.Value + KPI.Icon`
。保留 HeroUI 默认的左侧数值/右侧 32px 图标布局,不得用私有 Card/Chip 结构将标题、数字和图标纵向居中或把图标沉底。
-
数字指标卡直接使用 HeroUI Pro
`KPI`
compound API:
`KPI.Header + KPI.Title`
、可选
`KPI.Actions`
、
`KPI.Content + KPI.Value + KPI.Icon`
。卡体组合
`rc-card-surface rc-dashboard-card`
;右侧
`KPI.Icon`
组合共享
`.rc-metric-icon-surface`
,固定 48px、16px 圆角和 20px Ant Design 图标,不得用私有 Card/Chip 结构替代。
-
HeroUI Pro 当前
`KPI.Icon status`
只覆盖
`success / warning / danger`
,鑫元共享扩展统一提供
`info / success / teal / danger / warning / violet / indigo / coral / neutral`
九组图标表面 Token。浅底只混入 9% tone(neutral 为 8%),保持轻盈,不得在页面另行加深。
-
`.rc-kpi-grid`
必须带实际
`data-count`
:2 项为两列,5/6/9 项为三列,7/8 项为四列;移动端一列、平板两列。1–9 项未指定业务 tone 时按顺序自动获得九种不重复色;有明确成功、逾期等语义时可指定 tone,但同组业务图标、前景色和浅底组合仍不得重复。超过 9 项应拆分业务模块,不循环使用色板。
-
间距按信息关系分层:同一组 KPI/紧密重复项使用
`.rc-kpi-grid`
和 16px 组内间距,普通模块/图表网格使用 24px,一级模块之间使用 32px。不通过改写
`.kpi__*`
缩放 HeroUI 内部外观。
-
普通信息面使用 HeroUI Card;数字指标使用 HeroUI Pro KPI,不手写数字卡片外观。
-
状态使用 HeroUI Chip 的官方语义色;反馈使用 Alert,加载使用 Spinner/Skeleton,空状态使用 HeroUI Pro EmptyState。
-
弹窗使用 HeroUI Modal compound API,宽度使用统一尺寸梯度;内容较多的新增表单与详情弹窗统一使用
`4xl`
(1088px),正文采用响应式布局,但不覆盖 Modal 圆角、阴影、Backdrop 和选中态。
-
同一新增/编辑弹窗内的 Input、TextArea、DateField.Group、Autocomplete 和多选控件必须共用根主题解析出的 HeroUI field variant:浅色
`primary`
、深色
`secondary`
,不得用页面私有背景、描边或阴影补色。
-
弹窗层级固定为
`Backdrop -> Modal.Dialog overlay -> secondary -> rare tertiary`
:
`Modal.Dialog`
本身保持 HeroUI
`overlay`
,Header、Body、Footer 透明承载。普通详情查看态将整组只读标签/值放入一个 HeroUI
`Surface secondary`
,字段本身不重复套 Card;对象预览、Logo well 等确有第三层边界的静态内容可使用
`tertiary`
。进入编辑态后移除整组查看 Surface,由 HeroUI
`secondary`
表单控件直接位于 Overlay,避免 L2 套 L2。输入框、按钮、弹层和 Modal 永不使用 L3。
-
查看态字段组统一使用
`rounded-2xl px-5 py-2 sm:px-6 sm:py-3`
:16px 圆角、移动/桌面 20/24px 水平内边距;字段网格从
`md`
起双列,列间距 32px、行间距 8px,字段上下 12px,标签与值间距 6px。长文本、附件、引用和对象预览跨两列。加载/错误/空状态替换该 Surface,锁定等上下文 Alert 位于 Surface 之前。
-
详情查看态的空标量或空文本字段在原字段位置显示 14px 弱化文案“暂无内容”,不能留下无文字的灰色块,也不能为单个空字段增加输入框外观。进入编辑态后,HeroUI
`Input`
/
`TextArea`
使用“请输入{可见字段名}”描述预期内容;该提示只属于
`placeholder`
,不得写入草稿、校验值或提交 payload。
-
禁止在 Modal 的 Overlay 内使用默认
`Card`
(L1)表达需要可见边界的嵌套对象,因为
`overlay`
与
`surface`
同值会形成白底叠白底;也禁止为每个只读字段增加 Card,或用页面私有
`bg-*`
、边框和阴影修补层级。
-
同一新增/编辑弹窗内的 Input、TextArea、DateField.Group、Autocomplete 和多选控件在浅色、深色中统一使用 HeroUI
`secondary`
,由
`theme.css`
解析为 L2,不得用页面私有背景、描边或阴影补色。
-
`4xl`
新增/编辑表单在桌面使用等宽双列:普通文本、数字、日期、单选和多选字段均各占一列并按数据顺序配对;只有问题描述、整改情况、备注等长文本 TextArea 跨两列。移动端统一回退单列,不得因字段为多选就默认占满整行。
-
提交约束不得作为常驻提示占据表单布局。只有用户点击确认且校验未通过时,才打开受控 HeroUI
`AlertDialog`
展示一条简短可执行提示;关闭提示后保留原表单草稿,不触发接口、不修改原校验规则。
-
详情字段保持标签在上、内容在下;长文本、附件和关联表格可跨整行,移动端回退单列。
...
...
@@ -103,9 +137,17 @@
-
数字卡片使用
`@heroui-pro/react/kpi`
。
-
饼图、柱图和组合图分别使用
`@heroui-pro/react/pie-chart`
、
`bar-chart`
、
`composed-chart`
。
-
图表 Card 主标题统一在
`Card.Header`
内左对齐,并与
`Card.Content`
的左边缘一致;不得因图表类型、单双列或有无数据而居中。分类层级、时间范围和统计口径等当前内容参数使用紧凑
`secondary`
Autocomplete,位于 Header 右侧并与标题同一行;只有真正替换并列内容的视图切换使用 HeroUI Tabs。单位属于绘图区信息,不与主标题混排。
-
所有 HeroUI Pro 图表 Hover Tooltip 都使用官方
`ChartTooltipContent`
,每个数据项必须显示 8px 颜色圆点,并与当前扇区、柱、线或图例使用完全相同的共享图表 Token。环图/单系列也不能省略圆点;进入官方 Tooltip 前统一清理
`none`
、
`transparent`
等不可见 stroke,禁止页面 CSS 手画 Tooltip 或用固定品牌色代替真实系列色。
-
HeroUI Pro/Recharts 图表保留默认
`accessibilityLayer`
和键盘导航,但鼠标点击扇区、柱、折线或绘图区不得显示浏览器蓝色 Focus 方框。所有图表根使用共享
`rc-chart-interaction`
:仅对
`:focus:not(:focus-visible)`
清除 outline,键盘
`:focus-visible`
使用
`var(--focus)`
两像素主题焦点环。不得用
`accessibilityLayer={false}`
、全局
`*:focus`
或页面私有 Recharts 选择器消除焦点。
-
Tooltip 使用 HeroUI Pro
`ChartTooltipContent`
,坐标轴、网格、标签和暗色样式由 HeroUI Pro CSS 与
`theme.css`
继承。
-
仅在 HeroUI Pro 未导出的底层图表能力上直接使用 Recharts primitive,例如
`Legend`
、
`Cell`
;不得另建私有图表主题或 Tooltip 表面。
-
图表颜色只引用
`--accent`
、
`--success`
、
`--warning`
、
`--danger`
、
`--muted`
、
`--foreground`
。
-
HeroUI Pro 图表负责容器、坐标轴、网格、文字与 Tooltip 的深浅主题适配,但不提供或自动分配业务分类系列色;
`Pie/Bar/Line`
的
`fill/stroke`
仍由应用传入。分类图表统一使用
`--rc-chart-1`
至
`--rc-chart-8`
,色板由鑫元红、钴蓝、清透青蓝、靛蓝、深青蓝、清金、紫罗兰和蓝灰组成。
-
图表系列 Token 在浅色与深色模式下保持完全相同;切换主题时只允许 HeroUI 官方的
`background/surface/muted/overlay`
等结构色变化,不能让同一业务类别因主题切换改变身份颜色。禁止把
`success`
、
`danger`
、
`warning`
、
`foreground`
直接拼成分类色板,避免荧光绿、亮粉、纯黑、高饱和橙色以及砖红/棕灰等陈旧组合。
-
有明确业务语义的图表使用
`--rc-chart-primary`
、
`--rc-chart-risk`
、
`--rc-chart-warning`
、
`--rc-chart-positive`
、
`--rc-chart-comparison`
、
`--rc-chart-neutral`
;UI 的 Alert/Chip 状态色与数据系列色分开管理。
-
同一指标跨页面、跨图表保持同色;普通分类按稳定数据 key 分配颜色,不按数值排序后的位置重新换色。同一张图必须先收集完整系列 key,通过共享分配器进行确定性冲突消解,8 个以内不得出现两个系列同色;超过 8 个时改用分组或其他视觉编码。单序列柱图默认使用
`--rc-chart-primary`
,完成率折线使用
`--rc-chart-positive`
,逾期/低质效使用
`--rc-chart-risk`
。
-
图例说明文字统一使用扩展 Token
`--rc-chart-legend-font-size: 0.75rem`
和
`--rc-chart-legend-line-height: 1rem`
。通过 Recharts
`Legend.labelStyle`
或目标图表库的公开 label slot 传入,不全局选择图表库内部 DOM;图例圆点保持 8px,文字颜色继续跟随对应系列。
-
图表布局按“同一当前可见业务分组”内的同级图表数量决定,不按整个路由页、其他 Section、隐藏 Tab 或条件分支中的图表总数决定。宽桌面中,同一分组有两个及以上可配对图表时使用
`.rc-chart-grid`
的
`repeat(2, minmax(0, 1fr))`
等宽双列,不使用
`0.9fr/1.1fr`
等主观比例;分组当前只有一个可见图表时,该 Card 必须
`grid-column: 1 / -1`
通铺,不能因页面其他分组存在图表而留下右侧空白。小于 1280px 自动回退单列。
-
统计数据、统计口径、筛选范围和图表点击业务回调不得改变;视觉迁移不制造数据。
## 9. 验收
...
...
docs/xinyuan-ui-audit.md
View file @
f53d7e42
# 问题整改系统 UI/UX 全局审计
> 2026-08-19 复核:Modal 根面统一保持 HeroUI `overlay`;应用切换项、附件编辑器、流程详情摘要等需要独立边界的弹窗内对象统一使用 HeroUI `secondary`。普通详情查看态以一个 `Surface secondary` 承载整组无框标签/值,编辑态则由 `secondary` 控件直接位于 Overlay。已清理上述范围内的 L1-on-L1 默认 Card,不改变弹窗数据、回调或状态流转;深浅模式实际层级仍待浏览器验收。
审计日期:2026-08-17
目标分支:
`codex/ui-ux-xinyuan-migration`
...
...
@@ -31,7 +33,7 @@
-
迁移前
`package.json`
声明
`1.0.0-beta.8`
,锁文件和实际镜像为
`1.0.0-beta.7`
,
`npm ls`
报 invalid。
-
Pro 包通过跨项目符号链接加载;相邻工程与当前工程各有一套物理 HeroUI/React Aria,存在 Context 双实例风险。
-
本地镜像包含入口、CSS、Sidebar、Navbar、DataGrid、DropZone、ActionBar 和 EmptyState。
-
本地
`file:`
安装仅用于迁移启动,团队交付仍需授权私有 npm 或统一
`.tgz`
。
-
迁移启动阶段曾使用本地
`file:`
镜像;该方式已退出当前交付链。当前团队统一通过 HeroUI 官方
`hpsetup`
授权流程取得精确固定的
`1.0.0-beta.8`
完整制品,仓库与开发机不再依赖相邻项目路径
。
### 主题
...
...
@@ -71,8 +73,8 @@
## 迁移后复核
-
`src/design-system/theme.css`
已
与 2026-08-18 skill 权威文件逐字节一致,SHA-256 为
`8f98b720884ded1163cfb355a235cf050f203fec85e7ded2faec87a8cf29673a
`
。
-
`tokens.css`
已收敛为 Shell/Login 别名
层,不再覆盖 HeroUI 的 Card、Tabs、DataGrid、Modal、Checkbox、Pagination、控件高度、圆角或阴影;内容组件外观由 HeroUI/HeroUI Pro CSS 与权威
`theme.css`
决定
。
-
`src/design-system/theme.css`
已
同步 2026-08-19 从 HeroUI 官方主题工具下载并批准的基础值,并在同一权威主题文件内固化三项鑫元语义映射:
`field-background = surface`
、
`default = surface-secondary`
、
`overlay = surface`
。该映射属于共享主题,不是页面私有覆盖。
`/Users/liuchenlong/Desktop/theme.css`
、项目文件与 skill 基线逐字节一致,SHA-256 为
`3ba654ba04b040316eb3f6fcf8bde18bf5d5698281477b95e860bc5e2eeb6ddf
`
。
-
`tokens.css`
已收敛为 Shell/Login 别名
、页面几何与图表数据色扩展层,不再覆盖 HeroUI 的 Card、Tabs、DataGrid、Modal、Checkbox、Pagination、控件高度、圆角或阴影;内容组件外观由 HeroUI/HeroUI Pro CSS 与权威
`theme.css`
决定。图表系列色在深浅模式中固定,HeroUI Pro 负责坐标轴、网格、文字、Tooltip 和表面的主题适配
。
-
Tabs 复核确认当前
`@heroui/styles@3.2.2`
提供
`Tabs.ListContainer`
内置
`ScrollShadow`
,但默认 Tab 的
`w-full`
会压缩或均分中文标签。活动 Tabs 已仅在官方 compound slot 的
`className`
设置
`w-fit/w-max/w-auto`
、
`min-w-max`
、
`flex-none`
和
`whitespace-nowrap`
,使每项宽度跟随自身文字;未改选中逻辑,未新增外层滚动,也未覆盖官方外观。
-
行操作复核发现共享按钮的普通状态使用
`ghost`
,导致静止态没有正圆背景;操作组同时存在右对齐、6px 间距和 128px 固定列宽,三个操作会被挤向右侧。现已改为 HeroUI 官方
`secondary`
/
`danger-soft`
,Ant Design 包装层与 SVG 固定 16px 网格居中,组间距 8px并与表头左对齐,操作列按实际入口数量动态计算。两个推送弹窗内的重复删除按钮也已接入同一共享实现,业务回调与禁用条件未变。
-
短列表复核发现共享
`DataTable`
将所有业务列转换为固定像素宽度,HeroUI DataGrid 外层虽为全宽,实际单元格宽度总和不足时仍会在右侧留下大块次表面空白。共享层现已保留 HeroUI Pro 的
`%`
/
`fr`
`ColumnSize`
,非宽表第一业务主列使用
`1fr`
,内容层使用
`min-w-full`
;宽表路径、数据、排序、选择和行操作均未改变。
...
...
src/components/AppIcons.jsx
View file @
f53d7e42
...
...
@@ -17,6 +17,7 @@ import EyeOutlined from '@ant-design/icons/EyeOutlined';
import
ExportOutlined
from
'@ant-design/icons/ExportOutlined'
;
import
FileTextOutlined
from
'@ant-design/icons/FileTextOutlined'
;
import
FolderOpenOutlined
from
'@ant-design/icons/FolderOpenOutlined'
;
import
GroupOutlined
from
'@ant-design/icons/GroupOutlined'
;
import
PaperClipOutlined
from
'@ant-design/icons/PaperClipOutlined'
;
import
LeftOutlined
from
'@ant-design/icons/LeftOutlined'
;
import
LogoutOutlined
from
'@ant-design/icons/LogoutOutlined'
;
...
...
@@ -45,6 +46,10 @@ function iconStyle(size, style) {
return
size
===
undefined
?
style
:
{
fontSize
:
size
,
...
style
};
}
export
const
FIELD_SUFFIX_ICON_STYLE
=
Object
.
freeze
({
color
:
'var(--field-placeholder, var(--muted))'
,
});
function
withSize
(
Icon
)
{
return
function
AppIcon
({
size
,
style
,
...
props
})
{
return
(
...
...
@@ -73,6 +78,7 @@ export const OpenExternal = withSize(ExportOutlined);
export
const
FileText
=
withSize
(
FileTextOutlined
);
export
const
Info
=
withSize
(
InfoCircleOutlined
);
export
const
Folder
=
withSize
(
FolderOpenOutlined
);
export
const
Group
=
withSize
(
GroupOutlined
);
export
const
LogOut
=
withSize
(
LogoutOutlined
);
export
const
Lock
=
withSize
(
LockOutlined
);
export
function
SidebarPanel
({
size
=
20
,
style
,
...
props
})
{
...
...
src/components/DashboardSidebar.jsx
View file @
f53d7e42
...
...
@@ -21,12 +21,9 @@ function SidebarUserMenu({ userInfo, onLogout }) {
return
(
<
Dropdown
>
<
Button
fullWidth
<
Dropdown
.
Trigger
aria
-
label=
"打开用户菜单"
className=
"app-sidebar-user-trigger rc-sidebar-user-trigger"
size=
"sm"
variant=
"ghost"
>
<
Avatar
className=
"app-sidebar-user-avatar rc-sidebar-avatar"
>
{
userInfo
?.
avatar
?
(
...
...
@@ -41,7 +38,7 @@ function SidebarUserMenu({ userInfo, onLogout }) {
<
span
className=
"app-sidebar-user-name"
>
{
user
.
displayName
}
</
span
>
<
span
className=
"app-sidebar-user-description"
>
{
user
.
username
}
</
span
>
</
span
>
</
Button
>
</
Dropdown
.
Trigger
>
<
Dropdown
.
Popover
placement=
"top start"
>
<
Dropdown
.
Menu
aria
-
label=
"用户菜单"
...
...
src/components/FormDatePicker.jsx
View file @
f53d7e42
import
{
Calendar
as
HeroCalendar
,
CloseButton
,
DateField
,
DatePicker
,
I18nProvider
,
...
...
@@ -7,7 +8,11 @@ import {
}
from
'@heroui/react'
;
import
{
parseDate
,
parseDateTime
,
parseTime
}
from
'@internationalized/date'
;
import
{
useMemo
}
from
'react'
;
import
{
Calendar
as
CalendarIcon
}
from
'./AppIcons'
;
import
{
Calendar
as
CalendarIcon
,
Close
,
FIELD_SUFFIX_ICON_STYLE
,
}
from
'./AppIcons'
;
function
parseValue
(
value
,
mode
)
{
if
(
!
value
)
{
...
...
@@ -91,6 +96,7 @@ export default function FormDatePicker({
className
,
disabled
=
false
,
granularity
,
isClearable
=
true
,
max
,
min
,
mode
,
...
...
@@ -123,6 +129,8 @@ export default function FormDatePicker({
const
minValue
=
useMemo
(()
=>
parseValue
(
min
,
resolvedMode
),
[
min
,
resolvedMode
]);
const
maxValue
=
useMemo
(()
=>
parseValue
(
max
,
resolvedMode
),
[
max
,
resolvedMode
]);
const
timeValue
=
useMemo
(()
=>
parseTimeValue
(
value
),
[
value
]);
const
canClear
=
isClearable
&&
Boolean
(
value
)
&&
!
disabled
&&
!
readOnly
&&
!
required
;
return
(
<
I18nProvider
locale=
"zh-CN"
>
...
...
@@ -141,16 +149,35 @@ export default function FormDatePicker({
emitDateChange
(
onChange
,
name
,
formatValue
(
nextValue
,
resolvedMode
))
}
>
<
DateField
.
Group
fullWidth
variant=
{
variant
}
>
<
DateField
.
Input
>
<
DateField
.
Group
className=
"relative"
fullWidth
variant=
{
variant
}
>
<
DateField
.
Input
className=
{
canClear
?
'pe-14'
:
'pe-8'
}
>
{
(
segment
)
=>
(
<
DateField
.
Segment
segment=
{
segment
}
/>
)
}
</
DateField
.
Input
>
<
DateField
.
Suffix
>
<
DatePicker
.
Trigger
aria
-
label=
"打开日期选择"
>
<
DateField
.
Suffix
className=
"absolute inset-y-0 end-1 mr-0 gap-0"
>
{
canClear
?
(
<
CloseButton
aria
-
label=
{
`清空${ariaLabel || '日期'}`
}
onPress=
{
()
=>
emitDateChange
(
onChange
,
name
,
''
)
}
>
<
Close
className=
"grid size-4 place-items-center leading-none [&>svg]:block [&>svg]:size-4"
size=
{
16
}
style=
{
FIELD_SUFFIX_ICON_STYLE
}
/>
</
CloseButton
>
)
:
null
}
<
DatePicker
.
Trigger
aria
-
label=
"打开日期选择"
className=
"size-6 w-6 shrink-0 justify-center p-1 text-field-placeholder"
>
<
DatePicker
.
TriggerIndicator
>
<
CalendarIcon
/>
<
CalendarIcon
className=
"grid size-4 place-items-center leading-none [&>svg]:block [&>svg]:size-4"
size=
{
16
}
style=
{
FIELD_SUFFIX_ICON_STYLE
}
/>
</
DatePicker
.
TriggerIndicator
>
</
DatePicker
.
Trigger
>
</
DateField
.
Suffix
>
...
...
src/components/ReferenceSearchSelect.jsx
View file @
f53d7e42
...
...
@@ -306,7 +306,6 @@ export default function ReferenceSearchSelect({
pageSize
=
20
,
queryFilter
,
searchFields
=
DEFAULT_SEARCH_FIELDS
,
optionsPanelClassName
=
''
,
optionsAnchor
,
showClearAction
=
true
,
selectedSummaryMode
=
'labels'
,
...
...
@@ -602,7 +601,6 @@ export default function ReferenceSearchSelect({
options=
{
menuOptions
}
placement=
{
optionsAnchor
||
'bottom start'
}
placeholder=
{
placeholder
}
popoverClassName=
{
optionsPanelClassName
}
popoverHeader=
{
allowClear
?
(
<
div
className=
"flex justify-end px-3 pb-1"
>
<
Button
size=
"sm"
variant=
"tertiary"
onPress=
{
clearSelection
}
>
...
...
src/components/TableActionButton.jsx
View file @
f53d7e42
...
...
@@ -22,6 +22,13 @@ import { Children, Fragment, isValidElement } from 'react';
const
ACTION_ICON_CLASS
=
'grid size-4 shrink-0 place-items-center [line-height:0] [&>svg]:!m-0 [&>svg]:block [&>svg]:size-4'
;
const
ACTION_BUTTON_LAYOUT_CLASS
=
'grid place-items-center rounded-full p-0'
;
const
IMPORTANT_ACTION_TONES
=
new
Set
([
'approve'
,
'submit'
]);
function
getActionVariant
(
tone
)
{
if
([
'delete'
,
'reject'
].
includes
(
tone
))
return
'danger-soft'
;
if
(
IMPORTANT_ACTION_TONES
.
has
(
tone
))
return
'secondary'
;
return
'tertiary'
;
}
function
resolveActionTone
({
icon
,
tone
})
{
if
(
tone
)
{
...
...
@@ -108,9 +115,10 @@ export function TableActionGroup({ className, children, ...props }) {
buttonVariants
({
isIconOnly
:
true
,
size
:
'sm'
,
variant
:
'
second
ary'
,
variant
:
'
terti
ary'
,
}),
ACTION_BUTTON_LAYOUT_CLASS
,
'pm-table-action'
,
)
}
title=
"更多操作"
>
...
...
@@ -184,13 +192,15 @@ export function TableActionButton({
title=
{
label
}
isDisabled=
{
isDisabled
}
size=
"sm"
variant=
{
[
'delete'
,
'reject'
].
includes
(
resolvedTone
)
?
'danger-soft'
:
'secondary'
}
variant=
{
getActionVariant
(
resolvedTone
)
}
onPress=
{
onPress
}
className=
{
clsx
(
ACTION_BUTTON_LAYOUT_CLASS
,
className
)
}
className=
{
clsx
(
ACTION_BUTTON_LAYOUT_CLASS
,
'pm-table-action'
,
IMPORTANT_ACTION_TONES
.
has
(
resolvedTone
)
&&
'pm-table-action--important'
,
[
'delete'
,
'reject'
].
includes
(
resolvedTone
)
&&
'pm-table-action--danger'
,
className
,
)
}
>
<
Icon
className=
{
ACTION_ICON_CLASS
}
...
...
src/components/XinyuanAutocomplete.jsx
View file @
f53d7e42
import
{
ChevronDown
,
Close
,
Search
}
from
'@/components/AppIcons'
;
import
{
ChevronDown
,
Close
,
FIELD_SUFFIX_ICON_STYLE
,
Search
,
}
from
'@/components/AppIcons'
;
import
{
Autocomplete
,
Label
,
...
...
@@ -39,7 +44,6 @@ export default function XinyuanAutocomplete({
options
=
[],
placeholder
=
'请选择'
,
placement
=
'bottom start'
,
popoverClassName
,
popoverHeader
,
renderOption
,
searchPlaceholder
=
'输入关键词搜索'
,
...
...
@@ -94,17 +98,21 @@ export default function XinyuanAutocomplete({
</
span
>
</
Autocomplete
.
Value
>
<
Autocomplete
.
Indicator
>
<
ChevronDown
size=
{
1
4
}
/>
<
ChevronDown
size=
{
1
6
}
style=
{
FIELD_SUFFIX_ICON_STYLE
}
/>
</
Autocomplete
.
Indicator
>
</
Autocomplete
.
Trigger
>
<
Autocomplete
.
Popover
className=
{
popoverClassName
}
placement=
{
placement
}
>
<
Autocomplete
.
Popover
placement=
{
placement
}
>
<
Autocomplete
.
Filter
filter=
{
shouldFilter
?
filter
||
defaultFilter
:
()
=>
true
}
inputValue=
{
inputValue
}
onInputChange=
{
onInputChange
}
>
{
popoverHeader
}
<
SearchField
aria
-
label=
{
searchPlaceholder
}
fullWidth
>
<
SearchField
aria
-
label=
{
searchPlaceholder
}
fullWidth
variant=
{
variant
}
>
<
SearchField
.
Group
>
<
SearchField
.
SearchIcon
>
<
Search
size=
{
16
}
/>
...
...
src/components/record-workbench/index.jsx
View file @
f53d7e42
...
...
@@ -13,8 +13,10 @@ import {
Delete
,
Eye
,
ArrowPathIcon
,
ChartBarSquareIcon
,
ChevronDownIcon
,
ChevronRightIcon
,
Group
,
MagnifyingGlassIcon
,
}
from
'@/components/AppIcons'
;
import
FilterSelect
from
'@/components/FilterSelect'
;
...
...
@@ -23,12 +25,12 @@ import { formatFieldValue, getFieldValue } from '@/utils/subjectView';
import
{
Alert
,
Button
,
Card
,
Disclosure
,
Input
,
SearchField
,
S
witch
,
S
urface
,
Tabs
,
ToggleButton
,
Tooltip
,
}
from
'@heroui/react'
;
import
clsx
from
'clsx'
;
...
...
@@ -65,35 +67,6 @@ function FilterField({ label, children }) {
);
}
function
isRootDarkTheme
()
{
if
(
typeof
document
===
'undefined'
)
{
return
false
;
}
const
root
=
document
.
documentElement
;
return
root
.
classList
.
contains
(
'dark'
)
||
root
.
dataset
.
theme
===
'dark'
;
}
function
useRootDarkTheme
()
{
const
[
isDark
,
setIsDark
]
=
useState
(
isRootDarkTheme
);
useEffect
(()
=>
{
const
root
=
document
.
documentElement
;
const
syncTheme
=
()
=>
setIsDark
(
isRootDarkTheme
());
const
observer
=
new
MutationObserver
(
syncTheme
);
syncTheme
();
observer
.
observe
(
root
,
{
attributes
:
true
,
attributeFilter
:
[
'class'
,
'data-theme'
],
});
return
()
=>
observer
.
disconnect
();
},
[]);
return
isDark
;
}
function
getPinnedSide
(
column
)
{
const
pinnedValue
=
column
?.
lockPosition
||
column
?.
pinned
;
if
(
pinnedValue
===
'left'
||
pinnedValue
===
'right'
)
{
...
...
@@ -247,22 +220,23 @@ function orderColumnsByGroups(columns = [], fieldToGroup = {}, groupById = {}) {
return
orderedColumns
;
}
function
SummaryCard
({
label
,
value
,
tone
=
'neutral'
,
icon
:
Icon
})
{
const
iconStatus
=
[
'danger'
,
'success'
,
'warning'
].
includes
(
tone
)
?
tone
:
undefined
;
function
SummaryCard
({
label
,
value
,
tone
,
icon
:
Icon
})
{
const
numericValue
=
Number
(
value
);
return
(
<
KPI
>
<
KPI
className=
"rc-card-surface rc-dashboard-card"
>
<
KPI
.
Header
>
<
KPI
.
Title
>
{
label
}
</
KPI
.
Title
>
</
KPI
.
Header
>
<
KPI
.
Content
>
<
KPI
.
Value
value=
{
Number
.
isFinite
(
numericValue
)
?
numericValue
:
0
}
/>
{
Icon
?
(
<
KPI
.
Icon
status=
{
iconStatus
}
aria
-
label=
{
label
}
>
<
Icon
/>
<
KPI
.
Icon
aria
-
label=
{
label
}
className=
"rc-metric-icon-surface"
data
-
tone=
{
tone
||
undefined
}
>
<
Icon
size=
{
20
}
/>
</
KPI
.
Icon
>
)
:
null
}
</
KPI
.
Content
>
...
...
@@ -289,7 +263,7 @@ function getActionButtonVariant(action) {
return
action
?.
variant
===
'danger'
?
'danger'
:
'danger-soft'
;
}
return
action
?.
variant
||
(
action
?.
tone
===
'primary'
?
'primary'
:
'
outline
'
);
return
action
?.
variant
||
(
action
?.
tone
===
'primary'
?
'primary'
:
'
secondary
'
);
}
function
defaultGetRecordId
(
record
)
{
...
...
@@ -357,26 +331,23 @@ function getWorkbenchColumnWidth(column, stickyMeta) {
return
Math
.
min
(
readableWidth
,
280
);
}
function
ColumnGroupControl
({
group
,
collapsed
,
onToggle
})
{
const
visible
=
!
collapsed
;
function
ColumnGroupControl
({
group
,
collapsed
,
index
,
onToggle
})
{
const
Icon
=
index
%
2
===
0
?
Group
:
ChartBarSquareIcon
;
const
actionLabel
=
`
${
collapsed
?
'显示'
:
'隐藏'
}${
group
.
name
}
列`
;
return
(
<
Tooltip
delay=
{
300
}
>
<
Switch
<
ToggleButton
size=
"sm"
isSelected=
{
visible
}
isSelected=
{
collapsed
}
onChange=
{
onToggle
}
aria
-
label=
{
`显示${group.name}列`
}
aria
-
label=
{
actionLabel
}
>
<
Switch
.
Content
>
<
Switch
.
Control
>
<
Switch
.
Thumb
/>
</
Switch
.
Control
>
{
group
.
name
}
</
Switch
.
Content
>
</
Switch
>
<
Icon
size=
{
16
}
/>
<
span
>
{
group
.
name
}
</
span
>
</
ToggleButton
>
<
Tooltip
.
Content
>
显示或隐藏“
{
group
.
name
}
”的
{
group
.
children
.
length
}
个列表字段,不会筛选或修改数据
{
actionLabel
}
(共
{
group
.
children
.
length
}
个字段)
</
Tooltip
.
Content
>
</
Tooltip
>
);
...
...
@@ -473,8 +444,7 @@ export default function RecordWorkbench({
const
[
collapsedColumnGroups
,
setCollapsedColumnGroups
]
=
useState
({});
const
[
columnGroupAnnouncement
,
setColumnGroupAnnouncement
]
=
useState
(
''
);
const
[
expandedRowIds
,
setExpandedRowIds
]
=
useState
({});
const
isDarkTheme
=
useRootDarkTheme
();
const
filterFieldVariant
=
isDarkTheme
?
'secondary'
:
'primary'
;
const
filterFieldVariant
=
'secondary'
;
const
loading
=
Boolean
(
states
?.
loading
);
const
error
=
states
?.
error
||
''
;
const
totalRecords
=
pagination
?.
totalRecords
??
records
.
length
;
...
...
@@ -841,16 +811,6 @@ export default function RecordWorkbench({
const disabledDataGridRowKeys = dataGridRows
.filter((row) => row.isExpandedRecord || row.isExpandedPanel)
.map((row) => String(row.key));
const summaryGridClass =
summaryItems.length === 1
? 'md:grid-cols-1'
: summaryItems.length === 2
? 'md:grid-cols-2'
: summaryItems.length === 3
? 'md:grid-cols-3'
: summaryItems.length === 5 || summaryItems.length === 6
? 'md:grid-cols-2 xl:grid-cols-3'
: 'md:grid-cols-2 xl:grid-cols-4';
const toolbarSearchField = filterSchema.find((field) => field.icon === 'search');
const structuredFilterFields = filterSchema.filter(
(field) => field !== toolbarSearchField,
...
...
@@ -906,7 +866,7 @@ export default function RecordWorkbench({
return (
<div className="rc-page-stack">
{summaryItems.length > 0 ? (
<section className=
{clsx('rc-kpi-grid grid', summaryGridClass)
}>
<section className=
"rc-kpi-grid" data-count={summaryItems.length
}>
{summaryItems.map((item) => (
<SummaryCard
key={item.label}
...
...
@@ -945,8 +905,10 @@ export default function RecordWorkbench({
) : null}
{structuredFilterFields.length > 0 && !useInlineFilters ? (
<Card>
<Card.Content>
<Surface
className="rounded-3xl p-6"
variant="default"
>
<Disclosure
isExpanded={filtersExpanded}
onExpandedChange={setFiltersExpanded}
...
...
@@ -1013,8 +975,7 @@ export default function RecordWorkbench({
</div>
</form>
</Disclosure>
</Card.Content>
</Card>
</Surface>
) : null}
<section className={tableSectionClass}>
...
...
@@ -1053,16 +1014,19 @@ export default function RecordWorkbench({
</form>
) : null}
<div className="rc-page-toolbar">
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-3">
<div className="flex min-w-0 items-center">
<h2 className="text-base font-medium text-foreground">{title}</h2>
</div>
<div className="rc-page-toolbar">
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-2">
{toolbarSearchField ? (
<div className="w-full min-w-0 sm:w-80 lg:w-96">
<div className="w-full min-w-0
flex-none
sm:w-80 lg:w-96">
<SearchField
aria-label={toolbarSearchField.label || '关键词搜索'}
fullWidth
value={filters[toolbarSearchField.key] || ''}
variant="
second
ary"
variant="
prim
ary"
onChange={(value) =>
onFiltersChange?.(toolbarSearchField.key, value)
}
...
...
@@ -1080,12 +1044,13 @@ export default function RecordWorkbench({
</div>
) : null}
{hasColumnGroups ? (
<div className="flex flex-wrap items-center gap-
3
" aria-label="列分组显示控制">
{columnGroupMeta.groups.map((group) => (
<div className="flex flex-wrap items-center gap-
2
" aria-label="列分组显示控制">
{columnGroupMeta.groups.map((group
, index
) => (
<ColumnGroupControl
key={group.id}
group={group}
collapsed={Boolean(collapsedColumnGroups[group.id])}
index={index}
onToggle={() => toggleColumnGroup(group.id)}
/>
))}
...
...
src/design-system/chartPalette.js
0 → 100644
View file @
f53d7e42
export
const
XINYUAN_CHART_PALETTE
=
Object
.
freeze
([
'var(--rc-chart-1)'
,
'var(--rc-chart-2)'
,
'var(--rc-chart-3)'
,
'var(--rc-chart-4)'
,
'var(--rc-chart-5)'
,
'var(--rc-chart-6)'
,
'var(--rc-chart-7)'
,
'var(--rc-chart-8)'
,
]);
export
const
XINYUAN_CHART_SEMANTIC
=
Object
.
freeze
({
primary
:
'var(--rc-chart-primary)'
,
risk
:
'var(--rc-chart-risk)'
,
warning
:
'var(--rc-chart-warning)'
,
positive
:
'var(--rc-chart-positive)'
,
comparison
:
'var(--rc-chart-comparison)'
,
neutral
:
'var(--rc-chart-neutral)'
,
});
function
normalizeChartKey
(
key
)
{
return
String
(
key
??
''
).
trim
();
}
function
hashChartKey
(
key
)
{
return
Array
.
from
(
key
).
reduce
(
(
value
,
character
)
=>
((
value
*
31
)
+
character
.
codePointAt
(
0
))
>>>
0
,
0
,
);
}
export
function
getXinyuanChartColor
(
key
,
fallbackIndex
=
0
)
{
const
stableKey
=
normalizeChartKey
(
key
);
if
(
!
stableKey
)
{
return
XINYUAN_CHART_PALETTE
[
fallbackIndex
%
XINYUAN_CHART_PALETTE
.
length
];
}
return
XINYUAN_CHART_PALETTE
[
hashChartKey
(
stableKey
)
%
XINYUAN_CHART_PALETTE
.
length
];
}
export
function
createXinyuanChartColorMap
(
keys
=
[])
{
const
stableKeys
=
[...
new
Set
(
keys
.
map
(
normalizeChartKey
).
filter
(
Boolean
))]
.
sort
();
const
colorMap
=
new
Map
();
const
usedIndexes
=
new
Set
();
stableKeys
.
forEach
((
key
,
keyIndex
)
=>
{
const
preferredIndex
=
hashChartKey
(
key
)
%
XINYUAN_CHART_PALETTE
.
length
;
let
paletteIndex
=
preferredIndex
;
if
(
keyIndex
<
XINYUAN_CHART_PALETTE
.
length
)
{
for
(
let
offset
=
0
;
offset
<
XINYUAN_CHART_PALETTE
.
length
;
offset
+=
1
)
{
const
candidateIndex
=
(
preferredIndex
+
offset
)
%
XINYUAN_CHART_PALETTE
.
length
;
if
(
!
usedIndexes
.
has
(
candidateIndex
))
{
paletteIndex
=
candidateIndex
;
usedIndexes
.
add
(
candidateIndex
);
break
;
}
}
}
colorMap
.
set
(
key
,
XINYUAN_CHART_PALETTE
[
paletteIndex
]);
});
return
colorMap
;
}
function
isVisibleChartColor
(
color
)
{
if
(
typeof
color
!==
'string'
)
{
return
false
;
}
const
normalizedColor
=
color
.
trim
().
toLowerCase
();
return
Boolean
(
normalizedColor
)
&&
normalizedColor
!==
'none'
&&
normalizedColor
!==
'transparent'
;
}
export
function
colorizeXinyuanChartTooltipPayload
(
payload
=
[],
resolveColor
,
)
{
return
payload
.
map
((
entry
,
index
)
=>
{
const
rechartsPayload
=
entry
?.
payload
||
{};
const
sourceDatum
=
rechartsPayload
?.
payload
||
rechartsPayload
;
const
key
=
sourceDatum
?.
name
??
rechartsPayload
?.
name
??
entry
?.
name
??
entry
?.
dataKey
??
''
;
const
candidateColors
=
[
resolveColor
?.({
entry
,
index
,
key
,
sourceDatum
}),
rechartsPayload
?.
fill
,
sourceDatum
?.
fill
,
entry
?.
fill
,
entry
?.
color
,
entry
?.
stroke
,
getXinyuanChartColor
(
key
,
index
),
];
const
color
=
candidateColors
.
find
(
isVisibleChartColor
);
return
{
...
entry
,
color
,
fill
:
color
,
name
:
entry
?.
name
??
sourceDatum
?.
name
??
entry
?.
dataKey
,
stroke
:
color
,
};
});
}
src/design-system/shell.css
View file @
f53d7e42
...
...
@@ -440,11 +440,202 @@
gap
:
var
(
--rc-page-module-gap
);
}
.rc-chart-grid
{
display
:
grid
;
grid-template-columns
:
minmax
(
0
,
1
fr
);
}
.rc-chart-grid
>
*
{
min-width
:
0
;
}
.rc-chart-grid
>
:only-child
{
grid-column
:
1
/
-1
;
}
/* Recharts keeps keyboard navigation while pointer focus stays visually quiet. */
.rc-chart-interaction
:focus:not
(
:focus-visible
),
.rc-chart-interaction
:focus:not
(
:focus-visible
)
{
outline
:
none
;
}
.rc-chart-interaction
:focus-visible
,
.rc-chart-interaction
:focus-visible
{
outline
:
2px
solid
var
(
--focus
);
outline-offset
:
2px
;
}
.rc-kpi-grid
{
display
:
grid
;
grid-template-columns
:
minmax
(
0
,
1
fr
);
min-width
:
0
;
gap
:
var
(
--rc-related-item-gap
);
}
@media
(
min-width
:
48rem
)
{
.rc-kpi-grid
:not
([
data-count
=
'1'
])
{
grid-template-columns
:
repeat
(
2
,
minmax
(
0
,
1
fr
));
}
}
@media
(
min-width
:
80rem
)
{
.rc-kpi-grid
[
data-count
=
'1'
]
{
grid-template-columns
:
minmax
(
0
,
1
fr
);
}
.rc-kpi-grid
[
data-count
=
'2'
]
{
grid-template-columns
:
repeat
(
2
,
minmax
(
0
,
1
fr
));
}
.rc-kpi-grid
[
data-count
=
'3'
],
.rc-kpi-grid
[
data-count
=
'5'
],
.rc-kpi-grid
[
data-count
=
'6'
],
.rc-kpi-grid
[
data-count
=
'9'
]
{
grid-template-columns
:
repeat
(
3
,
minmax
(
0
,
1
fr
));
}
.rc-kpi-grid
[
data-count
=
'4'
],
.rc-kpi-grid
[
data-count
=
'7'
],
.rc-kpi-grid
[
data-count
=
'8'
]
{
grid-template-columns
:
repeat
(
4
,
minmax
(
0
,
1
fr
));
}
}
/* Shared HeroUI/HeroUI Pro business-card contract. */
.rc-card-surface
{
border
:
1px
solid
var
(
--rc-border-default
);
background-color
:
var
(
--rc-bg-surface
);
color
:
var
(
--rc-text-primary
);
box-shadow
:
var
(
--rc-shadow-card
);
}
.rc-dashboard-card
{
border-radius
:
1.5rem
;
}
/* HeroUI Pro KPI.Icon keeps its compound role; this class supplies Xinyuan tones. */
.rc-metric-icon-surface
{
--rc-metric-icon-color
:
var
(
--rc-metric-tone-neutral
);
--rc-metric-icon-background
:
var
(
--rc-metric-tone-neutral-soft
);
display
:
grid
;
width
:
3rem
;
min-width
:
3rem
;
max-width
:
3rem
;
height
:
3rem
;
min-height
:
3rem
;
max-height
:
3rem
;
flex
:
0
0
3rem
;
place-items
:
center
;
border-radius
:
1rem
;
color
:
var
(
--rc-metric-icon-color
);
background-color
:
var
(
--rc-metric-icon-background
);
line-height
:
1
;
}
.rc-metric-icon-surface
[
data-tone
=
'info'
]
{
--rc-metric-icon-color
:
var
(
--rc-metric-tone-info
);
--rc-metric-icon-background
:
var
(
--rc-metric-tone-info-soft
);
}
.rc-metric-icon-surface
[
data-tone
=
'success'
]
{
--rc-metric-icon-color
:
var
(
--rc-metric-tone-success
);
--rc-metric-icon-background
:
var
(
--rc-metric-tone-success-soft
);
}
.rc-metric-icon-surface
[
data-tone
=
'teal'
]
{
--rc-metric-icon-color
:
var
(
--rc-metric-tone-teal
);
--rc-metric-icon-background
:
var
(
--rc-metric-tone-teal-soft
);
}
.rc-metric-icon-surface
[
data-tone
=
'danger'
]
{
--rc-metric-icon-color
:
var
(
--rc-metric-tone-danger
);
--rc-metric-icon-background
:
var
(
--rc-metric-tone-danger-soft
);
}
.rc-metric-icon-surface
[
data-tone
=
'warning'
]
{
--rc-metric-icon-color
:
var
(
--rc-metric-tone-warning
);
--rc-metric-icon-background
:
var
(
--rc-metric-tone-warning-soft
);
}
.rc-metric-icon-surface
[
data-tone
=
'violet'
]
{
--rc-metric-icon-color
:
var
(
--rc-metric-tone-violet
);
--rc-metric-icon-background
:
var
(
--rc-metric-tone-violet-soft
);
}
.rc-metric-icon-surface
[
data-tone
=
'indigo'
]
{
--rc-metric-icon-color
:
var
(
--rc-metric-tone-indigo
);
--rc-metric-icon-background
:
var
(
--rc-metric-tone-indigo-soft
);
}
.rc-metric-icon-surface
[
data-tone
=
'coral'
]
{
--rc-metric-icon-color
:
var
(
--rc-metric-tone-coral
);
--rc-metric-icon-background
:
var
(
--rc-metric-tone-coral-soft
);
}
/* Untoned KPI items receive a unique ordered tone for boards of up to nine. */
.rc-kpi-grid
>
:nth-child
(
1
)
.rc-metric-icon-surface
:not
([
data-tone
])
{
--rc-metric-icon-color
:
var
(
--rc-metric-tone-info
);
--rc-metric-icon-background
:
var
(
--rc-metric-tone-info-soft
);
}
.rc-kpi-grid
>
:nth-child
(
2
)
.rc-metric-icon-surface
:not
([
data-tone
])
{
--rc-metric-icon-color
:
var
(
--rc-metric-tone-success
);
--rc-metric-icon-background
:
var
(
--rc-metric-tone-success-soft
);
}
.rc-kpi-grid
>
:nth-child
(
3
)
.rc-metric-icon-surface
:not
([
data-tone
])
{
--rc-metric-icon-color
:
var
(
--rc-metric-tone-warning
);
--rc-metric-icon-background
:
var
(
--rc-metric-tone-warning-soft
);
}
.rc-kpi-grid
>
:nth-child
(
4
)
.rc-metric-icon-surface
:not
([
data-tone
])
{
--rc-metric-icon-color
:
var
(
--rc-metric-tone-violet
);
--rc-metric-icon-background
:
var
(
--rc-metric-tone-violet-soft
);
}
.rc-kpi-grid
>
:nth-child
(
5
)
.rc-metric-icon-surface
:not
([
data-tone
])
{
--rc-metric-icon-color
:
var
(
--rc-metric-tone-danger
);
--rc-metric-icon-background
:
var
(
--rc-metric-tone-danger-soft
);
}
.rc-kpi-grid
>
:nth-child
(
6
)
.rc-metric-icon-surface
:not
([
data-tone
])
{
--rc-metric-icon-color
:
var
(
--rc-metric-tone-teal
);
--rc-metric-icon-background
:
var
(
--rc-metric-tone-teal-soft
);
}
.rc-kpi-grid
>
:nth-child
(
7
)
.rc-metric-icon-surface
:not
([
data-tone
])
{
--rc-metric-icon-color
:
var
(
--rc-metric-tone-indigo
);
--rc-metric-icon-background
:
var
(
--rc-metric-tone-indigo-soft
);
}
.rc-kpi-grid
>
:nth-child
(
8
)
.rc-metric-icon-surface
:not
([
data-tone
])
{
--rc-metric-icon-color
:
var
(
--rc-metric-tone-coral
);
--rc-metric-icon-background
:
var
(
--rc-metric-tone-coral-soft
);
}
.rc-kpi-grid
>
:nth-child
(
9
)
.rc-metric-icon-surface
:not
([
data-tone
])
{
--rc-metric-icon-color
:
var
(
--rc-metric-tone-neutral
);
--rc-metric-icon-background
:
var
(
--rc-metric-tone-neutral-soft
);
}
.rc-metric-icon-surface
>
.anticon
,
.rc-metric-icon-surface
>
svg
{
display
:
block
;
width
:
1.25rem
;
height
:
1.25rem
;
margin
:
0
;
color
:
inherit
;
font-size
:
1.25rem
;
line-height
:
1
;
}
.rc-metric-icon-surface
>
.anticon
>
svg
{
display
:
block
;
width
:
1em
;
height
:
1em
;
margin
:
0
;
}
.rc-page-toolbar
{
display
:
flex
;
min-width
:
0
;
...
...
@@ -464,6 +655,12 @@
gap
:
var
(
--rc-list-stack-gap
);
}
@media
(
min-width
:
80rem
)
{
.rc-chart-grid
{
grid-template-columns
:
repeat
(
2
,
minmax
(
0
,
1
fr
));
}
}
.rc-mobile-sidebar-backdrop
{
position
:
fixed
;
inset
:
0
;
...
...
@@ -728,7 +925,7 @@
padding
:
var
(
--rc-space-2
)
var
(
--rc-space-3
)
var
(
--rc-space-4
);
}
.rc-sidebar-user-trigger.
button
{
.rc-sidebar-user-trigger.
dropdown__trigger
{
display
:
flex
;
width
:
100%
;
min-width
:
0
;
...
...
@@ -743,9 +940,9 @@
text-align
:
left
;
}
.rc-sidebar-user-trigger.
button
:hover
,
.rc-sidebar-user-trigger.
button
[
data-hovered
=
'true'
],
.rc-sidebar-user-trigger.
button
[
aria-expanded
=
'true'
]
{
.rc-sidebar-user-trigger.
dropdown__trigger
:hover
,
.rc-sidebar-user-trigger.
dropdown__trigger
[
data-hovered
=
'true'
],
.rc-sidebar-user-trigger.
dropdown__trigger
[
aria-expanded
=
'true'
]
{
background
:
var
(
--default
);
}
...
...
@@ -1134,7 +1331,7 @@
padding
:
0.5rem
0.75rem
1rem
;
}
.app-sidebar-user-trigger.
button
{
.app-sidebar-user-trigger.
dropdown__trigger
{
display
:
flex
;
width
:
100%
;
min-width
:
0
;
...
...
@@ -1150,13 +1347,13 @@
}
@media
(
hover
:
hover
)
{
.app-sidebar-user-trigger.
button
:hover
,
.app-sidebar-user-trigger.
button
[
data-hovered
=
'true'
]
{
.app-sidebar-user-trigger.
dropdown__trigger
:hover
,
.app-sidebar-user-trigger.
dropdown__trigger
[
data-hovered
=
'true'
]
{
background
:
var
(
--default
);
}
}
.app-sidebar-user-trigger.
button
[
aria-expanded
=
'true'
]
{
.app-sidebar-user-trigger.
dropdown__trigger
[
aria-expanded
=
'true'
]
{
background
:
var
(
--default
);
}
...
...
src/design-system/theme.css
View file @
f53d7e42
...
...
@@ -13,34 +13,35 @@
/* Theme Colors (Light Mode) */
--accent
:
oklch
(
45.32%
0.1661
27.03
);
--accent-foreground
:
oklch
(
99.11%
0
0
);
--background
:
oklch
(
97.02%
0.00
19
27.03
);
--border
:
oklch
(
90.00%
0.00
19
27.03
);
--danger
:
oklch
(
65.32%
0.2337
358.52
);
--background
:
oklch
(
97.02%
0.00
45
27.03
);
--border
:
oklch
(
90.00%
0.00
45
27.03
);
--danger
:
oklch
(
45.32%
0.1661
27.03
);
--danger-foreground
:
oklch
(
99.11%
0
0
);
--default
:
oklch
(
94.00%
0.0019
27.03
);
--default-foreground
:
oklch
(
21.03%
0.0059
27.03
);
--field-background
:
oklch
(
100.00%
0.0009
27.03
);
/* HeroUI control aliases: primary = L1, secondary = L2. */
--default
:
var
(
--surface-secondary
);
--default-foreground
:
var
(
--surface-secondary-foreground
);
--field-background
:
var
(
--surface
);
--field-border
:
transparent
;
--field-foreground
:
oklch
(
21.03%
0.0019
27.03
);
--field-placeholder
:
oklch
(
55.17%
0.00
38
27.03
);
--field-foreground
:
var
(
--surface-foreground
);
--field-placeholder
:
oklch
(
55.17%
0.00
90
27.03
);
--focus
:
oklch
(
45.32%
0.1661
27.03
);
--foreground
:
oklch
(
21.03%
0.00
19
27.03
);
--muted
:
oklch
(
55.17%
0.00
38
27.03
);
--overlay
:
oklch
(
100.00%
0.0006
27.03
);
--overlay-foreground
:
oklch
(
21.03%
0.0019
27.03
);
--scrollbar
:
oklch
(
87.10%
0.00
19
27.03
);
--segment
:
oklch
(
100.00%
0.00
19
27.03
);
--segment-foreground
:
oklch
(
21.03%
0.00
19
27.03
);
--separator
:
oklch
(
92.00%
0.00
19
27.03
);
--success
:
oklch
(
73.29%
0.19
4
2
123.59
);
--foreground
:
oklch
(
21.03%
0.00
45
27.03
);
--muted
:
oklch
(
55.17%
0.00
90
27.03
);
--overlay
:
var
(
--surface
);
--overlay-foreground
:
var
(
--surface-foreground
);
--scrollbar
:
oklch
(
87.10%
0.00
45
27.03
);
--segment
:
oklch
(
100.00%
0.00
45
27.03
);
--segment-foreground
:
oklch
(
21.03%
0.00
45
27.03
);
--separator
:
oklch
(
92.00%
0.00
45
27.03
);
--success
:
oklch
(
73.29%
0.19
5
2
123.59
);
--success-foreground
:
oklch
(
21.03%
0.0059
123.59
);
--surface
:
oklch
(
100.00%
0.00
09
27.03
);
--surface-foreground
:
oklch
(
21.03%
0.00
19
27.03
);
--surface-secondary
:
oklch
(
95.24%
0.00
15
27.03
);
--surface-secondary-foreground
:
oklch
(
21.03%
0.00
19
27.03
);
--surface-tertiary
:
oklch
(
93.73%
0.00
15
27.03
);
--surface-tertiary-foreground
:
oklch
(
21.03%
0.00
19
27.03
);
--warning
:
oklch
(
78.19%
0.159
1
45.11
);
--surface
:
oklch
(
100.00%
0.00
22
27.03
);
--surface-foreground
:
oklch
(
21.03%
0.00
45
27.03
);
--surface-secondary
:
oklch
(
95.24%
0.00
36
27.03
);
--surface-secondary-foreground
:
oklch
(
21.03%
0.00
45
27.03
);
--surface-tertiary
:
oklch
(
93.73%
0.00
36
27.03
);
--surface-tertiary-foreground
:
oklch
(
21.03%
0.00
45
27.03
);
--warning
:
oklch
(
78.19%
0.159
9
45.11
);
--warning-foreground
:
oklch
(
21.03%
0.0059
45.11
);
/* Border Radius */
...
...
@@ -58,33 +59,34 @@
/* Theme Colors (Dark Mode) */
--accent
:
oklch
(
45.32%
0.1661
27.03
);
--accent-foreground
:
oklch
(
99.11%
0
0
);
--background
:
oklch
(
12.00%
0.00
19
27.03
);
--border
:
oklch
(
28.00%
0.00
19
27.03
);
--danger
:
oklch
(
59.40%
0.1974
357.41
);
--background
:
oklch
(
12.00%
0.00
45
27.03
);
--border
:
oklch
(
28.00%
0.00
45
27.03
);
--danger
:
oklch
(
62.00%
0.1600
27.03
);
--danger-foreground
:
oklch
(
99.11%
0
0
);
--default
:
oklch
(
27.40%
0.0019
27.03
);
--default-foreground
:
oklch
(
99.11%
0
0
);
--field-background
:
oklch
(
21.03%
0.0038
27.03
);
/* HeroUI control aliases: primary = L1, secondary = L2. */
--default
:
var
(
--surface-secondary
);
--default-foreground
:
var
(
--surface-secondary-foreground
);
--field-background
:
var
(
--surface
);
--field-border
:
transparent
;
--field-foreground
:
oklch
(
99.11%
0.0019
27.03
);
--field-placeholder
:
oklch
(
70.50%
0.00
38
27.03
);
--field-foreground
:
var
(
--surface-foreground
);
--field-placeholder
:
oklch
(
70.50%
0.00
90
27.03
);
--focus
:
oklch
(
45.32%
0.1661
27.03
);
--foreground
:
oklch
(
99.11%
0.00
19
27.03
);
--muted
:
oklch
(
70.50%
0.00
38
27.03
);
--overlay
:
oklch
(
21.03%
0.0038
27.03
);
--overlay-foreground
:
oklch
(
99.11%
0.0019
27.03
);
--scrollbar
:
oklch
(
70.50%
0.00
19
27.03
);
--segment
:
oklch
(
39.64%
0.00
19
27.03
);
--segment-foreground
:
oklch
(
99.11%
0.00
19
27.03
);
--separator
:
oklch
(
25.00%
0.00
19
27.03
);
--success
:
oklch
(
73.29%
0.19
4
2
123.59
);
--foreground
:
oklch
(
99.11%
0.00
45
27.03
);
--muted
:
oklch
(
70.50%
0.00
90
27.03
);
--overlay
:
var
(
--surface
);
--overlay-foreground
:
var
(
--surface-foreground
);
--scrollbar
:
oklch
(
70.50%
0.00
45
27.03
);
--segment
:
oklch
(
39.64%
0.00
45
27.03
);
--segment-foreground
:
oklch
(
99.11%
0.00
45
27.03
);
--separator
:
oklch
(
25.00%
0.00
45
27.03
);
--success
:
oklch
(
73.29%
0.19
5
2
123.59
);
--success-foreground
:
oklch
(
21.03%
0.0059
123.59
);
--surface
:
oklch
(
21.03%
0.00
38
27.03
);
--surface-foreground
:
oklch
(
99.11%
0.00
19
27.03
);
--surface-secondary
:
oklch
(
25.70%
0.00
29
27.03
);
--surface-secondary-foreground
:
oklch
(
99.11%
0.00
19
27.03
);
--surface-tertiary
:
oklch
(
27.21%
0.00
29
27.03
);
--surface-tertiary-foreground
:
oklch
(
99.11%
0.00
19
27.03
);
--warning
:
oklch
(
82.03%
0.1
393
49.12
);
--surface
:
oklch
(
21.03%
0.00
90
27.03
);
--surface-foreground
:
oklch
(
99.11%
0.00
45
27.03
);
--surface-secondary
:
oklch
(
25.70%
0.00
67
27.03
);
--surface-secondary-foreground
:
oklch
(
99.11%
0.00
45
27.03
);
--surface-tertiary
:
oklch
(
27.21%
0.00
67
27.03
);
--surface-tertiary-foreground
:
oklch
(
99.11%
0.00
45
27.03
);
--warning
:
oklch
(
82.03%
0.1
400
49.12
);
--warning-foreground
:
oklch
(
21.03%
0.0059
49.12
);
}
src/design-system/tokens.css
View file @
f53d7e42
...
...
@@ -5,7 +5,7 @@
[
data-theme
=
'default'
],
.dark
,
[
data-theme
=
'dark'
]
{
/*
Shell aliases only. HeroUI component
tokens are owned by theme.css. */
/*
Xinyuan aliases and extension tokens. HeroUI core
tokens are owned by theme.css. */
--rc-brand-primary
:
var
(
--accent
);
--rc-brand-light
:
color-mix
(
in
oklab
,
var
(
--accent
)
84%
,
white
);
--rc-brand-dark
:
color-mix
(
in
oklab
,
var
(
--accent
)
82%
,
black
);
...
...
@@ -78,6 +78,82 @@
--rc-shadow-raised
:
0
1px
2px
rgb
(
15
23
42
/
6%
),
0
8px
24px
rgb
(
15
23
42
/
8%
);
--rc-shadow-overlay
:
0
16px
40px
rgb
(
15
23
42
/
14%
);
--rc-shadow-card
:
0
1px
2px
rgb
(
15
23
42
/
2%
),
0
4px
12px
rgb
(
15
23
42
/
2%
);
/* KPI icon surfaces. Card bodies remain neutral; only the icon well carries tone. */
--rc-metric-tone-info
:
oklch
(
55%
0.14
255
);
--rc-metric-tone-success
:
oklch
(
54%
0.13
145
);
--rc-metric-tone-teal
:
oklch
(
55%
0.1
205
);
--rc-metric-tone-danger
:
oklch
(
52%
0.17
25
);
--rc-metric-tone-warning
:
oklch
(
62%
0.12
75
);
--rc-metric-tone-violet
:
oklch
(
56%
0.12
300
);
--rc-metric-tone-indigo
:
oklch
(
53%
0.13
275
);
--rc-metric-tone-coral
:
oklch
(
60%
0.12
48
);
--rc-metric-tone-neutral
:
var
(
--muted
);
--rc-metric-tone-info-soft
:
color-mix
(
in
oklab
,
var
(
--rc-metric-tone-info
)
9%
,
var
(
--surface
)
);
--rc-metric-tone-success-soft
:
color-mix
(
in
oklab
,
var
(
--rc-metric-tone-success
)
9%
,
var
(
--surface
)
);
--rc-metric-tone-teal-soft
:
color-mix
(
in
oklab
,
var
(
--rc-metric-tone-teal
)
9%
,
var
(
--surface
)
);
--rc-metric-tone-danger-soft
:
color-mix
(
in
oklab
,
var
(
--rc-metric-tone-danger
)
9%
,
var
(
--surface
)
);
--rc-metric-tone-warning-soft
:
color-mix
(
in
oklab
,
var
(
--rc-metric-tone-warning
)
9%
,
var
(
--surface
)
);
--rc-metric-tone-violet-soft
:
color-mix
(
in
oklab
,
var
(
--rc-metric-tone-violet
)
9%
,
var
(
--surface
)
);
--rc-metric-tone-indigo-soft
:
color-mix
(
in
oklab
,
var
(
--rc-metric-tone-indigo
)
9%
,
var
(
--surface
)
);
--rc-metric-tone-coral-soft
:
color-mix
(
in
oklab
,
var
(
--rc-metric-tone-coral
)
9%
,
var
(
--surface
)
);
--rc-metric-tone-neutral-soft
:
color-mix
(
in
oklab
,
var
(
--rc-metric-tone-neutral
)
8%
,
var
(
--surface
)
);
/* Data visualization only. Do not reuse UI status colors as a series palette. */
--rc-chart-1
:
oklch
(
45.32%
0.1661
27.03
);
--rc-chart-2
:
oklch
(
56%
0.135
255
);
--rc-chart-3
:
oklch
(
64%
0.11
220
);
--rc-chart-4
:
oklch
(
56%
0.115
280
);
--rc-chart-5
:
oklch
(
60%
0.095
200
);
--rc-chart-6
:
oklch
(
70%
0.12
80
);
--rc-chart-7
:
oklch
(
60%
0.1
295
);
--rc-chart-8
:
oklch
(
57%
0.035
250
);
--rc-chart-primary
:
var
(
--rc-chart-1
);
--rc-chart-risk
:
oklch
(
47%
0.145
27.03
);
--rc-chart-warning
:
var
(
--rc-chart-6
);
--rc-chart-positive
:
var
(
--rc-chart-2
);
--rc-chart-comparison
:
var
(
--rc-chart-4
);
--rc-chart-neutral
:
var
(
--rc-chart-8
);
--rc-chart-legend-font-size
:
0.75rem
;
--rc-chart-legend-line-height
:
1rem
;
}
.dark
,
...
...
@@ -96,4 +172,6 @@
--rc-shadow-raised
:
0
1px
2px
rgb
(
0
0
0
/
24%
),
0
8px
24px
rgb
(
0
0
0
/
24%
);
--rc-shadow-overlay
:
0
16px
40px
rgb
(
0
0
0
/
38%
);
--rc-shadow-card
:
0
1px
2px
rgb
(
0
0
0
/
20%
),
0
4px
12px
rgb
(
0
0
0
/
18%
);
}
src/pages/RecordDetailDrawer/components/SubjectViewContent.jsx
View file @
f53d7e42
import
clsx
from
'clsx'
;
import
{
EmptyState
}
from
'@heroui-pro/react/empty-state'
;
import
useRootDarkTheme
from
'@/hooks/useRootDarkTheme
'
;
import
{
Surface
}
from
'@heroui/react
'
;
import
TailwindEditableField
from
'./TailwindEditableField'
;
import
{
buildViewColumns
,
...
...
@@ -62,7 +62,7 @@ export default function SubjectViewContent({
const
contentFields
=
minimal
?
(
displayFields
||
[]).
filter
((
field
)
=>
field
.
field
!==
'title'
)
:
displayFields
;
const
fieldVariant
=
useRootDarkTheme
()
?
'secondary'
:
'prim
ary'
;
const
fieldVariant
=
'second
ary'
;
if
(
!
contentFields
||
contentFields
.
length
===
0
)
{
if
(
minimal
)
{
...
...
@@ -76,13 +76,14 @@ export default function SubjectViewContent({
);
}
return
(
const
fields
=
(
<
section
className=
"grid min-w-0 grid-cols-1 gap-x-8 gap-y-2 md:grid-cols-2"
>
{
contentFields
.
map
((
field
)
=>
{
const
value
=
getFieldValue
(
editableRecord
,
field
.
field
);
const
fieldType
=
field
.
fieldDef
?.
type
||
'text'
;
const
fieldLabel
=
getFieldLabel
(
field
);
const
isFullWidth
=
FULL_WIDTH_FIELD_TYPES
.
has
(
fieldType
);
const
fieldDisabled
=
isFieldDisabled
?.(
field
.
field
,
view
)
||
...
...
@@ -98,7 +99,7 @@ export default function SubjectViewContent({
)
}
>
<
div
className=
{
minimal
?
'sr-only'
:
'text-sm font-medium text-muted'
}
>
{
getFieldLabel
(
field
)
}
{
fieldLabel
}
</
div
>
<
div
className=
{
minimal
?
undefined
:
'mt-1.5 min-w-0'
}
>
<
TailwindEditableField
...
...
@@ -107,6 +108,7 @@ export default function SubjectViewContent({
fieldPath=
{
field
.
field
}
editSessionId=
{
editSessionId
}
fieldVariant=
{
fieldVariant
}
placeholder=
{
`请输入${fieldLabel}`
}
mode=
{
mode
}
onChange=
{
(
newValue
)
=>
onFieldChange
?.(
...
...
@@ -126,4 +128,14 @@ export default function SubjectViewContent({
})
}
</
section
>
);
if
(
mode
===
'view'
&&
!
minimal
)
{
return
(
<
Surface
className=
"rounded-2xl px-5 py-2 sm:px-6 sm:py-3"
variant=
"secondary"
>
{
fields
}
</
Surface
>
);
}
return
fields
;
}
src/pages/RecordDetailDrawer/components/TailwindEditableField.jsx
View file @
f53d7e42
...
...
@@ -74,9 +74,26 @@ function FieldError({ children, className }) {
);
}
function
isEmptyFieldValue
(
value
)
{
if
(
value
===
null
||
value
===
undefined
)
return
true
;
if
(
typeof
value
===
'string'
)
return
value
.
trim
()
===
''
;
if
(
Array
.
isArray
(
value
))
{
return
value
.
length
===
0
||
value
.
every
((
item
)
=>
isEmptyFieldValue
(
item
));
}
return
false
;
}
function
getTextPlaceholder
(
placeholder
,
fieldDef
)
{
const
explicitPlaceholder
=
String
(
placeholder
||
''
).
trim
();
if
(
explicitPlaceholder
)
return
explicitPlaceholder
;
const
fieldLabel
=
String
(
fieldDef
?.
label
||
''
).
trim
();
return
fieldLabel
?
`请输入
${
fieldLabel
}
`
:
'请输入内容'
;
}
function
FieldValue
({
value
,
fieldDef
})
{
if
(
value
===
null
||
value
===
undefined
||
value
===
''
||
(
Array
.
isArray
(
value
)
&&
value
.
length
===
0
))
{
return
<
span
className=
"text-
muted"
>
未填写
</
span
>;
if
(
isEmptyFieldValue
(
value
))
{
return
<
span
className=
"text-
sm text-muted"
>
暂无内容
</
span
>;
}
if
(
fieldDef
?.
type
===
'select'
)
{
...
...
@@ -128,7 +145,7 @@ function FieldValue({ value, fieldDef }) {
if
(
fieldDef
?.
type
===
'object'
||
fieldDef
?.
type
===
'form'
||
fieldDef
?.
type
===
'subject'
)
{
return
(
<
Card
variant=
"
second
ary"
>
<
Card
variant=
"
terti
ary"
>
<
Card
.
Content
>
<
pre
className=
"max-h-48 overflow-auto text-xs leading-5"
>
{
JSON
.
stringify
(
value
,
null
,
2
)
}
...
...
@@ -139,7 +156,11 @@ function FieldValue({ value, fieldDef }) {
}
const
text
=
formatFieldValue
(
value
,
fieldDef
);
return
<
span
className=
"whitespace-pre-wrap text-foreground"
>
{
text
||
'-'
}
</
span
>;
return
(
<
span
className=
{
clsx
(
'whitespace-pre-wrap'
,
isEmptyFieldValue
(
text
)
?
'text-sm text-muted'
:
'text-foreground'
)
}
>
{
isEmptyFieldValue
(
text
)
?
'暂无内容'
:
text
}
</
span
>
);
}
function
UserSelector
({
value
,
onChange
,
multiple
,
variant
})
{
...
...
@@ -280,6 +301,7 @@ function ControlledFieldEditor({
fieldPath
,
editSessionId
,
fieldVariant
,
placeholder
,
onChange
,
onValidationChange
,
})
{
...
...
@@ -288,6 +310,7 @@ function ControlledFieldEditor({
const
fieldType
=
fieldDef
?.
type
||
'text'
;
const
isDateField
=
isDateFieldType
(
fieldType
);
const
isLongText
=
[
'long_text'
,
'rich_text'
].
includes
(
fieldType
);
const
textPlaceholder
=
getTextPlaceholder
(
placeholder
,
fieldDef
);
const
handleFileUpload
=
async
(
files
)
=>
{
const
file
=
files
?.[
0
];
...
...
@@ -366,7 +389,7 @@ function ControlledFieldEditor({
if
(
fieldType
===
'file'
)
{
return
(
<
Card
>
<
Card
variant=
"secondary"
>
<
Card
.
Content
className=
"space-y-3"
>
{
toArray
(
value
).
length
===
0
?
(
<
span
className=
"text-sm text-muted"
>
未上传
</
span
>
...
...
@@ -450,6 +473,7 @@ function ControlledFieldEditor({
return
(
<
TextArea
fullWidth
placeholder=
{
textPlaceholder
}
rows=
{
5
}
value=
{
value
||
''
}
variant=
{
fieldVariant
}
...
...
@@ -471,6 +495,7 @@ function ControlledFieldEditor({
return
(
<
Input
fullWidth
placeholder=
{
textPlaceholder
}
type=
{
fieldType
===
'number'
?
'number'
:
'text'
}
value=
{
value
??
''
}
variant=
{
fieldVariant
}
...
...
@@ -489,7 +514,8 @@ export default function TailwindEditableField({
fieldDef
,
fieldPath
,
editSessionId
,
fieldVariant
=
'primary'
,
fieldVariant
=
'secondary'
,
placeholder
,
onSave
,
onChange
,
onValidationChange
,
...
...
@@ -517,6 +543,7 @@ export default function TailwindEditableField({
const
isDateField
=
isDateFieldType
(
fieldType
);
const
isLongText
=
[
'long_text'
,
'rich_text'
].
includes
(
fieldType
);
const
isJsonLike
=
[
'object'
,
'form'
,
'subject'
].
includes
(
fieldType
);
const
textPlaceholder
=
getTextPlaceholder
(
placeholder
,
fieldDef
);
if
(
mode
===
'view'
||
(
mode
===
'edit'
&&
(
readonly
||
disabled
)))
{
return
<
ReadonlyFieldValue
value=
{
value
}
fieldDef=
{
fieldDef
}
/>;
...
...
@@ -530,6 +557,7 @@ export default function TailwindEditableField({
fieldPath=
{
fieldPath
}
editSessionId=
{
editSessionId
}
fieldVariant=
{
fieldVariant
}
placeholder=
{
textPlaceholder
}
onChange=
{
onChange
}
onValidationChange=
{
onValidationChange
}
/>
...
...
@@ -734,7 +762,7 @@ export default function TailwindEditableField({
if
(
fieldType
===
'file'
)
{
return
(
<
Card
>
<
Card
variant=
"secondary"
>
<
Card
.
Content
className=
"space-y-3"
>
<
div
className=
"space-y-2"
>
{
toArray
(
draft
).
length
===
0
?
(
...
...
@@ -822,6 +850,7 @@ export default function TailwindEditableField({
<
TextArea
autoFocus
fullWidth
placeholder=
{
textPlaceholder
}
rows=
{
5
}
value=
{
draft
||
''
}
variant=
"secondary"
...
...
@@ -848,6 +877,7 @@ export default function TailwindEditableField({
<
Input
autoFocus
fullWidth
placeholder=
{
textPlaceholder
}
type=
{
fieldType
===
'number'
?
'number'
:
'text'
}
value=
{
draft
??
''
}
variant=
"secondary"
...
...
src/pages/problem_rectification/ApplicationSwitcherDialog.jsx
View file @
f53d7e42
...
...
@@ -9,6 +9,7 @@ import {
Description
,
Modal
,
Skeleton
,
Surface
,
}
from
'@heroui/react'
;
import
{
useEffect
,
useMemo
,
useState
}
from
'react'
;
...
...
@@ -124,7 +125,7 @@ function ApplicationCard({ application, current }) {
const
initial
=
application
.
title
.
trim
().
slice
(
0
,
1
).
toUpperCase
()
||
'A'
;
return
(
<
Card
className=
"h-full"
variant=
{
current
?
'secondary'
:
'default'
}
>
<
Card
className=
"h-full"
variant=
"secondary"
>
<
Card
.
Content
>
<
a
href=
{
application
.
url
}
...
...
@@ -133,10 +134,22 @@ function ApplicationCard({ application, current }) {
aria
-
current=
{
current
?
'page'
:
undefined
}
className=
"flex min-w-0 items-center gap-3 no-underline"
>
<
Avatar
size=
"md"
>
{
application
.
logo
?
<
Avatar
.
Image
src=
{
application
.
logo
}
alt=
{
application
.
title
}
/>
:
null
}
<
Surface
aria
-
hidden=
"true"
className=
"grid size-9 shrink-0 place-items-center overflow-hidden rounded-[var(--rc-radius-control)]"
variant=
"tertiary"
>
<
Avatar
className=
"size-full rounded-[var(--rc-radius-control)] bg-transparent"
color=
"accent"
variant=
"soft"
>
{
application
.
logo
?
(
<
Avatar
.
Image
className=
"object-contain p-1.5"
src=
{
application
.
logo
}
alt=
""
/>
)
:
null
}
<
Avatar
.
Fallback
>
{
initial
}
</
Avatar
.
Fallback
>
</
Avatar
>
</
Surface
>
<
span
className=
"min-w-0 flex-1"
>
<
span
className=
"block break-words font-medium leading-5 text-foreground"
title=
{
application
.
title
}
>
{
application
.
title
}
...
...
@@ -156,9 +169,9 @@ function LoadingSkeleton() {
return
(
<
div
className=
"grid gap-3 sm:grid-cols-2"
aria
-
label=
"正在加载应用"
>
{
Array
.
from
({
length
:
6
}).
map
((
_
,
index
)
=>
(
<
Card
key=
{
index
}
>
<
Card
key=
{
index
}
variant=
"secondary"
>
<
Card
.
Content
className=
"flex items-center gap-3"
>
<
Skeleton
className=
"size-
10
"
/>
<
Skeleton
className=
"size-
9
"
/>
<
div
className=
"flex-1 space-y-2"
>
<
Skeleton
className=
"h-4 w-2/3"
/>
<
Skeleton
className=
"h-3 w-full"
/>
...
...
src/pages/problem_rectification/archive/index.jsx
View file @
f53d7e42
...
...
@@ -131,7 +131,7 @@ export default function ProblemRectificationArchivePage() {
label
:
'关键词搜索'
,
type
:
'text'
,
icon
:
'search'
,
placeholder
:
'
搜索归档批次标题、编码或关联字段...
'
,
placeholder
:
'
请输入关键词
'
,
},
]
}
onFiltersChange=
{
(
key
,
value
)
=>
{
...
...
@@ -155,14 +155,14 @@ export default function ProblemRectificationArchivePage() {
{
key
:
'archive-export'
,
label
:
'导出批次'
,
variant
:
'
outline
'
,
variant
:
'
secondary
'
,
disabled
:
true
,
onClick
:
()
=>
{},
},
{
key
:
'archive-compare'
,
label
:
'批次对比'
,
variant
:
'
outline
'
,
variant
:
'
secondary
'
,
disabled
:
true
,
onClick
:
()
=>
{},
},
...
...
src/pages/problem_rectification/dashboard/index.jsx
View file @
f53d7e42
import
{
DataTable
}
from
'@/components/DataTable'
;
import
{
colorizeXinyuanChartTooltipPayload
,
createXinyuanChartColorMap
,
getXinyuanChartColor
,
XINYUAN_CHART_SEMANTIC
,
}
from
'@/design-system/chartPalette'
;
import
XinyuanModal
from
'@/components/XinyuanModal'
;
import
XinyuanAutocomplete
from
'@/components/XinyuanAutocomplete'
;
import
{
...
...
@@ -18,6 +24,7 @@ import {
}
from
'@heroui/react'
;
import
{
BarChart
}
from
'@heroui-pro/react/bar-chart'
;
import
{
ComposedChart
}
from
'@heroui-pro/react/composed-chart'
;
import
{
ChartTooltipContent
}
from
'@heroui-pro/react/chart-tooltip'
;
import
{
EmptyState
}
from
'@heroui-pro/react/empty-state'
;
import
{
KPI
}
from
'@heroui-pro/react/kpi'
;
import
{
PieChart
}
from
'@heroui-pro/react/pie-chart'
;
...
...
@@ -25,11 +32,11 @@ import { Cell, Legend } from 'recharts';
import
{
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
{
loadProblemRectificationDashboardData
}
from
'../problemRectificationDashboardService'
;
const
CATEGORY_LEVEL_
TAB
S
=
[
{
key
:
'level1'
,
label
:
'一级分类'
},
{
key
:
'level2'
,
label
:
'二级分类'
},
{
key
:
'level3'
,
label
:
'三级分类'
},
{
key
:
'level4'
,
label
:
'四级分类'
},
const
CATEGORY_LEVEL_
OPTION
S
=
[
{
id
:
'level1'
,
label
:
'一级分类'
},
{
id
:
'level2'
,
label
:
'二级分类'
},
{
id
:
'level3'
,
label
:
'三级分类'
},
{
id
:
'level4'
,
label
:
'四级分类'
},
];
const
FOCUS_ISSUE_TYPES
=
[
{
...
...
@@ -54,14 +61,10 @@ const FOCUS_ISSUE_TYPES = [
tone
:
'accent'
,
},
];
const
CHART_PALETTE
=
[
'var(--accent)'
,
'var(--warning)'
,
'var(--success)'
,
'var(--danger)'
,
'var(--muted)'
,
'var(--foreground)'
,
];
const
CHART_LEGEND_LABEL_STYLE
=
{
fontSize
:
'var(--rc-chart-legend-font-size)'
,
lineHeight
:
'var(--rc-chart-legend-line-height)'
,
};
function
formatPercent
(
value
)
{
const
numericValue
=
Number
(
value
||
0
);
...
...
@@ -83,14 +86,11 @@ function SectionHeading({ title }) {
return
<
h2
className=
"text-base font-medium text-foreground"
>
{
title
}
</
h2
>;
}
function
StatCard
({
label
,
value
,
icon
:
Icon
,
tone
=
'neutral'
})
{
const
iconStatus
=
[
'danger'
,
'success'
,
'warning'
].
includes
(
tone
)
?
tone
:
undefined
;
function
StatCard
({
label
,
value
,
icon
:
Icon
,
tone
})
{
const
numericValue
=
Number
.
parseFloat
(
String
(
value
).
replace
(
/,/g
,
''
));
return
(
<
KPI
>
<
KPI
className=
"rc-card-surface rc-dashboard-card"
>
<
KPI
.
Header
>
<
KPI
.
Title
>
{
label
}
</
KPI
.
Title
>
</
KPI
.
Header
>
...
...
@@ -99,7 +99,11 @@ function StatCard({ label, value, icon: Icon, tone = 'neutral' }) {
{
()
=>
value
}
</
KPI
.
Value
>
{
Icon
?
(
<
KPI
.
Icon
status=
{
iconStatus
}
aria
-
label=
{
label
}
>
<
KPI
.
Icon
aria
-
label=
{
label
}
className=
"rc-metric-icon-surface"
data
-
tone=
{
tone
||
undefined
}
>
<
Icon
size=
{
20
}
/>
</
KPI
.
Icon
>
)
:
null
}
...
...
@@ -108,12 +112,31 @@ function StatCard({ label, value, icon: Icon, tone = 'neutral' }) {
);
}
function
XinyuanChartTooltipContent
({
active
,
colorForEntry
,
hideHeader
,
label
,
payload
,
valueFormatter
,
})
{
return
(
<
ChartTooltipContent
active=
{
active
}
hideHeader=
{
hideHeader
}
label=
{
label
}
payload=
{
colorizeXinyuanChartTooltipPayload
(
payload
,
colorForEntry
)
}
valueFormatter=
{
valueFormatter
}
/>
);
}
function
Panel
({
title
,
actions
,
children
,
className
})
{
return
(
<
Card
className=
{
className
}
>
<
Card
.
Header
className=
"
flex flex-wrap items-center justify-between gap-3
"
>
<
Card
.
Title
>
{
title
}
</
Card
.
Title
>
{
actions
}
<
Card
.
Header
className=
"
w-full flex-row items-center gap-3 text-left
"
>
<
Card
.
Title
className=
"min-w-0 flex-1 text-left"
>
{
title
}
</
Card
.
Title
>
{
actions
?
<
div
className=
"shrink-0"
>
{
actions
}
</
div
>
:
null
}
</
Card
.
Header
>
<
Card
.
Content
>
{
children
}
</
Card
.
Content
>
</
Card
>
...
...
@@ -170,8 +193,9 @@ function FocusIssueDetailDialog({ issueType, items = [], onClose }) {
function
DonutChart
({
data
,
centerLabel
,
colorForItem
})
{
const
total
=
data
.
reduce
((
sum
,
item
)
=>
sum
+
Number
(
item
.
value
||
0
),
0
);
const
colorMap
=
createXinyuanChartColorMap
(
data
.
map
((
item
)
=>
item
.
name
));
return
(
<
PieChart
height=
{
320
}
>
<
PieChart
className=
"rc-chart-interaction"
height=
{
320
}
>
<
PieChart
.
Pie
data=
{
data
}
dataKey=
"value"
...
...
@@ -183,7 +207,9 @@ function DonutChart({ data, centerLabel, colorForItem }) {
{
data
.
map
((
item
,
index
)
=>
(
<
PieChart
.
Cell
key=
{
`${item.name}-${index}`
}
fill=
{
colorForItem
?.(
item
,
index
)
||
CHART_PALETTE
[
index
%
CHART_PALETTE
.
length
]
}
fill=
{
colorForItem
?.(
item
,
index
)
||
colorMap
.
get
(
String
(
item
.
name
??
''
).
trim
())
||
getXinyuanChartColor
(
item
.
name
,
index
)
}
/>
))
}
<
PieChart
.
Label
...
...
@@ -204,20 +230,40 @@ function DonutChart({ data, centerLabel, colorForItem }) {
/>
</
PieChart
.
Pie
>
<
PieChart
.
Tooltip
content=
{
<
PieChart
.
TooltipContent
valueFormatter=
{
(
value
)
=>
`${formatCount(value)} 条`
}
/>
}
content=
{
(
<
XinyuanChartTooltipContent
colorForEntry=
{
({
index
,
key
,
sourceDatum
})
=>
(
colorForItem
?.(
sourceDatum
,
index
)
||
colorMap
.
get
(
String
(
key
??
''
).
trim
())
||
getXinyuanChartColor
(
key
,
index
)
)
}
valueFormatter=
{
(
value
)
=>
`${formatCount(value)} 条`
}
/>
)
}
/>
</
PieChart
>
);
}
function
HorizontalBarChart
({
data
,
color
=
'var(--accent)'
,
onSelect
})
{
function
HorizontalBarChart
({
data
,
color
=
XINYUAN_CHART_SEMANTIC
.
primary
,
onSelect
})
{
return
(
<
BarChart
data=
{
data
}
height=
{
320
}
layout=
"vertical"
margin=
{
{
left
:
16
,
right
:
24
}
}
>
<
BarChart
className=
"rc-chart-interaction"
data=
{
data
}
height=
{
320
}
layout=
"vertical"
margin=
{
{
left
:
16
,
right
:
24
}
}
>
<
BarChart
.
Grid
horizontal=
{
false
}
/>
<
BarChart
.
XAxis
allowDecimals=
{
false
}
type=
"number"
/>
<
BarChart
.
YAxis
dataKey=
"name"
type=
"category"
width=
{
112
}
/>
<
BarChart
.
Tooltip
content=
{
<
BarChart
.
TooltipContent
valueFormatter=
{
(
value
)
=>
`${formatCount(value)} 条`
}
/>
}
content=
{
(
<
XinyuanChartTooltipContent
colorForEntry=
{
({
sourceDatum
})
=>
sourceDatum
?.
fill
||
color
}
valueFormatter=
{
(
value
)
=>
`${formatCount(value)} 条`
}
/>
)
}
/>
<
BarChart
.
Bar
dataKey=
"value"
...
...
@@ -236,6 +282,9 @@ function HorizontalBarChart({ data, color = 'var(--accent)', onSelect }) {
}
function
StackedSourceChart
({
sourceComparison
})
{
const
seriesColorMap
=
createXinyuanChartColorMap
(
sourceComparison
.
series
.
map
((
series
)
=>
series
.
name
),
);
const
chartData
=
sourceComparison
.
categories
.
map
((
name
,
index
)
=>
({
name
,
...
Object
.
fromEntries
(
...
...
@@ -244,19 +293,36 @@ function StackedSourceChart({ sourceComparison }) {
}));
return
(
<
BarChart
data=
{
chartData
}
height=
{
320
}
margin=
{
{
left
:
8
,
right
:
16
,
top
:
24
}
}
>
<
BarChart
className=
"rc-chart-interaction"
data=
{
chartData
}
height=
{
320
}
margin=
{
{
left
:
8
,
right
:
16
,
top
:
24
}
}
>
<
BarChart
.
Grid
vertical=
{
false
}
/>
<
BarChart
.
XAxis
dataKey=
"name"
/>
<
BarChart
.
YAxis
allowDecimals=
{
false
}
/>
<
BarChart
.
Tooltip
content=
{
<
BarChart
.
TooltipContent
valueFormatter=
{
(
value
)
=>
`${formatCount(value)} 条`
}
/>
}
content=
{
(
<
XinyuanChartTooltipContent
colorForEntry=
{
({
entry
})
=>
(
seriesColorMap
.
get
(
String
(
entry
?.
name
??
entry
?.
dataKey
??
''
).
trim
())
)
}
valueFormatter=
{
(
value
)
=>
`${formatCount(value)} 条`
}
/>
)
}
/>
<
Legend
iconSize=
{
8
}
iconType=
"circle"
labelStyle=
{
CHART_LEGEND_LABEL_STYLE
}
/>
<
Legend
iconSize=
{
8
}
iconType=
"circle"
/>
{
sourceComparison
.
series
.
map
((
series
,
index
)
=>
(
<
BarChart
.
Bar
key=
{
series
.
name
}
dataKey=
{
series
.
name
}
fill=
{
CHART_PALETTE
[
index
%
CHART_PALETTE
.
length
]
}
fill=
{
seriesColorMap
.
get
(
String
(
series
.
name
??
''
).
trim
())
||
getXinyuanChartColor
(
series
.
name
,
index
)
}
maxBarSize=
{
34
}
stackId=
"total"
/>
...
...
@@ -267,7 +333,12 @@ function StackedSourceChart({ sourceComparison }) {
function
DepartmentMatrixChart
({
data
})
{
return
(
<
ComposedChart
data=
{
data
}
height=
{
320
}
margin=
{
{
left
:
8
,
right
:
8
,
top
:
24
}
}
>
<
ComposedChart
className=
"rc-chart-interaction"
data=
{
data
}
height=
{
320
}
margin=
{
{
left
:
8
,
right
:
8
,
top
:
24
}
}
>
<
ComposedChart
.
Grid
vertical=
{
false
}
/>
<
ComposedChart
.
XAxis
dataKey=
"name"
/>
<
ComposedChart
.
YAxis
allowDecimals=
{
false
}
yAxisId=
"count"
/>
...
...
@@ -278,18 +349,32 @@ function DepartmentMatrixChart({ data }) {
tickFormatter=
{
(
value
)
=>
`${value}%`
}
yAxisId=
"rate"
/>
<
ComposedChart
.
Tooltip
content=
{
<
ComposedChart
.
TooltipContent
/>
}
/>
<
Legend
iconSize=
{
8
}
iconType=
"circle"
/>
<
ComposedChart
.
Tooltip
content=
{
(
<
XinyuanChartTooltipContent
colorForEntry=
{
({
entry
})
=>
({
completionRate
:
XINYUAN_CHART_SEMANTIC
.
positive
,
overdueCount
:
XINYUAN_CHART_SEMANTIC
.
warning
,
totalCount
:
XINYUAN_CHART_SEMANTIC
.
primary
,
})[
entry
?.
dataKey
]
}
/>
)
}
/>
<
Legend
iconSize=
{
8
}
iconType=
"circle"
labelStyle=
{
CHART_LEGEND_LABEL_STYLE
}
/>
<
ComposedChart
.
Bar
dataKey=
"totalCount"
fill=
"var(--accent)"
fill=
{
XINYUAN_CHART_SEMANTIC
.
primary
}
maxBarSize=
{
28
}
name=
"问题总数"
yAxisId=
"count"
/>
<
ComposedChart
.
Bar
dataKey=
"overdueCount"
fill=
"var(--danger)"
fill=
{
XINYUAN_CHART_SEMANTIC
.
warning
}
maxBarSize=
{
28
}
name=
"逾期未完成"
yAxisId=
"count"
...
...
@@ -298,7 +383,7 @@ function DepartmentMatrixChart({ data }) {
dataKey=
"completionRate"
dot=
{
false
}
name=
"整改完成率"
stroke=
"var(--success)"
stroke=
{
XINYUAN_CHART_SEMANTIC
.
positive
}
strokeWidth=
{
2
}
type=
"monotone"
yAxisId=
"rate"
...
...
@@ -381,10 +466,10 @@ export function ProblemRectificationDashboardPage({ dashboardMode = 'overview' }
name
:
type
.
label
,
value
:
summary
.
focusIssueGroups
?.[
type
.
key
]?.
length
||
0
,
fill
:
type
.
tone
===
'danger'
?
'var(--danger)'
?
XINYUAN_CHART_SEMANTIC
.
risk
:
type
.
tone
===
'warning'
?
'var(--warning)'
:
'var(--accent)'
,
?
XINYUAN_CHART_SEMANTIC
.
warning
:
XINYUAN_CHART_SEMANTIC
.
comparison
,
})),
[
summary
.
focusIssueGroups
],
);
...
...
@@ -477,7 +562,7 @@ export function ProblemRectificationDashboardPage({ dashboardMode = 'overview' }
{
!
isBusinessDashboard
?
(
<>
<
section
className=
"rc-page-section"
>
<
div
className=
"rc-kpi-grid
grid md:grid-cols-2 xl:grid-cols-3
"
>
<
div
className=
"rc-kpi-grid
"
data
-
count=
"5
"
>
<
StatCard
label=
"问题总数"
value=
{
formatCount
(
summary
.
totalCount
)
}
...
...
@@ -527,31 +612,25 @@ export function ProblemRectificationDashboardPage({ dashboardMode = 'overview' }
<>
<
section
className=
"rc-page-section"
>
<
SectionHeading
title=
"问题分析与风险定位"
/>
<
div
className=
"rc-page-grid
grid xl:grid-cols-[1.05fr_0.95fr]
"
>
<
div
className=
"rc-page-grid
rc-chart-grid
"
>
<
Panel
title=
"问题类型 / 业务领域分布"
actions=
{
(
<
Tabs
<
XinyuanAutocomplete
aria
-
label=
"分类层级"
className=
"w-fit max-w-full min-w-0"
className=
"w-36"
fullWidth=
{
false
}
options=
{
CATEGORY_LEVEL_OPTIONS
}
placement=
"bottom end"
placeholder=
"选择分类层级"
selectedKey=
{
activeCategoryLevel
}
shouldFilter=
{
false
}
valueText=
{
CATEGORY_LEVEL_OPTIONS
.
find
(
(
option
)
=>
option
.
id
===
activeCategoryLevel
,
)?.
label
}
variant=
"secondary"
onSelectionChange=
{
(
key
)
=>
setActiveCategoryLevel
(
String
(
key
))
}
>
<
Tabs
.
ListContainer
className=
"w-fit max-w-full min-w-0"
>
<
Tabs
.
List
className=
"w-max min-w-max"
>
{
CATEGORY_LEVEL_TABS
.
map
((
tab
)
=>
(
<
Tabs
.
Tab
className=
"w-auto min-w-max flex-none whitespace-nowrap"
id=
{
tab
.
key
}
key=
{
tab
.
key
}
>
{
tab
.
label
}
<
Tabs
.
Indicator
/>
</
Tabs
.
Tab
>
))
}
</
Tabs
.
List
>
</
Tabs
.
ListContainer
>
</
Tabs
>
/>
)
}
>
{
activeCategoryData
.
length
>
0
?
(
...
...
@@ -569,6 +648,7 @@ export function ProblemRectificationDashboardPage({ dashboardMode = 'overview' }
</
Panel
>
</
div
>
<
div
className=
"rc-page-grid rc-chart-grid"
>
<
Panel
title=
"问题来源对比"
>
{
summary
.
sourceComparison
.
categories
.
length
>
0
?
(
<
StackedSourceChart
sourceComparison=
{
summary
.
sourceComparison
}
/>
...
...
@@ -590,14 +670,18 @@ export function ProblemRectificationDashboardPage({ dashboardMode = 'overview' }
<
EmptyBlock
title=
"当前没有相关重点问题"
/>
)
}
</
Panel
>
</
div
>
</
section
>
<
section
className=
"rc-page-section"
>
<
SectionHeading
title=
"问题责任追踪与执行"
/>
<
div
className=
"rc-page-grid
grid xl:grid-cols-[0.9fr_1.1fr]
"
>
<
div
className=
"rc-page-grid
rc-chart-grid
"
>
<
Panel
title=
"逾期问题排行"
>
{
summary
.
overdueRanking
.
length
>
0
?
(
<
HorizontalBarChart
data=
{
summary
.
overdueRanking
}
color=
"var(--danger)"
/>
<
HorizontalBarChart
data=
{
summary
.
overdueRanking
}
color=
{
XINYUAN_CHART_SEMANTIC
.
risk
}
/>
)
:
(
<
EmptyBlock
title=
"当前没有逾期问题排行数据"
/>
)
}
...
...
@@ -614,6 +698,7 @@ export function ProblemRectificationDashboardPage({ dashboardMode = 'overview' }
<
section
className=
"rc-page-section"
>
<
SectionHeading
title=
"审计整改质效"
/>
<
div
className=
"rc-page-grid rc-chart-grid"
>
<
Panel
title=
"综合整改质效结果"
>
{
summary
.
qualityRatingDistribution
.
length
>
0
?
(
<
DonutChart
...
...
@@ -621,16 +706,17 @@ export function ProblemRectificationDashboardPage({ dashboardMode = 'overview' }
centerLabel=
"评级记录"
colorForItem=
{
(
item
)
=>
(
item
.
name
===
'高'
?
'var(--danger)'
?
XINYUAN_CHART_SEMANTIC
.
positive
:
item
.
name
===
'中'
?
'var(--warning)'
:
'var(--success)'
?
XINYUAN_CHART_SEMANTIC
.
warning
:
XINYUAN_CHART_SEMANTIC
.
risk
)
}
/>
)
:
(
<
EmptyBlock
title=
"暂无综合整改质效结果数据"
/>
)
}
</
Panel
>
</
div
>
</
section
>
</>
)
:
null
}
...
...
src/pages/problem_rectification/flow-table/index.jsx
View file @
f53d7e42
...
...
@@ -1288,7 +1288,7 @@ export default function ProblemRectificationFlowTablePage() {
{
isDelayFlow
?
(
<>
{
editable
?
(
<
Button
isDisabled=
{
submitting
}
size=
"sm"
variant=
"
outline
"
onPress=
{
()
=>
handleDelaySave
(
row
)
}
>
<
Button
isDisabled=
{
submitting
}
size=
"sm"
variant=
"
secondary
"
onPress=
{
()
=>
handleDelaySave
(
row
)
}
>
保存申请
</
Button
>
)
:
null
}
...
...
@@ -1324,7 +1324,7 @@ export default function ProblemRectificationFlowTablePage() {
)
:
(
<>
{
editable
?
(
<
Button
isDisabled=
{
submitting
}
size=
"sm"
variant=
"
outline
"
onPress=
{
()
=>
handleSave
(
row
)
}
>
<
Button
isDisabled=
{
submitting
}
size=
"sm"
variant=
"
secondary
"
onPress=
{
()
=>
handleSave
(
row
)
}
>
保存
</
Button
>
)
:
null
}
...
...
@@ -1573,7 +1573,7 @@ export default function ProblemRectificationFlowTablePage() {
aria
-
label=
"刷新流程"
isDisabled=
{
loading
||
Boolean
(
submittingKey
)
}
size=
"sm"
variant=
"
outline
"
variant=
"
secondary
"
onPress=
{
loadFlow
}
>
<
ArrowPathIcon
size=
{
16
}
/>
...
...
src/pages/problem_rectification/form/index.jsx
View file @
f53d7e42
...
...
@@ -12,6 +12,7 @@ import { normalizeText } from '@/utils/subjectView';
import
dayjs
from
'dayjs'
;
import
{
Alert
,
Button
,
Card
,
Chip
,
Spinner
}
from
'@heroui/react'
;
import
{
EmptyState
as
ProEmptyState
}
from
'@heroui-pro/react/empty-state'
;
import
{
KPI
}
from
'@heroui-pro/react/kpi'
;
import
{
useContext
,
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
{
useSearchParams
}
from
'umi'
;
import
{
UserContext
}
from
'@/wrapper/Auth'
;
...
...
@@ -73,22 +74,42 @@ function StepBadge({ title, status, active }) {
);
}
function
SummaryCard
({
label
,
value
,
hint
,
icon
:
Icon
,
tone
=
'neutral'
})
{
function
SummaryCard
({
label
,
value
,
hint
,
icon
:
Icon
,
tone
,
variant
=
'default'
})
{
if
(
Icon
)
{
const
numericValue
=
Number
.
parseFloat
(
String
(
value
).
replace
(
/,/g
,
''
));
return
(
<
Card
>
<
KPI
className=
"rc-card-surface rc-dashboard-card"
>
<
KPI
.
Header
>
<
KPI
.
Title
>
{
label
}
</
KPI
.
Title
>
</
KPI
.
Header
>
<
KPI
.
Content
>
<
KPI
.
Value
value=
{
Number
.
isFinite
(
numericValue
)
?
numericValue
:
0
}
>
{
()
=>
value
}
</
KPI
.
Value
>
<
KPI
.
Icon
aria
-
label=
{
label
}
className=
"rc-metric-icon-surface"
data
-
tone=
{
tone
||
undefined
}
>
<
Icon
size=
{
20
}
/>
</
KPI
.
Icon
>
</
KPI
.
Content
>
{
hint
?
<
KPI
.
Footer
className=
"text-xs text-muted"
>
{
hint
}
</
KPI
.
Footer
>
:
null
}
</
KPI
>
);
}
return
(
<
Card
variant=
{
variant
}
>
<
Card
.
Content
className=
"flex items-start justify-between gap-4"
>
<
div
className=
"min-w-0"
>
<
div
className=
"text-sm font-medium text-muted"
>
{
label
}
</
div
>
<
div
className=
{
Icon
?
'mt-2 text-2xl font-medium text-foreground'
:
'mt-2 text-lg font-medium text-foreground'
}
>
<
div
className=
"mt-2 text-lg font-medium text-foreground"
>
{
value
}
</
div
>
{
hint
?
<
div
className=
"mt-1 text-xs text-muted"
>
{
hint
}
</
div
>
:
null
}
</
div
>
{
Icon
?
(
<
Chip
color=
{
[
'danger'
,
'success'
,
'warning'
].
includes
(
tone
)
?
tone
:
undefined
}
size=
"lg"
variant=
"soft"
aria
-
label=
{
label
}
>
<
Icon
size=
{
20
}
/>
</
Chip
>
)
:
null
}
</
Card
.
Content
>
</
Card
>
);
...
...
@@ -387,7 +408,7 @@ export default function ProblemRectificationFormPage() {
/>
</
section
>
<
section
className=
"rc-
page-grid grid md:grid-cols-
3"
>
<
section
className=
"rc-
kpi-grid"
data
-
count=
"
3"
>
<
SummaryCard
label=
"关联记录"
value=
{
summary
.
total
}
icon=
{
Database
}
tone=
"info"
/>
<
SummaryCard
label=
"已整改"
...
...
@@ -451,7 +472,7 @@ export default function ProblemRectificationFormPage() {
render
:
(
record
)
=>
(
<
Button
size=
"sm"
variant=
"
outline
"
variant=
"
secondary
"
onPress=
{
()
=>
setSelectedRecord
(
record
)
}
>
{
canEditRecords
&&
!
record
.
isLocked
?
'查看/填写'
:
'查看详情'
}
...
...
@@ -491,7 +512,7 @@ export default function ProblemRectificationFormPage() {
</
Card
.
Header
>
<
Card
.
Content
>
<
div
className=
"flex flex-wrap gap-3"
>
<
Button
isDisabled=
{
submitting
}
size=
"sm"
variant=
"
outline
"
onPress=
{
reload
}
>
<
Button
isDisabled=
{
submitting
}
size=
"sm"
variant=
"
secondary
"
onPress=
{
reload
}
>
刷新流程
</
Button
>
...
...
@@ -553,10 +574,10 @@ export default function ProblemRectificationFormPage() {
</
Alert
>
)
:
null
}
<
div
className=
"grid gap-4 md:grid-cols-2"
>
<
SummaryCard
label=
"问题标题"
value=
{
selectedRecord
.
title
||
'-'
}
/>
<
SummaryCard
label=
"反馈单位"
value=
{
selectedRecord
.
feedbackUnit
||
'-'
}
/>
<
SummaryCard
label=
"整改进度"
value=
{
selectedRecord
.
progress
||
'-'
}
/>
<
SummaryCard
label=
"预计完成时间"
value=
{
formatDateText
(
selectedRecord
.
raw
?.
metadata
?.
the_time_of_rectification
)
}
/>
<
SummaryCard
label=
"问题标题"
value=
{
selectedRecord
.
title
||
'-'
}
variant=
"secondary"
/>
<
SummaryCard
label=
"反馈单位"
value=
{
selectedRecord
.
feedbackUnit
||
'-'
}
variant=
"secondary"
/>
<
SummaryCard
label=
"整改进度"
value=
{
selectedRecord
.
progress
||
'-'
}
variant=
"secondary"
/>
<
SummaryCard
label=
"预计完成时间"
value=
{
formatDateText
(
selectedRecord
.
raw
?.
metadata
?.
the_time_of_rectification
)
}
variant=
"secondary"
/>
</
div
>
<
div
className=
"grid gap-4 lg:grid-cols-2"
>
...
...
src/pages/problem_rectification/list/index.jsx
View file @
f53d7e42
...
...
@@ -14,7 +14,6 @@ import {
TableActionButton
,
}
from
'@/components/TableActionButton'
;
import
XinyuanModal
from
'@/components/XinyuanModal'
;
import
useRootDarkTheme
from
'@/hooks/useRootDarkTheme'
;
import
RecordWorkbench
from
'@/components/record-workbench'
;
import
RecordDetailDrawer
from
'@/pages/RecordDetailDrawer'
;
import
{
...
...
@@ -796,8 +795,7 @@ export default function ProblemRectificationListPage() {
});
const [gridViewCounts, setGridViewCounts] = useState({});
const [workflowUserLabels, setWorkflowUserLabels] = useState({});
const isDarkTheme = useRootDarkTheme();
const createFieldVariant = isDarkTheme ? 'secondary' : 'primary';
const createFieldVariant = 'secondary';
const reloadResources = useCallback(async () => {
setLoading(true);
...
...
@@ -1660,7 +1658,7 @@ export default function ProblemRectificationListPage() {
label: '关键词搜索',
type: 'text',
icon: 'search',
placeholder: '
搜索项目名称、严重程度、分类、责任部门、问题原因...
',
placeholder: '
请输入关键词
',
},
{
key: 'inspectionReportMonth',
...
...
@@ -1750,14 +1748,14 @@ export default function ProblemRectificationListPage() {
{
key: 'export',
label: '导出',
variant: '
outline
',
variant: '
secondary
',
disabled: true,
onClick: () => {},
},
{
key: 'archive',
label: '一键归档',
variant: '
outline
',
variant: '
secondary
',
disabled: true,
onClick: () => {},
},
...
...
@@ -1808,7 +1806,7 @@ export default function ProblemRectificationListPage() {
key: 'view-delay-request',
label: '查看延期申请',
icon: CalendarDaysIcon,
variant: '
outline
',
variant: '
secondary
',
hidden: ({ record }) => !resolveDelayRequestDisplay(record),
onClick: ({ record }) => setDelayDetailRecord(record),
},
...
...
@@ -1816,7 +1814,7 @@ export default function ProblemRectificationListPage() {
key: 'lock',
label: '锁定',
icon: LockClosedIcon,
variant: '
outline
',
variant: '
secondary
',
hidden: ({ record }) => record.isLocked || record.progress !== '已整改',
disabled: () => actionLoading,
onClick: ({ record }) => handleLockToggle(record, true),
...
...
@@ -1825,7 +1823,7 @@ export default function ProblemRectificationListPage() {
key: 'unlock',
label: '解锁',
icon: LockOpenIcon,
variant: '
outline
',
variant: '
secondary
',
hidden: ({ record }) => !record.isLocked,
disabled: () => actionLoading,
onClick: ({ record }) => handleLockToggle(record, false),
...
...
@@ -1833,14 +1831,14 @@ export default function ProblemRectificationListPage() {
{
key: 'clone',
label: '复制',
variant: '
outline
',
variant: '
secondary
',
disabled: ({ record }) => actionLoading || record.isLocked,
onClick: ({ record }) => setCloneTargetRecord(record),
},
{
key: 'delete',
label: '删除',
variant: '
outline
',
variant: '
danger-soft
',
tone: 'danger',
disabled: ({ record }) => actionLoading || record.isLocked,
onClick: ({ record }) => handleDelete([record]),
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment