Add Excel export feature for full production data

Add "导出全部" button that exports all contract production data to an
.xlsx file via a new API endpoint using exceljs, with formatted headers,
date columns, auto-width, and frozen header row.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Misaka_Company
2026-06-10 17:35:48 +08:00
parent 2c66faf207
commit 8235a29f82
4 changed files with 1128 additions and 7 deletions

954
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -14,6 +14,7 @@
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"antd": "^6.4.3",
"exceljs": "^4.4.0",
"mssql": "^12.5.5",
"next": "16.2.9",
"react": "19.2.4",

View File

@@ -0,0 +1,139 @@
import { NextResponse } from "next/server";
import sql from "mssql";
import ExcelJS from "exceljs";
const dbConfig = {
server: process.env.DB_SERVER!,
port: 1433,
database: process.env.DB_DATABASE!,
user: process.env.DB_USER!,
password: process.env.DB_PASSWORD!,
options: {
trustServerCertificate: process.env.DB_TRUST_CERT === "true",
encrypt: false,
},
};
const DATE_COLUMNS = new Set([
"签订日期",
"交货日期",
"接单日期",
"执行卡下发日期",
"焊接领料日期",
"领料单签收日期",
"库房发出日期",
"焊接接收日期",
"日期",
"超压日期",
"退火日期",
"氦测日期",
"表壳焊接日期",
"隔膜接收",
"隔离膜片接收",
"车波纹日期",
"膜片焊",
"喷涂发出",
"喷涂回来",
"调试日期",
"检验日期",
"入库日期",
"烘洗",
]);
export async function GET() {
let pool: sql.ConnectionPool | undefined;
try {
pool = await sql.connect(dbConfig);
const result = await pool
.request()
.execute("[productionContractData].[sp_压力表合同生产数据_全部]");
const recordset = result.recordset;
if (!recordset || recordset.length === 0) {
return NextResponse.json({ error: "没有数据可导出" }, { status: 404 });
}
const columns = Object.keys(recordset[0]);
const workbook = new ExcelJS.Workbook();
const sheet = workbook.addWorksheet("压力表合同生产数据");
// Header row
const headerRow = sheet.addRow(columns);
headerRow.eachCell((cell) => {
cell.font = { bold: true };
cell.fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FFE0EAF6" },
};
cell.alignment = { horizontal: "center" };
});
// Data rows
for (const row of recordset) {
const values = columns.map((col) => {
const val = row[col];
if (val instanceof Date) {
return val;
}
if (val === null || val === undefined) return "";
return val;
});
sheet.addRow(values);
}
// Format date columns and auto-width
const dateColIndices: number[] = [];
const maxWidths: number[] = columns.map((col) => col.length);
columns.forEach((col, idx) => {
if (DATE_COLUMNS.has(col)) {
dateColIndices.push(idx + 1);
}
});
sheet.eachRow((row, rowNumber) => {
row.eachCell({ includeEmpty: false }, (cell, colNumber) => {
if (rowNumber > 1 && dateColIndices.includes(colNumber)) {
if (cell.value instanceof Date) {
cell.numFmt = "yyyy/mm/dd";
}
}
// Track max width
const text = cell.text || "";
const width = Math.max(maxWidths[colNumber - 1] || 0, text.length + 2);
maxWidths[colNumber - 1] = width;
});
});
// Apply column widths (capped between 8 and 50)
columns.forEach((_, idx) => {
sheet.getColumn(idx + 1).width = Math.min(Math.max(maxWidths[idx], 8), 50);
});
// Freeze header row
sheet.views = [{ state: "frozen", ySplit: 1 }];
// Stream to buffer
const buffer = await workbook.xlsx.writeBuffer();
return new NextResponse(buffer, {
status: 200,
headers: {
"Content-Type":
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"Content-Disposition":
"attachment; filename*=UTF-8''" + encodeURIComponent("压力表合同生产数据.xlsx"),
},
});
} catch (err) {
const message = err instanceof Error ? err.message : "Unknown error";
console.error("Excel export error:", message);
return NextResponse.json({ error: message }, { status: 500 });
} finally {
if (pool) {
await pool.close();
}
}
}

View File

@@ -14,7 +14,7 @@ import {
Checkbox,
AutoComplete,
} from "antd";
import { SearchOutlined, SettingOutlined, HolderOutlined, ClockCircleOutlined } from "@ant-design/icons";
import { SearchOutlined, SettingOutlined, HolderOutlined, ClockCircleOutlined, DownloadOutlined } from "@ant-design/icons";
import type { ColumnsType } from "antd/es/table";
import {
DndContext,
@@ -333,6 +333,8 @@ function SearchBar({
onSearch,
onClear,
onOpenColumnSettings,
exporting,
onExport,
}: {
loading: boolean;
resultCount: number;
@@ -340,6 +342,8 @@ function SearchBar({
onSearch: (workshopNo: string) => Promise<void>;
onClear: () => void;
onOpenColumnSettings: () => void;
exporting: boolean;
onExport: () => void;
}) {
const [inputValue, setInputValue] = useState("");
const [history, setHistory] = useState<string[]>(() => loadHistory());
@@ -418,6 +422,13 @@ function SearchBar({
<Button icon={<SettingOutlined />} onClick={onOpenColumnSettings}>
</Button>
<Button
icon={<DownloadOutlined />}
onClick={onExport}
loading={exporting}
>
</Button>
</Space>
{searched && !loading && (
<Text type="secondary" style={{ marginLeft: 16 }}>
@@ -438,6 +449,7 @@ export default function Home() {
const [searched, setSearched] = useState(false);
const [columnConfig, setColumnConfig] = useState<ColumnConfig[]>(loadConfig);
const [modalOpen, setModalOpen] = useState(false);
const [exporting, setExporting] = useState(false);
// Persist config
useEffect(() => {
@@ -473,6 +485,31 @@ export default function Home() {
setError(null);
}, []);
const handleExport = useCallback(async () => {
setExporting(true);
try {
const res = await fetch("/api/export-excel");
if (!res.ok) {
const json = await res.json().catch(() => null);
setError(json?.error || "导出失败");
return;
}
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "压力表合同生产数据.xlsx";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
} catch {
setError("导出请求失败");
} finally {
setExporting(false);
}
}, []);
const handleApplyConfig = useCallback((newConfig: ColumnConfig[]) => {
setColumnConfig(newConfig);
setModalOpen(false);
@@ -500,6 +537,8 @@ export default function Home() {
onSearch={handleSearch}
onClear={handleClear}
onOpenColumnSettings={() => setModalOpen(true)}
exporting={exporting}
onExport={handleExport}
/>
<div style={{ flex: 1, minHeight: 0 }}>