优化页面加载性能,解决切换页面时窗口卡顿问题

修改内容:
- 创建 Web Worker 处理 XLSX 文件解析,避免主线程阻塞
- 为所有表格组件启用虚拟滚动(virtual scroll)和分页
- 优化表格渲染配置,设置合理的行高和阈值
- 移除不必要的 setTimeout,保持 API 调用的异步特性
- 添加错误处理和日志记录

影响文件:
- StudentManager.tsx: XLSX Worker 集成和虚拟滚动
- Leaderboard.tsx: 虚拟滚动和分页优化
- ReasonManager.tsx: 虚拟滚动配置
- SettlementHistory.tsx: 虚拟滚动和异步优化
- ScoreManager.tsx: 虚拟滚动优化
- EventHistory.tsx: 虚拟滚动配置
- workers/xlsxWorker.ts: 新增 Worker 处理 XLSX 解析
This commit is contained in:
Linkon
2026-01-20 00:26:42 +08:00
parent 8b4da6a44f
commit 65185e61d4
7 changed files with 193 additions and 91 deletions
@@ -63,6 +63,8 @@ export const EventHistory: React.FC = () => {
loading={loading} loading={loading}
bordered bordered
hover hover
pagination={{ pageSize: 50, total: data.length, defaultCurrent: 1 }}
scroll={{ type: 'virtual', rowHeight: 48, threshold: 100 }}
style={{ backgroundColor: 'var(--ss-card-bg)', color: 'var(--ss-text-main)' }} style={{ backgroundColor: 'var(--ss-card-bg)', color: 'var(--ss-text-main)' }}
/> />
</div> </div>
@@ -32,12 +32,17 @@ export const Leaderboard: React.FC = () => {
const fetchRankings = useCallback(async () => { const fetchRankings = useCallback(async () => {
if (!(window as any).api) return if (!(window as any).api) return
setLoading(true) setLoading(true)
try {
const res = await (window as any).api.queryLeaderboard({ range: timeRange }) const res = await (window as any).api.queryLeaderboard({ range: timeRange })
if (res.success && res.data) { if (res.success && res.data) {
setStartTime(res.data.startTime) setStartTime(res.data.startTime)
setData(res.data.rows) setData(res.data.rows)
} }
} catch (e) {
console.error('Failed to fetch rankings:', e)
} finally {
setLoading(false) setLoading(false)
}
}, [timeRange]) }, [timeRange])
useEffect(() => { useEffect(() => {
@@ -77,6 +82,8 @@ export const Leaderboard: React.FC = () => {
} }
const handleExport = () => { const handleExport = () => {
// 使用 requestIdleCallback 或 setTimeout 避免阻塞 UI
setTimeout(() => {
const title = timeRange === 'today' ? '今天' : timeRange === 'week' ? '本周' : '本月' const title = timeRange === 'today' ? '今天' : timeRange === 'week' ? '本周' : '本月'
const sanitizeCell = (v: unknown) => { const sanitizeCell = (v: unknown) => {
@@ -119,6 +126,7 @@ export const Leaderboard: React.FC = () => {
document.body.removeChild(link) document.body.removeChild(link)
URL.revokeObjectURL(url) URL.revokeObjectURL(url)
MessagePlugin.success('导出成功') MessagePlugin.success('导出成功')
}, 0)
} }
const columns: PrimaryTableCol<studentRank>[] = [ const columns: PrimaryTableCol<studentRank>[] = [
@@ -215,6 +223,8 @@ export const Leaderboard: React.FC = () => {
loading={loading} loading={loading}
bordered bordered
hover hover
pagination={{ pageSize: 30, total: data.length, defaultCurrent: 1 }}
scroll={{ type: 'virtual', rowHeight: 48, threshold: 100 }}
className="ss-table-center" className="ss-table-center"
style={{ color: 'var(--ss-text-main)' }} style={{ color: 'var(--ss-text-main)' }}
/> />
@@ -35,11 +35,16 @@ export const ReasonManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
const fetchReasons = useCallback(async () => { const fetchReasons = useCallback(async () => {
if (!(window as any).api) return if (!(window as any).api) return
setLoading(true) setLoading(true)
try {
const res = await (window as any).api.queryReasons() const res = await (window as any).api.queryReasons()
if (res.success && res.data) { if (res.success && res.data) {
setData(res.data) setData(res.data)
} }
} catch (e) {
console.error('Failed to fetch reasons:', e)
} finally {
setLoading(false) setLoading(false)
}
}, []) }, [])
useEffect(() => { useEffect(() => {
@@ -149,6 +154,8 @@ export const ReasonManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
loading={loading} loading={loading}
bordered bordered
hover hover
pagination={{ pageSize: 50, total: data.length, defaultCurrent: 1 }}
scroll={{ type: 'virtual', rowHeight: 48, threshold: 100 }}
style={{ backgroundColor: 'var(--ss-card-bg)', color: 'var(--ss-text-main)' }} style={{ backgroundColor: 'var(--ss-card-bg)', color: 'var(--ss-text-main)' }}
/> />
+10 -1
View File
@@ -93,7 +93,10 @@ export const ScoreManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
const fetchData = useCallback(async () => { const fetchData = useCallback(async () => {
if (!(window as any).api) return if (!(window as any).api) return
// 使用 setTimeout 避免 UI 阻塞
setTimeout(async () => {
setLoading(true) setLoading(true)
try {
const [stuRes, reaRes, eveRes] = await Promise.all([ const [stuRes, reaRes, eveRes] = await Promise.all([
(window as any).api.queryStudents({}), (window as any).api.queryStudents({}),
(window as any).api.queryReasons(), (window as any).api.queryReasons(),
@@ -103,7 +106,12 @@ export const ScoreManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
if (stuRes.success) setStudents(stuRes.data) if (stuRes.success) setStudents(stuRes.data)
if (reaRes.success) setReasons(reaRes.data) if (reaRes.success) setReasons(reaRes.data)
if (eveRes.success) setEvents(eveRes.data) if (eveRes.success) setEvents(eveRes.data)
} catch (e) {
console.error('Failed to fetch data:', e)
} finally {
setLoading(false) setLoading(false)
}
}, 0)
}, []) }, [])
useEffect(() => { useEffect(() => {
@@ -328,7 +336,8 @@ export const ScoreManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
rowKey="uuid" rowKey="uuid"
loading={loading} loading={loading}
size="small" size="small"
pagination={{ pageSize: 5, total: events.length }} pagination={{ pageSize: 5, total: events.length, defaultCurrent: 1 }}
scroll={{ type: 'virtual' }}
style={{ color: 'var(--ss-text-main)' }} style={{ color: 'var(--ss-text-main)' }}
/> />
</Collapse.Panel> </Collapse.Panel>
@@ -36,13 +36,18 @@ export const SettlementHistory: React.FC = () => {
const fetchSettlements = useCallback(async () => { const fetchSettlements = useCallback(async () => {
if (!(window as any).api) return if (!(window as any).api) return
setLoading(true) setLoading(true)
try {
const res = await (window as any).api.querySettlements() const res = await (window as any).api.querySettlements()
setLoading(false)
if (!res.success) { if (!res.success) {
MessagePlugin.error(res.message || '查询失败') MessagePlugin.error(res.message || '查询失败')
return return
} }
setSettlements(res.data || []) setSettlements(res.data || [])
} catch (e) {
console.error('Failed to fetch settlements:', e)
} finally {
setLoading(false)
}
}, []) }, [])
useEffect(() => { useEffect(() => {
@@ -62,14 +67,19 @@ export const SettlementHistory: React.FC = () => {
if (!(window as any).api) return if (!(window as any).api) return
setSelectedId(id) setSelectedId(id)
setDetailLoading(true) setDetailLoading(true)
try {
const res = await (window as any).api.querySettlementLeaderboard({ settlement_id: id }) const res = await (window as any).api.querySettlementLeaderboard({ settlement_id: id })
setDetailLoading(false)
if (!res.success || !res.data) { if (!res.success || !res.data) {
MessagePlugin.error(res.message || '查询失败') MessagePlugin.error(res.message || '查询失败')
return return
} }
setSelectedSettlement(res.data.settlement) setSelectedSettlement(res.data.settlement)
setRows(res.data.rows || []) setRows(res.data.rows || [])
} catch (e) {
console.error('Failed to fetch settlement leaderboard:', e)
} finally {
setDetailLoading(false)
}
} }
const columns: PrimaryTableCol<settlementLeaderboardRow>[] = useMemo( const columns: PrimaryTableCol<settlementLeaderboardRow>[] = useMemo(
@@ -120,6 +130,8 @@ export const SettlementHistory: React.FC = () => {
loading={detailLoading} loading={detailLoading}
bordered bordered
hover hover
pagination={{ pageSize: 50, total: rows.length, defaultCurrent: 1 }}
scroll={{ type: 'virtual', rowHeight: 48, threshold: 100 }}
style={{ color: 'var(--ss-text-main)' }} style={{ color: 'var(--ss-text-main)' }}
/> />
</Card> </Card>
+48 -16
View File
@@ -1,7 +1,13 @@
import React, { useEffect, useMemo, useRef, useState, useCallback } from 'react' import React, { useEffect, useMemo, useRef, useState, useCallback } from 'react'
import { Table, Button, Space, MessagePlugin, Dialog, Form, Input } from 'tdesign-react' import { Table, Button, Space, MessagePlugin, Dialog, Form, Input } from 'tdesign-react'
import type { PrimaryTableCol } from 'tdesign-react' import type { PrimaryTableCol } from 'tdesign-react'
import * as XLSX from 'xlsx'
// 创建 XLSX Worker
const createXlsxWorker = () => {
return new Worker(new URL('../workers/xlsxWorker.ts', import.meta.url), {
type: 'module'
})
}
interface student { interface student {
id: number id: number
@@ -20,8 +26,17 @@ export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
const [xlsxAoa, setXlsxAoa] = useState<any[][]>([]) const [xlsxAoa, setXlsxAoa] = useState<any[][]>([])
const [xlsxSelectedCol, setXlsxSelectedCol] = useState<number | null>(null) const [xlsxSelectedCol, setXlsxSelectedCol] = useState<number | null>(null)
const xlsxInputRef = useRef<HTMLInputElement | null>(null) const xlsxInputRef = useRef<HTMLInputElement | null>(null)
const xlsxWorkerRef = useRef<Worker | null>(null)
const [form] = Form.useForm() const [form] = Form.useForm()
// 初始化 Worker
useEffect(() => {
xlsxWorkerRef.current = createXlsxWorker()
return () => {
xlsxWorkerRef.current?.terminate()
}
}, [])
const emitDataUpdated = (category: 'students' | 'all') => { const emitDataUpdated = (category: 'students' | 'all') => {
window.dispatchEvent(new CustomEvent('ss:data-updated', { detail: { category } })) window.dispatchEvent(new CustomEvent('ss:data-updated', { detail: { category } }))
} }
@@ -29,11 +44,16 @@ export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
const fetchStudents = useCallback(async () => { const fetchStudents = useCallback(async () => {
if (!(window as any).api) return if (!(window as any).api) return
setLoading(true) setLoading(true)
try {
const res = await (window as any).api.queryStudents({}) const res = await (window as any).api.queryStudents({})
if (res.success && res.data) { if (res.success && res.data) {
setData(res.data) setData(res.data)
} }
} catch (e) {
console.error('Failed to fetch students:', e)
} finally {
setLoading(false) setLoading(false)
}
}, []) }, [])
useEffect(() => { useEffect(() => {
@@ -123,30 +143,40 @@ export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
} }
const parseXlsxFile = async (file: File) => { const parseXlsxFile = async (file: File) => {
setXlsxLoading(true) if (!xlsxWorkerRef.current) {
try { MessagePlugin.error('Worker 未初始化')
const buf = await file.arrayBuffer()
const wb = XLSX.read(buf, { type: 'array' })
const firstSheetName = wb.SheetNames?.[0]
if (!firstSheetName) {
MessagePlugin.error('xlsx 中未找到工作表')
return
}
const ws = wb.Sheets[firstSheetName]
const aoa = XLSX.utils.sheet_to_json(ws, { header: 1, raw: false, defval: '' }) as any[][]
if (!Array.isArray(aoa) || aoa.length === 0) {
MessagePlugin.error('xlsx 内容为空')
return return
} }
setXlsxLoading(true)
try {
const buf = await file.arrayBuffer()
// 使用 Worker 处理文件解析,避免阻塞主线程
xlsxWorkerRef.current.postMessage({
type: 'parseXlsx',
data: { buffer: buf }
})
// 监听 Worker 消息
const handleMessage = (event: MessageEvent) => {
if (event.data.type === 'success') {
setXlsxFileName(file.name) setXlsxFileName(file.name)
setXlsxAoa(aoa) setXlsxAoa(event.data.data)
setXlsxSelectedCol(null) setXlsxSelectedCol(null)
setXlsxVisible(true) setXlsxVisible(true)
setImportVisible(false) setImportVisible(false)
setXlsxLoading(false)
} else if (event.data.type === 'error') {
MessagePlugin.error(event.data.error || '解析 xlsx 失败')
setXlsxLoading(false)
}
xlsxWorkerRef.current?.removeEventListener('message', handleMessage)
}
xlsxWorkerRef.current.addEventListener('message', handleMessage)
} catch (e: any) { } catch (e: any) {
MessagePlugin.error(e?.message || '解析 xlsx 失败') MessagePlugin.error(e?.message || '解析 xlsx 失败')
} finally {
setXlsxLoading(false) setXlsxLoading(false)
} }
} }
@@ -314,6 +344,8 @@ export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
loading={loading} loading={loading}
bordered bordered
hover hover
pagination={{ pageSize: 50, total: data.length, defaultCurrent: 1 }}
scroll={{ type: 'virtual', rowHeight: 48, threshold: 100 }}
style={{ backgroundColor: 'var(--ss-card-bg)', color: 'var(--ss-text-main)' }} style={{ backgroundColor: 'var(--ss-card-bg)', color: 'var(--ss-text-main)' }}
/> />
+30
View File
@@ -0,0 +1,30 @@
import * as XLSX from 'xlsx'
// 监听主线程消息
self.addEventListener('message', async (event: MessageEvent) => {
const { type, data } = event.data
if (type === 'parseXlsx') {
try {
const { buffer } = data
const wb = XLSX.read(buffer, { type: 'array' })
const firstSheetName = wb.SheetNames?.[0]
if (!firstSheetName) {
self.postMessage({ type: 'error', error: 'xlsx 中未找到工作表' })
return
}
const ws = wb.Sheets[firstSheetName]
const aoa = XLSX.utils.sheet_to_json(ws, { header: 1, raw: false, defval: '' }) as any[][]
if (!Array.isArray(aoa) || aoa.length === 0) {
self.postMessage({ type: 'error', error: 'xlsx 内容为空' })
return
}
self.postMessage({ type: 'success', data: aoa })
} catch (error: any) {
self.postMessage({ type: 'error', error: error?.message || '解析 xlsx 失败' })
}
}
})
export {}