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
Hide whitespace changes
Inline
Side-by-side
Showing
13 changed files
with
967 additions
and
3 deletions
+967
-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
+508
-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
import
{
Bell
,
Close
,
OpenExternal
,
Search
}
from
'@/components/AppIcons'
;
import
AppPagination
from
'@/components/AppPagination'
;
import
AppShell
from
'@/components/AppShell'
;
import
ApplicationSwitcherDialog
from
'@/components/ApplicationSwitcherDialog'
;
import
LoginModal
from
'@/components/LoginModal'
;
import
{
ROUTE_TITLES
}
from
'@/config/application'
;
import
{
NOTIFICATION_PAGE_SIZE
,
executeNotificationAction
,
extractNotificationTarget
,
getNotificationContent
,
getNotificationSender
,
isNotificationRead
,
loadNotificationPage
,
loadUnreadNotificationCount
,
markAllNotificationsRead
,
markNotificationRead
,
resolveNotificationTarget
,
}
from
'@/features/notifications/notificationService'
;
import
{
EmptyState
}
from
'@heroui-pro/react/empty-state'
;
import
{
Alert
,
Avatar
,
Button
,
Card
,
Chip
,
SearchField
,
Spinner
,
Tabs
,
toast
,
}
from
'@heroui/react'
;
import
{
history
}
from
'@umijs/max'
;
import
{
useCallback
,
useEffect
,
useState
}
from
'react'
;
const
CATEGORY_OPTIONS
=
[
{
key
:
'message'
,
label
:
'消息'
},
{
key
:
'todo'
,
label
:
'待办'
},
];
const
READ_STATUS_OPTIONS
=
[
{
key
:
'all'
,
label
:
'全部'
},
{
key
:
'unread'
,
label
:
'未读'
},
];
function
getStoredUser
()
{
try
{
const
currentUser
=
localStorage
.
getItem
(
'current_user'
);
return
currentUser
?
JSON
.
parse
(
currentUser
)
:
null
;
}
catch
{
return
null
;
}
}
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
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"
>
<
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 text-foreground 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 text-foreground"
>
{
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
)
}
>
<
OpenExternal
size=
{
14
}
/>
进入流程
</
Button
>
)
:
null
}
</
div
>
</
Card
.
Content
>
</
Card
>
);
}
function
NotificationPageContent
({
userInfo
})
{
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
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
id=
{
option
.
key
}
key=
{
option
.
key
}
>
{
option
.
label
}
<
Tabs
.
Indicator
/>
</
Tabs
.
Tab
>
))
}
</
Tabs
.
List
>
</
Tabs
.
ListContainer
>
</
Tabs
>
<
Button
isDisabled=
{
loading
||
unreadTotal
===
0
}
isLoading=
{
busyKey
===
'all'
}
variant=
"secondary"
onPress=
{
handleMarkAllRead
}
>
全部已读
</
Button
>
</
div
>
<
div
className=
"rc-page-toolbar"
>
<
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
>
<
Search
size=
{
16
}
/>
</
SearchField
.
SearchIcon
>
<
SearchField
.
Input
placeholder=
"搜索通知内容"
/>
<
SearchField
.
ClearButton
aria
-
label=
"清空通知搜索"
>
<
Close
size=
{
12
}
/>
</
SearchField
.
ClearButton
>
</
SearchField
.
Group
>
</
SearchField
>
</
div
>
<
Tabs
selectedKey=
{
readStatus
}
onSelectionChange=
{
(
key
)
=>
{
setReadStatus
(
String
(
key
));
setPage
(
0
);
}
}
>
<
Tabs
.
ListContainer
>
<
Tabs
.
List
aria
-
label=
"通知状态"
>
{
READ_STATUS_OPTIONS
.
map
((
option
)
=>
(
<
Tabs
.
Tab
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
aria
-
label=
"通知加载中"
className=
"flex min-h-64 items-center justify-center"
>
<
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
aria
-
busy=
{
loading
}
className=
"flex min-w-0 flex-col gap-3"
>
{
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
?
(
<
AppPagination
isDisabled=
{
loading
}
itemLabel=
"条通知"
page=
{
page
}
pageSize=
{
NOTIFICATION_PAGE_SIZE
}
total=
{
total
}
onPageChange=
{
setPage
}
/>
)
:
null
}
</
div
>
);
}
export
default
function
NotificationsPage
()
{
const
[
userInfo
,
setUserInfo
]
=
useState
(
getStoredUser
);
const
[
sidebarOpen
,
setSidebarOpen
]
=
useState
(
true
);
const
[
applicationSwitcherOpen
,
setApplicationSwitcherOpen
]
=
useState
(
false
);
useEffect
(()
=>
{
document
.
title
=
`
${
ROUTE_TITLES
[
'/notifications'
]}
- AI 应用`
;
},
[]);
if
(
!
userInfo
)
{
return
(
<
LoginModal
open
onError=
{
()
=>
{}
}
onSuccess=
{
(
currentUser
)
=>
{
setUserInfo
(
currentUser
);
window
.
setTimeout
(()
=>
toast
.
success
(
'登录成功'
),
0
);
}
}
/>
);
}
return
(
<>
<
AppShell
activeKey=
""
conversations=
{
[]
}
open=
{
sidebarOpen
}
title=
{
ROUTE_TITLES
[
'/notifications'
]
}
userInfo=
{
userInfo
}
onDeleteConversation=
{
()
=>
{}
}
onLogout=
{
()
=>
{
localStorage
.
removeItem
(
'current_user'
);
setUserInfo
(
null
);
}
}
onNewConversation=
{
()
=>
history
.
push
(
'/'
)
}
onOpenApplicationSwitcher=
{
()
=>
setApplicationSwitcherOpen
(
true
)
}
onOpenChange=
{
setSidebarOpen
}
onOpenConversation=
{
()
=>
history
.
push
(
'/'
)
}
onPinConversation=
{
()
=>
{}
}
onRenameConversation=
{
()
=>
{}
}
>
<
NotificationPageContent
userInfo=
{
userInfo
}
/>
</
AppShell
>
<
ApplicationSwitcherDialog
open=
{
applicationSwitcherOpen
}
userInfo=
{
userInfo
}
onClose=
{
()
=>
setApplicationSwitcherOpen
(
false
)
}
/>
</>
);
}
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