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
cd281496
Commit
cd281496
authored
Aug 26, 2026
by
Andy-bubu
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: improve data grid readability and navbar notifications
parent
fa6182cc
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
302 additions
and
56 deletions
+302
-56
ui-architecture.md
docs/ui-architecture.md
+3
-0
ui-migration-checklist.md
docs/ui-migration-checklist.md
+8
-0
xinyuan-cross-app-feature-contract.md
docs/xinyuan-cross-app-feature-contract.md
+93
-0
xinyuan-design-system.md
docs/xinyuan-design-system.md
+6
-2
AppShell.jsx
src/components/AppShell.jsx
+6
-0
DashboardNavbar.jsx
src/components/DashboardNavbar.jsx
+38
-3
DataTable.jsx
src/components/DataTable.jsx
+53
-16
index.jsx
src/components/record-workbench/index.jsx
+81
-8
Layout.jsx
src/pages/problem_rectification/Layout.jsx
+3
-22
index.jsx
src/pages/problem_rectification/list/index.jsx
+9
-3
problem-rectification.business-flow.md
...blem_rectification/problem-rectification.business-flow.md
+2
-2
No files found.
docs/ui-architecture.md
View file @
cd281496
...
...
@@ -8,6 +8,7 @@
-
允许的共享组件只组合 HeroUI/HeroUI Pro 官方复合结构,不复刻旧 props,不重画组件外观。
-
HeroUI/HeroUI Pro 暂无直接对应组件时,优先组合官方 compound API;确需补充基础能力时先更新架构与设计系统评审结论,不得回退到 Catalyst 或可见原生控件。
-
业务数据、接口参数、权限、校验、保存回调和状态流转继续由页面与 service 层拥有。
-
五项跨应用功能的唯一范围与业务合同见
[
xinyuan-cross-app-feature-contract.md
](
./xinyuan-cross-app-feature-contract.md
)
。问题整改、审计项目管理、警示案例库、廉洁从业管理、廉洁风险防控、党风廉政建设实施全部五项;其它应用只能接入通知。共享组件不得通过默认启用或硬编码把完整功能扩散到白名单外。
## 标准实现
...
...
@@ -15,6 +16,7 @@
-
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 长文本:
`RecordWorkbench`
从动态视图字段定义、字段语义和当前真实记录样本共同推导展示策略,不绑定具体旧表头。普通长文本最小 360px,整改情况最小 420px,最多两行换行并由共享 HeroUI Tooltip 提供全文;
`DataTable`
根据列最小宽度总和与
`ResizeObserver`
获得的容器实宽决定内部横向滚动。任何单元格文本必须在自身边界内裁切,不得覆盖相邻列。
-
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 原位切换取消/保存并通过原更新服务一次提交,不允许字段级即时保存。
...
...
@@ -31,6 +33,7 @@
-
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。
-
Navbar 通知入口:通知中心作为全局 Shell 动作,不进入 Sidebar 菜单。使用 HeroUI
`Badge.Anchor + Badge + Button + Tooltip`
直接组合,复用原通知路由和未读数量服务;通知按钮在主题按钮左侧,0 条不显示 Badge,超过 99 条显示
`99+`
。禁止实现通知 Popover、伪造未读数或改变通知页、请求与已读状态流。
-
列表工具栏:数据集标题单独位于透明列表区首行,下一行才是命令工具栏。搜索与列分组并存时,左侧顺序固定为 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`
并按内容紧凑定宽。参数变化只连接原状态和回调,不改变统计口径、请求或数据结构。
...
...
docs/ui-migration-checklist.md
View file @
cd281496
...
...
@@ -16,9 +16,16 @@
| 整改处理 |
`/problem_rectification_form`
| Catalyst | HeroUI | 填写、复核、终审状态机 | 待浏览器验收 | 待验收 |
| 整改流程 |
`/problem_rectification_flow_table`
| 混合 | HeroUI Pro DataGrid | OA、通知、上传和审批状态机 | 待浏览器验收 | 待验收 |
| 详情与引用 | 全局弹窗 | Catalyst + 原生控件 | HeroUI Modal/ListBox | 字段权限、保存值和引用 ID | 待浏览器验收 | 待验收 |
| Grid
`shortTitle`
| 动态 Grid View | 视图配置 | HeroUI Pro DataGrid | 只隐藏标题展示,不改变标题数据与入口 | 待浏览器验收 | 待验收 |
| Detail
`shortTitle`
| 动态 Detail View | 视图配置 | HeroUI Modal | 隐藏记录标题/字段但保留功能标题和保存协议 | 待浏览器验收 | 待验收 |
| Subject View 权限 | Grid 与详情 | 多维表格配置 | View Tabs + 字段只读态 | 目标应用授权隔离、无权限不兜底 | 待浏览器验收 | 待验收 |
| 消息通知 |
`/notifications`
+ Navbar | 平台通知接口 | HeroUI 页面与 Badge | 未读、已读、动态 action、base-aware 跳转 | 待浏览器验收 | 待验收 |
| 审计材料文件 | 允许编辑的 file 字段 | Record/Upload | Autocomplete + DropZone | 快照协议、保存后建档、失败重试不重复 | 待浏览器验收 | 待验收 |
## 验收门槛
-
五项新增功能按
[
跨应用功能扩展规范
](
./xinyuan-cross-app-feature-contract.md
)
的白名单分流;白名单外应用只能出现消息通知。
-
全部运行代码、示例和测试中的 Catalyst、Headless UI、旧适配器和可见原生控件引用为零;历史隔离目录不得进入构建或依赖图。
-
Checkbox 单选、取消、全选、半选和禁用状态无 Context/slot 错误。
-
Autocomplete、SearchField、ListBox、Modal、Tooltip、ActionBar 在深浅主题和移动端正确继承 Portal 主题。
...
...
@@ -59,6 +66,7 @@
-
筛选页面内层级已统一为页面
`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。
-
问题清单及共享工作台的长文本识别已扩展为“动态字段类型 + 字段语义 + 当前真实样本长度”三层策略;普通长文本列至少 360px并显示两行,整改情况至少 420px。共享 DataTable 不再按列数猜测宽表,而是比较动态列最小宽度总和与容器实宽后切换 HeroUI Pro DataGrid 内部横向滚动,视图配置调整后会自动重算,单元格文字不会跨列遮挡。
-
驾驶舱与整改分析的年度范围控件已去除重复“统计年份”可见标签,保留
`aria-label`
,使用 144px 宽的 HeroUI Autocomplete
`primary`
触发器;年份来源、默认值和切换回调未改变。
-
问题整改详情已调整为 HeroUI Modal 的 1088px 内容型宽度,28px inset、内部滚动和右上角全屏/关闭操作组;全屏图标不再落到标题下方,字段内容移除逐字段阴影 Card,改为紧凑两列标签/值网格。
-
问题整改详情已改为弹窗级查看/编辑状态:默认字段只读,单一“编辑”入口进入整窗编辑,Footer 统一“取消 / 保存”;取消恢复原记录,保存沿用原
`updateRecord`
和嵌套 payload 一次提交变更,视图禁用、锁定和只读字段不进入编辑。生产构建与静态状态检查已通过,真实点击与接口请求仍待浏览器验收。
...
...
docs/xinyuan-cross-app-feature-contract.md
0 → 100644
View file @
cd281496
# 鑫元跨应用五项功能扩展规范
本规范定义已经在“问题整改”完成验证、后续需要按范围同步到其它鑫元应用的五项功能。它是功能与组件架构合同,不是可以自由发挥的视觉参考。除本规范明确授权的新增能力外,目标应用原有 API、路由、鉴权、校验、保存回调和业务状态流必须保持不变。
## 1. 应用范围矩阵
以下六个应用实施全部五项功能:
-
问题整改
-
审计项目管理
-
警示案例库
-
廉洁从业管理
-
廉洁风险防控
-
党风廉政建设
六项名单以应用统一展示名称为准;批量迁移时应由用户或目标项目配置明确应用名称,不能仅凭目录名或相似代码推断。
| 功能 | 六个完整功能应用 | 其它鑫元应用 |
| --- | --- | --- |
| Grid
`shortTitle`
| 必须 | 禁止新增 |
| Detail
`shortTitle`
| 必须 | 禁止新增 |
| Subject View 权限 | 必须 | 禁止新增 |
| 消息通知 | 必须 | 必须 |
| 文件接入审计材料中心 | 必须 | 禁止新增 |
“其它鑫元应用”只增加消息通知。不得因为复用共享代码而把
`shortTitle`
、View 权限或材料中心文件流程带入其业务页面。
## 2. Grid shortTitle
-
`view.settings.shortTitle === true`
只隐藏 DataGrid 中的
`title`
展示列;
`false`
或未配置时正常显示。
-
不删除、清空或改写记录标题,不改变标题参与的查询、关键词搜索、排序、筛选、权限、详情上下文或保存数据。
-
每次读取或切换动态 View 后重新解析配置,不缓存旧标题或旧字段常量;服务端调整 View 后页面必须随配置变化。
-
标题列原本承载的详情、状态或业务入口必须迁移到独立可见的上下文列或固定操作列,不能随标题列一起丢失。
-
上下文列属于表现层,不写入
`settings.columns`
,不参与 Record 查询、排序或字段权限。
## 3. Detail shortTitle
-
详情使用当前激活的
`type = "detail"`
View。该 View 的
`settings.shortTitle === true`
时,同时隐藏记录标题展示和字段区的
`title`
字段。
-
Modal 的功能标题和可访问名称始终保留,不能生成无标题弹窗。
-
切换详情 View 后立即重新计算;
`false`
或未配置时恢复记录标题和
`title`
字段。
-
隐藏只影响展示,不改变草稿、校验、保存 payload、详情查询或记录身份。
## 4. Subject View 权限
-
Grid View 的
`settings.viewAuth`
只控制视图可见性;活动 Grid View 的
`settings.auth`
控制详情字段编辑能力;
`settings.disableColumns`
对所有用户始终只读。
-
普通用户遇到缺失
`viewAuth`
的 View 或缺失字段
`auth`
的字段时默认拒绝。系统管理员、系统账号和目标应用自身的“管理”角色可以旁路 View/字段授权,但不能旁路
`disableColumns`
。
-
仅从
`user.applications[]`
中
`name`
等于目标应用授权标识的单个应用项读取
`groups`
、
`roles`
和
`permissions`
。不得复制问题整改的
`integrity_risk_manager`
,不得合并顶层身份或其它应用授权。
-
授权类型只接受
`group`
、
`role`
、
`permission`
;缺失或不支持的类型按“管理”角色规则处理。
-
从 Grid 打开详情时,活动 Grid View 是唯一字段编辑权限来源;Detail View 只负责字段编排、条件和
`shortTitle`
,不得叠加第二套字段权限。
-
没有可访问 Grid View 时不创建兜底 View、不查询记录,并隐藏新增、批量和行操作。只有一个可访问 View 时保留其 key、查询和筛选,但隐藏单项 Tabs 轨道。
-
View 的 restrictions、filters、排序、分页和请求参数继续按目标应用原协议执行。
## 5. 消息通知
该功能适用于所有鑫元应用。
-
使用独立
`/notifications`
页面,支持
`message / todo`
、
`全部 / 未读`
、300ms 搜索防抖、固定每页 10 条、单条已读、全部已读、服务端动态
`actions`
和关联流程跳转。
-
未读筛选必须传服务端
`status: "unread"`
;不能只过滤当前页。通知正文按纯文本渲染,不执行服务端 HTML。
-
`GET /notice/customer/count`
的真实结果进入全局 Shell;通知更新事件和 60 秒轮询刷新同一计数。不得伪造数字。
-
通知入口不占 Sidebar 导航。Navbar 使用 HeroUI
`Badge.Anchor + Badge + Button + Tooltip`
,36px Bell 位于最右侧主题按钮左边;0 不显示 Badge,1–99 显示真实值,100 及以上显示
`99+`
,Badge 可访问文本保留真实数量。
-
点击 Bell 进入目标应用原
`/notifications`
路由,不在 Navbar 复制通知 Popover。
-
站内链接必须适配目标应用自己的
`base/publicPath`
;目标应用内路由去除自身 base 后交给路由器,其它应用和外部 HTTP(S) 链接新窗口打开。
-
保留现有鉴权、通知权限、已读协议、动态 action 名称、
`notice_id`
请求体和错误处理。
## 6. 文件接入审计材料中心
该功能只适用于六个完整功能应用中允许编辑的
`file`
字段;若目标应用业务文档明确限定字段范围,以该范围为准。
-
UI 使用 HeroUI Tabs 在“材料中心 / 本地上传”之间切换;材料选择使用 HeroUI Autocomplete,本地文件使用 HeroUI Pro
`DropZone`
。必须运行时读取 Subject 字段
`multiple`
。
-
材料中心通过
`POST /record/query`
查询
`am_material`
最近 20 条,按当前用户 Record 权限返回,远程搜索
`metadata.material_title`
和
`metadata.file_asset.name`
,正则关键词必须转义。
-
选择已有材料只把
`{name,type,size,url}`
文件快照写入业务草稿,不保存
`am_material`
ID,不重新上传、不创建材料。
-
单值字段替换原值;多值字段追加并按 URL 去重,支持逐项删除。
-
本地文件选择后立即逐文件调用
`/upload`
,规范化为
`{name,type,size,url}`
并保留在当前详情草稿;上传期间禁止保存和修改附件。成功项保留,失败项给出文件级汇总并可重试。
-
只有用户点击详情“保存”时,才为仍在草稿中的本地文件逐个创建
`am_material`
。
`source_app`
必须使用目标应用真实稳定标识,
`uploader`
使用当前用户 ID,
`uploaded_at`
使用 ISO 时间;禁止复制
`issue_rectification`
。
-
所有待建档材料成功后才更新业务记录。部分建档成功或材料成功而业务更新失败时,当前编辑会话必须记住已建档项;再次保存只处理未建档项,不能产生重复材料。
-
取消编辑不创建材料主档;删除或替换业务附件不删除材料中心主档或底层文件,材料后续变化也不反向修改历史快照。
-
业务记录仍按目标字段既有协议保存单值对象或对象数组,不得改成材料 ID、引用字段或新数据结构。
## 7. HeroUI 架构边界
-
新页面和共享组件直接引用
`@heroui/react`
、
`@heroui-pro/react`
或 Pro 子路径;禁止 Catalyst、Headless UI、旧适配器和在旧组件内嵌 HeroUI。
-
DataGrid、Modal、Tabs、Autocomplete、Badge、Tooltip 和 DropZone 使用官方 compound API、slot 和 React Aria Context。
-
可新建业务无关的 HeroUI 原生组合,但应用授权标识、
`source_app`
、Subject、字段路径、路由和保存事务必须由目标业务层传入,不能硬编码问题整改名称。
-
这些功能允许新增其明确需要的路由、请求和状态;不得借机覆盖整页、复制问题整改业务流程或修改其它业务。
## 8. 验收
-
六个完整功能应用验证 Grid/Detail
`shortTitle`
的 true、false、缺失与动态切换。
-
验证系统管理员、目标应用管理角色、普通授权、无授权、无
`viewAuth`
、字段
`auth`
和
`disableColumns`
。
-
所有应用验证通知 0、1、99、100+,单条/全部已读、消息/待办、未读筛选、搜索、动态 action、站内外跳转和 Navbar 布局。
-
六个完整功能应用验证单值/多值附件、材料选择、本地上传、部分失败、取消、保存失败与无重复重试。
-
在 1440x900、1280x720、768x1024、390x844 的浅色/深色真实浏览器中验收,控制台无错误或 Context/slot 警告。
-
执行构建、
`git diff --check`
、鑫元审计脚本、旧组件扫描、HeroUI 依赖树和本机路径扫描。
docs/xinyuan-design-system.md
View file @
cd281496
# 鑫元问题整改系统设计规范
本文件是本项目新增功能和 UI 维护的执行标准。业务接口、参数、数据结构、路由、权限、校验、回调和状态流转不属于视觉迁移范围。
本文件是本项目新增功能和 UI 维护的执行标准。业务接口、参数、数据结构、路由、权限、校验、回调和状态流转不属于普通视觉迁移范围;下述跨应用五项功能是用户明确授权的窄范围功能扩展例外,例外之外的业务仍不得修改。
跨应用新增能力的范围矩阵、
`shortTitle`
、Subject View 权限、消息通知和审计材料中心文件事务统一遵循
[
xinyuan-cross-app-feature-contract.md
](
./xinyuan-cross-app-feature-contract.md
)
。六个指定应用实施全部五项,其它鑫元应用只实施消息通知;不得越过白名单扩展功能。
## 1. 唯一技术基线
...
...
@@ -59,6 +61,7 @@ Surface 用于表达页面内的包含关系,Overlay 用于表达浮动关系
-
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`
。
-
通知中心不占用 Sidebar 导航层级。存在真实通知功能时,在 Navbar 右侧操作组中使用 HeroUI
`Badge.Anchor + Badge + Button + Tooltip`
,通知按钮位于主题切换左侧,主题切换始终最右;按钮沿用 36px 中性圆形 Navbar 动作规范,图标为 16px Ant Design Bell。未读数只能来自真实接口,0 时不显示角标,超过 99 显示
`99+`
,点击仍进入应用原通知路由。
-
应用切换使用 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 图和加载失败回退,三者不得改变标题起点。
...
...
@@ -82,7 +85,8 @@ Surface 用于表达页面内的包含关系,Overlay 用于表达浮动关系
-
上述 class 只约束宽度、flex 收缩和换行;不得覆盖
`.tabs__list-container`
、
`.tabs__list`
、
`.tabs__tab`
、
`.tabs__indicator`
的背景、边框、圆角、颜色、阴影、尺寸、Hover 或选中态,也不得把文档示例 CSS 复制为项目私有 Tabs 皮肤。
-
结构化筛选区直接使用 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 长文本必须由共享层自适应识别:优先读取动态视图字段类型,其次识别问题描述、整改情况、措施、要求、原因、意见、备注、说明等字段语义,最后以当前真实数据样本长度兜底。一般长文本列基准最小宽度为 360px,“整改情况”为 420px;作为左侧固定列时分别限制为 320px/360px。单元格最多显示两行并在自身边界内裁切,超出内容通过 HeroUI Tooltip 展示全文,任何文字都不得跨越单元格遮挡相邻字段。
-
DataGrid 是否进入宽表模式不得只按列数判断。共享层必须比较所有动态列的可读最小宽度总和与 DataGrid 当前容器实宽;超过时保留像素最小宽度并只在 DataGrid 内横向滚动,未超过时才用
`1fr`
铺满。开发修改视图列、字段标题或数据内容后,列宽、换行和滚动策略必须自动重算,不得写死旧表头。
-
DataGrid 的用户类型字段直接使用 HeroUI
`Avatar`
:列表使用
`size="sm"`
、
`variant="soft"`
,有头像资源时显示图片,无资源时显示姓名首字回退;头像与姓名水平居中,人员列基准宽度不小于 220px,不使用页面私有渐变、边框或旧 Avatar 组件。
-
驾驶舱、分析页的单一年度范围使用紧凑 HeroUI Autocomplete,选择值本身表达“全部年份”或具体年份时不再重复显示外置“统计年份”标签;触发器使用官方
`primary`
外观和内容宽度,不添加页面私有胶囊、阴影或边框。
-
内容型详情弹窗使用 HeroUI Modal
`4xl`
基准宽度 1088px、内部滚动和 28px 内容 inset;全屏与关闭图标在标题栏右上组成同一 24px 操作组并垂直居中。详情字段使用无框标签/值网格,不为每个字段重复创建带阴影 Card。
...
...
src/components/AppShell.jsx
View file @
cd281496
...
...
@@ -20,6 +20,8 @@ function getInitialTheme() {
export
default
function
AppShell
({
children
,
notificationCount
,
notificationPath
,
open
,
onOpenChange
,
title
,
...
...
@@ -52,6 +54,10 @@ export default function AppShell({
<
DashboardSidebar
{
...
sidebarProps
}
/>
<
Sidebar
.
Main
className=
"app-main-glass rc-app-main"
>
<
DashboardNavbar
notificationCount=
{
notificationCount
}
onOpenNotifications=
{
notificationPath
?
()
=>
history
.
push
(
notificationPath
)
:
undefined
}
theme=
{
theme
}
title=
{
title
}
onThemeChange=
{
setTheme
}
...
...
src/components/DashboardNavbar.jsx
View file @
cd281496
import
{
Navbar
}
from
'@heroui-pro/react/navbar'
;
import
{
Sidebar
,
useSidebar
}
from
'@heroui-pro/react/sidebar'
;
import
{
Button
,
Tooltip
}
from
'@heroui/react'
;
import
{
Moon
,
SidebarPanel
,
Sun
}
from
'./AppIcons'
;
import
{
B
adge
,
B
utton
,
Tooltip
}
from
'@heroui/react'
;
import
{
Bell
,
Moon
,
SidebarPanel
,
Sun
}
from
'./AppIcons'
;
export
default
function
DashboardNavbar
({
onThemeChange
,
theme
,
title
})
{
export
default
function
DashboardNavbar
({
notificationCount
=
0
,
onOpenNotifications
,
onThemeChange
,
theme
,
title
,
})
{
const
{
isMobile
,
isOpen
}
=
useSidebar
();
const
isDark
=
theme
===
'dark'
;
const
themeLabel
=
isDark
?
'切换至浅色模式'
:
'切换至深色模式'
;
const
normalizedNotificationCount
=
Math
.
max
(
0
,
Number
(
notificationCount
)
||
0
);
return
(
<
Navbar
className=
"app-navbar rc-navbar"
maxWidth=
"full"
>
...
...
@@ -20,6 +27,34 @@ export default function DashboardNavbar({ onThemeChange, theme, title }) {
<
h1
className=
"app-navbar-page-title rc-navbar-title"
>
{
title
}
</
h1
>
<
Navbar
.
Spacer
/>
<
div
className=
"rc-navbar-actions"
>
{
onOpenNotifications
?
(
<
Tooltip
delay=
{
0
}
>
<
Badge
.
Anchor
>
<
Button
isIconOnly
aria
-
label=
"通知消息"
className=
"app-navbar-icon-button rc-navbar-icon-button"
size=
"sm"
variant=
"ghost"
onPress=
{
onOpenNotifications
}
>
<
Bell
size=
{
16
}
/>
</
Button
>
{
normalizedNotificationCount
>
0
?
(
<
Badge
aria
-
label=
{
`${normalizedNotificationCount}条未读通知`
}
color=
"danger"
size=
"sm"
>
{
normalizedNotificationCount
>
99
?
'99+'
:
normalizedNotificationCount
}
</
Badge
>
)
:
null
}
</
Badge
.
Anchor
>
<
Tooltip
.
Content
>
通知消息
</
Tooltip
.
Content
>
</
Tooltip
>
)
:
null
}
<
Tooltip
delay=
{
0
}
>
<
Button
isIconOnly
...
...
src/components/DataTable.jsx
View file @
cd281496
...
...
@@ -10,6 +10,7 @@ import {
Spinner
,
}
from
'@heroui/react'
;
import
{
Tooltip
}
from
'@heroui/react'
;
import
{
useLayoutEffect
,
useRef
,
useState
}
from
'react'
;
function
normalizeDataTableEmptyTitle
(
title
)
{
const
text
=
String
(
title
||
''
).
trim
();
...
...
@@ -78,6 +79,15 @@ function resolveColumnWidth(width) {
return
undefined
;
}
function
resolveColumnMinWidth
(
column
)
{
const
isActions
=
column
.
isActions
||
column
.
key
===
'actions'
;
const
configuredWidth
=
resolveColumnWidth
(
column
.
width
);
const
numericWidth
=
typeof
configuredWidth
===
'number'
?
configuredWidth
:
undefined
;
return
Number
(
column
.
minWidth
)
||
numericWidth
||
(
isActions
?
104
:
128
);
}
export
function
DataTable
({
ariaLabel
=
'数据表格'
,
columns
,
...
...
@@ -95,9 +105,37 @@ export function DataTable({
onSortChange
,
renderSelectionActions
,
})
{
const
containerRef
=
useRef
(
null
);
const
[
containerWidth
,
setContainerWidth
]
=
useState
(
0
);
const
selectionEnabled
=
Boolean
(
selectedRowKeys
&&
onSelectedRowKeysChange
);
const
selectedKeys
=
selectionEnabled
?
new
Set
(
selectedRowKeys
)
:
undefined
;
const
wide
=
isWide
??
columns
.
length
>
6
;
const
minimumContentWidth
=
columns
.
reduce
(
(
total
,
column
)
=>
total
+
resolveColumnMinWidth
(
column
),
selectionEnabled
?
40
:
0
,
);
const
measuredWide
=
containerWidth
>
0
?
minimumContentWidth
>
containerWidth
+
1
:
columns
.
length
>
6
;
const
wide
=
isWide
===
true
||
measuredWide
;
useLayoutEffect
(()
=>
{
const
container
=
containerRef
.
current
;
if
(
!
container
)
{
return
undefined
;
}
const
updateWidth
=
()
=>
setContainerWidth
(
container
.
clientWidth
);
updateWidth
();
if
(
typeof
ResizeObserver
===
'undefined'
)
{
return
undefined
;
}
const
observer
=
new
ResizeObserver
(
updateWidth
);
observer
.
observe
(
container
);
return
()
=>
observer
.
disconnect
();
},
[]);
const
firstBusinessColumnIndex
=
columns
.
findIndex
(
(
column
)
=>
!
column
.
isSelection
&&
...
...
@@ -109,15 +147,10 @@ export function DataTable({
const
isPrimary
=
column
.
isPrimary
??
(
!
isActions
&&
index
===
firstBusinessColumnIndex
);
const
configuredWidth
=
resolveColumnWidth
(
column
.
width
);
const
numericWidth
=
typeof
configuredWidth
===
'number'
?
configuredWidth
:
undefined
;
const
pinned
=
wide
?
column
.
pin
||
(
isActions
?
'end'
:
undefined
)
:
undefined
;
const
minWidth
=
column
.
minWidth
||
numericWidth
||
(
isActions
?
104
:
pinned
?
220
:
128
);
const
minWidth
=
resolveColumnMinWidth
(
column
);
const
width
=
!
wide
&&
isPrimary
?
typeof
configuredWidth
===
'string'
&&
configuredWidth
.
endsWith
(
'fr'
)
...
...
@@ -152,6 +185,7 @@ export function DataTable({
headerClassName
:
column
.
headerClassName
,
cellClassName
:
column
.
cellClassName
,
allowsResizing
:
true
,
maxWidth
:
resolveColumnWidth
(
column
.
maxWidth
),
minWidth
,
width
,
pinned
,
...
...
@@ -178,7 +212,10 @@ export function DataTable({
};
return
(
<
div
className=
{
[
'relative min-w-0 w-full'
,
className
].
filter
(
Boolean
).
join
(
' '
)
}
>
<
div
ref=
{
containerRef
}
className=
{
[
'relative min-w-0 w-full'
,
className
].
filter
(
Boolean
).
join
(
' '
)
}
>
<
div
className=
"relative min-w-0 w-full"
>
{
isLoading
&&
rows
.
length
>
0
?
(
<
div
...
...
@@ -272,20 +309,20 @@ export function DataTableText({
maxLines
=
1
,
})
{
const
fullText
=
String
(
text
??
children
??
''
).
trim
();
const
multiline
ClassName
=
const
text
ClassName
=
maxLines
===
2
?
'overflow-hidden whitespace-normal break-words [display:-webkit-box] [-webkit-box-orient:vertical] [-webkit-line-clamp:2] [overflow-wrap:anywhere]'
:
''
;
?
'
w-full min-w-0 max-w-full
overflow-hidden whitespace-normal break-words [display:-webkit-box] [-webkit-box-orient:vertical] [-webkit-line-clamp:2] [overflow-wrap:anywhere]'
:
'
block w-full min-w-0 max-w-full overflow-hidden truncate
'
;
const
trigger
=
showTooltip
?
(
<
DataTableTextTooltipTrigger
className=
{
[
multiline
ClassName
,
className
].
filter
(
Boolean
).
join
(
' '
)
}
className=
{
[
text
ClassName
,
className
].
filter
(
Boolean
).
join
(
' '
)
}
text=
{
fullText
}
truncate=
{
maxLines
===
1
}
truncate=
{
false
}
>
{
children
??
(
fullText
||
'-'
)
}
</
DataTableTextTooltipTrigger
>
)
:
(
<
span
className=
{
[
'block min-w-0 truncate'
,
className
].
filter
(
Boolean
).
join
(
' '
)
}
>
<
span
className=
{
[
textClassName
,
className
].
filter
(
Boolean
).
join
(
' '
)
}
>
{
children
??
(
fullText
||
'-'
)
}
</
span
>
);
...
...
@@ -300,9 +337,9 @@ export function DataTableText({
return
(
<
DataTableTextTooltipTrigger
className=
{
[
multiline
ClassName
,
className
].
filter
(
Boolean
).
join
(
' '
)
}
className=
{
[
text
ClassName
,
className
].
filter
(
Boolean
).
join
(
' '
)
}
text=
{
fullText
}
truncate=
{
maxLines
===
1
}
truncate=
{
false
}
>
{
children
??
(
fullText
||
'-'
)
}
</
DataTableTextTooltipTrigger
>
...
...
src/components/record-workbench/index.jsx
View file @
cd281496
import
{
DataTable
,
DataTablePagination
,
DataTableText
,
}
from
'@/components/DataTable'
;
import
{
KPI
}
from
'@heroui-pro/react/kpi'
;
import
{
...
...
@@ -289,16 +290,75 @@ function defaultGetRecordLabel(record) {
function
defaultRenderCell
(
record
,
column
)
{
const
sourceRecord
=
record
?.
raw
||
record
;
const
value
=
getFieldValue
(
sourceRecord
,
column
.
field
);
const
text
=
formatFieldValue
(
value
,
column
.
fieldDef
)
||
'-'
;
return
(
<
div
className=
"truncate text-sm text-foreground"
>
{
formatFieldValue
(
value
,
column
.
fieldDef
)
||
'-'
}
</
div
>
<
DataTableText
className=
"text-sm text-foreground"
maxLines=
{
column
.
dataGridPresentation
?.
maxLines
||
1
}
showTooltip=
{
Boolean
(
column
.
dataGridPresentation
?.
showTooltip
)
}
text=
{
text
}
/>
);
}
const
LONG_TEXT_FIELD_TYPES
=
new
Set
([
'html'
,
'long_text'
,
'markdown'
,
'rich_text'
,
'textarea'
,
]);
const
SHORT_VALUE_FIELD_TYPES
=
new
Set
([
'boolean'
,
'date'
,
'date_time'
,
'datetime'
,
'file'
,
'number'
,
'reference'
,
'select'
,
'user'
,
]);
function
resolveColumnDataGridPresentation
(
column
,
records
=
[])
{
const
label
=
String
(
column
?.
label
||
''
);
const
field
=
String
(
column
?.
field
||
''
).
toLowerCase
();
const
fieldType
=
String
(
column
?.
fieldDef
?.
type
||
''
).
toLowerCase
();
const
isTextLike
=
!
fieldType
||
!
SHORT_VALUE_FIELD_TYPES
.
has
(
fieldType
);
const
hasLongTextSemantics
=
/
(
problem_brief|description|rectification_situation|rectification_measure|requirement|delay_reason|remark|comment|opinion|explanation|content
)
/
.
test
(
field
)
||
/问题简述|问题描述|具体问题|整改情况|整改措施|整改要求|延期事由|备注|说明|意见|内容/
.
test
(
label
);
const
longestSampleLength
=
isTextLike
?
records
.
slice
(
0
,
20
).
reduce
((
longest
,
record
)
=>
{
const
sourceRecord
=
record
?.
raw
||
record
;
const
value
=
getFieldValue
(
sourceRecord
,
column
.
field
);
const
text
=
normalizeText
(
formatFieldValue
(
value
,
column
.
fieldDef
));
return
Math
.
max
(
longest
,
Array
.
from
(
text
).
length
);
},
0
)
:
0
;
const
isLongText
=
LONG_TEXT_FIELD_TYPES
.
has
(
fieldType
)
||
hasLongTextSemantics
||
longestSampleLength
>
36
;
const
isRectificationSituation
=
field
.
includes
(
'rectification_situation'
)
||
/整改情况/
.
test
(
label
);
return
{
isLongText
,
maxLines
:
isLongText
?
2
:
1
,
minWidth
:
isRectificationSituation
?
420
:
isLongText
?
360
:
undefined
,
showTooltip
:
isLongText
,
};
}
function
getReadableColumnWidth
(
column
)
{
const
configuredWidth
=
Number
(
column
?.
width
)
||
180
;
const
label
=
String
(
column
?.
label
||
''
);
const
presentationWidth
=
column
?.
dataGridPresentation
?.
minWidth
;
if
(
presentationWidth
)
{
return
Math
.
max
(
configuredWidth
,
presentationWidth
);
}
if
(
/整改情况/
.
test
(
label
))
{
return
Math
.
max
(
configuredWidth
,
420
);
...
...
@@ -329,6 +389,12 @@ function getWorkbenchColumnWidth(column, stickyMeta) {
}
const
label
=
String
(
column
?.
label
||
''
);
if
(
column
?.
dataGridPresentation
?.
isLongText
)
{
return
Math
.
min
(
readableWidth
,
/整改情况/
.
test
(
label
)
?
360
:
320
,
);
}
if
(
/整改情况/
.
test
(
label
))
{
return
Math
.
min
(
readableWidth
,
360
);
}
...
...
@@ -495,17 +561,25 @@ export default function RecordWorkbench({
const
loading
=
Boolean
(
states
?.
loading
);
const
error
=
states
?.
error
||
''
;
const
totalRecords
=
pagination
?.
totalRecords
??
records
.
length
;
const
presentationColumns
=
useMemo
(
()
=>
columns
.
map
((
column
)
=>
({
...
column
,
dataGridPresentation
:
resolveColumnDataGridPresentation
(
column
,
records
),
})),
[
columns
,
records
],
);
const
columnGroupMeta
=
useMemo
(
()
=>
buildColumnGroupMeta
(
columnGroups
,
c
olumns
),
[
columnGroups
,
c
olumns
],
()
=>
buildColumnGroupMeta
(
columnGroups
,
presentationC
olumns
),
[
columnGroups
,
presentationC
olumns
],
);
const
visibleColumns
=
useMemo
(
()
=>
c
olumns
.
filter
((
column
)
=>
{
presentationC
olumns
.
filter
((
column
)
=>
{
const
groupId
=
columnGroupMeta
.
fieldToGroup
[
column
.
field
];
return
!
groupId
||
!
collapsedColumnGroups
[
groupId
];
}),
[
c
olumns
,
columnGroupMeta
.
fieldToGroup
,
collapsedColumnGroups
],
[
presentationC
olumns
,
columnGroupMeta
.
fieldToGroup
,
collapsedColumnGroups
],
);
const
orderedVisibleColumns
=
useMemo
(
()
=>
...
...
@@ -1201,7 +1275,6 @@ export default function RecordWorkbench({
disabledRowKeys={disabledDataGridRowKeys}
emptyTitle={states?.emptyTitle || '暂无数据'}
emptyDescription={states?.emptyDescription}
isWide={dataGridColumns.length > 6}
renderSelectionActions={() => (
<>
<span className="text-sm">已选择记录</span>
...
...
src/pages/problem_rectification/Layout.jsx
View file @
cd281496
...
...
@@ -6,7 +6,6 @@ import AppShell from '@/components/AppShell';
import
{
Archive
,
BarChart
,
Bell
,
CheckCircleIcon
,
Project
,
}
from
'@/components/AppIcons'
;
...
...
@@ -45,12 +44,6 @@ const MENU_GROUPS = [
path
:
'/issue_rectification_archive'
,
icon
:
Archive
,
},
{
name
:
'通知消息'
,
path
:
'/notifications'
,
icon
:
Bell
,
showsNotificationCount
:
true
,
},
],
},
];
...
...
@@ -63,20 +56,6 @@ export default function ProblemRectificationLayout() {
const
[
isApplicationDialogOpen
,
setIsApplicationDialogOpen
]
=
useState
(
false
);
const
[
unreadNotificationCount
,
setUnreadNotificationCount
]
=
useState
(
0
);
const
menuGroups
=
useMemo
(
()
=>
MENU_GROUPS
.
map
((
group
)
=>
({
...
group
,
items
:
group
.
items
.
map
((
item
)
=>
({
...
item
,
count
:
item
.
showsNotificationCount
?
unreadNotificationCount
:
undefined
,
})),
})),
[
unreadNotificationCount
],
);
const
currentTitle
=
useMemo
(
()
=>
ROUTE_TITLES
[
location
.
pathname
]
||
APPLICATION_DISPLAY_NAME
,
[
location
.
pathname
],
...
...
@@ -112,7 +91,9 @@ export default function ProblemRectificationLayout() {
return
(
<>
<
AppShell
menuGroups=
{
menuGroups
}
menuGroups=
{
MENU_GROUPS
}
notificationCount=
{
unreadNotificationCount
}
notificationPath=
"/notifications"
onLogout=
{
userInfo
?.
logout
}
onOpenApplicationSwitcher=
{
()
=>
setIsApplicationDialogOpen
(
true
)
}
open=
{
sidebarOpen
}
...
...
src/pages/problem_rectification/list/index.jsx
View file @
cd281496
...
...
@@ -650,7 +650,7 @@ function FieldValuePreview({ column, record }) {
return (
<DataTableText
className="text-sm leading-6 text-foreground"
maxLines={2}
maxLines={
column.dataGridPresentation?.maxLines ||
2}
showTooltip
text={text}
>
...
...
@@ -694,10 +694,16 @@ function FieldValuePreview({ column, record }) {
}
const text = getRecordCellText(record, column);
const isLongText = ['long_text', 'rich_text'].includes(column.fieldDef?.type);
const isLongText =
column.dataGridPresentation?.isLongText ||
['long_text', 'rich_text'].includes(column.fieldDef?.type);
return (
<DataTableText text={text} showTooltip={isLongText}>
<DataTableText
maxLines={isLongText ? 2 : 1}
text={text}
showTooltip={isLongText}
>
{text}
</DataTableText>
);
...
...
src/pages/problem_rectification/problem-rectification.business-flow.md
View file @
cd281496
...
...
@@ -964,7 +964,7 @@ const result = await queryUsers(0, 20, {
### 17.1 功能入口
“通知消息”作为问题整改应用
侧边栏一级入口,路由为
`/notifications`
。侧边栏通过
`GET /notice/customer/count`
展示当前用户未读总数,超过 99 条显示
`99+`
;未读数为 0 时不展示角标。页面
不在顶部导航重复提供通知弹窗。
“通知消息”作为问题整改应用
Navbar 全局入口,路由仍为
`/notifications`
,不占用侧边栏导航层级。Navbar 通过
`GET /notice/customer/count`
展示当前用户未读总数,超过 99 条显示
`99+`
;未读数为 0 时不展示角标。入口仅负责进入通知页面,
不在顶部导航重复提供通知弹窗。
### 17.2 查询与筛选
...
...
@@ -979,7 +979,7 @@ const result = await queryUsers(0, 20, {
-
通知的
`data.logs`
中存在当前用户
`_id`
时视为已读;
`message`
显示“已读 / 未读”,
`todo`
显示“已完成 / 待处理”。
-
“标为已读”调用
`PUT /notice/customer/read`
并传通知
`name`
。
-
“全部已读”调用同一接口但不传
`name`
,处理当前用户全部通知;未读总数为 0 时按钮禁用。
-
待办按钮完全由通知
`actions`
配置生成,执行时只提交
`{notice_id: 通知 _id}`
。操作期间禁用当前通知的重复操作,成功后刷新列表和
侧边栏
角标,失败时保留原状态并提示错误。
-
待办按钮完全由通知
`actions`
配置生成,执行时只提交
`{notice_id: 通知 _id}`
。操作期间禁用当前通知的重复操作,成功后刷新列表和
Navbar
角标,失败时保留原状态并提示错误。
### 17.4 进入关联流程
...
...
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