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

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 [avatarValue, setAvatarValue] = useState<string | null>(null)
const avatarInputRef = useRef<HTMLInputElement | null>(null) const avatarInputRef = useRef<HTMLInputElement | null>(null)
const [xlsxLoading, setXlsxLoading] = useState(false) const [xlsxLoading, setXlsxLoading] = useState(false)
const [textImportLoading, setTextImportLoading] = useState(false)
const [xlsxFileName, setXlsxFileName] = useState("") const [xlsxFileName, setXlsxFileName] = useState("")
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 [textImportValue, setTextImportValue] = useState("")
const xlsxInputRef = useRef<HTMLInputElement | null>(null) const xlsxInputRef = useRef<HTMLInputElement | null>(null)
const xlsxWorkerRef = useRef<Worker | null>(null) const xlsxWorkerRef = useRef<Worker | null>(null)
const [form] = Form.useForm() const [form] = Form.useForm()
@@ -377,12 +379,11 @@ export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
return cols return cols
}, [xlsxMaxCols, xlsxSelectedCol]) }, [xlsxMaxCols, xlsxSelectedCol])
const extractNamesFromAoa = (aoa: any[][], colIdx: number) => { const extractUniqueNames = (rawNames: string[]) => {
const out: string[] = [] const out: string[] = []
const seen = new Set<string>() const seen = new Set<string>()
const banned = new Set([t("students.name").toLowerCase(), "name", t("students.name")]) const banned = new Set([t("students.name").toLowerCase(), "name", t("students.name")])
for (const row of aoa) { for (const raw of rawNames) {
const raw = row?.[colIdx]
const name = String(raw ?? "").trim() const name = String(raw ?? "").trim()
if (!name) continue if (!name) continue
if (banned.has(name.toLowerCase()) || banned.has(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 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 () => { const handleConfirmXlsxImport = async () => {
if (!(window as any).api) return
if (!canEdit) { if (!canEdit) {
messageApi.error(t("common.readOnly")) messageApi.error(t("common.readOnly"))
return return
@@ -412,25 +437,39 @@ export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
setXlsxLoading(true) setXlsxLoading(true)
try { try {
const res = await (window as any).api.importStudentsFromXlsx({ names }) const success = await importNames(names)
if (!res?.success) { if (!success) return
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 }))
setXlsxVisible(false) setXlsxVisible(false)
setXlsxAoa([]) setXlsxAoa([])
setXlsxFileName("") setXlsxFileName("")
setXlsxSelectedCol(null) setXlsxSelectedCol(null)
fetchStudents()
emitDataUpdated("students")
} finally { } finally {
setXlsxLoading(false) 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> = [ const columns: ColumnsType<student> = [
{ {
title: t("students.avatar"), title: t("students.avatar"),
@@ -600,6 +639,19 @@ export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
destroyOnHidden destroyOnHidden
> >
<Space orientation="vertical" style={{ width: "100%" }}> <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 <Button
loading={xlsxLoading} loading={xlsxLoading}
disabled={!canEdit} disabled={!canEdit}
+3
View File
@@ -369,6 +369,9 @@
"tagSaveSuccess": "Tags saved successfully", "tagSaveSuccess": "Tags saved successfully",
"tagSaveFailed": "Failed to save tags", "tagSaveFailed": "Failed to save tags",
"importTitle": "Import List", "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", "importByXlsx": "Import via xlsx",
"xlsxPreview": "XLSX Preview & Import", "xlsxPreview": "XLSX Preview & Import",
"file": "File", "file": "File",
+3
View File
@@ -369,6 +369,9 @@
"tagSaveSuccess": "标签保存成功", "tagSaveSuccess": "标签保存成功",
"tagSaveFailed": "保存标签失败", "tagSaveFailed": "保存标签失败",
"importTitle": "导入名单", "importTitle": "导入名单",
"importTextHint": "支持粘贴多行文本,一行一个姓名",
"importTextPlaceholder": "例如:\n张三\n李四\n王五",
"importByText": "从文本导入",
"importByXlsx": "通过 xlsx 导入", "importByXlsx": "通过 xlsx 导入",
"xlsxPreview": "xlsx 预览与导入", "xlsxPreview": "xlsx 预览与导入",
"file": "文件", "file": "文件",