Bläddra i källkod

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

# Conflicts:
#	src/api/system/dept/index.ts
YunaiV 6 månader sedan
förälder
incheckning
a269c5988a
100 ändrade filer med 1268 tillägg och 1775 borttagningar
  1. 1 1
      package.json
  2. 1 0
      src/api/bpm/processInstance/index.ts
  3. 4 0
      src/api/bpm/task/index.ts
  4. 5 0
      src/api/infra/codegen/index.ts
  5. 6 1
      src/api/infra/config/index.ts
  6. 5 0
      src/api/infra/dataSourceConfig/index.ts
  7. 5 0
      src/api/infra/file/index.ts
  8. 5 0
      src/api/infra/fileConfig/index.ts
  9. 5 0
      src/api/infra/job/index.ts
  10. 1 1
      src/api/mall/product/spu.ts
  11. 21 11
      src/api/system/dept/index.ts
  12. 16 6
      src/api/system/dict/dict.data.ts
  13. 15 6
      src/api/system/dict/dict.type.ts
  14. 1 1
      src/api/system/loginLog/index.ts
  15. 5 0
      src/api/system/mail/account/index.ts
  16. 5 0
      src/api/system/mail/log/index.ts
  17. 5 0
      src/api/system/mail/template/index.ts
  18. 5 0
      src/api/system/notice/index.ts
  19. 5 0
      src/api/system/notify/template/index.ts
  20. 5 0
      src/api/system/oauth2/client.ts
  21. 1 1
      src/api/system/operatelog/index.ts
  22. 6 1
      src/api/system/post/index.ts
  23. 5 0
      src/api/system/role/index.ts
  24. 5 0
      src/api/system/sms/smsChannel/index.ts
  25. 5 0
      src/api/system/sms/smsTemplate/index.ts
  26. 6 1
      src/api/system/social/user/index.ts
  27. 5 0
      src/api/system/tenant/index.ts
  28. 6 0
      src/api/system/tenantPackage/index.ts
  29. 6 1
      src/api/system/user/index.ts
  30. 0 4
      src/api/system/user/profile.ts
  31. 7 3
      src/components/AppLinkInput/AppLinkSelectDialog.vue
  32. 0 1
      src/components/Cropper/src/CopperModal.vue
  33. 3 0
      src/components/DiyEditor/components/mobile/Carousel/config.ts
  34. 1 1
      src/components/DiyEditor/components/mobile/Carousel/index.vue
  35. 3 0
      src/components/DiyEditor/components/mobile/Carousel/property.vue
  36. 31 1
      src/components/DiyEditor/components/mobile/NavigationBar/components/CellProperty.vue
  37. 6 0
      src/components/DiyEditor/components/mobile/NavigationBar/config.ts
  38. 5 2
      src/components/DiyEditor/components/mobile/NavigationBar/index.vue
  39. 5 0
      src/components/DiyEditor/index.vue
  40. 1 0
      src/components/Echart/src/Echart.vue
  41. 0 13
      src/components/SimpleProcessDesignerV2/src/NodeHandler.vue
  42. 50 21
      src/components/SimpleProcessDesignerV2/src/SimpleProcessDesigner.vue
  43. 2 0
      src/components/SimpleProcessDesignerV2/src/consts.ts
  44. 1 0
      src/components/SimpleProcessDesignerV2/src/node.ts
  45. 10 0
      src/components/SimpleProcessDesignerV2/src/nodes-config/UserTaskNodeConfig.vue
  46. 35 24
      src/components/SimpleProcessDesignerV2/src/nodes-config/components/HttpRequestParamSetting.vue
  47. 4 2
      src/components/SimpleProcessDesignerV2/src/nodes-config/components/HttpRequestSetting.vue
  48. 4 0
      src/components/UploadFile/src/UploadFile.vue
  49. 14 4
      src/components/UploadFile/src/UploadImgs.vue
  50. 10 0
      src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/flowableDescriptor.json
  51. 14 3
      src/components/bpmnProcessDesigner/package/penal/custom-config/components/BoundaryEventTimer.vue
  52. 7 6
      src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue
  53. 3 1
      src/plugins/formCreate/index.ts
  54. 1 1
      src/router/index.ts
  55. 1 1
      src/store/modules/user.ts
  56. 149 2
      src/utils/formCreate.ts
  57. 4 3
      src/utils/routerHelper.ts
  58. 1 1
      src/views/Login/Login.vue
  59. 18 18
      src/views/Login/components/ForgetPasswordForm.vue
  60. 25 23
      src/views/Login/components/LoginForm.vue
  61. 10 10
      src/views/Login/components/MobileForm.vue
  62. 7 7
      src/views/Login/components/QrCodeForm.vue
  63. 19 17
      src/views/Login/components/RegisterForm.vue
  64. 4 4
      src/views/Login/components/SSOLogin.vue
  65. 6 5
      src/views/Profile/components/UserAvatar.vue
  66. 8 8
      src/views/Profile/components/UserSocial.vue
  67. 70 151
      src/views/ai/chat/index/components/conversation/ConversationList.vue
  68. 59 136
      src/views/ai/chat/index/components/message/MessageList.vue
  69. 5 52
      src/views/ai/chat/index/components/message/MessageListEmpty.vue
  70. 1 10
      src/views/ai/chat/index/components/message/MessageLoading.vue
  71. 4 31
      src/views/ai/chat/index/components/message/MessageNewConversation.vue
  72. 2 16
      src/views/ai/chat/index/components/role/RoleCategoryList.vue
  73. 3 30
      src/views/ai/chat/index/components/role/RoleHeader.vue
  74. 26 94
      src/views/ai/chat/index/components/role/RoleList.vue
  75. 26 77
      src/views/ai/chat/index/components/role/RoleRepository.vue
  76. 26 218
      src/views/ai/chat/index/index.vue
  77. 22 53
      src/views/ai/image/index/components/ImageCard.vue
  78. 100 137
      src/views/ai/image/index/components/ImageDetail.vue
  79. 14 51
      src/views/ai/image/index/components/ImageList.vue
  80. 10 45
      src/views/ai/image/index/components/common/index.vue
  81. 17 148
      src/views/ai/image/index/components/dall3/index.vue
  82. 18 132
      src/views/ai/image/index/components/midjourney/index.vue
  83. 18 57
      src/views/ai/image/index/components/stableDiffusion/index.vue
  84. 12 53
      src/views/ai/image/index/index.vue
  85. 5 42
      src/views/ai/image/square/index.vue
  86. 0 1
      src/views/ai/write/index/components/Tag.vue
  87. 9 4
      src/views/bpm/model/CategoryDraggableModel.vue
  88. 18 2
      src/views/bpm/model/form/BasicInfo.vue
  89. 28 0
      src/views/bpm/model/form/ExtraSettings.vue
  90. 2 2
      src/views/bpm/model/form/ProcessDesign.vue
  91. 14 1
      src/views/bpm/model/form/index.vue
  92. 4 1
      src/views/bpm/model/index.vue
  93. 26 2
      src/views/bpm/processInstance/create/ProcessDefinitionDetail.vue
  94. 1 0
      src/views/bpm/processInstance/detail/ProcessInstanceOperationButton.vue
  95. 18 4
      src/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue
  96. 5 6
      src/views/bpm/simple/SimpleModelDesign.vue
  97. 1 1
      src/views/crm/customer/CustomerImportForm.vue
  98. 40 0
      src/views/crm/followup/index.vue
  99. 29 1
      src/views/infra/codegen/index.vue
  100. 0 0
      src/views/infra/config/index.vue

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "yudao-ui-admin-vue3",
-  "version": "2.6.0-snapshot",
+  "version": "2025.08-snapshot",
   "description": "基于vue3、vite4、element-plus、typesScript",
   "author": "xingyu",
   "private": false,

+ 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[]
 }

+ 4 - 0
src/api/bpm/task/index.ts

@@ -5,6 +5,10 @@ import request from '@/config/axios'
  */
 export enum TaskStatusEnum {
   /**
+   * 跳过
+   */
+  SKIP = -2,
+  /**
    * 未开始
    */
   NOT_START = -1,

+ 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 })

+ 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 精简列表

+ 21 - 11
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 = async () => {
-  return await request.get({ url: '/system/dept/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/log/index.ts

@@ -28,3 +28,8 @@ export const getMailLogPage = async (params: PageParam) => {
 export const getMailLog = async (id: number) => {
   return await request.get({ url: '/system/mail-log/get?id=' + id })
 }
+
+// 导出邮件日志
+export const exportMailLog = (params) => {
+  return request.download({ url: '/system/mail-log/export-excel', params })
+}

+ 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

@@ -42,6 +42,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: any) => {
   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

+ 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
 }
 

+ 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()
     }
   },
   {

+ 0 - 13
src/components/SimpleProcessDesignerV2/src/NodeHandler.vue

@@ -97,8 +97,6 @@ defineOptions({
   name: 'NodeHandler'
 })
 
-const message = useMessage() // 消息弹窗
-
 const popoverShow = ref(false)
 const props = defineProps({
   childNode: {
@@ -115,17 +113,6 @@ const emits = defineEmits(['update:childNode'])
 const readonly = inject<Boolean>('readonly') // 是否只读
 
 const addNode = (type: number) => {
-  // 校验:条件分支、包容分支后面,不允许直接添加并行分支
-  if (
-    type === NodeType.PARALLEL_BRANCH_NODE &&
-    [NodeType.CONDITION_BRANCH_NODE, NodeType.INCLUSIVE_BRANCH_NODE].includes(
-      props.currentNode?.type
-    )
-  ) {
-    message.error('条件分支、包容分支后面,不允许直接添加并行分支')
-    return
-  }
-
   popoverShow.value = false
   if (type === NodeType.USER_TASK_NODE || type === NodeType.TRANSACTOR_NODE) {
     const id = 'Activity_' + generateUUID()

+ 50 - 21
src/components/SimpleProcessDesignerV2/src/SimpleProcessDesigner.vue

@@ -26,8 +26,7 @@
 <script setup lang="ts">
 import SimpleProcessModel from './SimpleProcessModel.vue'
 import { SimpleFlowNode, NodeType, NodeId, NODE_DEFAULT_TEXT } from './consts'
-import { getModel } from '@/api/bpm/model'
-import { getForm, FormVO } from '@/api/bpm/form'
+import { getForm } from '@/api/bpm/form'
 import { handleTree } from '@/utils/tree'
 import * as RoleApi from '@/api/system/role'
 import * as DeptApi from '@/api/system/dept'
@@ -43,17 +42,21 @@ defineOptions({
 const emits = defineEmits(['success']) // 保存成功事件
 
 const props = defineProps({
-  modelId: {
+  modelName: {
     type: String,
     required: false
   },
-  modelKey: {
-    type: String,
-    required: false
+  // 流程表单 ID
+  modelFormId: {
+    type: Number,
+    required: false,
+    default: undefined,
   },
-  modelName: {
-    type: String,
-    required: false
+   // 表单类型
+  modelFormType: {
+    type: Number,
+    required: false,
+    default: BpmModelFormType.NORMAL,
   },
   // 可发起流程的人员编号
   startUserIds: {
@@ -70,7 +73,31 @@ const props = defineProps({
 const processData = inject('processData') as Ref
 const loading = ref(false)
 const formFields = ref<string[]>([])
-const formType = ref(20)
+const formType = ref(props.modelFormType);
+
+// 监听 modelFormType 变化
+watch(
+  () => props.modelFormType,
+  (newVal) => {
+    formType.value = newVal;
+  },
+);
+
+// 监听 modelFormId 变化
+watch(
+  () => props.modelFormId,
+  async (newVal) => {
+    if (newVal) {
+      const form = await getForm(newVal);
+      formFields.value = form?.fields;
+    } else {
+      // 如果 modelFormId 为空,清空表单字段
+      formFields.value = [];
+    }
+  },
+  { immediate: true },
+);
+
 const roleOptions = ref<RoleApi.RoleVO[]>([]) // 角色列表
 const postOptions = ref<PostApi.PostVO[]>([]) // 岗位列表
 const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
@@ -90,6 +117,8 @@ provide('startUserIds', props.startUserIds)
 provide('startDeptIds', props.startDeptIds)
 provide('tasks', [])
 provide('processInstance', {})
+
+
 const message = useMessage() // 国际化
 const processNodeTree = ref<SimpleFlowNode | undefined>()
 provide('processNodeTree', processNodeTree)
@@ -169,17 +198,17 @@ const validateNode = (node: SimpleFlowNode | undefined, errorNodes: SimpleFlowNo
 onMounted(async () => {
   try {
     loading.value = true
-    // 获取表单字段
-    if (props.modelId) {
-      const bpmnModel = await getModel(props.modelId)
-      if (bpmnModel) {
-        formType.value = bpmnModel.formType
-        if (formType.value === BpmModelFormType.NORMAL && bpmnModel.formId) {
-          const bpmnForm = (await getForm(bpmnModel.formId)) as unknown as FormVO
-          formFields.value = bpmnForm?.fields
-        }
-      }
-    }
+    // // 获取表单字段
+    // if (props.modelId) {
+    //   const bpmnModel = await getModel(props.modelId)
+    //   if (bpmnModel) {
+    //     formType.value = bpmnModel.formType
+    //     if (formType.value === BpmModelFormType.NORMAL && bpmnModel.formId) {
+    //       const bpmnForm = (await getForm(bpmnModel.formId)) as unknown as FormVO
+    //       formFields.value = bpmnForm?.fields
+    //     }
+    //   }
+    // }
     // 获得角色列表
     roleOptions.value = await RoleApi.getSimpleRoleList()
     // 获得岗位列表

+ 2 - 0
src/components/SimpleProcessDesignerV2/src/consts.ts

@@ -131,6 +131,8 @@ export interface SimpleFlowNode {
   signEnable?: boolean
   // 审批意见
   reasonRequire?: boolean
+  // 跳过表达式
+  skipExpression?: string
   // 触发器设置
   triggerSetting?: TriggerSetting
   // 子流程

+ 1 - 0
src/components/SimpleProcessDesignerV2/src/node.ts

@@ -177,6 +177,7 @@ export type UserTaskFormType = {
   }
   signEnable: boolean
   reasonRequire: boolean
+  skipExpression?: string
 }
 
 export type CopyTaskFormType = {

+ 10 - 0
src/components/SimpleProcessDesignerV2/src/nodes-config/UserTaskNodeConfig.vue

@@ -411,6 +411,12 @@
                 />
               </el-form-item>
             </div>
+            <div>
+              <el-divider content-position="left">跳过表达式</el-divider>
+              <el-form-item prop="skipExpression">
+                <el-input v-model="configForm.skipExpression" type="textarea"  />
+              </el-form-item>
+            </div>
           </el-form>
         </div>
       </el-tab-pane>
@@ -770,6 +776,8 @@ const saveConfig = async () => {
   currentNode.value.signEnable = configForm.value.signEnable
   // 审批意见
   currentNode.value.reasonRequire = configForm.value.reasonRequire
+  // 跳过表达式
+  currentNode.value.skipExpression = configForm.value.skipExpression
 
   currentNode.value.showText = showText
   settingVisible.value = false
@@ -851,6 +859,8 @@ const showUserTaskNodeConfig = (node: SimpleFlowNode) => {
   configForm.value.signEnable = node?.signEnable ?? false
   // 7. 审批意见
   configForm.value.reasonRequire = node?.reasonRequire ?? false
+  // 8. 跳过表达式
+  configForm.value.skipExpression = node?.skipExpression ?? ''
 }
 
 defineExpose({ openDrawer, showUserTaskNodeConfig }) // 暴露方法给父组件

+ 35 - 24
src/components/SimpleProcessDesignerV2/src/nodes-config/components/HttpRequestParamSetting.vue

@@ -1,6 +1,6 @@
 <template>
   <el-form-item label-position="top" label="请求头">
-    <div class="flex pt-2" v-for="(item, index) in props.header" :key="index">
+    <div class="flex pb-4" v-for="(item, index) in props.header" :key="index">
       <div class="mr-2">
         <el-form-item
           :prop="`${bind}.header.${index}.key`"
@@ -10,18 +10,20 @@
             trigger: 'blur'
           }"
         >
-          <el-input class="w-160px" v-model="item.key" />
+          <el-input v-model="item.key" style="width: 160px" />
         </el-form-item>
       </div>
       <div class="mr-2">
-        <el-select class="w-100px!" v-model="item.type">
-          <el-option
-            v-for="types in BPM_HTTP_REQUEST_PARAM_TYPES"
-            :key="types.value"
-            :label="types.label"
-            :value="types.value"
-          />
-        </el-select>
+        <el-form-item>
+          <el-select v-model="item.type" style="width: 160px" @change="handleTypeChange(item)">
+            <el-option
+              v-for="types in BPM_HTTP_REQUEST_PARAM_TYPES"
+              :key="types.value"
+              :label="types.label"
+              :value="types.value"
+            />
+          </el-select>
+        </el-form-item>
       </div>
       <div class="mr-2">
         <el-form-item
@@ -34,8 +36,8 @@
         >
           <el-input
             v-if="item.type === BpmHttpRequestParamTypeEnum.FIXED_VALUE"
-            class="w-160px"
             v-model="item.value"
+            style="width: 200px"
           />
         </el-form-item>
         <el-form-item
@@ -48,8 +50,8 @@
         >
           <el-select
             v-if="item.type === BpmHttpRequestParamTypeEnum.FROM_FORM"
-            class="w-160px!"
             v-model="item.value"
+            style="width: 200px"
           >
             <el-option
               v-for="(field, fIdx) in formFieldOptions"
@@ -70,7 +72,7 @@
     </el-button>
   </el-form-item>
   <el-form-item label-position="top" label="请求体">
-    <div class="flex pt-2" v-for="(item, index) in props.body" :key="index">
+    <div class="flex pb-4" v-for="(item, index) in props.body" :key="index">
       <div class="mr-2">
         <el-form-item
           :prop="`${bind}.body.${index}.key`"
@@ -80,18 +82,20 @@
             trigger: 'blur'
           }"
         >
-          <el-input class="w-160px" v-model="item.key" />
+          <el-input v-model="item.key" style="width: 160px" />
         </el-form-item>
       </div>
       <div class="mr-2">
-        <el-select class="w-100px!" v-model="item.type">
-          <el-option
-            v-for="types in BPM_HTTP_REQUEST_PARAM_TYPES"
-            :key="types.value"
-            :label="types.label"
-            :value="types.value"
-          />
-        </el-select>
+        <el-form-item>
+          <el-select v-model="item.type" style="width: 160px" @change="handleTypeChange(item)">
+            <el-option
+              v-for="types in BPM_HTTP_REQUEST_PARAM_TYPES"
+              :key="types.value"
+              :label="types.label"
+              :value="types.value"
+            />
+          </el-select>
+        </el-form-item>
       </div>
       <div class="mr-2">
         <el-form-item
@@ -104,8 +108,8 @@
         >
           <el-input
             v-if="item.type === BpmHttpRequestParamTypeEnum.FIXED_VALUE"
-            class="w-160px"
             v-model="item.value"
+            style="width: 200px"
           />
         </el-form-item>
         <el-form-item
@@ -118,8 +122,8 @@
         >
           <el-select
             v-if="item.type === BpmHttpRequestParamTypeEnum.FROM_FORM"
-            class="w-160px!"
             v-model="item.value"
+            style="width: 200px"
           >
             <el-option
               v-for="(field, fIdx) in formFieldOptions"
@@ -170,6 +174,13 @@ const props = defineProps({
 
 // 流程表单字段,发起人字段
 const formFieldOptions = useFormFieldsAndStartUser()
+
+/** 监听类型变化,清空值 */
+const handleTypeChange = (item: HttpRequestParam) => {
+  // 当类型改变时,清空值
+  item.value = ''
+}
+
 /** 添加请求配置项 */
 const addHttpRequestParam = (arr: HttpRequestParam[]) => {
   arr.push({

+ 4 - 2
src/components/SimpleProcessDesignerV2/src/nodes-config/components/HttpRequestSetting.vue

@@ -33,7 +33,7 @@
       />
     </el-form-item>
     <el-form-item>
-      <div class="flex pt-2" v-for="(item, index) in setting.response" :key="index">
+      <div class="flex pt-4" v-for="(item, index) in setting.response" :key="index">
         <div class="mr-2">
           <el-form-item
             :prop="`${formItemPrefix}.response.${index}.key`"
@@ -74,10 +74,12 @@
           />
         </div>
       </div>
+    </el-form-item>
+    <div class="pt-1">
       <el-button type="primary" text @click="addHttpResponseSetting(setting.response!)">
         <Icon icon="ep:plus" class="mr-5px" />添加一行
       </el-button>
-    </el-form-item>
+    </div>
   </div>
 </template>
 <script setup lang="ts">

+ 4 - 0
src/components/UploadFile/src/UploadFile.vue

@@ -122,7 +122,9 @@ const beforeUpload: UploadProps['beforeUpload'] = (file: UploadRawFile) => {
     return false
   }
   message.success('正在上传文件,请稍候...')
+  // 只有在验证通过后才增加计数器
   uploadNumber.value++
+  return true
 }
 // 处理上传的文件发生变化
 // const handleFileChange = (uploadFile: UploadFile): void => {
@@ -149,6 +151,8 @@ const handleExceed: UploadProps['onExceed'] = (): void => {
 // 上传错误提示
 const excelUploadError: UploadProps['onError'] = (): void => {
   message.error('导入数据失败,请您重新上传!')
+  // 上传失败时减少计数器,避免后续上传被阻塞
+  uploadNumber.value = Math.max(0, uploadNumber.value - 1)
 }
 // 删除上传文件
 const handleRemove = (file: UploadFile) => {

+ 14 - 4
src/components/UploadFile/src/UploadImgs.vue

@@ -97,20 +97,28 @@ const uploadList = ref<UploadUserFile[]>([])
 const beforeUpload: UploadProps['beforeUpload'] = (rawFile) => {
   const imgSize = rawFile.size / 1024 / 1024 < props.fileSize
   const imgType = props.fileType
-  if (!imgType.includes(rawFile.type as FileTypes))
+  const isValidType = imgType.includes(rawFile.type as FileTypes)
+  const isValidSize = imgSize
+
+  if (!isValidType)
     ElNotification({
       title: '温馨提示',
       message: '上传图片不符合所需的格式!',
       type: 'warning'
     })
-  if (!imgSize)
+  if (!isValidSize)
     ElNotification({
       title: '温馨提示',
       message: `上传图片大小不能超过 ${props.fileSize}M!`,
       type: 'warning'
     })
-  uploadNumber.value++
-  return imgType.includes(rawFile.type as FileTypes) && imgSize
+
+  // 只有在验证通过后才增加计数器
+  if (isValidType && isValidSize) {
+    uploadNumber.value++
+  }
+
+  return isValidType && isValidSize
 }
 
 // 图片上传成功
@@ -172,6 +180,8 @@ const uploadError = () => {
     message: '图片上传失败,请您重新上传!',
     type: 'error'
   })
+  // 上传失败时减少计数器,避免后续上传被阻塞
+  uploadNumber.value = Math.max(0, uploadNumber.value - 1)
 }
 
 // 文件数超出提示

+ 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
         }
       ]
     },

+ 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()
 }
 

+ 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)) {

+ 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

+ 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, '/')
 }
 
 // 路由降级

+ 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
         }
       }

+ 70 - 151
src/views/ai/chat/index/components/conversation/ConversationList.vue

@@ -1,9 +1,12 @@
 <!--  AI 对话  -->
 <template>
-  <el-aside width="260px" class="conversation-container h-100%">
+  <el-aside
+    width="260px"
+    class="h-100% relative flex flex-col justify-between px-2.5 pt-2.5 pb-0 overflow-hidden"
+  >
     <!-- 左顶部:对话 -->
     <div class="h-100%">
-      <el-button class="w-1/1 btn-new-conversation" type="primary" @click="createConversation">
+      <el-button class="w-1/1 py-4.5" type="primary" @click="createConversation">
         <Icon icon="ep:plus" class="mr-5px" />
         新建对话
       </el-button>
@@ -12,7 +15,7 @@
       <el-input
         v-model="searchName"
         size="large"
-        class="mt-10px search-input"
+        class="mt-5"
         placeholder="搜索历史记录"
         @keyup="searchConversation"
       >
@@ -22,19 +25,18 @@
       </el-input>
 
       <!-- 左中间:对话列表 -->
-      <div class="conversation-list">
+      <div class="overflow-auto h-full">
         <!-- 情况一:加载中 -->
         <el-empty v-if="loading" description="." :v-loading="loading" />
         <!-- 情况二:按照 group 分组,展示聊天会话 list 列表 -->
         <div v-for="conversationKey in Object.keys(conversationMap)" :key="conversationKey">
-          <div
-            class="conversation-item classify-title"
-            v-if="conversationMap[conversationKey].length"
-          >
-            <el-text class="mx-1" size="small" tag="b">{{ conversationKey }}</el-text>
+          <div class="mt-1.25 pt-2.5" v-if="conversationMap[conversationKey].length">
+            <el-text class="mx-1" size="small" tag="b">
+              {{ conversationKey }}
+            </el-text>
           </div>
           <div
-            class="conversation-item"
+            class="mt-1.25"
             v-for="conversation in conversationMap[conversationKey]"
             :key="conversation.id"
             @click="handleConversationClick(conversation.id)"
@@ -42,25 +44,48 @@
             @mouseout="hoverConversationId = ''"
           >
             <div
-              :class="
-                conversation.id === activeConversationId ? 'conversation active' : 'conversation'
+              class="flex flex-row justify-between flex-1 px-1.25 cursor-pointer rounded-1.25 items-center leading-7.5"
+              :style="
+                conversation.id === activeConversationId
+                  ? 'background-color: var(--el-color-primary-light-9); border: 1px solid var(--el-color-primary-light-7);'
+                  : ''
               "
             >
-              <div class="title-wrapper">
-                <img class="avatar" :src="conversation.roleAvatar || roleAvatarDefaultImg" />
-                <span class="title">{{ conversation.title }}</span>
+              <div class="flex flex-row items-center">
+                <img
+                  class="w-6.25 h-6.25 rounded-1.25 flex flex-row justify-center"
+                  :src="conversation.roleAvatar || roleAvatarDefaultImg"
+                />
+                <span
+                  class="py-0.5 px-2.5"
+                  style="
+                    max-width: 220px;
+                    font-size: 14px;
+                    font-weight: 400;
+                    color: var(--el-text-color-regular);
+                    overflow: hidden;
+                    white-space: nowrap;
+                    text-overflow: ellipsis;
+                  "
+                >
+                  {{ conversation.title }}
+                </span>
               </div>
-              <div class="button-wrapper" v-show="hoverConversationId === conversation.id">
-                <el-button class="btn" link @click.stop="handleTop(conversation)">
+              <div
+                class="right-0.5 flex flex-row justify-center"
+                style="color: var(--el-text-color-regular)"
+                v-show="hoverConversationId === conversation.id"
+              >
+                <el-button class="m-0" link @click.stop="handleTop(conversation)">
                   <el-icon title="置顶" v-if="!conversation.pinned"><Top /></el-icon>
                   <el-icon title="置顶" v-if="conversation.pinned"><Bottom /></el-icon>
                 </el-button>
-                <el-button class="btn" link @click.stop="updateConversationTitle(conversation)">
+                <el-button class="m-0" link @click.stop="updateConversationTitle(conversation)">
                   <el-icon title="编辑">
                     <Icon icon="ep:edit" />
                   </el-icon>
                 </el-button>
-                <el-button class="btn" link @click.stop="deleteChatConversation(conversation)">
+                <el-button class="m-0" link @click.stop="deleteChatConversation(conversation)">
                   <el-icon title="删除对话">
                     <Icon icon="ep:delete" />
                   </el-icon>
@@ -75,14 +100,29 @@
     </div>
 
     <!-- 左底部:工具栏 -->
-    <div class="tool-box">
-      <div @click="handleRoleRepository">
+    <div
+      class="absolute bottom-0 left-0 right-0 px-5 leading-8.75 flex justify-between items-center"
+      style="
+        background-color: var(--el-fill-color-extra-light);
+        box-shadow: 0 0 1px 1px var(--el-border-color-lighter);
+        color: var(--el-text-color);
+      "
+    >
+      <div
+        class="flex items-center p-0 m-0 cursor-pointer"
+        style="color: var(--el-text-color-regular)"
+        @click="handleRoleRepository"
+      >
         <Icon icon="ep:user" />
-        <el-text size="small">角色仓库</el-text>
+        <el-text class="ml-1.25" size="small">角色仓库</el-text>
       </div>
-      <div @click="handleClearConversation">
+      <div
+        class="flex items-center p-0 m-0 cursor-pointer"
+        style="color: var(--el-text-color-regular)"
+        @click="handleClearConversation"
+      >
         <Icon icon="ep:delete" />
-        <el-text size="small">清空未置顶对话</el-text>
+        <el-text class="ml-1.25" size="small">清空未置顶对话</el-text>
       </div>
     </div>
 
@@ -193,12 +233,12 @@ const getConversationGroupByCreateTime = async (list: ChatConversationVO[]) => {
   // 排序、指定、时间分组(今天、一天前、三天前、七天前、30天前)
   // noinspection NonAsciiCharacters
   const groupMap = {
-    置顶: [],
-    今天: [],
-    一天前: [],
-    三天前: [],
-    七天前: [],
-    三十天前: []
+    置顶: [] as ChatConversationVO[],
+    今天: [] as ChatConversationVO[],
+    一天前: [] as ChatConversationVO[],
+    三天前: [] as ChatConversationVO[],
+    七天前: [] as ChatConversationVO[],
+    三十天前: [] as ChatConversationVO[]
   }
   // 当前时间的时间戳
   const now = Date.now()
@@ -349,124 +389,3 @@ onMounted(async () => {
   }
 })
 </script>
-
-<style scoped lang="scss">
-.conversation-container {
-  position: relative;
-  display: flex;
-  flex-direction: column;
-  justify-content: space-between;
-  padding: 10px 10px 0;
-  overflow: hidden;
-
-  .btn-new-conversation {
-    padding: 18px 0;
-  }
-
-  .search-input {
-    margin-top: 20px;
-  }
-
-  .conversation-list {
-    overflow: auto;
-    height: 100%;
-
-    .classify-title {
-      padding-top: 10px;
-    }
-
-    .conversation-item {
-      margin-top: 5px;
-    }
-
-    .conversation {
-      display: flex;
-      flex-direction: row;
-      justify-content: space-between;
-      flex: 1;
-      padding: 0 5px;
-      cursor: pointer;
-      border-radius: 5px;
-      align-items: center;
-      line-height: 30px;
-
-      &.active {
-        background-color: #e6e6e6;
-
-        .button {
-          display: inline-block;
-        }
-      }
-
-      .title-wrapper {
-        display: flex;
-        flex-direction: row;
-        align-items: center;
-      }
-
-      .title {
-        padding: 2px 10px;
-        max-width: 220px;
-        font-size: 14px;
-        font-weight: 400;
-        color: rgba(0, 0, 0, 0.77);
-        overflow: hidden;
-        white-space: nowrap;
-        text-overflow: ellipsis;
-      }
-
-      .avatar {
-        width: 25px;
-        height: 25px;
-        border-radius: 5px;
-        display: flex;
-        flex-direction: row;
-        justify-items: center;
-      }
-
-      // 对话编辑、删除
-      .button-wrapper {
-        right: 2px;
-        display: flex;
-        flex-direction: row;
-        justify-items: center;
-        color: #606266;
-
-        .btn {
-          margin: 0;
-        }
-      }
-    }
-  }
-
-  // 角色仓库、清空未设置对话
-  .tool-box {
-    position: absolute;
-    bottom: 0;
-    left: 0;
-    right: 0;
-    //width: 100%;
-    padding: 0 20px;
-    background-color: #f4f4f4;
-    box-shadow: 0 0 1px 1px rgba(228, 228, 228, 0.8);
-    line-height: 35px;
-    display: flex;
-    justify-content: space-between;
-    align-items: center;
-    color: var(--el-text-color);
-
-    > div {
-      display: flex;
-      align-items: center;
-      color: #606266;
-      padding: 0;
-      margin: 0;
-      cursor: pointer;
-
-      > span {
-        margin-left: 5px;
-      }
-    }
-  }
-}
-</style>

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

@@ -1,52 +1,86 @@
 <template>
   <div ref="messageContainer" class="h-100% overflow-y-auto relative">
-    <div class="chat-list" v-for="(item, index) in list" :key="index">
+    <div class="flex flex-col overflow-y-hidden px-20px" v-for="(item, index) in list" :key="index">
       <!-- 靠左 message:system、assistant 类型 -->
-      <div class="left-message message-item" v-if="item.type !== 'user'">
+      <div class="flex flex-row mt-50px" v-if="item.type !== 'user'">
         <div class="avatar">
           <el-avatar :src="roleAvatar" />
         </div>
-        <div class="message">
+        <div class="flex flex-col text-left mx-15px">
           <div>
-            <el-text class="time">{{ formatDate(item.createTime) }}</el-text>
+            <el-text class="text-left leading-30px">{{ formatDate(item.createTime) }}</el-text>
           </div>
-          <div class="left-text-container" ref="markdownViewRef">
-            <MarkdownView class="left-text" :content="item.content" />
+          <div
+            class="relative flex flex-col break-words bg-[var(--el-fill-color-light)] shadow-[0_0_0_1px_var(--el-border-color-light)] rounded-10px pt-10px px-10px pb-5px"
+            ref="markdownViewRef"
+          >
+            <MarkdownView
+              class="text-[var(--el-text-color-primary)] text-[0.95rem]"
+              :content="item.content"
+            />
             <MessageKnowledge v-if="item.segments" :segments="item.segments" />
           </div>
-          <div class="left-btns">
-            <el-button class="btn-cus" link @click="copyContent(item.content)">
-              <img class="btn-image" src="@/assets/ai/copy.svg" />
+          <div class="flex flex-row mt-8px">
+            <el-button
+              class="flex bg-transparent items-center hover:cursor-pointer hover:bg-[var(--el-fill-color-lighter)]"
+              link
+              @click="copyContent(item.content)"
+            >
+              <img class="h-20px" src="@/assets/ai/copy.svg" />
             </el-button>
-            <el-button v-if="item.id > 0" class="btn-cus" link @click="onDelete(item.id)">
-              <img class="btn-image h-17px" src="@/assets/ai/delete.svg" />
+            <el-button
+              v-if="item.id > 0"
+              class="flex bg-transparent items-center hover:cursor-pointer hover:bg-[var(--el-fill-color-lighter)]"
+              link
+              @click="onDelete(item.id)"
+            >
+              <img class="h-17px" src="@/assets/ai/delete.svg" />
             </el-button>
           </div>
         </div>
       </div>
       <!-- 靠右 message:user 类型 -->
-      <div class="right-message message-item" v-if="item.type === 'user'">
+      <div class="flex flex-row-reverse justify-start mt-50px" v-if="item.type === 'user'">
         <div class="avatar">
           <el-avatar :src="userAvatar" />
         </div>
-        <div class="message">
+        <div class="flex flex-col text-left mx-15px">
           <div>
-            <el-text class="time">{{ formatDate(item.createTime) }}</el-text>
+            <el-text class="text-left leading-30px">{{ formatDate(item.createTime) }}</el-text>
           </div>
-          <div class="right-text-container">
-            <div class="right-text">{{ item.content }}</div>
+          <div class="flex flex-row-reverse">
+            <div
+              class="text-[0.95rem] text-[var(--el-color-white)] inline bg-[var(--el-color-primary)] shadow-[0_0_0_1px_var(--el-color-primary)] rounded-10px p-10px w-auto break-words whitespace-pre-wrap"
+              >{{ item.content }}</div
+            >
           </div>
-          <div class="right-btns">
-            <el-button class="btn-cus" link @click="copyContent(item.content)">
-              <img class="btn-image" src="@/assets/ai/copy.svg" />
+          <div class="flex flex-row-reverse mt-8px">
+            <el-button
+              class="flex bg-transparent items-center hover:cursor-pointer hover:bg-[var(--el-fill-color-lighter)]"
+              link
+              @click="copyContent(item.content)"
+            >
+              <img class="h-20px" src="@/assets/ai/copy.svg" />
             </el-button>
-            <el-button class="btn-cus" link @click="onDelete(item.id)">
-              <img class="btn-image h-17px mr-12px" src="@/assets/ai/delete.svg" />
+            <el-button
+              class="flex bg-transparent items-center hover:cursor-pointer hover:bg-[var(--el-fill-color-lighter)]"
+              link
+              @click="onDelete(item.id)"
+            >
+              <img class="h-17px mr-12px" src="@/assets/ai/delete.svg" />
             </el-button>
-            <el-button class="btn-cus" link @click="onRefresh(item)">
+            <el-button
+              class="flex bg-transparent items-center hover:cursor-pointer hover:bg-[var(--el-fill-color-lighter)]"
+              link
+              @click="onRefresh(item)"
+            >
               <el-icon size="17"><RefreshRight /></el-icon>
             </el-button>
-            <el-button class="btn-cus" link @click="onEdit(item)">
+            <el-button
+              class="flex bg-transparent items-center hover:cursor-pointer hover:bg-[var(--el-fill-color-lighter)]"
+              link
+              @click="onEdit(item)"
+            >
               <el-icon size="17"><Edit /></el-icon>
             </el-button>
           </div>
@@ -55,7 +89,7 @@
     </div>
   </div>
   <!-- 回到底部 -->
-  <div v-if="isScrolling" class="to-bottom" @click="handleGoBottom">
+  <div v-if="isScrolling" class="absolute z-1000 bottom-0 right-50%" @click="handleGoBottom">
     <el-button :icon="ArrowDownBold" circle />
   </div>
 </template>
@@ -142,7 +176,7 @@ defineExpose({ scrollToBottom, handlerGoTop }) // 提供方法给 parent 调用
 // ============ 处理消息操作 ==============
 
 /** 复制 */
-const copyContent = async (content) => {
+const copyContent = async (content: string) => {
   await copy(content)
   message.success('复制成功!')
 }
@@ -171,114 +205,3 @@ onMounted(async () => {
   messageContainer.value.addEventListener('scroll', handleScroll)
 })
 </script>
-
-<style scoped lang="scss">
-.message-container {
-  position: relative;
-  overflow-y: scroll;
-}
-
-// 中间
-.chat-list {
-  display: flex;
-  flex-direction: column;
-  overflow-y: hidden;
-  padding: 0 20px;
-  .message-item {
-    margin-top: 50px;
-  }
-
-  .left-message {
-    display: flex;
-    flex-direction: row;
-  }
-
-  .right-message {
-    display: flex;
-    flex-direction: row-reverse;
-    justify-content: flex-start;
-  }
-
-  .message {
-    display: flex;
-    flex-direction: column;
-    text-align: left;
-    margin: 0 15px;
-
-    .time {
-      text-align: left;
-      line-height: 30px;
-    }
-
-    .left-text-container {
-      position: relative;
-      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);
-      border-radius: 10px;
-      padding: 10px 10px 5px 10px;
-
-      .left-text {
-        color: #393939;
-        font-size: 0.95rem;
-      }
-    }
-
-    .right-text-container {
-      display: flex;
-      flex-direction: row-reverse;
-
-      .right-text {
-        font-size: 0.95rem;
-        color: #fff;
-        display: inline;
-        background-color: #267fff;
-        box-shadow: 0 0 0 1px #267fff;
-        border-radius: 10px;
-        padding: 10px;
-        width: auto;
-        overflow-wrap: break-word;
-        white-space: pre-wrap;
-      }
-    }
-
-    .left-btns {
-      display: flex;
-      flex-direction: row;
-      margin-top: 8px;
-    }
-
-    .right-btns {
-      display: flex;
-      flex-direction: row-reverse;
-      margin-top: 8px;
-    }
-  }
-
-  // 复制、删除按钮
-  .btn-cus {
-    display: flex;
-    background-color: transparent;
-    align-items: center;
-
-    .btn-image {
-      height: 20px;
-    }
-  }
-
-  .btn-cus:hover {
-    cursor: pointer;
-    background-color: #f6f6f6;
-  }
-}
-
-// 回到底部
-.to-bottom {
-  position: absolute;
-  z-index: 1000;
-  bottom: 0;
-  right: 50%;
-}
-</style>

+ 5 - 52
src/views/ai/chat/index/components/message/MessageListEmpty.vue

@@ -1,12 +1,12 @@
 <!-- 消息列表为空时,展示 prompt 列表 -->
 <template>
-  <div class="chat-empty">
+  <div class="relative flex flex-row justify-center w-full h-full">
     <!-- title -->
-    <div class="center-container">
-      <div class="title">芋道 AI</div>
-      <div class="role-list">
+    <div class="flex flex-col justify-center">
+      <div class="text-28px font-bold text-center">芋道 AI</div>
+      <div class="flex flex-row flex-wrap items-center justify-center w-460px mt-20px">
         <div
-          class="role-item"
+          class="flex justify-center w-180px leading-50px border border-solid border-[#e4e4e4] rounded-10px m-10px cursor-pointer hover:bg-[rgba(243,243,243,0.73)]"
           v-for="prompt in promptList"
           :key="prompt.prompt"
           @click="handlerPromptClick(prompt)"
@@ -34,50 +34,3 @@ const handlerPromptClick = async ({ prompt }) => {
   emits('onPrompt', prompt)
 }
 </script>
-<style scoped lang="scss">
-.chat-empty {
-  position: relative;
-  display: flex;
-  flex-direction: row;
-  justify-content: center;
-  width: 100%;
-  height: 100%;
-
-  .center-container {
-    display: flex;
-    flex-direction: column;
-    justify-content: center;
-
-    .title {
-      font-size: 28px;
-      font-weight: bold;
-      text-align: center;
-    }
-
-    .role-list {
-      display: flex;
-      flex-direction: row;
-      flex-wrap: wrap;
-      align-items: center;
-      justify-content: center;
-      width: 460px;
-      margin-top: 20px;
-
-      .role-item {
-        display: flex;
-        justify-content: center;
-        width: 180px;
-        line-height: 50px;
-        border: 1px solid #e4e4e4;
-        border-radius: 10px;
-        margin: 10px;
-        cursor: pointer;
-      }
-
-      .role-item:hover {
-        background-color: rgba(243, 243, 243, 0.73);
-      }
-    }
-  }
-}
-</style>

+ 1 - 10
src/views/ai/chat/index/components/message/MessageLoading.vue

@@ -1,15 +1,6 @@
 <!-- message 加载页面 -->
 <template>
-  <div class="message-loading" >
+  <div class="p-30px">
     <el-skeleton animated />
   </div>
 </template>
-
-<script setup lang="ts">
-
-</script>
-<style scoped lang="scss">
-.message-loading {
-  padding: 30px 30px;
-}
-</style>

+ 4 - 31
src/views/ai/chat/index/components/message/MessageNewConversation.vue

@@ -1,9 +1,9 @@
 <!-- 无聊天对话时,在 message 区域,可以新增对话 -->
 <template>
-  <div class="new-chat">
-    <div class="box-center">
-      <div class="tip">点击下方按钮,开始你的对话吧</div>
-      <div class="btns">
+  <div class="flex flex-row justify-center w-100% h-100%">
+    <div class="flex flex-col justify-center">
+      <div class="text-14px text-#858585">点击下方按钮,开始你的对话吧</div>
+      <div class="flex flex-row justify-center mt-20px">
         <el-button type="primary" round @click="handlerNewChat">新建对话</el-button>
       </div>
     </div>
@@ -17,30 +17,3 @@ const handlerNewChat = () => {
   emits('onNewConversation')
 }
 </script>
-<style scoped lang="scss">
-.new-chat {
-  display: flex;
-  flex-direction: row;
-  justify-content: center;
-  width: 100%;
-  height: 100%;
-
-  .box-center {
-    display: flex;
-    flex-direction: column;
-    justify-content: center;
-
-    .tip {
-      font-size: 14px;
-      color: #858585;
-    }
-
-    .btns {
-      display: flex;
-      flex-direction: row;
-      justify-content: center;
-      margin-top: 20px;
-    }
-  }
-}
-</style>

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

@@ -1,6 +1,6 @@
 <template>
-  <div class="category-list">
-    <div class="category" v-for="category in categoryList" :key="category">
+  <div class="flex flex-row flex-wrap items-center">
+    <div class="flex flex-row mr-10px" v-for="category in categoryList" :key="category">
       <el-button
         plain
         round
@@ -37,17 +37,3 @@ const handleCategoryClick = async (category: string) => {
   emits('onCategoryClick', category)
 }
 </script>
-<style scoped lang="scss">
-.category-list {
-  display: flex;
-  flex-direction: row;
-  flex-wrap: wrap;
-  align-items: center;
-
-  .category {
-    display: flex;
-    flex-direction: row;
-    margin-right: 10px;
-  }
-}
-</style>

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

@@ -1,10 +1,10 @@
 <!-- header -->
 <template>
-  <el-header class="chat-header">
-    <div class="title">
+  <el-header class="flex flex-row justify-between items-center px-10px whitespace-nowrap text-ellipsis w-full" :style="{ backgroundColor: 'var(--el-bg-color-page)' }">
+    <div class="text-20px font-bold overflow-hidden max-w-220px" :style="{ color: 'var(--el-text-color-primary)' }">
       {{ title }}
     </div>
-    <div class="title-right">
+    <div class="flex flex-row">
       <slot></slot>
     </div>
   </el-header>
@@ -19,30 +19,3 @@ defineProps({
   }
 })
 </script>
-
-<style scoped lang="scss">
-.chat-header {
-  display: flex;
-  flex-direction: row;
-  justify-content: space-between;
-  align-items: center;
-  padding: 0 10px;
-  white-space: nowrap;
-  text-overflow: ellipsis;
-  background-color: #ececec;
-  width: 100%;
-
-  .title {
-    font-size: 20px;
-    font-weight: bold;
-    overflow: hidden;
-    color: #3e3e3e;
-    max-width: 220px;
-  }
-
-  .title-right {
-    display: flex;
-    flex-direction: row;
-  }
-}
-</style>

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

@@ -1,9 +1,16 @@
 <template>
-  <div class="card-list" ref="tabsRef" @scroll="handleTabsScroll">
-    <div class="card-item" v-for="role in roleList" :key="role.id">
-      <el-card class="card" body-class="card-body">
+  <div
+    class="flex flex-row flex-wrap relative h-full overflow-auto px-25px pb-140px items-start content-start justify-start"
+    ref="tabsRef"
+    @scroll="handleTabsScroll"
+  >
+    <div v-for="role in roleList" :key="role.id">
+      <el-card
+        class="inline-block mr-20px rounded-10px mb-20px relative"
+        body-class="max-w-240px w-240px pt-15px px-15px pb-10px flex flex-row justify-start relative"
+      >
         <!-- 更多操作 -->
-        <div class="more-container" v-if="showMore">
+        <div class="absolute top-0 right-12px" v-if="showMore">
           <el-dropdown @command="handleMoreClick">
             <span class="el-dropdown-link">
               <el-button type="text">
@@ -13,10 +20,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>
@@ -24,14 +31,18 @@
         </div>
         <!-- 角色信息 -->
         <div>
-          <img class="avatar" :src="role.avatar" />
+          <img class="w-40px h-40px rounded-10px overflow-hidden" :src="role.avatar" />
         </div>
-        <div class="right-container">
-          <div class="content-container">
-            <div class="title">{{ role.name }}</div>
-            <div class="description">{{ role.description }}</div>
+        <div class="ml-10px w-full">
+          <div class="h-85px">
+            <div class="text-18px font-bold" style="color: var(--el-text-color-primary)">
+              {{ role.name }}
+            </div>
+            <div class="mt-10px text-14px" style="color: var(--el-text-color-regular)">
+              {{ role.description }}
+            </div>
           </div>
-          <div class="btn-container">
+          <div class="flex flex-row-reverse mt-2px">
             <el-button type="primary" size="small" @click="handleUseClick(role)">使用</el-button>
           </div>
         </div>
@@ -79,7 +90,7 @@ const handleMoreClick = async (data) => {
 }
 
 /** 选中 */
-const handleUseClick = (role) => {
+const handleUseClick = (role: any) => {
   emits('onUse', role)
 }
 
@@ -88,87 +99,8 @@ const handleTabsScroll = async () => {
   if (tabsRef.value) {
     const { scrollTop, scrollHeight, clientHeight } = tabsRef.value
     if (scrollTop + clientHeight >= scrollHeight - 20 && !props.loading) {
-      await emits('onPage')
+      emits('onPage')
     }
   }
 }
 </script>
-
-<style lang="scss">
-// 重写 card 组件 body 样式
-.card-body {
-  max-width: 240px;
-  width: 240px;
-  padding: 15px 15px 10px 15px;
-
-  display: flex;
-  flex-direction: row;
-  justify-content: flex-start;
-  position: relative;
-}
-</style>
-<style scoped lang="scss">
-// 卡片列表
-.card-list {
-  display: flex;
-  flex-direction: row;
-  flex-wrap: wrap;
-  position: relative;
-  height: 100%;
-  overflow: auto;
-  padding: 0px 25px;
-  padding-bottom: 140px;
-  align-items: start;
-  align-content: flex-start;
-  justify-content: start;
-
-  .card {
-    display: inline-block;
-    margin-right: 20px;
-    border-radius: 10px;
-    margin-bottom: 20px;
-    position: relative;
-
-    .more-container {
-      position: absolute;
-      top: 0;
-      right: 12px;
-    }
-
-    .avatar {
-      width: 40px;
-      height: 40px;
-      border-radius: 10px;
-      overflow: hidden;
-    }
-
-    .right-container {
-      margin-left: 10px;
-      width: 100%;
-      //height: 100px;
-
-      .content-container {
-        height: 85px;
-
-        .title {
-          font-size: 18px;
-          font-weight: bold;
-          color: #3e3e3e;
-        }
-
-        .description {
-          margin-top: 10px;
-          font-size: 14px;
-          color: #6a6a6a;
-        }
-      }
-
-      .btn-container {
-        display: flex;
-        flex-direction: row-reverse;
-        margin-top: 2px;
-      }
-    }
-  }
-}
-</style>

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

@@ -1,17 +1,19 @@
 <!-- chat 角色仓库 -->
 <template>
-  <el-container class="role-container">
+  <el-container
+    class="role-container absolute w-full h-full m-0 p-0 left-0 right-0 top-0 bottom-0 bg-[var(--el-bg-color)] overflow-hidden flex !flex-col"
+  >
     <ChatRoleForm ref="formRef" @success="handlerAddRoleSuccess" />
-    <!-- header  -->
+    <!-- header -->
     <RoleHeader title="角色仓库" class="relative" />
-    <!--  main  -->
-    <el-main class="role-main">
-      <div class="search-container">
+    <!-- main -->
+    <el-main class="flex-1 overflow-hidden m-0 !p-0 relative">
+      <div class="mx-5 mt-5 mb-0 absolute right-0 -top-1.25 z-100">
         <!-- 搜索按钮 -->
         <el-input
           :loading="loading"
           v-model="search"
-          class="search-input"
+          class="!w-60"
           size="default"
           placeholder="请输入搜索的内容"
           :suffix-icon="Search"
@@ -23,13 +25,21 @@
           @click="handlerAddRole"
           class="ml-20px"
         >
-          <Icon icon="ep:user" style="margin-right: 5px;" />
+          <Icon icon="ep:user" class="mr-1.25" />
           添加角色
         </el-button>
       </div>
       <!-- tabs -->
-      <el-tabs v-model="activeTab" class="tabs" @tab-click="handleTabsClick">
-        <el-tab-pane class="role-pane" label="我的角色" name="my-role">
+      <el-tabs
+        v-model="activeTab"
+        @tab-click="handleTabsClick"
+        class="relative h-full [&_.el-tabs__nav-scroll]:my-2.5 [&_.el-tabs__nav-scroll]:mx-5"
+      >
+        <el-tab-pane
+          label="我的角色"
+          name="my-role"
+          class="flex flex-col h-full overflow-y-auto relative"
+        >
           <RoleList
             :loading="loading"
             :role-list="myRoleList"
@@ -43,7 +53,7 @@
         </el-tab-pane>
         <el-tab-pane label="公共角色" name="public-role">
           <RoleCategoryList
-            class="role-category-list"
+            class="mx-6.75"
             :category-list="categoryList"
             :active="activeCategory"
             @on-category-click="handlerCategoryClick"
@@ -64,15 +74,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() // 路由对象
 
@@ -220,70 +230,9 @@ onMounted(async () => {
   await getActiveTabsRole()
 })
 </script>
-<!-- 覆盖 element ui css -->
+<!-- 覆盖 element plus css -->
 <style lang="scss">
-.el-tabs__content {
-  position: relative;
-  height: 100%;
-  overflow: hidden;
-}
 .el-tabs__nav-scroll {
   margin: 10px 20px;
 }
 </style>
-<!-- 样式 -->
-<style scoped lang="scss">
-// 跟容器
-.role-container {
-  position: absolute;
-  width: 100%;
-  height: 100%;
-  margin: 0;
-  padding: 0;
-  left: 0;
-  right: 0;
-  top: 0;
-  bottom: 0;
-  background-color: #ffffff;
-  overflow: hidden;
-  display: flex;
-  flex-direction: column;
-
-  .role-main {
-    flex: 1;
-    overflow: hidden;
-    margin: 0;
-    padding: 0;
-    position: relative;
-
-    .search-container {
-      margin: 20px 20px 0px 20px;
-      position: absolute;
-      right: 0;
-      top: -5px;
-      z-index: 100;
-    }
-
-    .search-input {
-      width: 240px;
-    }
-
-    .tabs {
-      position: relative;
-      height: 100%;
-
-      .role-category-list {
-        margin: 0 27px;
-      }
-    }
-
-    .role-pane {
-      display: flex;
-      flex-direction: column;
-      height: 100%;
-      overflow-y: auto;
-      position: relative;
-    }
-  }
-}
-</style>

+ 26 - 218
src/views/ai/chat/index/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <el-container class="ai-layout">
+  <el-container class="absolute flex-1 top-0 left-0 h-full w-full">
     <!-- 左侧:对话列表 -->
     <ConversationList
       :active-id="activeConversationId"
@@ -10,33 +10,38 @@
       @on-conversation-delete="handlerConversationDelete"
     />
     <!-- 右侧:对话详情 -->
-    <el-container class="detail-container">
-      <el-header class="header">
-        <div class="title">
+    <el-container class="bg-[var(--el-bg-color)]">
+      <el-header
+        class="flex flex-row items-center justify-between bg-[var(--el-bg-color-page)] shadow-[0_0_0_0_var(--el-border-color-light)]"
+      >
+        <div class="text-18px font-bold">
           {{ activeConversation?.title ? activeConversation?.title : '对话' }}
           <span v-if="activeMessageList.length">({{ activeMessageList.length }})</span>
         </div>
-        <div class="btns" v-if="activeConversation">
+        <div class="flex w-300px flex-row justify-end" v-if="activeConversation">
           <el-button type="primary" bg plain size="small" @click="openChatConversationUpdateForm">
             <span v-html="activeConversation?.modelName"></span>
             <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" />
+          <el-button size="small" class="p-10px" @click="handlerMessageClear">
+            <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" />
+          <el-button size="small" class="p-10px">
+            <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" />
+          <el-button size="small" class="p-10px" @click="handleGoTopMessage">
+            <Icon icon="ep:top" color="var(--el-text-color-placeholder)" />
           </el-button>
         </div>
       </el-header>
 
       <!-- main:消息列表 -->
-      <el-main class="main-container">
+      <el-main class="m-0 p-0 relative h-full w-full">
         <div>
-          <div class="message-container">
+          <div class="absolute top-0 bottom-0 left-0 right-0 overflow-y-hidden p-0 m-0">
             <!-- 情况一:消息加载中 -->
             <MessageLoading v-if="activeMessageListLoading" />
             <!-- 情况二:无聊天对话时 -->
@@ -64,10 +69,14 @@
       </el-main>
 
       <!-- 底部 -->
-      <el-footer class="footer-container">
-        <form class="prompt-from">
+      <el-footer class="flex flex-col !h-auto !p-0">
+        <!-- TODO @芋艿:这块要想办法迁移下! -->
+        <form
+          class="mt-10px mx-20px mb-20px py-9px px-10px flex flex-col h-auto rounded-10px"
+          style="border: 1px solid var(--el-border-color)"
+        >
           <textarea
-            class="prompt-input"
+            class="h-80px border-none box-border resize-none py-0 px-2px overflow-auto focus:outline-none"
             v-model="prompt"
             @keydown="handleSendByKeydown"
             @input="handlePromptInput"
@@ -75,7 +84,7 @@
             @compositionend="onCompositionend"
             placeholder="问我任何问题...(Shift+Enter 换行,按下 Enter 发送)"
           ></textarea>
-          <div class="prompt-btns">
+          <div class="flex justify-between pb-0 pt-5px">
             <div>
               <el-switch v-model="enableContext" />
               <span class="ml-5px text-14px text-#8f8f8f">上下文</span>
@@ -571,204 +580,3 @@ onMounted(async () => {
   await getMessageList()
 })
 </script>
-
-<style lang="scss" scoped>
-.ai-layout {
-  position: absolute;
-  flex: 1;
-  top: 0;
-  left: 0;
-  height: 100%;
-  width: 100%;
-}
-
-.conversation-container {
-  position: relative;
-  display: flex;
-  flex-direction: column;
-  justify-content: space-between;
-  padding: 10px 10px 0;
-
-  .btn-new-conversation {
-    padding: 18px 0;
-  }
-
-  .search-input {
-    margin-top: 20px;
-  }
-
-  .conversation-list {
-    margin-top: 20px;
-
-    .conversation {
-      display: flex;
-      flex-direction: row;
-      justify-content: space-between;
-      flex: 1;
-      padding: 0 5px;
-      margin-top: 10px;
-      cursor: pointer;
-      border-radius: 5px;
-      align-items: center;
-      line-height: 30px;
-
-      &.active {
-        background-color: #e6e6e6;
-
-        .button {
-          display: inline-block;
-        }
-      }
-
-      .title-wrapper {
-        display: flex;
-        flex-direction: row;
-        align-items: center;
-      }
-
-      .title {
-        padding: 5px 10px;
-        max-width: 220px;
-        font-size: 14px;
-        overflow: hidden;
-        white-space: nowrap;
-        text-overflow: ellipsis;
-      }
-
-      .avatar {
-        width: 28px;
-        height: 28px;
-        display: flex;
-        flex-direction: row;
-        justify-items: center;
-      }
-
-      // 对话编辑、删除
-      .button-wrapper {
-        right: 2px;
-        display: flex;
-        flex-direction: row;
-        justify-items: center;
-        color: #606266;
-
-        .el-icon {
-          margin-right: 5px;
-        }
-      }
-    }
-  }
-
-  // 角色仓库、清空未设置对话
-  .tool-box {
-    line-height: 35px;
-    display: flex;
-    justify-content: space-between;
-    align-items: center;
-    color: var(--el-text-color);
-
-    > div {
-      display: flex;
-      align-items: center;
-      color: #606266;
-      padding: 0;
-      margin: 0;
-      cursor: pointer;
-
-      > span {
-        margin-left: 5px;
-      }
-    }
-  }
-}
-
-// 头部
-.detail-container {
-  background: #ffffff;
-
-  .header {
-    display: flex;
-    flex-direction: row;
-    align-items: center;
-    justify-content: space-between;
-    background: #fbfbfb;
-    box-shadow: 0 0 0 0 #dcdfe6;
-
-    .title {
-      font-size: 18px;
-      font-weight: bold;
-    }
-
-    .btns {
-      display: flex;
-      width: 300px;
-      flex-direction: row;
-      justify-content: flex-end;
-      //justify-content: space-between;
-
-      .btn {
-        padding: 10px;
-      }
-    }
-  }
-}
-
-// main 容器
-.main-container {
-  margin: 0;
-  padding: 0;
-  position: relative;
-  height: 100%;
-  width: 100%;
-
-  .message-container {
-    position: absolute;
-    top: 0;
-    bottom: 0;
-    left: 0;
-    right: 0;
-    overflow-y: hidden;
-    padding: 0;
-    margin: 0;
-  }
-}
-
-// 底部
-.footer-container {
-  display: flex;
-  flex-direction: column;
-  height: auto;
-  margin: 0;
-  padding: 0;
-
-  .prompt-from {
-    display: flex;
-    flex-direction: column;
-    height: auto;
-    border: 1px solid #e3e3e3;
-    border-radius: 10px;
-    margin: 10px 20px 20px 20px;
-    padding: 9px 10px;
-  }
-
-  .prompt-input {
-    height: 80px;
-    //box-shadow: none;
-    border: none;
-    box-sizing: border-box;
-    resize: none;
-    padding: 0 2px;
-    overflow: auto;
-  }
-
-  .prompt-input:focus {
-    outline: none;
-  }
-
-  .prompt-btns {
-    display: flex;
-    justify-content: space-between;
-    padding-bottom: 0;
-    padding-top: 5px;
-  }
-}
-</style>

+ 22 - 53
src/views/ai/image/index/components/ImageCard.vue

@@ -1,6 +1,9 @@
 <template>
-  <el-card body-class="" class="image-card">
-    <div class="image-operation">
+  <el-card
+    body-class=""
+    class="!w-80 !h-auto !rounded-10px !relative !flex !flex-col"
+  >
+    <div class="!flex !flex-row !justify-between">
       <div>
         <el-button type="primary" text bg v-if="detail?.status === AiImageStatusEnum.IN_PROGRESS">
           生成中
@@ -15,24 +18,34 @@
       <!-- 操作区 -->
       <div>
         <el-button
-          class="btn"
+          class="!p-10px !m-0"
           text
           :icon="Download"
           @click="handleButtonClick('download', detail)"
         />
         <el-button
-          class="btn"
+          class="!p-10px !m-0"
           text
           :icon="RefreshRight"
           @click="handleButtonClick('regeneration', detail)"
         />
-        <el-button class="btn" text :icon="Delete" @click="handleButtonClick('delete', detail)" />
-        <el-button class="btn" text :icon="More" @click="handleButtonClick('more', detail)" />
+        <el-button
+          class="!p-10px !m-0"
+          text
+          :icon="Delete"
+          @click="handleButtonClick('delete', detail)"
+        />
+        <el-button
+          class="!p-10px !m-0"
+          text
+          :icon="More"
+          @click="handleButtonClick('more', detail)"
+        />
       </div>
     </div>
-    <div class="image-wrapper" ref="cardImageRef">
+    <div class="!overflow-hidden !mt-20px !h-280px !flex-1" ref="cardImageRef">
       <el-image
-        class="image"
+        class="!w-full !rounded-10px"
         :src="detail?.picUrl"
         :preview-src-list="[detail.picUrl]"
         preview-teleported
@@ -42,7 +55,7 @@
       </div>
     </div>
     <!-- Midjourney 专属操作 -->
-    <div class="image-mj-btns">
+    <div class="!mt-5px !w-full !flex !flex-row !flex-wrap !justify-start">
       <el-button
         size="small"
         v-for="button in detail?.buttons"
@@ -116,47 +129,3 @@ onMounted(async () => {
   await handleLoading(props.detail.status as string)
 })
 </script>
-
-<style scoped lang="scss">
-.image-card {
-  width: 320px;
-  height: auto;
-  border-radius: 10px;
-  position: relative;
-  display: flex;
-  flex-direction: column;
-
-  .image-operation {
-    display: flex;
-    flex-direction: row;
-    justify-content: space-between;
-
-    .btn {
-      //border: 1px solid red;
-      padding: 10px;
-      margin: 0;
-    }
-  }
-
-  .image-wrapper {
-    overflow: hidden;
-    margin-top: 20px;
-    height: 280px;
-    flex: 1;
-
-    .image {
-      width: 100%;
-      border-radius: 10px;
-    }
-  }
-
-  .image-mj-btns {
-    margin-top: 5px;
-    width: 100%;
-    display: flex;
-    flex-direction: row;
-    flex-wrap: wrap;
-    justify-content: flex-start;
-  }
-}
-</style>

+ 100 - 137
src/views/ai/image/index/components/ImageDetail.vue

@@ -5,139 +5,111 @@
     @close="handleDrawerClose"
     custom-class="drawer-class"
   >
-    <!-- 图片 -->
-    <div class="item">
-      <div class="body">
-        <el-image
-          class="image"
-          :src="detail?.picUrl"
-          :preview-src-list="[detail.picUrl]"
-          preview-teleported
-        />
-      </div>
-    </div>
-    <!-- 时间 -->
-    <div class="item">
-      <div class="tip">时间</div>
-      <div class="body">
-        <div>提交时间:{{ formatTime(detail.createTime, 'yyyy-MM-dd HH:mm:ss') }}</div>
-        <div>生成时间:{{ formatTime(detail.finishTime, 'yyyy-MM-dd HH:mm:ss') }}</div>
-      </div>
-    </div>
-    <!-- 模型 -->
-    <div class="item">
-      <div class="tip">模型</div>
-      <div class="body"> {{ detail.model }}({{ detail.height }}x{{ detail.width }}) </div>
-    </div>
-    <!-- 提示词 -->
-    <div class="item">
-      <div class="tip">提示词</div>
-      <div class="body">
-        {{ detail.prompt }}
-      </div>
-    </div>
-    <!-- 地址 -->
-    <div class="item">
-      <div class="tip">图片地址</div>
-      <div class="body">
-        {{ detail.picUrl }}
-      </div>
+    <!-- 图片预览 -->
+    <div class="mb-5">
+      <el-image
+        :src="detail?.picUrl"
+        :preview-src-list="[detail.picUrl]"
+        preview-teleported
+        class="w-full rounded-2"
+        fit="contain"
+      />
     </div>
+
+    <!-- 基础信息 -->
+    <el-descriptions title="基础信息" :column="1" :label-width="100" border>
+      <el-descriptions-item label="提交时间">
+        {{ formatTime(detail.createTime, 'yyyy-MM-dd HH:mm:ss') }}
+      </el-descriptions-item>
+      <el-descriptions-item label="生成时间">
+        {{ formatTime(detail.finishTime, 'yyyy-MM-dd HH:mm:ss') }}
+      </el-descriptions-item>
+      <el-descriptions-item label="模型">
+        {{ detail.model }}({{ detail.height }}x{{ detail.width }})
+      </el-descriptions-item>
+      <el-descriptions-item label="提示词">
+        <div class="break-words">{{ detail.prompt }}</div>
+      </el-descriptions-item>
+      <el-descriptions-item label="图片地址">
+        <div class="break-all text-xs">{{ detail.picUrl }}</div>
+      </el-descriptions-item>
+    </el-descriptions>
+
     <!-- StableDiffusion 专属区域 -->
-    <div
-      class="item"
-      v-if="detail.platform === AiPlatformEnum.STABLE_DIFFUSION && detail?.options?.sampler"
+    <el-descriptions
+      v-if="detail.platform === AiPlatformEnum.STABLE_DIFFUSION && hasStableDiffusionOptions"
+      title="StableDiffusion 参数"
+      :column="1"
+      :label-width="100"
+      border
+      class="mt-5"
     >
-      <div class="tip">采样方法</div>
-      <div class="body">
+      <el-descriptions-item v-if="detail?.options?.sampler" label="采样方法">
         {{
           StableDiffusionSamplers.find(
             (item: ImageModelVO) => item.key === detail?.options?.sampler
           )?.name
         }}
-      </div>
-    </div>
-    <div
-      class="item"
-      v-if="
-        detail.platform === AiPlatformEnum.STABLE_DIFFUSION && detail?.options?.clipGuidancePreset
-      "
-    >
-      <div class="tip">CLIP</div>
-      <div class="body">
+      </el-descriptions-item>
+      <el-descriptions-item v-if="detail?.options?.clipGuidancePreset" label="CLIP">
         {{
           StableDiffusionClipGuidancePresets.find(
             (item: ImageModelVO) => item.key === detail?.options?.clipGuidancePreset
           )?.name
         }}
-      </div>
-    </div>
-    <div
-      class="item"
-      v-if="detail.platform === AiPlatformEnum.STABLE_DIFFUSION && detail?.options?.stylePreset"
-    >
-      <div class="tip">风格</div>
-      <div class="body">
+      </el-descriptions-item>
+      <el-descriptions-item v-if="detail?.options?.stylePreset" label="风格">
         {{
           StableDiffusionStylePresets.find(
             (item: ImageModelVO) => item.key === detail?.options?.stylePreset
           )?.name
         }}
-      </div>
-    </div>
-    <div
-      class="item"
-      v-if="detail.platform === AiPlatformEnum.STABLE_DIFFUSION && detail?.options?.steps"
-    >
-      <div class="tip">迭代步数</div>
-      <div class="body">
+      </el-descriptions-item>
+      <el-descriptions-item v-if="detail?.options?.steps" label="迭代步数">
         {{ detail?.options?.steps }}
-      </div>
-    </div>
-    <div
-      class="item"
-      v-if="detail.platform === AiPlatformEnum.STABLE_DIFFUSION && detail?.options?.scale"
-    >
-      <div class="tip">引导系数</div>
-      <div class="body">
+      </el-descriptions-item>
+      <el-descriptions-item v-if="detail?.options?.scale" label="引导系数">
         {{ detail?.options?.scale }}
-      </div>
-    </div>
-    <div
-      class="item"
-      v-if="detail.platform === AiPlatformEnum.STABLE_DIFFUSION && detail?.options?.seed"
-    >
-      <div class="tip">随机因子</div>
-      <div class="body">
+      </el-descriptions-item>
+      <el-descriptions-item v-if="detail?.options?.seed" label="随机因子">
         {{ detail?.options?.seed }}
-      </div>
-    </div>
+      </el-descriptions-item>
+    </el-descriptions>
+
     <!-- Dall3 专属区域 -->
-    <div class="item" v-if="detail.platform === AiPlatformEnum.OPENAI && detail?.options?.style">
-      <div class="tip">风格选择</div>
-      <div class="body">
+    <el-descriptions
+      v-if="detail.platform === AiPlatformEnum.OPENAI && detail?.options?.style"
+      title="DALL-E 3 参数"
+      :column="1"
+      :label-width="100"
+      border
+      class="mt-5"
+    >
+      <el-descriptions-item label="风格选择">
         {{ Dall3StyleList.find((item: ImageModelVO) => item.key === detail?.options?.style)?.name }}
-      </div>
-    </div>
+      </el-descriptions-item>
+    </el-descriptions>
+
     <!-- Midjourney 专属区域 -->
-    <div
-      class="item"
-      v-if="detail.platform === AiPlatformEnum.MIDJOURNEY && detail?.options?.version"
+    <el-descriptions
+      v-if="detail.platform === AiPlatformEnum.MIDJOURNEY && hasMidjourneyOptions"
+      title="Midjourney 参数"
+      :column="1"
+      :label-width="100"
+      border
+      class="mt-5"
     >
-      <div class="tip">模型版本</div>
-      <div class="body">
+      <el-descriptions-item v-if="detail?.options?.version" label="模型版本">
         {{ detail?.options?.version }}
-      </div>
-    </div>
-    <div
-      class="item"
-      v-if="detail.platform === AiPlatformEnum.MIDJOURNEY && detail?.options?.referImageUrl"
-    >
-      <div class="tip">参考图</div>
-      <div class="body">
-        <el-image :src="detail.options.referImageUrl" />
-      </div>
-    </div>
+      </el-descriptions-item>
+      <el-descriptions-item v-if="detail?.options?.referImageUrl" label="参考图">
+        <el-image
+          :src="detail.options.referImageUrl"
+          class="max-w-[200px] rounded-2"
+          fit="contain"
+        />
+      </el-descriptions-item>
+    </el-descriptions>
   </el-drawer>
 </template>
 
@@ -156,6 +128,25 @@ import { formatTime } from '@/utils'
 const showDrawer = ref<boolean>(false) // 是否显示
 const detail = ref<ImageVO>({} as ImageVO) // 图片详细信息
 
+// 计算属性:判断是否有 StableDiffusion 选项
+const hasStableDiffusionOptions = computed(() => {
+  const options = detail.value?.options
+  return (
+    options?.sampler ||
+    options?.clipGuidancePreset ||
+    options?.stylePreset ||
+    options?.steps ||
+    options?.scale ||
+    options?.seed
+  )
+})
+
+// 计算属性:判断是否有 Midjourney 选项
+const hasMidjourneyOptions = computed(() => {
+  const options = detail.value?.options
+  return options?.version || options?.referImageUrl
+})
+
 const props = defineProps({
   show: {
     type: Boolean,
@@ -175,7 +166,7 @@ const handleDrawerClose = async () => {
 
 /** 监听 drawer 是否打开 */
 const { show } = toRefs(props)
-watch(show, async (newValue, oldValue) => {
+watch(show, async (newValue, _oldValue) => {
   showDrawer.value = newValue as boolean
 })
 
@@ -186,7 +177,7 @@ const getImageDetail = async (id: number) => {
 
 /** 监听 id 变化,加载最新图片详情 */
 const { id } = toRefs(props)
-watch(id, async (newVal, oldVal) => {
+watch(id, async (newVal, _oldVal) => {
   if (newVal) {
     await getImageDetail(newVal)
   }
@@ -194,31 +185,3 @@ watch(id, async (newVal, oldVal) => {
 
 const emits = defineEmits(['handleDrawerClose'])
 </script>
-<style scoped lang="scss">
-.item {
-  margin-bottom: 20px;
-  width: 100%;
-  overflow: hidden;
-  word-wrap: break-word;
-
-  .header {
-    display: flex;
-    flex-direction: row;
-    justify-content: space-between;
-  }
-
-  .tip {
-    font-weight: bold;
-    font-size: 16px;
-  }
-
-  .body {
-    margin-top: 10px;
-    color: #616161;
-
-    .taskImage {
-      border-radius: 10px;
-    }
-  }
-}
-</style>

+ 14 - 51
src/views/ai/image/index/components/ImageList.vue

@@ -1,12 +1,19 @@
 <template>
-  <el-card class="dr-task" body-class="task-card" shadow="never">
+  <el-card
+    class="wh-full"
+    :body-style="{ margin: 0, padding: 0, height: '100%', position: 'relative' }"
+    shadow="never"
+  >
     <template #header>
       绘画任务
       <!-- TODO @fan:看看,怎么优化下这个样子哈。 -->
       <el-button @click="handleViewPublic">绘画作品</el-button>
     </template>
     <!-- 图片列表 -->
-    <div class="task-image-list" ref="imageListRef">
+    <div
+      class="relative flex flex-row flex-wrap content-start h-full overflow-auto p-5 pb-[140px] box-border [&>div]:mr-5 [&>div]:mb-5"
+      ref="imageListRef"
+    >
       <ImageCard
         v-for="image in imageList"
         :key="image.id"
@@ -15,7 +22,9 @@
         @on-mj-btn-click="handleImageMidjourneyButtonClick"
       />
     </div>
-    <div class="task-image-pagination">
+    <div
+      class="absolute bottom-[60px] h-[50px] leading-[90px] w-full z-[999] bg-white flex flex-row justify-center items-center"
+    >
       <Pagination
         :total="pageTotal"
         v-model:page="queryParams.pageNo"
@@ -150,12 +159,12 @@ const handleImageButtonClick = async (type: string, imageDetail: ImageVO) => {
   }
   // 下载
   if (type === 'download') {
-    await download.image({ url: imageDetail.picUrl })
+    download.image({ url: imageDetail.picUrl })
     return
   }
   // 重新生成
   if (type === 'regeneration') {
-    await emits('onRegeneration', imageDetail)
+    emits('onRegeneration', imageDetail)
     return
   }
 }
@@ -197,49 +206,3 @@ onUnmounted(async () => {
   }
 })
 </script>
-<style lang="scss">
-.dr-task {
-  width: 100%;
-  height: 100%;
-}
-.task-card {
-  margin: 0;
-  padding: 0;
-  height: 100%;
-  position: relative;
-}
-
-.task-image-list {
-  position: relative;
-  display: flex;
-  flex-direction: row;
-  flex-wrap: wrap;
-  align-content: flex-start;
-  height: 100%;
-  overflow: auto;
-  padding: 20px 20px 140px;
-  box-sizing: border-box; /* 确保内边距不会增加高度 */
-
-  > div {
-    margin-right: 20px;
-    margin-bottom: 20px;
-  }
-  > div:last-of-type {
-    //margin-bottom: 100px;
-  }
-}
-
-.task-image-pagination {
-  position: absolute;
-  bottom: 60px;
-  height: 50px;
-  line-height: 90px;
-  width: 100%;
-  z-index: 999;
-  background-color: #ffffff;
-  display: flex;
-  flex-direction: row;
-  justify-content: center;
-  align-items: center;
-}
-</style>

+ 10 - 45
src/views/ai/image/index/components/common/index.vue

@@ -14,14 +14,14 @@
       type="textarea"
     />
   </div>
-  <div class="hot-words">
+  <div class="flex flex-col mt-30px">
     <div>
       <el-text tag="b">随机热词</el-text>
     </div>
-    <el-space wrap class="word-list">
+    <el-space wrap class="flex flex-row flex-wrap justify-start mt-15px">
       <el-button
         round
-        class="btn"
+        class="m-0"
         :type="selectHotWord === hotWord ? 'primary' : 'default'"
         v-for="hotWord in ImageHotWords"
         :key="hotWord"
@@ -31,11 +31,11 @@
       </el-button>
     </el-space>
   </div>
-  <div class="group-item">
+  <div class="mt-30px">
     <div>
       <el-text tag="b">平台</el-text>
     </div>
-    <el-space wrap class="group-item-body">
+    <el-space wrap class="mt-15px w-full">
       <el-select
         v-model="otherPlatform"
         placeholder="Select"
@@ -52,11 +52,11 @@
       </el-select>
     </el-space>
   </div>
-  <div class="group-item">
+  <div class="mt-30px">
     <div>
       <el-text tag="b">模型</el-text>
     </div>
-    <el-space wrap class="group-item-body">
+    <el-space wrap class="mt-15px w-full">
       <el-select v-model="modelId" placeholder="Select" size="large" class="!w-350px">
         <el-option
           v-for="item in platformModels"
@@ -67,16 +67,16 @@
       </el-select>
     </el-space>
   </div>
-  <div class="group-item">
+  <div class="mt-30px">
     <div>
       <el-text tag="b">图片尺寸</el-text>
     </div>
-    <el-space wrap class="group-item-body">
+    <el-space wrap class="mt-15px w-full">
       <el-input v-model="width" type="number" class="w-170px" placeholder="图片宽度" />
       <el-input v-model="height" type="number" class="w-170px" placeholder="图片高度" />
     </el-space>
   </div>
-  <div class="btns">
+  <div class="flex justify-center mt-50px">
     <el-button
       type="primary"
       size="large"
@@ -187,38 +187,3 @@ watch(
 /** 暴露组件方法 */
 defineExpose({ settingValues })
 </script>
-<style scoped lang="scss">
-.hot-words {
-  display: flex;
-  flex-direction: column;
-  margin-top: 30px;
-
-  .word-list {
-    display: flex;
-    flex-direction: row;
-    flex-wrap: wrap;
-    justify-content: start;
-    margin-top: 15px;
-
-    .btn {
-      margin: 0;
-    }
-  }
-}
-
-// 模型
-.group-item {
-  margin-top: 30px;
-
-  .group-item-body {
-    margin-top: 15px;
-    width: 100%;
-  }
-}
-
-.btns {
-  display: flex;
-  justify-content: center;
-  margin-top: 50px;
-}
-</style>

+ 17 - 148
src/views/ai/image/index/components/dall3/index.vue

@@ -14,14 +14,14 @@
       type="textarea"
     />
   </div>
-  <div class="hot-words">
+  <div class="flex flex-col mt-30px">
     <div>
       <el-text tag="b">随机热词</el-text>
     </div>
-    <el-space wrap class="word-list">
+    <el-space wrap class="flex flex-row flex-wrap justify-start mt-15px">
       <el-button
         round
-        class="btn"
+        class="m-0"
         :type="selectHotWord === hotWord ? 'primary' : 'default'"
         v-for="hotWord in ImageHotWords"
         :key="hotWord"
@@ -31,57 +31,57 @@
       </el-button>
     </el-space>
   </div>
-  <div class="model">
+  <div class="mt-30px">
     <div>
       <el-text tag="b">模型选择</el-text>
     </div>
-    <el-space wrap class="model-list">
+    <el-space wrap class="mt-15px">
       <div
-        :class="selectModel === model.key ? 'modal-item selectModel' : 'modal-item'"
+        :class="selectModel === model.key ? 'w-110px overflow-hidden flex flex-col items-center border-3 border-solid border-#1293ff rounded-5px cursor-pointer' : 'w-110px overflow-hidden flex flex-col items-center border-3 border-solid border-transparent cursor-pointer'"
         v-for="model in Dall3Models"
         :key="model.key"
       >
         <el-image :src="model.image" fit="contain" @click="handleModelClick(model)" />
-        <div class="model-font">{{ model.name }}</div>
+        <div class="text-14px color-#3e3e3e font-bold">{{ model.name }}</div>
       </div>
     </el-space>
   </div>
-  <div class="image-style">
+  <div class="mt-30px">
     <div>
       <el-text tag="b">风格选择</el-text>
     </div>
-    <el-space wrap class="image-style-list">
+    <el-space wrap class="mt-15px">
       <div
-        :class="style === imageStyle.key ? 'image-style-item selectImageStyle' : 'image-style-item'"
+        :class="style === imageStyle.key ? 'w-110px overflow-hidden flex flex-col items-center border-3 border-solid border-#1293ff rounded-5px cursor-pointer' : 'w-110px overflow-hidden flex flex-col items-center border-3 border-solid border-transparent cursor-pointer'"
         v-for="imageStyle in Dall3StyleList"
         :key="imageStyle.key"
       >
         <el-image :src="imageStyle.image" fit="contain" @click="handleStyleClick(imageStyle)" />
-        <div class="style-font">{{ imageStyle.name }}</div>
+        <div class="text-14px color-#3e3e3e font-bold">{{ imageStyle.name }}</div>
       </div>
     </el-space>
   </div>
-  <div class="image-size">
+  <div class="w-full mt-30px">
     <div>
       <el-text tag="b">画面比例</el-text>
     </div>
-    <el-space wrap class="size-list">
+    <el-space wrap class="flex flex-row justify-between w-full mt-20px">
       <div
-        class="size-item"
+        class="flex flex-col items-center cursor-pointer"
         v-for="imageSize in Dall3SizeList"
         :key="imageSize.key"
         @click="handleSizeClick(imageSize)"
       >
         <div
-          :class="selectSize === imageSize.key ? 'size-wrapper selectImageSize' : 'size-wrapper'"
+          :class="selectSize === imageSize.key ? 'flex flex-col items-center justify-center rounded-7px p-4px w-50px h-50px bg-white border-1 border-solid border-#1293ff' : 'flex flex-col items-center justify-center rounded-7px p-4px w-50px h-50px bg-white border-1 border-solid border-white'"
         >
           <div :style="imageSize.style"></div>
         </div>
-        <div class="size-font">{{ imageSize.name }}</div>
+        <div class="text-14px color-#3e3e3e font-bold">{{ imageSize.name }}</div>
       </div>
     </el-space>
   </div>
-  <div class="btns">
+  <div class="flex justify-center mt-50px">
     <el-button
       type="primary"
       size="large"
@@ -229,135 +229,4 @@ const settingValues = async (detail: ImageVO) => {
 /** 暴露组件方法 */
 defineExpose({ settingValues })
 </script>
-<style scoped lang="scss">
-// 热词
-.hot-words {
-  display: flex;
-  flex-direction: column;
-  margin-top: 30px;
 
-  .word-list {
-    display: flex;
-    flex-direction: row;
-    flex-wrap: wrap;
-    justify-content: start;
-    margin-top: 15px;
-
-    .btn {
-      margin: 0;
-    }
-  }
-}
-
-// 模型
-.model {
-  margin-top: 30px;
-
-  .model-list {
-    margin-top: 15px;
-
-    .modal-item {
-      width: 110px;
-      //outline: 1px solid blue;
-      overflow: hidden;
-      display: flex;
-      flex-direction: column;
-      align-items: center;
-      border: 3px solid transparent;
-      cursor: pointer;
-
-      .model-font {
-        font-size: 14px;
-        color: #3e3e3e;
-        font-weight: bold;
-      }
-    }
-
-    .selectModel {
-      border: 3px solid #1293ff;
-      border-radius: 5px;
-    }
-  }
-}
-
-// 样式 style
-.image-style {
-  margin-top: 30px;
-
-  .image-style-list {
-    margin-top: 15px;
-
-    .image-style-item {
-      width: 110px;
-      //outline: 1px solid blue;
-      overflow: hidden;
-      display: flex;
-      flex-direction: column;
-      align-items: center;
-      border: 3px solid transparent;
-      cursor: pointer;
-
-      .style-font {
-        font-size: 14px;
-        color: #3e3e3e;
-        font-weight: bold;
-      }
-    }
-
-    .selectImageStyle {
-      border: 3px solid #1293ff;
-      border-radius: 5px;
-    }
-  }
-}
-
-// 尺寸
-.image-size {
-  width: 100%;
-  margin-top: 30px;
-
-  .size-list {
-    display: flex;
-    flex-direction: row;
-    justify-content: space-between;
-    width: 100%;
-    margin-top: 20px;
-
-    .size-item {
-      display: flex;
-      flex-direction: column;
-      align-items: center;
-      cursor: pointer;
-
-      .size-wrapper {
-        display: flex;
-        flex-direction: column;
-        align-items: center;
-        justify-content: center;
-        border-radius: 7px;
-        padding: 4px;
-        width: 50px;
-        height: 50px;
-        background-color: #fff;
-        border: 1px solid #fff;
-      }
-
-      .size-font {
-        font-size: 14px;
-        color: #3e3e3e;
-        font-weight: bold;
-      }
-    }
-  }
-
-  .selectImageSize {
-    border: 1px solid #1293ff !important;
-  }
-}
-
-.btns {
-  display: flex;
-  justify-content: center;
-  margin-top: 50px;
-}
-</style>

+ 18 - 132
src/views/ai/image/index/components/midjourney/index.vue

@@ -14,14 +14,14 @@
       type="textarea"
     />
   </div>
-  <div class="hot-words">
+  <div class="flex flex-col mt-30px">
     <div>
       <el-text tag="b">随机热词</el-text>
     </div>
-    <el-space wrap class="word-list">
+    <el-space wrap class="flex flex-row flex-wrap justify-start mt-15px">
       <el-button
         round
-        class="btn"
+        class="m-0"
         :type="selectHotWord === hotWord ? 'primary' : 'default'"
         v-for="hotWord in ImageHotWords"
         :key="hotWord"
@@ -31,49 +31,49 @@
       </el-button>
     </el-space>
   </div>
-  <div class="image-size">
+  <div class="w-full mt-30px">
     <div>
       <el-text tag="b">尺寸</el-text>
     </div>
-    <el-space wrap class="size-list">
+    <el-space wrap class="flex flex-row justify-between w-full mt-20px">
       <div
-        class="size-item"
+        class="flex flex-col items-center cursor-pointer"
         v-for="imageSize in MidjourneySizeList"
         :key="imageSize.key"
         @click="handleSizeClick(imageSize)"
       >
         <div
-          :class="selectSize === imageSize.key ? 'size-wrapper selectImageSize' : 'size-wrapper'"
+          :class="selectSize === imageSize.key ? 'flex flex-col items-center justify-center rounded-7px p-4px w-50px h-50px bg-white border-1 border-solid border-#1293ff' : 'flex flex-col items-center justify-center rounded-7px p-4px w-50px h-50px bg-white border-1 border-solid border-white'"
         >
           <div :style="imageSize.style"></div>
         </div>
-        <div class="size-font">{{ imageSize.key }}</div>
+        <div class="text-14px color-#3e3e3e font-bold">{{ imageSize.key }}</div>
       </div>
     </el-space>
   </div>
-  <div class="model">
+  <div class="mt-30px">
     <div>
       <el-text tag="b">模型</el-text>
     </div>
-    <el-space wrap class="model-list">
+    <el-space wrap class="mt-15px">
       <div
-        :class="selectModel === model.key ? 'modal-item selectModel' : 'modal-item'"
+        :class="selectModel === model.key ? 'flex flex-col items-center w-150px overflow-hidden border-3 border-solid border-#1293ff rounded-5px cursor-pointer' : 'flex flex-col items-center w-150px overflow-hidden border-3 border-solid border-transparent cursor-pointer'"
         v-for="model in MidjourneyModels"
         :key="model.key"
       >
         <el-image :src="model.image" fit="contain" @click="handleModelClick(model)" />
-        <div class="model-font">{{ model.name }}</div>
+        <div class="text-14px color-#3e3e3e font-bold">{{ model.name }}</div>
       </div>
     </el-space>
   </div>
-  <div class="version">
+  <div class="mt-20px">
     <div>
       <el-text tag="b">版本</el-text>
     </div>
-    <el-space wrap class="version-list">
+    <el-space wrap class="mt-20px w-full">
       <el-select
         v-model="selectVersion"
-        class="version-select !w-350px"
+        class="!w-350px"
         clearable
         placeholder="请选择版本"
       >
@@ -86,15 +86,15 @@
       </el-select>
     </el-space>
   </div>
-  <div class="model">
+  <div class="mt-30px">
     <div>
       <el-text tag="b">参考图</el-text>
     </div>
-    <el-space wrap class="model-list">
+    <el-space wrap class="mt-15px">
       <UploadImg v-model="referImageUrl" height="120px" width="120px" />
     </el-space>
   </div>
-  <div class="btns">
+  <div class="flex justify-center mt-50px">
     <el-button
       type="primary"
       size="large"
@@ -233,118 +233,4 @@ const settingValues = async (detail: ImageVO) => {
 /** 暴露组件方法 */
 defineExpose({ settingValues })
 </script>
-<style scoped lang="scss">
-// 提示词
-.prompt {
-}
-
-// 热词
-.hot-words {
-  display: flex;
-  flex-direction: column;
-  margin-top: 30px;
-
-  .word-list {
-    display: flex;
-    flex-direction: row;
-    flex-wrap: wrap;
-    justify-content: start;
-    margin-top: 15px;
-
-    .btn {
-      margin: 0;
-    }
-  }
-}
-
-// version
-.version {
-  margin-top: 20px;
-
-  .version-list {
-    margin-top: 20px;
-    width: 100%;
-  }
-}
-
-// 模型
-.model {
-  margin-top: 30px;
-
-  .model-list {
-    margin-top: 15px;
-
-    .modal-item {
-      display: flex;
-      flex-direction: column;
-      align-items: center;
-      width: 150px;
-      //outline: 1px solid blue;
-      overflow: hidden;
-      border: 3px solid transparent;
-      cursor: pointer;
-
-      .model-font {
-        font-size: 14px;
-        color: #3e3e3e;
-        font-weight: bold;
-      }
-    }
 
-    .selectModel {
-      border: 3px solid #1293ff;
-      border-radius: 5px;
-    }
-  }
-}
-
-// 尺寸
-.image-size {
-  width: 100%;
-  margin-top: 30px;
-
-  .size-list {
-    display: flex;
-    flex-direction: row;
-    justify-content: space-between;
-    width: 100%;
-    margin-top: 20px;
-
-    .size-item {
-      display: flex;
-      flex-direction: column;
-      align-items: center;
-      cursor: pointer;
-
-      .size-wrapper {
-        display: flex;
-        flex-direction: column;
-        align-items: center;
-        justify-content: center;
-        border-radius: 7px;
-        padding: 4px;
-        width: 50px;
-        height: 50px;
-        background-color: #fff;
-        border: 1px solid #fff;
-      }
-
-      .size-font {
-        font-size: 14px;
-        color: #3e3e3e;
-        font-weight: bold;
-      }
-    }
-  }
-
-  .selectImageSize {
-    border: 1px solid #1293ff !important;
-  }
-}
-
-.btns {
-  display: flex;
-  justify-content: center;
-  margin-top: 50px;
-}
-</style>

+ 18 - 57
src/views/ai/image/index/components/stableDiffusion/index.vue

@@ -14,14 +14,14 @@
       type="textarea"
     />
   </div>
-  <div class="hot-words">
+  <div class="flex flex-col mt-30px">
     <div>
       <el-text tag="b">随机热词</el-text>
     </div>
-    <el-space wrap class="word-list">
+    <el-space wrap class="flex flex-row flex-wrap justify-start mt-15px">
       <el-button
         round
-        class="btn"
+        class="m-0"
         :type="selectHotWord === hotWord ? 'primary' : 'default'"
         v-for="hotWord in ImageHotEnglishWords"
         :key="hotWord"
@@ -31,11 +31,11 @@
       </el-button>
     </el-space>
   </div>
-  <div class="group-item">
+  <div class="mt-30px">
     <div>
       <el-text tag="b">采样方法</el-text>
     </div>
-    <el-space wrap class="group-item-body">
+    <el-space wrap class="mt-15px w-full">
       <el-select v-model="sampler" placeholder="Select" size="large" class="!w-350px">
         <el-option
           v-for="item in StableDiffusionSamplers"
@@ -46,11 +46,11 @@
       </el-select>
     </el-space>
   </div>
-  <div class="group-item">
+  <div class="mt-30px">
     <div>
       <el-text tag="b">CLIP</el-text>
     </div>
-    <el-space wrap class="group-item-body">
+    <el-space wrap class="mt-15px w-full">
       <el-select v-model="clipGuidancePreset" placeholder="Select" size="large" class="!w-350px">
         <el-option
           v-for="item in StableDiffusionClipGuidancePresets"
@@ -61,11 +61,11 @@
       </el-select>
     </el-space>
   </div>
-  <div class="group-item">
+  <div class="mt-30px">
     <div>
       <el-text tag="b">风格</el-text>
     </div>
-    <el-space wrap class="group-item-body">
+    <el-space wrap class="mt-15px w-full">
       <el-select v-model="stylePreset" placeholder="Select" size="large" class="!w-350px">
         <el-option
           v-for="item in StableDiffusionStylePresets"
@@ -76,20 +76,20 @@
       </el-select>
     </el-space>
   </div>
-  <div class="group-item">
+  <div class="mt-30px">
     <div>
       <el-text tag="b">图片尺寸</el-text>
     </div>
-    <el-space wrap class="group-item-body">
+    <el-space wrap class="mt-15px w-full">
       <el-input v-model="width" class="w-170px" placeholder="图片宽度" />
       <el-input v-model="height" class="w-170px" placeholder="图片高度" />
     </el-space>
   </div>
-  <div class="group-item">
+  <div class="mt-30px">
     <div>
       <el-text tag="b">迭代步数</el-text>
     </div>
-    <el-space wrap class="group-item-body">
+    <el-space wrap class="mt-15px w-full">
       <el-input
         v-model="steps"
         type="number"
@@ -99,11 +99,11 @@
       />
     </el-space>
   </div>
-  <div class="group-item">
+  <div class="mt-30px">
     <div>
       <el-text tag="b">引导系数</el-text>
     </div>
-    <el-space wrap class="group-item-body">
+    <el-space wrap class="mt-15px w-full">
       <el-input
         v-model="scale"
         type="number"
@@ -113,11 +113,11 @@
       />
     </el-space>
   </div>
-  <div class="group-item">
+  <div class="mt-30px">
     <div>
       <el-text tag="b">随机因子</el-text>
     </div>
-    <el-space wrap class="group-item-body">
+    <el-space wrap class="mt-15px w-full">
       <el-input
         v-model="seed"
         type="number"
@@ -127,7 +127,7 @@
       />
     </el-space>
   </div>
-  <div class="btns">
+  <div class="flex justify-center mt-50px">
     <el-button
       type="primary"
       size="large"
@@ -254,43 +254,4 @@ const settingValues = async (detail: ImageVO) => {
 /** 暴露组件方法 */
 defineExpose({ settingValues })
 </script>
-<style scoped lang="scss">
-// 提示词
-.prompt {
-}
-
-// 热词
-.hot-words {
-  display: flex;
-  flex-direction: column;
-  margin-top: 30px;
-
-  .word-list {
-    display: flex;
-    flex-direction: row;
-    flex-wrap: wrap;
-    justify-content: start;
-    margin-top: 15px;
-
-    .btn {
-      margin: 0;
-    }
-  }
-}
 
-// 模型
-.group-item {
-  margin-top: 30px;
-
-  .group-item-body {
-    margin-top: 15px;
-    width: 100%;
-  }
-}
-
-.btns {
-  display: flex;
-  justify-content: center;
-  margin-top: 50px;
-}
-</style>

+ 12 - 53
src/views/ai/image/index/index.vue

@@ -1,11 +1,15 @@
 <!-- image -->
 <template>
-  <div class="ai-image">
-    <div class="left">
-      <div class="segmented">
-        <el-segmented v-model="selectPlatform" :options="platformOptions" />
+  <div class="absolute inset-0 flex flex-row wh-full">
+    <div class="flex flex-col p-5 w-[390px]">
+      <div class="mb-[30px]">
+        <el-segmented
+          v-model="selectPlatform"
+          :options="platformOptions"
+          class="w-[350px] !bg-[#ececec] [--el-border-radius-base:16px] [--el-segmented-item-selected-color:#fff]"
+        />
       </div>
-      <div class="modal-switch-container">
+      <div class="h-full overflow-y-auto">
         <Common
           v-if="selectPlatform === 'common'"
           ref="commonRef"
@@ -32,7 +36,7 @@
         />
       </div>
     </div>
-    <div class="main">
+    <div class="flex-1 bg-white">
       <ImageList ref="imageListRef" @on-regeneration="handleRegeneration" />
     </div>
   </div>
@@ -79,10 +83,10 @@ const platformOptions = [
 const models = ref<ModelVO[]>([]) // 模型列表
 
 /** 绘画 start  */
-const handleDrawStart = async (platform: string) => {}
+const handleDrawStart = async (_platform: string) => {}
 
 /** 绘画 complete */
-const handleDrawComplete = async (platform: string) => {
+const handleDrawComplete = async (_platform: string) => {
   await imageListRef.value.getImageList()
 }
 
@@ -108,48 +112,3 @@ onMounted(async () => {
   models.value = await ModelApi.getModelSimpleList(AiModelTypeEnum.IMAGE)
 })
 </script>
-
-<style scoped lang="scss">
-.ai-image {
-  position: absolute;
-  left: 0;
-  right: 0;
-  bottom: 0;
-  top: 0;
-
-  display: flex;
-  flex-direction: row;
-  height: 100%;
-  width: 100%;
-
-  .left {
-    display: flex;
-    flex-direction: column;
-    padding: 20px;
-    width: 390px;
-
-    .segmented .el-segmented {
-      --el-border-radius-base: 16px;
-      --el-segmented-item-selected-color: #fff;
-      background-color: #ececec;
-      width: 350px;
-    }
-
-    .modal-switch-container {
-      height: 100%;
-      overflow-y: auto;
-      margin-top: 30px;
-    }
-  }
-
-  .main {
-    flex: 1;
-    background-color: #fff;
-  }
-
-  .right {
-    width: 350px;
-    background-color: #f7f8fa;
-  }
-}
-</style>

+ 5 - 42
src/views/ai/image/square/index.vue

@@ -1,19 +1,19 @@
 <template>
-  <div class="square-container">
+  <div class="bg-white p-20px">
     <!-- TODO @fan:style 建议换成 unocss -->
     <!-- TODO @fan:Search 可以换成 Icon 组件么? -->
     <el-input
       v-model="queryParams.prompt"
-      style="width: 100%; margin-bottom: 20px"
+      class="!w-full !mb-20px"
       size="large"
       placeholder="请输入要搜索的内容"
       :suffix-icon="Search"
       @keyup.enter="handleQuery"
     />
-    <div class="gallery">
+    <div class="grid grid-cols-[repeat(auto-fill,minmax(200px,1fr))] gap-10px bg-white shadow-[0_0_10px_rgba(0,0,0,0.1)]">
       <!-- TODO @fan:这个图片的风格,要不和 ImageCard.vue 界面一致?(只有卡片,没有操作);因为看着更有相框的感觉~~~ -->
-      <div v-for="item in list" :key="item.id" class="gallery-item">
-        <img :src="item.picUrl" class="img" />
+      <div v-for="item in list" :key="item.id" class="relative overflow-hidden bg-gray-100 cursor-pointer transition-transform duration-300 hover:scale-105">
+        <img :src="item.picUrl" class="w-full h-auto block transition-transform duration-300 hover:scale-110" />
       </div>
     </div>
     <!-- TODO @fan:缺少翻页 -->
@@ -64,41 +64,4 @@ onMounted(async () => {
   await getList()
 })
 </script>
-<style scoped lang="scss">
-.square-container {
-  background-color: #fff;
-  padding: 20px;
 
-  .gallery {
-    display: grid;
-    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
-    gap: 10px;
-    //max-width: 1000px;
-    background-color: #fff;
-    box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
-  }
-
-  .gallery-item {
-    position: relative;
-    overflow: hidden;
-    background: #f0f0f0;
-    cursor: pointer;
-    transition: transform 0.3s;
-  }
-
-  .gallery-item img {
-    width: 100%;
-    height: auto;
-    display: block;
-    transition: transform 0.3s;
-  }
-
-  .gallery-item:hover img {
-    transform: scale(1.1);
-  }
-
-  .gallery-item:hover {
-    transform: scale(1.05);
-  }
-}
-</style>

+ 0 - 1
src/views/ai/write/index/components/Tag.vue

@@ -29,4 +29,3 @@ const emits = defineEmits<{
   (e: 'update:modelValue', value: string): void
 }>()
 </script>
-<style scoped></style>

+ 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' }],

+ 28 - 0
src/views/bpm/model/form/ExtraSettings.vue

@@ -232,6 +232,34 @@ import { ProcessVariableEnum } from '@/components/SimpleProcessDesignerV2/src/co
 import HttpRequestSetting from '@/components/SimpleProcessDesignerV2/src/nodes-config/components/HttpRequestSetting.vue'
 
 const modelData = defineModel<any>()
+const formFields = ref<string[]>([])
+   
+const props = defineProps({
+  // 流程表单 ID
+  modelFormId: {
+    type: Number,
+    required: false,
+    default: undefined,
+  }
+})
+
+
+// 监听 modelFormId 变化
+watch(
+  () => props.modelFormId,
+  async (newVal) => {
+    if (newVal) {
+      const form = await FormApi.getForm(newVal);
+      formFields.value = form?.fields;
+    } else {
+      // 如果 modelFormId 为空,清空表单字段
+      formFields.value = [];
+    }
+  },
+  { immediate: true },
+);
+// 暴露给子组件使用
+provide('formFields', formFields)
 
 /** 自定义 ID 流程编码 */
 const timeOptions = ref([

+ 2 - 2
src/views/bpm/model/form/ProcessDesign.vue

@@ -14,9 +14,9 @@
   <template v-else>
     <SimpleModelDesign
       v-if="showDesigner"
-      :model-id="modelData.id"
-      :model-key="modelData.key"
       :model-name="modelData.name"
+      :model-form-id="modelData.formId"
+      :model-form-type="modelData.formType"
       :start-user-ids="modelData.startUserIds"
       :start-dept-ids="modelData.startDeptIds"
       @success="handleDesignSuccess"

+ 14 - 1
src/views/bpm/model/form/index.vue

@@ -77,7 +77,10 @@
 
         <!-- 第四步:更多设置 -->
         <div v-show="currentStep === 3" class="mx-auto w-700px">
-          <ExtraSettings v-model="formData" ref="extraSettingsRef" />
+          <ExtraSettings
+            ref="extraSettingsRef"   
+            v-model="formData" 
+            :model-form-id="formData.formId"/>
         </div>
       </div>
     </div>
@@ -216,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('复制流程')

+ 4 - 1
src/views/bpm/model/index.vue

@@ -209,15 +209,18 @@ onActivated(() => {
 
 <style lang="scss" scoped>
 :deep() {
-  .el-table--fit .el-table__inner-wrapper:before {
+  .el-table--fit .el-table__inner-wrapper::before {
     height: 0;
   }
+
   .el-card {
     border-radius: 8px;
   }
+
   .el-form--inline .el-form-item {
     margin-right: 10px;
   }
+  
   .el-divider--horizontal {
     margin-top: 6px;
   }

+ 26 - 2
src/views/bpm/processInstance/create/ProcessDefinitionDetail.vue

@@ -231,8 +231,25 @@ const getApprovalDetail = async (row: any) => {
  */
 const setFieldPermission = (field: string, permission: string) => {
   if (permission === FieldPermissionType.READ) {
+    // 1. 设置字段为只读
     //@ts-ignore
     fApi.value?.disabled(true, field)
+    // 2. 只读字段, 去掉验证规则
+    //  fApi.value?.updateValidate(field, []); 这个方法貌似不起作用,
+    try {
+      //@ts-ignore
+      const rule = fApi.value?.getRule(field)
+      if (rule) {
+        // 必填验证设置为false
+        rule.$required = false
+        // 清空所有验证规则
+        if (rule.validate) {
+          rule.validate = []
+        }
+      }
+    } catch (error) {
+      console.warn('修改字段验证规则失败:', error)
+    }
   }
   if (permission === FieldPermissionType.WRITE) {
     //@ts-ignore
@@ -249,8 +266,15 @@ const submitForm = async () => {
   if (!fApi.value || !props.selectProcessDefinition) {
     return
   }
-  // 流程表单校验
-  await fApi.value.validate()
+  
+  try {
+    // 流程表单校验
+    await fApi.value.validate()
+  } catch (error) {
+    // 如果验证失败,检查是否是只读字段的验证错误
+    console.warn('表单验证失败:', error)
+    return
+  }
   // 如果有指定审批人,需要校验
   if (startUserSelectTasks.value?.length > 0) {
     for (const userTask of startUserSelectTasks.value) {

+ 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()

+ 18 - 4
src/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue

@@ -28,7 +28,9 @@
       <div class="flex flex-col items-start gap2" :id="`activity-task-${activity.id}-${index}`">
         <!-- 第一行:节点名称、时间 -->
         <div class="flex w-full">
-          <div class="font-bold"> {{ activity.name }}</div>
+          <div class="font-bold">
+            {{ activity.name }} <span v-if="activity.status === TaskStatusEnum.SKIP">【跳过】</span>
+          </div>
           <!-- 信息:时间 -->
           <div
             v-if="activity.status !== TaskStatusEnum.NOT_START"
@@ -38,7 +40,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>
@@ -179,7 +187,7 @@ import * as ProcessInstanceApi from '@/api/bpm/processInstance'
 import { TaskStatusEnum } from '@/api/bpm/task'
 import { NodeType, CandidateStrategy } from '@/components/SimpleProcessDesignerV2/src/consts'
 import { isEmpty } from '@/utils/is'
-import { Check, Close, Loading, Clock, Minus, Delete } from '@element-plus/icons-vue'
+import { Check, Close, Loading, Clock, Minus, Delete, ArrowDown } from '@element-plus/icons-vue'
 import starterSvg from '@/assets/svgs/bpm/starter.svg'
 import auditorSvg from '@/assets/svgs/bpm/auditor.svg'
 import copySvg from '@/assets/svgs/bpm/copy.svg'
@@ -203,6 +211,8 @@ const { push } = useRouter() // 路由
 
 // 审批节点
 const statusIconMap2 = {
+  // 跳过
+  '-2': { color: '#cccccc', icon: 'ep:arrow-down' },
   // 未开始
   '-1': { color: '#909398', icon: 'ep-clock' },
   // 待审批
@@ -224,6 +234,8 @@ const statusIconMap2 = {
 }
 
 const statusIconMap = {
+  // 跳过
+  '-2': { color: '#909398', icon: ArrowDown },
   // 审批未开始
   '-1': { color: '#909398', icon: Clock },
   '0': { color: '#00b32a', icon: Clock },
@@ -319,7 +331,9 @@ const handleUserSelectConfirm = (activityId: string, userList: any[]) => {
 
 /** 跳转子流程 */
 const handleChildProcess = (activity: any) => {
-  // TODO @lesan:貌似跳不过去?!
+  if (!activity.processInstanceId) {
+    return
+  }
   push({
     name: 'BpmProcessInstanceDetail',
     query: {

+ 5 - 6
src/views/bpm/simple/SimpleModelDesign.vue

@@ -1,12 +1,11 @@
 <template>
   <ContentWrap :bodyStyle="{ padding: '20px 16px' }">
     <SimpleProcessDesigner
-      :model-id="modelId"
-      :model-key="modelKey"
-      :model-name="modelName"
-      @success="handleSuccess"
+      :model-form-id="modelFormId"
+      :model-form-type="modelFormType"
       :start-user-ids="startUserIds"
       :start-dept-ids="startDeptIds"
+      @success="handleSuccess"
       ref="designerRef"
     />
   </ContentWrap>
@@ -19,9 +18,9 @@ defineOptions({
 })
 
 defineProps<{
-  modelId?: string
-  modelKey?: string
   modelName?: string
+  modelFormId?: number
+  modelFormType?: number
   startUserIds?: number[]
   startDeptIds?: number[]
 }>()

+ 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

+ 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 同步

+ 0 - 0
src/views/infra/config/index.vue


Vissa filer visades inte eftersom för många filer har ändrats