feat(admin): sortable product list column headers
CI / Go (api) (pull_request) Successful in 11s
CI / Python (ingestion) (pull_request) Successful in 9s
CI / Migrations (postgres) (pull_request) Successful in 14s

Click a column header (名称/品牌/条码/品类/状态/质量分) to sort asc, click
again for desc, and a third time to clear back to the default
most-recently-updated order. Sort key/direction are whitelisted server-side.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
sulaimaannaasif6866
2026-06-21 08:00:36 +00:00
parent 7434e5195e
commit 241fd38a56
4 changed files with 112 additions and 14 deletions
+11 -2
View File
@@ -51,14 +51,23 @@ export const api = {
body: JSON.stringify({ username, password }),
}),
me: () => request<{ username: string }>("/me"),
listProducts: (q: string, page: number, size: number) =>
listProducts: (
q: string,
page: number,
size: number,
sort?: string,
order?: string,
) =>
request<{
items: import("./types").ProductRow[];
page: number;
size: number;
total: number;
completeness_fields: string[];
}>(`/products?q=${encodeURIComponent(q)}&page=${page}&size=${size}`),
}>(
`/products?q=${encodeURIComponent(q)}&page=${page}&size=${size}` +
(sort ? `&sort=${sort}&order=${order || "asc"}` : ""),
),
getProduct: (id: string) =>
request<import("./types").ProductDetail>(`/products/${id}`),
createProduct: (body: unknown) =>
+68 -9
View File
@@ -1,7 +1,15 @@
import { useEffect, useState } from "react";
import { api, ApiError } from "../api";
import { Brand, Category, FIELD_LABELS, ProductRow } from "../types";
import { Search, AlertCircle, Plus } from "lucide-react";
import { Search, AlertCircle, Plus, ChevronUp, ChevronDown, ChevronsUpDown } from "lucide-react";
type SortKey =
| "name"
| "brand"
| "gtin"
| "category_path"
| "status"
| "quality_score";
const STATUS_LABEL: Record<string, string> = {
active: "在用",
@@ -24,6 +32,42 @@ function QualityBadge({ score }: { score: number }) {
);
}
function SortableTh({
label,
sortKey,
sort,
order,
onSort,
}: {
label: string;
sortKey: SortKey;
sort: SortKey | "";
order: "asc" | "desc";
onSort: (key: SortKey) => void;
}) {
const active = sort === sortKey;
return (
<th className="px-4 py-3">
<button
type="button"
onClick={() => onSort(sortKey)}
className={`flex items-center gap-1 uppercase hover:text-gray-700 ${
active ? "text-emerald-600" : ""
}`}
>
{label}
{!active ? (
<ChevronsUpDown className="h-3.5 w-3.5 text-gray-300" />
) : order === "asc" ? (
<ChevronUp className="h-3.5 w-3.5" />
) : (
<ChevronDown className="h-3.5 w-3.5" />
)}
</button>
</th>
);
}
export default function ProductList({
onOpen,
}: {
@@ -33,6 +77,8 @@ export default function ProductList({
const [input, setInput] = useState("");
const [page, setPage] = useState(1);
const [size, setSize] = useState(20);
const [sort, setSort] = useState<SortKey | "">("");
const [order, setOrder] = useState<"asc" | "desc">("asc");
const [jump, setJump] = useState("");
const [rows, setRows] = useState<ProductRow[]>([]);
const [total, setTotal] = useState(0);
@@ -49,7 +95,7 @@ export default function ProductList({
setLoading(true);
setError("");
api
.listProducts(q, page, size)
.listProducts(q, page, size, sort || undefined, order)
.then((r) => {
setRows(r.items);
setTotal(r.total);
@@ -61,7 +107,20 @@ export default function ProductList({
useEffect(() => {
setSelected(new Set());
reload();
}, [q, page, size]);
}, [q, page, size, sort, order]);
function toggleSort(key: SortKey) {
setPage(1);
if (sort !== key) {
setSort(key);
setOrder("asc");
} else if (order === "asc") {
setOrder("desc");
} else {
setSort("");
setOrder("asc");
}
}
useEffect(() => {
api.listCategories().then((r) => setCategories(r.items)).catch(() => {});
@@ -245,12 +304,12 @@ export default function ProductList({
aria-label="全选"
/>
</th>
<th className="px-4 py-3"></th>
<th className="px-4 py-3"></th>
<th className="px-4 py-3"></th>
<th className="px-4 py-3"></th>
<th className="px-4 py-3"></th>
<th className="px-4 py-3"></th>
<SortableTh label="名称" sortKey="name" sort={sort} order={order} onSort={toggleSort} />
<SortableTh label="品牌" sortKey="brand" sort={sort} order={order} onSort={toggleSort} />
<SortableTh label="条码" sortKey="gtin" sort={sort} order={order} onSort={toggleSort} />
<SortableTh label="品类" sortKey="category_path" sort={sort} order={order} onSort={toggleSort} />
<SortableTh label="状态" sortKey="status" sort={sort} order={order} onSort={toggleSort} />
<SortableTh label="质量分" sortKey="quality_score" sort={sort} order={order} onSort={toggleSort} />
<th className="px-4 py-3"></th>
</tr>
</thead>