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:
1 parent
02b1dbe6f8
commit
a4960b514e
6 files changed
+383
-24
No files matched your search
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user