feat(web): migrate notification inbox to React
Add the React notification route with cursor-based inbox and sent-message views, read receipts, filters, unread badges, and safe fallback links for functionality that remains in the legacy frontend.
This commit is contained in:
1 parent
44f09cec1d
commit
68f74aec86
6 files changed
+232
-5
No files matched your search
@@ -8,5 +8,6 @@
|
|||||||
| `/dashboard` | 角色化首页 | 已登录用户 | 管理总览;学生课表/消息/考试/预警;教师教学班/成绩登记进度 | verification |
|
| `/dashboard` | 角色化首页 | 已登录用户 | 管理总览;学生课表/消息/考试/预警;教师教学班/成绩登记进度 | verification |
|
||||||
| `/account` | 个人账户 | 已登录用户 | TOTP、恢复码、SSO 绑定;Passkey 浏览器仪式尚未开放 | verification |
|
| `/account` | 个人账户 | 已登录用户 | TOTP、恢复码、SSO 绑定;Passkey 浏览器仪式尚未开放 | verification |
|
||||||
| `/users` | 用户与权限 | SuperAdmin | 列表、筛选、分页、创建、启停、重置密码、角色范围、Excel | verification |
|
| `/users` | 用户与权限 | SuperAdmin | 列表、筛选、分页、创建、启停、重置密码、角色范围、Excel | verification |
|
||||||
|
| `/notifications` | 通知中心 | 已登录用户 | 收件箱筛选、游标分页、已读回执;已发送;定向群发暂回旧系统 | building |
|
||||||
|
|
||||||
后续从 `web/src/router/index.ts` 逐条补全。只有关键合同和各状态验收完整后,状态才能更新为 `complete`。
|
后续从 `web/src/router/index.ts` 逐条补全。只有关键合同和各状态验收完整后,状态才能更新为 `complete`。
|
||||||
@@ -9,6 +9,7 @@ const LoginPage = lazy(() => import('../pages/LoginPage'))
|
|||||||
const DashboardPage = lazy(() => import('../pages/DashboardPage'))
|
const DashboardPage = lazy(() => import('../pages/DashboardPage'))
|
||||||
const AccountPage = lazy(() => import('../pages/AccountPage'))
|
const AccountPage = lazy(() => import('../pages/AccountPage'))
|
||||||
const UsersPage = lazy(() => import('../pages/UsersPage'))
|
const UsersPage = lazy(() => import('../pages/UsersPage'))
|
||||||
|
const NotificationsPage = lazy(() => import('../pages/NotificationsPage'))
|
||||||
const SsoCallbackPage = lazy(() => import('../pages/SsoCallbackPage'))
|
const SsoCallbackPage = lazy(() => import('../pages/SsoCallbackPage'))
|
||||||
const SsoBindPage = lazy(() => import('../pages/SsoBindPage'))
|
const SsoBindPage = lazy(() => import('../pages/SsoBindPage'))
|
||||||
const AppShell = lazy(() => import('../layout/AppShell'))
|
const AppShell = lazy(() => import('../layout/AppShell'))
|
||||||
@@ -52,6 +53,7 @@ function AppRoutes() {
|
|||||||
<Route path="/dashboard" element={<DashboardPage />} />
|
<Route path="/dashboard" element={<DashboardPage />} />
|
||||||
<Route path="/account" element={<AccountPage />} />
|
<Route path="/account" element={<AccountPage />} />
|
||||||
<Route path="/users" element={<UsersPage />} />
|
<Route path="/users" element={<UsersPage />} />
|
||||||
|
<Route path="/notifications" element={<NotificationsPage />} />
|
||||||
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="*" element={<Navigate to="/dashboard" replace />} />
|
<Route path="*" element={<Navigate to="/dashboard" replace />} />
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ import {
|
|||||||
MenuUnfoldOutlined,
|
MenuUnfoldOutlined,
|
||||||
SafetyCertificateOutlined,
|
SafetyCertificateOutlined,
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import { Avatar, Button, Layout, Menu, Tag } from 'antd'
|
import { Avatar, Badge, Button, Layout, Menu, Tag } from 'antd'
|
||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { Outlet, useLocation, useNavigate } from 'react-router-dom'
|
import { Outlet, useLocation, useNavigate } from 'react-router-dom'
|
||||||
|
import http from '../api/http'
|
||||||
import { branding } from '../config/branding'
|
import { branding } from '../config/branding'
|
||||||
import { useAuthStore } from '../stores/auth'
|
import { useAuthStore } from '../stores/auth'
|
||||||
import BrandMark from '../components/BrandMark'
|
import BrandMark from '../components/BrandMark'
|
||||||
@@ -26,13 +28,18 @@ export default function AppShell() {
|
|||||||
const roles = user?.roles ?? []
|
const roles = user?.roles ?? []
|
||||||
const isStudentHome = roles.includes('Student') && !roles.some((role) => ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Counselor'].includes(role))
|
const isStudentHome = roles.includes('Student') && !roles.some((role) => ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Counselor'].includes(role))
|
||||||
const isTeacherHome = roles.includes('Teacher') && !roles.some((role) => ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Counselor', 'Student'].includes(role))
|
const isTeacherHome = roles.includes('Teacher') && !roles.some((role) => ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Counselor', 'Student'].includes(role))
|
||||||
const legacyBase = (import.meta.env.VITE_LEGACY_APP_URL ?? 'http://localhost:5255').replace(/\/$/, '')
|
const unreadQuery = useQuery({
|
||||||
|
queryKey: ['notification-unread-count'],
|
||||||
|
queryFn: async () => (await http.get<{ count: number }>('/notifications/unread-count')).data.count,
|
||||||
|
refetchInterval: 60_000,
|
||||||
|
})
|
||||||
|
|
||||||
useEffect(() => { void refreshUser().catch(() => undefined) }, [refreshUser])
|
useEffect(() => { void refreshUser().catch(() => undefined) }, [refreshUser])
|
||||||
|
|
||||||
const items = useMemo(() => [
|
const items = useMemo(() => [
|
||||||
{ key: '/dashboard', icon: <AppstoreOutlined />, label: isStudentHome ? '学习首页' : isTeacherHome ? '教学工作台' : '教务总览' },
|
{ key: '/dashboard', icon: <AppstoreOutlined />, label: isStudentHome ? '学习首页' : isTeacherHome ? '教学工作台' : '教务总览' },
|
||||||
{ key: '/account', icon: <IdcardOutlined />, label: '个人账户' },
|
{ key: '/account', icon: <IdcardOutlined />, label: '个人账户' },
|
||||||
|
{ key: '/notifications', icon: <BellOutlined />, label: '通知中心' },
|
||||||
...(user?.roles.includes('SuperAdmin')
|
...(user?.roles.includes('SuperAdmin')
|
||||||
? [{ key: '/users', icon: <SafetyCertificateOutlined />, label: '用户与权限' }]
|
? [{ key: '/users', icon: <SafetyCertificateOutlined />, label: '用户与权限' }]
|
||||||
: []),
|
: []),
|
||||||
@@ -87,7 +94,9 @@ export default function AppShell() {
|
|||||||
<div className="shell-context"><span>当前工作区</span><strong>{pageTitle}</strong></div>
|
<div className="shell-context"><span>当前工作区</span><strong>{pageTitle}</strong></div>
|
||||||
</div>
|
</div>
|
||||||
<div className="shell-user">
|
<div className="shell-user">
|
||||||
<Button type="text" aria-label="通知中心" icon={<BellOutlined />} onClick={() => window.location.assign(`${legacyBase}/notifications`)} />
|
<Badge count={unreadQuery.data ?? 0} size="small" overflowCount={99}>
|
||||||
|
<Button type="text" aria-label="通知中心" icon={<BellOutlined />} onClick={() => navigate('/notifications')} />
|
||||||
|
</Badge>
|
||||||
<Tag color="geekblue">预览</Tag>
|
<Tag color="geekblue">预览</Tag>
|
||||||
<div className="shell-user__copy"><strong>{user?.displayName ?? '用户'}</strong><small>{user?.roles.join(' · ') || '正在读取权限'}</small></div>
|
<div className="shell-user__copy"><strong>{user?.displayName ?? '用户'}</strong><small>{user?.roles.join(' · ') || '正在读取权限'}</small></div>
|
||||||
<Avatar style={{ background: '#54cfbd', color: '#163f43' }}>{user?.displayName?.slice(0, 1) ?? '明'}</Avatar>
|
<Avatar style={{ background: '#54cfbd', color: '#163f43' }}>{user?.displayName?.slice(0, 1) ?? '明'}</Avatar>
|
||||||
|
|||||||
Loaded 3 of 6 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user