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

feat: align OA process monitor with Xinyuan UI

parent d9575173
......@@ -23,7 +23,7 @@
7. 分页不统一:上一页/下一页与页码采用普通文本按钮,当前页、每页条数和总数摘要未使用鑫元分页视觉。
8. 提示信息过多直出:页面内存在开发背景、迁移说明和重复帮助文案,应收敛为标题旁 `info` 小提示,仅在需要时查看。
9. 表面层级不统一:页面存在 22/24/28/32px 多套圆角、彩色渐变、玻璃效果和页面私有阴影。
10. 暗色主题缺失:当前仅有单一视觉,Overlay、表面和文本未随根主题统一切换。
10. 主题策略已统一为固定浅色内容画布与深色 Sidebar;Overlay 继承根主题变量,不提供页面级主题切换。
## 本轮迁移基线
......@@ -31,7 +31,7 @@
- 使用本地完整 HeroUI Pro 镜像及 HeroUI 样式层。
- 同步已验收鑫元 Token、Shell 和组件 CSS。
- 登录页使用鑫元红 Logo、紧凑品牌区、统一输入高度和不透明浅红 filled/autofill 状态。
- Shell 使用浅色同画布侧栏、紧凑品牌区、中性选中项、全局主题切换和无阴影导航表面。
- Shell 使用深色 Sidebar、紧凑品牌区、中性选中项和无阴影导航表面;主题切换不属于当前基准交互。
- 页面说明收敛为标题旁 `info` 图标;业务状态和错误信息仍保留可见反馈。
- 分组切换改为统一页面 Tabs 视觉;筛选区按统一 Card、字段、按钮和响应式规则治理。
- 分页当前页使用鑫元红实心状态,其余操作保持中性。
......@@ -46,4 +46,3 @@
- 检查分页总数、每页条数、当前页、禁用态和页码省略符。
- 检查提示字段仅以 `info` 图标存在,Tooltip 可访问且不遮挡内容。
- 检查浅色/深色主题下 Dropdown、Modal、Tooltip、表格和图表表面。
......@@ -11,7 +11,8 @@
## 原生 HeroUI 边界
- 基础组件直接来自 `@heroui/react`,Sidebar/Navbar/DataGrid/KPI/EmptyState 等直接来自 `@heroui-pro/react` 官方子路径。
- 共享组件只做业务无关组合:Shell、应用切换、Autocomplete、详情导航、标题说明和行操作。
- 共享组件只做业务无关组合:Shell、应用切换、Autocomplete、顶部页面标题、标题说明和行操作。
- 顶部页面标题由 Navbar 内的 HeroUI `Breadcrumbs` 直接承载;当前项保持原标题字号和深色,前级项与分隔箭头使用 `muted` 灰色并支持点击返回,页面内容区不再挂载第二套页面级 Breadcrumbs。
- 应用切换直接使用 HeroUI Modal/Card/Surface/Avatar 与 HeroUI Pro EmptyState;Modal Body 是唯一滚动容器,桌面及平板固定两列、移动端一列。
- compound API 使用点语法,Button 使用 `onPress`;不提供 Catalyst/Headless 旧 props 兼容层。
- `XinyuanAutocomplete` 组合官方 Autocomplete、Popover Heading、SearchField 和 ListBox,不定义组件外观。
......@@ -26,7 +27,7 @@
@import "./design-system/tokens.css";
```
- `theme.css` 是技能权威原始副本;`tokens.css`、`shell.css`、`tailwind.css` 与问题整改同步。
- `theme.css` 是技能权威原始副本;`tokens.css`、`shell.css`、`tailwind.css` 与审计项目管理同步。
- Inter 400/500/600 的拉丁字符字体文件位于 `public/fonts/`,由 `.umirc.ts` 生成的常驻 `inter.css` link 加载,不参与 `umi.css` 热替换。
- `src/tailwind.css` 不导入 `@fontsource/inter/*.css`,并用显式 `@source` 只扫描入口、组件、页面和 wrapper;`src/.umi` 不进入 Tailwind 扫描范围。
- 开发时同一工作区只运行一个 Umi dev 实例,防止多个进程交叉改写 `src/.umi` 形成编译循环。
......@@ -36,7 +37,7 @@
## 列表与反馈
- 流程列表固定每页 10 条,使用 Pro DataGrid 与 HeroUI compound Pagination。
- 流程列表默认每页 10 条,并可通过 HeroUI Autocomplete 切换 10/20/50/100 条,使用 Pro DataGrid 与 HeroUI compound Pagination。
- 列表模块使用基准的透明三层结构:独立数据集标题行、`rc-page-toolbar` 命令行、`rc-list-data-stack` 数据与分页;不使用 Card 或页面外观把三层框在一起。
- 搜索位于左侧,业务操作在右,主操作最右;移动端搜索独占行。
- 成功反馈使用 HeroUI toast;需要用户处理的错误保留上下文 Alert。
......
......@@ -7,9 +7,9 @@
- [x] 官方 hpsetup 在目标根目录安装成功
- [x] Pro 版本、解析路径、dist 和要求子路径验证
- [x] 移除跨项目 `file:` 依赖,统一 pnpm 锁文件
- [x] 启动修改前目标应用与问题整改基准并记录截图/几何
- [x] 启动修改前目标应用与审计项目管理基准并记录截图/几何
- [x] 权威 `theme.css` 与技能副本逐字节一致
- [x] `tokens.css`、`shell.css`、`tailwind.css` 与问题整改逐字节一致
- [x] `tokens.css`、`shell.css`、`tailwind.css` 与审计项目管理当前基准一致
## 架构与 Shell
......
......@@ -3,7 +3,7 @@
## 权威来源
- 主题唯一权威为 Xinyuan Skill 的 `assets/baseline/design-system/theme.css`,项目副本必须逐字节一致。
- 视觉和交互唯一运行基准为已确认完成的“问题整改”应用。
- 视觉和交互唯一运行基准为已确认完成的“审计项目管理”应用。
- 页面不得自定义 HeroUI/HeroUI Pro 的颜色、背景、描边、圆角、阴影或交互状态。
## 字体与样式入口
......@@ -26,6 +26,7 @@
- Sidebar 宽 240px,`collapsible="offcanvas"`,收起后宽度为零且无 icon rail。
- Navbar 高 64px,直接使用 HeroUI Pro `Sidebar.Trigger` 默认图形。
- Navbar 和页面共用左右 gutter:桌面 48px、平板 32px、手机 16px。
- Navbar 标题本身使用 HeroUI `Breadcrumbs` 作为唯一页面级导航:当前层级沿用原 Navbar 标题的字号、字重、行高和深色;前级层级与分隔箭头使用 `muted` 灰色,前级节点必须可点击返回对应路由;内容区不得再重复渲染页面级面包屑或返回导航。
- Sidebar 用户入口通过 `Dropdown.Trigger` 打开菜单,退出必须调用现有真实 logout。
- 应用切换使用 HeroUI Modal,桌面宽 46rem/最大高 42rem,手机一列、`sm` 起固定两列;分组来自 `/info -> site.group`,保留后端顺序并提供“其他应用/全部应用”回退。
......@@ -45,7 +46,7 @@
- 普通业务文字 Button 不带装饰图标;主操作在最右。
- 详情/表单使用 HeroUI Modal;查看态整组字段放一个 secondary Surface,编辑态移除该 Surface。
- DataGrid 使用原生 selection、排序、固定列、横向滚动、ActionBar、EmptyState 和 compound Pagination。
- 业务列表固定每页 10 条,不显示 page-size 选择器。
- 流程业务列表默认每页 10 条,分页条使用 HeroUI Autocomplete 提供 10/20/50/100 条选择;通知中心仍固定每页 10 条。
## 主题与资产
......@@ -59,7 +60,7 @@
`shortTitle`、Subject View 授权或审计材料中心文件流程。
- 通知页路由为 `/notifications`,沿用应用 base `/oa_process_monitor_new/`。
- Navbar 使用 HeroUI `Badge.Anchor + Badge + Button + Tooltip`,Ant Design
`BellOutlined` 位于主题按钮左侧;未读数为 0 时隐藏徽标,1-99 显示真实值,100 及以上显示
`BellOutlined` 为 Navbar 全局动作;未读数为 0 时隐藏徽标,1-99 显示真实值,100 及以上显示
`99+`,可访问名称保留真实数量。
- 通知页使用 HeroUI Tabs、SearchField、Card、Chip、Pagination、Toast 和 HeroUI Pro
EmptyState。查询固定每页 10 条,搜索延迟 300ms 并转义正则字符,未读状态通过服务端
......
This diff is collapsed.
......@@ -29,6 +29,8 @@
"clsx": "^2.1.1",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.20",
"echarts": "^6.1.0",
"echarts-for-react": "^3.0.6",
"embla-carousel": "^8.6.0",
"embla-carousel-react": "^8.6.0",
"maplibre-gl": "^6.4.1",
......
......@@ -71,6 +71,12 @@ importers:
dayjs:
specifier: ^1.11.20
version: 1.11.21
echarts:
specifier: ^6.1.0
version: 6.1.0
echarts-for-react:
specifier: ^3.0.6
version: 3.0.6(echarts@6.1.0)(react@19.2.8)
embla-carousel:
specifier: ^8.6.0
version: 8.6.0
......@@ -3665,6 +3671,15 @@ packages:
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
echarts-for-react@3.0.6:
resolution: {integrity: sha512-4zqLgTGWS3JvkQDXjzkR1k1CHRdpd6by0988TWMJgnvDytegWLbeP/VNZmMa+0VJx2eD7Y632bi2JquXDgiGJg==}
peerDependencies:
echarts: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0
react: ^15.0.0 || >=16.0.0
echarts@6.1.0:
resolution: {integrity: sha512-q0yaFPggC9FUdsWH4blavRWFmxdrIodbkoKNAjJudAI6CA9gNPxHtV2RcZNEepZVlk4yvBYkOkbk6HIVpIyHZA==}
ee-first@1.1.1:
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
......@@ -7143,6 +7158,9 @@ packages:
sisteransi@1.0.5:
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
size-sensor@1.0.3:
resolution: {integrity: sha512-+k9mJ2/rQMiRmQUcjn+qznch260leIXY8r4FyYKKyRBO/s5UoeMAHGkCJyE1R/4wrIhTJONfyloY55SkE7ve3A==}
slash@3.0.0:
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
engines: {node: '>=8'}
......@@ -7541,6 +7559,9 @@ packages:
tslib@1.14.1:
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
tslib@2.3.0:
resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==}
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
......@@ -7956,6 +7977,9 @@ packages:
resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==}
engines: {node: '>=12.20'}
zrender@6.1.0:
resolution: {integrity: sha512-oEGMDB6pOP2S6OwRR4PdVv610zrjnA3Bh+JnSG12fYJlBKjtNAoEb5fSUoCOOINlH96I2fU38/A2UpRKs67xYQ==}
zwitch@2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
......@@ -12247,6 +12271,18 @@ snapshots:
eastasianwidth@0.2.0: {}
echarts-for-react@3.0.6(echarts@6.1.0)(react@19.2.8):
dependencies:
echarts: 6.1.0
fast-deep-equal: 3.1.3
react: 19.2.8
size-sensor: 1.0.3
echarts@6.1.0:
dependencies:
tslib: 2.3.0
zrender: 6.1.0
ee-first@1.1.1: {}
electron-to-chromium@1.5.398: {}
......@@ -16558,6 +16594,8 @@ snapshots:
sisteransi@1.0.5: {}
size-sensor@1.0.3: {}
slash@3.0.0: {}
slash@4.0.0: {}
......@@ -17033,6 +17071,8 @@ snapshots:
tslib@1.14.1: {}
tslib@2.3.0: {}
tslib@2.8.1: {}
tsutils@3.21.0(typescript@5.9.3):
......@@ -17528,4 +17568,8 @@ snapshots:
yocto-queue@1.2.2: {}
zrender@6.1.0:
dependencies:
tslib: 2.3.0
zwitch@2.0.4: {}
......@@ -27,7 +27,6 @@ import LockOutlined from '@ant-design/icons/LockOutlined';
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';
......@@ -37,7 +36,6 @@ 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';
......@@ -114,11 +112,9 @@ export function SidebarPanel({ size = 20, style, ...props }) {
);
}
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);
......
import { useLayoutEffect, useState } from 'react';
import { Sidebar } from '@heroui-pro/react/sidebar';
import { Toast } from '@heroui/react';
import { THEME_STORAGE_KEY } from '@/config/application';
import { history } from 'umi';
import DashboardNavbar from './DashboardNavbar';
import DashboardSidebar from './DashboardSidebar';
function getInitialTheme() {
if (typeof window === 'undefined') return 'light';
try {
return window.localStorage.getItem(THEME_STORAGE_KEY) === 'dark'
? 'dark'
: 'light';
} catch {
return 'light';
}
}
export default function AppShell({
children,
notificationCount,
notificationPath,
open,
onOpenChange,
breadcrumbItems,
title,
...sidebarProps
}) {
const [theme, setTheme] = useState(getInitialTheme);
useLayoutEffect(() => {
const root = document.documentElement;
root.classList.toggle('dark', theme === 'dark');
root.classList.toggle('light', theme === 'light');
root.dataset.theme = theme;
root.style.colorScheme = theme;
try {
window.localStorage.setItem(THEME_STORAGE_KEY, theme);
} catch {
// The selected theme still applies for this session when storage is unavailable.
}
}, [theme]);
return (
<Sidebar.Provider
className={`template-dashboard-shell rc-app-shell ${theme}`}
className="template-dashboard-shell rc-app-shell light"
collapsible="offcanvas"
navigate={(href) => history.push(href)}
open={open}
......@@ -58,9 +29,8 @@ export default function AppShell({
onOpenNotifications={
notificationPath ? () => history.push(notificationPath) : undefined
}
theme={theme}
breadcrumbItems={breadcrumbItems}
title={title}
onThemeChange={setTheme}
/>
<div className="app-shell-content-scroll min-h-0 flex-1 overflow-y-auto">
<div className="rc-app-page">{children}</div>
......
import { Navbar } from '@heroui-pro/react/navbar';
import { Sidebar, useSidebar } from '@heroui-pro/react/sidebar';
import { Badge, Button, Tooltip } from '@heroui/react';
import { Bell, Moon, SidebarPanel, Sun } from './AppIcons';
import { Badge, Breadcrumbs, Button, Tooltip } from '@heroui/react';
import { Bell, ChevronRight, SidebarPanel } from './AppIcons';
export default function DashboardNavbar({
notificationCount = 0,
onOpenNotifications,
onThemeChange,
theme,
breadcrumbItems = [],
title,
}) {
const { isMobile, isOpen } = useSidebar();
const isDark = theme === 'dark';
const themeLabel = isDark ? '切换至浅色模式' : '切换至深色模式';
const normalizedNotificationCount = Math.max(0, Number(notificationCount) || 0);
return (
......@@ -24,7 +21,21 @@ export default function DashboardNavbar({
>
<SidebarPanel />
</Sidebar.Trigger>
<h1 className="app-navbar-page-title rc-navbar-title">{title}</h1>
<Breadcrumbs
aria-label="当前页面"
className="app-navbar-page-title rc-navbar-title min-w-0"
separator={<ChevronRight size={16} />}
>
{(breadcrumbItems.length ? breadcrumbItems : [{ label: title }]).map((item, index, items) => (
<Breadcrumbs.Item
className={index === items.length - 1 ? 'rc-navbar-breadcrumb-current' : undefined}
href={index === items.length - 1 ? undefined : item.href}
key={`${item.href || 'current'}-${item.label}`}
>
{item.label}
</Breadcrumbs.Item>
))}
</Breadcrumbs>
<Navbar.Spacer />
<div className="rc-navbar-actions">
{onOpenNotifications ? (
......@@ -55,20 +66,6 @@ export default function DashboardNavbar({
<Tooltip.Content>通知消息</Tooltip.Content>
</Tooltip>
) : null}
<Tooltip delay={0}>
<Button
isIconOnly
aria-label={themeLabel}
aria-pressed={isDark}
className="app-navbar-icon-button 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>
</div>
</Navbar.Header>
</Navbar>
......
import xyLogoRed from '@/assets/xy-logo-red.png';
import xyLogoWhite from '@/assets/xy-logo-white.png';
import { APPLICATION_DISPLAY_NAME } from '@/config/application';
import { Sidebar, useSidebar } from '@heroui-pro/react/sidebar';
import { Sheet } from '@heroui-pro/react/sheet';
import { Avatar, Button, Dropdown, Label, Tooltip } from '@heroui/react';
import clsx from 'clsx';
import { AppGrid, LogOut } from './AppIcons';
import { Sheet } from '@heroui-pro/react';
import { Avatar, Button, Disclosure, Dropdown, Label } from '@heroui/react';
import { AppGrid, ChevronDown, LogOut } from './AppIcons';
function getUserDisplay(userInfo) {
const displayName = userInfo?.display_name || userInfo?.username || '当前用户';
......@@ -65,24 +63,34 @@ function SidebarContents({
onLogout,
onOpenApplicationSwitcher,
pathname,
theme,
userInfo,
}) {
const renderMenu = (group) => (
<Sidebar.Menu aria-label={group.title}>
{group.items.map((item) => {
const Icon = item.icon;
return (
<Sidebar.MenuItem
href={item.path}
isCurrent={pathname === item.path}
key={item.path}
textValue={item.name}
tooltip={item.name}
>
<Sidebar.MenuIcon><Icon size={16} /></Sidebar.MenuIcon>
<Sidebar.MenuLabel>{item.name}</Sidebar.MenuLabel>
</Sidebar.MenuItem>
);
})}
</Sidebar.Menu>
);
return (
<>
<Sidebar.Header>
<div className="app-sidebar-brand rc-sidebar-brand">
<div className="app-sidebar-brand-mark rc-sidebar-brand-mark">
<img
alt="鑫元"
className="rc-sidebar-logo rc-sidebar-logo--light"
src={xyLogoRed}
/>
<img
alt="鑫元"
className="rc-sidebar-logo rc-sidebar-logo--dark"
src={xyLogoWhite}
/>
<img alt="鑫元" className="rc-sidebar-logo" src={xyLogoWhite} />
</div>
<div
className="app-sidebar-brand-main rc-sidebar-brand-main"
......@@ -96,50 +104,44 @@ function SidebarContents({
className="app-sidebar-app-switcher rc-sidebar-app-switcher"
data-sidebar="label"
>
<Tooltip delay={0}>
<Button
isIconOnly
aria-label="切换应用"
className="app-sidebar-app-trigger rc-sidebar-app-trigger"
size="sm"
variant="ghost"
onPress={onOpenApplicationSwitcher}
>
<AppGrid size={16} />
</Button>
<Tooltip.Content>切换应用</Tooltip.Content>
</Tooltip>
<Button
isIconOnly
aria-label="切换应用"
className="app-sidebar-app-trigger rc-sidebar-app-trigger"
size="sm"
variant="ghost"
onPress={onOpenApplicationSwitcher}
>
<AppGrid size={16} />
</Button>
</div>
</div>
</Sidebar.Header>
<Sidebar.Content>
{menuGroups.map((group) => (
<Sidebar.Group key={group.title}>
{group.showLabel !== false ? (
<Sidebar.GroupLabel>{group.title}</Sidebar.GroupLabel>
) : null}
<Sidebar.Menu aria-label={group.title}>
{group.items.map((item) => {
const Icon = item.icon;
return (
<Sidebar.MenuItem
href={item.path}
isCurrent={pathname === item.path}
key={item.path}
textValue={item.name}
tooltip={item.name}
>
<Sidebar.MenuIcon>
<Icon size={16} />
</Sidebar.MenuIcon>
<Sidebar.MenuLabel>{item.name}</Sidebar.MenuLabel>
</Sidebar.MenuItem>
);
})}
</Sidebar.Menu>
</Sidebar.Group>
))}
{menuGroups.map((group) => {
if (group.showLabel === false) {
return <Sidebar.Group key={group.title}>{renderMenu(group)}</Sidebar.Group>;
}
const hasCurrentItem = group.items.some((item) => pathname === item.path);
const GroupIcon = group.icon;
return (
<Sidebar.Group key={group.title}>
<Disclosure defaultExpanded={hasCurrentItem}>
<Disclosure.Heading>
<Sidebar.GroupLabel>
<Disclosure.Trigger className="flex w-full min-w-0 items-center gap-2 text-left">
{GroupIcon ? <GroupIcon size={16} /> : null}
<span className="min-w-0 flex-1 truncate">{group.title}</span>
<Disclosure.Indicator><ChevronDown size={14} /></Disclosure.Indicator>
</Disclosure.Trigger>
</Sidebar.GroupLabel>
</Disclosure.Heading>
<Disclosure.Content><Disclosure.Body>{renderMenu(group)}</Disclosure.Body></Disclosure.Content>
</Disclosure>
</Sidebar.Group>
);
})}
</Sidebar.Content>
<Sidebar.Footer>
......@@ -150,7 +152,7 @@ function SidebarContents({
}
function AccessibleMobileSidebar(props) {
const { isMobile, isMobileOpen, setMobileOpen, side, slots } = useSidebar();
const { isMobile, isMobileOpen, setMobileOpen, side } = useSidebar();
if (!isMobile) return null;
......@@ -166,13 +168,7 @@ function AccessibleMobileSidebar(props) {
aria-label={`${APPLICATION_DISPLAY_NAME}导航`}
className="sidebar__mobile-dialog"
>
<div
className={clsx(
slots?.mobile?.(),
'app-sidebar-glass rc-mobile-sidebar',
)}
data-slot="sidebar-mobile"
>
<div className="sidebar__mobile app-sidebar-glass rc-mobile-sidebar" data-slot="sidebar-mobile">
<SidebarContents {...props} />
</div>
</Sheet.Dialog>
......
import { ArrowLeft } from '@/components/AppIcons';
import { Breadcrumbs, Button, Tooltip } from '@heroui/react';
import { ArrowLeft, ChevronRight } from '@/components/AppIcons';
import { Breadcrumbs, Button } from '@heroui/react';
import { useNavigate } from 'umi';
export default function DetailNavigation({ backHref, backLabel, items, currentLabel }) {
export default function DetailNavigation({
backHref,
backLabel,
items,
currentLabel,
}) {
const navigate = useNavigate();
return (
<nav className="rc-detail-navigation flex min-w-0 items-center gap-3" aria-label="详情页导航">
<Tooltip delay={400}>
<Button
isIconOnly
aria-label={backLabel}
className="rc-detail-navigation__back shrink-0"
size="sm"
variant="outline"
onPress={() => navigate(backHref)}
>
<ArrowLeft size={16} />
</Button>
<Tooltip.Content placement="bottom">{backLabel}</Tooltip.Content>
</Tooltip>
<nav className="rc-detail-navigation flex min-w-0 items-center gap-4" aria-label="详情页导航">
<Button
isIconOnly
aria-label={backLabel}
className="rc-detail-navigation__back shrink-0"
size="lg"
variant="outline"
onPress={() => navigate(backHref)}
>
<ArrowLeft size={16} />
</Button>
<Breadcrumbs
className="rc-detail-navigation__breadcrumbs min-w-0"
separator={<ChevronRight size={16} />}
>
{items.map((item) => (
<Breadcrumbs.Item href={item.href} key={`${item.href}-${item.label}`}>
<Breadcrumbs.Item className="no-underline" href={item.href} key={`${item.href}-${item.label}`}>
{item.label}
</Breadcrumbs.Item>
))}
......
import {
buildDepartmentLookup,
buildMemberLookup,
downloadProcessAttachment,
formatDateTime,
getDisplayFieldMap,
} from '@/pages/process_monitor/processMonitorService';
import { Button, Surface } from '@heroui/react';
import { EmptyState } from '@heroui-pro/react/empty-state';
import { FileText } from '@/components/AppIcons';
import { useMemo } from 'react';
function resolveDetailValue(field, fieldMap, lookups) {
if (field?.type === 'file') {
const files = Array.isArray(field?.value) ? field.value : [];
if (!files.length) return <span className="text-muted">暂无内容</span>;
return <div className="flex flex-wrap gap-2">{files.map((file) => <Button key={`${file.fileUrl || file.url}-${file.filename}`} size="sm" variant="secondary" onPress={() => downloadProcessAttachment(file)}>{file.filename || file.fileName || '附件'}</Button>)}</div>;
}
const fieldDefinition = fieldMap[field?.label];
if (fieldDefinition?.type === 'member') {
const value = String(field?.value || '').split(',').map((item) => item.trim()).filter(Boolean).map((item) => lookups.memberLookup[item] || item).join('、');
return value || <span className="text-muted">暂无内容</span>;
}
if (fieldDefinition?.type === 'department') {
const value = String(field?.value || '').split(',').map((item) => item.trim()).filter(Boolean).map((item) => lookups.departmentLookup[item] || item).join('、');
return value || <span className="text-muted">暂无内容</span>;
}
if (fieldDefinition?.type === 'date') return formatDateTime(field?.value, 'YYYY-MM-DD HH:mm');
return field?.value || <span className="text-muted">暂无内容</span>;
}
function DetailField({ label, children, fullWidth = false }) {
return <div className={fullWidth ? 'min-w-0 sm:col-span-2' : 'min-w-0'}><div className="text-sm font-medium text-muted">{label}</div><div className="mt-1.5 min-w-0 text-sm text-foreground">{children || <span className="text-muted">暂无内容</span>}</div></div>;
}
export default function ProcessInstanceDetailContent({ data, loading = false, withSurface = false }) {
const record = data?.record;
const fieldsDefinition = data?.fieldsDefinition || [];
const members = data?.members || [];
const departments = data?.departments || [];
const lookups = useMemo(() => ({ memberLookup: buildMemberLookup(members), departmentLookup: buildDepartmentLookup(departments) }), [departments, members]);
const fieldMap = useMemo(() => getDisplayFieldMap(fieldsDefinition), [fieldsDefinition]);
const details = useMemo(() => record?.metadata?.detail || [], [record]);
const relatedTemplateTitle = record?.metadata?.related_progress?.title || '-';
const relatedCondition = record?.metadata?.related_progress?.metadata?.conditions;
if (loading) return <div className="py-8 text-sm text-muted">正在加载字段详情...</div>;
if (!record) return <EmptyState size="sm"><EmptyState.Header><EmptyState.Media variant="icon"><FileText size={20} /></EmptyState.Media><EmptyState.Title>暂无详情</EmptyState.Title></EmptyState.Header></EmptyState>;
const fieldGrid = <div className="grid gap-x-6 gap-y-5 sm:grid-cols-2">
<DetailField label="提交流程模板">{relatedTemplateTitle}</DetailField>
<DetailField label="提交时间">{formatDateTime(record?.metadata?.submit_at, 'YYYY-MM-DD HH:mm')}</DetailField>
<DetailField label="提交者">{record?.metadata?.submitter || <span className="text-muted">暂无内容</span>}</DetailField>
<DetailField label="审批者">{record?.metadata?.approver || <span className="text-muted">暂无内容</span>}</DetailField>
<DetailField label="所属部门">{record?.metadata?.department || <span className="text-muted">暂无内容</span>}</DetailField>
<DetailField label="显示条件">{relatedCondition?.key ? `${relatedCondition.key} = ${relatedCondition.value || '-'}` : '未配置'}</DetailField>
{details.map((field, index) => <DetailField key={`${field.label}-${index}`} label={field.label || `字段 ${index + 1}`} fullWidth={field?.type === 'file' || String(field?.value || '').length > 80}>{resolveDetailValue(field, fieldMap, lookups)}</DetailField>)}
</div>;
return withSurface ? <Surface className="px-5 py-5 sm:px-6 sm:py-6" variant="secondary">{fieldGrid}</Surface> : fieldGrid;
}
......@@ -40,9 +40,11 @@ export default function XinyuanAutocomplete({
options = [],
placeholder = '请选择',
searchPlaceholder = '输入关键词搜索',
showSearch = true,
selectedKey,
selectedKeys,
selectionMode = 'single',
valueText: valueTextOverride,
variant = 'secondary',
}) {
const selectedKeySet = getSelectedKeySet(
......@@ -56,9 +58,9 @@ export default function XinyuanAutocomplete({
const selectedOptions = options.filter((option) =>
selectedKeyStringSet.has(toSelectionKey(option.id)),
);
const valueText = selectedOptions.length
const valueText = valueTextOverride ?? (selectedOptions.length
? selectedOptions.map((option) => option.label).join('、')
: placeholder;
: placeholder);
const rootSelectionProps =
selectionMode === 'multiple'
? { selectedKeys }
......@@ -120,18 +122,20 @@ export default function XinyuanAutocomplete({
<Popover.Heading className="sr-only">
{ariaLabel || placeholder}
</Popover.Heading>
<Autocomplete.Filter filter={filterOption}>
<SearchField aria-label={searchPlaceholder} fullWidth variant="secondary">
<SearchField.Group>
<SearchField.SearchIcon>
<Search size={16} />
</SearchField.SearchIcon>
<SearchField.Input autoFocus placeholder={searchPlaceholder} />
<SearchField.ClearButton aria-label="清空搜索">
<Close size={14} />
</SearchField.ClearButton>
</SearchField.Group>
</SearchField>
<Autocomplete.Filter filter={showSearch ? filterOption : () => true}>
{showSearch ? (
<SearchField aria-label={searchPlaceholder} fullWidth variant="secondary">
<SearchField.Group>
<SearchField.SearchIcon>
<Search size={16} />
</SearchField.SearchIcon>
<SearchField.Input autoFocus placeholder={searchPlaceholder} />
<SearchField.ClearButton aria-label="清空搜索">
<Close size={14} />
</SearchField.ClearButton>
</SearchField.Group>
</SearchField>
) : null}
<ListBox
aria-label={ariaLabel || placeholder}
items={options}
......
export const APPLICATION_DISPLAY_NAME = 'OA流程监控';
export const APPLICATION_NAME = 'oa_process_monitor_new';
export const APPLICATION_BASE_PATH = '/oa_process_monitor_new/';
export const THEME_STORAGE_KEY = 'xinyuan_color_theme';
export const ROUTE_TITLES = Object.freeze({
'/process_monitor': '流程监控',
......
This diff is collapsed.
......@@ -11,38 +11,37 @@
[data-theme="light"],
[data-theme="default"] {
/* Theme Colors (Light Mode) */
--accent: oklch(45.32% 0.1661 27.03);
--accent: oklch(57.14% 0.2156 29.20);
--accent-foreground: oklch(99.11% 0 0);
--background: oklch(97.02% 0.0045 27.03);
--border: oklch(90.00% 0.0045 27.03);
--background: oklch(97.02% 0.0000 29.20);
--border: oklch(90.00% 0.0000 29.20);
--danger: oklch(45.32% 0.1661 27.03);
--danger-foreground: oklch(99.11% 0 0);
/* HeroUI control aliases: primary = L1, secondary = L2. */
--default: var(--surface-secondary);
--default-foreground: var(--surface-secondary-foreground);
--field-background: var(--surface);
--default: oklch(94.00% 0.0000 29.20);
--default-foreground: oklch(21.03% 0.0059 29.20);
--field-background: oklch(100.00% 0.0000 29.20);
--field-border: transparent;
--field-foreground: var(--surface-foreground);
--field-placeholder: oklch(55.17% 0.0090 27.03);
--focus: oklch(45.32% 0.1661 27.03);
--foreground: oklch(21.03% 0.0045 27.03);
--muted: oklch(55.17% 0.0090 27.03);
--overlay: var(--surface);
--overlay-foreground: var(--surface-foreground);
--scrollbar: oklch(87.10% 0.0045 27.03);
--segment: oklch(100.00% 0.0045 27.03);
--segment-foreground: oklch(21.03% 0.0045 27.03);
--separator: oklch(92.00% 0.0045 27.03);
--success: oklch(73.29% 0.1952 123.59);
--success-foreground: oklch(21.03% 0.0059 123.59);
--surface: oklch(100.00% 0.0022 27.03);
--surface-foreground: oklch(21.03% 0.0045 27.03);
--surface-secondary: oklch(95.24% 0.0036 27.03);
--surface-secondary-foreground: oklch(21.03% 0.0045 27.03);
--surface-tertiary: oklch(93.73% 0.0036 27.03);
--surface-tertiary-foreground: oklch(21.03% 0.0045 27.03);
--warning: oklch(78.19% 0.1599 45.11);
--warning-foreground: oklch(21.03% 0.0059 45.11);
--field-foreground: oklch(21.03% 0.0000 29.20);
--field-placeholder: oklch(55.17% 0.0000 29.20);
--focus: oklch(57.14% 0.2156 29.20);
--foreground: oklch(21.03% 0.0000 29.20);
--muted: oklch(55.17% 0.0000 29.20);
--overlay: oklch(100.00% 0.0000 29.20);
--overlay-foreground: oklch(21.03% 0.0000 29.20);
--scrollbar: oklch(87.10% 0.0000 29.20);
--segment: oklch(100.00% 0.0000 29.20);
--segment-foreground: oklch(21.03% 0.0000 29.20);
--separator: oklch(92.00% 0.0000 29.20);
--success: oklch(73.29% 0.1935 123.85);
--success-foreground: oklch(21.03% 0.0059 123.85);
--surface: oklch(100.00% 0.0000 29.20);
--surface-foreground: oklch(21.03% 0.0000 29.20);
--surface-secondary: oklch(95.24% 0.0000 29.20);
--surface-secondary-foreground: oklch(21.03% 0.0000 29.20);
--surface-tertiary: oklch(93.73% 0.0000 29.20);
--surface-tertiary-foreground: oklch(21.03% 0.0000 29.20);
--warning: oklch(78.19% 0.1585 45.37);
--warning-foreground: oklch(21.03% 0.0059 45.37);
/* Border Radius */
--radius: 0.5rem;
......@@ -51,42 +50,86 @@
/* Font Family */
/* Make sure to load Inter font in your app */
--font-sans: var(--font-inter);
/*
* Xinyuan fixed split-shell contract.
* The application canvas always uses this light theme. The Sidebar is the
* only persistent dark plane and must consume these semantic tokens.
*/
--xinyuan-sidebar-background: oklch(16% 0.020 255);
--xinyuan-sidebar-border: oklch(26% 0.022 255);
--xinyuan-sidebar-foreground: oklch(97% 0.004 255);
--xinyuan-sidebar-muted: oklch(76% 0.018 255);
--xinyuan-sidebar-item-hover: oklch(22% 0.026 255);
--xinyuan-sidebar-item-selected: oklch(28% 0.016 255);
--xinyuan-sidebar-item-selected-foreground: oklch(99% 0.002 255);
--xinyuan-sidebar-focus: oklch(78% 0.105 29.20);
/*
* ECharts is the required chart renderer for Xinyuan applications.
* Ant Design remains the icon source. Resolve these CSS variables to
* concrete colors before assigning them to an ECharts option object.
*/
--xinyuan-chart-1: #d92d20;
--xinyuan-chart-2: #ef4444;
--xinyuan-chart-3: #f97316;
--xinyuan-chart-4: #f59e0b;
--xinyuan-chart-5: #22c55e;
--xinyuan-chart-6: #14b87a;
--xinyuan-chart-7: #06b6d4;
--xinyuan-chart-8: #0ea5e9;
--xinyuan-chart-9: #3b82f6;
--xinyuan-chart-10: #6366f1;
--xinyuan-chart-11: #8b5cf6;
--xinyuan-chart-12: #7c3aed;
--xinyuan-chart-13: #b42318;
--xinyuan-chart-14: #84cc16;
--xinyuan-chart-15: #a855f7;
--xinyuan-chart-16: #64748b;
--xinyuan-chart-grid: #e8e8e8;
--xinyuan-chart-axis: #737373;
--xinyuan-chart-label: #737373;
--xinyuan-chart-tooltip-background: #ffffff;
--xinyuan-chart-tooltip-border: #dedede;
--xinyuan-chart-tooltip-foreground: #222222;
--xinyuan-chart-tooltip-shadow: 0 16px 40px rgb(15 23 42 / 14%);
--xinyuan-chart-legend-font-size: 0.75rem;
--xinyuan-chart-legend-line-height: 1rem;
}
.dark,
[data-theme="dark"] {
color-scheme: dark;
/* Theme Colors (Dark Mode) */
--accent: oklch(45.32% 0.1661 27.03);
--accent: oklch(57.14% 0.2156 29.20);
--accent-foreground: oklch(99.11% 0 0);
--background: oklch(12.00% 0.0045 27.03);
--border: oklch(28.00% 0.0045 27.03);
--danger: oklch(62.00% 0.1600 27.03);
--background: oklch(12.00% 0.0000 29.20);
--border: oklch(28.00% 0.0000 29.20);
--danger: oklch(62% 0.16 27.03);
--danger-foreground: oklch(99.11% 0 0);
/* HeroUI control aliases: primary = L1, secondary = L2. */
--default: var(--surface-secondary);
--default-foreground: var(--surface-secondary-foreground);
--field-background: var(--surface);
--default: oklch(27.40% 0.0000 29.20);
--default-foreground: oklch(99.11% 0 0);
--field-background: oklch(21.03% 0.0000 29.20);
--field-border: transparent;
--field-foreground: var(--surface-foreground);
--field-placeholder: oklch(70.50% 0.0090 27.03);
--focus: oklch(45.32% 0.1661 27.03);
--foreground: oklch(99.11% 0.0045 27.03);
--muted: oklch(70.50% 0.0090 27.03);
--overlay: var(--surface);
--overlay-foreground: var(--surface-foreground);
--scrollbar: oklch(70.50% 0.0045 27.03);
--segment: oklch(39.64% 0.0045 27.03);
--segment-foreground: oklch(99.11% 0.0045 27.03);
--separator: oklch(25.00% 0.0045 27.03);
--success: oklch(73.29% 0.1952 123.59);
--success-foreground: oklch(21.03% 0.0059 123.59);
--surface: oklch(21.03% 0.0090 27.03);
--surface-foreground: oklch(99.11% 0.0045 27.03);
--surface-secondary: oklch(25.70% 0.0067 27.03);
--surface-secondary-foreground: oklch(99.11% 0.0045 27.03);
--surface-tertiary: oklch(27.21% 0.0067 27.03);
--surface-tertiary-foreground: oklch(99.11% 0.0045 27.03);
--warning: oklch(82.03% 0.1400 49.12);
--warning-foreground: oklch(21.03% 0.0059 49.12);
--field-foreground: oklch(99.11% 0.0000 29.20);
--field-placeholder: oklch(70.50% 0.0000 29.20);
--focus: oklch(57.14% 0.2156 29.20);
--foreground: oklch(99.11% 0.0000 29.20);
--muted: oklch(70.50% 0.0000 29.20);
--overlay: oklch(21.03% 0.0000 29.20);
--overlay-foreground: oklch(99.11% 0.0000 29.20);
--scrollbar: oklch(70.50% 0.0000 29.20);
--segment: oklch(39.64% 0.0000 29.20);
--segment-foreground: oklch(99.11% 0.0000 29.20);
--separator: oklch(25.00% 0.0000 29.20);
--success: oklch(73.29% 0.1935 123.85);
--success-foreground: oklch(21.03% 0.0059 123.85);
--surface: oklch(21.03% 0.0000 29.20);
--surface-foreground: oklch(99.11% 0.0000 29.20);
--surface-secondary: oklch(25.70% 0.0000 29.20);
--surface-secondary-foreground: oklch(99.11% 0.0000 29.20);
--surface-tertiary: oklch(27.21% 0.0000 29.20);
--surface-tertiary-foreground: oklch(99.11% 0.0000 29.20);
--warning: oklch(82.03% 0.1388 49.38);
--warning-foreground: oklch(21.03% 0.0059 49.38);
}
......@@ -2,9 +2,7 @@
.light,
.default,
[data-theme='light'],
[data-theme='default'],
.dark,
[data-theme='dark'] {
[data-theme='default'] {
/* Xinyuan aliases and extension tokens. HeroUI core tokens are owned by theme.css. */
--rc-brand-primary: var(--accent);
--rc-brand-light: color-mix(in oklab, var(--accent) 84%, white);
......@@ -27,6 +25,17 @@
var(--background)
);
--rc-text-muted: var(--muted);
--rc-text-empty: color-mix(in oklab, var(--muted) 72%, var(--background));
/* Fixed dark Sidebar aliases. The content canvas remains light. */
--rc-sidebar-background: var(--xinyuan-sidebar-background);
--rc-sidebar-border: var(--xinyuan-sidebar-border);
--rc-sidebar-foreground: var(--xinyuan-sidebar-foreground);
--rc-sidebar-muted: var(--xinyuan-sidebar-muted);
--rc-sidebar-item-hover: var(--xinyuan-sidebar-item-hover);
--rc-sidebar-item-selected: var(--xinyuan-sidebar-item-selected);
--rc-sidebar-item-selected-foreground: var(--xinyuan-sidebar-item-selected-foreground);
--rc-sidebar-focus: var(--xinyuan-sidebar-focus);
--rc-font-sans: var(--font-sans);
--rc-font-medium: 500;
......@@ -45,6 +54,7 @@
/* Page geometry only. Component appearance remains owned by HeroUI. */
--rc-shell-inline-gutter: 3rem;
--rc-sidebar-width: 15rem;
--rc-navbar-inline-padding: var(--rc-shell-inline-gutter);
--rc-page-inline-start: var(--rc-shell-inline-gutter);
--rc-page-inline-end: var(--rc-shell-inline-gutter);
......@@ -138,40 +148,28 @@
);
/* Data visualization only. Do not reuse UI status colors as a series palette. */
--rc-chart-1: oklch(45.32% 0.1661 27.03);
--rc-chart-2: oklch(56% 0.135 255);
--rc-chart-3: oklch(64% 0.11 220);
--rc-chart-4: oklch(56% 0.115 280);
--rc-chart-5: oklch(60% 0.095 200);
--rc-chart-6: oklch(70% 0.12 80);
--rc-chart-7: oklch(60% 0.1 295);
--rc-chart-8: oklch(57% 0.035 250);
--rc-chart-1: var(--xinyuan-chart-1);
--rc-chart-2: var(--xinyuan-chart-2);
--rc-chart-3: var(--xinyuan-chart-3);
--rc-chart-4: var(--xinyuan-chart-4);
--rc-chart-5: var(--xinyuan-chart-5);
--rc-chart-6: var(--xinyuan-chart-6);
--rc-chart-7: var(--xinyuan-chart-7);
--rc-chart-8: var(--xinyuan-chart-8);
--rc-chart-9: var(--xinyuan-chart-9);
--rc-chart-10: var(--xinyuan-chart-10);
--rc-chart-11: var(--xinyuan-chart-11);
--rc-chart-12: var(--xinyuan-chart-12);
--rc-chart-13: var(--xinyuan-chart-13);
--rc-chart-14: var(--xinyuan-chart-14);
--rc-chart-15: var(--xinyuan-chart-15);
--rc-chart-16: var(--xinyuan-chart-16);
--rc-chart-primary: var(--rc-chart-1);
--rc-chart-risk: oklch(47% 0.145 27.03);
--rc-chart-warning: var(--rc-chart-6);
--rc-chart-positive: var(--rc-chart-2);
--rc-chart-comparison: var(--rc-chart-4);
--rc-chart-risk: var(--rc-chart-1);
--rc-chart-warning: var(--rc-chart-3);
--rc-chart-positive: var(--rc-chart-7);
--rc-chart-comparison: var(--rc-chart-5);
--rc-chart-neutral: var(--rc-chart-8);
--rc-chart-legend-font-size: 0.75rem;
--rc-chart-legend-line-height: 1rem;
}
.dark,
[data-theme='dark'] {
color-scheme: dark;
--rc-text-secondary: color-mix(
in oklab,
var(--foreground) 76%,
var(--background)
);
--rc-button-secondary-border: color-mix(
in oklab,
var(--foreground) 18%,
transparent
);
--rc-shadow-raised: 0 1px 2px rgb(0 0 0 / 24%),
0 8px 24px rgb(0 0 0 / 24%);
--rc-shadow-overlay: 0 16px 40px rgb(0 0 0 / 38%);
--rc-shadow-card: 0 1px 2px rgb(0 0 0 / 20%),
0 4px 12px rgb(0 0 0 / 18%);
--rc-chart-legend-font-size: var(--xinyuan-chart-legend-font-size);
--rc-chart-legend-line-height: var(--xinyuan-chart-legend-line-height);
}
......@@ -25,10 +25,34 @@ export default function ProcessMonitorLayout() {
const [isApplicationDialogOpen, setIsApplicationDialogOpen] = useState(false);
const [unreadNotificationCount, setUnreadNotificationCount] = useState(0);
const currentTitle = useMemo(
() => ROUTE_TITLES[location.pathname] || (location.pathname === '/process_monitor' ? '流程监控' : '流程详情'),
[location.pathname],
);
const currentTitle = useMemo(() => {
if (ROUTE_TITLES[location.pathname]) return ROUTE_TITLES[location.pathname];
const segments = location.pathname.split('/').filter(Boolean);
if (segments.length === 2) return '实例列表';
if (segments.length >= 3) return '流程详情';
return '流程监控';
}, [location.pathname]);
const breadcrumbItems = useMemo(() => {
const segments = location.pathname.split('/').filter(Boolean);
const tab = new URLSearchParams(location.search).get('tab');
const tabSuffix = tab ? `?tab=${encodeURIComponent(tab)}` : '';
if (segments.length >= 3) {
const processId = segments[1];
return [
{ label: '流程监控', href: `/process_monitor${tabSuffix}` },
{ label: '实例列表', href: `/process_monitor/${processId}${tabSuffix}` },
{ label: '流程详情' },
];
}
if (segments.length === 2) {
return [
{ label: '流程监控', href: `/process_monitor${tabSuffix}` },
{ label: '实例列表' },
];
}
return [{ label: currentTitle }];
}, [currentTitle, location.pathname, location.search]);
useEffect(() => {
document.title = `${currentTitle} - ${APPLICATION_DISPLAY_NAME}`;
......@@ -68,6 +92,7 @@ export default function ProcessMonitorLayout() {
onOpenChange={setSidebarOpen}
open={sidebarOpen}
pathname={location.pathname}
breadcrumbItems={breadcrumbItems}
title={currentTitle}
userInfo={userInfo}
>
......
import {
buildDepartmentLookup,
buildMemberLookup,
downloadProcessAttachment,
formatDateTime,
getDisplayFieldMap,
loadProcessInstanceDetail,
} from '@/pages/process_monitor/processMonitorService';
import DetailNavigation from '@/components/DetailNavigation';
import ProcessInstanceDetailContent from '@/components/ProcessInstanceDetailContent';
import { TitleInfo } from '@/components/XinyuanUi';
import { Alert, Button, Surface } from '@heroui/react';
import { EmptyState } from '@heroui-pro/react/empty-state';
import { FileText } from '@/components/AppIcons';
import { Alert } from '@heroui/react';
import { useEffect, useMemo, useState } from 'react';
import { useParams, useSearchParams } from 'umi';
function resolveDetailValue(field, fieldMap, lookups) {
if (field?.type === 'file') {
const files = Array.isArray(field?.value) ? field.value : [];
if (!files.length) return <span className="text-muted">暂无内容</span>;
return <div className="flex flex-wrap gap-2">{files.map((file) => <Button key={`${file.fileUrl || file.url}-${file.filename}`} size="sm" variant="secondary" onPress={() => downloadProcessAttachment(file)}>{file.filename || file.fileName || '附件'}</Button>)}</div>;
}
const fieldDefinition = fieldMap[field?.label];
if (fieldDefinition?.type === 'member') {
const value = String(field?.value || '').split(',').map((item) => item.trim()).filter(Boolean).map((item) => lookups.memberLookup[item] || item).join('、');
return value || <span className="text-muted">暂无内容</span>;
}
if (fieldDefinition?.type === 'department') {
const value = String(field?.value || '').split(',').map((item) => item.trim()).filter(Boolean).map((item) => lookups.departmentLookup[item] || item).join('、');
return value || <span className="text-muted">暂无内容</span>;
}
if (fieldDefinition?.type === 'date') return formatDateTime(field?.value, 'YYYY-MM-DD HH:mm');
return field?.value || <span className="text-muted">暂无内容</span>;
}
function DetailField({ label, children, fullWidth = false }) {
return <div className={fullWidth ? 'min-w-0 sm:col-span-2' : 'min-w-0'}><div className="text-sm font-medium text-muted">{label}</div><div className="mt-1.5 min-w-0 text-sm text-foreground">{children || <span className="text-muted">暂无内容</span>}</div></div>;
}
import { useParams } from 'umi';
export default function ProcessDetailPage() {
const { processId, application } = useParams();
const [searchParams] = useSearchParams();
const tabName = searchParams.get('tab') || '';
const { application } = useParams();
const [loading, setLoading] = useState(true);
const [errorMessage, setErrorMessage] = useState('');
const [record, setRecord] = useState(null);
......@@ -55,32 +22,14 @@ export default function ProcessDetailPage() {
setRecord(result.record); setFieldsDefinition(result.fieldsDefinition); setMembers(result.members); setDepartments(result.departments); setErrorMessage('');
}).catch((error) => setErrorMessage(error?.message || '加载流程详情失败')).finally(() => setLoading(false));
}, [application]);
const lookups = useMemo(() => ({ memberLookup: buildMemberLookup(members), departmentLookup: buildDepartmentLookup(departments) }), [departments, members]);
const fieldMap = useMemo(() => getDisplayFieldMap(fieldsDefinition), [fieldsDefinition]);
const details = useMemo(() => record?.metadata?.detail || [], [record]);
const relatedTemplateTitle = record?.metadata?.related_progress?.title || '-';
const relatedCondition = record?.metadata?.related_progress?.metadata?.conditions;
const detailData = useMemo(() => ({ record, fieldsDefinition, members, departments }), [departments, fieldsDefinition, members, record]);
return (
<div className="rc-page-stack">
<DetailNavigation backHref={`/process_monitor/${processId}${tabName ? `?tab=${tabName}` : ''}`} backLabel="返回实例列表" currentLabel="流程实例详情" items={[{ label: '流程监控', href: tabName ? `/process_monitor?tab=${tabName}` : '/process_monitor' }, { label: relatedTemplateTitle === '-' ? '实例列表' : relatedTemplateTitle, href: `/process_monitor/${processId}${tabName ? `?tab=${tabName}` : ''}` }]} />
{errorMessage ? <Alert status="danger"><Alert.Indicator /><Alert.Content><Alert.Title>详情加载失败</Alert.Title><Alert.Description>{errorMessage}</Alert.Description></Alert.Content></Alert> : null}
<div className="rc-list-action-bar"><div className="flex items-center gap-1.5"><h2>流程实例信息</h2><TitleInfo label="流程实例信息" content="展示该实例的流程信息和动态表单字段,附件保留独立下载入口。" /></div></div>
{loading ? <Surface className="px-5 py-8 text-sm text-muted sm:px-6" variant="secondary">正在加载字段详情...</Surface> : !record ? <EmptyState size="sm"><EmptyState.Header><EmptyState.Media variant="icon"><FileText size={20} /></EmptyState.Media><EmptyState.Title>暂无详情</EmptyState.Title></EmptyState.Header></EmptyState> : (
<Surface className="px-5 py-5 sm:px-6 sm:py-6" variant="secondary">
<div className="grid gap-x-6 gap-y-5 sm:grid-cols-2">
<DetailField label="提交流程模板">{relatedTemplateTitle}</DetailField>
<DetailField label="提交时间">{formatDateTime(record?.metadata?.submit_at, 'YYYY-MM-DD HH:mm')}</DetailField>
<DetailField label="提交者">{record?.metadata?.submitter || <span className="text-muted">暂无内容</span>}</DetailField>
<DetailField label="审批者">{record?.metadata?.approver || <span className="text-muted">暂无内容</span>}</DetailField>
<DetailField label="所属部门">{record?.metadata?.department || <span className="text-muted">暂无内容</span>}</DetailField>
<DetailField label="显示条件">{relatedCondition?.key ? `${relatedCondition.key} = ${relatedCondition.value || '-'}` : '未配置'}</DetailField>
{details.map((field, index) => <DetailField key={`${field.label}-${index}`} label={field.label || `字段 ${index + 1}`} fullWidth={field?.type === 'file' || String(field?.value || '').length > 80}>{resolveDetailValue(field, fieldMap, lookups)}</DetailField>)}
</div>
</Surface>
)}
<ProcessInstanceDetailContent data={detailData} loading={loading} withSurface />
</div>
);
}
......@@ -93,7 +93,7 @@ Shell 中的应用切换入口继续读取当前登录用户可访问应用,
- 在当前分组下展示 `metadata.progresses` 关联的监控模板
- 模板名称列仅展示文本,通过操作列的“查看”图标进入模板实例列表;管理角色在其后继续使用编辑和取消监控操作
- 按模板标题和所属部门筛选当前分组模板
- “所属部门”以同名 HeroUI Popover 入口收纳在模板列表工具栏中,按需展开且不单独占用筛选 Surface;选择和重置仍沿用原有实时过滤状态
- “所属部门”以同名 HeroUI Popover 入口收纳在模板列表工具栏中,按需展开且不单独占用筛选 Surface;弹窗内标签独占一行,选择器与重置按钮同行,选择和重置仍沿用原有实时过滤状态
- 管理角色可执行:
- 新建分组
- 删除空分组
......@@ -118,10 +118,11 @@ Shell 中的应用切换入口继续读取当前登录用户可访问应用,
- 读取当前监控模板记录 `audit_related_progress`
- 根据模板的 `metadata.fields_definition` 动态生成列表列
- 查询 `audit_progress_info` 并按 `metadata.related_progress` 过滤
- 支持关键字搜索和固定每页 10 条的分页切换
- 支持关键字搜索和每页 10/20/50/100 条的分页切换,默认每页 10 条
- 支持批量触发附件下载
- 支持维护实例列表的显示字段定义
- 页面头部将返回入口与面包屑放在左侧同行,批量下载附件与定义显示字段保持同行并右对齐;窄屏空间不足时操作组整体换到下一行,两个操作按钮不拆行
- 打开“定义显示字段”时,已有字段按配置回显;无字段配置时默认展示一行空字段,字段名和字段类型分别使用“请输入字段名”“请选择”提示,不额外展示重复的字段表头
- 页面导航统一使用顶部 Navbar 页面标题,页面内容区不再重复渲染返回入口或面包屑;批量下载附件与定义显示字段保持同行并右对齐,窄屏空间不足时操作组整体换到下一行,两个操作按钮不拆行
- 实例列表的标题、搜索工具栏、DataGrid 和 Pagination 作为同一列表板块组织,标题与工具栏、工具栏与 DataGrid、DataGrid 与 Pagination 均采用 16px 关联间距
筛选规则:
......@@ -132,8 +133,8 @@ Shell 中的应用切换入口继续读取当前登录用户可访问应用,
列表交互:
- 使用 HeroUI Pro DataGrid 生成动态列并承载选择状态
- 表单名称列仅展示文本,通过操作列的“查看”图标进入单条流程实例详情
- Pagination 保留原有页码语义和 `page` / `page_size` 请求参数,页面不提供 page-size 切换控件
- 表单名称列仅展示文本,通过操作列的“查看”图标在当前列表页打开单条流程实例详情 Modal;详情路由仍保留用于直接访问和兼容既有链接
- Pagination 保留原有页码语义和 `page` / `page_size` 请求参数,条数选择器支持 10/20/50/100 条
- 有选中记录时由 HeroUI Pro ActionBar 承载批量附件下载;空数据使用 HeroUI Pro EmptyState
### 5.3 流程实例详情页
......@@ -171,7 +172,7 @@ Shell 中的应用切换入口继续读取当前登录用户可访问应用,
- 旧版实例列表批量下载会打包 zip;当前迁移版使用逐个触发浏览器下载,保持“批量下载附件”能力但不强依赖额外前端压缩库
- 旧版详情页曾预留字段类型即时修改能力,但实际入口已注释;当前迁移版不开放详情页内字段类型编辑,统一回到模板实例列表页的“定义显示字段”
- 列表按统一设计合同固定为每页 10 条,不再展示 page-size 选择器;后端分页协议没有变化
- 列表按统一设计合同默认每页 10 条,并通过 HeroUI 条数选择器支持 10/20/50/100 条;后端分页协议没有变化
- 401 响应仍在原时机清理登录态,只将登录跳转修正为包含 `/oa_process_monitor_new/` base 的应用内地址
---
......
......@@ -19,19 +19,12 @@
--font-inter:
"Inter", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
color-scheme: light !important;
--pm-brand: var(--rc-brand-primary);
--pm-brand-strong: var(--rc-brand-dark);
}
html {
color-scheme: light !important;
}
html.dark,
html[data-theme="dark"] {
color-scheme: dark !important;
}
body {
margin: 0;
font-family: var(--rc-font-sans);
......@@ -40,6 +33,43 @@ body {
color-scheme: inherit;
}
:is(.checkbox[data-selected="true"], .checkbox[data-indeterminate="true"])
.checkbox__control,
:is(
.checkbox__content[data-selected="true"],
.checkbox__content[data-indeterminate="true"]
)
.checkbox__control {
border-color: var(--rc-brand-primary) !important;
background: var(--rc-brand-primary) !important;
box-shadow: none !important;
filter: none !important;
}
:is(.checkbox[data-selected="true"], .checkbox[data-indeterminate="true"])
.checkbox__control::before,
:is(
.checkbox__content[data-selected="true"],
.checkbox__content[data-indeterminate="true"]
)
.checkbox__control::before {
border-color: var(--rc-brand-primary) !important;
background: var(--rc-brand-primary) !important;
box-shadow: none !important;
filter: none !important;
}
:is(.checkbox[data-selected="true"], .checkbox[data-indeterminate="true"])
.checkbox__indicator,
:is(
.checkbox__content[data-selected="true"],
.checkbox__content[data-indeterminate="true"]
)
.checkbox__indicator {
color: var(--accent-foreground) !important;
filter: none !important;
}
body .login-page {
--rc-login-brand-logo-size: 2.25rem;
--rc-login-card-brand-gap: 0.875rem;
......
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