create-jnrs-vue 1.2.26 → 1.2.28

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,6 +1,6 @@
1
1
  {
2
2
  "name": "jnrs-vue",
3
- "version": "1.2.26",
3
+ "version": "1.2.28",
4
4
  "description": "JNRS 信息化管理系统",
5
5
  "author": "talia_tan",
6
6
  "private": true,
@@ -19,9 +19,9 @@
19
19
  },
20
20
  "dependencies": {
21
21
  "@element-plus/icons-vue": "^2.3.2",
22
- "@jnrs/lingshu-smart": "2.2.7",
23
- "@jnrs/shared": "1.1.20",
24
- "@jnrs/vue-core": "1.2.14",
22
+ "@jnrs/lingshu-smart": "2.2.8",
23
+ "@jnrs/shared": "1.1.23",
24
+ "@jnrs/vue-core": "1.2.15",
25
25
  "@vueuse/core": "^14.1.0",
26
26
  "element-plus": "^2.13.3",
27
27
  "pinia": "^3.0.4",
@@ -27,6 +27,15 @@
27
27
  },
28
28
  "component": "/demos/simpleTable/index"
29
29
  },
30
+ {
31
+ "path": "/compositionTable",
32
+ "name": "compositionTable",
33
+ "meta": {
34
+ "title": "简单数据表格 - 组合式",
35
+ "todoCount": 0
36
+ },
37
+ "component": "/demos/compositionTable/index"
38
+ },
30
39
  {
31
40
  "path": "/crud",
32
41
  "name": "Crud",
@@ -36,6 +45,15 @@
36
45
  },
37
46
  "component": "/demos/crud/index"
38
47
  },
48
+ {
49
+ "path": "/compositionCrud",
50
+ "name": "compositionCrud",
51
+ "meta": {
52
+ "title": "完整增删改查 - 组合式",
53
+ "todoCount": 0
54
+ },
55
+ "component": "/demos/compositionCrud/index"
56
+ },
39
57
  {
40
58
  "path": "/testRedirect",
41
59
  "name": "TestRedirect",
@@ -0,0 +1,159 @@
1
+ /**
2
+ * @Author : TanRui
3
+ * @WeChat : Tan578853789
4
+ * @File : project/ai_agent_context.ts
5
+ * @Date : 2026/03/01
6
+ * @Desc. : 示例模块接口文档(对于标准业务功能可作为 AI Agent 上下文)
7
+ */
8
+
9
+ import type {
10
+ // 列表数据及数据总数
11
+ IPageTableData,
12
+ // 分页
13
+ IPagination,
14
+ // 附件
15
+ IFile
16
+ } from '@/types'
17
+ import {
18
+ // axios 请求方法泛型拓展封装
19
+ axiosRequest
20
+ } from '../request'
21
+ import {
22
+ // 将对象转为 FormData
23
+ objectToFormData
24
+ } from '@/utils'
25
+
26
+ export interface Project extends IFile {
27
+ id: string
28
+ programCode: string
29
+ program: string
30
+ code: string
31
+ name: string
32
+ description: string
33
+ projectType?: '敏捷型' | '瀑布型' | '混合型'
34
+ manager: string
35
+ budget: number
36
+ plannedStartDate: string
37
+ plannedFinishDate: string
38
+ progress: string
39
+ status: '进行中' | '已完成' | '未开始' | '已暂停'
40
+ }
41
+
42
+ type ProjectReadOnly =
43
+ | 'programCode'
44
+ | 'program'
45
+ | 'code'
46
+ | 'manager'
47
+ | 'plannedFinishDate'
48
+ | 'progress'
49
+ | 'status'
50
+ | 'imageDocument'
51
+ | 'attachmentDocument'
52
+
53
+ export type EditProject = Omit<Project, ProjectReadOnly> & {
54
+ newAttachmentFile: []
55
+ newImageFiles: []
56
+ }
57
+
58
+ export interface ProjectQuery extends IPagination {
59
+ code?: string
60
+ projectType?: '敏捷型' | '瀑布型' | '混合型'
61
+ manager?: string
62
+ }
63
+
64
+ /**
65
+ * 项目 - 列表数据
66
+ */
67
+ export const ProjectListApi = (params?: ProjectQuery): Promise<IPageTableData<Project>> => {
68
+ return axiosRequest({
69
+ url: '/project/table',
70
+ method: 'get',
71
+ params
72
+ })
73
+ }
74
+
75
+ /**
76
+ * 项目 - 详情数据
77
+ */
78
+ export const DataApiTest = (params: { id: string }): Promise<Project> => {
79
+ return axiosRequest({
80
+ url: '/project/info',
81
+ method: 'get',
82
+ params
83
+ })
84
+ }
85
+
86
+ /**
87
+ * 项目 - 新增
88
+ */
89
+ export const CreateProjectApi = (data: Omit<Project, 'id'>) => {
90
+ return axiosRequest({
91
+ url: '/project/save',
92
+ method: 'post',
93
+ data
94
+ })
95
+ }
96
+
97
+ /**
98
+ * 项目 - 编辑
99
+ */
100
+ export const EditProjectApi = (data: EditProject) => {
101
+ return axiosRequest({
102
+ url: '/project/save',
103
+ method: 'post',
104
+ data: objectToFormData(data) // 请求体如果为 formData 类型,使用 objectToFormData 方法转换
105
+ })
106
+ }
107
+
108
+ /**
109
+ * 项目 - 更新
110
+ */
111
+ export const UpdateProjectApi = (data: Project) => {
112
+ return axiosRequest({
113
+ url: '/project/update',
114
+ method: 'put',
115
+ data
116
+ })
117
+ }
118
+
119
+ /**
120
+ * 项目 - 删除
121
+ */
122
+ export const DeleteProjectApi = (id: string) => {
123
+ return axiosRequest({
124
+ url: `/project/delete/${id}`,
125
+ method: 'delete'
126
+ })
127
+ }
128
+
129
+ /**
130
+ * 项目 - 导入
131
+ */
132
+ export const ImportDataApi = (data: FormData) => {
133
+ return axiosRequest({
134
+ url: '/project/import',
135
+ method: 'post',
136
+ data
137
+ })
138
+ }
139
+
140
+ /**
141
+ * 项目 - 导出
142
+ */
143
+ export const ExportApi = (data: Record<string, unknown>): Promise<Blob> => {
144
+ return axiosRequest({
145
+ url: '/project/export',
146
+ method: 'post',
147
+ data
148
+ })
149
+ }
150
+
151
+ /**
152
+ * 项目 - 文件下载
153
+ */
154
+ export const DownloadFileApi = (): Promise<Blob> => {
155
+ return axiosRequest({
156
+ url: '/project/files',
157
+ method: 'post'
158
+ })
159
+ }
@@ -3,10 +3,10 @@
3
3
  * @WeChat : Tan578853789
4
4
  * @File : demos/index.ts
5
5
  * @Date : 2026/03/01
6
- * @Desc. : 示例模块接口文档(可作为 AI Agent 上下文来源)
6
+ * @Desc. : 示例模块接口文档(非标业务功能人工编码参考)
7
7
  */
8
8
 
9
- import type { IBusinessResponse } from '@jnrs/shared'
9
+ import type { IFullResponse } from '@jnrs/shared'
10
10
  import type { IPagination, IPageTableData, IFile, IUser } from '@/types'
11
11
  import { axiosRequest } from '../request'
12
12
  import { objectToFormData } from '@/utils'
@@ -15,7 +15,7 @@ import { objectToFormData } from '@/utils'
15
15
  * 项目 - 详情
16
16
  */
17
17
  export interface Project extends IFile {
18
- id: string
18
+ id: number
19
19
  programCode: string
20
20
  program: string
21
21
  code: string
@@ -31,10 +31,9 @@ export interface Project extends IFile {
31
31
  }
32
32
 
33
33
  /**
34
- * 新增项目 - 忽略字段
34
+ * 创建时不能修改的属性
35
35
  */
36
- type CreateProjectOmit =
37
- | 'id'
36
+ type ProjectReadOnly =
38
37
  | 'programCode'
39
38
  | 'program'
40
39
  | 'code'
@@ -48,8 +47,7 @@ type CreateProjectOmit =
48
47
  /**
49
48
  * 项目 - 创建
50
49
  */
51
- export type EditProject = Omit<Project, CreateProjectOmit> & {
52
- id?: string
50
+ export type EditProject = Omit<Project, ProjectReadOnly> & {
53
51
  managerId?: Record<string, unknown> | number
54
52
  newAttachmentFile: []
55
53
  newImageFiles: []
@@ -62,27 +60,24 @@ export interface ProjectQuery extends IPagination {
62
60
  code?: string
63
61
  projectType?: '敏捷型' | '瀑布型' | '混合型'
64
62
  manager?: string
63
+ plannedStartDate?: string
65
64
  }
66
65
 
67
66
  // 测试 获取数据
68
- export const DataApiTest = (id: string): Promise<IUser> => {
67
+ export const DataApiTest = (params: { id: string }): Promise<IUser> => {
69
68
  return axiosRequest({
70
69
  url: '/auth/user-info',
71
70
  method: 'get',
72
- params: {
73
- id
74
- }
71
+ params
75
72
  })
76
73
  }
77
74
 
78
75
  // 测试 获取全量数据
79
- export const FullDataApiTest = (id: string): Promise<IBusinessResponse<IUser>> => {
76
+ export const FullDataApiTest = (params: { id: string }): Promise<IFullResponse<IUser>> => {
80
77
  return axiosRequest({
81
78
  url: '/auth/user-info',
82
79
  method: 'get',
83
- params: {
84
- id
85
- },
80
+ params,
86
81
  returnFullResponse: true // 需要返回完整响应数据时使用该选项
87
82
  })
88
83
  }
@@ -154,7 +149,7 @@ export const UpdateProjectApi = (data: Project) => {
154
149
  /**
155
150
  * 删除数据
156
151
  */
157
- export const DeleteProjectApi = (id: string) => {
152
+ export const DeleteProjectApi = (id: number) => {
158
153
  return axiosRequest({
159
154
  url: `/demos/delete/${id}`,
160
155
  method: 'delete'
@@ -6,7 +6,7 @@
6
6
  * @Desc. : axios 网络请求实例
7
7
  */
8
8
 
9
- import type { IBusinessRequest, IBusinessResponse } from '@jnrs/shared'
9
+ import type { IBusinessRequest, IFullResponse } from '@jnrs/shared'
10
10
  import { createAxiosInstance } from '@jnrs/shared/request'
11
11
  import { useMockStore } from '@jnrs/vue-core/pinia'
12
12
  import { useAuthStore } from '@/stores'
@@ -32,7 +32,7 @@ const axiosInstance = createAxiosInstance({
32
32
  * @param options 请求配置项
33
33
  * @returns Promise<T> 响应数据
34
34
  */
35
- const axiosRequest = <T = IBusinessResponse>(options: IBusinessRequest): Promise<T> => {
35
+ const axiosRequest = <T = IFullResponse>(options: IBusinessRequest): Promise<T> => {
36
36
  if (!axiosInstance) {
37
37
  throw new Error('请先调用 createRequest 初始化 axios 实例')
38
38
  }
@@ -12,7 +12,7 @@ import { getDictLabel, getDictColor } from '@/utils'
12
12
 
13
13
  export interface Props {
14
14
  dictName: string
15
- value: string | number
15
+ value?: string | number
16
16
  /**
17
17
  * 是否使用颜色
18
18
  */
@@ -1,58 +1,177 @@
1
- import { ref, type Ref } from 'vue'
2
- import type { FormInstance } from 'element-plus'
1
+ /**
2
+ * @Author : TanRui
3
+ * @WeChat : Tan578853789
4
+ * @File : useCrud.ts
5
+ * @Date : 2026/04/16
6
+ * @Desc. : 通用 CRUD 组合式 API - 适用于所有业务模块
7
+ */
8
+
9
+ import { ref, nextTick } from 'vue'
10
+ import type { FormInstance, FormRules } from 'element-plus'
3
11
  import { ElMessage, ElMessageBox } from 'element-plus'
4
12
  import { debounce } from '@jnrs/shared/lodash'
5
13
  import { objectMatchAssign } from '@jnrs/shared'
14
+ import { useI18n } from '@/locales'
6
15
  import type { IPagination, IPageTableData } from '@/types'
7
16
 
17
+ /**
18
+ * CRUD 配置选项
19
+ */
8
20
  interface CrudOptions<TItem, TForm, TQuery> {
9
- // 初始值
10
- defaultForm: () => TForm
21
+ // ========== 必需配置 ==========
22
+ /** 表单默认值工厂函数 */
23
+ defaultForm?: () => TForm
24
+ /** 列表查询 API */
25
+ listApi?: (params: Partial<IPagination> & TQuery) => Promise<IPageTableData<TItem> | TItem[]>
26
+ /** 保存 API(新增/编辑) */
27
+ saveApi?: (data: TForm) => Promise<unknown>
28
+
29
+ // ========== 可选配置 ==========
30
+ /** 查询表单默认值工厂函数 */
11
31
  defaultQuery?: () => TQuery
12
- // API
13
- listApi: (params: Partial<IPagination> & TQuery) => Promise<IPageTableData<TItem> | TItem[]>
14
- saveApi: (data: TForm) => Promise<unknown>
32
+ /** 删除 API */
15
33
  deleteApi?: (id: number) => Promise<unknown>
16
- // 可选:是否启用分页,默认 true
17
- pagination?: boolean
18
- // 可选:数据转换
34
+ /** 是否启用分页,默认 true */
35
+ usePagination?: boolean
36
+ /** 初始页码,默认 1 */
37
+ initialPageNo?: number
38
+ /** 初始每页条数,默认 20 */
39
+ initialPageSize?: number
40
+ /** 防抖延迟(毫秒),默认 300 */
41
+ debounceDelay?: number
42
+
43
+ // ========== 数据处理钩子 ==========
44
+ /** 列表项数据转换函数(用于处理附件等字段映射) */
19
45
  transformItem?: (item: TItem) => TItem
46
+ /** 编辑前数据处理(用于 Select 组件等特殊字段回填) */
20
47
  beforeEdit?: (row: TItem, form: TForm) => TForm
48
+ /** 提交前数据处理(用于 Select 组件等特殊字段转换) */
49
+ beforeSubmit?: (form: TForm) => TForm
50
+
51
+ // ========== UI 配置 ==========
52
+ /** 成功提示消息 */
53
+ successMessage?: {
54
+ save?: string
55
+ delete?: string
56
+ }
57
+ /** 删除确认配置 */
58
+ deleteConfirm?: {
59
+ title?: string
60
+ message?: string
61
+ confirmText?: string
62
+ cancelText?: string
63
+ }
21
64
  }
22
65
 
23
- export function useCrud<TItem, TForm, TQuery = object>(options: CrudOptions<TItem, TForm, TQuery>) {
66
+ /**
67
+ * 通用 CRUD 组合式 API
68
+ *
69
+ * 适用于所有具有增删改查功能的业务模块,提供统一的表格管理、表单编辑、删除确认等功能。
70
+ *
71
+ * @example
72
+ * ```typescript
73
+ * // 基础用法
74
+ * const {
75
+ * loading, tableData, total, pagination, queryForm,
76
+ * dialogRef, formRef, form,
77
+ * getList, openCreate, openEdit, submitForm, handleDelete
78
+ * } = useCrud<TItem, TForm, TQuery>({
79
+ * defaultForm: () => ({ id: '', name: '' }),
80
+ * listApi: YourListApi,
81
+ * saveApi: YourSaveApi,
82
+ * deleteApi: YourDeleteApi
83
+ * })
84
+ *
85
+ * // 高级用法:带数据转换和钩子
86
+ * const { ... } = useCrud<TItem, TForm, TQuery>({
87
+ * defaultForm: () => ({ ... }),
88
+ * defaultQuery: () => ({ code: '', status: undefined }),
89
+ * listApi: DeviceListApi,
90
+ * saveApi: SaveDeviceApi,
91
+ * deleteApi: DeleteDeviceApi,
92
+ *
93
+ * // 数据转换:处理附件字段
94
+ * transformItem: (item) => ({
95
+ * ...item,
96
+ * images: item.imageDocument?.attachments
97
+ * }),
98
+ *
99
+ * // 编辑前:Select 组件回填
100
+ * beforeEdit: (row, form) => ({
101
+ * ...form,
102
+ * categoryId: { id: row.categoryId, name: row.categoryName }
103
+ * }),
104
+ *
105
+ * // 提交前:Select 组件转换
106
+ * beforeSubmit: (form) => ({
107
+ * ...form,
108
+ * categoryId: extractFieldId(form.categoryId, 'id')
109
+ * })
110
+ * })
111
+ * ```
112
+ */
113
+ export function useCrud<TItem, TForm, TQuery>(options: CrudOptions<TItem, TForm, TQuery>) {
114
+ const { t: $t } = useI18n()
115
+
24
116
  const {
25
- defaultForm,
26
- defaultQuery,
117
+ defaultForm = () => ({}) as TForm,
118
+ defaultQuery = () => ({}) as TQuery,
27
119
  listApi,
28
120
  saveApi,
29
121
  deleteApi,
30
- pagination: enablePagination = true,
122
+ usePagination = true,
123
+ initialPageNo = 1,
124
+ initialPageSize = 20,
125
+ debounceDelay = 300,
31
126
  transformItem,
32
- beforeEdit
127
+ beforeEdit,
128
+ beforeSubmit,
129
+ successMessage = {
130
+ save: '数据已保存',
131
+ delete: '删除成功'
132
+ },
133
+ deleteConfirm = {
134
+ title: '确定要删除吗?',
135
+ message: '<p style="color: #f30">请注意,您尚未保存的数据将会丢失。</p>',
136
+ confirmText: $t('global.action.confirm'),
137
+ cancelText: $t('global.action.cancel')
138
+ }
33
139
  } = options
34
140
 
35
- // 列表状态
141
+ // ==================== 列表状态 ====================
36
142
  const loading = ref(false)
37
- const tableData = ref<TItem[]>([]) as Ref<TItem[]>
143
+ const tableData = ref<TItem[]>([])
38
144
  const total = ref(0)
39
- const pagination = ref<IPagination>({ pageNo: 1, pageSize: 20 })
40
- const queryForm = ref(defaultQuery?.() ?? {}) as Ref<TQuery>
145
+ const pagination = ref<IPagination>({
146
+ pageNo: initialPageNo,
147
+ pageSize: initialPageSize
148
+ })
149
+ const queryForm = ref<TQuery>(defaultQuery?.())
41
150
 
42
- // 表单状态
151
+ // ==================== 表单状态 ====================
43
152
  const dialogRef = ref()
44
153
  const formRef = ref<FormInstance>()
45
- const form = ref(defaultForm()) as Ref<TForm>
154
+ const form = ref(defaultForm())
155
+
156
+ // ==================== 核心方法 ====================
46
157
 
47
- // 获取列表
158
+ /**
159
+ * 获取列表数据(带防抖)
160
+ */
48
161
  const getList = debounce(async () => {
162
+ if (!listApi) {
163
+ return
164
+ }
165
+
49
166
  loading.value = true
50
167
  try {
51
168
  const params = {
52
- ...(enablePagination ? pagination.value : {}),
169
+ ...(usePagination ? pagination.value : {}),
53
170
  ...queryForm.value
54
171
  } as Partial<IPagination> & TQuery
172
+
55
173
  const res = await listApi(params)
174
+
56
175
  // 支持返回数组或 { list, count } 格式
57
176
  if (Array.isArray(res)) {
58
177
  tableData.value = transformItem ? res.map(transformItem) : res
@@ -61,78 +180,128 @@ export function useCrud<TItem, TForm, TQuery = object>(options: CrudOptions<TIte
61
180
  tableData.value = transformItem ? res.list.map(transformItem) : res.list
62
181
  total.value = res.count ?? res.list.length
63
182
  }
64
- } catch (e) {
65
- console.error(e)
183
+ } catch (error) {
184
+ console.error('获取列表失败:', error)
66
185
  } finally {
67
186
  loading.value = false
68
187
  }
69
- }, 300)
188
+ }, debounceDelay)
70
189
 
71
- // 打开新增
190
+ /**
191
+ * 打开新增弹窗
192
+ */
72
193
  const openCreate = () => {
73
194
  dialogRef.value?.open()
74
- queueMicrotask(() => {
195
+ nextTick(() => {
75
196
  formRef.value?.resetFields()
76
- form.value = defaultForm() as TForm
197
+ form.value = defaultForm()
77
198
  })
78
199
  }
79
200
 
80
- // 打开编辑
201
+ /**
202
+ * 打开编辑弹窗
203
+ * @param row 要编辑的行数据
204
+ */
81
205
  const openEdit = (row: TItem) => {
82
206
  dialogRef.value?.open()
83
- queueMicrotask(() => {
207
+ nextTick(() => {
84
208
  formRef.value?.resetFields()
85
- const matched = objectMatchAssign(
86
- defaultForm() as Record<string, unknown>,
87
- row as Record<string, unknown>
88
- ) as TForm
89
- form.value = (beforeEdit ? beforeEdit(row, matched) : matched) as TForm
209
+ const matched = objectMatchAssign(defaultForm(), row)
210
+ form.value = beforeEdit ? beforeEdit(row, matched) : matched
90
211
  })
91
212
  }
92
213
 
93
- // 提交表单
94
- const submitForm = async () => {
95
- const valid = await formRef.value?.validate().catch(() => false)
96
- if (!valid) return
214
+ /**
215
+ * 提交表单(新增或编辑)
216
+ * @param rules 表单验证规则(可选)
217
+ */
218
+ const submitForm = async (rules?: FormRules) => {
219
+ // 如果有传入 rules,则进行表单验证
220
+ if (rules && formRef.value) {
221
+ const valid = await formRef.value.validate().catch(() => false)
222
+ if (!valid) return
223
+ }
97
224
 
98
225
  loading.value = true
99
226
  try {
100
- await saveApi(form.value)
101
- ElMessage.success('保存成功')
227
+ // 提交前数据处理
228
+ const submitData = beforeSubmit ? beforeSubmit(form.value) : form.value
229
+
230
+ await saveApi?.(submitData)
231
+
232
+ ElMessage({
233
+ message: successMessage.save,
234
+ grouping: true,
235
+ showClose: true,
236
+ type: 'success'
237
+ })
238
+
102
239
  dialogRef.value?.close()
103
240
  getList()
104
- } catch (e) {
105
- console.error(e)
241
+ } catch (error) {
242
+ console.error('保存失败:', error)
106
243
  } finally {
107
244
  loading.value = false
108
245
  }
109
246
  }
110
247
 
111
- // 删除
248
+ /**
249
+ * 删除数据
250
+ * @param id 要删除的数据 ID
251
+ */
112
252
  const handleDelete = async (id: number) => {
113
- if (!deleteApi) return
253
+ if (!deleteApi) {
254
+ console.warn('未配置 deleteApi')
255
+ return
256
+ }
114
257
 
115
258
  try {
116
- await ElMessageBox.confirm('确定要删除吗?', '操作确认', {
117
- confirmButtonText: '删除',
118
- cancelButtonText: '取消',
119
- type: 'warning'
259
+ await ElMessageBox.confirm(deleteConfirm.message, deleteConfirm.title, {
260
+ dangerouslyUseHTMLString: true,
261
+ confirmButtonText: deleteConfirm.confirmText,
262
+ cancelButtonText: deleteConfirm.cancelText,
263
+ confirmButtonType: 'danger'
120
264
  })
121
265
 
122
266
  loading.value = true
123
267
  await deleteApi(id)
124
- ElMessage.success('删除成功')
268
+
269
+ ElMessage({
270
+ message: successMessage.delete,
271
+ grouping: true,
272
+ showClose: true,
273
+ type: 'success'
274
+ })
275
+
125
276
  getList()
126
- } catch (e) {
127
- if (e !== 'cancel') {
128
- console.error(e)
277
+ } catch (error) {
278
+ if (error !== 'cancel') {
279
+ console.error('删除失败:', error)
129
280
  }
130
281
  } finally {
131
282
  loading.value = false
132
283
  }
133
284
  }
134
285
 
286
+ /**
287
+ * 处理表格选择变化
288
+ */
289
+ const handleSelectionChange = (rows: TItem[]) => {
290
+ console.log('选中的行:', rows)
291
+ }
292
+
293
+ /**
294
+ * 重置查询条件
295
+ */
296
+ const resetQuery = () => {
297
+ queryForm.value = defaultQuery?.()
298
+ pagination.value.pageNo = initialPageNo
299
+ getList()
300
+ }
301
+
302
+ // ==================== 返回 ====================
135
303
  return {
304
+ // 状态
136
305
  loading,
137
306
  tableData,
138
307
  total,
@@ -141,10 +310,14 @@ export function useCrud<TItem, TForm, TQuery = object>(options: CrudOptions<TIte
141
310
  dialogRef,
142
311
  formRef,
143
312
  form,
313
+
314
+ // 方法
144
315
  getList,
145
316
  openCreate,
146
317
  openEdit,
147
318
  submitForm,
148
- handleDelete
319
+ handleDelete,
320
+ handleSelectionChange,
321
+ resetQuery
149
322
  }
150
323
  }
@@ -17,7 +17,7 @@ import { FileApi } from '@/api/common'
17
17
  */
18
18
  export const objectToFormData = (obj: object): FormData => {
19
19
  // 根据后端返回结果处理的映射关系
20
- const mapConfig = {
20
+ const transMap = {
21
21
  // 图片
22
22
  newImageFiles: { finallyKey: 'originImageNames', valueKey: 'uniqueFileName' },
23
23
  // 通用附件
@@ -27,7 +27,7 @@ export const objectToFormData = (obj: object): FormData => {
27
27
  // 数据库
28
28
  databaseFile: { finallyKey: 'databaseFile', valueKey: 'uniqueFileName' }
29
29
  }
30
- return _objectToFormData(obj as Record<string, unknown>, mapConfig)
30
+ return _objectToFormData(obj as Record<string, unknown>, transMap)
31
31
  }
32
32
 
33
33
  /**
@@ -0,0 +1,350 @@
1
+ <script setup lang="ts">
2
+ import type { FormRules } from 'element-plus'
3
+ import { onActivated, ref } from 'vue'
4
+ import { Plus } from '@element-plus/icons-vue'
5
+ import { useRoute } from '@jnrs/vue-core/router'
6
+ import { dateFormatsToObject, verifyNumberGtZero } from '@jnrs/shared'
7
+ import { getDictList, downloadFile, extractFieldId } from '@/utils'
8
+ import {
9
+ ProjectListApi,
10
+ EditProjectApi,
11
+ DeleteProjectApi,
12
+ DownloadTemplateApi,
13
+ ImportDataApi,
14
+ ExportApi
15
+ } from '@/api/demos/index'
16
+ import type { Project, EditProject, ProjectQuery } from '@/api/demos/index'
17
+ import { JnDialog, JnDatetime, JnPagination, JnFileUpload, JnTable, JnImportAndExport } from '@jnrs/vue-core/components'
18
+ import ImageView from '@/components/common/ImageView.vue'
19
+ import PdfView from '@/components/common/PdfView.vue'
20
+ import DictTag from '@/components/common/DictTag.vue'
21
+ import SelectManager from '@/components/select/SelectManager.vue'
22
+ import { useCrud } from '@/composables/useCrud'
23
+
24
+ const route = useRoute()
25
+
26
+ // 使用通用 CRUD 组合式 API(让 TypeScript 自动推断类型)
27
+ const {
28
+ loading,
29
+ tableData,
30
+ total,
31
+ pagination,
32
+ queryForm,
33
+ formRef,
34
+ dialogRef,
35
+ form,
36
+ getList,
37
+ openCreate,
38
+ openEdit,
39
+ handleDelete: crudHandleDelete,
40
+ submitForm: crudSubmitForm,
41
+ handleSelectionChange
42
+ } = useCrud<Project, EditProject, ProjectQuery>({
43
+ // 表单默认值
44
+ defaultForm: () => ({
45
+ id: 0,
46
+ name: '',
47
+ projectType: undefined,
48
+ managerId: undefined,
49
+ budget: 0,
50
+ plannedStartDate: '',
51
+ description: '',
52
+ newImageFiles: [],
53
+ newAttachmentFile: []
54
+ }),
55
+
56
+ // 查询表单默认值
57
+ defaultQuery: () => ({
58
+ pageNo: 1,
59
+ pageSize: 20,
60
+ code: '',
61
+ projectType: undefined,
62
+ manager: '',
63
+ plannedStartDate: ''
64
+ }),
65
+
66
+ // API
67
+ listApi: ProjectListApi,
68
+ saveApi: EditProjectApi,
69
+ deleteApi: DeleteProjectApi,
70
+
71
+ // 数据转换:处理附件字段映射
72
+ transformItem: (item: Project) => ({
73
+ ...item,
74
+ newImageFiles: item.imageDocument?.attachments,
75
+ newAttachmentFile: item.attachmentDocument?.attachments
76
+ }),
77
+
78
+ // 编辑前处理:Select 组件数据回填
79
+ beforeEdit: (row: Project, form: EditProject) => {
80
+ if ('managerId' in row && 'manager' in row) {
81
+ return {
82
+ ...form,
83
+ managerId: {
84
+ managerId: row.managerId,
85
+ manager: row.manager
86
+ }
87
+ }
88
+ }
89
+ return form
90
+ },
91
+
92
+ // 提交前处理:Select 组件值转换
93
+ beforeSubmit: (form: EditProject) => ({
94
+ ...form,
95
+ managerId: extractFieldId(form.managerId, 'managerId') as number
96
+ })
97
+ })
98
+
99
+ // 表单验证规则
100
+ const rules = ref<FormRules>({
101
+ name: [{ required: true, message: '请输入', trigger: 'change' }],
102
+ projectType: [{ required: true, message: '请选择', trigger: 'change' }],
103
+ plannedStartDate: [{ required: true, message: '请选择', trigger: 'change' }],
104
+ newImageFiles: [{ required: true, message: '请上传', trigger: 'change' }],
105
+ managerId: [{ required: true, message: '请选择', trigger: 'change' }],
106
+ budget: [
107
+ { required: true, message: '请输入', trigger: 'change' },
108
+ {
109
+ validator: verifyNumberGtZero,
110
+ trigger: 'change'
111
+ }
112
+ ]
113
+ })
114
+
115
+ // 包装删除方法,传递正确的 ID 类型
116
+ const handleDelete = (row: Project) => {
117
+ crudHandleDelete(row.id)
118
+ }
119
+
120
+ // 包装提交方法,传入验证规则
121
+ const submitForm = () => {
122
+ crudSubmitForm(rules.value)
123
+ }
124
+
125
+ onActivated(() => {
126
+ console.log(route.meta) // 获取路由元信息
127
+ getList()
128
+ })
129
+ </script>
130
+
131
+ <template>
132
+ <!-- 编辑弹窗 -->
133
+ <JnDialog ref="dialogRef" title="项目管理" width="600px" :close-on-click-modal="false" :close-on-press-escape="true">
134
+ <el-form ref="formRef" :model="form" :rules="rules" label-width="auto" v-loading="loading">
135
+ <el-form-item prop="id"></el-form-item>
136
+ <el-form-item label="项目名称" prop="name">
137
+ <el-input v-model.trim="form.name" maxlength="20" show-word-limit />
138
+ </el-form-item>
139
+ <el-form-item label="类型" prop="projectType">
140
+ <el-select v-model="form.projectType" filterable placeholder="">
141
+ <el-option
142
+ :label="item.label"
143
+ :value="item.value"
144
+ v-for="item in getDictList('projectType')"
145
+ :key="item.value"
146
+ />
147
+ </el-select>
148
+ </el-form-item>
149
+ <el-form-item label="项目经理" prop="managerId">
150
+ <SelectManager
151
+ v-model="form.managerId"
152
+ :formRef="formRef"
153
+ validateFieldName="managerId"
154
+ :limit="1"
155
+ :simpleValue="false"
156
+ ></SelectManager>
157
+ </el-form-item>
158
+ <el-form-item label="预算" prop="budget">
159
+ <el-input-number v-model="form.budget" :min="0" :precision="2" :step="10000" controls-position="right">
160
+ <template #prefix>
161
+ <span>¥</span>
162
+ </template>
163
+ </el-input-number>
164
+ </el-form-item>
165
+ <el-form-item label="计划开始时间" prop="plannedStartDate">
166
+ <el-date-picker
167
+ v-model="form.plannedStartDate"
168
+ type="datetime"
169
+ format="YYYY-MM-DD HH:mm:ss"
170
+ value-format="YYYY-MM-DD HH:mm:ss"
171
+ :disabled-date="
172
+ (time: Date) => {
173
+ return time.getTime() < Date.now() - 86400000
174
+ }
175
+ "
176
+ />
177
+ </el-form-item>
178
+ <el-form-item label="描述" prop="description">
179
+ <el-input
180
+ v-model="form.description"
181
+ :autosize="{ minRows: 2, maxRows: 4 }"
182
+ type="textarea"
183
+ maxlength="200"
184
+ show-word-limit
185
+ />
186
+ </el-form-item>
187
+ <el-form-item label="上传图片" prop="newImageFiles">
188
+ <JnFileUpload
189
+ v-model="form.newImageFiles"
190
+ :formRef="formRef"
191
+ validateFieldName="newImageFiles"
192
+ accept=".png,.jpg,.bmp,.gif"
193
+ :fileSizeMb="50"
194
+ :limit="3"
195
+ drag
196
+ :downloadFileFn="(file) => downloadFile(file.uniqueFileName, file.fileName)"
197
+ />
198
+ </el-form-item>
199
+ <el-form-item label="上传文件" prop="newAttachmentFile">
200
+ <JnFileUpload v-model="form.newAttachmentFile" accept=".pdf" :limit="1" />
201
+ </el-form-item>
202
+ </el-form>
203
+ <template #footer>
204
+ <el-button type="success" icon="Select" :loading="loading" @click="submitForm()">提交</el-button>
205
+ </template>
206
+ </JnDialog>
207
+
208
+ <el-card v-loading="loading">
209
+ <template #header>
210
+ <div style="display: flex; justify-content: space-between; align-items: center">
211
+ <span>项目管理 - 组合式</span>
212
+ <div style="display: flex; justify-content: space-between; align-items: center">
213
+ <JnImportAndExport
214
+ :importTemplateApi="DownloadTemplateApi"
215
+ importBtnName="导入项目"
216
+ :importApi="ImportDataApi"
217
+ exportBtnName="导出项目"
218
+ :exportApi="ExportApi"
219
+ :exportParams="{
220
+ ...dateFormatsToObject(queryForm.plannedStartDate || '')
221
+ }"
222
+ :exportDynamicParamsConfig="{
223
+ label: '项目编号',
224
+ prop: 'code'
225
+ }"
226
+ :exportDisabled="tableData.length === 0"
227
+ size="small"
228
+ @change="getList()"
229
+ />
230
+ <el-button type="primary" size="small" :icon="Plus" @click="openCreate()" style="margin-left: 12px">
231
+ 新增
232
+ </el-button>
233
+ </div>
234
+ </div>
235
+ </template>
236
+
237
+ <!-- 查询条件 -->
238
+ <el-form :model="queryForm" size="small" inline v-loading="loading">
239
+ <el-form-item label="项目编号" prop="code">
240
+ <el-input v-model="queryForm.code" clearable style="width: 200px">
241
+ <template #append>
242
+ <el-button icon="Search" @click="getList()" />
243
+ </template>
244
+ </el-input>
245
+ </el-form-item>
246
+ <el-form-item label="项目名称" prop="projectType">
247
+ <el-select
248
+ v-model="queryForm.projectType"
249
+ filterable
250
+ placeholder=""
251
+ clearable
252
+ style="width: 200px"
253
+ @change="getList()"
254
+ >
255
+ <el-option
256
+ :label="item.label"
257
+ :value="item.value"
258
+ v-for="item in getDictList('projectType')"
259
+ :key="item.value"
260
+ />
261
+ </el-select>
262
+ </el-form-item>
263
+ <el-form-item label="项目经理" prop="manager">
264
+ <SelectManager
265
+ v-model="queryForm.manager"
266
+ :initialParams="{ role: 1 }"
267
+ size="small"
268
+ width="200px"
269
+ @change="getList()"
270
+ ></SelectManager>
271
+ </el-form-item>
272
+ <el-form-item label="计划开始时间" prop="plannedStartDate">
273
+ <el-date-picker
274
+ v-model="queryForm.plannedStartDate"
275
+ value-format="YYYY-MM-DD"
276
+ size="small"
277
+ style="width: 200px"
278
+ @change="getList()"
279
+ />
280
+ </el-form-item>
281
+ </el-form>
282
+
283
+ <!-- 数据列表 -->
284
+ <JnTable
285
+ :data="tableData"
286
+ :pagination="pagination"
287
+ :autoHeight="true"
288
+ :showScrollbar="true"
289
+ :showIndexColumn="true"
290
+ :showSelectionColumn="true"
291
+ :showMouseSelection="true"
292
+ @selection-change="handleSelectionChange"
293
+ >
294
+ <el-table-column prop="code" label="项目编号" min-width="200" sortable show-overflow-tooltip />
295
+ <el-table-column prop="name" label="项目名称" min-width="200" sortable show-overflow-tooltip />
296
+ <el-table-column prop="program" label="项目集名称" min-width="200" sortable show-overflow-tooltip />
297
+ <el-table-column prop="projectType" label="项目类型" width="100" align="center" sortable>
298
+ <template #default="{ row }">
299
+ <DictTag dictName="projectType" :value="row.projectType" />
300
+ </template>
301
+ </el-table-column>
302
+ <el-table-column prop="manager" label="项目经理" width="100" align="center" sortable />
303
+ <el-table-column prop="budget" label="预算(¥)" width="100" align="center" sortable>
304
+ <template #default="{ row }">
305
+ {{ row.budget }}
306
+ </template>
307
+ </el-table-column>
308
+ <el-table-column prop="plannedStartDate" label="计划开始时间" width="130" align="center" sortable>
309
+ <template #default="{ row }">
310
+ <JnDatetime :value="row.plannedStartDate" />
311
+ </template>
312
+ </el-table-column>
313
+ <el-table-column prop="plannedFinishDate" label="计划完成日期" width="130" align="center" sortable>
314
+ <template #default="{ row }">
315
+ {{ row.plannedFinishDate }}
316
+ </template>
317
+ </el-table-column>
318
+ <el-table-column prop="progress" label="进度" width="100" align="center" sortable>
319
+ <template #default="{ row }">
320
+ <span>{{ row.progress }}</span>
321
+ </template>
322
+ </el-table-column>
323
+ <el-table-column prop="status" label="状态" width="100" align="center" sortable>
324
+ <template #default="{ row }">
325
+ <DictTag dictName="status" :value="row.status" :showColor="false" />
326
+ </template>
327
+ </el-table-column>
328
+ <el-table-column prop="description" label="描述" min-width="200" show-overflow-tooltip />
329
+ <el-table-column prop="imageDocument" label="图片" width="100" align="center" sortable>
330
+ <template #default="{ row }">
331
+ <ImageView :loadKeys="row.newImageFiles" preview maxHeight="50px" />
332
+ </template>
333
+ </el-table-column>
334
+ <el-table-column prop="attachmentDocument" label="附件" width="100" align="center" sortable>
335
+ <template #default="{ row }">
336
+ <PdfView :loadKeys="row.newAttachmentFile" isPdf />
337
+ </template>
338
+ </el-table-column>
339
+ <el-table-column label="操作" width="120" align="center" fixed="right">
340
+ <template #default="{ row }">
341
+ <el-button link type="primary" @click.stop="openEdit(row)">编辑</el-button>
342
+ <el-button link type="danger" @click.stop="handleDelete(row)">删除</el-button>
343
+ </template>
344
+ </el-table-column>
345
+ </JnTable>
346
+
347
+ <!-- 表格分页 -->
348
+ <JnPagination :total="total" v-model="pagination" @change="getList" />
349
+ </el-card>
350
+ </template>
@@ -0,0 +1,85 @@
1
+ <script setup lang="ts">
2
+ import { onActivated } from 'vue'
3
+ import { ProjectListApi } from '@/api/demos/index'
4
+ import { JnDatetime, JnPagination, JnTable } from '@jnrs/vue-core/components'
5
+ import ImageView from '@/components/common/ImageView.vue'
6
+ import PdfView from '@/components/common/PdfView.vue'
7
+ import DictTag from '@/components/common/DictTag.vue'
8
+ import { useCrud } from '@/composables/useCrud'
9
+
10
+ const { loading, tableData, total, pagination, getList } = useCrud({
11
+ listApi: ProjectListApi
12
+ })
13
+
14
+ onActivated(() => {
15
+ getList()
16
+ })
17
+ </script>
18
+
19
+ <template>
20
+ <el-card v-loading="loading">
21
+ <template #header>
22
+ <div style="display: flex; justify-content: space-between; align-items: center">
23
+ <span>项目列表 - 组合式</span>
24
+ </div>
25
+ </template>
26
+
27
+ <!-- 数据列表 -->
28
+ <JnTable
29
+ :data="tableData"
30
+ :pagination="pagination"
31
+ :autoHeight="true"
32
+ :showScrollbar="true"
33
+ :showIndexColumn="true"
34
+ >
35
+ <el-table-column prop="code" label="项目编号" min-width="200" sortable show-overflow-tooltip />
36
+ <el-table-column prop="name" label="项目名称" min-width="200" sortable show-overflow-tooltip />
37
+ <el-table-column prop="program" label="项目集名称" min-width="200" sortable show-overflow-tooltip />
38
+ <el-table-column prop="projectType" label="项目类型" width="100" align="center" sortable>
39
+ <template #default="{ row }">
40
+ <DictTag dictName="projectType" :value="row.projectType" />
41
+ </template>
42
+ </el-table-column>
43
+ <el-table-column prop="manager" label="项目经理" width="100" align="center" sortable />
44
+ <el-table-column prop="budget" label="预算(¥)" width="100" align="center" sortable>
45
+ <template #default="{ row }">
46
+ {{ row.budget }}
47
+ </template>
48
+ </el-table-column>
49
+ <el-table-column prop="plannedStartDate" label="计划开始时间" width="130" align="center" sortable>
50
+ <template #default="{ row }">
51
+ <JnDatetime :value="row.plannedStartDate" />
52
+ </template>
53
+ </el-table-column>
54
+ <el-table-column prop="plannedFinishDate" label="计划完成日期" width="130" align="center" sortable>
55
+ <template #default="{ row }">
56
+ {{ row.plannedFinishDate }}
57
+ </template>
58
+ </el-table-column>
59
+ <el-table-column prop="progress" label="进度" width="100" align="center" sortable>
60
+ <template #default="{ row }">
61
+ <span>{{ row.progress }}</span>
62
+ </template>
63
+ </el-table-column>
64
+ <el-table-column prop="status" label="状态" width="100" align="center" sortable>
65
+ <template #default="{ row }">
66
+ <DictTag dictName="status" :value="row.status" :showColor="false" />
67
+ </template>
68
+ </el-table-column>
69
+ <el-table-column prop="description" label="描述" min-width="200" show-overflow-tooltip />
70
+ <el-table-column prop="imageDocument" label="图片" width="100" align="center" sortable>
71
+ <template #default="{ row }">
72
+ <ImageView :loadKeys="row.newImageFiles" preview maxHeight="50px" />
73
+ </template>
74
+ </el-table-column>
75
+ <el-table-column prop="attachmentDocument" label="附件" width="100" align="center" sortable>
76
+ <template #default="{ row }">
77
+ <PdfView :loadKeys="row.newAttachmentFile" isPdf />
78
+ </template>
79
+ </el-table-column>
80
+ </JnTable>
81
+
82
+ <!-- 表格分页 -->
83
+ <JnPagination :total="total" v-model="pagination" @change="getList" />
84
+ </el-card>
85
+ </template>
@@ -6,7 +6,7 @@ import { ref, onActivated, nextTick } from 'vue'
6
6
  import { ElMessage, ElMessageBox } from 'element-plus'
7
7
  import { Plus } from '@element-plus/icons-vue'
8
8
  import { useRoute } from '@jnrs/vue-core/router'
9
- import { objectMatchAssign, dateFormatsToObject, isNumberGtZero } from '@jnrs/shared'
9
+ import { objectMatchAssign, dateFormatsToObject, verifyNumberGtZero } from '@jnrs/shared'
10
10
  import { debounce } from '@jnrs/shared/lodash'
11
11
  import { getDictList, downloadFile, extractFieldId } from '@/utils'
12
12
  import { useI18n } from '@/locales'
@@ -40,7 +40,7 @@ const route = useRoute()
40
40
  const editDialogRef = ref()
41
41
  const ruleFormRef = ref<FormInstance>()
42
42
  const ruleForm = ref<EditProject>({
43
- id: '',
43
+ id: 0,
44
44
  name: '',
45
45
  projectType: undefined,
46
46
  managerId: undefined,
@@ -59,7 +59,7 @@ const rules = ref<FormRules>({
59
59
  budget: [
60
60
  { required: true, message: '请输入', trigger: 'change' },
61
61
  {
62
- validator: isNumberGtZero,
62
+ validator: verifyNumberGtZero,
63
63
  trigger: 'change'
64
64
  }
65
65
  ]
@@ -223,8 +223,8 @@ onActivated(() => {
223
223
  format="YYYY-MM-DD HH:mm:ss"
224
224
  value-format="YYYY-MM-DD HH:mm:ss"
225
225
  :disabled-date="
226
- (time: number) => {
227
- return time < Date.now() - 86400000
226
+ (time: Date) => {
227
+ return time.getTime() < Date.now() - 86400000
228
228
  }
229
229
  "
230
230
  />
@@ -17,7 +17,7 @@ const loginParams = ref({
17
17
 
18
18
  const handleDataApi = async () => {
19
19
  try {
20
- const res = await DataApiTest('123')
20
+ const res = await DataApiTest({ id: '123' })
21
21
  console.log(res)
22
22
  console.log(res.name)
23
23
  ElMessage({
@@ -33,7 +33,7 @@ const handleDataApi = async () => {
33
33
 
34
34
  const handleFullDataApi = async () => {
35
35
  try {
36
- const res = await FullDataApiTest('123')
36
+ const res = await FullDataApiTest({ id: '123' })
37
37
  console.log(res)
38
38
  console.log(res.data?.name)
39
39
  ElMessage({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-jnrs-vue",
3
- "version": "1.2.26",
3
+ "version": "1.2.28",
4
4
  "description": "巨能前端工程化开发,Vue 项目模板脚手架",
5
5
  "keywords": [
6
6
  "vue",