mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-19 19:04:23 +08:00
学生管理支持多行文本导入名单
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -369,6 +369,9 @@
|
||||
"tagSaveSuccess": "标签保存成功",
|
||||
"tagSaveFailed": "保存标签失败",
|
||||
"importTitle": "导入名单",
|
||||
"importTextHint": "支持粘贴多行文本,一行一个姓名",
|
||||
"importTextPlaceholder": "例如:\n张三\n李四\n王五",
|
||||
"importByText": "从文本导入",
|
||||
"importByXlsx": "通过 xlsx 导入",
|
||||
"xlsxPreview": "xlsx 预览与导入",
|
||||
"file": "文件",
|
||||
|
||||
Reference in New Issue
Block a user