From 5ea59a77d412c7f7ddf05d102a85c5152183dea1 Mon Sep 17 00:00:00 2001 From: JSR Date: Wed, 18 Mar 2026 18:32:14 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AD=A6=E7=94=9F=E7=AE=A1=E7=90=86=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E5=A4=9A=E8=A1=8C=E6=96=87=E6=9C=AC=E5=AF=BC=E5=85=A5?= =?UTF-8?q?=E5=90=8D=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/StudentManager.tsx | 80 +++++++++++++++++++++++++------ src/i18n/locales/en-US.json | 3 ++ src/i18n/locales/zh-CN.json | 3 ++ 3 files changed, 72 insertions(+), 14 deletions(-) diff --git a/src/components/StudentManager.tsx b/src/components/StudentManager.tsx index c83e4f3..a58b669 100644 --- a/src/components/StudentManager.tsx +++ b/src/components/StudentManager.tsx @@ -39,9 +39,11 @@ export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => { const [avatarValue, setAvatarValue] = useState(null) const avatarInputRef = useRef(null) const [xlsxLoading, setXlsxLoading] = useState(false) + const [textImportLoading, setTextImportLoading] = useState(false) const [xlsxFileName, setXlsxFileName] = useState("") const [xlsxAoa, setXlsxAoa] = useState([]) const [xlsxSelectedCol, setXlsxSelectedCol] = useState(null) + const [textImportValue, setTextImportValue] = useState("") const xlsxInputRef = useRef(null) const xlsxWorkerRef = useRef(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() 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 = [ { title: t("students.avatar"), @@ -600,6 +639,19 @@ export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => { destroyOnHidden > +
+ {t("students.importTextHint")} +
+ setTextImportValue(e.target.value)} + rows={8} + placeholder={t("students.importTextPlaceholder")} + disabled={!canEdit || textImportLoading} + /> +