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
@@ -5,15 +5,16 @@ import { PermissionGuard } from '../components/PermissionGuard'
5
5
  import { Permission } from '@shared/modules/permission'
6
6
  import { apiClient } from '../services/apiClient'
7
7
  import type { Ticket } from '@shared/modules/ticket'
8
+ import { useLanguage } from '../i18n/useLanguage'
8
9
 
9
- const PRIORITY_COLORS = {
10
+ const PRIORITY_COLORS: Record<string, string> = {
10
11
  low: 'default',
11
12
  medium: 'blue',
12
13
  high: 'orange',
13
14
  urgent: 'red',
14
15
  }
15
16
 
16
- const STATUS_COLORS = {
17
+ const STATUS_COLORS: Record<string, string> = {
17
18
  open: 'blue',
18
19
  in_progress: 'processing',
19
20
  waiting_customer: 'orange',
@@ -21,27 +22,34 @@ const STATUS_COLORS = {
21
22
  closed: 'default',
22
23
  }
23
24
 
24
- const PRIORITY_LABELS = {
25
- low: '低',
26
- medium: '中',
27
- high: '高',
28
- urgent: '紧急',
29
- }
30
-
31
- const STATUS_LABELS = {
32
- open: '待处理',
33
- in_progress: '处理中',
34
- waiting_customer: '等待客户',
35
- resolved: '已解决',
36
- closed: '已关闭',
37
- }
38
-
39
25
  export const TicketsPage: React.FC = () => {
26
+ const { t, formatDate } = useLanguage()
40
27
  const [tickets, setTickets] = useState<Ticket[]>([])
41
28
  const [loading, setLoading] = useState(false)
42
29
  const [selectedTicket, setSelectedTicket] = useState<Ticket | null>(null)
43
30
  const [detailVisible, setDetailVisible] = useState(false)
44
31
 
32
+ const getPriorityLabel = (priority: string) => {
33
+ const map: Record<string, string> = {
34
+ low: t('tickets.priorityLow'),
35
+ medium: t('tickets.priorityMedium'),
36
+ high: t('tickets.priorityHigh'),
37
+ urgent: t('tickets.priorityUrgent'),
38
+ }
39
+ return map[priority] || priority
40
+ }
41
+
42
+ const getStatusLabel = (status: string) => {
43
+ const map: Record<string, string> = {
44
+ open: t('tickets.statusOpen'),
45
+ in_progress: t('tickets.statusInProgress'),
46
+ waiting_customer: t('tickets.statusWaitingCustomer'),
47
+ resolved: t('tickets.statusResolved'),
48
+ closed: t('tickets.statusClosed'),
49
+ }
50
+ return map[status] || status
51
+ }
52
+
45
53
  useEffect(() => {
46
54
  fetchTickets()
47
55
  }, [])
@@ -54,10 +62,10 @@ export const TicketsPage: React.FC = () => {
54
62
  if (result.success) {
55
63
  setTickets(result.data)
56
64
  } else {
57
- message.error(result.error || 'Failed to load tickets')
65
+ message.error(result.error || t('tickets.loadFailed'))
58
66
  }
59
67
  } catch {
60
- message.error('获取工单列表失败')
68
+ message.error(t('tickets.loadFailed'))
61
69
  } finally {
62
70
  setLoading(false)
63
71
  }
@@ -65,8 +73,8 @@ export const TicketsPage: React.FC = () => {
65
73
 
66
74
  const handleClose = async (ticketId: string) => {
67
75
  Modal.confirm({
68
- title: '确认关闭',
69
- content: '确定要关闭这个工单吗?',
76
+ title: t('tickets.confirmClose'),
77
+ content: t('tickets.confirmCloseContent'),
70
78
  onOk: async () => {
71
79
  try {
72
80
  const response = await apiClient.api.tickets[':id'].close.$put({
@@ -74,11 +82,11 @@ export const TicketsPage: React.FC = () => {
74
82
  })
75
83
  const result = await response.json()
76
84
  if (result.success) {
77
- message.success('工单已关闭')
85
+ message.success(t('tickets.ticketClosed'))
78
86
  fetchTickets()
79
87
  }
80
88
  } catch {
81
- message.error('关闭工单失败')
89
+ message.error(t('tickets.closeFailed'))
82
90
  }
83
91
  },
84
92
  })
@@ -91,18 +99,18 @@ export const TicketsPage: React.FC = () => {
91
99
 
92
100
  const columns = [
93
101
  {
94
- title: '工单号',
102
+ title: t('tickets.ticketNo'),
95
103
  dataIndex: 'ticketNo',
96
104
  key: 'ticketNo',
97
105
  render: (text: string) => <code className="text-sm">{text}</code>,
98
106
  },
99
107
  {
100
- title: '主题',
108
+ title: t('tickets.subject'),
101
109
  dataIndex: 'subject',
102
110
  key: 'subject',
103
111
  },
104
112
  {
105
- title: '客户',
113
+ title: t('tickets.customer'),
106
114
  key: 'customer',
107
115
  render: (_: unknown, record: Ticket) => (
108
116
  <div>
@@ -112,29 +120,29 @@ export const TicketsPage: React.FC = () => {
112
120
  ),
113
121
  },
114
122
  {
115
- title: '优先级',
123
+ title: t('tickets.priority'),
116
124
  dataIndex: 'priority',
117
125
  key: 'priority',
118
- render: (priority: keyof typeof PRIORITY_COLORS) => (
119
- <Tag color={PRIORITY_COLORS[priority]}>{PRIORITY_LABELS[priority]}</Tag>
126
+ render: (priority: string) => (
127
+ <Tag color={PRIORITY_COLORS[priority]}>{getPriorityLabel(priority)}</Tag>
120
128
  ),
121
129
  },
122
130
  {
123
- title: '状态',
131
+ title: t('common.status'),
124
132
  dataIndex: 'status',
125
133
  key: 'status',
126
- render: (status: keyof typeof STATUS_COLORS) => (
127
- <Tag color={STATUS_COLORS[status]}>{STATUS_LABELS[status]}</Tag>
134
+ render: (status: string) => (
135
+ <Tag color={STATUS_COLORS[status]}>{getStatusLabel(status)}</Tag>
128
136
  ),
129
137
  },
130
138
  {
131
- title: '创建时间',
139
+ title: t('common.createdAt'),
132
140
  dataIndex: 'createdAt',
133
141
  key: 'createdAt',
134
- render: (date: string) => new Date(date).toLocaleString('zh-CN'),
142
+ render: (date: string) => formatDate(date),
135
143
  },
136
144
  {
137
- title: '操作',
145
+ title: t('common.actions'),
138
146
  key: 'actions',
139
147
  render: (_: unknown, record: Ticket) => (
140
148
  <Space>
@@ -144,7 +152,7 @@ export const TicketsPage: React.FC = () => {
144
152
  icon={<Eye className="w-4 h-4" />}
145
153
  onClick={() => showDetail(record)}
146
154
  >
147
- 查看
155
+ {t('common.view')}
148
156
  </Button>
149
157
  <PermissionGuard permission={Permission.TICKET_CLOSE}>
150
158
  {record.status !== 'closed' && (
@@ -154,7 +162,7 @@ export const TicketsPage: React.FC = () => {
154
162
  icon={<CheckCircle className="w-4 h-4" />}
155
163
  onClick={() => handleClose(record.id)}
156
164
  >
157
- 关闭
165
+ {t('common.close')}
158
166
  </Button>
159
167
  )}
160
168
  </PermissionGuard>
@@ -167,17 +175,17 @@ export const TicketsPage: React.FC = () => {
167
175
  <div>
168
176
  <div className="flex justify-between items-center mb-6">
169
177
  <div>
170
- <h1 className="text-2xl font-bold text-gray-900">客服中心</h1>
171
- <p className="text-gray-600 mt-1">处理客户工单和咨询</p>
178
+ <h1 className="text-2xl font-bold text-gray-900">{t('tickets.title')}</h1>
179
+ <p className="text-gray-600 mt-1">{t('tickets.subtitle')}</p>
172
180
  </div>
173
- <Button onClick={fetchTickets}>刷新</Button>
181
+ <Button onClick={fetchTickets}>{t('common.refresh')}</Button>
174
182
  </div>
175
183
 
176
184
  <div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
177
185
  <Card>
178
186
  <div className="flex items-center justify-between">
179
187
  <div>
180
- <div className="text-sm text-gray-500">总工单</div>
188
+ <div className="text-sm text-gray-500">{t('tickets.totalTickets')}</div>
181
189
  <div className="text-2xl font-bold">{tickets.length}</div>
182
190
  </div>
183
191
  <MessageCircle className="w-8 h-8 text-blue-500" />
@@ -186,7 +194,7 @@ export const TicketsPage: React.FC = () => {
186
194
  <Card>
187
195
  <div className="flex items-center justify-between">
188
196
  <div>
189
- <div className="text-sm text-gray-500">待处理</div>
197
+ <div className="text-sm text-gray-500">{t('tickets.pending')}</div>
190
198
  <div className="text-2xl font-bold text-blue-500">
191
199
  {tickets.filter(t => t.status === 'open').length}
192
200
  </div>
@@ -197,7 +205,7 @@ export const TicketsPage: React.FC = () => {
197
205
  <Card>
198
206
  <div className="flex items-center justify-between">
199
207
  <div>
200
- <div className="text-sm text-gray-500">紧急</div>
208
+ <div className="text-sm text-gray-500">{t('tickets.urgent')}</div>
201
209
  <div className="text-2xl font-bold text-red-500">
202
210
  {tickets.filter(t => t.priority === 'urgent').length}
203
211
  </div>
@@ -208,7 +216,7 @@ export const TicketsPage: React.FC = () => {
208
216
  <Card>
209
217
  <div className="flex items-center justify-between">
210
218
  <div>
211
- <div className="text-sm text-gray-500">已解决</div>
219
+ <div className="text-sm text-gray-500">{t('tickets.resolved')}</div>
212
220
  <div className="text-2xl font-bold text-green-500">
213
221
  {tickets.filter(t => t.status === 'resolved' || t.status === 'closed').length}
214
222
  </div>
@@ -226,13 +234,13 @@ export const TicketsPage: React.FC = () => {
226
234
  loading={loading}
227
235
  pagination={{
228
236
  pageSize: 10,
229
- showTotal: total => `共 ${total} 条`,
237
+ showTotal: total => t('common.total', { count: total }),
230
238
  }}
231
239
  />
232
240
  </Card>
233
241
 
234
242
  <Modal
235
- title="工单详情"
243
+ title={t('tickets.detail')}
236
244
  open={detailVisible}
237
245
  onCancel={() => setDetailVisible(false)}
238
246
  footer={null}
@@ -241,35 +249,35 @@ export const TicketsPage: React.FC = () => {
241
249
  {selectedTicket && (
242
250
  <div>
243
251
  <Descriptions column={2} bordered className="mb-4">
244
- <Descriptions.Item label="工单号">
252
+ <Descriptions.Item label={t('tickets.ticketNo')}>
245
253
  <code>{selectedTicket.ticketNo}</code>
246
254
  </Descriptions.Item>
247
- <Descriptions.Item label="状态">
255
+ <Descriptions.Item label={t('common.status')}>
248
256
  <Tag color={STATUS_COLORS[selectedTicket.status]}>
249
- {STATUS_LABELS[selectedTicket.status]}
257
+ {getStatusLabel(selectedTicket.status)}
250
258
  </Tag>
251
259
  </Descriptions.Item>
252
- <Descriptions.Item label="客户姓名">{selectedTicket.customerName}</Descriptions.Item>
253
- <Descriptions.Item label="客户邮箱">{selectedTicket.customerEmail}</Descriptions.Item>
254
- <Descriptions.Item label="优先级">
260
+ <Descriptions.Item label={t('tickets.customerName')}>{selectedTicket.customerName}</Descriptions.Item>
261
+ <Descriptions.Item label={t('tickets.customerEmail')}>{selectedTicket.customerEmail}</Descriptions.Item>
262
+ <Descriptions.Item label={t('tickets.priority')}>
255
263
  <Tag color={PRIORITY_COLORS[selectedTicket.priority]}>
256
- {PRIORITY_LABELS[selectedTicket.priority]}
264
+ {getPriorityLabel(selectedTicket.priority)}
257
265
  </Tag>
258
266
  </Descriptions.Item>
259
- <Descriptions.Item label="负责人">
260
- {selectedTicket.assignedTo || '未分配'}
267
+ <Descriptions.Item label={t('tickets.assignedTo')}>
268
+ {selectedTicket.assignedTo || t('tickets.unassigned')}
261
269
  </Descriptions.Item>
262
- <Descriptions.Item label="主题" span={2}>
270
+ <Descriptions.Item label={t('tickets.subject')} span={2}>
263
271
  {selectedTicket.subject}
264
272
  </Descriptions.Item>
265
- <Descriptions.Item label="描述" span={2}>
273
+ <Descriptions.Item label={t('tickets.description')} span={2}>
266
274
  {selectedTicket.description}
267
275
  </Descriptions.Item>
268
276
  </Descriptions>
269
277
 
270
278
  {selectedTicket.replies.length > 0 && (
271
279
  <div className="mt-4">
272
- <h4 className="font-semibold mb-2">回复记录</h4>
280
+ <h4 className="font-semibold mb-2">{t('tickets.replies')}</h4>
273
281
  {selectedTicket.replies.map(reply => (
274
282
  <div
275
283
  key={reply.id}
@@ -278,7 +286,7 @@ export const TicketsPage: React.FC = () => {
278
286
  <div className="flex justify-between items-center mb-1">
279
287
  <span className="font-medium">{reply.author}</span>
280
288
  <span className="text-sm text-gray-500">
281
- {new Date(reply.createdAt).toLocaleString('zh-CN')}
289
+ {formatDate(reply.createdAt)}
282
290
  </span>
283
291
  </div>
284
292
  <div className="text-gray-700">{reply.content}</div>
@@ -25,10 +25,12 @@ import { useRoleLabels } from '../hooks/useConfig'
25
25
  import { Permission, Role } from '@shared/modules/permission'
26
26
  import { PermissionGuard } from '../components/PermissionGuard'
27
27
  import type { User, CreateUserRequest } from '@shared/modules/admin'
28
+ import { useLanguage } from '../i18n/useLanguage'
28
29
 
29
30
  type UserFormData = CreateUserRequest & { password?: string }
30
31
 
31
32
  export const UsersPage: React.FC = () => {
33
+ const { t, formatDate } = useLanguage()
32
34
  const [users, setUsers] = useState<User[]>([])
33
35
  const [modalVisible, setModalVisible] = useState(false)
34
36
  const [editingUser, setEditingUser] = useState<User | null>(null)
@@ -37,10 +39,10 @@ export const UsersPage: React.FC = () => {
37
39
 
38
40
  const fetchUsers = async () => {
39
41
  try {
40
- const data = await api(apiClient.api.admin.users.$get()).withLoading('加载用户列表...').json()
42
+ const data = await api(apiClient.api.admin.users.$get()).withLoading(t('users.loading')).json()
41
43
  setUsers(data)
42
44
  } catch {
43
- // 错误已由 api-request 自动处理
45
+ // handled by api-request
44
46
  }
45
47
  }
46
48
 
@@ -74,9 +76,9 @@ export const UsersPage: React.FC = () => {
74
76
  json: values,
75
77
  })
76
78
  )
77
- .withLoading('更新中...')
79
+ .withLoading(t('users.updating'))
78
80
  .json()
79
- message.success('用户更新成功')
81
+ message.success(t('users.updated'))
80
82
  setModalVisible(false)
81
83
  fetchUsers()
82
84
  } else {
@@ -91,14 +93,14 @@ export const UsersPage: React.FC = () => {
91
93
  },
92
94
  })
93
95
  )
94
- .withLoading('创建中...')
96
+ .withLoading(t('users.creating'))
95
97
  .json()
96
- message.success('用户创建成功')
98
+ message.success(t('users.created'))
97
99
  setModalVisible(false)
98
100
  fetchUsers()
99
101
  }
100
102
  } catch {
101
- // 错误已由 api-request 自动处理
103
+ // handled by api-request
102
104
  }
103
105
  }
104
106
 
@@ -113,10 +115,10 @@ export const UsersPage: React.FC = () => {
113
115
  )
114
116
  .withLoading()
115
117
  .json()
116
- message.success(newStatus === 'locked' ? '用户已锁定' : '用户已解锁')
118
+ message.success(newStatus === 'locked' ? t('users.userLocked') : t('users.userUnlocked'))
117
119
  fetchUsers()
118
120
  } catch {
119
- // 错误已由 api-request 自动处理
121
+ // handled by api-request
120
122
  }
121
123
  }
122
124
 
@@ -127,63 +129,63 @@ export const UsersPage: React.FC = () => {
127
129
  param: { id: userId },
128
130
  })
129
131
  )
130
- .withLoading('删除中...')
132
+ .withLoading(t('users.deleting'))
131
133
  .json()
132
- message.success('用户已删除')
134
+ message.success(t('users.deleted'))
133
135
  fetchUsers()
134
136
  } catch {
135
- // 错误已由 api-request 自动处理
137
+ // handled by api-request
136
138
  }
137
139
  }
138
140
 
139
141
  const getStatusTag = (status: string) => {
140
- const statusConfig: Record<string, { color: string; text: string }> = {
141
- active: { color: 'green', text: '正常' },
142
- inactive: { color: 'orange', text: '未激活' },
143
- locked: { color: 'red', text: '已锁定' },
142
+ const statusConfig: Record<string, { color: string; textKey: string }> = {
143
+ active: { color: 'green', textKey: 'users.active' },
144
+ inactive: { color: 'orange', textKey: 'users.inactive' },
145
+ locked: { color: 'red', textKey: 'users.locked' },
144
146
  }
145
147
  const config = statusConfig[status]
146
148
  if (!config) return <Tag>{status}</Tag>
147
- return <Tag color={config.color}>{config.text}</Tag>
149
+ return <Tag color={config.color}>{t(config.textKey)}</Tag>
148
150
  }
149
151
 
150
152
  const columns: ColumnsType<User> = [
151
153
  {
152
- title: '用户名',
154
+ title: t('users.username'),
153
155
  dataIndex: 'username',
154
156
  key: 'username',
155
157
  },
156
158
  {
157
- title: '邮箱',
159
+ title: t('users.email'),
158
160
  dataIndex: 'email',
159
161
  key: 'email',
160
162
  },
161
163
  {
162
- title: '角色',
164
+ title: t('users.role'),
163
165
  dataIndex: 'role',
164
166
  key: 'role',
165
167
  render: (role: Role) => <Tag color="blue">{roleLabels[role] || role}</Tag>,
166
168
  },
167
169
  {
168
- title: '状态',
170
+ title: t('users.status'),
169
171
  dataIndex: 'status',
170
172
  key: 'status',
171
173
  render: (status: string) => getStatusTag(status),
172
174
  },
173
175
  {
174
- title: '创建时间',
176
+ title: t('common.createdAt'),
175
177
  dataIndex: 'createdAt',
176
178
  key: 'createdAt',
177
- render: (date: string) => new Date(date).toLocaleString('zh-CN'),
179
+ render: (date: string) => formatDate(date),
178
180
  },
179
181
  {
180
- title: '操作',
182
+ title: t('common.actions'),
181
183
  key: 'actions',
182
184
  render: (_: unknown, record: User) => (
183
185
  <Space>
184
186
  <PermissionGuard permission={Permission.USER_EDIT}>
185
187
  <Button type="link" icon={<EditOutlined />} onClick={() => handleEdit(record)}>
186
- 编辑
188
+ {t('common.edit')}
187
189
  </Button>
188
190
  </PermissionGuard>
189
191
  <PermissionGuard permission={Permission.USER_EDIT}>
@@ -193,18 +195,18 @@ export const UsersPage: React.FC = () => {
193
195
  onClick={() => handleToggleLock(record)}
194
196
  danger={record.status !== 'locked'}
195
197
  >
196
- {record.status === 'locked' ? '解锁' : '锁定'}
198
+ {record.status === 'locked' ? t('users.unlock') : t('users.lock')}
197
199
  </Button>
198
200
  </PermissionGuard>
199
201
  <PermissionGuard permission={Permission.USER_DELETE}>
200
202
  <Popconfirm
201
- title="确定要删除此用户吗?"
203
+ title={t('users.deleteConfirm')}
202
204
  onConfirm={() => handleDelete(record.id)}
203
- okText="确定"
204
- cancelText="取消"
205
+ okText={t('common.confirm')}
206
+ cancelText={t('common.cancel')}
205
207
  >
206
208
  <Button type="link" icon={<DeleteOutlined />} danger>
207
- 删除
209
+ {t('common.delete')}
208
210
  </Button>
209
211
  </Popconfirm>
210
212
  </PermissionGuard>
@@ -216,11 +218,11 @@ export const UsersPage: React.FC = () => {
216
218
  return (
217
219
  <div className="p-6">
218
220
  <Card
219
- title="用户管理"
221
+ title={t('users.title')}
220
222
  extra={
221
223
  <PermissionGuard permission={Permission.USER_CREATE}>
222
224
  <Button type="primary" icon={<PlusOutlined />} onClick={handleCreate}>
223
- 创建用户
225
+ {t('users.createUser')}
224
226
  </Button>
225
227
  </PermissionGuard>
226
228
  }
@@ -229,7 +231,7 @@ export const UsersPage: React.FC = () => {
229
231
  </Card>
230
232
 
231
233
  <Modal
232
- title={editingUser ? '编辑用户' : '创建用户'}
234
+ title={editingUser ? t('users.editUser') : t('users.createUser')}
233
235
  open={modalVisible}
234
236
  onCancel={() => setModalVisible(false)}
235
237
  onOk={() => form.submit()}
@@ -238,42 +240,50 @@ export const UsersPage: React.FC = () => {
238
240
  <Form form={form} layout="vertical" onFinish={handleSubmit}>
239
241
  <Form.Item
240
242
  name="username"
241
- label="用户名"
242
- rules={[{ required: true, message: '请输入用户名' }]}
243
+ label={t('users.username')}
244
+ rules={[{ required: true, message: t('users.usernameRequired') }]}
243
245
  >
244
- <Input placeholder="请输入用户名" />
246
+ <Input placeholder={t('users.usernamePlaceholder')} />
245
247
  </Form.Item>
246
248
  <Form.Item
247
249
  name="email"
248
- label="邮箱"
250
+ label={t('users.email')}
249
251
  rules={[
250
- { required: true, message: '请输入邮箱' },
251
- { type: 'email', message: '请输入有效的邮箱地址' },
252
+ { required: true, message: t('users.emailRequired') },
253
+ { type: 'email', message: t('users.emailInvalid') },
252
254
  ]}
253
255
  >
254
- <Input placeholder="请输入邮箱" />
256
+ <Input placeholder={t('users.emailPlaceholder')} />
255
257
  </Form.Item>
256
258
  {!editingUser && (
257
259
  <Form.Item
258
260
  name="password"
259
- label="密码"
260
- rules={[{ required: true, message: '请输入密码' }]}
261
+ label={t('users.password')}
262
+ rules={[{ required: true, message: t('users.passwordRequired') }]}
261
263
  >
262
- <Input.Password placeholder="请输入密码" />
264
+ <Input.Password placeholder={t('users.passwordPlaceholder')} />
263
265
  </Form.Item>
264
266
  )}
265
- <Form.Item name="role" label="角色" rules={[{ required: true, message: '请选择角色' }]}>
266
- <Select placeholder="请选择角色">
267
- <Select.Option value={Role.SUPER_ADMIN}>超级管理员</Select.Option>
268
- <Select.Option value={Role.CUSTOMER_SERVICE}>客服人员</Select.Option>
269
- <Select.Option value={Role.USER}>普通用户</Select.Option>
267
+ <Form.Item
268
+ name="role"
269
+ label={t('users.role')}
270
+ rules={[{ required: true, message: t('users.roleRequired') }]}
271
+ >
272
+ <Select placeholder={t('users.rolePlaceholder')}>
273
+ <Select.Option value={Role.SUPER_ADMIN}>{t('users.superAdmin')}</Select.Option>
274
+ <Select.Option value={Role.CUSTOMER_SERVICE}>{t('users.customerService')}</Select.Option>
275
+ <Select.Option value={Role.USER}>{t('users.normalUser')}</Select.Option>
270
276
  </Select>
271
277
  </Form.Item>
272
- <Form.Item name="status" label="状态" rules={[{ required: true, message: '请选择状态' }]}>
273
- <Select placeholder="请选择状态">
274
- <Select.Option value="active">正常</Select.Option>
275
- <Select.Option value="inactive">未激活</Select.Option>
276
- <Select.Option value="locked">已锁定</Select.Option>
278
+ <Form.Item
279
+ name="status"
280
+ label={t('users.status')}
281
+ rules={[{ required: true, message: t('users.statusRequired') }]}
282
+ >
283
+ <Select placeholder={t('users.statusPlaceholder')}>
284
+ <Select.Option value="active">{t('users.active')}</Select.Option>
285
+ <Select.Option value="inactive">{t('users.inactive')}</Select.Option>
286
+ <Select.Option value="locked">{t('users.locked')}</Select.Option>
277
287
  </Select>
278
288
  </Form.Item>
279
289
  </Form>
@@ -2,9 +2,9 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'
2
2
  import { render, screen, waitFor } from '@testing-library/react'
3
3
  import userEvent from '@testing-library/user-event'
4
4
  import { RolesPage } from '../RolesPage'
5
- import type { RoleType } from '@shared/modules/role/schemas'
5
+ import type { RoleDataType } from '@shared/modules/role/schemas'
6
6
 
7
- const mockRoles: RoleType[] = [
7
+ const mockRoles: RoleDataType[] = [
8
8
  {
9
9
  id: '1',
10
10
  code: 'super_admin',
@@ -0,0 +1,32 @@
1
+ import { create } from 'zustand'
2
+ import { persist } from 'zustand/middleware'
3
+
4
+ type ThemeMode = 'light' | 'dark'
5
+
6
+ interface ThemeState {
7
+ mode: ThemeMode
8
+ toggleTheme: () => void
9
+ }
10
+
11
+ export const useThemeStore = create<ThemeState>()(
12
+ persist(
13
+ (set, get) => ({
14
+ mode: 'light' as ThemeMode,
15
+ toggleTheme: () => {
16
+ const newMode = get().mode === 'light' ? 'dark' : 'light'
17
+ document.documentElement.setAttribute('data-theme', newMode)
18
+ set({ mode: newMode })
19
+ },
20
+ }),
21
+ {
22
+ name: 'admin-theme',
23
+ onRehydrateStorage: () => {
24
+ return (state) => {
25
+ if (state) {
26
+ document.documentElement.setAttribute('data-theme', state.mode)
27
+ }
28
+ }
29
+ },
30
+ }
31
+ )
32
+ )