mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-21 09:39:03 +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;
|
z-index: 10;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
animation: twinkle-animation 3s infinite ease-in-out;
|
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 {
|
.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;
|
padding: 12px 8px;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
width: 60px;
|
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;
|
backface-visibility: hidden;
|
||||||
transform: translateZ(0);
|
transform: translateZ(0);
|
||||||
|
contain: paint; /* 性能优化:限制重绘范围 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-content-area.hidden {
|
.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 { 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'
|
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
|
const { Content } = Layout
|
||||||
|
|
||||||
interface ContentAreaProps {
|
interface ContentAreaProps {
|
||||||
@@ -85,22 +87,37 @@ export function ContentArea({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Content style={{ flex: 1, overflowY: 'auto' }}>
|
<Content style={{ flex: 1, overflowY: 'auto' }}>
|
||||||
<Routes>
|
<Suspense
|
||||||
<Route
|
fallback={
|
||||||
path="/"
|
<div
|
||||||
element={<Home canEdit={permission === 'admin' || permission === 'points'} />}
|
style={{
|
||||||
/>
|
display: 'flex',
|
||||||
<Route path="/students" element={<StudentManager canEdit={permission === 'admin'} />} />
|
alignItems: 'center',
|
||||||
<Route
|
justifyContent: 'center',
|
||||||
path="/score"
|
height: '100%'
|
||||||
element={<ScoreManager canEdit={permission === 'admin' || permission === 'points'} />}
|
}}
|
||||||
/>
|
>
|
||||||
<Route path="/leaderboard" element={<Leaderboard />} />
|
<Loading text="正在载入页面..." />
|
||||||
<Route path="/settlements" element={<SettlementHistory />} />
|
</div>
|
||||||
<Route path="/reasons" element={<ReasonManager canEdit={permission === 'admin'} />} />
|
}
|
||||||
<Route path="/settings" element={<Settings permission={permission} />} />
|
>
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Routes>
|
||||||
</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>
|
</Content>
|
||||||
</Layout>
|
</Layout>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -38,10 +38,10 @@ export const GlobalSidebar: React.FC = () => {
|
|||||||
if ((window as any).api) {
|
if ((window as any).api) {
|
||||||
(window as any).api.windowResize(24, 300)
|
(window as any).api.windowResize(24, 300)
|
||||||
}
|
}
|
||||||
// 3. 最后重新显示三角
|
// 3. 最后重新显示三角(等待透明度动画完成)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setShowToggle(true)
|
setShowToggle(true)
|
||||||
}, 20)
|
}, 150)
|
||||||
}, 150)
|
}, 150)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ interface student {
|
|||||||
id: number
|
id: number
|
||||||
name: string
|
name: string
|
||||||
score: number
|
score: number
|
||||||
|
pinyinName?: string
|
||||||
|
pinyinFirst?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface reason {
|
interface reason {
|
||||||
@@ -51,33 +53,6 @@ export const Home: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
|||||||
window.dispatchEvent(new CustomEvent('ss:data-updated', { detail: { category } }))
|
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) => {
|
const getSurname = (name: string) => {
|
||||||
if (!name) return ''
|
if (!name) return ''
|
||||||
@@ -95,6 +70,38 @@ export const Home: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
|||||||
return py ? py.toUpperCase() : '#'
|
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) => {
|
const getDisplayText = (name: string) => {
|
||||||
if (!name) return ''
|
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()
|
const q0 = keyword.trim().toLowerCase()
|
||||||
if (!q0) return true
|
if (!q0) return true
|
||||||
|
|
||||||
const nameLower = String(name).toLowerCase()
|
const nameLower = String(s.name).toLowerCase()
|
||||||
if (nameLower.includes(q0)) return true
|
if (nameLower.includes(q0)) return true
|
||||||
|
|
||||||
|
const pyLower = s.pinyinName || ''
|
||||||
|
if (pyLower.includes(q0)) return true
|
||||||
|
|
||||||
const q1 = q0.replace(/\s+/g, '')
|
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 {
|
try {
|
||||||
const m0 = match(name, q0)
|
const m0 = match(s.name, q0)
|
||||||
if (Array.isArray(m0)) return true
|
if (Array.isArray(m0)) return true
|
||||||
if (q1 && q1 !== q0) {
|
|
||||||
const m1 = match(name, q1)
|
|
||||||
if (Array.isArray(m1)) return true
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -128,13 +134,13 @@ export const Home: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
|||||||
|
|
||||||
// 过滤和排序学生
|
// 过滤和排序学生
|
||||||
const sortedStudents = useMemo(() => {
|
const sortedStudents = useMemo(() => {
|
||||||
const filtered = students.filter((s) => matchStudentName(s.name, searchKeyword))
|
const filtered = students.filter((s) => matchStudentName(s, searchKeyword))
|
||||||
|
|
||||||
switch (sortType) {
|
switch (sortType) {
|
||||||
case 'alphabet':
|
case 'alphabet':
|
||||||
return filtered.sort((a, b) => {
|
return filtered.sort((a, b) => {
|
||||||
const pyA = pinyin(a.name, { toneType: 'none' })
|
const pyA = a.pinyinName || ''
|
||||||
const pyB = pinyin(b.name, { toneType: 'none' })
|
const pyB = b.pinyinName || ''
|
||||||
return pyA.localeCompare(pyB)
|
return pyA.localeCompare(pyB)
|
||||||
})
|
})
|
||||||
case 'surname':
|
case 'surname':
|
||||||
@@ -161,7 +167,7 @@ export const Home: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
|||||||
|
|
||||||
const groups: Record<string, student[]> = {}
|
const groups: Record<string, student[]> = {}
|
||||||
sortedStudents.forEach((s) => {
|
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] = []
|
if (!groups[key]) groups[key] = []
|
||||||
groups[key].push(s)
|
groups[key].push(s)
|
||||||
})
|
})
|
||||||
@@ -822,7 +828,7 @@ export const Home: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
|||||||
suffixIcon={
|
suffixIcon={
|
||||||
reasonContent ? (
|
reasonContent ? (
|
||||||
<DeleteIcon
|
<DeleteIcon
|
||||||
onClick={() => setReasonContent('')}
|
onClick={() => setSearchKeyword('')}
|
||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: 'pointer' }}
|
||||||
/>
|
/>
|
||||||
) : undefined
|
) : undefined
|
||||||
@@ -883,4 +889,4 @@ export const Home: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user