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:
954
package-lock.json
generated
954
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
139
src/app/api/export-excel/route.ts
Normal file
139
src/app/api/export-excel/route.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 }}>
|
||||
|
||||
Reference in New Issue
Block a user