Files
2026-08-01 23:09:49 +08:00

219 lines
8.2 KiB
TypeScript

import { useState, useEffect, lazy, Suspense } from 'react'
import { Routes, Route, useNavigate, useLocation } from 'react-router-dom'
import { LoadingBar } from './components/ui'
const Dashboard = lazy(() => import('./pages/Dashboard').then(m => ({ default: m.Dashboard })))
const WorkList = lazy(() => import('./pages/WorkList').then(m => ({ default: m.WorkList })))
const ReviewCenter = lazy(() => import('./pages/ReviewCenter').then(m => ({ default: m.ReviewCenter })))
const PeoplePage = lazy(() => import('./pages/People').then(m => ({ default: m.PeoplePage })))
const StationsPage = lazy(() => import('./pages/Stations').then(m => ({ default: m.StationsPage })))
const ArchivePage = lazy(() => import('./pages/Archive').then(m => ({ default: m.ArchivePage })))
const NoticesPage = lazy(() => import('./pages/Notices').then(m => ({ default: m.NoticesPage })))
const SettingsPage = lazy(() => import('./pages/Settings').then(m => ({ default: m.SettingsPage })))
const RulesPage = lazy(() => import('./pages/Rules').then(m => ({ default: m.RulesPage })))
const ScoresPage = lazy(() => import('./pages/Scores').then(m => ({ default: m.ScoresPage })))
const LeaderboardPage = lazy(() => import('./pages/Leaderboard').then(m => ({ default: m.LeaderboardPage })))
const AppealsPage = lazy(() => import('./pages/Appeals').then(m => ({ default: m.AppealsPage })))
const SystemLogsPage = lazy(() => import('./pages/SystemLogs').then(m => ({ default: m.SystemLogsPage })))
const StationMapPage = lazy(() => import('./pages/StationMap').then(m => ({ default: m.StationMapPage })))
const CockpitPage = lazy(() => import('./pages/Cockpit').then(m => ({ default: m.CockpitPage })))
const ProfilePage = lazy(() => import('./pages/Profile').then(m => ({ default: m.ProfilePage })))
const LoginPage = lazy(() => import('./pages/Login').then(m => ({ default: m.LoginPage })))
import { Sidebar, TopBar } from './components/layout'
const CreateRecordModal = lazy(() => import('./modals/CreateRecordModal').then(m => ({ default: m.CreateRecordModal })))
const RecordDrawer = lazy(() => import('./modals/RecordDrawer').then(m => ({ default: m.RecordDrawer })))
import { RoleProvider, ToastProvider, useRole, useToast } from './context'
import { api } from './api'
import { navLabels, pathMap, pathToPage } from './routes'
import type { WorkRecord } from './types'
import type { PageKey } from './routes'
function AppShell() {
const { role, isAuthenticated } = useRole()
const navigate = useNavigate()
const location = useLocation()
const { showToast: showToastCtx } = useToast()
const [sidebarOpen, setSidebarOpen] = useState(false)
const [createOpen, setCreateOpen] = useState(false)
const [selected, setSelected] = useState<WorkRecord | null>(null)
const [records, setRecords] = useState<WorkRecord[]>([])
const [loading, setLoading] = useState(true)
const [loadError, setLoadError] = useState('')
const [toast, setToast] = useState('')
const page: PageKey = pathToPage(location.pathname)
useEffect(() => {
let active = true
setLoading(true)
setLoadError('')
api.records(role)
.then(data => { if (active) setRecords(data) })
.catch(err => { if (active) setLoadError(err.message) })
.finally(() => { if (active) setLoading(false) })
return () => { active = false }
}, [role])
// 角色受限页面自动跳转
useEffect(() => {
const restricted: Record<string, PageKey[]> = {
reporter: ['review', 'people', 'stations', 'stationmap', 'cockpit'],
station: ['stations', 'stationmap', 'cockpit'],
}
if (restricted[role]?.includes(page)) {
navigate('/')
}
}, [role, page, navigate])
const navigateTo = (p: PageKey) => {
navigate(pathMap[p])
setSidebarOpen(false)
}
const pendingCount = records.filter(r =>
role === 'station' ? r.status === 'station_review' : r.status === 'headquarters_review'
).length
const handleCreate = async (record: WorkRecord) => {
setRecords(items => [record, ...items])
setCreateOpen(false)
showToast('工作记录已提交,进入分站审核')
}
const handleUpdate = async (id: string, patch: Partial<WorkRecord>) => {
try {
const updated = await api.reviewRecord(role, id, {
decision: patch.status === 'returned' ? 'return' : 'pass',
score: patch.score ?? 0,
note: patch.reviewNote ?? '',
})
setRecords(items => items.map(item => item.id === id ? updated : item))
setSelected(null)
return true
} catch (error) {
showToast(error instanceof Error ? error.message : '审核操作失败')
return false
}
}
const showToast = (msg: string) => {
setToast(msg)
showToastCtx?.(msg)
setTimeout(() => setToast(''), 2400)
}
// 未认证时显示登录页
if (!isAuthenticated && !localStorage.getItem('auth_token')) {
// 演示模式:允许直接进入
const isDemoMode = !localStorage.getItem('auth_token') && localStorage.getItem('auth_role')
if (!isDemoMode) {
return (
<Suspense fallback={<LoadingBar />}>
<LoginPage />
</Suspense>
)
}
}
return (
<div className="app-shell">
<Sidebar
page={page}
onNavigate={navigateTo}
open={sidebarOpen}
onClose={() => setSidebarOpen(false)}
badge={pendingCount}
/>
<main className="main">
<TopBar
pageTitle={navLabels[page]}
onMenuToggle={() => setSidebarOpen(true)}
/>
<section className="content">
{loadError && (
<div className="error-banner">
{loadError}
<button onClick={() => window.location.reload()}></button>
</div>
)}
{loading && <LoadingBar />}
<Suspense fallback={<LoadingBar />}>
<Routes>
<Route path="/" element={
<Dashboard
records={records}
onNavigate={navigateTo}
onCreate={() => setCreateOpen(true)}
onSelect={setSelected}
/>
} />
<Route path="/work" element={
<WorkList
records={records}
onCreate={() => setCreateOpen(true)}
onSelect={setSelected}
/>
} />
<Route path="/review" element={
<ReviewCenter records={records} onSelect={setSelected} />
} />
<Route path="/people" element={<PeoplePage />} />
<Route path="/stations" element={<StationsPage />} />
<Route path="/archive" element={<ArchivePage records={records} onSelect={setSelected} />} />
<Route path="/notices" element={<NoticesPage />} />
<Route path="/settings" element={<SettingsPage />} />
<Route path="/rules" element={<RulesPage />} />
<Route path="/scores" element={<ScoresPage />} />
<Route path="/leaderboard" element={<LeaderboardPage />} />
<Route path="/appeals" element={<AppealsPage />} />
<Route path="/logs" element={<SystemLogsPage />} />
<Route path="/stationmap" element={<StationMapPage />} />
<Route path="/cockpit" element={<CockpitPage records={records} />} />
<Route path="/profile" element={<ProfilePage records={records} />} />
</Routes>
</Suspense>
</section>
</main>
{createOpen && (
<Suspense fallback={null}>
<CreateRecordModal
onClose={() => setCreateOpen(false)}
onSubmit={handleCreate}
/>
</Suspense>
)}
{selected && (
<Suspense fallback={null}>
<RecordDrawer
record={selected}
role={role}
onClose={() => setSelected(null)}
onUpdate={handleUpdate}
onNotify={showToast}
/>
</Suspense>
)}
{toast && <div className="toast">{toast}</div>}
{sidebarOpen && (
<div className="scrim" onClick={() => setSidebarOpen(false)} />
)}
</div>
)
}
export default function App() {
return (
<RoleProvider>
<ToastProvider>
<AppShell />
</ToastProvider>
</RoleProvider>
)
}