refactor: 统一管理页面布局 — FilterToolbar + usePageSearchParams + parseListParams
This commit is contained in:
74
src/web/shared/components/FilterToolbar.tsx
Normal file
74
src/web/shared/components/FilterToolbar.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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],
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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],
|
||||
|
||||
24
src/web/shared/hooks/useConfirmAction.ts
Normal file
24
src/web/shared/hooks/useConfirmAction.ts
Normal 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 };
|
||||
}
|
||||
74
src/web/shared/hooks/usePageSearchParams.ts
Normal file
74
src/web/shared/hooks/usePageSearchParams.ts
Normal 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 };
|
||||
}
|
||||
5
src/web/shared/utils/format.ts
Normal file
5
src/web/shared/utils/format.ts
Normal 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())}`;
|
||||
}
|
||||
Reference in New Issue
Block a user