Add exam score import and management workflows

This commit is contained in:
biss authored and biss committed 2026-10-03 21:38:14 +08:00
1 parent 71ee21b063
commit 24d2a96144
26 files changed
+3865

No files matched your search

+89
View File
@@ -0,0 +1,89 @@
import { useEffect, useMemo, useState } from 'react'
import {
AppstoreOutlined,
BellOutlined,
IdcardOutlined,
MenuFoldOutlined,
MenuUnfoldOutlined,
SafetyCertificateOutlined,
} from '@ant-design/icons'
import { Avatar, Button, Layout, Menu, Tag } from 'antd'
import { Outlet, useLocation, useNavigate } from 'react-router-dom'
import { branding } from '../config/branding'
import { useAuthStore } from '../stores/auth'
import BrandMark from '../components/BrandMark'
const { Header, Sider, Content } = Layout
export default function AppShell() {
const location = useLocation()
const navigate = useNavigate()
const user = useAuthStore((state) => state.user)
const logout = useAuthStore((state) => state.logout)
const refreshUser = useAuthStore((state) => state.refreshUser)
const [collapsed, setCollapsed] = useState(false)
useEffect(() => { void refreshUser().catch(() => undefined) }, [refreshUser])
const items = useMemo(() => [
{ key: '/dashboard', icon: <AppstoreOutlined />, label: '教务总览' },
{ key: '/account', icon: <IdcardOutlined />, label: '个人账户' },
...(user?.roles.includes('SuperAdmin')
? [{ key: '/users', icon: <SafetyCertificateOutlined />, label: '用户与权限' }]
: []),
], [user?.roles])
const pageTitle = items.find((item) => location.pathname.startsWith(item.key))?.label ?? '迁移预览'
function signOut() {
logout()
navigate('/login', { replace: true })
}
return (
<Layout className="app-shell">
<Sider
className="app-shell__sider"
width={238}
collapsedWidth={72}
collapsed={collapsed}
breakpoint="lg"
onBreakpoint={setCollapsed}
>
<div className="shell-brand">
<BrandMark />
{!collapsed && <div><strong>{branding.shortName}</strong><small>REACT MIGRATION</small></div>}
</div>
<Menu
className="shell-menu"
theme="dark"
mode="inline"
selectedKeys={[items.find((item) => location.pathname.startsWith(item.key))?.key ?? '/dashboard']}
items={items}
onClick={({ key }) => navigate(key)}
/>
</Sider>
<Layout>
<Header className="shell-header">
<div style={{ display: 'flex', alignItems: 'center', gap: 14, minWidth: 0 }}>
<Button
type="text"
aria-label={collapsed ? '展开导航' : '收起导航'}
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
onClick={() => setCollapsed((value) => !value)}
/>
<div className="shell-context"><span>当前工作区</span><strong>{pageTitle}</strong></div>
</div>
<div className="shell-user">
<Button type="text" aria-label="通知中心" icon={<BellOutlined />} />
<Tag color="geekblue">预览</Tag>
<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>
<Button type="text" onClick={signOut}>退出</Button>
</div>
</Header>
<Content className="shell-content"><Outlet /></Content>
</Layout>
</Layout>
)
}