Commit ef2b315a authored by Andy-bubu's avatar Andy-bubu

feat: unify audit UI with Xinyuan HeroUI design

parent c0e5d603
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
/.umi-test /.umi-test
/dist /dist
/.mfsu /.mfsu
/.playwright-cli
.swc .swc
.pnpm-store .pnpm-store
.DS_Store .DS_Store
...@@ -82,6 +82,9 @@ export default defineConfig({ ...@@ -82,6 +82,9 @@ export default defineConfig({
} }
}, },
esbuildMinifyIIFE: true, esbuildMinifyIIFE: true,
jsMinifierOptions: {
target: ['chrome80', 'es2020'],
},
define: { define: {
STREAMS_API_URL: '/api', STREAMS_API_URL: '/api',
STREAMS_PARSER_URL: '/parser', STREAMS_PARSER_URL: '/parser',
......
# 监察稽核管理 UI/UX 全局审计
审计日期:2026-08-14
审计基线:鑫元业务应用设计系统 1.2、HeroUI 3、HeroUI Pro
## 1. 审计范围
- 运行路由:登录、稽核项目、稽核报告、历史报告详情、外部填报。
- 全局结构:应用 Shell、Sidebar、Navbar、应用切换器、主题与用户菜单。
- 业务组件:项目切换、搜索、表格、状态、分页、表单、引用选择、文件上传、确认弹窗与反馈。
- 视口:1440x900、1280x720、768x1024、390x844。
- 数据状态:真实账号登录后的已填充列表、空态、加载态、错误态和弹层打开态。
- 非活动代码:`src/pages/process_monitor`、`src/pages/RecordDetailDrawer` 和 `src/components/record-workbench` 当前未进入 `.umirc.ts` 路由,不属于本次运行时页面,但仍纳入静态兼容性检查。
## 2. 技术现状
- React 18、Umi Max 4、Tailwind CSS 4。
- UI 主要由 Catalyst / Headless UI、Heroicons、原生表格与局部原生表单控件组成。
- 未安装 HeroUI / HeroUI Pro,缺少统一 Token、主题、Shell、DataGrid 和共享表单适配层。
- 应用基路径为 `/audit_manager_new/`,认证重定向和应用内导航必须保持 base-aware。
## 3. 关键审计结论
### 登录
- 使用横向产品字标,未使用批准的 36px 鑫元花形 Logo。
- 卡片边界和品牌区层级不明确,系统标题与站点副标题存在身份分裂。
- 输入框、验证码和按钮高度不统一;已有值和浏览器自动填充态未使用鑫元浅红表面。
- 登录主按钮仍为蓝色。
### Shell 与应用切换
- Sidebar 是固定白色分栏,缺少 HeroUI Pro offcanvas 折叠、透明 Navbar 和全局主题切换。
- 用户区把“退出登录”作为常驻导航项,未使用可点击用户菜单。
- Navbar 不拥有当前路由标题,内容区重复渲染页面大标题和说明。
- 应用切换器是接近全屏的渐变展示面板,包含应用数量、权限标签、外链装饰和重复标题,不符合批准的紧凑三列应用列表。
### 稽核项目
- 项目选择使用带统计数字的页面私有按钮组,轨道横向延伸且层级噪音较高。
- 搜索和操作区缺少统一筛选 Card;普通文字按钮含装饰图标。
- 旧表格无 20px DataGrid 表面、列宽契约、稳定截断、共享 Tooltip、分页和空态插图。
- 控制点创建弹窗偏窄,标题下存在通用说明;控制措施仍为原生/Catalyst Select。
- 成功反馈占据页面流,会推挤内容。
### 稽核报告与详情
- 多个旧表格按项目纵向堆叠,表头、状态、备注输入与操作列缺少统一密度和宽表滚动策略。
- 页面级“进行中 / 历史报告”使用手写下划线 Tab;统计区不是标准 KPI/紧凑摘要模块。
- 文件选择仍通过隐藏原生 input 和页面按钮驱动,缺少 HeroUI Pro DropZone。
- 历史列表没有共享 Pagination;报告操作按钮混用装饰图标。
- 删除、提交和重置确认未统一到鑫元 Modal / AlertDialog 合同。
### 颜色、图标与暗色主题
- 源码中存在蓝、sky、rose、pink、渐变和页面私有品牌色;主色未统一为 `#9E1B1B`。
- 同时存在 Heroicons 和局部自绘图标,缺少 Ant Design Icons 适配层。
- 没有根级暗色主题,Portal 中的 Dropdown、Popover、Modal、Tooltip 和上传面板无法保证一致暗色表面。
- 当前活动路由未渲染数据图表;图表规范作为后续功能的强制合同写入设计文档。
## 4. 迁移边界
本次只替换视觉结构和交互组件,不改变:
- API URL、请求方法、请求体和响应解析;
- Umi 路由、应用基路径和认证协议;
- 权限检查、状态机、报告快照、提交与归档逻辑;
- 表单字段、校验条件、引用 ID 写入规则和上传接口;
- 业务回调、保存时机、筛选语义和数据计算。
## 5. 验收门槛
- 登录、Shell、应用切换器、筛选、Tab、DataGrid、Pagination、Modal、上传和反馈均使用共享鑫元模式。
- 仅保留批准的鑫元红/白 Logo,应用显示名统一为“监察稽核管理”。
- 关闭态与打开态控件均通过浅色/暗色和移动端检查。
- 无页面级横向滚动、文字重叠、空按钮、重复空态或 Portal 白色孤岛。
- 生产构建、`git diff --check`、本地 HeroUI Pro 子路径导入验证通过。
# 监察稽核管理鑫元设计规范
本文件是本项目后续新增页面和功能的 UI 实现合同。业务功能开发必须复用共享组件和 Token,不得新增第二套局部样式体系。
## 1. 品牌与技术基线
- 应用显示名:`监察稽核管理`。登录、Sidebar、Navbar、应用切换当前项和浏览器标题必须一致。
- 主色:`#9E1B1B`。选中、焦点、Checkbox、日期、Pagination 和信息态统一使用品牌 Token。
- 浅色 Logo 使用 `src/assets/identity/xy-logo-red.png`;暗色 Logo 使用 `xy-logo-white.png`。不得滤镜、反色、加底板或替换为 RSM 标识。
- 基础组件使用 `@heroui/react`,高级导航、DataGrid、DropZone、ActionBar 和 EmptyState 使用本地镜像 `@heroui-pro/react`。
- 图标只从 `src/components/AppIcons.jsx` 导出,底层统一使用 Ant Design Icons。
- 样式导入顺序固定为 Tailwind、HeroUI、HeroUI Pro、鑫元 Token、项目页面样式。
## 2. Token 与尺寸
- 页面画布:浅色 `#F5F5F5`,暗色 `#18181B`。
- 页面左右 inset:桌面 68px,移动端 72px;真实滚动容器 `.rc-app-page` 直接持有该边距。
- 工作台模块和 KPI Card:24px 圆角;DataGrid:20px;普通控件:8px;Popover:12px;Modal:24px。
- 页面标题:20px/600/28px;模块标题:16px/600;普通正文:14px/400。
- DataGrid:表头 12px/500,正文 14px/400,主字段 14px/550,次字段 13px/400,Chip 12px/500。
- 常规按钮:桌面 32px、触屏 36px、24px 圆角、14px/500。主次按钮尺寸完全一致。
- 字距统一为 0,不使用随视口缩放的字号、重阴影、装饰渐变或大面积单色板。
## 3. Shell、登录与应用切换
- HeroUI Pro Sidebar 宽 240px,`collapsible="offcanvas"`,收起后宽度为 0,不保留图标轨道。
- Navbar 高 64px、透明无边框;使用 HeroUI Pro 默认样式的 filled `Sidebar.Trigger` 图标。
- Sidebar 底部用户行整体可点击,菜单内提供退出登录并复用现有 auth service。
- 主题切换位于 Navbar 最右侧,作用于文档根节点并保存到 `xinyuan_color_theme`。
- 登录采用居中认证 Card。花形 Logo 约 36px,标题约 32px/600;不重复显示“登录”标题。
- 账号、密码、验证码可见高度统一为 40px,字段组可见间距 24px,提交按钮上方保留 24px。
- 登录字段有值或自动填充时使用 `#fbf1f1`,Hover 使用 `#faeaea`,Focus 使用鑫元红描边和浅红焦点环。
- 应用切换器固定为居中的紧凑三列列表,标题“应用切换”、副标题“选择你可访问的应用”;最多一个“当前”标签,不显示数量、权限标签、外链图标或统计内容。
## 4. 页面、筛选与 Tab
- Navbar 是唯一可见页面标题;内容区只保留 `sr-only` 标题。
- 页面顺序:页面级 Tabs、筛选 Card、透明列表标题/操作行、DataGrid、Pagination。
- 实时筛选不显示“查询”按钮;首行最多三个字段,右侧只允许重置图标和文字型展开/收起。
- Input、Select、远程引用、多选和 DatePicker 使用共享 HeroUI 适配器,关闭态与打开态必须一致。
- 页面主数据集切换使用 `filter-tabs-wrap`;权限过滤后只有一个选项时隐藏整个轨道。
- 模块内部视角/模式/范围切换使用 `rc-segmented-control`,不得与页面级 Tabs 混用。
- 常规文字按钮保持纯文本;图标只用于真正的 icon-only 操作,并必须有 Tooltip 与 `aria-label`。
## 5. DataGrid、分页与反馈
- 业务列表使用 HeroUI Pro DataGrid。表格链路始终 `width: 100%`,仅在真实最小列宽超过视口时内部横向滚动。
- 横向滚动时固定首个业务列和操作列;不滚动时不固定。
- 操作按钮 32px 正圆、16px Ant 图标;1-3 个操作直接展示,4 个及以上保留两个高频操作,其余进入更多菜单。
- 状态和标签为单行紧凑 Chip,12px/500;不使用蓝、青、玫红或页面私有方角标签。
- 多选动作仅显示在当前视口底部居中的 ActionBar,右侧使用关闭图标清除选择。
- Pagination 提供 10/20/50 页容量,当前页使用圆形品牌描边和品牌文字,距离 DataGrid 约 8px。
- 成功反馈使用顶部居中的 3-4 秒 Toast,不得用页面流 Alert 推挤筛选或表格。
## 6. 表单、上传与弹窗
- Label 14px/500;Input、Textarea、Select 和引用控件只允许一个可见边框与一个字段阴影。
- 可见下拉禁止原生 `<select>`;日期/时间使用共享 HeroUI DatePicker,日期时间不得丢失时间编辑。
- 文件选择使用 HeroUI Pro DropZone,保留原 accept、单/多文件、disabled 和上传回调协议。
- Modal 居中显示,24px 圆角;标准内边距 28px,确认弹窗 24px;遮罩约 50% 黑并带 12px blur。
- 普通 CRUD 使用 `lg` 至 `3xl`,详情、上传、批量编辑和导入预览优先 `4xl`。
- Footer 顺序为取消在左、主操作在右;最终删除确认才使用 solid danger。
- 常规成功结果使用 Toast;需要用户处理的错误和警告可保留在对应上下文。
## 7. 看板与图表
- KPI 网格移动端 1 列、中屏 2 列;桌面 4 张为 4 列、5 张为 3+2、6 张为 3+3、8 张为 4+4。
- KPI Card 使用不透明中性表面、24px 圆角、细边框与共享轻阴影;不得使用渐变或每卡异色背景。
- 密集 KPI 图标面为 48px、16px 圆角、20px Ant 图标,容器与 SVG 中心偏差不得超过 0.5px。
- ECharts 必须从共享主题 Hook 读取浅色/暗色文字、网格、系列和 Tooltip Token。
- 图表 Tooltip 使用根主题 overlay surface,不能成为白色孤岛,也不能遮挡关键数据。
## 8. 响应式与交付检查
- 必测视口:1440x900、1280x720、768x1024、390x844。
- 必测状态:Sidebar 开/关、移动 offcanvas、用户菜单、应用切换、登录 filled/autofill、全部 Popover、选择态、空态、Modal 和暗色 Portal。
- 正式交付前运行生产构建、鑫元审计脚本、`git diff --check`,并确认浏览器控制台无新增错误。
# UI 架构合同
## 1. 直接引用原则
- 页面和新建共享组件必须直接从 `@heroui/react`、`@heroui-pro/react` 或已验证的 Pro 子路径导入。
- 禁止在 `catalyst-ui-kit`、旧 Button/Input/Checkbox/Dialog/Table/Select/DatePicker/Pagination/Upload 内嵌 HeroUI。
- 禁止继续扩展或修改 `src/catalyst-ui-kit`。新功能不得引用旧 UI 组件。
- 禁止创建复刻旧 props、DOM event、class hook 或 DOM 结构的兼容包装层。
- 一个功能面只能使用一套 UI 交互模型。允许的共享组件必须是业务中立、HeroUI-native 的复合组件,并暴露当前 HeroUI selection/value/event 合同。
## 2. 标准实现
- **DataGrid**:使用 HeroUI Pro `DataGrid` 原生列、排序、固定列、`selectedKeys`、`onSelectionChange`、`selectionMode` 与 `showSelectionCheckboxes`。分页使用 compound Pagination,批量操作使用 Pro ActionBar。禁止手写选择列或在 Cell 内嵌旧 Checkbox。
- **Modal**:使用 `Modal.Root > Modal.Backdrop > Modal.Container > Modal.Dialog`,业务页直接传受控 open/close、异步状态和提交回调。删除/重置使用受控 AlertDialog/Modal,不使用 `window.confirm`。
- **表单**:使用 HeroUI TextField/Input/TextArea/Checkbox/Select/DatePicker 的 compound 结构。Select 必须包含 ListBox;Checkbox、Select、Tabs、Modal 在 React Aria Context 中按官方 slot/compound 结构实现。
- **Tabs**:主数据集切换使用 HeroUI Tabs;只有一个可访问视图时隐藏轨道但保留原 key 与查询行为。模块级模式切换使用紧凑分段控件。
- **上传**:使用 `@heroui-pro/react/drop-zone`,向业务回调传真实 `File` 或 `File[]`,不伪造旧 input event。
- **图表**:ECharts 配置从鑫元 Token 读取颜色、文字、网格、Tooltip、图例和暗色表面;统计口径和数据转换留在现有业务层。
## 3. 分层
```text
页面/controller(请求、权限、状态、业务回调)
-> HeroUI/HeroUI Pro 官方组件
-> 少量 HeroUI-native 业务中立复合组件(设计、布局、无障碍)
-> 鑫元 Token 与项目语义样式
```
旧 Catalyst/Headless 组件不在这条链路中。`assets/portable` 只作为历史视觉参考,不作为代码 API。
## 4. HeroUI Pro 版本与交付
- 本机开发镜像:`@heroui-pro/react@1.0.0-beta.7`,来源为本机 `project_manager_new` 的完整安装。
- 本地 bootstrap 可执行 `npm install --legacy-peer-deps ../project_manager_new/node_modules/@heroui-pro/react`,但该 `file:` 路径不能作为跨机器交付方案。
- 团队正式安装必须使用私有 npm 或统一授权、带版本和校验信息的 `.tgz`。需要同时固定 `@heroui/react@3.2.2`、`@heroui/styles@3.2.2`、`react-aria-components@1.19.0` 和兼容 React 19 运行时。
- 正式同步前验证 `css`、`sidebar`、`navbar`、`data-grid`、`drop-zone`、`action-bar`、`empty-state` 子路径,并运行 `npm ls @heroui-pro/react @heroui/react @heroui/styles react-aria-components @react-aria/utils`。
- 当前 sibling `file:` 图存在 React 18/19 双实例风险,尚不能视为可复现交付。
## 5. 业务保护
UI 层不得修改 API 地址、请求方法/参数、响应映射、权限与禁用条件、校验规则、提交数据、登录退出协议、表格选择结果、分页排序筛选语义或图表统计口径。发现业务缺陷时只记录,另开业务变更。
## 6. 当前依赖状态
- 浏览器运行时通过 Umi alias 使用顶层 React/React DOM 19,活动页面没有出现重复 React Aria Context、slot 或 provider 异常。
- 用户指定的 `npm ls @heroui-pro/react @heroui/react @heroui/styles react-aria-components @react-aria/utils` 当前通过,并显示一套 HeroUI/React Aria 版本。加入 `react react-dom` 后仍会报告 `ELSPROBLEMS`:HeroUI 3 使用 React 19,而 Umi 构建依赖保留 React 16-18 peer 声明及嵌套 React 18。这不是可复现的干净完整依赖树。
- 当前 `file:../project_manager_new/node_modules/...` 只保证本机启动。团队未提供私有 npm 或授权 `.tgz` 前,不得提交为跨机器安装方案,也不得宣称依赖交付完成。
## 7. 代码审查门槛
每个迁移模块都要扫描 Catalyst/Headless/旧本地 UI import,真实浏览器检查打开态和控制台。活动范围旧引用必须为零;非活动旧模块单独列出。构建和浏览器验收通过可以确认本机运行质量,但不能覆盖当前依赖树和团队包源的可复现风险。
# UI 迁移清单
状态说明:`已迁移`表示活动运行时入口已经直接使用 HeroUI/HeroUI Pro;浏览器状态只记录真实数据和真实业务条件下实际执行过的交互,不用伪造数据补齐场景。
| 页面/模块 | 路由 | 迁移结果 | 保留的业务回调 | 状态 | 浏览器验收 |
| --- | --- | --- | --- | --- | --- |
| 登录 | `/login` | HeroUI Card/TextField/Input/Button/Alert | captcha、AES 加密、userLogin、redirect | 已迁移 | 通过:登录、跳转、统一 Logo/标题、输入状态和响应式 |
| AppShell | 活动鉴权路由 | HeroUI Pro Sidebar/Navbar + HeroUI Dropdown/Tooltip | history 路由、主题持久化 | 已迁移 | 通过:桌面完全收起/展开、移动 offcanvas、浅色/暗色 |
| 左侧用户/退出 | 活动鉴权路由 | HeroUI Dropdown compound API | userLogout、clearUser、base-aware 登录跳转 | 已迁移 | 通过:菜单打开、退出并回到登录页 |
| 应用切换 | 全局 | HeroUI Modal compound API | getApplications、URL 导航 | 已迁移 | 通过:打开/关闭、当前项唯一、浅色/暗色 Portal |
| 稽核项目 | `/audit_process` | HeroUI 表单/Modal/Tabs + HeroUI Pro DataGrid | 查询、筛选、增删改、引用选择 | 已迁移 | 通过:真实数据、Tab、实时筛选、新增/编辑/删除弹窗、枚举和远程引用关闭态回显/打开态搜索 |
| 稽核报告 | `/audit_report` | HeroUI Tabs/Modal + HeroUI Pro DataGrid | 当前/历史查询、填报链接、提交、删除 | 已迁移 | 通过:当前/历史 Tab、填报链接、历史详情跳转;提交按钮因 8 个控制点未完成而禁用,未绕过业务限制 |
| 报告内容 | 活动报告与历史详情复用 | HeroUI 表单 + HeroUI Pro DataGrid/DropZone | update、confirm point/process | 已迁移 | 通过:真实内容表、横向滚动、固定列、暗色表面;未提交真实变更 |
| 历史详情 | `/audit_report/:reportId` | HeroUI Button/Chip + HeroUI Pro DataGrid | 加载、返回、导出 | 已迁移 | 通过:真实历史报告加载和多组明细表 |
| 外部填报 | `/audit_report_fill` | HeroUI 表单/AlertDialog + HeroUI Pro DataGrid/DropZone | ticket 鉴权、保存、重置、beforeunload | 已迁移 | 通过:真实填报链接、DropZone、Select 打开态、根主题、标题和移动端;未选择文件,避免立即调用真实上传接口 |
| PageState | 多路由复用 | HeroUI Spinner/Button/Alert | retry | 已迁移 | 通过:活动页面加载/空状态可见,控制台无异常 |
## 浏览器验收记录
- 视口:`1440x900`、`1280x720`、`768x1024`、`390x844`。
- Shell:Sidebar 展开后 240px;桌面收起后移出视口且主内容扩展到完整宽度;移动 offcanvas 铺满面板。
- 响应式:四个视口均无页面级横向滚动;Tabs 和 DataGrid 在自己的容器内滚动。
- 表格:稽核项目、当前报告、历史详情和外部填报均使用真实记录验收;固定列和操作列在宽表内部滚动时保持可见。
- 表单与弹层:新增/编辑 Modal、删除 AlertDialog、应用切换、枚举 Select、远程引用 ListBox、外部填报 Select 已检查打开态和暗色表面。
- 选择回显:空值显示占位符;责任部门等单选关闭态显示真实名称;标签/制度多选关闭态显示已选名称并保留可移除标签;打开后切换为搜索词,不改变提交 ID/数组。
- 主题:浅色和暗色的 Sidebar、Navbar、Modal、Select/ListBox、DataGrid、DropZone 与移动 offcanvas 已检查;外部填报页同步根主题和浏览器标题。
- 控制台:最终代码修改后的全新认证会话为 `0 errors / 0 warnings`;仅有 Webpack 连接日志和 React DevTools 开发提示。
## 受真实业务状态限制的场景
- 当前报告仍有 8 个控制点未完成,提交按钮按原业务规则禁用,因此没有打开提交确认,也没有绕过禁用条件。
- DropZone 选中文件后会立即调用真实后端上传。本次只验证可见表面、打开态、暗色和响应式,没有选择文件或制造上传数据。
- 当前真实数据量不足一页,运行时没有出现 Pagination;当前活动流程也没有选择依赖的批量业务动作,因此 ActionBar 没有可触发入口。未伪造记录,也不把这两项写成已交互通过。
- 没有执行新增、编辑、删除、保存、提交或重置的最终确认,避免修改真实业务数据;只验证弹窗、字段、选择器和取消路径。
## 非活动旧模块
以下目录未进入当前 `.umirc.ts` 活动路由,仍含 Catalyst/Headless/Heroicons 引用,不计入已迁移范围:
- `src/pages/process_monitor`
- `src/pages/RecordDetailDrawer`
- `src/components/record-workbench`
恢复这些入口前必须先按同一 HeroUI 原生架构迁移,不能直接重新挂载。
## 最终门槛
- [x] 活动路由 Catalyst/Headless/Heroicons 引用为零。
- [x] `npm run build` 通过(Webpack 编译成功)。
- [x] `git diff --check` 通过。
- [x] 本地 Pro CSS 与六个 JS 子路径全部可解析/导入。
- [x] 指定 HeroUI/React Aria `npm ls` 命令通过并显示一套版本。
- [ ] 包含 React/Umi 的完整依赖树可跨机器复现。当前本地镜像、Umi peer 声明和嵌套 React 18 仍不满足此项。
- [x] 鑫元审计脚本完成最终重跑;非活动旧代码剩余引用已单独报告。
# 监察稽核管理鑫元设计规范
本文件是后续新增页面与功能的强制 UI 合同,视觉基线为鑫元设计系统 1.2。
## 1. 品牌与基础
- 应用显示名统一为“监察稽核管理”,用于登录、Sidebar、Navbar、应用切换当前项和浏览器标题。
- 主色为 `#9E1B1B`。选中、焦点、Checkbox、日期、Pagination 和信息状态通过 Token 使用品牌红。
- 浅色 Logo 使用 `src/assets/identity/xy-logo-red.png`,暗色使用 `xy-logo-white.png`;不使用滤镜、反色、色块或 RSM 标识。
- 样式顺序为 Tailwind、HeroUI、HeroUI Pro、鑫元 Token、项目语义样式。
- 字距固定为 0,不使用渐变、营销式大标题、装饰性多卡片或随视口缩放的字号。
## 2. 尺寸与层级
- 画布:浅色 `#F5F5F5`,暗色 `#18181B`;表面分别使用语义 surface Token。
- 页面左右 inset:桌面 68px、移动 72px;页面级横向溢出隐藏,宽表只在 DataGrid 内滚动。
- 工作台/KPI 模块 24px 圆角,DataGrid 20px,控件 8px,Popover 12px,Modal 24px。
- 页面标题 20/600/28;模块标题 16/600;正文 14/400;字距均为 0。
- DataGrid 表头 12/500,正文 14/400,主字段 14/550,次字段 13/400,Chip 12/500。
- 常规按钮桌面高 32px、触屏 36px、24px 圆角、14/500;主次按钮同尺寸,主操作最右。
## 3. 登录、Shell 与应用切换
- 登录使用居中认证 Card;36px 花形 Logo 与约 32/600 系统标题横向排列,并作为一个品牌组在 Card 内容区水平居中;表单标签和控件仍保持左对齐,不重复“登录”标题。
- 账号、密码、验证码可见高度统一 40px;字段组可见间距 24px,提交按钮上方 24px。
- filled/autofill 使用不透明浅红 `#fbf1f1`,Hover `#faeaea`,Focus 用品牌红;必须覆盖 Chrome autofill inset fill。
- HeroUI Pro Sidebar 宽 240px,`collapsible="offcanvas"`,收起后宽度为 0,不保留图标轨道。
- Sidebar 底部整行用户入口使用 HeroUI Dropdown compound API,显示头像、姓名、登录名,菜单内提供现有业务退出。
- “退出登录”统一使用 HeroUI `Dropdown.Item variant="danger"`:文字与退出图标在默认、Hover、Focus 状态均使用 `var(--danger)`,反馈背景使用主题语义表面,不得在不同应用间混用黑色与红色。
- Navbar 高 64px、透明无边框;Sidebar.Trigger 使用 Pro 默认 filled 图形。全局主题切换在最右并持久化。
- 应用切换使用居中 HeroUI Modal:约 46rem 宽、42rem 最大高、三列紧凑应用卡;最多一个“当前”,无统计、权限或装饰信息。
## 4. 页面、筛选和 Tabs
- Navbar 是唯一可见页面标题;内容区仅保留无障碍标题。
- 标准列表顺序:页面级 Tabs、筛选 Card、透明列表标题/操作行、DataGrid、Pagination。
- 实时筛选首行最多三个字段,不显示查询按钮;右侧使用刷新 icon button 和 text-only 展开/收起。
- Input、Select/ListBox、远程引用、DatePicker 的关闭态和打开态统一为 8px 控件、单边框、单阴影。
- 选择控件仅在空值时显示占位符。已有值时关闭态必须显示实际名称;远程名称尚在加载时显示“已选择 N 项”,不得退回搜索占位符。已选值使用主文本色与 500 字重,打开后再进入搜索状态。
- 主数据集切换使用 HeroUI Tabs;只有一个可访问视图时隐藏轨道。模块局部模式使用紧凑 segmented control。
- 普通业务按钮为纯文本;icon-only 只用于真实图标操作,必须有 Tooltip 与 `aria-label`。
## 5. DataGrid 与分页
- 所有业务列表使用 HeroUI Pro DataGrid 原生排序、选择、固定列和空状态;不手写 Checkbox 列。
- 表格始终填满内容宽度。仅在真实列最小宽度超过 viewport 时固定首个业务列和操作列并内部横向滚动。
- 操作列仅在当前可见数据至少有一行能够渲染真实操作时展示;无可用操作时隐藏整列并回收宽度,不使用空白单元格或 `-` 占位。
- 操作按钮为 32px 正圆、16px Ant Design icon。1-3 个直接显示,4 个以上显示两个高频项,其余进入更多菜单。
- 多选后在当前可视区底部显示 HeroUI Pro ActionBar,清除选择是最右 icon-only 关闭操作。
- Pagination 提供 10/20/50,切换页容量回到第一页并保留筛选、排序、权限范围。
- 空状态只显示一次“暂无数据”,表头和列宽在 loading/empty 时不能跳动。
## 6. 表单、上传、弹窗和反馈
- 页面直接使用 HeroUI TextField/Input/TextArea/Checkbox/Select/DatePicker compound API;禁止可见原生 select/date/file 控件。
- Checkbox 16px、4px 圆角;选中/半选的背景、边框和内填充均为 `#9E1B1B`,指标为白色。
- 上传使用 HeroUI Pro DropZone,保留 accept、disabled、单/多文件和原业务回调。
- Modal 所有视口居中,24px 圆角;标准 padding 28px、确认 24px,遮罩约 50% 黑和 12px blur。
- Footer 桌面右对齐,取消在前、主操作最右;移动端全宽反向堆叠。最终删除确认才使用 solid danger。
- 成功保存/删除/导入/导出用顶部居中 3-4 秒 Toast,不使用推挤页面布局的 Alert。
## 7. KPI 与图表
- KPI 网格:移动 1 列、中屏 2 列;桌面 4 张 4 列、5 张 3+2、6 张 3+3、8 张 4+4。
- KPI 使用不透明表面、24px 圆角、细边框和轻阴影,不使用每卡异色背景。
- 图标面 48px、16px 圆角、20px Ant icon,容器与 SVG 中心偏差不超过 0.5px。
- 同一 KPI 组的图标按业务语义使用不同 Token 前景/浅色表面,不能全部使用品牌红;卡片主体仍保持统一中性表面。监察稽核报告固定映射为:全部控制点 teal、未填写 danger、待确认 warning、已完成 success。
- ECharts 从 Token 读取系列、文字、网格、图例、Tooltip 和暗色表面;不修改业务统计口径。
## 8. 暗色、响应式与验收
- 根节点统一控制 light/dark,所有 Dropdown、Popover、Tooltip、Modal、DatePicker、DropZone、ActionBar 与图表 Portal 继承主题。
- 页面不得出现 white island、文字遮挡、页面级横向滚动或移动端按钮溢出。
- 必测视口:1440x900、1280x720、768x1024、390x844。
- 每个模块必须在真实浏览器验证打开态、键盘焦点、业务回调和控制台;构建成功不能替代页面验收。
This diff is collapsed.
This diff is collapsed.
...@@ -8,8 +8,12 @@ ...@@ -8,8 +8,12 @@
"start": "npm run dev" "start": "npm run dev"
}, },
"dependencies": { "dependencies": {
"@ant-design/icons": "file:../project_manager_new/node_modules/@ant-design/icons",
"@headlessui/react": "^2.2.10", "@headlessui/react": "^2.2.10",
"@heroicons/react": "^2.2.0", "@heroicons/react": "^2.2.0",
"@heroui-pro/react": "file:../project_manager_new/node_modules/@heroui-pro/react",
"@heroui/react": "file:../project_manager_new/node_modules/@heroui/react",
"@heroui/styles": "file:../project_manager_new/node_modules/@heroui/styles",
"@tailwindcss/postcss": "^4.2.0", "@tailwindcss/postcss": "^4.2.0",
"@umijs/max": "^4.5.3", "@umijs/max": "^4.5.3",
"clsx": "^2.1.1", "clsx": "^2.1.1",
...@@ -17,8 +21,12 @@ ...@@ -17,8 +21,12 @@
"dayjs": "^1.11.20", "dayjs": "^1.11.20",
"echarts": "^6.0.0", "echarts": "^6.0.0",
"echarts-for-react": "^3.0.6", "echarts-for-react": "^3.0.6",
"react": "^18.3.1", "motion": "file:../project_manager_new/node_modules/motion",
"react-dom": "^18.3.1", "react": "file:../project_manager_new/node_modules/react",
"react-aria-components": "file:../project_manager_new/node_modules/react-aria-components",
"react-dom": "file:../project_manager_new/node_modules/react-dom",
"tailwind-merge": "file:../project_manager_new/node_modules/tailwind-merge",
"tailwind-variants": "file:../project_manager_new/node_modules/tailwind-variants",
"tailwindcss": "^4.2.0", "tailwindcss": "^4.2.0",
"umi-request": "^1.4.0", "umi-request": "^1.4.0",
"umi-request-progress": "^1.1.0", "umi-request-progress": "^1.1.0",
......
import '@/global.less';
import '@/tailwind.css'; import '@/tailwind.css';
import '@/global.less';
import { defineApp } from 'umi'; import { defineApp } from 'umi';
export async function getInitialState() { export async function getInitialState() {
......
import BankOutlined from '@ant-design/icons/BankOutlined';
import BarChartOutlined from '@ant-design/icons/BarChartOutlined';
import AppstoreOutlined from '@ant-design/icons/AppstoreOutlined';
import CalendarOutlined from '@ant-design/icons/CalendarOutlined';
import CheckOutlined from '@ant-design/icons/CheckOutlined';
import CheckCircleOutlined from '@ant-design/icons/CheckCircleOutlined';
import DownloadOutlined from '@ant-design/icons/DownloadOutlined';
import DownOutlined from '@ant-design/icons/DownOutlined';
import ClockCircleOutlined from '@ant-design/icons/ClockCircleOutlined';
import CloseOutlined from '@ant-design/icons/CloseOutlined';
import DeleteOutlined from '@ant-design/icons/DeleteOutlined';
import DollarOutlined from '@ant-design/icons/DollarOutlined';
import DatabaseOutlined from '@ant-design/icons/DatabaseOutlined';
import EditOutlined from '@ant-design/icons/EditOutlined';
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 PaperClipOutlined from '@ant-design/icons/PaperClipOutlined';
import LeftOutlined from '@ant-design/icons/LeftOutlined';
import LogoutOutlined from '@ant-design/icons/LogoutOutlined';
import InboxOutlined from '@ant-design/icons/InboxOutlined';
import InfoCircleOutlined from '@ant-design/icons/InfoCircleOutlined';
import MoreOutlined from '@ant-design/icons/MoreOutlined';
import MoonOutlined from '@ant-design/icons/MoonOutlined';
import OrderedListOutlined from '@ant-design/icons/OrderedListOutlined';
import PieChartOutlined from '@ant-design/icons/PieChartOutlined';
import PlusOutlined from '@ant-design/icons/PlusOutlined';
import ProjectOutlined from '@ant-design/icons/ProjectOutlined';
import ReloadOutlined from '@ant-design/icons/ReloadOutlined';
import RightOutlined from '@ant-design/icons/RightOutlined';
import RiseOutlined from '@ant-design/icons/RiseOutlined';
import SearchOutlined from '@ant-design/icons/SearchOutlined';
import SendOutlined from '@ant-design/icons/SendOutlined';
import SunOutlined from '@ant-design/icons/SunOutlined';
import TeamOutlined from '@ant-design/icons/TeamOutlined';
import UserOutlined from '@ant-design/icons/UserOutlined';
import UploadOutlined from '@ant-design/icons/UploadOutlined';
import WarningOutlined from '@ant-design/icons/WarningOutlined';
function iconStyle(size, style) {
return size === undefined ? style : { fontSize: size, ...style };
}
function withSize(Icon) {
return function AppIcon({ size, style, ...props }) {
return (
<Icon aria-hidden="true" {...props} style={iconStyle(size, style)} />
);
};
}
export const Bank = withSize(BankOutlined);
export const Archive = withSize(InboxOutlined);
export const AppGrid = withSize(AppstoreOutlined);
export const BarChart = withSize(BarChartOutlined);
export const Calendar = withSize(CalendarOutlined);
export const Check = withSize(CheckOutlined);
export const ChevronDown = withSize(DownOutlined);
export const ChevronRight = withSize(RightOutlined);
export const Clock = withSize(ClockCircleOutlined);
export const Close = withSize(CloseOutlined);
export const Delete = withSize(DeleteOutlined);
export const Dollar = withSize(DollarOutlined);
export const Database = withSize(DatabaseOutlined);
export const Edit = withSize(EditOutlined);
export const Eye = withSize(EyeOutlined);
export const OpenExternal = withSize(ExportOutlined);
export const FileText = withSize(FileTextOutlined);
export const Info = withSize(InfoCircleOutlined);
export const Folder = withSize(FolderOpenOutlined);
export const LogOut = withSize(LogoutOutlined);
export function SidebarPanel({ size = 20, style, ...props }) {
return (
<span
aria-hidden="true"
role="img"
{...props}
style={iconStyle(size, style)}
className={['anticon', props.className].filter(Boolean).join(' ')}
>
<svg
viewBox="0 0 16 16"
width="1em"
height="1em"
fill="none"
xmlns="http://www.w3.org/2000/svg"
focusable="false"
aria-hidden="true"
>
<path
clipRule="evenodd"
d="M6 3.5h6A1.5 1.5 0 0 1 13.5 5v6a1.5 1.5 0 0 1-1.5 1.5H6zm-1.5 0H4A1.5 1.5 0 0 0 2.5 5v6A1.5 1.5 0 0 0 4 12.5h.5zM1 5a3 3 0 0 1 3-3h8a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3z"
fill="currentColor"
fillRule="evenodd"
/>
</svg>
</span>
);
}
export const More = withSize(MoreOutlined);
export const Moon = withSize(MoonOutlined);
export const Project = withSize(ProjectOutlined);
export const Search = withSize(SearchOutlined);
export const Send = withSize(SendOutlined);
export const Sun = withSize(SunOutlined);
export const Team = withSize(TeamOutlined);
export const User = withSize(UserOutlined);
export const Upload = withSize(UploadOutlined);
export const Back = withSize(LeftOutlined);
export const ArrowDownTrayIcon = withSize(DownloadOutlined);
export const ArrowPathIcon = withSize(ReloadOutlined);
export const ArrowTrendingUpIcon = withSize(RiseOutlined);
export const BanknotesIcon = withSize(DollarOutlined);
export const CalendarDaysIcon = Calendar;
export const ChartBarSquareIcon = BarChart;
export const ChartPieIcon = withSize(PieChartOutlined);
export const CheckCircleIcon = withSize(CheckCircleOutlined);
export const ChevronDownIcon = ChevronDown;
export const ChevronRightIcon = ChevronRight;
export const ClipboardDocumentListIcon = withSize(OrderedListOutlined);
export const ClockIcon = Clock;
export const ExclamationTriangleIcon = withSize(WarningOutlined);
export const FolderOpenIcon = Folder;
export const InformationCircleIcon = Info;
export const MagnifyingGlassIcon = Search;
export const PaperClipIcon = withSize(PaperClipOutlined);
export const PlusIcon = withSize(PlusOutlined);
export const UserGroupIcon = Team;
This diff is collapsed.
import { Upload } from '@/components/AppIcons';
import { DropZone } from '@heroui-pro/react/drop-zone';
import { useCallback } from 'react';
export default function FileUploadField({
accept,
ariaLabel = '选择文件',
className = '',
description = '将文件拖到此处,或从本地选择。',
isDisabled = false,
fileName = '',
onFilesChange,
triggerLabel = '选择文件',
}) {
const emitFiles = useCallback(
(files) => {
if (!isDisabled) {
onFilesChange?.(files);
}
},
[isDisabled, onFilesChange],
);
const handleDrop = useCallback(
async (event) => {
const fileItems = Array.from(event?.items || []).filter(
(item) => item?.kind === 'file' && typeof item.getFile === 'function',
);
if (fileItems.length === 0) return;
const files = await Promise.all(fileItems.map((item) => item.getFile()));
emitFiles(files.slice(0, 1));
},
[emitFiles],
);
return (
<DropZone className={`rc-file-upload ${className}`.trim()}>
<DropZone.Area
aria-label={ariaLabel}
isDisabled={isDisabled}
onDrop={handleDrop}
>
<DropZone.Input
accept={accept}
disabled={isDisabled}
onSelect={emitFiles}
/>
<DropZone.Icon>
<Upload aria-hidden="true" />
</DropZone.Icon>
<DropZone.Label>{fileName || '将文件拖到此处'}</DropZone.Label>
<DropZone.Description>
{fileName ? '已选择文件,可重新选择进行替换。' : description}
</DropZone.Description>
<DropZone.Trigger isDisabled={isDisabled}>{triggerLabel}</DropZone.Trigger>
</DropZone.Area>
</DropZone>
);
}
import { Label, ListBox, Select } from '@heroui/react';
import clsx from 'clsx';
import { useMemo } from 'react';
import { ChevronDown } from './AppIcons';
const EMPTY_OPTION_KEY = '__rc_filter_empty__';
function normalizeOption(option) {
if (option && typeof option === 'object') {
return {
label: String(option.label ?? option.value ?? ''),
value: String(option.value ?? ''),
disabled: Boolean(option.disabled),
};
}
return {
label: String(option ?? ''),
value: String(option ?? ''),
disabled: false,
};
}
export default function FilterSelect({
'aria-label': ariaLabel,
className,
isDisabled = false,
emptyText = '暂无可选项',
onChange,
options = [],
placeholder = '请选择',
value = '',
}) {
const normalizedOptions = useMemo(
() => options.map(normalizeOption),
[options],
);
const selectedValue = String(value ?? '');
const selectedOption = normalizedOptions.find(
(option) => option.value === selectedValue,
);
const hasValue = selectedValue !== '';
const displayLabel = selectedOption?.label || (hasValue ? selectedValue : placeholder);
const selectedKey = selectedValue === '' ? EMPTY_OPTION_KEY : selectedValue;
return (
<Select
aria-label={ariaLabel || placeholder}
className={clsx('rc-filter-select', className)}
fullWidth
isDisabled={isDisabled}
selectedKey={selectedKey}
variant="secondary"
onSelectionChange={(key) => {
const nextValue = key === EMPTY_OPTION_KEY ? '' : String(key ?? '');
onChange?.(nextValue);
}}
>
<Select.Trigger className="rc-filter-select__trigger" data-has-value={hasValue ? 'true' : undefined}>
<Select.Value className="rc-filter-select__value">
<span
className={clsx(
'block min-w-0 truncate',
!hasValue && 'rc-filter-select__placeholder',
)}
title={displayLabel}
>
{displayLabel}
</span>
</Select.Value>
<ChevronDown className="rc-filter-select__indicator" />
</Select.Trigger>
<Select.Popover
className="rc-filter-select__popover"
placement="bottom start"
>
<ListBox
aria-label={ariaLabel || placeholder}
className="rc-filter-select__listbox"
>
{normalizedOptions.length > 0 ? (
normalizedOptions.map((option) => (
<ListBox.Item
key={option.value || EMPTY_OPTION_KEY}
id={option.value || EMPTY_OPTION_KEY}
className="rc-filter-select__option"
isDisabled={option.disabled}
textValue={option.label}
>
<Label
className="rc-filter-select__option-label"
title={option.label}
>
{option.label}
</Label>
<ListBox.ItemIndicator className="rc-filter-select__option-indicator" />
</ListBox.Item>
))
) : (
<div className="rc-filter-select__empty">{emptyText}</div>
)}
</ListBox>
</Select.Popover>
</Select>
);
}
import { Back, Check, Clock, Close, Delete, Edit, Eye, More, Send } from '@/components/AppIcons';
import { Button, Dropdown, Tooltip } from '@heroui/react';
import clsx from 'clsx';
import { Children, Fragment, isValidElement } from 'react';
const ACTION_TONE_CLASSES = {
approve: 'pm-table-action--approve',
delete: 'pm-table-action--danger',
edit: 'pm-table-action--default',
history: 'pm-table-action--default',
reject: 'pm-table-action--danger',
revoke: 'pm-table-action--warning',
submit: 'pm-table-action--primary',
view: 'pm-table-action--default',
};
export const TABLE_ACTION_ICONS = {
approve: Check,
delete: Delete,
edit: Edit,
history: Clock,
reject: Close,
revoke: Back,
submit: Send,
view: Eye,
};
function flattenActions(children) {
const actions = [];
Children.forEach(children, (child) => {
if (!child) return;
if (isValidElement(child) && child.type === Fragment) {
actions.push(...flattenActions(child.props.children));
return;
}
actions.push(child);
});
return actions;
}
export function TableActionGroup({ className, children, ...props }) {
const actions = flattenActions(children);
const hasOverflow = actions.length >= 4;
const visibleActions = hasOverflow ? actions.slice(0, 2) : actions;
const overflowActions = hasOverflow ? actions.slice(2) : [];
return (
<div {...props} className={clsx('icon-actions flex flex-nowrap items-center gap-1.5 whitespace-nowrap', className)}>
{visibleActions}
{overflowActions.length > 0 ? (
<Dropdown>
<Tooltip delay={0}>
<Dropdown.Trigger aria-label="更多操作" className="pm-table-action pm-table-action--default"><More size={16} /></Dropdown.Trigger>
<Tooltip.Content>更多操作</Tooltip.Content>
</Tooltip>
<Dropdown.Popover className="rc-dropdown-popover min-w-36" placement="bottom end">
<Dropdown.Menu aria-label="更多操作" onAction={(key) => overflowActions[Number(key)]?.props?.onPress?.()}>
{overflowActions.map((action, index) => {
const label = action?.props?.label || '操作 ' + (index + 3);
const Icon = action?.props?.icon;
return (
<Dropdown.Item id={String(index)} isDisabled={action?.props?.isDisabled} key={label + '-' + index} className="rc-dropdown-option" textValue={label}>
<span className="flex items-center gap-2">{Icon ? <Icon size={16} aria-hidden="true" /> : null}<span>{label}</span></span>
</Dropdown.Item>
);
})}
</Dropdown.Menu>
</Dropdown.Popover>
</Dropdown>
) : null}
</div>
);
}
export function TableActionButton({ className, icon: Icon, isDisabled = false, label, onPress, tone = 'view' }) {
return (
<Tooltip delay={0}>
<Button
isIconOnly
aria-label={label}
className={clsx('pm-table-action', ACTION_TONE_CLASSES[tone] || ACTION_TONE_CLASSES.view, className)}
isDisabled={isDisabled}
size="sm"
variant="ghost"
onPress={onPress}
>
<Icon size={16} />
</Button>
<Tooltip.Content>{label}</Tooltip.Content>
</Tooltip>
);
}
export const APP_DISPLAY_NAME = '监察稽核管理';
export const APP_ROUTE_NAME = 'audit_manager_new';
export const APP_LOGIN_SUBTITLE = '内部控制与报告归档';
export const APP_NAV_ITEMS = Object.freeze([
{ label: '稽核项目', path: '/audit_process', icon: 'project' },
{ label: '稽核报告', path: '/audit_report', icon: 'report' },
]);
export function getAppRouteTitle(pathname) {
if (pathname.startsWith('/audit_report/')) return '报告详情';
return APP_NAV_ITEMS.find((item) => pathname === item.path)?.label || APP_DISPLAY_NAME;
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
import xyLogo from '@/assets/xy-logo.png'; import xyLogo from '@/assets/identity/xy-logo-red.png';
import { AuthLayout } from '@/catalyst-ui-kit/javascript/auth-layout'; import xyLogoWhite from '@/assets/identity/xy-logo-white.png';
import { Button } from '@/catalyst-ui-kit/javascript/button'; import { APP_DISPLAY_NAME } from '@/constants/app';
import { ErrorMessage, Field, Label } from '@/catalyst-ui-kit/javascript/fieldset';
import { Heading } from '@/catalyst-ui-kit/javascript/heading';
import { Input } from '@/catalyst-ui-kit/javascript/input';
import authService from '@/services/authService'; import authService from '@/services/authService';
import { getCaptcha, userLogin } from '@/services/DataService'; import { getCaptcha, userLogin } from '@/services/DataService';
import { SiteContext } from '@/wrapper/Site'; import { SiteContext } from '@/wrapper/Site';
import { Alert, Button, Card, Input, Label, Spinner, TextField } from '@heroui/react';
import CryptoJS from 'crypto-js'; import CryptoJS from 'crypto-js';
import { useContext, useEffect, useState } from 'react'; import { useContext, useEffect, useState } from 'react';
import { history, useSearchParams } from 'umi'; import { history, useSearchParams } from 'umi';
...@@ -41,6 +39,7 @@ export default function LoginPage() { ...@@ -41,6 +39,7 @@ export default function LoginPage() {
}; };
useEffect(() => { useEffect(() => {
document.title = APP_DISPLAY_NAME;
refreshCaptcha(); refreshCaptcha();
}, []); }, []);
...@@ -69,74 +68,45 @@ export default function LoginPage() { ...@@ -69,74 +68,45 @@ export default function LoginPage() {
}; };
return ( return (
<div className="min-h-dvh bg-zinc-100"> <main className="login-page">
<AuthLayout> <Card className="login-card">
<form onSubmit={handleSubmit} className="w-full max-w-sm space-y-7"> <Card.Content>
<div> <div className="login-brand login-card-brand">
<img src={xyLogo} alt="鑫元基金" className="h-9 w-auto object-contain" /> <img alt="鑫元" className="login-brand-logo login-brand-logo--light" src={xyLogo} />
<Heading className="mt-7">监察稽核管理</Heading> <img alt="" aria-hidden="true" className="login-brand-logo login-brand-logo--dark" src={xyLogoWhite} />
<p className="mt-2 text-sm text-zinc-500">{site?.title || site?.name || '鑫元基金'}</p> <strong>{APP_DISPLAY_NAME}</strong>
</div> </div>
<form className="login-form" onSubmit={handleSubmit}>
<div className="space-y-5"> <TextField className="login-field" isDisabled={loading} isRequired value={identity} onChange={setIdentity}>
<Field>
<Label>账号</Label> <Label>账号</Label>
<Input <Input autoComplete="username" className="login-input" data-filled={identity ? 'true' : undefined} placeholder="请输入账号" variant="secondary" />
value={identity} </TextField>
onChange={(event) => setIdentity(event.target.value)} <TextField className="login-field" isDisabled={loading} isRequired value={password} onChange={setPassword}>
disabled={loading}
autoComplete="username"
required
/>
</Field>
<Field>
<Label>密码</Label> <Label>密码</Label>
<Input <Input autoComplete="current-password" className="login-input" data-filled={password ? 'true' : undefined} placeholder="请输入密码" type="password" variant="secondary" />
type="password" </TextField>
value={password} <TextField className="login-field" isDisabled={loading} isRequired value={captchaCode} onChange={setCaptchaCode}>
onChange={(event) => setPassword(event.target.value)}
disabled={loading}
autoComplete="current-password"
required
/>
</Field>
<Field>
<Label>验证码</Label> <Label>验证码</Label>
<div className="flex items-stretch gap-3"> <div className="login-captcha-row">
<Input <Input className="login-input" data-filled={captchaCode ? 'true' : undefined} placeholder="请输入验证码" variant="secondary" />
value={captchaCode} <button
onChange={(event) => setCaptchaCode(event.target.value)} aria-label="刷新验证码"
disabled={loading} className="login-captcha"
required
/>
<Button
outline
type="button"
onClick={() => {
setError('');
refreshCaptcha();
}}
disabled={loading || captchaLoading} disabled={loading || captchaLoading}
className="h-11 w-36 shrink-0 overflow-hidden" onClick={() => { setError(''); refreshCaptcha(); }}
title="刷新验证码" type="button"
> >
{captchaData?.image ? ( {captchaData?.image ? <span dangerouslySetInnerHTML={{ __html: captchaData.image }} /> : captchaLoading ? <Spinner size="sm" /> : '刷新验证码'}
<span </button>
className="captcha-image flex h-full w-full items-center justify-center"
dangerouslySetInnerHTML={{ __html: captchaData.image }}
/>
) : captchaLoading ? '加载中...' : '刷新验证码'}
</Button>
</div> </div>
</Field> </TextField>
{error ? <ErrorMessage>{error}</ErrorMessage> : null} {error ? <Alert className="login-error" color="danger" role="alert">{error}</Alert> : null}
</div> <Button className="login-submit-button" fullWidth isDisabled={captchaLoading} isPending={loading} type="submit" variant="primary">
{loading ? '正在登录...' : '登录'}
<Button color="blue" type="submit" className="w-full" disabled={loading || captchaLoading}> </Button>
{loading ? '登录中...' : '登录'} </form>
</Button> </Card.Content>
</form> </Card>
</AuthLayout> </main>
</div>
); );
} }
This diff is collapsed.
This diff is collapsed.
import { Badge } from '@/catalyst-ui-kit/javascript/badge'; import { Back } from '@/components/AppIcons';
import { Button } from '@/catalyst-ui-kit/javascript/button'; import { Button, Chip, Tooltip } from '@heroui/react';
import { Heading } from '@/catalyst-ui-kit/javascript/heading';
import { ArrowDownTrayIcon, ArrowLeftIcon } from '@heroicons/react/20/solid';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { history, useParams } from 'umi'; import { history, useParams } from 'umi';
...@@ -50,35 +48,30 @@ export default function AuditReportDetail() { ...@@ -50,35 +48,30 @@ export default function AuditReportDetail() {
const stats = getReportStats(report); const stats = getReportStats(report);
return ( return (
<div className="space-y-6"> <div className="audit-page audit-report-detail">
<div className="flex flex-wrap items-start justify-between gap-4"> <div className="audit-detail-header">
<div className="flex items-start gap-3"> <div className="audit-detail-heading">
<Button plain aria-label="返回历史报告" title="返回" onClick={() => history.back()}> <Tooltip delay={0}><Button className="audit-back-button" isIconOnly aria-label="返回历史报告" size="sm" variant="ghost" onPress={() => history.back()}><Back size={16} /></Button><Tooltip.Content>返回</Tooltip.Content></Tooltip>
<ArrowLeftIcon />
</Button>
<div> <div>
<div className="flex flex-wrap items-center gap-2"> <div className="audit-detail-title-row">
<Heading>{report.title}</Heading> <h2>{report.title}</h2>
<Badge color="green">已归档</Badge> <Chip className="rc-status-chip--success" size="sm">已归档</Chip>
</div> </div>
<p className="mt-1 text-sm text-zinc-500"> <p>
提交时间:{report.metadata?.submitted_time ? dayjs(report.metadata.submitted_time).format('YYYY-MM-DD HH:mm') : '-'} 提交时间:{report.metadata?.submitted_time ? dayjs(report.metadata.submitted_time).format('YYYY-MM-DD HH:mm') : '-'}
</p> </p>
</div> </div>
</div> </div>
<Button outline onClick={() => exportAuditReport(report)}> <Button size="sm" variant="outline" onPress={() => exportAuditReport(report)}>导出</Button>
<ArrowDownTrayIcon />导出
</Button>
</div> </div>
<div className="flex flex-wrap gap-x-8 gap-y-2 border-y border-zinc-200 py-4 text-sm"> <div className="audit-detail-summary">
<span className="text-zinc-500">全部 <strong className="ml-1 text-zinc-950">{stats.total}</strong></span> <span>全部 <strong>{stats.total}</strong></span>
<span className="text-zinc-500">未填写 <strong className="ml-1 text-zinc-950">{stats.empty}</strong></span> <span>未填写 <strong>{stats.empty}</strong></span>
<span className="text-zinc-500">已完成 <strong className="ml-1 text-emerald-700">{stats.completed}</strong></span> <span>已完成 <strong className="is-success">{stats.completed}</strong></span>
</div> </div>
<AuditReportContent report={report} mode="readonly" issueOptions={issueOptions} /> <AuditReportContent report={report} mode="readonly" issueOptions={issueOptions} />
</div> </div>
); );
} }
This diff is collapsed.
import { Button } from '@/catalyst-ui-kit/javascript/button'; import { CheckCircleIcon, Close, ExclamationTriangleIcon } from '@/components/AppIcons';
import { ArrowPathIcon, ExclamationTriangleIcon } from '@heroicons/react/20/solid'; import { EmptyState as HeroEmptyState } from '@heroui-pro/react/empty-state';
import { Button, Spinner } from '@heroui/react';
import { useEffect } from 'react';
export function LoadingState({ label = '正在加载...' }) { export function LoadingState({ label = '正在加载...' }) {
return ( return <div className="audit-state"><Spinner size="sm" /><span>{label}</span></div>;
<div className="flex min-h-64 items-center justify-center gap-2 text-sm text-zinc-500">
<ArrowPathIcon className="size-4 animate-spin" />
{label}
</div>
);
} }
export function ErrorState({ message, onRetry }) { export function ErrorState({ message, onRetry }) {
return ( return (
<div className="flex min-h-64 flex-col items-center justify-center gap-3 text-center"> <div className="audit-state audit-state--error">
<ExclamationTriangleIcon className="size-7 text-red-600" /> <ExclamationTriangleIcon size={24} />
<div className="text-sm text-zinc-600">{message || '数据加载失败'}</div> <span>{message || '数据加载失败'}</span>
{onRetry ? ( {onRetry ? <Button size="sm" variant="outline" onPress={onRetry}>重新加载</Button> : null}
<Button outline onClick={onRetry}>
<ArrowPathIcon />
重新加载
</Button>
) : null}
</div> </div>
); );
} }
export function EmptyState({ title, description, action }) { export function EmptyState({ title, description, action }) {
return ( return (
<div className="flex min-h-56 flex-col items-center justify-center border-y border-dashed border-zinc-200 px-6 text-center"> <HeroEmptyState className="audit-empty-state" size="md">
<div className="text-sm font-semibold text-zinc-900">{title}</div> <HeroEmptyState.Header>
{description ? <div className="mt-1 max-w-lg text-sm text-zinc-500">{description}</div> : null} <HeroEmptyState.Title>{title}</HeroEmptyState.Title>
{action ? <div className="mt-4">{action}</div> : null} {description ? <HeroEmptyState.Description>{description}</HeroEmptyState.Description> : null}
</div> </HeroEmptyState.Header>
{action ? <HeroEmptyState.Content>{action}</HeroEmptyState.Content> : null}
</HeroEmptyState>
); );
} }
export function InlineNotice({ tone = 'success', children, onDismiss }) { export function InlineNotice({ tone = 'success', children, onDismiss }) {
const tones = { useEffect(() => {
success: 'border-emerald-200 bg-emerald-50 text-emerald-800', if (!onDismiss) return undefined;
error: 'border-red-200 bg-red-50 text-red-800', const timeout = window.setTimeout(onDismiss, 3600);
info: 'border-sky-200 bg-sky-50 text-sky-800', return () => window.clearTimeout(timeout);
}; }, [children, onDismiss]);
return ( return (
<div className={`flex items-center justify-between gap-3 rounded-lg border px-4 py-3 text-sm ${tones[tone]}`}> <div className={`rc-toast rc-toast--${tone}`} role={tone === 'error' ? 'alert' : 'status'} aria-live={tone === 'error' ? 'assertive' : 'polite'}>
<span className="rc-toast__icon">{tone === 'error' ? <ExclamationTriangleIcon size={16} /> : <CheckCircleIcon size={16} />}</span>
<span>{children}</span> <span>{children}</span>
{onDismiss ? ( {onDismiss ? <button aria-label="关闭提示" className="rc-toast__close" onClick={onDismiss} type="button"><Close size={14} /></button> : null}
<button type="button" onClick={onDismiss} className="font-medium">关闭</button>
) : null}
</div> </div>
); );
} }
This diff is collapsed.
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