feat(ui): keep pagination in list pages

This commit is contained in:
Fu Diwei
2025-07-22 22:20:39 +08:00
parent 0acbbe2ee6
commit 98ce9eb8d2
9 changed files with 200 additions and 53 deletions
+2 -1
View File
@@ -1,8 +1,9 @@
import useAntdForm from "./useAntdForm";
import useAntdFormName from "./useAntdFormName";
import useAppSettings from "./useAppSettings";
import useBrowserTheme from "./useBrowserTheme";
import useTriggerElement from "./useTriggerElement";
import useVersionChecker from "./useVersionChecker";
import useZustandShallowSelector from "./useZustandShallowSelector";
export { useAntdForm, useAntdFormName, useBrowserTheme, useTriggerElement, useVersionChecker, useZustandShallowSelector };
export { useAntdForm, useAntdFormName, useAppSettings, useBrowserTheme, useTriggerElement, useVersionChecker, useZustandShallowSelector };
+42
View File
@@ -0,0 +1,42 @@
import { useCallback } from "react";
import { useLocalStorageState } from "ahooks";
const LOCAL_STORAGE_KEY = "certimate-ui-appsettings";
if (!localStorage.getItem(LOCAL_STORAGE_KEY)) {
localStorage.setItem(LOCAL_STORAGE_KEY, "{}");
}
type AppSettings = {
// 每页显示的默认条目数
defaultPerPage?: number;
};
export type UseAppSettingsReturns = {
appSettings: AppSettings;
setAppSettings: (value: AppSettings) => void;
resetAppSettings: () => void;
};
/**
* 获取并设置当前应用全局配置。
* @returns {UseAppSettingsReturns}
*/
const useAppSettings = (): UseAppSettingsReturns => {
const [state, setState] = useLocalStorageState<AppSettings>(LOCAL_STORAGE_KEY, {
defaultValue: {},
});
state.defaultPerPage ??= 15;
const resetState = useCallback(() => {
localStorage.removeItem(LOCAL_STORAGE_KEY);
setState({});
}, [setState]);
return {
appSettings: state,
setAppSettings: setState,
resetAppSettings: resetState,
};
};
export default useAppSettings;
+4
View File
@@ -25,6 +25,10 @@
"settings.appearance.theme.form.value.extra": "Reload the page for the change to take effect.",
"settings.appearance.language.title": "Language",
"settings.appearance.language.form.value.extra": "Reload the page for the change to take effect.",
"settings.appearance.pagination.title": "Pagination",
"settings.appearance.pagination.form.default_per_page.label": "Default items per page",
"settings.appearance.pagination.form.default_per_page.placeholder": "Please enter default items per page",
"settings.appearance.pagination.form.default_per_page.unit": "items per page",
"settings.sslprovider.tab": "Certificate authority",
"settings.sslprovider.ca.title": "System-wide CA",
+4
View File
@@ -25,6 +25,10 @@
"settings.appearance.theme.form.value.extra": "重新加载页面生效。",
"settings.appearance.language.title": "语言",
"settings.appearance.language.form.value.extra": "重新加载页面生效。",
"settings.appearance.pagination.title": "分页",
"settings.appearance.pagination.form.default_per_page.label": "列表页默认显示数量",
"settings.appearance.pagination.form.default_per_page.placeholder": "请输入列表页默认显示数量",
"settings.appearance.pagination.form.default_per_page.unit": "条每页",
"settings.sslprovider.tab": "证书颁发机构",
"settings.sslprovider.ca.title": "全局证书颁发机构",
+36 -15
View File
@@ -11,32 +11,34 @@ import AccessEditDrawer, { type AccessEditDrawerProps } from "@/components/acces
import Empty from "@/components/Empty";
import { type AccessModel } from "@/domain/access";
import { ACCESS_USAGES, accessProvidersMap } from "@/domain/provider";
import { useZustandShallowSelector } from "@/hooks";
import { useAppSettings, useZustandShallowSelector } from "@/hooks";
import { useAccessesStore } from "@/stores/access";
import { getErrMsg } from "@/utils/error";
type AccessUsageProp = AccessEditDrawerProps["usage"];
type AccessUsages = AccessEditDrawerProps["usage"];
const AccessList = () => {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const [searchParams, setSearchParams] = useSearchParams();
const { t } = useTranslation();
const { modal, notification } = App.useApp();
const { appSettings: globalAppSettings } = useAppSettings();
const { accesses, loadedAtOnce, fetchAccesses, deleteAccess } = useAccessesStore(
useZustandShallowSelector(["accesses", "loadedAtOnce", "fetchAccesses", "deleteAccess"])
);
const [filters, setFilters] = useState<Record<string, unknown>>(() => {
return {
usage: "dns-hosting" satisfies AccessUsageProp,
usage: searchParams.get("usage") || ("dns-hosting" satisfies AccessUsages),
keyword: searchParams.get("keyword"),
};
});
const [page, setPage] = useState<number>(() => parseInt(+searchParams.get("page")! + "") || 1);
const [pageSize, setPageSize] = useState<number>(() => parseInt(+searchParams.get("perPage")! + "") || 15);
const [pageSize, setPageSize] = useState<number>(() => parseInt(+searchParams.get("perPage")! + "") || globalAppSettings.defaultPerPage!);
const tableColumns: TableProps<AccessModel>["columns"] = [
{
@@ -144,7 +146,11 @@ const AccessList = () => {
})
.filter((e) => {
const provider = accessProvidersMap.get(e.provider);
switch (filters["usage"] as AccessUsageProp) {
switch (filters["usage"] as AccessUsages) {
case "dns":
return !e.reserve && provider?.usages?.includes(ACCESS_USAGES.DNS);
case "hosting":
return !e.reserve && provider?.usages?.includes(ACCESS_USAGES.HOSTING);
case "dns-hosting":
return !e.reserve && (provider?.usages?.includes(ACCESS_USAGES.DNS) || provider?.usages?.includes(ACCESS_USAGES.HOSTING));
case "ca":
@@ -160,6 +166,22 @@ const AccessList = () => {
},
{
refreshDeps: [accesses, filters, page, pageSize],
onBefore: () => {
setSearchParams((prev) => {
if (filters["keyword"]) {
prev.set("keyword", filters["keyword"] as string);
} else {
prev.delete("keyword");
}
prev.set("usage", filters["usage"] as string);
prev.set("page", page.toString());
prev.set("perPage", pageSize.toString());
return prev;
});
},
onSuccess: (res) => {
setTableData(res.items);
setTableTotal(res.totalItems);
@@ -183,6 +205,11 @@ const AccessList = () => {
fetchAccesses();
};
const handlePaginationChange = (page: number, pageSize: number) => {
setPage(page);
setPageSize(pageSize);
};
const handleCreateClick = () => {
navigate(`/accesses/new?usage=${filters["usage"]}`);
};
@@ -302,14 +329,8 @@ const AccessList = () => {
pageSize: pageSize,
total: tableTotal,
showSizeChanger: true,
onChange: (page: number, pageSize: number) => {
setPage(page);
setPageSize(pageSize);
},
onShowSizeChange: (page: number, pageSize: number) => {
setPage(page);
setPageSize(pageSize);
},
onChange: handlePaginationChange,
onShowSizeChange: handlePaginationChange,
}}
rowClassName="cursor-pointer"
rowKey={(record) => record.id}
@@ -321,7 +342,7 @@ const AccessList = () => {
})}
/>
<AccessEditDrawer data={detailRecord} open={detailOpen} mode={detailMode} usage={filters["usage"] as AccessUsageProp} onOpenChange={setDetailOpen} />
<AccessEditDrawer data={detailRecord} open={detailOpen} mode={detailMode} usage={filters["usage"] as AccessUsages} onOpenChange={setDetailOpen} />
</div>
</div>
);
+32 -10
View File
@@ -10,17 +10,20 @@ import { ClientResponseError } from "pocketbase";
import CertificateDetailDrawer from "@/components/certificate/CertificateDetailDrawer";
import Empty from "@/components/Empty";
import { type CertificateModel } from "@/domain/certificate";
import { useAppSettings } from "@/hooks";
import { list as listCertificates, type ListRequest as listCertificatesRequest, remove as removeCertificate } from "@/repository/certificate";
import { getErrMsg } from "@/utils/error";
const CertificateList = () => {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const [searchParams, setSearchParams] = useSearchParams();
const { t } = useTranslation();
const { modal, notification } = App.useApp();
const { appSettings: globalAppSettings } = useAppSettings();
const [filters, setFilters] = useState<Record<string, unknown>>(() => {
return {
keyword: searchParams.get("keyword"),
@@ -31,7 +34,7 @@ const CertificateList = () => {
return {};
});
const [page, setPage] = useState<number>(() => parseInt(+searchParams.get("page")! + "") || 1);
const [pageSize, setPageSize] = useState<number>(() => parseInt(+searchParams.get("perPage")! + "") || 15);
const [pageSize, setPageSize] = useState<number>(() => parseInt(+searchParams.get("perPage")! + "") || globalAppSettings.defaultPerPage!);
const tableColumns: TableProps<CertificateModel>["columns"] = [
{
@@ -186,6 +189,26 @@ const CertificateList = () => {
},
{
refreshDeps: [filters, sorter, page, pageSize],
onBefore: () => {
setSearchParams((prev) => {
if (filters["keyword"]) {
prev.set("keyword", filters["keyword"] as string);
} else {
prev.delete("keyword");
}
if (filters["state"]) {
prev.set("state", filters["state"] as string);
} else {
prev.delete("state");
}
prev.set("page", page.toString());
prev.set("perPage", pageSize.toString());
return prev;
});
},
onSuccess: (res) => {
setTableData(res.items);
setTableTotal(res.totalItems);
@@ -214,6 +237,11 @@ const CertificateList = () => {
refreshData();
};
const handlePaginationChange = (page: number, pageSize: number) => {
setPage(page);
setPageSize(pageSize);
};
const [detailRecord, setDetailRecord] = useState<CertificateModel>();
const [detailOpen, setDetailOpen] = useState<boolean>(false);
@@ -320,14 +348,8 @@ const CertificateList = () => {
pageSize: pageSize,
total: tableTotal,
showSizeChanger: true,
onChange: (page: number, pageSize: number) => {
setPage(page);
setPageSize(pageSize);
},
onShowSizeChange: (page: number, pageSize: number) => {
setPage(page);
setPageSize(pageSize);
},
onChange: handlePaginationChange,
onShowSizeChange: handlePaginationChange,
}}
rowClassName="cursor-pointer"
rowKey={(record) => record.id}
+2 -6
View File
@@ -15,16 +15,12 @@ const SettingsAccount = () => {
return (
<>
<h2>{t("settings.account.username.title")}</h2>
<div className="md:max-w-160">
<SettingsAccountUsername />
</div>
<SettingsAccountUsername className="md:max-w-160" />
<Divider />
<h2>{t("settings.account.password.title")}</h2>
<div className="md:max-w-160">
<SettingsAccountPassword />
</div>
<SettingsAccountPassword className="md:max-w-160" />
<Divider />
+46 -11
View File
@@ -1,10 +1,11 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Divider, Form, Radio, type RadioChangeEvent, Select, theme } from "antd";
import { produce } from "immer";
import { useAppLocaleMenuItems } from "@/components/AppLocale";
import { useAppThemeMenuItems } from "@/components/AppTheme";
import { useBrowserTheme } from "@/hooks";
import { useAppSettings, useBrowserTheme } from "@/hooks";
const SettingsAppearance = () => {
const { t } = useTranslation();
@@ -12,16 +13,17 @@ const SettingsAppearance = () => {
return (
<>
<h2>{t("settings.appearance.theme.title")}</h2>
<div className="md:max-w-160">
<SettingsAppearanceTheme />
</div>
<SettingsAppearanceTheme className="md:max-w-160" />
<Divider />
<h2>{t("settings.appearance.language.title")}</h2>
<div className="md:max-w-160">
<SettingsAppearanceLanguage />
</div>
<SettingsAppearanceLanguage className="md:max-w-160" />
<Divider />
<h2>{t("settings.appearance.pagination.title")}</h2>
<SettingsPaginationLanguage className="md:max-w-160" />
</>
);
};
@@ -35,7 +37,7 @@ const SettingsAppearanceTheme = ({ className, style }: { className?: string; sty
const themeItems = useAppThemeMenuItems();
const [themeChanged, setThemeChanged] = useState(false);
const handleThemeChange = (e: RadioChangeEvent) => {
const handleChange = (e: RadioChangeEvent) => {
if (e.target.value !== themeMode) {
setThemeChanged(true);
setThemeMode(e.target.value);
@@ -46,7 +48,7 @@ const SettingsAppearanceTheme = ({ className, style }: { className?: string; sty
<div className={className} style={style}>
<Form layout="vertical">
<Form.Item extra={themeChanged ? t("settings.appearance.theme.form.value.extra") : undefined}>
<Radio.Group className="w-full" value={themeMode} onChange={handleThemeChange}>
<Radio.Group className="w-full" value={themeMode} onChange={handleChange}>
<div className="flex w-full items-center gap-4 max-md:flex-wrap">
{themeItems.map((item) => (
<div className="relative max-w-44 flex-1/3 max-md:flex-1/2 max-sm:flex-1" key={item.key}>
@@ -72,7 +74,7 @@ const SettingsAppearanceLanguage = ({ className, style }: { className?: string;
const localeItems = useAppLocaleMenuItems();
const [localeChanged, setLocaleChanged] = useState(false);
const handleLocaleChange = (value: string) => {
const handleChange = (value: string) => {
if (value !== i18n.language) {
setLocaleChanged(true);
i18n.changeLanguage(value);
@@ -90,7 +92,40 @@ const SettingsAppearanceLanguage = ({ className, style }: { className?: string;
label: item.label,
}))}
value={i18n.language}
onChange={handleLocaleChange}
onChange={handleChange}
/>
</Form.Item>
</Form>
</div>
);
};
const SettingsPaginationLanguage = ({ className, style }: { className?: string; style?: React.CSSProperties }) => {
const { t } = useTranslation();
const { appSettings: globalAppSettings, setAppSettings: setGlobalAppSettings } = useAppSettings();
const handleChange = (value: number) => {
setGlobalAppSettings(
produce(globalAppSettings, (draft) => {
draft.defaultPerPage = value;
})
);
};
return (
<div className={className} style={style}>
<Form layout="vertical">
<Form.Item label={t("settings.appearance.pagination.form.default_per_page.label")}>
<Select
options={[10, 15, 20, 30, 50, 100].map((value) => ({
key: value,
value: value,
label: `${value} ${t("settings.appearance.pagination.form.default_per_page.unit")}`,
}))}
placeholder={t("settings.appearance.pagination.form.default_per_page.placeholder")}
defaultValue={globalAppSettings.defaultPerPage}
onChange={handleChange}
/>
</Form.Item>
</Form>
+32 -10
View File
@@ -10,17 +10,20 @@ import { ClientResponseError } from "pocketbase";
import Empty from "@/components/Empty";
import WorkflowStatusIcon from "@/components/workflow/WorkflowStatusIcon";
import { WORKFLOW_TRIGGERS, type WorkflowModel, cloneNode, initWorkflow, isAllNodesValidated } from "@/domain/workflow";
import { useAppSettings } from "@/hooks";
import { list as listWorkflows, remove as removeWorkflow, save as saveWorkflow } from "@/repository/workflow";
import { getErrMsg } from "@/utils/error";
const WorkflowList = () => {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const [searchParams, setSearchParams] = useSearchParams();
const { t } = useTranslation();
const { message, modal, notification } = App.useApp();
const { appSettings: globalAppSettings } = useAppSettings();
const [filters, setFilters] = useState<Record<string, unknown>>(() => {
return {
keyword: searchParams.get("keyword"),
@@ -31,7 +34,7 @@ const WorkflowList = () => {
return {};
});
const [page, setPage] = useState<number>(() => parseInt(+searchParams.get("page")! + "") || 1);
const [pageSize, setPageSize] = useState<number>(() => parseInt(+searchParams.get("perPage")! + "") || 15);
const [pageSize, setPageSize] = useState<number>(() => parseInt(+searchParams.get("perPage")! + "") || globalAppSettings.defaultPerPage!);
const tableColumns: TableProps<WorkflowModel>["columns"] = [
{
@@ -185,6 +188,26 @@ const WorkflowList = () => {
},
{
refreshDeps: [filters, sorter, page, pageSize],
onBefore: () => {
setSearchParams((prev) => {
if (filters["keyword"]) {
prev.set("keyword", filters["keyword"] as string);
} else {
prev.delete("keyword");
}
if (filters["state"]) {
prev.set("state", filters["state"] as string);
} else {
prev.delete("state");
}
prev.set("page", page.toString());
prev.set("perPage", pageSize.toString());
return prev;
});
},
onSuccess: (res) => {
setTableData(res.items);
setTableTotal(res.totalItems);
@@ -213,6 +236,11 @@ const WorkflowList = () => {
refreshData();
};
const handlePaginationChange = (page: number, pageSize: number) => {
setPage(page);
setPageSize(pageSize);
};
const handleCreateClick = () => {
navigate("/workflows/new");
};
@@ -380,14 +408,8 @@ const WorkflowList = () => {
pageSize: pageSize,
total: tableTotal,
showSizeChanger: true,
onChange: (page: number, pageSize: number) => {
setPage(page);
setPageSize(pageSize);
},
onShowSizeChange: (page: number, pageSize: number) => {
setPage(page);
setPageSize(pageSize);
},
onChange: handlePaginationChange,
onShowSizeChange: handlePaginationChange,
}}
rowClassName="cursor-pointer"
rowKey={(record) => record.id}