feat(traffic): 流量列表支持内容检索、路径/状态码/长度筛选与列排序 (#177)

- 后端 traffic.Page 改为 PageQuery:新增响应内容(FTS)、路径(url_template)、
  状态码(精确/Nxx 分桶)、响应长度范围筛选,并支持按 ts/status/resp_len 排序
  (尾随 id DESC 保证分页稳定);补 status/resp_len 两个索引(IF NOT EXISTS,旧库自动生效)
- server getTraffic 解析 body/path/status/resp_min/resp_max/sort/order 参数
- 前端流量页新增高级筛选行(响应内容/路径/状态码/长度范围)与可排序表头,
  排序偏好持久化到 localStorage;抽出公共组件 ui/sortable-head
- 仅改动前端页走的 Page,不动 Agent 工具的 query 契约
This commit is contained in:
Autumn-27 committed 2026-09-29 21:58:17 -04:00
1 parent 02b1dbe6f8
commit a4960b514e
6 files changed
+383 -24

No files matched your search

+57
View File
@@ -0,0 +1,57 @@
"use client";
import { ArrowDownIcon, ArrowUpDownIcon, ArrowUpIcon } from "lucide-react";
import type * as React from "react";
import { TableHead } from "@/components/ui/table";
import type { SortDirection } from "@/lib/sort-preference";
import { cn } from "@/lib/utils";
// SortableHead is a table header cell that toggles a column's sort on click.
// It renders a neutral up/down glyph when inactive and a directional arrow when
// its field is the active sort, mirroring the pattern first used on the tasks
// table so sortable columns look and behave the same across the app.
export function SortableHead<Field extends string>({
field,
label,
activeField,
direction,
align = "left",
className,
onSort,
}: {
field: Field;
label: string;
activeField: Field | null;
direction: SortDirection;
align?: "left" | "right";
className?: string;
onSort: (field: Field) => void;
}) {
const active = activeField === field;
let ariaSort: React.AriaAttributes["aria-sort"] = "none";
if (active) ariaSort = direction === "asc" ? "ascending" : "descending";
let actionLabel = `按${label}倒序排序`;
if (active) actionLabel = `${label}当前${direction === "asc" ? "正序" : "倒序"},点击切换排序方向`;
let icon = <ArrowUpDownIcon className="size-3.5 opacity-40 transition-opacity group-hover/sort:opacity-100" />;
if (active) icon = direction === "asc" ? <ArrowUpIcon className="size-3.5" /> : <ArrowDownIcon className="size-3.5" />;
return (
<TableHead className={className} aria-sort={ariaSort}>
<button
type="button"
className={cn(
"group/sort inline-flex h-full w-full items-center gap-1 outline-none focus-visible:underline",
align === "right" && "justify-end",
)}
aria-label={actionLabel}
onClick={() => onSort(field)}
>
<span>{label}</span>
{icon}
</button>
</TableHead>
);
}