mirror of
https://github.com/zuiidea/antd-admin
synced 2026-09-24 01:55:12 +00:00
feat(with-lingui): register route, Lingui refresh, and MSW auth
Add register page and endpoint wiring; optional email on register schema; shared AppFooter for login/register; MSW register and session mocks; dashboard timeline and styles; locale catalogs and Lingui config. Made-with: Cursor
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import { formatter } from "@lingui/format-po";
|
||||
|
||||
const config = {
|
||||
locales: ["en", "zh"],
|
||||
sourceLocale: "en",
|
||||
@@ -7,7 +9,7 @@ const config = {
|
||||
include: ["src"],
|
||||
},
|
||||
],
|
||||
format: "po",
|
||||
format: formatter({ lineNumbers: false }),
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
@@ -16,34 +16,36 @@
|
||||
"test:e2e:core": "playwright test e2e/login.spec.ts e2e/users.spec.ts e2e/auth-refresh.spec.ts e2e/rbac.spec.ts e2e/url-state.spec.ts",
|
||||
"test:e2e:ui": "playwright test --ui",
|
||||
"i18n:extract": "vp exec lingui extract -- --clean",
|
||||
"i18n:compile": "vp exec lingui compile"
|
||||
"i18n:compile": "vp exec lingui compile",
|
||||
"i18n:check": "pnpm run i18n:extract && git diff --exit-code -- src/locales"
|
||||
},
|
||||
"dependencies": {
|
||||
"@lingui/core": "^5.9.3",
|
||||
"@lingui/react": "^5.9.3",
|
||||
"@tanstack/react-query": "^5.90.21",
|
||||
"@tanstack/react-router": "^1.167.4",
|
||||
"antd": "^6.3.3",
|
||||
"lucide-react": "^1.7.0",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"@lingui/core": "^6.0.0",
|
||||
"@lingui/react": "^6.0.0",
|
||||
"@tanstack/react-query": "^5.100.5",
|
||||
"@tanstack/react-router": "^1.168.25",
|
||||
"antd": "^6.3.7",
|
||||
"lucide-react": "^1.11.0",
|
||||
"react": "^19.2.5",
|
||||
"react-dom": "^19.2.5",
|
||||
"zod": "^4.3.6",
|
||||
"zustand": "^5.0.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lingui/cli": "^5.9.3",
|
||||
"@lingui/swc-plugin": "^5.11.0",
|
||||
"@lingui/vite-plugin": "^5.9.3",
|
||||
"@playwright/test": "^1.58.2",
|
||||
"@tanstack/router-plugin": "^1.166.13",
|
||||
"@lingui/cli": "^6.0.0",
|
||||
"@lingui/format-po": "^6.0.0",
|
||||
"@lingui/swc-plugin": "^6.0.0",
|
||||
"@lingui/vite-plugin": "^6.0.0",
|
||||
"@playwright/test": "^1.59.1",
|
||||
"@tanstack/router-plugin": "^1.167.28",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react-swc": "^4.3.0",
|
||||
"msw": "^2.12.12",
|
||||
"typescript": "~5.9.3",
|
||||
"msw": "^2.13.6",
|
||||
"typescript": "~6.0.3",
|
||||
"vite": "npm:@voidzero-dev/vite-plus-core@latest",
|
||||
"vite-plus": "latest",
|
||||
"vitest": "3.0.5"
|
||||
"vitest": "4.1.5"
|
||||
},
|
||||
"packageManager": "pnpm@10.32.1",
|
||||
"pnpm": {
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import type { AuthTokens, LoginRequest, User, MenuItem, PermissionsList } from "./schemas";
|
||||
import type {
|
||||
AuthTokens,
|
||||
LoginRequest,
|
||||
User,
|
||||
MenuItem,
|
||||
PermissionsList,
|
||||
RegisterRequest,
|
||||
} from "./schemas";
|
||||
|
||||
export const AUTH_ENDPOINTS = {
|
||||
login: "/api/auth/login",
|
||||
register: "/api/auth/register",
|
||||
refresh: "/api/auth/refresh",
|
||||
logout: "/api/auth/logout",
|
||||
user: "/api/auth/user",
|
||||
permissions: "/api/auth/permissions",
|
||||
} as const;
|
||||
|
||||
export type { AuthTokens, LoginRequest, User, MenuItem, PermissionsList };
|
||||
export type { AuthTokens, LoginRequest, User, MenuItem, PermissionsList, RegisterRequest };
|
||||
|
||||
@@ -34,6 +34,24 @@ export const LoginRequestSchema = z.object({
|
||||
|
||||
export type LoginRequest = z.infer<typeof LoginRequestSchema>;
|
||||
|
||||
/** Ant Design Form may submit `""` for an empty optional email */
|
||||
const registerOptionalEmailSchema = z
|
||||
.union([z.string(), z.undefined()])
|
||||
.transform((v) => {
|
||||
if (v === undefined) return undefined;
|
||||
const t = String(v).trim();
|
||||
return t === "" ? undefined : t;
|
||||
})
|
||||
.pipe(z.union([z.string().email(), z.undefined()]));
|
||||
|
||||
export const RegisterRequestSchema = z.object({
|
||||
username: z.string().min(1),
|
||||
password: z.string().min(6),
|
||||
email: registerOptionalEmailSchema,
|
||||
});
|
||||
|
||||
export type RegisterRequest = z.infer<typeof RegisterRequestSchema>;
|
||||
|
||||
export const PermissionsListSchema = z.array(z.string());
|
||||
|
||||
export type PermissionsList = z.infer<typeof PermissionsListSchema>;
|
||||
|
||||
@@ -1,41 +1,71 @@
|
||||
import { Flex, Typography, theme } from "antd";
|
||||
import { Button, Flex, Space, Typography, theme } from "antd";
|
||||
import { useLingui } from "@lingui/react/macro";
|
||||
import { Languages } from "lucide-react";
|
||||
import { useSettingsStore } from "@/stores/settings";
|
||||
import { GitHub } from "@/components/Icon";
|
||||
import { Theme } from "@/components/Icon";
|
||||
|
||||
const ANTD_ADMIN_REPO = "https://github.com/zuiidea/antd-admin";
|
||||
|
||||
export function AppFooter() {
|
||||
const { token } = theme.useToken();
|
||||
const { t } = useLingui();
|
||||
const locale = useSettingsStore((s) => s.locale);
|
||||
const setLocale = useSettingsStore((s) => s.setLocale);
|
||||
const toggleDarkMode = useSettingsStore((s) => s.toggleDarkMode);
|
||||
const iconSize = Math.max(12, Math.round(Number(token.fontSizeSM)));
|
||||
|
||||
const toggleLocale = () => {
|
||||
setLocale(locale === "en" ? "zh" : "en");
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex
|
||||
align="center"
|
||||
justify="center"
|
||||
wrap
|
||||
gap={4}
|
||||
style={{
|
||||
lineHeight: token.lineHeight,
|
||||
}}
|
||||
>
|
||||
<Typography.Text type="secondary" style={{ fontSize: token.fontSizeSM, marginBottom: 0 }}>
|
||||
{t`Powered by`}
|
||||
</Typography.Text>
|
||||
<a
|
||||
href={ANTD_ADMIN_REPO}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
<Flex vertical align="center" style={{ textAlign: "center" }}>
|
||||
<Flex justify="center" style={{ marginBottom: token.marginSM }}>
|
||||
<Space>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
onClick={toggleLocale}
|
||||
icon={<Languages size={token.size} />}
|
||||
aria-label={t`Switch language`}
|
||||
/>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
onClick={toggleDarkMode}
|
||||
icon={<Theme size={token.size} />}
|
||||
aria-label={t`Toggle Theme`}
|
||||
/>
|
||||
</Space>
|
||||
</Flex>
|
||||
<Flex
|
||||
align="center"
|
||||
justify="center"
|
||||
wrap
|
||||
gap={4}
|
||||
style={{
|
||||
color: token.colorLink,
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: 4,
|
||||
lineHeight: token.lineHeight,
|
||||
}}
|
||||
>
|
||||
<GitHub size={iconSize} />
|
||||
antd-admin
|
||||
</a>
|
||||
<Typography.Text type="secondary" style={{ fontSize: token.fontSizeSM, marginBottom: 0 }}>
|
||||
{t`Powered by`}
|
||||
</Typography.Text>
|
||||
<a
|
||||
href={ANTD_ADMIN_REPO}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{
|
||||
color: token.colorLink,
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
<GitHub size={iconSize} />
|
||||
antd-admin
|
||||
</a>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,299 +13,394 @@ msgstr ""
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/routes/_auth/users/index.tsx:263
|
||||
#: src/routes/_auth/users/index.tsx
|
||||
msgid "{total} rows"
|
||||
msgstr "{total} rows"
|
||||
|
||||
#: src/routes/_auth/dashboard/index.tsx:141
|
||||
#: src/routes/_auth/dashboard/index.tsx
|
||||
msgid "+180.1% from last month"
|
||||
msgstr "+180.1% from last month"
|
||||
|
||||
#: src/routes/_auth/dashboard/index.tsx:147
|
||||
#: src/routes/_auth/dashboard/index.tsx
|
||||
msgid "+19% from last month"
|
||||
msgstr "+19% from last month"
|
||||
|
||||
#: src/routes/_auth/dashboard/index.tsx:135
|
||||
#: src/routes/_auth/dashboard/index.tsx
|
||||
msgid "+20.1% from last month"
|
||||
msgstr "+20.1% from last month"
|
||||
|
||||
#: src/routes/_auth/dashboard/index.tsx:153
|
||||
#: src/routes/_auth/dashboard/index.tsx
|
||||
msgid "+201 since last hour"
|
||||
msgstr "+201 since last hour"
|
||||
|
||||
#: src/components/Layout/Header/index.tsx:79
|
||||
#: src/routes/_auth/dashboard/index.tsx
|
||||
msgid "08:30 · Deploy V3.2.0"
|
||||
msgstr "08:30 · Deploy V3.2.0"
|
||||
|
||||
#: src/routes/_auth/dashboard/index.tsx
|
||||
msgid "10:10 · Menu policy updated"
|
||||
msgstr "10:10 · Menu policy updated"
|
||||
|
||||
#: src/routes/_auth/dashboard/index.tsx
|
||||
msgid "13:20 · Security review"
|
||||
msgstr "13:20 · Security review"
|
||||
|
||||
#: src/routes/_auth/dashboard/index.tsx
|
||||
msgid "15:50 · Incident recovery"
|
||||
msgstr "15:50 · Incident recovery"
|
||||
|
||||
#: src/components/Layout/Header/index.tsx
|
||||
msgid "403"
|
||||
msgstr "403"
|
||||
|
||||
#: src/routes/_auth/dashboard/index.tsx:151
|
||||
#: src/routes/register/index.tsx
|
||||
msgid "Account created successfully"
|
||||
msgstr "Account created successfully"
|
||||
|
||||
#: src/routes/_auth/dashboard/index.tsx
|
||||
msgid "Active Now"
|
||||
msgstr "Active Now"
|
||||
|
||||
#: src/routes/_auth/users/-FormModal.tsx:52
|
||||
#: src/routes/_auth/users/-Toolbar.tsx:63
|
||||
#: src/routes/_auth/users/-FormModal.tsx
|
||||
#: src/routes/_auth/users/-Toolbar.tsx
|
||||
msgid "Admin"
|
||||
msgstr "Admin"
|
||||
|
||||
#: src/routes/_auth/users/index.tsx:125
|
||||
#: src/routes/_auth/dashboard/index.tsx
|
||||
msgid "Admin changed sidebar visibility and permission mapping."
|
||||
msgstr "Admin changed sidebar visibility and permission mapping."
|
||||
|
||||
#: src/routes/register/index.tsx
|
||||
msgid "Already have an account?"
|
||||
msgstr "Already have an account?"
|
||||
|
||||
#: src/routes/_auth/users/index.tsx
|
||||
msgid "Are you absolutely sure?"
|
||||
msgstr "Are you absolutely sure?"
|
||||
|
||||
#: src/routes/login/index.tsx:181
|
||||
#: src/routes/login/index.tsx
|
||||
msgid "Auto login"
|
||||
msgstr "Auto login"
|
||||
|
||||
#: src/components/NotFound/index.tsx:57
|
||||
#: src/routes/_auth/403/index.tsx:35
|
||||
#: src/components/NotFound/index.tsx
|
||||
#: src/routes/_auth/403/index.tsx
|
||||
msgid "Back to Home"
|
||||
msgstr "Back to Home"
|
||||
|
||||
#: src/routes/_auth/users/-FormModal.tsx:31
|
||||
#: src/routes/_auth/users/index.tsx:129
|
||||
#: src/routes/_auth/users/-FormModal.tsx
|
||||
#: src/routes/_auth/users/index.tsx
|
||||
msgid "Cancel"
|
||||
msgstr "Cancel"
|
||||
|
||||
#: src/routes/_auth/dashboard/index.tsx:247
|
||||
msgid "Chart Placeholder"
|
||||
msgstr "Chart Placeholder"
|
||||
#: src/routes/register/index.tsx
|
||||
msgid "Confirm password"
|
||||
msgstr "Confirm password"
|
||||
|
||||
#: src/hooks/useCrudToasts.ts:40
|
||||
#: src/routes/register/index.tsx
|
||||
msgid "Create account"
|
||||
msgstr "Create account"
|
||||
|
||||
#: src/hooks/useCrudToasts.ts
|
||||
msgid "Create failed"
|
||||
msgstr "Create failed"
|
||||
|
||||
#: src/routes/_auth/users/-Toolbar.tsx:88
|
||||
#: src/routes/_auth/users/-Toolbar.tsx
|
||||
msgid "Create User"
|
||||
msgstr "Create User"
|
||||
|
||||
#: src/hooks/useCrudToasts.ts:37
|
||||
#: src/hooks/useCrudToasts.ts
|
||||
msgid "Created successfully"
|
||||
msgstr "Created successfully"
|
||||
|
||||
#: src/components/Layout/Header/index.tsx:75
|
||||
#: src/components/Layout/Header/index.tsx:88
|
||||
#: src/components/Layout/Header/index.tsx:92
|
||||
#: src/components/Layout/Sidebar/index.tsx:32
|
||||
#: src/components/Layout/Header/index.tsx
|
||||
#: src/components/Layout/Sidebar/index.tsx
|
||||
msgid "Dashboard"
|
||||
msgstr "Dashboard"
|
||||
|
||||
#: src/routes/_auth/users/index.tsx:127
|
||||
#: src/routes/_auth/users/index.tsx:224
|
||||
#: src/routes/_auth/users/index.tsx
|
||||
msgid "Delete"
|
||||
msgstr "Delete"
|
||||
|
||||
#: src/hooks/useCrudToasts.ts:62
|
||||
#: src/hooks/useCrudToasts.ts
|
||||
msgid "Delete failed"
|
||||
msgstr "Delete failed"
|
||||
|
||||
#: src/hooks/useCrudToasts.ts:59
|
||||
#: src/hooks/useCrudToasts.ts
|
||||
msgid "Deleted successfully"
|
||||
msgstr "Deleted successfully"
|
||||
|
||||
#: src/hooks/useCrudToasts.ts:56
|
||||
#: src/hooks/useCrudToasts.ts
|
||||
msgid "Deleting…"
|
||||
msgstr "Deleting…"
|
||||
|
||||
#: src/components/Layout/Sidebar/index.tsx:34
|
||||
#: src/components/Layout/Sidebar/index.tsx
|
||||
msgid "Design Engineering"
|
||||
msgstr "Design Engineering"
|
||||
|
||||
#: src/routes/_auth/users/index.tsx:214
|
||||
#: src/routes/login/index.tsx
|
||||
msgid "Don't have an account?"
|
||||
msgstr "Don't have an account?"
|
||||
|
||||
#: src/routes/_auth/users/index.tsx
|
||||
msgid "Edit"
|
||||
msgstr "Edit"
|
||||
|
||||
#: src/routes/_auth/users/-FormModal.tsx:29
|
||||
#: src/routes/_auth/users/-FormModal.tsx
|
||||
msgid "Edit User"
|
||||
msgstr "Edit User"
|
||||
|
||||
#: src/routes/_auth/users/-FormModal.tsx:53
|
||||
#: src/routes/_auth/users/-Toolbar.tsx:64
|
||||
#: src/routes/_auth/users/-FormModal.tsx
|
||||
#: src/routes/_auth/users/-Toolbar.tsx
|
||||
msgid "Editor"
|
||||
msgstr "Editor"
|
||||
|
||||
#: src/routes/_auth/users/-FormModal.tsx:57
|
||||
#: src/routes/_auth/users/index.tsx:169
|
||||
#: src/routes/_auth/users/-FormModal.tsx
|
||||
#: src/routes/_auth/users/index.tsx
|
||||
msgid "Email"
|
||||
msgstr "Email"
|
||||
|
||||
#: src/routes/login/index.tsx:188
|
||||
#: src/routes/register/index.tsx
|
||||
msgid "Email address"
|
||||
msgstr "Email address"
|
||||
|
||||
#: src/routes/register/index.tsx
|
||||
msgid "Enter at least 6 characters"
|
||||
msgstr "Enter at least 6 characters"
|
||||
|
||||
#: src/routes/login/index.tsx
|
||||
msgid "Forgot password?"
|
||||
msgstr "Forgot password?"
|
||||
|
||||
#: src/components/NotFound/index.tsx:60
|
||||
#: src/components/NotFound/index.tsx
|
||||
msgid "Go back"
|
||||
msgstr "Go back"
|
||||
|
||||
#: src/routes/login/index.tsx:54
|
||||
#: src/routes/login/index.tsx
|
||||
msgid "Login failed"
|
||||
msgstr "Login failed"
|
||||
|
||||
#: src/routes/login/index.tsx:50
|
||||
#: src/routes/login/index.tsx
|
||||
msgid "Login successful"
|
||||
msgstr "Login successful"
|
||||
|
||||
#: src/routes/_auth/users/-Toolbar.tsx:91
|
||||
#: src/routes/_auth/users/-Toolbar.tsx:92
|
||||
#: src/routes/_auth/users/-Toolbar.tsx
|
||||
msgid "More filters"
|
||||
msgstr "More filters"
|
||||
|
||||
#: src/routes/_auth/users/-FormModal.tsx:29
|
||||
#: src/routes/_auth/users/-FormModal.tsx
|
||||
msgid "New User"
|
||||
msgstr "New User"
|
||||
|
||||
#: src/components/DataTable/DataTableEmpty.tsx:46
|
||||
#: src/components/DataTable/DataTableEmpty.tsx
|
||||
msgid "No data"
|
||||
msgstr "No data"
|
||||
|
||||
#: src/components/DataTable/DataTableEmpty.tsx:56
|
||||
#: src/components/DataTable/DataTableEmpty.tsx
|
||||
msgid "Nothing to show in this list yet"
|
||||
msgstr "Nothing to show in this list yet"
|
||||
|
||||
#: src/routes/_auth/users/-FormModal.tsx:30
|
||||
#: src/routes/_auth/users/-FormModal.tsx
|
||||
msgid "OK"
|
||||
msgstr "OK"
|
||||
|
||||
#: src/components/Layout/Sidebar/index.tsx:350
|
||||
#: src/components/Layout/Sidebar/index.tsx
|
||||
msgid "Open account menu"
|
||||
msgstr "Open account menu"
|
||||
|
||||
#: src/routes/_auth/dashboard/index.tsx:230
|
||||
msgid "Overview"
|
||||
msgstr "Overview"
|
||||
|
||||
#: src/routes/login/index.tsx:162
|
||||
#: src/routes/login/index.tsx:168
|
||||
#: src/routes/login/index.tsx
|
||||
#: src/routes/register/index.tsx
|
||||
msgid "Password"
|
||||
msgstr "Password"
|
||||
|
||||
#: src/components/Layout/Sidebar/index.tsx:30
|
||||
#: src/routes/register/index.tsx
|
||||
msgid "Password must be at least 6 characters long"
|
||||
msgstr "Password must be at least 6 characters long"
|
||||
|
||||
#: src/routes/register/index.tsx
|
||||
msgid "Passwords do not match"
|
||||
msgstr "Passwords do not match"
|
||||
|
||||
#: src/components/Layout/Sidebar/index.tsx
|
||||
msgid "Platform"
|
||||
msgstr "Platform"
|
||||
|
||||
#: src/routes/login/index.tsx:163
|
||||
#: src/routes/register/index.tsx
|
||||
msgid "Please confirm your password"
|
||||
msgstr "Please confirm your password"
|
||||
|
||||
#: src/routes/register/index.tsx
|
||||
msgid "Please enter a valid email address"
|
||||
msgstr "Please enter a valid email address"
|
||||
|
||||
#: src/routes/login/index.tsx
|
||||
msgid "Please enter password"
|
||||
msgstr "Please enter password"
|
||||
|
||||
#: src/routes/_auth/users/-FormModal.tsx:40
|
||||
#: src/routes/login/index.tsx:150
|
||||
#: src/routes/_auth/users/-FormModal.tsx
|
||||
#: src/routes/login/index.tsx
|
||||
msgid "Please enter username"
|
||||
msgstr "Please enter username"
|
||||
|
||||
#: src/routes/_auth/users/-FormModal.tsx:47
|
||||
#: src/routes/register/index.tsx
|
||||
msgid "Please enter your password"
|
||||
msgstr "Please enter your password"
|
||||
|
||||
#: src/routes/register/index.tsx
|
||||
msgid "Please enter your username"
|
||||
msgstr "Please enter your username"
|
||||
|
||||
#: src/routes/_auth/users/-FormModal.tsx
|
||||
msgid "Please select roles"
|
||||
msgstr "Please select roles"
|
||||
|
||||
#: src/components/RouteError.tsx:17
|
||||
#: src/components/RouteError.tsx
|
||||
msgid "Please try again or return to the dashboard."
|
||||
msgstr "Please try again or return to the dashboard."
|
||||
|
||||
#: src/components/Layout/AppFooter/index.tsx:23
|
||||
#: src/components/Layout/AppFooter/index.tsx
|
||||
msgid "Powered by"
|
||||
msgstr "Powered by"
|
||||
|
||||
#: src/components/Layout/Sidebar/index.tsx:31
|
||||
#: src/components/Layout/Sidebar/index.tsx
|
||||
msgid "Projects"
|
||||
msgstr "Projects"
|
||||
|
||||
#: src/routes/_auth/dashboard/index.tsx:256
|
||||
#: src/routes/register/index.tsx
|
||||
msgid "Re-enter your password"
|
||||
msgstr "Re-enter your password"
|
||||
|
||||
#: src/routes/_auth/dashboard/index.tsx
|
||||
msgid "Recent Sales"
|
||||
msgstr "Recent Sales"
|
||||
|
||||
#: src/components/RouteError.tsx:20
|
||||
#: src/routes/register/index.tsx
|
||||
msgid "Registration failed"
|
||||
msgstr "Registration failed"
|
||||
|
||||
#: src/routes/_auth/dashboard/index.tsx
|
||||
msgid "Release branch merged and production rollout completed."
|
||||
msgstr "Release branch merged and production rollout completed."
|
||||
|
||||
#: src/components/RouteError.tsx
|
||||
msgid "Retry"
|
||||
msgstr "Retry"
|
||||
|
||||
#: src/routes/_auth/users/-Toolbar.tsx:57
|
||||
#: src/routes/_auth/users/-Toolbar.tsx
|
||||
msgid "Role"
|
||||
msgstr "Role"
|
||||
|
||||
#: src/routes/_auth/users/-FormModal.tsx:46
|
||||
#: src/routes/_auth/users/index.tsx:176
|
||||
#: src/routes/_auth/users/-FormModal.tsx
|
||||
#: src/routes/_auth/users/index.tsx
|
||||
msgid "Roles"
|
||||
msgstr "Roles"
|
||||
|
||||
#: src/routes/_auth/users/index.tsx:235
|
||||
#: src/routes/_auth/users/index.tsx
|
||||
msgid "Row actions"
|
||||
msgstr "Row actions"
|
||||
|
||||
#: src/routes/_auth/dashboard/index.tsx:145
|
||||
#: src/routes/_auth/dashboard/index.tsx
|
||||
msgid "Sales"
|
||||
msgstr "Sales"
|
||||
|
||||
#: src/components/Layout/Sidebar/index.tsx:35
|
||||
#: src/components/Layout/Sidebar/index.tsx
|
||||
msgid "Sales & Marketing"
|
||||
msgstr "Sales & Marketing"
|
||||
|
||||
#: src/routes/_auth/users/-Toolbar.tsx:42
|
||||
#: src/routes/_auth/users/-Toolbar.tsx
|
||||
msgid "Search User"
|
||||
msgstr "Search User"
|
||||
|
||||
#: src/routes/login/index.tsx:200
|
||||
#: src/routes/register/index.tsx
|
||||
msgid "Sign in"
|
||||
msgstr "Sign in"
|
||||
|
||||
#: src/routes/login/index.tsx
|
||||
msgid "Sign In"
|
||||
msgstr "Sign In"
|
||||
|
||||
#: src/components/Layout/Sidebar/index.tsx:191
|
||||
#: src/components/Layout/Sidebar/index.tsx
|
||||
msgid "Sign Out"
|
||||
msgstr "Sign Out"
|
||||
|
||||
#: src/components/RouteError.tsx:16
|
||||
#: src/routes/login/index.tsx
|
||||
msgid "Sign up"
|
||||
msgstr "Sign up"
|
||||
|
||||
#: src/components/RouteError.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "Something went wrong"
|
||||
|
||||
#: src/components/NotFound/index.tsx:53
|
||||
#: src/components/NotFound/index.tsx
|
||||
msgid "Sorry, the page you visited does not exist."
|
||||
msgstr "Sorry, the page you visited does not exist."
|
||||
|
||||
#: src/routes/_auth/403/index.tsx:32
|
||||
#: src/routes/_auth/403/index.tsx
|
||||
msgid "Sorry, you don't have permission to access this page."
|
||||
msgstr "Sorry, you don't have permission to access this page."
|
||||
|
||||
#: src/routes/_auth/dashboard/index.tsx:139
|
||||
#: src/routes/_auth/dashboard/index.tsx
|
||||
msgid "Subscriptions"
|
||||
msgstr "Subscriptions"
|
||||
|
||||
#: src/components/Layout/Header/index.tsx:149
|
||||
#: src/routes/login/index.tsx:221
|
||||
#: src/components/Layout/AppFooter/index.tsx
|
||||
#: src/components/Layout/Header/index.tsx
|
||||
msgid "Switch language"
|
||||
msgstr "Switch language"
|
||||
|
||||
#: src/routes/_auth/users/index.tsx:126
|
||||
#: src/routes/_auth/users/index.tsx
|
||||
msgid "This action cannot be undone. This will permanently delete the user."
|
||||
msgstr "This action cannot be undone. This will permanently delete the user."
|
||||
|
||||
#: src/components/Layout/Header/index.tsx:134
|
||||
#: src/components/Layout/Sidebar/index.tsx:263
|
||||
#: src/components/Layout/Sidebar/index.tsx:311
|
||||
#: src/routes/_auth/dashboard/index.tsx
|
||||
msgid "Timeline"
|
||||
msgstr "Timeline"
|
||||
|
||||
#: src/routes/_auth/dashboard/index.tsx
|
||||
msgid "Today"
|
||||
msgstr "Today"
|
||||
|
||||
#: src/components/Layout/Header/index.tsx
|
||||
#: src/components/Layout/Sidebar/index.tsx
|
||||
msgid "Toggle sidebar"
|
||||
msgstr "Toggle sidebar"
|
||||
|
||||
#: src/components/Layout/Header/index.tsx:155
|
||||
#: src/routes/login/index.tsx:228
|
||||
#: src/components/Layout/AppFooter/index.tsx
|
||||
#: src/components/Layout/Header/index.tsx
|
||||
msgid "Toggle Theme"
|
||||
msgstr "Toggle Theme"
|
||||
|
||||
#: src/routes/_auth/dashboard/index.tsx:133
|
||||
#: src/routes/_auth/dashboard/index.tsx
|
||||
msgid "Token refresh behavior and 403 routes validated."
|
||||
msgstr "Token refresh behavior and 403 routes validated."
|
||||
|
||||
#: src/routes/_auth/dashboard/index.tsx
|
||||
msgid "Total Revenue"
|
||||
msgstr "Total Revenue"
|
||||
|
||||
#: src/hooks/useCrudToasts.ts:51
|
||||
#: src/hooks/useCrudToasts.ts
|
||||
msgid "Update failed"
|
||||
msgstr "Update failed"
|
||||
|
||||
#: src/hooks/useCrudToasts.ts:48
|
||||
#: src/hooks/useCrudToasts.ts
|
||||
msgid "Updated successfully"
|
||||
msgstr "Updated successfully"
|
||||
|
||||
#: src/hooks/useCrudToasts.ts:45
|
||||
#: src/hooks/useCrudToasts.ts
|
||||
msgid "Updating…"
|
||||
msgstr "Updating…"
|
||||
|
||||
#: src/routes/_auth/users/-FormModal.tsx:39
|
||||
#: src/routes/_auth/users/index.tsx:143
|
||||
#: src/routes/login/index.tsx:149
|
||||
#: src/routes/login/index.tsx:154
|
||||
#: src/routes/_auth/dashboard/index.tsx
|
||||
msgid "User creation spike handled and queue restored."
|
||||
msgstr "User creation spike handled and queue restored."
|
||||
|
||||
#: src/routes/_auth/users/-FormModal.tsx
|
||||
#: src/routes/_auth/users/index.tsx
|
||||
#: src/routes/login/index.tsx
|
||||
#: src/routes/register/index.tsx
|
||||
msgid "Username"
|
||||
msgstr "Username"
|
||||
|
||||
#: src/components/Layout/Header/index.tsx:77
|
||||
#: src/components/Layout/Sidebar/index.tsx:33
|
||||
#: src/components/Layout/Header/index.tsx
|
||||
#: src/components/Layout/Sidebar/index.tsx
|
||||
msgid "Users"
|
||||
msgstr "Users"
|
||||
|
||||
@@ -3,18 +3,25 @@ import type { Locale } from "@/stores/settings";
|
||||
|
||||
const catalogCache: Partial<Record<Locale, Messages>> = {};
|
||||
|
||||
const localeLoaders: Record<Locale, () => Promise<{ messages: Messages }>> = {
|
||||
en: () => import("./en/messages.po"),
|
||||
zh: () => import("./zh/messages.po"),
|
||||
};
|
||||
|
||||
function resolveLocale(locale: string): Locale {
|
||||
return locale in localeLoaders ? (locale as Locale) : "en";
|
||||
}
|
||||
|
||||
export async function loadLocaleCatalog(locale: Locale): Promise<Messages> {
|
||||
const hit = catalogCache[locale];
|
||||
const normalizedLocale = resolveLocale(locale);
|
||||
|
||||
const hit = catalogCache[normalizedLocale];
|
||||
if (hit) {
|
||||
return hit;
|
||||
}
|
||||
|
||||
const mod =
|
||||
locale === "en"
|
||||
? await import("./en/messages.po")
|
||||
: await import("./zh/messages.po");
|
||||
|
||||
const mod = await localeLoaders[normalizedLocale]();
|
||||
const messages = mod.messages;
|
||||
catalogCache[locale] = messages;
|
||||
catalogCache[normalizedLocale] = messages;
|
||||
return messages;
|
||||
}
|
||||
|
||||
@@ -13,299 +13,394 @@ msgstr ""
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/routes/_auth/users/index.tsx:263
|
||||
#: src/routes/_auth/users/index.tsx
|
||||
msgid "{total} rows"
|
||||
msgstr "共 {total} 条"
|
||||
|
||||
#: src/routes/_auth/dashboard/index.tsx:141
|
||||
#: src/routes/_auth/dashboard/index.tsx
|
||||
msgid "+180.1% from last month"
|
||||
msgstr "较上月 +180.1%"
|
||||
|
||||
#: src/routes/_auth/dashboard/index.tsx:147
|
||||
#: src/routes/_auth/dashboard/index.tsx
|
||||
msgid "+19% from last month"
|
||||
msgstr "较上月 +19%"
|
||||
|
||||
#: src/routes/_auth/dashboard/index.tsx:135
|
||||
#: src/routes/_auth/dashboard/index.tsx
|
||||
msgid "+20.1% from last month"
|
||||
msgstr "较上月 +20.1%"
|
||||
|
||||
#: src/routes/_auth/dashboard/index.tsx:153
|
||||
#: src/routes/_auth/dashboard/index.tsx
|
||||
msgid "+201 since last hour"
|
||||
msgstr "较上一小时 +201"
|
||||
|
||||
#: src/components/Layout/Header/index.tsx:79
|
||||
#: src/routes/_auth/dashboard/index.tsx
|
||||
msgid "08:30 · Deploy V3.2.0"
|
||||
msgstr "08:30 · 部署 V3.2.0"
|
||||
|
||||
#: src/routes/_auth/dashboard/index.tsx
|
||||
msgid "10:10 · Menu policy updated"
|
||||
msgstr "10:10 · 菜单策略已更新"
|
||||
|
||||
#: src/routes/_auth/dashboard/index.tsx
|
||||
msgid "13:20 · Security review"
|
||||
msgstr "13:20 · 安全评审"
|
||||
|
||||
#: src/routes/_auth/dashboard/index.tsx
|
||||
msgid "15:50 · Incident recovery"
|
||||
msgstr "15:50 · 事故恢复"
|
||||
|
||||
#: src/components/Layout/Header/index.tsx
|
||||
msgid "403"
|
||||
msgstr "403"
|
||||
|
||||
#: src/routes/_auth/dashboard/index.tsx:151
|
||||
#: src/routes/register/index.tsx
|
||||
msgid "Account created successfully"
|
||||
msgstr "账户创建成功"
|
||||
|
||||
#: src/routes/_auth/dashboard/index.tsx
|
||||
msgid "Active Now"
|
||||
msgstr "当前在线"
|
||||
|
||||
#: src/routes/_auth/users/-FormModal.tsx:52
|
||||
#: src/routes/_auth/users/-Toolbar.tsx:63
|
||||
#: src/routes/_auth/users/-FormModal.tsx
|
||||
#: src/routes/_auth/users/-Toolbar.tsx
|
||||
msgid "Admin"
|
||||
msgstr "管理员"
|
||||
|
||||
#: src/routes/_auth/users/index.tsx:125
|
||||
#: src/routes/_auth/dashboard/index.tsx
|
||||
msgid "Admin changed sidebar visibility and permission mapping."
|
||||
msgstr "管理员已调整侧栏可见性与权限映射。"
|
||||
|
||||
#: src/routes/register/index.tsx
|
||||
msgid "Already have an account?"
|
||||
msgstr "已有账户?"
|
||||
|
||||
#: src/routes/_auth/users/index.tsx
|
||||
msgid "Are you absolutely sure?"
|
||||
msgstr "确定要执行此操作吗?"
|
||||
|
||||
#: src/routes/login/index.tsx:181
|
||||
#: src/routes/login/index.tsx
|
||||
msgid "Auto login"
|
||||
msgstr "自动登录"
|
||||
|
||||
#: src/components/NotFound/index.tsx:57
|
||||
#: src/routes/_auth/403/index.tsx:35
|
||||
#: src/components/NotFound/index.tsx
|
||||
#: src/routes/_auth/403/index.tsx
|
||||
msgid "Back to Home"
|
||||
msgstr "返回首页"
|
||||
|
||||
#: src/routes/_auth/users/-FormModal.tsx:31
|
||||
#: src/routes/_auth/users/index.tsx:129
|
||||
#: src/routes/_auth/users/-FormModal.tsx
|
||||
#: src/routes/_auth/users/index.tsx
|
||||
msgid "Cancel"
|
||||
msgstr "取消"
|
||||
|
||||
#: src/routes/_auth/dashboard/index.tsx:247
|
||||
msgid "Chart Placeholder"
|
||||
msgstr "图表占位"
|
||||
#: src/routes/register/index.tsx
|
||||
msgid "Confirm password"
|
||||
msgstr "确认密码"
|
||||
|
||||
#: src/hooks/useCrudToasts.ts:40
|
||||
#: src/routes/register/index.tsx
|
||||
msgid "Create account"
|
||||
msgstr "创建账户"
|
||||
|
||||
#: src/hooks/useCrudToasts.ts
|
||||
msgid "Create failed"
|
||||
msgstr "创建失败"
|
||||
|
||||
#: src/routes/_auth/users/-Toolbar.tsx:88
|
||||
#: src/routes/_auth/users/-Toolbar.tsx
|
||||
msgid "Create User"
|
||||
msgstr "创建用户"
|
||||
|
||||
#: src/hooks/useCrudToasts.ts:37
|
||||
#: src/hooks/useCrudToasts.ts
|
||||
msgid "Created successfully"
|
||||
msgstr "创建成功"
|
||||
|
||||
#: src/components/Layout/Header/index.tsx:75
|
||||
#: src/components/Layout/Header/index.tsx:88
|
||||
#: src/components/Layout/Header/index.tsx:92
|
||||
#: src/components/Layout/Sidebar/index.tsx:32
|
||||
#: src/components/Layout/Header/index.tsx
|
||||
#: src/components/Layout/Sidebar/index.tsx
|
||||
msgid "Dashboard"
|
||||
msgstr "仪表盘"
|
||||
|
||||
#: src/routes/_auth/users/index.tsx:127
|
||||
#: src/routes/_auth/users/index.tsx:224
|
||||
#: src/routes/_auth/users/index.tsx
|
||||
msgid "Delete"
|
||||
msgstr "删除"
|
||||
|
||||
#: src/hooks/useCrudToasts.ts:62
|
||||
#: src/hooks/useCrudToasts.ts
|
||||
msgid "Delete failed"
|
||||
msgstr "删除失败"
|
||||
|
||||
#: src/hooks/useCrudToasts.ts:59
|
||||
#: src/hooks/useCrudToasts.ts
|
||||
msgid "Deleted successfully"
|
||||
msgstr "删除成功"
|
||||
|
||||
#: src/hooks/useCrudToasts.ts:56
|
||||
#: src/hooks/useCrudToasts.ts
|
||||
msgid "Deleting…"
|
||||
msgstr "正在删除…"
|
||||
|
||||
#: src/components/Layout/Sidebar/index.tsx:34
|
||||
#: src/components/Layout/Sidebar/index.tsx
|
||||
msgid "Design Engineering"
|
||||
msgstr "设计工程"
|
||||
|
||||
#: src/routes/_auth/users/index.tsx:214
|
||||
#: src/routes/login/index.tsx
|
||||
msgid "Don't have an account?"
|
||||
msgstr "没有账户?"
|
||||
|
||||
#: src/routes/_auth/users/index.tsx
|
||||
msgid "Edit"
|
||||
msgstr "编辑"
|
||||
|
||||
#: src/routes/_auth/users/-FormModal.tsx:29
|
||||
#: src/routes/_auth/users/-FormModal.tsx
|
||||
msgid "Edit User"
|
||||
msgstr "编辑用户"
|
||||
|
||||
#: src/routes/_auth/users/-FormModal.tsx:53
|
||||
#: src/routes/_auth/users/-Toolbar.tsx:64
|
||||
#: src/routes/_auth/users/-FormModal.tsx
|
||||
#: src/routes/_auth/users/-Toolbar.tsx
|
||||
msgid "Editor"
|
||||
msgstr "编辑者"
|
||||
|
||||
#: src/routes/_auth/users/-FormModal.tsx:57
|
||||
#: src/routes/_auth/users/index.tsx:169
|
||||
#: src/routes/_auth/users/-FormModal.tsx
|
||||
#: src/routes/_auth/users/index.tsx
|
||||
msgid "Email"
|
||||
msgstr "邮箱"
|
||||
|
||||
#: src/routes/login/index.tsx:188
|
||||
#: src/routes/register/index.tsx
|
||||
msgid "Email address"
|
||||
msgstr "邮箱地址"
|
||||
|
||||
#: src/routes/register/index.tsx
|
||||
msgid "Enter at least 6 characters"
|
||||
msgstr "请输入至少6个字符"
|
||||
|
||||
#: src/routes/login/index.tsx
|
||||
msgid "Forgot password?"
|
||||
msgstr "忘记密码?"
|
||||
|
||||
#: src/components/NotFound/index.tsx:60
|
||||
#: src/components/NotFound/index.tsx
|
||||
msgid "Go back"
|
||||
msgstr "返回上一页"
|
||||
|
||||
#: src/routes/login/index.tsx:54
|
||||
#: src/routes/login/index.tsx
|
||||
msgid "Login failed"
|
||||
msgstr "登录失败"
|
||||
|
||||
#: src/routes/login/index.tsx:50
|
||||
#: src/routes/login/index.tsx
|
||||
msgid "Login successful"
|
||||
msgstr "登录成功"
|
||||
|
||||
#: src/routes/_auth/users/-Toolbar.tsx:91
|
||||
#: src/routes/_auth/users/-Toolbar.tsx:92
|
||||
#: src/routes/_auth/users/-Toolbar.tsx
|
||||
msgid "More filters"
|
||||
msgstr "更多筛选"
|
||||
|
||||
#: src/routes/_auth/users/-FormModal.tsx:29
|
||||
#: src/routes/_auth/users/-FormModal.tsx
|
||||
msgid "New User"
|
||||
msgstr "新建用户"
|
||||
|
||||
#: src/components/DataTable/DataTableEmpty.tsx:46
|
||||
#: src/components/DataTable/DataTableEmpty.tsx
|
||||
msgid "No data"
|
||||
msgstr "暂无数据"
|
||||
|
||||
#: src/components/DataTable/DataTableEmpty.tsx:56
|
||||
#: src/components/DataTable/DataTableEmpty.tsx
|
||||
msgid "Nothing to show in this list yet"
|
||||
msgstr "当前列表为空,暂无可展示的记录"
|
||||
|
||||
#: src/routes/_auth/users/-FormModal.tsx:30
|
||||
#: src/routes/_auth/users/-FormModal.tsx
|
||||
msgid "OK"
|
||||
msgstr "确定"
|
||||
|
||||
#: src/components/Layout/Sidebar/index.tsx:350
|
||||
#: src/components/Layout/Sidebar/index.tsx
|
||||
msgid "Open account menu"
|
||||
msgstr "打开账户菜单"
|
||||
|
||||
#: src/routes/_auth/dashboard/index.tsx:230
|
||||
msgid "Overview"
|
||||
msgstr "概览"
|
||||
|
||||
#: src/routes/login/index.tsx:162
|
||||
#: src/routes/login/index.tsx:168
|
||||
#: src/routes/login/index.tsx
|
||||
#: src/routes/register/index.tsx
|
||||
msgid "Password"
|
||||
msgstr "密码"
|
||||
|
||||
#: src/components/Layout/Sidebar/index.tsx:30
|
||||
#: src/routes/register/index.tsx
|
||||
msgid "Password must be at least 6 characters long"
|
||||
msgstr "密码长度至少为6位"
|
||||
|
||||
#: src/routes/register/index.tsx
|
||||
msgid "Passwords do not match"
|
||||
msgstr "两次输入的密码不一致"
|
||||
|
||||
#: src/components/Layout/Sidebar/index.tsx
|
||||
msgid "Platform"
|
||||
msgstr "平台"
|
||||
|
||||
#: src/routes/login/index.tsx:163
|
||||
#: src/routes/register/index.tsx
|
||||
msgid "Please confirm your password"
|
||||
msgstr "请确认您的密码"
|
||||
|
||||
#: src/routes/register/index.tsx
|
||||
msgid "Please enter a valid email address"
|
||||
msgstr "请输入有效的邮箱地址"
|
||||
|
||||
#: src/routes/login/index.tsx
|
||||
msgid "Please enter password"
|
||||
msgstr "请输入密码"
|
||||
|
||||
#: src/routes/_auth/users/-FormModal.tsx:40
|
||||
#: src/routes/login/index.tsx:150
|
||||
#: src/routes/_auth/users/-FormModal.tsx
|
||||
#: src/routes/login/index.tsx
|
||||
msgid "Please enter username"
|
||||
msgstr "请输入用户名"
|
||||
|
||||
#: src/routes/_auth/users/-FormModal.tsx:47
|
||||
#: src/routes/register/index.tsx
|
||||
msgid "Please enter your password"
|
||||
msgstr "请输入您的密码"
|
||||
|
||||
#: src/routes/register/index.tsx
|
||||
msgid "Please enter your username"
|
||||
msgstr "请输入您的用户名"
|
||||
|
||||
#: src/routes/_auth/users/-FormModal.tsx
|
||||
msgid "Please select roles"
|
||||
msgstr "请选择角色"
|
||||
|
||||
#: src/components/RouteError.tsx:17
|
||||
#: src/components/RouteError.tsx
|
||||
msgid "Please try again or return to the dashboard."
|
||||
msgstr "请重试或返回控制台。"
|
||||
|
||||
#: src/components/Layout/AppFooter/index.tsx:23
|
||||
#: src/components/Layout/AppFooter/index.tsx
|
||||
msgid "Powered by"
|
||||
msgstr "由"
|
||||
|
||||
#: src/components/Layout/Sidebar/index.tsx:31
|
||||
#: src/components/Layout/Sidebar/index.tsx
|
||||
msgid "Projects"
|
||||
msgstr "项目"
|
||||
|
||||
#: src/routes/_auth/dashboard/index.tsx:256
|
||||
#: src/routes/register/index.tsx
|
||||
msgid "Re-enter your password"
|
||||
msgstr "请再次输入您的密码"
|
||||
|
||||
#: src/routes/_auth/dashboard/index.tsx
|
||||
msgid "Recent Sales"
|
||||
msgstr "近期销售"
|
||||
|
||||
#: src/components/RouteError.tsx:20
|
||||
#: src/routes/register/index.tsx
|
||||
msgid "Registration failed"
|
||||
msgstr "注册失败"
|
||||
|
||||
#: src/routes/_auth/dashboard/index.tsx
|
||||
msgid "Release branch merged and production rollout completed."
|
||||
msgstr "发布分支已合并,生产发布已完成。"
|
||||
|
||||
#: src/components/RouteError.tsx
|
||||
msgid "Retry"
|
||||
msgstr "重试"
|
||||
|
||||
#: src/routes/_auth/users/-Toolbar.tsx:57
|
||||
#: src/routes/_auth/users/-Toolbar.tsx
|
||||
msgid "Role"
|
||||
msgstr "角色"
|
||||
|
||||
#: src/routes/_auth/users/-FormModal.tsx:46
|
||||
#: src/routes/_auth/users/index.tsx:176
|
||||
#: src/routes/_auth/users/-FormModal.tsx
|
||||
#: src/routes/_auth/users/index.tsx
|
||||
msgid "Roles"
|
||||
msgstr "角色"
|
||||
|
||||
#: src/routes/_auth/users/index.tsx:235
|
||||
#: src/routes/_auth/users/index.tsx
|
||||
msgid "Row actions"
|
||||
msgstr "行操作"
|
||||
|
||||
#: src/routes/_auth/dashboard/index.tsx:145
|
||||
#: src/routes/_auth/dashboard/index.tsx
|
||||
msgid "Sales"
|
||||
msgstr "销售额"
|
||||
|
||||
#: src/components/Layout/Sidebar/index.tsx:35
|
||||
#: src/components/Layout/Sidebar/index.tsx
|
||||
msgid "Sales & Marketing"
|
||||
msgstr "销售与市场"
|
||||
|
||||
#: src/routes/_auth/users/-Toolbar.tsx:42
|
||||
#: src/routes/_auth/users/-Toolbar.tsx
|
||||
msgid "Search User"
|
||||
msgstr "搜索用户"
|
||||
|
||||
#: src/routes/login/index.tsx:200
|
||||
#: src/routes/register/index.tsx
|
||||
msgid "Sign in"
|
||||
msgstr "登录"
|
||||
|
||||
#: src/routes/login/index.tsx
|
||||
msgid "Sign In"
|
||||
msgstr "登录"
|
||||
|
||||
#: src/components/Layout/Sidebar/index.tsx:191
|
||||
#: src/components/Layout/Sidebar/index.tsx
|
||||
msgid "Sign Out"
|
||||
msgstr "退出登录"
|
||||
|
||||
#: src/components/RouteError.tsx:16
|
||||
#: src/routes/login/index.tsx
|
||||
msgid "Sign up"
|
||||
msgstr "注册"
|
||||
|
||||
#: src/components/RouteError.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "出了点问题"
|
||||
|
||||
#: src/components/NotFound/index.tsx:53
|
||||
#: src/components/NotFound/index.tsx
|
||||
msgid "Sorry, the page you visited does not exist."
|
||||
msgstr "抱歉,您访问的页面不存在。"
|
||||
|
||||
#: src/routes/_auth/403/index.tsx:32
|
||||
#: src/routes/_auth/403/index.tsx
|
||||
msgid "Sorry, you don't have permission to access this page."
|
||||
msgstr "抱歉,您没有权限访问此页面。"
|
||||
|
||||
#: src/routes/_auth/dashboard/index.tsx:139
|
||||
#: src/routes/_auth/dashboard/index.tsx
|
||||
msgid "Subscriptions"
|
||||
msgstr "订阅"
|
||||
|
||||
#: src/components/Layout/Header/index.tsx:149
|
||||
#: src/routes/login/index.tsx:221
|
||||
#: src/components/Layout/AppFooter/index.tsx
|
||||
#: src/components/Layout/Header/index.tsx
|
||||
msgid "Switch language"
|
||||
msgstr "切换语言"
|
||||
|
||||
#: src/routes/_auth/users/index.tsx:126
|
||||
#: src/routes/_auth/users/index.tsx
|
||||
msgid "This action cannot be undone. This will permanently delete the user."
|
||||
msgstr "此操作无法撤销,将永久删除该用户。"
|
||||
|
||||
#: src/components/Layout/Header/index.tsx:134
|
||||
#: src/components/Layout/Sidebar/index.tsx:263
|
||||
#: src/components/Layout/Sidebar/index.tsx:311
|
||||
#: src/routes/_auth/dashboard/index.tsx
|
||||
msgid "Timeline"
|
||||
msgstr "时间线"
|
||||
|
||||
#: src/routes/_auth/dashboard/index.tsx
|
||||
msgid "Today"
|
||||
msgstr "今天"
|
||||
|
||||
#: src/components/Layout/Header/index.tsx
|
||||
#: src/components/Layout/Sidebar/index.tsx
|
||||
msgid "Toggle sidebar"
|
||||
msgstr "折叠侧栏"
|
||||
|
||||
#: src/components/Layout/Header/index.tsx:155
|
||||
#: src/routes/login/index.tsx:228
|
||||
#: src/components/Layout/AppFooter/index.tsx
|
||||
#: src/components/Layout/Header/index.tsx
|
||||
msgid "Toggle Theme"
|
||||
msgstr "切换主题"
|
||||
|
||||
#: src/routes/_auth/dashboard/index.tsx:133
|
||||
#: src/routes/_auth/dashboard/index.tsx
|
||||
msgid "Token refresh behavior and 403 routes validated."
|
||||
msgstr "已验证令牌刷新行为与 403 路由。"
|
||||
|
||||
#: src/routes/_auth/dashboard/index.tsx
|
||||
msgid "Total Revenue"
|
||||
msgstr "总收入"
|
||||
|
||||
#: src/hooks/useCrudToasts.ts:51
|
||||
#: src/hooks/useCrudToasts.ts
|
||||
msgid "Update failed"
|
||||
msgstr "更新失败"
|
||||
|
||||
#: src/hooks/useCrudToasts.ts:48
|
||||
#: src/hooks/useCrudToasts.ts
|
||||
msgid "Updated successfully"
|
||||
msgstr "更新成功"
|
||||
|
||||
#: src/hooks/useCrudToasts.ts:45
|
||||
#: src/hooks/useCrudToasts.ts
|
||||
msgid "Updating…"
|
||||
msgstr "正在更新…"
|
||||
|
||||
#: src/routes/_auth/users/-FormModal.tsx:39
|
||||
#: src/routes/_auth/users/index.tsx:143
|
||||
#: src/routes/login/index.tsx:149
|
||||
#: src/routes/login/index.tsx:154
|
||||
#: src/routes/_auth/dashboard/index.tsx
|
||||
msgid "User creation spike handled and queue restored."
|
||||
msgstr "用户创建高峰已处理,队列已恢复。"
|
||||
|
||||
#: src/routes/_auth/users/-FormModal.tsx
|
||||
#: src/routes/_auth/users/index.tsx
|
||||
#: src/routes/login/index.tsx
|
||||
#: src/routes/register/index.tsx
|
||||
msgid "Username"
|
||||
msgstr "用户名"
|
||||
|
||||
#: src/components/Layout/Header/index.tsx:77
|
||||
#: src/components/Layout/Sidebar/index.tsx:33
|
||||
#: src/components/Layout/Header/index.tsx
|
||||
#: src/components/Layout/Sidebar/index.tsx
|
||||
msgid "Users"
|
||||
msgstr "用户"
|
||||
|
||||
@@ -13,6 +13,15 @@ const MOCK_IDENTITIES: ReadonlyArray<[string, string]> = [
|
||||
["ava.nguyen", "ava.nguyen@northstar.io"],
|
||||
["noah.berg", "noah.berg@northstar.io"],
|
||||
["mia.silva", "mia.silva@northstar.io"],
|
||||
["lucas.rossi", "lucas.rossi@northstar.io"],
|
||||
["isabella.ali", "isabella.ali@northstar.io"],
|
||||
["ethan.kovacs", "ethan.kovacs@northstar.io"],
|
||||
["sophia.dubois", "sophia.dubois@northstar.io"],
|
||||
["mateo.santos", "mateo.santos@northstar.io"],
|
||||
["harper.ivanov", "harper.ivanov@northstar.io"],
|
||||
["elijah.mohamed", "elijah.mohamed@northstar.io"],
|
||||
["amelia.schmidt", "amelia.schmidt@northstar.io"],
|
||||
["henry.okafor", "henry.okafor@northstar.io"],
|
||||
];
|
||||
|
||||
export const GUEST_AUTH_USER_BODY = {
|
||||
|
||||
@@ -13,14 +13,24 @@ import {
|
||||
LoginRequestSchema,
|
||||
PermissionsListSchema,
|
||||
RefreshTokenRequestSchema,
|
||||
RegisterRequestSchema,
|
||||
} from "@/api/schemas";
|
||||
|
||||
const GUEST_REFRESH = "mock-guest-refresh";
|
||||
const REGISTERED_ACCESS = "mock-registered-access";
|
||||
const REGISTERED_REFRESH = "mock-registered-refresh";
|
||||
|
||||
/** Last successful register (in-memory) so /user matches the new account in MSW */
|
||||
let mockRegisteredSession: { username: string; email: string | null } | null = null;
|
||||
|
||||
function isGuestAuth(authorization: string | null): boolean {
|
||||
return (authorization ?? "").includes("mock-guest-access");
|
||||
}
|
||||
|
||||
function isRegisteredAuth(authorization: string | null): boolean {
|
||||
return (authorization ?? "").includes(REGISTERED_ACCESS);
|
||||
}
|
||||
|
||||
export const authHandlers = [
|
||||
http.post("/api/auth/login", async ({ request }) => {
|
||||
await withDelay(300);
|
||||
@@ -50,6 +60,36 @@ export const authHandlers = [
|
||||
return errorResponse(ERROR_CODES.INVALID_CREDENTIALS, "Invalid username or password");
|
||||
}),
|
||||
|
||||
http.post("/api/auth/register", async ({ request }) => {
|
||||
await withDelay(300);
|
||||
let json: unknown;
|
||||
try {
|
||||
json = await request.json();
|
||||
} catch {
|
||||
return errorResponse(ERROR_CODES.BAD_REQUEST, "Invalid JSON body");
|
||||
}
|
||||
const parsed = RegisterRequestSchema.safeParse(json);
|
||||
if (!parsed.success) {
|
||||
return errorResponse(ERROR_CODES.BAD_REQUEST, "Invalid register body");
|
||||
}
|
||||
const { username, password, email } = parsed.data;
|
||||
if (password.length < 6) {
|
||||
return errorResponse(ERROR_CODES.BAD_REQUEST, "Password must be at least 6 characters");
|
||||
}
|
||||
const taken = username === "guest" || MOCK_USERS.some((u) => u.username === username);
|
||||
if (taken) {
|
||||
return errorResponse(ERROR_CODES.BAD_REQUEST, "Username already taken");
|
||||
}
|
||||
mockRegisteredSession = {
|
||||
username,
|
||||
email: email ?? null,
|
||||
};
|
||||
return successWithSchema(AuthTokensSchema, {
|
||||
accessToken: REGISTERED_ACCESS,
|
||||
refreshToken: REGISTERED_REFRESH,
|
||||
});
|
||||
}),
|
||||
|
||||
http.post("/api/auth/refresh", async ({ request }) => {
|
||||
await withDelay(100);
|
||||
let json: unknown;
|
||||
@@ -68,6 +108,15 @@ export const authHandlers = [
|
||||
refreshToken: "mock-guest-refresh-refreshed",
|
||||
});
|
||||
}
|
||||
if (
|
||||
parsed.data.refreshToken === REGISTERED_REFRESH ||
|
||||
parsed.data.refreshToken.startsWith(`${REGISTERED_REFRESH}-`)
|
||||
) {
|
||||
return successWithSchema(AuthTokensSchema, {
|
||||
accessToken: `${REGISTERED_ACCESS}-refreshed`,
|
||||
refreshToken: `${REGISTERED_REFRESH}-refreshed`,
|
||||
});
|
||||
}
|
||||
return successWithSchema(AuthTokensSchema, {
|
||||
accessToken: "mock-new-access-token",
|
||||
refreshToken: "mock-new-refresh-token",
|
||||
@@ -78,6 +127,18 @@ export const authHandlers = [
|
||||
|
||||
http.get("/api/auth/user", ({ request }) => {
|
||||
const auth = request.headers.get("authorization");
|
||||
if (isRegisteredAuth(auth)) {
|
||||
const session = mockRegisteredSession;
|
||||
if (session) {
|
||||
return successWithSchema(AuthUserResponseSchema, {
|
||||
id: "reg-mock",
|
||||
username: session.username,
|
||||
avatar: null,
|
||||
email: session.email,
|
||||
roles: ["editor"],
|
||||
});
|
||||
}
|
||||
}
|
||||
if (isGuestAuth(auth)) {
|
||||
return successWithSchema(AuthUserResponseSchema, GUEST_AUTH_USER_BODY);
|
||||
}
|
||||
@@ -87,6 +148,9 @@ export const authHandlers = [
|
||||
|
||||
http.get("/api/auth/permissions", ({ request }) => {
|
||||
const auth = request.headers.get("authorization");
|
||||
if (isRegisteredAuth(auth) && mockRegisteredSession) {
|
||||
return successWithSchema(PermissionsListSchema, ["user:view"] as string[]);
|
||||
}
|
||||
if (isGuestAuth(auth)) {
|
||||
return successWithSchema(PermissionsListSchema, [] as string[]);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
import { Route as rootRouteImport } from "./routes/__root";
|
||||
import { Route as AuthRouteImport } from "./routes/_auth";
|
||||
import { Route as IndexRouteImport } from "./routes/index";
|
||||
import { Route as RegisterIndexRouteImport } from "./routes/register/index";
|
||||
import { Route as LoginIndexRouteImport } from "./routes/login/index";
|
||||
import { Route as R404IndexRouteImport } from "./routes/404/index";
|
||||
import { Route as AuthUsersIndexRouteImport } from "./routes/_auth/users/index";
|
||||
@@ -26,6 +27,11 @@ const IndexRoute = IndexRouteImport.update({
|
||||
path: "/",
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any);
|
||||
const RegisterIndexRoute = RegisterIndexRouteImport.update({
|
||||
id: "/register/",
|
||||
path: "/register/",
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any);
|
||||
const LoginIndexRoute = LoginIndexRouteImport.update({
|
||||
id: "/login/",
|
||||
path: "/login/",
|
||||
@@ -56,6 +62,7 @@ export interface FileRoutesByFullPath {
|
||||
"/": typeof IndexRoute;
|
||||
"/404/": typeof R404IndexRoute;
|
||||
"/login/": typeof LoginIndexRoute;
|
||||
"/register/": typeof RegisterIndexRoute;
|
||||
"/403/": typeof Auth403IndexRoute;
|
||||
"/dashboard/": typeof AuthDashboardIndexRoute;
|
||||
"/users/": typeof AuthUsersIndexRoute;
|
||||
@@ -64,6 +71,7 @@ export interface FileRoutesByTo {
|
||||
"/": typeof IndexRoute;
|
||||
"/404": typeof R404IndexRoute;
|
||||
"/login": typeof LoginIndexRoute;
|
||||
"/register": typeof RegisterIndexRoute;
|
||||
"/403": typeof Auth403IndexRoute;
|
||||
"/dashboard": typeof AuthDashboardIndexRoute;
|
||||
"/users": typeof AuthUsersIndexRoute;
|
||||
@@ -74,21 +82,23 @@ export interface FileRoutesById {
|
||||
"/_auth": typeof AuthRouteWithChildren;
|
||||
"/404/": typeof R404IndexRoute;
|
||||
"/login/": typeof LoginIndexRoute;
|
||||
"/register/": typeof RegisterIndexRoute;
|
||||
"/_auth/403/": typeof Auth403IndexRoute;
|
||||
"/_auth/dashboard/": typeof AuthDashboardIndexRoute;
|
||||
"/_auth/users/": typeof AuthUsersIndexRoute;
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath;
|
||||
fullPaths: "/" | "/404/" | "/login/" | "/403/" | "/dashboard/" | "/users/";
|
||||
fullPaths: "/" | "/404/" | "/login/" | "/register/" | "/403/" | "/dashboard/" | "/users/";
|
||||
fileRoutesByTo: FileRoutesByTo;
|
||||
to: "/" | "/404" | "/login" | "/403" | "/dashboard" | "/users";
|
||||
to: "/" | "/404" | "/login" | "/register" | "/403" | "/dashboard" | "/users";
|
||||
id:
|
||||
| "__root__"
|
||||
| "/"
|
||||
| "/_auth"
|
||||
| "/404/"
|
||||
| "/login/"
|
||||
| "/register/"
|
||||
| "/_auth/403/"
|
||||
| "/_auth/dashboard/"
|
||||
| "/_auth/users/";
|
||||
@@ -99,6 +109,7 @@ export interface RootRouteChildren {
|
||||
AuthRoute: typeof AuthRouteWithChildren;
|
||||
R404IndexRoute: typeof R404IndexRoute;
|
||||
LoginIndexRoute: typeof LoginIndexRoute;
|
||||
RegisterIndexRoute: typeof RegisterIndexRoute;
|
||||
}
|
||||
|
||||
declare module "@tanstack/react-router" {
|
||||
@@ -117,6 +128,13 @@ declare module "@tanstack/react-router" {
|
||||
preLoaderRoute: typeof IndexRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
"/register/": {
|
||||
id: "/register/";
|
||||
path: "/register";
|
||||
fullPath: "/register/";
|
||||
preLoaderRoute: typeof RegisterIndexRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
"/login/": {
|
||||
id: "/login/";
|
||||
path: "/login";
|
||||
@@ -174,6 +192,7 @@ const rootRouteChildren: RootRouteChildren = {
|
||||
AuthRoute: AuthRouteWithChildren,
|
||||
R404IndexRoute: R404IndexRoute,
|
||||
LoginIndexRoute: LoginIndexRoute,
|
||||
RegisterIndexRoute: RegisterIndexRoute,
|
||||
};
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
|
||||
@@ -20,12 +20,12 @@
|
||||
background-color: var(--dash-recent-hover-bg);
|
||||
}
|
||||
|
||||
.dash-chart-placeholder {
|
||||
transition: border-color 0.2s ease;
|
||||
.dash-timeline {
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.dash-chart-placeholder:hover {
|
||||
border-color: var(--dash-chart-hover-border);
|
||||
.dash-timeline .ant-timeline-item-content {
|
||||
min-height: 52px;
|
||||
}
|
||||
|
||||
.dash-skel-stat .ant-skeleton-input {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { CSSProperties } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { Card, Col, Row, Typography, Avatar, theme, Flex, Skeleton } from "antd";
|
||||
import { Card, Col, Row, Typography, Avatar, theme, Flex, Skeleton, Timeline, Tag } from "antd";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useLingui } from "@lingui/react/macro";
|
||||
import { DollarSign, Users, CreditCard, Activity } from "lucide-react";
|
||||
@@ -193,6 +193,52 @@ function DashboardPage() {
|
||||
[],
|
||||
);
|
||||
|
||||
const timelineItems = useMemo(
|
||||
() => [
|
||||
{
|
||||
color: "green",
|
||||
content: (
|
||||
<Flex vertical gap={4}>
|
||||
<Text strong>{t`08:30 · Deploy V3.2.0`}</Text>
|
||||
<Text type="secondary">
|
||||
{t`Release branch merged and production rollout completed.`}
|
||||
</Text>
|
||||
</Flex>
|
||||
),
|
||||
},
|
||||
{
|
||||
color: "blue",
|
||||
content: (
|
||||
<Flex vertical gap={4}>
|
||||
<Text strong>{t`10:10 · Menu policy updated`}</Text>
|
||||
<Text type="secondary">
|
||||
{t`Admin changed sidebar visibility and permission mapping.`}
|
||||
</Text>
|
||||
</Flex>
|
||||
),
|
||||
},
|
||||
{
|
||||
color: "gold",
|
||||
content: (
|
||||
<Flex vertical gap={4}>
|
||||
<Text strong>{t`13:20 · Security review`}</Text>
|
||||
<Text type="secondary">{t`Token refresh behavior and 403 routes validated.`}</Text>
|
||||
</Flex>
|
||||
),
|
||||
},
|
||||
{
|
||||
color: "red",
|
||||
content: (
|
||||
<Flex vertical gap={4}>
|
||||
<Text strong>{t`15:50 · Incident recovery`}</Text>
|
||||
<Text type="secondary">{t`User creation spike handled and queue restored.`}</Text>
|
||||
</Flex>
|
||||
),
|
||||
},
|
||||
],
|
||||
[t],
|
||||
);
|
||||
|
||||
if (isPending) {
|
||||
return <DashboardSkeleton />;
|
||||
}
|
||||
@@ -227,26 +273,18 @@ function DashboardPage() {
|
||||
<Card
|
||||
className="dash-card-interactive"
|
||||
style={{ ...cardHoverStyle, height: "100%" }}
|
||||
title={<Title level={5} style={{ margin: 0 }}>{t`Overview`}</Title>}
|
||||
title={
|
||||
<Flex align="center" gap={token.marginSM}>
|
||||
<Title level={5} style={{ margin: 0 }}>
|
||||
{t`Timeline`}
|
||||
</Title>
|
||||
<Tag variant="filled" color="processing">
|
||||
{t`Today`}
|
||||
</Tag>
|
||||
</Flex>
|
||||
}
|
||||
>
|
||||
<Flex
|
||||
className="dash-chart-placeholder"
|
||||
style={{
|
||||
height: 300,
|
||||
width: "100%",
|
||||
backgroundColor: token.colorBgLayout,
|
||||
borderRadius: token.borderRadius,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
border: `1px dashed ${token.colorBorder}`,
|
||||
["--dash-chart-hover-bg" as string]: token.colorBgLayout,
|
||||
["--dash-chart-hover-border" as string]: token.colorTextTertiary,
|
||||
}}
|
||||
>
|
||||
<Text type="secondary" style={{ fontSize: token.fontSizeSM }}>
|
||||
{t`Chart Placeholder`}
|
||||
</Text>
|
||||
</Flex>
|
||||
<Timeline className="dash-timeline" items={timelineItems} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} lg={10}>
|
||||
|
||||
@@ -5,3 +5,17 @@
|
||||
-webkit-backdrop-filter: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Keep browser autofill from painting a solid background over the glass card. */
|
||||
.login-page__card .ant-input:-webkit-autofill,
|
||||
.login-page__card .ant-input:-webkit-autofill:hover,
|
||||
.login-page__card .ant-input:-webkit-autofill:focus,
|
||||
.login-page__card .ant-input-affix-wrapper input:-webkit-autofill,
|
||||
.login-page__card .ant-input-affix-wrapper input:-webkit-autofill:hover,
|
||||
.login-page__card .ant-input-affix-wrapper input:-webkit-autofill:focus {
|
||||
-webkit-text-fill-color: var(--ant-color-text, currentColor) !important;
|
||||
caret-color: var(--ant-color-text, currentColor);
|
||||
-webkit-box-shadow: 0 0 0 1000px transparent inset !important;
|
||||
box-shadow: 0 0 0 1000px transparent inset !important;
|
||||
transition: background-color 9999s ease-out 0s;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createFileRoute, redirect, useNavigate } from "@tanstack/react-router";
|
||||
import { Form, Input, Button, Card, App, theme, Typography, Flex, Checkbox, Space } from "antd";
|
||||
import { createFileRoute, Link, redirect, useNavigate } from "@tanstack/react-router";
|
||||
import { Form, Input, Button, Card, App, theme, Typography, Flex, Checkbox } from "antd";
|
||||
import type { CSSProperties } from "react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useLingui } from "@lingui/react/macro";
|
||||
@@ -11,8 +11,6 @@ import { LoginRequestSchema, AuthTokensSchema } from "@/api/schemas";
|
||||
import { fetchSessionAndApplyToStore } from "@/utils/session";
|
||||
import type { LoginRequest } from "@/api/schemas";
|
||||
import { APP_BRAND_NAME, APP_FAVICON_SRC } from "@/utils/constants";
|
||||
import { Languages } from "lucide-react";
|
||||
import { Theme } from "@/components/Icon";
|
||||
import { AppFooter } from "@/components/Layout/AppFooter";
|
||||
import { Aurora } from "@/components/Aurora";
|
||||
import "./index.css";
|
||||
@@ -32,9 +30,6 @@ function LoginPage() {
|
||||
const { message } = App.useApp();
|
||||
const { t } = useLingui();
|
||||
const setTokens = useAuthStore((s) => s.setTokens);
|
||||
const locale = useSettingsStore((s) => s.locale);
|
||||
const setLocale = useSettingsStore((s) => s.setLocale);
|
||||
const toggleDarkMode = useSettingsStore((s) => s.toggleDarkMode);
|
||||
const darkMode = useSettingsStore((s) => s.darkMode);
|
||||
const { token } = theme.useToken();
|
||||
|
||||
@@ -55,10 +50,6 @@ function LoginPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const toggleLocale = () => {
|
||||
setLocale(locale === "en" ? "zh" : "en");
|
||||
};
|
||||
|
||||
const shellStyle: CSSProperties = {
|
||||
position: "relative",
|
||||
isolation: "isolate",
|
||||
@@ -146,7 +137,7 @@ function LoginPage() {
|
||||
>
|
||||
<Form.Item
|
||||
name="username"
|
||||
label={<span style={{ fontWeight: 500 }}>{t`Username`}</span>}
|
||||
label={<span>{t`Username`}</span>}
|
||||
rules={[{ required: true, message: t`Please enter username` }]}
|
||||
>
|
||||
<Input
|
||||
@@ -159,7 +150,7 @@ function LoginPage() {
|
||||
|
||||
<Form.Item
|
||||
name="password"
|
||||
label={<span style={{ fontWeight: 500 }}>{t`Password`}</span>}
|
||||
label={<span>{t`Password`}</span>}
|
||||
rules={[{ required: true, message: t`Please enter password` }]}
|
||||
style={{ marginBottom: token.marginLG }}
|
||||
>
|
||||
@@ -201,6 +192,14 @@ function LoginPage() {
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<Flex justify="center" style={{ marginTop: token.margin }}>
|
||||
<Typography.Text type="secondary">
|
||||
{t`Don't have an account?`}{" "}
|
||||
<Link to="/register" style={{ color: token.colorPrimary }}>
|
||||
{t`Sign up`}
|
||||
</Link>
|
||||
</Typography.Text>
|
||||
</Flex>
|
||||
</Card>
|
||||
</Flex>
|
||||
<Flex
|
||||
@@ -211,24 +210,6 @@ function LoginPage() {
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<Flex justify="center" style={{ marginBottom: token.marginSM }}>
|
||||
<Space>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
onClick={toggleLocale}
|
||||
icon={<Languages size={token.size} />}
|
||||
aria-label={t`Switch language`}
|
||||
/>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
onClick={toggleDarkMode}
|
||||
icon={<Theme size={token.size} />}
|
||||
aria-label={t`Toggle Theme`}
|
||||
/>
|
||||
</Space>
|
||||
</Flex>
|
||||
<AppFooter />
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
217
apps/with-lingui/src/routes/register/index.tsx
Normal file
217
apps/with-lingui/src/routes/register/index.tsx
Normal file
@@ -0,0 +1,217 @@
|
||||
import { Link, createFileRoute, redirect, useNavigate } from "@tanstack/react-router";
|
||||
import { Form, Input, Button, Card, App, theme, Typography, Flex } from "antd";
|
||||
import type { CSSProperties } from "react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useLingui } from "@lingui/react/macro";
|
||||
import { httpClient } from "@/utils/http";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { AUTH_ENDPOINTS } from "@/api/auth";
|
||||
import { AuthTokensSchema, RegisterRequestSchema } from "@/api/schemas";
|
||||
import { fetchSessionAndApplyToStore } from "@/utils/session";
|
||||
import type { RegisterRequest } from "@/api/auth";
|
||||
import { APP_BRAND_NAME, APP_FAVICON_SRC } from "@/utils/constants";
|
||||
import { Aurora } from "@/components/Aurora";
|
||||
import { AppFooter } from "@/components/Layout/AppFooter";
|
||||
import "../login/index.css";
|
||||
|
||||
export const Route = createFileRoute("/register/")({
|
||||
beforeLoad: () => {
|
||||
const { isAuthenticated } = useAuthStore.getState();
|
||||
if (isAuthenticated) {
|
||||
throw redirect({ to: "/dashboard" });
|
||||
}
|
||||
},
|
||||
component: RegisterPage,
|
||||
});
|
||||
|
||||
function RegisterPage() {
|
||||
const navigate = useNavigate();
|
||||
const { message } = App.useApp();
|
||||
const { t } = useLingui();
|
||||
const setTokens = useAuthStore((s) => s.setTokens);
|
||||
const { token } = theme.useToken();
|
||||
|
||||
const registerMutation = useMutation({
|
||||
mutationFn: async (values: RegisterRequest) => {
|
||||
const parsed = RegisterRequestSchema.parse(values);
|
||||
const tokens = await httpClient.post(AUTH_ENDPOINTS.register, parsed);
|
||||
const validTokens = AuthTokensSchema.parse(tokens);
|
||||
setTokens(validTokens);
|
||||
await fetchSessionAndApplyToStore();
|
||||
},
|
||||
onSuccess: () => {
|
||||
message.success(t`Account created successfully`);
|
||||
void navigate({ to: "/dashboard" });
|
||||
},
|
||||
onError: (err) => {
|
||||
message.error(err instanceof Error ? err.message : t`Registration failed`);
|
||||
},
|
||||
});
|
||||
|
||||
const shellStyle: CSSProperties = {
|
||||
position: "relative",
|
||||
isolation: "isolate",
|
||||
minHeight: "100vh",
|
||||
backgroundColor: token.colorBgLayout,
|
||||
color: token.colorText,
|
||||
};
|
||||
|
||||
const contentStyle: CSSProperties = {
|
||||
position: "relative",
|
||||
zIndex: 1,
|
||||
flex: "1 1 0%",
|
||||
minWidth: 0,
|
||||
minHeight: 0,
|
||||
overflow: "hidden",
|
||||
};
|
||||
|
||||
const cardStyle: CSSProperties = {
|
||||
width: "100%",
|
||||
maxWidth: 384,
|
||||
background: `color-mix(in srgb, ${token.colorBgContainer} 44%, transparent)`,
|
||||
backdropFilter: "blur(18px) saturate(1.35)",
|
||||
WebkitBackdropFilter: "blur(18px) saturate(1.35)",
|
||||
borderColor: token.colorBorderSecondary,
|
||||
boxShadow: token.boxShadow,
|
||||
["--login-card-fallback-bg" as string]: token.colorBgElevated,
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex vertical style={shellStyle}>
|
||||
<Aurora />
|
||||
<Flex vertical style={contentStyle}>
|
||||
<Flex
|
||||
flex={1}
|
||||
align="center"
|
||||
justify="center"
|
||||
style={{ padding: token.padding, minHeight: 0 }}
|
||||
>
|
||||
<Card
|
||||
className="login-page__card"
|
||||
style={cardStyle}
|
||||
styles={{
|
||||
body: { padding: token.paddingLG, background: "transparent" },
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
align="center"
|
||||
justify="center"
|
||||
gap={token.margin}
|
||||
wrap="wrap"
|
||||
style={{ marginBottom: token.marginLG }}
|
||||
>
|
||||
<img
|
||||
src={APP_FAVICON_SRC}
|
||||
alt=""
|
||||
width={32}
|
||||
height={32}
|
||||
draggable={false}
|
||||
style={{ display: "block", flexShrink: 0 }}
|
||||
/>
|
||||
<Typography.Title
|
||||
level={3}
|
||||
style={{
|
||||
margin: 0,
|
||||
fontWeight: "bold",
|
||||
letterSpacing: "-0.025em",
|
||||
lineHeight: 1.2,
|
||||
textTransform: "uppercase",
|
||||
}}
|
||||
>
|
||||
{APP_BRAND_NAME}
|
||||
</Typography.Title>
|
||||
</Flex>
|
||||
|
||||
<Form
|
||||
layout="vertical"
|
||||
onFinish={(values) => {
|
||||
const payload = RegisterRequestSchema.parse(values);
|
||||
registerMutation.mutate(payload);
|
||||
}}
|
||||
initialValues={{ username: "", password: "", confirmPassword: "", email: "" }}
|
||||
validateTrigger={"onBlur"}
|
||||
requiredMark={false}
|
||||
>
|
||||
<Form.Item
|
||||
name="username"
|
||||
label={<span>{t`Username`}</span>}
|
||||
rules={[{ required: true, message: t`Please enter your username` }]}
|
||||
>
|
||||
<Input placeholder="new-user" size="large" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="email"
|
||||
label={<span>{t`Email address`}</span>}
|
||||
rules={[{ type: "email", message: t`Please enter a valid email address` }]}
|
||||
>
|
||||
<Input placeholder="you@example.com" size="large" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="password"
|
||||
label={<span>{t`Password`}</span>}
|
||||
rules={[
|
||||
{ required: true, message: t`Please enter your password` },
|
||||
{ min: 6, message: t`Password must be at least 6 characters long` },
|
||||
]}
|
||||
>
|
||||
<Input.Password placeholder={t`Enter at least 6 characters`} size="large" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="confirmPassword"
|
||||
label={<span>{t`Confirm password`}</span>}
|
||||
dependencies={["password"]}
|
||||
rules={[
|
||||
{ required: true, message: t`Please confirm your password` },
|
||||
({ getFieldValue }) => ({
|
||||
validator(_, value) {
|
||||
if (!value || getFieldValue("password") === value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(new Error(t`Passwords do not match`));
|
||||
},
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<Input.Password placeholder={t`Re-enter your password`} size="large" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item style={{ marginBottom: 0, marginTop: token.marginLG }}>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={registerMutation.isPending}
|
||||
block
|
||||
size="large"
|
||||
>
|
||||
{t`Create account`}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
|
||||
<Flex justify="center" style={{ marginTop: token.margin }}>
|
||||
<Typography.Text type="secondary">
|
||||
{t`Already have an account?`}{" "}
|
||||
<Link to="/login" style={{ color: token.colorPrimary }}>
|
||||
{t`Sign in`}
|
||||
</Link>
|
||||
</Typography.Text>
|
||||
</Flex>
|
||||
</Form>
|
||||
</Card>
|
||||
</Flex>
|
||||
<Flex
|
||||
vertical
|
||||
align="center"
|
||||
style={{
|
||||
padding: `${token.paddingSM}px ${token.padding}px`,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<AppFooter />
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user