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
Show whitespace changes
Inline
Side-by-side
Showing
15 changed files
with
867 additions
and
6 deletions
+867
-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
+484
-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
import
{
Bell
,
Close
,
MagnifyingGlassIcon
}
from
'@/components/AppIcons'
;
import
{
useUserInfo
}
from
'@/hooks/user'
;
import
{
EmptyState
}
from
'@heroui-pro/react/empty-state'
;
import
{
Alert
,
Avatar
,
Button
,
Card
,
Chip
,
Pagination
,
SearchField
,
Spinner
,
Tabs
,
toast
,
}
from
'@heroui/react'
;
import
{
useCallback
,
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
{
history
}
from
'umi'
;
import
{
executeNotificationAction
,
extractNotificationTarget
,
getNotificationContent
,
getNotificationSender
,
isNotificationRead
,
loadNotificationPage
,
loadUnreadNotificationCount
,
markAllNotificationsRead
,
markNotificationRead
,
NOTIFICATION_PAGE_SIZE
,
resolveNotificationTarget
,
}
from
'./notificationService'
;
const
CATEGORY_OPTIONS
=
[
{
key
:
'message'
,
label
:
'消息'
},
{
key
:
'todo'
,
label
:
'待办'
},
];
const
READ_STATUS_OPTIONS
=
[
{
key
:
'all'
,
label
:
'全部'
},
{
key
:
'unread'
,
label
:
'未读'
},
];
function
formatNotificationTime
(
value
)
{
if
(
!
value
)
return
'时间未知'
;
const
date
=
new
Date
(
value
);
if
(
Number
.
isNaN
(
date
.
getTime
()))
return
String
(
value
);
return
new
Intl
.
DateTimeFormat
(
'zh-CN'
,
{
year
:
'numeric'
,
month
:
'2-digit'
,
day
:
'2-digit'
,
hour
:
'2-digit'
,
minute
:
'2-digit'
,
second
:
'2-digit'
,
hour12
:
false
,
}).
format
(
date
);
}
function
NotificationPagination
({
isDisabled
,
page
,
total
,
onPageChange
})
{
const
totalPages
=
Math
.
ceil
(
total
/
NOTIFICATION_PAGE_SIZE
);
const
start
=
Math
.
max
(
0
,
Math
.
min
(
page
-
2
,
totalPages
-
5
));
const
end
=
Math
.
min
(
totalPages
,
start
+
5
);
return
(
<
Pagination
size=
"sm"
>
<
Pagination
.
Summary
>
共
{
total
}
条,第
{
total
?
page
+
1
:
0
}
/
{
totalPages
}
页
</
Pagination
.
Summary
>
<
Pagination
.
Content
>
<
Pagination
.
Item
>
<
Pagination
.
Previous
isDisabled=
{
isDisabled
||
page
<=
0
}
onPress=
{
()
=>
onPageChange
(
page
-
1
)
}
>
<
Pagination
.
PreviousIcon
/>
<
span
>
上一页
</
span
>
</
Pagination
.
Previous
>
</
Pagination
.
Item
>
{
Array
.
from
({
length
:
end
-
start
},
(
_
,
index
)
=>
start
+
index
).
map
(
(
pageIndex
)
=>
(
<
Pagination
.
Item
key=
{
pageIndex
}
>
<
Pagination
.
Link
isActive=
{
pageIndex
===
page
}
onPress=
{
()
=>
onPageChange
(
pageIndex
)
}
>
{
pageIndex
+
1
}
</
Pagination
.
Link
>
</
Pagination
.
Item
>
),
)
}
<
Pagination
.
Item
>
<
Pagination
.
Next
isDisabled=
{
isDisabled
||
page
+
1
>=
totalPages
}
onPress=
{
()
=>
onPageChange
(
page
+
1
)
}
>
<
span
>
下一页
</
span
>
<
Pagination
.
NextIcon
/>
</
Pagination
.
Next
>
</
Pagination
.
Item
>
</
Pagination
.
Content
>
</
Pagination
>
);
}
function
NotificationItem
({
notification
,
category
,
currentUserId
,
isBusy
,
onAction
,
onMarkRead
,
onOpenTarget
,
})
{
const
isRead
=
isNotificationRead
(
notification
,
currentUserId
);
const
sender
=
getNotificationSender
(
notification
);
const
content
=
getNotificationContent
(
notification
)
||
'暂无通知内容'
;
const
target
=
resolveNotificationTarget
(
extractNotificationTarget
(
notification
));
const
actions
=
Array
.
isArray
(
notification
?.
actions
)
?
notification
.
actions
:
[];
const
avatarText
=
sender
.
trim
().
slice
(
0
,
1
).
toUpperCase
()
||
'系'
;
return
(
<
Card
>
<
Card
.
Content
className=
"flex flex-col gap-4 p-4 sm:p-5"
>
<
div
className=
"flex min-w-0 items-start gap-3"
>
<
Avatar
className=
"shrink-0"
size=
"sm"
variant=
"soft"
>
<
Avatar
.
Fallback
>
{
avatarText
}
</
Avatar
.
Fallback
>
</
Avatar
>
<
div
className=
"min-w-0 flex-1"
>
<
div
className=
"flex flex-wrap items-start justify-between gap-2"
>
<
div
className=
"min-w-0"
>
<
h2
className=
"break-words text-sm font-medium sm:text-base"
>
{
notification
?.
title
||
(
category
===
'todo'
?
'待办通知'
:
'通知消息'
)
}
</
h2
>
<
p
className=
"mt-1 text-xs text-muted sm:text-sm"
>
{
sender
}
·
{
' '
}
{
formatNotificationTime
(
notification
?.
sent_at
||
notification
?.
created_at
,
)
}
</
p
>
</
div
>
<
Chip
color=
{
isRead
?
undefined
:
'danger'
}
size=
"sm"
variant=
"soft"
>
{
isRead
?
category
===
'todo'
?
'已完成'
:
'已读'
:
category
===
'todo'
?
'待处理'
:
'未读'
}
</
Chip
>
</
div
>
<
p
className=
"mt-3 whitespace-pre-wrap break-words text-sm leading-6"
>
{
content
}
</
p
>
</
div
>
</
div
>
<
div
className=
"flex min-h-8 flex-wrap items-center justify-end gap-2"
>
{
!
isRead
?
(
<
Button
isDisabled=
{
isBusy
}
size=
"sm"
variant=
"ghost"
onPress=
{
()
=>
onMarkRead
(
notification
)
}
>
标为已读
</
Button
>
)
:
null
}
{
category
===
'todo'
?
actions
.
map
((
action
,
index
)
=>
(
<
Button
isDisabled=
{
isBusy
||
isRead
}
key=
{
`${action?.label || 'action'}-${index}`
}
size=
"sm"
variant=
"secondary"
onPress=
{
()
=>
onAction
(
notification
,
action
)
}
>
{
action
?.
label
||
'处理'
}
</
Button
>
))
:
null
}
{
target
?
(
<
Button
isDisabled=
{
isBusy
}
size=
"sm"
variant=
"primary"
onPress=
{
()
=>
onOpenTarget
(
notification
,
target
)
}
>
进入流程
</
Button
>
)
:
null
}
</
div
>
</
Card
.
Content
>
</
Card
>
);
}
export
default
function
NotificationListPage
()
{
const
userInfo
=
useUserInfo
();
const
[
category
,
setCategory
]
=
useState
(
'message'
);
const
[
readStatus
,
setReadStatus
]
=
useState
(
'all'
);
const
[
searchText
,
setSearchText
]
=
useState
(
''
);
const
[
keyword
,
setKeyword
]
=
useState
(
''
);
const
[
page
,
setPage
]
=
useState
(
0
);
const
[
notifications
,
setNotifications
]
=
useState
([]);
const
[
total
,
setTotal
]
=
useState
(
0
);
const
[
unreadTotal
,
setUnreadTotal
]
=
useState
(
0
);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
error
,
setError
]
=
useState
(
''
);
const
[
busyKey
,
setBusyKey
]
=
useState
(
''
);
const
[
reloadKey
,
setReloadKey
]
=
useState
(
0
);
useEffect
(()
=>
{
const
timer
=
window
.
setTimeout
(()
=>
{
setKeyword
(
searchText
.
trim
());
setPage
(
0
);
},
300
);
return
()
=>
window
.
clearTimeout
(
timer
);
},
[
searchText
]);
useEffect
(()
=>
{
let
cancelled
=
false
;
setLoading
(
true
);
setError
(
''
);
loadNotificationPage
({
page
,
pageSize
:
NOTIFICATION_PAGE_SIZE
,
category
,
readStatus
,
keyword
,
})
.
then
((
result
)
=>
{
if
(
cancelled
)
return
;
setNotifications
(
result
.
list
);
setTotal
(
result
.
total
);
})
.
catch
((
requestError
)
=>
{
if
(
cancelled
)
return
;
setNotifications
([]);
setTotal
(
0
);
setError
(
requestError
?.
message
||
'通知列表加载失败,请稍后重试'
);
})
.
finally
(()
=>
{
if
(
!
cancelled
)
setLoading
(
false
);
});
return
()
=>
{
cancelled
=
true
;
};
},
[
category
,
keyword
,
page
,
readStatus
,
reloadKey
]);
useEffect
(()
=>
{
let
cancelled
=
false
;
loadUnreadNotificationCount
()
.
then
((
count
)
=>
{
if
(
!
cancelled
)
setUnreadTotal
(
count
);
})
.
catch
(()
=>
{
if
(
!
cancelled
)
setUnreadTotal
(
0
);
});
return
()
=>
{
cancelled
=
true
;
};
},
[
reloadKey
]);
const
refresh
=
useCallback
(()
=>
setReloadKey
((
value
)
=>
value
+
1
),
[]);
const
totalPages
=
useMemo
(
()
=>
Math
.
ceil
(
total
/
NOTIFICATION_PAGE_SIZE
),
[
total
],
);
useEffect
(()
=>
{
if
(
totalPages
>
0
&&
page
>=
totalPages
)
setPage
(
totalPages
-
1
);
},
[
page
,
totalPages
]);
const
runItemOperation
=
useCallback
(
async
(
key
,
operation
,
successMessage
)
=>
{
setBusyKey
(
key
);
try
{
await
operation
();
toast
.
success
(
successMessage
,
{
timeout
:
3000
});
refresh
();
return
true
;
}
catch
(
operationError
)
{
toast
.
danger
(
operationError
?.
message
||
'操作失败,请稍后重试'
,
{
timeout
:
4000
,
});
return
false
;
}
finally
{
setBusyKey
(
''
);
}
},
[
refresh
],
);
const
handleMarkRead
=
useCallback
(
(
notification
)
=>
runItemOperation
(
notification
.
name
,
()
=>
markNotificationRead
(
notification
.
name
),
'已标记为已读'
,
),
[
runItemOperation
],
);
const
handleMarkAllRead
=
useCallback
(
()
=>
runItemOperation
(
'all'
,
markAllNotificationsRead
,
'全部通知已标记为已读'
),
[
runItemOperation
],
);
const
handleAction
=
useCallback
(
(
notification
,
action
)
=>
runItemOperation
(
notification
.
name
,
()
=>
executeNotificationAction
(
notification
,
action
),
'待办已处理'
,
),
[
runItemOperation
],
);
const
handleOpenTarget
=
useCallback
(
async
(
notification
,
target
)
=>
{
setBusyKey
(
notification
.
name
);
try
{
if
(
!
isNotificationRead
(
notification
,
userInfo
?.
_id
))
{
await
markNotificationRead
(
notification
.
name
);
}
if
(
target
.
external
)
{
window
.
open
(
target
.
href
,
'_blank'
,
'noopener,noreferrer'
);
}
else
{
history
.
push
(
target
.
href
);
}
refresh
();
}
catch
(
operationError
)
{
toast
.
danger
(
operationError
?.
message
||
'无法打开通知关联流程'
,
{
timeout
:
4000
,
});
}
finally
{
setBusyKey
(
''
);
}
},
[
refresh
,
userInfo
?.
_id
],
);
return
(
<
div
className=
"rc-page-stack"
>
<
div
className=
"rc-page-toolbar rc-page-toolbar-responsive"
>
<
Tabs
className=
"w-fit max-w-full min-w-0"
selectedKey=
{
category
}
onSelectionChange=
{
(
key
)
=>
{
setCategory
(
String
(
key
));
setPage
(
0
);
}
}
>
<
Tabs
.
ListContainer
className=
"w-fit max-w-full min-w-0"
>
<
Tabs
.
List
aria
-
label=
"通知分类"
className=
"w-max min-w-max"
>
{
CATEGORY_OPTIONS
.
map
((
option
)
=>
(
<
Tabs
.
Tab
className=
"w-auto min-w-max flex-none whitespace-nowrap"
id=
{
option
.
key
}
key=
{
option
.
key
}
>
{
option
.
label
}
<
Tabs
.
Indicator
/>
</
Tabs
.
Tab
>
))
}
</
Tabs
.
List
>
</
Tabs
.
ListContainer
>
</
Tabs
>
<
Button
isDisabled=
{
loading
||
unreadTotal
===
0
}
isPending=
{
busyKey
===
'all'
}
variant=
"secondary"
onPress=
{
handleMarkAllRead
}
>
全部已读
</
Button
>
</
div
>
<
div
className=
"rc-page-toolbar rc-page-toolbar-responsive"
>
<
div
className=
"w-full min-w-0 sm:w-80 lg:w-96"
>
<
SearchField
aria
-
label=
"搜索通知内容"
fullWidth
value=
{
searchText
}
variant=
"primary"
onChange=
{
setSearchText
}
>
<
SearchField
.
Group
>
<
SearchField
.
SearchIcon
>
<
MagnifyingGlassIcon
size=
{
16
}
/>
</
SearchField
.
SearchIcon
>
<
SearchField
.
Input
placeholder=
"搜索通知内容"
/>
<
SearchField
.
ClearButton
aria
-
label=
"清空通知搜索"
>
<
Close
size=
{
12
}
/>
</
SearchField
.
ClearButton
>
</
SearchField
.
Group
>
</
SearchField
>
</
div
>
<
Tabs
className=
"w-fit max-w-full min-w-0"
selectedKey=
{
readStatus
}
onSelectionChange=
{
(
key
)
=>
{
setReadStatus
(
String
(
key
));
setPage
(
0
);
}
}
>
<
Tabs
.
ListContainer
className=
"w-fit max-w-full min-w-0"
>
<
Tabs
.
List
aria
-
label=
"通知状态"
className=
"w-max min-w-max"
>
{
READ_STATUS_OPTIONS
.
map
((
option
)
=>
(
<
Tabs
.
Tab
className=
"w-auto min-w-max flex-none whitespace-nowrap"
id=
{
option
.
key
}
key=
{
option
.
key
}
>
{
option
.
label
}
<
Tabs
.
Indicator
/>
</
Tabs
.
Tab
>
))
}
</
Tabs
.
List
>
</
Tabs
.
ListContainer
>
</
Tabs
>
</
div
>
{
error
?
(
<
Alert
status=
"danger"
>
<
Alert
.
Content
>
<
Alert
.
Description
>
{
error
}
</
Alert
.
Description
>
</
Alert
.
Content
>
<
Button
size=
"sm"
variant=
"ghost"
onPress=
{
refresh
}
>
重试
</
Button
>
</
Alert
>
)
:
null
}
{
loading
&&
notifications
.
length
===
0
?
(
<
div
className=
"flex min-h-64 items-center justify-center"
aria
-
label=
"通知加载中"
>
<
Spinner
size=
"sm"
/>
</
div
>
)
:
null
}
{
!
loading
&&
!
error
&&
notifications
.
length
===
0
?
(
<
EmptyState
className=
"min-h-64"
size=
"md"
>
<
EmptyState
.
Media
>
<
Bell
size=
{
22
}
/>
</
EmptyState
.
Media
>
<
EmptyState
.
Header
>
<
EmptyState
.
Title
>
{
category
===
'todo'
?
'暂无待办通知'
:
'暂无通知消息'
}
</
EmptyState
.
Title
>
<
EmptyState
.
Description
>
{
keyword
?
'没有找到匹配的通知,请调整搜索条件。'
:
readStatus
===
'unread'
?
'当前没有未读通知。'
:
'新的通知会显示在这里。'
}
</
EmptyState
.
Description
>
</
EmptyState
.
Header
>
</
EmptyState
>
)
:
null
}
{
notifications
.
length
>
0
?
(
<
div
className=
"flex min-w-0 flex-col gap-3"
aria
-
busy=
{
loading
}
>
{
notifications
.
map
((
notification
)
=>
(
<
NotificationItem
category=
{
category
}
currentUserId=
{
userInfo
?.
_id
}
isBusy=
{
busyKey
===
notification
.
name
}
key=
{
notification
.
name
||
notification
.
_id
}
notification=
{
notification
}
onAction=
{
handleAction
}
onMarkRead=
{
handleMarkRead
}
onOpenTarget=
{
handleOpenTarget
}
/>
))
}
</
div
>
)
:
null
}
{
!
error
&&
total
>
0
?
(
<
NotificationPagination
isDisabled=
{
loading
}
page=
{
page
}
total=
{
total
}
onPageChange=
{
setPage
}
/>
)
:
null
}
</
div
>
);
}
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