فهرست منبع

Merge branch 'master' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 into dev

YunaiV 6 ماه پیش
والد
کامیت
f659899f8e
100فایلهای تغییر یافته به همراه19651 افزوده شده و 564 حذف شده
  1. 5 4
      .vscode/settings.json
  2. 17486 0
      package-lock.json
  3. 2 2
      package.json
  4. 1 0
      src/api/bpm/processInstance/index.ts
  5. 5 0
      src/api/infra/codegen/index.ts
  6. 6 1
      src/api/infra/config/index.ts
  7. 5 0
      src/api/infra/dataSourceConfig/index.ts
  8. 40 30
      src/api/infra/demo/demo01/index.ts
  9. 119 83
      src/api/infra/demo/demo03/erp/index.ts
  10. 63 39
      src/api/infra/demo/demo03/inner/index.ts
  11. 63 39
      src/api/infra/demo/demo03/normal/index.ts
  12. 5 0
      src/api/infra/file/index.ts
  13. 5 0
      src/api/infra/fileConfig/index.ts
  14. 5 0
      src/api/infra/job/index.ts
  15. 1 1
      src/api/mall/product/spu.ts
  16. 19 9
      src/api/system/dept/index.ts
  17. 16 6
      src/api/system/dict/dict.data.ts
  18. 15 6
      src/api/system/dict/dict.type.ts
  19. 1 1
      src/api/system/loginLog/index.ts
  20. 5 0
      src/api/system/mail/account/index.ts
  21. 5 0
      src/api/system/mail/template/index.ts
  22. 5 0
      src/api/system/notice/index.ts
  23. 5 0
      src/api/system/notify/template/index.ts
  24. 5 0
      src/api/system/oauth2/client.ts
  25. 1 1
      src/api/system/operatelog/index.ts
  26. 6 1
      src/api/system/post/index.ts
  27. 5 0
      src/api/system/role/index.ts
  28. 5 0
      src/api/system/sms/smsChannel/index.ts
  29. 5 0
      src/api/system/sms/smsTemplate/index.ts
  30. 6 1
      src/api/system/social/user/index.ts
  31. 5 0
      src/api/system/tenant/index.ts
  32. 6 0
      src/api/system/tenantPackage/index.ts
  33. 6 1
      src/api/system/user/index.ts
  34. 0 4
      src/api/system/user/profile.ts
  35. 1 1
      src/assets/svgs/pay/icon/mock.svg
  36. 1 0
      src/assets/svgs/pay/icon/wallet.svg
  37. 7 3
      src/components/AppLinkInput/AppLinkSelectDialog.vue
  38. 0 1
      src/components/Cropper/src/CopperModal.vue
  39. 18 2
      src/components/Dialog/src/Dialog.vue
  40. 3 0
      src/components/DiyEditor/components/mobile/Carousel/config.ts
  41. 1 1
      src/components/DiyEditor/components/mobile/Carousel/index.vue
  42. 3 0
      src/components/DiyEditor/components/mobile/Carousel/property.vue
  43. 31 1
      src/components/DiyEditor/components/mobile/NavigationBar/components/CellProperty.vue
  44. 6 0
      src/components/DiyEditor/components/mobile/NavigationBar/config.ts
  45. 5 2
      src/components/DiyEditor/components/mobile/NavigationBar/index.vue
  46. 5 0
      src/components/DiyEditor/index.vue
  47. 1 0
      src/components/Echart/src/Echart.vue
  48. 10 0
      src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/flowableDescriptor.json
  49. 15 17
      src/components/bpmnProcessDesigner/package/designer/plugins/translate/customTranslate.js
  50. 2 2
      src/components/bpmnProcessDesigner/package/designer/plugins/translate/zh.js
  51. 54 1
      src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue
  52. 14 3
      src/components/bpmnProcessDesigner/package/penal/custom-config/components/BoundaryEventTimer.vue
  53. 7 6
      src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue
  54. 285 0
      src/components/bpmnProcessDesigner/package/penal/time-event-config/CycleConfig.vue
  55. 86 0
      src/components/bpmnProcessDesigner/package/penal/time-event-config/DurationConfig.vue
  56. 312 0
      src/components/bpmnProcessDesigner/package/penal/time-event-config/TimeEventConfig.vue
  57. 3 1
      src/plugins/formCreate/index.ts
  58. 1 1
      src/router/index.ts
  59. 41 4
      src/store/modules/app.ts
  60. 1 1
      src/store/modules/user.ts
  61. 43 0
      src/utils/color.ts
  62. 149 2
      src/utils/formCreate.ts
  63. 4 3
      src/utils/routerHelper.ts
  64. 11 6
      src/views/Home/Index.vue
  65. 1 1
      src/views/Login/Login.vue
  66. 18 18
      src/views/Login/components/ForgetPasswordForm.vue
  67. 25 23
      src/views/Login/components/LoginForm.vue
  68. 10 10
      src/views/Login/components/MobileForm.vue
  69. 7 7
      src/views/Login/components/QrCodeForm.vue
  70. 19 17
      src/views/Login/components/RegisterForm.vue
  71. 4 4
      src/views/Login/components/SSOLogin.vue
  72. 6 5
      src/views/Profile/components/UserAvatar.vue
  73. 8 8
      src/views/Profile/components/UserSocial.vue
  74. 7 6
      src/views/ai/chat/index/components/conversation/ConversationList.vue
  75. 7 7
      src/views/ai/chat/index/components/message/MessageList.vue
  76. 2 2
      src/views/ai/chat/index/components/role/RoleHeader.vue
  77. 5 5
      src/views/ai/chat/index/components/role/RoleList.vue
  78. 7 7
      src/views/ai/chat/index/components/role/RoleRepository.vue
  79. 14 10
      src/views/ai/chat/index/index.vue
  80. 9 4
      src/views/bpm/model/CategoryDraggableModel.vue
  81. 18 2
      src/views/bpm/model/form/BasicInfo.vue
  82. 10 0
      src/views/bpm/model/form/index.vue
  83. 1 0
      src/views/bpm/processInstance/detail/ProcessInstanceOperationButton.vue
  84. 10 2
      src/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue
  85. 1 1
      src/views/bpm/task/todo/index.vue
  86. 1 1
      src/views/crm/customer/CustomerImportForm.vue
  87. 40 0
      src/views/crm/followup/index.vue
  88. 15 1
      src/views/infra/codegen/components/ColumInfoForm.vue
  89. 29 1
      src/views/infra/codegen/index.vue
  90. 29 1
      src/views/infra/config/index.vue
  91. 30 1
      src/views/infra/dataSourceConfig/index.vue
  92. 9 6
      src/views/infra/demo/demo01/Demo01ContactForm.vue
  93. 49 11
      src/views/infra/demo/demo01/index.vue
  94. 8 5
      src/views/infra/demo/demo03/erp/Demo03StudentForm.vue
  95. 8 8
      src/views/infra/demo/demo03/erp/components/Demo03CourseForm.vue
  96. 53 21
      src/views/infra/demo/demo03/erp/components/Demo03CourseList.vue
  97. 8 8
      src/views/infra/demo/demo03/erp/components/Demo03GradeForm.vue
  98. 53 21
      src/views/infra/demo/demo03/erp/components/Demo03GradeList.vue
  99. 78 53
      src/views/infra/demo/demo03/erp/index.vue
  100. 0 0
      src/views/infra/demo/demo03/inner/Demo03StudentForm.vue

+ 5 - 4
.vscode/settings.json

@@ -62,16 +62,16 @@
     "editor.defaultFormatter": "esbenp.prettier-vscode"
   },
   "[typescript]": {
-    "editor.defaultFormatter": "rvest.vs-code-prettier-eslint"
+    "editor.defaultFormatter": "esbenp.prettier-vscode"
   },
   "[typescriptreact]": {
-    "editor.defaultFormatter": "rvest.vs-code-prettier-eslint"
+    "editor.defaultFormatter": "esbenp.prettier-vscode"
   },
   "[html]": {
     "editor.defaultFormatter": "esbenp.prettier-vscode"
   },
   "[css]": {
-    "editor.defaultFormatter": "rvest.vs-code-prettier-eslint"
+    "editor.defaultFormatter": "esbenp.prettier-vscode"
   },
   "[less]": {
     "editor.defaultFormatter": "esbenp.prettier-vscode"
@@ -86,8 +86,9 @@
     "source.fixAll.eslint": "explicit",
     "source.fixAll.stylelint": "explicit"
   },
+  "editor.formatOnSave": true,
   "[vue]": {
-    "editor.defaultFormatter": "octref.vetur"
+    "editor.defaultFormatter": "esbenp.prettier-vscode"
   },
   "i18n-ally.localesPaths": ["src/locales"],
   "i18n-ally.keystyle": "nested",

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 17486 - 0
package-lock.json


+ 2 - 2
package.json

@@ -1,6 +1,6 @@
 {
   "name": "yudao-ui-admin-vue3",
-  "version": "2.4.2-snapshot",
+  "version": "2.6.1-snapshot",
   "description": "基于vue3、vite4、element-plus、typesScript",
   "author": "xingyu",
   "private": false,
@@ -36,7 +36,7 @@
     "@wangeditor/editor-for-vue": "^5.1.10",
     "@zxcvbn-ts/core": "^3.0.4",
     "animate.css": "^4.1.1",
-    "axios": "^1.6.8",
+    "axios": "1.9.0",
     "benz-amr-recorder": "^1.1.5",
     "bpmn-js-token-simulation": "^0.36.0",
     "camunda-bpmn-moddle": "^7.0.1",

+ 1 - 0
src/api/bpm/processInstance/index.ts

@@ -48,6 +48,7 @@ export type ApprovalNodeInfo = {
   status: number
   startTime?: Date
   endTime?: Date
+  processInstanceId?: string
   candidateUsers?: User[]
   tasks: ApprovalTaskInfo[]
 }

+ 5 - 0
src/api/infra/codegen/index.ts

@@ -105,3 +105,8 @@ export const createCodegenList = (data) => {
 export const deleteCodegenTable = (id: number) => {
   return request.delete({ url: '/infra/codegen/delete?tableId=' + id })
 }
+
+// 批量删除代码生成表定义
+export const deleteCodegenTableList = (ids: number[]) => {
+  return request.delete({ url: '/infra/codegen/delete-list', params: { tableIds: ids.join(',') } })
+}

+ 6 - 1
src/api/infra/config/index.ts

@@ -42,7 +42,12 @@ export const deleteConfig = (id: number) => {
   return request.delete({ url: '/infra/config/delete?id=' + id })
 }
 
+// 批量删除参数
+export const deleteConfigList = (ids: number[]) => {
+  return request.delete({ url: '/infra/config/delete-list', params: { ids: ids.join(',') } })
+}
+
 // 导出参数
 export const exportConfig = (params) => {
-  return request.download({ url: '/infra/config/export', params })
+  return request.download({ url: '/infra/config/export-excel', params })
 }

+ 5 - 0
src/api/infra/dataSourceConfig/index.ts

@@ -24,6 +24,11 @@ export const deleteDataSourceConfig = (id: number) => {
   return request.delete({ url: '/infra/data-source-config/delete?id=' + id })
 }
 
+// 批量删除数据源配置
+export const deleteDataSourceConfigList = (ids: number[]) => {
+  return request.delete({ url: '/infra/data-source-config/delete-list', params: { ids: ids.join(',') } })
+}
+
 // 查询数据源配置详情
 export const getDataSourceConfig = (id: number) => {
   return request.get({ url: '/infra/data-source-config/get?id=' + id })

+ 40 - 30
src/api/infra/demo/demo01/index.ts

@@ -1,40 +1,50 @@
 import request from '@/config/axios'
+import type { Dayjs } from 'dayjs'
 
-export interface Demo01ContactVO {
-  id: number
-  name: string
-  sex: number
-  birthday: Date
-  description: string
-  avatar: string
+/** 示例联系人信息 */
+export interface Demo01Contact {
+  id: number // 编号
+  name?: string // 名字
+  sex?: number // 性别
+  birthday?: string | Dayjs // 出生年
+  description?: string // 简介
+  avatar: string // 头像
 }
 
-// 查询示例联系人分页
-export const getDemo01ContactPage = async (params) => {
-  return await request.get({ url: `/infra/demo01-contact/page`, params })
-}
+// 示例联系人 API
+export const Demo01ContactApi = {
+  // 查询示例联系人分页
+  getDemo01ContactPage: async (params: any) => {
+    return await request.get({ url: `/infra/demo01-contact/page`, params })
+  },
 
-// 查询示例联系人详情
-export const getDemo01Contact = async (id: number) => {
-  return await request.get({ url: `/infra/demo01-contact/get?id=` + id })
-}
+  // 查询示例联系人详情
+  getDemo01Contact: async (id: number) => {
+    return await request.get({ url: `/infra/demo01-contact/get?id=` + id })
+  },
 
-// 新增示例联系人
-export const createDemo01Contact = async (data: Demo01ContactVO) => {
-  return await request.post({ url: `/infra/demo01-contact/create`, data })
-}
+  // 新增示例联系人
+  createDemo01Contact: async (data: Demo01Contact) => {
+    return await request.post({ url: `/infra/demo01-contact/create`, data })
+  },
 
-// 修改示例联系人
-export const updateDemo01Contact = async (data: Demo01ContactVO) => {
-  return await request.put({ url: `/infra/demo01-contact/update`, data })
-}
+  // 修改示例联系人
+  updateDemo01Contact: async (data: Demo01Contact) => {
+    return await request.put({ url: `/infra/demo01-contact/update`, data })
+  },
 
-// 删除示例联系人
-export const deleteDemo01Contact = async (id: number) => {
-  return await request.delete({ url: `/infra/demo01-contact/delete?id=` + id })
-}
+  // 删除示例联系人
+  deleteDemo01Contact: async (id: number) => {
+    return await request.delete({ url: `/infra/demo01-contact/delete?id=` + id })
+  },
+
+  /** 批量删除示例联系人 */
+  deleteDemo01ContactList: async (ids: number[]) => {
+    return await request.delete({ url: `/infra/demo01-contact/delete-list?ids=${ids.join(',')}` })
+  },
 
-// 导出示例联系人 Excel
-export const exportDemo01Contact = async (params) => {
-  return await request.download({ url: `/infra/demo01-contact/export-excel`, params })
+  // 导出示例联系人 Excel
+  exportDemo01Contact: async (params) => {
+    return await request.download({ url: `/infra/demo01-contact/export-excel`, params })
+  }
 }

+ 119 - 83
src/api/infra/demo/demo03/erp/index.ts

@@ -1,91 +1,127 @@
 import request from '@/config/axios'
-
-export interface Demo03StudentVO {
-  id: number
-  name: string
-  sex: number
-  birthday: Date
-  description: string
-}
-
-// 查询学生分页
-export const getDemo03StudentPage = async (params) => {
-  return await request.get({ url: `/infra/demo03-student/page`, params })
-}
-
-// 查询学生详情
-export const getDemo03Student = async (id: number) => {
-  return await request.get({ url: `/infra/demo03-student/get?id=` + id })
-}
-
-// 新增学生
-export const createDemo03Student = async (data: Demo03StudentVO) => {
-  return await request.post({ url: `/infra/demo03-student/create`, data })
-}
-
-// 修改学生
-export const updateDemo03Student = async (data: Demo03StudentVO) => {
-  return await request.put({ url: `/infra/demo03-student/update`, data })
-}
-
-// 删除学生
-export const deleteDemo03Student = async (id: number) => {
-  return await request.delete({ url: `/infra/demo03-student/delete?id=` + id })
-}
-
-// 导出学生 Excel
-export const exportDemo03Student = async (params) => {
-  return await request.download({ url: `/infra/demo03-student/export-excel`, params })
-}
+import type { Dayjs } from 'dayjs';
+
+/** 学生课程信息 */
+export interface Demo03Course {
+  id: number; // 编号
+  studentId?: number; // 学生编号
+  name?: string; // 名字
+  score?: number; // 分数
+}
+
+/** 学生班级信息 */
+export interface Demo03Grade {
+  id: number; // 编号
+  studentId?: number; // 学生编号
+  name?: string; // 名字
+  teacher?: string; // 班主任
+}
+
+/** 学生信息 */
+export interface Demo03Student {
+  id: number; // 编号
+  name?: string; // 名字
+  sex?: number; // 性别
+  birthday?: string | Dayjs; // 出生日期
+  description?: string; // 简介
+}
+
+// 学生 API
+export const Demo03StudentApi = {
+  // 查询学生分页
+  getDemo03StudentPage: async (params: any) => {
+    return await request.get({ url: `/infra/demo03-student-erp/page`, params })
+  },
+
+  // 查询学生详情
+  getDemo03Student: async (id: number) => {
+    return await request.get({ url: `/infra/demo03-student-erp/get?id=` + id })
+  },
+
+  // 新增学生
+  createDemo03Student: async (data: Demo03Student) => {
+    return await request.post({ url: `/infra/demo03-student-erp/create`, data })
+  },
+
+  // 修改学生
+  updateDemo03Student: async (data: Demo03Student) => {
+    return await request.put({ url: `/infra/demo03-student-erp/update`, data })
+  },
+
+  // 删除学生
+  deleteDemo03Student: async (id: number) => {
+    return await request.delete({ url: `/infra/demo03-student-erp/delete?id=` + id })
+  },
+
+  /** 批量删除学生 */
+  deleteDemo03StudentList: async (ids: number[]) => {
+    return await request.delete({ url: `/infra/demo03-student-erp/delete-list?ids=${ids.join(',')}` })
+  },
+
+  // 导出学生 Excel
+  exportDemo03Student: async (params) => {
+    return await request.download({ url: `/infra/demo03-student-erp/export-excel`, params })
+  },
 
 // ==================== 子表(学生课程) ====================
 
-// 获得学生课程分页
-export const getDemo03CoursePage = async (params) => {
-  return await request.get({ url: `/infra/demo03-student/demo03-course/page`, params })
-}
-// 新增学生课程
-export const createDemo03Course = async (data) => {
-  return await request.post({ url: `/infra/demo03-student/demo03-course/create`, data })
-}
-
-// 修改学生课程
-export const updateDemo03Course = async (data) => {
-  return await request.put({ url: `/infra/demo03-student/demo03-course/update`, data })
-}
-
-// 删除学生课程
-export const deleteDemo03Course = async (id: number) => {
-  return await request.delete({ url: `/infra/demo03-student/demo03-course/delete?id=` + id })
-}
-
-// 获得学生课程
-export const getDemo03Course = async (id: number) => {
-  return await request.get({ url: `/infra/demo03-student/demo03-course/get?id=` + id })
-}
+  // 获得学生课程分页
+  getDemo03CoursePage: async (params) => {
+    return await request.get({ url: `/infra/demo03-student-erp/demo03-course/page`, params })
+  },
+  // 新增学生课程
+  createDemo03Course: async (data: Demo03Course) => {
+    return await request.post({ url: `/infra/demo03-student-erp/demo03-course/create`, data })
+  },
+
+  // 修改学生课程
+  updateDemo03Course: async (data: Demo03Course) => {
+    return await request.put({ url: `/infra/demo03-student-erp/demo03-course/update`, data })
+  },
+
+  // 删除学生课程
+  deleteDemo03Course: async (id: number) => {
+    return await request.delete({ url: `/infra/demo03-student-erp/demo03-course/delete?id=` + id })
+  },
+
+  /** 批量删除学生课程 */
+  deleteDemo03CourseList: async (ids: number[]) => {
+    return await request.delete({ url: `/infra/demo03-student-erp/demo03-course/delete-list?ids=${ids.join(',')}` })
+  },
+
+  // 获得学生课程
+  getDemo03Course: async (id: number) => {
+    return await request.get({ url: `/infra/demo03-student-erp/demo03-course/get?id=` + id })
+  },
 
 // ==================== 子表(学生班级) ====================
 
-// 获得学生班级分页
-export const getDemo03GradePage = async (params) => {
-  return await request.get({ url: `/infra/demo03-student/demo03-grade/page`, params })
-}
-// 新增学生班级
-export const createDemo03Grade = async (data) => {
-  return await request.post({ url: `/infra/demo03-student/demo03-grade/create`, data })
-}
-
-// 修改学生班级
-export const updateDemo03Grade = async (data) => {
-  return await request.put({ url: `/infra/demo03-student/demo03-grade/update`, data })
-}
-
-// 删除学生班级
-export const deleteDemo03Grade = async (id: number) => {
-  return await request.delete({ url: `/infra/demo03-student/demo03-grade/delete?id=` + id })
-}
-
-// 获得学生班级
-export const getDemo03Grade = async (id: number) => {
-  return await request.get({ url: `/infra/demo03-student/demo03-grade/get?id=` + id })
+  // 获得学生班级分页
+  getDemo03GradePage: async (params) => {
+    return await request.get({ url: `/infra/demo03-student-erp/demo03-grade/page`, params })
+  },
+  // 新增学生班级
+  createDemo03Grade: async (data: Demo03Grade) => {
+    return await request.post({ url: `/infra/demo03-student-erp/demo03-grade/create`, data })
+  },
+
+  // 修改学生班级
+  updateDemo03Grade: async (data: Demo03Grade) => {
+    return await request.put({ url: `/infra/demo03-student-erp/demo03-grade/update`, data })
+  },
+
+  // 删除学生班级
+  deleteDemo03Grade: async (id: number) => {
+    return await request.delete({ url: `/infra/demo03-student-erp/demo03-grade/delete?id=` + id })
+  },
+
+  /** 批量删除学生班级 */
+  deleteDemo03GradeList: async (ids: number[]) => {
+    return await request.delete({ url: `/infra/demo03-student-erp/demo03-grade/delete-list?ids=${ids.join(',')}` })
+  },
+
+  // 获得学生班级
+  getDemo03Grade: async (id: number) => {
+    return await request.get({ url: `/infra/demo03-student-erp/demo03-grade/get?id=` + id })
+  },
 }

+ 63 - 39
src/api/infra/demo/demo03/inner/index.ts

@@ -1,57 +1,81 @@
 import request from '@/config/axios'
+import type { Dayjs } from 'dayjs';
 
-export interface Demo03StudentVO {
-  id: number
-  name: string
-  sex: number
-  birthday: Date
-  description: string
+/** 学生课程信息 */
+export interface Demo03Course {
+  id: number; // 编号
+  studentId?: number; // 学生编号
+  name?: string; // 名字
+  score?: number; // 分数
 }
 
-// 查询学生分页
-export const getDemo03StudentPage = async (params) => {
-  return await request.get({ url: `/infra/demo03-student/page`, params })
+/** 学生班级信息 */
+export interface Demo03Grade {
+  id: number; // 编号
+  studentId?: number; // 学生编号
+  name?: string; // 名字
+  teacher?: string; // 班主任
 }
 
-// 查询学生详情
-export const getDemo03Student = async (id: number) => {
-  return await request.get({ url: `/infra/demo03-student/get?id=` + id })
+/** 学生信息 */
+export interface Demo03Student {
+  id: number; // 编号
+  name?: string; // 名字
+  sex?: number; // 性别
+  birthday?: string | Dayjs; // 出生日期
+  description?: string; // 简介
+  demo03courses?: Demo03Course[]
+  demo03grade?: Demo03Grade
 }
 
-// 新增学生
-export const createDemo03Student = async (data: Demo03StudentVO) => {
-  return await request.post({ url: `/infra/demo03-student/create`, data })
-}
+// 学生 API
+export const Demo03StudentApi = {
+  // 查询学生分页
+  getDemo03StudentPage: async (params: any) => {
+    return await request.get({ url: `/infra/demo03-student-inner/page`, params })
+  },
 
-// 修改学生
-export const updateDemo03Student = async (data: Demo03StudentVO) => {
-  return await request.put({ url: `/infra/demo03-student/update`, data })
-}
+  // 查询学生详情
+  getDemo03Student: async (id: number) => {
+    return await request.get({ url: `/infra/demo03-student-inner/get?id=` + id })
+  },
 
-// 删除学生
-export const deleteDemo03Student = async (id: number) => {
-  return await request.delete({ url: `/infra/demo03-student/delete?id=` + id })
-}
+  // 新增学生
+  createDemo03Student: async (data: Demo03Student) => {
+    return await request.post({ url: `/infra/demo03-student-inner/create`, data })
+  },
 
-// 导出学生 Excel
-export const exportDemo03Student = async (params) => {
-  return await request.download({ url: `/infra/demo03-student/export-excel`, params })
-}
+  // 修改学生
+  updateDemo03Student: async (data: Demo03Student) => {
+    return await request.put({ url: `/infra/demo03-student-inner/update`, data })
+  },
+
+  // 删除学生
+  deleteDemo03Student: async (id: number) => {
+    return await request.delete({ url: `/infra/demo03-student-inner/delete?id=` + id })
+  },
+
+  /** 批量删除学生 */
+  deleteDemo03StudentList: async (ids: number[]) => {
+    return await request.delete({ url: `/infra/demo03-student-inner/delete-list?ids=${ids.join(',')}` })
+  },
+
+  // 导出学生 Excel
+  exportDemo03Student: async (params) => {
+    return await request.download({ url: `/infra/demo03-student-inner/export-excel`, params })
+  },
 
 // ==================== 子表(学生课程) ====================
 
-// 获得学生课程列表
-export const getDemo03CourseListByStudentId = async (studentId) => {
-  return await request.get({
-    url: `/infra/demo03-student/demo03-course/list-by-student-id?studentId=` + studentId
-  })
-}
+  // 获得学生课程列表
+  getDemo03CourseListByStudentId: async (studentId) => {
+    return await request.get({ url: `/infra/demo03-student-inner/demo03-course/list-by-student-id?studentId=` + studentId })
+  },
 
 // ==================== 子表(学生班级) ====================
 
-// 获得学生班级
-export const getDemo03GradeByStudentId = async (studentId) => {
-  return await request.get({
-    url: `/infra/demo03-student/demo03-grade/get-by-student-id?studentId=` + studentId
-  })
+  // 获得学生班级
+  getDemo03GradeByStudentId: async (studentId) => {
+    return await request.get({ url: `/infra/demo03-student-inner/demo03-grade/get-by-student-id?studentId=` + studentId })
+  },
 }

+ 63 - 39
src/api/infra/demo/demo03/normal/index.ts

@@ -1,57 +1,81 @@
 import request from '@/config/axios'
+import type { Dayjs } from 'dayjs';
 
-export interface Demo03StudentVO {
-  id: number
-  name: string
-  sex: number
-  birthday: Date
-  description: string
+/** 学生课程信息 */
+export interface Demo03Course {
+  id: number; // 编号
+  studentId?: number; // 学生编号
+  name?: string; // 名字
+  score?: number; // 分数
 }
 
-// 查询学生分页
-export const getDemo03StudentPage = async (params) => {
-  return await request.get({ url: `/infra/demo03-student/page`, params })
+/** 学生班级信息 */
+export interface Demo03Grade {
+  id: number; // 编号
+  studentId?: number; // 学生编号
+  name?: string; // 名字
+  teacher?: string; // 班主任
 }
 
-// 查询学生详情
-export const getDemo03Student = async (id: number) => {
-  return await request.get({ url: `/infra/demo03-student/get?id=` + id })
+/** 学生信息 */
+export interface Demo03Student {
+  id: number; // 编号
+  name?: string; // 名字
+  sex?: number; // 性别
+  birthday?: string | Dayjs; // 出生日期
+  description?: string; // 简介
+  demo03courses?: Demo03Course[]
+  demo03grade?: Demo03Grade
 }
 
-// 新增学生
-export const createDemo03Student = async (data: Demo03StudentVO) => {
-  return await request.post({ url: `/infra/demo03-student/create`, data })
-}
+// 学生 API
+export const Demo03StudentApi = {
+  // 查询学生分页
+  getDemo03StudentPage: async (params: any) => {
+    return await request.get({ url: `/infra/demo03-student-normal/page`, params })
+  },
 
-// 修改学生
-export const updateDemo03Student = async (data: Demo03StudentVO) => {
-  return await request.put({ url: `/infra/demo03-student/update`, data })
-}
+  // 查询学生详情
+  getDemo03Student: async (id: number) => {
+    return await request.get({ url: `/infra/demo03-student-normal/get?id=` + id })
+  },
 
-// 删除学生
-export const deleteDemo03Student = async (id: number) => {
-  return await request.delete({ url: `/infra/demo03-student/delete?id=` + id })
-}
+  // 新增学生
+  createDemo03Student: async (data: Demo03Student) => {
+    return await request.post({ url: `/infra/demo03-student-normal/create`, data })
+  },
 
-// 导出学生 Excel
-export const exportDemo03Student = async (params) => {
-  return await request.download({ url: `/infra/demo03-student/export-excel`, params })
-}
+  // 修改学生
+  updateDemo03Student: async (data: Demo03Student) => {
+    return await request.put({ url: `/infra/demo03-student-normal/update`, data })
+  },
+
+  // 删除学生
+  deleteDemo03Student: async (id: number) => {
+    return await request.delete({ url: `/infra/demo03-student-normal/delete?id=` + id })
+  },
+
+  /** 批量删除学生 */
+  deleteDemo03StudentList: async (ids: number[]) => {
+    return await request.delete({ url: `/infra/demo03-student-normal/delete-list?ids=${ids.join(',')}` })
+  },
+
+  // 导出学生 Excel
+  exportDemo03Student: async (params) => {
+    return await request.download({ url: `/infra/demo03-student-normal/export-excel`, params })
+  },
 
 // ==================== 子表(学生课程) ====================
 
-// 获得学生课程列表
-export const getDemo03CourseListByStudentId = async (studentId) => {
-  return await request.get({
-    url: `/infra/demo03-student/demo03-course/list-by-student-id?studentId=` + studentId
-  })
-}
+  // 获得学生课程列表
+  getDemo03CourseListByStudentId: async (studentId) => {
+    return await request.get({ url: `/infra/demo03-student-normal/demo03-course/list-by-student-id?studentId=` + studentId })
+  },
 
 // ==================== 子表(学生班级) ====================
 
-// 获得学生班级
-export const getDemo03GradeByStudentId = async (studentId) => {
-  return await request.get({
-    url: `/infra/demo03-student/demo03-grade/get-by-student-id?studentId=` + studentId
-  })
+  // 获得学生班级
+  getDemo03GradeByStudentId: async (studentId) => {
+    return await request.get({ url: `/infra/demo03-student-normal/demo03-grade/get-by-student-id?studentId=` + studentId })
+  },
 }

+ 5 - 0
src/api/infra/file/index.ts

@@ -22,6 +22,11 @@ export const deleteFile = (id: number) => {
   return request.delete({ url: '/infra/file/delete?id=' + id })
 }
 
+// 批量删除文件
+export const deleteFileList = (ids: number[]) => {
+  return request.delete({ url: '/infra/file/delete-list', params: { ids: ids.join(',') } })
+}
+
 // 获取文件预签名地址
 export const getFilePresignedUrl = (name: string, directory?: string) => {
   return request.get<FilePresignedUrlRespVO>({

+ 5 - 0
src/api/infra/fileConfig/index.ts

@@ -56,6 +56,11 @@ export const deleteFileConfig = (id: number) => {
   return request.delete({ url: '/infra/file-config/delete?id=' + id })
 }
 
+// 批量删除文件配置
+export const deleteFileConfigList = (ids: number[]) => {
+  return request.delete({ url: '/infra/file-config/delete-list', params: { ids: ids.join(',') } })
+}
+
 // 测试文件配置
 export const testFileConfig = (id: number) => {
   return request.get({ url: '/infra/file-config/test?id=' + id })

+ 5 - 0
src/api/infra/job/index.ts

@@ -38,6 +38,11 @@ export const deleteJob = (id: number) => {
   return request.delete({ url: '/infra/job/delete?id=' + id })
 }
 
+// 批量删除定时任务调度
+export const deleteJobList = (ids: number[]) => {
+  return request.delete({ url: '/infra/job/delete-list', params: { ids: ids.join(',') } })
+}
+
 // 导出定时任务调度
 export const exportJob = (params) => {
   return request.download({ url: '/infra/job/export-excel', params })

+ 1 - 1
src/api/mall/product/spu.ts

@@ -102,7 +102,7 @@ export const deleteSpu = (id: number) => {
 
 // 导出商品 Spu Excel
 export const exportSpu = async (params: any) => {
-  return await request.download({ url: '/product/spu/export', params })
+  return await request.download({ url: '/product/spu/export-excel', params })
 }
 
 // 获得商品 SPU 精简列表

+ 19 - 9
src/api/system/dept/index.ts

@@ -1,7 +1,7 @@
 import request from '@/config/axios'
 
 export interface DeptVO {
-  id?: number
+  id: number
   name: string
   parentId: number
   status: number
@@ -13,31 +13,41 @@ export interface DeptVO {
 }
 
 // 查询部门(精简)列表
-export const getSimpleDeptList = async (): Promise<DeptVO[]> => {
-  return await request.get({ url: '/system/dept/simple-list' })
+export const getSimpleDeptList = (): Promise<DeptVO[]> => {
+  return request.get({ url: '/system/dept/simple-list' })
 }
 
 // 查询部门列表
+export const getDeptList = (params: any) => {
+  return request.get({ url: '/system/dept/list', params })
+}
+
+// 查询部门分页
 export const getDeptPage = async (params: PageParam) => {
   return await request.get({ url: '/system/dept/list', params })
 }
 
 // 查询部门详情
-export const getDept = async (id: number) => {
-  return await request.get({ url: '/system/dept/get?id=' + id })
+export const getDept = (id: number) => {
+  return request.get({ url: '/system/dept/get?id=' + id })
 }
 
 // 新增部门
-export const createDept = async (data: DeptVO) => {
-  return await request.post({ url: '/system/dept/create', data: data })
+export const createDept = (data: DeptVO) => {
+  return request.post({ url: '/system/dept/create', data })
 }
 
 // 修改部门
-export const updateDept = async (params: DeptVO) => {
-  return await request.put({ url: '/system/dept/update', data: params })
+export const updateDept = (data: DeptVO) => {
+  return request.put({ url: '/system/dept/update', data })
 }
 
 // 删除部门
 export const deleteDept = async (id: number) => {
   return await request.delete({ url: '/system/dept/delete?id=' + id })
 }
+
+// 批量删除部门
+export const deleteDeptList = async (ids: number[]) => {
+  return await request.delete({ url: '/system/dept/delete-list', params: { ids: ids.join(',') } })
+}

+ 16 - 6
src/api/system/dict/dict.data.ts

@@ -1,8 +1,8 @@
 import request from '@/config/axios'
 
-export type DictDataVO = {
-  id: number | undefined
-  sort: number | undefined
+export interface DictDataVO {
+  id: number
+  sort: number
   label: string
   value: string
   dictType: string
@@ -28,6 +28,11 @@ export const getDictData = (id: number) => {
   return request.get({ url: '/system/dict-data/get?id=' + id })
 }
 
+// 根据字典类型查询字典数据
+export const getDictDataByType = (dictType: string) => {
+  return request.get({ url: '/system/dict-data/type?type=' + dictType })
+}
+
 // 新增字典数据
 export const createDictData = (data: DictDataVO) => {
   return request.post({ url: '/system/dict-data/create', data })
@@ -43,7 +48,12 @@ export const deleteDictData = (id: number) => {
   return request.delete({ url: '/system/dict-data/delete?id=' + id })
 }
 
-// 导出字典类型数据
-export const exportDictData = (params) => {
-  return request.download({ url: '/system/dict-data/export', params })
+// 批量删除字典数据
+export const deleteDictDataList = (ids: number[]) => {
+  return request.delete({ url: '/system/dict-data/delete-list', params: { ids: ids.join(',') } })
+}
+
+// 导出字典数据
+export const exportDictData = (params: any) => {
+  return request.download({ url: '/system/dict-data/export-excel', params })
 }

+ 15 - 6
src/api/system/dict/dict.type.ts

@@ -1,7 +1,7 @@
 import request from '@/config/axios'
 
-export type DictTypeVO = {
-  id: number | undefined
+export interface DictTypeVO {
+  id: number
   name: string
   type: string
   status: number
@@ -10,8 +10,8 @@ export type DictTypeVO = {
 }
 
 // 查询字典(精简)列表
-export const getSimpleDictTypeList = () => {
-  return request.get({ url: '/system/dict-type/list-all-simple' })
+export const getSimpleDictTypeList = (): Promise<DictTypeVO[]> => {
+  return request.get({ url: '/system/dict-type/simple-list' })
 }
 
 // 查询字典列表
@@ -38,7 +38,16 @@ export const updateDictType = (data: DictTypeVO) => {
 export const deleteDictType = (id: number) => {
   return request.delete({ url: '/system/dict-type/delete?id=' + id })
 }
-// 导出字典类型
+
+// 批量删除字典类型
+export const deleteDictTypeList = (ids: number[]) => {
+  return request.delete({ url: '/system/dict-type/delete-list', params: { ids: ids.join(',') } })
+}
+
+// 导出字典
 export const exportDictType = (params) => {
-  return request.download({ url: '/system/dict-type/export', params })
+  return request.download({
+    url: '/system/dict-type/export-excel',
+    params
+  })
 }

+ 1 - 1
src/api/system/loginLog/index.ts

@@ -21,5 +21,5 @@ export const getLoginLogPage = (params: PageParam) => {
 
 // 导出登录日志
 export const exportLoginLog = (params) => {
-  return request.download({ url: '/system/login-log/export', params })
+  return request.download({ url: '/system/login-log/export-excel', params })
 }

+ 5 - 0
src/api/system/mail/account/index.ts

@@ -36,6 +36,11 @@ export const deleteMailAccount = async (id: number) => {
   return await request.delete({ url: '/system/mail-account/delete?id=' + id })
 }
 
+// 批量删除邮箱账号
+export const deleteMailAccountList = async (ids: number[]) => {
+  return await request.delete({ url: '/system/mail-account/delete-list', params: { ids: ids.join(',') } })
+}
+
 // 获得邮箱账号精简列表
 export const getSimpleMailAccountList = async () => {
   return request.get({ url: '/system/mail-account/simple-list' })

+ 5 - 0
src/api/system/mail/template/index.ts

@@ -44,6 +44,11 @@ export const deleteMailTemplate = async (id: number) => {
   return await request.delete({ url: '/system/mail-template/delete?id=' + id })
 }
 
+// 批量删除邮件模版
+export const deleteMailTemplateList = async (ids: number[]) => {
+  return await request.delete({ url: '/system/mail-template/delete-list', params: { ids: ids.join(',') } })
+}
+
 // 发送邮件
 export const sendMail = (data: MailSendReqVO) => {
   return request.post({ url: '/system/mail-template/send-mail', data })

+ 5 - 0
src/api/system/notice/index.ts

@@ -36,6 +36,11 @@ export const deleteNotice = (id: number) => {
   return request.delete({ url: '/system/notice/delete?id=' + id })
 }
 
+// 批量删除公告
+export const deleteNoticeList = (ids: number[]) => {
+  return request.delete({ url: '/system/notice/delete-list', params: { ids: ids.join(',') } })
+}
+
 // 推送公告
 export const pushNotice = (id: number) => {
   return request.post({ url: '/system/notice/push?id=' + id })

+ 5 - 0
src/api/system/notify/template/index.ts

@@ -43,6 +43,11 @@ export const deleteNotifyTemplate = async (id: number) => {
   return await request.delete({ url: '/system/notify-template/delete?id=' + id })
 }
 
+// 批量删除站内信模板
+export const deleteNotifyTemplateList = async (ids: number[]) => {
+  return await request.delete({ url: '/system/notify-template/delete-list', params: { ids: ids.join(',') } })
+}
+
 // 发送站内信
 export const sendNotify = (data: NotifySendReqVO) => {
   return request.post({ url: '/system/notify-template/send-notify', data })

+ 5 - 0
src/api/system/oauth2/client.ts

@@ -45,3 +45,8 @@ export const updateOAuth2Client = (data: OAuth2ClientVO) => {
 export const deleteOAuth2Client = (id: number) => {
   return request.delete({ url: '/system/oauth2-client/delete?id=' + id })
 }
+
+// 批量删除 OAuth2 客户端
+export const deleteOAuth2ClientList = (ids: number[]) => {
+  return request.delete({ url: '/system/oauth2-client/delete-list', params: { ids: ids.join(',') } })
+}

+ 1 - 1
src/api/system/operatelog/index.ts

@@ -26,5 +26,5 @@ export const getOperateLogPage = (params: PageParam) => {
 }
 // 导出操作日志
 export const exportOperateLog = (params: any) => {
-  return request.download({ url: '/system/operate-log/export', params })
+  return request.download({ url: '/system/operate-log/export-excel', params })
 }

+ 6 - 1
src/api/system/post/index.ts

@@ -40,7 +40,12 @@ export const deletePost = async (id: number) => {
   return await request.delete({ url: '/system/post/delete?id=' + id })
 }
 
+// 批量删除岗位
+export const deletePostList = async (ids: number[]) => {
+  return await request.delete({ url: '/system/post/delete-list', params: { ids: ids.join(',') } })
+}
+
 // 导出岗位
 export const exportPost = async (params) => {
-  return await request.download({ url: '/system/post/export', params })
+  return await request.download({ url: '/system/post/export-excel', params })
 }

+ 5 - 0
src/api/system/role/index.ts

@@ -52,6 +52,11 @@ export const deleteRole = async (id: number) => {
   return await request.delete({ url: '/system/role/delete?id=' + id })
 }
 
+// 批量删除角色
+export const deleteRoleList = async (ids: number[]) => {
+  return await request.delete({ url: '/system/role/delete-list', params: { ids: ids.join(',') } })
+}
+
 // 导出角色
 export const exportRole = (params) => {
   return request.download({

+ 5 - 0
src/api/system/sms/smsChannel/index.ts

@@ -41,3 +41,8 @@ export const updateSmsChannel = (data: SmsChannelVO) => {
 export const deleteSmsChannel = (id: number) => {
   return request.delete({ url: '/system/sms-channel/delete?id=' + id })
 }
+
+// 批量删除短信渠道
+export const deleteSmsChannelList = (ids: number[]) => {
+  return request.delete({ url: '/system/sms-channel/delete-list', params: { ids: ids.join(',') } })
+}

+ 5 - 0
src/api/system/sms/smsTemplate/index.ts

@@ -46,6 +46,11 @@ export const deleteSmsTemplate = (id: number) => {
   return request.delete({ url: '/system/sms-template/delete?id=' + id })
 }
 
+// 批量删除短信模板
+export const deleteSmsTemplateList = (ids: number[]) => {
+  return request.delete({ url: '/system/sms-template/delete-list', params: { ids: ids.join(',') } })
+}
+
 // 导出短信模板
 export const exportSmsTemplate = (params) => {
   return request.download({

+ 6 - 1
src/api/system/social/user/index.ts

@@ -14,7 +14,7 @@ export interface SocialUserVO {
 }
 
 // 查询社交用户列表
-export const getSocialUserPage = async (params) => {
+export const getSocialUserPage = async (params: any) => {
   return await request.get({ url: `/system/social-user/page`, params })
 }
 
@@ -22,3 +22,8 @@ export const getSocialUserPage = async (params) => {
 export const getSocialUser = async (id: number) => {
   return await request.get({ url: `/system/social-user/get?id=` + id })
 }
+
+// 获得绑定社交用户列表
+export const getBindSocialUserList = async () => {
+  return await request.get({ url: '/system/social-user/get-bind-list' })
+}

+ 5 - 0
src/api/system/tenant/index.ts

@@ -61,6 +61,11 @@ export const deleteTenant = (id: number) => {
   return request.delete({ url: '/system/tenant/delete?id=' + id })
 }
 
+// 批量删除租户
+export const deleteTenantList = (ids: number[]) => {
+  return request.delete({ url: '/system/tenant/delete-list', params: { ids: ids.join(',') } })
+}
+
 // 导出租户
 export const exportTenant = (params: TenantExportReqVO) => {
   return request.download({ url: '/system/tenant/export-excel', params })

+ 6 - 0
src/api/system/tenantPackage/index.ts

@@ -36,6 +36,12 @@ export const updateTenantPackage = (data: TenantPackageVO) => {
 export const deleteTenantPackage = (id: number) => {
   return request.delete({ url: '/system/tenant-package/delete?id=' + id })
 }
+
+// 批量删除租户套餐
+export const deleteTenantPackageList = (ids: number[]) => {
+  return request.delete({ url: '/system/tenant-package/delete-list', params: { ids: ids.join(',') } })
+}
+
 // 获取租户套餐精简信息列表
 export const getTenantPackageList = () => {
   return request.get({ url: '/system/tenant-package/simple-list' })

+ 6 - 1
src/api/system/user/index.ts

@@ -42,9 +42,14 @@ export const deleteUser = (id: number) => {
   return request.delete({ url: '/system/user/delete?id=' + id })
 }
 
+// 批量删除用户
+export const deleteUserList = (ids: number[]) => {
+  return request.delete({ url: '/system/user/delete-list', params: { ids: ids.join(',') } })
+}
+
 // 导出用户
 export const exportUser = (params: any) => {
-  return request.download({ url: '/system/user/export', params })
+  return request.download({ url: '/system/user/export-excel', params })
 }
 
 // 下载用户导入模板

+ 0 - 4
src/api/system/user/profile.ts

@@ -16,10 +16,6 @@ export interface ProfileVO {
     id: number
     name: string
   }[]
-  socialUsers: {
-    type: number
-    openid: string
-  }[]
   email: string
   mobile: string
   sex: number

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1 - 1
src/assets/svgs/pay/icon/mock.svg


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1 - 0
src/assets/svgs/pay/icon/wallet.svg


+ 7 - 3
src/components/AppLinkInput/AppLinkSelectDialog.vue

@@ -80,7 +80,8 @@ const activeAppLink = ref({} as AppLink)
 /** 打开弹窗 */
 const dialogVisible = ref(false)
 const open = (link: string) => {
-  activeAppLink.value.path = link
+  // 进入页面时先重置 activeAppLink
+  activeAppLink.value = { name: '', path: '' }
   dialogVisible.value = true
 
   // 滚动到当前的链接
@@ -102,8 +103,11 @@ defineExpose({ open })
 
 // 处理 APP 链接选中
 const handleAppLinkSelected = (appLink: AppLink) => {
+  // 只有不同链接时才更新(避免重复触发)
   if (!isSameLink(appLink.path, activeAppLink.value.path)) {
-    activeAppLink.value = appLink
+    // 如果新链接的 path 为空,则沿用当前 activeAppLink 的 path
+    const path = appLink.path || activeAppLink.value.path
+    activeAppLink.value = { ...appLink, path: path }
   }
   switch (appLink.type) {
     case APP_LINK_TYPE_ENUM.PRODUCT_CATEGORY_LIST:
@@ -170,7 +174,7 @@ const groupBtnRefs = ref<ButtonInstance[]>([])
 const scrollToGroupBtn = (group: string) => {
   const groupBtn = groupBtnRefs.value
     .map((btn: ButtonInstance) => btn['ref'])
-    .find((ref: Node) => ref.textContent === group)
+    .find((ref: HTMLButtonElement) => ref.textContent === group)
   if (groupBtn) {
     groupScrollbar.value?.setScrollTop(groupBtn.offsetTop)
   }

+ 0 - 1
src/components/Cropper/src/CopperModal.vue

@@ -181,7 +181,6 @@ function openModal() {
 }
 
 function closeModal() {
-  debugger
   dialogVisible.value = false
 }
 

+ 18 - 2
src/components/Dialog/src/Dialog.vue

@@ -1,9 +1,11 @@
 <script lang="ts" setup>
 import { propTypes } from '@/utils/propTypes'
 import { isNumber } from '@/utils/is'
+
 defineOptions({ name: 'Dialog' })
 
 const slots = useSlots()
+const emits = defineEmits(['update:modelValue'])
 
 const props = defineProps({
   modelValue: propTypes.bool.def(false),
@@ -55,6 +57,17 @@ const dialogStyle = computed(() => {
     height: unref(dialogHeight)
   }
 })
+
+const closing = ref(false)
+
+function closeHandler() {
+  emits('update:modelValue', false)
+  closing.value = true
+}
+
+function closedHandler() {
+  closing.value = false
+}
 </script>
 
 <template>
@@ -68,7 +81,8 @@ const dialogStyle = computed(() => {
     draggable
     class="com-dialog"
     :show-close="false"
-    @close="$emit('update:modelValue', false)"
+    @close="closeHandler"
+    @closed="closedHandler"
   >
     <template #header="{ close }">
       <div class="relative h-54px flex items-center justify-between pl-15px pr-15px">
@@ -102,7 +116,9 @@ const dialogStyle = computed(() => {
     </ElScrollbar>
     <slot v-else></slot>
     <template v-if="slots.footer" #footer>
-      <slot name="footer"></slot>
+      <div :style="{ 'pointer-events': closing ? 'none' : 'auto' }">
+        <slot name="footer"></slot>
+      </div>
     </template>
   </ElDialog>
 </template>

+ 3 - 0
src/components/DiyEditor/components/mobile/Carousel/config.ts

@@ -10,6 +10,8 @@ export interface CarouselProperty {
   autoplay: boolean
   // 播放间隔
   interval: number
+  // 轮播高度
+  height: number
   // 轮播内容
   items: CarouselItemProperty[]
   // 组件样式
@@ -37,6 +39,7 @@ export const component = {
     indicator: 'dot',
     autoplay: false,
     interval: 3,
+    height: 174,
     items: [
       { type: 'img', imgUrl: 'https://static.iocoder.cn/mall/banner-01.jpg', videoUrl: '' },
       { type: 'img', imgUrl: 'https://static.iocoder.cn/mall/banner-02.jpg', videoUrl: '' }

+ 1 - 1
src/components/DiyEditor/components/mobile/Carousel/index.vue

@@ -8,7 +8,7 @@
   </div>
   <div v-else class="relative">
     <el-carousel
-      height="174px"
+      :height="property.height + 'px'"
       :type="property.type === 'card' ? 'card' : ''"
       :autoplay="property.autoplay"
       :interval="property.interval * 1000"

+ 3 - 0
src/components/DiyEditor/components/mobile/Carousel/property.vue

@@ -16,6 +16,9 @@
             </el-tooltip>
           </el-radio-group>
         </el-form-item>
+        <el-form-item label="高度" prop="height">
+          <el-input-number class="!w-50% mr-10px" controls-position="right" v-model="formData.height" /> px
+        </el-form-item>
         <el-form-item label="指示器" prop="indicator">
           <el-radio-group v-model="formData.indicator">
             <el-radio value="dot">小圆点</el-radio>

+ 31 - 1
src/components/DiyEditor/components/mobile/NavigationBar/components/CellProperty.vue

@@ -13,7 +13,7 @@
   <template v-for="(cell, cellIndex) in cellList" :key="cellIndex">
     <template v-if="selectedHotAreaIndex === cellIndex">
       <el-form-item :prop="`cell[${cellIndex}].type`" label="类型">
-        <el-radio-group v-model="cell.type">
+        <el-radio-group v-model="cell.type" @change="handleHotAreaSelected(cell, cellIndex)">
           <el-radio value="text">文字</el-radio>
           <el-radio value="image">图片</el-radio>
           <el-radio value="search">搜索框</el-radio>
@@ -44,9 +44,32 @@
       </template>
       <!-- 3. 搜索框 -->
       <template v-else>
+        <el-form-item label="框体颜色" prop="backgroundColor">
+          <ColorInput v-model="cell.backgroundColor" />
+        </el-form-item>
+        <el-form-item class="lef" label="文本颜色" prop="textColor">
+          <ColorInput v-model="cell.textColor" />
+        </el-form-item>
         <el-form-item :prop="`cell[${cellIndex}].placeholder`" label="提示文字">
           <el-input v-model="cell.placeholder" maxlength="10" show-word-limit />
         </el-form-item>
+        <el-form-item label="文本位置" prop="placeholderPosition">
+          <el-radio-group v-model="cell!.placeholderPosition">
+            <el-tooltip content="居左" placement="top">
+              <el-radio-button value="left">
+                <Icon icon="ant-design:align-left-outlined" />
+              </el-radio-button>
+            </el-tooltip>
+            <el-tooltip content="居中" placement="top">
+              <el-radio-button value="center">
+                <Icon icon="ant-design:align-center-outlined" />
+              </el-radio-button>
+            </el-tooltip>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="扫一扫" prop="showScan">
+          <el-switch v-model="cell!.showScan" />
+        </el-form-item>
         <el-form-item :prop="`cell[${cellIndex}].borderRadius`" label="圆角">
           <el-slider
             v-model="cell.borderRadius"
@@ -88,10 +111,17 @@ const cellCount = computed(() => (props.isMp ? 6 : 8))
 const selectedHotAreaIndex = ref(0)
 const handleHotAreaSelected = (cellValue: NavigationBarCellProperty, index: number) => {
   selectedHotAreaIndex.value = index
+  // 默认设置为选中文字,并设置属性
   if (!cellValue.type) {
     cellValue.type = 'text'
     cellValue.textColor = '#111111'
   }
+  // 如果点击的是搜索框,则初始化搜索框的属性
+  if (cellValue.type === 'search') {
+    cellValue.placeholderPosition = 'left'
+    cellValue.backgroundColor = '#EEEEEE'
+    cellValue.textColor = '#969799'
+  }
 }
 </script>
 

+ 6 - 0
src/components/DiyEditor/components/mobile/NavigationBar/config.ts

@@ -45,8 +45,14 @@ export interface NavigationBarCellProperty {
   imgUrl: string
   // 图片链接
   url: string
+  // 搜索框:框体颜色
+  backgroundColor: string
   // 搜索框:提示文字
   placeholder: string
+  // 搜索框:提示文字位置
+  placeholderPosition: string
+  // 搜索框:是否显示扫一扫
+  showScan: boolean
   // 搜索框:边框圆角半径
   borderRadius: number
 }

+ 5 - 2
src/components/DiyEditor/components/mobile/NavigationBar/index.vue

@@ -54,9 +54,12 @@ const getCellStyle = (cell: NavigationBarCellProperty) => {
 const getSearchProp = computed(() => (cell: NavigationBarCellProperty) => {
   return {
     height: 30,
-    showScan: false,
+    backgroundColor: cell.backgroundColor,
+    showScan: cell.showScan,
     placeholder: cell.placeholder,
-    borderRadius: cell.borderRadius
+    borderRadius: cell.borderRadius,
+    textColor: cell.textColor,
+    placeholderPosition: cell.placeholderPosition
   } as SearchProperty
 })
 </script>

+ 5 - 0
src/components/DiyEditor/index.vue

@@ -269,6 +269,11 @@ watch(
     if (!val || selectedComponentIndex.value === -1) {
       return
     }
+    // 如果是基础设置页,默认选中的索引改成 -1,为了防止删除组件后切换到此页导致报错
+    // https://gitee.com/yudaocode/yudao-ui-admin-vue3/pulls/792
+    if (props.showTabBar) {
+      selectedComponentIndex.value = -1
+    }
     pageComponents.value[selectedComponentIndex.value] = selectedComponent.value!
   },
   { deep: true }

+ 1 - 0
src/components/Echart/src/Echart.vue

@@ -72,6 +72,7 @@ watch(
   (options) => {
     if (echartRef) {
       echartRef?.setOption(options)
+      echartRef?.resize()
     }
   },
   {

+ 10 - 0
src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/flowableDescriptor.json

@@ -1014,6 +1014,16 @@
           "name": "fields",
           "type": "Field",
           "isMany": true
+        },
+        {
+          "name": "id",
+          "type": "String",
+          "isAttr": true
+        },
+        {
+          "name": "eventDefinitions",
+          "type": "bpmn:TimerEventDefinition",
+          "isMany": true
         }
       ]
     },

+ 15 - 17
src/components/bpmnProcessDesigner/package/designer/plugins/translate/customTranslate.js

@@ -23,22 +23,20 @@
 
 export default function customTranslate(translations) {
   return function (template, replacements) {
-    replacements = replacements || {}
-    // Translate
-    template = translations[template] || template
+    replacements = replacements || {};
+    // 将模板和翻译字典的键统一转换为小写进行匹配
+    const lowerTemplate = template.toLowerCase();
+    const translation = Object.keys(translations).find(key => key.toLowerCase() === lowerTemplate);
 
-    // Replace
+    // 如果找到匹配的翻译,使用翻译后的模板
+    if (translation) {
+      template = translations[translation];
+    }
+
+    // 替换模板中的占位符
     return template.replace(/{([^}]+)}/g, function (_, key) {
-      let str = replacements[key]
-      if (
-        translations[replacements[key]] !== null &&
-        translations[replacements[key]] !== undefined
-      ) {
-        // eslint-disable-next-line no-mixed-spaces-and-tabs
-        str = translations[replacements[key]]
-        // eslint-disable-next-line no-mixed-spaces-and-tabs
-      }
-      return str || '{' + key + '}'
-    })
-  }
-}
+      // 如果替换值存在,返回替换值;否则返回原始占位符
+      return replacements[key] !== undefined ? replacements[key] : `{${key}}`;
+    });
+  };
+}

+ 2 - 2
src/components/bpmnProcessDesigner/package/designer/plugins/translate/zh.js

@@ -45,8 +45,8 @@ export default {
   'Service Task': '服务任务',
   'Script Task': '脚本任务',
   'Call Activity': '调用活动',
-  'Sub Process (collapsed)': '子流程(折叠的)',
-  'Sub Process (expanded)': '子流程(展开的)',
+  'Sub-Process (collapsed)': '子流程(折叠的)',
+  'Sub-Process (expanded)': '子流程(展开的)',
   'Start Event': '开始事件',
   StartEvent: '开始事件',
   'Intermediate Throw Event': '中间事件',

+ 54 - 1
src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue

@@ -68,6 +68,11 @@
           :business-object="elementBusinessObject"
         />
       </el-collapse-item>
+      <!-- 新增的时间事件配置项 -->
+      <el-collapse-item v-if="elementType === 'IntermediateCatchEvent'" name="timeEvent">
+        <template #title><Icon icon="ep:timer" />时间事件</template>
+        <TimeEventConfig :businessObject="bpmnElement.value?.businessObject" :key="elementId" />
+      </el-collapse-item>
     </el-collapse>
   </div>
 </template>
@@ -83,6 +88,8 @@ import ElementProperties from './properties/ElementProperties.vue'
 // import ElementForm from './form/ElementForm.vue'
 import UserTaskListeners from './listeners/UserTaskListeners.vue'
 import { getTaskCollapseItemName, isTaskCollapseItemShow } from './task/data'
+import TimeEventConfig from './time-event-config/TimeEventConfig.vue'
+import { ref, watch, onMounted } from 'vue'
 
 defineOptions({ name: 'MyPropertiesPanel' })
 
@@ -121,6 +128,8 @@ const formVisible = ref(false) // 表单配置
 const bpmnElement = ref()
 const isReady = ref(false)
 
+const type = ref('time')
+const condition = ref('')
 provide('prefix', props.prefix)
 provide('width', props.width)
 
@@ -141,7 +150,7 @@ const initBpmnInstances = () => {
     }
 
     // 检查所有实例是否都存在
-    const allInstancesExist = Object.values(instances).every(instance => instance)
+    const allInstancesExist = Object.values(instances).every((instance) => instance)
     if (allInstancesExist) {
       const w = window as any
       w.bpmnInstances = instances
@@ -255,4 +264,48 @@ watch(
     activeTab.value = 'base'
   }
 )
+
+function updateNode() {
+  const moddle = window.bpmnInstances?.moddle
+  const modeling = window.bpmnInstances?.modeling
+  const elementRegistry = window.bpmnInstances?.elementRegistry
+  if (!moddle || !modeling || !elementRegistry) return
+
+  const element = elementRegistry.get(props.businessObject.id)
+  if (!element) return
+
+  let timerDef = moddle.create('bpmn:TimerEventDefinition', {})
+  if (type.value === 'time') {
+    timerDef.timeDate = moddle.create('bpmn:FormalExpression', { body: condition.value })
+  } else if (type.value === 'duration') {
+    timerDef.timeDuration = moddle.create('bpmn:FormalExpression', { body: condition.value })
+  } else if (type.value === 'cycle') {
+    timerDef.timeCycle = moddle.create('bpmn:FormalExpression', { body: condition.value })
+  }
+
+  modeling.updateModdleProperties(element, element.businessObject, {
+    eventDefinitions: [timerDef]
+  })
+}
+
+// 初始化和监听
+function syncFromBusinessObject() {
+  if (props.businessObject) {
+    const timerDef = (props.businessObject.eventDefinitions || [])[0]
+    if (timerDef) {
+      if (timerDef.timeDate) {
+        type.value = 'time'
+        condition.value = timerDef.timeDate.body
+      } else if (timerDef.timeDuration) {
+        type.value = 'duration'
+        condition.value = timerDef.timeDuration.body
+      } else if (timerDef.timeCycle) {
+        type.value = 'cycle'
+        condition.value = timerDef.timeCycle.body
+      }
+    }
+  }
+}
+onMounted(syncFromBusinessObject)
+watch(() => props.businessObject, syncFromBusinessObject, { deep: true })
 </script>

+ 14 - 3
src/components/bpmnProcessDesigner/package/penal/custom-config/components/BoundaryEventTimer.vue

@@ -28,7 +28,12 @@
           v-model="timeDuration"
           :min="1"
           controls-position="right"
-          @change="() => updateTimeModdle()"
+          @change="
+            () => {
+              updateTimeModdle()
+              updateElementExtensions()
+            }
+          "
         />
       </el-form-item>
       <el-select
@@ -55,7 +60,12 @@
         v-model="maxRemindCount"
         :min="1"
         :max="10"
-        @change="() => updateTimeModdle()"
+        @change="
+          () => {
+            updateTimeModdle()
+            updateElementExtensions()
+          }
+        "
       />
     </el-form-item>
   </div>
@@ -65,7 +75,7 @@
 import {
   TimeUnitType,
   TIME_UNIT_TYPES,
-  TIMEOUT_HANDLER_TYPES,
+  TIMEOUT_HANDLER_TYPES
 } from '@/components/SimpleProcessDesignerV2/src/consts'
 import { convertTimeUnit } from '@/components/SimpleProcessDesignerV2/src/utils'
 
@@ -195,6 +205,7 @@ const onTimeUnitChange = () => {
     timeDuration.value = 1
   }
   updateTimeModdle()
+  updateElementExtensions()
 }
 
 const updateTimeModdle = () => {

+ 7 - 6
src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue

@@ -354,12 +354,13 @@ const resetTaskForm = () => {
 const changeCandidateStrategy = () => {
   userTaskForm.value.candidateParam = []
   deptLevel.value = 1
-  if (userTaskForm.value.candidateStrategy === CandidateStrategy.FORM_USER) {
-    // 特殊处理表单内用户字段,当只有发起人选项时应选中发起人
-    if (!userFieldOnFormOptions.value || userFieldOnFormOptions.value.length <= 1) {
-      userTaskForm.value.candidateStrategy = CandidateStrategy.START_USER
-    }
-  }
+  // 注释 by 芋艿:这个交互很多用户反馈费解,https://t.zsxq.com/xNmas 所以暂时屏蔽
+  // if (userTaskForm.value.candidateStrategy === CandidateStrategy.FORM_USER) {
+  //   // 特殊处理表单内用户字段,当只有发起人选项时应选中发起人
+  //   if (!userFieldOnFormOptions.value || userFieldOnFormOptions.value.length <= 1) {
+  //     userTaskForm.value.candidateStrategy = CandidateStrategy.START_USER
+  //   }
+  // }
   updateElementTask()
 }
 

+ 285 - 0
src/components/bpmnProcessDesigner/package/penal/time-event-config/CycleConfig.vue

@@ -0,0 +1,285 @@
+<template>
+  <el-tabs v-model="tab">
+    <el-tab-pane label="CRON表达式" name="cron">
+      <div style="margin-bottom: 10px">
+        <el-input
+          v-model="cronStr"
+          readonly
+          style="width: 400px; font-weight: bold"
+          :key="'cronStr'"
+        />
+      </div>
+      <div style="display: flex; gap: 8px; margin-bottom: 8px">
+        <el-input v-model="fields.second" placeholder="秒" style="width: 80px" :key="'second'" />
+        <el-input v-model="fields.minute" placeholder="分" style="width: 80px" :key="'minute'" />
+        <el-input v-model="fields.hour" placeholder="时" style="width: 80px" :key="'hour'" />
+        <el-input v-model="fields.day" placeholder="天" style="width: 80px" :key="'day'" />
+        <el-input v-model="fields.month" placeholder="月" style="width: 80px" :key="'month'" />
+        <el-input v-model="fields.week" placeholder="周" style="width: 80px" :key="'week'" />
+        <el-input v-model="fields.year" placeholder="年" style="width: 80px" :key="'year'" />
+      </div>
+      <el-tabs v-model="activeField" type="card" style="margin-bottom: 8px">
+        <el-tab-pane v-for="f in cronFieldList" :label="f.label" :name="f.key" :key="f.key">
+          <div style="margin-bottom: 8px">
+            <el-radio-group v-model="cronMode[f.key]" :key="'radio-' + f.key">
+              <el-radio label="every" :key="'every-' + f.key">每{{ f.label }}</el-radio>
+              <el-radio label="range" :key="'range-' + f.key"
+                >从
+                <el-input-number
+                  v-model="cronRange[f.key][0]"
+                  :min="f.min"
+                  :max="f.max"
+                  size="small"
+                  style="width: 60px"
+                  :key="'range0-' + f.key"
+                />
+                到
+                <el-input-number
+                  v-model="cronRange[f.key][1]"
+                  :min="f.min"
+                  :max="f.max"
+                  size="small"
+                  style="width: 60px"
+                  :key="'range1-' + f.key"
+                />
+                之间每{{ f.label }}</el-radio
+              >
+              <el-radio label="step" :key="'step-' + f.key"
+                >从第
+                <el-input-number
+                  v-model="cronStep[f.key][0]"
+                  :min="f.min"
+                  :max="f.max"
+                  size="small"
+                  style="width: 60px"
+                  :key="'step0-' + f.key"
+                />
+                开始每
+                <el-input-number
+                  v-model="cronStep[f.key][1]"
+                  :min="1"
+                  :max="f.max"
+                  size="small"
+                  style="width: 60px"
+                  :key="'step1-' + f.key"
+                />
+                {{ f.label }}</el-radio
+              >
+              <el-radio label="appoint" :key="'appoint-' + f.key">指定</el-radio>
+            </el-radio-group>
+          </div>
+          <div v-if="cronMode[f.key] === 'appoint'">
+            <el-checkbox-group v-model="cronAppoint[f.key]" :key="'group-' + f.key">
+              <el-checkbox
+                v-for="n in f.max + 1"
+                :label="pad(n - 1)"
+                :key="'cb-' + f.key + '-' + (n - 1)"
+                >{{ pad(n - 1) }}</el-checkbox
+              >
+            </el-checkbox-group>
+          </div>
+        </el-tab-pane>
+      </el-tabs>
+    </el-tab-pane>
+    <el-tab-pane label="标准格式" name="iso" :key="'iso-tab'">
+      <div style="margin-bottom: 10px">
+        <el-input
+          v-model="isoStr"
+          placeholder="如R1/2025-05-21T21:59:54/P3DT30M30S"
+          style="width: 400px; font-weight: bold"
+          :key="'isoStr'"
+        />
+      </div>
+      <div style="margin-bottom: 10px"
+        >循环次数:<el-input-number v-model="repeat" :min="1" style="width: 100px" :key="'repeat'"
+      /></div>
+      <div style="margin-bottom: 10px"
+        >日期时间:<el-date-picker
+          v-model="isoDate"
+          type="datetime"
+          placeholder="选择日期时间"
+          style="width: 200px"
+          :key="'isoDate'"
+      /></div>
+      <div style="margin-bottom: 10px"
+        >当前时长:<el-input
+          v-model="isoDuration"
+          placeholder="如P3DT30M30S"
+          style="width: 200px"
+          :key="'isoDuration'"
+      /></div>
+      <div>
+        <div
+          >秒:<el-button
+            v-for="s in [5, 10, 30, 50]"
+            @click="setDuration('S', s)"
+            :key="'sec-' + s"
+            >{{ s }}</el-button
+          >自定义</div
+        >
+        <div
+          >分:<el-button
+            v-for="m in [5, 10, 30, 50]"
+            @click="setDuration('M', m)"
+            :key="'min-' + m"
+            >{{ m }}</el-button
+          >自定义</div
+        >
+        <div
+          >小时:<el-button
+            v-for="h in [4, 8, 12, 24]"
+            @click="setDuration('H', h)"
+            :key="'hour-' + h"
+            >{{ h }}</el-button
+          >自定义</div
+        >
+        <div
+          >天:<el-button
+            v-for="d in [1, 2, 3, 4]"
+            @click="setDuration('D', d)"
+            :key="'day-' + d"
+            >{{ d }}</el-button
+          >自定义</div
+        >
+        <div
+          >月:<el-button
+            v-for="mo in [1, 2, 3, 4]"
+            @click="setDuration('M', mo)"
+            :key="'mon-' + mo"
+            >{{ mo }}</el-button
+          >自定义</div
+        >
+        <div
+          >年:<el-button
+            v-for="y in [1, 2, 3, 4]"
+            @click="setDuration('Y', y)"
+            :key="'year-' + y"
+            >{{ y }}</el-button
+          >自定义</div
+        >
+      </div>
+    </el-tab-pane>
+  </el-tabs>
+</template>
+<script setup>
+import { ref, watch, computed } from 'vue'
+const props = defineProps({ value: String })
+const emit = defineEmits(['change'])
+
+const tab = ref('cron')
+const cronStr = ref(props.value || '* * * * * ?')
+const fields = ref({
+  second: '*',
+  minute: '*',
+  hour: '*',
+  day: '*',
+  month: '*',
+  week: '?',
+  year: ''
+})
+const cronFieldList = [
+  { key: 'second', label: '秒', min: 0, max: 59 },
+  { key: 'minute', label: '分', min: 0, max: 59 },
+  { key: 'hour', label: '时', min: 0, max: 23 },
+  { key: 'day', label: '天', min: 1, max: 31 },
+  { key: 'month', label: '月', min: 1, max: 12 },
+  { key: 'week', label: '周', min: 1, max: 7 },
+  { key: 'year', label: '年', min: 1970, max: 2099 }
+]
+const activeField = ref('second')
+const cronMode = ref({
+  second: 'appoint',
+  minute: 'every',
+  hour: 'every',
+  day: 'every',
+  month: 'every',
+  week: 'every',
+  year: 'every'
+})
+const cronAppoint = ref({
+  second: ['00', '01'],
+  minute: [],
+  hour: [],
+  day: [],
+  month: [],
+  week: [],
+  year: []
+})
+const cronRange = ref({
+  second: [0, 1],
+  minute: [0, 1],
+  hour: [0, 1],
+  day: [1, 2],
+  month: [1, 2],
+  week: [1, 2],
+  year: [1970, 1971]
+})
+const cronStep = ref({
+  second: [1, 1],
+  minute: [1, 1],
+  hour: [1, 1],
+  day: [1, 1],
+  month: [1, 1],
+  week: [1, 1],
+  year: [1970, 1]
+})
+
+function pad(n) {
+  return n < 10 ? '0' + n : '' + n
+}
+
+watch(
+  [fields, cronMode, cronAppoint, cronRange, cronStep],
+  () => {
+    // 组装cron表达式
+    let arr = cronFieldList.map((f) => {
+      if (cronMode.value[f.key] === 'every') return '*'
+      if (cronMode.value[f.key] === 'appoint') return cronAppoint.value[f.key].join(',') || '*'
+      if (cronMode.value[f.key] === 'range')
+        return `${cronRange.value[f.key][0]}-${cronRange.value[f.key][1]}`
+      if (cronMode.value[f.key] === 'step')
+        return `${cronStep.value[f.key][0]}/${cronStep.value[f.key][1]}`
+      return fields.value[f.key] || '*'
+    })
+    // week和year特殊处理
+    arr[5] = arr[5] || '?'
+    cronStr.value = arr.join(' ')
+    if (tab.value === 'cron') emit('change', cronStr.value)
+  },
+  { deep: true }
+)
+
+// 标准格式
+const isoStr = ref('')
+const repeat = ref(1)
+const isoDate = ref('')
+const isoDuration = ref('')
+function setDuration(type, val) {
+  // 组装ISO 8601字符串
+  let d = isoDuration.value
+  if (!d.includes(type)) d += val + type
+  else d = d.replace(new RegExp(`\\d+${type}`), val + type)
+  isoDuration.value = d
+  updateIsoStr()
+}
+function updateIsoStr() {
+  let str = `R${repeat.value}`
+  if (isoDate.value)
+    str +=
+      '/' +
+      (typeof isoDate.value === 'string' ? isoDate.value : new Date(isoDate.value).toISOString())
+  if (isoDuration.value) str += '/' + isoDuration.value
+  isoStr.value = str
+  if (tab.value === 'iso') emit('change', isoStr.value)
+}
+watch([repeat, isoDate, isoDuration], updateIsoStr)
+watch(
+  () => props.value,
+  (val) => {
+    if (!val) return
+    if (tab.value === 'cron') cronStr.value = val
+    if (tab.value === 'iso') isoStr.value = val
+  },
+  { immediate: true }
+)
+</script>

+ 86 - 0
src/components/bpmnProcessDesigner/package/penal/time-event-config/DurationConfig.vue

@@ -0,0 +1,86 @@
+<template>
+  <div>
+    <div style="margin-bottom: 10px"
+      >当前选择:<el-input v-model="isoString" readonly style="width: 300px"
+    /></div>
+    <div v-for="unit in units" :key="unit.key" style="margin-bottom: 8px">
+      <span>{{ unit.label }}:</span>
+      <el-button-group>
+        <el-button
+          v-for="val in unit.presets"
+          :key="val"
+          size="mini"
+          @click="setUnit(unit.key, val)"
+          >{{ val }}</el-button
+        >
+        <el-input
+          v-model.number="custom[unit.key]"
+          size="mini"
+          style="width: 60px; margin-left: 8px"
+          placeholder="自定义"
+          @change="setUnit(unit.key, custom[unit.key])"
+        />
+      </el-button-group>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, watch, computed } from 'vue'
+const props = defineProps({ value: String })
+const emit = defineEmits(['change'])
+
+const units = [
+  { key: 'Y', label: '年', presets: [1, 2, 3, 4] },
+  { key: 'M', label: '月', presets: [1, 2, 3, 4] },
+  { key: 'D', label: '天', presets: [1, 2, 3, 4] },
+  { key: 'H', label: '时', presets: [4, 8, 12, 24] },
+  { key: 'm', label: '分', presets: [5, 10, 30, 50] },
+  { key: 'S', label: '秒', presets: [5, 10, 30, 50] }
+]
+const custom = ref({ Y: '', M: '', D: '', H: '', m: '', S: '' })
+const isoString = ref('')
+
+function setUnit(key, val) {
+  if (!val || isNaN(val)) {
+    custom.value[key] = ''
+    return
+  }
+  custom.value[key] = val
+  updateIsoString()
+}
+
+function updateIsoString() {
+  let str = 'P'
+  if (custom.value.Y) str += custom.value.Y + 'Y'
+  if (custom.value.M) str += custom.value.M + 'M'
+  if (custom.value.D) str += custom.value.D + 'D'
+  if (custom.value.H || custom.value.m || custom.value.S) str += 'T'
+  if (custom.value.H) str += custom.value.H + 'H'
+  if (custom.value.m) str += custom.value.m + 'M'
+  if (custom.value.S) str += custom.value.S + 'S'
+  isoString.value = str === 'P' ? '' : str
+  emit('change', isoString.value)
+}
+
+watch(
+  () => props.value,
+  (val) => {
+    if (!val) return
+    // 解析ISO 8601字符串到custom
+    const match = val.match(
+      /^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/
+    )
+    if (match) {
+      custom.value.Y = match[1] || ''
+      custom.value.M = match[2] || ''
+      custom.value.D = match[3] || ''
+      custom.value.H = match[4] || ''
+      custom.value.m = match[5] || ''
+      custom.value.S = match[6] || ''
+      updateIsoString()
+    }
+  },
+  { immediate: true }
+)
+</script>

+ 312 - 0
src/components/bpmnProcessDesigner/package/penal/time-event-config/TimeEventConfig.vue

@@ -0,0 +1,312 @@
+<template>
+  <div class="panel-tab__content">
+    <div style="margin-top: 10px">
+      <span>类型:</span>
+      <el-button-group>
+        <el-button size="mini" :type="type === 'time' ? 'primary' : ''" @click="setType('time')"
+          >时间</el-button
+        >
+        <el-button
+          size="mini"
+          :type="type === 'duration' ? 'primary' : ''"
+          @click="setType('duration')"
+          >持续</el-button
+        >
+        <el-button size="mini" :type="type === 'cycle' ? 'primary' : ''" @click="setType('cycle')"
+          >循环</el-button
+        >
+      </el-button-group>
+      <el-icon v-if="valid" color="green" style="margin-left: 8px"><CircleCheckFilled /></el-icon>
+    </div>
+    <div style="margin-top: 10px; display: flex; align-items: center">
+      <span>条件:</span>
+      <el-input
+        v-model="condition"
+        :placeholder="placeholder"
+        style="width: calc(100% - 100px)"
+        :readonly="type !== 'duration' && type !== 'cycle'"
+        @focus="handleInputFocus"
+        @blur="updateNode"
+      >
+        <template #suffix>
+          <el-tooltip v-if="!valid" content="格式错误" placement="top">
+            <el-icon color="orange"><WarningFilled /></el-icon>
+          </el-tooltip>
+          <el-tooltip :content="helpText" placement="top">
+            <el-icon color="#409EFF" style="cursor: pointer" @click="showHelp = true"
+              ><QuestionFilled
+            /></el-icon>
+          </el-tooltip>
+          <el-button
+            v-if="type === 'time'"
+            @click="showDatePicker = true"
+            style="margin-left: 4px"
+            circle
+            size="small"
+          >
+            <Icon icon="ep:calendar" />
+          </el-button>
+          <el-button
+            v-if="type === 'duration'"
+            @click="showDurationDialog = true"
+            style="margin-left: 4px"
+            circle
+            size="small"
+          >
+            <Icon icon="ep:timer" />
+          </el-button>
+          <el-button
+            v-if="type === 'cycle'"
+            @click="showCycleDialog = true"
+            style="margin-left: 4px"
+            circle
+            size="small"
+          >
+            <Icon icon="ep:setting" />
+          </el-button>
+        </template>
+      </el-input>
+    </div>
+    <!-- 时间选择器 -->
+    <el-dialog
+      v-model="showDatePicker"
+      title="选择时间"
+      width="400px"
+      @close="showDatePicker = false"
+    >
+      <el-date-picker
+        v-model="dateValue"
+        type="datetime"
+        placeholder="选择日期时间"
+        style="width: 100%"
+        @change="onDateChange"
+      />
+      <template #footer>
+        <el-button @click="showDatePicker = false">取消</el-button>
+        <el-button type="primary" @click="onDateConfirm">确定</el-button>
+      </template>
+    </el-dialog>
+    <!-- 持续时长选择器 -->
+    <el-dialog
+      v-model="showDurationDialog"
+      title="时间配置"
+      width="600px"
+      @close="showDurationDialog = false"
+    >
+      <DurationConfig :value="condition" @change="onDurationChange" />
+      <template #footer>
+        <el-button @click="showDurationDialog = false">取消</el-button>
+        <el-button type="primary" @click="onDurationConfirm">确定</el-button>
+      </template>
+    </el-dialog>
+    <!-- 循环配置器 -->
+    <el-dialog
+      v-model="showCycleDialog"
+      title="时间配置"
+      width="800px"
+      @close="showCycleDialog = false"
+    >
+      <CycleConfig :value="condition" @change="onCycleChange" />
+      <template #footer>
+        <el-button @click="showCycleDialog = false">取消</el-button>
+        <el-button type="primary" @click="onCycleConfirm">确定</el-button>
+      </template>
+    </el-dialog>
+    <!-- 帮助说明 -->
+    <el-dialog v-model="showHelp" title="格式说明" width="600px" @close="showHelp = false">
+      <div v-html="helpHtml"></div>
+      <template #footer>
+        <el-button @click="showHelp = false">关闭</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, computed, watch, onMounted } from 'vue'
+import { CircleCheckFilled, WarningFilled, QuestionFilled } from '@element-plus/icons-vue'
+import DurationConfig from './DurationConfig.vue'
+import CycleConfig from './CycleConfig.vue'
+import { createListenerObject, updateElementExtensions } from '../../utils'
+const bpmnInstances = () => (window as any).bpmnInstances
+const props = defineProps({ businessObject: Object })
+const type = ref('time')
+const condition = ref('')
+const valid = ref(true)
+const showDatePicker = ref(false)
+const showDurationDialog = ref(false)
+const showCycleDialog = ref(false)
+const showHelp = ref(false)
+const dateValue = ref(null)
+const bpmnElement = ref(null)
+
+const placeholder = computed(() => {
+  if (type.value === 'time') return '请输入时间'
+  if (type.value === 'duration') return '请输入持续时长'
+  if (type.value === 'cycle') return '请输入循环表达式'
+  return ''
+})
+const helpText = computed(() => {
+  if (type.value === 'time') return '选择具体时间'
+  if (type.value === 'duration') return 'ISO 8601格式,如PT1H'
+  if (type.value === 'cycle') return 'CRON表达式或ISO 8601周期'
+  return ''
+})
+const helpHtml = computed(() => {
+  if (type.value === 'duration') {
+    return `指定定时器之前要等待多长时间。S表示秒,M表示分,D表示天;P表示时间段,T表示精确到时间的时间段。<br>
+    时间格式依然为ISO 8601格式,一年两个月三天四小时五分六秒内,可以写成P1Y2M3DT4H5M6S。<br>
+    P是开始标记,T是时间和日期分割标记,没有日期只有时间T是不能省去的,比如1小时执行一次应写成PT1H。`
+  }
+  if (type.value === 'cycle') {
+    return `支持CRON表达式(如0 0/30 * * * ?)或ISO 8601周期(如R3/PT10M)。`
+  }
+  return ''
+})
+
+// 初始化和监听
+function syncFromBusinessObject() {
+  if (props.businessObject) {
+    const timerDef = (props.businessObject.eventDefinitions || [])[0]
+    if (timerDef) {
+      if (timerDef.timeDate) {
+        type.value = 'time'
+        condition.value = timerDef.timeDate.body
+      } else if (timerDef.timeDuration) {
+        type.value = 'duration'
+        condition.value = timerDef.timeDuration.body
+      } else if (timerDef.timeCycle) {
+        type.value = 'cycle'
+        condition.value = timerDef.timeCycle.body
+      }
+    }
+  }
+}
+onMounted(syncFromBusinessObject)
+
+// 切换类型
+function setType(t) {
+  type.value = t
+  condition.value = ''
+  updateNode()
+}
+
+// 输入校验
+watch([type, condition], () => {
+  valid.value = validate()
+  // updateNode() // 可以注释掉,避免频繁触发
+})
+
+function validate() {
+  if (type.value === 'time') {
+    return !!condition.value && !isNaN(Date.parse(condition.value))
+  }
+  if (type.value === 'duration') {
+    return /^P.*$/.test(condition.value)
+  }
+  if (type.value === 'cycle') {
+    return /^([0-9*\/?, ]+|R\d*\/P.*)$/.test(condition.value)
+  }
+  return true
+}
+
+// 选择时间
+function onDateChange(val) {
+  dateValue.value = val
+}
+function onDateConfirm() {
+  if (dateValue.value) {
+    condition.value = new Date(dateValue.value).toISOString()
+    showDatePicker.value = false
+    updateNode()
+  }
+}
+
+// 持续时长
+function onDurationChange(val) {
+  condition.value = val
+}
+function onDurationConfirm() {
+  showDurationDialog.value = false
+  updateNode()
+}
+
+// 循环
+function onCycleChange(val) {
+  condition.value = val
+}
+function onCycleConfirm() {
+  showCycleDialog.value = false
+  updateNode()
+}
+
+// 输入框聚焦时弹窗(可选)
+function handleInputFocus() {
+  if (type.value === 'time') showDatePicker.value = true
+  if (type.value === 'duration') showDurationDialog.value = true
+  if (type.value === 'cycle') showCycleDialog.value = true
+}
+
+// 同步到节点
+function updateNode() {
+  const moddle = window.bpmnInstances?.moddle
+  const modeling = window.bpmnInstances?.modeling
+  const elementRegistry = window.bpmnInstances?.elementRegistry
+  if (!moddle || !modeling || !elementRegistry) return
+
+  // 获取元素
+  if (!props.businessObject || !props.businessObject.id) return
+  const element = elementRegistry.get(props.businessObject.id)
+  if (!element) return
+
+  // 1. 复用原有 timerDef,或新建
+  let timerDef =
+    element.businessObject.eventDefinitions && element.businessObject.eventDefinitions[0]
+  if (!timerDef) {
+    timerDef = bpmnInstances().bpmnFactory.create('bpmn:TimerEventDefinition', {})
+    modeling.updateProperties(element, {
+      eventDefinitions: [timerDef]
+    })
+  }
+
+  // 2. 清空原有
+  delete timerDef.timeDate
+  delete timerDef.timeDuration
+  delete timerDef.timeCycle
+
+  // 3. 设置新的
+  if (type.value === 'time' && condition.value) {
+    timerDef.timeDate = bpmnInstances().bpmnFactory.create('bpmn:FormalExpression', {
+      body: condition.value
+    })
+  } else if (type.value === 'duration' && condition.value) {
+    timerDef.timeDuration = bpmnInstances().bpmnFactory.create('bpmn:FormalExpression', {
+      body: condition.value
+    })
+  } else if (type.value === 'cycle' && condition.value) {
+    timerDef.timeCycle = bpmnInstances().bpmnFactory.create('bpmn:FormalExpression', {
+      body: condition.value
+    })
+  }
+
+  bpmnInstances().modeling.updateProperties(toRaw(element), {
+    eventDefinitions: [timerDef]
+  })
+}
+
+watch(
+  () => props.businessObject,
+  (val) => {
+    if (val) {
+      nextTick(() => {
+        syncFromBusinessObject()
+      })
+    }
+  },
+  { immediate: true }
+)
+</script>
+
+<style scoped>
+/* 相关样式 */
+</style>

+ 3 - 1
src/plugins/formCreate/index.ts

@@ -55,6 +55,7 @@ import {
   ElCollapse,
   ElCollapseItem,
   ElCard,
+  ElTreeSelect
   // ElFormItem,
   // ElOption
 } from 'element-plus'
@@ -97,6 +98,7 @@ const components = [
   ElTableColumn,
   ElTabPane,
   ElTabs,
+  ElTreeSelect,
   ElDropdown,
   ElDropdownMenu,
   ElDropdownItem,
@@ -119,7 +121,7 @@ const components = [
   Editor,
   ElCollapse,
   ElCollapseItem,
-  ElCard,
+  ElCard
 ]
 
 // 参考 http://www.form-create.com/v3/element-ui/auto-import.html 文档

+ 1 - 1
src/router/index.ts

@@ -12,7 +12,7 @@ const router = createRouter({
 })
 
 export const resetRouter = (): void => {
-  const resetWhiteNameList = ['Redirect', 'Login', 'NoFind', 'Root']
+  const resetWhiteNameList = ['Redirect', 'Login', 'NoFound', 'Home']
   router.getRoutes().forEach((route) => {
     const { name } = route
     if (name && !resetWhiteNameList.includes(name as string)) {

+ 41 - 4
src/store/modules/app.ts

@@ -1,11 +1,12 @@
-import { defineStore } from 'pinia'
-import { store } from '../index'
-import { humpToUnderline, setCssVar } from '@/utils'
-import { ElMessage } from 'element-plus'
 import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
 import { ElementPlusSize } from '@/types/elementPlus'
 import { LayoutType } from '@/types/layout'
 import { ThemeTypes } from '@/types/theme'
+import { humpToUnderline, setCssVar } from '@/utils'
+import { getCssColorVariable, hexToRGB, mix } from '@/utils/color'
+import { ElMessage } from 'element-plus'
+import { defineStore } from 'pinia'
+import { store } from '../index'
 
 const { wsCache } = useCache()
 
@@ -183,6 +184,40 @@ export const useAppStore = defineStore('app', {
     }
   },
   actions: {
+    setPrimaryLight() {
+      if (this.theme.elColorPrimary) {
+        const elColorPrimary = this.theme.elColorPrimary
+        const color = this.isDark ? '#000000' : '#ffffff'
+        const lightList = [3, 5, 7, 8, 9]
+        lightList.forEach((v) => {
+          setCssVar(`--el-color-primary-light-${v}`, mix(color, elColorPrimary, v / 10))
+        })
+        setCssVar(`--el-color-primary-dark-2`, mix(color, elColorPrimary, 0.2))
+
+        this.setAllColorRgbVars()
+      }
+    },
+
+    // 处理element自带的主题色和辅助色的-rgb切换主题变化,如:--el-color-primary-rgb
+    setAllColorRgbVars() {
+      // 需要处理的颜色类型列表
+      const colorTypes = ['primary', 'success', 'warning', 'danger', 'error', 'info']
+
+      colorTypes.forEach((type) => {
+        // 获取当前颜色值
+        const colorValue = getCssColorVariable(`--el-color-${type}`)
+        if (colorValue) {
+          // 转换为rgba并提取RGB部分
+          const rgbaString = hexToRGB(colorValue, 1)
+          const rgbValues = rgbaString.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/i)
+          if (rgbValues) {
+            const [, r, g, b] = rgbValues
+            // 设置对应的RGB变量
+            setCssVar(`--el-color-${type}-rgb`, `${r}, ${g}, ${b}`)
+          }
+        }
+      })
+    },
     setBreadcrumb(breadcrumb: boolean) {
       this.breadcrumb = breadcrumb
     },
@@ -256,6 +291,7 @@ export const useAppStore = defineStore('app', {
         document.documentElement.classList.remove('dark')
       }
       wsCache.set(CACHE_KEY.IS_DARK, this.isDark)
+      this.setPrimaryLight()
     },
     setCurrentSize(currentSize: ElementPlusSize) {
       this.currentSize = currentSize
@@ -272,6 +308,7 @@ export const useAppStore = defineStore('app', {
       for (const key in this.theme) {
         setCssVar(`--${humpToUnderline(key)}`, this.theme[key])
       }
+      this.setPrimaryLight()
     },
     setFooter(footer: boolean) {
       this.footer = footer

+ 1 - 1
src/store/modules/user.ts

@@ -62,7 +62,7 @@ export const useUserStore = defineStore('admin-user', {
           userInfo = await getInfo()
         } catch (error) {}
       }
-      this.permissions = new Set(userInfo.permissions)
+      this.permissions = new Set(userInfo.permissions || []) // 兜底为 [] https://t.zsxq.com/xCJew
       this.roles = userInfo.roles
       this.user = userInfo.user
       this.isSetUser = true

+ 43 - 0
src/utils/color.ts

@@ -172,3 +172,46 @@ export const PREDEFINE_COLORS = [
   '#1f73c3',
   '#711f57'
 ]
+
+
+/**
+ * Mixes two colors.
+ *
+ * @param {string} color1 - The first color, should be a 6-digit hexadecimal color code starting with `#`.
+ * @param {string} color2 - The second color, should be a 6-digit hexadecimal color code starting with `#`.
+ * @param {number} [weight=0.5] - The weight of color1 in the mix, should be a number between 0 and 1, where 0 represents 100% of color2, and 1 represents 100% of color1.
+ * @returns {string} The mixed color, a 6-digit hexadecimal color code starting with `#`.
+ */
+export const mix = (color1: string, color2: string, weight: number = 0.5): string => {
+  let color = '#'
+  for (let i = 0; i <= 2; i++) {
+    const c1 = parseInt(color1.substring(1 + i * 2, 3 + i * 2), 16)
+    const c2 = parseInt(color2.substring(1 + i * 2, 3 + i * 2), 16)
+    const c = Math.round(c1 * weight + c2 * (1 - weight))
+    color += c.toString(16).padStart(2, '0')
+  }
+  return color
+}
+
+/**
+ * getCssColorVariable
+ * @description 获取css变量的颜色值
+ * @param colorVariable css变量名
+ * @param opacity 透明度
+ * @returns {string} 颜色值
+ * @example getCssColorVariable('--el-color-primary', 0.5)
+ * @example getCssColorVariable('--el-color-primary')
+ * @example getCssColorVariable()
+ */
+export const getCssColorVariable = (
+  colorVariable: string = '--el-color-primary',
+  opacity?: number
+) => {
+  const colorValue = getComputedStyle(document.documentElement)
+    .getPropertyValue(colorVariable)
+    .trim()
+  if (colorValue) {
+    return opacity ? hexToRGB(colorValue, opacity) : colorValue
+  }
+  return ''
+}

+ 149 - 2
src/utils/formCreate.ts

@@ -1,6 +1,7 @@
 /**
  * 针对 https://github.com/xaboy/form-create-designer 封装的工具类
  */
+import { isRef } from 'vue'
 
 // 编码表单 Conf
 export const encodeConf = (designerRef: object) => {
@@ -47,10 +48,156 @@ export const setConfAndFields2 = (
     // @ts-ignore
     detailPreview = detailPreview.value
   }
+
+  // 修复所有函数类型(解决设计器保存后函数变成字符串的问题)。例如说:
+  // https://t.zsxq.com/rADff
+  // https://t.zsxq.com/ZfbGt
+  // https://t.zsxq.com/mHOoj
+  // https://t.zsxq.com/BSylB
+  const option = JSON.parse(conf)
+  const rule = decodeFields(fields)
+  // 🔧 修复所有函数类型 - 解决设计器保存后函数变成字符串的问题
+  const fixFunctions = (obj: any) => {
+    if (obj && typeof obj === 'object') {
+      Object.keys(obj).forEach((key) => {
+        // 检查是否是函数相关的属性
+        if (isFunctionProperty(key)) {
+          // 如果不是函数类型,重新构建为函数
+          if (typeof obj[key] !== 'function') {
+            obj[key] = createDefaultFunction(key)
+          }
+        } else if (typeof obj[key] === 'object' && obj[key] !== null) {
+          // 递归处理嵌套对象
+          fixFunctions(obj[key])
+        }
+      })
+    }
+  }
+  // 判断是否是函数属性
+  const isFunctionProperty = (key: string): boolean => {
+    const functionKeys = [
+      'beforeFetch', // 请求前处理
+      'afterFetch', // 请求后处理
+      'onSubmit', // 表单提交
+      'onReset', // 表单重置
+      'onChange', // 值变化
+      'onInput', // 输入事件
+      'onClick', // 点击事件
+      'onFocus', // 获取焦点
+      'onBlur', // 失去焦点
+      'onMounted', // 组件挂载
+      'onCreated', // 组件创建
+      'onReload', // 重新加载
+      'remoteMethod', // 远程搜索方法
+      'parseFunc', // 解析函数
+      'validator', // 验证器
+      'asyncValidator', // 异步验证器
+      'formatter', // 格式化函数
+      'parser', // 解析函数
+      'beforeUpload', // 上传前处理
+      'onSuccess', // 成功回调
+      'onError', // 错误回调
+      'onProgress', // 进度回调
+      'onPreview', // 预览回调
+      'onRemove', // 移除回调
+      'onExceed', // 超出限制回调
+      'filterMethod', // 过滤方法
+      'sortMethod', // 排序方法
+      'loadData', // 加载数据
+      'renderContent', // 渲染内容
+      'render' // 渲染函数
+    ]
+    // 检查是否以函数相关前缀开头
+    const functionPrefixes = ['on', 'before', 'after', 'handle']
+    return functionKeys.includes(key) || functionPrefixes.some((prefix) => key.startsWith(prefix))
+  }
+  // 根据函数名创建默认函数
+  const createDefaultFunction = (key: string): Function => {
+    switch (key) {
+      case 'beforeFetch':
+        return (config: any) => {
+          // 添加 Token 认证头。例如说:
+          // https://t.zsxq.com/hK3FO
+          const token = localStorage.getItem('token')
+          if (token) {
+            config.headers = {
+              ...config.headers,
+              Authorization: 'Bearer ' + token
+            }
+          }
+          // 添加通用请求头
+          config.headers = {
+            ...config.headers,
+            'Content-Type': 'application/json',
+            'X-Requested-With': 'XMLHttpRequest'
+          }
+          // 添加时间戳防止缓存
+          config.params = {
+            ...config.params,
+            _t: Date.now()
+          }
+          return config
+        }
+      case 'afterFetch':
+        return (data: any) => {
+          return data
+        }
+      case 'onSubmit':
+        return (_formData: any) => {
+          return true
+        }
+      case 'onReset':
+        return () => {
+          return true
+        }
+      case 'onChange':
+        return (_value: any, _oldValue: any) => {}
+      case 'remoteMethod':
+        return (query: string) => {
+          console.log('remoteMethod被调用:', query)
+        }
+      case 'parseFunc':
+        return (data: any) => {
+          // 默认解析逻辑:如果是数组直接返回,否则尝试获取list属性
+          if (Array.isArray(data)) {
+            return data
+          }
+          return data?.list || data?.data || []
+        }
+      case 'validator':
+        return (_rule: any, _value: any, callback: Function) => {
+          callback()
+        }
+      case 'beforeUpload':
+        return (_file: any) => {
+          return true
+        }
+      default:
+        // 通用默认函数
+        return (...args: any[]) => {
+          // 对于事件处理函数,返回true表示继续执行
+          if (key.startsWith('on') || key.startsWith('handle')) {
+            return true
+          }
+          // 对于其他函数,返回第一个参数(通常是数据传递)
+          return args[0]
+        }
+    }
+  }
+  // 修复 option 中的所有函数
+  fixFunctions(option)
+  // 修复 rule 中的所有函数(包括组件的 props)
+  if (Array.isArray(rule)) {
+    rule.forEach((item: any) => {
+      fixFunctions(item)
+    })
+  }
+
   // @ts-ignore
-  detailPreview.option = JSON.parse(conf)
+  detailPreview.option = option
   // @ts-ignore
-  detailPreview.rule = decodeFields(fields)
+  detailPreview.rule = rule
+
   if (value) {
     // @ts-ignore
     detailPreview.value = value

+ 4 - 3
src/utils/routerHelper.ts

@@ -101,7 +101,7 @@ export const generateRoute = (routes: AppCustomRouteRecordRaw[]): AppRouteRecord
     if (!route.children && route.parentId == 0 && route.component) {
       data.component = Layout
       data.meta = {
-        hidden: meta.hidden,
+        hidden: meta.hidden
       }
       data.name = toCamelCase(route.path, true) + 'Parent'
       data.redirect = ''
@@ -170,8 +170,9 @@ const generateRoutePath = (parentPath: string, path: string) => {
 }
 export const pathResolve = (parentPath: string, path: string) => {
   if (isUrl(path)) return path
-  const childPath = path.startsWith('/') || !path ? path : `/${path}`
-  return `${parentPath}${childPath}`.replace(/\/\//g, '/')
+  if (!path) return parentPath // 修复 path 为空时返回 parentPath,避免拼接出错 https://t.zsxq.com/QVr6b
+  const childPath = path.startsWith('/') ? path : `/${path}`
+  return `${parentPath}${childPath}`.replace(/\/+/g, '/')
 }
 
 // 路由降级

+ 11 - 6
src/views/Home/Index.vue

@@ -83,13 +83,18 @@
               :sm="24"
               :xs="24"
             >
-              <el-card 
-                shadow="hover" 
+              <el-card
+                shadow="hover"
                 class="mr-5px mt-5px cursor-pointer"
                 @click="handleProjectClick(item.message)"
               >
                 <div class="flex items-center">
-                  <Icon :icon="item.icon" :size="25" class="mr-8px" :style="{ color: item.color }" />
+                  <Icon
+                    :icon="item.icon"
+                    :size="25"
+                    class="mr-8px"
+                    :style="{ color: item.color }"
+                  />
                   <span class="text-16px">{{ item.name }}</span>
                 </div>
                 <div class="mt-12px text-12px text-gray-400">{{ t(item.message) }}</div>
@@ -181,17 +186,17 @@ import { EChartsOption } from 'echarts'
 import { formatTime } from '@/utils'
 
 import { useUserStore } from '@/store/modules/user'
-import { useWatermark } from '@/hooks/web/useWatermark'
+// import { useWatermark } from '@/hooks/web/useWatermark'
 import type { WorkplaceTotal, Project, Notice, Shortcut } from './types'
 import { pieOptions, barOptions } from './echarts-data'
 import { useRouter } from 'vue-router'
 
-defineOptions({ name: 'Home' })
+defineOptions({ name: 'Index' })
 
 const { t } = useI18n()
 const router = useRouter()
 const userStore = useUserStore()
-const { setWatermark } = useWatermark()
+// const { setWatermark } = useWatermark()
 const loading = ref(true)
 const avatar = userStore.getUser.avatar
 const username = userStore.getUser.nickname

+ 1 - 1
src/views/Login/Login.vue

@@ -118,4 +118,4 @@ $prefix-cls: #{$namespace}-login;
     background-color: var(--login-bg-color);
   }
 }
-</style>
+</style>

+ 18 - 18
src/views/Login/components/ForgetPasswordForm.vue

@@ -9,14 +9,14 @@
     label-width="120px"
     size="large"
   >
-    <el-row style="margin-right: -10px; margin-left: -10px">
+    <el-row class="mx-[-10px]">
       <!-- 租户名 -->
-      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+      <el-col :span="24" class="px-10px">
         <el-form-item>
-          <LoginFormTitle style="width: 100%" />
+          <LoginFormTitle class="w-full" />
         </el-form-item>
       </el-col>
-      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+      <el-col :span="24" class="px-10px">
         <el-form-item v-if="resetPasswordData.tenantEnable === 'true'" prop="tenantName">
           <el-input
             v-model="resetPasswordData.tenantName"
@@ -28,7 +28,7 @@
         </el-form-item>
       </el-col>
       <!-- 手机号 -->
-      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+      <el-col :span="24" class="px-10px">
         <el-form-item prop="mobile">
           <el-input
             v-model="resetPasswordData.mobile"
@@ -45,7 +45,7 @@
         @success="getSmsCode"
       />
       <!-- 验证码 -->
-      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+      <el-col :span="24" class="px-10px">
         <el-form-item prop="code">
           <el-row :gutter="5" justify="space-between" style="width: 100%">
             <el-col :span="24">
@@ -73,44 +73,44 @@
           </el-row>
         </el-form-item>
       </el-col>
-      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+      <el-col :span="24" class="px-10px">
         <el-form-item prop="password">
           <InputPassword
             v-model="resetPasswordData.password"
             :placeholder="t('login.passwordPlaceholder')"
-            style="width: 100%"
-            strength="true"
+            class="w-full"
+            :strength="true"
           />
         </el-form-item>
       </el-col>
-      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+      <el-col :span="24" class="px-10px">
         <el-form-item prop="check_password">
           <InputPassword
             v-model="resetPasswordData.check_password"
             :placeholder="t('login.checkPassword')"
-            style="width: 100%"
-            strength="true"
+            class="w-full"
+            :strength="true"
           />
         </el-form-item>
       </el-col>
       <!-- 登录按钮 / 返回按钮 -->
-      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+      <el-col :span="24" class="px-10px">
         <el-form-item>
           <XButton
             :loading="loginLoading"
             :title="t('login.resetPassword')"
-            class="w-[100%]"
+            class="w-full"
             type="primary"
             @click="resetPassword()"
           />
         </el-form-item>
       </el-col>
-      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+      <el-col :span="24" class="px-10px">
         <el-form-item>
           <XButton
             :loading="loginLoading"
             :title="t('login.backLogin')"
-            class="w-[100%]"
+            class="w-full"
             @click="handleBackLogin()"
           />
         </el-form-item>
@@ -134,7 +134,7 @@ const verify = ref()
 
 const { t } = useI18n()
 const message = useMessage()
-const { currentRoute, push } = useRouter()
+const { currentRoute } = useRouter()
 const formSmsResetPassword = ref()
 const loginLoading = ref(false)
 const iconHouse = useIcon({ icon: 'ep:house' })
@@ -145,7 +145,7 @@ const { handleBackLogin, getLoginState, setLoginState } = useLoginState()
 const getShow = computed(() => unref(getLoginState) === LoginStateEnum.RESET_PASSWORD)
 const captchaType = ref('blockPuzzle') // blockPuzzle 滑块 clickWord 点击文字
 
-const validatePass2 = (rule, value, callback) => {
+const validatePass2 = (_rule, value, callback) => {
   if (value === '') {
     callback(new Error('请再次输入密码'))
   } else if (value !== resetPasswordData.password) {

+ 25 - 23
src/views/Login/components/LoginForm.vue

@@ -9,13 +9,13 @@
     label-width="120px"
     size="large"
   >
-    <el-row style="margin-right: -10px; margin-left: -10px">
-      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+    <el-row class="mx-[-10px]">
+      <el-col :span="24" class="px-10px">
         <el-form-item>
-          <LoginFormTitle style="width: 100%" />
+          <LoginFormTitle class="w-full" />
         </el-form-item>
       </el-col>
-      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+      <el-col :span="24" class="px-10px">
         <el-form-item v-if="loginData.tenantEnable === 'true'" prop="tenantName">
           <el-input
             v-model="loginData.loginForm.tenantName"
@@ -26,7 +26,7 @@
           />
         </el-form-item>
       </el-col>
-      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+      <el-col :span="24" class="px-10px">
         <el-form-item prop="username">
           <el-input
             v-model="loginData.loginForm.username"
@@ -35,7 +35,7 @@
           />
         </el-form-item>
       </el-col>
-      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+      <el-col :span="24" class="px-10px">
         <el-form-item prop="password">
           <el-input
             v-model="loginData.loginForm.password"
@@ -49,7 +49,7 @@
       </el-col>
       <el-col
         :span="24"
-        style="padding-right: 10px; padding-left: 10px; margin-top: -20px; margin-bottom: -20px"
+        class="px-10px mt-[-20px] mb-[-20px]"
       >
         <el-form-item>
           <el-row justify="space-between" style="width: 100%">
@@ -60,7 +60,7 @@
             </el-col>
             <el-col :offset="6" :span="12">
               <el-link
-                style="float: right"
+                class="float-right"
                 type="primary"
                 @click="setLoginState(LoginStateEnum.RESET_PASSWORD)"
               >
@@ -70,12 +70,12 @@
           </el-row>
         </el-form-item>
       </el-col>
-      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+      <el-col :span="24" class="px-10px">
         <el-form-item>
           <XButton
             :loading="loginLoading"
             :title="t('login.login')"
-            class="w-[100%]"
+            class="w-full"
             type="primary"
             @click="getCode()"
           />
@@ -89,27 +89,27 @@
         mode="pop"
         @success="handleLogin"
       />
-      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+      <el-col :span="24" class="px-10px">
         <el-form-item>
           <el-row :gutter="5" justify="space-between" style="width: 100%">
             <el-col :span="8">
               <XButton
                 :title="t('login.btnMobile')"
-                class="w-[100%]"
+                class="w-full"
                 @click="setLoginState(LoginStateEnum.MOBILE)"
               />
             </el-col>
             <el-col :span="8">
               <XButton
                 :title="t('login.btnQRCode')"
-                class="w-[100%]"
+                class="w-full"
                 @click="setLoginState(LoginStateEnum.QR_CODE)"
               />
             </el-col>
             <el-col :span="8">
               <XButton
                 :title="t('login.btnRegister')"
-                class="w-[100%]"
+                class="w-full"
                 @click="setLoginState(LoginStateEnum.REGISTER)"
               />
             </el-col>
@@ -117,9 +117,9 @@
         </el-form-item>
       </el-col>
       <el-divider content-position="center">{{ t('login.otherLogin') }}</el-divider>
-      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+      <el-col :span="24" class="px-10px">
         <el-form-item>
-          <div class="w-[100%] flex justify-between">
+          <div class="w-full flex justify-between">
             <Icon
               v-for="(item, key) in socialList"
               :key="key"
@@ -133,9 +133,9 @@
         </el-form-item>
       </el-col>
       <el-divider content-position="center">萌新必读</el-divider>
-      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+      <el-col :span="24" class="px-10px">
         <el-form-item>
-          <div class="w-[100%] flex justify-between">
+          <div class="w-full flex justify-between">
             <el-link href="https://doc.iocoder.cn/" target="_blank">📚开发指南</el-link>
             <el-link href="https://doc.iocoder.cn/video/" target="_blank">🔥视频教程</el-link>
             <el-link href="https://www.iocoder.cn/Interview/good-collection/" target="_blank">
@@ -239,11 +239,13 @@ const getLoginFormCache = () => {
 }
 // 根据域名,获得租户信息
 const getTenantByWebsite = async () => {
-  const website = location.host
-  const res = await LoginApi.getTenantByWebsite(website)
-  if (res) {
-    loginData.loginForm.tenantName = res.name
-    authUtil.setTenantId(res.id)
+  if (loginData.tenantEnable === 'true') {
+    const website = location.host
+    const res = await LoginApi.getTenantByWebsite(website)
+    if (res) {
+      loginData.loginForm.tenantName = res.name
+      authUtil.setTenantId(res.id)
+    }
   }
 }
 const loading = ref() // ElLoading.service 返回的实例

+ 10 - 10
src/views/Login/components/MobileForm.vue

@@ -9,14 +9,14 @@
     label-width="120px"
     size="large"
   >
-    <el-row style="margin-right: -10px; margin-left: -10px">
+    <el-row class="mx-[-10px]">
       <!-- 租户名 -->
-      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+      <el-col :span="24" class="px-10px">
         <el-form-item>
-          <LoginFormTitle style="width: 100%" />
+          <LoginFormTitle class="w-full" />
         </el-form-item>
       </el-col>
-      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+      <el-col :span="24" class="px-10px">
         <el-form-item v-if="loginData.tenantEnable === 'true'" prop="tenantName">
           <el-input
             v-model="loginData.loginForm.tenantName"
@@ -28,7 +28,7 @@
         </el-form-item>
       </el-col>
       <!-- 手机号 -->
-      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+      <el-col :span="24" class="px-10px">
         <el-form-item prop="mobileNumber">
           <el-input
             v-model="loginData.loginForm.mobileNumber"
@@ -38,7 +38,7 @@
         </el-form-item>
       </el-col>
       <!-- 验证码 -->
-      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+      <el-col :span="24" class="px-10px">
         <el-form-item prop="code">
           <el-row :gutter="5" justify="space-between" style="width: 100%">
             <el-col :span="24">
@@ -68,23 +68,23 @@
         </el-form-item>
       </el-col>
       <!-- 登录按钮 / 返回按钮 -->
-      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+      <el-col :span="24" class="px-10px">
         <el-form-item>
           <XButton
             :loading="loginLoading"
             :title="t('login.login')"
-            class="w-[100%]"
+            class="w-full"
             type="primary"
             @click="signIn()"
           />
         </el-form-item>
       </el-col>
-      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+      <el-col :span="24" class="px-10px">
         <el-form-item>
           <XButton
             :loading="loginLoading"
             :title="t('login.backLogin')"
-            class="w-[100%]"
+            class="w-full"
             @click="handleBackLogin()"
           />
         </el-form-item>

+ 7 - 7
src/views/Login/components/QrCodeForm.vue

@@ -1,17 +1,17 @@
 <template>
-  <el-row v-show="getShow" class="login-form" style="margin-right: -10px; margin-left: -10px">
-    <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
-      <LoginFormTitle style="width: 100%" />
+  <el-row v-show="getShow" class="login-form mx-[-10px]">
+    <el-col :span="24" class="px-10px">
+      <LoginFormTitle class="w-full" />
     </el-col>
-    <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+    <el-col :span="24" class="px-10px">
       <el-card class="mb-10px text-center" shadow="hover">
         <Qrcode :logo="logoImg" />
       </el-card>
     </el-col>
     <el-divider class="enter-x">{{ t('login.qrcode') }}</el-divider>
-    <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
-      <div class="mt-15px w-[100%]">
-        <XButton :title="t('login.backLogin')" class="w-[100%]" @click="handleBackLogin()" />
+    <el-col :span="24" class="px-10px">
+      <div class="mt-4 w-full">
+        <XButton :title="t('login.backLogin')" class="w-full" @click="handleBackLogin()" />
       </div>
     </el-col>
   </el-row>

+ 19 - 17
src/views/Login/components/RegisterForm.vue

@@ -9,13 +9,13 @@
     label-width="120px"
     size="large"
   >
-    <el-row style="margin-right: -10px; margin-left: -10px">
-      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+    <el-row class="mx-[-10px]">
+      <el-col :span="24" class="px-10px">
         <el-form-item>
-          <LoginFormTitle style="width: 100%" />
+          <LoginFormTitle class="w-full" />
         </el-form-item>
       </el-col>
-      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+      <el-col :span="24" class="px-10px">
         <el-form-item v-if="registerData.tenantEnable === 'true'" prop="tenantName">
           <el-input
             v-model="registerData.registerForm.tenantName"
@@ -27,7 +27,7 @@
           />
         </el-form-item>
       </el-col>
-      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+      <el-col :span="24" class="px-10px">
         <el-form-item prop="username">
           <el-input
             v-model="registerData.registerForm.username"
@@ -37,7 +37,7 @@
           />
         </el-form-item>
       </el-col>
-      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+      <el-col :span="24" class="px-10px">
         <el-form-item prop="username">
           <el-input
             v-model="registerData.registerForm.nickname"
@@ -47,7 +47,7 @@
           />
         </el-form-item>
       </el-col>
-      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+      <el-col :span="24" class="px-10px">
         <el-form-item prop="password">
           <el-input
             v-model="registerData.registerForm.password"
@@ -60,7 +60,7 @@
           />
         </el-form-item>
       </el-col>
-      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+      <el-col :span="24" class="px-10px">
         <el-form-item prop="confirmPassword">
           <el-input
             v-model="registerData.registerForm.confirmPassword"
@@ -73,12 +73,12 @@
           />
         </el-form-item>
       </el-col>
-      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+      <el-col :span="24" class="px-10px">
         <el-form-item>
           <XButton
             :loading="loginLoading"
             :title="t('login.register')"
-            class="w-[100%]"
+            class="w-full"
             type="primary"
             @click="getCode()"
           />
@@ -93,7 +93,7 @@
         @success="handleRegister"
       />
     </el-row>
-    <XButton :title="t('login.hasUser')" class="w-[100%]" @click="handleBackLogin()" />
+    <XButton :title="t('login.hasUser')" class="w-full" @click="handleBackLogin()" />
   </el-form>
 </template>
 <script lang="ts" setup>
@@ -123,7 +123,7 @@ const captchaType = ref('blockPuzzle') // blockPuzzle 滑块 clickWord 点击文
 
 const getShow = computed(() => unref(getLoginState) === LoginStateEnum.REGISTER)
 
-const equalToPassword = (rule, value, callback) => {
+const equalToPassword = (_rule, value, callback) => {
   if (registerData.registerForm.password !== value) {
     callback(new Error('两次输入的密码不一致'))
   } else {
@@ -233,11 +233,13 @@ const getTenantId = async () => {
 
 // 根据域名,获得租户信息
 const getTenantByWebsite = async () => {
-  const website = location.host
-  const res = await LoginApi.getTenantByWebsite(website)
-  if (res) {
-    registerData.registerForm.tenantName = res.name
-    authUtil.setTenantId(res.id)
+  if (registerData.tenantEnable === 'true') {
+    const website = location.host
+    const res = await LoginApi.getTenantByWebsite(website)
+    if (res) {
+      registerData.registerForm.tenantName = res.name
+      authUtil.setTenantId(res.id)
+    }
   }
 }
 const loading = ref() // ElLoading.service 返回的实例

+ 4 - 4
src/views/Login/components/SSOLogin.vue

@@ -1,7 +1,7 @@
 <template>
   <div v-show="ssoVisible" class="form-cont">
     <!-- 应用名 -->
-    <LoginFormTitle style="width: 100%" />
+    <LoginFormTitle class="w-full" />
     <el-tabs class="form" style="float: none" value="uname">
       <el-tab-pane :label="client.name" name="uname" />
     </el-tabs>
@@ -15,17 +15,17 @@
               v-for="scope in queryParams.scopes"
               :key="scope"
               :value="scope"
-              style="display: block; margin-bottom: -10px"
+              class="block mb-[-10px]"
             >
               {{ formatScope(scope) }}
             </el-checkbox>
           </el-checkbox-group>
         </el-form-item>
         <!-- 下方的登录按钮 -->
-        <el-form-item class="w-1/1">
+        <el-form-item class="w-full">
           <el-button
             :loading="formLoading"
-            class="w-6/10"
+            class="w-3/5"
             type="primary"
             @click.prevent="handleAuthorize(true)"
           >

+ 6 - 5
src/views/Profile/components/UserAvatar.vue

@@ -18,7 +18,6 @@ import { useUserStore } from '@/store/modules/user'
 import { useUpload } from '@/components/UploadFile/src/useUpload'
 import { UploadRequestOptions } from 'element-plus/es/components/upload/src/upload'
 
-// TODO @芋艿:合并到 ProfileUser 组件中,更简洁一点
 defineOptions({ name: 'UserAvatar' })
 
 defineProps({
@@ -30,10 +29,12 @@ const userStore = useUserStore()
 const cropperRef = ref()
 const handelUpload = async ({ data }) => {
   const { httpRequest } = useUpload()
-  const avatar = ((await httpRequest({
-    file: data,
-    filename: 'avatar.png',
-  } as UploadRequestOptions)) as unknown as { data: string }).data
+  const avatar = (
+    (await httpRequest({
+      file: data,
+      filename: 'avatar.png'
+    } as UploadRequestOptions)) as unknown as { data: string }
+  ).data
   await updateUserProfile({ avatar })
 
   // 关闭弹窗,并更新 userStore

+ 8 - 8
src/views/Profile/components/UserSocial.vue

@@ -23,7 +23,7 @@
 </template>
 <script lang="ts" setup>
 import { SystemUserSocialTypeEnum } from '@/utils/constants'
-import { getUserProfile, ProfileVO } from '@/api/system/user/profile'
+import { getBindSocialUserList } from '@/api/system/social/user'
 import { socialAuthRedirect, socialBind, socialUnbind } from '@/api/system/user/socialUser'
 
 defineOptions({ name: 'UserSocial' })
@@ -32,19 +32,19 @@ defineProps<{
 }>()
 const message = useMessage()
 const socialUsers = ref<any[]>([])
-const userInfo = ref<ProfileVO>()
 
 const initSocial = async () => {
   socialUsers.value = [] // 重置避免无限增长
-  const res = await getUserProfile()
-  userInfo.value = res
+  // 获取已绑定的社交用户列表
+  const bindSocialUserList = await getBindSocialUserList()
+  // 检查该社交平台是否已绑定
   for (const i in SystemUserSocialTypeEnum) {
     const socialUser = { ...SystemUserSocialTypeEnum[i] }
     socialUsers.value.push(socialUser)
-    if (userInfo.value?.socialUsers) {
-      for (const j in userInfo.value.socialUsers) {
-        if (socialUser.type === userInfo.value.socialUsers[j].type) {
-          socialUser.openid = userInfo.value.socialUsers[j].openid
+    if (bindSocialUserList && bindSocialUserList.length > 0) {
+      for (const bindUser of bindSocialUserList) {
+        if (socialUser.type === bindUser.type) {
+          socialUser.openid = bindUser.openid
           break
         }
       }

+ 7 - 6
src/views/ai/chat/index/components/conversation/ConversationList.vue

@@ -391,7 +391,8 @@ onMounted(async () => {
       line-height: 30px;
 
       &.active {
-        background-color: #e6e6e6;
+        background-color: var(--el-color-primary-light-9);
+        border: 1px solid var(--el-color-primary-light-7);
 
         .button {
           display: inline-block;
@@ -409,7 +410,7 @@ onMounted(async () => {
         max-width: 220px;
         font-size: 14px;
         font-weight: 400;
-        color: rgba(0, 0, 0, 0.77);
+        color: var(--el-text-color-regular);
         overflow: hidden;
         white-space: nowrap;
         text-overflow: ellipsis;
@@ -430,7 +431,7 @@ onMounted(async () => {
         display: flex;
         flex-direction: row;
         justify-items: center;
-        color: #606266;
+        color: var(--el-text-color-regular);
 
         .btn {
           margin: 0;
@@ -447,8 +448,8 @@ onMounted(async () => {
     right: 0;
     //width: 100%;
     padding: 0 20px;
-    background-color: #f4f4f4;
-    box-shadow: 0 0 1px 1px rgba(228, 228, 228, 0.8);
+    background-color: var(--el-fill-color-extra-light);
+    box-shadow: 0 0 1px 1px var(--el-border-color-lighter);
     line-height: 35px;
     display: flex;
     justify-content: space-between;
@@ -458,7 +459,7 @@ onMounted(async () => {
     > div {
       display: flex;
       align-items: center;
-      color: #606266;
+      color: var(--el-text-color-regular);
       padding: 0;
       margin: 0;
       cursor: pointer;

+ 7 - 7
src/views/ai/chat/index/components/message/MessageList.vue

@@ -215,13 +215,13 @@ onMounted(async () => {
       display: flex;
       flex-direction: column;
       overflow-wrap: break-word;
-      background-color: rgba(228, 228, 228, 0.8);
-      box-shadow: 0 0 0 1px rgba(228, 228, 228, 0.8);
+      background-color: var(--el-fill-color-light);
+      box-shadow: 0 0 0 1px var(--el-border-color-light);
       border-radius: 10px;
       padding: 10px 10px 5px 10px;
 
       .left-text {
-        color: #393939;
+        color: var(--el-text-color-primary);
         font-size: 0.95rem;
       }
     }
@@ -232,10 +232,10 @@ onMounted(async () => {
 
       .right-text {
         font-size: 0.95rem;
-        color: #fff;
+        color: var(--el-color-white);
         display: inline;
-        background-color: #267fff;
-        box-shadow: 0 0 0 1px #267fff;
+        background-color: var(--el-color-primary);
+        box-shadow: 0 0 0 1px var(--el-color-primary);
         border-radius: 10px;
         padding: 10px;
         width: auto;
@@ -270,7 +270,7 @@ onMounted(async () => {
 
   .btn-cus:hover {
     cursor: pointer;
-    background-color: #f6f6f6;
+    background-color: var(--el-fill-color-lighter);
   }
 }
 

+ 2 - 2
src/views/ai/chat/index/components/role/RoleHeader.vue

@@ -29,14 +29,14 @@ defineProps({
   padding: 0 10px;
   white-space: nowrap;
   text-overflow: ellipsis;
-  background-color: #ececec;
+  background-color: var(--el-bg-color-page);
   width: 100%;
 
   .title {
     font-size: 20px;
     font-weight: bold;
     overflow: hidden;
-    color: #3e3e3e;
+    color: var(--el-text-color-primary);
     max-width: 220px;
   }
 

+ 5 - 5
src/views/ai/chat/index/components/role/RoleList.vue

@@ -13,10 +13,10 @@
             <template #dropdown>
               <el-dropdown-menu>
                 <el-dropdown-item :command="['edit', role]">
-                  <Icon icon="ep:edit" color="#787878" />编辑
+                  <Icon icon="ep:edit" color="var(--el-text-color-placeholder)" />编辑
                 </el-dropdown-item>
-                <el-dropdown-item :command="['delete', role]" style="color: red">
-                  <Icon icon="ep:delete" color="red" />删除
+                <el-dropdown-item :command="['delete', role]" style="color: var(--el-color-danger)">
+                  <Icon icon="ep:delete" color="var(--el-color-danger)" />删除
                 </el-dropdown-item>
               </el-dropdown-menu>
             </template>
@@ -153,13 +153,13 @@ const handleTabsScroll = async () => {
         .title {
           font-size: 18px;
           font-weight: bold;
-          color: #3e3e3e;
+          color: var(--el-text-color-primary);
         }
 
         .description {
           margin-top: 10px;
           font-size: 14px;
-          color: #6a6a6a;
+          color: var(--el-text-color-regular);
         }
       }
 

+ 7 - 7
src/views/ai/chat/index/components/role/RoleRepository.vue

@@ -23,7 +23,7 @@
           @click="handlerAddRole"
           class="ml-20px"
         >
-          <Icon icon="ep:user" style="margin-right: 5px;" />
+          <Icon icon="ep:user" style="margin-right: 5px" />
           添加角色
         </el-button>
       </div>
@@ -64,15 +64,15 @@
 </template>
 
 <script setup lang="ts">
-import {ref} from 'vue'
+import { ref } from 'vue'
 import RoleHeader from './RoleHeader.vue'
 import RoleList from './RoleList.vue'
 import ChatRoleForm from '@/views/ai/model/chatRole/ChatRoleForm.vue'
 import RoleCategoryList from './RoleCategoryList.vue'
-import {ChatRoleApi, ChatRolePageReqVO, ChatRoleVO} from '@/api/ai/model/chatRole'
-import {ChatConversationApi, ChatConversationVO} from '@/api/ai/chat/conversation'
-import {Search} from '@element-plus/icons-vue'
-import {TabsPaneContext} from 'element-plus'
+import { ChatRoleApi, ChatRolePageReqVO, ChatRoleVO } from '@/api/ai/model/chatRole'
+import { ChatConversationApi, ChatConversationVO } from '@/api/ai/chat/conversation'
+import { Search } from '@element-plus/icons-vue'
+import { TabsPaneContext } from 'element-plus'
 
 const router = useRouter() // 路由对象
 
@@ -244,7 +244,7 @@ onMounted(async () => {
   right: 0;
   top: 0;
   bottom: 0;
-  background-color: #ffffff;
+  background-color: var(--el-bg-color);
   overflow: hidden;
   display: flex;
   flex-direction: column;

+ 14 - 10
src/views/ai/chat/index/index.vue

@@ -22,13 +22,16 @@
             <Icon icon="ep:setting" class="ml-10px" />
           </el-button>
           <el-button size="small" class="btn" @click="handlerMessageClear">
-            <Icon icon="heroicons-outline:archive-box-x-mark" color="#787878" />
+            <Icon
+              icon="heroicons-outline:archive-box-x-mark"
+              color="var(--el-text-color-placeholder)"
+            />
           </el-button>
           <el-button size="small" class="btn">
-            <Icon icon="ep:download" color="#787878" />
+            <Icon icon="ep:download" color="var(--el-text-color-placeholder)" />
           </el-button>
           <el-button size="small" class="btn" @click="handleGoTopMessage">
-            <Icon icon="ep:top" color="#787878" />
+            <Icon icon="ep:top" color="var(--el-text-color-placeholder)" />
           </el-button>
         </div>
       </el-header>
@@ -613,7 +616,8 @@ onMounted(async () => {
       line-height: 30px;
 
       &.active {
-        background-color: #e6e6e6;
+        background-color: var(--el-color-primary-light-9);
+        border: 1px solid var(--el-color-primary-light-7);
 
         .button {
           display: inline-block;
@@ -649,7 +653,7 @@ onMounted(async () => {
         display: flex;
         flex-direction: row;
         justify-items: center;
-        color: #606266;
+        color: var(--el-text-color-regular);
 
         .el-icon {
           margin-right: 5px;
@@ -669,7 +673,7 @@ onMounted(async () => {
     > div {
       display: flex;
       align-items: center;
-      color: #606266;
+      color: var(--el-text-color-regular);
       padding: 0;
       margin: 0;
       cursor: pointer;
@@ -683,15 +687,15 @@ onMounted(async () => {
 
 // 头部
 .detail-container {
-  background: #ffffff;
+  background: var(--el-bg-color);
 
   .header {
     display: flex;
     flex-direction: row;
     align-items: center;
     justify-content: space-between;
-    background: #fbfbfb;
-    box-shadow: 0 0 0 0 #dcdfe6;
+    background: var(--el-bg-color-page);
+    box-shadow: 0 0 0 0 var(--el-border-color-light);
 
     .title {
       font-size: 18px;
@@ -744,7 +748,7 @@ onMounted(async () => {
     display: flex;
     flex-direction: column;
     height: auto;
-    border: 1px solid #e3e3e3;
+    border: 1px solid var(--el-border-color);
     border-radius: 10px;
     margin: 10px 20px 20px 20px;
     padding: 9px 10px;

+ 9 - 4
src/views/bpm/model/CategoryDraggableModel.vue

@@ -510,10 +510,15 @@ const isManagerUser = (row: any) => {
 
 /** 处理模型的排序 **/
 const handleModelSort = () => {
-  // 保存初始数据
-  originalData.value = cloneDeep(props.categoryInfo.modelList)
-  isModelSorting.value = true
-  initSort()
+  if (isModelSorting.value) {
+    // 如果已经在排序状态,则取消排序
+    handleModelSortCancel()
+  } else {
+    // 保存初始数据
+    originalData.value = cloneDeep(props.categoryInfo.modelList)
+    isModelSorting.value = true
+    initSort()
+  }
 }
 
 /** 处理模型的排序提交 */

+ 18 - 2
src/views/bpm/model/form/BasicInfo.vue

@@ -102,7 +102,7 @@
       </div>
       <div v-if="modelData.startUserType === 2" class="mt-2 flex flex-wrap gap-2">
         <div
-          v-for="dept in selectedStartDepts" 
+          v-for="dept in selectedStartDepts"
           :key="dept.id"
           class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative"
         >
@@ -186,7 +186,23 @@ const currentSelectType = ref<'start' | 'manager'>('start')
 
 const rules = {
   name: [{ required: true, message: '流程名称不能为空', trigger: 'blur' }],
-  key: [{ required: true, message: '流程标识不能为空', trigger: 'blur' }],
+  key: [
+    { required: true, message: '流程标识不能为空', trigger: 'blur' },
+    {
+      validator: (_rule: any, value: string, callback: any) => {
+        if (!value) {
+          callback()
+          return
+        }
+        if (!/^[a-zA-Z_][\-_.0-9_a-zA-Z$]*$/.test(value)) {
+          callback(new Error('只能包含字母、数字、下划线、连字符和点号,且必须以字母或下划线开头'))
+          return
+        }
+        callback()
+      },
+      trigger: 'blur'
+    }
+  ],
   category: [{ required: true, message: '流程分类不能为空', trigger: 'blur' }],
   type: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }],
   visible: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }],

+ 10 - 0
src/views/bpm/model/form/index.vue

@@ -219,6 +219,16 @@ const initData = async () => {
     // 特殊:复制场景
     if (route.params.type === 'copy') {
       delete formData.value.id
+      if (formData.value.bpmnXml) {
+        formData.value.bpmnXml = formData.value.bpmnXml.replaceAll(
+          formData.value.name,
+          formData.value.name + '副本'
+        )
+        formData.value.bpmnXml = formData.value.bpmnXml.replaceAll(
+          formData.value.key,
+          formData.value.key + '_copy'
+        )
+      }
       formData.value.name += '副本'
       formData.value.key += '_copy'
       tagsView.setTitle('复制流程')

+ 1 - 0
src/views/bpm/processInstance/detail/ProcessInstanceOperationButton.vue

@@ -685,6 +685,7 @@ watch(
 
 /** 弹出气泡卡 */
 const openPopover = async (type: string) => {
+  if (popOverVisible.value[type] === true) return
   if (type === 'approve') {
     // 校验流程表单
     const valid = await validateNormalForm()

+ 10 - 2
src/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue

@@ -38,7 +38,13 @@
           </div>
         </div>
         <div v-if="activity.nodeType === NodeType.CHILD_PROCESS_NODE">
-          <el-button type="primary" plain size="small" @click="handleChildProcess(activity)">
+          <el-button
+            type="primary"
+            plain
+            size="small"
+            @click="handleChildProcess(activity)"
+            :disabled="!activity.processInstanceId"
+          >
             查看子流程
           </el-button>
         </div>
@@ -319,7 +325,9 @@ const handleUserSelectConfirm = (activityId: string, userList: any[]) => {
 
 /** 跳转子流程 */
 const handleChildProcess = (activity: any) => {
-  // TODO @lesan:貌似跳不过去?!
+  if (!activity.processInstanceId) {
+    return
+  }
   push({
     name: 'BpmProcessInstanceDetail',
     query: {

+ 1 - 1
src/views/bpm/task/todo/index.vue

@@ -131,7 +131,7 @@
         :formatter="dateFormatter"
         align="center"
         label="发起时间"
-        prop="createTime"
+        prop="processInstance.createTime"
         width="180"
       />
       <el-table-column align="center" label="当前任务" prop="name" width="180" />

+ 1 - 1
src/views/crm/customer/CustomerImportForm.vue

@@ -56,7 +56,7 @@ import type { UploadUserFile } from 'element-plus'
 import * as UserApi from '@/api/system/user'
 import { useUserStore } from '@/store/modules/user'
 
-defineOptions({ name: 'SystemUserImportForm' })
+defineOptions({ name: 'CrmCustomerImportForm' })
 
 const message = useMessage() // 消息弹窗
 

+ 40 - 0
src/views/crm/followup/index.vue

@@ -24,6 +24,38 @@
         </template>
       </el-table-column>
       <el-table-column align="center" label="跟进内容" prop="content" />
+      <el-table-column label="图片" align="center">
+        <template #default="scope">
+          <div v-if="scope.row.picUrls && scope.row.picUrls.length > 0" class="flex">
+            <el-image
+              v-for="(url, index) in scope.row.picUrls"
+              :key="index"
+              :src="url"
+              :preview-src-list="scope.row.picUrls"
+              class="w-10 h-10 mr-1"
+              :initial-index="index"
+              fit="cover"
+              preview-teleported
+            />
+          </div>
+        </template>
+      </el-table-column>
+      <el-table-column label="附件" align="center">
+        <template #default="scope">
+          <div v-if="scope.row.fileUrls && scope.row.fileUrls.length > 0" class="flex flex-col">
+            <el-link
+              v-for="(url, index) in scope.row.fileUrls"
+              :key="index"
+              :href="url"
+              type="primary"
+              target="_blank"
+              download
+            >
+              {{ getFileName(url) }}
+            </el-link>
+          </div>
+        </template>
+      </el-table-column>
       <el-table-column
         :formatter="dateFormatter"
         align="center"
@@ -97,6 +129,14 @@ import { BizTypeEnum } from '@/api/crm/permission'
 
 /** 跟进记录列表 */
 defineOptions({ name: 'FollowUpRecord' })
+
+const getFileName = (url: string) => {
+  if (!url) {
+    return ''
+  }
+  return url.substring(url.lastIndexOf('/') + 1)
+}
+
 const props = defineProps<{
   bizType: number
   bizId: number

+ 15 - 1
src/views/infra/codegen/components/ColumInfoForm.vue

@@ -95,7 +95,20 @@
     </el-table-column>
     <el-table-column label="字典类型" min-width="12%">
       <template #default="scope">
-        <el-select v-model="scope.row.dictType" clearable filterable placeholder="请选择">
+        <el-select v-model="scope.row.dictType" :value-on-clear="''" clearable filterable placeholder="请选择">
+          <template #header>
+            <div class="flex justify-end">
+              <el-popover
+                class="box-item"
+                content="加载最新字典"
+                placement="top-start"
+              >
+                <template #reference>
+                  <el-button :icon="Refresh" size="small" circle @click="getDictOptions" class=""/>
+                </template>
+              </el-popover>
+            </div>
+          </template>
           <el-option
             v-for="dict in dictOptions"
             :key="dict.id"
@@ -114,6 +127,7 @@
 </template>
 <script lang="ts" setup>
 import { PropType } from 'vue'
+import { Refresh } from '@element-plus/icons-vue'
 import * as CodegenApi from '@/api/infra/codegen'
 import * as DictDataApi from '@/api/system/dict/dict.type'
 

+ 29 - 1
src/views/infra/codegen/index.vue

@@ -55,13 +55,23 @@
           <Icon class="mr-5px" icon="ep:zoom-in" />
           导入
         </el-button>
+        <el-button
+          v-hasPermi="['infra:codegen:delete']"
+          type="danger"
+          :disabled="checkedIds.length === 0"
+          @click="handleDeleteBatch"
+        >
+          <Icon class="mr-5px" icon="ep:delete" />
+          批量删除
+        </el-button>
       </el-form-item>
     </el-form>
   </ContentWrap>
 
   <!-- 列表 -->
   <ContentWrap>
-    <el-table v-loading="loading" :data="list">
+    <el-table v-loading="loading" :data="list" @selection-change="handleRowCheckboxChange">
+      <el-table-column type="selection" width="55" />
       <el-table-column align="center" label="数据源">
         <template #default="scope">
           {{
@@ -232,6 +242,24 @@ const handleDelete = async (id: number) => {
   } catch {}
 }
 
+/** 批量删除操作 */
+const checkedIds = ref<number[]>([])
+const handleRowCheckboxChange = (rows: CodegenApi.CodegenTableVO[]) => {
+  checkedIds.value = rows.map((row) => row.id)
+}
+
+const handleDeleteBatch = async () => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起批量删除
+    await CodegenApi.deleteCodegenTableList(checkedIds.value)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
 /** 同步操作  */
 const handleSyncDB = async (row: CodegenApi.CodegenTableVO) => {
   // 基于 DB 同步

+ 29 - 1
src/views/infra/config/index.vue

@@ -66,6 +66,15 @@
           <Icon icon="ep:plus" class="mr-5px" /> 新增
         </el-button>
         <el-button
+          type="danger"
+          plain
+          :disabled="checkedIds.length === 0"
+          @click="handleDeleteBatch"
+          v-hasPermi="['infra:config:delete']"
+        >
+          <Icon icon="ep:delete" class="mr-5px" /> 批量删除
+        </el-button>
+        <el-button
           type="success"
           plain
           @click="handleExport"
@@ -80,7 +89,8 @@
 
   <!-- 列表 -->
   <ContentWrap>
-    <el-table v-loading="loading" :data="list">
+    <el-table v-loading="loading" :data="list" @selection-change="handleRowCheckboxChange">
+      <el-table-column type="selection" width="55" />
       <el-table-column label="参数主键" align="center" prop="id" />
       <el-table-column label="参数分类" align="center" prop="category" />
       <el-table-column label="参数名称" align="center" prop="name" :show-overflow-tooltip="true" />
@@ -206,6 +216,24 @@ const handleDelete = async (id: number) => {
   } catch {}
 }
 
+/** 批量删除按钮操作 */
+const checkedIds = ref<number[]>([])
+const handleRowCheckboxChange = (rows: ConfigApi.ConfigVO[]) => {
+  checkedIds.value = rows.map((row) => row.id!).filter(Boolean)
+}
+
+const handleDeleteBatch = async () => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起批量删除
+    await ConfigApi.deleteConfigList(checkedIds.value)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
 /** 导出按钮操作 */
 const handleExport = async () => {
   try {

+ 30 - 1
src/views/infra/dataSourceConfig/index.vue

@@ -11,13 +11,23 @@
         >
           <Icon icon="ep:plus" class="mr-5px" /> 新增
         </el-button>
+        <el-button
+          type="danger"
+          plain
+          :disabled="checkedIds.length === 0"
+          @click="handleDeleteBatch"
+          v-hasPermi="['infra:data-source-config:delete']"
+        >
+          <Icon icon="ep:delete" class="mr-5px" /> 批量删除
+        </el-button>
       </el-form-item>
     </el-form>
   </ContentWrap>
 
   <!-- 列表 -->
   <ContentWrap>
-    <el-table v-loading="loading" :data="list">
+    <el-table v-loading="loading" :data="list" @selection-change="handleRowCheckboxChange">
+      <el-table-column type="selection" width="55" />
       <el-table-column label="主键编号" align="center" prop="id" />
       <el-table-column label="数据源名称" align="center" prop="name" />
       <el-table-column label="数据源连接" align="center" prop="url" :show-overflow-tooltip="true" />
@@ -99,6 +109,25 @@ const handleDelete = async (id: number) => {
   } catch {}
 }
 
+/** 批量删除按钮操作 */
+const checkedIds = ref<number[]>([])
+const handleRowCheckboxChange = (rows: DataSourceConfigApi.DataSourceConfigVO[]) => {
+  // 过滤掉id为 0 的主数据源
+  checkedIds.value = rows.map((row) => row.id!).filter((id) => id !== 0 && Boolean(id))
+}
+
+const handleDeleteBatch = async () => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起批量删除
+    await DataSourceConfigApi.deleteDataSourceConfigList(checkedIds.value)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
 /** 初始化 **/
 onMounted(() => {
   getList()

+ 9 - 6
src/views/infra/demo/demo01/Demo01ContactForm.vue

@@ -15,7 +15,7 @@
           <el-radio
             v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
             :key="dict.value"
-            :value="dict.value"
+            :label="dict.value"
           >
             {{ dict.label }}
           </el-radio>
@@ -44,7 +44,10 @@
 </template>
 <script setup lang="ts">
 import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
-import * as Demo01ContactApi from '@/api/infra/demo/demo01'
+import { Demo01ContactApi, Demo01Contact } from '@/api/infra/demo/demo01'
+
+/** 示例联系人 表单 */
+defineOptions({ name: 'Demo01ContactForm' })
 
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
@@ -59,13 +62,13 @@ const formData = ref({
   sex: undefined,
   birthday: undefined,
   description: undefined,
-  avatar: undefined
+  avatar: undefined,
 })
 const formRules = reactive({
   name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
   sex: [{ required: true, message: '性别不能为空', trigger: 'blur' }],
   birthday: [{ required: true, message: '出生年不能为空', trigger: 'blur' }],
-  description: [{ required: true, message: '简介不能为空', trigger: 'blur' }]
+  description: [{ required: true, message: '简介不能为空', trigger: 'blur' }],
 })
 const formRef = ref() // 表单 Ref
 
@@ -95,7 +98,7 @@ const submitForm = async () => {
   // 提交请求
   formLoading.value = true
   try {
-    const data = formData.value as unknown as Demo01ContactApi.Demo01ContactVO
+    const data = formData.value as unknown as Demo01Contact
     if (formType.value === 'create') {
       await Demo01ContactApi.createDemo01Contact(data)
       message.success(t('common.createSuccess'))
@@ -119,7 +122,7 @@ const resetForm = () => {
     sex: undefined,
     birthday: undefined,
     description: undefined,
-    avatar: undefined
+    avatar: undefined,
   }
   formRef.value?.resetFields()
 }

+ 49 - 11
src/views/infra/demo/demo01/index.vue

@@ -1,6 +1,4 @@
 <template>
-  <doc-alert title="代码生成(单表)" url="https://doc.iocoder.cn/new-feature/" />
-
   <ContentWrap>
     <!-- 搜索工作栏 -->
     <el-form
@@ -20,7 +18,12 @@
         />
       </el-form-item>
       <el-form-item label="性别" prop="sex">
-        <el-select v-model="queryParams.sex" placeholder="请选择性别" clearable class="!w-240px">
+        <el-select
+          v-model="queryParams.sex"
+          placeholder="请选择性别"
+          clearable
+          class="!w-240px"
+        >
           <el-option
             v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
             :key="dict.value"
@@ -37,7 +40,7 @@
           start-placeholder="开始日期"
           end-placeholder="结束日期"
           :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
-          class="!w-240px"
+          class="!w-220px"
         />
       </el-form-item>
       <el-form-item>
@@ -60,13 +63,30 @@
         >
           <Icon icon="ep:download" class="mr-5px" /> 导出
         </el-button>
+        <el-button
+          type="danger"
+          plain
+          :disabled="isEmpty(checkedIds)"
+          @click="handleDeleteBatch"
+          v-hasPermi="['infra:demo01-contact:delete']"
+        >
+          <Icon icon="ep:delete" class="mr-5px" /> 批量删除
+        </el-button>
       </el-form-item>
     </el-form>
   </ContentWrap>
 
   <!-- 列表 -->
   <ContentWrap>
-    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+    <el-table
+      row-key="id"
+      v-loading="loading"
+      :data="list"
+      :stripe="true"
+      :show-overflow-tooltip="true"
+      @selection-change="handleRowCheckboxChange"
+    >
+      <el-table-column type="selection" width="55" />
       <el-table-column label="编号" align="center" prop="id" />
       <el-table-column label="名字" align="center" prop="name" />
       <el-table-column label="性别" align="center" prop="sex">
@@ -90,7 +110,7 @@
         :formatter="dateFormatter"
         width="180px"
       />
-      <el-table-column label="操作" align="center">
+      <el-table-column label="操作" align="center" min-width="120px">
         <template #default="scope">
           <el-button
             link
@@ -126,25 +146,27 @@
 
 <script setup lang="ts">
 import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { isEmpty } from '@/utils/is'
 import { dateFormatter } from '@/utils/formatTime'
 import download from '@/utils/download'
-import * as Demo01ContactApi from '@/api/infra/demo/demo01'
+import { Demo01ContactApi, Demo01Contact } from '@/api/infra/demo/demo01'
 import Demo01ContactForm from './Demo01ContactForm.vue'
 
+/** 示例联系人 列表 */
 defineOptions({ name: 'Demo01Contact' })
 
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
 
 const loading = ref(true) // 列表的加载中
-const list = ref([]) // 列表的数据
+const list = ref<Demo01Contact[]>([]) // 列表的数据
 const total = ref(0) // 列表的总页数
 const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
-  name: null,
-  sex: null,
-  createTime: []
+  name: undefined,
+  sex: undefined,
+  createTime: [],
 })
 const queryFormRef = ref() // 搜索的表单
 const exportLoading = ref(false) // 导出的加载中
@@ -192,6 +214,22 @@ const handleDelete = async (id: number) => {
   } catch {}
 }
 
+/** 批量删除示例联系人 */
+const handleDeleteBatch = async () => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    await Demo01ContactApi.deleteDemo01ContactList(checkedIds.value);
+    message.success(t('common.delSuccess'))
+    await getList();
+  } catch {}
+}
+
+const checkedIds = ref<number[]>([])
+const handleRowCheckboxChange = (records: Demo01Contact[]) => {
+  checkedIds.value = records.map((item) => item.id);
+}
+
 /** 导出按钮操作 */
 const handleExport = async () => {
   try {

+ 8 - 5
src/views/infra/demo/demo03/erp/Demo03StudentForm.vue

@@ -41,7 +41,10 @@
 </template>
 <script setup lang="ts">
 import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
-import * as Demo03StudentApi from '@/api/infra/demo/demo03/erp'
+import { Demo03StudentApi, Demo03Student } from '@/api/infra/demo/demo03/erp'
+
+/** 学生 表单 */
+defineOptions({ name: 'Demo03StudentForm' })
 
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
@@ -55,13 +58,13 @@ const formData = ref({
   name: undefined,
   sex: undefined,
   birthday: undefined,
-  description: undefined
+  description: undefined,
 })
 const formRules = reactive({
   name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
   sex: [{ required: true, message: '性别不能为空', trigger: 'blur' }],
   birthday: [{ required: true, message: '出生日期不能为空', trigger: 'blur' }],
-  description: [{ required: true, message: '简介不能为空', trigger: 'blur' }]
+  description: [{ required: true, message: '简介不能为空', trigger: 'blur' }],
 })
 const formRef = ref() // 表单 Ref
 
@@ -91,7 +94,7 @@ const submitForm = async () => {
   // 提交请求
   formLoading.value = true
   try {
-    const data = formData.value as unknown as Demo03StudentApi.Demo03StudentVO
+    const data = formData.value as unknown as Demo03Student
     if (formType.value === 'create') {
       await Demo03StudentApi.createDemo03Student(data)
       message.success(t('common.createSuccess'))
@@ -114,7 +117,7 @@ const resetForm = () => {
     name: undefined,
     sex: undefined,
     birthday: undefined,
-    description: undefined
+    description: undefined,
   }
   formRef.value?.resetFields()
 }

+ 8 - 8
src/views/infra/demo/demo03/erp/components/Demo03CourseForm.vue

@@ -7,7 +7,7 @@
       label-width="100px"
       v-loading="formLoading"
     >
-      <el-form-item label="名字" prop="name">
+       <el-form-item label="名字" prop="name">
         <el-input v-model="formData.name" placeholder="请输入名字" />
       </el-form-item>
       <el-form-item label="分数" prop="score">
@@ -21,7 +21,7 @@
   </Dialog>
 </template>
 <script setup lang="ts">
-import * as Demo03StudentApi from '@/api/infra/demo/demo03/erp'
+import { Demo03StudentApi, Demo03Course } from '@/api/infra/demo/demo03/erp'
 
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
@@ -34,22 +34,22 @@ const formData = ref({
   id: undefined,
   studentId: undefined,
   name: undefined,
-  score: undefined
+  score: undefined,
 })
 const formRules = reactive({
   studentId: [{ required: true, message: '学生编号不能为空', trigger: 'blur' }],
   name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
-  score: [{ required: true, message: '分数不能为空', trigger: 'blur' }]
+  score: [{ required: true, message: '分数不能为空', trigger: 'blur' }],
 })
 const formRef = ref() // 表单 Ref
 
 /** 打开弹窗 */
-const open = async (type: string, id?: number, studentId: number) => {
+const open = async (type: string, id?: number, studentId?: number) => {
   dialogVisible.value = true
   dialogTitle.value = t('action.' + type)
   formType.value = type
   resetForm()
-  formData.value.studentId = studentId
+  formData.value.studentId = studentId  as any
   // 修改时,设置数据
   if (id) {
     formLoading.value = true
@@ -70,7 +70,7 @@ const submitForm = async () => {
   // 提交请求
   formLoading.value = true
   try {
-    const data = formData.value
+    const data = formData.value as unknown as  Demo03Course
     if (formType.value === 'create') {
       await Demo03StudentApi.createDemo03Course(data)
       message.success(t('common.createSuccess'))
@@ -92,7 +92,7 @@ const resetForm = () => {
     id: undefined,
     studentId: undefined,
     name: undefined,
-    score: undefined
+    score: undefined,
   }
   formRef.value?.resetFields()
 }

+ 53 - 21
src/views/infra/demo/demo03/erp/components/Demo03CourseList.vue

@@ -2,40 +2,56 @@
   <!-- 列表 -->
   <ContentWrap>
     <el-button
-      v-hasPermi="['infra:demo03-student:create']"
-      plain
       type="primary"
+      plain
       @click="openForm('create')"
+      v-hasPermi="['infra:demo03-student:create']"
     >
-      <Icon class="mr-5px" icon="ep:plus" />
-      新增
+      <Icon icon="ep:plus" class="mr-5px" /> 新增
     </el-button>
-    <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
-      <el-table-column align="center" label="编号" prop="id" />
-      <el-table-column align="center" label="名字" prop="name" />
-      <el-table-column align="center" label="分数" prop="score" />
+      <el-button
+          type="danger"
+          plain
+          :disabled="isEmpty(checkedIds)"
+          @click="handleDeleteBatch"
+          v-hasPermi="['infra:demo03-student:delete']"
+      >
+        <Icon icon="ep:delete" class="mr-5px" /> 批量删除
+      </el-button>
+    <el-table
+        row-key="id"
+        v-loading="loading"
+        :data="list"
+        :stripe="true"
+        :show-overflow-tooltip="true"
+        @selection-change="handleRowCheckboxChange"
+    >
+          <el-table-column type="selection" width="55" />
+      <el-table-column label="编号" align="center" prop="id" />
+       <el-table-column label="名字" align="center" prop="name" />
+      <el-table-column label="分数" align="center" prop="score" />
       <el-table-column
-        :formatter="dateFormatter"
-        align="center"
         label="创建时间"
+        align="center"
         prop="createTime"
+        :formatter="dateFormatter"
         width="180px"
       />
-      <el-table-column align="center" label="操作">
+      <el-table-column label="操作" align="center">
         <template #default="scope">
           <el-button
-            v-hasPermi="['infra:demo03-student:update']"
             link
             type="primary"
             @click="openForm('update', scope.row.id)"
+            v-hasPermi="['infra:demo03-student:update']"
           >
             编辑
           </el-button>
           <el-button
-            v-hasPermi="['infra:demo03-student:delete']"
             link
             type="danger"
             @click="handleDelete(scope.row.id)"
+            v-hasPermi="['infra:demo03-student:delete']"
           >
             删除
           </el-button>
@@ -44,19 +60,19 @@
     </el-table>
     <!-- 分页 -->
     <Pagination
-      v-model:limit="queryParams.pageSize"
-      v-model:page="queryParams.pageNo"
       :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
       @pagination="getList"
     />
   </ContentWrap>
-  <!-- 表单弹窗:添加/修改 -->
-  <Demo03CourseForm ref="formRef" @success="getList" />
+    <!-- 表单弹窗:添加/修改 -->
+    <Demo03CourseForm ref="formRef" @success="getList" />
 </template>
-
-<script lang="ts" setup>
+<script setup lang="ts">
 import { dateFormatter } from '@/utils/formatTime'
-import * as Demo03StudentApi from '@/api/infra/demo/demo03/erp'
+import { isEmpty } from '@/utils/is'
+import {Demo03Course, Demo03StudentApi} from '@/api/infra/demo/demo03/erp'
 import Demo03CourseForm from './Demo03CourseForm.vue'
 
 const { t } = useI18n() // 国际化
@@ -84,7 +100,7 @@ watch(
     queryParams.studentId = val
     handleQuery()
   },
-  { immediate: true, deep: true }
+    { immediate: true, deep: true }
 )
 
 /** 查询列表 */
@@ -127,4 +143,20 @@ const handleDelete = async (id: number) => {
     await getList()
   } catch {}
 }
+
+/** 批量删除学生课程 */
+const handleDeleteBatch = async () => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    await Demo03StudentApi.deleteDemo03CourseList(checkedIds.value);
+    message.success(t('common.delSuccess'))
+    await getList();
+  } catch {}
+}
+
+const checkedIds = ref<number[]>([])
+const handleRowCheckboxChange = (records: Demo03Course[]) => {
+  checkedIds.value = records.map((item) => item.id);
+}
 </script>

+ 8 - 8
src/views/infra/demo/demo03/erp/components/Demo03GradeForm.vue

@@ -7,7 +7,7 @@
       label-width="100px"
       v-loading="formLoading"
     >
-      <el-form-item label="名字" prop="name">
+       <el-form-item label="名字" prop="name">
         <el-input v-model="formData.name" placeholder="请输入名字" />
       </el-form-item>
       <el-form-item label="班主任" prop="teacher">
@@ -21,7 +21,7 @@
   </Dialog>
 </template>
 <script setup lang="ts">
-import * as Demo03StudentApi from '@/api/infra/demo/demo03/erp'
+import { Demo03StudentApi, Demo03Grade } from '@/api/infra/demo/demo03/erp'
 
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
@@ -34,22 +34,22 @@ const formData = ref({
   id: undefined,
   studentId: undefined,
   name: undefined,
-  teacher: undefined
+  teacher: undefined,
 })
 const formRules = reactive({
   studentId: [{ required: true, message: '学生编号不能为空', trigger: 'blur' }],
   name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
-  teacher: [{ required: true, message: '班主任不能为空', trigger: 'blur' }]
+  teacher: [{ required: true, message: '班主任不能为空', trigger: 'blur' }],
 })
 const formRef = ref() // 表单 Ref
 
 /** 打开弹窗 */
-const open = async (type: string, id?: number, studentId: number) => {
+const open = async (type: string, id?: number, studentId?: number) => {
   dialogVisible.value = true
   dialogTitle.value = t('action.' + type)
   formType.value = type
   resetForm()
-  formData.value.studentId = studentId
+  formData.value.studentId = studentId  as any
   // 修改时,设置数据
   if (id) {
     formLoading.value = true
@@ -70,7 +70,7 @@ const submitForm = async () => {
   // 提交请求
   formLoading.value = true
   try {
-    const data = formData.value
+    const data = formData.value as unknown as  Demo03Grade
     if (formType.value === 'create') {
       await Demo03StudentApi.createDemo03Grade(data)
       message.success(t('common.createSuccess'))
@@ -92,7 +92,7 @@ const resetForm = () => {
     id: undefined,
     studentId: undefined,
     name: undefined,
-    teacher: undefined
+    teacher: undefined,
   }
   formRef.value?.resetFields()
 }

+ 53 - 21
src/views/infra/demo/demo03/erp/components/Demo03GradeList.vue

@@ -2,40 +2,56 @@
   <!-- 列表 -->
   <ContentWrap>
     <el-button
-      v-hasPermi="['infra:demo03-student:create']"
-      plain
       type="primary"
+      plain
       @click="openForm('create')"
+      v-hasPermi="['infra:demo03-student:create']"
     >
-      <Icon class="mr-5px" icon="ep:plus" />
-      新增
+      <Icon icon="ep:plus" class="mr-5px" /> 新增
     </el-button>
-    <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
-      <el-table-column align="center" label="编号" prop="id" />
-      <el-table-column align="center" label="名字" prop="name" />
-      <el-table-column align="center" label="班主任" prop="teacher" />
+      <el-button
+          type="danger"
+          plain
+          :disabled="isEmpty(checkedIds)"
+          @click="handleDeleteBatch"
+          v-hasPermi="['infra:demo03-student:delete']"
+      >
+        <Icon icon="ep:delete" class="mr-5px" /> 批量删除
+      </el-button>
+    <el-table
+        row-key="id"
+        v-loading="loading"
+        :data="list"
+        :stripe="true"
+        :show-overflow-tooltip="true"
+        @selection-change="handleRowCheckboxChange"
+    >
+          <el-table-column type="selection" width="55" />
+      <el-table-column label="编号" align="center" prop="id" />
+       <el-table-column label="名字" align="center" prop="name" />
+      <el-table-column label="班主任" align="center" prop="teacher" />
       <el-table-column
-        :formatter="dateFormatter"
-        align="center"
         label="创建时间"
+        align="center"
         prop="createTime"
+        :formatter="dateFormatter"
         width="180px"
       />
-      <el-table-column align="center" label="操作">
+      <el-table-column label="操作" align="center">
         <template #default="scope">
           <el-button
-            v-hasPermi="['infra:demo03-student:update']"
             link
             type="primary"
             @click="openForm('update', scope.row.id)"
+            v-hasPermi="['infra:demo03-student:update']"
           >
             编辑
           </el-button>
           <el-button
-            v-hasPermi="['infra:demo03-student:delete']"
             link
             type="danger"
             @click="handleDelete(scope.row.id)"
+            v-hasPermi="['infra:demo03-student:delete']"
           >
             删除
           </el-button>
@@ -44,19 +60,19 @@
     </el-table>
     <!-- 分页 -->
     <Pagination
-      v-model:limit="queryParams.pageSize"
-      v-model:page="queryParams.pageNo"
       :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
       @pagination="getList"
     />
   </ContentWrap>
-  <!-- 表单弹窗:添加/修改 -->
-  <Demo03GradeForm ref="formRef" @success="getList" />
+    <!-- 表单弹窗:添加/修改 -->
+    <Demo03GradeForm ref="formRef" @success="getList" />
 </template>
-
-<script lang="ts" setup>
+<script setup lang="ts">
 import { dateFormatter } from '@/utils/formatTime'
-import * as Demo03StudentApi from '@/api/infra/demo/demo03/erp'
+import { isEmpty } from '@/utils/is'
+import {Demo03Grade, Demo03StudentApi} from '@/api/infra/demo/demo03/erp'
 import Demo03GradeForm from './Demo03GradeForm.vue'
 
 const { t } = useI18n() // 国际化
@@ -84,7 +100,7 @@ watch(
     queryParams.studentId = val
     handleQuery()
   },
-  { immediate: true, deep: true }
+    { immediate: true, deep: true }
 )
 
 /** 查询列表 */
@@ -127,4 +143,20 @@ const handleDelete = async (id: number) => {
     await getList()
   } catch {}
 }
+
+/** 批量删除学生班级 */
+const handleDeleteBatch = async () => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    await Demo03StudentApi.deleteDemo03GradeList(checkedIds.value);
+    message.success(t('common.delSuccess'))
+    await getList();
+  } catch {}
+}
+
+const checkedIds = ref<number[]>([])
+const handleRowCheckboxChange = (records: Demo03Grade[]) => {
+  checkedIds.value = records.map((item) => item.id);
+}
 </script>

+ 78 - 53
src/views/infra/demo/demo03/erp/index.vue

@@ -1,26 +1,29 @@
 <template>
-  <doc-alert title="代码生成(主子表)" url="https://doc.iocoder.cn/new-feature/master-sub/" />
-
   <ContentWrap>
     <!-- 搜索工作栏 -->
     <el-form
+      class="-mb-15px"
+      :model="queryParams"
       ref="queryFormRef"
       :inline="true"
-      :model="queryParams"
-      class="-mb-15px"
       label-width="68px"
     >
       <el-form-item label="名字" prop="name">
         <el-input
           v-model="queryParams.name"
-          class="!w-240px"
-          clearable
           placeholder="请输入名字"
+          clearable
           @keyup.enter="handleQuery"
+          class="!w-240px"
         />
       </el-form-item>
       <el-form-item label="性别" prop="sex">
-        <el-select v-model="queryParams.sex" class="!w-240px" clearable placeholder="请选择性别">
+        <el-select
+          v-model="queryParams.sex"
+          placeholder="请选择性别"
+          clearable
+          class="!w-240px"
+        >
           <el-option
             v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
             :key="dict.value"
@@ -32,41 +35,42 @@
       <el-form-item label="创建时间" prop="createTime">
         <el-date-picker
           v-model="queryParams.createTime"
-          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
-          class="!w-240px"
-          end-placeholder="结束日期"
-          start-placeholder="开始日期"
-          type="daterange"
           value-format="YYYY-MM-DD HH:mm:ss"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-220px"
         />
       </el-form-item>
       <el-form-item>
-        <el-button @click="handleQuery">
-          <Icon class="mr-5px" icon="ep:search" />
-          搜索
-        </el-button>
-        <el-button @click="resetQuery">
-          <Icon class="mr-5px" icon="ep:refresh" />
-          重置
-        </el-button>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
         <el-button
-          v-hasPermi="['infra:demo03-student:create']"
-          plain
           type="primary"
+          plain
           @click="openForm('create')"
+          v-hasPermi="['infra:demo03-student:create']"
         >
-          <Icon class="mr-5px" icon="ep:plus" />
-          新增
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
         </el-button>
         <el-button
-          v-hasPermi="['infra:demo03-student:export']"
-          :loading="exportLoading"
-          plain
           type="success"
+          plain
           @click="handleExport"
+          :loading="exportLoading"
+          v-hasPermi="['infra:demo03-student:export']"
         >
-          <Icon class="mr-5px" icon="ep:download" />
-          导出
+          <Icon icon="ep:download" class="mr-5px" /> 导出
+        </el-button>
+        <el-button
+            type="danger"
+            plain
+            :disabled="isEmpty(checkedIds)"
+            @click="handleDeleteBatch"
+            v-hasPermi="['infra:demo03-student:delete']"
+        >
+          <Icon icon="ep:delete" class="mr-5px" /> 批量删除
         </el-button>
       </el-form-item>
     </el-form>
@@ -75,50 +79,53 @@
   <!-- 列表 -->
   <ContentWrap>
     <el-table
+      row-key="id"
       v-loading="loading"
       :data="list"
-      :show-overflow-tooltip="true"
       :stripe="true"
+      :show-overflow-tooltip="true"
       highlight-current-row
       @current-change="handleCurrentChange"
+      @selection-change="handleRowCheckboxChange"
     >
-      <el-table-column align="center" label="编号" prop="id" />
-      <el-table-column align="center" label="名字" prop="name" />
-      <el-table-column align="center" label="性别" prop="sex">
+    <el-table-column type="selection" width="55" />
+      <el-table-column label="编号" align="center" prop="id" />
+      <el-table-column label="名字" align="center" prop="name" />
+      <el-table-column label="性别" align="center" prop="sex">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="scope.row.sex" />
         </template>
       </el-table-column>
       <el-table-column
-        :formatter="dateFormatter"
-        align="center"
         label="出生日期"
+        align="center"
         prop="birthday"
+        :formatter="dateFormatter"
         width="180px"
       />
-      <el-table-column align="center" label="简介" prop="description" />
+      <el-table-column label="简介" align="center" prop="description" />
       <el-table-column
-        :formatter="dateFormatter"
-        align="center"
         label="创建时间"
+        align="center"
         prop="createTime"
+        :formatter="dateFormatter"
         width="180px"
       />
-      <el-table-column align="center" label="操作">
+      <el-table-column label="操作" align="center" min-width="120px">
         <template #default="scope">
           <el-button
-            v-hasPermi="['infra:demo03-student:update']"
             link
             type="primary"
             @click="openForm('update', scope.row.id)"
+            v-hasPermi="['infra:demo03-student:update']"
           >
             编辑
           </el-button>
           <el-button
-            v-hasPermi="['infra:demo03-student:delete']"
             link
             type="danger"
             @click="handleDelete(scope.row.id)"
+            v-hasPermi="['infra:demo03-student:delete']"
           >
             删除
           </el-button>
@@ -127,9 +134,9 @@
     </el-table>
     <!-- 分页 -->
     <Pagination
-      v-model:limit="queryParams.pageSize"
-      v-model:page="queryParams.pageNo"
       :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
       @pagination="getList"
     />
   </ContentWrap>
@@ -140,39 +147,41 @@
   <ContentWrap>
     <el-tabs model-value="demo03Course">
       <el-tab-pane label="学生课程" name="demo03Course">
-        <Demo03CourseList :student-id="currentRow?.id" />
+        <Demo03CourseList :student-id="currentRow.id" />
       </el-tab-pane>
       <el-tab-pane label="学生班级" name="demo03Grade">
-        <Demo03GradeList :student-id="currentRow?.id" />
+        <Demo03GradeList :student-id="currentRow.id" />
       </el-tab-pane>
     </el-tabs>
   </ContentWrap>
 </template>
 
-<script lang="ts" setup>
-import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { isEmpty } from '@/utils/is'
 import { dateFormatter } from '@/utils/formatTime'
 import download from '@/utils/download'
-import * as Demo03StudentApi from '@/api/infra/demo/demo03/erp'
+import { Demo03StudentApi, Demo03Student } from '@/api/infra/demo/demo03/erp'
 import Demo03StudentForm from './Demo03StudentForm.vue'
 import Demo03CourseList from './components/Demo03CourseList.vue'
 import Demo03GradeList from './components/Demo03GradeList.vue'
 
+/** 学生 列表 */
 defineOptions({ name: 'Demo03Student' })
 
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
 
 const loading = ref(true) // 列表的加载中
-const list = ref([]) // 列表的数据
+const list = ref<Demo03Student[]>([]) // 列表的数据
 const total = ref(0) // 列表的总页数
 const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
-  name: null,
-  sex: null,
-  description: null,
-  createTime: []
+  name: undefined,
+  sex: undefined,
+  description: undefined,
+  createTime: [],
 })
 const queryFormRef = ref() // 搜索的表单
 const exportLoading = ref(false) // 导出的加载中
@@ -220,6 +229,22 @@ const handleDelete = async (id: number) => {
   } catch {}
 }
 
+/** 批量删除学生 */
+const handleDeleteBatch = async () => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    await Demo03StudentApi.deleteDemo03StudentList(checkedIds.value);
+    message.success(t('common.delSuccess'))
+    await getList();
+  } catch {}
+}
+
+const checkedIds = ref<number[]>([])
+const handleRowCheckboxChange = (records: Demo03Student[]) => {
+  checkedIds.value = records.map((item) => item.id);
+}
+
 /** 导出按钮操作 */
 const handleExport = async () => {
   try {

+ 0 - 0
src/views/infra/demo/demo03/inner/Demo03StudentForm.vue


برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است