From 37ec20e5a1ee2db2da75b7604dc3ef4e17b2031b Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Tue, 15 Jul 2025 20:03:44 +0800 Subject: [PATCH] feat(ui): new ConsoleLayout --- ui/eslint.config.mjs | 1 + ui/src/components/AppDocument.tsx | 6 +- ui/src/components/AppLocale.tsx | 24 ++- ui/src/components/AppTheme.tsx | 32 +-- ui/src/components/AppVersion.tsx | 30 ++- .../components/icons/createIconComponent.ts | 2 +- ui/src/domain/app.ts | 8 + ui/src/domain/version.ts | 2 - ui/src/global.css | 8 +- ui/src/hooks/useVersionChecker.ts | 6 +- ui/src/i18n/locales/en/nls.access.json | 2 +- ui/src/i18n/locales/en/nls.common.json | 3 +- ui/src/i18n/locales/zh/nls.common.json | 1 + ui/src/index.css | 2 + ui/src/pages/ConsoleLayout.tsx | 182 ++++++++++++------ ui/src/pages/dashboard/Dashboard.tsx | 4 +- 16 files changed, 212 insertions(+), 101 deletions(-) create mode 100644 ui/src/domain/app.ts delete mode 100644 ui/src/domain/version.ts diff --git a/ui/eslint.config.mjs b/ui/eslint.config.mjs index eeced04e..78d9d481 100644 --- a/ui/eslint.config.mjs +++ b/ui/eslint.config.mjs @@ -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": { diff --git a/ui/src/components/AppDocument.tsx b/ui/src/components/AppDocument.tsx index 17b7a06a..b23ee6c4 100644 --- a/ui/src/components/AppDocument.tsx +++ b/ui/src/components/AppDocument.tsx @@ -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 ( - +
- {showIcon ? : <>} + {showIcon ? : <>} {t("common.menu.document")}
diff --git a/ui/src/components/AppLocale.tsx b/ui/src/components/AppLocale.tsx index e3f8ce9d..5d412dca 100644 --- a/ui/src/components/AppLocale.tsx +++ b/ui/src/components/AppLocale.tsx @@ -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["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 ( {children} @@ -67,7 +73,7 @@ const AppLocaleLinkButton = (props: AppLocaleLinkButtonProps) => {
- {showIcon ? : <>} + {showIcon ? : <>} {String(localeResources[i18n.language]?.name ?? t("common.menu.locale"))}
diff --git a/ui/src/components/AppTheme.tsx b/ui/src/components/AppTheme.tsx index ec961334..48cec7a8 100644 --- a/ui/src/components/AppTheme.tsx +++ b/ui/src/components/AppTheme.tsx @@ -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["items"] = [ - ["light", t("common.theme.light"), ], - ["dark", t("common.theme.dark"), ], - ["system", t("common.theme.system"), ], + ["light", "common.theme.light", ], + ["dark", "common.theme.dark", ], + ["system", "common.theme.system", ], ].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 ( {children} @@ -69,7 +75,7 @@ const AppThemeLinkButton = (props: AppThemeLinkButtonProps) => {
- {showIcon ? : <>} + {showIcon ? : <>} {t(`common.theme.${themeMode}`)}
diff --git a/ui/src/components/AppVersion.tsx b/ui/src/components/AppVersion.tsx index 4934a303..47a31a53 100644 --- a/ui/src/components/AppVersion.tsx +++ b/ui/src/components/AppVersion.tsx @@ -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 ( + + + {APP_VERSION} + + + ); +}; + +export type AppVersionBadgeProps = { + className?: string; + style?: React.CSSProperties; + children?: React.ReactNode; +}; + +const AppVersionBadge = ({ className, style, children }: AppVersionBadgeProps) => { const { hasNewVersion } = useVersionChecker(); return ( - - - {version} - + + {children} ); }; export default { LinkButton: memo(AppVersionLinkButton), + Badge: memo(AppVersionBadge), }; diff --git a/ui/src/components/icons/createIconComponent.ts b/ui/src/components/icons/createIconComponent.ts index 3fa06780..82f14f21 100644 --- a/ui/src/components/icons/createIconComponent.ts +++ b/ui/src/components/icons/createIconComponent.ts @@ -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, diff --git a/ui/src/domain/app.ts b/ui/src/domain/app.ts new file mode 100644 index 00000000..58c7cd9b --- /dev/null +++ b/ui/src/domain/app.ts @@ -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"; diff --git a/ui/src/domain/version.ts b/ui/src/domain/version.ts deleted file mode 100644 index 19cfffa0..00000000 --- a/ui/src/domain/version.ts +++ /dev/null @@ -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"; diff --git a/ui/src/global.css b/ui/src/global.css index f91b1709..8711e7a1 100644 --- a/ui/src/global.css +++ b/ui/src/global.css @@ -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; } } diff --git a/ui/src/hooks/useVersionChecker.ts b/ui/src/hooks/useVersionChecker.ts index 75e27eaa..3c6f1cf2 100644 --- a/ui/src/hooks/useVersionChecker.ts +++ b/ui/src/hooks/useVersionChecker.ts @@ -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; } diff --git a/ui/src/i18n/locales/en/nls.access.json b/ui/src/i18n/locales/en/nls.access.json index 0dd233d6..b543bade 100644 --- a/ui/src/i18n/locales/en/nls.access.json +++ b/ui/src/i18n/locales/en/nls.access.json @@ -1,5 +1,5 @@ { - "access.page.title": "Authorization", + "access.page.title": "Credentials", "access.nodata": "No accesses. Please create an credential first.", diff --git a/ui/src/i18n/locales/en/nls.common.json b/ui/src/i18n/locales/en/nls.common.json index 29d4f3d6..189f4bc9 100644 --- a/ui/src/i18n/locales/en/nls.common.json +++ b/ui/src/i18n/locales/en/nls.common.json @@ -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", diff --git a/ui/src/i18n/locales/zh/nls.common.json b/ui/src/i18n/locales/zh/nls.common.json index bb8ab7ea..872d1759 100644 --- a/ui/src/i18n/locales/zh/nls.common.json +++ b/ui/src/i18n/locales/zh/nls.common.json @@ -23,6 +23,7 @@ "common.menu.document": "文档", "common.menu.theme": "切换主题", "common.menu.locale": "切换语言", + "common.menu.gethelp": "获取帮助", "common.menu.logout": "退出登录", "common.theme.light": "浅色", diff --git a/ui/src/index.css b/ui/src/index.css index 8f84eac6..87e60fc1 100644 --- a/ui/src/index.css +++ b/ui/src/index.css @@ -12,5 +12,7 @@ body { margin: 0; padding: 0; min-width: 320px; + min-height: 480px; min-height: 100vh; + min-height: calc(min(480px, 100vh)); } diff --git a/ui/src/pages/ConsoleLayout.tsx b/ui/src/pages/ConsoleLayout.tsx index b9760678..c75c8f88 100644 --- a/ui/src/pages/ConsoleLayout.tsx +++ b/ui/src/pages/ConsoleLayout.tsx @@ -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 ; } return ( - - -
-
- -
-
- } size={4}> - - - -
-
-
+ + + + - - - - - - -
-
- } />} /> + + +
+
+
-
- - -
- + - - - + + +
+
+ } />} /> +
+
+ +
+
+
+ + + + +
); @@ -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["items"] = [ - [MENU_KEY_HOME, , t("dashboard.page.title")], - [MENU_KEY_WORKFLOWS, , t("workflow.page.title")], - [MENU_KEY_CERTIFICATES, , t("certificate.page.title")], - [MENU_KEY_ACCESSES, , t("access.page.title")], - [MENU_KEY_SETTINGS, , t("settings.page.title")], + [MENU_KEY_HOME, , t("dashboard.page.title")], + [MENU_KEY_WORKFLOWS, , t("workflow.page.title")], + [MENU_KEY_CERTIFICATES, , t("certificate.page.title")], + [MENU_KEY_ACCESSES, , t("access.page.title")], + [MENU_KEY_SETTINGS, , t("settings.page.title")], ].map(([key, icon, label]) => { return { key: key as string, - icon: icon, + icon: {icon}, label: label, onClick: () => { navigate(key as string); @@ -143,12 +203,14 @@ const SiderMenu = memo(({ onSelect }: { onSelect?: (key: string) => void }) => { return ( <> -
+
- Certimate + Certimate +
{ } + icon={} label={t("dashboard.statistics.all_certificates")} loading={statisticsLoading} value={statistics?.certificateTotal ?? "-"}