feat: add column visibility toggle with localStorage persistence
Add a "列设置" dropdown next to the results header that lets users check/uncheck columns to show or hide them. Preferences persist across page refreshes via localStorage. Also add allowedDevOrigins to next.config.ts for LAN access. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
95
app/page.tsx
95
app/page.tsx
@@ -4,6 +4,7 @@ import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import {
|
||||
Search, ChevronLeft, ChevronRight, ChevronUp, ChevronDown, X,
|
||||
CheckCircle2, Circle, ArrowUpDown, Boxes, SlidersHorizontal, Loader2,
|
||||
Settings2, Check,
|
||||
} from "lucide-react";
|
||||
import type { WarehouseRow, QueryResult, FilterOptions } from "@/types/warehouse";
|
||||
|
||||
@@ -107,6 +108,39 @@ export default function WarehouseDashboard() {
|
||||
const [data, setData] = useState<QueryResult | null>(null);
|
||||
const [filterOptions, setFilterOptions] = useState<FilterOptions | null>(null);
|
||||
|
||||
/* ── Column visibility ── */
|
||||
const ALL_COL_KEYS = COLUMNS.map((c) => c.key);
|
||||
const [visibleKeys, setVisibleKeys] = useState<Set<string>>(() => new Set(ALL_COL_KEYS));
|
||||
const [showColumnConfig, setShowColumnConfig] = useState(false);
|
||||
const columnConfigRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
/* Hydrate from localStorage after mount to avoid SSR mismatch */
|
||||
useEffect(() => {
|
||||
try {
|
||||
const saved = localStorage.getItem("warehouse-visible-columns");
|
||||
if (saved) setVisibleKeys(new Set(JSON.parse(saved) as string[]));
|
||||
} catch { /* ignore */ }
|
||||
}, []);
|
||||
|
||||
const visibleColumns = COLUMNS.filter((c) => visibleKeys.has(c.key));
|
||||
|
||||
/* Persist column visibility to localStorage */
|
||||
useEffect(() => {
|
||||
localStorage.setItem("warehouse-visible-columns", JSON.stringify([...visibleKeys]));
|
||||
}, [visibleKeys]);
|
||||
|
||||
/* Close column config on outside click */
|
||||
useEffect(() => {
|
||||
if (!showColumnConfig) return;
|
||||
const handler = (e: MouseEvent) => {
|
||||
if (columnConfigRef.current && !columnConfigRef.current.contains(e.target as Node)) {
|
||||
setShowColumnConfig(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", handler);
|
||||
return () => document.removeEventListener("mousedown", handler);
|
||||
}, [showColumnConfig]);
|
||||
|
||||
const searchTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
/* Fetch filter options on mount */
|
||||
@@ -309,7 +343,62 @@ export default function WarehouseDashboard() {
|
||||
{/* ── 查询结果 ── */}
|
||||
<section className="overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 border-b border-slate-200 px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="text-sm font-semibold text-slate-700">查询结果</h2>
|
||||
<div className="relative" ref={columnConfigRef}>
|
||||
<button
|
||||
onClick={() => setShowColumnConfig((v) => !v)}
|
||||
className={`grid h-7 w-7 place-items-center rounded-lg border text-slate-500 transition hover:bg-slate-100 ${showColumnConfig ? "border-blue-300 bg-blue-50 text-blue-600" : "border-slate-200"}`}
|
||||
title="列设置"
|
||||
>
|
||||
<Settings2 className="h-4 w-4" />
|
||||
</button>
|
||||
{showColumnConfig && (
|
||||
<div className="absolute left-0 top-full z-30 mt-1 w-52 rounded-xl border border-slate-200 bg-white py-2 shadow-lg">
|
||||
<div className="max-h-72 overflow-y-auto px-1">
|
||||
{COLUMNS.map((col) => (
|
||||
<label
|
||||
key={col.key}
|
||||
className="flex cursor-pointer items-center gap-2 rounded-lg px-3 py-1.5 text-sm text-slate-700 hover:bg-slate-50"
|
||||
>
|
||||
<span className={`grid h-4 w-4 shrink-0 place-items-center rounded border ${visibleKeys.has(col.key) ? "border-blue-500 bg-blue-500 text-white" : "border-slate-300 bg-white"}`}>
|
||||
{visibleKeys.has(col.key) && <Check className="h-3 w-3" strokeWidth={3} />}
|
||||
</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={visibleKeys.has(col.key)}
|
||||
onChange={() => {
|
||||
setVisibleKeys((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(col.key)) next.delete(col.key);
|
||||
else next.add(col.key);
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
className="sr-only"
|
||||
/>
|
||||
{col.label}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex gap-2 border-t border-slate-100 px-3 pt-2">
|
||||
<button
|
||||
onClick={() => setVisibleKeys(new Set(ALL_COL_KEYS))}
|
||||
className="flex-1 rounded-lg bg-slate-100 px-2 py-1.5 text-xs font-medium text-slate-600 hover:bg-slate-200"
|
||||
>
|
||||
全选
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setVisibleKeys(new Set())}
|
||||
className="flex-1 rounded-lg bg-slate-100 px-2 py-1.5 text-xs font-medium text-slate-600 hover:bg-slate-200"
|
||||
>
|
||||
取消全选
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-xs text-slate-400">数据来源:生产合同 + 装箱明细 + 库位 + 入库记录(LEFT JOIN)</span>
|
||||
</div>
|
||||
|
||||
@@ -333,7 +422,7 @@ export default function WarehouseDashboard() {
|
||||
<table className="w-full border-collapse text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
{COLUMNS.map((col) => (
|
||||
{visibleColumns.map((col) => (
|
||||
<th
|
||||
key={col.key}
|
||||
onClick={() => toggleSort(col.key)}
|
||||
@@ -354,7 +443,7 @@ export default function WarehouseDashboard() {
|
||||
<tbody>
|
||||
{!loading && rows.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={COLUMNS.length + 1} className="px-4 py-16 text-center">
|
||||
<td colSpan={visibleColumns.length + 1} className="px-4 py-16 text-center">
|
||||
<div className="flex flex-col items-center gap-2 text-slate-400">
|
||||
<Search className="h-8 w-8" />
|
||||
<div className="text-sm">未找到匹配的记录</div>
|
||||
@@ -368,7 +457,7 @@ export default function WarehouseDashboard() {
|
||||
key={`${r.zongpai}-${r.boxNo}-${i}`}
|
||||
className={`transition-colors ${i % 2 === 0 ? "bg-white hover:bg-slate-50" : "bg-blue-50 hover:bg-blue-100"}`}
|
||||
>
|
||||
{COLUMNS.map((col) => {
|
||||
{visibleColumns.map((col) => {
|
||||
const raw = r[col.key as keyof WarehouseRow];
|
||||
const empty = raw === "" || raw == null;
|
||||
const align = col.align === "right" ? "text-right" : "text-left";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
allowedDevOrigins: ["192.168.110.146"],
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
Reference in New Issue
Block a user