mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-19 19:04:23 +08:00
feat: 优化侧边栏动画延迟和性能,实现路由懒加载
调整侧边栏动画延迟时间以匹配透明度动画,添加contain: paint优化性能 将路由组件改为懒加载模式并添加加载状态 优化学生搜索功能,预计算拼音数据提升性能
This commit is contained in:
@@ -114,7 +114,7 @@ html[theme-mode='dark'] .ss-sidebar .t-menu__item.t-is-active svg:not([fill='non
|
||||
z-index: 10;
|
||||
flex-shrink: 0;
|
||||
animation: twinkle-animation 3s infinite ease-in-out;
|
||||
transition: opacity 0.1s ease, transform 0.1s ease;
|
||||
transition: opacity 0.07s ease, transform 0.15s ease;
|
||||
}
|
||||
|
||||
.global-sidebar-toggle.hidden {
|
||||
@@ -134,9 +134,10 @@ html[theme-mode='dark'] .ss-sidebar .t-menu__item.t-is-active svg:not([fill='non
|
||||
padding: 12px 8px;
|
||||
gap: 12px;
|
||||
width: 60px;
|
||||
transition: opacity 0.15s ease, transform 0.15s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
transition: opacity 0.1s ease, transform 0.1s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
backface-visibility: hidden;
|
||||
transform: translateZ(0);
|
||||
contain: paint; /* 性能优化:限制重绘范围 */
|
||||
}
|
||||
|
||||
.sidebar-content-area.hidden {
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { Layout, Space, Button, Tag } from 'tdesign-react'
|
||||
import React, { Suspense, lazy } from 'react'
|
||||
import { Layout, Space, Button, Tag, Loading } from 'tdesign-react'
|
||||
import { Routes, Route, Navigate } from 'react-router-dom'
|
||||
import { Home } from './Home'
|
||||
import { StudentManager } from './StudentManager'
|
||||
import { Settings } from './Settings'
|
||||
import { ReasonManager } from './ReasonManager'
|
||||
import { ScoreManager } from './ScoreManager'
|
||||
import { Leaderboard } from './Leaderboard'
|
||||
import { SettlementHistory } from './SettlementHistory'
|
||||
import { WindowControls } from './WindowControls'
|
||||
|
||||
const Home = lazy(() => import('./Home').then(m => ({ default: m.Home })))
|
||||
const StudentManager = lazy(() => import('./StudentManager').then(m => ({ default: m.StudentManager })))
|
||||
const Settings = lazy(() => import('./Settings').then(m => ({ default: m.Settings })))
|
||||
const ReasonManager = lazy(() => import('./ReasonManager').then(m => ({ default: m.ReasonManager })))
|
||||
const ScoreManager = lazy(() => import('./ScoreManager').then(m => ({ default: m.ScoreManager })))
|
||||
const Leaderboard = lazy(() => import('./Leaderboard').then(m => ({ default: m.Leaderboard })))
|
||||
const SettlementHistory = lazy(() => import('./SettlementHistory').then(m => ({ default: m.SettlementHistory })))
|
||||
|
||||
const { Content } = Layout
|
||||
|
||||
interface ContentAreaProps {
|
||||
@@ -85,22 +87,37 @@ export function ContentArea({
|
||||
</div>
|
||||
|
||||
<Content style={{ flex: 1, overflowY: 'auto' }}>
|
||||
<Routes>
|
||||
<Route
|
||||
path="/"
|
||||
element={<Home canEdit={permission === 'admin' || permission === 'points'} />}
|
||||
/>
|
||||
<Route path="/students" element={<StudentManager canEdit={permission === 'admin'} />} />
|
||||
<Route
|
||||
path="/score"
|
||||
element={<ScoreManager canEdit={permission === 'admin' || permission === 'points'} />}
|
||||
/>
|
||||
<Route path="/leaderboard" element={<Leaderboard />} />
|
||||
<Route path="/settlements" element={<SettlementHistory />} />
|
||||
<Route path="/reasons" element={<ReasonManager canEdit={permission === 'admin'} />} />
|
||||
<Route path="/settings" element={<Settings permission={permission} />} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
<Suspense
|
||||
fallback={
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100%'
|
||||
}}
|
||||
>
|
||||
<Loading text="正在载入页面..." />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Routes>
|
||||
<Route
|
||||
path="/"
|
||||
element={<Home canEdit={permission === 'admin' || permission === 'points'} />}
|
||||
/>
|
||||
<Route path="/students" element={<StudentManager canEdit={permission === 'admin'} />} />
|
||||
<Route
|
||||
path="/score"
|
||||
element={<ScoreManager canEdit={permission === 'admin' || permission === 'points'} />}
|
||||
/>
|
||||
<Route path="/leaderboard" element={<Leaderboard />} />
|
||||
<Route path="/settlements" element={<SettlementHistory />} />
|
||||
<Route path="/reasons" element={<ReasonManager canEdit={permission === 'admin'} />} />
|
||||
<Route path="/settings" element={<Settings permission={permission} />} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</Content>
|
||||
</Layout>
|
||||
)
|
||||
|
||||
@@ -38,10 +38,10 @@ export const GlobalSidebar: React.FC = () => {
|
||||
if ((window as any).api) {
|
||||
(window as any).api.windowResize(24, 300)
|
||||
}
|
||||
// 3. 最后重新显示三角
|
||||
// 3. 最后重新显示三角(等待透明度动画完成)
|
||||
setTimeout(() => {
|
||||
setShowToggle(true)
|
||||
}, 20)
|
||||
}, 150)
|
||||
}, 150)
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ interface student {
|
||||
id: number
|
||||
name: string
|
||||
score: number
|
||||
pinyinName?: string
|
||||
pinyinFirst?: string
|
||||
}
|
||||
|
||||
interface reason {
|
||||
@@ -51,33 +53,6 @@ export const Home: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
||||
window.dispatchEvent(new CustomEvent('ss:data-updated', { detail: { category } }))
|
||||
}
|
||||
|
||||
const fetchData = useCallback(async (silent = false) => {
|
||||
if (!(window as any).api) return
|
||||
if (!silent) setLoading(true)
|
||||
const [stuRes, reaRes] = await Promise.all([
|
||||
(window as any).api.queryStudents({}),
|
||||
(window as any).api.queryReasons()
|
||||
])
|
||||
|
||||
if (stuRes.success) setStudents(stuRes.data)
|
||||
if (reaRes.success) setReasons(reaRes.data)
|
||||
if (!silent) setLoading(false)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
fetchData()
|
||||
const onDataUpdated = (e: any) => {
|
||||
const category = e?.detail?.category
|
||||
// 仅在学生名单、理由列表或全量更新时才重新拉取数据
|
||||
// 积分变动(events)现在由本地状态维护,不再触发全量刷新以防止页面跳动
|
||||
if (category === 'students' || category === 'reasons' || category === 'all') {
|
||||
fetchData(true)
|
||||
}
|
||||
}
|
||||
window.addEventListener('ss:data-updated', onDataUpdated as any)
|
||||
return () => window.removeEventListener('ss:data-updated', onDataUpdated as any)
|
||||
}, [fetchData])
|
||||
|
||||
// 获取姓氏
|
||||
const getSurname = (name: string) => {
|
||||
if (!name) return ''
|
||||
@@ -95,6 +70,38 @@ export const Home: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
||||
return py ? py.toUpperCase() : '#'
|
||||
}
|
||||
|
||||
const fetchData = useCallback(async (silent = false) => {
|
||||
if (!(window as any).api) return
|
||||
if (!silent) setLoading(true)
|
||||
const [stuRes, reaRes] = await Promise.all([
|
||||
(window as any).api.queryStudents({}),
|
||||
(window as any).api.queryReasons()
|
||||
])
|
||||
|
||||
if (stuRes.success) {
|
||||
const enrichedStudents = (stuRes.data as student[]).map(s => ({
|
||||
...s,
|
||||
pinyinName: pinyin(s.name, { toneType: 'none' }).toLowerCase(),
|
||||
pinyinFirst: getFirstLetter(s.name)
|
||||
}))
|
||||
setStudents(enrichedStudents)
|
||||
}
|
||||
if (reaRes.success) setReasons(reaRes.data)
|
||||
if (!silent) setLoading(false)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
fetchData()
|
||||
const onDataUpdated = (e: any) => {
|
||||
const category = e?.detail?.category
|
||||
if (category === 'students' || category === 'reasons' || category === 'all') {
|
||||
fetchData(true)
|
||||
}
|
||||
}
|
||||
window.addEventListener('ss:data-updated', onDataUpdated as any)
|
||||
return () => window.removeEventListener('ss:data-updated', onDataUpdated as any)
|
||||
}, [fetchData])
|
||||
|
||||
// 获取展示用的文字
|
||||
const getDisplayText = (name: string) => {
|
||||
if (!name) return ''
|
||||
@@ -102,23 +109,22 @@ export const Home: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
||||
}
|
||||
|
||||
// 拼音匹配
|
||||
const matchStudentName = useCallback((name: string, keyword: string) => {
|
||||
const matchStudentName = useCallback((s: student, keyword: string) => {
|
||||
const q0 = keyword.trim().toLowerCase()
|
||||
if (!q0) return true
|
||||
|
||||
const nameLower = String(name).toLowerCase()
|
||||
const nameLower = String(s.name).toLowerCase()
|
||||
if (nameLower.includes(q0)) return true
|
||||
|
||||
const pyLower = s.pinyinName || ''
|
||||
if (pyLower.includes(q0)) return true
|
||||
|
||||
const q1 = q0.replace(/\s+/g, '')
|
||||
if (q1 && nameLower.replace(/\s+/g, '').includes(q1)) return true
|
||||
if (q1 && (nameLower.replace(/\s+/g, '').includes(q1) || pyLower.replace(/\s+/g, '').includes(q1))) return true
|
||||
|
||||
try {
|
||||
const m0 = match(name, q0)
|
||||
const m0 = match(s.name, q0)
|
||||
if (Array.isArray(m0)) return true
|
||||
if (q1 && q1 !== q0) {
|
||||
const m1 = match(name, q1)
|
||||
if (Array.isArray(m1)) return true
|
||||
}
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
@@ -128,13 +134,13 @@ export const Home: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
||||
|
||||
// 过滤和排序学生
|
||||
const sortedStudents = useMemo(() => {
|
||||
const filtered = students.filter((s) => matchStudentName(s.name, searchKeyword))
|
||||
const filtered = students.filter((s) => matchStudentName(s, searchKeyword))
|
||||
|
||||
switch (sortType) {
|
||||
case 'alphabet':
|
||||
return filtered.sort((a, b) => {
|
||||
const pyA = pinyin(a.name, { toneType: 'none' })
|
||||
const pyB = pinyin(b.name, { toneType: 'none' })
|
||||
const pyA = a.pinyinName || ''
|
||||
const pyB = b.pinyinName || ''
|
||||
return pyA.localeCompare(pyB)
|
||||
})
|
||||
case 'surname':
|
||||
@@ -161,7 +167,7 @@ export const Home: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
||||
|
||||
const groups: Record<string, student[]> = {}
|
||||
sortedStudents.forEach((s) => {
|
||||
const key = sortType === 'alphabet' ? getFirstLetter(s.name) : getSurname(s.name)
|
||||
const key = sortType === 'alphabet' ? (s.pinyinFirst || '#') : getSurname(s.name)
|
||||
if (!groups[key]) groups[key] = []
|
||||
groups[key].push(s)
|
||||
})
|
||||
@@ -822,7 +828,7 @@ export const Home: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
||||
suffixIcon={
|
||||
reasonContent ? (
|
||||
<DeleteIcon
|
||||
onClick={() => setReasonContent('')}
|
||||
onClick={() => setSearchKeyword('')}
|
||||
style={{ cursor: 'pointer' }}
|
||||
/>
|
||||
) : undefined
|
||||
@@ -883,4 +889,4 @@ export const Home: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user