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:
97
app/page.tsx
97
app/page.tsx
@@ -4,6 +4,7 @@ import { useState, useEffect, useCallback, useRef } from "react";
|
|||||||
import {
|
import {
|
||||||
Search, ChevronLeft, ChevronRight, ChevronUp, ChevronDown, X,
|
Search, ChevronLeft, ChevronRight, ChevronUp, ChevronDown, X,
|
||||||
CheckCircle2, Circle, ArrowUpDown, Boxes, SlidersHorizontal, Loader2,
|
CheckCircle2, Circle, ArrowUpDown, Boxes, SlidersHorizontal, Loader2,
|
||||||
|
Settings2, Check,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import type { WarehouseRow, QueryResult, FilterOptions } from "@/types/warehouse";
|
import type { WarehouseRow, QueryResult, FilterOptions } from "@/types/warehouse";
|
||||||
|
|
||||||
@@ -107,6 +108,39 @@ export default function WarehouseDashboard() {
|
|||||||
const [data, setData] = useState<QueryResult | null>(null);
|
const [data, setData] = useState<QueryResult | null>(null);
|
||||||
const [filterOptions, setFilterOptions] = useState<FilterOptions | 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);
|
const searchTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
/* Fetch filter options on mount */
|
/* 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">
|
<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 flex-wrap items-center justify-between gap-2 border-b border-slate-200 px-4 py-3">
|
||||||
<h2 className="text-sm font-semibold text-slate-700">查询结果</h2>
|
<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>
|
<span className="text-xs text-slate-400">数据来源:生产合同 + 装箱明细 + 库位 + 入库记录(LEFT JOIN)</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -333,7 +422,7 @@ export default function WarehouseDashboard() {
|
|||||||
<table className="w-full border-collapse text-sm">
|
<table className="w-full border-collapse text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
{COLUMNS.map((col) => (
|
{visibleColumns.map((col) => (
|
||||||
<th
|
<th
|
||||||
key={col.key}
|
key={col.key}
|
||||||
onClick={() => toggleSort(col.key)}
|
onClick={() => toggleSort(col.key)}
|
||||||
@@ -354,7 +443,7 @@ export default function WarehouseDashboard() {
|
|||||||
<tbody>
|
<tbody>
|
||||||
{!loading && rows.length === 0 ? (
|
{!loading && rows.length === 0 ? (
|
||||||
<tr>
|
<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">
|
<div className="flex flex-col items-center gap-2 text-slate-400">
|
||||||
<Search className="h-8 w-8" />
|
<Search className="h-8 w-8" />
|
||||||
<div className="text-sm">未找到匹配的记录</div>
|
<div className="text-sm">未找到匹配的记录</div>
|
||||||
@@ -368,7 +457,7 @@ export default function WarehouseDashboard() {
|
|||||||
key={`${r.zongpai}-${r.boxNo}-${i}`}
|
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"}`}
|
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 raw = r[col.key as keyof WarehouseRow];
|
||||||
const empty = raw === "" || raw == null;
|
const empty = raw === "" || raw == null;
|
||||||
const align = col.align === "right" ? "text-right" : "text-left";
|
const align = col.align === "right" ? "text-right" : "text-left";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
allowedDevOrigins: ["192.168.110.146"],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
Reference in New Issue
Block a user