mirror of
https://github.com/certimate-go/certimate.git
synced 2026-06-13 21:01:32 +08:00
feat(ui): new ConsoleLayout
This commit is contained in:
parent
3bd2bee321
commit
37ec20e5a1
@ -142,6 +142,7 @@ export default defineConfig(
|
||||
...tailwindcssPlugin.configs["recommended-error"].rules,
|
||||
|
||||
"better-tailwindcss/enforce-consistent-line-wrapping": "off",
|
||||
"better-tailwindcss/no-unregistered-classes": "off",
|
||||
},
|
||||
settings: {
|
||||
"better-tailwindcss": {
|
||||
|
||||
@ -3,6 +3,8 @@ import { useTranslation } from "react-i18next";
|
||||
import { IconBook } from "@tabler/icons-react";
|
||||
import { Typography } from "antd";
|
||||
|
||||
import { APP_DOCUMENT_URL } from "@/domain/app";
|
||||
|
||||
export type AppDocumentLinkButtonProps = {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
@ -15,9 +17,9 @@ const AppDocumentLinkButton = (props: AppDocumentLinkButtonProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Typography.Link className={className} style={style} type="secondary" href="https://docs.certimate.me" target="_blank">
|
||||
<Typography.Link className={className} style={style} type="secondary" href={APP_DOCUMENT_URL} target="_blank">
|
||||
<div className="flex items-center justify-center space-x-1">
|
||||
{showIcon ? <IconBook size={16} /> : <></>}
|
||||
{showIcon ? <IconBook size="1em" /> : <></>}
|
||||
<span>{t("common.menu.document")}</span>
|
||||
</div>
|
||||
</Typography.Link>
|
||||
|
||||
@ -7,14 +7,7 @@ import { IconLanguageEnZh, IconLanguageZhEn } from "@/components/icons";
|
||||
import { localeNames, localeResources } from "@/i18n";
|
||||
import { mergeCls } from "@/utils/css";
|
||||
|
||||
export type AppLocaleDropdownProps = {
|
||||
children?: React.ReactNode;
|
||||
trigger?: DropdownProps["trigger"];
|
||||
};
|
||||
|
||||
const AppLocaleDropdown = (props: AppLocaleDropdownProps) => {
|
||||
const { children, trigger = ["click"] } = props;
|
||||
|
||||
export const useAppLocaleMenuItems = () => {
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
const items: Required<MenuProps>["items"] = Object.keys(i18n.store.data).map((key) => {
|
||||
@ -30,6 +23,19 @@ const AppLocaleDropdown = (props: AppLocaleDropdownProps) => {
|
||||
};
|
||||
});
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
export type AppLocaleDropdownProps = {
|
||||
children?: React.ReactNode;
|
||||
trigger?: DropdownProps["trigger"];
|
||||
};
|
||||
|
||||
const AppLocaleDropdown = (props: AppLocaleDropdownProps) => {
|
||||
const { children, trigger = ["click"] } = props;
|
||||
|
||||
const items = useAppLocaleMenuItems();
|
||||
|
||||
return (
|
||||
<Dropdown menu={{ items }} trigger={trigger}>
|
||||
{children}
|
||||
@ -67,7 +73,7 @@ const AppLocaleLinkButton = (props: AppLocaleLinkButtonProps) => {
|
||||
<AppLocaleDropdown trigger={["click", "hover"]}>
|
||||
<Typography.Text className={mergeCls("cursor-pointer", className)} style={style} type="secondary">
|
||||
<div className="flex items-center justify-center space-x-1">
|
||||
{showIcon ? <AppLocaleIcon size={16} /> : <></>}
|
||||
{showIcon ? <AppLocaleIcon size="1em" /> : <></>}
|
||||
<span>{String(localeResources[i18n.language]?.name ?? t("common.menu.locale"))}</span>
|
||||
</div>
|
||||
</Typography.Text>
|
||||
|
||||
@ -7,26 +7,19 @@ import { Dropdown, type DropdownProps, type MenuProps, Typography } from "antd";
|
||||
import { useBrowserTheme } from "@/hooks";
|
||||
import { mergeCls } from "@/utils/css";
|
||||
|
||||
export type AppThemeDropdownProps = {
|
||||
children?: React.ReactNode;
|
||||
trigger?: DropdownProps["trigger"];
|
||||
};
|
||||
|
||||
const AppThemeDropdown = (props: AppThemeDropdownProps) => {
|
||||
const { children, trigger = ["click"] } = props;
|
||||
|
||||
export const useAppThemeMenuItems = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { themeMode, setThemeMode } = useBrowserTheme();
|
||||
|
||||
const items: Required<MenuProps>["items"] = [
|
||||
["light", t("common.theme.light"), <IconSun size={16} />],
|
||||
["dark", t("common.theme.dark"), <IconMoon size={16} />],
|
||||
["system", t("common.theme.system"), <IconSunMoon size={16} />],
|
||||
["light", "common.theme.light", <IconSun size="1em" />],
|
||||
["dark", "common.theme.dark", <IconMoon size="1em" />],
|
||||
["system", "common.theme.system", <IconSunMoon size="1em" />],
|
||||
].map(([key, label, icon]) => {
|
||||
return {
|
||||
key: key as string,
|
||||
label: label as string,
|
||||
label: t(label as string),
|
||||
icon: icon as React.ReactElement,
|
||||
onClick: () => {
|
||||
if (key !== themeMode) {
|
||||
@ -37,6 +30,19 @@ const AppThemeDropdown = (props: AppThemeDropdownProps) => {
|
||||
};
|
||||
});
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
export type AppThemeDropdownProps = {
|
||||
children?: React.ReactNode;
|
||||
trigger?: DropdownProps["trigger"];
|
||||
};
|
||||
|
||||
const AppThemeDropdown = (props: AppThemeDropdownProps) => {
|
||||
const { children, trigger = ["click"] } = props;
|
||||
|
||||
const items = useAppThemeMenuItems();
|
||||
|
||||
return (
|
||||
<Dropdown menu={{ items }} trigger={trigger}>
|
||||
{children}
|
||||
@ -69,7 +75,7 @@ const AppThemeLinkButton = (props: AppThemeLinkButtonProps) => {
|
||||
<AppThemeDropdown trigger={["click", "hover"]}>
|
||||
<Typography.Text className={mergeCls("cursor-pointer", className)} style={style} type="secondary">
|
||||
<div className="flex items-center justify-center space-x-1">
|
||||
{showIcon ? <AppThemeIcon size={16} /> : <></>}
|
||||
{showIcon ? <AppThemeIcon size="1em" /> : <></>}
|
||||
<span>{t(`common.theme.${themeMode}`)}</span>
|
||||
</div>
|
||||
</Typography.Text>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { memo } from "react";
|
||||
import { Badge, Typography } from "antd";
|
||||
|
||||
import { version } from "@/domain/version";
|
||||
import { APP_DOWNLOAD_URL, APP_VERSION } from "@/domain/app";
|
||||
import { useVersionChecker } from "@/hooks";
|
||||
|
||||
export type AppVersionLinkButtonProps = {
|
||||
@ -10,17 +10,37 @@ export type AppVersionLinkButtonProps = {
|
||||
};
|
||||
|
||||
const AppVersionLinkButton = ({ className, style }: AppVersionLinkButtonProps) => {
|
||||
return (
|
||||
<AppVersionBadge>
|
||||
<Typography.Link className={className} style={style} type="secondary" href={APP_DOWNLOAD_URL} target="_blank">
|
||||
{APP_VERSION}
|
||||
</Typography.Link>
|
||||
</AppVersionBadge>
|
||||
);
|
||||
};
|
||||
|
||||
export type AppVersionBadgeProps = {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
const AppVersionBadge = ({ className, style, children }: AppVersionBadgeProps) => {
|
||||
const { hasNewVersion } = useVersionChecker();
|
||||
|
||||
return (
|
||||
<Badge styles={{ indicator: { transform: "scale(0.75) translate(50%, -50%)" } }} count={hasNewVersion ? "NEW" : undefined}>
|
||||
<Typography.Link className={className} style={style} type="secondary" href="https://github.com/certimate-go/certimate/releases" target="_blank">
|
||||
{version}
|
||||
</Typography.Link>
|
||||
<Badge
|
||||
className={className}
|
||||
style={style}
|
||||
styles={{ indicator: { transform: "scale(0.75) translate(50%, -50%)" } }}
|
||||
count={hasNewVersion ? "NEW" : undefined}
|
||||
>
|
||||
{children}
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
export default {
|
||||
LinkButton: memo(AppVersionLinkButton),
|
||||
Badge: memo(AppVersionBadge),
|
||||
};
|
||||
|
||||
@ -33,7 +33,7 @@ const createIconComponent = (type: "outline" | "filled", iconName: string, iconA
|
||||
...iconAttrs,
|
||||
width: size,
|
||||
height: size,
|
||||
className: className,
|
||||
className: ["icon", className],
|
||||
...(type === "filled"
|
||||
? {
|
||||
fill: color,
|
||||
|
||||
8
ui/src/domain/app.ts
Normal file
8
ui/src/domain/app.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export const APP_REPO_URL = "https://github.com/certimate-go/certimate";
|
||||
|
||||
export const APP_DOWNLOAD_URL = APP_REPO_URL + "/releases";
|
||||
|
||||
export const APP_DOCUMENT_URL = "https://docs.certimate.me";
|
||||
|
||||
// fallback policy: .env > git tag > "v0.0.0-dev"
|
||||
export const APP_VERSION = import.meta.env.VITE_APP_VERSION || __APP_VERSION__ || "v0.0.0-dev";
|
||||
@ -1,2 +0,0 @@
|
||||
// fallback policy: .env > git tag > "v0.0.0-dev"
|
||||
export const version = import.meta.env.VITE_APP_VERSION || __APP_VERSION__ || "v0.0.0-dev";
|
||||
@ -24,9 +24,13 @@
|
||||
--foreground: 60 9.1% 97.8%;
|
||||
--primary: 20.5 90.2% 48.2%;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-btn > .ant-btn-icon {
|
||||
/* Fix non-antd icon's position not correct */
|
||||
@layer base {
|
||||
/* Fix non-antd icon's position not correct */
|
||||
.ant-btn > .ant-btn-icon,
|
||||
svg.tabler-icon,
|
||||
svg.icon {
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useRequest } from "ahooks";
|
||||
|
||||
import { version } from "@/domain/version";
|
||||
import { APP_VERSION } from "@/domain/app";
|
||||
|
||||
export type UseVersionCheckerReturns = {
|
||||
hasNewVersion: boolean;
|
||||
@ -42,12 +42,12 @@ const useVersionChecker = () => {
|
||||
.then((res) => res.json())
|
||||
.then((res) => Array.from(res));
|
||||
|
||||
const cIdx = releases.findIndex((e: any) => e.name === version);
|
||||
const cIdx = releases.findIndex((e: any) => e.name === APP_VERSION);
|
||||
if (cIdx === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const nIdx = releases.findIndex((e: any) => compareVersions(e.name, version) !== -1);
|
||||
const nIdx = releases.findIndex((e: any) => compareVersions(e.name, APP_VERSION) !== -1);
|
||||
if (cIdx !== -1 && cIdx <= nIdx) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"access.page.title": "Authorization",
|
||||
"access.page.title": "Credentials",
|
||||
|
||||
"access.nodata": "No accesses. Please create an credential first.",
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
|
||||
"common.text.copied": "Copied",
|
||||
"common.text.import_from_file": "Import from file ...",
|
||||
"common.text.happy_browser": "The browser version is too low, and Certimate WebUI is not working well. Recommend using modern browsers such as Google Chrome v119.0 or higher.",
|
||||
"common.text.happy_browser": "The browser version is too low to make Certimate WebUI working well. Recommend using modern browsers such as Google Chrome v119.0 or higher.",
|
||||
"common.text.nodata": "No data available",
|
||||
"common.text.operation_confirm": "Operation confirm",
|
||||
"common.text.operation_succeeded": "Operation succeeded",
|
||||
@ -23,6 +23,7 @@
|
||||
"common.menu.document": "Document",
|
||||
"common.menu.theme": "Change theme",
|
||||
"common.menu.locale": "Change language",
|
||||
"common.menu.gethelp": "Get help",
|
||||
"common.menu.logout": "Log-out",
|
||||
|
||||
"common.theme.light": "Light",
|
||||
|
||||
@ -23,6 +23,7 @@
|
||||
"common.menu.document": "文档",
|
||||
"common.menu.theme": "切换主题",
|
||||
"common.menu.locale": "切换语言",
|
||||
"common.menu.gethelp": "获取帮助",
|
||||
"common.menu.logout": "退出登录",
|
||||
|
||||
"common.theme.light": "浅色",
|
||||
|
||||
@ -12,5 +12,7 @@ body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
min-width: 320px;
|
||||
min-height: 480px;
|
||||
min-height: 100vh;
|
||||
min-height: calc(min(480px, 100vh));
|
||||
}
|
||||
|
||||
@ -2,20 +2,23 @@ import { memo, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Navigate, Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||
import {
|
||||
CloudServerOutlined as CloudServerOutlinedIcon,
|
||||
HomeOutlined as HomeOutlinedIcon,
|
||||
NodeIndexOutlined as NodeIndexOutlinedIcon,
|
||||
SafetyOutlined as SafetyOutlinedIcon,
|
||||
SettingOutlined as SettingOutlinedIcon,
|
||||
} from "@ant-design/icons";
|
||||
import { IconLogout, IconMenu2 } from "@tabler/icons-react";
|
||||
import { Alert, Button, Divider, Drawer, Layout, Menu, type MenuProps, Space, Tooltip, theme } from "antd";
|
||||
IconBrandGithub,
|
||||
IconCircuitChangeover,
|
||||
IconDashboard,
|
||||
IconFingerprint,
|
||||
IconHelpCircle,
|
||||
IconLogout,
|
||||
IconMenu2,
|
||||
IconSettings,
|
||||
IconShieldCheckered,
|
||||
} from "@tabler/icons-react";
|
||||
import { Alert, Button, Drawer, Layout, Menu, type MenuProps, theme } from "antd";
|
||||
|
||||
import AppDocument from "@/components/AppDocument";
|
||||
import AppLocale from "@/components/AppLocale";
|
||||
import AppTheme from "@/components/AppTheme";
|
||||
import AppLocale, { useAppLocaleMenuItems } from "@/components/AppLocale";
|
||||
import AppTheme, { useAppThemeMenuItems } from "@/components/AppTheme";
|
||||
import AppVersion from "@/components/AppVersion";
|
||||
import Show from "@/components/Show";
|
||||
import { APP_DOCUMENT_URL, APP_REPO_URL } from "@/domain/app";
|
||||
import { useTriggerElement } from "@/hooks";
|
||||
import { getAuthStore } from "@/repository/admin";
|
||||
import { isBrowserHappy } from "@/utils/browser";
|
||||
@ -27,63 +30,120 @@ const ConsoleLayout = () => {
|
||||
|
||||
const { token: themeToken } = theme.useToken();
|
||||
|
||||
const localeMenuItems = useAppLocaleMenuItems();
|
||||
const themeMenuItems = useAppThemeMenuItems();
|
||||
|
||||
const handleLogoutClick = () => {
|
||||
auth.clear();
|
||||
navigate("/login");
|
||||
};
|
||||
|
||||
const handleDocumentClick = () => {
|
||||
window.open(APP_DOCUMENT_URL, "_blank");
|
||||
};
|
||||
|
||||
const handleGitHubClick = () => {
|
||||
window.open(APP_REPO_URL, "_blank");
|
||||
};
|
||||
|
||||
const auth = getAuthStore();
|
||||
if (!auth.isValid || !auth.isSuperuser) {
|
||||
return <Navigate to="/login" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout className="h-screen" hasSider>
|
||||
<Layout.Sider className="fixed top-0 left-0 z-20 h-full max-md:static max-md:hidden" width="256px" theme="light">
|
||||
<div className="flex size-full flex-col items-center justify-between overflow-hidden">
|
||||
<div className="w-full">
|
||||
<SiderMenu />
|
||||
</div>
|
||||
<div className="w-full py-2 text-center">
|
||||
<Space align="center" split={<Divider type="vertical" />} size={4}>
|
||||
<AppDocument.LinkButton />
|
||||
<AppVersion.LinkButton />
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
</Layout.Sider>
|
||||
<Layout className="h-screen">
|
||||
<Show when={!isBrowserHappy()}>
|
||||
<Alert message={t("common.text.happy_browser")} type="warning" showIcon closable />
|
||||
</Show>
|
||||
|
||||
<Layout className="flex flex-col overflow-hidden pl-[256px] max-md:pl-0">
|
||||
<Show when={!isBrowserHappy()}>
|
||||
<Alert message={t("common.text.happy_browser")} type="warning" showIcon closable />
|
||||
</Show>
|
||||
|
||||
<Layout.Header className="shadow-xs" style={{ background: themeToken.colorBgContainer, padding: 0 }}>
|
||||
<div className="flex size-full items-center justify-between overflow-hidden px-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<SiderMenuDrawer trigger={<Button className="md:hidden" icon={<IconMenu2 size={20} stroke="1.25" />} />} />
|
||||
<Layout className="h-screen" hasSider>
|
||||
<Layout.Sider className="z-20 h-full max-md:static max-md:hidden" width="256px" theme="light">
|
||||
<div className="flex size-full flex-col items-center justify-between overflow-hidden select-none">
|
||||
<div className="w-full">
|
||||
<SiderMenu />
|
||||
</div>
|
||||
<div className="flex size-full grow items-center justify-end gap-4 overflow-hidden">
|
||||
<AppTheme.Dropdown>
|
||||
<Tooltip title={t("common.menu.theme")} mouseEnterDelay={2}>
|
||||
<Button icon={<AppTheme.Icon size={20} stroke="1.25" />} />
|
||||
</Tooltip>
|
||||
</AppTheme.Dropdown>
|
||||
<AppLocale.Dropdown>
|
||||
<Tooltip title={t("common.menu.locale")} mouseEnterDelay={2}>
|
||||
<Button icon={<AppLocale.Icon size={20} stroke="1.25" />} />
|
||||
</Tooltip>
|
||||
</AppLocale.Dropdown>
|
||||
<Tooltip title={t("common.menu.logout")} mouseEnterDelay={2}>
|
||||
<Button danger icon={<IconLogout size={20} stroke="1.25" />} onClick={handleLogoutClick} />
|
||||
</Tooltip>
|
||||
<div className="w-full">
|
||||
<Menu
|
||||
style={{ borderInlineEnd: "none" }}
|
||||
items={[
|
||||
{
|
||||
type: "divider",
|
||||
},
|
||||
{
|
||||
key: "theme",
|
||||
icon: (
|
||||
<span className="anticon">
|
||||
<AppTheme.Icon size="1em" />
|
||||
</span>
|
||||
),
|
||||
label: t("common.menu.theme"),
|
||||
children: themeMenuItems,
|
||||
},
|
||||
{
|
||||
key: "locale",
|
||||
icon: (
|
||||
<span className="anticon">
|
||||
<AppLocale.Icon size="1em" />
|
||||
</span>
|
||||
),
|
||||
label: t("common.menu.locale"),
|
||||
children: localeMenuItems,
|
||||
},
|
||||
{
|
||||
key: "document",
|
||||
icon: (
|
||||
<span className="anticon">
|
||||
<IconHelpCircle size="1em" />
|
||||
</span>
|
||||
),
|
||||
label: t("common.menu.gethelp"),
|
||||
onClick: handleDocumentClick,
|
||||
},
|
||||
{
|
||||
key: "logout",
|
||||
danger: true,
|
||||
icon: (
|
||||
<span className="anticon">
|
||||
<IconLogout size="1em" />
|
||||
</span>
|
||||
),
|
||||
label: t("common.menu.logout"),
|
||||
onClick: handleLogoutClick,
|
||||
},
|
||||
]}
|
||||
mode="vertical"
|
||||
selectable={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Layout.Header>
|
||||
</Layout.Sider>
|
||||
|
||||
<Layout.Content className="flex-1 overflow-x-hidden overflow-y-auto">
|
||||
<Outlet />
|
||||
</Layout.Content>
|
||||
<Layout className="flex flex-col overflow-hidden">
|
||||
<Layout.Header className="shadow-xs md:hidden" style={{ background: themeToken.colorBgContainer, padding: 0 }}>
|
||||
<div className="flex size-full items-center justify-between overflow-hidden px-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<SiderMenuDrawer trigger={<Button icon={<IconMenu2 size={18} stroke="1.25" />} />} />
|
||||
</div>
|
||||
<div className="flex size-full grow items-center justify-end gap-4 overflow-hidden">
|
||||
<AppTheme.Dropdown>
|
||||
<Button icon={<AppTheme.Icon size={18} stroke="1.25" />} />
|
||||
</AppTheme.Dropdown>
|
||||
<AppLocale.Dropdown>
|
||||
<Button icon={<AppLocale.Icon size={18} stroke="1.25" />} />
|
||||
</AppLocale.Dropdown>
|
||||
<AppVersion.Badge>
|
||||
<Button icon={<IconBrandGithub size={18} stroke="1.25" />} onClick={handleGitHubClick} />
|
||||
</AppVersion.Badge>
|
||||
<Button danger icon={<IconLogout size={18} stroke="1.25" />} onClick={handleLogoutClick} />
|
||||
</div>
|
||||
</div>
|
||||
</Layout.Header>
|
||||
|
||||
<Layout.Content className="flex-1 overflow-x-hidden overflow-y-auto">
|
||||
<Outlet />
|
||||
</Layout.Content>
|
||||
</Layout>
|
||||
</Layout>
|
||||
</Layout>
|
||||
);
|
||||
@ -101,15 +161,15 @@ const SiderMenu = memo(({ onSelect }: { onSelect?: (key: string) => void }) => {
|
||||
const MENU_KEY_ACCESSES = "/accesses";
|
||||
const MENU_KEY_SETTINGS = "/settings";
|
||||
const menuItems: Required<MenuProps>["items"] = [
|
||||
[MENU_KEY_HOME, <HomeOutlinedIcon />, t("dashboard.page.title")],
|
||||
[MENU_KEY_WORKFLOWS, <NodeIndexOutlinedIcon />, t("workflow.page.title")],
|
||||
[MENU_KEY_CERTIFICATES, <SafetyOutlinedIcon />, t("certificate.page.title")],
|
||||
[MENU_KEY_ACCESSES, <CloudServerOutlinedIcon />, t("access.page.title")],
|
||||
[MENU_KEY_SETTINGS, <SettingOutlinedIcon />, t("settings.page.title")],
|
||||
[MENU_KEY_HOME, <IconDashboard size="1em" />, t("dashboard.page.title")],
|
||||
[MENU_KEY_WORKFLOWS, <IconCircuitChangeover size="1em" />, t("workflow.page.title")],
|
||||
[MENU_KEY_CERTIFICATES, <IconShieldCheckered size="1em" />, t("certificate.page.title")],
|
||||
[MENU_KEY_ACCESSES, <IconFingerprint size="1em" />, t("access.page.title")],
|
||||
[MENU_KEY_SETTINGS, <IconSettings size="1em" />, t("settings.page.title")],
|
||||
].map(([key, icon, label]) => {
|
||||
return {
|
||||
key: key as string,
|
||||
icon: icon,
|
||||
icon: <span className="anticon">{icon}</span>,
|
||||
label: label,
|
||||
onClick: () => {
|
||||
navigate(key as string);
|
||||
@ -143,12 +203,14 @@ const SiderMenu = memo(({ onSelect }: { onSelect?: (key: string) => void }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex w-full items-center gap-2 overflow-hidden px-4 font-semibold">
|
||||
<div className="flex w-full items-center gap-2 overflow-hidden px-4">
|
||||
<img src="/logo.svg" className="size-[36px]" />
|
||||
<span className="h-[64px] w-[74px] truncate leading-[64px] dark:text-white">Certimate</span>
|
||||
<span className="h-[64px] w-[81px] truncate text-base leading-[64px] font-semibold">Certimate</span>
|
||||
<AppVersion.LinkButton className="text-xs" />
|
||||
</div>
|
||||
<div className="w-full grow overflow-x-hidden overflow-y-auto">
|
||||
<Menu
|
||||
style={{ borderInlineEnd: "none" }}
|
||||
items={menuItems}
|
||||
mode="vertical"
|
||||
selectedKeys={menuSelectedKey ? [menuSelectedKey] : []}
|
||||
|
||||
@ -14,7 +14,7 @@ import {
|
||||
SyncOutlined as SyncOutlinedIcon,
|
||||
} from "@ant-design/icons";
|
||||
import { PageHeader } from "@ant-design/pro-components";
|
||||
import { IconActivity, IconCircuitChangeover, IconShield, IconShieldExclamation, IconShieldX } from "@tabler/icons-react";
|
||||
import { IconActivity, IconCircuitChangeover, IconShieldCheckered, IconShieldExclamation, IconShieldX } from "@tabler/icons-react";
|
||||
import { useRequest } from "ahooks";
|
||||
import { Button, Card, Col, Divider, Empty, Flex, Grid, Row, Space, Statistic, Table, type TableProps, Tag, Typography, notification, theme } from "antd";
|
||||
import dayjs from "dayjs";
|
||||
@ -200,7 +200,7 @@ const Dashboard = () => {
|
||||
<Row className="justify-stretch" gutter={[16, 16]}>
|
||||
<Col {...statisticsGridSpans}>
|
||||
<StatisticCard
|
||||
icon={<IconShield size={48} strokeWidth={1} color={themeToken.colorInfo} />}
|
||||
icon={<IconShieldCheckered size={48} strokeWidth={1} color={themeToken.colorInfo} />}
|
||||
label={t("dashboard.statistics.all_certificates")}
|
||||
loading={statisticsLoading}
|
||||
value={statistics?.certificateTotal ?? "-"}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user