## 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 完整记录所有新增表、模块、路由、权限、依赖关系
174 lines
4.1 KiB
TypeScript
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)
|
|
}
|