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

设计统一

parent 6dc24cea
...@@ -76,6 +76,9 @@ export default defineConfig({ ...@@ -76,6 +76,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',
......
# OA 流程监控 UI/UX 全局审计
审计日期:2026-08-11
审计分支:`codex/xinyuan-ui-ux`
约束:只调整 UI/UX,不修改接口、路由、权限、数据结构、表单提交和业务状态机。
## 审计范围
- 登录页、鉴权加载态与验证码。
- 桌面/移动端 Shell、侧栏、导航、应用切换和用户信息。
- 流程监控工作台、分组切换、模板列表及管理弹窗。
- 模板配置、流程实例详情、记录抽屉和引用记录表格。
- 通用筛选、表格、空状态、反馈状态、分页和行操作。
## 迁移前主要问题
1. 品牌身份不统一:登录页为深色营销式布局,Shell 使用深灰侧栏,局部存在 teal、sky、cyan、rose 等页面私有品牌色。
2. Logo 与标题关系不统一:展开侧栏缺少完整鑫元 Logo,折叠态与应用切换按钮尺寸过大,品牌区间距未使用统一 Token。
3. 页面标题装饰过重:工作台使用大面积渐变 Banner,并展示面向开发者的说明文字,不符合企业业务应用的信息层级。
4. 页面切换样式不统一:分组切换使用大卡片,未采用统一的紧凑 Tabs/Segment 视觉。
5. 筛选模块分散:搜索和选择器直接放在标题区域或普通 Card 中,按钮、字段间距、展开区和响应式行为不一致。
6. 列表样式不统一:Catalyst Table 与页面私有表格并存,表头、固定列、行操作、空状态和选择状态缺乏统一语义。
7. 分页不统一:上一页/下一页与页码采用普通文本按钮,当前页、每页条数和总数摘要未使用鑫元分页视觉。
8. 提示信息过多直出:页面内存在开发背景、迁移说明和重复帮助文案,应收敛为标题旁 `info` 小提示,仅在需要时查看。
9. 表面层级不统一:页面存在 22/24/28/32px 多套圆角、彩色渐变、玻璃效果和页面私有阴影。
10. 暗色主题缺失:当前仅有单一视觉,Overlay、表面和文本未随根主题统一切换。
## 本轮迁移基线
- 主品牌色统一为 `#9E1B1B`,禁止将 sky/teal/cyan/rose 作为可见品牌色。
- 使用本地完整 HeroUI Pro 镜像及 HeroUI 样式层。
- 同步已验收鑫元 Token、Shell 和组件 CSS。
- 登录页使用鑫元红 Logo、紧凑品牌区、统一输入高度和不透明浅红 filled/autofill 状态。
- Shell 使用浅色同画布侧栏、紧凑品牌区、中性选中项、全局主题切换和无阴影导航表面。
- 页面说明收敛为标题旁 `info` 图标;业务状态和错误信息仍保留可见反馈。
- 分组切换改为统一页面 Tabs 视觉;筛选区按统一 Card、字段、按钮和响应式规则治理。
- 分页当前页使用鑫元红实心状态,其余操作保持中性。
- 页面级 Card 使用统一模块圆角、边框和 `--rc-shadow-card`,DataGrid/列表主体不叠加多余阴影。
## 后续验收清单
- 1440px 与 390px 下检查登录、Shell、列表和弹层无横向溢出。
- 检查 Logo 与标题间距、应用切换按钮尺寸、折叠侧栏几何。
- 检查 Tabs 的默认、Hover、选中和键盘 Focus 状态。
- 检查筛选字段、重置/查询/展开按钮和移动端堆叠。
- 检查分页总数、每页条数、当前页、禁用态和页码省略符。
- 检查提示字段仅以 `info` 图标存在,Tooltip 可访问且不遮挡内容。
- 检查浅色/深色主题下 Dropdown、Modal、Tooltip、表格和图表表面。
# 鑫元 OA 流程监控设计规范
本文档是本项目后续新增页面和功能的 UI 执行基线。新功能必须复用这里定义的 Token、组件和布局模式,不得新增页面私有品牌体系。
## 1. 基本原则
- 保持业务逻辑不变,UI 组件只承接展示与交互。
- 品牌主色固定为 `#9E1B1B`,颜色通过 `src/design-system/tokens.css` 的语义变量使用。
- 优先使用 HeroUI / HeroUI Pro;现有 Catalyst 组件在迁移期间必须通过统一 CSS 语义层呈现。
- 图标使用单一图标层,操作图标必须居中并提供 `aria-label` 或 Tooltip。
- 页面不得出现开发背景、迁移说明等冗余文案;补充说明放在标题旁 `info` 图标中。
## 2. 依赖与样式顺序
```css
@import 'tailwindcss';
@import '@heroui/styles';
@import '@heroui-pro/react/css';
@import './design-system/tokens.css';
@import './design-system/shell.css';
@import './design-system/components.css';
```
HeroUI Pro 必须使用本地完整镜像:
```json
"@heroui-pro/react": "file:../project_manager_new/node_modules/@heroui-pro/react"
```
## 3. 品牌与 Logo
- 浅色主题:`src/assets/xy-logo-red.png`。
- 深色主题:`src/assets/xy-logo-white.png`。
- Logo 使用 `object-fit: contain`,不得通过 CSS 反色或放置彩色底板模拟。
- 侧栏品牌区按“Logo → 8~10px 间距 → 系统标题 → 应用切换按钮”排列。
- 应用切换按钮统一为 36px 圆形按钮,不得比标题区域更抢眼。
- 登录 Card 内容内边距为 36px,品牌区与表单间距为 32px;登录字段标签到控件为 12px,账号/密码/验证码字段根节点之间必须有 24px 真实可见间距,不能只依赖 HeroUI 外层 `form gap`;验证码区域到登录按钮保留 24px 顶间距,禁止标签贴近上一输入框。
## 4. Shell
- 页面背景和侧栏默认使用 `--rc-bg-canvas`,主内容使用 `--rc-bg-surface`。
- Sidebar、Navbar 不使用阴影;选中导航使用中性表面,图标使用品牌红。
- 桌面页面标题左侧基准为 `--rc-page-title-inset: 68px`,移动端为 `72px`。
- 顶栏右侧提供 36px 主题切换按钮,主题写入 `xinyuan_color_theme`。
- 应用切换弹窗以当前已验收的 `src/components/ApplicationSwitcherDialog.jsx` 为唯一基线,后续必须原样复用 Headless Dialog DOM 和 class list,只允许适配应用数据、当前应用判断与跳转逻辑,禁止使用 Catalyst/HeroUI 通用 Modal 骨架做近似替代。
- 弹窗固定居中:遮罩 `rgba(15,23,42,0.42)` 并轻微模糊;面板宽 `min(46rem, 100vw - 2rem)`、最大高 `min(42rem, 100dvh - 2rem)`、16px 圆角、1px `--rc-border-default` 描边、`--rc-bg-surface` 背景和 `--rc-shadow-overlay` 阴影。
- Header 与 Body 均使用 `px-5 py-4`;标题固定“应用切换”,下方显示“选择你可访问的应用”。这里的副标题是应用切换器明确例外,不改成 info Tooltip。关闭按钮固定 24×24px 圆形,内部 16px Close icon。
- 应用列表固定使用 `grid gap-2 sm:grid-cols-2 lg:grid-cols-3`;卡片 12px 圆角、`px-3 py-3`、`--rc-bg-subtle` 表面,Logo/首字回退均为 36×36px。标题 14px/500/20px,描述 12px/400/16px并单行截断。
- 当前应用只允许一个 `当前` 标记,卡片使用 `data-current="true"`、鑫元 soft 背景和低透明度品牌描边。禁止分组标题、应用数量、访问级别/角色/分组标签、外链 icon、统计卡、装饰内容、营销文案和文字“关闭”按钮。
- 验收必须与已验收组件逐项比对面板位置/尺寸、Header 高度、关闭按钮、列数/间距、卡片高度/内边距、Logo、字级、当前态和内部滚动;只做到颜色或圆角接近不算完成。
## 5. 页面标题与提示
- 页面主标题建议 `24px / 600`,不得使用大面积营销 Banner。
- 标题后的补充说明使用 `info` 小图标和 Tooltip,不直接占用正文空间。
- 错误、警告、成功等业务反馈必须继续可见,不能隐藏在 Tooltip 中。
- 页面操作区的主按钮固定在最右侧,次按钮从右向左排列;常规文字按钮禁止添加装饰性加号、上传、下载或搜索图标,只有真正的 icon-only 操作保留图标并提供 Tooltip。
## 6. Tabs
- 页面级切换使用紧凑轨道:浅色中性背景、12~14px 圆角。
- 选中项使用白色/主题表面、品牌红文字和轻微交互阴影。
- 禁止用大卡片、底部粗红线或大面积品牌色块表示选中。
- 页面级 Tab 桌面端必须按选项内容收缩,只有外层横向滚动容器允许占满页面;禁止外层和 `Tabs` 根节点同时使用 `filter-tabs-wrap`,禁止最后一个选项后残留整行空轨道。
- Tab 标签默认只显示名称,不显示装饰性数量徽标;选中项使用鑫元红文字和白色表面。
- 页面级 Tab 与其作用域内的新增/管理操作放在同一工具行:Tab 左对齐,次按钮和主按钮在右侧,鑫元红主按钮固定最右;移动端允许整组操作换到 Tab 下方并自适应宽度。
## 7. 筛选模块
- 筛选区是独立 Card,主筛选字段优先单行排列,移动端按一列堆叠。
- 操作顺序固定为:重置、查询、展开/收起。
- 查询按钮使用品牌主色;重置使用 outline;展开使用 ghost。
- 字段标签、控件高度、间距和错误信息必须统一,禁止裸原生 Select。
- Catalyst/HeroUI 输入框必须通过 `rc-form-input` 单表面适配器呈现:默认态最外层必须显示 1px 中性描边,Hover 描边加深,Focus 切换为鑫元红描边和 soft ring;内部真实 `input` 必须透明、无描边、无阴影。禁止默认态无边框、双白框或聚焦时内外双红框。
## 8. 表格与分页
- 数据列表优先使用 HeroUI Pro DataGrid;迁移期 Catalyst Table 必须服从统一表头、边框和状态样式。
- 列表区域不使用 Card 阴影;固定列阴影只表达滚动层级。
- 行操作优先使用 32px 圆形图标按钮,危险操作使用 danger-soft。
- 分页显示总数、当前范围、每页条数和页码;当前页使用鑫元红实心状态。
- 流程监控模板列表必须使用 HeroUI Pro `DataGrid`,不再使用 Catalyst Table;DataGrid 自身拥有 20px 圆角、1px `--rc-border-default` 描边和不透明表面,列表标题/action 行保持透明无框。
- 表头使用 12px/500 弱化文字,普通单元格 14px/400,主标题 14px/550,辅助 ID 13px/400;状态使用共享 success Chip,操作列固定在右侧并使用 32px icon-only 操作。
- 列表中作为详情入口的主标题默认使用鑫元红,Hover/键盘聚焦时颜色加深并显示 1px 下划线;普通不可点击主文本仍使用 `--rc-text-primary`,不得把整列普通文字全部染红。
- 操作列必须复用 `TableActionGroup` 和 `TableActionButton`:双操作列宽度 112px,表头、单元格和按钮组全部右对齐,按钮组必须占满单元格宽度以避免被共享 `.icon-actions` 左对齐规则覆盖;按钮固定 32×32、圆形、间距 6px。编辑使用 `pm-table-action--default` 中性浅底,删除/取消使用 `pm-table-action--danger` 红色 soft 底,不得直接混用 HeroUI `ghost`/`danger-soft` 形成大小和底色不一致的按钮。
- 列表默认每页 10 条,并提供 10/20/50 页容量;分页显示总数和当前页,筛选、切换分组或页容量变化后回到第一页。
- 有数据的 DataGrid 必须按实际行数收缩高度,禁止因默认 `min-height` 在单行数据后留下大块空白。圆角只属于最外层 DataGrid,body 单元格和固定操作列不得各自出现底部圆角;固定操作列使用同一行背景和轻量左分隔,禁止形成独立弧形白色岛。
- 模板实例详情页使用同一列表合同:返回导航与页面操作同一工具行,主按钮最右;筛选 Card 只承载业务筛选字段和重置操作,页容量必须放在分页区域;动态字段列表使用 HeroUI Pro DataGrid,标题入口使用鑫元红可点击样式,空状态只保留一个来源。
## 9. Card、弹层与状态
- 页面模块圆角使用 `--rc-radius-module`,静态 Card 阴影使用 `--rc-shadow-card`。
- 数字/KPI Card 必须在右侧提供与指标含义对应的业务图标;密集卡片使用 48×48 图标底板、16px 圆角和 20×20 图标。
- 数字/KPI Card 必须直接复用 `rc-card-surface rc-dashboard-card`:Card 圆角固定 24px,1px 描边使用 `--rc-border-default`,不得硬编码深色描边、直角或页面私有圆角。
- 同一组数字卡片的 Card 本体必须统一使用不透明中性纯色表面 `--rc-bg-surface`,禁止渐变、半透明染色或每张卡片使用不同颜色。
- 视觉变化只允许放在克制的图标底板中;同一组指标必须使用不同的语义 tone 和不同业务图标,禁止重复颜色或相邻色调,禁止“彩虹卡片墙”式的卡片本体着色。
- Modal 使用 `--rc-shadow-overlay`,Dropdown/Tooltip 使用 raised/overlay 层级。
- EmptyState 包含标题、简短说明和最多一个主操作。
- 深色主题画布、Card、嵌套区域使用明确不透明表面,禁止叠加半透明浅色灰块。
## 10. 开发检查
新增或修改页面完成后至少执行:
```bash
npm run build
```
并在 1440px、390px 下检查浅色/深色、Hover、Focus、打开的下拉层、Modal、分页和空状态。
KPI 卡片属于首轮验收硬门槛,不能只看构建结果。必须在浏览器计算样式中逐项确认:Card 圆角为 24px、描边为 1px、描边色来自 `--rc-border-default`、背景来自 `--rc-bg-surface`,每张卡片右侧有对应业务图标,并且同组图标的图标、前景色和 soft 底色均不重复。任一项不符都不得标记迁移完成。
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": "^6.3.2",
"@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,16 +21,19 @@ ...@@ -17,16 +21,19 @@
"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", "react": "^19.2.8",
"react-dom": "^18.3.1", "react-aria-components": "^1.20.0",
"react-dom": "^19.2.8",
"tailwind-merge": "^3.6.0",
"tailwind-variants": "^3.3.1",
"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",
"xlsx": "^0.18.5" "xlsx": "^0.18.5"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^18.0.33", "@types/react": "^19.2.18",
"@types/react-dom": "^18.0.11", "@types/react-dom": "^19.2.4",
"typescript": "^5.0.3" "typescript": "^5.0.3"
} }
} }
import '@/global.less'; import '@/global.less';
import '@/tailwind.css'; import '@/tailwind.css';
import { I18nProvider } from '@heroui/react';
import { createElement } from 'react';
import { defineApp } from 'umi'; import { defineApp } from 'umi';
export async function getInitialState() { export async function getInitialState() {
...@@ -8,4 +10,8 @@ export async function getInitialState() { ...@@ -8,4 +10,8 @@ export async function getInitialState() {
}; };
} }
export default defineApp({}); export default defineApp({
rootContainer(container) {
return createElement(I18nProvider, { locale: 'zh-CN' }, container);
},
});
import AppstoreOutlined from '@ant-design/icons/AppstoreOutlined';
import ArrowLeftOutlined from '@ant-design/icons/ArrowLeftOutlined';
import DatabaseOutlined from '@ant-design/icons/DatabaseOutlined';
import DeleteOutlined from '@ant-design/icons/DeleteOutlined';
import EditOutlined from '@ant-design/icons/EditOutlined';
import ExportOutlined from '@ant-design/icons/ExportOutlined';
import FieldTimeOutlined from '@ant-design/icons/FieldTimeOutlined';
import FileTextOutlined from '@ant-design/icons/FileTextOutlined';
import FilterOutlined from '@ant-design/icons/FilterOutlined';
import FolderOpenOutlined from '@ant-design/icons/FolderOpenOutlined';
import InfoCircleOutlined from '@ant-design/icons/InfoCircleOutlined';
import LogoutOutlined from '@ant-design/icons/LogoutOutlined';
import MoonOutlined from '@ant-design/icons/MoonOutlined';
import OrderedListOutlined from '@ant-design/icons/OrderedListOutlined';
import ReloadOutlined from '@ant-design/icons/ReloadOutlined';
import RightOutlined from '@ant-design/icons/RightOutlined';
import SunOutlined from '@ant-design/icons/SunOutlined';
import TeamOutlined from '@ant-design/icons/TeamOutlined';
import CloseOutlined from '@ant-design/icons/CloseOutlined';
function withSize(Icon) {
return function AppIcon({ size, style, ...props }) {
return <Icon aria-hidden="true" {...props} style={size ? { fontSize: size, ...style } : style} />;
};
}
export const AppGrid = withSize(AppstoreOutlined);
export const ArrowLeft = withSize(ArrowLeftOutlined);
export const DataStore = withSize(DatabaseOutlined);
export const Delete = withSize(DeleteOutlined);
export const Edit = withSize(EditOutlined);
export const External = withSize(ExportOutlined);
export const FieldTime = withSize(FieldTimeOutlined);
export const FileText = withSize(FileTextOutlined);
export const Filter = withSize(FilterOutlined);
export const FolderOpen = withSize(FolderOpenOutlined);
export const Info = withSize(InfoCircleOutlined);
export const LogOut = withSize(LogoutOutlined);
export function SidebarPanel({ size = 16, style, className, ...props }) {
return (
<span
aria-hidden="true"
role="img"
{...props}
className={['anticon', className].filter(Boolean).join(' ')}
style={size ? { fontSize: size, ...style } : style}
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="1em"
viewBox="0 0 16 16"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<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 Moon = withSize(MoonOutlined);
export const ProcessList = withSize(OrderedListOutlined);
export const Reload = withSize(ReloadOutlined);
export const ChevronRight = withSize(RightOutlined);
export const Sun = withSize(SunOutlined);
export const Team = withSize(TeamOutlined);
export const Close = withSize(CloseOutlined);
import { Sidebar } from '@heroui-pro/react/sidebar';
export default function AppShell({ theme, open, onOpenChange, navigate, sidebar, navbar, children, overlays }) {
return (
<Sidebar.Provider
className={`rc-app-shell ${theme}`}
collapsible="offcanvas"
navigate={navigate}
open={open}
onOpenChange={onOpenChange}
>
{sidebar}
<Sidebar.Main className="rc-app-main">
{navbar}
<div className="app-shell-content-scroll min-h-0 flex-1 overflow-y-auto">
<main className="rc-app-page">{children}</main>
</div>
</Sidebar.Main>
{overlays}
</Sidebar.Provider>
);
}
import { Moon, SidebarPanel, Sun } from '@/components/AppIcons';
import { Navbar } from '@heroui-pro/react/navbar';
import { Sidebar, useSidebar } from '@heroui-pro/react/sidebar';
import { Button, Tooltip } from '@heroui/react';
export default function DashboardNavbar({ theme, title, onThemeChange }) {
const { isMobile, isOpen } = useSidebar();
const isDark = theme === 'dark';
const themeLabel = isDark ? '切换至浅色模式' : '切换至深色模式';
return (
<Navbar className="rc-navbar" maxWidth="full">
<Navbar.Header>
<Sidebar.Trigger aria-label={isMobile ? '打开导航' : isOpen ? '收起侧栏' : '展开侧栏'}>
<SidebarPanel size={16} />
</Sidebar.Trigger>
<h1 className="rc-navbar-title">{title}</h1>
<Navbar.Spacer />
<Tooltip delay={0}>
<Button
isIconOnly
aria-label={themeLabel}
aria-pressed={isDark}
className="rc-navbar-icon-button"
size="sm"
variant="ghost"
onPress={() => onThemeChange(isDark ? 'light' : 'dark')}
>
{isDark ? <Sun size={16} /> : <Moon size={16} />}
</Button>
<Tooltip.Content>{themeLabel}</Tooltip.Content>
</Tooltip>
</Navbar.Header>
</Navbar>
);
}
import xyLogoRed from '@/assets/xy-logo-red.png';
import xyLogoWhite from '@/assets/xy-logo-white.png';
import { AppGrid, LogOut } from '@/components/AppIcons';
import { Sidebar } from '@heroui-pro/react/sidebar';
import { Avatar, Button, Dropdown, Label, Tooltip } from '@heroui/react';
function SidebarUserMenu({ userInfo }) {
const displayName = userInfo?.display_name || userInfo?.name || userInfo?.username || '当前用户';
const username = userInfo?.username || userInfo?.email || '已登录';
return (
<Dropdown>
<Button fullWidth className="rc-sidebar-user-trigger" size="sm" variant="ghost">
<Avatar className="rc-sidebar-avatar">
{userInfo?.avatar ? <Avatar.Image alt={displayName} src={userInfo.avatar} /> : null}
<Avatar.Fallback>{String(displayName).trim().slice(0, 1).toUpperCase() || 'U'}</Avatar.Fallback>
</Avatar>
<span className="rc-sidebar-user-copy" data-sidebar="label">
<strong>{displayName}</strong>
<small>{username}</small>
</span>
</Button>
<Dropdown.Popover placement="top start">
<Dropdown.Menu aria-label="用户菜单" onAction={(key) => key === 'logout' && userInfo?.logout?.()}>
<Dropdown.Item id="logout" textValue="退出登录" variant="danger">
<div className="flex w-full items-center justify-between gap-3">
<Label>退出登录</Label>
<LogOut size={14} />
</div>
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown.Popover>
</Dropdown>
);
}
function SidebarContents({ appName, menuGroups, pathname, userInfo, onOpenApplications }) {
return (
<>
<Sidebar.Header>
<div className="rc-sidebar-brand">
<div className="rc-sidebar-brand-mark">
<img src={xyLogoRed} alt="鑫元" className="rc-sidebar-logo rc-sidebar-logo-light" />
<img src={xyLogoWhite} alt="鑫元" className="rc-sidebar-logo rc-sidebar-logo-dark" />
</div>
<div className="rc-sidebar-brand-main" data-sidebar="label"><span className="rc-sidebar-title">{appName}</span></div>
<div className="rc-sidebar-app-switcher" data-sidebar="label">
<Tooltip delay={0}>
<Button isIconOnly aria-label="切换应用" className="rc-sidebar-app-trigger" size="sm" variant="ghost" onPress={onOpenApplications}>
<AppGrid size={16} />
</Button>
<Tooltip.Content>切换应用</Tooltip.Content>
</Tooltip>
</div>
</div>
</Sidebar.Header>
<Sidebar.Content>
{menuGroups.map((group) => (
<Sidebar.Group key={group.title}>
<Sidebar.GroupLabel>{group.title}</Sidebar.GroupLabel>
<Sidebar.Menu aria-label={group.title}>
{group.items.map((item) => (
<Sidebar.MenuItem href={item.path} isCurrent={pathname.startsWith(item.path)} key={item.path} textValue={item.name} tooltip={item.name}>
<Sidebar.MenuIcon><item.icon size={16} /></Sidebar.MenuIcon>
<Sidebar.MenuLabel>{item.name}</Sidebar.MenuLabel>
</Sidebar.MenuItem>
))}
</Sidebar.Menu>
</Sidebar.Group>
))}
</Sidebar.Content>
<Sidebar.Footer><SidebarUserMenu userInfo={userInfo} /></Sidebar.Footer>
</>
);
}
export default function DashboardSidebar(props) {
return (
<>
<Sidebar className="rc-sidebar"><SidebarContents {...props} /></Sidebar>
<Sidebar.Mobile className="rc-mobile-sidebar" aria-label={`${props.appName}导航`}><SidebarContents {...props} /></Sidebar.Mobile>
</>
);
}
import { ArrowLeft } from '@/components/AppIcons';
import { Breadcrumbs, Button, Tooltip } from '@heroui/react';
import { useNavigate } from 'umi';
export default function DetailNavigation({ backHref, backLabel, items, currentLabel }) {
const navigate = useNavigate();
return (
<nav className="rc-detail-navigation" aria-label="详情页导航">
<Tooltip delay={400}>
<Tooltip.Trigger>
<Button
isIconOnly
aria-label={backLabel}
className="rc-detail-navigation__back"
size="sm"
variant="outline"
onPress={() => navigate(backHref)}
>
<ArrowLeft size={16} />
</Button>
</Tooltip.Trigger>
<Tooltip.Content placement="bottom">{backLabel}</Tooltip.Content>
</Tooltip>
<Breadcrumbs className="rc-detail-navigation__breadcrumbs" separator={<span aria-hidden="true">/</span>}>
{items.map((item) => (
<Breadcrumbs.Item href={item.href} key={`${item.href}-${item.label}`}>
{item.label}
</Breadcrumbs.Item>
))}
<Breadcrumbs.Item>{currentLabel}</Breadcrumbs.Item>
</Breadcrumbs>
</nav>
);
}
import { ChevronDownIcon as ChevronDown } from '@heroicons/react/20/solid';
import { Label, ListBox, Select } from '@heroui/react';
import clsx from 'clsx';
import { useMemo } from 'react';
const EMPTY_OPTION_KEY = '__rc_filter_empty__';
export default function FilterSelect({ 'aria-label': ariaLabel, className, onChange, options = [], placeholder = '请选择', value = '' }) {
const normalizedOptions = useMemo(() => options.map((option) => typeof option === 'object' ? { label: String(option.label ?? option.value ?? ''), value: String(option.value ?? '') } : { label: String(option), value: String(option) }), [options]);
const selectedValue = String(value ?? '');
const selectedOption = normalizedOptions.find((option) => option.value === selectedValue);
return (
<Select aria-label={ariaLabel || placeholder} className={clsx('rc-filter-select', className)} fullWidth selectedKey={selectedValue || EMPTY_OPTION_KEY} variant="secondary" onSelectionChange={(key) => onChange?.(key === EMPTY_OPTION_KEY ? '' : String(key ?? ''))}>
<Select.Trigger className="rc-filter-select__trigger">
<Select.Value className="rc-filter-select__value"><span className={clsx('block min-w-0 truncate', !selectedOption && 'rc-filter-select__placeholder')}>{selectedOption?.label || placeholder}</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.map((option) => (
<ListBox.Item key={option.value || EMPTY_OPTION_KEY} id={option.value || EMPTY_OPTION_KEY} className="rc-filter-select__option" textValue={option.label}>
<Label className="rc-filter-select__option-label">{option.label}</Label>
<ListBox.ItemIndicator className="rc-filter-select__option-indicator" />
</ListBox.Item>
))}
</ListBox>
</Select.Popover>
</Select>
);
}
import { Button, Tooltip } from '@heroui/react';
import clsx from 'clsx';
export function TableActionGroup({ children, className }) {
return (
<div className={clsx('icon-actions flex flex-nowrap items-center justify-end gap-1.5 whitespace-nowrap', className)}>
{children}
</div>
);
}
export function TableActionButton({ icon: Icon, label, onPress, tone = 'default' }) {
return (
<Tooltip delay={0}>
<Button
isIconOnly
aria-label={label}
className={clsx('pm-table-action', `pm-table-action--${tone}`)}
size="sm"
title={label}
variant="ghost"
onPress={onPress}
>
<Icon size={16} />
</Button>
<Tooltip.Content>{label}</Tooltip.Content>
</Tooltip>
);
}
import { Info } from '@/components/AppIcons';
import { Button, Tooltip } from '@heroui/react';
export function TitleInfo({ label, content }) {
if (!content) return null;
return (
<Tooltip delay={0}>
<Button isIconOnly aria-label={`${label}说明`} className="rc-title-info-trigger" size="sm" variant="ghost">
<Info size={15} />
</Button>
<Tooltip.Content>{content}</Tooltip.Content>
</Tooltip>
);
}
...@@ -377,7 +377,7 @@ function WorkbenchPagination({ pagination }) { ...@@ -377,7 +377,7 @@ function WorkbenchPagination({ pagination }) {
const pageItems = buildPaginationPageItems(currentPage, totalPages); const pageItems = buildPaginationPageItems(currentPage, totalPages);
return ( return (
<div className="flex flex-col gap-3 border-t border-zinc-100 bg-zinc-50/70 px-4 py-3 sm:flex-row sm:items-center sm:justify-between"> <div className="rc-list-pagination flex flex-col gap-3 border-t border-zinc-100 px-4 py-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex flex-wrap items-center gap-3 text-sm text-slate-500"> <div className="flex flex-wrap items-center gap-3 text-sm text-slate-500">
<span> <span>
共 {totalRecords} 条,显示 {startRecord}-{endRecord} 共 {totalRecords} 条,显示 {startRecord}-{endRecord}
...@@ -398,7 +398,7 @@ function WorkbenchPagination({ pagination }) { ...@@ -398,7 +398,7 @@ function WorkbenchPagination({ pagination }) {
</label> </label>
</div> </div>
<div className="flex flex-wrap items-center gap-1.5"> <div className="rc-pagination-pages flex flex-wrap items-center gap-1.5">
<Button <Button
plain plain
disabled={currentPage <= 0} disabled={currentPage <= 0}
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
...@@ -6,9 +6,10 @@ import { ...@@ -6,9 +6,10 @@ import {
getDisplayFieldMap, getDisplayFieldMap,
loadProcessInstanceDetail, loadProcessInstanceDetail,
} from '@/pages/process_monitor/processMonitorService'; } from '@/pages/process_monitor/processMonitorService';
import { ArrowLeftIcon } from '@heroicons/react/24/outline'; import DetailNavigation from '@/components/DetailNavigation';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { Link, useParams, useSearchParams } from 'umi'; import { useParams, useSearchParams } from 'umi';
import { TitleInfo } from '@/components/XinyuanUi';
function InfoCard({ label, value }) { function InfoCard({ label, value }) {
return ( return (
...@@ -123,30 +124,18 @@ export default function ProcessDetailPage() { ...@@ -123,30 +124,18 @@ export default function ProcessDetailPage() {
const relatedCondition = record?.metadata?.related_progress?.metadata?.conditions; const relatedCondition = record?.metadata?.related_progress?.metadata?.conditions;
return ( return (
<div className="space-y-6"> <div className="rc-page-stack">
<div className="flex flex-wrap items-center gap-3 text-sm text-slate-500"> <DetailNavigation
<Link backHref={`/process_monitor/${processId}${tabName ? `?tab=${tabName}` : ''}`}
to={tabName ? `/process_monitor?tab=${tabName}` : '/process_monitor'} backLabel="返回实例列表"
className="inline-flex items-center gap-2 rounded-full border border-slate-200 bg-white px-3 py-1.5 font-medium text-slate-600 transition hover:border-sky-200 hover:text-sky-700" currentLabel="流程实例详情"
> items={[
<ArrowLeftIcon className="size-4" /> { label: '流程监控', href: tabName ? `/process_monitor?tab=${tabName}` : '/process_monitor' },
返回流程监控 { label: relatedTemplateTitle === '-' ? '实例列表' : relatedTemplateTitle, href: `/process_monitor/${processId}${tabName ? `?tab=${tabName}` : ''}` },
</Link> ]}
<Link />
to={`/process_monitor/${processId}${tabName ? `?tab=${tabName}` : ''}`}
className="inline-flex items-center gap-2 rounded-full border border-slate-200 bg-white px-3 py-1.5 font-medium text-slate-600 transition hover:border-sky-200 hover:text-sky-700" <section className="rc-detail-status">
>
返回实例列表
</Link>
</div>
<section className="overflow-hidden rounded-[32px] border border-slate-200/70 bg-[linear-gradient(135deg,rgba(15,118,110,0.10),rgba(14,165,233,0.12))] px-6 py-7 shadow-sm sm:px-8">
<h1 className="text-3xl font-semibold tracking-tight text-slate-950 sm:text-4xl">
{record?.title || '流程实例详情'}
</h1>
<p className="mt-3 max-w-3xl text-sm leading-6 text-slate-600 sm:text-base">
对应旧版 `ProcessDetail` 的单条流程实例详情页,使用新的卡片布局展示提交人、部门、时间和动态表单字段。
</p>
{message ? ( {message ? (
<div className="mt-4 rounded-2xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-700"> <div className="mt-4 rounded-2xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-700">
{message} {message}
...@@ -174,7 +163,8 @@ export default function ProcessDetailPage() { ...@@ -174,7 +163,8 @@ export default function ProcessDetailPage() {
<InfoCard label="动态字段数" value={String(details.length || 0)} /> <InfoCard label="动态字段数" value={String(details.length || 0)} />
</div> </div>
<SectionCard title="表单字段详情" description="按实际提交流水中的明细字段逐项展开,附件保持单独下载入口。"> <section className="rc-detail-section">
<div className="rc-list-action-bar"><div className="flex items-center gap-2"><h2>表单字段详情</h2><TitleInfo label="表单字段详情" content="按实际提交流水中的明细字段逐项展开,附件保留独立下载入口。" /></div></div>
{loading ? ( {loading ? (
<div className="rounded-[24px] border border-slate-200 bg-slate-50/70 px-6 py-10 text-sm text-slate-500"> <div className="rounded-[24px] border border-slate-200 bg-slate-50/70 px-6 py-10 text-sm text-slate-500">
正在加载字段详情... 正在加载字段详情...
...@@ -188,7 +178,7 @@ export default function ProcessDetailPage() { ...@@ -188,7 +178,7 @@ export default function ProcessDetailPage() {
{details.map((field, index) => ( {details.map((field, index) => (
<article <article
key={`${field.label}-${index}`} key={`${field.label}-${index}`}
className="rounded-[24px] border border-slate-200 bg-[linear-gradient(180deg,rgba(255,255,255,0.96),rgba(248,250,252,0.96))] p-5 shadow-sm" className="rc-detail-field-card rounded-[24px] border border-slate-200 p-5"
> >
<div className="flex items-start justify-between gap-3"> <div className="flex items-start justify-between gap-3">
<div> <div>
...@@ -210,7 +200,7 @@ export default function ProcessDetailPage() { ...@@ -210,7 +200,7 @@ export default function ProcessDetailPage() {
))} ))}
</div> </div>
)} )}
</SectionCard> </section>
</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