Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in / Register
Toggle navigation
A
ai-chat-xy
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
ai-chat-xy
Commits
aa343ada
Commit
aa343ada
authored
Aug 27, 2026
by
Andy-bubu
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: add notification center
parent
1da38cbc
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
13 changed files
with
459 additions
and
3 deletions
+459
-3
.umirc.ts
.umirc.ts
+5
-0
ui-architecture.md
docs/ui-architecture.md
+22
-0
ui-migration-checklist.md
docs/ui-migration-checklist.md
+16
-0
xinyuan-design-system.md
docs/xinyuan-design-system.md
+8
-0
xinyuan-ui-audit.md
docs/xinyuan-ui-audit.md
+26
-0
AppIcons.jsx
src/components/AppIcons.jsx
+6
-0
AppPagination.jsx
src/components/AppPagination.jsx
+64
-0
AppShell.jsx
src/components/AppShell.jsx
+45
-1
DashboardNavbar.jsx
src/components/DashboardNavbar.jsx
+37
-2
application.js
src/config/application.js
+5
-0
notificationService.js
src/features/notifications/notificationService.js
+193
-0
index.jsx
src/pages/Notifications/index.jsx
+0
-0
DataService.js
src/services/DataService.js
+32
-0
No files found.
.umirc.ts
View file @
aa343ada
...
@@ -40,6 +40,11 @@ export default defineConfig({
...
@@ -40,6 +40,11 @@ export default defineConfig({
// redirect: '/chat',
// redirect: '/chat',
component
:
'./Home'
,
component
:
'./Home'
,
},
},
{
name
:
'通知消息'
,
path
:
'/notifications'
,
component
:
'./Notifications'
,
},
// {
// {
// name: '聊天',
// name: '聊天',
// path: '/chat',
// path: '/chat',
...
...
docs/ui-architecture.md
View file @
aa343ada
...
@@ -78,3 +78,25 @@ Umi route /ai_chat/
...
@@ -78,3 +78,25 @@ Umi route /ai_chat/
## 验收边界
## 验收边界
业务保护优先于制造测试状态。当前账号没有真实会话,且没有提供审计知识库 Excel,因此不会创建测试会话或伪造风险点数据。会话行操作和解析后 DataGrid 仍需在真实数据可用时完成最终操作验收;这两个项目在检查清单中保持进行中,不作为“迁移完成”宣称。
业务保护优先于制造测试状态。当前账号没有真实会话,且没有提供审计知识库 Excel,因此不会创建测试会话或伪造风险点数据。会话行操作和解析后 DataGrid 仍需在真实数据可用时完成最终操作验收;这两个项目在检查清单中保持进行中,不作为“迁移完成”宣称。
## 通知架构
```
text
Umi /ai_chat/notifications
-> existing current_user authentication and AppShell
-> AppShell GET /api/notice/customer/count
-> DashboardNavbar HeroUI Badge
-> notification page
-> message/todo and all/unread Tabs
-> debounced SearchField
-> plain notification Cards and dynamic actions
-> HeroUI compound Pagination
-> PUT /api/notice/customer/read
-> notification state event -> list/count refresh
-> PUT /api/notice/customer/{action}/action/{method}
```
-
通知计数由
`AppShell`
所有,页面不复制 Navbar 状态;通知状态事件和 60 秒轮询复用同一刷新函数。
-
通知页沿用现有
`current_user`
登录协议、AppShell、退出和应用切换;不读取其他应用授权来决定通知可见性。
-
内部目标只接受当前
`/ai_chat`
base 并转为 Umi 内部路径;其他绝对路径和外部 HTTP(S) 目标作为外链处理。
-
本应用只实施通知能力;
`shortTitle`
、Subject/View 权限与材料中心文件流程不进入架构。
docs/ui-migration-checklist.md
View file @
aa343ada
...
@@ -77,3 +77,19 @@
...
@@ -77,3 +77,19 @@
-
[
x
]
HeroUI Pro 指定子路径 ESM 导入检查通过
-
[
x
]
HeroUI Pro 指定子路径 ESM 导入检查通过
-
[
x
]
授权值扫描为零
-
[
x
]
授权值扫描为零
-
[
x
]
未提交、未推送、未合并
-
[
x
]
未提交、未推送、未合并
## 消息通知增量
-
[
x
]
确认应用为“仅消息通知”范围;功能 1、2、3、5 不适用且未实施
-
[
x
]
审计
`current_user`
、
`applications[]`
、basePath、Shell 数据流、通知接口和现有回调
-
[
x
]
新增独立
`/notifications`
路由,保留
`/ai_chat/`
base
-
[
x
]
Navbar 使用 HeroUI Badge.Anchor、Badge、Button、Tooltip;36px Bell 位于主题按钮左侧
-
[
x
]
Sidebar 无通知入口,未读 0/1/99/100 显示规则通过
-
[
x
]
消息/待办、全部/未读、搜索、防抖、分页、单条/全部已读通过
-
[
x
]
动态 action 路径及
`{notice_id}`
请求体通过
-
[
x
]
站内链接去除自身 base;其他应用与外部链接新窗口打开
-
[
x
]
通知内容保持纯文本,不执行 HTML
-
[
x
]
1440x900、1280x720、768x1024、390x844 浅色/深色通过
-
[
x
]
与“问题整改”同视口对照通过;目标通知页控制台 0 error / 0 warning
-
[
x
]
浏览器内存夹具未进入源码、文档或业务数据
-
[
x
]
Andy 真实验证码登录、空通知接口、Sidebar 真实退出通过
docs/xinyuan-design-system.md
View file @
aa343ada
...
@@ -70,3 +70,11 @@ Inter 字体文件独立存放于 `public/fonts/`,并由 `.umirc.ts` 注入稳
...
@@ -70,3 +70,11 @@ Inter 字体文件独立存放于 `public/fonts/`,并由 `.umirc.ts` 注入稳
-
页面、业务模块和共享 JSX 的私有视觉 utility 扫描为零;共享外观仅位于权威主题、批准 Tokens 和共享 Shell 设计系统层。
-
页面、业务模块和共享 JSX 的私有视觉 utility 扫描为零;共享外观仅位于权威主题、批准 Tokens 和共享 Shell 设计系统层。
-
Ant Design、Ant Design Pro、Ant Design X、Ant X Markdown、antd-style、Headless UI 和 Catalyst UI Kit 活动引用为零;后续开发必须持续保持为零。
-
Ant Design、Ant Design Pro、Ant Design X、Ant X Markdown、antd-style、Headless UI 和 Catalyst UI Kit 活动引用为零;后续开发必须持续保持为零。
-
当前业务没有真实 Tabs、筛选、KPI、图表或分页;不得为补齐目录而新增非业务模块。
-
当前业务没有真实 Tabs、筛选、KPI、图表或分页;不得为补齐目录而新增非业务模块。
## 消息通知
-
`AI 应用`
不在六个完整功能应用白名单内,本次只接入消息通知;不得加入
`shortTitle`
、Subject/View 权限或审计材料中心能力。
-
Navbar 通知入口直接使用 HeroUI
`Badge.Anchor + Badge + Button + Tooltip`
,36px Bell 位于主题按钮左侧;Sidebar 不增加通知入口。
-
`/notifications`
使用 HeroUI Tabs、SearchField、Card、Chip、Button、Toast 和 HeroUI Pro EmptyState;通知分页使用 HeroUI compound Pagination。
-
徽标在未读数为 0 时隐藏,1–99 显示真实值,超过 99 显示
`99+`
;按钮可访问名称始终保留真实未读数。
-
通知正文只作为 React 文本渲染,不执行 HTML。站内链接只去除自身
`/ai_chat`
base,其他应用路径与外部 HTTP(S) 链接均新窗口打开。
docs/xinyuan-ui-audit.md
View file @
aa343ada
...
@@ -109,3 +109,29 @@
...
@@ -109,3 +109,29 @@
-
官方安装将 React 升至 19.2.8,而 Umi 的部分开发依赖仍声明/携带 React 18 运行图,需要在最终依赖树与浏览器 Context 检查中确认运行时唯一性。
-
官方安装将 React 升至 19.2.8,而 Umi 的部分开发依赖仍声明/携带 React 18 运行图,需要在最终依赖树与浏览器 Context 检查中确认运行时唯一性。
-
活动 UI 文件合计超过 7,000 行,大量视觉与业务逻辑同文件;迁移必须逐个保留回调,不能用整页演示代码替换。
-
活动 UI 文件合计超过 7,000 行,大量视觉与业务逻辑同文件;迁移必须逐个保留回调,不能用整页演示代码替换。
-
应用不是典型表格后台。Shell、登录、应用切换必须精确复刻基准;不存在的 Tabs、KPI、图表和列表业务不得伪造。
-
应用不是典型表格后台。Shell、登录、应用切换必须精确复刻基准;不存在的 Tabs、KPI、图表和列表业务不得伪造。
## 2026-08-27 消息通知审计
-
应用范围:
`AI 应用`
不属于完整功能白名单,仅实施功能 4“消息通知”;功能 1、2、3、5 明确不适用。
-
路由 base/publicPath 为
`/ai_chat/`
;新增独立应用内路由
`/notifications`
,浏览器地址为
`/ai_chat/notifications`
。
-
登录状态沿用
`localStorage.current_user`
,Navbar 与通知页不读取、复制或合并其他应用的
`user.applications[]`
权限。
-
原 Shell 的页面操作由
`useNavbarActions`
注入;通知计数提升到
`AppShell`
,登录用户变化、通知状态事件及 60 秒定时器触发刷新。
-
Sidebar 保留原会话和用户数据流,不增加通知入口。Navbar 的通知入口位于页面操作与主题按钮之间。
-
通知接口沿用平台协议:
`POST /notice/customer/query`
、
`PUT /notice/customer/read`
、
`GET /notice/customer/count`
、
`PUT /notice/customer/{action}/action/{method}`
;目标项目请求层继续统一添加
`/api`
前缀。
-
查询固定每页 10 条,搜索 300ms 防抖并转义正则,未读筛选发送
`status: "unread"`
;动态 action 只提交
`{notice_id}`
。
-
现有目标项目无 Subject/View、record-workbench、RecordDetailDrawer、材料中心文件字段或通知模块;未新增相关能力,也未改变登录、会话和 AI 业务回调。
### 浏览器验收
目标通知页与运行中的“问题整改”通知页同时打开并按相同视口对照。1440x900 下关键坐标一致:Sidebar 240px、Navbar 64px、页面左右 gutter 48px、工具栏和列表宽度一致,“进入流程”按钮均为带图标的 100px。
| 视口 | 浅色 | 深色 | 结果 |
| --- | --- | --- | --- |
| 1440x900 | 通过 | 通过 | 桌面 Sidebar、Navbar、Tabs、筛选、列表、分页对齐 |
| 1280x720 | 通过 | 通过 | 无横向溢出,工具栏与卡片布局稳定 |
| 768x1024 | 通过 | 通过 | Sidebar offcanvas,32px gutter,无横向溢出 |
| 390x844 | 通过 | 通过 | Sidebar offcanvas,16px gutter,工具栏换行,无横向溢出 |
实测覆盖未读 0/1/99/100、单条已读、全部已读、消息/待办、全部/未读、搜索防抖与正则转义、分页、动态 action、站内外跳转。通知页验收会话控制台为 0 error / 0 warning;夹具只存在于浏览器内存,未写入源码或业务数据。
使用提供的 Andy 账号完成真实验证码登录,
`/api/auth/login`
、
`/api/user/info`
、通知查询与未读计数接口均返回 200;该账号当前通知列表为空。随后从 Sidebar 用户菜单真实退出,
`current_user`
已清除并回到登录界面,控制台保持 0 error / 0 warning。
src/components/AppIcons.jsx
View file @
aa343ada
import
AppstoreOutlined
from
'@ant-design/icons/AppstoreOutlined'
;
import
AppstoreOutlined
from
'@ant-design/icons/AppstoreOutlined'
;
import
BellOutlined
from
'@ant-design/icons/BellOutlined'
;
import
CloseOutlined
from
'@ant-design/icons/CloseOutlined'
;
import
CloseOutlined
from
'@ant-design/icons/CloseOutlined'
;
import
CommentOutlined
from
'@ant-design/icons/CommentOutlined'
;
import
CommentOutlined
from
'@ant-design/icons/CommentOutlined'
;
import
DeleteOutlined
from
'@ant-design/icons/DeleteOutlined'
;
import
DeleteOutlined
from
'@ant-design/icons/DeleteOutlined'
;
import
EditOutlined
from
'@ant-design/icons/EditOutlined'
;
import
EditOutlined
from
'@ant-design/icons/EditOutlined'
;
import
ExportOutlined
from
'@ant-design/icons/ExportOutlined'
;
import
LogoutOutlined
from
'@ant-design/icons/LogoutOutlined'
;
import
LogoutOutlined
from
'@ant-design/icons/LogoutOutlined'
;
import
MoonOutlined
from
'@ant-design/icons/MoonOutlined'
;
import
MoonOutlined
from
'@ant-design/icons/MoonOutlined'
;
import
MoreOutlined
from
'@ant-design/icons/MoreOutlined'
;
import
MoreOutlined
from
'@ant-design/icons/MoreOutlined'
;
import
PlusOutlined
from
'@ant-design/icons/PlusOutlined'
;
import
PlusOutlined
from
'@ant-design/icons/PlusOutlined'
;
import
PushpinOutlined
from
'@ant-design/icons/PushpinOutlined'
;
import
PushpinOutlined
from
'@ant-design/icons/PushpinOutlined'
;
import
SearchOutlined
from
'@ant-design/icons/SearchOutlined'
;
import
SunOutlined
from
'@ant-design/icons/SunOutlined'
;
import
SunOutlined
from
'@ant-design/icons/SunOutlined'
;
import
WarningOutlined
from
'@ant-design/icons/WarningOutlined'
;
import
WarningOutlined
from
'@ant-design/icons/WarningOutlined'
;
...
@@ -24,16 +27,19 @@ function withSize(Icon) {
...
@@ -24,16 +27,19 @@ function withSize(Icon) {
}
}
export
const
AppGrid
=
withSize
(
AppstoreOutlined
);
export
const
AppGrid
=
withSize
(
AppstoreOutlined
);
export
const
Bell
=
withSize
(
BellOutlined
);
export
const
Close
=
withSize
(
CloseOutlined
);
export
const
Close
=
withSize
(
CloseOutlined
);
export
const
Comment
=
withSize
(
CommentOutlined
);
export
const
Comment
=
withSize
(
CommentOutlined
);
export
const
Delete
=
withSize
(
DeleteOutlined
);
export
const
Delete
=
withSize
(
DeleteOutlined
);
export
const
Edit
=
withSize
(
EditOutlined
);
export
const
Edit
=
withSize
(
EditOutlined
);
export
const
OpenExternal
=
withSize
(
ExportOutlined
);
export
const
ExclamationTriangleIcon
=
withSize
(
WarningOutlined
);
export
const
ExclamationTriangleIcon
=
withSize
(
WarningOutlined
);
export
const
LogOut
=
withSize
(
LogoutOutlined
);
export
const
LogOut
=
withSize
(
LogoutOutlined
);
export
const
Moon
=
withSize
(
MoonOutlined
);
export
const
Moon
=
withSize
(
MoonOutlined
);
export
const
More
=
withSize
(
MoreOutlined
);
export
const
More
=
withSize
(
MoreOutlined
);
export
const
Plus
=
withSize
(
PlusOutlined
);
export
const
Plus
=
withSize
(
PlusOutlined
);
export
const
Pin
=
withSize
(
PushpinOutlined
);
export
const
Pin
=
withSize
(
PushpinOutlined
);
export
const
Search
=
withSize
(
SearchOutlined
);
export
const
Sun
=
withSize
(
SunOutlined
);
export
const
Sun
=
withSize
(
SunOutlined
);
export
function
SidebarPanel
({
size
=
16
,
style
,
...
props
})
{
export
function
SidebarPanel
({
size
=
16
,
style
,
...
props
})
{
...
...
src/components/AppPagination.jsx
0 → 100644
View file @
aa343ada
import
{
Pagination
}
from
'@heroui/react'
;
export
default
function
AppPagination
({
isDisabled
=
false
,
itemLabel
=
'条'
,
page
,
pageSize
,
total
,
onPageChange
,
})
{
const
totalPages
=
Math
.
max
(
1
,
Math
.
ceil
(
total
/
pageSize
));
const
pageWindowStart
=
Math
.
max
(
0
,
Math
.
min
(
page
-
2
,
totalPages
-
5
));
const
pageWindowEnd
=
total
?
Math
.
min
(
totalPages
,
pageWindowStart
+
5
)
:
pageWindowStart
;
return
(
<
div
className=
"flex min-w-0 flex-wrap items-center justify-between gap-3"
>
<
Pagination
aria
-
label=
"分页"
size=
"sm"
>
<
Pagination
.
Summary
>
共
{
total
}
{
itemLabel
}
,第
{
total
?
page
+
1
:
0
}
/
{
' '
}
{
total
?
totalPages
:
0
}
页
</
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
:
pageWindowEnd
-
pageWindowStart
},
(
_
,
index
)
=>
{
const
pageIndex
=
pageWindowStart
+
index
;
return
(
<
Pagination
.
Item
key=
{
pageIndex
}
>
<
Pagination
.
Link
isActive=
{
pageIndex
===
page
}
isDisabled=
{
isDisabled
}
onPress=
{
()
=>
onPageChange
(
pageIndex
)
}
>
{
pageIndex
+
1
}
</
Pagination
.
Link
>
</
Pagination
.
Item
>
);
},
)
}
<
Pagination
.
Item
>
<
Pagination
.
Next
isDisabled=
{
isDisabled
||
page
+
1
>=
totalPages
||
total
===
0
}
onPress=
{
()
=>
onPageChange
(
page
+
1
)
}
>
<
span
>
下一页
</
span
>
<
Pagination
.
NextIcon
/>
</
Pagination
.
Next
>
</
Pagination
.
Item
>
</
Pagination
.
Content
>
</
Pagination
>
</
div
>
);
}
src/components/AppShell.jsx
View file @
aa343ada
import
{
THEME_STORAGE_KEY
}
from
'@/config/application'
;
import
{
THEME_STORAGE_KEY
}
from
'@/config/application'
;
import
{
loadUnreadNotificationCount
,
NOTIFICATION_STATE_EVENT
,
}
from
'@/features/notifications/notificationService'
;
import
{
Sidebar
}
from
'@heroui-pro/react/sidebar'
;
import
{
Sidebar
}
from
'@heroui-pro/react/sidebar'
;
import
{
Toast
}
from
'@heroui/react'
;
import
{
Toast
}
from
'@heroui/react'
;
import
{
createContext
,
useContext
,
useLayoutEffect
,
useState
}
from
'react'
;
import
{
history
}
from
'@umijs/max'
;
import
{
createContext
,
useContext
,
useEffect
,
useLayoutEffect
,
useState
,
}
from
'react'
;
import
DashboardNavbar
from
'./DashboardNavbar'
;
import
DashboardNavbar
from
'./DashboardNavbar'
;
import
DashboardSidebar
from
'./DashboardSidebar'
;
import
DashboardSidebar
from
'./DashboardSidebar'
;
...
@@ -41,6 +52,7 @@ export default function AppShell({
...
@@ -41,6 +52,7 @@ export default function AppShell({
})
{
})
{
const
[
theme
,
setTheme
]
=
useState
(
getInitialTheme
);
const
[
theme
,
setTheme
]
=
useState
(
getInitialTheme
);
const
[
navbarActions
,
setNavbarActions
]
=
useState
(
null
);
const
[
navbarActions
,
setNavbarActions
]
=
useState
(
null
);
const
[
notificationCount
,
setNotificationCount
]
=
useState
(
0
);
useLayoutEffect
(()
=>
{
useLayoutEffect
(()
=>
{
const
root
=
document
.
documentElement
;
const
root
=
document
.
documentElement
;
...
@@ -56,6 +68,36 @@ export default function AppShell({
...
@@ -56,6 +68,36 @@ export default function AppShell({
}
}
},
[
theme
]);
},
[
theme
]);
useEffect
(()
=>
{
if
(
!
sidebarProps
.
userInfo
)
{
setNotificationCount
(
0
);
return
undefined
;
}
let
cancelled
=
false
;
const
refreshNotificationCount
=
async
()
=>
{
try
{
const
count
=
await
loadUnreadNotificationCount
();
if
(
!
cancelled
)
setNotificationCount
(
count
);
}
catch
{
if
(
!
cancelled
)
setNotificationCount
(
0
);
}
};
refreshNotificationCount
();
window
.
addEventListener
(
NOTIFICATION_STATE_EVENT
,
refreshNotificationCount
);
const
timer
=
window
.
setInterval
(
refreshNotificationCount
,
60000
);
return
()
=>
{
cancelled
=
true
;
window
.
removeEventListener
(
NOTIFICATION_STATE_EVENT
,
refreshNotificationCount
,
);
window
.
clearInterval
(
timer
);
};
},
[
sidebarProps
.
userInfo
]);
return
(
return
(
<
NavbarActionsContext
.
Provider
value=
{
setNavbarActions
}
>
<
NavbarActionsContext
.
Provider
value=
{
setNavbarActions
}
>
<
Sidebar
.
Provider
<
Sidebar
.
Provider
...
@@ -68,8 +110,10 @@ export default function AppShell({
...
@@ -68,8 +110,10 @@ export default function AppShell({
<
Sidebar
.
Main
className=
"app-main-glass rc-app-main"
>
<
Sidebar
.
Main
className=
"app-main-glass rc-app-main"
>
<
DashboardNavbar
<
DashboardNavbar
actions=
{
navbarActions
}
actions=
{
navbarActions
}
notificationCount=
{
notificationCount
}
theme=
{
theme
}
theme=
{
theme
}
title=
{
title
}
title=
{
title
}
onOpenNotifications=
{
()
=>
history
.
push
(
'/notifications'
)
}
onThemeChange=
{
setTheme
}
onThemeChange=
{
setTheme
}
/>
/>
<
div
className=
"app-shell-content-scroll min-h-0 flex-1 overflow-y-auto"
>
<
div
className=
"app-shell-content-scroll min-h-0 flex-1 overflow-y-auto"
>
...
...
src/components/DashboardNavbar.jsx
View file @
aa343ada
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
({
export
default
function
DashboardNavbar
({
actions
,
actions
,
notificationCount
=
0
,
onOpenNotifications
,
onThemeChange
,
onThemeChange
,
theme
,
theme
,
title
,
title
,
...
@@ -12,6 +14,13 @@ export default function DashboardNavbar({
...
@@ -12,6 +14,13 @@ export default function DashboardNavbar({
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
,
);
const
notificationLabel
=
normalizedNotificationCount
?
`通知消息,
${
normalizedNotificationCount
}
条未读通知`
:
'通知消息'
;
return
(
return
(
<
Navbar
className=
"app-navbar rc-navbar"
maxWidth=
"full"
>
<
Navbar
className=
"app-navbar rc-navbar"
maxWidth=
"full"
>
...
@@ -26,6 +35,32 @@ export default function DashboardNavbar({
...
@@ -26,6 +35,32 @@ export default function DashboardNavbar({
<
Navbar
.
Spacer
/>
<
Navbar
.
Spacer
/>
<
div
className=
"rc-navbar-actions"
>
<
div
className=
"rc-navbar-actions"
>
{
actions
}
{
actions
}
<
Tooltip
delay=
{
0
}
>
<
Badge
.
Anchor
>
<
Button
isIconOnly
aria
-
label=
{
notificationLabel
}
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
>
<
Tooltip
delay=
{
0
}
>
<
Tooltip
delay=
{
0
}
>
<
Button
<
Button
isIconOnly
isIconOnly
...
...
src/config/application.js
View file @
aa343ada
export
const
APPLICATION_DISPLAY_NAME
=
'AI 应用'
;
export
const
APPLICATION_DISPLAY_NAME
=
'AI 应用'
;
export
const
APPLICATION_BASE_PATH
=
'/ai_chat/'
;
export
const
APPLICATION_BASE_PATH
=
'/ai_chat/'
;
export
const
THEME_STORAGE_KEY
=
'xinyuan_color_theme'
;
export
const
THEME_STORAGE_KEY
=
'xinyuan_color_theme'
;
export
const
ROUTE_TITLES
=
{
'/'
:
APPLICATION_DISPLAY_NAME
,
'/notifications'
:
'通知消息'
,
};
src/features/notifications/notificationService.js
0 → 100644
View file @
aa343ada
import
{
APPLICATION_BASE_PATH
}
from
'@/config/application'
;
import
{
executeUserNoticeAction
,
queryUserNotice
,
readUserNotice
,
unreadUserNoticeCount
,
}
from
'@/services/DataService'
;
export
const
NOTIFICATION_PAGE_SIZE
=
10
;
export
const
NOTIFICATION_STATE_EVENT
=
'ai-chat: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
unreadUserNoticeCount
();
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
executeUserNoticeAction
(
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
basePath
=
APPLICATION_BASE_PATH
.
replace
(
/
\/
$/
,
''
);
const
escapedBasePath
=
basePath
.
replace
(
/
[
.*+?^${}()|[
\]\\]
/g
,
'
\\
$&'
);
const
match
=
content
.
match
(
new
RegExp
(
`https?://[^\\s<>"']+|
${
escapedBasePath
}
/[^\\s<>"']+`
,
'i'
),
);
return
trimTarget
(
match
?.[
0
]);
}
export
function
resolveNotificationTarget
(
target
)
{
const
normalizedTarget
=
trimTarget
(
target
);
if
(
!
normalizedTarget
)
return
null
;
const
basePath
=
APPLICATION_BASE_PATH
.
replace
(
/
\/
$/
,
''
);
if
(
/^https
?
:
\/\/
/i
.
test
(
normalizedTarget
))
{
try
{
const
url
=
new
URL
(
normalizedTarget
);
if
(
typeof
window
!==
'undefined'
&&
url
.
origin
===
window
.
location
.
origin
&&
(
url
.
pathname
===
basePath
||
url
.
pathname
.
startsWith
(
`
${
basePath
}
/`
))
)
{
return
resolveNotificationTarget
(
`
${
url
.
pathname
}${
url
.
search
}${
url
.
hash
}
`
,
);
}
return
{
href
:
url
.
toString
(),
external
:
true
};
}
catch
{
return
null
;
}
}
if
(
normalizedTarget
===
basePath
)
{
return
{
href
:
'/'
,
external
:
false
};
}
if
(
normalizedTarget
.
startsWith
(
`
${
basePath
}
/`
))
{
return
{
href
:
normalizedTarget
.
slice
(
basePath
.
length
)
||
'/'
,
external
:
false
,
};
}
return
normalizedTarget
.
startsWith
(
'/'
)
?
{
href
:
normalizedTarget
,
external
:
true
}
:
null
;
}
export
function
notifyNotificationStateChanged
()
{
if
(
typeof
window
!==
'undefined'
)
{
window
.
dispatchEvent
(
new
CustomEvent
(
NOTIFICATION_STATE_EVENT
));
}
}
src/pages/Notifications/index.jsx
0 → 100644
View file @
aa343ada
This diff is collapsed.
Click to expand it.
src/services/DataService.js
View file @
aa343ada
...
@@ -480,6 +480,38 @@ export function getApplications() {
...
@@ -480,6 +480,38 @@ export function getApplications() {
});
});
}
}
export
function
queryUserNotice
(
page
,
pageSize
,
filter
=
{},
status
=
null
)
{
return
request
(
'/api/notice/customer/query'
,
{
method
:
'post'
,
data
:
{
page
,
page_size
:
pageSize
,
filter
,
status
,
},
});
}
export
function
readUserNotice
(
name
)
{
return
request
(
'/api/notice/customer/read'
,
{
method
:
'put'
,
data
:
{
name
},
});
}
export
function
unreadUserNoticeCount
()
{
return
request
(
'/api/notice/customer/count'
,
{
method
:
'get'
,
});
}
export
function
executeUserNoticeAction
(
action
,
methodName
,
data
)
{
return
request
(
`/api/notice/customer/
${
action
}
/action/
${
methodName
}
`
,
{
method
:
'put'
,
data
,
});
}
export
function
getRecord
(
name
)
{
export
function
getRecord
(
name
)
{
return
request
(
`/api/record/
${
name
}
`
,
{
return
request
(
`/api/record/
${
name
}
`
,
{
method
:
'get'
,
method
:
'get'
,
...
...
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