refactor: 统一管理页面布局 — FilterToolbar + usePageSearchParams + parseListParams

This commit is contained in:
2026-06-04 17:25:36 +08:00
parent 61b479e2be
commit 6f547560d1
40 changed files with 1805 additions and 628 deletions

View File

@@ -1,8 +1,8 @@
import type Database from "bun:sqlite";
import { desc, eq, like, or, sql } from "drizzle-orm";
import { asc, desc, eq, like, or, sql } from "drizzle-orm";
import type { CreateModelRequest, Model, ModelCapability, UpdateModelRequest } from "../../shared/api";
import type { CreateModelRequest, Model, ModelCapability, SortOrder, UpdateModelRequest } from "../../shared/api";
import type { Logger } from "../logger";
import { paginateQuery, wrap } from "./connection";
@@ -124,7 +124,15 @@ export function getModelWithProvider(
export function listModels(
raw: Database,
options: { keyword?: string; page: number; pageSize: number; providerId?: string },
options: {
capabilities?: string;
keyword?: string;
page: number;
pageSize: number;
providerId?: string;
sortBy?: string;
sortOrder?: SortOrder;
},
): { items: Model[]; page: number; pageSize: number; total: number } {
const conditions = [];
@@ -137,10 +145,16 @@ export function listModels(
conditions.push(or(like(models.name, pattern), like(models.modelId, pattern))!);
}
if (options.capabilities) {
conditions.push(like(models.capabilities, `%"${options.capabilities}"%`));
}
const orderByFn = buildModelOrderBy(options.sortBy, options.sortOrder);
return paginateQuery(raw, models, {
conditions,
mapRow: toModel,
orderBy: () => desc(models.createdAt),
orderBy: orderByFn,
page: options.page,
pageSize: options.pageSize,
});
@@ -212,6 +226,17 @@ export function updateModel(
return { model: toModel(updated!) };
}
function buildModelOrderBy(
sortBy: string | undefined,
sortOrder: SortOrder | undefined,
): ((table: typeof models) => ReturnType<typeof desc>) | undefined {
if (!sortBy) return (t) => desc(t.createdAt);
return sortOrder === "asc"
? (t) => asc(t[sortBy as keyof typeof t] as Parameters<typeof asc>[0])
: (t) => desc(t[sortBy as keyof typeof t] as Parameters<typeof desc>[0]);
}
function toModel(row: typeof models.$inferSelect): Model {
return {
capabilities: JSON.parse(row.capabilities) as ModelCapability[],

View File

@@ -1,8 +1,8 @@
import type Database from "bun:sqlite";
import { desc, eq, like, or } from "drizzle-orm";
import { asc, desc, eq, like, or } from "drizzle-orm";
import type { CreateProjectRequest, Project, ProjectStatus, UpdateProjectRequest } from "../../shared/api";
import type { CreateProjectRequest, Project, ProjectStatus, SortOrder, UpdateProjectRequest } from "../../shared/api";
import type { Logger } from "../logger";
import { paginateQuery, wrap } from "./connection";
@@ -88,7 +88,14 @@ export function getProject(raw: Database, id: string): { error: string; status:
export function listProjects(
raw: Database,
options: { keyword?: string; page: number; pageSize: number; status?: ProjectStatus },
options: {
keyword?: string;
page: number;
pageSize: number;
sortBy?: string;
sortOrder?: SortOrder;
status?: ProjectStatus;
},
): { items: Project[]; page: number; pageSize: number; total: number } {
const conditions = [];
@@ -101,10 +108,12 @@ export function listProjects(
conditions.push(or(like(projects.name, pattern), like(projects.description, pattern))!);
}
const orderByFn = buildProjectOrderBy(options.sortBy, options.sortOrder);
return paginateQuery(raw, projects, {
conditions,
mapRow: toProject,
orderBy: () => desc(projects.createdAt),
orderBy: orderByFn,
page: options.page,
pageSize: options.pageSize,
});
@@ -174,6 +183,17 @@ export function updateProject(
return { project: toProject(updated!) };
}
function buildProjectOrderBy(
sortBy: string | undefined,
sortOrder: SortOrder | undefined,
): ((table: typeof projects) => ReturnType<typeof desc>) | undefined {
if (!sortBy) return (t) => desc(t.createdAt);
return sortOrder === "asc"
? (t) => asc(t[sortBy as keyof typeof t] as Parameters<typeof asc>[0])
: (t) => desc(t[sortBy as keyof typeof t] as Parameters<typeof desc>[0]);
}
function toProject(row: typeof projects.$inferSelect): Project {
return {
archivedAt: row.archivedAt,

View File

@@ -1,8 +1,14 @@
import type Database from "bun:sqlite";
import { desc, eq, like } from "drizzle-orm";
import { asc, desc, eq, like } from "drizzle-orm";
import type { CreateProviderRequest, Provider, ProviderOption, UpdateProviderRequest } from "../../shared/api";
import type {
CreateProviderRequest,
Provider,
ProviderOption,
SortOrder,
UpdateProviderRequest,
} from "../../shared/api";
import type { Logger } from "../logger";
import { paginateQuery, wrap } from "./connection";
@@ -85,7 +91,7 @@ export function listProviderOptions(raw: Database): ProviderOption[] {
export function listProviders(
raw: Database,
options: { keyword?: string; page: number; pageSize: number },
options: { keyword?: string; page: number; pageSize: number; sortBy?: string; sortOrder?: SortOrder; type?: string },
): { items: Provider[]; page: number; pageSize: number; total: number } {
const conditions = [];
@@ -94,10 +100,16 @@ export function listProviders(
conditions.push(like(providers.name, pattern));
}
if (options.type) {
conditions.push(eq(providers.type, options.type as Provider["type"]));
}
const orderByFn = buildProviderOrderBy(options.sortBy, options.sortOrder);
return paginateQuery(raw, providers, {
conditions,
mapRow: toProvider,
orderBy: () => desc(providers.createdAt),
orderBy: orderByFn,
page: options.page,
pageSize: options.pageSize,
});
@@ -158,6 +170,17 @@ export function updateProvider(
return { provider: toProvider(updated!) };
}
function buildProviderOrderBy(
sortBy: string | undefined,
sortOrder: SortOrder | undefined,
): ((table: typeof providers) => ReturnType<typeof desc>) | undefined {
if (!sortBy) return (t) => desc(t.createdAt);
return sortOrder === "asc"
? (t) => asc(t[sortBy as keyof typeof t] as Parameters<typeof asc>[0])
: (t) => desc(t[sortBy as keyof typeof t] as Parameters<typeof desc>[0]);
}
function toProvider(row: typeof providers.$inferSelect): Provider {
return {
apiKey: row.apiKey,

View File

@@ -1,2 +1,4 @@
export type { ParsedListParams } from "./list-params";
export { parseListParams } from "./list-params";
export { createApiError, createHeaders, createMetaResponse, formatDuration, jsonResponse } from "./response";
export { parseIdFromUrl } from "./url";

View File

@@ -0,0 +1,59 @@
import type { RuntimeMode, SortOrder } from "../../shared/api";
import { createApiError, jsonResponse } from "./index";
export interface ParsedListParams {
keyword?: string;
page: number;
pageSize: number;
sortBy?: string;
sortOrder?: SortOrder;
}
export function parseListParams(
url: URL,
mode: RuntimeMode,
options?: { allowedSortBy?: string[] },
): ParsedListParams | Response {
const pageParam = url.searchParams.get("page");
const pageSizeParam = url.searchParams.get("pageSize");
let page = 1;
let pageSize = 20;
if (pageParam !== null) {
page = Number(pageParam);
if (!Number.isInteger(page) || page <= 0) {
return jsonResponse(createApiError("无效的 page 参数", 400), { mode, status: 400 });
}
}
if (pageSizeParam !== null) {
pageSize = Number(pageSizeParam);
if (!Number.isInteger(pageSize) || pageSize <= 0) {
return jsonResponse(createApiError("无效的 pageSize 参数", 400), { mode, status: 400 });
}
if (pageSize > 200) {
return jsonResponse(createApiError("pageSize 不能超过 200", 400), { mode, status: 400 });
}
}
const keyword = url.searchParams.get("keyword") ?? undefined;
const sortBy = url.searchParams.get("sortBy") ?? undefined;
const sortOrderParam = url.searchParams.get("sortOrder") ?? undefined;
if (sortBy && options?.allowedSortBy && !options.allowedSortBy.includes(sortBy)) {
return jsonResponse(createApiError("无效的 sortBy 参数", 400), { mode, status: 400 });
}
let sortOrder: SortOrder | undefined;
if (sortOrderParam) {
if (sortOrderParam !== "asc" && sortOrderParam !== "desc") {
return jsonResponse(createApiError("无效的 sortOrder 参数", 400), { mode, status: 400 });
}
sortOrder = sortOrderParam;
}
return { keyword, page, pageSize, sortBy, sortOrder };
}

View File

@@ -4,24 +4,26 @@ import type { RuntimeMode } from "../../../shared/api";
import type { Logger } from "../../logger";
import { listModels } from "../../db/models";
import { jsonResponse } from "../../helpers";
import { validatePagination } from "../../middleware";
import { jsonResponse, parseListParams } from "../../helpers";
const ALLOWED_SORT_BY = ["createdAt", "name"];
export function handleListModels(req: Request, db: Database, mode: RuntimeMode, _logger: Logger): Response {
const url = new URL(req.url);
const pageParam = url.searchParams.get("page");
const pageSizeParam = url.searchParams.get("pageSize");
const keyword = url.searchParams.get("keyword");
const providerId = url.searchParams.get("providerId");
const capabilities = url.searchParams.get("capabilities");
const pagination = validatePagination(pageParam, pageSizeParam, mode);
if (pagination instanceof Response) return pagination;
const parsed = parseListParams(url, mode, { allowedSortBy: ALLOWED_SORT_BY });
if (parsed instanceof Response) return parsed;
const result = listModels(db, {
keyword: keyword ?? undefined,
page: pagination.page,
pageSize: pagination.pageSize,
capabilities: capabilities ?? undefined,
keyword: parsed.keyword,
page: parsed.page,
pageSize: parsed.pageSize,
providerId: providerId ?? undefined,
sortBy: parsed.sortBy,
sortOrder: parsed.sortOrder,
});
return jsonResponse(result, { mode });

View File

@@ -4,27 +4,27 @@ import type { RuntimeMode } from "../../../shared/api";
import type { Logger } from "../../logger";
import { listProjects } from "../../db/projects";
import { createApiError, jsonResponse } from "../../helpers";
import { validatePagination } from "../../middleware";
import { createApiError, jsonResponse, parseListParams } from "../../helpers";
const ALLOWED_SORT_BY = ["createdAt", "name", "updatedAt"];
export function handleListProjects(req: Request, db: Database, mode: RuntimeMode, _logger: Logger): Response {
const url = new URL(req.url);
const pageParam = url.searchParams.get("page");
const pageSizeParam = url.searchParams.get("pageSize");
const keyword = url.searchParams.get("keyword");
const statusParam = url.searchParams.get("status");
const pagination = validatePagination(pageParam, pageSizeParam, mode);
if (pagination instanceof Response) return pagination;
if (statusParam && statusParam !== "active" && statusParam !== "archived") {
return jsonResponse(createApiError("Invalid status parameter", 400), { mode, status: 400 });
}
const parsed = parseListParams(url, mode, { allowedSortBy: ALLOWED_SORT_BY });
if (parsed instanceof Response) return parsed;
const result = listProjects(db, {
keyword: keyword ?? undefined,
page: pagination.page,
pageSize: pagination.pageSize,
keyword: parsed.keyword,
page: parsed.page,
pageSize: parsed.pageSize,
sortBy: parsed.sortBy,
sortOrder: parsed.sortOrder,
status: (statusParam as "active" | "archived") ?? undefined,
});

View File

@@ -4,22 +4,24 @@ import type { RuntimeMode } from "../../../shared/api";
import type { Logger } from "../../logger";
import { listProviders } from "../../db/providers";
import { jsonResponse } from "../../helpers";
import { validatePagination } from "../../middleware";
import { jsonResponse, parseListParams } from "../../helpers";
const ALLOWED_SORT_BY = ["createdAt", "name"];
export function handleListProviders(req: Request, db: Database, mode: RuntimeMode, _logger: Logger): Response {
const url = new URL(req.url);
const pageParam = url.searchParams.get("page");
const pageSizeParam = url.searchParams.get("pageSize");
const keyword = url.searchParams.get("keyword");
const typeParam = url.searchParams.get("type");
const pagination = validatePagination(pageParam, pageSizeParam, mode);
if (pagination instanceof Response) return pagination;
const parsed = parseListParams(url, mode, { allowedSortBy: ALLOWED_SORT_BY });
if (parsed instanceof Response) return parsed;
const result = listProviders(db, {
keyword: keyword ?? undefined,
page: pagination.page,
pageSize: pagination.pageSize,
keyword: parsed.keyword,
page: parsed.page,
pageSize: parsed.pageSize,
sortBy: parsed.sortBy,
sortOrder: parsed.sortOrder,
type: typeParam ?? undefined,
});
return jsonResponse(result, { mode });