mirror of
https://github.com/certimate-go/certimate.git
synced 2026-09-24 23:10:13 +08:00
feat(ui): keep pagination in list pages
This commit is contained in:
@@ -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 };
|
||||
|
||||
@@ -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;
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "全局证书颁发机构",
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 />
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user