@robsun/create-keystone-app 0.2.11 → 0.2.13

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.
@@ -1,532 +1,532 @@
1
- # Keystone 代码模板
2
-
3
- ## 前端模板
4
-
5
- ### Ant Design v6 注意事项
6
- - `Space` 使用 `orientation`,不要用 `direction`。
7
- - `Modal` 使用 `destroyOnHidden`,不要用 `destroyOnClose`。
8
- - `Drawer` 使用 `size`,不要用 `width`。
9
-
10
- ### routes.tsx
11
- ```tsx
12
- import { RouteObject } from 'react-router-dom'
13
- import { registerModule } from '@robsun/keystone-web-core'
14
-
15
- const routes: RouteObject[] = [
16
- {
17
- path: '{name}',
18
- handle: {
19
- menu: { title: '{Title}', icon: 'icon-name' },
20
- permission: '{name}:{resource}:view',
21
- helpKey: '{name}/index',
22
- },
23
- children: [
24
- { index: true, lazy: () => import('./pages/List') },
25
- { path: 'create', lazy: () => import('./pages/Form') },
26
- { path: ':id', lazy: () => import('./pages/Detail') },
27
- { path: ':id/edit', lazy: () => import('./pages/Form') },
28
- ],
29
- },
30
- ]
31
-
32
- registerModule({ name: '{name}', routes })
33
- ```
34
-
35
- ### List.tsx (ProTable)
36
- ```tsx
37
- import { useEffect, useState } from 'react'
38
- import { ProTable } from '@robsun/keystone-web-core'
39
- import type { PaginatedData } from '@robsun/keystone-web-core'
40
- import type { {Entity} } from '../types'
41
- import { list{Entity} } from '../services/api'
42
-
43
- const INITIAL_DATA: PaginatedData<{Entity}> = {
44
- items: [],
45
- total: 0,
46
- page: 1,
47
- page_size: 10,
48
- }
49
-
50
- export function Component() {
51
- const [loading, setLoading] = useState(false)
52
- const [data, setData] = useState(INITIAL_DATA)
53
-
54
- const columns = [
55
- { title: 'ID', dataIndex: 'id' },
56
- { title: '名称', dataIndex: 'name' },
57
- ]
58
-
59
- const fetchData = async (page = data.page, pageSize = data.page_size) => {
60
- setLoading(true)
61
- try {
62
- const result = await list{Entity}({ page, page_size: pageSize })
63
- setData(result)
64
- } finally {
65
- setLoading(false)
66
- }
67
- }
68
-
69
- useEffect(() => {
70
- void fetchData()
71
- }, [])
72
-
73
- return (
74
- <ProTable
75
- columns={columns}
76
- dataSource={data.items}
77
- rowKey="id"
78
- loading={loading}
79
- pagination={{
80
- current: data.page,
81
- pageSize: data.page_size,
82
- total: data.total,
83
- onChange: (page, pageSize) => {
84
- void fetchData(page, pageSize)
85
- },
86
- }}
87
- />
88
- )
89
- }
90
- ```
91
-
92
- ### Form.tsx (ProForm)
93
- ```tsx
94
- import { ProForm } from '@robsun/keystone-web-core'
95
- import { useParams, useNavigate } from 'react-router-dom'
96
- import { create{Entity}, update{Entity} } from '../services/api'
97
-
98
- export function Component() {
99
- const { id: rawId } = useParams()
100
- const navigate = useNavigate()
101
- const id = Number(rawId)
102
- const isEdit = Number.isFinite(id)
103
-
104
- const onSubmit = async (values: any) => {
105
- if (isEdit) {
106
- await update{Entity}(id, values)
107
- } else {
108
- await create{Entity}(values)
109
- }
110
- navigate('/{name}')
111
- }
112
-
113
- return (
114
- <ProForm onFinish={onSubmit}>
115
- {/* 表单字段 */}
116
- </ProForm>
117
- )
118
- }
119
- ```
120
-
121
- ### services/api.ts
122
- ```typescript
123
- import { api, type ApiResponse, type PaginatedData, type PaginationParams } from '@robsun/keystone-web-core'
124
- import type { {Entity} } from '../types'
125
-
126
- // 注意:使用 api 而非 apiClient,响应格式为 { data: { ... } }
127
- export const list{Entity} = async (params: PaginationParams = {}) => {
128
- const { data } = await api.get<ApiResponse<PaginatedData<{Entity}>>>(
129
- '/{module}/{resources}',
130
- { params }
131
- )
132
- return data.data
133
- }
134
-
135
- export const create{Entity} = async (payload: Partial<{Entity}>) => {
136
- const { data } = await api.post<ApiResponse<{Entity}>>('/{module}/{resources}', payload)
137
- return data.data
138
- }
139
-
140
- export const update{Entity} = async (id: number, payload: Partial<{Entity}>) => {
141
- const { data } = await api.patch<ApiResponse<{Entity}>>(`/{module}/{resources}/${id}`, payload)
142
- return data.data
143
- }
144
-
145
- export const delete{Entity} = async (id: number) => {
146
- await api.delete<ApiResponse<{ id: number }>>(`/{module}/{resources}/${id}`)
147
- }
148
- ```
149
-
150
- ### types.ts
151
- ```typescript
152
- export interface {Entity} {
153
- id: string
154
- name: string
155
- createdAt: string
156
- updatedAt: string
157
- }
158
- ```
159
-
160
- ### i18n 翻译文件
161
-
162
- **locales/zh-CN/{module}.json**
163
- ```json
164
- {
165
- "title": "{模块标题}",
166
- "list": {
167
- "title": "{实体}列表",
168
- "empty": "暂无数据",
169
- "columns": {
170
- "name": "名称",
171
- "status": "状态",
172
- "createdAt": "创建时间"
173
- }
174
- },
175
- "form": {
176
- "create": "创建{实体}",
177
- "edit": "编辑{实体}",
178
- "fields": {
179
- "name": "名称",
180
- "description": "描述"
181
- },
182
- "placeholders": {
183
- "name": "请输入名称"
184
- }
185
- },
186
- "actions": {
187
- "create": "新建",
188
- "edit": "编辑",
189
- "delete": "删除",
190
- "save": "保存",
191
- "cancel": "取消"
192
- },
193
- "messages": {
194
- "createSuccess": "创建成功",
195
- "updateSuccess": "更新成功",
196
- "deleteSuccess": "删除成功",
197
- "deleteConfirm": "确定要删除吗?"
198
- }
199
- }
200
- ```
201
-
202
- **locales/en-US/{module}.json**
203
- ```json
204
- {
205
- "title": "{Module Title}",
206
- "list": {
207
- "title": "{Entity} List",
208
- "empty": "No data",
209
- "columns": {
210
- "name": "Name",
211
- "status": "Status",
212
- "createdAt": "Created At"
213
- }
214
- },
215
- "form": {
216
- "create": "Create {Entity}",
217
- "edit": "Edit {Entity}",
218
- "fields": {
219
- "name": "Name",
220
- "description": "Description"
221
- },
222
- "placeholders": {
223
- "name": "Enter name"
224
- }
225
- },
226
- "actions": {
227
- "create": "Create",
228
- "edit": "Edit",
229
- "delete": "Delete",
230
- "save": "Save",
231
- "cancel": "Cancel"
232
- },
233
- "messages": {
234
- "createSuccess": "Created successfully",
235
- "updateSuccess": "Updated successfully",
236
- "deleteSuccess": "Deleted successfully",
237
- "deleteConfirm": "Are you sure to delete?"
238
- }
239
- }
240
- ```
241
-
242
- ### 使用翻译的 List.tsx
243
- ```tsx
244
- import { useTranslation } from 'react-i18next'
245
- import { ProTable } from '@robsun/keystone-web-core'
246
-
247
- export function Component() {
248
- const { t } = useTranslation() // 自动使用当前模块的命名空间
249
-
250
- const columns = [
251
- { title: t('{module}:list.columns.name'), dataIndex: 'name' },
252
- { title: t('{module}:list.columns.status'), dataIndex: 'status' },
253
- { title: t('{module}:list.columns.createdAt'), dataIndex: 'createdAt' },
254
- ]
255
-
256
- return (
257
- <ProTable
258
- columns={columns}
259
- // ...
260
- />
261
- )
262
- }
263
- ```
264
-
265
- ## 后端模板
266
-
267
- ### module.go
268
- ```go
269
- package {name}
270
-
271
- import (
272
- "github.com/gin-gonic/gin"
273
- "gorm.io/gorm"
274
- )
275
-
276
- type Module struct{}
277
-
278
- func (m *Module) Name() string { return "{name}" }
279
-
280
- func (m *Module) RegisterRoutes(r *gin.RouterGroup) {
281
- g := r.Group("/{name}")
282
- g.GET("", handler.List)
283
- g.GET("/:id", handler.Detail)
284
- g.POST("", handler.Create)
285
- g.PATCH("/:id", handler.Update)
286
- g.DELETE("/:id", handler.Delete)
287
- }
288
-
289
- func (m *Module) RegisterPermissions() []Permission {
290
- return []Permission{
291
- {Name: "{name}:{resource}:view", Title: "查看{Title}"},
292
- {Name: "{name}:{resource}:create", Title: "创建{Title}"},
293
- {Name: "{name}:{resource}:update", Title: "编辑{Title}"},
294
- {Name: "{name}:{resource}:delete", Title: "删除{Title}"},
295
- }
296
- }
297
-
298
- func (m *Module) Migrate(db *gorm.DB) error {
299
- return db.AutoMigrate(&models.{Entity}{})
300
- }
301
-
302
- func (m *Module) Seed(db *gorm.DB) error { return nil }
303
- ```
304
-
305
- ### handler/list.go
306
- ```go
307
- package handler
308
-
309
- import (
310
- "github.com/gin-gonic/gin"
311
- )
312
-
313
- func (h *Handler) List(c *gin.Context) {
314
- page, pageSize := parsePagination(c)
315
- items, total, err := h.svc.List(c, page, pageSize)
316
- if err != nil {
317
- respondError(c, err)
318
- return
319
- }
320
- respondPaginated(c, items, total, page, pageSize)
321
- }
322
- ```
323
-
324
- ### domain/models/entity.go
325
- ```go
326
- package models
327
-
328
- import "time"
329
-
330
- type {Entity} struct {
331
- ID string `json:"id" gorm:"primaryKey"`
332
- Name string `json:"name"`
333
- CreatedAt time.Time `json:"createdAt"`
334
- UpdatedAt time.Time `json:"updatedAt"`
335
- }
336
- ```
337
-
338
- ### domain/service/service.go
339
- ```go
340
- package service
341
-
342
- type Service struct {
343
- repo *repository.Repository
344
- }
345
-
346
- func NewService(repo *repository.Repository) *Service {
347
- return &Service{repo: repo}
348
- }
349
-
350
- func (s *Service) List(ctx context.Context, page, pageSize int) ([]models.{Entity}, int64, error) {
351
- return s.repo.List(ctx, page, pageSize)
352
- }
353
-
354
- func (s *Service) Get(ctx context.Context, id string) (*models.{Entity}, error) {
355
- return s.repo.Get(ctx, id)
356
- }
357
-
358
- func (s *Service) Create(ctx context.Context, entity *models.{Entity}) error {
359
- return s.repo.Create(ctx, entity)
360
- }
361
-
362
- func (s *Service) Update(ctx context.Context, id string, entity *models.{Entity}) error {
363
- return s.repo.Update(ctx, id, entity)
364
- }
365
-
366
- func (s *Service) Delete(ctx context.Context, id string) error {
367
- return s.repo.Delete(ctx, id)
368
- }
369
- ```
370
-
371
- ### infra/repository/repo.go
372
- ```go
373
- package repository
374
-
375
- import (
376
- "context"
377
- "gorm.io/gorm"
378
- )
379
-
380
- type Repository struct {
381
- db *gorm.DB
382
- }
383
-
384
- func NewRepository(db *gorm.DB) *Repository {
385
- return &Repository{db: db}
386
- }
387
-
388
- func (r *Repository) List(ctx context.Context, page, pageSize int) ([]models.{Entity}, int64, error) {
389
- var items []models.{Entity}
390
- var total int64
391
-
392
- r.db.Model(&models.{Entity}{}).Count(&total)
393
- r.db.Offset((page - 1) * pageSize).Limit(pageSize).Find(&items)
394
-
395
- return items, total, nil
396
- }
397
-
398
- func (r *Repository) Get(ctx context.Context, id string) (*models.{Entity}, error) {
399
- var item models.{Entity}
400
- if err := r.db.First(&item, "id = ?", id).Error; err != nil {
401
- return nil, err
402
- }
403
- return &item, nil
404
- }
405
-
406
- func (r *Repository) Create(ctx context.Context, entity *models.{Entity}) error {
407
- return r.db.Create(entity).Error
408
- }
409
-
410
- func (r *Repository) Update(ctx context.Context, id string, entity *models.{Entity}) error {
411
- return r.db.Model(&models.{Entity}{}).Where("id = ?", id).Updates(entity).Error
412
- }
413
-
414
- func (r *Repository) Delete(ctx context.Context, id string) error {
415
- return r.db.Delete(&models.{Entity}{}, "id = ?", id).Error
416
- }
417
- ```
418
-
419
- ### i18n/i18n.go
420
- ```go
421
- package i18n
422
-
423
- import (
424
- "embed"
425
- "github.com/robsuncn/keystone/infra/i18n"
426
- )
427
-
428
- //go:embed locales/*.json
429
- var Translations embed.FS
430
-
431
- func init() {
432
- // 注册模块翻译文件
433
- i18n.MustLoadModuleTranslations("{module}", Translations)
434
- }
435
- ```
436
-
437
- ### i18n/keys.go
438
- ```go
439
- package i18n
440
-
441
- // 翻译键常量定义
442
- const (
443
- // 成功消息
444
- KeyItemCreated = "{module}.item.created"
445
- KeyItemUpdated = "{module}.item.updated"
446
- KeyItemDeleted = "{module}.item.deleted"
447
-
448
- // 错误消息
449
- KeyItemNotFound = "{module}.item.notFound"
450
- KeyItemInvalid = "{module}.item.invalid"
451
- KeyItemAlreadyExists = "{module}.item.alreadyExists"
452
-
453
- // 验证消息
454
- KeyNameRequired = "{module}.validation.nameRequired"
455
- KeyNameTooLong = "{module}.validation.nameTooLong"
456
- )
457
- ```
458
-
459
- ### i18n/locales/zh-CN.json
460
- ```json
461
- {
462
- "{module}": {
463
- "item": {
464
- "created": "{实体}创建成功",
465
- "updated": "{实体}更新成功",
466
- "deleted": "{实体}删除成功",
467
- "notFound": "{实体}不存在",
468
- "invalid": "{实体}数据无效",
469
- "alreadyExists": "{实体}已存在"
470
- },
471
- "validation": {
472
- "nameRequired": "名称不能为空",
473
- "nameTooLong": "名称长度不能超过 {{max}} 个字符"
474
- }
475
- }
476
- }
477
- ```
478
-
479
- ### i18n/locales/en-US.json
480
- ```json
481
- {
482
- "{module}": {
483
- "item": {
484
- "created": "{Entity} created successfully",
485
- "updated": "{Entity} updated successfully",
486
- "deleted": "{Entity} deleted successfully",
487
- "notFound": "{Entity} not found",
488
- "invalid": "Invalid {entity} data",
489
- "alreadyExists": "{Entity} already exists"
490
- },
491
- "validation": {
492
- "nameRequired": "Name is required",
493
- "nameTooLong": "Name cannot exceed {{max}} characters"
494
- }
495
- }
496
- }
497
- ```
498
-
499
- ### 使用翻译的 service.go
500
- ```go
501
- package service
502
-
503
- import (
504
- "errors"
505
- "github.com/gin-gonic/gin"
506
- "github.com/robsuncn/keystone/infra/i18n"
507
- modulei18n "app/internal/modules/{module}/i18n"
508
- )
509
-
510
- func (s *Service) Get(c *gin.Context, id string) (*models.{Entity}, error) {
511
- item, err := s.repo.Get(c, id)
512
- if err != nil {
513
- // 使用翻译键返回错误
514
- return nil, errors.New(i18n.T(c, modulei18n.KeyItemNotFound))
515
- }
516
- return item, nil
517
- }
518
-
519
- func (s *Service) Create(c *gin.Context, entity *models.{Entity}) error {
520
- if entity.Name == "" {
521
- return errors.New(i18n.T(c, modulei18n.KeyNameRequired))
522
- }
523
-
524
- err := s.repo.Create(c, entity)
525
- if err != nil {
526
- return err
527
- }
528
-
529
- // 可以在日志或审计中使用翻译消息
530
- return nil
531
- }
532
- ```
1
+ # Keystone 代码模板
2
+
3
+ ## 前端模板
4
+
5
+ ### Ant Design v6 注意事项
6
+ - `Space` 使用 `orientation`,不要用 `direction`。
7
+ - `Modal` 使用 `destroyOnHidden`,不要用 `destroyOnClose`。
8
+ - `Drawer` 使用 `size`,不要用 `width`。
9
+
10
+ ### routes.tsx
11
+ ```tsx
12
+ import { RouteObject } from 'react-router-dom'
13
+ import { registerModule } from '@robsun/keystone-web-core'
14
+
15
+ const routes: RouteObject[] = [
16
+ {
17
+ path: '{name}',
18
+ handle: {
19
+ menu: { title: '{Title}', icon: 'icon-name' },
20
+ permission: '{name}:{resource}:view',
21
+ helpKey: '{name}/index',
22
+ },
23
+ children: [
24
+ { index: true, lazy: () => import('./pages/List') },
25
+ { path: 'create', lazy: () => import('./pages/Form') },
26
+ { path: ':id', lazy: () => import('./pages/Detail') },
27
+ { path: ':id/edit', lazy: () => import('./pages/Form') },
28
+ ],
29
+ },
30
+ ]
31
+
32
+ registerModule({ name: '{name}', routes })
33
+ ```
34
+
35
+ ### List.tsx (ProTable)
36
+ ```tsx
37
+ import { useEffect, useState } from 'react'
38
+ import { ProTable } from '@robsun/keystone-web-core'
39
+ import type { PaginatedData } from '@robsun/keystone-web-core'
40
+ import type { {Entity} } from '../types'
41
+ import { list{Entity} } from '../services/api'
42
+
43
+ const INITIAL_DATA: PaginatedData<{Entity}> = {
44
+ items: [],
45
+ total: 0,
46
+ page: 1,
47
+ page_size: 10,
48
+ }
49
+
50
+ export function Component() {
51
+ const [loading, setLoading] = useState(false)
52
+ const [data, setData] = useState(INITIAL_DATA)
53
+
54
+ const columns = [
55
+ { title: 'ID', dataIndex: 'id' },
56
+ { title: '名称', dataIndex: 'name' },
57
+ ]
58
+
59
+ const fetchData = async (page = data.page, pageSize = data.page_size) => {
60
+ setLoading(true)
61
+ try {
62
+ const result = await list{Entity}({ page, page_size: pageSize })
63
+ setData(result)
64
+ } finally {
65
+ setLoading(false)
66
+ }
67
+ }
68
+
69
+ useEffect(() => {
70
+ void fetchData()
71
+ }, [])
72
+
73
+ return (
74
+ <ProTable
75
+ columns={columns}
76
+ dataSource={data.items}
77
+ rowKey="id"
78
+ loading={loading}
79
+ pagination={{
80
+ current: data.page,
81
+ pageSize: data.page_size,
82
+ total: data.total,
83
+ onChange: (page, pageSize) => {
84
+ void fetchData(page, pageSize)
85
+ },
86
+ }}
87
+ />
88
+ )
89
+ }
90
+ ```
91
+
92
+ ### Form.tsx (ProForm)
93
+ ```tsx
94
+ import { ProForm } from '@robsun/keystone-web-core'
95
+ import { useParams, useNavigate } from 'react-router-dom'
96
+ import { create{Entity}, update{Entity} } from '../services/api'
97
+
98
+ export function Component() {
99
+ const { id: rawId } = useParams()
100
+ const navigate = useNavigate()
101
+ const id = Number(rawId)
102
+ const isEdit = Number.isFinite(id)
103
+
104
+ const onSubmit = async (values: any) => {
105
+ if (isEdit) {
106
+ await update{Entity}(id, values)
107
+ } else {
108
+ await create{Entity}(values)
109
+ }
110
+ navigate('/{name}')
111
+ }
112
+
113
+ return (
114
+ <ProForm onFinish={onSubmit}>
115
+ {/* 表单字段 */}
116
+ </ProForm>
117
+ )
118
+ }
119
+ ```
120
+
121
+ ### services/api.ts
122
+ ```typescript
123
+ import { api, type ApiResponse, type PaginatedData, type PaginationParams } from '@robsun/keystone-web-core'
124
+ import type { {Entity} } from '../types'
125
+
126
+ // 注意:使用 api 而非 apiClient,响应格式为 { data: { ... } }
127
+ export const list{Entity} = async (params: PaginationParams = {}) => {
128
+ const { data } = await api.get<ApiResponse<PaginatedData<{Entity}>>>(
129
+ '/{module}/{resources}',
130
+ { params }
131
+ )
132
+ return data.data
133
+ }
134
+
135
+ export const create{Entity} = async (payload: Partial<{Entity}>) => {
136
+ const { data } = await api.post<ApiResponse<{Entity}>>('/{module}/{resources}', payload)
137
+ return data.data
138
+ }
139
+
140
+ export const update{Entity} = async (id: number, payload: Partial<{Entity}>) => {
141
+ const { data } = await api.patch<ApiResponse<{Entity}>>(`/{module}/{resources}/${id}`, payload)
142
+ return data.data
143
+ }
144
+
145
+ export const delete{Entity} = async (id: number) => {
146
+ await api.delete<ApiResponse<{ id: number }>>(`/{module}/{resources}/${id}`)
147
+ }
148
+ ```
149
+
150
+ ### types.ts
151
+ ```typescript
152
+ export interface {Entity} {
153
+ id: string
154
+ name: string
155
+ createdAt: string
156
+ updatedAt: string
157
+ }
158
+ ```
159
+
160
+ ### i18n 翻译文件
161
+
162
+ **locales/zh-CN/{module}.json**
163
+ ```json
164
+ {
165
+ "title": "{模块标题}",
166
+ "list": {
167
+ "title": "{实体}列表",
168
+ "empty": "暂无数据",
169
+ "columns": {
170
+ "name": "名称",
171
+ "status": "状态",
172
+ "createdAt": "创建时间"
173
+ }
174
+ },
175
+ "form": {
176
+ "create": "创建{实体}",
177
+ "edit": "编辑{实体}",
178
+ "fields": {
179
+ "name": "名称",
180
+ "description": "描述"
181
+ },
182
+ "placeholders": {
183
+ "name": "请输入名称"
184
+ }
185
+ },
186
+ "actions": {
187
+ "create": "新建",
188
+ "edit": "编辑",
189
+ "delete": "删除",
190
+ "save": "保存",
191
+ "cancel": "取消"
192
+ },
193
+ "messages": {
194
+ "createSuccess": "创建成功",
195
+ "updateSuccess": "更新成功",
196
+ "deleteSuccess": "删除成功",
197
+ "deleteConfirm": "确定要删除吗?"
198
+ }
199
+ }
200
+ ```
201
+
202
+ **locales/en-US/{module}.json**
203
+ ```json
204
+ {
205
+ "title": "{Module Title}",
206
+ "list": {
207
+ "title": "{Entity} List",
208
+ "empty": "No data",
209
+ "columns": {
210
+ "name": "Name",
211
+ "status": "Status",
212
+ "createdAt": "Created At"
213
+ }
214
+ },
215
+ "form": {
216
+ "create": "Create {Entity}",
217
+ "edit": "Edit {Entity}",
218
+ "fields": {
219
+ "name": "Name",
220
+ "description": "Description"
221
+ },
222
+ "placeholders": {
223
+ "name": "Enter name"
224
+ }
225
+ },
226
+ "actions": {
227
+ "create": "Create",
228
+ "edit": "Edit",
229
+ "delete": "Delete",
230
+ "save": "Save",
231
+ "cancel": "Cancel"
232
+ },
233
+ "messages": {
234
+ "createSuccess": "Created successfully",
235
+ "updateSuccess": "Updated successfully",
236
+ "deleteSuccess": "Deleted successfully",
237
+ "deleteConfirm": "Are you sure to delete?"
238
+ }
239
+ }
240
+ ```
241
+
242
+ ### 使用翻译的 List.tsx
243
+ ```tsx
244
+ import { useTranslation } from 'react-i18next'
245
+ import { ProTable } from '@robsun/keystone-web-core'
246
+
247
+ export function Component() {
248
+ const { t } = useTranslation() // 自动使用当前模块的命名空间
249
+
250
+ const columns = [
251
+ { title: t('{module}:list.columns.name'), dataIndex: 'name' },
252
+ { title: t('{module}:list.columns.status'), dataIndex: 'status' },
253
+ { title: t('{module}:list.columns.createdAt'), dataIndex: 'createdAt' },
254
+ ]
255
+
256
+ return (
257
+ <ProTable
258
+ columns={columns}
259
+ // ...
260
+ />
261
+ )
262
+ }
263
+ ```
264
+
265
+ ## 后端模板
266
+
267
+ ### module.go
268
+ ```go
269
+ package {name}
270
+
271
+ import (
272
+ "github.com/gin-gonic/gin"
273
+ "gorm.io/gorm"
274
+ )
275
+
276
+ type Module struct{}
277
+
278
+ func (m *Module) Name() string { return "{name}" }
279
+
280
+ func (m *Module) RegisterRoutes(r *gin.RouterGroup) {
281
+ g := r.Group("/{name}")
282
+ g.GET("", handler.List)
283
+ g.GET("/:id", handler.Detail)
284
+ g.POST("", handler.Create)
285
+ g.PATCH("/:id", handler.Update)
286
+ g.DELETE("/:id", handler.Delete)
287
+ }
288
+
289
+ func (m *Module) RegisterPermissions() []Permission {
290
+ return []Permission{
291
+ {Name: "{name}:{resource}:view", Title: "查看{Title}"},
292
+ {Name: "{name}:{resource}:create", Title: "创建{Title}"},
293
+ {Name: "{name}:{resource}:update", Title: "编辑{Title}"},
294
+ {Name: "{name}:{resource}:delete", Title: "删除{Title}"},
295
+ }
296
+ }
297
+
298
+ func (m *Module) Migrate(db *gorm.DB) error {
299
+ return db.AutoMigrate(&models.{Entity}{})
300
+ }
301
+
302
+ func (m *Module) Seed(db *gorm.DB) error { return nil }
303
+ ```
304
+
305
+ ### handler/list.go
306
+ ```go
307
+ package handler
308
+
309
+ import (
310
+ "github.com/gin-gonic/gin"
311
+ )
312
+
313
+ func (h *Handler) List(c *gin.Context) {
314
+ page, pageSize := parsePagination(c)
315
+ items, total, err := h.svc.List(c, page, pageSize)
316
+ if err != nil {
317
+ respondError(c, err)
318
+ return
319
+ }
320
+ respondPaginated(c, items, total, page, pageSize)
321
+ }
322
+ ```
323
+
324
+ ### domain/models/entity.go
325
+ ```go
326
+ package models
327
+
328
+ import "time"
329
+
330
+ type {Entity} struct {
331
+ ID string `json:"id" gorm:"primaryKey"`
332
+ Name string `json:"name"`
333
+ CreatedAt time.Time `json:"createdAt"`
334
+ UpdatedAt time.Time `json:"updatedAt"`
335
+ }
336
+ ```
337
+
338
+ ### domain/service/service.go
339
+ ```go
340
+ package service
341
+
342
+ type Service struct {
343
+ repo *repository.Repository
344
+ }
345
+
346
+ func NewService(repo *repository.Repository) *Service {
347
+ return &Service{repo: repo}
348
+ }
349
+
350
+ func (s *Service) List(ctx context.Context, page, pageSize int) ([]models.{Entity}, int64, error) {
351
+ return s.repo.List(ctx, page, pageSize)
352
+ }
353
+
354
+ func (s *Service) Get(ctx context.Context, id string) (*models.{Entity}, error) {
355
+ return s.repo.Get(ctx, id)
356
+ }
357
+
358
+ func (s *Service) Create(ctx context.Context, entity *models.{Entity}) error {
359
+ return s.repo.Create(ctx, entity)
360
+ }
361
+
362
+ func (s *Service) Update(ctx context.Context, id string, entity *models.{Entity}) error {
363
+ return s.repo.Update(ctx, id, entity)
364
+ }
365
+
366
+ func (s *Service) Delete(ctx context.Context, id string) error {
367
+ return s.repo.Delete(ctx, id)
368
+ }
369
+ ```
370
+
371
+ ### infra/repository/repo.go
372
+ ```go
373
+ package repository
374
+
375
+ import (
376
+ "context"
377
+ "gorm.io/gorm"
378
+ )
379
+
380
+ type Repository struct {
381
+ db *gorm.DB
382
+ }
383
+
384
+ func NewRepository(db *gorm.DB) *Repository {
385
+ return &Repository{db: db}
386
+ }
387
+
388
+ func (r *Repository) List(ctx context.Context, page, pageSize int) ([]models.{Entity}, int64, error) {
389
+ var items []models.{Entity}
390
+ var total int64
391
+
392
+ r.db.Model(&models.{Entity}{}).Count(&total)
393
+ r.db.Offset((page - 1) * pageSize).Limit(pageSize).Find(&items)
394
+
395
+ return items, total, nil
396
+ }
397
+
398
+ func (r *Repository) Get(ctx context.Context, id string) (*models.{Entity}, error) {
399
+ var item models.{Entity}
400
+ if err := r.db.First(&item, "id = ?", id).Error; err != nil {
401
+ return nil, err
402
+ }
403
+ return &item, nil
404
+ }
405
+
406
+ func (r *Repository) Create(ctx context.Context, entity *models.{Entity}) error {
407
+ return r.db.Create(entity).Error
408
+ }
409
+
410
+ func (r *Repository) Update(ctx context.Context, id string, entity *models.{Entity}) error {
411
+ return r.db.Model(&models.{Entity}{}).Where("id = ?", id).Updates(entity).Error
412
+ }
413
+
414
+ func (r *Repository) Delete(ctx context.Context, id string) error {
415
+ return r.db.Delete(&models.{Entity}{}, "id = ?", id).Error
416
+ }
417
+ ```
418
+
419
+ ### i18n/i18n.go
420
+ ```go
421
+ package i18n
422
+
423
+ import (
424
+ "embed"
425
+ "github.com/robsuncn/keystone/infra/i18n"
426
+ )
427
+
428
+ //go:embed locales/*.json
429
+ var Translations embed.FS
430
+
431
+ func init() {
432
+ // 注册模块翻译文件
433
+ i18n.MustLoadModuleTranslations("{module}", Translations)
434
+ }
435
+ ```
436
+
437
+ ### i18n/keys.go
438
+ ```go
439
+ package i18n
440
+
441
+ // 翻译键常量定义
442
+ const (
443
+ // 成功消息
444
+ KeyItemCreated = "{module}.item.created"
445
+ KeyItemUpdated = "{module}.item.updated"
446
+ KeyItemDeleted = "{module}.item.deleted"
447
+
448
+ // 错误消息
449
+ KeyItemNotFound = "{module}.item.notFound"
450
+ KeyItemInvalid = "{module}.item.invalid"
451
+ KeyItemAlreadyExists = "{module}.item.alreadyExists"
452
+
453
+ // 验证消息
454
+ KeyNameRequired = "{module}.validation.nameRequired"
455
+ KeyNameTooLong = "{module}.validation.nameTooLong"
456
+ )
457
+ ```
458
+
459
+ ### i18n/locales/zh-CN.json
460
+ ```json
461
+ {
462
+ "{module}": {
463
+ "item": {
464
+ "created": "{实体}创建成功",
465
+ "updated": "{实体}更新成功",
466
+ "deleted": "{实体}删除成功",
467
+ "notFound": "{实体}不存在",
468
+ "invalid": "{实体}数据无效",
469
+ "alreadyExists": "{实体}已存在"
470
+ },
471
+ "validation": {
472
+ "nameRequired": "名称不能为空",
473
+ "nameTooLong": "名称长度不能超过 {{max}} 个字符"
474
+ }
475
+ }
476
+ }
477
+ ```
478
+
479
+ ### i18n/locales/en-US.json
480
+ ```json
481
+ {
482
+ "{module}": {
483
+ "item": {
484
+ "created": "{Entity} created successfully",
485
+ "updated": "{Entity} updated successfully",
486
+ "deleted": "{Entity} deleted successfully",
487
+ "notFound": "{Entity} not found",
488
+ "invalid": "Invalid {entity} data",
489
+ "alreadyExists": "{Entity} already exists"
490
+ },
491
+ "validation": {
492
+ "nameRequired": "Name is required",
493
+ "nameTooLong": "Name cannot exceed {{max}} characters"
494
+ }
495
+ }
496
+ }
497
+ ```
498
+
499
+ ### 使用翻译的 service.go
500
+ ```go
501
+ package service
502
+
503
+ import (
504
+ "errors"
505
+ "github.com/gin-gonic/gin"
506
+ "github.com/robsuncn/keystone/infra/i18n"
507
+ modulei18n "app/internal/modules/{module}/i18n"
508
+ )
509
+
510
+ func (s *Service) Get(c *gin.Context, id string) (*models.{Entity}, error) {
511
+ item, err := s.repo.Get(c, id)
512
+ if err != nil {
513
+ // 使用翻译键返回错误
514
+ return nil, errors.New(i18n.T(c, modulei18n.KeyItemNotFound))
515
+ }
516
+ return item, nil
517
+ }
518
+
519
+ func (s *Service) Create(c *gin.Context, entity *models.{Entity}) error {
520
+ if entity.Name == "" {
521
+ return errors.New(i18n.T(c, modulei18n.KeyNameRequired))
522
+ }
523
+
524
+ err := s.repo.Create(c, entity)
525
+ if err != nil {
526
+ return err
527
+ }
528
+
529
+ // 可以在日志或审计中使用翻译消息
530
+ return nil
531
+ }
532
+ ```