mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-21 18:19:03 +08:00
优化页面加载性能,解决切换页面时窗口卡顿问题
修改内容: - 创建 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:
@@ -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)' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -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 {}
|
||||||
Reference in New Issue
Block a user