feat: Web端401队列拦截器 + 消息/任务/个人中心重写 + TabBar红点 + Admin页面完善

This commit is contained in:
2026-08-07 11:44:12 +08:00
parent b71c5a2d07
commit 721cfe1504
25 changed files with 1730 additions and 435 deletions

View File

@ -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>
)}