1
0
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:
zuiidea
2026-04-27 18:38:11 +08:00
parent 39a6339c59
commit b277f522bd
16 changed files with 897 additions and 298 deletions

View File

@@ -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;

View File

@@ -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": {

View File

@@ -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 };

View File

@@ -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>;

View File

@@ -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>
);
}

View File

@@ -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"

View File

@@ -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;
}

View File

@@ -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 "用户"

View File

@@ -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 = {

View File

@@ -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[]);
}

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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}>

View File

@@ -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;
}

View File

@@ -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>

View 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>
);
}