Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in / Register
Toggle navigation
M
model_analysis
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
model_analysis
Commits
082b0507
Commit
082b0507
authored
Aug 27, 2026
by
Andy-bubu
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: add Xinyuan notification center
parent
50992ed4
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
420 additions
and
11 deletions
+420
-11
.gitignore
.gitignore
+1
-0
.umirc.ts
.umirc.ts
+5
-0
ui-architecture.md
docs/ui-architecture.md
+6
-1
ui-migration-checklist.md
docs/ui-migration-checklist.md
+9
-2
xinyuan-design-system.md
docs/xinyuan-design-system.md
+8
-0
xinyuan-ui-audit.md
docs/xinyuan-ui-audit.md
+31
-6
app.js
src/constants/app.js
+1
-0
xinyuan.css
src/design-system/xinyuan.css
+15
-0
Layout.jsx
src/pages/project_manager_xy/Layout.jsx
+70
-2
model-monitor.business-flow.md
src/pages/project_manager_xy/model-monitor.business-flow.md
+1
-0
NotificationListPage.jsx
...project_manager_xy/notifications/NotificationListPage.jsx
+0
-0
notificationService.js
...s/project_manager_xy/notifications/notificationService.js
+183
-0
notifications.business-flow.md
...t_manager_xy/notifications/notifications.business-flow.md
+37
-0
record-api.md
src/services/record-api.md
+53
-0
No files found.
.gitignore
View file @
082b0507
...
...
@@ -10,6 +10,7 @@
/.umi-test
/dist
/.mfsu
/.playwright-cli/
.swc
.pnpm-store
.DS_Store
.umirc.ts
View file @
082b0507
...
...
@@ -7,6 +7,7 @@ function resolvePackageRoot(packageName: string) {
const
ROUTE_PATHS
=
{
login
:
'/login'
,
notifications
:
'/notifications'
,
modelOverview
:
'/models/overview'
,
models
:
'/models'
,
modelForwards
:
'/models/forwards'
,
...
...
@@ -59,6 +60,10 @@ export default defineConfig({
path
:
'/resource/booking'
,
redirect
:
ROUTE_PATHS
.
models
,
},
{
path
:
ROUTE_PATHS
.
notifications
,
component
:
'./project_manager_xy/notifications/NotificationListPage'
,
},
{
path
:
ROUTE_PATHS
.
modelOverview
,
component
:
'./project_manager_xy/ModelOverview'
,
...
...
docs/ui-architecture.md
View file @
082b0507
...
...
@@ -16,7 +16,10 @@
## 组件边界
-
`Layout.jsx`
:HeroUI Pro Sidebar/Navbar、主题切换、应用切换、退出。
-
`Layout.jsx`
:HeroUI Pro Sidebar/Navbar、主题切换、应用切换、退出;移动端
`Sidebar.Mobile`
使用 HeroUI Pro
`Sheet.Heading`
提供不可见但可访问的 Dialog 标题。
-
`Layout.jsx`
:从通知 service 读取真实未读数,监听跨页面状态事件并每 60 秒刷新;Navbar 只负责铃铛展示和路由跳转。
-
`notifications/notificationService.js`
:复用
`DataService`
的平台通知请求,负责 300ms 搜索参数、未读状态、已读事件、动态 action 和 base-aware 链接解析。
-
`notifications/NotificationListPage.jsx`
:HeroUI 原生消息/待办列表、搜索、状态 Tabs、Pagination、EmptyState 与 Toast,不持有模型监控业务状态。
-
`ApplicationSwitcherDialog.jsx`
:HeroUI Modal/Card/Surface/Avatar;从
`SiteContext`
的动态
`site.group`
构建应用分组,不在 UI 层写死分组数据。
-
`XinyuanModal.jsx`
:HeroUI
`Modal.Root > Backdrop > Container > Dialog`
的业务无关组合,集中实现 320–1216px 设计系统宽度梯度和 28px 标准内容 inset;扩展尺寸由官方
`lg`
Container 加共享最大宽度实现,不向 HeroUI v3 传递无效的
`xl/4xl`
size。圆角、背景、阴影和交互状态仍由 HeroUI 官方 Modal 样式负责。
-
`ModelMonitorShared.jsx`
:业务中性的 SearchField、DataGrid、Pagination、ActionBar、状态 Chip、动态字段组合。
...
...
@@ -27,6 +30,8 @@
页面状态和 service 调用保持迁移前的业务边界。组件只把 HeroUI 的
`onPress`
、
`onChange`
、
`onSelectionChange`
和
`selectedKeys`
映射回已有 handler;API 路径、metadata、Record/Subject 字段和审批状态不在 UI 层重定义。
通知是本项目唯一获准新增的跨应用能力。
`/notifications`
复用既有
`/notice/customer/*`
API;模型分析不引入
`subjectView`
、
`shortTitle`
、View 授权或材料中心附件适配代码。
## 删除边界
`src/catalyst-ui-kit`
、不可达资源预约模板及 ECharts 统计模板已从仓库移除。它们不在
`.umirc.ts`
的活动路由中,备份为
`/tmp/model-analysis-legacy-ui.iaSi6Y`
。
docs/ui-migration-checklist.md
View file @
082b0507
...
...
@@ -11,6 +11,9 @@
-
[
x
]
DataGrid 原生选择、真实溢出固定列、横向滚动、单一空态
-
[
x
]
Pagination 固定 10 条、最多五页窗口、无页大小选择
-
[
x
]
ActionBar 固定定位并对齐活动内容区
-
[
x
]
模型分析按跨应用白名单只接入消息通知,未引入
`shortTitle`
、View 权限或材料中心文件功能
-
[
x
]
新增
`/notifications`
,支持消息/待办、全部/未读、300ms 搜索、固定 10 条分页、单条/全部已读、动态 action 和关联跳转
-
[
x
]
Navbar 使用 HeroUI Badge.Anchor + Badge + Button + Tooltip,Bell 位于主题按钮左侧,Sidebar 无通知入口
-
[
x
]
弹窗使用 HeroUI Modal/AlertDialog;全部业务 Modal 接入共享 320–1216px 尺寸梯度,审批详情使用单一 secondary Surface
-
[
x
]
批量同步弹窗使用 HeroUI Pro Stepper/ListView、HeroUI ScrollShadow 和正确 Checkbox anatomy;移除手写灰色模型/批次分组及重复 Surface
-
[
x
]
KPI 使用 HeroUI Pro compound API 与 16px grid gap
...
...
@@ -32,6 +35,10 @@
-
[
x
]
1280x720 登录页浅色/深色
-
[
x
]
768x1024 登录页浅色/深色
-
[
x
]
390x844 登录页浅色/深色
-
[
]
鉴权后 Shell、退出、应用切换、Dropdown、Tabs、DataGrid、Pagination、ActionBar、Modal、KPI 实数据验收
-
[
x
]
目标与问题整改通知页在 1440x900、1280x720、768x1024、390x844 完成浅色/深色同视口对照
-
[
x
]
未读 0/1/99/100、消息/待办、全部/未读、搜索、单条/全部已读、动态 action、站内外跳转、HTML 纯文本、Pagination、EmptyState、Toast
-
[
x
]
Navbar 36px Bell/主题按钮、8px 间距、48/32/16px gutter;移动 offcanvas、Escape、无页面横向溢出
-
[
x
]
最终通知验收控制台 0 error / 0 warning
-
[
]
真实通知数据端到端验收:Andy 会话调用
`/notice/customer/count`
和
`/notice/customer/query`
返回 401
鉴权后验收必须使用真实登录凭据或既有有效 session;不得伪造 token、用户或业务数据。2026-08-21 检查现有 Playwright 会话,未发现模型分析的有效登录态,因此该项当前被真实凭据阻断
。
浏览器结构和交互验收所用通知响应仅存在于 Playwright route 内存,不写入仓库、不作为业务数据。真实接口 401 继续交由平台权限/环境修复,前端未伪造成功状态或绕过既有请求拦截器
。
docs/xinyuan-design-system.md
View file @
082b0507
...
...
@@ -15,6 +15,8 @@
-
Sidebar 为 HeroUI Pro offcanvas,宽 240px,折叠为零宽;单菜单组不显示 GroupLabel。
-
Sidebar Footer 使用直接
`Dropdown.Trigger`
、HeroUI Avatar 和真实退出回调。
-
Navbar 为透明 64px HeroUI Pro Navbar;Trigger 使用与基准一致的实心 16px Sidebar 图标。
-
Navbar 通知使用 HeroUI
`Badge.Anchor + Badge + Button + Tooltip`
,36px Ant Bell 位于主题按钮左侧;未读 0 隐藏、1-99 原值、100+ 显示
`99+`
,Sidebar 不重复展示通知入口。
-
Navbar Bell 与主题按钮进入同一
`rc-navbar-actions`
动作组,间距固定 8px;两者静止态均使用 HeroUI
`--default`
中性 Surface,Hover 使用
`--default-hover`
。
-
Navbar 与页面共用 48/32/16px 对称 gutter。
-
登录页使用居中 28rem Card、36px Logo、40px 字段、24px 字段节奏和 46px 提交按钮;验证码及原鉴权协议保留。
-
应用切换为 HeroUI Modal,宽 46rem、高 42rem,移动一列、
`sm`
起两列,36px identity well。
...
...
@@ -29,6 +31,12 @@
-
普通 Modal 保持 HeroUI 官方 24px 圆角和 Overlay shadow,统一使用 28px panel inset;Modal 内承担真实 L2 边界的
`Surface secondary`
使用基准项目一致的
`rounded-2xl`
(16px),不得保留大面积直角灰色块。关闭按钮直接使用无子节点的
`Modal.CloseTrigger`
,由 HeroUI 渲染原生 16px CloseIcon,禁止替换为受字体基线影响的第三方图标;关闭按钮、标题、正文和 Footer 对齐同一内容边界,标题到正文 12px、正文到 Footer 24px。批量同步使用紧凑 HeroUI Pro Stepper,年份工具区使用正确 Checkbox compound anatomy,批次选择使用单一 HeroUI Pro ListView
`primary`
原生多选和 HeroUI ScrollShadow,不使用页面自制灰色分组块或重复 Surface。
-
KPI:HeroUI Pro KPI compound API;四项使用
`.rc-kpi-grid`
16px gap,标题不附加说明图标。
-
当前活动业务没有图表;不得为视觉验收制造图表或测试业务数据。未来增加时只使用 HeroUI Pro chart 与 ChartTooltip。
-
通知中心使用 HeroUI Tabs、SearchField、Card、Avatar、Chip、Pagination、Toast 和 HeroUI Pro EmptyState;一级模块桌面间距 32px、移动端 24px,工具栏间距 16px;页面不设置组件背景、颜色、描边、圆角、阴影或状态皮肤。
## 跨应用功能范围
-
当前应用显示名为“模型分析”,不属于问题整改、审计项目管理、警示案例库、廉洁从业管理、廉洁风险防控、党风廉政建设六个完整功能应用。
-
本项目只实施全局消息通知。不得加入 Grid/Detail
`shortTitle`
、Subject View 权限或审计材料中心文件工作流,即使相邻基准仓库存在这些实现。
## 数据保护
...
...
docs/xinyuan-ui-audit.md
View file @
082b0507
# 模型分析 UI 审计
审计日期:2026-08-2
1
审计日期:2026-08-2
7
实现基准:已确认的“问题整改”应用。
...
...
@@ -12,8 +12,9 @@
-
实际解析:
`node_modules/.pnpm/@heroui-pro+react@1.0.0-beta.8/node_modules/@heroui-pro/react/package.json`
-
完整包文件:
`dist/index.js`
、
`dist/css/index.css`
均存在。
-
已验证子路径:
`sidebar`
、
`navbar`
、
`data-grid`
、
`drop-zone`
、
`action-bar`
、
`empty-state`
。
-
本轮复核:
`@heroui-pro/react@1.0.0-beta.8`
解析到当前仓库
`node_modules/.pnpm`
,
`dist/index.js`
与
`dist/css/index.css`
存在,六个 JS 子路径动态 import 全部成功;CSS export 可解析,裸 Node 不执行 CSS 文件。
-
依赖变化:更新
`package.json`
和
`pnpm-lock.yaml`
,新增
`pnpm-workspace.yaml`
;删除旧
`package-lock.json`
与本机 vendor TGZ。唯一锁文件为
`pnpm-lock.yaml`
。
-
依赖中不存在
`file:`
、相邻项目
`node_modules`
、
软链接或
`/Users/...`
路径
。
-
依赖中不存在
`file:`
、相邻项目
`node_modules`
、
手工软链接替代或
`/Users/...`
路径;
`node_modules`
内仅有 pnpm 正常生成的内容寻址链接
。
## 业务与路由
...
...
@@ -25,6 +26,7 @@
|
`/models/:modelId/batches`
| 模型批次 | HeroUI Pro DataGrid、Modal、ProgressBar |
|
`/models/:modelId/batches/:batchId/data`
| 批次数据 | DataGrid 原生选择、固定列、Pagination、ActionBar、Modal |
|
`/models/forwards`
| 转发审批 | Tabs、DataGrid、单一 secondary Surface 详情、4xl Modal |
|
`/notifications`
| 通知消息 | HeroUI Tabs/SearchField/Card/Pagination/Toast、HeroUI Pro EmptyState;消息/待办与全部/未读 |
历史
`/analysis/*`
与
`/resource/*`
仅重定向到上述活动路由。不可达的资源预约/统计模板以及
`src/catalyst-ui-kit`
已从仓库移除,备份位于
`/tmp/model-analysis-legacy-ui.iaSi6Y`
。
...
...
@@ -34,7 +36,16 @@
-
Auth wrapper 继续通过
`getUserInfo()`
合并用户;退出继续调用
`userLogout(username)`
,最终清理登录态并进入
`/login`
。
-
模型同步、批次同步、动态 Subject、隐藏记录、转发、填写和两级审批的 service、参数、校验、调用顺序与状态条件未修改。
-
批次数据每页固定 10 条;Pagination 最多显示五个连续页码,无页大小选择器。
-
没有生成测试数字、浏览器 fixture 或伪造业务数据。
-
仓库没有生成测试数字、浏览器 fixture 或伪造业务数据;验收 route 响应只存在于 Playwright 会话内存。
## 跨应用功能审计
-
应用统一显示名为“模型分析”,应用 route/name 为
`model_analysis`
,base/publicPath 为
`/model_analysis/`
;不在六个完整功能应用白名单内,因此只实施消息通知。
-
当前模型监控页面由固定 Subject 与自有 service 驱动,没有动态 Grid/Detail Subject View 链路;未发现可接入
`shortTitle`
或
`viewAuth`
的合法范围。
-
Andy 本轮登录返回
`type: admin`
且
`applications: []`
。通知能力沿用平台用户会话,不从其他应用复制或合并授权,也不读取
`integrity_risk_manager`
。
-
项目已有材料归档
`materialService.js`
,但本轮没有可编辑通用 file 字段或详情草稿事务接入需求;该业务保持不变,没有加入问题整改的材料中心文件流。
-
`DataService.js`
已存在
`queryUserNotice`
、
`readUserNotice`
、
`unReadNoticeCount`
、
`exacteActionFunction`
。本轮只增加通知业务 service、页面、Navbar 状态和路由,没有改变这些 API 函数的 URL、方法或请求体。
-
真实 Andy 会话下,当前环境的
`/notice/customer/count`
与
`/notice/customer/query`
返回 401;原请求拦截器按既有规则清理会话。该后端权限/环境阻塞保留并在浏览器验收中单独报告,不在前端伪造数据规避。
## 组件与样式审计
...
...
@@ -45,6 +56,7 @@
-
核心列表均使用 DataGrid;空态由
`renderEmptyState`
单一渲染。
-
页面视觉色、背景、描边、圆角、阴影及 HeroUI 内部状态覆盖扫描为零;次要文字直接使用 HeroUI
`Description`
,必填状态由
`Label isRequired`
管理。
-
`xinyuan.css`
仅保留批准的登录基准、Shell 几何、应用切换宽高和 KPI 布局;组件外观由权威主题及官方 variant 管理。
-
Navbar 通知与主题按钮复用批准的共享动作组:36x36px、8px 间距、HeroUI 中性 Surface;移动端 Sidebar Dialog 通过 HeroUI Pro
`Sheet.Heading`
获得可访问名称,复验控制台无警告。
-
当前业务需求没有活动图表;旧 ECharts 页面已移除,因此没有伪造图表或业务数据。
## 主题
...
...
@@ -57,6 +69,19 @@
## 浏览器证据
-
目标:
`http://localhost:8004/model_analysis/`
-
基准:
`http://localhost:8002/problem_rectification_new/`
-
同一 Playwright 会话内完成登录页 1440x900 基准比较;关键卡片、字段、验证码和提交按钮几何一致。
-
最终多视口、浅/深色与鉴权页面验收结果见
`docs/ui-migration-checklist.md`
。
-
基准:
`http://localhost:8005/problem_rectification_new/`
-
使用独立 Playwright 会话同时打开目标通知页和问题整改通知页,在 1440x900、1280x720、768x1024、390x844 的浅色与深色完成同视口对照。基准真实登录受验证码接口拒绝,因此仅在浏览器内存中拦截鉴权/通知请求用于结构比较,未写入仓库或充当业务数据。
-
桌面通知页的一级 Tabs、搜索、状态 Tabs、Card 和 Pagination 几何与基准一致;目标继续执行当前合同要求的 48/32/16px Shell gutter。所有视口
`scrollWidth === clientWidth`
。
-
Navbar Bell/主题按钮均为 36x36px、间距 8px;未读 0/1/99/100 分别验证为隐藏/
`1`
/
`99`
/
`99+`
,Badge 可访问名称保留真实计数。
-
验证消息/待办、全部/未读、300ms 搜索、单条已读、全部已读、动态 action、站内去 base 跳转、外链新窗口、HTML 纯文本、Pagination、EmptyState 与 Toast;移动端 offcanvas 可由 Escape 关闭,Dialog 标题为“模型分析导航”。最终目标控制台 0 error / 0 warning。
-
真实 Andy 会话仍被
`/notice/customer/count`
与
`/notice/customer/query`
的 401 阻断;因此前端实现和浏览器结构验收完成,但真实通知数据的端到端验收未完成。
## 最终扫描
-
`npm run build`
:成功,通知页异步 chunk 已生成。
-
`git diff --check`
:成功。
-
`bash /Users/liuchenlong/.codex/skills/xinyuan-design-system/scripts/audit_project.sh "$(git rev-parse --show-toplevel)"`
:退出码 0;脚本的 CommonJS 探测对六个 ESM Pro 子路径报告 warning,后续
`npm run verify:ui-runtime`
动态 import 全部成功。
-
审计脚本另保留 7 条当前分支既有启发式 warning:共享 Modal panel 探测、DataGrid typography token、DataGrid 主次字段层级、人员身份组合、可点击主字段、固定列 stacking rule、
`--rc-shadow-card`
基准比对。本轮通知范围未扩大修改这些既有模块。
-
旧 Catalyst/Headless/旧组件 import、可见原生 select/date/file、通知页私有视觉 class、HeroUI 内部 BEM/伪元素覆盖:扫描无命中。
-
`file:`
、相邻
`node_modules`
和
`/Users/...`
package/lock 依赖:扫描无命中。
-
`pnpm list @heroui-pro/react @heroui/react @heroui/styles @react-aria/utils react react-dom react-aria-components --depth 1`
:应用运行时为 React/ReactDOM 19.2.7、HeroUI 3.2.2、React Aria Components 1.20.0。
src/constants/app.js
View file @
082b0507
...
...
@@ -38,6 +38,7 @@ export function stripAppBase(pathname = '/') {
}
export
const
ROUTE_TITLES
=
[
{
match
:
/^
\/
notifications$/
,
title
:
'通知消息'
},
{
match
:
/^
\/
models
\/[^/]
+
\/
batches
\/[^/]
+
\/
data$/
,
title
:
'批次数据'
},
{
match
:
/^
\/
models
\/[^/]
+
\/
batches$/
,
title
:
'模型批次'
},
{
match
:
/^
\/
models
\/
forwards$/
,
title
:
'转发审批'
},
...
...
src/design-system/xinyuan.css
View file @
082b0507
...
...
@@ -319,6 +319,21 @@ input.login-input:-webkit-autofill {
padding-inline
:
var
(
--rc-shell-inline-gutter
);
}
.rc-navbar-actions
{
display
:
flex
;
align-items
:
center
;
gap
:
8px
;
}
.rc-navbar-icon-button.button
{
background
:
var
(
--default
);
}
.rc-navbar-icon-button.button
:hover
,
.rc-navbar-icon-button.button
[
data-hovered
=
'true'
]
{
background
:
var
(
--default-hover
);
}
.xy-navbar
.sidebar__trigger
,
.xy-navbar
[
data-slot
=
'sidebar-trigger'
]
{
width
:
var
(
--rc-navbar-trigger-size
);
...
...
src/pages/project_manager_xy/Layout.jsx
View file @
082b0507
...
...
@@ -13,6 +13,7 @@ import { UserContext } from '@/wrapper/Auth';
import
{
SiteContext
}
from
'@/wrapper/Site'
;
import
{
AppstoreOutlined
,
BellOutlined
,
DatabaseOutlined
,
LogoutOutlined
,
MoonOutlined
,
...
...
@@ -20,10 +21,15 @@ import {
SunOutlined
,
UnorderedListOutlined
,
}
from
'@ant-design/icons'
;
import
{
Avatar
,
B
utton
,
Description
,
Dropdown
,
Label
,
Tooltip
}
from
'@heroui/react'
;
import
{
Avatar
,
B
adge
,
Button
,
Description
,
Dropdown
,
Label
,
Toast
,
Tooltip
}
from
'@heroui/react'
;
import
{
Navbar
}
from
'@heroui-pro/react/navbar'
;
import
{
Sheet
}
from
'@heroui-pro/react/sheet'
;
import
{
Sidebar
}
from
'@heroui-pro/react/sidebar'
;
import
{
useContext
,
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
{
loadUnreadNotificationCount
,
NOTIFICATION_STATE_EVENT
,
}
from
'@/pages/project_manager_xy/notifications/notificationService'
;
import
{
useCallback
,
useContext
,
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
{
Outlet
,
history
,
useLocation
}
from
'umi'
;
const
MENU_ITEMS
=
[
...
...
@@ -140,6 +146,7 @@ export default function ProjectManagerLayout() {
const
siteInfo
=
useContext
(
SiteContext
);
const
[
theme
,
setTheme
]
=
useState
(
getInitialTheme
);
const
[
isApplicationDialogOpen
,
setIsApplicationDialogOpen
]
=
useState
(
false
);
const
[
unreadNotificationCount
,
setUnreadNotificationCount
]
=
useState
(
0
);
const
pageTitle
=
useMemo
(()
=>
getRouteTitle
(
location
.
pathname
),
[
location
.
pathname
]);
const
isDark
=
theme
===
'dark'
;
...
...
@@ -152,6 +159,31 @@ export default function ProjectManagerLayout() {
document
.
title
=
`
${
pageTitle
}
-
${
APP_DISPLAY_NAME
}
`
;
},
[
pageTitle
]);
const
refreshUnreadNotificationCount
=
useCallback
(
async
()
=>
{
try
{
setUnreadNotificationCount
(
await
loadUnreadNotificationCount
());
}
catch
(
error
)
{
setUnreadNotificationCount
(
0
);
}
},
[]);
useEffect
(()
=>
{
refreshUnreadNotificationCount
();
window
.
addEventListener
(
NOTIFICATION_STATE_EVENT
,
refreshUnreadNotificationCount
,
);
const
timer
=
window
.
setInterval
(
refreshUnreadNotificationCount
,
60000
);
return
()
=>
{
window
.
removeEventListener
(
NOTIFICATION_STATE_EVENT
,
refreshUnreadNotificationCount
,
);
window
.
clearInterval
(
timer
);
};
},
[
refreshUnreadNotificationCount
]);
const
themeLabel
=
isDark
?
'切换至浅色模式'
:
'切换至深色模式'
;
return
(
...
...
@@ -168,6 +200,9 @@ export default function ProjectManagerLayout() {
/>
</
Sidebar
>
<
Sidebar
.
Mobile
className=
"xy-sidebar"
>
<
Sheet
.
Heading
className=
"sr-only"
>
{
APP_DISPLAY_NAME
}
导航
</
Sheet
.
Heading
>
<
SidebarContents
pathname=
{
location
.
pathname
}
userInfo=
{
userInfo
}
...
...
@@ -180,6 +215,37 @@ export default function ProjectManagerLayout() {
<
Sidebar
.
Trigger
aria
-
label=
"切换侧栏"
className=
"rc-navbar-trigger"
><
SidebarPanelIcon
/></
Sidebar
.
Trigger
>
<
h1
className=
"xy-navbar-title"
>
{
pageTitle
}
</
h1
>
<
Navbar
.
Spacer
/>
<
div
className=
"rc-navbar-actions"
>
<
Tooltip
delay=
{
0
}
>
<
Badge
.
Anchor
>
<
Button
isIconOnly
aria
-
label=
{
unreadNotificationCount
>
0
?
`通知消息,${unreadNotificationCount}条未读`
:
'通知消息'
}
className=
"xy-icon-button rc-navbar-icon-button"
size=
"sm"
variant=
"ghost"
onPress=
{
()
=>
history
.
push
(
'/notifications'
)
}
>
<
BellOutlined
/>
</
Button
>
{
unreadNotificationCount
>
0
?
(
<
Badge
aria
-
label=
{
`${unreadNotificationCount}条未读通知`
}
color=
"danger"
size=
"sm"
>
{
unreadNotificationCount
>
99
?
'99+'
:
unreadNotificationCount
}
</
Badge
>
)
:
null
}
</
Badge
.
Anchor
>
<
Tooltip
.
Content
>
通知消息
</
Tooltip
.
Content
>
</
Tooltip
>
<
Tooltip
delay=
{
0
}
>
<
Button
isIconOnly
...
...
@@ -194,6 +260,7 @@ export default function ProjectManagerLayout() {
</
Button
>
<
Tooltip
.
Content
>
{
themeLabel
}
</
Tooltip
.
Content
>
</
Tooltip
>
</
div
>
</
Navbar
.
Header
>
</
Navbar
>
<
div
className=
"xy-shell-scroll"
>
...
...
@@ -209,6 +276,7 @@ export default function ProjectManagerLayout() {
userInfo=
{
userInfo
}
onOpenChange=
{
setIsApplicationDialogOpen
}
/>
<
Toast
.
Provider
maxVisibleToasts=
{
3
}
placement=
"top"
/>
</
Sidebar
.
Provider
>
);
}
src/pages/project_manager_xy/model-monitor.business-flow.md
View file @
082b0507
...
...
@@ -25,6 +25,7 @@
-
侧栏应用切换入口打开 HeroUI 应用切换弹窗,不改变当前模型监控路由和登录状态。
-
可访问应用继续由
`GET /application/info`
与当前登录用户的
`applications`
合并得到,保留原排序、回退和跳转规则。
-
应用分组从
`GET /info`
返回的
`site.group`
动态读取;每个分组通过
`applications`
中的应用
`name`
关联,前端不写死分组名称或成员。
-
全局通知入口位于 Navbar 主题按钮左侧,点击进入
`/notifications`
;Sidebar 不增加通知菜单。通知中心属于平台能力,不改变模型、批次、批次数据或转发审批状态流,详细协议见
`notifications/notifications.business-flow.md`
。
-
没有动态分组配置时展示“全部应用”;存在动态分组时,未被任何分组引用的可访问应用展示在“其他应用”中,空分组不渲染。
## 3. 外部数据源契约
...
...
src/pages/project_manager_xy/notifications/NotificationListPage.jsx
0 → 100644
View file @
082b0507
This diff is collapsed.
Click to expand it.
src/pages/project_manager_xy/notifications/notificationService.js
0 → 100644
View file @
082b0507
import
{
exacteActionFunction
,
queryUserNotice
,
readUserNotice
,
unReadNoticeCount
,
}
from
'@/services/DataService'
;
import
{
APP_BASE_PATH
}
from
'@/constants/app'
;
export
const
NOTIFICATION_PAGE_SIZE
=
10
;
export
const
NOTIFICATION_STATE_EVENT
=
'model-analysis: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
),
page
:
Number
(
response
?.
current_page
??
page
)
||
0
,
pageSize
:
Number
(
response
?.
page_size
??
pageSize
)
||
pageSize
,
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
escapedBasePath
=
APP_BASE_PATH
.
replace
(
/
[
.*+?^${}()|[
\]\\]
/g
,
'
\\
$&'
);
const
match
=
content
.
match
(
new
RegExp
(
`https?:\\/\\/[^\\s<>"']+|
${
escapedBasePath
}
\\/[^\\s<>"']+`
),
);
return
trimTarget
(
match
?.[
0
]);
}
export
function
resolveNotificationTarget
(
target
)
{
const
normalizedTarget
=
trimTarget
(
target
);
if
(
!
normalizedTarget
)
return
null
;
if
(
/^https
?
:
\/\/
/i
.
test
(
normalizedTarget
))
{
try
{
const
url
=
new
URL
(
normalizedTarget
);
if
(
url
.
pathname
===
APP_BASE_PATH
||
url
.
pathname
.
startsWith
(
`
${
APP_BASE_PATH
}
/`
))
{
return
resolveNotificationTarget
(
`
${
url
.
pathname
}${
url
.
search
}${
url
.
hash
}
`
);
}
return
{
href
:
url
.
toString
(),
external
:
true
};
}
catch
(
error
)
{
return
null
;
}
}
if
(
normalizedTarget
!==
APP_BASE_PATH
&&
!
normalizedTarget
.
startsWith
(
`
${
APP_BASE_PATH
}
/`
)
)
{
return
normalizedTarget
.
startsWith
(
'/'
)
?
{
href
:
normalizedTarget
,
external
:
true
}
:
null
;
}
return
{
href
:
normalizedTarget
.
slice
(
APP_BASE_PATH
.
length
)
||
'/'
,
external
:
false
,
};
}
export
function
notifyNotificationStateChanged
()
{
if
(
typeof
window
!==
'undefined'
)
{
window
.
dispatchEvent
(
new
CustomEvent
(
NOTIFICATION_STATE_EVENT
));
}
}
src/pages/project_manager_xy/notifications/notifications.business-flow.md
0 → 100644
View file @
082b0507
# 模型分析通知中心业务流程
## 功能入口
-
路由:
`/notifications`
,部署后完整地址为
`/model_analysis/notifications`
。
-
Navbar 在主题按钮左侧显示 36px 通知铃铛;Sidebar 不提供重复入口。
-
Bell 与主题按钮间距为 8px;移动端 Sidebar Dialog 具有“模型分析导航”可访问标题。
-
未读数量通过
`GET /notice/customer/count`
获取,0 隐藏、1-99 显示真实值、100 及以上显示
`99+`
;页面操作完成后通过
`model-analysis:notification-state`
事件刷新,并每 60 秒自动刷新。
## 列表与筛选
1.
页面通过
`POST /notice/customer/query`
查询通知,每页固定 10 条。
2.
一级分类为
`message`
(消息)和
`todo`
(待办)。
3.
状态为全部或未读;未读向服务端发送
`status: "unread"`
,不得只过滤当前页。
4.
搜索输入延迟 300ms,将转义后的内容写入
`filter.content.$regex`
,并回到第 1 页。
5.
通知正文以普通文本渲染,不解析或执行服务端 HTML。
## 已读与待办状态
-
单条“标为已读”调用
`PUT /notice/customer/read`
并传通知
`name`
。
-
“全部已读”调用同一平台接口但不传具体名称。
-
待办按钮按通知返回的
`actions`
动态渲染;执行时调用
`PUT /notice/customer/:action/action/:methodName`
,请求体只包含
`{ notice_id }`
。
-
操作成功后显示 HeroUI Toast,重新查询列表和真实未读数量;失败保留当前页面状态并显示错误。
## 关联流程跳转
-
优先读取通知
`settings.url/settings.link/data.url/data.link`
,无配置时才从纯文本内容识别 URL。
-
`/model_analysis`
内的链接只去除当前应用 base 后交给 Umi 路由。
-
其他应用路径和外部 HTTP(S) 地址在新窗口打开,并使用
`noopener,noreferrer`
。
-
打开关联流程前先按现有协议标记当前通知已读;不得执行通知 HTML 或脚本。
## 异常与限制
-
所有请求继续使用现有登录 Token、请求拦截器和退出处理,不新增通知专用鉴权。
-
接口 401/403、网络失败或动态 action 配置缺失时不得伪造成功状态或未读数量。
-
通知中心不读取或修改模型监控 Subject、Record、审批字段及用户
`applications`
授权结构。
-
本应用不在五项完整功能白名单中,不实现
`shortTitle`
、Subject View 权限或审计材料中心文件工作流。
src/services/record-api.md
View file @
082b0507
...
...
@@ -231,3 +231,56 @@ loadSourceBatchData(sourceModel: SourceModel, sourceBatch: SourceBatch): Promise
-
同步部分失败时保留已成功记录,并把批次标记为
`partial`
。
-
同步校验失败时不得开始写入动态数据 Record。
-
页面必须分别呈现 loading、success、error 状态;已创建批次不提供重试入口。
## 7. 通知中心接口
通知中心复用平台原接口,不新增模型分析私有通知协议。
### 7.1 查询通知
`POST /notice/customer/query`
```
json
{
"page"
:
0
,
"page_size"
:
10
,
"filter"
:
{
"category"
:
"message"
,
"content"
:
{
"$regex"
:
"转义后的搜索词"
,
"$options"
:
"i"
}
},
"status"
:
"unread"
}
```
-
`category`
取平台值
`message`
或
`todo`
。
-
全部状态传
`status: null`
;未读状态传
`status: "unread"`
,由服务端过滤全部记录。
-
搜索仅写入
`filter.content`
,并在发送前转义正则字符。
-
分页固定每页 10 条,页码从 0 开始。
### 7.2 未读数量
`GET /notice/customer/count`
Navbar 读取真实未读数;0 不显示 Badge,1-99 显示原值,100 及以上显示
`99+`
。
### 7.3 标记已读
`PUT /notice/customer/read`
-
单条已读请求体为
`{ "name": "通知名称" }`
。
-
全部已读沿用平台协议,调用同一接口且不提供具体通知名称。
### 7.4 执行待办动作
`PUT /notice/customer/:action/action/:methodName`
```
json
{
"notice_id"
:
"通知 _id"
}
```
`:action`
与
`:methodName`
必须来自通知返回的动态
`actions`
配置,前端不得写死流程动作。
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