mirror of
https://github.com/usual2970/certimate.git
synced 2025-06-11 06:59:51 +00:00
feat(ui): antd i18n
This commit is contained in:
parent
3b50741f19
commit
fdfe54b6da
55
ui/package-lock.json
generated
55
ui/package-lock.json
generated
@ -26,6 +26,7 @@
|
||||
"@radix-ui/react-toast": "^1.2.1",
|
||||
"@radix-ui/react-tooltip": "^1.1.2",
|
||||
"@tanstack/react-table": "^8.20.5",
|
||||
"ahooks": "^3.8.4",
|
||||
"antd": "^5.22.2",
|
||||
"antd-zod": "^6.0.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
@ -38,7 +39,6 @@
|
||||
"immer": "^10.1.1",
|
||||
"jszip": "^3.10.1",
|
||||
"lucide-react": "^0.417.0",
|
||||
"moment": "^2.30.1",
|
||||
"nanoid": "^5.0.7",
|
||||
"pocketbase": "^0.21.4",
|
||||
"react": "^18.3.1",
|
||||
@ -3607,6 +3607,28 @@
|
||||
"object-assign": "4.x"
|
||||
}
|
||||
},
|
||||
"node_modules/ahooks": {
|
||||
"version": "3.8.4",
|
||||
"resolved": "https://registry.npmmirror.com/ahooks/-/ahooks-3.8.4.tgz",
|
||||
"integrity": "sha512-39wDEw2ZHvypaT14EpMMk4AzosHWt0z9bulY0BeDsvc9PqJEV+Kjh/4TZfftSsotBMq52iYIOFPd3PR56e0ZJg==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.21.0",
|
||||
"dayjs": "^1.9.1",
|
||||
"intersection-observer": "^0.12.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"lodash": "^4.17.21",
|
||||
"react-fast-compare": "^3.2.2",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"screenfull": "^5.0.0",
|
||||
"tslib": "^2.4.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz",
|
||||
@ -5345,6 +5367,11 @@
|
||||
"resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"node_modules/intersection-observer": {
|
||||
"version": "0.12.2",
|
||||
"resolved": "https://registry.npmmirror.com/intersection-observer/-/intersection-observer-0.12.2.tgz",
|
||||
"integrity": "sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg=="
|
||||
},
|
||||
"node_modules/invariant": {
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmmirror.com/invariant/-/invariant-2.2.4.tgz",
|
||||
@ -5454,6 +5481,14 @@
|
||||
"jiti": "bin/jiti.js"
|
||||
}
|
||||
},
|
||||
"node_modules/js-cookie": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmmirror.com/js-cookie/-/js-cookie-3.0.5.tgz",
|
||||
"integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@ -5699,6 +5734,8 @@
|
||||
"version": "2.30.1",
|
||||
"resolved": "https://registry.npmmirror.com/moment/-/moment-2.30.1.tgz",
|
||||
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
@ -6842,6 +6879,11 @@
|
||||
"react": "^18.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-fast-compare": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmmirror.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz",
|
||||
"integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ=="
|
||||
},
|
||||
"node_modules/react-hook-form": {
|
||||
"version": "7.52.1",
|
||||
"resolved": "https://registry.npmmirror.com/react-hook-form/-/react-hook-form-7.52.1.tgz",
|
||||
@ -7173,6 +7215,17 @@
|
||||
"loose-envify": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/screenfull": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/screenfull/-/screenfull-5.2.0.tgz",
|
||||
"integrity": "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/scroll-into-view-if-needed": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz",
|
||||
|
@ -28,6 +28,7 @@
|
||||
"@radix-ui/react-toast": "^1.2.1",
|
||||
"@radix-ui/react-tooltip": "^1.1.2",
|
||||
"@tanstack/react-table": "^8.20.5",
|
||||
"ahooks": "^3.8.4",
|
||||
"antd": "^5.22.2",
|
||||
"antd-zod": "^6.0.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
@ -40,7 +41,6 @@
|
||||
"immer": "^10.1.1",
|
||||
"jszip": "^3.10.1",
|
||||
"lucide-react": "^0.417.0",
|
||||
"moment": "^2.30.1",
|
||||
"nanoid": "^5.0.7",
|
||||
"pocketbase": "^0.21.4",
|
||||
"react": "^18.3.1",
|
||||
|
49
ui/src/App.tsx
Normal file
49
ui/src/App.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import { useLayoutEffect, useState } from "react";
|
||||
import { RouterProvider } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { App as AntdApp, ConfigProvider as AntdConfigProvider } from "antd";
|
||||
import { type Locale } from "antd/es/locale";
|
||||
import AntdLocaleEnUs from "antd/locale/en_US";
|
||||
import AntdLocaleZhCN from "antd/locale/zh_CN";
|
||||
import dayjs from "dayjs";
|
||||
import "dayjs/locale/zh-cn";
|
||||
|
||||
import { localeNames } from "./i18n";
|
||||
import { router } from "./router.tsx";
|
||||
import { ThemeProvider } from "./components/ThemeProvider.tsx";
|
||||
|
||||
const App = () => {
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
const antdLocalesMap: Record<string, Locale> = {
|
||||
[localeNames.ZH]: AntdLocaleZhCN,
|
||||
[localeNames.EN]: AntdLocaleEnUs,
|
||||
};
|
||||
const [antdLocale, setAntdLocale] = useState(antdLocalesMap[i18n.language]);
|
||||
|
||||
const handleLanguageChanged = () => {
|
||||
setAntdLocale(antdLocalesMap[i18n.language]);
|
||||
dayjs.locale(i18n.language);
|
||||
};
|
||||
i18n.on("languageChanged", handleLanguageChanged);
|
||||
useLayoutEffect(handleLanguageChanged, [i18n]);
|
||||
|
||||
return (
|
||||
<AntdConfigProvider
|
||||
locale={antdLocale}
|
||||
theme={{
|
||||
token: {
|
||||
colorPrimary: "hsl(24.6 95% 53.1%)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<AntdApp>
|
||||
<ThemeProvider defaultTheme="system" storageKey="vite-ui-theme">
|
||||
<RouterProvider router={router} />
|
||||
</ThemeProvider>
|
||||
</AntdApp>
|
||||
</AntdConfigProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
@ -1,25 +0,0 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Languages } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||
|
||||
export default function LocaleToggle() {
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="icon">
|
||||
<Languages className="h-[1.2rem] w-[1.2rem] dark:text-white" />
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{Object.keys(i18n.store.data).map((key) => (
|
||||
<DropdownMenuItem onClick={() => i18n.changeLanguage(key)}>{i18n.store.data[key].name as string}</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
@ -2,14 +2,14 @@ import i18n from "i18next";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
import LanguageDetector from "i18next-browser-languagedetector";
|
||||
|
||||
import resources from "./locales";
|
||||
import resources, { LOCALE_ZH_NAME, LOCALE_EN_NAME } from "./locales";
|
||||
|
||||
i18n
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources,
|
||||
fallbackLng: "zh",
|
||||
fallbackLng: LOCALE_ZH_NAME,
|
||||
debug: true,
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
@ -19,4 +19,9 @@ i18n
|
||||
},
|
||||
});
|
||||
|
||||
export const localeNames = {
|
||||
ZH: LOCALE_ZH_NAME,
|
||||
EN: LOCALE_EN_NAME,
|
||||
};
|
||||
|
||||
export default i18n;
|
||||
|
@ -3,12 +3,15 @@ import { Resource } from "i18next";
|
||||
import zh from "./zh";
|
||||
import en from "./en";
|
||||
|
||||
export const LOCALE_ZH_NAME = "zh" as const;
|
||||
export const LOCALE_EN_NAME = "en" as const;
|
||||
|
||||
const resources: Resource = {
|
||||
zh: {
|
||||
[LOCALE_ZH_NAME]: {
|
||||
name: "简体中文",
|
||||
translation: zh,
|
||||
},
|
||||
en: {
|
||||
[LOCALE_EN_NAME]: {
|
||||
name: "English",
|
||||
translation: en,
|
||||
},
|
||||
|
@ -1,31 +1,17 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { RouterProvider } from "react-router-dom";
|
||||
import { App, ConfigProvider } from "antd";
|
||||
import AntdLocaleZhCN from "antd/locale/zh_CN";
|
||||
import dayjs from "dayjs";
|
||||
import dayjsUtc from "dayjs/plugin/utc";
|
||||
import "dayjs/locale/zh-cn";
|
||||
|
||||
import { router } from "./router.tsx";
|
||||
import { ThemeProvider } from "./components/ThemeProvider.tsx";
|
||||
import App from "./App";
|
||||
import "./i18n";
|
||||
import "./global.css";
|
||||
|
||||
// TODO: antd i18n
|
||||
dayjs.extend(dayjsUtc);
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<App>
|
||||
<ConfigProvider
|
||||
locale={AntdLocaleZhCN}
|
||||
theme={{
|
||||
token: {
|
||||
colorPrimary: "hsl(24.6 95% 53.1%)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ThemeProvider defaultTheme="system" storageKey="vite-ui-theme">
|
||||
<RouterProvider router={router} />
|
||||
</ThemeProvider>
|
||||
</ConfigProvider>
|
||||
</App>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { Avatar, Button, Empty, Modal, notification, Space, Table, Tooltip, Typography, type TableProps } from "antd";
|
||||
import { PageHeader } from "@ant-design/pro-components";
|
||||
import { Copy as CopyIcon, Pencil as PencilIcon, Plus as PlusIcon, Trash2 as Trash2Icon } from "lucide-react";
|
||||
import moment from "moment";
|
||||
import dayjs from "dayjs";
|
||||
import { ClientResponseError } from "pocketbase";
|
||||
|
||||
import AccessEditDialog from "@/components/certimate/AccessEditDialog";
|
||||
@ -50,7 +50,7 @@ const AccessList = () => {
|
||||
title: t("common.text.created_at"),
|
||||
ellipsis: true,
|
||||
render: (_, record) => {
|
||||
return moment(record.created!).format("YYYY-MM-DD HH:mm:ss");
|
||||
return dayjs(record.created!).format("YYYY-MM-DD HH:mm:ss");
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -58,7 +58,7 @@ const AccessList = () => {
|
||||
title: t("common.text.updated_at"),
|
||||
ellipsis: true,
|
||||
render: (_, record) => {
|
||||
return moment(record.updated!).format("YYYY-MM-DD HH:mm:ss");
|
||||
return dayjs(record.updated!).format("YYYY-MM-DD HH:mm:ss");
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -4,7 +4,7 @@ import { useTranslation } from "react-i18next";
|
||||
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 { Eye as EyeIcon, Filter as FilterIcon } from "lucide-react";
|
||||
import moment from "moment";
|
||||
import dayjs from "dayjs";
|
||||
import { ClientResponseError } from "pocketbase";
|
||||
|
||||
import CertificateDetailDrawer from "@/components/certificate/CertificateDetailDrawer";
|
||||
@ -87,8 +87,8 @@ const CertificateList = () => {
|
||||
},
|
||||
filterIcon: () => <FilterIcon size={14} />,
|
||||
render: (_, record) => {
|
||||
const total = moment(record.expireAt).diff(moment(record.created), "d") + 1;
|
||||
const left = moment(record.expireAt).diff(moment(), "d");
|
||||
const total = dayjs(record.expireAt).diff(dayjs(record.created), "d") + 1;
|
||||
const left = dayjs(record.expireAt).diff(dayjs(), "d");
|
||||
return (
|
||||
<Space className="max-w-full" direction="vertical" size={4}>
|
||||
{left > 0 ? (
|
||||
@ -98,7 +98,7 @@ const CertificateList = () => {
|
||||
)}
|
||||
|
||||
<Typography.Text type="secondary">
|
||||
{t("certificate.props.expiry.expiration", { date: moment(record.expireAt).format("YYYY-MM-DD") })}
|
||||
{t("certificate.props.expiry.expiration", { date: dayjs(record.expireAt).format("YYYY-MM-DD") })}
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
);
|
||||
@ -132,7 +132,7 @@ const CertificateList = () => {
|
||||
title: t("common.text.created_at"),
|
||||
ellipsis: true,
|
||||
render: (_, record) => {
|
||||
return moment(record.created!).format("YYYY-MM-DD HH:mm:ss");
|
||||
return dayjs(record.created!).format("YYYY-MM-DD HH:mm:ss");
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -140,7 +140,7 @@ const CertificateList = () => {
|
||||
title: t("common.text.updated_at"),
|
||||
ellipsis: true,
|
||||
render: (_, record) => {
|
||||
return moment(record.updated!).format("YYYY-MM-DD HH:mm:ss");
|
||||
return dayjs(record.updated!).format("YYYY-MM-DD HH:mm:ss");
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -20,7 +20,7 @@ import {
|
||||
} from "antd";
|
||||
import { PageHeader } from "@ant-design/pro-components";
|
||||
import { Filter as FilterIcon, Pencil as PencilIcon, Plus as PlusIcon, Trash2 as Trash2Icon } from "lucide-react";
|
||||
import moment from "moment";
|
||||
import dayjs from "dayjs";
|
||||
import { ClientResponseError } from "pocketbase";
|
||||
|
||||
import { Workflow as WorkflowType } from "@/domain/workflow";
|
||||
@ -153,7 +153,7 @@ const WorkflowList = () => {
|
||||
title: t("common.text.created_at"),
|
||||
ellipsis: true,
|
||||
render: (_, record) => {
|
||||
return moment(record.created!).format("YYYY-MM-DD HH:mm:ss");
|
||||
return dayjs(record.created!).format("YYYY-MM-DD HH:mm:ss");
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -161,7 +161,7 @@ const WorkflowList = () => {
|
||||
title: t("common.text.updated_at"),
|
||||
ellipsis: true,
|
||||
render: (_, record) => {
|
||||
return moment(record.updated!).format("YYYY-MM-DD HH:mm:ss");
|
||||
return dayjs(record.updated!).format("YYYY-MM-DD HH:mm:ss");
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -1,4 +1,4 @@
|
||||
import moment from "moment";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { type Access } from "@/domain/access";
|
||||
import { getPocketBase } from "./pocketbase";
|
||||
@ -19,6 +19,6 @@ export const save = async (record: Access) => {
|
||||
};
|
||||
|
||||
export const remove = async (record: Access) => {
|
||||
record.deleted = moment.utc().format("YYYY-MM-DD HH:mm:ss");
|
||||
record.deleted = dayjs.utc().format("YYYY-MM-DD HH:mm:ss");
|
||||
return await getPocketBase().collection("access").update(record.id, record);
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
import dayjs from "dayjs";
|
||||
import { type RecordListOptions } from "pocketbase";
|
||||
import moment from "moment";
|
||||
|
||||
import { type Certificate } from "@/domain/certificate";
|
||||
import { getPocketBase } from "./pocketbase";
|
||||
@ -23,7 +23,7 @@ export const list = async (req: CertificateListReq) => {
|
||||
|
||||
if (req.state === "expireSoon") {
|
||||
options.filter = pb.filter("expireAt<{:expiredAt}", {
|
||||
expiredAt: moment().add(15, "d").toDate(),
|
||||
expiredAt: dayjs().add(15, "d").toDate(),
|
||||
});
|
||||
} else if (req.state === "expired") {
|
||||
options.filter = pb.filter("expireAt<={:expiredAt}", {
|
||||
|
Loading…
x
Reference in New Issue
Block a user