Files
NextEdu/src/shared/lib/excel.ts
SpecialX 3b6272c99d feat: 完成 P1 全部功能 + 修复 proxy 导出 + 切换 MySQL 端口至 14013
## P1 功能(20 项)
- 站内消息系统、家长仪表盘、学生考勤管理
- Excel 导入导出、用户批量导入、成绩导出
- 排课规则+自动排课+课表调整
- 成绩趋势+对比分析、密码安全策略、速率限制
- 数据变更日志、文件预览+存储策略、全文检索
- 依赖审计集成 CI、数据库定时备份、E2E 测试完善
- 通知偏好管理

## 基础设施修复
- src/proxy.ts: 将 middleware 导出重命名为 proxy(Next.js 16 要求)
- .env: MySQL 端口从 13002 切换至 14013
- scripts/create-db.ts: 新增数据库初始化脚本

## 架构文档同步
- 004_architecture_impact_map.md 和 005_architecture_data.json
  完整记录所有新增表、模块、路由、权限、依赖关系
2026-06-17 13:44:37 +08:00

174 lines
4.1 KiB
TypeScript

import "server-only"
import ExcelJS from "exceljs"
export type ExcelColumn = {
header: string
key: string
width?: number
}
export type ExcelSheet = {
name: string
columns: ExcelColumn[]
rows: Record<string, unknown>[]
}
export type TemplateColumn = ExcelColumn & {
note?: string
}
export type TemplateSheet = {
name: string
columns: TemplateColumn[]
sampleRows?: Record<string, unknown>[]
}
export type ParsedSheet = {
sheetName: string
rows: Record<string, unknown>[]
}
/**
* 导出数据到 Excel Buffer
*/
export async function exportToExcel(params: {
sheets: ExcelSheet[]
}): Promise<Buffer> {
const workbook = new ExcelJS.Workbook()
for (const sheet of params.sheets) {
const worksheet = workbook.addWorksheet(sheet.name, {
views: [{ state: "frozen", ySplit: 1 }],
})
worksheet.columns = sheet.columns.map((col) => ({
header: col.header,
key: col.key,
width: col.width ?? 20,
}))
// Header style
const headerRow = worksheet.getRow(1)
headerRow.font = { bold: true }
headerRow.fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FFE0E7FF" },
}
headerRow.alignment = { vertical: "middle", horizontal: "left" }
for (const row of sheet.rows) {
worksheet.addRow(row)
}
worksheet.autoFilter = {
from: { row: 1, column: 1 },
to: { row: 1, column: sheet.columns.length },
}
}
const arrayBuffer = await workbook.xlsx.writeBuffer()
return Buffer.from(arrayBuffer)
}
/**
* 从 Buffer 解析 Excel
*/
export async function parseExcel(buffer: Buffer): Promise<ParsedSheet[]> {
const workbook = new ExcelJS.Workbook()
await workbook.xlsx.load(buffer as unknown as ArrayBuffer)
const result: ParsedSheet[] = []
workbook.worksheets.forEach((worksheet) => {
if (worksheet.rowCount === 0) {
result.push({ sheetName: worksheet.name, rows: [] })
return
}
const headerRow = worksheet.getRow(1)
const headers: string[] = []
const keys: string[] = []
headerRow.eachCell((cell, colNumber) => {
const header = String(cell.value ?? "").trim()
headers.push(header)
keys.push(header || `column_${colNumber}`)
})
const rows: Record<string, unknown>[] = []
for (let rowNum = 2; rowNum <= worksheet.rowCount; rowNum++) {
const row = worksheet.getRow(rowNum)
const record: Record<string, unknown> = {}
let hasValue = false
keys.forEach((key, idx) => {
const cell = row.getCell(idx + 1)
const value = cell.value
if (value !== null && value !== undefined && value !== "") {
record[key] = typeof value === "object" && "text" in value
? String((value as { text: string }).text)
: value
hasValue = true
} else {
record[key] = ""
}
})
if (hasValue) rows.push(record)
}
result.push({ sheetName: worksheet.name, rows })
})
return result
}
/**
* 生成导入模板 Buffer
*/
export async function generateTemplate(params: {
sheets: TemplateSheet[]
}): Promise<Buffer> {
const workbook = new ExcelJS.Workbook()
for (const sheet of params.sheets) {
const worksheet = workbook.addWorksheet(sheet.name)
worksheet.columns = sheet.columns.map((col) => ({
header: col.header,
key: col.key,
width: col.width ?? 22,
}))
const headerRow = worksheet.getRow(1)
headerRow.font = { bold: true }
headerRow.fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FFE0E7FF" },
}
// Notes row (row 2)
const noteRow = worksheet.getRow(2)
sheet.columns.forEach((col, idx) => {
const cell = noteRow.getCell(idx + 1)
cell.value = col.note ?? ""
cell.font = { italic: true, color: { argb: "FF6B7280" } }
})
// Sample rows (starting from row 3)
const sampleRows = sheet.sampleRows ?? []
sampleRows.forEach((row) => worksheet.addRow(row))
// Empty row for user input
if (sampleRows.length === 0) {
worksheet.addRow({})
}
}
const arrayBuffer = await workbook.xlsx.writeBuffer()
return Buffer.from(arrayBuffer)
}