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
@@ -7,80 +7,82 @@ import {
7
7
  RocketOutlined,
8
8
  } from '@ant-design/icons'
9
9
  import { NavLink, useLocation } from 'react-router-dom'
10
+ import { useLanguage } from '../i18n/useLanguage'
10
11
 
11
12
  interface SidebarProps {
12
13
  collapsed: boolean
13
14
  onCollapse: (collapsed: boolean) => void
14
15
  }
15
16
 
16
- const MENU_ITEMS = [
17
- {
18
- key: 'dashboard',
19
- icon: <DashboardOutlined />,
20
- label: <NavLink to="/dashboard">Dashboard</NavLink>,
21
- },
22
- {
23
- key: 'content',
24
- icon: <FileTextOutlined />,
25
- label: 'Content Management',
26
- children: [
27
- {
28
- key: 'content-list',
29
- label: <NavLink to="/content">Content List</NavLink>,
30
- },
31
- {
32
- key: 'categories',
33
- label: <NavLink to="/categories">Categories</NavLink>,
34
- },
35
- ],
36
- },
37
- {
38
- key: 'users-orders',
39
- icon: <TeamOutlined />,
40
- label: 'Users & Orders',
41
- children: [
42
- {
43
- key: 'users',
44
- label: <NavLink to="/users">Users</NavLink>,
45
- },
46
- {
47
- key: 'orders',
48
- label: <NavLink to="/orders">Orders</NavLink>,
49
- },
50
- {
51
- key: 'tickets',
52
- label: <NavLink to="/tickets">Tickets</NavLink>,
53
- },
54
- {
55
- key: 'disputes',
56
- label: <NavLink to="/disputes">Disputes</NavLink>,
57
- },
58
- ],
59
- },
60
- {
61
- key: 'system',
62
- icon: <SettingOutlined />,
63
- label: 'System',
64
- children: [
65
- {
66
- key: 'roles',
67
- label: <NavLink to="/roles">Roles & Permissions</NavLink>,
68
- },
69
- {
70
- key: 'system-settings',
71
- label: <NavLink to="/settings">Settings</NavLink>,
72
- },
73
- {
74
- key: 'logs',
75
- label: <NavLink to="/system-logs">System Logs</NavLink>,
76
- },
77
- ],
78
- },
79
- ]
80
-
81
17
  export const Sidebar: React.FC<SidebarProps> = ({ collapsed, onCollapse }) => {
18
+ const { t } = useLanguage()
82
19
  const location = useLocation()
83
20
 
21
+ const MENU_ITEMS = [
22
+ {
23
+ key: 'dashboard',
24
+ icon: <DashboardOutlined />,
25
+ label: <NavLink to="/dashboard">{t('sidebar.dashboard')}</NavLink>,
26
+ },
27
+ {
28
+ key: 'content',
29
+ icon: <FileTextOutlined />,
30
+ label: t('sidebar.content'),
31
+ children: [
32
+ {
33
+ key: 'content-list',
34
+ label: <NavLink to="/content">{t('sidebar.contentList')}</NavLink>,
35
+ },
36
+ {
37
+ key: 'categories',
38
+ label: <NavLink to="/categories">{t('sidebar.categories')}</NavLink>,
39
+ },
40
+ ],
41
+ },
42
+ {
43
+ key: 'users-orders',
44
+ icon: <TeamOutlined />,
45
+ label: t('sidebar.usersOrders'),
46
+ children: [
47
+ {
48
+ key: 'users',
49
+ label: <NavLink to="/users">{t('sidebar.users')}</NavLink>,
50
+ },
51
+ {
52
+ key: 'orders',
53
+ label: <NavLink to="/orders">{t('sidebar.orders')}</NavLink>,
54
+ },
55
+ {
56
+ key: 'tickets',
57
+ label: <NavLink to="/tickets">{t('sidebar.tickets')}</NavLink>,
58
+ },
59
+ {
60
+ key: 'disputes',
61
+ label: <NavLink to="/disputes">{t('sidebar.disputes')}</NavLink>,
62
+ },
63
+ ],
64
+ },
65
+ {
66
+ key: 'system',
67
+ icon: <SettingOutlined />,
68
+ label: t('sidebar.system'),
69
+ children: [
70
+ {
71
+ key: 'roles',
72
+ label: <NavLink to="/system/roles">{t('sidebar.roles')}</NavLink>,
73
+ },
74
+ {
75
+ key: 'system-settings',
76
+ label: <NavLink to="/system/settings">{t('sidebar.settings')}</NavLink>,
77
+ },
78
+ {
79
+ key: 'logs',
80
+ label: <NavLink to="/system/logs">{t('sidebar.logs')}</NavLink>,
81
+ },
82
+ ],
83
+ },
84
+ ]
85
+
84
86
  const getSelectedKey = () => {
85
87
  const path = location.pathname
86
88
  if (path === '/dashboard') return 'dashboard'
@@ -90,9 +92,9 @@ export const Sidebar: React.FC<SidebarProps> = ({ collapsed, onCollapse }) => {
90
92
  if (path.startsWith('/orders')) return 'orders'
91
93
  if (path.startsWith('/tickets')) return 'tickets'
92
94
  if (path.startsWith('/disputes')) return 'disputes'
93
- if (path.startsWith('/roles')) return 'roles'
94
- if (path.startsWith('/settings') || path === '/settings') return 'system-settings'
95
- if (path.startsWith('/system-logs')) return 'logs'
95
+ if (path.startsWith('/system/roles')) return 'roles'
96
+ if (path.startsWith('/system/settings')) return 'system-settings'
97
+ if (path.startsWith('/system/logs')) return 'logs'
96
98
  return 'dashboard'
97
99
  }
98
100
 
@@ -1,5 +1,6 @@
1
1
  import React from 'react'
2
2
  import ReactDOM from 'react-dom/client'
3
+ import './i18n'
3
4
  import { App } from './App'
4
5
  import '../client/index.css'
5
6
  import { setupApiRequestDeps } from '@shared/core/api-request'
@@ -5,34 +5,42 @@ import { PermissionGuard } from '../components/PermissionGuard'
5
5
  import { Permission } from '@shared/modules/permission'
6
6
  import { apiClient } from '../services/apiClient'
7
7
  import type { Content, CreateContentInput } from '@shared/modules/content'
8
+ import { useLanguage } from '../i18n/useLanguage'
8
9
 
9
- const CATEGORY_LABELS = {
10
- article: '文章',
11
- announcement: '公告',
12
- tutorial: '教程',
13
- news: '新闻',
14
- policy: '政策',
15
- }
16
-
17
- const STATUS_COLORS = {
10
+ const STATUS_COLORS: Record<string, string> = {
18
11
  draft: 'default',
19
12
  published: 'success',
20
13
  archived: 'warning',
21
14
  }
22
15
 
23
- const STATUS_LABELS = {
24
- draft: '草稿',
25
- published: '已发布',
26
- archived: '已归档',
27
- }
28
-
29
16
  export const ContentPage: React.FC = () => {
17
+ const { t, formatDate } = useLanguage()
30
18
  const [contents, setContents] = useState<Content[]>([])
31
19
  const [loading, setLoading] = useState(false)
32
20
  const [modalVisible, setModalVisible] = useState(false)
33
21
  const [editingContent, setEditingContent] = useState<Content | null>(null)
34
22
  const [form] = Form.useForm<CreateContentInput>()
35
23
 
24
+ const getCategoryLabel = (category: string) => {
25
+ const map: Record<string, string> = {
26
+ article: t('content.catArticle'),
27
+ announcement: t('content.catAnnouncement'),
28
+ tutorial: t('content.catTutorial'),
29
+ news: t('content.catNews'),
30
+ policy: t('content.catPolicy'),
31
+ }
32
+ return map[category] || category
33
+ }
34
+
35
+ const getStatusLabel = (status: string) => {
36
+ const map: Record<string, string> = {
37
+ draft: t('content.statusDraft'),
38
+ published: t('content.statusPublished'),
39
+ archived: t('content.statusArchived'),
40
+ }
41
+ return map[status] || status
42
+ }
43
+
36
44
  useEffect(() => {
37
45
  fetchContents()
38
46
  }, [])
@@ -45,10 +53,10 @@ export const ContentPage: React.FC = () => {
45
53
  if (result.success) {
46
54
  setContents(result.data)
47
55
  } else {
48
- message.error('Failed to load content')
56
+ message.error(t('content.loadFailed'))
49
57
  }
50
58
  } catch {
51
- message.error('获取内容列表失败')
59
+ message.error(t('content.loadFailed'))
52
60
  } finally {
53
61
  setLoading(false)
54
62
  }
@@ -68,8 +76,8 @@ export const ContentPage: React.FC = () => {
68
76
 
69
77
  const handleDelete = (id: string) => {
70
78
  Modal.confirm({
71
- title: '确认删除',
72
- content: '确定要删除这个内容吗?',
79
+ title: t('content.confirmDelete'),
80
+ content: t('content.confirmDeleteContent'),
73
81
  onOk: async () => {
74
82
  try {
75
83
  const response = await apiClient.api.contents[':id'].$delete({
@@ -77,11 +85,11 @@ export const ContentPage: React.FC = () => {
77
85
  })
78
86
  const result = await response.json()
79
87
  if (result.success) {
80
- message.success('内容已删除')
88
+ message.success(t('content.deleted'))
81
89
  fetchContents()
82
90
  }
83
91
  } catch {
84
- message.error('删除失败')
92
+ message.error(t('content.deleteFailed'))
85
93
  }
86
94
  },
87
95
  })
@@ -98,7 +106,7 @@ export const ContentPage: React.FC = () => {
98
106
  })
99
107
  const result = await response.json()
100
108
  if (result.success) {
101
- message.success('内容已更新')
109
+ message.success(t('content.updated'))
102
110
  setModalVisible(false)
103
111
  fetchContents()
104
112
  }
@@ -108,43 +116,43 @@ export const ContentPage: React.FC = () => {
108
116
  })
109
117
  const result = await response.json()
110
118
  if (result.success) {
111
- message.success('内容已创建')
119
+ message.success(t('content.created'))
112
120
  setModalVisible(false)
113
121
  fetchContents()
114
122
  }
115
123
  }
116
124
  } catch {
117
- message.error('操作失败')
125
+ message.error(t('common.error'))
118
126
  }
119
127
  }
120
128
 
121
129
  const columns = [
122
130
  {
123
- title: '标题',
131
+ title: t('content.titleCol'),
124
132
  dataIndex: 'title',
125
133
  key: 'title',
126
134
  },
127
135
  {
128
- title: '分类',
136
+ title: t('content.category'),
129
137
  dataIndex: 'category',
130
138
  key: 'category',
131
- render: (category: keyof typeof CATEGORY_LABELS) => <Tag>{CATEGORY_LABELS[category]}</Tag>,
139
+ render: (category: string) => <Tag>{getCategoryLabel(category)}</Tag>,
132
140
  },
133
141
  {
134
- title: '状态',
142
+ title: t('common.status'),
135
143
  dataIndex: 'status',
136
144
  key: 'status',
137
- render: (status: keyof typeof STATUS_COLORS) => (
138
- <Tag color={STATUS_COLORS[status]}>{STATUS_LABELS[status]}</Tag>
145
+ render: (status: string) => (
146
+ <Tag color={STATUS_COLORS[status]}>{getStatusLabel(status)}</Tag>
139
147
  ),
140
148
  },
141
149
  {
142
- title: '作者',
150
+ title: t('content.author'),
143
151
  dataIndex: 'author',
144
152
  key: 'author',
145
153
  },
146
154
  {
147
- title: '浏览/点赞',
155
+ title: t('content.viewsLikes'),
148
156
  key: 'stats',
149
157
  render: (_: unknown, record: Content) => (
150
158
  <span>
@@ -153,13 +161,13 @@ export const ContentPage: React.FC = () => {
153
161
  ),
154
162
  },
155
163
  {
156
- title: '创建时间',
164
+ title: t('common.createdAt'),
157
165
  dataIndex: 'createdAt',
158
166
  key: 'createdAt',
159
- render: (date: string) => new Date(date).toLocaleDateString('zh-CN'),
167
+ render: (date: string) => formatDate(date),
160
168
  },
161
169
  {
162
- title: '操作',
170
+ title: t('common.actions'),
163
171
  key: 'actions',
164
172
  render: (_: unknown, record: Content) => (
165
173
  <Space>
@@ -170,7 +178,7 @@ export const ContentPage: React.FC = () => {
170
178
  icon={<Edit className="w-4 h-4" />}
171
179
  onClick={() => handleEdit(record)}
172
180
  >
173
- 编辑
181
+ {t('common.edit')}
174
182
  </Button>
175
183
  </PermissionGuard>
176
184
  <PermissionGuard permission={Permission.CONTENT_DELETE}>
@@ -181,7 +189,7 @@ export const ContentPage: React.FC = () => {
181
189
  icon={<Delete className="w-4 h-4" />}
182
190
  onClick={() => handleDelete(record.id)}
183
191
  >
184
- 删除
192
+ {t('common.delete')}
185
193
  </Button>
186
194
  </PermissionGuard>
187
195
  </Space>
@@ -193,12 +201,12 @@ export const ContentPage: React.FC = () => {
193
201
  <div>
194
202
  <div className="flex justify-between items-center mb-6">
195
203
  <div>
196
- <h1 className="text-2xl font-bold text-gray-900">内容管理</h1>
197
- <p className="text-gray-600 mt-1">管理文章、公告和教程</p>
204
+ <h1 className="text-2xl font-bold text-gray-900">{t('content.title')}</h1>
205
+ <p className="text-gray-600 mt-1">{t('content.subtitle')}</p>
198
206
  </div>
199
207
  <PermissionGuard permission={Permission.CONTENT_CREATE}>
200
208
  <Button type="primary" icon={<Plus className="w-4 h-4" />} onClick={handleCreate}>
201
- 创建内容
209
+ {t('content.createContent')}
202
210
  </Button>
203
211
  </PermissionGuard>
204
212
  </div>
@@ -214,38 +222,42 @@ export const ContentPage: React.FC = () => {
214
222
  </Card>
215
223
 
216
224
  <Modal
217
- title={editingContent ? '编辑内容' : '创建内容'}
225
+ title={editingContent ? t('content.editContent') : t('content.createContent')}
218
226
  open={modalVisible}
219
227
  onOk={handleSubmit}
220
228
  onCancel={() => setModalVisible(false)}
221
229
  width={600}
222
230
  >
223
231
  <Form form={form} layout="vertical">
224
- <Form.Item name="title" label="标题" rules={[{ required: true, message: '请输入标题' }]}>
225
- <Input placeholder="请输入标题" />
232
+ <Form.Item
233
+ name="title"
234
+ label={t('content.titleCol')}
235
+ rules={[{ required: true, message: t('content.titleRequired') }]}
236
+ >
237
+ <Input placeholder={t('content.titlePlaceholder')} />
226
238
  </Form.Item>
227
239
  <Form.Item
228
240
  name="category"
229
- label="分类"
230
- rules={[{ required: true, message: '请选择分类' }]}
241
+ label={t('content.category')}
242
+ rules={[{ required: true, message: t('content.categoryRequired') }]}
231
243
  >
232
- <Select placeholder="请选择分类">
233
- <Select.Option value="article">文章</Select.Option>
234
- <Select.Option value="announcement">公告</Select.Option>
235
- <Select.Option value="tutorial">教程</Select.Option>
236
- <Select.Option value="news">新闻</Select.Option>
237
- <Select.Option value="policy">政策</Select.Option>
244
+ <Select placeholder={t('content.categoryPlaceholder')}>
245
+ <Select.Option value="article">{t('content.catArticle')}</Select.Option>
246
+ <Select.Option value="announcement">{t('content.catAnnouncement')}</Select.Option>
247
+ <Select.Option value="tutorial">{t('content.catTutorial')}</Select.Option>
248
+ <Select.Option value="news">{t('content.catNews')}</Select.Option>
249
+ <Select.Option value="policy">{t('content.catPolicy')}</Select.Option>
238
250
  </Select>
239
251
  </Form.Item>
240
252
  <Form.Item
241
253
  name="content"
242
- label="内容"
243
- rules={[{ required: true, message: '请输入内容' }]}
254
+ label={t('content.contentLabel')}
255
+ rules={[{ required: true, message: t('content.contentRequired') }]}
244
256
  >
245
- <Input.TextArea rows={6} placeholder="请输入内容" />
257
+ <Input.TextArea rows={6} placeholder={t('content.contentPlaceholder')} />
246
258
  </Form.Item>
247
- <Form.Item name="tags" label="标签">
248
- <Select mode="tags" placeholder="输入标签后按回车" />
259
+ <Form.Item name="tags" label={t('content.tags')}>
260
+ <Select mode="tags" placeholder={t('content.tagsPlaceholder')} />
249
261
  </Form.Item>
250
262
  </Form>
251
263
  </Modal>
@@ -1,11 +1,14 @@
1
1
  import { useEffect, useState } from 'react'
2
+ import { Button, Select, message, theme } from 'antd'
3
+ import { Activity, CheckCircle, Clock, TrendingUp, BellRing } from 'lucide-react'
2
4
  import { apiClient } from '../services/apiClient'
5
+ import { useLanguage } from '../i18n/useLanguage'
3
6
  import type { SystemStats } from '@shared/modules/admin'
4
7
  import type { NotificationType } from '@shared/schemas'
5
- import { Activity, CheckCircle, Clock, TrendingUp, Bell, BellRing } from 'lucide-react'
6
- import { Button, Select, message } from 'antd'
7
8
 
8
9
  export const DashboardPage: React.FC = () => {
10
+ const { t } = useLanguage()
11
+ const { token } = theme.useToken()
9
12
  const [stats, setStats] = useState<SystemStats | null>(null)
10
13
  const [loading, setLoading] = useState(true)
11
14
  const [sendingNotification, setSendingNotification] = useState(false)
@@ -18,20 +21,40 @@ export const DashboardPage: React.FC = () => {
18
21
  const result = await response.json()
19
22
  if (result.success) {
20
23
  setStats(result.data)
21
- } else {
22
- message.error(result.error || 'Failed to load stats')
23
24
  }
24
- } catch (error) {
25
- console.error('Failed to fetch stats:', error)
25
+ } catch {
26
+ message.error(t('dashboard.loadFailed'))
26
27
  } finally {
27
28
  setLoading(false)
28
29
  }
29
30
  }
30
31
 
31
32
  fetchStats()
32
- }, [])
33
+ }, [t])
33
34
 
34
- const handleSendTestNotification = async () => {
35
+ if (loading) {
36
+ return (
37
+ <div className="p-5" style={{ color: token.colorText }}>
38
+ {t('common.loading')}
39
+ </div>
40
+ )
41
+ }
42
+
43
+ const statCards = [
44
+ { title: t('dashboard.totalTodos'), value: stats?.totalTodos || 0, icon: CheckCircle, color: 'bg-blue-500' },
45
+ { title: t('dashboard.pending'), value: stats?.pendingTodos || 0, icon: Clock, color: 'bg-yellow-500' },
46
+ { title: t('dashboard.completed'), value: stats?.completedTodos || 0, icon: TrendingUp, color: 'bg-green-500' },
47
+ { title: t('dashboard.lastUpdated'), value: stats?.lastUpdated || '-', icon: Activity, color: 'bg-purple-500' },
48
+ ]
49
+
50
+ const notificationTypeOptions: { value: NotificationType; label: string }[] = [
51
+ { value: 'info', label: t('dashboard.notificationInfo') },
52
+ { value: 'success', label: t('dashboard.notificationSuccess') },
53
+ { value: 'warning', label: t('dashboard.notificationWarning') },
54
+ { value: 'error', label: t('dashboard.notificationError') },
55
+ ]
56
+
57
+ const handleSendTest = async () => {
35
58
  setSendingNotification(true)
36
59
  try {
37
60
  const response = await apiClient.api.admin.notifications.test.$post({
@@ -39,102 +62,85 @@ export const DashboardPage: React.FC = () => {
39
62
  })
40
63
  const result = await response.json()
41
64
  if (result.success) {
42
- message.success(`测试通知已发送 (${notificationType})`)
65
+ message.success(t('dashboard.sent', { type: notificationType }))
43
66
  } else {
44
- message.error('发送失败')
67
+ message.error(t('dashboard.sendFailed'))
45
68
  }
46
- } catch (error) {
47
- console.error('Failed to send test notification:', error)
48
- message.error('发送失败')
69
+ } catch {
70
+ message.error(t('dashboard.sendFailed'))
49
71
  } finally {
50
72
  setSendingNotification(false)
51
73
  }
52
74
  }
53
75
 
54
- if (loading) {
55
- return (
56
- <div className="flex items-center justify-center h-64">
57
- <div className="text-gray-500">Loading...</div>
58
- </div>
59
- )
60
- }
61
-
62
- const statCards = [
63
- {
64
- title: 'Total Todos',
65
- value: stats?.totalTodos || 0,
66
- icon: CheckCircle,
67
- color: 'bg-blue-500',
68
- },
69
- {
70
- title: 'Pending',
71
- value: stats?.pendingTodos || 0,
72
- icon: Clock,
73
- color: 'bg-yellow-500',
74
- },
75
- {
76
- title: 'Completed',
77
- value: stats?.completedTodos || 0,
78
- icon: TrendingUp,
79
- color: 'bg-green-500',
80
- },
81
- {
82
- title: 'Last Updated',
83
- value: stats?.lastUpdated || '-',
84
- icon: Activity,
85
- color: 'bg-purple-500',
86
- },
87
- ]
88
-
89
76
  return (
90
77
  <div>
91
- <h1 className="text-2xl font-bold text-gray-900 mb-6">Dashboard</h1>
78
+ <h1
79
+ className="text-2xl font-bold mb-4"
80
+ style={{ color: token.colorText }}
81
+ >
82
+ {t('dashboard.title')}
83
+ </h1>
92
84
 
93
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
85
+ <div className="grid grid-cols-4 gap-4 mb-6">
94
86
  {statCards.map((card, index) => {
95
87
  const Icon = card.icon
96
88
  return (
97
- <div key={index} className="bg-white rounded-lg shadow-sm p-6">
98
- <div className="flex items-center justify-between mb-4">
99
- <div className={`p-3 rounded-lg ${card.color}`}>
89
+ <div
90
+ key={index}
91
+ className="rounded-lg p-6"
92
+ style={{
93
+ backgroundColor: token.colorBgContainer,
94
+ boxShadow: `0 1px 3px ${token.colorBorderSecondary}`,
95
+ }}
96
+ >
97
+ <div className="flex items-center justify-between mb-2">
98
+ <div className={`p-3 rounded-md ${card.color} w-10 h-10 flex items-center justify-center`}>
100
99
  <Icon className="w-6 h-6 text-white" />
101
100
  </div>
102
101
  </div>
103
- <h3 className="text-sm font-medium text-gray-500">{card.title}</h3>
104
- <p className="text-2xl font-bold text-gray-900 mt-1">{card.value}</p>
102
+ <h3
103
+ className="text-sm font-medium"
104
+ style={{ color: token.colorTextSecondary }}
105
+ >
106
+ {card.title}
107
+ </h3>
108
+ <p
109
+ className="text-2xl font-bold mt-1"
110
+ style={{ color: token.colorText }}
111
+ >
112
+ {card.value}
113
+ </p>
105
114
  </div>
106
115
  )
107
116
  })}
108
117
  </div>
109
118
 
110
- <div className="bg-white rounded-lg shadow-sm p-6">
111
- <div className="flex items-center gap-3 mb-4">
112
- <BellRing className="w-5 h-5 text-gray-600" />
113
- <h2 className="text-lg font-semibold text-gray-900">测试通知功能</h2>
119
+ <div
120
+ className="rounded-lg p-6"
121
+ style={{
122
+ backgroundColor: token.colorBgContainer,
123
+ boxShadow: `0 1px 3px ${token.colorBorderSecondary}`,
124
+ }}
125
+ >
126
+ <div className="flex items-center gap-2 mb-4">
127
+ <BellRing className="w-5 h-5" style={{ color: token.colorTextSecondary }} />
128
+ <h2 className="text-base font-semibold" style={{ color: token.colorText }}>
129
+ {t('dashboard.testNotification')}
130
+ </h2>
114
131
  </div>
115
- <p className="text-sm text-gray-500 mb-4">
116
- 发送测试通知到所有已连接的客户端。error 和 warning 类型的通知会弹出 antd
117
- notification,info 和 success 类型只会出现在铃铛列表中。
132
+ <p className="text-sm mb-4" style={{ color: token.colorTextSecondary }}>
133
+ {t('dashboard.testNotificationDesc')}
118
134
  </p>
119
- <div className="flex items-center gap-4">
135
+ <div className="flex gap-2">
120
136
  <Select
121
137
  value={notificationType}
122
138
  onChange={setNotificationType}
123
139
  style={{ width: 120 }}
124
- options={[
125
- { value: 'info', label: 'ℹ️ Info' },
126
- { value: 'success', label: '✅ Success' },
127
- { value: 'warning', label: '⚠️ Warning' },
128
- { value: 'error', label: '❌ Error' },
129
- ]}
140
+ options={notificationTypeOptions}
130
141
  />
131
- <Button
132
- type="primary"
133
- icon={<Bell className="w-4 h-4" />}
134
- loading={sendingNotification}
135
- onClick={handleSendTestNotification}
136
- >
137
- 发送测试通知
142
+ <Button type="primary" loading={sendingNotification} onClick={handleSendTest}>
143
+ {t('dashboard.sendTest')}
138
144
  </Button>
139
145
  </div>
140
146
  </div>