feat: Web端401队列拦截器 + 消息/任务/个人中心重写 + TabBar红点 + Admin页面完善
This commit is contained in:
@ -1,5 +1,6 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useState, useRef, useCallback } from "react";
|
||||
import { Printer, RefreshCw, Loader2, QrCode, Plus, Settings, X, Loader } from "lucide-react";
|
||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||
import api from "../../services/api";
|
||||
import type { ProductResponse } from "../../types/admin";
|
||||
import CreateProductDialog from "./CreateProductDialog";
|
||||
@ -26,6 +27,35 @@ export default function AdminProductsPage() {
|
||||
const [printCopies, setPrintCopies] = useState(1);
|
||||
const [printing, setPrinting] = useState(false);
|
||||
|
||||
// 🚀 虚拟滚动:响应式列数 + 行虚拟化
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const [columns, setColumns] = useState(5);
|
||||
|
||||
useEffect(() => {
|
||||
const el = scrollRef.current;
|
||||
if (!el) return;
|
||||
const calc = () => {
|
||||
const w = el.clientWidth;
|
||||
if (w >= 1280) setColumns(6);
|
||||
else if (w >= 1024) setColumns(5);
|
||||
else if (w >= 768) setColumns(4);
|
||||
else if (w >= 640) setColumns(3);
|
||||
else setColumns(2);
|
||||
};
|
||||
calc();
|
||||
const ro = new ResizeObserver(calc);
|
||||
ro.observe(el);
|
||||
return () => ro.disconnect();
|
||||
}, [products.length > 0]);
|
||||
|
||||
const rowCount = Math.ceil(products.length / columns);
|
||||
const rowVirtualizer = useVirtualizer({
|
||||
count: rowCount,
|
||||
getScrollElement: () => scrollRef.current,
|
||||
estimateSize: () => 320,
|
||||
overscan: 2,
|
||||
});
|
||||
|
||||
async function loadProducts() {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
@ -124,7 +154,7 @@ export default function AdminProductsPage() {
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-gray-800">产品管理</h2>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
查看所有产品序列号并生成二维码用于打印标签
|
||||
查看所有产品身份证并生成二维码用于打印标签
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
@ -176,38 +206,74 @@ export default function AdminProductsPage() {
|
||||
)}
|
||||
|
||||
{!loading && products.length > 0 && (
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6">
|
||||
{products.map((p) => (
|
||||
<div
|
||||
key={p.id}
|
||||
className="flex flex-col items-center rounded-xl bg-white p-4 shadow-sm transition-shadow hover:shadow-md"
|
||||
>
|
||||
{/* 二维码 */}
|
||||
<img
|
||||
src={`${QR_BASE}/${p.serial_number}`}
|
||||
alt={`QR-${p.serial_number}`}
|
||||
className="h-40 w-40 rounded-lg border border-gray-100"
|
||||
loading="lazy"
|
||||
/>
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className="h-[calc(100vh-220px)] overflow-auto rounded-xl"
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: rowVirtualizer.getTotalSize(),
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
|
||||
const start = virtualRow.index * columns;
|
||||
const rowProducts = products.slice(start, start + columns);
|
||||
return (
|
||||
<div
|
||||
key={virtualRow.key}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
height: virtualRow.size,
|
||||
transform: `translateY(${virtualRow.start}px)`,
|
||||
}}
|
||||
className="grid gap-4 px-1"
|
||||
data-index={virtualRow.index}
|
||||
>
|
||||
{/* 动态 inline grid 列数(Tailwind 不支持动态 columns) */}
|
||||
<div
|
||||
className="grid gap-4"
|
||||
style={{ gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))` }}
|
||||
>
|
||||
{rowProducts.map((p) => (
|
||||
<div
|
||||
key={p.id}
|
||||
className="flex flex-col items-center rounded-xl bg-white p-4 shadow-sm transition-shadow hover:shadow-md"
|
||||
>
|
||||
{/* 二维码 */}
|
||||
<img
|
||||
src={`${QR_BASE}/${p.serial_number}`}
|
||||
alt={`QR-${p.serial_number}`}
|
||||
className="h-40 w-40 rounded-lg border border-gray-100"
|
||||
loading="lazy"
|
||||
/>
|
||||
|
||||
{/* 序列号 */}
|
||||
<p className="mt-3 font-mono text-sm font-bold tracking-wider text-gray-800">
|
||||
{p.serial_number}
|
||||
</p>
|
||||
<p className="mt-0.5 text-xs text-gray-400">
|
||||
{p.order_no ?? "—"}
|
||||
</p>
|
||||
{/* 产品身份证 */}
|
||||
<p className="mt-3 font-mono text-sm font-bold tracking-wider text-gray-800">
|
||||
{p.serial_number}
|
||||
</p>
|
||||
<p className="mt-0.5 text-xs text-gray-400">
|
||||
{p.order_no ?? "—"}
|
||||
</p>
|
||||
|
||||
{/* 打印按钮 */}
|
||||
<button
|
||||
onClick={() => handleOpenPrint(p)}
|
||||
className="mt-3 flex w-full items-center justify-center gap-1.5 rounded-lg border border-blue-200 bg-blue-50 py-2 text-xs font-medium text-blue-700 transition-colors hover:bg-blue-100 hover:border-blue-300"
|
||||
>
|
||||
<Printer className="h-3.5 w-3.5" />
|
||||
打印标签
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
{/* 打印按钮 */}
|
||||
<button
|
||||
onClick={() => handleOpenPrint(p)}
|
||||
className="mt-3 flex w-full items-center justify-center gap-1.5 rounded-lg border border-blue-200 bg-blue-50 py-2 text-xs font-medium text-blue-700 transition-colors hover:bg-blue-100 hover:border-blue-300"
|
||||
>
|
||||
<Printer className="h-3.5 w-3.5" />
|
||||
打印标签
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user