学生管理支持多行文本导入名单

This commit is contained in:
JSR
2026-03-18 18:32:14 +08:00
parent a4bb260226
commit 5ea59a77d4
3 changed files with 72 additions and 14 deletions
+66 -14
View File
@@ -39,9 +39,11 @@ export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
const [avatarValue, setAvatarValue] = useState<string | null>(null)
const avatarInputRef = useRef<HTMLInputElement | null>(null)
const [xlsxLoading, setXlsxLoading] = useState(false)
const [textImportLoading, setTextImportLoading] = useState(false)
const [xlsxFileName, setXlsxFileName] = useState("")
const [xlsxAoa, setXlsxAoa] = useState<any[][]>([])
const [xlsxSelectedCol, setXlsxSelectedCol] = useState<number | null>(null)
const [textImportValue, setTextImportValue] = useState("")
const xlsxInputRef = useRef<HTMLInputElement | null>(null)
const xlsxWorkerRef = useRef<Worker | null>(null)
const [form] = Form.useForm()
@@ -377,12 +379,11 @@ export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
return cols
}, [xlsxMaxCols, xlsxSelectedCol])
const extractNamesFromAoa = (aoa: any[][], colIdx: number) => {
const extractUniqueNames = (rawNames: string[]) => {
const out: string[] = []
const seen = new Set<string>()
const banned = new Set([t("students.name").toLowerCase(), "name", t("students.name")])
for (const row of aoa) {
const raw = row?.[colIdx]
for (const raw of rawNames) {
const name = String(raw ?? "").trim()
if (!name) continue
if (banned.has(name.toLowerCase()) || banned.has(name)) continue
@@ -393,8 +394,32 @@ export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
return out
}
const extractNamesFromAoa = (aoa: any[][], colIdx: number) => {
const names = aoa.map((row) => String(row?.[colIdx] ?? ""))
return extractUniqueNames(names)
}
const extractNamesFromText = (text: string) => {
const lines = text.split(/\r?\n/)
return extractUniqueNames(lines)
}
const importNames = async (names: string[]) => {
if (!(window as any).api) return false
const res = await (window as any).api.importStudentsFromXlsx({ names })
if (!res?.success) {
messageApi.error(res?.message || t("students.importFailed"))
return false
}
const inserted = Number(res?.data?.inserted ?? 0)
const skipped = Number(res?.data?.skipped ?? 0)
messageApi.success(t("students.importComplete", { inserted, skipped }))
fetchStudents()
emitDataUpdated("students")
return true
}
const handleConfirmXlsxImport = async () => {
if (!(window as any).api) return
if (!canEdit) {
messageApi.error(t("common.readOnly"))
return
@@ -412,25 +437,39 @@ export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
setXlsxLoading(true)
try {
const res = await (window as any).api.importStudentsFromXlsx({ names })
if (!res?.success) {
messageApi.error(res?.message || t("students.importFailed"))
return
}
const inserted = Number(res?.data?.inserted ?? 0)
const skipped = Number(res?.data?.skipped ?? 0)
messageApi.success(t("students.importComplete", { inserted, skipped }))
const success = await importNames(names)
if (!success) return
setXlsxVisible(false)
setXlsxAoa([])
setXlsxFileName("")
setXlsxSelectedCol(null)
fetchStudents()
emitDataUpdated("students")
} finally {
setXlsxLoading(false)
}
}
const handleImportTextNames = async () => {
if (!canEdit) {
messageApi.error(t("common.readOnly"))
return
}
const names = extractNamesFromText(textImportValue)
if (!names.length) {
messageApi.warning(t("students.noNamesFound"))
return
}
setTextImportLoading(true)
try {
const success = await importNames(names)
if (!success) return
setTextImportValue("")
setImportVisible(false)
} finally {
setTextImportLoading(false)
}
}
const columns: ColumnsType<student> = [
{
title: t("students.avatar"),
@@ -600,6 +639,19 @@ export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
destroyOnHidden
>
<Space orientation="vertical" style={{ width: "100%" }}>
<div style={{ color: "var(--ss-text-secondary)", fontSize: 12 }}>
{t("students.importTextHint")}
</div>
<Input.TextArea
value={textImportValue}
onChange={(e) => setTextImportValue(e.target.value)}
rows={8}
placeholder={t("students.importTextPlaceholder")}
disabled={!canEdit || textImportLoading}
/>
<Button type="primary" loading={textImportLoading} disabled={!canEdit} onClick={handleImportTextNames}>
{t("students.importByText")}
</Button>
<Button
loading={xlsxLoading}
disabled={!canEdit}
+3
View File
@@ -369,6 +369,9 @@
"tagSaveSuccess": "Tags saved successfully",
"tagSaveFailed": "Failed to save tags",
"importTitle": "Import List",
"importTextHint": "Paste multiple lines, one student name per line",
"importTextPlaceholder": "Example:\nAlice\nBob\nCharlie",
"importByText": "Import from Text",
"importByXlsx": "Import via xlsx",
"xlsxPreview": "XLSX Preview & Import",
"file": "File",
+3
View File
@@ -369,6 +369,9 @@
"tagSaveSuccess": "标签保存成功",
"tagSaveFailed": "保存标签失败",
"importTitle": "导入名单",
"importTextHint": "支持粘贴多行文本,一行一个姓名",
"importTextPlaceholder": "例如:\n张三\n李四\n王五",
"importByText": "从文本导入",
"importByXlsx": "通过 xlsx 导入",
"xlsxPreview": "xlsx 预览与导入",
"file": "文件",