create-fullstack-scaffold 0.4.9 → 0.4.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/dist/cli/index.js +499 -100
  2. package/dist/cli/index.js.map +1 -1
  3. package/package.json +2 -2
  4. package/template/CLAUDE.md +18 -6
  5. package/template/drizzle/0004_add_merchants_products.sql +30 -0
  6. package/template/drizzle/meta/_journal.json +7 -0
  7. package/template/eslint-rules/module-boundary.js +6 -2
  8. package/template/eslint-rules/no-cross-module-service-import.js +105 -0
  9. package/template/eslint-rules/no-disable-type-safe-client.js +5 -0
  10. package/template/eslint.config.js +9 -0
  11. package/template/lint-scripts/config/project.config.ts +21 -0
  12. package/template/lint-scripts/validate-all.ts +48 -12
  13. package/template/lint-scripts/validators/index.ts +29 -0
  14. package/template/lint-scripts/validators/module-public-api.validator.ts +103 -0
  15. package/template/lint-scripts/validators/schema-uniqueness.validator.ts +147 -0
  16. package/template/modules.config.ts +2 -0
  17. package/template/package.json +2 -0
  18. package/template/playwright.config.ts +14 -0
  19. package/template/src/admin/App.tsx +40 -3
  20. package/template/src/admin/components/LanguageSwitcher.tsx +22 -0
  21. package/template/src/admin/components/NotificationDrawer.tsx +108 -38
  22. package/template/src/admin/components/StatsCard.tsx +3 -1
  23. package/template/src/admin/components/ThemeToggle.tsx +28 -0
  24. package/template/src/admin/hooks/useRoles.ts +2 -2
  25. package/template/src/admin/i18n/index.ts +18 -0
  26. package/template/src/admin/i18n/locales/en-US.json +381 -0
  27. package/template/src/admin/i18n/locales/zh-CN.json +381 -0
  28. package/template/src/admin/i18n/useLanguage.ts +21 -0
  29. package/template/src/admin/layouts/Header.tsx +37 -16
  30. package/template/src/admin/layouts/Layout.tsx +7 -3
  31. package/template/src/admin/layouts/Sidebar.tsx +70 -68
  32. package/template/src/admin/main.tsx +1 -0
  33. package/template/src/admin/pages/ContentPage.tsx +68 -56
  34. package/template/src/admin/pages/DashboardPage.tsx +82 -76
  35. package/template/src/admin/pages/DisputesPage.tsx +61 -53
  36. package/template/src/admin/pages/LoginPage.tsx +41 -33
  37. package/template/src/admin/pages/OrdersPage.tsx +69 -64
  38. package/template/src/admin/pages/PermissionsPage.tsx +10 -8
  39. package/template/src/admin/pages/PluginDashboardPage.tsx +3 -1
  40. package/template/src/admin/pages/PluginManagementPage.tsx +3 -1
  41. package/template/src/admin/pages/RegisterPage.tsx +18 -16
  42. package/template/src/admin/pages/RolesPage.tsx +51 -49
  43. package/template/src/admin/pages/SettingsPage.tsx +22 -22
  44. package/template/src/admin/pages/SystemLogsPage.tsx +32 -30
  45. package/template/src/admin/pages/TicketsPage.tsx +67 -59
  46. package/template/src/admin/pages/UsersPage.tsx +63 -53
  47. package/template/src/admin/pages/__tests__/RolesPage.test.tsx +2 -2
  48. package/template/src/admin/stores/themeStore.ts +32 -0
  49. package/template/src/cli/modules/admin/index.ts +200 -0
  50. package/template/src/cli/modules/captcha/index.ts +40 -0
  51. package/template/src/cli/modules/chat/index.ts +21 -0
  52. package/template/src/cli/modules/content/index.ts +144 -0
  53. package/template/src/cli/modules/dispute/index.ts +150 -0
  54. package/template/src/cli/modules/file/index.ts +55 -0
  55. package/template/src/cli/modules/index.ts +30 -5
  56. package/template/src/cli/modules/order/index.ts +170 -0
  57. package/template/src/cli/modules/permission/index.ts +195 -0
  58. package/template/src/cli/modules/tenant/index.ts +142 -0
  59. package/template/src/cli/modules/ticket/index.ts +167 -0
  60. package/template/src/cli/rpc/client.ts +2 -2
  61. package/template/src/client/index.css +73 -0
  62. package/template/src/merchant/App.tsx +2 -0
  63. package/template/src/merchant/components/MerchantGuard.tsx +2 -6
  64. package/template/src/merchant/components/__tests__/MerchantGuard.test.tsx +117 -0
  65. package/template/src/merchant/layouts/Header.tsx +2 -2
  66. package/template/src/merchant/pages/DashboardPage.tsx +2 -2
  67. package/template/src/merchant/pages/DisputesPage.tsx +14 -10
  68. package/template/src/merchant/pages/LoginPage.tsx +49 -0
  69. package/template/src/merchant/pages/OrdersPage.tsx +17 -10
  70. package/template/src/merchant/pages/ProductsPage.tsx +31 -8
  71. package/template/src/merchant/pages/SettingsPage.tsx +3 -7
  72. package/template/src/merchant/stores/__tests__/merchantStore.test.ts +317 -0
  73. package/template/src/merchant/stores/merchantStore.ts +24 -27
  74. package/template/src/server/db/schema/index.ts +2 -0
  75. package/template/src/server/db/schema/merchants.ts +26 -0
  76. package/template/src/server/db/schema/products.ts +25 -0
  77. package/template/src/server/db/test-setup.ts +237 -0
  78. package/template/src/server/middleware/__tests__/audit-log.test.ts +144 -0
  79. package/template/src/server/middleware/__tests__/cors.test.ts +84 -0
  80. package/template/src/server/middleware/__tests__/logger.test.ts +117 -0
  81. package/template/src/server/middleware/__tests__/rate-limit.test.ts +73 -0
  82. package/template/src/server/middleware/__tests__/realtime-env.test.ts +56 -0
  83. package/template/src/server/middleware/__tests__/tenant-isolation.test.ts +150 -0
  84. package/template/src/server/module-admin/__tests__/admin-routes.test.ts +1 -1
  85. package/template/src/server/module-admin/module.ts +4 -0
  86. package/template/src/server/module-admin/routes/admin-notification-routes.ts +3 -3
  87. package/template/src/server/module-admin/routes/user-management-routes.ts +2 -2
  88. package/template/src/server/module-auth/__tests__/auth-routes.test.ts +315 -0
  89. package/template/src/server/module-auth/__tests__/profile-routes.test.ts +83 -0
  90. package/template/src/server/module-auth/module.ts +2 -0
  91. package/template/src/server/module-content/module.ts +1 -0
  92. package/template/src/server/module-content/routes/content-routes.ts +2 -2
  93. package/template/src/server/module-dispute/module.ts +1 -0
  94. package/template/src/server/module-dispute/routes/dispute-routes.ts +2 -2
  95. package/template/src/server/module-merchant/__tests__/merchant-routes.test.ts +218 -0
  96. package/template/src/server/module-merchant/__tests__/merchant-service.test.ts +183 -0
  97. package/template/src/server/module-merchant/index.ts +10 -0
  98. package/template/src/server/module-merchant/module.ts +33 -0
  99. package/template/src/server/module-merchant/routes/merchant-routes.ts +138 -0
  100. package/template/src/server/module-merchant/services/merchant-service.ts +292 -0
  101. package/template/src/server/module-notifications/index.ts +18 -0
  102. package/template/src/server/module-notifications/module.ts +2 -0
  103. package/template/src/server/module-order/module.ts +1 -0
  104. package/template/src/server/module-order/routes/order-routes.ts +2 -2
  105. package/template/src/server/module-permission/routes/role-routes.ts +3 -3
  106. package/template/src/server/module-plugin/__tests__/admin-category-service.test.ts +181 -0
  107. package/template/src/server/module-plugin/__tests__/admin-plugin-service.test.ts +277 -0
  108. package/template/src/server/module-plugin/__tests__/admin-stats-service.test.ts +170 -0
  109. package/template/src/server/module-plugin/__tests__/plugin-review-service.test.ts +260 -0
  110. package/template/src/server/module-plugin/__tests__/plugin-seed-service.test.ts +170 -0
  111. package/template/src/server/module-plugin/module.ts +3 -0
  112. package/template/src/server/module-tenant/__tests__/tenant-routes.test.ts +142 -0
  113. package/template/src/server/module-tenant/__tests__/tenant-service.test.ts +152 -0
  114. package/template/src/server/module-tenant/module.ts +1 -0
  115. package/template/src/server/module-tenant/services/tenant-service.ts +1 -37
  116. package/template/src/server/module-ticket/module.ts +1 -0
  117. package/template/src/server/module-ticket/routes/ticket-routes.ts +2 -2
  118. package/template/src/server/module-todos/module.ts +3 -0
  119. package/template/src/server/route-registry.ts +4 -0
  120. package/template/src/server/utils/__tests__/date.test.ts +130 -0
  121. package/template/src/server/utils/__tests__/env.test.ts +25 -0
  122. package/template/src/server/utils/__tests__/generate.test.ts +82 -0
  123. package/template/src/server/utils/__tests__/id-helpers.test.ts +29 -0
  124. package/template/src/server/utils/__tests__/json.test.ts +49 -0
  125. package/template/src/server/utils/__tests__/permission-utils.test.ts +26 -0
  126. package/template/src/server/utils/__tests__/uuid.test.ts +25 -0
  127. package/template/src/shared/core/module-manifest.ts +15 -0
  128. package/template/src/shared/modules/admin/schemas.ts +1 -1
  129. package/template/src/shared/modules/content/schemas.ts +2 -2
  130. package/template/src/shared/modules/dispute/schemas.ts +2 -2
  131. package/template/src/shared/modules/index.ts +181 -1
  132. package/template/src/shared/modules/merchant/index.ts +12 -0
  133. package/template/src/shared/modules/merchant/schemas.ts +63 -0
  134. package/template/src/shared/modules/order/schemas.ts +2 -2
  135. package/template/src/shared/modules/permission/index.ts +1 -1
  136. package/template/src/shared/modules/permission/schemas.ts +1 -1
  137. package/template/src/shared/modules/role/schemas.ts +2 -2
  138. package/template/src/shared/modules/ticket/schemas.ts +2 -2
  139. package/template/src/shared/schemas/index.ts +74 -2
@@ -0,0 +1,147 @@
1
+ import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs'
2
+ import { join, basename } from 'node:path'
3
+
4
+ export interface SchemaUniquenessConfig {
5
+ modulesDir: string
6
+ checkDirs: string[]
7
+ ignoreDirs: string[]
8
+ }
9
+
10
+ export interface SchemaUniquenessError {
11
+ exportName: string
12
+ modules: string[]
13
+ suggestion: string
14
+ }
15
+
16
+ const SCHEMA_EXPORT_RE = /export\s+(?:const|type|function)\s+([A-Z][A-Za-z0-9]*Schema)\b/g
17
+ const TYPE_EXPORT_RE = /export\s+type\s+([A-Z][A-Za-z0-9]*)\b/g
18
+ const RE_EXPORT_SCHEMA_RE = /\b([A-Z][A-Za-z0-9]*Schema)\b/g
19
+ const RE_EXPORT_TYPE_RE = /type\s+([A-Z][A-Za-z0-9]*)\b/g
20
+
21
+ function capitalize(s: string): string {
22
+ return s.charAt(0).toUpperCase() + s.slice(1)
23
+ }
24
+
25
+ function extractExportsFromFile(filePath: string): Set<string> {
26
+ const content = readFileSync(filePath, 'utf-8')
27
+ const names = new Set<string>()
28
+
29
+ let m: RegExpExecArray | null
30
+ SCHEMA_EXPORT_RE.lastIndex = 0
31
+ while ((m = SCHEMA_EXPORT_RE.exec(content)) !== null) {
32
+ names.add(m[1])
33
+ }
34
+
35
+ TYPE_EXPORT_RE.lastIndex = 0
36
+ while ((m = TYPE_EXPORT_RE.exec(content)) !== null) {
37
+ if (!m[1].endsWith('Schema')) {
38
+ names.add(m[1])
39
+ }
40
+ }
41
+
42
+ return names
43
+ }
44
+
45
+ function extractReExportsFromFile(filePath: string): Set<string> {
46
+ const content = readFileSync(filePath, 'utf-8')
47
+ const names = new Set<string>()
48
+
49
+ if (!/^export\s*\{/m.test(content) && !/^export\s+\*\s+from/m.test(content)) {
50
+ return names
51
+ }
52
+
53
+ let m: RegExpExecArray | null
54
+ RE_EXPORT_SCHEMA_RE.lastIndex = 0
55
+ while ((m = RE_EXPORT_SCHEMA_RE.exec(content)) !== null) {
56
+ names.add(m[1])
57
+ }
58
+
59
+ RE_EXPORT_TYPE_RE.lastIndex = 0
60
+ while ((m = RE_EXPORT_TYPE_RE.exec(content)) !== null) {
61
+ if (!m[1].endsWith('Schema')) {
62
+ names.add(m[1])
63
+ }
64
+ }
65
+
66
+ return names
67
+ }
68
+
69
+ export function validateSchemaUniqueness(
70
+ config: SchemaUniquenessConfig,
71
+ rootPath: string
72
+ ): SchemaUniquenessError[] {
73
+ const modulesPath = join(rootPath, config.modulesDir)
74
+
75
+ if (!existsSync(modulesPath)) {
76
+ return []
77
+ }
78
+
79
+ const exportMap = new Map<string, string[]>()
80
+
81
+ const entries = readdirSync(modulesPath, { withFileTypes: true })
82
+ for (const entry of entries) {
83
+ if (!entry.isDirectory()) continue
84
+ if (config.ignoreDirs.includes(entry.name)) continue
85
+
86
+ const moduleName = entry.name
87
+ const moduleDir = join(modulesPath, entry.name)
88
+
89
+ const schemasFile = join(moduleDir, 'schemas.ts')
90
+ if (existsSync(schemasFile)) {
91
+ const exports = extractExportsFromFile(schemasFile)
92
+ for (const name of exports) {
93
+ const existing = exportMap.get(name) || []
94
+ if (!existing.includes(moduleName)) {
95
+ existing.push(moduleName)
96
+ }
97
+ exportMap.set(name, existing)
98
+ }
99
+ }
100
+
101
+ const indexFile = join(moduleDir, 'index.ts')
102
+ if (existsSync(schemasFile) && existsSync(indexFile)) {
103
+ const reExports = extractReExportsFromFile(indexFile)
104
+ for (const name of reExports) {
105
+ if (exportMap.has(name)) continue
106
+ const existing = exportMap.get(name) || []
107
+ if (!existing.includes(moduleName)) {
108
+ existing.push(moduleName)
109
+ }
110
+ exportMap.set(name, existing)
111
+ }
112
+ }
113
+ }
114
+
115
+ const errors: SchemaUniquenessError[] = []
116
+ for (const [exportName, modules] of exportMap) {
117
+ if (modules.length >= 2) {
118
+ const suggestions = modules.map(mod => `${capitalize(mod)}${exportName}`)
119
+ errors.push({
120
+ exportName,
121
+ modules,
122
+ suggestion: suggestions.join(' or '),
123
+ })
124
+ }
125
+ }
126
+
127
+ errors.sort((a, b) => a.exportName.localeCompare(b.exportName))
128
+ return errors
129
+ }
130
+
131
+ export function formatSchemaUniquenessErrors(errors: SchemaUniquenessError[]): string {
132
+ if (errors.length === 0) return ''
133
+
134
+ let output = `❌ Found ${errors.length} cross-module schema naming collision(s):\n\n`
135
+
136
+ for (const err of errors) {
137
+ output += ` "${err.exportName}" defined in: ${err.modules.join(', ')}\n`
138
+ output += ` → Suggestion: rename to ${err.suggestion}\n\n`
139
+ }
140
+
141
+ output += '📋 Guidelines:\n'
142
+ output += ' Each module should use unique schema names to avoid collisions\n'
143
+ output += ' when unified via src/shared/schemas/index.ts.\n'
144
+ output += ' Recommended pattern: {ModuleName}{SchemaName} (e.g. TodoSchema, OrderSchema)\n'
145
+
146
+ return output
147
+ }
@@ -24,11 +24,13 @@ export const TEMPLATE_PRESETS: TemplatePreset[] = [
24
24
  'admin',
25
25
  'auth',
26
26
  'plugin',
27
+ 'tenant',
27
28
  // Business (all depend on permission)
28
29
  'order',
29
30
  'ticket',
30
31
  'dispute',
31
32
  'content',
33
+ 'merchant',
32
34
  ],
33
35
  },
34
36
  {
@@ -89,6 +89,8 @@
89
89
  "react-router-dom": "^7.13.1",
90
90
  "ws": "^8.18.3",
91
91
  "zod": "^4.4.3",
92
+ "i18next": "^24.2.3",
93
+ "react-i18next": "^15.5.3",
92
94
  "zustand": "^5.0.2"
93
95
  },
94
96
  "devDependencies": {
@@ -58,5 +58,19 @@ export default defineConfig({
58
58
  : {}),
59
59
  },
60
60
  },
61
+ {
62
+ name: 'firefox',
63
+ use: {
64
+ ...devices['Desktop Firefox'],
65
+ ignoreHTTPSErrors: true,
66
+ },
67
+ },
68
+ {
69
+ name: 'webkit',
70
+ use: {
71
+ ...devices['Desktop Safari'],
72
+ ignoreHTTPSErrors: true,
73
+ },
74
+ },
61
75
  ],
62
76
  })
@@ -1,5 +1,5 @@
1
1
  import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
2
- import { ConfigProvider } from 'antd'
2
+ import { ConfigProvider, theme } from 'antd'
3
3
  import { Layout } from './layouts/Layout'
4
4
  import { DashboardPage } from './pages/DashboardPage'
5
5
  import { LoginPage } from './pages/LoginPage'
@@ -14,13 +14,50 @@ import { TicketsPage } from './pages/TicketsPage'
14
14
  import { DisputesPage } from './pages/DisputesPage'
15
15
  import { ContentPage } from './pages/ContentPage'
16
16
  import { ProtectedRoute, CaptchaModal } from './components'
17
+ import { useThemeStore } from './stores/themeStore'
18
+ import { useLanguage } from './i18n/useLanguage'
19
+
20
+ const ComingSoonPage: React.FC<{ title: string }> = ({ title }) => {
21
+ const { t } = useLanguage()
22
+ return (
23
+ <div className="flex items-center justify-center h-full min-h-[400px]">
24
+ <div className="text-center">
25
+ <h2 className="text-2xl font-semibold mb-2">{title}</h2>
26
+ <p className="text-gray-400">{t('monitor.comingSoon')}</p>
27
+ </div>
28
+ </div>
29
+ )
30
+ }
17
31
 
18
32
  export const App: React.FC<{ basePath?: string }> = ({ basePath = '/admin' }) => {
33
+ const mode = useThemeStore(state => state.mode)
34
+ const isDark = mode === 'dark'
35
+
19
36
  return (
20
37
  <ConfigProvider
21
38
  theme={{
39
+ algorithm: isDark ? theme.darkAlgorithm : theme.defaultAlgorithm,
22
40
  token: {
23
- colorPrimary: '#1890ff',
41
+ colorPrimary: '#6366f1',
42
+ },
43
+ components: {
44
+ Menu: {
45
+ colorItemBg: 'transparent',
46
+ colorItemText: 'rgba(255, 255, 255, 0.7)',
47
+ colorItemTextHover: 'rgba(255, 255, 255, 0.9)',
48
+ colorItemBgHover: 'rgba(255, 255, 255, 0.08)',
49
+ colorItemBgSelected: 'rgba(99, 102, 241, 0.15)',
50
+ colorItemTextSelected: '#6366f1',
51
+ colorSubItemBg: '#000c17',
52
+ itemBorderRadius: 8,
53
+ itemMarginBlock: 2,
54
+ itemHeight: 40,
55
+ },
56
+ Layout: {
57
+ siderBg: '#001529',
58
+ headerBg: isDark ? '#141414' : '#ffffff',
59
+ bodyBg: isDark ? '#1f1f1f' : '#f5f6fa',
60
+ },
24
61
  },
25
62
  }}
26
63
  >
@@ -43,7 +80,7 @@ export const App: React.FC<{ basePath?: string }> = ({ basePath = '/admin' }) =>
43
80
  <Route path="/content" element={<ContentPage />} />
44
81
  <Route path="/system/settings" element={<SettingsPage />} />
45
82
  <Route path="/system/logs" element={<SystemLogsPage />} />
46
- <Route path="/system/monitor" element={<div>系统监控页面(待开发)</div>} />
83
+ <Route path="/system/monitor" element={<ComingSoonPage title="System Monitor" />} />
47
84
  <Route path="/system/permissions" element={<PermissionsPage />} />
48
85
  <Route path="/system/roles" element={<RolesPage />} />
49
86
  </Routes>
@@ -0,0 +1,22 @@
1
+ import { Select } from 'antd'
2
+ import { useLanguage, type Language } from '../i18n/useLanguage'
3
+
4
+ const LANGUAGE_OPTIONS = [
5
+ { label: '中文', value: 'zh-CN' as Language },
6
+ { label: 'English', value: 'en-US' as Language },
7
+ ]
8
+
9
+ export const LanguageSwitcher: React.FC = () => {
10
+ const { currentLanguage, changeLanguage } = useLanguage()
11
+
12
+ return (
13
+ <Select
14
+ size="small"
15
+ value={currentLanguage}
16
+ options={LANGUAGE_OPTIONS}
17
+ onChange={changeLanguage}
18
+ style={{ width: 90 }}
19
+ data-testid="language-switcher"
20
+ />
21
+ )
22
+ }
@@ -1,6 +1,7 @@
1
1
  import { Bell, CheckCheck, Info, AlertTriangle, XCircle, CheckCircle } from 'lucide-react'
2
- import { Badge, Button, Drawer, Empty, Spin, Tabs } from 'antd'
2
+ import { Badge, Button, Drawer, Empty, Spin, Tabs, theme } from 'antd'
3
3
  import type { AppNotification, NotificationType } from '@shared/schemas'
4
+ import { useLanguage } from '../i18n/useLanguage'
4
5
 
5
6
  interface NotificationDrawerProps {
6
7
  open: boolean
@@ -14,31 +15,43 @@ interface NotificationDrawerProps {
14
15
 
15
16
  const typeConfig: Record<
16
17
  NotificationType,
17
- { color: string; bgColor: string; icon: React.ReactNode }
18
+ {
19
+ colorToken: 'colorInfo' | 'colorWarning' | 'colorError' | 'colorSuccess'
20
+ bgToken:
21
+ | 'colorInfoBg'
22
+ | 'colorWarningBg'
23
+ | 'colorErrorBg'
24
+ | 'colorSuccessBg'
25
+ icon: React.ReactNode
26
+ }
18
27
  > = {
19
28
  info: {
20
- color: '#1890ff',
21
- bgColor: '#e6f7ff',
29
+ colorToken: 'colorInfo',
30
+ bgToken: 'colorInfoBg',
22
31
  icon: <Info className="w-4 h-4" />,
23
32
  },
24
33
  warning: {
25
- color: '#faad14',
26
- bgColor: '#fffbe6',
34
+ colorToken: 'colorWarning',
35
+ bgToken: 'colorWarningBg',
27
36
  icon: <AlertTriangle className="w-4 h-4" />,
28
37
  },
29
38
  error: {
30
- color: '#ff4d4f',
31
- bgColor: '#fff2f0',
39
+ colorToken: 'colorError',
40
+ bgToken: 'colorErrorBg',
32
41
  icon: <XCircle className="w-4 h-4" />,
33
42
  },
34
43
  success: {
35
- color: '#52c41a',
36
- bgColor: '#f6ffed',
44
+ colorToken: 'colorSuccess',
45
+ bgToken: 'colorSuccessBg',
37
46
  icon: <CheckCircle className="w-4 h-4" />,
38
47
  },
39
48
  }
40
49
 
41
- function formatTime(dateString: string): string {
50
+ function formatTime(
51
+ dateString: string,
52
+ locale: string,
53
+ t: (key: string, options?: Record<string, unknown>) => string,
54
+ ): string {
42
55
  const date = new Date(dateString)
43
56
  const now = new Date()
44
57
  const diff = now.getTime() - date.getTime()
@@ -47,11 +60,11 @@ function formatTime(dateString: string): string {
47
60
  const hours = Math.floor(diff / 3600000)
48
61
  const days = Math.floor(diff / 86400000)
49
62
 
50
- if (minutes < 1) return '刚刚'
51
- if (minutes < 60) return `${minutes}分钟前`
52
- if (hours < 24) return `${hours}小时前`
53
- if (days < 7) return `${days}天前`
54
- return date.toLocaleDateString('zh-CN')
63
+ if (minutes < 1) return t('notification.justNow')
64
+ if (minutes < 60) return t('notification.minutesAgo', { count: minutes })
65
+ if (hours < 24) return t('notification.hoursAgo', { count: hours })
66
+ if (days < 7) return t('notification.daysAgo', { count: days })
67
+ return date.toLocaleDateString(locale)
55
68
  }
56
69
 
57
70
  export const NotificationDrawer: React.FC<NotificationDrawerProps> = ({
@@ -63,31 +76,65 @@ export const NotificationDrawer: React.FC<NotificationDrawerProps> = ({
63
76
  onMarkAllAsRead,
64
77
  loading,
65
78
  }) => {
79
+ const { t, currentLanguage } = useLanguage()
80
+ const { token } = theme.useToken()
81
+
66
82
  const renderNotificationItem = (notif: AppNotification) => {
67
83
  const config = typeConfig[notif.type]
84
+ const bgColor = token[config.bgToken]
85
+ const fgColor = token[config.colorToken]
68
86
 
69
87
  return (
70
88
  <div
71
89
  key={notif.id}
72
- className={`p-4 border-b border-gray-100 hover:bg-gray-50 cursor-pointer transition-colors ${
73
- !notif.read ? 'bg-blue-50/50' : ''
74
- }`}
90
+ className="p-4 cursor-pointer transition-colors"
91
+ style={{
92
+ borderBottom: `1px solid ${token.colorBorderSecondary}`,
93
+ backgroundColor: !notif.read ? token.colorInfoBg : undefined,
94
+ }}
75
95
  onClick={() => !notif.read && onMarkAsRead(notif.id)}
96
+ onMouseEnter={e =>
97
+ (e.currentTarget.style.backgroundColor = token.colorBgTextHover)
98
+ }
99
+ onMouseLeave={e =>
100
+ (e.currentTarget.style.backgroundColor =
101
+ !notif.read ? token.colorInfoBg : undefined)
102
+ }
76
103
  >
77
104
  <div className="flex items-start gap-3">
78
105
  <div
79
106
  className="flex-shrink-0 w-10 h-10 rounded-full flex items-center justify-center"
80
- style={{ backgroundColor: config.bgColor, color: config.color }}
107
+ style={{ backgroundColor: bgColor, color: fgColor }}
81
108
  >
82
109
  {config.icon}
83
110
  </div>
84
111
  <div className="flex-1 min-w-0">
85
112
  <div className="flex items-center gap-2">
86
- <span className="font-medium text-gray-900 truncate">{notif.title}</span>
87
- {!notif.read && <span className="w-2 h-2 rounded-full bg-blue-500 flex-shrink-0" />}
113
+ <span
114
+ className="font-medium truncate"
115
+ style={{ color: token.colorText }}
116
+ >
117
+ {notif.title}
118
+ </span>
119
+ {!notif.read && (
120
+ <span
121
+ className="w-2 h-2 rounded-full flex-shrink-0"
122
+ style={{ backgroundColor: token.colorPrimary }}
123
+ />
124
+ )}
88
125
  </div>
89
- <p className="text-sm text-gray-500 mt-1 line-clamp-2">{notif.message}</p>
90
- <span className="text-xs text-gray-400 mt-1 block">{formatTime(notif.createdAt)}</span>
126
+ <p
127
+ className="text-sm mt-1 line-clamp-2"
128
+ style={{ color: token.colorTextSecondary }}
129
+ >
130
+ {notif.message}
131
+ </p>
132
+ <span
133
+ className="text-xs mt-1 block"
134
+ style={{ color: token.colorTextTertiary }}
135
+ >
136
+ {formatTime(notif.createdAt, currentLanguage, t)}
137
+ </span>
91
138
  </div>
92
139
  </div>
93
140
  </div>
@@ -100,15 +147,22 @@ export const NotificationDrawer: React.FC<NotificationDrawerProps> = ({
100
147
  const tabItems = [
101
148
  {
102
149
  key: 'all',
103
- label: `全部 (${notifications.length})`,
150
+ label: `${t('notification.all')} (${notifications.length})`,
104
151
  children: (
105
- <div className="overflow-y-auto" style={{ maxHeight: 'calc(100vh - 200px)' }}>
152
+ <div
153
+ className="overflow-y-auto"
154
+ style={{ maxHeight: 'calc(100vh - 200px)' }}
155
+ >
106
156
  {loading ? (
107
157
  <div className="flex items-center justify-center py-12">
108
158
  <Spin />
109
159
  </div>
110
160
  ) : notifications.length === 0 ? (
111
- <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无通知" className="py-12" />
161
+ <Empty
162
+ image={Empty.PRESENTED_IMAGE_SIMPLE}
163
+ description={t('notification.noNotifications')}
164
+ className="py-12"
165
+ />
112
166
  ) : (
113
167
  notifications.map(renderNotificationItem)
114
168
  )}
@@ -117,13 +171,16 @@ export const NotificationDrawer: React.FC<NotificationDrawerProps> = ({
117
171
  },
118
172
  {
119
173
  key: 'unread',
120
- label: `未读 (${unreadNotifications.length})`,
174
+ label: `${t('notification.unread')} (${unreadNotifications.length})`,
121
175
  children: (
122
- <div className="overflow-y-auto" style={{ maxHeight: 'calc(100vh - 200px)' }}>
176
+ <div
177
+ className="overflow-y-auto"
178
+ style={{ maxHeight: 'calc(100vh - 200px)' }}
179
+ >
123
180
  {unreadNotifications.length === 0 ? (
124
181
  <Empty
125
182
  image={Empty.PRESENTED_IMAGE_SIMPLE}
126
- description="暂无未读通知"
183
+ description={t('notification.noUnread')}
127
184
  className="py-12"
128
185
  />
129
186
  ) : (
@@ -134,13 +191,16 @@ export const NotificationDrawer: React.FC<NotificationDrawerProps> = ({
134
191
  },
135
192
  {
136
193
  key: 'read',
137
- label: `已读 (${readNotifications.length})`,
194
+ label: `${t('notification.read')} (${readNotifications.length})`,
138
195
  children: (
139
- <div className="overflow-y-auto" style={{ maxHeight: 'calc(100vh - 200px)' }}>
196
+ <div
197
+ className="overflow-y-auto"
198
+ style={{ maxHeight: 'calc(100vh - 200px)' }}
199
+ >
140
200
  {readNotifications.length === 0 ? (
141
201
  <Empty
142
202
  image={Empty.PRESENTED_IMAGE_SIMPLE}
143
- description="暂无已读通知"
203
+ description={t('notification.noRead')}
144
204
  className="py-12"
145
205
  />
146
206
  ) : (
@@ -155,7 +215,7 @@ export const NotificationDrawer: React.FC<NotificationDrawerProps> = ({
155
215
  <Drawer
156
216
  title={
157
217
  <div className="flex items-center justify-between">
158
- <span>通知中心</span>
218
+ <span>{t('notification.center')}</span>
159
219
  {unreadCount > 0 && (
160
220
  <Button
161
221
  type="link"
@@ -164,7 +224,7 @@ export const NotificationDrawer: React.FC<NotificationDrawerProps> = ({
164
224
  onClick={onMarkAllAsRead}
165
225
  className="text-xs"
166
226
  >
167
- 全部已读
227
+ {t('notification.markAllRead')}
168
228
  </Button>
169
229
  )}
170
230
  </div>
@@ -174,7 +234,7 @@ export const NotificationDrawer: React.FC<NotificationDrawerProps> = ({
174
234
  open={open}
175
235
  width={400}
176
236
  styles={{
177
- header: { borderBottom: '1px solid #f0f0f0' },
237
+ header: { borderBottom: `1px solid ${token.colorBorderSecondary}` },
178
238
  body: { padding: 0 },
179
239
  }}
180
240
  >
@@ -187,12 +247,22 @@ export const NotificationBell: React.FC<{
187
247
  unreadCount: number
188
248
  onClick: () => void
189
249
  }> = ({ unreadCount, onClick }) => {
250
+ const { token } = theme.useToken()
251
+
190
252
  return (
191
253
  <button
192
- className="p-2 rounded-lg hover:bg-gray-100 transition-colors relative"
254
+ data-testid="notification-bell"
255
+ className="p-2 rounded-lg transition-colors relative"
256
+ style={{ color: token.colorTextSecondary }}
193
257
  onClick={onClick}
258
+ onMouseEnter={e =>
259
+ (e.currentTarget.style.backgroundColor = token.colorBgTextHover)
260
+ }
261
+ onMouseLeave={e =>
262
+ (e.currentTarget.style.backgroundColor = 'transparent')
263
+ }
194
264
  >
195
- <Bell className="w-5 h-5 text-gray-600" />
265
+ <Bell className="w-5 h-5" />
196
266
  {unreadCount > 0 && (
197
267
  <Badge
198
268
  count={unreadCount > 99 ? '99+' : unreadCount}
@@ -1,6 +1,7 @@
1
1
  import { Card, Statistic } from 'antd'
2
2
  import { ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons'
3
3
  import type { ReactNode } from 'react'
4
+ import { useLanguage } from '../i18n/useLanguage'
4
5
 
5
6
  interface StatsCardProps {
6
7
  title: string
@@ -22,6 +23,7 @@ export const StatsCard: React.FC<StatsCardProps> = ({
22
23
  trend,
23
24
  loading,
24
25
  }) => {
26
+ const { t } = useLanguage()
25
27
  return (
26
28
  <Card loading={loading}>
27
29
  <Statistic
@@ -36,7 +38,7 @@ export const StatsCard: React.FC<StatsCardProps> = ({
36
38
  <span style={{ color: trend.isUp ? '#3f8600' : '#cf1322' }}>
37
39
  {trend.isUp ? <ArrowUpOutlined /> : <ArrowDownOutlined />} {Math.abs(trend.value)}%
38
40
  </span>
39
- <span className="text-gray-400 ml-2">vs last month</span>
41
+ <span className="text-gray-400 ml-2">{t('common.vsLastMonth')}</span>
40
42
  </div>
41
43
  )}
42
44
  </Card>
@@ -0,0 +1,28 @@
1
+ import { Sun, Moon } from 'lucide-react'
2
+ import { theme } from 'antd'
3
+ import { useThemeStore } from '../stores/themeStore'
4
+ import { useLanguage } from '../i18n/useLanguage'
5
+
6
+ export const ThemeToggle: React.FC = () => {
7
+ const { mode, toggleTheme } = useThemeStore()
8
+ const { t } = useLanguage()
9
+ const { token } = theme.useToken()
10
+
11
+ return (
12
+ <button
13
+ onClick={toggleTheme}
14
+ className="p-2 rounded-lg transition-colors cursor-pointer border-none bg-transparent"
15
+ style={{ color: token.colorText }}
16
+ onMouseEnter={(e) => {
17
+ e.currentTarget.style.backgroundColor = token.colorFillQuaternary
18
+ }}
19
+ onMouseLeave={(e) => {
20
+ e.currentTarget.style.backgroundColor = 'transparent'
21
+ }}
22
+ title={mode === 'light' ? t('theme.toggleDark') : t('theme.toggleLight')}
23
+ data-testid="theme-toggle"
24
+ >
25
+ {mode === 'light' ? <Moon className="w-4 h-4" /> : <Sun className="w-4 h-4" />}
26
+ </button>
27
+ )
28
+ }
@@ -1,9 +1,9 @@
1
1
  import { create } from 'zustand'
2
2
  import { apiClient } from '../services/apiClient'
3
- import type { RoleType, CreateRoleType, UpdateRoleType } from '@shared/modules/role/schemas'
3
+ import type { RoleDataType, CreateRoleType, UpdateRoleType } from '@shared/modules/role/schemas'
4
4
 
5
5
  interface RoleState {
6
- roles: RoleType[]
6
+ roles: RoleDataType[]
7
7
  loading: boolean
8
8
  error: string | null
9
9
  fetchRoles: () => Promise<void>
@@ -0,0 +1,18 @@
1
+ import i18n from 'i18next'
2
+ import { initReactI18next } from 'react-i18next'
3
+ import zhCN from './locales/zh-CN.json'
4
+ import enUS from './locales/en-US.json'
5
+
6
+ i18n.use(initReactI18next).init({
7
+ resources: {
8
+ 'zh-CN': { translation: zhCN },
9
+ 'en-US': { translation: enUS },
10
+ },
11
+ lng: 'zh-CN',
12
+ fallbackLng: 'zh-CN',
13
+ interpolation: {
14
+ escapeValue: false,
15
+ },
16
+ })
17
+
18
+ export default i18n