mirror of https://github.com/usual2970/certimate
feat(ui): optimize table UI
parent
5c6be439e8
commit
7db933199a
|
@ -66,16 +66,12 @@ const CertificateList = ({ withPagination }: CertificateListProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="">
|
<div className="">
|
||||||
{leftDays > 0 ? (
|
{leftDays > 0 ? (
|
||||||
<div className="text-green-500">
|
<div className="text-green-500">{t("certificate.props.expiry.left_days", { left: leftDays, total: allDays })}</div>
|
||||||
{leftDays} / {allDays} {t("certificate.props.expiry.days")}
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="text-red-500">{t("certificate.props.expiry.expired")}</div>
|
<div className="text-red-500">{t("certificate.props.expiry.expired")}</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div>
|
<div>{t("certificate.props.expiry.expiration", { date: new Date(expireAt).toLocaleString().split(" ")[0] })}</div>
|
||||||
{new Date(expireAt).toLocaleString().split(" ")[0]} {t("certificate.props.expiry.text.expire")}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -8,9 +8,11 @@
|
||||||
|
|
||||||
"certificate.props.domain": "Name",
|
"certificate.props.domain": "Name",
|
||||||
"certificate.props.expiry": "Expiry",
|
"certificate.props.expiry": "Expiry",
|
||||||
"certificate.props.expiry.days": "Days",
|
"certificate.props.expiry.left_days": "{{left}} / {{total}} days left",
|
||||||
"certificate.props.expiry.expired": "Expired",
|
"certificate.props.expiry.expired": "Expired",
|
||||||
"certificate.props.expiry.text.expire": "Expire",
|
"certificate.props.expiry.expiration": "Expire on {{date}}",
|
||||||
|
"certificate.props.expiry.filter.expire_soon": "Expire Soon",
|
||||||
|
"certificate.props.expiry.filter.expired": "Expired",
|
||||||
"certificate.props.workflow": "Workflow",
|
"certificate.props.workflow": "Workflow",
|
||||||
"certificate.props.source": "Source",
|
"certificate.props.source": "Source",
|
||||||
"certificate.props.certificate_chain": "Certificate Chain",
|
"certificate.props.certificate_chain": "Certificate Chain",
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
"common.delete.succeeded.message": "Delete Successful",
|
"common.delete.succeeded.message": "Delete Successful",
|
||||||
"common.delete.failed.message": "Delete Failed",
|
"common.delete.failed.message": "Delete Failed",
|
||||||
"common.next": "Next",
|
"common.next": "Next",
|
||||||
|
"common.reset": "Reset",
|
||||||
"common.confirm": "Confirm",
|
"common.confirm": "Confirm",
|
||||||
"common.cancel": "Cancel",
|
"common.cancel": "Cancel",
|
||||||
"common.submit": "Submit",
|
"common.submit": "Submit",
|
||||||
|
|
|
@ -21,7 +21,9 @@
|
||||||
"workflow.props.description": "Description",
|
"workflow.props.description": "Description",
|
||||||
"workflow.props.description.placeholder": "Please enter description",
|
"workflow.props.description.placeholder": "Please enter description",
|
||||||
"workflow.props.executionMethod": "Execution Method",
|
"workflow.props.executionMethod": "Execution Method",
|
||||||
"workflow.props.enabled": "Enabled",
|
"workflow.props.state": "State",
|
||||||
|
"workflow.props.state.filter.enabled": "Enabled",
|
||||||
|
"workflow.props.state.filter.disabled": "Disabled",
|
||||||
"workflow.props.createdAt": "Created",
|
"workflow.props.createdAt": "Created",
|
||||||
"workflow.props.updatedAt": "Updated",
|
"workflow.props.updatedAt": "Updated",
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"access.page.title": "授权管理",
|
"access.page.title": "授权管理",
|
||||||
|
|
||||||
"access.nodata": "暂无授权信息,请先创建。",
|
"access.nodata": "暂无授权信息,请先新建",
|
||||||
|
|
||||||
"access.action.add": "新建授权",
|
"access.action.add": "新建授权",
|
||||||
"access.action.edit": "编辑授权",
|
"access.action.edit": "编辑授权",
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
{
|
{
|
||||||
"certificate.page.title": "证书管理",
|
"certificate.page.title": "证书管理",
|
||||||
|
|
||||||
"certificate.nodata": "暂无证书,创建一个工作流去生成证书吧~ 😀",
|
"certificate.nodata": "暂无证书,新建一个工作流去生成证书吧~ 😀",
|
||||||
|
|
||||||
"certificate.action.view": "查看证书",
|
"certificate.action.view": "查看证书",
|
||||||
"certificate.action.download": "下载证书",
|
"certificate.action.download": "下载证书",
|
||||||
|
|
||||||
"certificate.props.domain": "名称",
|
"certificate.props.domain": "名称",
|
||||||
"certificate.props.expiry": "有效期限",
|
"certificate.props.expiry": "有效期限",
|
||||||
"certificate.props.expiry.days": "天",
|
"certificate.props.expiry.left_days": "{{left}} / {{total}} 天",
|
||||||
"certificate.props.expiry.expired": "已到期",
|
"certificate.props.expiry.expired": "已到期",
|
||||||
"certificate.props.expiry.text.expire": "到期",
|
"certificate.props.expiry.expiration": "{{date}} 到期",
|
||||||
|
"certificate.props.expiry.filter.expire_soon": "即将到期",
|
||||||
|
"certificate.props.expiry.filter.expired": "已到期",
|
||||||
"certificate.props.workflow": "所属工作流",
|
"certificate.props.workflow": "所属工作流",
|
||||||
"certificate.props.source": "来源",
|
"certificate.props.source": "来源",
|
||||||
"certificate.props.certificate_chain": "证书内容",
|
"certificate.props.certificate_chain": "证书内容",
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
"common.delete.succeeded.message": "删除成功",
|
"common.delete.succeeded.message": "删除成功",
|
||||||
"common.delete.failed.message": "删除失败",
|
"common.delete.failed.message": "删除失败",
|
||||||
"common.next": "下一步",
|
"common.next": "下一步",
|
||||||
|
"common.reset": "重置",
|
||||||
"common.confirm": "确认",
|
"common.confirm": "确认",
|
||||||
"common.cancel": "取消",
|
"common.cancel": "取消",
|
||||||
"common.submit": "提交",
|
"common.submit": "提交",
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
{
|
{
|
||||||
"workflow.page.title": "工作流",
|
"workflow.page.title": "工作流",
|
||||||
|
|
||||||
"certificate.nodata": "暂无证书,创建一个工作流去生成证书吧~ 😀",
|
"workflow.nodata": "暂无工作流,请先新建",
|
||||||
|
|
||||||
"workflow.nodata": "No workflows yet. Try to create a workflow to generate certificates! 😀",
|
|
||||||
|
|
||||||
"workflow.detail.title": "流程",
|
"workflow.detail.title": "流程",
|
||||||
"workflow.detail.history": "历史",
|
"workflow.detail.history": "历史",
|
||||||
|
@ -23,7 +21,9 @@
|
||||||
"workflow.props.description": "描述",
|
"workflow.props.description": "描述",
|
||||||
"workflow.props.description.placeholder": "请输入描述",
|
"workflow.props.description.placeholder": "请输入描述",
|
||||||
"workflow.props.executionMethod": "执行方式",
|
"workflow.props.executionMethod": "执行方式",
|
||||||
"workflow.props.enabled": "是否启用",
|
"workflow.props.state": "启用状态",
|
||||||
|
"workflow.props.state.filter.enabled": "启用",
|
||||||
|
"workflow.props.state.filter.disabled": "未启用",
|
||||||
"workflow.props.createdAt": "创建时间",
|
"workflow.props.createdAt": "创建时间",
|
||||||
"workflow.props.updatedAt": "更新时间",
|
"workflow.props.updatedAt": "更新时间",
|
||||||
|
|
||||||
|
|
|
@ -16,11 +16,6 @@ export const convertZulu2Beijing = (zuluTime: string) => {
|
||||||
return formattedBeijingTime;
|
return formattedBeijingTime;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getDate = (zuluTime: string) => {
|
|
||||||
const time = convertZulu2Beijing(zuluTime);
|
|
||||||
return time.split(" ")[0];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getLeftDays = (zuluTime: string) => {
|
export const getLeftDays = (zuluTime: string) => {
|
||||||
const time = convertZulu2Beijing(zuluTime);
|
const time = convertZulu2Beijing(zuluTime);
|
||||||
const date = time.split(" ")[0];
|
const date = time.split(" ")[0];
|
||||||
|
@ -38,49 +33,3 @@ export const diffDays = (date1: string, date2: string) => {
|
||||||
const days = Math.ceil(diff / (1000 * 60 * 60 * 24));
|
const days = Math.ceil(diff / (1000 * 60 * 60 * 24));
|
||||||
return days;
|
return days;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getTimeBefore(days: number): string {
|
|
||||||
// 获取当前时间
|
|
||||||
const currentDate = new Date();
|
|
||||||
|
|
||||||
// 减去指定的天数
|
|
||||||
currentDate.setUTCDate(currentDate.getUTCDate() - days);
|
|
||||||
|
|
||||||
// 格式化日期为 yyyy-mm-dd
|
|
||||||
const year = currentDate.getUTCFullYear();
|
|
||||||
const month = String(currentDate.getUTCMonth() + 1).padStart(2, "0"); // 月份从 0 开始
|
|
||||||
const day = String(currentDate.getUTCDate()).padStart(2, "0");
|
|
||||||
|
|
||||||
// 格式化时间为 hh:ii:ss
|
|
||||||
const hours = String(currentDate.getUTCHours()).padStart(2, "0");
|
|
||||||
const minutes = String(currentDate.getUTCMinutes()).padStart(2, "0");
|
|
||||||
const seconds = String(currentDate.getUTCSeconds()).padStart(2, "0");
|
|
||||||
|
|
||||||
// 组合成 yyyy-mm-dd hh:ii:ss 格式
|
|
||||||
const formattedDate = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
||||||
|
|
||||||
return formattedDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getTimeAfter(days: number): string {
|
|
||||||
// 获取当前时间
|
|
||||||
const currentDate = new Date();
|
|
||||||
|
|
||||||
// 加上指定的天数
|
|
||||||
currentDate.setUTCDate(currentDate.getUTCDate() + days);
|
|
||||||
|
|
||||||
// 格式化日期为 yyyy-mm-dd
|
|
||||||
const year = currentDate.getUTCFullYear();
|
|
||||||
const month = String(currentDate.getUTCMonth() + 1).padStart(2, "0"); // 月份从 0 开始
|
|
||||||
const day = String(currentDate.getUTCDate()).padStart(2, "0");
|
|
||||||
|
|
||||||
// 格式化时间为 hh:ii:ss
|
|
||||||
const hours = String(currentDate.getUTCHours()).padStart(2, "0");
|
|
||||||
const minutes = String(currentDate.getUTCMinutes()).padStart(2, "0");
|
|
||||||
const seconds = String(currentDate.getUTCSeconds()).padStart(2, "0");
|
|
||||||
|
|
||||||
// 组合成 yyyy-mm-dd hh:ii:ss 格式
|
|
||||||
const formattedDate = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
||||||
|
|
||||||
return formattedDate;
|
|
||||||
}
|
|
||||||
|
|
|
@ -24,9 +24,7 @@ const ConsoleLayout = () => {
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const {
|
const { token: themeToken } = theme.useToken();
|
||||||
token: { colorBgContainer },
|
|
||||||
} = theme.useToken();
|
|
||||||
|
|
||||||
const menuItems: Required<MenuProps>["items"] = [
|
const menuItems: Required<MenuProps>["items"] = [
|
||||||
{
|
{
|
||||||
|
@ -56,10 +54,15 @@ const ConsoleLayout = () => {
|
||||||
];
|
];
|
||||||
const [menuSelectedKey, setMenuSelectedKey] = useState<string>();
|
const [menuSelectedKey, setMenuSelectedKey] = useState<string>();
|
||||||
|
|
||||||
useEffect(() => {
|
const getActiveMenuItem = () => {
|
||||||
const item =
|
const item =
|
||||||
menuItems.find((item) => item!.key === location.pathname) ??
|
menuItems.find((item) => item!.key === location.pathname) ??
|
||||||
menuItems.find((item) => item!.key !== "/" && location.pathname.startsWith(item!.key as string));
|
menuItems.find((item) => item!.key !== "/" && location.pathname.startsWith(item!.key as string));
|
||||||
|
return item;
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const item = getActiveMenuItem();
|
||||||
if (item) {
|
if (item) {
|
||||||
setMenuSelectedKey(item.key as string);
|
setMenuSelectedKey(item.key as string);
|
||||||
} else {
|
} else {
|
||||||
|
@ -68,7 +71,7 @@ const ConsoleLayout = () => {
|
||||||
}, [location.pathname]);
|
}, [location.pathname]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (menuSelectedKey) {
|
if (menuSelectedKey && menuSelectedKey !== getActiveMenuItem()?.key) {
|
||||||
navigate(menuSelectedKey);
|
navigate(menuSelectedKey);
|
||||||
}
|
}
|
||||||
}, [menuSelectedKey]);
|
}, [menuSelectedKey]);
|
||||||
|
@ -116,7 +119,7 @@ const ConsoleLayout = () => {
|
||||||
</Layout.Sider>
|
</Layout.Sider>
|
||||||
|
|
||||||
<Layout>
|
<Layout>
|
||||||
<Layout.Header style={{ padding: 0, background: colorBgContainer }}>
|
<Layout.Header style={{ padding: 0, background: themeToken.colorBgContainer }}>
|
||||||
<div className="flex items-center justify-between size-full px-4 overflow-hidden">
|
<div className="flex items-center justify-between size-full px-4 overflow-hidden">
|
||||||
<div className="flex items-center gap-4 size-full">{/* <Button icon={<MenuIcon />} size="large" /> */}</div>
|
<div className="flex items-center gap-4 size-full">{/* <Button icon={<MenuIcon />} size="large" /> */}</div>
|
||||||
<div className="flex-grow flex items-center justify-end gap-4 size-full overflow-hidden">
|
<div className="flex-grow flex items-center justify-end gap-4 size-full overflow-hidden">
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Avatar, Button, Empty, Modal, notification, Space, Table, Tooltip, Typography, type TableProps } from "antd";
|
import { Avatar, Button, Empty, Modal, notification, Space, Table, Tooltip, Typography, type TableProps } from "antd";
|
||||||
import { PageHeader } from "@ant-design/pro-components";
|
import { PageHeader } from "@ant-design/pro-components";
|
||||||
|
@ -13,9 +13,6 @@ import { useConfigContext } from "@/providers/config";
|
||||||
const AccessList = () => {
|
const AccessList = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
// a flag to fix the twice-rendering issue in strict mode
|
|
||||||
const mountRef = useRef(true);
|
|
||||||
|
|
||||||
const [modalApi, ModelContextHolder] = Modal.useModal();
|
const [modalApi, ModelContextHolder] = Modal.useModal();
|
||||||
const [notificationApi, NotificationContextHolder] = notification.useNotification();
|
const [notificationApi, NotificationContextHolder] = notification.useNotification();
|
||||||
|
|
||||||
|
@ -134,11 +131,6 @@ const AccessList = () => {
|
||||||
}, [page, pageSize, configContext.config.accesses]);
|
}, [page, pageSize, configContext.config.accesses]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (mountRef.current) {
|
|
||||||
mountRef.current = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchTableData();
|
fetchTableData();
|
||||||
}, [fetchTableData]);
|
}, [fetchTableData]);
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Button, Empty, notification, Space, Table, Tooltip, Typography, type TableProps } from "antd";
|
import { Button, Divider, Empty, Menu, notification, Radio, Space, Table, theme, Tooltip, Typography, type MenuProps, type TableProps } from "antd";
|
||||||
import { PageHeader } from "@ant-design/pro-components";
|
import { PageHeader } from "@ant-design/pro-components";
|
||||||
import { Eye as EyeIcon } from "lucide-react";
|
import { Eye as EyeIcon, Filter as FilterIcon } from "lucide-react";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
|
|
||||||
import CertificateDetailDrawer from "@/components/certificate/CertificateDetailDrawer";
|
import CertificateDetailDrawer from "@/components/certificate/CertificateDetailDrawer";
|
||||||
import { Certificate as CertificateType } from "@/domain/certificate";
|
import { Certificate as CertificateType } from "@/domain/certificate";
|
||||||
import { list as listCertificate, type CertificateListReq } from "@/repository/certificate";
|
import { list as listCertificate, type CertificateListReq } from "@/repository/certificate";
|
||||||
import { diffDays, getLeftDays } from "@/lib/time";
|
|
||||||
|
|
||||||
const CertificateList = () => {
|
const CertificateList = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
@ -17,8 +16,7 @@ const CertificateList = () => {
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
// a flag to fix the twice-rendering issue in strict mode
|
const { token: themeToken } = theme.useToken();
|
||||||
const mountRef = useRef(true);
|
|
||||||
|
|
||||||
const [notificationApi, NotificationContextHolder] = notification.useNotification();
|
const [notificationApi, NotificationContextHolder] = notification.useNotification();
|
||||||
|
|
||||||
|
@ -40,21 +38,65 @@ const CertificateList = () => {
|
||||||
{
|
{
|
||||||
key: "expiry",
|
key: "expiry",
|
||||||
title: t("certificate.props.expiry"),
|
title: t("certificate.props.expiry"),
|
||||||
|
filterDropdown: ({ setSelectedKeys, confirm, clearFilters }) => {
|
||||||
|
const items: Required<MenuProps>["items"] = [
|
||||||
|
["expireSoon", "certificate.props.expiry.filter.expire_soon"],
|
||||||
|
["expired", "certificate.props.expiry.filter.expired"],
|
||||||
|
].map(([key, label]) => {
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
label: <Radio checked={filters["state"] === key}>{t(label)}</Radio>,
|
||||||
|
onClick: () => {
|
||||||
|
if (filters["state"] !== key) {
|
||||||
|
setFilters((prev) => ({ ...prev, state: key }));
|
||||||
|
setSelectedKeys([key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
confirm({ closeDropdown: true });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleResetClick = () => {
|
||||||
|
setFilters((prev) => ({ ...prev, state: undefined }));
|
||||||
|
setSelectedKeys([]);
|
||||||
|
clearFilters?.();
|
||||||
|
confirm();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmClick = () => {
|
||||||
|
confirm();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: 0 }}>
|
||||||
|
<Menu items={items} selectable={false} />
|
||||||
|
<Divider style={{ margin: 0 }} />
|
||||||
|
<Space className="justify-end w-full" style={{ padding: themeToken.paddingSM }}>
|
||||||
|
<Button size="small" disabled={!filters.state} onClick={handleResetClick}>
|
||||||
|
{t("common.reset")}
|
||||||
|
</Button>
|
||||||
|
<Button type="primary" size="small" onClick={handleConfirmClick}>
|
||||||
|
{t("common.confirm")}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
filterIcon: () => <FilterIcon size={14} />,
|
||||||
render: (_, record) => {
|
render: (_, record) => {
|
||||||
const leftDays = getLeftDays(record.expireAt);
|
const total = moment(record.expireAt).diff(moment(record.created), "d") + 1;
|
||||||
const allDays = diffDays(record.expireAt, record.created);
|
const left = moment(record.expireAt).diff(moment(), "d");
|
||||||
return (
|
return (
|
||||||
<Space className="max-w-full" direction="vertical" size={4}>
|
<Space className="max-w-full" direction="vertical" size={4}>
|
||||||
{leftDays > 0 ? (
|
{left > 0 ? (
|
||||||
<Typography.Text type="success">
|
<Typography.Text type="success">{t("certificate.props.expiry.left_days", { left, total })}</Typography.Text>
|
||||||
{leftDays} / {allDays} {t("certificate.props.expiry.days")}
|
|
||||||
</Typography.Text>
|
|
||||||
) : (
|
) : (
|
||||||
<Typography.Text type="danger">{t("certificate.props.expiry.expired")}</Typography.Text>
|
<Typography.Text type="danger">{t("certificate.props.expiry.expired")}</Typography.Text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Typography.Text type="secondary">
|
<Typography.Text type="secondary">
|
||||||
{moment(record.expireAt).format("YYYY-MM-DD")} {t("certificate.props.expiry.text.expire")}
|
{t("certificate.props.expiry.expiration", { date: moment(record.expireAt).format("YYYY-MM-DD") })}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</Space>
|
</Space>
|
||||||
);
|
);
|
||||||
|
@ -122,21 +164,31 @@ const CertificateList = () => {
|
||||||
const [tableData, setTableData] = useState<CertificateType[]>([]);
|
const [tableData, setTableData] = useState<CertificateType[]>([]);
|
||||||
const [tableTotal, setTableTotal] = useState<number>(0);
|
const [tableTotal, setTableTotal] = useState<number>(0);
|
||||||
|
|
||||||
|
const [filters, setFilters] = useState<Record<string, unknown>>({});
|
||||||
|
|
||||||
const [page, setPage] = useState<number>(1);
|
const [page, setPage] = useState<number>(1);
|
||||||
const [pageSize, setPageSize] = useState<number>(10);
|
const [pageSize, setPageSize] = useState<number>(10);
|
||||||
|
|
||||||
|
const [currentRecord, setCurrentRecord] = useState<CertificateType>();
|
||||||
|
|
||||||
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFilters({ ...filters, state: searchParams.get("state") });
|
||||||
|
setPage(parseInt(+searchParams.get("page")! + "") || 1);
|
||||||
|
setPageSize(parseInt(+searchParams.get("perPage")! + "") || 10);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const fetchTableData = useCallback(async () => {
|
const fetchTableData = useCallback(async () => {
|
||||||
if (loading) return;
|
if (loading) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const state = searchParams.get("state");
|
|
||||||
const req: CertificateListReq = { page: page, perPage: pageSize };
|
|
||||||
if (state) {
|
|
||||||
req.state = state as CertificateListReq["state"];
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await listCertificate(req);
|
const resp = await listCertificate({
|
||||||
|
page: page,
|
||||||
|
perPage: pageSize,
|
||||||
|
state: filters["state"] as CertificateListReq["state"],
|
||||||
|
});
|
||||||
|
|
||||||
setTableData(resp.items);
|
setTableData(resp.items);
|
||||||
setTableTotal(resp.totalItems);
|
setTableTotal(resp.totalItems);
|
||||||
|
@ -146,20 +198,12 @@ const CertificateList = () => {
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [page, pageSize]);
|
}, [filters, page, pageSize]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (mountRef.current) {
|
|
||||||
mountRef.current = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchTableData();
|
fetchTableData();
|
||||||
}, [fetchTableData]);
|
}, [fetchTableData]);
|
||||||
|
|
||||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
||||||
const [currentRecord, setCurrentRecord] = useState<CertificateType>();
|
|
||||||
|
|
||||||
const handleViewClick = (certificate: CertificateType) => {
|
const handleViewClick = (certificate: CertificateType) => {
|
||||||
setDrawerOpen(true);
|
setDrawerOpen(true);
|
||||||
setCurrentRecord(certificate);
|
setCurrentRecord(certificate);
|
||||||
|
@ -194,6 +238,10 @@ const CertificateList = () => {
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
rowKey={(record) => record.id}
|
rowKey={(record) => record.id}
|
||||||
|
onChange={(_, filters, __, extra) => {
|
||||||
|
console.log(filters);
|
||||||
|
extra.action === "filter" && fetchTableData();
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<CertificateDetailDrawer
|
<CertificateDetailDrawer
|
||||||
|
|
|
@ -1,13 +1,29 @@
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Button, Empty, Modal, notification, Space, Switch, Table, Tooltip, Typography, type TableProps } from "antd";
|
import {
|
||||||
|
Button,
|
||||||
|
Divider,
|
||||||
|
Empty,
|
||||||
|
Menu,
|
||||||
|
Modal,
|
||||||
|
notification,
|
||||||
|
Radio,
|
||||||
|
Space,
|
||||||
|
Switch,
|
||||||
|
Table,
|
||||||
|
theme,
|
||||||
|
Tooltip,
|
||||||
|
Typography,
|
||||||
|
type MenuProps,
|
||||||
|
type TableProps,
|
||||||
|
} from "antd";
|
||||||
import { PageHeader } from "@ant-design/pro-components";
|
import { PageHeader } from "@ant-design/pro-components";
|
||||||
import { Pencil as PencilIcon, Plus as PlusIcon, Trash2 as Trash2Icon } from "lucide-react";
|
import { Filter as FilterIcon, Pencil as PencilIcon, Plus as PlusIcon, Trash2 as Trash2Icon } from "lucide-react";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
|
|
||||||
import { Workflow as WorkflowType } from "@/domain/workflow";
|
import { Workflow as WorkflowType } from "@/domain/workflow";
|
||||||
import { list as listWorkflow, remove as removeWorkflow, save as saveWorkflow, type WorkflowListReq } from "@/repository/workflow";
|
import { list as listWorkflow, remove as removeWorkflow, save as saveWorkflow } from "@/repository/workflow";
|
||||||
|
|
||||||
const WorkflowList = () => {
|
const WorkflowList = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
@ -15,8 +31,7 @@ const WorkflowList = () => {
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
// a flag to fix the twice-rendering issue in strict mode
|
const { token: themeToken } = theme.useToken();
|
||||||
const mountRef = useRef(true);
|
|
||||||
|
|
||||||
const [modalApi, ModelContextHolder] = Modal.useModal();
|
const [modalApi, ModelContextHolder] = Modal.useModal();
|
||||||
const [notificationApi, NotificationContextHolder] = notification.useNotification();
|
const [notificationApi, NotificationContextHolder] = notification.useNotification();
|
||||||
|
@ -63,19 +78,63 @@ const WorkflowList = () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "enabled",
|
key: "state",
|
||||||
title: t("workflow.props.enabled"),
|
title: t("workflow.props.state"),
|
||||||
|
filterDropdown: ({ setSelectedKeys, confirm, clearFilters }) => {
|
||||||
|
const items: Required<MenuProps>["items"] = [
|
||||||
|
["enabled", "workflow.props.state.filter.enabled"],
|
||||||
|
["disabled", "workflow.props.state.filter.disabled"],
|
||||||
|
].map(([key, label]) => {
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
label: <Radio checked={filters["state"] === key}>{t(label)}</Radio>,
|
||||||
|
onClick: () => {
|
||||||
|
if (filters["state"] !== key) {
|
||||||
|
setFilters((prev) => ({ ...prev, state: key }));
|
||||||
|
setSelectedKeys([key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
confirm({ closeDropdown: true });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleResetClick = () => {
|
||||||
|
setFilters((prev) => ({ ...prev, state: undefined }));
|
||||||
|
setSelectedKeys([]);
|
||||||
|
clearFilters?.();
|
||||||
|
confirm();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmClick = () => {
|
||||||
|
confirm();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: 0 }}>
|
||||||
|
<Menu items={items} selectable={false} />
|
||||||
|
<Divider style={{ margin: 0 }} />
|
||||||
|
<Space className="justify-end w-full" style={{ padding: themeToken.paddingSM }}>
|
||||||
|
<Button size="small" disabled={!filters.state} onClick={handleResetClick}>
|
||||||
|
{t("common.reset")}
|
||||||
|
</Button>
|
||||||
|
<Button type="primary" size="small" onClick={handleConfirmClick}>
|
||||||
|
{t("common.confirm")}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
filterIcon: () => <FilterIcon size={14} />,
|
||||||
render: (_, record) => {
|
render: (_, record) => {
|
||||||
const enabled = record.enabled;
|
const enabled = record.enabled;
|
||||||
return (
|
return (
|
||||||
<>
|
<Switch
|
||||||
<Switch
|
checked={enabled}
|
||||||
checked={enabled}
|
onChange={() => {
|
||||||
onChange={() => {
|
handleEnabledChange(record);
|
||||||
handleEnabledChange(record);
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -136,22 +195,27 @@ const WorkflowList = () => {
|
||||||
const [tableData, setTableData] = useState<WorkflowType[]>([]);
|
const [tableData, setTableData] = useState<WorkflowType[]>([]);
|
||||||
const [tableTotal, setTableTotal] = useState<number>(0);
|
const [tableTotal, setTableTotal] = useState<number>(0);
|
||||||
|
|
||||||
|
const [filters, setFilters] = useState<Record<string, unknown>>({});
|
||||||
|
|
||||||
const [page, setPage] = useState<number>(1);
|
const [page, setPage] = useState<number>(1);
|
||||||
const [pageSize, setPageSize] = useState<number>(10);
|
const [pageSize, setPageSize] = useState<number>(10);
|
||||||
|
|
||||||
// TODO: 表头筛选
|
useEffect(() => {
|
||||||
|
setFilters({ ...filters, state: searchParams.get("state") });
|
||||||
|
setPage(parseInt(+searchParams.get("page")! + "") || 1);
|
||||||
|
setPageSize(parseInt(+searchParams.get("perPage")! + "") || 10);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const fetchTableData = useCallback(async () => {
|
const fetchTableData = useCallback(async () => {
|
||||||
if (loading) return;
|
if (loading) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const state = searchParams.get("state");
|
|
||||||
const req: WorkflowListReq = { page: page, perPage: pageSize };
|
|
||||||
if (state == "enabled") {
|
|
||||||
req.enabled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await listWorkflow(req);
|
const resp = await listWorkflow({
|
||||||
|
page: page,
|
||||||
|
perPage: pageSize,
|
||||||
|
enabled: (filters["state"] as string) === "enabled" ? true : (filters["state"] as string) === "disabled" ? false : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
setTableData(resp.items);
|
setTableData(resp.items);
|
||||||
setTableTotal(resp.totalItems);
|
setTableTotal(resp.totalItems);
|
||||||
|
@ -161,14 +225,9 @@ const WorkflowList = () => {
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [searchParams, page, pageSize]);
|
}, [filters, page, pageSize]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (mountRef.current) {
|
|
||||||
mountRef.current = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchTableData();
|
fetchTableData();
|
||||||
}, [fetchTableData]);
|
}, [fetchTableData]);
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { type RecordListOptions } from "pocketbase";
|
import { type RecordListOptions } from "pocketbase";
|
||||||
|
import moment from "moment";
|
||||||
|
|
||||||
import { type Certificate } from "@/domain/certificate";
|
import { type Certificate } from "@/domain/certificate";
|
||||||
import { getTimeAfter } from "@/lib/time";
|
|
||||||
import { getPocketBase } from "./pocketbase";
|
import { getPocketBase } from "./pocketbase";
|
||||||
|
|
||||||
export type CertificateListReq = {
|
export type CertificateListReq = {
|
||||||
|
@ -23,7 +23,7 @@ export const list = async (req: CertificateListReq) => {
|
||||||
|
|
||||||
if (req.state === "expireSoon") {
|
if (req.state === "expireSoon") {
|
||||||
options.filter = pb.filter("expireAt<{:expiredAt}", {
|
options.filter = pb.filter("expireAt<{:expiredAt}", {
|
||||||
expiredAt: getTimeAfter(15),
|
expiredAt: moment().add(15, "d").toDate(),
|
||||||
});
|
});
|
||||||
} else if (req.state === "expired") {
|
} else if (req.state === "expired") {
|
||||||
options.filter = pb.filter("expireAt<={:expiredAt}", {
|
options.filter = pb.filter("expireAt<={:expiredAt}", {
|
||||||
|
|
Loading…
Reference in New Issue