mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-21 11:49:02 +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 [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}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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": "文件",
|
||||||
|
|||||||
Reference in New Issue
Block a user