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:
Misaka_Company
2026-06-04 10:31:58 +08:00
parent c581206343
commit ca8aa782da
2 changed files with 94 additions and 5 deletions

View File

@@ -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";

View File

@@ -1,7 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
allowedDevOrigins: ["192.168.110.146"],
};
export default nextConfig;