- AI resolve 生成向导时按阶段携带全部 sheetNames,不再只取第一张表 - ImportStageRequest 支持 sheets 数组并兼容旧 sheet;手动重传同步修复 - headerMatches 收窄为单向包含,避免宿舍号被原/新宿舍号反向匹配 - suggestStep 增加入住/换宿显式表头信号,修复入住表误判为换宿 - 预检与预览按工作表逐表解析列映射,兼容异构表头 - 修复预检卡生成向导成功后按钮未复位 loading 的问题 - 补充 mapping/预检/run/ai-chat 多工作表测试
98 lines
3.1 KiB
TypeScript
98 lines
3.1 KiB
TypeScript
import * as ExcelJS from 'exceljs';
|
||
import type { CellValue } from './imports.types';
|
||
|
||
export function parseJson<T>(raw: string | null | undefined): T | null {
|
||
if (!raw) return null;
|
||
try {
|
||
return JSON.parse(raw) as T;
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
export function textValue(value: CellValue): string {
|
||
if (value === null || value === undefined) return '';
|
||
return String(value).trim();
|
||
}
|
||
|
||
export function normalizeHeader(value: string): string {
|
||
return value
|
||
.trim()
|
||
.toLowerCase()
|
||
.replace(/[\s()()]/g, '');
|
||
}
|
||
|
||
export function headerMatches(header: string, alias: string): boolean {
|
||
const h = normalizeHeader(header);
|
||
const a = normalizeHeader(alias);
|
||
if (!h || !a) return false;
|
||
// 只允许“表头包含别名”的单向匹配,避免短表头(如“宿舍号”)
|
||
// 被长复合别名(如“原宿舍号/新宿舍号”)反向包含而误判。
|
||
return h === a || h.includes(a);
|
||
}
|
||
|
||
export function cellValue(cell: ExcelJS.Cell | undefined): CellValue {
|
||
if (!cell) return null;
|
||
const value = cell.value;
|
||
if (value === null || value === undefined) return null;
|
||
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
||
return value;
|
||
}
|
||
if (value instanceof Date) return value;
|
||
if (typeof value === 'object') {
|
||
const candidate = value as { text?: unknown; result?: unknown };
|
||
if (typeof candidate.text === 'string') return candidate.text;
|
||
if (typeof candidate.result === 'string' || typeof candidate.result === 'number') {
|
||
return candidate.result;
|
||
}
|
||
if (candidate.result instanceof Date) return candidate.result;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
export function parseDateValue(value: CellValue): string | null {
|
||
if (value instanceof Date && !Number.isNaN(value.getTime())) {
|
||
const year = value.getFullYear();
|
||
const month = String(value.getMonth() + 1).padStart(2, '0');
|
||
const day = String(value.getDate()).padStart(2, '0');
|
||
return `${year}-${month}-${day}`;
|
||
}
|
||
const raw = textValue(value);
|
||
if (!raw) return null;
|
||
const match = /^(\d{4})[-/](\d{1,2})[-/](\d{1,2})/.exec(raw);
|
||
if (!match) return null;
|
||
const year = Number(match[1]);
|
||
const month = Number(match[2]);
|
||
const day = Number(match[3]);
|
||
const date = new Date(Date.UTC(year, month - 1, day));
|
||
if (
|
||
date.getUTCFullYear() !== year ||
|
||
date.getUTCMonth() !== month - 1 ||
|
||
date.getUTCDate() !== day
|
||
) {
|
||
return null;
|
||
}
|
||
return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
||
}
|
||
|
||
export function safeError(error: unknown): string {
|
||
if (error instanceof Error) return error.message.slice(0, 120);
|
||
return '未知错误';
|
||
}
|
||
|
||
export function applyString(target: object, key: string, value: CellValue): void {
|
||
const text = textValue(value);
|
||
if (text) (target as Record<string, unknown>)[key] = text;
|
||
}
|
||
|
||
export function optionalNumber(value: CellValue): number | null {
|
||
const text = textValue(value);
|
||
if (!text) return null;
|
||
const parsed = Number(text);
|
||
return Number.isFinite(parsed) ? parsed : null;
|
||
}
|
||
|
||
export function csvCell(value: string): string {
|
||
return `"${value.replace(/"/g, '""')}"`;
|
||
}
|