"use client"; import { useCallback, useEffect, useMemo, useState } from "react"; import { Button, Input, Table, Space, Spin, Alert, Typography, Empty, Modal, Checkbox, AutoComplete, } from "antd"; import { SearchOutlined, SettingOutlined, HolderOutlined, ClockCircleOutlined, DownloadOutlined } from "@ant-design/icons"; import type { ColumnsType } from "antd/es/table"; import { DndContext, closestCenter, PointerSensor, useSensor, useSensors, } from "@dnd-kit/core"; import type { DragEndEvent } from "@dnd-kit/core"; import { SortableContext, useSortable, verticalListSortingStrategy, arrayMove, } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { nextLockedRow, type DataRow } from "./table-filters"; const { Title, Text } = Typography; interface ColumnConfig { key: string; visible: boolean; } const COLUMN_DEFS: { key: string; width: number }[] = [ { key: "ID", width: 70 }, { key: "总排号", width: 100 }, { key: "序号", width: 60 }, { key: "生产订单号", width: 150 }, { key: "车间号", width: 100 }, { key: "订单号", width: 120 }, { key: "经办人", width: 80 }, { key: "签订日期", width: 110 }, { key: "交货日期", width: 110 }, { key: "客户名称", width: 180 }, { key: "产品型号", width: 200 }, { key: "量程", width: 120 }, { key: "数量", width: 60 }, { key: "技术参数", width: 300 }, { key: "车间", width: 80 }, { key: "工令号", width: 100 }, { key: "备注", width: 150 }, { key: "隔膜类型", width: 100 }, { key: "标准", width: 80 }, { key: "隔膜大小", width: 100 }, { key: "隔膜材质", width: 100 }, { key: "膜片尺寸", width: 100 }, { key: "膜片材质", width: 100 }, { key: "新参数", width: 300 }, { key: "盘号", width: 80 }, { key: "特殊要求", width: 200 }, { key: "位号", width: 80 }, { key: "CRM订单明细ID", width: 160 }, { key: "成品物料码", width: 120 }, { key: "接单日期", width: 110 }, { key: "执行卡下发日期", width: 130 }, { key: "缺件明细", width: 250 }, { key: "物料类别", width: 100 }, { key: "焊接领料日期", width: 130 }, { key: "领料单签收日期", width: 140 }, { key: "库房发出日期", width: 130 }, { key: "焊接接收日期", width: 130 }, { key: "操作者", width: 80 }, { key: "日期", width: 110 }, { key: "超压日期", width: 110 }, { key: "退火日期", width: 110 }, { key: "氦测日期", width: 110 }, { key: "壳焊接员", width: 80 }, { key: "表壳焊接日期", width: 130 }, { key: "隔膜接收", width: 110 }, { key: "隔离膜片接收", width: 140 }, { key: "车波纹日期", width: 110 }, { key: "膜片焊", width: 110 }, { key: "喷涂发出", width: 110 }, { key: "喷涂回来", width: 110 }, { key: "调校人", width: 80 }, { key: "调试日期", width: 110 }, { key: "检验员", width: 80 }, { key: "检验日期", width: 110 }, { key: "入库日期", width: 110 }, { key: "烘洗", width: 110 }, ]; const DEFAULT_CONFIG: ColumnConfig[] = COLUMN_DEFS.map((d) => ({ key: d.key, visible: true, })); const STORAGE_KEY = "web-table-column-config"; function loadConfig(): ColumnConfig[] { try { const saved = localStorage.getItem(STORAGE_KEY); if (saved) { const parsed: ColumnConfig[] = JSON.parse(saved); const savedKeys = new Set(parsed.map((c) => c.key)); if (COLUMN_DEFS.every((d) => savedKeys.has(d.key))) return parsed; } } catch { /* ignore */ } return DEFAULT_CONFIG.map((c) => ({ ...c })); } function saveConfig(config: ColumnConfig[]) { localStorage.setItem(STORAGE_KEY, JSON.stringify(config)); } function buildTableColumns( config: ColumnConfig[], dataColumns: string[] ): ColumnsType { const available = new Set(dataColumns); return config .filter((c) => c.visible && available.has(c.key)) .map((c) => { const def = COLUMN_DEFS.find((d) => d.key === c.key); return { title: c.key, dataIndex: c.key, key: c.key, width: def?.width ?? 120, ellipsis: true, render: (val: string | number | null) => val === null || val === undefined ? "" : String(val), }; }); } /* ─── Sortable Row in Modal ─── */ function SortableRow({ id, label, visible, onToggle, }: { id: string; label: string; visible: boolean; onToggle: () => void; }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id }); const style: React.CSSProperties = { transform: CSS.Transform.toString(transform), transition, display: "flex", alignItems: "center", gap: 8, padding: "6px 8px", borderBottom: "1px solid #f5f5f5", background: isDragging ? "#e6f4ff" : "#fff", cursor: "default", }; return (
{label}
); } /* ─── Column Settings Modal ─── */ function ColumnSettingsModal({ open, config, onApply, onCancel, }: { open: boolean; config: ColumnConfig[]; onApply: (newConfig: ColumnConfig[]) => void; onCancel: () => void; }) { const [local, setLocal] = useState([]); const [search, setSearch] = useState(""); const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 4 } })); // Sync when modal opens useEffect(() => { if (open) { setLocal(config.map((c) => ({ ...c }))); setSearch(""); } }, [open, config]); const filtered = search ? local.filter((c) => c.key.toLowerCase().includes(search.toLowerCase())) : local; const visibleCount = local.filter((c) => c.visible).length; const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event; if (!over || active.id === over.id) return; setLocal((prev) => { const oldIdx = prev.findIndex((c) => c.key === active.id); const newIdx = prev.findIndex((c) => c.key === over.id); return arrayMove(prev, oldIdx, newIdx); }); }; const toggle = (key: string) => { setLocal((prev) => prev.map((c) => (c.key === key ? { ...c, visible: !c.visible } : c)) ); }; const selectAll = () => setLocal((prev) => prev.map((c) => ({ ...c, visible: true }))); const deselectAll = () => setLocal((prev) => prev.map((c) => ({ ...c, visible: false }))); const resetDefault = () => setLocal(DEFAULT_CONFIG.map((c) => ({ ...c }))); return ( 取消 , , ]} >
setSearch(e.target.value)} allowClear style={{ width: 180 }} size="small" />
c.key)} strategy={verticalListSortingStrategy}> {filtered.map((c) => ( toggle(c.key)} /> ))}
); } /* ─── Search History ─── */ const HISTORY_KEY = "web-table-search-history"; const MAX_HISTORY = 10; function loadHistory(): string[] { try { const saved = localStorage.getItem(HISTORY_KEY); return saved ? JSON.parse(saved) : []; } catch { return []; } } function saveHistoryToStorage(history: string[]) { localStorage.setItem(HISTORY_KEY, JSON.stringify(history)); } function addToHistory(value: string, history: string[]): string[] { const filtered = history.filter((h) => h !== value); return [value, ...filtered].slice(0, MAX_HISTORY); } /* ─── Search Bar (isolated) ─── */ function SearchBar({ loading, resultCount, searched, onSearch, onClear, onOpenColumnSettings, exporting, onExport, }: { loading: boolean; resultCount: number; searched: boolean; onSearch: (workshopNo: string) => Promise; onClear: () => void; onOpenColumnSettings: () => void; exporting: boolean; onExport: () => void; }) { const [inputValue, setInputValue] = useState(""); const [history, setHistory] = useState(() => loadHistory()); const [open, setOpen] = useState(false); const filteredHistory = inputValue ? history.filter((h) => h.toLowerCase().includes(inputValue.toLowerCase())) : history; const options = filteredHistory.map((h) => ({ value: h, label: ( {h} ), })); const handleSearch = useCallback(() => { const val = inputValue.trim(); if (!val) return; const updated = addToHistory(val, history); setHistory(updated); saveHistoryToStorage(updated); onSearch(val); }, [inputValue, history, onSearch]); const handleSelect = useCallback( (val: string) => { setInputValue(val); const updated = addToHistory(val, history); setHistory(updated); saveHistoryToStorage(updated); onSearch(val); }, [history, onSearch] ); return (
压力表合同生产数据 0} options={options} value={inputValue} onChange={(val) => setInputValue(val)} onSelect={handleSelect} onFocus={() => setOpen(true)} onBlur={() => setOpen(false)} style={{ width: 320 }} > } placeholder="支持模糊搜索,输入排产号数字部分即可查询" onPressEnter={handleSearch} allowClear /> {searched && !loading && ( 共 {resultCount} 条记录 )}
); } /* ─── Main Page ─── */ export default function Home() { const [data, setData] = useState([]); const [columns, setColumns] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [searched, setSearched] = useState(false); const [columnConfig, setColumnConfig] = useState(loadConfig); const [modalOpen, setModalOpen] = useState(false); const [exporting, setExporting] = useState(false); const [lockedRowKey, setLockedRowKey] = useState(null); // Persist config useEffect(() => { saveConfig(columnConfig); }, [columnConfig]); const handleSearch = useCallback(async (workshopNo: string) => { setLoading(true); setError(null); try { const res = await fetch( `/api/production-data?workshopNo=${encodeURIComponent(workshopNo)}` ); const json = await res.json(); if (!res.ok) { setError(json.error || "查询失败"); return; } setColumns(json.columns); setData(json.data); setSearched(true); setLockedRowKey(null); } catch { setError("网络请求失败"); } finally { setLoading(false); } }, []); const handleClear = useCallback(() => { setData([]); setColumns([]); setSearched(false); setError(null); setLockedRowKey(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); }, []); const tableColumns = useMemo( () => buildTableColumns(columnConfig, columns), [columnConfig, columns] ); const totalWidth = useMemo( () => tableColumns.reduce((sum, col) => sum + (col.width as number), 0), [tableColumns] ); const scrollConfig = useMemo( () => ({ x: totalWidth, y: "calc(100vh - 140px)" }), [totalWidth] ); return (
setModalOpen(true)} exporting={exporting} onExport={handleExport} />
{error && ( )} {loading && (
)} {!loading && !error && !searched && (
)} {!loading && !error && searched && data.length > 0 && ( columns={tableColumns} dataSource={data} rowKey="ID" onRow={(record) => ({ onClick: () => setLockedRowKey((prev) => nextLockedRow(prev, String(record.ID))), })} rowClassName={(record) => String(record.ID) === lockedRowKey ? "row-locked" : "" } bordered size="small" pagination={false} scroll={scrollConfig} sticky /> )} {!loading && !error && searched && data.length === 0 && (
)}
setModalOpen(false)} />
); }