import * as ExcelJS from 'exceljs'; export interface StudentImportRow { name: string; studentNo?: string; phone?: string; idNumber?: string; gender?: string; ethnicity?: string; emergencyContact?: string; emergencyPhone?: string; organization?: string; supervisor?: string; organizationId?: number; targetCollege?: string; targetMajor?: string; collegeSchool?: string; collegeMajor?: string; subjectDirection?: string; grade?: string; profileDate?: string; notes?: string; cultureFinalScore?: number; professionalFinalScore?: number; admissionStatus?: string; admittedCollege?: string; admittedMajor?: string; } export interface StudentEnrollmentImportRow { phone: string; name?: string; courseCategory: string; classType: string; className?: string; headTeacher?: string; subjectTeacher?: string; startDate?: string; endDate?: string; status?: string; } export interface ExamScoreImportRow { phone: string; name?: string; examType: string; examName?: string; subject: string; score?: number; classAvg?: number; rank?: number; examDate?: string; enrollmentName?: string; } export interface LearningRecordImportRow { phone: string; name?: string; recordDate: string; recordType: string; content: string; followUpMethod?: string; nextStep?: string; } export interface StudentWorkbookImport { students: StudentImportRow[]; enrollments: StudentEnrollmentImportRow[]; examScores: ExamScoreImportRow[]; learningRecords: LearningRecordImportRow[]; } type ColumnDef = { header: string; key: keyof T; width: number; aliases?: string[]; kind?: 'text' | 'date' | 'number' | 'integer'; }; export const STUDENT_IMPORT_COLUMNS: ColumnDef[] = [ { header: '手机号*', key: 'phone', width: 18, aliases: ['手机号', '电话'] }, { header: '姓名*', key: 'name', width: 15, aliases: ['姓名'] }, { header: '学号', key: 'studentNo', width: 15 }, { header: '性别', key: 'gender', width: 8 }, { header: '身份证号', key: 'idNumber', width: 22, aliases: ['身份证', '学号/身份证'] }, { header: '民族', key: 'ethnicity', width: 10 }, { header: '紧急联系人', key: 'emergencyContact', width: 15 }, { header: '紧急联系人电话', key: 'emergencyPhone', width: 18 }, { header: '所属机构', key: 'organization', width: 18, aliases: ['所属机构名称'] }, { header: '负责人', key: 'supervisor', width: 15, aliases: ['负责人/班主任'] }, { header: '目标院校', key: 'targetCollege', width: 18 }, { header: '目标专业', key: 'targetMajor', width: 22 }, { header: '大专院校', key: 'collegeSchool', width: 18 }, { header: '大专专业', key: 'collegeMajor', width: 22 }, { header: '选科方向', key: 'subjectDirection', width: 14 }, { header: '年级', key: 'grade', width: 10 }, { header: '建档日期', key: 'profileDate', width: 14, kind: 'date' }, { header: '档案备注', key: 'notes', width: 28 }, { header: '文化课最终分', key: 'cultureFinalScore', width: 14, kind: 'number' }, { header: '专业课最终分', key: 'professionalFinalScore', width: 14, kind: 'number' }, { header: '录取状态', key: 'admissionStatus', width: 14 }, { header: '录取院校', key: 'admittedCollege', width: 18 }, { header: '录取专业', key: 'admittedMajor', width: 22 }, ]; export const STUDENT_EXPORT_COLUMNS = [ ...STUDENT_IMPORT_COLUMNS, ]; const ENROLLMENT_IMPORT_COLUMNS: ColumnDef[] = [ { header: '手机号*', key: 'phone', width: 18, aliases: ['手机号', '电话'] }, { header: '姓名', key: 'name', width: 15 }, { header: '课程类别*', key: 'courseCategory', width: 14, aliases: ['课程类别'] }, { header: '班型*', key: 'classType', width: 14, aliases: ['班型'] }, { header: '班级名称', key: 'className', width: 20 }, { header: '班主任', key: 'headTeacher', width: 15 }, { header: '任课教师', key: 'subjectTeacher', width: 15 }, { header: '开始日期', key: 'startDate', width: 14, kind: 'date' }, { header: '结束日期', key: 'endDate', width: 14, kind: 'date' }, { header: '状态', key: 'status', width: 12 }, ]; const EXAM_SCORE_IMPORT_COLUMNS: ColumnDef[] = [ { header: '手机号*', key: 'phone', width: 18, aliases: ['手机号', '电话'] }, { header: '姓名', key: 'name', width: 15 }, { header: '考试类型*', key: 'examType', width: 14, aliases: ['考试类型'] }, { header: '考试名称', key: 'examName', width: 20 }, { header: '科目*', key: 'subject', width: 14, aliases: ['科目'] }, { header: '成绩*', key: 'score', width: 12, aliases: ['成绩'], kind: 'number' }, { header: '班级均分', key: 'classAvg', width: 12, kind: 'number' }, { header: '排名', key: 'rank', width: 10, kind: 'integer' }, { header: '考试日期', key: 'examDate', width: 14, kind: 'date' }, { header: '关联报读(班级名)', key: 'enrollmentName', width: 22, aliases: ['关联报读(班级名)'] }, ]; const LEARNING_RECORD_IMPORT_COLUMNS: ColumnDef[] = [ { header: '手机号*', key: 'phone', width: 18, aliases: ['手机号', '电话'] }, { header: '姓名', key: 'name', width: 15 }, { header: '记录日期*', key: 'recordDate', width: 14, aliases: ['记录日期'], kind: 'date' }, { header: '记录类型*', key: 'recordType', width: 14, aliases: ['记录类型'] }, { header: '内容*', key: 'content', width: 36, aliases: ['内容'] }, { header: '跟进方式', key: 'followUpMethod', width: 14 }, { header: '下一步计划', key: 'nextStep', width: 28 }, ]; function normalizeHeader(header: string): string { return header.trim().replace(/\*+$/u, '').trim(); } function getCellPrimitiveValue(cell: ExcelJS.Cell): unknown { const value = cell.value; if (value === null || value === undefined) return undefined; if (value instanceof Date) return value; if (typeof value === 'object') { if ('result' in value) return value.result; if ('text' in value) return value.text; if ('richText' in value && Array.isArray(value.richText)) { return value.richText.map((part) => part.text).join(''); } } return value; } function cellToText(cell: ExcelJS.Cell): string { const value = getCellPrimitiveValue(cell); if (value === null || value === undefined) return ''; if (value instanceof Date) return formatDate(value); return String(value).trim(); } function formatDate(date: Date): string { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; } function excelSerialToDate(serial: number): string | undefined { if (!Number.isFinite(serial) || serial < 1) return undefined; const epoch = Date.UTC(1899, 11, 30); return formatDate(new Date(epoch + serial * 24 * 60 * 60 * 1000)); } function parseDateText(cell: ExcelJS.Cell): string | undefined { const value = getCellPrimitiveValue(cell); if (value instanceof Date) return formatDate(value); if (typeof value === 'number') return excelSerialToDate(value); const text = value === null || value === undefined ? '' : String(value).trim(); if (!text) return undefined; const normalized = text.replace(/[/.]/g, '-'); const match = normalized.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/u); if (!match) return text; return `${match[1]}-${match[2].padStart(2, '0')}-${match[3].padStart(2, '0')}`; } function parseNumber(cell: ExcelJS.Cell): number | undefined { const text = cellToText(cell).replace(/,/g, ''); if (!text) return undefined; const value = Number(text); return Number.isFinite(value) ? value : undefined; } function buildHeaderMap(ws: ExcelJS.Worksheet, columns: ColumnDef[]) { const columnByHeader = new Map>(); for (const column of columns) { columnByHeader.set(normalizeHeader(column.header), column); for (const alias of column.aliases || []) { columnByHeader.set(normalizeHeader(alias), column); } } const headerIndex = new Map>(); ws.getRow(1).eachCell((cell, colNumber) => { const column = columnByHeader.get(normalizeHeader(cellToText(cell))); if (column) headerIndex.set(colNumber, column); }); return headerIndex; } function parseWorksheetRows( ws: ExcelJS.Worksheet | undefined, columns: ColumnDef[], ): T[] { if (!ws) return []; const headerIndex = buildHeaderMap(ws, columns); const rows: T[] = []; ws.eachRow((row, idx) => { if (idx === 1) return; const parsed: Partial> = {}; headerIndex.forEach((column, colNumber) => { const cell = row.getCell(colNumber); const value = column.kind === 'date' ? parseDateText(cell) : column.kind === 'number' || column.kind === 'integer' ? parseNumber(cell) : cellToText(cell); if (value !== undefined && value !== '') { parsed[column.key] = column.kind === 'integer' && typeof value === 'number' ? Math.trunc(value) : value; } }); if (Object.keys(parsed).length > 0) rows.push(parsed as T); }); return rows; } function findWorksheet(workbook: ExcelJS.Workbook, names: string[], fallbackIndex?: number) { for (const name of names) { const sheet = workbook.getWorksheet(name); if (sheet) return sheet; } return fallbackIndex === undefined ? undefined : workbook.worksheets[fallbackIndex]; } export function parseStudentImportWorkbook(workbook: ExcelJS.Workbook): StudentWorkbookImport { return { students: parseWorksheetRows( findWorksheet(workbook, ['学生基础+档案+录取', '学生导入模板', '学生名单'], 0), STUDENT_IMPORT_COLUMNS, ).map((row) => ({ ...row, name: row.name || '', phone: row.phone || '' })), enrollments: parseWorksheetRows( findWorksheet(workbook, ['报读班型']), ENROLLMENT_IMPORT_COLUMNS, ), examScores: parseWorksheetRows( findWorksheet(workbook, ['考试成绩']), EXAM_SCORE_IMPORT_COLUMNS, ), learningRecords: parseWorksheetRows( findWorksheet(workbook, ['课堂回访']), LEARNING_RECORD_IMPORT_COLUMNS, ), }; } function applyHeaderStyle(ws: ExcelJS.Worksheet) { ws.getRow(1).font = { bold: true }; ws.getRow(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE0E0E0' } }; } function setupWorksheet(workbook: ExcelJS.Workbook, name: string, columns: ColumnDef[]) { const ws = workbook.addWorksheet(name); ws.columns = columns.map(({ header, key, width }) => ({ header, key: String(key), width })); applyHeaderStyle(ws); return ws; } export function createStudentImportTemplateWorkbook(): ExcelJS.Workbook { const workbook = new ExcelJS.Workbook(); setupWorksheet(workbook, '学生基础+档案+录取', STUDENT_IMPORT_COLUMNS).addRow({ phone: '13800138000', name: '张三', studentNo: '2024001', gender: '男', idNumber: '11010120060101001X', ethnicity: '汉族', emergencyContact: '张父', emergencyPhone: '13900139000', organization: '北京校区', supervisor: '李老师', targetCollege: '北京大学', targetMajor: '计算机科学与技术', collegeSchool: '北京职业技术学院', collegeMajor: '软件技术', subjectDirection: '物化生', grade: '高三', profileDate: '2024-09-01', notes: '学习态度积极,目标明确', cultureFinalScore: 620, professionalFinalScore: 580, admissionStatus: 'pending', admittedCollege: '', admittedMajor: '', }); setupWorksheet(workbook, '报读班型', ENROLLMENT_IMPORT_COLUMNS); setupWorksheet(workbook, '考试成绩', EXAM_SCORE_IMPORT_COLUMNS); setupWorksheet(workbook, '课堂回访', LEARNING_RECORD_IMPORT_COLUMNS); return workbook; }