Commit 0b9490a0 authored by Andy-bubu's avatar Andy-bubu

Merge remote-tracking branch 'origin/codex/ui-ux-xinyuan-migration' into…

Merge remote-tracking branch 'origin/codex/ui-ux-xinyuan-migration' into codex/ui-ux-xinyuan-migration
parents 89a10892 a2e249fe
...@@ -21,8 +21,9 @@ ...@@ -21,8 +21,9 @@
- **常量**:使用全大写下划线 (SNAKE_CASE)。 - **常量**:使用全大写下划线 (SNAKE_CASE)。
- **技术栈要求**: - **技术栈要求**:
- 使用 React Hooks 和函数组件。 - 使用 React Hooks 和函数组件。
- 页面和共享 UI 组件必须直接使用 `@heroui/react` 与 `@heroui-pro/react`。 - HeroUI 与 HeroUI Pro 是项目唯一 UI 组件体系。页面和共享 UI 组件必须直接使用 `@heroui/react` 与 `@heroui-pro/react`。
- 禁止新增或扩展 `src/catalyst-ui-kit`、`@headlessui/react`、旧 UI 适配器和可见原生表单控件。 - `src/catalyst-ui-kit` 已废弃,仅作为待清理的历史源码保留。禁止在新功能、存量维护、重构、示例和测试中引用、复制或扩展其中的组件。
- 禁止新增 `@headlessui/react`、旧 UI 适配器或其他替代组件库,禁止以包装层兼容 Catalyst props,禁止使用可见原生表单控件替代 HeroUI。
- HeroUI 复合组件遵守官方 compound component 与 `slot` 结构,交互使用 `onPress`。 - HeroUI 复合组件遵守官方 compound component 与 `slot` 结构,交互使用 `onPress`。
- DataGrid 使用 HeroUI Pro 原生选择、排序、分页、固定列和 ActionBar,不手写选择列。 - DataGrid 使用 HeroUI Pro 原生选择、排序、分页、固定列和 ActionBar,不手写选择列。
...@@ -40,7 +41,7 @@ ...@@ -40,7 +41,7 @@
## 6. UI 规范(鑫元 HeroUI 强制约束) ## 6. UI 规范(鑫元 HeroUI 强制约束)
- **全项目 UI 统一**:遵循 `xinyuan-design-system` 与 `docs/xinyuan-design-system.md`。 - **全项目 UI 统一**:遵循 `xinyuan-design-system` 与 `docs/xinyuan-design-system.md`。
- **直接引用**:页面不得通过 Catalyst、Headless UI 或兼容包装层间接使用 HeroUI。 - **直接引用**:页面与共享组件必须从 `@heroui/react`、`@heroui-pro/react` 的官方导出直接组合;不得通过 Catalyst、Headless UI、旧适配器或兼容包装层间接使用。
- **一致性**:表单使用 HeroUI,复杂表格和 Shell 使用 HeroUI Pro,弹窗使用 HeroUI Modal/AlertDialog。 - **一致性**:表单使用 HeroUI,复杂表格和 Shell 使用 HeroUI Pro,弹窗使用 HeroUI Modal/AlertDialog。
- **默认外观**:除 `src/design-system/theme.css` 的主题变量和批准的 Login/AppShell 布局外,组件尺寸、圆角、边框、阴影、Hover、选中态均使用 HeroUI/HeroUI Pro 默认实现;禁止页面 CSS 重画组件。 - **默认外观**:除 `src/design-system/theme.css` 的主题变量和批准的 Login/AppShell 布局外,组件尺寸、圆角、边框、阴影、Hover、选中态均使用 HeroUI/HeroUI Pro 默认实现;禁止页面 CSS 重画组件。
- **主题唯一性**:`src/design-system/theme.css` 是组件主题唯一权威;`tokens.css` 只允许提供 Shell/Login 别名,不得覆盖 DataGrid、Tabs、Card、Modal、Checkbox、Pagination 或图表样式。 - **主题唯一性**:`src/design-system/theme.css` 是组件主题唯一权威;`tokens.css` 只允许提供 Shell/Login 别名,不得覆盖 DataGrid、Tabs、Card、Modal、Checkbox、Pagination 或图表样式。
...@@ -82,13 +83,14 @@ AI 在编写 API 调用或数据转换逻辑时,必须遵守: ...@@ -82,13 +83,14 @@ AI 在编写 API 调用或数据转换逻辑时,必须遵守:
1. **Step 1**: 查找 `subjectsDefinition.md` 获取该 Subject 的基础字段。 1. **Step 1**: 查找 `subjectsDefinition.md` 获取该 Subject 的基础字段。
2. **Step 2**: 检查字段类型。如果遇到 `Subject` 类型,**自动生成**一段调用 `loadSubject` 的代码以获取深层结构。 2. **Step 2**: 检查字段类型。如果遇到 `Subject` 类型,**自动生成**一段调用 `loadSubject` 的代码以获取深层结构。
3. **Step 3**: 参照上表(第 1 节)选择 tailwind (或你指定的 UI 库) 的组件。 3. **Step 3**: 参照上表(第 1 节)选择 HeroUI/HeroUI Pro 官方组件;Tailwind 仅用于页面布局,不得用于自制或重画交互组件。
4. **Step 4**: 自动生成 `onChange` 处理函数,确保数据实时同步到 `metadata` 对象的对应路径下。 4. **Step 4**: 自动生成 `onChange` 处理函数,确保数据实时同步到 `metadata` 对象的对应路径下。
--- ---
## 4. 强制约束 (Hard Constraints) ## 4. 强制约束 (Hard Constraints)
- **组件库唯一性**:任何需求都不得重新引入 Catalyst、Headless UI 或其它 UI 组件库;HeroUI/HeroUI Pro 暂无对应组件时,应先按官方 compound API 组合,并遵循 `docs/xinyuan-design-system.md`,不得回退到 `src/catalyst-ui-kit`。
- **禁止硬编码**:Select 类型的选项必须从 `settings.options` 读取,严禁在代码里写死。 - **禁止硬编码**:Select 类型的选项必须从 `settings.options` 读取,严禁在代码里写死。
- **主键保护**:如果字段定义中 `isPrimaryKey: true`,在编辑模式下该组件必须设为 `disabled`。 - **主键保护**:如果字段定义中 `isPrimaryKey: true`,在编辑模式下该组件必须设为 `disabled`。
- **必填校验**:根据 `isRequired` 自动在 UI 上添加红星标记,并生成相应的 Form Validation 规则。 - **必填校验**:根据 `isRequired` 自动在 UI 上添加红星标记,并生成相应的 Form Validation 规则。
......
...@@ -2,10 +2,11 @@ ...@@ -2,10 +2,11 @@
## 组件边界 ## 组件边界
- 页面直接使用 `@heroui/react` 和 `@heroui-pro/react`。 - HeroUI 与 HeroUI Pro 是全项目唯一 UI 组件体系;页面、共享组件、示例和测试直接使用 `@heroui/react` 和 `@heroui-pro/react`。
- 禁止在 `catalyst-ui-kit`、Headless UI 或旧适配器内部嵌入 HeroUI。 - `src/catalyst-ui-kit` 已停止使用,仅作为待清理的历史源码隔离保留,不属于可用组件目录;新功能、存量维护和重构都不得引用、复制或扩展其中的实现。
- 禁止继续扩展 `catalyst-ui-kit`;新功能不得引用旧 UI 组件。 - 禁止引入 Headless UI、其它替代组件库或旧适配器,禁止在兼容包装层内嵌入 HeroUI 并继续暴露 Catalyst props。
- 允许的共享组件只组合 HeroUI/HeroUI Pro 官方复合结构,不复刻旧 props,不重画组件外观。 - 允许的共享组件只组合 HeroUI/HeroUI Pro 官方复合结构,不复刻旧 props,不重画组件外观。
- HeroUI/HeroUI Pro 暂无直接对应组件时,优先组合官方 compound API;确需补充基础能力时先更新架构与设计系统评审结论,不得回退到 Catalyst 或可见原生控件。
- 业务数据、接口参数、权限、校验、保存回调和状态流转继续由页面与 service 层拥有。 - 业务数据、接口参数、权限、校验、保存回调和状态流转继续由页面与 service 层拥有。
## 标准实现 ## 标准实现
......
# 鑫元 UI 迁移清单 # 鑫元 UI 迁移清单
> 当前规范:HeroUI 与 HeroUI Pro 已成为项目唯一 UI 组件体系。下表“当前来源”列仅记录迁移前状态,不代表仍可使用;新功能、存量维护、重构、示例和测试均不得引用或复制 `src/catalyst-ui-kit`,也不得重新引入 Headless UI 或旧适配器。
状态:`待迁移`、`迁移中`、`待浏览器验收`、`完成`。 状态:`待迁移`、`迁移中`、`待浏览器验收`、`完成`。
| 页面/模块 | 路由 | 当前来源 | 目标组件 | 必须保留的业务边界 | 状态 | 浏览器验收 | | 页面/模块 | 路由 | 当前来源 | 目标组件 | 必须保留的业务边界 | 状态 | 浏览器验收 |
...@@ -17,7 +19,7 @@ ...@@ -17,7 +19,7 @@
## 验收门槛 ## 验收门槛
- 已迁移范围的 Catalyst、Headless UI、旧适配器和可见原生控件引用为零。 - 全部运行代码、示例和测试中的 Catalyst、Headless UI、旧适配器和可见原生控件引用为零;历史隔离目录不得进入构建或依赖图。
- Checkbox 单选、取消、全选、半选和禁用状态无 Context/slot 错误。 - Checkbox 单选、取消、全选、半选和禁用状态无 Context/slot 错误。
- Autocomplete、SearchField、ListBox、Modal、Tooltip、ActionBar 在深浅主题和移动端正确继承 Portal 主题。 - Autocomplete、SearchField、ListBox、Modal、Tooltip、ActionBar 在深浅主题和移动端正确继承 Portal 主题。
- DataGrid 横向滚动、固定列、分页、排序、选择和批量操作保持原业务结果。 - DataGrid 横向滚动、固定列、分页、排序、选择和批量操作保持原业务结果。
...@@ -30,7 +32,7 @@ ...@@ -30,7 +32,7 @@
- 驾驶舱和整改分析已移除 ECharts、私有 option、主题监听和 HTML Tooltip,改用 HeroUI Pro KPI、PieChart、BarChart、ComposedChart 与默认 ChartTooltip。 - 驾驶舱和整改分析已移除 ECharts、私有 option、主题监听和 HTML Tooltip,改用 HeroUI Pro KPI、PieChart、BarChart、ComposedChart 与默认 ChartTooltip。
- 整改处理、整改流程、问题清单、详情与引用的页面私有旧色类已清零,状态、提示、空状态、卡片与 KPI 已切换到 HeroUI/HeroUI Pro 和鑫元语义 Token。 - 整改处理、整改流程、问题清单、详情与引用的页面私有旧色类已清零,状态、提示、空状态、卡片与 KPI 已切换到 HeroUI/HeroUI Pro 和鑫元语义 Token。
- 未启用的 `ResourceManagementUi.jsx`、`ModuleEmptyState.jsx` 已同步迁移,避免后续功能重新引用旧式表面。 - 未启用的 `ResourceManagementUi.jsx`、`ModuleEmptyState.jsx` 已同步迁移,避免后续功能重新引用旧式表面。
- 业务页面与共享组件中的 Catalyst/Headless/Heroicons 引用、可见原生控件和旧调色板类扫描均为零;隔离保留的 `src/catalyst-ui-kit` 不属于运行引用范围。 - 业务页面、共享组件、示例和测试中的 Catalyst/Headless/Heroicons 引用、可见原生控件和旧调色板类扫描均为零;隔离保留的 `src/catalyst-ui-kit` 仅属待清理历史源码,不属于可用组件目录、构建范围或运行依赖。
- HeroUI Pro CSS 与六个要求的组件子路径可解析/导入,HeroUI/React Aria 依赖树为单套兼容版本。 - HeroUI Pro CSS 与六个要求的组件子路径可解析/导入,HeroUI/React Aria 依赖树为单套兼容版本。
- `npm run build` 与 `git diff --check` 已通过。 - `npm run build` 与 `git diff --check` 已通过。
- 当前桌面会话没有可用浏览器连接,因此表格勾选、Portal、暗色切换和响应式仍保持“待浏览器验收”,不以构建结果替代。 - 当前桌面会话没有可用浏览器连接,因此表格勾选、Portal、暗色切换和响应式仍保持“待浏览器验收”,不以构建结果替代。
......
...@@ -4,12 +4,14 @@ ...@@ -4,12 +4,14 @@
## 1. 唯一技术基线 ## 1. 唯一技术基线
- 页面和共享组件直接使用 `@heroui/react`、`@heroui-pro/react`。 - HeroUI 与 HeroUI Pro 是项目唯一 UI 组件体系;页面、共享组件、示例和测试直接使用 `@heroui/react`、`@heroui-pro/react`。
- HeroUI Pro 精确固定为 `1.0.0-beta.8`,通过官方 `hpsetup` 授权流程获取完整制品;禁止仓库内 `vendor` 镜像、跨项目 `file:` 路径和符号链接。 - HeroUI Pro 精确固定为 `1.0.0-beta.8`,通过官方 `hpsetup` 授权流程获取完整制品;禁止仓库内 `vendor` 镜像、跨项目 `file:` 路径和符号链接。
- 项目以 npm 和 `package-lock.json` 为唯一包管理基线。本地使用 `HEROUI_KEY=<team-secret> npx -y hpsetup@latest --auto`;CI 将 `HEROUI_KEY` 设为受保护、已遮罩的环境变量。授权密钥不得写入仓库或文档。 - 项目以 npm 和 `package-lock.json` 为唯一包管理基线。本地使用 `HEROUI_KEY=<team-secret> npx -y hpsetup@latest --auto`;CI 将 `HEROUI_KEY` 设为受保护、已遮罩的环境变量。授权密钥不得写入仓库或文档。
- 图标统一从 `src/components/AppIcons.jsx` 引用 Ant Design Icons;业务页面不得引入 Heroicons、Lucide、Iconify 或自绘功能图标。 - 图标统一从 `src/components/AppIcons.jsx` 引用 Ant Design Icons;业务页面不得引入 Heroicons、Lucide、Iconify 或自绘功能图标。
- 禁止引用或扩展 `src/catalyst-ui-kit`,禁止旧组件适配器,禁止在旧组件内部嵌套 HeroUI。 - `src/catalyst-ui-kit` 已废弃并退出运行时,只能作为待清理的历史源码隔离保留;禁止在新功能、存量维护、重构、示例或测试中引用、复制、扩展或恢复其依赖。
- 禁止引入 `@headlessui/react`、其它替代组件库或旧组件适配器,禁止在旧组件内部嵌套 HeroUI,禁止包装 HeroUI 来兼容 Catalyst props。
- 共享组件只能组合 HeroUI/HeroUI Pro 的官方 compound API,不得复刻旧组件 props。 - 共享组件只能组合 HeroUI/HeroUI Pro 的官方 compound API,不得复刻旧组件 props。
- 官方组件暂不覆盖的场景应优先组合 HeroUI/HeroUI Pro compound API,并在本设计系统中补充经过评审的约定;不得以 Catalyst 或可见原生控件作为降级方案。
## 2. 主题权威 ## 2. 主题权威
...@@ -152,7 +154,7 @@ Surface 用于表达页面内的包含关系,Overlay 用于表达浮动关系 ...@@ -152,7 +154,7 @@ Surface 用于表达页面内的包含关系,Overlay 用于表达浮动关系
## 9. 验收 ## 9. 验收
- 扫描活动页面和共享组件,Catalyst、Headless UI、旧适配器、其他图标库、可见原生控件和私有组件 CSS 必须为零。 - 扫描全部运行代码、示例和测试,Catalyst、Headless UI、旧适配器、其他图标库、可见原生控件和私有组件 CSS 引用必须为零;`src/catalyst-ui-kit` 历史隔离目录不得进入构建或依赖图。
- 检查 `npm ls @heroui-pro/react @heroui/react @heroui/styles react-aria-components @react-aria/utils`,运行时只能有一套兼容实例。 - 检查 `npm ls @heroui-pro/react @heroui/react @heroui/styles react-aria-components @react-aria/utils`,运行时只能有一套兼容实例。
- 验证登录/退出、路由、Tabs、Autocomplete 搜索/单选/多选/清空、筛选、Checkbox 全选/取消、排序、分页、固定列、ActionBar、所有弹窗、图表 Tooltip、深浅主题和响应式。 - 验证登录/退出、路由、Tabs、Autocomplete 搜索/单选/多选/清空、筛选、Checkbox 全选/取消、排序、分页、固定列、ActionBar、所有弹窗、图表 Tooltip、深浅主题和响应式。
- 最终执行 `npm run build`、`git diff --check`、HeroUI Pro 子路径解析和旧引用扫描。 - 最终执行 `npm run build`、`git diff --check`、HeroUI Pro 子路径解析和旧引用扫描。
......
# 问题整改系统 UI/UX 全局审计 # 问题整改系统 UI/UX 全局审计
> 当前组件库结论:HeroUI 与 HeroUI Pro 是项目唯一 UI 组件体系。文中 Catalyst/Headless UI 只描述迁移前问题和历史整改记录,不构成可用技术选项。
> 2026-08-19 复核:Modal 根面统一保持 HeroUI `overlay`;应用切换项、附件编辑器、流程详情摘要等需要独立边界的弹窗内对象统一使用 HeroUI `secondary`。普通详情查看态以一个 `Surface secondary` 承载整组无框标签/值,编辑态则由 `secondary` 控件直接位于 Overlay。已清理上述范围内的 L1-on-L1 默认 Card,不改变弹窗数据、回调或状态流转;深浅模式实际层级仍待浏览器验收。 > 2026-08-19 复核:Modal 根面统一保持 HeroUI `overlay`;应用切换项、附件编辑器、流程详情摘要等需要独立边界的弹窗内对象统一使用 HeroUI `secondary`。普通详情查看态以一个 `Surface secondary` 承载整组无框标签/值,编辑态则由 `secondary` 控件直接位于 Overlay。已清理上述范围内的 L1-on-L1 默认 Card,不改变弹窗数据、回调或状态流转;深浅模式实际层级仍待浏览器验收。
审计日期:2026-08-17 审计日期:2026-08-17
...@@ -83,7 +85,7 @@ ...@@ -83,7 +85,7 @@
- 整改处理、整改流程、问题清单和详情/引用四个活动区域残留的页面私有 `white/zinc/slate/sky/rose/red/amber/emerald` 视觉类已归零;状态、提示、加载、空状态、信息 Card 和 KPI 已改为直接使用 HeroUI/HeroUI Pro 与鑫元语义 Token。 - 整改处理、整改流程、问题清单和详情/引用四个活动区域残留的页面私有 `white/zinc/slate/sky/rose/red/amber/emerald` 视觉类已归零;状态、提示、加载、空状态、信息 Card 和 KPI 已改为直接使用 HeroUI/HeroUI Pro 与鑫元语义 Token。
- 暂未被路由引用的 `ResourceManagementUi.jsx` 与 `ModuleEmptyState.jsx` 也已完成同一边界迁移;前者同时移除了已不存在的旧共享样式依赖,避免未来重新启用时形成隐性构建故障。 - 暂未被路由引用的 `ResourceManagementUi.jsx` 与 `ModuleEmptyState.jsx` 也已完成同一边界迁移;前者同时移除了已不存在的旧共享样式依赖,避免未来重新启用时形成隐性构建故障。
- 登录验证码暗色表面与暗色填充态已改为 `theme.css`/`tokens.css` 语义变量和 `color-mix()` 派生值,不再形成硬编码白色孤岛。 - 登录验证码暗色表面与暗色填充态已改为 `theme.css`/`tokens.css` 语义变量和 `color-mix()` 派生值,不再形成硬编码白色孤岛。
- `src/catalyst-ui-kit` 作为未引用历史源码隔离保留,本次没有进入或修改该目录;新功能禁止引用。 - `src/catalyst-ui-kit` 作为待清理的未引用历史源码隔离保留,不属于可用组件目录;新功能、存量维护、重构、示例和测试均禁止引用、复制或扩展,且不得进入构建或依赖图。
- `RecordWorkbench`、问题清单、归档、驾驶舱明细、整改处理和整改流程均已改用 HeroUI Pro DataGrid;选择、分页和 ActionBar 由原生能力承载。 - `RecordWorkbench`、问题清单、归档、驾驶舱明细、整改处理和整改流程均已改用 HeroUI Pro DataGrid;选择、分页和 ActionBar 由原生能力承载。
- 驾驶舱和整改分析已删除 ECharts 与私有图表主题,直接使用 HeroUI Pro KPI、PieChart、BarChart、ComposedChart 和 ChartTooltip;统计数据、筛选状态与重点问题点击回调保持原样。 - 驾驶舱和整改分析已删除 ECharts 与私有图表主题,直接使用 HeroUI Pro KPI、PieChart、BarChart、ComposedChart 和 ChartTooltip;统计数据、筛选状态与重点问题点击回调保持原样。
- 应用切换、详情、创建、推送、延期、复制、删除和重点问题详情均已改用 HeroUI Modal compound API。 - 应用切换、详情、创建、推送、延期、复制、删除和重点问题详情均已改用 HeroUI Modal compound API。
......
# Catalyst UI Kit 组件文档 # Catalyst UI Kit 组件文档
本文档详细说明了 Catalyst UI Kit 中所有可用组件的使用方法、Props 接口、样式特点和使用示例。 > **历史文档,禁止作为开发依据。** Catalyst UI Kit 已退出本项目,HeroUI 与 HeroUI Pro 是唯一 UI 组件体系。禁止引用、复制、扩展下列组件或恢复 `@headlessui/react` 依赖;请改用 [`docs/xinyuan-design-system.md`](../../docs/xinyuan-design-system.md) 和 [`docs/ui-architecture.md`](../../docs/ui-architecture.md) 中规定的 HeroUI/HeroUI Pro 组件。
本文档仅保留 Catalyst 历史组件的 Props、样式和示例,用于迁移追溯。
--- ---
......
# Catalyst UI Kit # Catalyst UI Kit
> **已废弃:禁止在本项目中使用。** 项目 UI 已统一为 `@heroui/react` 与 `@heroui-pro/react`。本目录仅保留历史源码,等待后续清理;不得从中导入、复制或扩展组件,不得安装其 Headless UI 依赖,也不得把本目录加入构建。当前开发规范见 [`docs/xinyuan-design-system.md`](../../docs/xinyuan-design-system.md) 和 [`docs/ui-architecture.md`](../../docs/ui-architecture.md)。以下内容仅供追溯原始来源。
Catalyst is a modern application UI kit built with [Tailwind CSS](https://tailwindcss.com) and [React](https://react.dev/), designed and built by the Tailwind CSS team and included as part of [Tailwind Plus](https://tailwindcss.com/plus). Catalyst is a modern application UI kit built with [Tailwind CSS](https://tailwindcss.com) and [React](https://react.dev/), designed and built by the Tailwind CSS team and included as part of [Tailwind Plus](https://tailwindcss.com/plus).
## Getting started ## Getting started
......
# Catalyst Demo # Catalyst Demo
> **已废弃:请勿运行或复用。** 本项目 UI 已统一为 `@heroui/react` 与 `@heroui-pro/react`;该 Demo 仅为历史源码,不得安装依赖、复制组件或加入构建。
To run the Catalyst demo, first install the npm dependencies: To run the Catalyst demo, first install the npm dependencies:
```bash ```bash
......
# Catalyst Demo # Catalyst Demo
> **已废弃:请勿运行或复用。** 本项目 UI 已统一为 `@heroui/react` 与 `@heroui-pro/react`;该 Demo 仅为历史源码,不得安装依赖、复制组件或加入构建。
To run the Catalyst demo, first install the npm dependencies: To run the Catalyst demo, first install the npm dependencies:
```bash ```bash
......
...@@ -53,10 +53,12 @@ export default function SubjectViewContent({ ...@@ -53,10 +53,12 @@ export default function SubjectViewContent({
editSessionId, editSessionId,
mode = 'view', mode = 'view',
onFieldChange, onFieldChange,
onPendingLocalMaterialChange,
onFieldValidation, onFieldValidation,
readonly, readonly,
isFieldDisabled, isFieldDisabled,
minimal = false, minimal = false,
useAuditMaterialCenter = false,
}) { }) {
const displayFields = buildViewColumns(view, subject); const displayFields = buildViewColumns(view, subject);
const contentFields = minimal const contentFields = minimal
...@@ -119,8 +121,10 @@ export default function SubjectViewContent({ ...@@ -119,8 +121,10 @@ export default function SubjectViewContent({
) )
} }
onValidationChange={onFieldValidation} onValidationChange={onFieldValidation}
onPendingLocalMaterialChange={onPendingLocalMaterialChange}
readonly={readonly} readonly={readonly}
disabled={fieldDisabled} disabled={fieldDisabled}
useAuditMaterialCenter={useAuditMaterialCenter}
/> />
</div> </div>
</div> </div>
......
...@@ -15,6 +15,7 @@ import FileUploadField from '@/components/FileUploadField'; ...@@ -15,6 +15,7 @@ import FileUploadField from '@/components/FileUploadField';
import { queryUsers, uploadFile } from '@/services/DataService'; import { queryUsers, uploadFile } from '@/services/DataService';
import ReferenceSearchSelect from '@/components/ReferenceSearchSelect'; import ReferenceSearchSelect from '@/components/ReferenceSearchSelect';
import XinyuanAutocomplete from '@/components/XinyuanAutocomplete'; import XinyuanAutocomplete from '@/components/XinyuanAutocomplete';
import ProblemRectificationMaterialField from '@/pages/problem_rectification/components/ProblemRectificationMaterialField';
import ReferenceRecordsTable from './ReferenceRecordsTable'; import ReferenceRecordsTable from './ReferenceRecordsTable';
import { import {
dateInputValue, dateInputValue,
...@@ -303,7 +304,9 @@ function ControlledFieldEditor({ ...@@ -303,7 +304,9 @@ function ControlledFieldEditor({
fieldVariant, fieldVariant,
placeholder, placeholder,
onChange, onChange,
onPendingLocalMaterialChange,
onValidationChange, onValidationChange,
useAuditMaterialCenter,
}) { }) {
const [error, setError] = useState(''); const [error, setError] = useState('');
const [uploading, setUploading] = useState(false); const [uploading, setUploading] = useState(false);
...@@ -388,6 +391,22 @@ function ControlledFieldEditor({ ...@@ -388,6 +391,22 @@ function ControlledFieldEditor({
} }
if (fieldType === 'file') { if (fieldType === 'file') {
if (
useAuditMaterialCenter &&
fieldPath === 'metadata.rectification_proof_materials'
) {
return (
<ProblemRectificationMaterialField
editSessionId={editSessionId}
fieldPath={fieldPath}
onChange={onChange}
onPendingLocalMaterialChange={onPendingLocalMaterialChange}
onValidationChange={onValidationChange}
value={value}
/>
);
}
return ( return (
<Card variant="secondary"> <Card variant="secondary">
<Card.Content className="space-y-3"> <Card.Content className="space-y-3">
...@@ -518,7 +537,9 @@ export default function TailwindEditableField({ ...@@ -518,7 +537,9 @@ export default function TailwindEditableField({
placeholder, placeholder,
onSave, onSave,
onChange, onChange,
onPendingLocalMaterialChange,
onValidationChange, onValidationChange,
useAuditMaterialCenter = false,
mode = 'inline', mode = 'inline',
readonly = false, readonly = false,
disabled = false, disabled = false,
...@@ -559,7 +580,9 @@ export default function TailwindEditableField({ ...@@ -559,7 +580,9 @@ export default function TailwindEditableField({
fieldVariant={fieldVariant} fieldVariant={fieldVariant}
placeholder={textPlaceholder} placeholder={textPlaceholder}
onChange={onChange} onChange={onChange}
onPendingLocalMaterialChange={onPendingLocalMaterialChange}
onValidationChange={onValidationChange} onValidationChange={onValidationChange}
useAuditMaterialCenter={useAuditMaterialCenter}
/> />
); );
} }
......
...@@ -4,6 +4,7 @@ import { Close } from '@/components/AppIcons'; ...@@ -4,6 +4,7 @@ import { Close } from '@/components/AppIcons';
import { Alert, Button, Modal, Spinner, Tabs, Tooltip } from '@heroui/react'; import { Alert, Button, Modal, Spinner, Tabs, Tooltip } from '@heroui/react';
import React, { import React, {
useCallback, useCallback,
useContext,
forwardRef, forwardRef,
useEffect, useEffect,
useImperativeHandle, useImperativeHandle,
...@@ -13,6 +14,8 @@ import React, { ...@@ -13,6 +14,8 @@ import React, {
} from 'react'; } from 'react';
import { useSubjectRecord } from './hooks/useSubjectRecord'; import { useSubjectRecord } from './hooks/useSubjectRecord';
import SubjectViewContent from './components/SubjectViewContent'; import SubjectViewContent from './components/SubjectViewContent';
import { UserContext } from '@/wrapper/Auth';
import { createProblemRectificationMaterial } from '@/pages/problem_rectification/problemRectificationMaterialService';
import { import {
buildViewColumns, buildViewColumns,
checkViewCondition, checkViewCondition,
...@@ -58,8 +61,11 @@ const RecordDetailDrawer = (props, ref) => { ...@@ -58,8 +61,11 @@ const RecordDetailDrawer = (props, ref) => {
disabledFields = EMPTY_ARRAY, disabledFields = EMPTY_ARRAY,
authorizationView = null, authorizationView = null,
deriveFieldValues, deriveFieldValues,
useAuditMaterialCenter = false,
} = props; } = props;
const currentUser = useContext(UserContext);
const [refreshKey, setRefreshKey] = useState(() => Date.now()); const [refreshKey, setRefreshKey] = useState(() => Date.now());
const [isFullscreen, setIsFullscreen] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false);
const [activeTabKey, setActiveTabKey] = useState(); const [activeTabKey, setActiveTabKey] = useState();
...@@ -70,6 +76,7 @@ const RecordDetailDrawer = (props, ref) => { ...@@ -70,6 +76,7 @@ const RecordDetailDrawer = (props, ref) => {
const [validationErrors, setValidationErrors] = useState({}); const [validationErrors, setValidationErrors] = useState({});
const [saveError, setSaveError] = useState(''); const [saveError, setSaveError] = useState('');
const editSessionRef = useRef(0); const editSessionRef = useRef(0);
const pendingLocalMaterialsRef = useRef({});
const { const {
editableRecord, editableRecord,
...@@ -100,6 +107,7 @@ const RecordDetailDrawer = (props, ref) => { ...@@ -100,6 +107,7 @@ const RecordDetailDrawer = (props, ref) => {
setDirtyFields({}); setDirtyFields({});
setValidationErrors({}); setValidationErrors({});
setSaveError(''); setSaveError('');
pendingLocalMaterialsRef.current = {};
editSessionRef.current += 1; editSessionRef.current += 1;
}, [open, recordName]); }, [open, recordName]);
...@@ -200,6 +208,7 @@ const RecordDetailDrawer = (props, ref) => { ...@@ -200,6 +208,7 @@ const RecordDetailDrawer = (props, ref) => {
setDirtyFields({}); setDirtyFields({});
setValidationErrors({}); setValidationErrors({});
setSaveError(''); setSaveError('');
pendingLocalMaterialsRef.current = {};
}; };
const handleRequestClose = () => { const handleRequestClose = () => {
...@@ -215,6 +224,7 @@ const RecordDetailDrawer = (props, ref) => { ...@@ -215,6 +224,7 @@ const RecordDetailDrawer = (props, ref) => {
setDirtyFields({}); setDirtyFields({});
setValidationErrors({}); setValidationErrors({});
setSaveError(''); setSaveError('');
pendingLocalMaterialsRef.current = {};
setIsEditing(true); setIsEditing(true);
}; };
...@@ -257,6 +267,21 @@ const RecordDetailDrawer = (props, ref) => { ...@@ -257,6 +267,21 @@ const RecordDetailDrawer = (props, ref) => {
}); });
}, []); }, []);
const handlePendingLocalMaterialChange = useCallback(
(fieldPath, pendingMaterial, editSessionId) => {
if (editSessionId !== editSessionRef.current) return;
const nextPendingMaterials = { ...pendingLocalMaterialsRef.current };
if (pendingMaterial) {
nextPendingMaterials[fieldPath] = pendingMaterial;
} else {
delete nextPendingMaterials[fieldPath];
}
pendingLocalMaterialsRef.current = nextPendingMaterials;
},
[],
);
const handleSave = async () => { const handleSave = async () => {
if (!hasDirtyFields || hasValidationErrors || isSaving) return; if (!hasDirtyFields || hasValidationErrors || isSaving) return;
...@@ -264,10 +289,30 @@ const RecordDetailDrawer = (props, ref) => { ...@@ -264,10 +289,30 @@ const RecordDetailDrawer = (props, ref) => {
setSaveError(''); setSaveError('');
try { try {
const pendingMaterials = Object.entries(pendingLocalMaterialsRef.current)
.filter(([fieldPath]) => dirtyFields[fieldPath]);
for (const [fieldPath, pendingMaterial] of pendingMaterials) {
if (pendingMaterial.materialRecordId) continue;
const createdMaterial = await createProblemRectificationMaterial({
fileAsset: pendingMaterial.fileAsset,
currentUser,
});
pendingLocalMaterialsRef.current = {
...pendingLocalMaterialsRef.current,
[fieldPath]: {
...pendingMaterial,
materialRecordId: createdMaterial.id,
},
};
}
await handleFieldsUpdate(Object.values(dirtyFields)); await handleFieldsUpdate(Object.values(dirtyFields));
setIsEditing(false); setIsEditing(false);
setDirtyFields({}); setDirtyFields({});
setValidationErrors({}); setValidationErrors({});
pendingLocalMaterialsRef.current = {};
onDataChange?.(true); onDataChange?.(true);
} catch (saveFailure) { } catch (saveFailure) {
setSaveError(saveFailure?.message || '保存失败,请稍后重试。'); setSaveError(saveFailure?.message || '保存失败,请稍后重试。');
...@@ -389,9 +434,11 @@ const RecordDetailDrawer = (props, ref) => { ...@@ -389,9 +434,11 @@ const RecordDetailDrawer = (props, ref) => {
mode={isEditing ? 'edit' : 'view'} mode={isEditing ? 'edit' : 'view'}
onFieldChange={handleDraftFieldChange} onFieldChange={handleDraftFieldChange}
onFieldValidation={handleFieldValidation} onFieldValidation={handleFieldValidation}
onPendingLocalMaterialChange={handlePendingLocalMaterialChange}
readonly={effectiveReadonly} readonly={effectiveReadonly}
isFieldDisabled={isDrawerFieldDisabled} isFieldDisabled={isDrawerFieldDisabled}
minimal={minimalContent} minimal={minimalContent}
useAuditMaterialCenter={useAuditMaterialCenter}
/> />
</div> </div>
)} )}
......
import {
ArrowPathIcon,
ChevronDown,
Close,
Delete,
FIELD_SUFFIX_ICON_STYLE,
Search,
} from '@/components/AppIcons';
import FileUploadField from '@/components/FileUploadField';
import {
queryAuditMaterials,
uploadProblemRectificationFile,
} from '../problemRectificationMaterialService';
import {
Alert,
Autocomplete,
Button,
Label,
ListBox,
SearchField,
Spinner,
Tabs,
Tooltip,
} from '@heroui/react';
import { useEffect, useMemo, useState } from 'react';
const MATERIAL_CENTER_TAB = 'material-center';
const LOCAL_UPLOAD_TAB = 'local-upload';
function formatMaterialDescription(material) {
return [
material.fileAsset?.name,
material.archiveCategory,
material.archiveYear,
].filter(Boolean).join(' · ');
}
export default function ProblemRectificationMaterialField({
editSessionId,
fieldPath,
onChange,
onPendingLocalMaterialChange,
onValidationChange,
value,
}) {
const [activeTab, setActiveTab] = useState(MATERIAL_CENTER_TAB);
const [keyword, setKeyword] = useState('');
const [debouncedKeyword, setDebouncedKeyword] = useState('');
const [materials, setMaterials] = useState([]);
const [selectedMaterialId, setSelectedMaterialId] = useState(null);
const [materialsLoading, setMaterialsLoading] = useState(false);
const [materialsError, setMaterialsError] = useState('');
const [retryKey, setRetryKey] = useState(0);
const [uploading, setUploading] = useState(false);
const [uploadError, setUploadError] = useState('');
const [localFileAsset, setLocalFileAsset] = useState(null);
useEffect(() => {
setActiveTab(MATERIAL_CENTER_TAB);
setKeyword('');
setDebouncedKeyword('');
setSelectedMaterialId(null);
setMaterialsError('');
setUploadError('');
setLocalFileAsset(null);
}, [editSessionId]);
useEffect(() => {
const timeoutId = window.setTimeout(() => {
setDebouncedKeyword(keyword);
}, 300);
return () => window.clearTimeout(timeoutId);
}, [keyword]);
useEffect(() => {
if (activeTab !== MATERIAL_CENTER_TAB) return undefined;
let cancelled = false;
setMaterialsLoading(true);
setMaterialsError('');
queryAuditMaterials(debouncedKeyword)
.then((nextMaterials) => {
if (!cancelled) setMaterials(nextMaterials);
})
.catch((error) => {
if (!cancelled) {
setMaterials([]);
setMaterialsError(error?.message || '审计材料加载失败,请稍后重试。');
}
})
.finally(() => {
if (!cancelled) setMaterialsLoading(false);
});
return () => {
cancelled = true;
};
}, [activeTab, debouncedKeyword, retryKey]);
const selectedMaterial = useMemo(
() => materials.find((material) => material.id === selectedMaterialId),
[materials, selectedMaterialId],
);
const selectedValueText = selectedMaterial?.materialTitle || value?.name || '请选择审计材料';
const handleMaterialSelection = (key) => {
const materialId = key ? String(key) : '';
const material = materials.find((item) => String(item.id) === materialId);
if (!material?.fileAsset?.url) return;
setSelectedMaterialId(material.id);
setLocalFileAsset(null);
setUploadError('');
onPendingLocalMaterialChange?.(fieldPath, null, editSessionId);
onChange?.({
name: material.fileAsset.name,
type: material.fileAsset.type || 'application/octet-stream',
size: Number(material.fileAsset.size || 0),
url: material.fileAsset.url,
});
};
const handleLocalUpload = async (files) => {
const file = files?.[0];
if (!file) return;
setUploading(true);
setUploadError('');
onValidationChange?.(fieldPath, '附件正在上传,请稍候。', editSessionId);
try {
const fileAsset = await uploadProblemRectificationFile(file);
setLocalFileAsset(fileAsset);
setSelectedMaterialId(null);
onPendingLocalMaterialChange?.(
fieldPath,
{ fileAsset, materialRecordId: null },
editSessionId,
);
onChange?.(fileAsset);
} catch (error) {
setUploadError(error?.message || '附件上传失败,请稍后重试。');
} finally {
setUploading(false);
onValidationChange?.(fieldPath, '', editSessionId);
}
};
const handleRemove = () => {
setSelectedMaterialId(null);
setLocalFileAsset(null);
setUploadError('');
onPendingLocalMaterialChange?.(fieldPath, null, editSessionId);
onChange?.(null);
};
return (
<div className="space-y-4">
{value?.name || value?.url ? (
<div className="flex min-w-0 items-center justify-between gap-3">
<div className="min-w-0">
<div className="truncate text-sm font-medium text-foreground" title={value.name || value.url}>
{value.name || value.url}
</div>
<div className="mt-1 text-xs text-muted">
{localFileAsset ? '本地文件,保存后同步至审计材料中心' : '已选择附件'}
</div>
</div>
<Tooltip>
<Button
aria-label="删除当前附件"
isIconOnly
size="sm"
variant="danger-soft"
onPress={handleRemove}
>
<Delete size={16} />
</Button>
<Tooltip.Content>删除当前附件</Tooltip.Content>
</Tooltip>
</div>
) : (
<span className="text-sm text-muted">未上传</span>
)}
<Tabs
aria-label="附件来源"
className="w-fit max-w-full min-w-0"
selectedKey={activeTab}
variant="secondary"
onSelectionChange={(key) => setActiveTab(String(key))}
>
<Tabs.ListContainer className="w-fit max-w-full min-w-0">
<Tabs.List className="w-max min-w-max">
<Tabs.Tab className="w-auto min-w-max flex-none whitespace-nowrap" id={MATERIAL_CENTER_TAB}>
材料中心
<Tabs.Indicator />
</Tabs.Tab>
<Tabs.Tab className="w-auto min-w-max flex-none whitespace-nowrap" id={LOCAL_UPLOAD_TAB}>
本地上传
<Tabs.Indicator />
</Tabs.Tab>
</Tabs.List>
</Tabs.ListContainer>
<Tabs.Panel className="pt-3" id={MATERIAL_CENTER_TAB}>
<div className="space-y-3">
<Autocomplete
aria-label="选择审计材料"
fullWidth
selectedKey={selectedMaterialId}
selectionMode="single"
variant="secondary"
onSelectionChange={handleMaterialSelection}
>
<Autocomplete.Trigger>
<Autocomplete.Value>
<span className="block min-w-0 truncate" title={selectedValueText}>
{selectedValueText}
</span>
</Autocomplete.Value>
<Autocomplete.Indicator>
<ChevronDown size={16} style={FIELD_SUFFIX_ICON_STYLE} />
</Autocomplete.Indicator>
</Autocomplete.Trigger>
<Autocomplete.Popover placement="bottom start">
<Autocomplete.Filter
filter={() => true}
inputValue={keyword}
onInputChange={setKeyword}
>
<SearchField aria-label="搜索审计材料" fullWidth variant="secondary">
<SearchField.Group>
<SearchField.SearchIcon><Search size={16} /></SearchField.SearchIcon>
<SearchField.Input autoFocus placeholder="搜索材料标题或文件名" />
<SearchField.ClearButton aria-label="清空搜索"><Close size={14} /></SearchField.ClearButton>
</SearchField.Group>
</SearchField>
<ListBox
aria-label="审计材料列表"
items={materialsLoading || materialsError ? [] : materials}
renderEmptyState={() => (
<div className="grid min-h-24 place-items-center px-4 py-5 text-sm text-muted">
{materialsLoading ? <Spinner size="sm" /> : materialsError || '没有找到可选择的审计材料'}
</div>
)}
>
{(material) => (
<ListBox.Item
id={material.id}
textValue={`${material.materialTitle} ${material.fileAsset?.name || ''}`}
>
<span className="min-w-0">
<Label className="block truncate" title={material.materialTitle}>
{material.materialTitle}
</Label>
<span className="mt-0.5 block truncate text-xs text-muted" title={formatMaterialDescription(material)}>
{formatMaterialDescription(material)}
</span>
</span>
<ListBox.ItemIndicator />
</ListBox.Item>
)}
</ListBox>
</Autocomplete.Filter>
</Autocomplete.Popover>
</Autocomplete>
{materialsError ? (
<Alert status="danger">
<Alert.Content>
<Alert.Description>{materialsError}</Alert.Description>
</Alert.Content>
<Button size="sm" variant="danger-soft" onPress={() => setRetryKey((key) => key + 1)}>
<ArrowPathIcon size={14} />
重试
</Button>
</Alert>
) : null}
</div>
</Tabs.Panel>
<Tabs.Panel className="pt-3" id={LOCAL_UPLOAD_TAB}>
<div className="space-y-3">
<FileUploadField
description="上传后先保留为详情草稿,保存详情时同步写入审计材料中心。"
fileName={localFileAsset?.name || ''}
isDisabled={uploading}
triggerLabel={uploading ? '上传中...' : '选择本地文件'}
onFilesChange={handleLocalUpload}
/>
{uploadError ? (
<Alert status="danger">
<Alert.Content><Alert.Description>{uploadError}</Alert.Description></Alert.Content>
</Alert>
) : null}
</div>
</Tabs.Panel>
</Tabs>
</div>
);
}
...@@ -1872,6 +1872,7 @@ export default function ProblemRectificationListPage() { ...@@ -1872,6 +1872,7 @@ export default function ProblemRectificationListPage() {
}} }}
preferredViewName={activeGridView?.title || activeGridView?.name} preferredViewName={activeGridView?.title || activeGridView?.name}
authorizationView={activeGridView} authorizationView={activeGridView}
useAuditMaterialCenter
/> />
), ),
}} }}
......
...@@ -142,7 +142,13 @@ ...@@ -142,7 +142,13 @@
- `problem_rectification_archive_records`:归档后的问题快照明细 - `problem_rectification_archive_records`:归档后的问题快照明细
- `issue_rectification_archive`:一次归档批次主单 - `issue_rectification_archive`:一次归档批次主单
### 4.3 流程辅助对象 ### 4.3 审计材料共享对象
- `am_material`:审计材料中心共享主档。问题清单详情可查询已有主档;从本地上传证明材料并保存详情时同步创建主档
问题记录仍在 `metadata.rectification_proof_materials` 保存 `{name,type,size,url}` 文件快照,不保存材料主档 `_id`,不建立自动联动关系。
### 4.4 流程辅助对象
- `message_reference_id`(信息关联id):旧应用中用于承载“推送填写、复核、最终审核”状态的消息流程主单 - `message_reference_id`(信息关联id):旧应用中用于承载“推送填写、复核、最终审核”状态的消息流程主单
...@@ -745,9 +751,18 @@ ...@@ -745,9 +751,18 @@
- 使用抽屉或对话框,而不是表格内直接改单元格 - 使用抽屉或对话框,而不是表格内直接改单元格
- 基础信息与整改信息分区展示 - 基础信息与整改信息分区展示
- 附件上传单独区域展示 - 附件在单独区域展示,编辑时默认进入“材料中心”,也可切换到“本地上传”
- 状态、责任人、计划时间等字段用结构化控件维护 - 状态、责任人、计划时间等字段用结构化控件维护
整改证明材料交互限定在问题清单详情弹框,不改变整改处理页、整改流程表格和归档详情:
1. “材料中心”首次读取当前用户可访问的最近 20 条 `am_material`,支持按材料标题或文件名远程搜索;选中后仅把 `metadata.file_asset` 快照写入详情草稿。
2. “本地上传”选择文件后立即调用 `/upload`,上传期间禁止保存;成功后将文件对象写入详情草稿,并提示保存后同步到材料中心。
3. 用户点击详情“保存”时,先创建 `am_material`,写入 `source_app = issue_rectification`、当前用户 `_id` 和 ISO 上传时间,再更新 `problem_rectification.metadata.rectification_proof_materials`。
4. 用户取消编辑时不创建材料主档;选择已有材料不重复上传、不创建新主档。
5. 材料创建失败时保留编辑草稿且不更新问题;材料创建成功但问题更新失败时保留已建档标记,再次保存只重试问题更新,避免重复材料。
6. 清空或替换问题附件不删除材料中心主档或底层文件;已有材料后续修改或软删除不联动历史问题快照。
建议字段分组: 建议字段分组:
- 基础信息 - 基础信息
......
import dayjs from 'dayjs';
import { createRecord, queryRecords, uploadFile } from '@/services/DataService';
export const AUDIT_MATERIAL_SUBJECT = 'am_material';
export const ISSUE_RECTIFICATION_SOURCE_APP = 'issue_rectification';
const MATERIAL_PAGE_SIZE = 20;
function extractRecords(response) {
const candidates = [
response,
response?.data,
response?.data?.list,
response?.data?.records,
response?.list,
response?.records,
response?.items,
response?.result,
response?.result?.records,
];
return candidates.find((candidate) => Array.isArray(candidate)) || [];
}
function escapeRegExp(value = '') {
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function inferMaterialTitle(fileName = '') {
return String(fileName || '').replace(/\.[^.]+$/, '').trim() || '未命名材料';
}
function normalizeFileAsset(uploadResult, file) {
const candidates = [
uploadResult?.file,
uploadResult?.data?.file,
uploadResult?.data,
uploadResult?.result?.file,
uploadResult?.result,
uploadResult,
];
for (const candidate of candidates) {
if (!candidate || typeof candidate !== 'object') continue;
const url =
candidate.url ||
candidate.path ||
candidate.location ||
candidate?.data?.url ||
'';
if (!url) continue;
return {
name: candidate.name || candidate.filename || file?.name || '未命名文件',
type: candidate.type || candidate.mimetype || file?.type || 'application/octet-stream',
size: Number(candidate.size || file?.size || 0),
url,
};
}
throw new Error('文件上传成功,但未获取到可用的文件地址。');
}
function normalizeMaterial(record = {}) {
const metadata = record?.metadata || {};
const fileAsset = metadata.file_asset || {};
return {
id: record._id || record.id || record.name || '',
materialTitle: metadata.material_title || record.title || fileAsset.name || '未命名材料',
fileAsset,
archiveCategory: metadata.archive_category || '',
archiveYear: metadata.archive_year || '',
sourceApp: metadata.source_app || '',
uploadedAt: metadata.uploaded_at || record.created_at || '',
};
}
export async function queryAuditMaterials(keyword = '') {
const normalizedKeyword = String(keyword || '').trim();
const escapedKeyword = escapeRegExp(normalizedKeyword);
const filter = escapedKeyword
? {
$or: [
{
'metadata.material_title': {
$regex: escapedKeyword,
$options: 'i',
},
},
{
'metadata.file_asset.name': {
$regex: escapedKeyword,
$options: 'i',
},
},
],
}
: {};
const response = await queryRecords({
subject: AUDIT_MATERIAL_SUBJECT,
page: 0,
page_size: MATERIAL_PAGE_SIZE,
filter,
sort: '-updated_at -created_at',
mode: 'full',
});
return extractRecords(response)
.map(normalizeMaterial)
.filter((material) => material.id && material.fileAsset?.url);
}
export async function uploadProblemRectificationFile(file) {
if (!file) {
throw new Error('请选择要上传的本地文件。');
}
const uploadResult = await uploadFile(file);
return normalizeFileAsset(uploadResult, file);
}
export async function createProblemRectificationMaterial({ fileAsset, currentUser }) {
if (!fileAsset?.url) {
throw new Error('缺少有效的文件地址,无法写入审计材料中心。');
}
const uploaderId = currentUser?._id || currentUser?.id || '';
if (!uploaderId) {
throw new Error('未获取到当前登录用户,无法写入审计材料中心。');
}
const materialTitle = inferMaterialTitle(fileAsset.name);
const response = await createRecord({
subject: AUDIT_MATERIAL_SUBJECT,
title: materialTitle,
content_type: 'text',
metadata: {
material_title: materialTitle,
file_asset: {
name: fileAsset.name,
type: fileAsset.type || 'application/octet-stream',
size: Number(fileAsset.size || 0),
url: fileAsset.url,
},
source_app: ISSUE_RECTIFICATION_SOURCE_APP,
uploader: uploaderId,
uploaded_at: dayjs().toISOString(),
},
});
const createdRecord = response?.data || response?.result || response;
return {
id: createdRecord?._id || createdRecord?.id || createdRecord?.name || true,
record: createdRecord,
};
}
# Record 接口文档 # Record 接口文档
**最后更新**:2026 年 3 月 **最后更新**:2026 年 8 月
**基于代码实现整理** **基于代码实现整理**
参考文件: 参考文件:
- `src/routers/record.js` - `src/routers/record.js`
...@@ -267,5 +267,67 @@ Query:`calculated=true|false` ...@@ -267,5 +267,67 @@ Query:`calculated=true|false`
6. **软删除 vs 物理删除**:默认软删除(status=removed),需显式 `force` 才物理删除。 6. **软删除 vs 物理删除**:默认软删除(status=removed),需显式 `force` 才物理删除。
7. **导入接口**:支持 name 自动转 id,但仅限同一 subject 内查找。 7. **导入接口**:支持 name 自动转 id,但仅限同一 subject 内查找。
如需补充特定 subject 的 metadata 字段规范、文件上传流程、权限点详细对应表等,请继续提供信息,我可以进一步扩展对应章节。 ## 5. 问题详情与审计材料中心接入
问题清单详情弹框中的 `metadata.rectification_proof_materials` 支持从统一材料池选择已有文件,或上传本地文件后同步建档。整改处理页、整改流程表格和归档详情不使用本节流程。
### 5.1 查询已有材料
调用 `POST /record/query`,按当前用户的 Record 访问权限返回有效材料:
```json
{
"subject": "am_material",
"page": 0,
"page_size": 20,
"filter": {
"$or": [
{
"metadata.material_title": {
"$regex": "整改说明",
"$options": "i"
}
},
{
"metadata.file_asset.name": {
"$regex": "整改说明",
"$options": "i"
}
}
]
},
"sort": "-updated_at -created_at",
"mode": "full"
}
``` ```
搜索关键词必须先转义正则特殊字符。选中材料后,只将 `metadata.file_asset` 的 `{name,type,size,url}` 快照写入问题详情草稿,不保存 `am_material` `_id`。
### 5.2 本地上传并写入材料中心
1. 选择文件后立即调用 `POST /upload`,将返回结果规范化为 `{name,type,size,url}` 并保留在详情草稿。
2. 用户点击详情“保存”时调用 `POST /record` 创建材料主档:
```json
{
"subject": "am_material",
"title": "整改说明",
"content_type": "text",
"metadata": {
"material_title": "整改说明",
"file_asset": {
"name": "整改说明.pdf",
"type": "application/pdf",
"size": 204800,
"url": "https://example.com/files/proof.pdf"
},
"source_app": "issue_rectification",
"uploader": "69b12a47e92b1de77c94a9f5",
"uploaded_at": "2026-08-25T08:30:00.000Z"
}
}
```
3. 材料主档创建成功后,再调用 `PUT /record/:name` 更新问题记录的 `metadata.rectification_proof_materials`。
4. 材料创建失败时不更新问题记录;材料创建成功但问题更新失败时,当前编辑会话记住已创建的材料,重试保存不得重复创建。
5. 取消详情编辑不创建 `am_material`;已经上传但未建档的二进制文件遵循上传服务自身的清理策略。
...@@ -4,10 +4,10 @@ ...@@ -4,10 +4,10 @@
本文件定义当前项目使用的 Subject(业务对象)字段协议,供前端页面、服务层、动态表单与 AI 代码生成使用。 本文件定义当前项目使用的 Subject(业务对象)字段协议,供前端页面、服务层、动态表单与 AI 代码生成使用。
当前项目当前涉及两个业务模块: 当前项目涉及以下业务对象:
- `pm_project_task`:项目管理
- `problem_rectification`:问题整改 - `problem_rectification`:问题整改
- `am_material`:审计材料中心共享主档,仅供问题详情查询和本地附件同步建档
--- ---
...@@ -187,6 +187,7 @@ type Record = { ...@@ -187,6 +187,7 @@ type Record = {
- 新增驾驶舱扩展字段当前在线上 Subject 中大多仍为 `text` 类型,前端工作台按标准值引导录入,用于支持后续严重程度、问题分类、问题原因和整改质效分析 - 新增驾驶舱扩展字段当前在线上 Subject 中大多仍为 `text` 类型,前端工作台按标准值引导录入,用于支持后续严重程度、问题分类、问题原因和整改质效分析
- 其余 `select` 类型的可选值必须以线上 Subject 配置中的 `settings.options` 为准,禁止在页面内硬编码 - 其余 `select` 类型的可选值必须以线上 Subject 配置中的 `settings.options` 为准,禁止在页面内硬编码
- `problem_discovery_situation` 曾在历史文档和归档对象中出现,但本次主对象字段定义未包含;新页面只应在 Subject View 或线上字段配置存在时展示,不能作为 `problem_rectification` 固定字段依赖 - `problem_discovery_situation` 曾在历史文档和归档对象中出现,但本次主对象字段定义未包含;新页面只应在 Subject View 或线上字段配置存在时展示,不能作为 `problem_rectification` 固定字段依赖
- 问题清单详情弹框中的 `rectification_proof_materials` 支持从 `am_material.metadata.file_asset` 复制文件快照,或上传本地文件后创建 `am_material`。问题字段仍只保存 `{name,type,size,url}`,不保存材料主档 `_id`,材料后续修改或软删除不反向联动问题记录
- `department_rectification_items` 用于新转发流程的部门级整改明细。无论单条还是多条推送,每个问题都按责任部门生成明细;单条问题即使只有 1 个责任部门,也生成 1 条明细。已推送过的问题再次推送时,必须保留原部门明细记录和 `item_id`,只在本次参与推送的原明细行上更新填写人、审批人、流程状态和待办 ID;未参与本次推送的原明细行保持不变,不因弹窗删除部门配置而从父记录移除。转发后 `rectification_progress`、`rectification_situation`、`the_time_of_rectification`、`rectification_proof_materials`、`audit_status`、`final_audit_status` 以明细为事实来源,父字段仅做兼容汇总展示;整改进度、审批状态和最终审批状态按部门明细中最落后的状态汇总,例如“整改中”优先于“已整改”,“未审批”优先于“已审批” - `department_rectification_items` 用于新转发流程的部门级整改明细。无论单条还是多条推送,每个问题都按责任部门生成明细;单条问题即使只有 1 个责任部门,也生成 1 条明细。已推送过的问题再次推送时,必须保留原部门明细记录和 `item_id`,只在本次参与推送的原明细行上更新填写人、审批人、流程状态和待办 ID;未参与本次推送的原明细行保持不变,不因弹窗删除部门配置而从父记录移除。转发后 `rectification_progress`、`rectification_situation`、`the_time_of_rectification`、`rectification_proof_materials`、`audit_status`、`final_audit_status` 以明细为事实来源,父字段仅做兼容汇总展示;整改进度、审批状态和最终审批状态按部门明细中最落后的状态汇总,例如“整改中”优先于“已整改”,“未审批”优先于“已审批”
- `problem_rectification_delay_request` 为历史问题级单次延期申请嵌套对象,与 `department_rectification_items` 平级;新发起延期默认按部门写入 `problem_rectification_delay_request_items` - `problem_rectification_delay_request` 为历史问题级单次延期申请嵌套对象,与 `department_rectification_items` 平级;新发起延期默认按部门写入 `problem_rectification_delay_request_items`
- `problem_rectification_delay_request_items` 为部门级延期申请明细数组;每个责任部门 1 条延期申请,独立保存填写人、一级审批人、最终审批人、申请完成时间、延期事由、状态和待办 ID - `problem_rectification_delay_request_items` 为部门级延期申请明细数组;每个责任部门 1 条延期申请,独立保存填写人、一级审批人、最终审批人、申请完成时间、延期事由、状态和待办 ID
...@@ -290,7 +291,26 @@ type Record = { ...@@ -290,7 +291,26 @@ type Record = {
} }
``` ```
## 3.2 问题整改归档明细 ## 3.2 审计材料中心共享主档
- **subject**: `am_material`
本项目只使用该对象完成问题详情的材料查询和本地附件同步建档,不修改线上 Subject 配置。
| 字段标签 | 字段别名 | 字段类型 | 是否必填 | 问题详情写入规则 |
| --- | --- | --- | --- | --- |
| 材料标题 | `material_title` | text | 是 | 默认取文件名去后缀 |
| 文件对象 | `file_asset` | file | 是 | `{name,type,size,url}` |
| 来源应用 | `source_app` | text | 否 | 固定写入 `issue_rectification` |
| 上传人 | `uploader` | user | 是 | 当前登录用户 `_id` |
| 上传时间 | `uploaded_at` | date | 是 | ISO 8601 字符串 |
| 来源系统 | `source_system` | select | 否 | 问题详情不写入 |
| 材料分类 | `archive_category` | select | 否 | 问题详情不写入 |
| 所属周期/年份 | `archive_year` | text | 否 | 问题详情不写入 |
问题详情选择已有材料时读取 `material_title` 与 `file_asset`;搜索范围为 `metadata.material_title` 和 `metadata.file_asset.name`。列表只展示当前登录用户通过 Record 权限可读取且 `file_asset.url` 有效的记录。
## 3.3 问题整改归档明细
- **subject**: `problem_rectification_archive_records` - **subject**: `problem_rectification_archive_records`
...@@ -319,14 +339,14 @@ type Record = { ...@@ -319,14 +339,14 @@ type Record = {
| 检查项目名称 | `project_name` | text | 否 | 否 | 否 | | 检查项目名称 | `project_name` | text | 否 | 否 | 否 |
| 整改证明材料 | `rectification_proof_materials` | file | 否 | 否 | 否 | | 整改证明材料 | `rectification_proof_materials` | file | 否 | 否 | 否 |
### 3.2.1 归档写入约定 ### 3.3.1 归档写入约定
- 归档对象存的是快照,不再要求保留原来的 `select` 选项结构 - 归档对象存的是快照,不再要求保留原来的 `select` 选项结构
- 旧应用归档时会把 `responsible_department` 转为逗号拼接文本 - 旧应用归档时会把 `responsible_department` 转为逗号拼接文本
- 归档记录仍需保留 `fill_in_the_people`、`review_people` 的用户 `_id[]` - 归档记录仍需保留 `fill_in_the_people`、`review_people` 的用户 `_id[]`
- `the_time_of_rectification` 继续按 ISO 字符串写入 - `the_time_of_rectification` 继续按 ISO 字符串写入
## 3.3 问题整改归档批次 ## 3.4 问题整改归档批次
- **subject**: `issue_rectification_archive` - **subject**: `issue_rectification_archive`
...@@ -336,7 +356,7 @@ type Record = { ...@@ -336,7 +356,7 @@ type Record = {
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| 归档明细记录 | `reference_records` | reference | 否 | 是 | 是 | | 归档明细记录 | `reference_records` | reference | 否 | 是 | 是 |
### 3.3.1 metadata 示例 ### 3.4.1 metadata 示例
```json ```json
{ {
...@@ -347,7 +367,7 @@ type Record = { ...@@ -347,7 +367,7 @@ type Record = {
} }
``` ```
## 3.4 信息关联id(问题整改流程消息主单) ## 3.5 信息关联id(问题整改流程消息主单)
- **subject**: `message_reference_id` - **subject**: `message_reference_id`
- **对象名称**: 信息关联id - **对象名称**: 信息关联id
...@@ -373,7 +393,7 @@ type Record = { ...@@ -373,7 +393,7 @@ type Record = {
| 审核状态 | `auditor_status` | select | 否 | 否 | 否 | | 审核状态 | `auditor_status` | select | 否 | 否 | 否 |
| 发送人状态 | `sender_status` | select | 否 | 否 | 否 | | 发送人状态 | `sender_status` | select | 否 | 否 | 否 |
### 3.4.1 当前流程写入约定 ### 3.5.1 当前流程写入约定
- `restriction_key` 默认写 `_id` - `restriction_key` 默认写 `_id`
- `restriction_value` 保存被推送整改记录的 `_id[]` - `restriction_value` 保存被推送整改记录的 `_id[]`
...@@ -388,7 +408,7 @@ type Record = { ...@@ -388,7 +408,7 @@ type Record = {
- `sender_status`:当前项目统一使用 `未完成`、`已完成` - `sender_status`:当前项目统一使用 `未完成`、`已完成`
- 当批量流程复核或最终审核驳回时,流程主单 `editor_status` 重置为 `未完成` 并回写新的 `progress_id_editor`,同一批次的全部问题共同重新进入填写阶段 - 当批量流程复核或最终审核驳回时,流程主单 `editor_status` 重置为 `未完成` 并回写新的 `progress_id_editor`,同一批次的全部问题共同重新进入填写阶段
## 3.5 项目管理 ## 3.6 项目管理
- **subject**: `pm_project_task` - **subject**: `pm_project_task`
...@@ -414,7 +434,7 @@ type Record = { ...@@ -414,7 +434,7 @@ type Record = {
| 延期原因 | `delay_reason` | text | 否 | 否 | 否 | | 延期原因 | `delay_reason` | text | 否 | 否 | 否 |
| 周进度记录 | `weekly_updates` | object | 否 | 是 | 否 | | 周进度记录 | `weekly_updates` | object | 否 | 是 | 否 |
### 3.5.1 `project_status` 选项 ### 3.6.1 `project_status` 选项
| 显示值 | 存储值 | | 显示值 | 存储值 |
| --- | --- | | --- | --- |
...@@ -425,7 +445,7 @@ type Record = { ...@@ -425,7 +445,7 @@ type Record = {
| 受阻 | `blocked` | | 受阻 | `blocked` |
| 已取消 | `cancelled` | | 已取消 | `cancelled` |
### 3.5.2 `priority` 建议选项 ### 3.6.2 `priority` 建议选项
| 显示值 | 存储值 | | 显示值 | 存储值 |
| --- | --- | | --- | --- |
...@@ -433,7 +453,7 @@ type Record = { ...@@ -433,7 +453,7 @@ type Record = {
| 中 | `medium` | | 中 | `medium` |
| 低 | `low` | | 低 | `low` |
### 3.5.3 `result_status` 建议选项 ### 3.6.3 `result_status` 建议选项
| 显示值 | 存储值 | | 显示值 | 存储值 |
| --- | --- | | --- | --- |
...@@ -444,7 +464,7 @@ type Record = { ...@@ -444,7 +464,7 @@ type Record = {
| 准时完成 | `completed_on_time` | | 准时完成 | `completed_on_time` |
| 超时完成 | `completed_overdue` | | 超时完成 | `completed_overdue` |
### 3.5.4 `weekly_updates` 子结构建议 ### 3.6.4 `weekly_updates` 子结构建议
`weekly_updates` 为对象数组,建议每项至少包含以下字段: `weekly_updates` 为对象数组,建议每项至少包含以下字段:
...@@ -465,7 +485,7 @@ type Record = { ...@@ -465,7 +485,7 @@ type Record = {
- 周进度记录通过“新增本周进度 / 编辑周进度”弹窗录入 - 周进度记录通过“新增本周进度 / 编辑周进度”弹窗录入
- 列表页或项目详情只展示最近一条摘要,不直接暴露底层 JSON - 列表页或项目详情只展示最近一条摘要,不直接暴露底层 JSON
### 3.5.5 metadata 示例 ### 3.6.5 metadata 示例
```json ```json
{ {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment