Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in / Register
Toggle navigation
O
oa_process_monitor
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
David Yang
oa_process_monitor
Commits
bf90cc17
Commit
bf90cc17
authored
Aug 27, 2026
by
Andy-bubu
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: add notification center
parent
2abf70b5
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
15 changed files
with
383 additions
and
6 deletions
+383
-6
.umirc.ts
.umirc.ts
+5
-0
ui-architecture.md
docs/ui-architecture.md
+15
-0
ui-migration-checklist.md
docs/ui-migration-checklist.md
+17
-1
xinyuan-design-system.md
docs/xinyuan-design-system.md
+14
-0
xinyuan-ui-audit.md
docs/xinyuan-ui-audit.md
+23
-2
AppIcons.jsx
src/components/AppIcons.jsx
+2
-0
AppShell.jsx
src/components/AppShell.jsx
+6
-0
DashboardNavbar.jsx
src/components/DashboardNavbar.jsx
+38
-3
application.js
src/config/application.js
+1
-0
NotificationListPage.jsx
src/pages/notifications/NotificationListPage.jsx
+0
-0
notificationService.js
src/pages/notifications/notificationService.js
+181
-0
Layout.jsx
src/pages/process_monitor/Layout.jsx
+30
-0
process-monitor.business-flow.md
src/pages/process_monitor/process-monitor.business-flow.md
+24
-0
record-api.md
src/services/record-api.md
+14
-0
subjectsDefinition.md
src/services/subjectsDefinition.md
+13
-0
No files found.
.umirc.ts
View file @
bf90cc17
...
@@ -8,6 +8,7 @@ function resolvePackageRoot(packageName: string) {
...
@@ -8,6 +8,7 @@ function resolvePackageRoot(packageName: string) {
const
ROUTE_PATHS
=
{
const
ROUTE_PATHS
=
{
login
:
'/login'
,
login
:
'/login'
,
processMonitor
:
'/process_monitor'
,
processMonitor
:
'/process_monitor'
,
notifications
:
'/notifications'
,
processTemplateList
:
'/process_monitor/:processId'
,
processTemplateList
:
'/process_monitor/:processId'
,
processDetail
:
'/process_monitor/:processId/:application'
,
processDetail
:
'/process_monitor/:processId/:application'
,
}
as
const
;
}
as
const
;
...
@@ -47,6 +48,10 @@ export default defineConfig({
...
@@ -47,6 +48,10 @@ export default defineConfig({
path
:
ROUTE_PATHS
.
processMonitor
,
path
:
ROUTE_PATHS
.
processMonitor
,
component
:
'./process_monitor/ProcessMonitor'
,
component
:
'./process_monitor/ProcessMonitor'
,
},
},
{
path
:
ROUTE_PATHS
.
notifications
,
component
:
'./notifications/NotificationListPage'
,
},
{
{
path
:
ROUTE_PATHS
.
processTemplateList
,
path
:
ROUTE_PATHS
.
processTemplateList
,
component
:
'./process_monitor/ProcessTemplateList'
,
component
:
'./process_monitor/ProcessTemplateList'
,
...
...
docs/ui-architecture.md
View file @
bf90cc17
...
@@ -48,3 +48,18 @@
...
@@ -48,3 +48,18 @@
-
`fields_definition`
继续兼容数组/对象映射并驱动动态列。
-
`fields_definition`
继续兼容数组/对象映射并驱动动态列。
-
附件继续先调用
`sync_progress:get_attachment_link`
。
-
附件继续先调用
`sync_progress:get_attachment_link`
。
-
登录参数、验证码、密码加密、logout、权限、保存回调、请求时机和分页协议保持不变。
-
登录参数、验证码、密码加密、logout、权限、保存回调、请求时机和分页协议保持不变。
## 通知中心架构
-
`src/pages/notifications/NotificationListPage.jsx`
只负责通知列表状态和 HeroUI 交互;
`notificationService.js`
负责通知 API、状态判断、动作调用和 base-aware 跳转。
-
真实未读数由
`GET /notice/customer/count`
注入 AppShell/Navbar;通知操作派发
`oa-process-monitor:notification-state`
事件,Shell 监听该事件并每 60 秒轮询刷新。
-
`POST /notice/customer/query`
使用
`{ page, page_size, filter, status }`
,消息/待办由
`filter.category`
区分,未读通过
`status: "unread"`
;读取使用
`PUT /notice/customer/read`
,动态 action 使用
`PUT /notice/customer/:action/action/:fname`
和
`{ notice_id }`
。
-
站内链接仅剥离
`/oa_process_monitor_new/`
后交给 Umi history;其他应用路径与 HTTP(S)
链接在新窗口打开。通知文本不作为 HTML 插入 DOM。
-
Sidebar 不显示通知菜单项,通知中心是 Navbar 全局动作;通知失败使用 HeroUI Toast/Alert
反馈,不改变原有登录、退出和业务页面请求。
docs/ui-migration-checklist.md
View file @
bf90cc17
...
@@ -29,6 +29,22 @@
...
@@ -29,6 +29,22 @@
-
[
x
]
同步更新
`process-monitor.business-flow.md`
-
[
x
]
同步更新
`process-monitor.business-flow.md`
-
[
N/A
]
图表:目标应用没有图表业务页面
-
[
N/A
]
图表:目标应用没有图表业务页面
## 白名单功能范围
-
[
x
]
OA流程监控按跨应用白名单仅实施功能 4:消息通知
-
[
N/A
]
Grid/Detail shortTitle:非六个完整功能应用,明确不实施
-
[
N/A
]
Subject View 授权:非六个完整功能应用,明确不实施
-
[
N/A
]
审计材料中心文件流程:非六个完整功能应用,明确不实施
## 消息通知
-
[
x
]
`/notifications`
独立路由和应用 base-aware 导航
-
[
x
]
Navbar
`Badge.Anchor + Badge + Button + Tooltip`
,真实未读数和 99+ 上限
-
[
x
]
消息/待办、全部/未读、300ms 转义搜索、固定 10 条分页
-
[
x
]
单条已读、全部已读、动态 action 和
`{ notice_id }`
请求体
-
[
x
]
站内链接剥离自身 base,外部链接新窗口,通知内容纯文本渲染
-
[
x
]
状态事件刷新和 60 秒未读轮询
## 浏览器验收
## 浏览器验收
-
[
x
]
1440x900 浅色/深色目标与基准对照
-
[
x
]
1440x900 浅色/深色目标与基准对照
...
@@ -38,7 +54,7 @@
...
@@ -38,7 +54,7 @@
-
[
x
]
登录、Sidebar、Navbar、应用切换、Modal、Autocomplete 打开态和空列表
-
[
x
]
登录、Sidebar、Navbar、应用切换、Modal、Autocomplete 打开态和空列表
-
[
x
]
干净退出与应用切换会话控制台 0 error / 0 warning
-
[
x
]
干净退出与应用切换会话控制台 0 error / 0 warning
-
[
x
]
Autocomplete、Modal、应用切换和移动 Sidebar 对话框可访问名称
-
[
x
]
Autocomplete、Modal、应用切换和移动 Sidebar 对话框可访问名称
-
[
~
]
populated DataGrid、Checkbox 半选、ActionBar、真实记录 CRUD、实例详情和附件:缺少有效业务会话
-
[
~
]
populated DataGrid、Checkbox 半选、ActionBar、真实记录 CRUD、实例详情和附件:缺少有效业务会话
;不属于本次通知白名单范围
## 最终门槛
## 最终门槛
...
...
docs/xinyuan-design-system.md
View file @
bf90cc17
...
@@ -52,3 +52,17 @@
...
@@ -52,3 +52,17 @@
-
浅色使用
`xy-logo-red.png`
,深色使用
`xy-logo-white.png`
,不滤镜、不反色、不替换 RSM 标识。
-
浅色使用
`xy-logo-red.png`
,深色使用
`xy-logo-white.png`
,不滤镜、不反色、不替换 RSM 标识。
-
根元素持有
`.light/.dark`
与匹配的
`data-theme`
,所有 Portal 继承根主题。
-
根元素持有
`.light/.dark`
与匹配的
`data-theme`
,所有 Portal 继承根主题。
-
组件外观只来自 HeroUI CSS、Pro CSS、权威 theme.css 和批准的共享 Tokens。
-
组件外观只来自 HeroUI CSS、Pro CSS、权威 theme.css 和批准的共享 Tokens。
## 通知中心
-
`OA流程监控`
不在六个完整功能应用白名单内,本项目只实施消息通知,不加入
`shortTitle`
、Subject View 授权或审计材料中心文件流程。
-
通知页路由为
`/notifications`
,沿用应用 base
`/oa_process_monitor_new/`
。
-
Navbar 使用 HeroUI
`Badge.Anchor + Badge + Button + Tooltip`
,Ant Design
`BellOutlined`
位于主题按钮左侧;未读数为 0 时隐藏徽标,1-99 显示真实值,100 及以上显示
`99+`
,可访问名称保留真实数量。
-
通知页使用 HeroUI Tabs、SearchField、Card、Chip、Pagination、Toast 和 HeroUI Pro
EmptyState。查询固定每页 10 条,搜索延迟 300ms 并转义正则字符,未读状态通过服务端
`status: "unread"`
查询。
-
单条已读、全部已读和服务端动态待办 action 继续使用平台接口;action 请求体只传
`{ notice_id }`
。通知内容按纯文本渲染,禁止执行服务端 HTML。
docs/xinyuan-ui-audit.md
View file @
bf90cc17
# OA流程监控鑫元 UI 审计
# OA流程监控鑫元 UI 审计
审计日期:2026-08-2
0
审计日期:2026-08-2
7
## 范围与基准
## 范围与基准
-
目标仓库:
`oa_process_monitor`
,分支
`codex/xinyuan-ui-ux`
。
-
目标仓库:
`oa_process_monitor`
,分支
`codex/xinyuan-ui-ux`
。
-
整体实现基准:本机
`problem_rectification`
的实际代码与运行页面;应用切换专项基准为本机
`model_analysis`
当前实现。
-
整体实现基准:本机
`problem_rectification`
的实际代码与运行页面;应用切换专项基准为本机
`model_analysis`
当前实现。
-
路由基址:
`/oa_process_monitor_new/`
。
-
路由基址:
`/oa_process_monitor_new/`
。
-
活动路由:
`/login`
、
`/process_monitor`
、
`/process_monitor/:processId`
、
`/process_monitor/:processId/:application`
。
-
活动路由:
`/login`
、
`/process_monitor`
、
`/
notifications`
、
`/
process_monitor/:processId`
、
`/process_monitor/:processId/:application`
。
-
业务依据:
`src/services/record-api.md`
、
`src/services/subjectsDefinition.md`
、
`src/pages/process_monitor/process-monitor.business-flow.md`
。
-
业务依据:
`src/services/record-api.md`
、
`src/services/subjectsDefinition.md`
、
`src/pages/process_monitor/process-monitor.business-flow.md`
。
## 安装与依赖结论
## 安装与依赖结论
...
@@ -16,6 +16,7 @@
...
@@ -16,6 +16,7 @@
-
已在该目录成功执行用户指定的官方
`pnpm dlx hpsetup@latest`
授权安装命令;授权值未写入源码、文档、环境文件或 Git 提交。
-
已在该目录成功执行用户指定的官方
`pnpm dlx hpsetup@latest`
授权安装命令;授权值未写入源码、文档、环境文件或 Git 提交。
-
`@heroui-pro/react@1.0.0-beta.8`
解析到
`/Users/liuchenlong/Desktop/job/oa_process_monitor/node_modules/.pnpm/@heroui-pro+react@1.0.0-beta.8/node_modules/@heroui-pro/react/package.json`
。
-
`@heroui-pro/react@1.0.0-beta.8`
解析到
`/Users/liuchenlong/Desktop/job/oa_process_monitor/node_modules/.pnpm/@heroui-pro+react@1.0.0-beta.8/node_modules/@heroui-pro/react/package.json`
。
-
`@heroui/react@3.2.4`
、
`@heroui/styles@3.2.4`
使用正式版本,并与问题整改当前运行依赖一致,不再存在跨项目
`file:`
依赖。
-
`@heroui/react@3.2.4`
、
`@heroui/styles@3.2.4`
使用正式版本,并与问题整改当前运行依赖一致,不再存在跨项目
`file:`
依赖。
-
本次功能没有改动
`package.json`
或
`pnpm-lock.yaml`
;HeroUI Pro 已在目标仓库独立安装,因而没有再次执行安装命令。
-
`package.json`
写入正式版本依赖及 Pro 运行依赖;
`package-lock.json`
已移除,
`pnpm-lock.yaml`
已更新并作为唯一锁文件保留,Umi 的
`npmClient`
已切换为
`pnpm`
。
-
`package.json`
写入正式版本依赖及 Pro 运行依赖;
`package-lock.json`
已移除,
`pnpm-lock.yaml`
已更新并作为唯一锁文件保留,Umi 的
`npmClient`
已切换为
`pnpm`
。
-
`@heroui-pro/react/css`
、
`sidebar`
、
`navbar`
、
`data-grid`
、
`drop-zone`
、
`action-bar`
、
`empty-state`
均通过 ESM 导入检查,CSS 解析目标文件存在。
-
`@heroui-pro/react/css`
、
`sidebar`
、
`navbar`
、
`data-grid`
、
`drop-zone`
、
`action-bar`
、
`empty-state`
均通过 ESM 导入检查,CSS 解析目标文件存在。
-
监控分组 Tabs 使用问题整改相同的 compound 布局合同;同步 HeroUI 3.2.4 后,Tabs 根轨道与各标签均按内容宽度收敛,不再均分或占满页面剩余宽度。
-
监控分组 Tabs 使用问题整改相同的 compound 布局合同;同步 HeroUI 3.2.4 后,Tabs 根轨道与各标签均按内容宽度收敛,不再均分或占满页面剩余宽度。
...
@@ -46,6 +47,23 @@
...
@@ -46,6 +47,23 @@
-
仅按批准合同把列表固定为每页 10 条并移除 page-size UI;请求仍使用原有
`page`
/
`page_size`
协议。
-
仅按批准合同把列表固定为每页 10 条并移除 page-size UI;请求仍使用原有
`page`
/
`page_size`
协议。
-
401 仍在相同响应时机清理登录态并跳转登录页,只修正为带 Umi base 的应用内地址。
-
401 仍在相同响应时机清理登录态并跳转登录页,只修正为带 Umi base 的应用内地址。
## 通知中心审计
-
按跨应用白名单判定,OA流程监控仅新增通知中心;没有新增 shortTitle、视图权限或材料中心
文件服务/路由。
-
目标与问题整改均以 Andy 真实账号登录,在 1280x720 下打开
`/notifications`
对照,Shell、
Navbar Bell/主题按钮、Tabs、搜索、空状态和间距一致;目标真实接口返回 0 条通知。
-
浏览器内路由拦截覆盖了 0/1/99/100 未读数、消息/待办、未读查询、正则转义搜索、分页、
单条已读、全部已读、动态 action、站内/外部跳转和纯文本 HTML 渲染。拦截只存在于 Playwright
会话,不写入仓库或服务端数据。
-
通知 API 实测:
`POST /api/notice/customer/query`
、
`GET /api/notice/customer/count`
均
`200`
;动态 action 使用
`PUT /api/notice/customer/approval/action/approve`
,请求体为
`{ "notice_id": "..." }`
;控制台 0 error / 0 warning。
-
目标与问题整改通知页已在 1440x900、1280x720、768x1024、390x844 的浅色/深色验证,
均无页面横向溢出。1280x720 两边使用真实登录和真实 0 条通知;其余基准视口在验证码会话失效后
使用浏览器内只读认证/空通知响应夹具,只验证视觉结构,不写入仓库或服务端。截图均保存在
`/tmp`
,未纳入版本控制。
## 浏览器证据与限制
## 浏览器证据与限制
-
已覆盖 1440x900、1280x720、768x1024、390x844;包含浅色、深色、Sidebar offcanvas、移动 Sidebar、应用切换和创建分组 Modal。
-
已覆盖 1440x900、1280x720、768x1024、390x844;包含浅色、深色、Sidebar offcanvas、移动 Sidebar、应用切换和创建分组 Modal。
...
@@ -65,3 +83,6 @@
...
@@ -65,3 +83,6 @@
-
旧组件、原生可见控件、页面私有视觉 CSS、页面局部 CSS/LESS、
`file:`
/本机路径依赖扫描:均为零。
-
旧组件、原生可见控件、页面私有视觉 CSS、页面局部 CSS/LESS、
`file:`
/本机路径依赖扫描:均为零。
-
`theme.css`
与 Skill 权威副本逐字节一致;
`tokens.css`
、
`shell.css`
、
`tailwind.css`
与问题整改基准逐字节一致。
-
`theme.css`
与 Skill 权威副本逐字节一致;
`tokens.css`
、
`shell.css`
、
`tailwind.css`
与问题整改基准逐字节一致。
-
HeroUI/React Aria 依赖树和 Pro 子路径检查:通过;目标仓库不存在相邻项目依赖、软链接或本机 vendor 替代包。
-
HeroUI/React Aria 依赖树和 Pro 子路径检查:通过;目标仓库不存在相邻项目依赖、软链接或本机 vendor 替代包。
-
`pnpm list`
显示应用运行层统一为 React/ReactDOM 19.2.8、HeroUI 3.2.4、React Aria
Components 1.20.0;完整
`npm ls`
仍报告 Umi/Ant Design 传递依赖声明的旧 React peer 警告,
属于本次功能前已存在的依赖元数据风险,本次未更改该依赖树。
src/components/AppIcons.jsx
View file @
bf90cc17
import
BankOutlined
from
'@ant-design/icons/BankOutlined'
;
import
BankOutlined
from
'@ant-design/icons/BankOutlined'
;
import
BarChartOutlined
from
'@ant-design/icons/BarChartOutlined'
;
import
BarChartOutlined
from
'@ant-design/icons/BarChartOutlined'
;
import
BellOutlined
from
'@ant-design/icons/BellOutlined'
;
import
AppstoreOutlined
from
'@ant-design/icons/AppstoreOutlined'
;
import
AppstoreOutlined
from
'@ant-design/icons/AppstoreOutlined'
;
import
CalendarOutlined
from
'@ant-design/icons/CalendarOutlined'
;
import
CalendarOutlined
from
'@ant-design/icons/CalendarOutlined'
;
import
CheckOutlined
from
'@ant-design/icons/CheckOutlined'
;
import
CheckOutlined
from
'@ant-design/icons/CheckOutlined'
;
...
@@ -63,6 +64,7 @@ export const Bank = withSize(BankOutlined);
...
@@ -63,6 +64,7 @@ export const Bank = withSize(BankOutlined);
export
const
Archive
=
withSize
(
InboxOutlined
);
export
const
Archive
=
withSize
(
InboxOutlined
);
export
const
AppGrid
=
withSize
(
AppstoreOutlined
);
export
const
AppGrid
=
withSize
(
AppstoreOutlined
);
export
const
BarChart
=
withSize
(
BarChartOutlined
);
export
const
BarChart
=
withSize
(
BarChartOutlined
);
export
const
Bell
=
withSize
(
BellOutlined
);
export
const
Calendar
=
withSize
(
CalendarOutlined
);
export
const
Calendar
=
withSize
(
CalendarOutlined
);
export
const
Check
=
withSize
(
CheckOutlined
);
export
const
Check
=
withSize
(
CheckOutlined
);
export
const
ChevronDown
=
withSize
(
DownOutlined
);
export
const
ChevronDown
=
withSize
(
DownOutlined
);
...
...
src/components/AppShell.jsx
View file @
bf90cc17
...
@@ -20,6 +20,8 @@ function getInitialTheme() {
...
@@ -20,6 +20,8 @@ function getInitialTheme() {
export
default
function
AppShell
({
export
default
function
AppShell
({
children
,
children
,
notificationCount
,
notificationPath
,
open
,
open
,
onOpenChange
,
onOpenChange
,
title
,
title
,
...
@@ -52,6 +54,10 @@ export default function AppShell({
...
@@ -52,6 +54,10 @@ export default function AppShell({
<
DashboardSidebar
{
...
sidebarProps
}
/>
<
DashboardSidebar
{
...
sidebarProps
}
/>
<
Sidebar
.
Main
className=
"app-main-glass rc-app-main"
>
<
Sidebar
.
Main
className=
"app-main-glass rc-app-main"
>
<
DashboardNavbar
<
DashboardNavbar
notificationCount=
{
notificationCount
}
onOpenNotifications=
{
notificationPath
?
()
=>
history
.
push
(
notificationPath
)
:
undefined
}
theme=
{
theme
}
theme=
{
theme
}
title=
{
title
}
title=
{
title
}
onThemeChange=
{
setTheme
}
onThemeChange=
{
setTheme
}
...
...
src/components/DashboardNavbar.jsx
View file @
bf90cc17
import
{
Navbar
}
from
'@heroui-pro/react/navbar'
;
import
{
Navbar
}
from
'@heroui-pro/react/navbar'
;
import
{
Sidebar
,
useSidebar
}
from
'@heroui-pro/react/sidebar'
;
import
{
Sidebar
,
useSidebar
}
from
'@heroui-pro/react/sidebar'
;
import
{
Button
,
Tooltip
}
from
'@heroui/react'
;
import
{
B
adge
,
B
utton
,
Tooltip
}
from
'@heroui/react'
;
import
{
Moon
,
SidebarPanel
,
Sun
}
from
'./AppIcons'
;
import
{
Bell
,
Moon
,
SidebarPanel
,
Sun
}
from
'./AppIcons'
;
export
default
function
DashboardNavbar
({
onThemeChange
,
theme
,
title
})
{
export
default
function
DashboardNavbar
({
notificationCount
=
0
,
onOpenNotifications
,
onThemeChange
,
theme
,
title
,
})
{
const
{
isMobile
,
isOpen
}
=
useSidebar
();
const
{
isMobile
,
isOpen
}
=
useSidebar
();
const
isDark
=
theme
===
'dark'
;
const
isDark
=
theme
===
'dark'
;
const
themeLabel
=
isDark
?
'切换至浅色模式'
:
'切换至深色模式'
;
const
themeLabel
=
isDark
?
'切换至浅色模式'
:
'切换至深色模式'
;
const
normalizedNotificationCount
=
Math
.
max
(
0
,
Number
(
notificationCount
)
||
0
);
return
(
return
(
<
Navbar
className=
"app-navbar rc-navbar"
maxWidth=
"full"
>
<
Navbar
className=
"app-navbar rc-navbar"
maxWidth=
"full"
>
...
@@ -20,6 +27,34 @@ export default function DashboardNavbar({ onThemeChange, theme, title }) {
...
@@ -20,6 +27,34 @@ export default function DashboardNavbar({ onThemeChange, theme, title }) {
<
h1
className=
"app-navbar-page-title rc-navbar-title"
>
{
title
}
</
h1
>
<
h1
className=
"app-navbar-page-title rc-navbar-title"
>
{
title
}
</
h1
>
<
Navbar
.
Spacer
/>
<
Navbar
.
Spacer
/>
<
div
className=
"rc-navbar-actions"
>
<
div
className=
"rc-navbar-actions"
>
{
onOpenNotifications
?
(
<
Tooltip
delay=
{
0
}
>
<
Badge
.
Anchor
>
<
Button
isIconOnly
aria
-
label=
"通知消息"
className=
"app-navbar-icon-button rc-navbar-icon-button"
size=
"sm"
variant=
"ghost"
onPress=
{
onOpenNotifications
}
>
<
Bell
size=
{
16
}
/>
</
Button
>
{
normalizedNotificationCount
>
0
?
(
<
Badge
aria
-
label=
{
`${normalizedNotificationCount}条未读通知`
}
color=
"danger"
size=
"sm"
>
{
normalizedNotificationCount
>
99
?
'99+'
:
normalizedNotificationCount
}
</
Badge
>
)
:
null
}
</
Badge
.
Anchor
>
<
Tooltip
.
Content
>
通知消息
</
Tooltip
.
Content
>
</
Tooltip
>
)
:
null
}
<
Tooltip
delay=
{
0
}
>
<
Tooltip
delay=
{
0
}
>
<
Button
<
Button
isIconOnly
isIconOnly
...
...
src/config/application.js
View file @
bf90cc17
...
@@ -4,4 +4,5 @@ export const THEME_STORAGE_KEY = 'xinyuan_color_theme';
...
@@ -4,4 +4,5 @@ export const THEME_STORAGE_KEY = 'xinyuan_color_theme';
export
const
ROUTE_TITLES
=
Object
.
freeze
({
export
const
ROUTE_TITLES
=
Object
.
freeze
({
'/process_monitor'
:
'流程监控'
,
'/process_monitor'
:
'流程监控'
,
'/notifications'
:
'通知消息'
,
});
});
src/pages/notifications/NotificationListPage.jsx
0 → 100644
View file @
bf90cc17
This diff is collapsed.
Click to expand it.
src/pages/notifications/notificationService.js
0 → 100644
View file @
bf90cc17
import
{
APPLICATION_BASE_PATH
}
from
'@/config/application'
;
import
{
exacteActionFunction
,
queryUserNotice
,
readUserNotice
,
unReadNoticeCount
,
}
from
'@/services/DataService'
;
export
const
NOTIFICATION_PAGE_SIZE
=
10
;
export
const
NOTIFICATION_STATE_EVENT
=
'oa-process-monitor:notification-state'
;
function
toArray
(
value
)
{
return
Array
.
isArray
(
value
)
?
value
:
[];
}
function
toId
(
value
)
{
if
(
value
&&
typeof
value
===
'object'
)
{
return
String
(
value
.
_id
||
value
.
id
||
value
.
name
||
''
);
}
return
String
(
value
||
''
);
}
function
escapeRegExp
(
value
)
{
return
String
(
value
||
''
).
replace
(
/
[
.*+?^${}()|[
\]\\]
/g
,
'
\\
$&'
);
}
export
async
function
loadNotificationPage
({
page
=
0
,
pageSize
=
NOTIFICATION_PAGE_SIZE
,
category
=
'message'
,
readStatus
=
'all'
,
keyword
=
''
,
}
=
{})
{
const
filter
=
{
category
};
const
normalizedKeyword
=
String
(
keyword
||
''
).
trim
();
if
(
normalizedKeyword
)
{
filter
.
content
=
{
$regex
:
escapeRegExp
(
normalizedKeyword
),
$options
:
'i'
,
};
}
const
response
=
await
queryUserNotice
(
page
,
pageSize
,
filter
,
readStatus
===
'unread'
?
'unread'
:
null
,
);
return
{
list
:
toArray
(
response
?.
list
),
total
:
Number
(
response
?.
total_notices
??
response
?.
total
??
0
)
||
0
,
};
}
export
async
function
loadUnreadNotificationCount
()
{
const
response
=
await
unReadNoticeCount
();
return
Math
.
max
(
0
,
Number
(
response
?.
total_notices
??
response
?.
count
??
response
?.
total
??
response
??
0
,
)
||
0
,
);
}
export
async
function
markNotificationRead
(
name
)
{
if
(
!
name
)
throw
new
Error
(
'通知缺少已读标识'
);
await
readUserNotice
(
name
);
notifyNotificationStateChanged
();
}
export
async
function
markAllNotificationsRead
()
{
await
readUserNotice
();
notifyNotificationStateChanged
();
}
export
async
function
executeNotificationAction
(
notification
,
action
)
{
const
actionName
=
typeof
action
?.
action
===
'string'
?
action
.
action
:
action
?.
action
?.
name
||
action
?.
name
;
const
methodName
=
action
?.
method_name
||
action
?.
methodName
;
if
(
!
actionName
||
!
methodName
)
{
throw
new
Error
(
'该待办操作缺少执行配置'
);
}
const
result
=
await
exacteActionFunction
(
actionName
,
methodName
,
{
notice_id
:
notification
?.
_id
,
});
notifyNotificationStateChanged
();
return
result
;
}
export
function
isNotificationRead
(
notification
,
userId
)
{
const
normalizedUserId
=
toId
(
userId
);
if
(
!
normalizedUserId
)
return
false
;
return
toArray
(
notification
?.
data
?.
logs
).
some
(
(
log
)
=>
toId
(
log
?.
user_id
)
===
normalizedUserId
,
);
}
export
function
getNotificationSender
(
notification
)
{
return
(
notification
?.
from
?.
display_name
||
notification
?.
from
?.
username
||
notification
?.
created_by
?.
display_name
||
notification
?.
created_by
?.
username
||
'系统'
);
}
export
function
getNotificationContent
(
notification
)
{
return
String
(
notification
?.
content
||
notification
?.
content_compile
||
''
).
trim
();
}
function
trimTarget
(
target
)
{
return
String
(
target
||
''
).
replace
(
/
[
),.;!?,。;!?)
]
+$/
u
,
''
);
}
export
function
extractNotificationTarget
(
notification
)
{
const
configuredTarget
=
notification
?.
settings
?.
url
||
notification
?.
settings
?.
link
||
notification
?.
data
?.
url
||
notification
?.
data
?.
link
;
if
(
configuredTarget
)
return
trimTarget
(
configuredTarget
);
const
content
=
getNotificationContent
(
notification
);
const
escapedBase
=
APPLICATION_BASE_PATH
.
replace
(
/
[
.*+?^${}()|[
\]\\]
/g
,
'
\\
$&'
);
const
match
=
content
.
match
(
new
RegExp
(
`https?:\\/\\/[^\\s<>"']+|
${
escapedBase
}
[^\\s<>"']+`
),
);
return
trimTarget
(
match
?.[
0
]);
}
export
function
resolveNotificationTarget
(
target
)
{
const
normalizedTarget
=
trimTarget
(
target
);
if
(
!
normalizedTarget
)
return
null
;
const
appPrefix
=
APPLICATION_BASE_PATH
.
replace
(
/
\/
$/
,
''
);
if
(
/^https
?
:
\/\/
/i
.
test
(
normalizedTarget
))
{
try
{
const
url
=
new
URL
(
normalizedTarget
);
if
(
url
.
pathname
===
appPrefix
||
url
.
pathname
.
startsWith
(
`
${
appPrefix
}
/`
))
{
return
resolveNotificationTarget
(
`
${
url
.
pathname
}${
url
.
search
}${
url
.
hash
}
`
);
}
return
{
href
:
url
.
toString
(),
external
:
true
};
}
catch
(
error
)
{
return
null
;
}
}
if
(
normalizedTarget
===
appPrefix
)
{
return
{
href
:
'/'
,
external
:
false
};
}
if
(
!
normalizedTarget
.
startsWith
(
`
${
appPrefix
}
/`
))
{
return
normalizedTarget
.
startsWith
(
'/'
)
?
{
href
:
normalizedTarget
,
external
:
true
}
:
null
;
}
return
{
href
:
normalizedTarget
.
slice
(
appPrefix
.
length
),
external
:
false
,
};
}
export
function
notifyNotificationStateChanged
()
{
if
(
typeof
window
!==
'undefined'
)
{
window
.
dispatchEvent
(
new
CustomEvent
(
NOTIFICATION_STATE_EVENT
));
}
}
src/pages/process_monitor/Layout.jsx
View file @
bf90cc17
...
@@ -3,6 +3,10 @@ import ApplicationSwitcherDialog from '@/components/ApplicationSwitcherDialog';
...
@@ -3,6 +3,10 @@ import ApplicationSwitcherDialog from '@/components/ApplicationSwitcherDialog';
import
{
ProcessList
}
from
'@/components/AppIcons'
;
import
{
ProcessList
}
from
'@/components/AppIcons'
;
import
{
APPLICATION_DISPLAY_NAME
,
ROUTE_TITLES
}
from
'@/config/application'
;
import
{
APPLICATION_DISPLAY_NAME
,
ROUTE_TITLES
}
from
'@/config/application'
;
import
{
useUserInfo
}
from
'@/hooks/user'
;
import
{
useUserInfo
}
from
'@/hooks/user'
;
import
{
loadUnreadNotificationCount
,
NOTIFICATION_STATE_EVENT
,
}
from
'@/pages/notifications/notificationService'
;
import
{
SiteContext
}
from
'@/wrapper/Site'
;
import
{
SiteContext
}
from
'@/wrapper/Site'
;
import
{
useContext
,
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
{
useContext
,
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
{
Outlet
,
useLocation
}
from
'umi'
;
import
{
Outlet
,
useLocation
}
from
'umi'
;
...
@@ -19,6 +23,7 @@ export default function ProcessMonitorLayout() {
...
@@ -19,6 +23,7 @@ export default function ProcessMonitorLayout() {
const
siteInfo
=
useContext
(
SiteContext
);
const
siteInfo
=
useContext
(
SiteContext
);
const
[
sidebarOpen
,
setSidebarOpen
]
=
useState
(
true
);
const
[
sidebarOpen
,
setSidebarOpen
]
=
useState
(
true
);
const
[
isApplicationDialogOpen
,
setIsApplicationDialogOpen
]
=
useState
(
false
);
const
[
isApplicationDialogOpen
,
setIsApplicationDialogOpen
]
=
useState
(
false
);
const
[
unreadNotificationCount
,
setUnreadNotificationCount
]
=
useState
(
0
);
const
currentTitle
=
useMemo
(
const
currentTitle
=
useMemo
(
()
=>
ROUTE_TITLES
[
location
.
pathname
]
||
(
location
.
pathname
===
'/process_monitor'
?
'流程监控'
:
'流程详情'
),
()
=>
ROUTE_TITLES
[
location
.
pathname
]
||
(
location
.
pathname
===
'/process_monitor'
?
'流程监控'
:
'流程详情'
),
...
@@ -29,10 +34,35 @@ export default function ProcessMonitorLayout() {
...
@@ -29,10 +34,35 @@ export default function ProcessMonitorLayout() {
document
.
title
=
`
${
currentTitle
}
-
${
APPLICATION_DISPLAY_NAME
}
`
;
document
.
title
=
`
${
currentTitle
}
-
${
APPLICATION_DISPLAY_NAME
}
`
;
},
[
currentTitle
]);
},
[
currentTitle
]);
useEffect
(()
=>
{
let
cancelled
=
false
;
const
refreshUnreadCount
=
async
()
=>
{
try
{
const
count
=
await
loadUnreadNotificationCount
();
if
(
!
cancelled
)
setUnreadNotificationCount
(
count
);
}
catch
(
error
)
{
if
(
!
cancelled
)
setUnreadNotificationCount
(
0
);
}
};
refreshUnreadCount
();
window
.
addEventListener
(
NOTIFICATION_STATE_EVENT
,
refreshUnreadCount
);
const
timer
=
window
.
setInterval
(
refreshUnreadCount
,
60000
);
return
()
=>
{
cancelled
=
true
;
window
.
removeEventListener
(
NOTIFICATION_STATE_EVENT
,
refreshUnreadCount
);
window
.
clearInterval
(
timer
);
};
},
[
location
.
pathname
]);
return
(
return
(
<>
<>
<
AppShell
<
AppShell
menuGroups=
{
MENU_GROUPS
}
menuGroups=
{
MENU_GROUPS
}
notificationCount=
{
unreadNotificationCount
}
notificationPath=
"/notifications"
onLogout=
{
userInfo
?.
logout
}
onLogout=
{
userInfo
?.
logout
}
onOpenApplicationSwitcher=
{
()
=>
setIsApplicationDialogOpen
(
true
)
}
onOpenApplicationSwitcher=
{
()
=>
setIsApplicationDialogOpen
(
true
)
}
onOpenChange=
{
setSidebarOpen
}
onOpenChange=
{
setSidebarOpen
}
...
...
src/pages/process_monitor/process-monitor.business-flow.md
View file @
bf90cc17
...
@@ -183,3 +183,27 @@ Shell 中的应用切换入口继续读取当前登录用户可访问应用,
...
@@ -183,3 +183,27 @@ Shell 中的应用切换入口继续读取当前登录用户可访问应用,
-
`src/pages/process_monitor/ProcessTemplateList.jsx`
-
`src/pages/process_monitor/ProcessTemplateList.jsx`
-
`src/pages/process_monitor/ProcessDetail.jsx`
-
`src/pages/process_monitor/ProcessDetail.jsx`
-
`src/pages/process_monitor/processMonitorService.js`
-
`src/pages/process_monitor/processMonitorService.js`
## 9. 消息通知流程
### 9.1 入口与范围
Navbar 的 Bell 进入
`/notifications`
。OA流程监控不在六个完整功能应用白名单内,因此本项目
只提供通知中心,不提供 shortTitle、视图授权或材料中心文件能力;Sidebar 不增加通知菜单。
### 9.2 查询与筛选
页面默认查询消息,支持切换待办、全部/未读和 300ms 防抖文本搜索。查询固定每页 10 条,未读
筛选发送
`status: "unread"`
给
`/notice/customer/query`
,搜索文本先转义正则字符。
### 9.3 已读与待办
单条已读调用
`/notice/customer/read`
并传通知
`name`
;全部已读沿用平台接口的空名称请求。
待办动作读取服务端
`actions[]`
,调用
`/notice/customer/:action/action/:fname`
,请求体只含
`{ notice_id }`
。成功后通过
`oa-process-monitor:notification-state`
通知 Shell 刷新未读数。
### 9.4 关联流程与安全
通知中的站内 URL 只剥离
`/oa_process_monitor_new/`
后交给 Umi history;其他应用或外部
HTTP(S) 链接新窗口打开。
`content`
始终作为纯文本渲染,不执行服务端 HTML。Navbar 首次加载
及后续每 60 秒调用
`/notice/customer/count`
,真实 0 条时不显示徽标,100 及以上显示
`99+`
。
src/services/record-api.md
View file @
bf90cc17
...
@@ -257,6 +257,20 @@ Query:`calculated=true|false`
...
@@ -257,6 +257,20 @@ Query:`calculated=true|false`
-
PUT
`/record/:record/restore`
-
PUT
`/record/:record/restore`
-
DELETE
`/record/clear?subject=xxx`
(清空主题记录,默认软删)
-
DELETE
`/record/clear?subject=xxx`
(清空主题记录,默认软删)
## 3.7 通知中心接口
通知中心沿用平台通知接口,不改变记录 API 协议:
| 方法 | 路径 | 用途 |
| --- | --- | --- |
| POST |
`/notice/customer/query`
| 分页查询消息/待办;请求体包含
`page`
、
`page_size`
、
`filter`
、
`status`
|
| PUT |
`/notice/customer/read`
| 单条已读传
`{ name }`
;全部已读沿用平台空名称请求 |
| GET |
`/notice/customer/count`
| 获取真实未读数量 |
| PUT |
`/notice/customer/:action/action/:fname`
| 执行服务端配置的待办动作,正文为
`{ notice_id }`
|
通知查询的
`filter.category`
使用
`message`
或
`todo`
,未读列表向服务端发送
`status: "unread"`
;前端不在当前页自行推断未读总数。
## 4. 常见问题 & 注意事项
## 4. 常见问题 & 注意事项
1.
**name 与 _id**
:路径参数
`:name`
同时支持业务 name 和 ObjectId 字符串。
1.
**name 与 _id**
:路径参数
`:name`
同时支持业务 name 和 ObjectId 字符串。
...
...
src/services/subjectsDefinition.md
View file @
bf90cc17
...
@@ -208,3 +208,16 @@ type DetailField = {
...
@@ -208,3 +208,16 @@ type DetailField = {
-
实例列表页的显示列必须由
`metadata.fields_definition`
驱动,禁止在页面内写死固定业务列
-
实例列表页的显示列必须由
`metadata.fields_definition`
驱动,禁止在页面内写死固定业务列
-
`related_progress`
写入时只传模板记录
`_id`
-
`related_progress`
写入时只传模板记录
`_id`
-
`detail`
仅作为动态明细展示源,不在前端构造成固定 schema 后回写
-
`detail`
仅作为动态明细展示源,不在前端构造成固定 schema 后回写
## 5. 通知对象(展示协议)
通知不属于本项目 Subject 记录写入范围,页面仅消费平台返回对象。展示所需字段包括:
-
`name`
/
`_id`
:通知和动态操作标识
-
`title`
、
`content`
或
`content_compile`
:标题与纯文本内容
-
`category`
:
`message`
或
`todo`
-
`from`
/
`created_by`
:发送者展示信息
-
`data.logs`
:当前用户已读记录;
`data.url`
或
`settings.url/link`
:关联流程
-
`actions[]`
:服务端配置的待办动作,执行时只发送通知
`_id`
作为
`notice_id`
页面不把通知对象转换为业务 Record,也不修改任何 Subject 字段结构。
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment