-
+
-
+
+ {dbStatus.type === 'postgresql' && (
+
+ {contextHolder}
+
+
+
+
+ 远程数据库
+
+
+ {dbStatus.connected ? '已连接' : '未连接'}
+
+
+ }
+ onClick={handleSync}
+ loading={syncLoading}
+ style={{
+ width: '100%',
+ height: '24px',
+ fontSize: '12px',
+ padding: '0 8px',
+ color: 'var(--ss-text-secondary)'
+ }}
+ >
+ 刷新状态
+
+ }
+ onClick={handleForceSync}
+ loading={forceSyncLoading}
+ disabled={!dbStatus.connected}
+ style={{
+ width: '100%',
+ height: '24px',
+ fontSize: '12px',
+ padding: '0 8px'
+ }}
+ >
+ 立即同步
+
+
+
+ )}
+
)
}
diff --git a/src/renderer/src/components/StudentManager.tsx b/src/renderer/src/components/StudentManager.tsx
index 8fb87fb..7e2e1e1 100644
--- a/src/renderer/src/components/StudentManager.tsx
+++ b/src/renderer/src/components/StudentManager.tsx
@@ -1,9 +1,8 @@
import React, { useEffect, useMemo, useRef, useState, useCallback } from 'react'
-import { Table, Button, Space, MessagePlugin, Dialog, Form, Input, Tag } from 'tdesign-react'
-import type { PrimaryTableCol } from 'tdesign-react'
+import { Table, Button, Space, message, Modal, Form, Input, Tag, Pagination } from 'antd'
+import type { ColumnsType } from 'antd/es/table'
import { TagEditorDialog } from './TagEditorDialog'
-// 创建 XLSX Worker
const createXlsxWorker = () => {
return new Worker(new URL('../workers/xlsxWorker.ts', import.meta.url), {
type: 'module'
@@ -35,8 +34,8 @@ export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
const xlsxInputRef = useRef
(null)
const xlsxWorkerRef = useRef(null)
const [form] = Form.useForm()
+ const [messageApi, contextHolder] = message.useMessage()
- // 初始化 Worker
useEffect(() => {
xlsxWorkerRef.current = createXlsxWorker()
return () => {
@@ -106,36 +105,31 @@ export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
const handleAdd = async () => {
if (!(window as any).api) return
if (!canEdit) {
- MessagePlugin.error('当前为只读权限')
+ messageApi.error('当前为只读权限')
return
}
try {
- const validateResult = await form.validate()
- if (validateResult !== true) {
- return
- }
-
- const values = form.getFieldsValue(true) as { name: string }
+ const values = await form.validateFields()
if (!values.name) {
- MessagePlugin.warning('请输入姓名')
+ messageApi.warning('请输入姓名')
return
}
const name = values.name.trim()
if (data.some((s) => s.name === name)) {
- MessagePlugin.warning('学生姓名已存在')
+ messageApi.warning('学生姓名已存在')
return
}
const res = await (window as any).api.createStudent({ ...values, name })
if (res.success) {
- MessagePlugin.success('添加成功')
+ messageApi.success('添加成功')
setVisible(false)
- form.reset()
+ form.resetFields()
fetchStudents()
emitDataUpdated('students')
} else {
- MessagePlugin.error(res.message || '添加失败')
+ messageApi.error(res.message || '添加失败')
}
} catch (err) {
try {
@@ -155,22 +149,22 @@ export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
const handleDelete = async (id: number) => {
if (!(window as any).api) return
if (!canEdit) {
- MessagePlugin.error('当前为只读权限')
+ messageApi.error('当前为只读权限')
return
}
const res = await (window as any).api.deleteStudent(id)
if (res.success) {
- MessagePlugin.success('删除成功')
+ messageApi.success('删除成功')
fetchStudents()
emitDataUpdated('students')
} else {
- MessagePlugin.error(res.message || '删除失败')
+ messageApi.error(res.message || '删除失败')
}
}
const handleOpenTagEditor = (student: student) => {
if (!canEdit) {
- MessagePlugin.error('当前为只读权限')
+ messageApi.error('当前为只读权限')
return
}
setEditingStudent(student)
@@ -183,18 +177,18 @@ export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
try {
const res = await (window as any).api.tagsUpdateStudentTags(editingStudent.id, tagIds)
if (res && res.success) {
- MessagePlugin.success('标签保存成功')
+ messageApi.success('标签保存成功')
setTagEditVisible(false)
setEditingStudent(null)
fetchStudents()
emitDataUpdated('students')
} else {
const errorMsg = res?.message || '保存标签失败'
- MessagePlugin.error(errorMsg)
+ messageApi.error(errorMsg)
}
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error)
- MessagePlugin.error(`保存标签失败: ${errorMsg}`)
+ messageApi.error(`保存标签失败: ${errorMsg}`)
}
}
@@ -211,7 +205,7 @@ export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
const parseXlsxFile = async (file: File) => {
if (!xlsxWorkerRef.current) {
- MessagePlugin.error('Worker 未初始化')
+ messageApi.error('Worker 未初始化')
return
}
@@ -219,13 +213,11 @@ export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
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)
@@ -235,7 +227,7 @@ export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
setImportVisible(false)
setXlsxLoading(false)
} else if (event.data.type === 'error') {
- MessagePlugin.error(event.data.error || '解析 xlsx 失败')
+ messageApi.error(event.data.error || '解析 xlsx 失败')
setXlsxLoading(false)
}
xlsxWorkerRef.current?.removeEventListener('message', handleMessage)
@@ -243,7 +235,7 @@ export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
xlsxWorkerRef.current.addEventListener('message', handleMessage)
} catch (e: any) {
- MessagePlugin.error(e?.message || '解析 xlsx 失败')
+ messageApi.error(e?.message || '解析 xlsx 失败')
setXlsxLoading(false)
}
}
@@ -269,26 +261,34 @@ export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
}, [xlsxAoa, xlsxMaxCols])
const xlsxPreviewColumns = useMemo(() => {
- const cols: PrimaryTableCol[] = [
- { colKey: '__row', title: '#', width: 60, align: 'center', fixed: 'left' as any }
+ const cols: any[] = [
+ {
+ title: '#',
+ dataIndex: '__row',
+ key: '__row',
+ width: 60,
+ align: 'center' as const,
+ fixed: 'left' as const
+ }
]
for (let c = 0; c < xlsxMaxCols; c++) {
const selected = xlsxSelectedCol === c
cols.push({
- colKey: `c${c}`,
title: (
setXlsxSelectedCol(c)}
>
{excelColName(c)}
),
- minWidth: 120
+ dataIndex: `c${c}`,
+ key: `c${c}`,
+ width: 120
})
}
return cols
@@ -313,17 +313,17 @@ export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
const handleConfirmXlsxImport = async () => {
if (!(window as any).api) return
if (!canEdit) {
- MessagePlugin.error('当前为只读权限')
+ messageApi.error('当前为只读权限')
return
}
if (xlsxSelectedCol == null) {
- MessagePlugin.warning('请先点击选择“姓名列”')
+ messageApi.warning('请先点击选择"姓名列"')
return
}
const names = extractNamesFromAoa(xlsxAoa, xlsxSelectedCol)
if (!names.length) {
- MessagePlugin.error('所选列未解析到可导入的姓名')
+ messageApi.error('所选列未解析到可导入的姓名')
return
}
@@ -331,12 +331,12 @@ export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
try {
const res = await (window as any).api.importStudentsFromXlsx({ names })
if (!res?.success) {
- MessagePlugin.error(res?.message || '导入失败')
+ messageApi.error(res?.message || '导入失败')
return
}
const inserted = Number(res?.data?.inserted ?? 0)
const skipped = Number(res?.data?.skipped ?? 0)
- MessagePlugin.success(`导入完成:新增 ${inserted},跳过 ${skipped}`)
+ messageApi.success(`导入完成:新增 ${inserted},跳过 ${skipped}`)
setXlsxVisible(false)
setXlsxAoa([])
setXlsxFileName('')
@@ -348,75 +348,60 @@ export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
}
}
- const columns: PrimaryTableCol[] = [
- { colKey: 'name', title: '姓名', width: 100 },
+ const columns: ColumnsType = [
+ { title: '姓名', dataIndex: 'name', key: 'name', width: 100 },
{
- colKey: 'score',
title: '当前积分',
+ dataIndex: 'score',
+ key: 'score',
width: 160,
align: 'center',
- cell: ({ row }) => (
+ render: (score: number) => (
0
- ? 'var(--td-success-color)'
- : row.score < 0
- ? 'var(--td-error-color)'
+ score > 0
+ ? 'var(--ant-color-success, #52c41a)'
+ : score < 0
+ ? 'var(--ant-color-error, #ff4d4f)'
: 'inherit'
}}
>
- {row.score > 0 ? `+${row.score}` : row.score}
+ {score > 0 ? `+${score}` : score}
)
},
{
- colKey: 'tags',
title: '标签',
+ dataIndex: 'tags',
+ key: 'tags',
width: 200,
- cell: ({ row }) => {
- const tags = row.tags || []
- return (
-
- {tags.length === 0 ? (
- 无标签
- ) : (
- tags.slice(0, 3).map((tag) => (
-
- {tag}
-
- ))
- )}
- {tags.length > 3 && (
-
- +{tags.length - 3}
+ render: (tags: string[] = []) => (
+
+ {tags.length === 0 ? (
+ 无标签
+ ) : (
+ tags.slice(0, 3).map((tag) => (
+
+ {tag}
- )}
-
- )
- }
+ ))
+ )}
+ {tags.length > 3 && +{tags.length - 3}}
+
+ )
},
{
- colKey: 'operation',
title: '操作',
+ key: 'operation',
width: 150,
- cell: ({ row }) => (
+ render: (_, row) => (
-
@@ -424,59 +409,69 @@ export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
}
]
+ const paginatedData = data.slice((currentPage - 1) * pageSize, currentPage * pageSize)
+
return (
+ {contextHolder}
学生管理
- setImportVisible(true)}>
+ setImportVisible(true)}>
导入名单
- setVisible(true)}>
+ setVisible(true)}>
添加学生
setCurrentPage(pageInfo.current),
- onPageSizeChange: (size) => setPageSize(size)
- }}
+ pagination={false}
style={{ backgroundColor: 'var(--ss-card-bg)', color: 'var(--ss-text-main)' }}
/>
+
+
{
+ setCurrentPage(page)
+ setPageSize(size)
+ }}
+ showSizeChanger
+ showTotal={(total) => `共 ${total} 条`}
+ />
+
- {/* 添加学生弹窗 */}
-
+
-
+
-
+
- {/* 标签编辑弹窗 */}
{
diff --git a/src/renderer/src/components/TagEditorDialog.tsx b/src/renderer/src/components/TagEditorDialog.tsx
index b15a821..f0a7564 100644
--- a/src/renderer/src/components/TagEditorDialog.tsx
+++ b/src/renderer/src/components/TagEditorDialog.tsx
@@ -1,8 +1,7 @@
import React, { useState, useEffect } from 'react'
-import { Dialog, Input, Button, Space, Tag, MessagePlugin } from 'tdesign-react'
-import { useTheme } from '../contexts/ThemeContext' // 导入主题上下文
+import { Modal, Input, Button, Space, Tag, message } from 'antd'
-interface Tag {
+interface TagItem {
id: number
name: string
}
@@ -23,13 +22,10 @@ export const TagEditorDialog: React.FC = ({
title = '编辑标签'
}) => {
const [inputValue, setInputValue] = useState('')
- const [allTags, setAllTags] = useState([])
+ const [allTags, setAllTags] = useState([])
const [selectedTagIds, setSelectedTagIds] = useState>(new Set(initialTagIds))
- const { currentTheme } = useTheme() // 获取当前主题
+ const [messageApi, contextHolder] = message.useMessage()
- const themeMode = currentTheme?.mode || 'light' // 默认为 light
-
- // fetchAllTags is declared as a function so it can be called from useEffect
async function fetchAllTags() {
if (!(window as any).api) return
try {
@@ -39,7 +35,7 @@ export const TagEditorDialog: React.FC = ({
}
} catch (e) {
console.error('Failed to fetch tags:', e)
- MessagePlugin.error('获取标签列表失败')
+ messageApi.error('获取标签列表失败')
}
}
@@ -56,7 +52,7 @@ export const TagEditorDialog: React.FC = ({
if (!trimmed) return
if (trimmed.length > 30) {
- MessagePlugin.error('标签名称不能超过 30 个字符')
+ messageApi.error('标签名称不能超过 30 个字符')
return
}
@@ -76,11 +72,11 @@ export const TagEditorDialog: React.FC = ({
setSelectedTagIds((prev) => new Set([...prev, res.data.id]))
setInputValue('')
} else {
- MessagePlugin.error(res.message || '添加标签失败')
+ messageApi.error(res.message || '添加标签失败')
}
} catch (e) {
console.error('Failed to create tag:', e)
- MessagePlugin.error('添加标签失败')
+ messageApi.error('添加标签失败')
}
}
@@ -106,23 +102,16 @@ export const TagEditorDialog: React.FC = ({
newSet.delete(tagId)
return newSet
})
- MessagePlugin.success('标签删除成功')
+ messageApi.success('标签删除成功')
} else {
- MessagePlugin.error(res.message || '删除标签失败')
+ messageApi.error(res.message || '删除标签失败')
}
} catch (e) {
console.error('Failed to delete tag:', e)
- MessagePlugin.error('删除标签失败')
+ messageApi.error('删除标签失败')
}
}
- /* const handleKeyDown = (value: string, context: { e: React.KeyboardEvent }) => {
- if (context.e.key === 'Enter') {
- context.e.preventDefault()
- handleAddTag()
- }
- } */
-
const handleConfirm = () => {
onConfirm(Array.from(selectedTagIds))
onClose()
@@ -132,33 +121,31 @@ export const TagEditorDialog: React.FC = ({
const availableTags = allTags.filter((t) => !selectedTagIds.has(t.id))
return (
-
+
)
}
diff --git a/src/renderer/src/components/ThemeEditor.tsx b/src/renderer/src/components/ThemeEditor.tsx
index 8786d18..cb2f83c 100644
--- a/src/renderer/src/components/ThemeEditor.tsx
+++ b/src/renderer/src/components/ThemeEditor.tsx
@@ -1,16 +1,6 @@
import React, { useEffect } from 'react'
-import {
- Drawer,
- Form,
- Input,
- Select,
- ColorPicker,
- Button,
- Space,
- Divider,
- Row,
- Col
-} from 'tdesign-react'
+import { Drawer, Form, Input, Select, Button, Space, Divider, Row, Col, ColorPicker } from 'antd'
+import type { Color } from 'antd/es/color-picker'
import { useThemeEditor } from '../contexts/ThemeEditorContext'
import { useTheme } from '../contexts/ThemeContext'
import { generateColorMap } from '../utils/color'
@@ -59,7 +49,6 @@ export const ThemeEditor: React.FC = () => {
const { currentTheme } = useTheme()
- // 实时预览逻辑
useEffect(() => {
if (!isEditing || !editingTheme) return
@@ -67,10 +56,8 @@ export const ThemeEditor: React.FC = () => {
const { tdesign, custom } = theme.config
const root = document.documentElement
- // 1. 设置 TDesign 模式
root.setAttribute('theme-mode', theme.mode)
- // 2. 设置 TDesign 品牌色
if (tdesign.brandColor) {
const colorMap = generateColorMap(tdesign.brandColor, theme.mode)
Object.entries(colorMap).forEach(([key, value]) => {
@@ -78,7 +65,6 @@ export const ThemeEditor: React.FC = () => {
})
}
- // 3. 应用自定义变量
Object.entries(custom).forEach(([key, value]) => {
root.style.setProperty(key, value)
})
@@ -87,7 +73,6 @@ export const ThemeEditor: React.FC = () => {
applyPreview(editingTheme)
}, [editingTheme, isEditing])
- // 关闭时恢复原有主题
useEffect(() => {
if (!isEditing && currentTheme) {
const { tdesign, custom } = currentTheme.config
@@ -112,39 +97,36 @@ export const ThemeEditor: React.FC = () => {
return (
-
+
保存主题
-
- 取消
-
+ 取消
}
- destroyOnClose
+ destroyOnHidden
>
-