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

@@ -0,0 +1,74 @@
import type { ReactNode } from "react";
import { SearchOutlined, UndoOutlined } from "@ant-design/icons";
import { Button, Flex, Input, Select, Space } from "antd";
import { useEffect, useRef, useState } from "react";
export interface FilterConfig {
key: string;
label: string;
onChange: (value: string | undefined) => void;
options: Array<{ label: string; value: string }>;
placeholder: string;
value?: string;
}
export interface FilterToolbarProps {
actions?: ReactNode;
filters?: FilterConfig[];
search?: SearchConfig;
}
export interface SearchConfig {
keyword?: string;
onReset: () => void;
onSearch: (value: string) => void;
placeholder: string;
}
export function FilterToolbar({ actions, filters, search }: FilterToolbarProps) {
const [draftKeyword, setDraftKeyword] = useState(search?.keyword ?? "");
const prevKeywordRef = useRef(search?.keyword);
useEffect(() => {
if (search?.keyword !== prevKeywordRef.current) {
prevKeywordRef.current = search?.keyword;
setDraftKeyword(search?.keyword ?? "");
}
}, [search?.keyword]);
return (
<Flex align="center" gap="large" justify="space-between" wrap="wrap">
<Space size="small" wrap>
{filters?.map((filter) => (
<Select
allowClear
key={filter.key}
onChange={filter.onChange}
options={filter.options}
placeholder={filter.placeholder}
style={{ minWidth: 120 }}
value={filter.value}
/>
))}
{search && (
<Input.Search
allowClear
enterButton={<Button icon={<SearchOutlined />} type="primary" />}
onChange={(e) => setDraftKeyword(e.target.value)}
onClear={() => {
setDraftKeyword("");
search.onSearch("");
}}
onSearch={(value) => search.onSearch(value)}
placeholder={search.placeholder}
style={{ width: 220 }}
value={draftKeyword}
/>
)}
<Button icon={<UndoOutlined />} onClick={search?.onReset} title="重置" />
</Space>
{actions && <Space size="small">{actions}</Space>}
</Flex>
);
}

View File

@@ -37,16 +37,22 @@ export async function fetchModel(id: string): Promise<Model> {
}
export async function fetchModelList(params: {
capabilities?: string;
keyword?: string;
page?: number;
pageSize?: number;
providerId?: string;
sortBy?: string;
sortOrder?: string;
}): Promise<ModelListResponse> {
const searchParams = new URLSearchParams();
if (params.page) searchParams.set("page", String(params.page));
if (params.pageSize) searchParams.set("pageSize", String(params.pageSize));
if (params.keyword) searchParams.set("keyword", params.keyword);
if (params.providerId) searchParams.set("providerId", params.providerId);
if (params.sortBy) searchParams.set("sortBy", params.sortBy);
if (params.sortOrder) searchParams.set("sortOrder", params.sortOrder);
if (params.capabilities) searchParams.set("capabilities", params.capabilities);
const qs = searchParams.toString();
const url = `/api/models${qs ? `?${qs}` : ""}`;
const response = await fetch(url);
@@ -110,7 +116,15 @@ export function useModel(id: string) {
});
}
export function useModelList(params: { keyword?: string; page?: number; pageSize?: number; providerId?: string }) {
export function useModelList(params: {
capabilities?: string;
keyword?: string;
page?: number;
pageSize?: number;
providerId?: string;
sortBy?: string;
sortOrder?: string;
}) {
return useQuery({
queryFn: () => fetchModelList(params),
queryKey: [...MODELS_KEY, "list", params],

View File

@@ -43,12 +43,16 @@ export async function fetchProjectList(params: {
keyword?: string;
page?: number;
pageSize?: number;
sortBy?: string;
sortOrder?: string;
status?: ProjectStatus;
}): Promise<ProjectListResponse> {
const searchParams = new URLSearchParams();
if (params.page) searchParams.set("page", String(params.page));
if (params.pageSize) searchParams.set("pageSize", String(params.pageSize));
if (params.keyword) searchParams.set("keyword", params.keyword);
if (params.sortBy) searchParams.set("sortBy", params.sortBy);
if (params.sortOrder) searchParams.set("sortOrder", params.sortOrder);
if (params.status) searchParams.set("status", params.status);
const qs = searchParams.toString();
const url = `/api/projects${qs ? `?${qs}` : ""}`;
@@ -115,7 +119,14 @@ export function useProject(id: string) {
});
}
export function useProjectList(params: { keyword?: string; page?: number; pageSize?: number; status?: ProjectStatus }) {
export function useProjectList(params: {
keyword?: string;
page?: number;
pageSize?: number;
sortBy?: string;
sortOrder?: string;
status?: ProjectStatus;
}) {
return useQuery({
queryFn: () => fetchProjectList(params),
queryKey: [...PROJECTS_KEY, "list", params],

View File

@@ -41,11 +41,17 @@ export async function fetchProviderList(params: {
keyword?: string;
page?: number;
pageSize?: number;
sortBy?: string;
sortOrder?: string;
type?: string;
}): Promise<ProviderListResponse> {
const searchParams = new URLSearchParams();
if (params.page) searchParams.set("page", String(params.page));
if (params.pageSize) searchParams.set("pageSize", String(params.pageSize));
if (params.keyword) searchParams.set("keyword", params.keyword);
if (params.sortBy) searchParams.set("sortBy", params.sortBy);
if (params.sortOrder) searchParams.set("sortOrder", params.sortOrder);
if (params.type) searchParams.set("type", params.type);
const qs = searchParams.toString();
const url = `/api/providers${qs ? `?${qs}` : ""}`;
const response = await fetch(url);
@@ -119,7 +125,14 @@ export function useProvider(id: string) {
});
}
export function useProviderList(params: { keyword?: string; page?: number; pageSize?: number }) {
export function useProviderList(params: {
keyword?: string;
page?: number;
pageSize?: number;
sortBy?: string;
sortOrder?: string;
type?: string;
}) {
return useQuery({
queryFn: () => fetchProviderList(params),
queryKey: [...PROVIDERS_KEY, "list", params],

View File

@@ -0,0 +1,24 @@
import { App } from "antd";
import { useCallback } from "react";
export interface UseConfirmActionResult {
confirmAction: (action: () => Promise<unknown>, successMessage: string) => Promise<void>;
}
export function useConfirmAction(): UseConfirmActionResult {
const { message } = App.useApp();
const confirmAction = useCallback(
async (action: () => Promise<unknown>, successMessage: string) => {
try {
await action();
message.success(successMessage);
} catch (err: unknown) {
message.error((err as Error).message);
}
},
[message],
);
return { confirmAction };
}

View File

@@ -0,0 +1,74 @@
import { useCallback, useMemo } from "react";
import { useSearchParams } from "react-router";
export interface UsePageSearchParamsOptions {
defaults?: Record<string, string>;
}
export interface UsePageSearchParamsResult {
params: Record<string, string>;
resetAll: () => void;
setParam: (key: string, value: string | undefined) => void;
setParams: (patch: Record<string, string | undefined>) => void;
}
export function usePageSearchParams(options?: UsePageSearchParamsOptions): UsePageSearchParamsResult {
const [searchParams, setSearchParams] = useSearchParams();
const defaults = useMemo(() => options?.defaults ?? {}, [options?.defaults]);
const params = useMemo(() => {
const result: Record<string, string> = {};
for (const [key, value] of searchParams.entries()) {
result[key] = value;
}
for (const [key, value] of Object.entries(defaults)) {
if (!(key in result)) result[key] = value;
}
return result;
}, [searchParams, defaults]);
const setParam = useCallback(
(key: string, value: string | undefined) => {
setSearchParams(
(prev) => {
const next = new URLSearchParams(prev);
if (value === undefined || value === "") {
next.delete(key);
} else {
next.set(key, value);
}
return next;
},
{ replace: true },
);
},
[setSearchParams],
);
const setParams = useCallback(
(patch: Record<string, string | undefined>) => {
setSearchParams(
(prev) => {
const next = new URLSearchParams(prev);
for (const [key, value] of Object.entries(patch)) {
if (value === undefined || value === "") {
next.delete(key);
} else {
next.set(key, value);
}
}
return next;
},
{ replace: true },
);
},
[setSearchParams],
);
const resetAll = useCallback(() => {
setSearchParams(new URLSearchParams(), { replace: true });
}, [setSearchParams]);
return { params, resetAll, setParam, setParams };
}

View File

@@ -0,0 +1,5 @@
export function formatDatetime(dateStr: string): string {
const d = new Date(dateStr);
const pad = (n: number) => String(n).padStart(2, "0");
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
}