فهرست منبع

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

# Conflicts:
#	pnpm-lock.yaml
YunaiV 10 ماه پیش
والد
کامیت
9fe69a7a9a
100فایلهای تغییر یافته به همراه12378 افزوده شده و 618 حذف شده
  1. BIN
      .image/common/ai-feature.png
  2. BIN
      .image/demo/vue3-ep.png
  3. 9 9
      README.md
  4. 1 2
      build/vite/index.ts
  5. 2 2
      package.json
  6. 174 177
      pnpm-lock.yaml
  7. BIN
      public/home.png
  8. 25 0
      src/api/ai/workflow/index.ts
  9. 0 15
      src/api/infra/codegen/index.ts
  10. 5 9
      src/api/infra/file/index.ts
  11. 1 0
      src/api/infra/fileConfig/index.ts
  12. 0 2
      src/api/login/index.ts
  13. 7 3
      src/api/mall/trade/brokerage/withdraw/index.ts
  14. 1 8
      src/api/pay/demo/order/index.ts
  15. 0 25
      src/api/pay/demo/transfer/index.ts
  16. 30 0
      src/api/pay/demo/withdraw/index.ts
  17. 7 18
      src/api/pay/transfer/index.ts
  18. 5 0
      src/api/system/tenant/index.ts
  19. 2 7
      src/api/system/user/index.ts
  20. 5 9
      src/api/system/user/profile.ts
  21. 1 1
      src/assets/svgs/pay/icon/mock.svg
  22. 1 0
      src/assets/svgs/pay/icon/wallet.svg
  23. 2 1
      src/components/Cropper/src/CopperModal.vue
  24. 122 0
      src/components/DeptSelectForm/index.vue
  25. 2 1
      src/components/Dialog/src/Dialog.vue
  26. 2 2
      src/components/DiyEditor/components/mobile/CouponCard/component.tsx
  27. 10 3
      src/components/DiyEditor/components/mobile/CouponCard/index.vue
  28. 10 7
      src/components/DiyEditor/components/mobile/MagicCube/index.vue
  29. 2 2
      src/components/DiyEditor/components/mobile/MenuSwiper/index.vue
  30. 8 3
      src/components/DiyEditor/components/mobile/NavigationBar/property.vue
  31. 2 2
      src/components/DiyEditor/components/mobile/ProductCard/config.ts
  32. 4 1
      src/components/DiyEditor/components/mobile/ProductCard/index.vue
  33. 15 1
      src/components/DiyEditor/components/mobile/SearchBar/property.vue
  34. 7 1
      src/components/DiyEditor/components/mobile/TitleBar/config.ts
  35. 14 20
      src/components/DiyEditor/components/mobile/TitleBar/index.vue
  36. 45 22
      src/components/DiyEditor/components/mobile/TitleBar/property.vue
  37. 4 2
      src/components/Draggable/index.vue
  38. 8 1
      src/components/FormCreate/src/components/useApiSelect.tsx
  39. 1 0
      src/components/FormCreate/src/config/useSelectRule.ts
  40. 2 1
      src/components/FormCreate/src/type/index.ts
  41. 2 1
      src/components/FormCreate/src/useFormCreateDesigner.ts
  42. 2 2
      src/components/MagicCubeEditor/index.vue
  43. 3 2
      src/components/SimpleProcessDesignerV2/src/NodeHandler.vue
  44. 6 0
      src/components/SimpleProcessDesignerV2/src/SimpleProcessDesigner.vue
  45. 57 14
      src/components/SimpleProcessDesignerV2/src/nodes-config/StartUserNodeConfig.vue
  46. 6 5
      src/components/SimpleProcessDesignerV2/src/nodes-config/TriggerNodeConfig.vue
  47. 2 1
      src/components/SimpleProcessDesignerV2/src/nodes-config/components/Condition.vue
  48. 4 3
      src/components/SimpleProcessDesignerV2/src/nodes-config/components/ConditionDialog.vue
  49. 11 3
      src/components/SimpleProcessDesignerV2/src/nodes/ExclusiveNode.vue
  50. 11 3
      src/components/SimpleProcessDesignerV2/src/nodes/InclusiveNode.vue
  51. 63 0
      src/components/Tinyflow/Tinyflow.vue
  52. 0 0
      src/components/Tinyflow/ui/index.css
  53. 41 0
      src/components/Tinyflow/ui/index.d.ts
  54. 10206 0
      src/components/Tinyflow/ui/index.js
  55. 0 0
      src/components/Tinyflow/ui/index.umd.js
  56. 3 2
      src/components/UploadFile/src/UploadFile.vue
  57. 3 2
      src/components/UploadFile/src/UploadImg.vue
  58. 3 2
      src/components/UploadFile/src/UploadImgs.vue
  59. 16 15
      src/components/UploadFile/src/useUpload.ts
  60. 2 1
      src/components/bpmnProcessDesigner/package/penal/form/ElementForm.vue
  61. 0 1
      src/components/bpmnProcessDesigner/package/penal/listeners/ElementListeners.vue
  62. 0 1
      src/components/bpmnProcessDesigner/package/utils.ts
  63. 18 2
      src/config/axios/service.ts
  64. 2 0
      src/hooks/web/useCache.ts
  65. 18 1
      src/hooks/web/useWatermark.ts
  66. 46 0
      src/layout/components/TenantVisit/index.vue
  67. 8 0
      src/layout/components/ToolHeader.vue
  68. 6 6
      src/locales/zh-CN.ts
  69. 28 4
      src/router/modules/remaining.ts
  70. 5 0
      src/store/modules/user.ts
  71. 10 2
      src/utils/auth.ts
  72. 2 2
      src/utils/constants.ts
  73. 0 1
      src/utils/dict.ts
  74. 2 2
      src/utils/index.ts
  75. 91 60
      src/views/Home/Index.vue
  76. 2 0
      src/views/Home/types.ts
  77. 2 2
      src/views/Login/components/LoginForm.vue
  78. 9 7
      src/views/Profile/Index.vue
  79. 8 0
      src/views/Profile/components/BasicInfo.vue
  80. 6 0
      src/views/Profile/components/ProfileUser.vue
  81. 13 5
      src/views/Profile/components/UserAvatar.vue
  82. 2 0
      src/views/ai/chat/index/index.vue
  83. 2 0
      src/views/ai/mindmap/index/index.vue
  84. 5 2
      src/views/ai/model/model/ModelForm.vue
  85. 5 0
      src/views/ai/utils/constants.ts
  86. 54 0
      src/views/ai/workflow/form/BasicInfo.vue
  87. 250 0
      src/views/ai/workflow/form/WorkflowDesign.vue
  88. 240 0
      src/views/ai/workflow/form/index.vue
  89. 193 0
      src/views/ai/workflow/index.vue
  90. 4 2
      src/views/ai/write/index/index.vue
  91. 14 2
      src/views/bpm/model/CategoryDraggableModel.vue
  92. 73 1
      src/views/bpm/model/form/BasicInfo.vue
  93. 81 5
      src/views/bpm/model/form/ExtraSettings.vue
  94. 1 0
      src/views/bpm/model/form/ProcessDesign.vue
  95. 12 3
      src/views/bpm/model/form/index.vue
  96. 152 85
      src/views/bpm/oa/leave/create.vue
  97. 17 0
      src/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue
  98. 2 0
      src/views/bpm/simple/SimpleModelDesign.vue
  99. 1 1
      src/views/bpm/task/todo/index.vue
  100. 17 8
      src/views/infra/codegen/ImportTable.vue

BIN
.image/common/ai-feature.png


BIN
.image/demo/vue3-ep.png


+ 9 - 9
README.md

@@ -11,7 +11,7 @@
 
 * nodejs > 16.18.0 && pnpm > 8.6.0 (强制使用pnpm)
 * 演示地址【Vue3 + element-plus】:<http://dashboard-vue3.yudao.iocoder.cn>
-* 演示地址【Vue3 + vben(ant-design-vue)】:<http://dashboard-vben.yudao.iocoder.cn>
+* 演示地址【Vue3 + vben5.0(ant-design-vue)】:<http://dashboard-vben.yudao.iocoder.cn>
 * 演示地址【Vue2 + element-ui】:<http://dashboard.yudao.iocoder.cn>
 * 启动文档:<https://doc.iocoder.cn/quick-start/>
 * 视频教程:<https://doc.iocoder.cn/video/>
@@ -24,7 +24,7 @@
 * 改换 saas,自动引入等功能
 * 使用 Element Plus 免费开源的中后台模版,具备如下特性:
 
-![首页](public/home.png)
+![首页](.image/demo/vue3-ep.png)
 
 * **最新技术栈**:使用 Vue3、Vite4 等前端前沿技术开发
 * **TypeScript**: 应用程序级 JavaScript 的语言
@@ -38,15 +38,15 @@
 
 | 框架                                                                   | 说明               | 版本     |
 |----------------------------------------------------------------------|------------------|--------|
-| [Vue](https://staging-cn.vuejs.org/)                                 | Vue 框架           | 3.3.8 |
+| [Vue](https://staging-cn.vuejs.org/)                                 | Vue 框架           | 3.3.8  |
 | [Vite](https://cn.vitejs.dev//)                                      | 开发与构建工具          | 4.5.0  |
-| [Element Plus](https://element-plus.org/zh-CN/)                      | Element Plus     | 2.4.2 |
+| [Element Plus](https://element-plus.org/zh-CN/)                      | Element Plus     | 2.4.2  |
 | [TypeScript](https://www.typescriptlang.org/docs/)                   | JavaScript 的超集   | 5.2.2  |
-| [pinia](https://pinia.vuejs.org/)                                    | Vue 存储库 替代 vuex5 | 2.1.7 |
+| [pinia](https://pinia.vuejs.org/)                                    | Vue 存储库 替代 vuex5 | 2.1.7  |
 | [vueuse](https://vueuse.org/)                                        | 常用工具集            | 10.6.1 |
 | [vue-i18n](https://kazupon.github.io/vue-i18n/zh/introduction.html/) | 国际化              | 9.6.5  |
 | [vue-router](https://router.vuejs.org/)                              | Vue 路由           | 4.2.5  |
-| [unocss](https://uno.antfu.me/)                                      | 原子 css          | 0.57.4  |
+| [unocss](https://uno.antfu.me/)                                      | 原子 css           | 0.57.4 |
 | [iconify](https://icon-sets.iconify.design/)                         | 在线图标库            | 3.1.1  |
 | [wangeditor](https://www.wangeditor.com/)                            | 富文本编辑器           | 5.1.23 |
 
@@ -121,9 +121,9 @@
 
 基于 Flowable 构建,可支持信创(国产)数据库,满足中国特色流程操作:
 
-| BPMN 设计器                     | 钉钉/飞书设计器                       |
-|------------------------------|--------------------------------|
-| ![](/.image/工作流设计器-bpmn.jpg) | ![](/.image/工作流设计器-simple.jpg) |
+| BPMN 设计器                    | 钉钉/飞书设计器                      |
+|-----------------------------|-------------------------------|
+| ![](.image/工作流设计器-bpmn.jpg) | ![](.image/工作流设计器-simple.jpg) |
 
 > 历经头部企业生产验证,工作流引擎须标配仿钉钉/飞书 + BPMN 双设计器!!!
 >

+ 1 - 2
build/vite/index.ts

@@ -13,7 +13,7 @@ import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
 import viteCompression from 'vite-plugin-compression'
 import topLevelAwait from 'vite-plugin-top-level-await'
 import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite'
-import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
+import { createSvgIconsPlugin } from 'vite-plugin-svg-icons-ng'
 import UnoCSS from 'unocss/vite'
 
 export function createVitePlugins() {
@@ -78,7 +78,6 @@ export function createVitePlugins() {
     createSvgIconsPlugin({
       iconDirs: [pathResolve('src/assets/svgs')],
       symbolId: 'icon-[dir]-[name]',
-      svgoOptions: true
     }),
     viteCompression({
       verbose: true, // 是否在控制台输出压缩结果

+ 2 - 2
package.json

@@ -1,6 +1,6 @@
 {
   "name": "yudao-ui-admin-vue3",
-  "version": "2.4.1-snapshot",
+  "version": "2.6.0-snapshot",
   "description": "基于vue3、vite4、element-plus、typesScript",
   "author": "xingyu",
   "private": false,
@@ -134,7 +134,7 @@
     "vite-plugin-eslint": "^1.8.1",
     "vite-plugin-progress": "^0.0.7",
     "vite-plugin-purge-icons": "^0.10.0",
-    "vite-plugin-svg-icons": "^2.0.1",
+    "vite-plugin-svg-icons-ng": "^1.3.1",
     "vite-plugin-top-level-await": "^1.4.4",
     "vue-eslint-parser": "^9.3.2",
     "vue-tsc": "^1.8.27"

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 174 - 177
pnpm-lock.yaml


BIN
public/home.png


+ 25 - 0
src/api/ai/workflow/index.ts

@@ -0,0 +1,25 @@
+import request from '@/config/axios'
+
+export const getWorkflowPage = async (params) => {
+  return await request.get({ url: '/ai/workflow/page', params })
+}
+
+export const getWorkflow = async (id) => {
+  return await request.get({ url: '/ai/workflow/get?id=' + id })
+}
+
+export const createWorkflow = async (data) => {
+  return await request.post({ url: '/ai/workflow/create', data })
+}
+
+export const updateWorkflow = async (data) => {
+  return await request.put({ url: '/ai/workflow/update', data })
+}
+
+export const deleteWorkflow = async (id) => {
+  return await request.delete({ url: '/ai/workflow/delete?id=' + id })
+}
+
+export const testWorkflow = async (data) => {
+  return await request.post({ url: '/ai/workflow/test', data })
+}

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

@@ -46,11 +46,6 @@ export type DatabaseTableVO = {
   comment: string
 }
 
-export type CodegenDetailVO = {
-  table: CodegenTableVO
-  columns: CodegenColumnVO[]
-}
-
 export type CodegenPreviewVO = {
   filePath: string
   code: string
@@ -61,11 +56,6 @@ export type CodegenUpdateReqVO = {
   columns: CodegenColumnVO[]
 }
 
-export type CodegenCreateListReqVO = {
-  dataSourceConfigId: number
-  tableNames: string[]
-}
-
 // 查询列表代码生成表定义
 export const getCodegenTableList = (dataSourceConfigId: number) => {
   return request.get({ url: '/infra/codegen/table/list?dataSourceConfigId=' + dataSourceConfigId })
@@ -81,11 +71,6 @@ export const getCodegenTable = (id: number) => {
   return request.get({ url: '/infra/codegen/detail?tableId=' + id })
 }
 
-// 新增代码生成表定义
-export const createCodegenTable = (data: CodegenCreateListReqVO) => {
-  return request.post({ url: '/infra/codegen/create', data })
-}
-
 // 修改代码生成表定义
 export const updateCodegenTable = (data: CodegenUpdateReqVO) => {
   return request.put({ url: '/infra/codegen/update', data })

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

@@ -1,11 +1,5 @@
 import request from '@/config/axios'
 
-export interface FilePageReqVO extends PageParam {
-  path?: string
-  type?: string
-  createTime?: Date[]
-}
-
 // 文件预签名地址 Response VO
 export interface FilePresignedUrlRespVO {
   // 文件配置编号
@@ -14,10 +8,12 @@ export interface FilePresignedUrlRespVO {
   uploadUrl: string
   // 文件 URL
   url: string
+  // 文件路径
+  path: string
 }
 
 // 查询文件列表
-export const getFilePage = (params: FilePageReqVO) => {
+export const getFilePage = (params: PageParam) => {
   return request.get({ url: '/infra/file/page', params })
 }
 
@@ -27,10 +23,10 @@ export const deleteFile = (id: number) => {
 }
 
 // 获取文件预签名地址
-export const getFilePresignedUrl = (path: string) => {
+export const getFilePresignedUrl = (name: string, directory?: string) => {
   return request.get<FilePresignedUrlRespVO>({
     url: '/infra/file/presigned-url',
-    params: { path }
+    params: { name, directory }
   })
 }
 

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

@@ -11,6 +11,7 @@ export interface FileClientConfig {
   bucket?: string
   accessKey?: string
   accessSecret?: string
+  enablePathStyleAccess?: boolean
   domain: string
 }
 

+ 0 - 2
src/api/login/index.ts

@@ -1,5 +1,4 @@
 import request from '@/config/axios'
-import { getRefreshToken } from '@/utils/auth'
 import type { RegisterVO, UserLoginVO } from './types'
 
 export interface SmsCodeVO {
@@ -72,7 +71,6 @@ export const socialAuthRedirect = (type: number, redirectUri: string) => {
 }
 // 获取验证图片以及 token
 export const getCode = (data: any) => {
-  debugger
   return request.postOriginal({ url: 'system/captcha/get', data })
 }
 

+ 7 - 3
src/api/mall/trade/brokerage/withdraw/index.ts

@@ -7,15 +7,19 @@ export interface BrokerageWithdrawVO {
   feePrice: number
   totalPrice: number
   type: number
-  name: string
-  accountNo: string
+  userName: string
+  userAccount: string
   bankName: string
   bankAddress: string
-  accountQrCodeUrl: string
+  qrCodeUrl: string
   status: number
   auditReason: string
   auditTime: Date
   remark: string
+  payTransferId?: number
+  transferChannelCode?: string
+  transferTime?: Date
+  transferErrorMsg?: string
 }
 
 // 查询佣金提现列表

+ 1 - 8
src/api/pay/demo/index.ts → src/api/pay/demo/order/index.ts

@@ -13,13 +13,6 @@ export function createDemoOrder(data: DemoOrderVO) {
   })
 }
 
-// 获得示例订单
-export function getDemoOrder(id: number) {
-  return request.get({
-    url: '/pay/demo-order/get?id=' + id
-  })
-}
-
 // 获得示例订单分页
 export function getDemoOrderPage(query: PageParam) {
   return request.get({
@@ -29,7 +22,7 @@ export function getDemoOrderPage(query: PageParam) {
 }
 
 // 退款示例订单
-export function refundDemoOrder(id) {
+export function refundDemoOrder(id: number) {
   return request.put({
     url: '/pay/demo-order/refund?id=' + id
   })

+ 0 - 25
src/api/pay/demo/transfer/index.ts

@@ -1,25 +0,0 @@
-import request from '@/config/axios'
-
-export interface DemoTransferVO {
-  price: number
-  type: number
-  userName: string
-  alipayLogonId: string
-  openid: string
-}
-
-// 创建示例转账单
-export function createDemoTransfer(data: DemoTransferVO) {
-  return request.post({
-    url: '/pay/demo-transfer/create',
-    data: data
-  })
-}
-
-// 获得示例订单分页
-export function getDemoTransferPage(query: PageParam) {
-  return request.get({
-    url: '/pay/demo-transfer/page',
-    params: query
-  })
-}

+ 30 - 0
src/api/pay/demo/withdraw/index.ts

@@ -0,0 +1,30 @@
+import request from '@/config/axios'
+
+export interface PayDemoWithdrawVO {
+  id?: number
+  subject: string
+  price: number
+  userName: string
+  userAccount: string
+  type: number
+  status?: number
+  payTransferId?: number
+  transferChannelCode?: string
+  transferTime?: Date
+  transferErrorMsg?: string
+}
+
+// 查询示例提现单列表
+export const getDemoWithdrawPage = (params: PageParam) => {
+  return request.get({ url: '/pay/demo-withdraw/page', params })
+}
+
+// 创建示例提现单
+export const createDemoWithdraw = (data: PayDemoWithdrawVO) => {
+  return request.post({ url: '/pay/demo-withdraw/create', data })
+}
+
+// 发起提现单转账
+export const transferDemoWithdraw = (id: number) => {
+  return request.post({ url: '/pay/demo-withdraw/transfer', params: { id } })
+}

+ 7 - 18
src/api/pay/transfer/index.ts

@@ -1,27 +1,16 @@
 import request from '@/config/axios'
 
-export interface TransferVO {
-  appId: number
-  channelCode: string
-  merchantTransferId: string
-  type: number
-  price: number
-  subject: string
-  userName: string
-  alipayLogonId: string
-  openid: string
-}
-
-// 新增转账单
-export const createTransfer = async (data: TransferVO) => {
-  return await request.post({ url: `/pay/transfer/create`, data })
-}
-
 // 查询转账单列表
-export const getTransferPage = async (params) => {
+export const getTransferPage = async (params: PageParam) => {
   return await request.get({ url: `/pay/transfer/page`, params })
 }
 
+// 查询转账单详情
 export const getTransfer = async (id: number) => {
   return await request.get({ url: '/pay/transfer/get?id=' + id })
 }
+
+// 导出转账单
+export const exportTransfer = async (params: PageParam) => {
+  return await request.download({ url: '/pay/transfer/export-excel', params })
+}

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

@@ -41,6 +41,11 @@ export const getTenant = (id: number) => {
   return request.get({ url: '/system/tenant/get?id=' + id })
 }
 
+// 获取租户精简信息列表
+export const getTenantList = () => {
+  return request.get({ url: '/system/tenant/simple-list' })
+}
+
 // 新增租户
 export const createTenant = (data: TenantVO) => {
   return request.post({ url: '/system/tenant/create', data })

+ 2 - 7
src/api/system/user/index.ts

@@ -22,11 +22,6 @@ export const getUserPage = (params: PageParam) => {
   return request.get({ url: '/system/user/page', params })
 }
 
-// 查询所有用户列表
-export const getAllUser = () => {
-  return request.get({ url: '/system/user/all' })
-}
-
 // 查询用户详情
 export const getUser = (id: number) => {
   return request.get({ url: '/system/user/get?id=' + id })
@@ -48,7 +43,7 @@ export const deleteUser = (id: number) => {
 }
 
 // 导出用户
-export const exportUser = (params) => {
+export const exportUser = (params: any) => {
   return request.download({ url: '/system/user/export', params })
 }
 
@@ -58,7 +53,7 @@ export const importUserTemplate = () => {
 }
 
 // 用户密码重置
-export const resetUserPwd = (id: number, password: string) => {
+export const resetUserPassword = (id: number, password: string) => {
   const data = {
     id,
     password

+ 5 - 9
src/api/system/user/profile.ts

@@ -32,10 +32,11 @@ export interface ProfileVO {
 }
 
 export interface UserProfileUpdateReqVO {
-  nickname: string
-  email: string
-  mobile: string
-  sex: number
+  nickname?: string
+  email?: string
+  mobile?: string
+  sex?: number
+  avatar?: string
 }
 
 // 查询用户个人信息
@@ -58,8 +59,3 @@ export const updateUserPassword = (oldPassword: string, newPassword: string) =>
     }
   })
 }
-
-// 用户头像上传
-export const uploadAvatar = (data) => {
-  return request.upload({ url: '/system/user/profile/update-avatar', data: data })
-}

+ 1 - 1
src/assets/svgs/pay/icon/mock.svg

@@ -1 +1 @@
-<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1676209854312" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3033" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M173.077333 362.666667l91.114667-214.677334a65.6 65.6 0 0 1 86.016-34.773333c11.584 4.906667 24.96 10.282667 40.896 16.448 8.277333 3.2 16.789333 6.464 27.904 10.666667 28.202667 10.709333 39.296 14.933333 46.144 17.642666l51.477333-51.669333c28.181333-28.16 74.112-27.946667 102.570667 0.533333l195.925333 195.925334c16.426667 16.426667 23.445333 38.634667 21.056 59.904H896a42.666667 42.666667 0 0 1 42.666667 42.666666v490.666667a42.666667 42.666667 0 0 1-42.666667 42.666667H128a42.666667 42.666667 0 0 1-42.666667-42.666667V405.333333a42.666667 42.666667 0 0 1 42.666667-42.666666h45.077333z m48.96 0h39.104l169.194667-169.770667-27.328-10.389333c-11.2-4.245333-19.818667-7.530667-28.224-10.794667a1459.2 1459.2 0 0 1-42.197333-17.002667 20.522667 20.522667 0 0 0-26.901334 10.88L222.037333 362.666667z m108.842667 0h454.954667a23.509333 23.509333 0 0 0-5.290667-25.322667l-195.925333-195.925333a23.36 23.36 0 0 0-33.024-0.213334L330.88 362.666667zM128 405.333333v490.666667h768V405.333333H128z m597.333333 320a85.333333 85.333333 0 1 1 0-170.666666 85.333333 85.333333 0 0 1 0 170.666666z m0-42.666666a42.666667 42.666667 0 1 0 0-85.333334 42.666667 42.666667 0 0 0 0 85.333334z" fill="#4296d5" p-id="3034"></path></svg>
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1747409043186" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4834" width="128" height="128" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M44.416 853.333333v-85.205333a170.666667 170.666667 0 0 1 170.666667-170.666667h170.837333a37637.589333 37637.589333 0 0 1 0-206.165333C324.309333 352.170667 281.6 285.141333 281.6 211.968c0-116.906667 90.197333-211.072 231.168-211.072 140.970667 0 230.741333 94.208 230.741333 211.072 0 73.216-40.96 140.245333-102.528 179.328 0.256 0.170667 0.256 68.906667 0 206.165333h171.989334a170.666667 170.666667 0 0 1 170.666666 170.666667V853.333333a170.666667 170.666667 0 0 1-170.666666 170.666667H215.082667a170.666667 170.666667 0 0 1-170.666667-170.666667z m84.266667-84.650666v104.277333a85.333333 85.333333 0 0 0 85.333333 85.333333H811.52a85.333333 85.333333 0 0 0 85.333333-85.333333v-104.277333a85.333333 85.333333 0 0 0-85.333333-85.333334h-256.64l8.96-342.698666c66.944-21.333333 100.394667-64.256 100.394667-128.682667 0-61.952-57.344-129.322667-151.466667-129.322667-94.122667 0-146.645333 61.610667-146.645333 129.322667 0 71.466667 34.816 114.346667 104.362666 128.682667v342.698666H214.016a85.333333 85.333333 0 0 0-85.333333 85.333334z m167.125333 138.368c-50.432 0-91.434667-41.557333-91.434667-92.586667s41.002667-92.586667 91.434667-92.586667c50.389333 0 91.434667 41.557333 91.434667 92.586667 0 24.832-9.6 48.170667-27.008 65.706667-17.237333 17.322667-40.106667 26.88-64.426667 26.88z m0-119.466667a27.093333 27.093333 0 0 0-27.306667 26.88c0 14.805333 12.245333 26.88 27.306667 26.88a27.306667 27.306667 0 0 0 19.498667-8.106667 26.453333 26.453333 0 0 0 7.808-18.773333 27.093333 27.093333 0 0 0-27.306667-26.88z" fill="#1296db" p-id="4835"></path></svg>

+ 1 - 0
src/assets/svgs/pay/icon/wallet.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1676209854312" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3033" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M173.077333 362.666667l91.114667-214.677334a65.6 65.6 0 0 1 86.016-34.773333c11.584 4.906667 24.96 10.282667 40.896 16.448 8.277333 3.2 16.789333 6.464 27.904 10.666667 28.202667 10.709333 39.296 14.933333 46.144 17.642666l51.477333-51.669333c28.181333-28.16 74.112-27.946667 102.570667 0.533333l195.925333 195.925334c16.426667 16.426667 23.445333 38.634667 21.056 59.904H896a42.666667 42.666667 0 0 1 42.666667 42.666666v490.666667a42.666667 42.666667 0 0 1-42.666667 42.666667H128a42.666667 42.666667 0 0 1-42.666667-42.666667V405.333333a42.666667 42.666667 0 0 1 42.666667-42.666666h45.077333z m48.96 0h39.104l169.194667-169.770667-27.328-10.389333c-11.2-4.245333-19.818667-7.530667-28.224-10.794667a1459.2 1459.2 0 0 1-42.197333-17.002667 20.522667 20.522667 0 0 0-26.901334 10.88L222.037333 362.666667z m108.842667 0h454.954667a23.509333 23.509333 0 0 0-5.290667-25.322667l-195.925333-195.925333a23.36 23.36 0 0 0-33.024-0.213334L330.88 362.666667zM128 405.333333v490.666667h768V405.333333H128z m597.333333 320a85.333333 85.333333 0 1 1 0-170.666666 85.333333 85.333333 0 0 1 0 170.666666z m0-42.666666a42.666667 42.666667 0 1 0 0-85.333334 42.666667 42.666667 0 0 0 0 85.333334z" fill="#4296d5" p-id="3034"></path></svg>

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

@@ -1,5 +1,5 @@
 <template>
-  <div>
+  <div @click.stop>
     <Dialog
       v-model="dialogVisible"
       :canFullscreen="false"
@@ -181,6 +181,7 @@ function openModal() {
 }
 
 function closeModal() {
+  debugger
   dialogVisible.value = false
 }
 

+ 122 - 0
src/components/DeptSelectForm/index.vue

@@ -0,0 +1,122 @@
+<template>
+  <Dialog v-model="dialogVisible" title="部门选择" width="600">
+    <el-row v-loading="formLoading">
+      <el-col :span="24">
+        <ContentWrap class="h-1/1">
+          <el-tree
+            ref="treeRef"
+            :data="deptTree"
+            :props="defaultProps"
+            show-checkbox
+            :check-strictly="checkStrictly"
+            check-on-click-node
+            default-expand-all
+            highlight-current
+            node-key="id"
+            @check="handleCheck"
+          />
+        </ContentWrap>
+      </el-col>
+    </el-row>
+    <template #footer>
+      <el-button
+        :disabled="formLoading || !selectedDeptIds?.length"
+        type="primary"
+        @click="submitForm"
+      >
+        确 定
+      </el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { defaultProps, handleTree } from '@/utils/tree'
+import * as DeptApi from '@/api/system/dept'
+
+defineOptions({ name: 'DeptSelectForm' })
+
+const emit = defineEmits<{
+  confirm: [deptList: any[]]
+}>()
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const props = defineProps({
+  // 是否严格的遵循父子不互相关联
+  checkStrictly: {
+    type: Boolean,
+    default: false
+  },
+  // 是否支持多选
+  multiple: {
+    type: Boolean,
+    default: true
+  }
+})
+
+const treeRef = ref()
+const deptTree = ref<Tree[]>([]) // 部门树形结构
+const selectedDeptIds = ref<number[]>([]) // 选中的部门 ID 列表
+const dialogVisible = ref(false) // 弹窗的是否展示
+const formLoading = ref(false) // 表单的加载中
+
+/** 打开弹窗 */
+const open = async (selectedList?: DeptApi.DeptVO[]) => {
+  resetForm()
+  formLoading.value = true
+  try {
+    // 加载部门列表
+    const deptData = await DeptApi.getSimpleDeptList()
+    deptTree.value = handleTree(deptData)
+  } finally {
+    formLoading.value = false
+  }
+  dialogVisible.value = true
+  // 设置已选择的部门
+  if (selectedList?.length) {
+    await nextTick()
+    const selectedIds = selectedList
+      .map((dept) => dept.id)
+      .filter((id): id is number => id !== undefined)
+    selectedDeptIds.value = selectedIds
+    treeRef.value?.setCheckedKeys(selectedIds)
+  }
+}
+
+/** 处理选中状态变化 */
+const handleCheck = (data: any, checked: any) => {
+  selectedDeptIds.value = treeRef.value.getCheckedKeys()
+  if (!props.multiple && selectedDeptIds.value.length > 1) {
+    // 单选模式下,只保留最后选择的节点
+    const lastSelectedId = selectedDeptIds.value[selectedDeptIds.value.length - 1]
+    selectedDeptIds.value = [lastSelectedId]
+    treeRef.value.setCheckedKeys([lastSelectedId])
+  }
+}
+
+/** 提交选择 */
+const submitForm = async () => {
+  try {
+    // 获取选中的完整部门数据
+    const checkedNodes = treeRef.value.getCheckedNodes()
+    message.success(t('common.updateSuccess'))
+    dialogVisible.value = false
+    emit('confirm', checkedNodes)
+  } finally {
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  deptTree.value = []
+  selectedDeptIds.value = []
+  if (treeRef.value) {
+    treeRef.value.setCheckedKeys([])
+  }
+}
+
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+</script>

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

@@ -68,6 +68,7 @@ const dialogStyle = computed(() => {
     draggable
     class="com-dialog"
     :show-close="false"
+    @close="$emit('update:modelValue', false)"
   >
     <template #header="{ close }">
       <div class="relative h-54px flex items-center justify-between pl-15px pr-15px">
@@ -90,7 +91,7 @@ const dialogStyle = computed(() => {
             icon="ep:close"
             hover-color="var(--el-color-primary)"
             color="var(--el-color-info)"
-            @click="close"
+            @click.stop="close"
           />
         </div>
       </div>

+ 2 - 2
src/components/DiyEditor/components/mobile/CouponCard/component.tsx

@@ -13,7 +13,7 @@ export const CouponDiscount = defineComponent({
   setup(props) {
     const coupon = props.coupon as CouponTemplateApi.CouponTemplateVO
     // 折扣
-    let value = coupon.discountPercent + ''
+    let value = coupon.discountPercent / 10 + ''
     let suffix = ' 折'
     // 满减
     if (coupon.discountType === PromotionDiscountTypeEnum.PRICE.type) {
@@ -43,7 +43,7 @@ export const CouponDiscountDesc = defineComponent({
     const discountDesc =
       coupon.discountType === PromotionDiscountTypeEnum.PRICE.type
         ? `减${floatToFixed2(coupon.discountPrice)}元`
-        : `打${coupon.discountPercent}折`
+        : `打${coupon.discountPercent / 10.0}折`
     return () => (
       <div>
         <span>{useCondition}</span>

+ 10 - 3
src/components/DiyEditor/components/mobile/CouponCard/index.vue

@@ -49,7 +49,13 @@
           <div class="flex flex-col justify-evenly gap-4px">
             <!-- 优惠值 -->
             <CouponDiscount :coupon="coupon" />
-            <div>{{ coupon.name }}</div>
+            <!-- 优惠描述 -->
+            <CouponDiscountDesc :coupon="coupon" />
+            <!-- 领取说明 -->
+            <div v-if="coupon.totalCount >= 0">
+              仅剩:{{ coupon.totalCount - coupon.takeCount }}张
+            </div>
+            <div v-else-if="coupon.totalCount === -1">仅剩:不限制</div>
           </div>
           <div class="flex flex-col">
             <div
@@ -67,7 +73,8 @@
         <div v-else class="flex flex-col items-center justify-around gap-4px p-4px">
           <!-- 优惠值 -->
           <CouponDiscount :coupon="coupon" />
-          <div>{{ coupon.name }}</div>
+          <!-- 优惠描述 -->
+          <CouponDiscountDesc :coupon="coupon" />
           <div
             class="rounded-20px p-x-8px p-y-2px"
             :style="{
@@ -124,7 +131,7 @@ watch(
   () => {
     // 每列的宽度为:(总宽度 - 间距 * (列数 - 1))/ 列数
     couponWidth.value =
-      (phoneWidth.value * 0.95 - props.property.space * (props.property.columns - 1)) /
+      (phoneWidth.value - props.property.space * (props.property.columns - 1)) /
       props.property.columns
     // 显示滚动条
     scrollbarWidth.value = `${

+ 10 - 7
src/components/DiyEditor/components/mobile/MagicCube/index.vue

@@ -1,16 +1,19 @@
 <template>
   <div
     class="relative"
-    :style="{ height: `${rowCount * CUBE_SIZE}px`, width: `${4 * CUBE_SIZE}px` }"
+    :style="{
+      height: `${rowCount * CUBE_SIZE}px`,
+      width: `${4 * CUBE_SIZE}px`,
+      padding: `${property.space}px`
+    }"
   >
     <div
       v-for="(item, index) in property.list"
       :key="index"
       class="absolute"
       :style="{
-        width: `${item.width * CUBE_SIZE - property.space * 2}px`,
-        height: `${item.height * CUBE_SIZE - property.space * 2}px`,
-        margin: `${property.space}px`,
+        width: `${item.width * CUBE_SIZE - property.space}px`,
+        height: `${item.height * CUBE_SIZE - property.space}px`,
         top: `${item.top * CUBE_SIZE}px`,
         left: `${item.left * CUBE_SIZE}px`
       }"
@@ -63,10 +66,10 @@ const rowCount = computed(() => {
   let count = 0
   if (props.property.list.length > 0) {
     // 最大行号
-    count = Math.max(...props.property.list.map((item) => item.bottom))
+    count = Math.max(...props.property.list.map((item) => item.top + item.height))
   }
-  // 行号从 0 开始,所以加 1
-  return count + 1
+  // 保证至少有一
+  return count == 0 ? 1 : count
 })
 </script>
 

+ 2 - 2
src/components/DiyEditor/components/mobile/MenuSwiper/index.vue

@@ -39,7 +39,7 @@
           </span>
         </div>
       </div>
-    </el-carousel-item>
+    </el-carousel-item> 
   </el-carousel>
 </template>
 
@@ -51,7 +51,7 @@ const props = defineProps<{ property: MenuSwiperProperty }>()
 // 标题的高度
 const TITLE_HEIGHT = 20
 // 图标的高度
-const ICON_SIZE = 42
+const ICON_SIZE = 32
 // 垂直间距:一行上下的间距
 const SPACE_Y = 16
 

+ 8 - 3
src/components/DiyEditor/components/mobile/NavigationBar/property.vue

@@ -29,7 +29,10 @@
       <ColorInput v-model="formData.bgColor" />
     </el-form-item>
     <el-form-item label="背景图片" prop="bgImg" v-else>
-      <UploadImg v-model="formData.bgImg" :limit="1" width="56px" height="56px" />
+      <div class="flex items-center">
+        <UploadImg v-model="formData.bgImg" :limit="1" width="56px" height="56px" />
+        <span class="text-xs text-gray-400 ml-2 mb-2">建议宽度:750</span>
+      </div>
     </el-form-item>
     <el-card class="property-group" shadow="never">
       <template #header>
@@ -39,8 +42,9 @@
             <el-checkbox
               v-model="formData._local.previewMp"
               @change="formData._local.previewOther = !formData._local.previewMp"
-              >预览</el-checkbox
             >
+              预览
+            </el-checkbox>
           </el-form-item>
         </div>
       </template>
@@ -54,8 +58,9 @@
             <el-checkbox
               v-model="formData._local.previewOther"
               @change="formData._local.previewMp = !formData._local.previewOther"
-              >预览</el-checkbox
             >
+              预览
+            </el-checkbox>
           </el-form-item>
         </div>
       </template>

+ 2 - 2
src/components/DiyEditor/components/mobile/ProductCard/config.ts

@@ -82,8 +82,8 @@ export const component = {
       bgEndColor: '#FE832A',
       imgUrl: ''
     },
-    borderRadiusTop: 8,
-    borderRadiusBottom: 8,
+    borderRadiusTop: 6,
+    borderRadiusBottom: 6,
     space: 8,
     spuIds: [],
     style: {

+ 4 - 1
src/components/DiyEditor/components/mobile/ProductCard/index.vue

@@ -14,7 +14,10 @@
       :key="index"
     >
       <!-- 角标 -->
-      <div v-if="property.badge.show" class="absolute left-0 top-0 z-1 items-center justify-center">
+      <div
+        v-if="property.badge.show && property.badge.imgUrl"
+        class="absolute left-0 top-0 z-1 items-center justify-center"
+      >
         <el-image fit="cover" :src="property.badge.imgUrl" class="h-26px w-38px" />
       </div>
       <!-- 商品封面图 -->

+ 15 - 1
src/components/DiyEditor/components/mobile/SearchBar/property.vue

@@ -3,7 +3,7 @@
     <!-- 表单 -->
     <el-form label-width="80px" :model="formData" class="m-t-8px">
       <el-card header="搜索热词" class="property-group" shadow="never">
-        <Draggable v-model="formData.hotKeywords" :empty-item="''">
+        <Draggable v-model="formData.hotKeywords" :empty-item="''" :min="0">
           <template #default="{ index }">
             <el-input v-model="formData.hotKeywords[index]" placeholder="请输入热词" />
           </template>
@@ -61,6 +61,7 @@
 <script setup lang="ts">
 import { useVModel } from '@vueuse/core'
 import { SearchProperty } from '@/components/DiyEditor/components/mobile/SearchBar/config'
+import { isString } from '@/utils/is'
 
 /** 搜索框属性面板 */
 defineOptions({ name: 'SearchProperty' })
@@ -68,6 +69,19 @@ defineOptions({ name: 'SearchProperty' })
 const props = defineProps<{ modelValue: SearchProperty }>()
 const emit = defineEmits(['update:modelValue'])
 const formData = useVModel(props, 'modelValue', emit)
+
+// 监听热词数组变化
+watch(
+  () => formData.value.hotKeywords,
+  (newVal) => {
+    // 找到非字符串项的索引
+    const nonStringIndex = newVal.findIndex((item) => !isString(item))
+    if (nonStringIndex !== -1) {
+      formData.value.hotKeywords[nonStringIndex] = ''
+    }
+  },
+  { deep: true, flush: 'post' }
+)
 </script>
 
 <style scoped lang="scss"></style>

+ 7 - 1
src/components/DiyEditor/components/mobile/TitleBar/config.ts

@@ -1,7 +1,9 @@
-import {ComponentStyle, DiyComponent} from '@/components/DiyEditor/util'
+import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
 
 /** 标题栏属性 */
 export interface TitleBarProperty {
+  // 背景图
+  bgImgUrl: string
   // 偏移
   marginLeft: number
   // 显示位置
@@ -22,6 +24,8 @@ export interface TitleBarProperty {
   titleColor: string
   // 描述颜色
   descriptionColor: string
+  // 高度
+  height: number
   // 查看更多
   more: {
     // 是否显示查看更多
@@ -52,6 +56,8 @@ export const component = {
     descriptionWeight: 200,
     titleColor: 'rgba(50, 50, 51, 10)',
     descriptionColor: 'rgba(150, 151, 153, 10)',
+    marginLeft: 0,
+    height: 40,
     more: {
       //查看更多
       show: false,

+ 14 - 20
src/components/DiyEditor/components/mobile/TitleBar/index.vue

@@ -1,55 +1,49 @@
 <template>
-  <div
-    :style="{
-      background:
-        property.style.bgType === 'color' ? property.style.bgColor : `url(${property.style.bgImg})`,
-      backgroundSize: '100% 100%',
-      backgroundRepeat: 'no-repeat'
-    }"
-    class="title-bar"
-  >
-    <!-- 内容 -->
-    <div>
+  <div class="title-bar" :style="{ height: `${property.height}px` }">
+    <el-image v-if="property.bgImgUrl" :src="property.bgImgUrl" fit="cover" class="w-full" />
+    <div class="absolute left-0 top-0 w-full h-full flex flex-col justify-center">
       <!-- 标题 -->
       <div
-        v-if="property.title"
         :style="{
           fontSize: `${property.titleSize}px`,
           fontWeight: property.titleWeight,
           color: property.titleColor,
-          textAlign: property.textAlign
+          textAlign: property.textAlign,
+          marginLeft: `${property.marginLeft}px`,
+          marginBottom: '4px'
         }"
+        v-if="property.title"
       >
         {{ property.title }}
       </div>
       <!-- 副标题 -->
       <div
-        v-if="property.description"
         :style="{
           fontSize: `${property.descriptionSize}px`,
           fontWeight: property.descriptionWeight,
           color: property.descriptionColor,
-          textAlign: property.textAlign
+          textAlign: property.textAlign,
+          marginLeft: `${property.marginLeft}px`
         }"
-        class="m-t-8px"
+        v-if="property.description"
       >
         {{ property.description }}
       </div>
     </div>
     <!-- 更多 -->
     <div
+      class="more"
       v-show="property.more.show"
       :style="{
         color: property.descriptionColor
       }"
-      class="more"
     >
       <span v-if="property.more.type !== 'icon'"> {{ property.more.text }} </span>
-      <Icon v-if="property.more.type !== 'text'" icon="ep:arrow-right" />
+      <Icon icon="ep:arrow-right" v-if="property.more.type !== 'text'" />
     </div>
   </div>
 </template>
-<script lang="ts" setup>
+<script setup lang="ts">
 import { TitleBarProperty } from './config'
 
 /** 标题栏 */
@@ -57,7 +51,7 @@ defineOptions({ name: 'TitleBar' })
 
 defineProps<{ property: TitleBarProperty }>()
 </script>
-<style lang="scss" scoped>
+<style scoped lang="scss">
 .title-bar {
   position: relative;
   width: 100%;

+ 45 - 22
src/components/DiyEditor/components/mobile/TitleBar/property.vue

@@ -1,7 +1,12 @@
 <template>
   <ComponentContainerProperty v-model="formData.style">
-    <el-form :model="formData" :rules="rules" label-width="85px">
-      <el-card class="property-group" header="风格" shadow="never">
+    <el-form label-width="85px" :model="formData" :rules="rules">
+      <el-card header="风格" class="property-group" shadow="never">
+        <el-form-item label="背景图片" prop="bgImgUrl">
+          <UploadImg v-model="formData.bgImgUrl" width="100%" height="40px">
+            <template #tip>建议尺寸 750*80</template>
+          </UploadImg>
+        </el-form-item>
         <el-form-item label="标题位置" prop="textAlign">
           <el-radio-group v-model="formData!.textAlign">
             <el-tooltip content="居左" placement="top">
@@ -16,66 +21,84 @@
             </el-tooltip>
           </el-radio-group>
         </el-form-item>
+        <el-form-item label="偏移量" prop="marginLeft" label-width="70px">
+          <el-slider
+            v-model="formData.marginLeft"
+            :max="100"
+            :min="0"
+            show-input
+            input-size="small"
+          />
+        </el-form-item>
+        <el-form-item label="高度" prop="height" label-width="70px">
+          <el-slider
+            v-model="formData.height"
+            :max="200"
+            :min="20"
+            show-input
+            input-size="small"
+          />
+        </el-form-item>
       </el-card>
-      <el-card class="property-group" header="主标题" shadow="never">
-        <el-form-item label="文字" label-width="40px" prop="title">
+      <el-card header="主标题" class="property-group" shadow="never">
+        <el-form-item label="文字" prop="title" label-width="40px">
           <InputWithColor
             v-model="formData.title"
             v-model:color="formData.titleColor"
-            maxlength="20"
             show-word-limit
+            maxlength="20"
           />
         </el-form-item>
-        <el-form-item label="大小" label-width="40px" prop="titleSize">
+        <el-form-item label="大小" prop="titleSize" label-width="40px">
           <el-slider
             v-model="formData.titleSize"
             :max="60"
             :min="10"
-            input-size="small"
             show-input
+            input-size="small"
           />
         </el-form-item>
-        <el-form-item label="粗细" label-width="40px" prop="titleWeight">
+        <el-form-item label="粗细" prop="titleWeight" label-width="40px">
           <el-slider
             v-model="formData.titleWeight"
-            :max="900"
             :min="100"
+            :max="900"
             :step="100"
-            input-size="small"
             show-input
+            input-size="small"
           />
         </el-form-item>
       </el-card>
-      <el-card class="property-group" header="副标题" shadow="never">
-        <el-form-item label="文字" label-width="40px" prop="description">
+      <el-card header="副标题" class="property-group" shadow="never">
+        <el-form-item label="文字" prop="description" label-width="40px">
           <InputWithColor
             v-model="formData.description"
             v-model:color="formData.descriptionColor"
-            maxlength="50"
             show-word-limit
+            maxlength="50"
           />
         </el-form-item>
-        <el-form-item label="大小" label-width="40px" prop="descriptionSize">
+        <el-form-item label="大小" prop="descriptionSize" label-width="40px">
           <el-slider
             v-model="formData.descriptionSize"
             :max="60"
             :min="10"
-            input-size="small"
             show-input
+            input-size="small"
           />
         </el-form-item>
-        <el-form-item label="粗细" label-width="40px" prop="descriptionWeight">
+        <el-form-item label="粗细" prop="descriptionWeight" label-width="40px">
           <el-slider
             v-model="formData.descriptionWeight"
-            :max="900"
             :min="100"
+            :max="900"
             :step="100"
-            input-size="small"
             show-input
+            input-size="small"
           />
         </el-form-item>
       </el-card>
-      <el-card class="property-group" header="查看更多" shadow="never">
+      <el-card header="查看更多" class="property-group" shadow="never">
         <el-form-item label="是否显示" prop="more.show">
           <el-checkbox v-model="formData.more.show" />
         </el-form-item>
@@ -88,7 +111,7 @@
               <el-radio value="all">文字+图标</el-radio>
             </el-radio-group>
           </el-form-item>
-          <el-form-item v-show="formData.more.type !== 'icon'" label="更多文字" prop="more.text">
+          <el-form-item label="更多文字" prop="more.text" v-show="formData.more.type !== 'icon'">
             <el-input v-model="formData.more.text" />
           </el-form-item>
           <el-form-item label="跳转链接" prop="more.url">
@@ -99,7 +122,7 @@
     </el-form>
   </ComponentContainerProperty>
 </template>
-<script lang="ts" setup>
+<script setup lang="ts">
 import { TitleBarProperty } from './config'
 import { useVModel } from '@vueuse/core'
 // 导航栏属性面板
@@ -113,4 +136,4 @@ const formData = useVModel(props, 'modelValue', emit)
 const rules = {}
 </script>
 
-<style lang="scss" scoped></style>
+<style scoped lang="scss"></style>

+ 4 - 2
src/components/Draggable/index.vue

@@ -28,7 +28,7 @@
             <Icon
               icon="ep:delete"
               class="cursor-pointer text-red-5"
-              v-if="formData.length > 1"
+              v-if="formData.length > min"
               @click="handleDelete(index)"
             />
           </el-tooltip>
@@ -69,7 +69,9 @@ const props = defineProps({
   // 空的元素:点击添加按钮时,创建元素并添加到列表;默认为空对象
   emptyItem: any<unknown>().def({}),
   // 数量限制:默认为0,表示不限制
-  limit: propTypes.number.def(0)
+  limit: propTypes.number.def(0),
+  // 最小数量:默认为1
+  min: propTypes.number.def(1)
 })
 // 定义事件
 const emit = defineEmits(['update:modelValue'])

+ 8 - 1
src/components/FormCreate/src/components/useApiSelect.tsx

@@ -69,11 +69,18 @@ export const useApiSelect = (option: ApiSelectProps) => {
         if (isEmpty(props.url)) {
           return
         }
+
         switch (props.method) {
           case 'GET':
             let url: string = props.url
             if (props.remote) {
-              url = `${url}?${props.remoteField}=${queryParam.value}`
+              if (queryParam.value != undefined) {
+                if (url.includes('?')) {
+                  url = `${url}&${props.remoteField}=${queryParam.value}`
+                } else {
+                  url = `${url}?${props.remoteField}=${queryParam.value}`
+                }
+              }
             }
             parseOptions(await request.get({ url: url }))
             break

+ 1 - 0
src/components/FormCreate/src/config/useSelectRule.ts

@@ -17,6 +17,7 @@ export const useSelectRule = (option: SelectRuleOption) => {
     icon: option.icon,
     label,
     name,
+    event: option.event,
     rule() {
       return {
         type: name,

+ 2 - 1
src/components/FormCreate/src/type/index.ts

@@ -46,5 +46,6 @@ export interface SelectRuleOption {
   label: string // label 名称
   name: string // 组件名称
   icon: string // 组件图标
-  props?: any[] // 组件规则
+  props?: any[], // 组件规则
+  event?: any[] // 事件配置
 }

+ 2 - 1
src/components/FormCreate/src/useFormCreateDesigner.ts

@@ -63,7 +63,8 @@ export const useFormCreateDesigner = async (designer: Ref) => {
     name: 'ApiSelect',
     label: '接口选择器',
     icon: 'icon-server',
-    props: [...apiSelectRule]
+    props: [...apiSelectRule],
+    event: ['click', 'change', 'visibleChange', 'clear', 'blur', 'focus']
   })
 
   /**

+ 2 - 2
src/components/MagicCubeEditor/index.vue

@@ -35,13 +35,13 @@
       >
         <!-- 右上角热区删除按钮 -->
         <div
-          v-if="selectedHotAreaIndex === index"
+          v-if="selectedHotAreaIndex === index && hotArea.width && hotArea.height"
           class="btn-delete"
           @click="handleDeleteHotArea(index)"
         >
           <Icon icon="ep:circle-close-filled" />
         </div>
-        {{ `${hotArea.width}×${hotArea.height}` }}
+        <span v-if="hotArea.width">{{ `${hotArea.width}×${hotArea.height}` }}</span>
       </div>
     </table>
   </div>

+ 3 - 2
src/components/SimpleProcessDesignerV2/src/NodeHandler.vue

@@ -91,6 +91,7 @@ import {
   DEFAULT_CONDITION_GROUP_VALUE
 } from './consts'
 import { generateUUID } from '@/utils'
+import { cloneDeep } from 'lodash-es'
 
 defineOptions({
   name: 'NodeHandler'
@@ -184,7 +185,7 @@ const addNode = (type: number) => {
           conditionSetting: {
             defaultFlow: false,
             conditionType: ConditionType.RULE,
-            conditionGroups: DEFAULT_CONDITION_GROUP_VALUE
+            conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE)
           }
         },
         {
@@ -242,7 +243,7 @@ const addNode = (type: number) => {
           conditionSetting: {
             defaultFlow: false,
             conditionType: ConditionType.RULE,
-            conditionGroups: DEFAULT_CONDITION_GROUP_VALUE
+            conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE)
           }
         },
         {

+ 6 - 0
src/components/SimpleProcessDesignerV2/src/SimpleProcessDesigner.vue

@@ -59,6 +59,11 @@ const props = defineProps({
   startUserIds: {
     type: Array,
     required: false
+  },
+  // 可发起流程的部门编号
+  startDeptIds: {
+    type: Array,
+    required: false
   }
 })
 
@@ -82,6 +87,7 @@ provide('deptList', deptOptions)
 provide('userGroupList', userGroupOptions)
 provide('deptTree', deptTreeOptions)
 provide('startUserIds', props.startUserIds)
+provide('startDeptIds', props.startDeptIds)
 provide('tasks', [])
 provide('processInstance', {})
 const message = useMessage() // 国际化

+ 57 - 14
src/components/SimpleProcessDesignerV2/src/nodes-config/StartUserNodeConfig.vue

@@ -25,21 +25,46 @@
     </template>
     <el-tabs type="border-card" v-model="activeTabName">
       <el-tab-pane label="权限" name="user">
-        <el-text v-if="!startUserIds || startUserIds.length === 0"> 全部成员可以发起流程 </el-text>
-        <el-text v-else-if="startUserIds.length == 1">
-          {{ getUserNicknames(startUserIds) }} 可发起流程
-        </el-text>
-        <el-text v-else>
-          <el-tooltip
-            class="box-item"
-            effect="dark"
-            placement="top"
-            :content="getUserNicknames(startUserIds)"
-          >
-            {{ getUserNicknames(startUserIds.slice(0, 2)) }} 等
-            {{ startUserIds.length }} 人可发起流程
-          </el-tooltip>
+        <el-text
+          v-if="
+            (!startUserIds || startUserIds.length === 0) &&
+            (!startDeptIds || startDeptIds.length === 0)
+          "
+        >
+          全部成员可以发起流程
         </el-text>
+        <div v-else-if="startUserIds && startUserIds.length > 0">
+          <el-text v-if="startUserIds.length == 1">
+            {{ getUserNicknames(startUserIds) }} 可发起流程
+          </el-text>
+          <el-text v-else>
+            <el-tooltip
+              class="box-item"
+              effect="dark"
+              placement="top"
+              :content="getUserNicknames(startUserIds)"
+            >
+              {{ getUserNicknames(startUserIds.slice(0, 2)) }} 等
+              {{ startUserIds.length }} 人可发起流程
+            </el-tooltip>
+          </el-text>
+        </div>
+        <div v-else-if="startDeptIds && startDeptIds.length > 0">
+          <el-text v-if="startDeptIds.length == 1">
+            {{ getDeptNames(startDeptIds) }} 可发起流程
+          </el-text>
+          <el-text v-else>
+            <el-tooltip
+              class="box-item"
+              effect="dark"
+              placement="top"
+              :content="getDeptNames(startDeptIds)"
+            >
+              {{ getDeptNames(startDeptIds.slice(0, 2)) }} 等
+              {{ startDeptIds.length }} 个部门可发起流程
+            </el-tooltip>
+          </el-text>
+        </div>
       </el-tab-pane>
       <el-tab-pane label="表单字段权限" name="fields" v-if="formType === 10">
         <div class="field-setting-pane">
@@ -107,6 +132,7 @@
 import { SimpleFlowNode, NodeType, FieldPermissionType, START_USER_BUTTON_SETTING } from '../consts'
 import { useWatchNode, useDrawer, useNodeName, useFormFieldsPermission } from '../node'
 import * as UserApi from '@/api/system/user'
+import * as DeptApi from '@/api/system/dept'
 defineOptions({
   name: 'StartUserNodeConfig'
 })
@@ -118,8 +144,12 @@ const props = defineProps({
 })
 // 可发起流程的用户编号
 const startUserIds = inject<Ref<any[]>>('startUserIds')
+// 可发起流程的部门编号
+const startDeptIds = inject<Ref<any[]>>('startDeptIds')
 // 用户列表
 const userOptions = inject<Ref<UserApi.UserVO[]>>('userList')
+// 部门列表
+const deptOptions = inject<Ref<DeptApi.DeptVO[]>>('deptList')
 // 抽屉配置
 const { settingVisible, closeDrawer, openDrawer } = useDrawer()
 // 当前节点
@@ -145,6 +175,19 @@ const getUserNicknames = (userIds: number[]): string => {
   })
   return nicknames.join(',')
 }
+const getDeptNames = (deptIds: number[]): string => {
+  if (!deptIds || deptIds.length === 0) {
+    return ''
+  }
+  const deptNames: string[] = []
+  deptIds.forEach((deptId) => {
+    const found = deptOptions?.value.find((item) => item.id === deptId)
+    if (found && found.name) {
+      deptNames.push(found.name)
+    }
+  })
+  return deptNames.join(',')
+}
 // 保存配置
 const saveConfig = async () => {
   activeTabName.value = 'user'

+ 6 - 5
src/components/SimpleProcessDesignerV2/src/nodes-config/TriggerNodeConfig.vue

@@ -254,6 +254,7 @@ import {
 import { useWatchNode, useDrawer, useNodeName, useFormFields, getConditionShowText } from '../node'
 import HttpRequestSetting from './components/HttpRequestSetting.vue'
 import ConditionDialog from './components/ConditionDialog.vue'
+import { cloneDeep } from 'lodash-es'
 const { proxy } = getCurrentInstance() as any
 
 defineOptions({
@@ -290,7 +291,7 @@ const configForm = ref<TriggerSetting>({
   },
   formSettings: [
     {
-      conditionGroups: DEFAULT_CONDITION_GROUP_VALUE,
+      conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE),
       updateFormFields: {},
       deleteFields: []
     }
@@ -346,7 +347,7 @@ const changeTriggerType = () => {
         ? originalSetting.formSettings
         : [
             {
-              conditionGroups: DEFAULT_CONDITION_GROUP_VALUE,
+              conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE),
               updateFormFields: {},
               deleteFields: []
             }
@@ -361,7 +362,7 @@ const changeTriggerType = () => {
         ? originalSetting.formSettings
         : [
             {
-              conditionGroups: DEFAULT_CONDITION_GROUP_VALUE,
+              conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE),
               updateFormFields: undefined,
               deleteFields: []
             }
@@ -374,7 +375,7 @@ const changeTriggerType = () => {
 /** 添加新的修改表单设置 */
 const addFormSetting = () => {
   configForm.value.formSettings!.push({
-    conditionGroups: DEFAULT_CONDITION_GROUP_VALUE,
+    conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE),
     updateFormFields: {},
     deleteFields: []
   })
@@ -509,7 +510,7 @@ const showTriggerNodeConfig = (node: SimpleFlowNode) => {
       },
       formSettings: node.triggerSetting.formSettings || [
         {
-          conditionGroups: DEFAULT_CONDITION_GROUP_VALUE,
+          conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE),
           updateFormFields: {},
           deleteFields: []
         }

+ 2 - 1
src/components/SimpleProcessDesignerV2/src/nodes-config/components/Condition.vue

@@ -154,6 +154,7 @@ import {
 } from '../../consts'
 import { BpmModelFormType } from '@/utils/constants'
 import { useFormFieldsAndStartUser } from '../../node'
+import { cloneDeep } from 'lodash-es'
 
 const props = defineProps({
   modelValue: {
@@ -196,7 +197,7 @@ const formRef = ref() // 表单 Ref
 const changeConditionType = () => {
   if (condition.value.conditionType === ConditionType.RULE) {
     if (!condition.value.conditionGroups) {
-      condition.value.conditionGroups = DEFAULT_CONDITION_GROUP_VALUE
+      condition.value.conditionGroups = cloneDeep(DEFAULT_CONDITION_GROUP_VALUE)
     }
   }
 }

+ 4 - 3
src/components/SimpleProcessDesignerV2/src/nodes-config/components/ConditionDialog.vue

@@ -1,5 +1,5 @@
 <!-- TODO @jason:有可能,它里面套 Condition 么?  -->
-<!-- TODO 怕影响其它节点功能,后面看看如何如何复用 Condtion --> 
+<!-- TODO 怕影响其它节点功能,后面看看如何如何复用 Condtion -->
 <template>
   <Dialog v-model="dialogVisible" title="条件配置" width="600px" :fullscreen="false">
     <div class="h-410px">
@@ -165,6 +165,7 @@ import {
 } from '../../consts'
 import { BpmModelFormType } from '@/utils/constants'
 import { useFormFieldsAndStartUser } from '../../node'
+import { cloneDeep } from 'lodash-es'
 defineOptions({
   name: 'ConditionDialog'
 })
@@ -175,7 +176,7 @@ const condition = ref<{
   conditionGroups?: ConditionGroup
 }>({
   conditionType: ConditionType.RULE,
-  conditionGroups: DEFAULT_CONDITION_GROUP_VALUE
+  conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE)
 })
 
 const emit = defineEmits<{
@@ -210,7 +211,7 @@ const formRef = ref() // 表单 Ref
 const changeConditionType = () => {
   if (condition.value.conditionType === ConditionType.RULE) {
     if (!condition.value.conditionGroups) {
-      condition.value.conditionGroups = DEFAULT_CONDITION_GROUP_VALUE
+      condition.value.conditionGroups = cloneDeep(DEFAULT_CONDITION_GROUP_VALUE)
     }
   }
 }

+ 11 - 3
src/components/SimpleProcessDesignerV2/src/nodes/ExclusiveNode.vue

@@ -108,11 +108,18 @@
 <script setup lang="ts">
 import NodeHandler from '../NodeHandler.vue'
 import ProcessNodeTree from '../ProcessNodeTree.vue'
-import { SimpleFlowNode, NodeType, ConditionType, DEFAULT_CONDITION_GROUP_VALUE, NODE_DEFAULT_TEXT } from '../consts'
+import {
+  SimpleFlowNode,
+  NodeType,
+  ConditionType,
+  DEFAULT_CONDITION_GROUP_VALUE,
+  NODE_DEFAULT_TEXT
+} from '../consts'
 import { getDefaultConditionNodeName } from '../utils'
 import { useTaskStatusClass } from '../node'
 import { generateUUID } from '@/utils'
 import ConditionNodeConfig from '../nodes-config/ConditionNodeConfig.vue'
+import { cloneDeep } from 'lodash-es'
 const { proxy } = getCurrentInstance() as any
 defineOptions({
   name: 'ExclusiveNode'
@@ -149,7 +156,8 @@ const blurEvent = (index: number) => {
   showInputs.value[index] = false
   const conditionNode = currentNode.value.conditionNodes?.at(index) as SimpleFlowNode
   conditionNode.name =
-    conditionNode.name || getDefaultConditionNodeName(index, conditionNode.conditionSetting?.defaultFlow)
+    conditionNode.name ||
+    getDefaultConditionNodeName(index, conditionNode.conditionSetting?.defaultFlow)
 }
 
 // 点击条件名称
@@ -181,7 +189,7 @@ const addCondition = () => {
       conditionSetting: {
         defaultFlow: false,
         conditionType: ConditionType.RULE,
-        conditionGroups: DEFAULT_CONDITION_GROUP_VALUE
+        conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE)
       }
     }
     conditionNodes.splice(lastIndex, 0, conditionData)

+ 11 - 3
src/components/SimpleProcessDesignerV2/src/nodes/InclusiveNode.vue

@@ -110,11 +110,18 @@
 <script setup lang="ts">
 import NodeHandler from '../NodeHandler.vue'
 import ProcessNodeTree from '../ProcessNodeTree.vue'
-import { SimpleFlowNode, NodeType, ConditionType, DEFAULT_CONDITION_GROUP_VALUE, NODE_DEFAULT_TEXT } from '../consts'
+import {
+  SimpleFlowNode,
+  NodeType,
+  ConditionType,
+  DEFAULT_CONDITION_GROUP_VALUE,
+  NODE_DEFAULT_TEXT
+} from '../consts'
 import { useTaskStatusClass } from '../node'
 import { getDefaultInclusiveConditionNodeName } from '../utils'
 import { generateUUID } from '@/utils'
 import ConditionNodeConfig from '../nodes-config/ConditionNodeConfig.vue'
+import { cloneDeep } from 'lodash-es'
 const { proxy } = getCurrentInstance() as any
 defineOptions({
   name: 'InclusiveNode'
@@ -153,7 +160,8 @@ const blurEvent = (index: number) => {
   showInputs.value[index] = false
   const conditionNode = currentNode.value.conditionNodes?.at(index) as SimpleFlowNode
   conditionNode.name =
-    conditionNode.name || getDefaultInclusiveConditionNodeName(index, conditionNode.conditionSetting?.defaultFlow)
+    conditionNode.name ||
+    getDefaultInclusiveConditionNodeName(index, conditionNode.conditionSetting?.defaultFlow)
 }
 
 // 点击条件名称
@@ -185,7 +193,7 @@ const addCondition = () => {
       conditionSetting: {
         defaultFlow: false,
         conditionType: ConditionType.RULE,
-        conditionGroups: DEFAULT_CONDITION_GROUP_VALUE
+        conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE)
       }
     }
     conditionNodes.splice(lastIndex, 0, conditionData)

+ 63 - 0
src/components/Tinyflow/Tinyflow.vue

@@ -0,0 +1,63 @@
+<template>
+  <div ref="divRef" :class="['tinyflow', className]" :style="style" style="height: 100%"> </div>
+</template>
+
+<script setup lang="ts">
+import { Item, Tinyflow as TinyflowNative } from './ui'
+import './ui/index.css'
+import { onMounted, onUnmounted, ref } from 'vue'
+
+const props = defineProps<{
+  className?: string
+  style?: Record<string, string>
+  data?: Record<string, any>
+  provider?: {
+    llm?: () => Item[] | Promise<Item[]>
+    knowledge?: () => Item[] | Promise<Item[]>
+    internal?: () => Item[] | Promise<Item[]>
+  }
+}>()
+
+const divRef = ref<HTMLDivElement | null>(null)
+let tinyflow: TinyflowNative | null = null
+// 定义默认的 provider 方法
+const defaultProvider = {
+  llm: () => [] as Item[],
+  knowledge: () => [] as Item[],
+  internal: () => [] as Item[]
+}
+
+onMounted(() => {
+  if (divRef.value) {
+    // 合并默认 provider 和传入的 props.provider
+    const mergedProvider = {
+      ...defaultProvider,
+      ...props.provider
+    }
+    tinyflow = new TinyflowNative({
+      element: divRef.value as Element,
+      data: props.data || {},
+      provider: mergedProvider
+    })
+  }
+})
+
+onUnmounted(() => {
+  if (tinyflow) {
+    tinyflow.destroy()
+    tinyflow = null
+  }
+})
+
+const getData = () => {
+  if (tinyflow) {
+    return tinyflow.getData()
+  }
+  console.warn('Tinyflow instance is not initialized')
+  return null
+}
+
+defineExpose({
+  getData
+})
+</script>

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
src/components/Tinyflow/ui/index.css


+ 41 - 0
src/components/Tinyflow/ui/index.d.ts

@@ -0,0 +1,41 @@
+import { Edge } from '@xyflow/svelte';
+import { Node as Node_2 } from '@xyflow/svelte';
+import { useSvelteFlow } from '@xyflow/svelte';
+import { Viewport } from '@xyflow/svelte';
+
+export declare type Item = {
+    value: number | string;
+    label: string;
+    children?: Item[];
+};
+
+export declare class Tinyflow {
+    private options;
+    private rootEl;
+    private svelteFlowInstance;
+    constructor(options: TinyflowOptions);
+    private _init;
+    private _setOptions;
+    getOptions(): TinyflowOptions;
+    getData(): {
+        nodes: Node_2[];
+        edges: Edge[];
+        viewport: Viewport;
+    };
+    setData(data: TinyflowData): void;
+    destroy(): void;
+}
+
+export declare type TinyflowData = Partial<ReturnType<ReturnType<typeof useSvelteFlow>['toObject']>>;
+
+export declare type TinyflowOptions = {
+    element: string | Element;
+    data?: TinyflowData;
+    provider?: {
+        llm?: () => Item[] | Promise<Item[]>;
+        knowledge?: () => Item[] | Promise<Item[]>;
+        internal?: () => Item[] | Promise<Item[]>;
+    };
+};
+
+export { }

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 10206 - 0
src/components/Tinyflow/ui/index.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
src/components/Tinyflow/ui/index.umd.js


+ 3 - 2
src/components/UploadFile/src/UploadFile.vue

@@ -86,7 +86,8 @@ const props = defineProps({
   autoUpload: propTypes.bool.def(true), // 自动上传
   drag: propTypes.bool.def(false), // 拖拽上传
   isShowTip: propTypes.bool.def(true), // 是否显示提示
-  disabled: propTypes.bool.def(false) // 是否禁用上传组件 ==> 非必传(默认为 false)
+  disabled: propTypes.bool.def(false), // 是否禁用上传组件 ==> 非必传(默认为 false)
+  directory: propTypes.string.def(undefined) // 上传目录 ==> 非必传(默认为 undefined)
 })
 
 // ========== 上传相关 ==========
@@ -95,7 +96,7 @@ const uploadList = ref<UploadUserFile[]>([])
 const fileList = ref<UploadUserFile[]>([])
 const uploadNumber = ref<number>(0)
 
-const { uploadUrl, httpRequest } = useUpload()
+const { uploadUrl, httpRequest } = useUpload(props.directory)
 
 // 文件上传之前判断
 const beforeUpload: UploadProps['beforeUpload'] = (file: UploadRawFile) => {

+ 3 - 2
src/components/UploadFile/src/UploadImg.vue

@@ -79,7 +79,8 @@ const props = defineProps({
   width: propTypes.string.def('150px'), // 组件宽度 ==> 非必传(默认为 150px)
   borderradius: propTypes.string.def('8px'), // 组件边框圆角 ==> 非必传(默认为 8px)
   showDelete: propTypes.bool.def(true), // 是否显示删除按钮
-  showBtnText: propTypes.bool.def(true) // 是否显示按钮文字
+  showBtnText: propTypes.bool.def(true), // 是否显示按钮文字
+  directory: propTypes.string.def(undefined) // 上传目录 ==> 非必传(默认为 undefined)
 })
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
@@ -99,7 +100,7 @@ const deleteImg = () => {
   emit('update:modelValue', '')
 }
 
-const { uploadUrl, httpRequest } = useUpload()
+const { uploadUrl, httpRequest } = useUpload(props.directory)
 
 const editImg = () => {
   const dom = document.querySelector(`#${uuid.value} .el-upload__input`)

+ 3 - 2
src/components/UploadFile/src/UploadImgs.vue

@@ -81,10 +81,11 @@ const props = defineProps({
   fileType: propTypes.array.def(['image/jpeg', 'image/png', 'image/gif']), // 图片类型限制 ==> 非必传(默认为 ["image/jpeg", "image/png", "image/gif"])
   height: propTypes.string.def('150px'), // 组件高度 ==> 非必传(默认为 150px)
   width: propTypes.string.def('150px'), // 组件宽度 ==> 非必传(默认为 150px)
-  borderradius: propTypes.string.def('8px') // 组件边框圆角 ==> 非必传(默认为 8px)
+  borderradius: propTypes.string.def('8px'), // 组件边框圆角 ==> 非必传(默认为 8px)
+  directory: propTypes.string.def(undefined) // 上传目录 ==> 非必传(默认为 undefined)
 })
 
-const { uploadUrl, httpRequest } = useUpload()
+const { uploadUrl, httpRequest } = useUpload(props.directory)
 
 const fileList = ref<UploadUserFile[]>([])
 const uploadNumber = ref<number>(0)

+ 16 - 15
src/components/UploadFile/src/useUpload.ts

@@ -1,5 +1,5 @@
 import * as FileApi from '@/api/infra/file'
-import CryptoJS from 'crypto-js'
+// import CryptoJS from 'crypto-js'
 import { UploadRawFile, UploadRequestOptions } from 'element-plus/es/components/upload/src/upload'
 import axios from 'axios'
 
@@ -10,7 +10,7 @@ export const getUploadUrl = (): string => {
   return import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL + '/infra/file/upload'
 }
 
-export const useUpload = () => {
+export const useUpload = (directory?: string) => {
   // 后端上传地址
   const uploadUrl = getUploadUrl()
   // 是否使用前端直连上传
@@ -22,7 +22,7 @@ export const useUpload = () => {
       // 1.1 生成文件名称
       const fileName = await generateFileName(options.file)
       // 1.2 获取文件预签名地址
-      const presignedInfo = await FileApi.getFilePresignedUrl(fileName)
+      const presignedInfo = await FileApi.getFilePresignedUrl(fileName, directory)
       // 1.3 上传文件(不能使用 ElUpload 的 ajaxUpload 方法的原因:其使用的是 FormData 上传,Minio 不支持)
       return axios
         .put(presignedInfo.uploadUrl, options.file, {
@@ -32,7 +32,7 @@ export const useUpload = () => {
         })
         .then(() => {
           // 1.4. 记录文件信息到后端(异步)
-          createFile(presignedInfo, fileName, options.file)
+          createFile(presignedInfo, options.file)
           // 通知成功,数据格式保持与后端上传的返回结果一致
           return { data: presignedInfo.url }
         })
@@ -40,7 +40,7 @@ export const useUpload = () => {
       // 模式二:后端上传
       // 重写 el-upload httpRequest 文件上传成功会走成功的钩子,失败走失败的钩子
       return new Promise((resolve, reject) => {
-        FileApi.updateFile({ file: options.file })
+        FileApi.updateFile({ file: options.file, directory })
           .then((res) => {
             if (res.code === 0) {
               resolve(res)
@@ -67,11 +67,11 @@ export const useUpload = () => {
  * @param name 文件名称
  * @param file 文件
  */
-function createFile(vo: FileApi.FilePresignedUrlRespVO, name: string, file: UploadRawFile) {
+function createFile(vo: FileApi.FilePresignedUrlRespVO, file: UploadRawFile) {
   const fileVo = {
     configId: vo.configId,
     url: vo.url,
-    path: name,
+    path: vo.path,
     name: file.name,
     type: file.type,
     size: file.size
@@ -85,14 +85,15 @@ function createFile(vo: FileApi.FilePresignedUrlRespVO, name: string, file: Uplo
  * @param file 要上传的文件
  */
 async function generateFileName(file: UploadRawFile) {
-  // 读取文件内容
-  const data = await file.arrayBuffer()
-  const wordArray = CryptoJS.lib.WordArray.create(data)
-  // 计算SHA256
-  const sha256 = CryptoJS.SHA256(wordArray).toString()
-  // 拼接后缀
-  const ext = file.name.substring(file.name.lastIndexOf('.'))
-  return `${sha256}${ext}`
+  // // 读取文件内容
+  // const data = await file.arrayBuffer()
+  // const wordArray = CryptoJS.lib.WordArray.create(data)
+  // // 计算SHA256
+  // const sha256 = CryptoJS.SHA256(wordArray).toString()
+  // // 拼接后缀
+  // const ext = file.name.substring(file.name.lastIndexOf('.'))
+  // return `${sha256}${ext}`
+  return file.name
 }
 
 /**

+ 2 - 1
src/components/bpmnProcessDesigner/package/penal/form/ElementForm.vue

@@ -237,7 +237,7 @@ const props = defineProps({
 const prefix = inject('prefix')
 const width = inject('width')
 
-const formKey = ref('')
+const formKey = ref(undefined)
 const businessKey = ref('')
 const optionModelTitle = ref('')
 const fieldList = ref<any[]>([])
@@ -462,6 +462,7 @@ const updateElementExtensions = () => {
 const formList = ref([]) // 流程表单的下拉框的数据
 onMounted(async () => {
   formList.value = await FormApi.getFormSimpleList()
+  formKey.value = parseInt(formKey.value)
 })
 
 watch(

+ 0 - 1
src/components/bpmnProcessDesigner/package/penal/listeners/ElementListeners.vue

@@ -370,7 +370,6 @@ const removeListenerField = (index) => {
 }
 // 移除监听器
 const removeListener = (index) => {
-  debugger
   ElMessageBox.confirm('确认移除该监听器吗?', '提示', {
     confirmButtonText: '确 认',
     cancelButtonText: '取 消'

+ 0 - 1
src/components/bpmnProcessDesigner/package/utils.ts

@@ -2,7 +2,6 @@ import { toRaw } from 'vue'
 const bpmnInstances = () => (window as any)?.bpmnInstances
 // 创建监听器实例
 export function createListenerObject(options, isTask, prefix) {
-  debugger
   const listenerObj = Object.create(null)
   listenerObj.event = options.event
   isTask && (listenerObj.id = options.id) // 任务监听器特有的 id 字段

+ 18 - 2
src/config/axios/service.ts

@@ -3,7 +3,14 @@ import axios, { AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestCo
 import { ElMessage, ElMessageBox, ElNotification } from 'element-plus'
 import qs from 'qs'
 import { config } from '@/config/axios/config'
-import { getAccessToken, getRefreshToken, getTenantId, removeToken, setToken } from '@/utils/auth'
+import {
+  getAccessToken,
+  getRefreshToken,
+  getTenantId,
+  getVisitTenantId,
+  removeToken,
+  setToken
+} from '@/utils/auth'
 import errorCode from './errorCode'
 
 import { resetRouter } from '@/router'
@@ -24,7 +31,7 @@ export const isRelogin = { show: false }
 let requestList: any[] = []
 // 是否正在刷新中
 let isRefreshToken = false
-// 请求白名单,无须token的接口
+// 请求白名单,无须 token 的接口
 const whiteList: string[] = ['/login', '/refresh-token']
 
 // 创建axios实例
@@ -55,6 +62,11 @@ service.interceptors.request.use(
     if (tenantEnable && tenantEnable === 'true') {
       const tenantId = getTenantId()
       if (tenantId) config.headers['tenant-id'] = tenantId
+      // 只有登录时,才设置 visit-tenant-id 访问租户
+      const visitTenantId = getVisitTenantId()
+      if (config.headers.Authorization && visitTenantId) {
+        config.headers['visit-tenant-id'] = visitTenantId
+      }
     }
     const method = config.method?.toUpperCase()
     // 防止 GET 请求缓存
@@ -201,6 +213,10 @@ const refreshToken = async () => {
 const handleAuthorized = () => {
   const { t } = useI18n()
   if (!isRelogin.show) {
+    // 如果已经到登录页面则不进行弹窗提示
+    if (window.location.href.includes('login')) {
+      return
+    }
     isRelogin.show = true
     ElMessageBox.confirm(t('sys.api.timeoutMessage'), t('common.confirmTitle'), {
       showCancelButton: false,

+ 2 - 0
src/hooks/web/useCache.ts

@@ -10,6 +10,7 @@ export const CACHE_KEY = {
   // 用户相关
   ROLE_ROUTERS: 'roleRouters',
   USER: 'user',
+  VisitTenantId: 'visitTenantId',
   // 系统设置
   IS_DARK: 'isDark',
   LANG: 'lang',
@@ -35,5 +36,6 @@ export const deleteUserCache = () => {
   const { wsCache } = useCache()
   wsCache.delete(CACHE_KEY.USER)
   wsCache.delete(CACHE_KEY.ROLE_ROUTERS)
+  wsCache.delete(CACHE_KEY.VisitTenantId)
   // 注意,不要清理 LoginForm 登录表单
 }

+ 18 - 1
src/hooks/web/useWatermark.ts

@@ -1,8 +1,14 @@
+import { useAppStore } from '@/store/modules/app'
+import { watch } from 'vue'
+
 const domSymbol = Symbol('watermark-dom')
 
 export function useWatermark(appendEl: HTMLElement | null = document.body) {
   let func: Fn = () => {}
   const id = domSymbol.toString()
+  const appStore = useAppStore()
+  let watermarkStr = ''
+  
   const clear = () => {
     const domId = document.getElementById(id)
     if (domId) {
@@ -22,7 +28,7 @@ export function useWatermark(appendEl: HTMLElement | null = document.body) {
     if (cans) {
       cans.rotate((-20 * Math.PI) / 120)
       cans.font = '15px Vedana'
-      cans.fillStyle = 'rgba(0, 0, 0, 0.15)'
+      cans.fillStyle = appStore.getIsDark ? 'rgba(255, 255, 255, 0.15)' : 'rgba(0, 0, 0, 0.15)'
       cans.textAlign = 'left'
       cans.textBaseline = 'middle'
       cans.fillText(str, can.width / 20, can.height)
@@ -44,6 +50,7 @@ export function useWatermark(appendEl: HTMLElement | null = document.body) {
   }
 
   function setWatermark(str: string) {
+    watermarkStr = str
     createWatermark(str)
     func = () => {
       createWatermark(str)
@@ -51,5 +58,15 @@ export function useWatermark(appendEl: HTMLElement | null = document.body) {
     window.addEventListener('resize', func)
   }
 
+  // 监听主题变化
+  watch(
+    () => appStore.getIsDark,
+    () => {
+      if (watermarkStr) {
+        createWatermark(watermarkStr)
+      }
+    }
+  )
+
   return { setWatermark, clear }
 }

+ 46 - 0
src/layout/components/TenantVisit/index.vue

@@ -0,0 +1,46 @@
+<template>
+  <div>
+    <el-select
+      filterable
+      placeholder="请选择租户"
+      class="!w-180px"
+      v-model="value"
+      @change="handleChange"
+      clearable
+    >
+      <el-option v-for="item in tenants" :key="item.id" :label="item.name" :value="item.id" />
+    </el-select>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, onMounted } from 'vue'
+import * as TenantApi from '@/api/system/tenant'
+import { getVisitTenantId, setVisitTenantId } from '@/utils/auth'
+import { useMessage } from '@/hooks/web/useMessage'
+import { useTagsView } from '@/hooks/web/useTagsView'
+
+const message = useMessage() // 消息弹窗
+const tagsView = useTagsView() // 标签页操作
+
+const value = ref(getVisitTenantId()) // 当前选中的租户 ID
+const tenants = ref<any[]>([]) // 租户列表
+
+const handleChange = (id: number) => {
+  // 设置访问租户 ID
+  setVisitTenantId(id)
+  // 关闭其他标签页,只保留当前页
+  tagsView.closeOther()
+  // 刷新当前页面
+  tagsView.refreshPage()
+  // 提示切换成功
+  const tenant = tenants.value.find((item) => item.id === id)
+  if (tenant) {
+    message.success(`切换当前租户为: ${tenant.name}`)
+  }
+}
+
+onMounted(async () => {
+  tenants.value = await TenantApi.getTenantList()
+})
+</script>

+ 8 - 0
src/layout/components/ToolHeader.vue

@@ -8,8 +8,10 @@ import { Breadcrumb } from '@/layout/components/Breadcrumb'
 import { SizeDropdown } from '@/layout/components/SizeDropdown'
 import { LocaleDropdown } from '@/layout/components/LocaleDropdown'
 import RouterSearch from '@/components/RouterSearch/index.vue'
+import TenantVisit from '@/layout/components/TenantVisit/index.vue'
 import { useAppStore } from '@/store/modules/app'
 import { useDesign } from '@/hooks/web/useDesign'
+import { checkPermi } from '@/utils/permission'
 
 const { getPrefixCls, variables } = useDesign()
 
@@ -41,6 +43,11 @@ const locale = computed(() => appStore.getLocale)
 // 消息图标
 const message = computed(() => appStore.getMessage)
 
+// 租户切换权限
+const hasTenantVisitPermission = computed(
+  () => import.meta.env.VITE_APP_TENANT_ENABLE === 'true' && checkPermi(['system:tenant:visit'])
+)
+
 export default defineComponent({
   name: 'ToolHeader',
   setup() {
@@ -62,6 +69,7 @@ export default defineComponent({
           </div>
         ) : undefined}
         <div class="h-full flex items-center">
+          {hasTenantVisitPermission.value ? <TenantVisit /> : undefined}
           {screenfull.value ? (
             <Screenfull class="custom-hover" color="var(--top-header-text-color)"></Screenfull>
           ) : undefined}

+ 6 - 6
src/locales/zh-CN.ts

@@ -142,9 +142,9 @@ export default {
     qrcode: '扫描二维码登录',
     btnRegister: '注册',
     SmsSendMsg: '验证码已发送',
-    resetPassword: "重置密码",
-    resetPasswordSuccess: "重置密码成功",
-    invalidTenantName: "无效的租户名称"
+    resetPassword: '重置密码',
+    resetPasswordSuccess: '重置密码成功',
+    invalidTenantName: '无效的租户名称'
   },
   captcha: {
     verification: '请完成安全验证',
@@ -416,9 +416,9 @@ export default {
     },
     info: {
       title: '基本信息',
-      basicInfo: '基本资料',
-      resetPwd: '修改密码',
-      userSocial: '社交信息'
+      basicInfo: '基本设置',
+      resetPwd: '密码设置',
+      userSocial: '社交绑定'
     },
     rules: {
       nickname: '请输入用户昵称',

+ 28 - 4
src/router/modules/remaining.ts

@@ -476,9 +476,9 @@ const remainingRouter: AppRouteRecordRaw[] = [
         name: 'DiyTemplateDecorate',
         meta: {
           title: '模板装修',
-          noCache: true,
+          noCache: false,
           hidden: true,
-          activeMenu: '/mall/promotion/diy/template'
+          activeMenu: '/mall/promotion/diy-template/diy-template'
         },
         component: () => import('@/views/mall/promotion/diy/template/decorate.vue')
       },
@@ -487,9 +487,9 @@ const remainingRouter: AppRouteRecordRaw[] = [
         name: 'DiyPageDecorate',
         meta: {
           title: '页面装修',
-          noCache: true,
+          noCache: false,
           hidden: true,
-          activeMenu: '/mall/promotion/diy/page'
+          activeMenu: '/mall/promotion/diy-template/diy-page'
         },
         component: () => import('@/views/mall/promotion/diy/page/decorate.vue')
       }
@@ -667,6 +667,30 @@ const remainingRouter: AppRouteRecordRaw[] = [
           hidden: true,
           activeMenu: '/ai/knowledge'
         }
+      },
+      {
+        path: 'console/workflow/create',
+        component: () => import('@/views/ai/workflow/form/index.vue'),
+        name: 'AiWorkflowCreate',
+        meta: {
+          noCache: true,
+          hidden: true,
+          canTo: true,
+          title: '设计 AI 工作流',
+          activeMenu: '/ai/console/workflow'
+        }
+      },
+      {
+        path: 'console/workflow/:type/:id',
+        component: () => import('@/views/ai/workflow/form/index.vue'),
+        name: 'AiWorkflowUpdate',
+        meta: {
+          noCache: true,
+          hidden: true,
+          canTo: true,
+          title: '设计 AI 工作流',
+          activeMenu: '/ai/console/workflow'
+        }
       }
     ]
   },

+ 5 - 0
src/store/modules/user.ts

@@ -56,6 +56,11 @@ export const useUserStore = defineStore('admin-user', {
       let userInfo = wsCache.get(CACHE_KEY.USER)
       if (!userInfo) {
         userInfo = await getInfo()
+      } else {
+        // 特殊:在有缓存的情况下,进行加载。但是即使加载失败,也不影响后续的操作,保证可以进入系统
+        try {
+          userInfo = await getInfo()
+        } catch (error) {}
       }
       this.permissions = new Set(userInfo.permissions)
       this.roles = userInfo.roles

+ 10 - 2
src/utils/auth.ts

@@ -67,6 +67,14 @@ export const getTenantId = () => {
   return wsCache.get(CACHE_KEY.TenantId)
 }
 
-export const setTenantId = (username: string) => {
-  wsCache.set(CACHE_KEY.TenantId, username)
+export const setTenantId = (tenantId: number) => {
+  wsCache.set(CACHE_KEY.TenantId, tenantId)
+}
+
+export const getVisitTenantId = () => {
+  return wsCache.get(CACHE_KEY.VisitTenantId)
+}
+
+export const setVisitTenantId = (visitTenantId: number) => {
+  wsCache.set(CACHE_KEY.VisitTenantId, visitTenantId)
 }

+ 2 - 2
src/utils/constants.ts

@@ -71,7 +71,7 @@ export const SystemUserSocialTypeEnum = {
 export const InfraCodegenTemplateTypeEnum = {
   CRUD: 1, // 基础 CRUD
   TREE: 2, // 树形 CRUD
-  SUB: 3 // 主子表 CRUD
+  SUB: 15 // 主子表 CRUD
 }
 
 /**
@@ -461,5 +461,5 @@ export const BpmProcessInstanceStatus = {
 export const BpmAutoApproveType = {
   NONE: 0, // 不自动通过
   APPROVE_ALL: 1, // 仅审批一次,后续重复的审批节点均自动通过
-  APPROVE_SEQUENT: 2, // 仅针对连续审批的节点自动通过
+  APPROVE_SEQUENT: 2 // 仅针对连续审批的节点自动通过
 }

+ 0 - 1
src/utils/dict.ts

@@ -159,7 +159,6 @@ export enum DICT_TYPE {
   PAY_NOTIFY_STATUS = 'pay_notify_status', // 商户支付回调状态
   PAY_NOTIFY_TYPE = 'pay_notify_type', // 商户支付回调状态
   PAY_TRANSFER_STATUS = 'pay_transfer_status', // 转账订单状态
-  PAY_TRANSFER_TYPE = 'pay_transfer_type', // 转账订单状态
 
   // ========== MP 模块 ==========
   MP_AUTO_REPLY_REQUEST_MATCH = 'mp_auto_reply_request_match', // 自动回复请求匹配类型

+ 2 - 2
src/utils/index.ts

@@ -517,8 +517,8 @@ export function jsonParse(str: string) {
   try {
     return JSON.parse(str)
   } catch (e) {
-    console.log(`str[${str}] 不是一个 JSON 字符串`)
-    return ''
+    console.warn(`str[${str}] 不是一个 JSON 字符串`)
+    return str
   }
 }
 

+ 91 - 60
src/views/Home/Index.vue

@@ -83,12 +83,21 @@
               :sm="24"
               :xs="24"
             >
-              <el-card shadow="hover" class="mr-5px mt-5px">
+              <el-card
+                shadow="hover"
+                class="mr-5px mt-5px cursor-pointer"
+                @click="handleProjectClick(item.message)"
+              >
                 <div class="flex items-center">
-                  <Icon :icon="item.icon" :size="25" class="mr-8px" />
+                  <Icon
+                    :icon="item.icon"
+                    :size="25"
+                    class="mr-8px"
+                    :style="{ color: item.color }"
+                  />
                   <span class="text-16px">{{ item.name }}</span>
                 </div>
-                <div class="mt-12px text-9px text-gray-400">{{ t(item.message) }}</div>
+                <div class="mt-12px text-12px text-gray-400">{{ t(item.message) }}</div>
                 <div class="mt-12px flex justify-between text-12px text-gray-400">
                   <span>{{ item.personal }}</span>
                   <span>{{ formatTime(item.time, 'yyyy-MM-dd') }}</span>
@@ -131,8 +140,8 @@
           <el-row>
             <el-col v-for="item in shortcut" :key="`team-${item.name}`" :span="8" class="mb-8px">
               <div class="flex items-center">
-                <Icon :icon="item.icon" class="mr-8px" />
-                <el-link type="default" :underline="false" @click="setWatermark(item.name)">
+                <Icon :icon="item.icon" class="mr-8px" :style="{ color: item.color }" />
+                <el-link type="default" :underline="false" @click="handleShortcutClick(item.url)">
                   {{ item.name }}
                 </el-link>
               </div>
@@ -177,15 +186,17 @@ import { EChartsOption } from 'echarts'
 import { formatTime } from '@/utils'
 
 import { useUserStore } from '@/store/modules/user'
-import { useWatermark } from '@/hooks/web/useWatermark'
+// import { useWatermark } from '@/hooks/web/useWatermark'
 import type { WorkplaceTotal, Project, Notice, Shortcut } from './types'
 import { pieOptions, barOptions } from './echarts-data'
+import { useRouter } from 'vue-router'
 
-defineOptions({ name: 'Home' })
+defineOptions({ name: 'Index' })
 
 const { t } = useI18n()
+const router = useRouter()
 const userStore = useUserStore()
-const { setWatermark } = useWatermark()
+// const { setWatermark } = useWatermark()
 const loading = ref(true)
 const avatar = userStore.getUser.avatar
 const username = userStore.getUser.nickname
@@ -212,45 +223,51 @@ const getProject = async () => {
   const data = [
     {
       name: 'ruoyi-vue-pro',
-      icon: 'akar-icons:github-fill',
-      message: 'https://github.com/YunaiV/ruoyi-vue-pro',
+      icon: 'simple-icons:springboot',
+      message: 'github.com/YunaiV/ruoyi-vue-pro',
       personal: 'Spring Boot 单体架构',
-      time: new Date()
+      time: new Date('2025-01-02'),
+      color: '#6DB33F'
     },
     {
       name: 'yudao-ui-admin-vue3',
-      icon: 'logos:vue',
-      message: 'https://github.com/yudaocode/yudao-ui-admin-vue3',
-      personal: 'Vue3 + element-plus',
-      time: new Date()
+      icon: 'ep:element-plus',
+      message: 'github.com/yudaocode/yudao-ui-admin-vue3',
+      personal: 'Vue3 + element-plus 管理后台',
+      time: new Date('2025-02-03'),
+      color: '#409EFF'
     },
     {
-      name: 'yudao-ui-admin-vben',
-      icon: 'logos:vue',
-      message: 'https://github.com/yudaocode/yudao-ui-admin-vben',
-      personal: 'Vue3 + vben(antd)',
-      time: new Date()
+      name: 'yudao-ui-mall-uniapp',
+      icon: 'icon-park-outline:mall-bag',
+      message: 'github.com/yudaocode/yudao-ui-mall-uniapp',
+      personal: 'Vue3 + uniapp 商城手机端',
+      time: new Date('2025-03-04'),
+      color: '#ff4d4f'
     },
     {
       name: 'yudao-cloud',
-      icon: 'akar-icons:github',
-      message: 'https://github.com/YunaiV/yudao-cloud',
+      icon: 'material-symbols:cloud-outline',
+      message: 'github.com/YunaiV/yudao-cloud',
       personal: 'Spring Cloud 微服务架构',
-      time: new Date()
+      time: new Date('2025-04-05'),
+      color: '#1890ff'
     },
     {
-      name: 'yudao-ui-mall-uniapp',
-      icon: 'logos:vue',
-      message: 'https://github.com/yudaocode/yudao-ui-admin-uniapp',
-      personal: 'Vue3 + uniapp',
-      time: new Date()
+      name: 'yudao-ui-admin-vben',
+      icon: 'devicon:antdesign',
+      message: 'github.com/yudaocode/yudao-ui-admin-vben',
+      personal: 'Vue3 + vben5(antd) 管理后台',
+      time: new Date('2025-05-06'),
+      color: '#e18525'
     },
     {
-      name: 'yudao-ui-admin-vue2',
-      icon: 'logos:vue',
-      message: 'https://github.com/yudaocode/yudao-ui-admin-vue2',
-      personal: 'Vue2 + element-ui',
-      time: new Date()
+      name: 'yudao-ui-admin-uniapp',
+      icon: 'ant-design:mobile',
+      message: 'github.com/yudaocode/yudao-ui-admin-uniapp',
+      personal: 'Vue3 + uniapp 管理手机端',
+      time: new Date('2025-06-01'),
+      color: '#2979ff'
     }
   ]
   projects = Object.assign(projects, data)
@@ -262,26 +279,26 @@ const getNotice = async () => {
   const data = [
     {
       title: '系统支持 JDK 8/17/21,Vue 2/3',
-      type: '通知',
-      keys: ['通知', '8', '17', '21', '2', '3'],
+      type: '技术兼容性',
+      keys: ['JDK', 'Vue'],
       date: new Date()
     },
     {
       title: '后端提供 Spring Boot 2.7/3.2 + Cloud 双架构',
-      type: '公告',
-      keys: ['公告', 'Boot', 'Cloud'],
+      type: '架构灵活性',
+      keys: ['Boot', 'Cloud'],
       date: new Date()
     },
     {
       title: '全部开源,个人与企业可 100% 直接使用,无需授权',
-      type: '通知',
-      keys: ['通知', '无需授权'],
+      type: '开源免授权',
+      keys: ['无需授权'],
       date: new Date()
     },
     {
-      title: '国内使用最广泛的快速开发平台,超 300+ 人贡献',
-      type: '公告',
-      keys: ['公告', '最广泛'],
+      title: '国内使用最广泛的快速开发平台,远超 10w+ 企业使用',
+      type: '广泛企业认可',
+      keys: ['最广泛', '10w+'],
       date: new Date()
     }
   ]
@@ -294,34 +311,40 @@ let shortcut = reactive<Shortcut[]>([])
 const getShortcut = async () => {
   const data = [
     {
-      name: 'Github',
-      icon: 'akar-icons:github-fill',
-      url: 'github.io'
+      name: '首页',
+      icon: 'ion:home-outline',
+      url: '/',
+      color: '#1fdaca'
     },
     {
-      name: 'Vue',
-      icon: 'logos:vue',
-      url: 'vuejs.org'
+      name: '商城中心',
+      icon: 'ep:shop',
+      url: '/mall/home',
+      color: '#ff6b6b'
     },
     {
-      name: 'Vite',
-      icon: 'vscode-icons:file-type-vite',
-      url: 'https://vitejs.dev/'
+      name: 'AI 大模型',
+      icon: 'tabler:ai',
+      url: '/ai/chat',
+      color: '#7c3aed'
     },
     {
-      name: 'Angular',
-      icon: 'logos:angular-icon',
-      url: 'github.io'
+      name: 'ERP 系统',
+      icon: 'simple-icons:erpnext',
+      url: '/erp/home',
+      color: '#3fb27f'
     },
     {
-      name: 'React',
-      icon: 'logos:react',
-      url: 'github.io'
+      name: 'CRM 系统',
+      icon: 'simple-icons:civicrm',
+      url: '/crm/backlog',
+      color: '#4daf1bc9'
     },
     {
-      name: 'Webpack',
-      icon: 'logos:webpack',
-      url: 'github.io'
+      name: 'IoT 物联网',
+      icon: 'fa-solid:hdd',
+      url: '/iot/home',
+      color: '#1a73e8'
     }
   ]
   shortcut = Object.assign(shortcut, data)
@@ -387,5 +410,13 @@ const getAllApi = async () => {
   loading.value = false
 }
 
+const handleProjectClick = (message: string) => {
+  window.open(`https://${message}`, '_blank')
+}
+
+const handleShortcutClick = (url: string) => {
+  router.push(url)
+}
+
 getAllApi()
 </script>

+ 2 - 0
src/views/Home/types.ts

@@ -10,6 +10,7 @@ export type Project = {
   message: string
   personal: string
   time: Date | number | string
+  color: string
 }
 
 export type Notice = {
@@ -23,6 +24,7 @@ export type Shortcut = {
   name: string
   icon: string
   url: string
+  color: string
 }
 
 export type RadarData = {

+ 2 - 2
src/views/Login/components/LoginForm.vue

@@ -312,8 +312,8 @@ const doSocialLogin = async (type: number) => {
       }
     }
     // 计算 redirectUri
-    // tricky: type、redirect需要先encode一次,否则钉钉回调会丢失。
-    // 配合 Login/SocialLogin.vue#getUrlValue() 使用
+    // 注意: type、redirect 需要先 encode 一次,否则钉钉回调会丢失。
+    // 配合 social-login.vue#getUrlValue() 使用
     const redirectUri =
       location.origin +
       '/social-login?' +

+ 9 - 7
src/views/Profile/Index.vue

@@ -1,4 +1,5 @@
 <template>
+  <!-- TODO @芋艿:可优化,对标 vben 版本 -->
   <div class="flex">
     <el-card class="user w-1/3" shadow="hover">
       <template #header>
@@ -6,18 +7,13 @@
           <span>{{ t('profile.user.title') }}</span>
         </div>
       </template>
-      <ProfileUser />
+      <ProfileUser ref="profileUserRef" />
     </el-card>
     <el-card class="user ml-3 w-2/3" shadow="hover">
-      <template #header>
-        <div class="card-header">
-          <span>{{ t('profile.info.title') }}</span>
-        </div>
-      </template>
       <div>
         <el-tabs v-model="activeName" class="profile-tabs" style="height: 400px" tab-position="top">
           <el-tab-pane :label="t('profile.info.basicInfo')" name="basicInfo">
-            <BasicInfo />
+            <BasicInfo @success="handleBasicInfoSuccess" />
           </el-tab-pane>
           <el-tab-pane :label="t('profile.info.resetPwd')" name="resetPwd">
             <ResetPwd />
@@ -36,6 +32,12 @@ import { BasicInfo, ProfileUser, ResetPwd, UserSocial } from './components'
 const { t } = useI18n()
 defineOptions({ name: 'Profile' })
 const activeName = ref('basicInfo')
+const profileUserRef = ref()
+
+// 处理基本信息更新成功
+const handleBasicInfoSuccess = async () => {
+  await profileUserRef.value?.refresh()
+}
 </script>
 <style scoped>
 .user {

+ 8 - 0
src/views/Profile/components/BasicInfo.vue

@@ -28,6 +28,12 @@ defineOptions({ name: 'BasicInfo' })
 const { t } = useI18n()
 const message = useMessage() // 消息弹窗
 const userStore = useUserStore()
+
+// 定义事件
+const emit = defineEmits<{
+  (e: 'success'): void
+}>()
+
 // 表单校验
 const rules = reactive<FormRules>({
   nickname: [{ required: true, message: t('profile.rules.nickname'), trigger: 'blur' }],
@@ -82,6 +88,8 @@ const submit = () => {
       message.success(t('common.updateSuccess'))
       const profile = await init()
       userStore.setUserNicknameAction(profile.nickname)
+      // 发送成功事件
+      emit('success')
     }
   })
 }

+ 6 - 0
src/views/Profile/components/ProfileUser.vue

@@ -60,6 +60,12 @@ const getUserInfo = async () => {
   const users = await getUserProfile()
   userInfo.value = users
 }
+
+// 暴露刷新方法
+defineExpose({
+  refresh: getUserInfo
+})
+
 onMounted(async () => {
   await getUserInfo()
 })

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

@@ -12,11 +12,13 @@
 </template>
 <script lang="ts" setup>
 import { propTypes } from '@/utils/propTypes'
-import { uploadAvatar } from '@/api/system/user/profile'
+import { updateUserProfile } from '@/api/system/user/profile'
 import { CropperAvatar } from '@/components/Cropper'
 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({
@@ -25,12 +27,18 @@ defineProps({
 
 const userStore = useUserStore()
 
-
 const cropperRef = ref()
 const handelUpload = async ({ data }) => {
-  const res = await uploadAvatar({ avatarFile: data })
+  const { httpRequest } = useUpload()
+  const avatar = ((await httpRequest({
+    file: data,
+    filename: 'avatar.png',
+  } as UploadRequestOptions)) as unknown as { data: string }).data
+  await updateUserProfile({ avatar })
+
+  // 关闭弹窗,并更新 userStore
   cropperRef.value.close()
-  userStore.setUserAvatarAction(res.data)
+  await userStore.setUserAvatarAction(avatar)
 }
 </script>
 

+ 2 - 0
src/views/ai/chat/index/index.vue

@@ -462,6 +462,8 @@ const doSendMessageStream = async (userMessage: ChatMessageVO) => {
       (error) => {
         message.alert(`对话异常! ${error}`)
         stopStream()
+        // 需要抛出异常,禁止重试
+        throw error
       },
       () => {
         stopStream()

+ 2 - 0
src/views/ai/mindmap/index/index.vue

@@ -80,6 +80,8 @@ const submit = (data: AiMindMapGenerateReqVO) => {
     onError(err) {
       console.error('生成思维导图失败', err)
       stopStream()
+      // 需要抛出异常,禁止重试
+      throw error
     },
     ctrl: ctrl.value
   })

+ 5 - 2
src/views/ai/model/model/ModelForm.vue

@@ -4,7 +4,7 @@
       ref="formRef"
       :model="formData"
       :rules="formRules"
-      label-width="120px"
+      label-width="130px"
       v-loading="formLoading"
     >
       <el-form-item label="所属平台" prop="platform">
@@ -146,7 +146,10 @@ const formRules = reactive({
   platform: [{ required: true, message: '所属平台不能为空', trigger: 'blur' }],
   type: [{ required: true, message: '模型类型不能为空', trigger: 'blur' }],
   sort: [{ required: true, message: '排序不能为空', trigger: 'blur' }],
-  status: [{ required: true, message: '状态不能为空', trigger: 'blur' }]
+  status: [{ required: true, message: '状态不能为空', trigger: 'blur' }],
+  temperature: [{ required: true, message: '温度参数不能为空', trigger: 'blur' }],
+  maxTokens: [{ required: true, message: '回复数 Token 数不能为空', trigger: 'blur' }],
+  maxContexts: [{ required: true, message: '上下文数量不能为空', trigger: 'blur' }]
 })
 const formRef = ref() // 表单 Ref
 const apiKeyList = ref([] as ApiKeyVO[]) // API 密钥列表

+ 5 - 0
src/views/ai/utils/constants.ts

@@ -16,6 +16,7 @@ export const AiPlatformEnum = {
   DEEP_SEEK: 'DeepSeek', // DeepSeek
   ZHI_PU: 'ZhiPu', // 智谱 AI
   XING_HUO: 'XingHuo', // 讯飞
+  SiliconFlow: 'SiliconFlow', // 硅基流动
   OPENAI: 'OpenAI',
   Ollama: 'Ollama',
   STABLE_DIFFUSION: 'StableDiffusion', // Stability AI
@@ -44,6 +45,10 @@ export const OtherPlatformEnum: ImageModelVO[] = [
   {
     key: AiPlatformEnum.ZHI_PU,
     name: '智谱 AI'
+  },
+  {
+    key: AiPlatformEnum.SiliconFlow,
+    name: '硅基流动'
   }
 ]
 

+ 54 - 0
src/views/ai/workflow/form/BasicInfo.vue

@@ -0,0 +1,54 @@
+<template>
+  <el-form ref="formRef" :model="modelData" :rules="formRules" label-width="120px">
+    <el-row>
+      <el-col :span="24">
+        <el-form-item label="流程标识" prop="code">
+          <el-input v-model="modelData.code" placeholder="请输入流程标识" />
+        </el-form-item>
+      </el-col>
+      <el-col :span="24">
+        <el-form-item label="流程名称" prop="name">
+          <el-input v-model="modelData.name" placeholder="请输入流程名称" />
+        </el-form-item>
+      </el-col>
+      <el-col :span="24">
+        <el-form-item label="状态" prop="status">
+          <el-select v-model="modelData.status" placeholder="请选择状态">
+            <el-option
+              v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+              :key="dict.value"
+              :label="dict.label"
+              :value="dict.value"
+            />
+          </el-select>
+        </el-form-item>
+      </el-col>
+      <el-col :span="24">
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="modelData.remark" :rows="2" type="textarea" placeholder="请输入备注" />
+        </el-form-item>
+      </el-col>
+    </el-row>
+  </el-form>
+</template>
+<script lang="ts" setup>
+import { FormRules } from 'element-plus'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+
+const modelData = defineModel<any>()
+
+const formRef = ref() // 表单 Ref
+const formRules = reactive<FormRules>({
+  code: [{ required: true, message: '流程标识不能为空', trigger: 'blur' }],
+  name: [{ required: true, message: '流程名称不能为空', trigger: 'blur' }],
+  status: [{ required: true, message: '状态不能为空', trigger: 'change' }]
+})
+
+/** 表单校验 */
+const validate = async () => {
+  await formRef.value?.validate()
+}
+defineExpose({
+  validate
+})
+</script>

+ 250 - 0
src/views/ai/workflow/form/WorkflowDesign.vue

@@ -0,0 +1,250 @@
+<template>
+  <div class="relative" style="width: 100%; height: 700px">
+    <Tinyflow
+      v-if="workflowData"
+      ref="tinyflowRef"
+      :className="'custom-class'"
+      :style="{ width: '100%', height: '100%' }"
+      :data="workflowData"
+      :provider="provider"
+    />
+    <div class="absolute top-30px right-30px">
+      <el-button @click="testWorkflowModel" type="primary" v-hasPermi="['ai:workflow:test']">
+        测试
+      </el-button>
+    </div>
+
+    <!-- 测试窗口 -->
+    <el-drawer v-model="showTestDrawer" title="工作流测试" :modal="false">
+      <fieldset>
+        <legend class="ml-15px"><h3>运行参数配置</h3></legend>
+        <div class="p-20px">
+          <div
+            class="flex justify-around mb-10px"
+            v-for="(param, index) in params4Test"
+            :key="index"
+          >
+            <el-select class="w-200px!" v-model="param.key" placeholder="参数名">
+              <el-option
+                v-for="(value, key) in paramsOfStartNode"
+                :key="key"
+                :label="value?.description || key"
+                :value="key"
+                :disabled="!!value?.disabled"
+              />
+            </el-select>
+            <el-input class="w-200px!" v-model="param.value" placeholder="参数值" />
+            <el-button type="danger" plain :icon="Delete" circle @click="removeParam(index)" />
+          </div>
+          <!-- TODO @lesan:是不是不用添加和删除参数,直接把必填和选填列出来,然后加上参数校验? -->
+          <el-button type="primary" plain @click="addParam">添加参数</el-button>
+        </div>
+      </fieldset>
+      <fieldset class="mt-20px bg-#f8f9fa">
+        <legend class="ml-15px"><h3>运行结果</h3></legend>
+        <div class="p-20px">
+          <div v-if="loading"> <el-text type="primary">执行中...</el-text></div>
+          <div v-else-if="error">
+            <el-text type="danger">{{ error }}</el-text>
+          </div>
+          <pre v-else-if="testResult" class="result-content"
+            >{{ JSON.stringify(testResult, null, 2) }}
+          </pre>
+          <div v-else> <el-text type="info">点击运行查看结果</el-text> </div>
+        </div>
+      </fieldset>
+      <el-button class="mt-20px w-100%" size="large" type="success" @click="goRun">
+        运行流程
+      </el-button>
+    </el-drawer>
+  </div>
+</template>
+
+<script setup lang="ts">
+import Tinyflow from '@/components/Tinyflow/Tinyflow.vue'
+import * as WorkflowApi from '@/api/ai/workflow'
+// TODO @lesan:要不使用 ICon 哪个组件哈
+import { Delete } from '@element-plus/icons-vue'
+
+defineProps<{
+  provider: any
+}>()
+
+const tinyflowRef = ref()
+const workflowData = inject('workflowData') as Ref
+const showTestDrawer = ref(false)
+const params4Test = ref([])
+const paramsOfStartNode = ref({})
+const testResult = ref(null)
+const loading = ref(false)
+const error = ref(null)
+
+/** 展示工作流测试抽屉 */
+const testWorkflowModel = () => {
+  showTestDrawer.value = !showTestDrawer.value
+}
+
+/** 运行流程 */
+const goRun = async () => {
+  try {
+    const val = tinyflowRef.value.getData()
+    loading.value = true
+    error.value = null
+    testResult.value = null
+    /// 查找start节点
+    const startNode = getStartNode()
+
+    // 获取参数定义
+    const parameters = startNode.data?.parameters || []
+    const paramDefinitions = {}
+    parameters.forEach((param) => {
+      paramDefinitions[param.name] = param.dataType
+    })
+
+    // 参数类型转换
+    const convertedParams = {}
+    for (const { key, value } of params4Test.value) {
+      const paramKey = key.trim()
+      if (!paramKey) continue
+
+      let dataType = paramDefinitions[paramKey]
+      if (!dataType) {
+        dataType = 'String'
+      }
+
+      try {
+        convertedParams[paramKey] = convertParamValue(value, dataType)
+      } catch (e) {
+        throw new Error(`参数 ${paramKey} 转换失败: ${e.message}`)
+      }
+    }
+
+    const data = {
+      graph: JSON.stringify(val),
+      params: convertedParams
+    }
+
+    const response = await WorkflowApi.testWorkflow(data)
+    testResult.value = response
+  } catch (err) {
+    error.value = err.response?.data?.message || '运行失败,请检查参数和网络连接'
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 监听测试抽屉的开启,获取开始节点参数列表 */
+watch(showTestDrawer, (value) => {
+  if (!value) return
+
+  /// 查找start节点
+  const startNode = getStartNode()
+
+  // 获取参数定义
+  const parameters = startNode.data?.parameters || []
+  const paramDefinitions = {}
+
+  // 加入参数选项方便用户添加非必须参数
+  parameters.forEach((param) => {
+    paramDefinitions[param.name] = param
+  })
+
+  function mergeIfRequiredButNotSet(target) {
+    let needPushList = []
+    for (let key in paramDefinitions) {
+      let param = paramDefinitions[key]
+
+      if (param.required) {
+        let item = target.find((item) => item.key === key)
+
+        if (!item) {
+          needPushList.push({ key: param.name, value: param.defaultValue || '' })
+        }
+      }
+    }
+    target.push(...needPushList)
+  }
+  // 自动装载需必填的参数
+  mergeIfRequiredButNotSet(params4Test.value)
+
+  paramsOfStartNode.value = paramDefinitions
+})
+
+/** 获取开始节点 */
+const getStartNode = () => {
+  const val = tinyflowRef.value.getData()
+  const startNode = val.nodes.find((node) => node.type === 'startNode')
+  if (!startNode) {
+    throw new Error('流程缺少开始节点')
+  }
+  return startNode
+}
+
+/** 添加参数项 */
+const addParam = () => {
+  params4Test.value.push({ key: '', value: '' })
+}
+
+/** 删除参数项 */
+const removeParam = (index) => {
+  params4Test.value.splice(index, 1)
+}
+
+/** 类型转换函数 */
+const convertParamValue = (value, dataType) => {
+  if (value === '') return null // 空值处理
+
+  switch (dataType) {
+    case 'String':
+      return String(value)
+    case 'Number':
+      const num = Number(value)
+      if (isNaN(num)) throw new Error('非数字格式')
+      return num
+    case 'Boolean':
+      if (value.toLowerCase() === 'true') return true
+      if (value.toLowerCase() === 'false') return false
+      throw new Error('必须为 true/false')
+    case 'Object':
+    case 'Array':
+      try {
+        return JSON.parse(value)
+      } catch (e) {
+        throw new Error(`JSON格式错误: ${e.message}`)
+      }
+    default:
+      throw new Error(`不支持的类型: ${dataType}`)
+  }
+}
+
+/** 表单校验 */
+const validate = async () => {
+  try {
+    // 获取最新的流程数据
+    if (!workflowData.value) {
+      throw new Error('请设计流程')
+    }
+    workflowData.value = tinyflowRef.value.getData()
+    return true
+  } catch (error) {
+    throw error
+  }
+}
+defineExpose({
+  validate
+})
+</script>
+
+<style lang="css" scoped>
+.result-content {
+  background: white;
+  padding: 12px;
+  border-radius: 4px;
+  max-height: 300px;
+  overflow: auto;
+  font-family: Monaco, Consolas, monospace;
+  font-size: 14px;
+  line-height: 1.5;
+  white-space: pre-wrap;
+}
+</style>

+ 240 - 0
src/views/ai/workflow/form/index.vue

@@ -0,0 +1,240 @@
+<template>
+  <ContentWrap>
+    <div class="mx-auto">
+      <!-- 头部导航栏 -->
+      <div
+        class="absolute top-0 left-0 right-0 h-50px bg-white border-bottom z-10 flex items-center px-20px"
+      >
+        <!-- 左侧标题 -->
+        <div class="w-200px flex items-center overflow-hidden">
+          <Icon icon="ep:arrow-left" class="cursor-pointer flex-shrink-0" @click="handleBack" />
+          <span class="ml-10px text-16px truncate" :title="formData.name || '创建流程'">
+            {{ formData.name || '创建流程' }}
+          </span>
+        </div>
+
+        <!-- 步骤条 -->
+        <div class="flex-1 flex items-center justify-center h-full">
+          <div class="w-400px flex items-center justify-between h-full">
+            <div
+              v-for="(step, index) in steps"
+              :key="index"
+              class="flex items-center cursor-pointer mx-15px relative h-full"
+              :class="[
+                currentStep === index
+                  ? 'text-[#3473ff] border-[#3473ff] border-b-2 border-b-solid'
+                  : 'text-gray-500'
+              ]"
+              @click="handleStepClick(index)"
+            >
+              <div
+                class="w-28px h-28px rounded-full flex items-center justify-center mr-8px border-2 border-solid text-15px"
+                :class="[
+                  currentStep === index
+                    ? 'bg-[#3473ff] text-white border-[#3473ff]'
+                    : 'border-gray-300 bg-white text-gray-500'
+                ]"
+              >
+                {{ index + 1 }}
+              </div>
+              <span class="text-16px font-bold whitespace-nowrap">{{ step.title }}</span>
+            </div>
+          </div>
+        </div>
+
+        <!-- 右侧按钮 -->
+        <div class="w-200px flex items-center justify-end gap-2">
+          <el-button type="primary" @click="handleSave"> 保 存 </el-button>
+        </div>
+      </div>
+
+      <!-- 主体内容 -->
+      <div class="mt-50px">
+        <!-- 第一步:基本信息 -->
+        <div v-if="currentStep === 0" class="mx-auto w-560px">
+          <BasicInfo v-model="formData" ref="basicInfoRef" />
+        </div>
+
+        <!-- 第二步:工作流设计 -->
+        <WorkflowDesign
+          v-if="currentStep === 1"
+          v-model="formData"
+          :provider="llmProvider"
+          ref="workflowDesignRef"
+        />
+      </div>
+    </div>
+  </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import { CommonStatusEnum } from '@/utils/constants'
+import * as WorkflowApi from '@/api/ai/workflow'
+import BasicInfo from './BasicInfo.vue'
+import WorkflowDesign from './WorkflowDesign.vue'
+import { ModelApi } from '@/api/ai/model/model'
+import { AiModelTypeEnum } from '@/views/ai/utils/constants'
+
+const router = useRouter()
+const { delView } = useTagsViewStore()
+const route = useRoute()
+const message = useMessage()
+
+const basicInfoRef = ref()
+const workflowDesignRef = ref()
+
+const validateBasic = async () => {
+  await basicInfoRef.value?.validate()
+}
+const validateWorkflow = async () => {
+  await workflowDesignRef.value?.validate()
+}
+
+const currentStep = ref(-1)
+const steps = [
+  { title: '基本信息', validator: validateBasic },
+  { title: '工作流设计', validator: validateWorkflow }
+]
+
+const formData: any = ref({
+  id: undefined,
+  name: '',
+  code: '',
+  remark: '',
+  graph: '',
+  status: CommonStatusEnum.ENABLE
+})
+const llmProvider = ref<any>([])
+const workflowData = ref<any>({})
+provide('workflowData', workflowData)
+
+/** 初始化数据 */
+const actionType = route.params.type as string
+const initData = async () => {
+  // 编辑情况下,需要加载工作流配置
+  if (actionType === 'update') {
+    const workflowId = route.params.id as string
+    formData.value = await WorkflowApi.getWorkflow(workflowId)
+    workflowData.value = JSON.parse(formData.value.graph)
+  }
+
+  // 加载模型列表
+  const models = await ModelApi.getModelSimpleList(AiModelTypeEnum.CHAT)
+  llmProvider.value = {
+    llm: () =>
+      models.map(({ id, name }) => ({
+        value: id,
+        label: name
+      })),
+    knowledge: () => [],
+    internal: () => []
+  }
+  // TODO @lesan:知识库(可以看下 knowledge)
+  // TODO @lesan:搜索引擎(这个之前有个 pr 搞了,,,可能来接下)
+
+  // 设置当前步骤
+  currentStep.value = 0
+}
+
+/** 校验所有步骤数据是否完整 */
+const validateAllSteps = async () => {
+  try {
+    // 基本信息校验
+    try {
+      await validateBasic()
+    } catch (error) {
+      currentStep.value = 0
+      throw new Error('请完善基本信息')
+    }
+
+    // 工作流设计校验
+    try {
+      await validateWorkflow()
+    } catch (error) {
+      currentStep.value = 1
+      throw new Error('请完善工作流信息')
+    }
+    return true
+  } catch (error) {
+    throw error
+  }
+}
+
+/** 保存操作 */
+const handleSave = async () => {
+  try {
+    // 保存前校验所有步骤的数据
+    await validateAllSteps()
+
+    // 更新表单数据
+    const data = {
+      ...formData.value,
+      graph: JSON.stringify(workflowData.value)
+    }
+    if (actionType === 'update') {
+      await WorkflowApi.updateWorkflow(data)
+    } else {
+      await WorkflowApi.createWorkflow(data)
+    }
+
+    // 保存成功,提示并跳转到列表页
+    message.success('保存成功')
+    delView(unref(router.currentRoute))
+    await router.push({ name: 'AiWorkflow' })
+  } catch (error: any) {
+    console.error('保存失败:', error)
+    message.warning(error.message || '请完善所有步骤的必填信息')
+  }
+}
+
+/** 步骤切换处理 */
+const handleStepClick = async (index: number) => {
+  try {
+    if (index !== 0) {
+      await validateBasic()
+    }
+    if (index !== 1) {
+      await validateWorkflow()
+    }
+
+    // 切换步骤
+    currentStep.value = index
+  } catch (error) {
+    console.error('步骤切换失败:', error)
+    message.warning('请先完善当前步骤必填信息')
+  }
+}
+
+/** 返回列表页 */
+const handleBack = () => {
+  // 先删除当前页签
+  delView(unref(router.currentRoute))
+  // 跳转到列表页
+  router.push({ name: 'AiWorkflow' })
+}
+
+/** 初始化 */
+onMounted(async () => {
+  await initData()
+})
+</script>
+
+<!-- TODO @lesan:可以用 cursor 搞成 unocss 哈 -->
+<style lang="scss" scoped>
+.border-bottom {
+  border-bottom: 1px solid #dcdfe6;
+}
+
+.text-primary {
+  color: #3473ff;
+}
+
+.bg-primary {
+  background-color: #3473ff;
+}
+
+.border-primary {
+  border-color: #3473ff;
+}
+</style>

+ 193 - 0
src/views/ai/workflow/index.vue

@@ -0,0 +1,193 @@
+<template>
+  <!-- 搜索工作栏 -->
+  <ContentWrap>
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="流程标识" prop="code">
+        <el-input
+          v-model="queryParams.code"
+          placeholder="请输入流程标识"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="流程名称" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入流程名称"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-select v-model="queryParams.status" placeholder="状态" clearable class="!w-240px">
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+        <el-button
+          type="primary"
+          plain
+          @click="openForm('create')"
+          v-hasPermi="['ai:workflow:create']"
+        >
+          <Icon icon="ep:plus" /> 新增
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column label="编号" align="center" prop="id" />
+      <el-table-column label="流程标识" align="center" prop="code" />
+      <el-table-column label="流程名称" align="center" prop="name" />
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+      />
+      <el-table-column label="备注" align="center" prop="remark" />
+      <el-table-column label="状态" align="center" key="status">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" fixed="right">
+        <template #default="scope">
+          <el-button
+            type="primary"
+            link
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['ai:workflow:update']"
+          >
+            修改
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['ai:workflow:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 添加或修改工作流对话框 -->
+</template>
+
+<script setup lang="ts">
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import * as WorkflowApi from '@/api/ai/workflow'
+import { dateFormatter } from '@/utils/formatTime'
+
+defineOptions({ name: 'AiWorkflow' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+const { push } = useRouter() // 路由
+
+const loading = ref(true) // 列表的加载中
+const list = ref([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  code: '',
+  name: '',
+  status: undefined,
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await WorkflowApi.getWorkflowPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await WorkflowApi.deleteWorkflow(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 添加/修改操作 */
+const openForm = async (type: string, id?: number) => {
+  if (type === 'create') {
+    await push({ name: 'AiWorkflowCreate' })
+  } else {
+    await push({
+      name: 'AiWorkflowUpdate',
+      params: { id, type }
+    })
+  }
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>

+ 4 - 2
src/views/ai/write/index/index.vue

@@ -57,9 +57,11 @@ const submit = (data: WriteVO) => {
     },
     ctrl: abortController.value,
     onClose: stopStream,
-    onError: (...err) => {
-      console.error('写作异常', ...err)
+    onError: (error) => {
+      console.error('写作异常', error)
       stopStream()
+      // 需要抛出异常,禁止重试
+      throw error
     }
   })
 }

+ 14 - 2
src/views/bpm/model/CategoryDraggableModel.vue

@@ -97,10 +97,23 @@
         </el-table-column>
         <el-table-column label="可见范围" prop="startUserIds" min-width="150">
           <template #default="{ row }">
-            <el-text v-if="!row.startUsers?.length"> 全部可见 </el-text>
+            <el-text v-if="!row.startUsers?.length && !row.startDepts?.length"> 全部可见 </el-text>
             <el-text v-else-if="row.startUsers.length === 1">
               {{ row.startUsers[0].nickname }}
             </el-text>
+            <el-text v-else-if="row.startDepts?.length === 1">
+              {{ row.startDepts[0].name }}
+            </el-text>
+            <el-text v-else-if="row.startDepts?.length > 1">
+              <el-tooltip
+                class="box-item"
+                effect="dark"
+                placement="top"
+                :content="row.startDepts.map((dept: any) => dept.name).join('、')"
+              >
+                {{ row.startDepts[0].name }}等 {{ row.startDepts.length }} 个部门可见
+              </el-tooltip>
+            </el-text>
             <el-text v-else>
               <el-tooltip
                 class="box-item"
@@ -436,7 +449,6 @@ const handleChangeState = async (row: any) => {
   try {
     // 修改状态的二次确认
     const id = row.id
-    debugger
     const statusState = state === 1 ? '停用' : '启用'
     const content = '是否确认' + statusState + '流程名字为"' + row.name + '"的数据项?'
     await message.confirm(content)

+ 73 - 1
src/views/bpm/model/form/BasicInfo.vue

@@ -77,6 +77,7 @@
       >
         <el-option label="全员" :value="0" />
         <el-option label="指定人员" :value="1" />
+        <el-option label="指定部门" :value="2" />
       </el-select>
       <div v-if="modelData.startUserType === 1" class="mt-2 flex flex-wrap gap-2">
         <div
@@ -99,6 +100,24 @@
           <Icon icon="ep:plus" /> 选择人员
         </el-button>
       </div>
+      <div v-if="modelData.startUserType === 2" class="mt-2 flex flex-wrap gap-2">
+        <div
+          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"
+        >
+          <Icon icon="ep:office-building" class="!m-5px text-20px" />
+          {{ dept.name }}
+          <Icon
+            icon="ep:close"
+            class="ml-2 cursor-pointer hover:text-red-500"
+            @click="handleRemoveStartDept(dept)"
+          />
+        </div>
+        <el-button type="primary" link @click="openStartDeptSelect">
+          <Icon icon="ep:plus" /> 选择部门
+        </el-button>
+      </div>
     </el-form-item>
     <el-form-item label="流程管理员" prop="managerUserIds" class="mb-20px">
       <div class="flex flex-wrap gap-2">
@@ -127,11 +146,19 @@
 
   <!-- 用户选择弹窗 -->
   <UserSelectForm ref="userSelectFormRef" @confirm="handleUserSelectConfirm" />
+  <!-- 部门选择弹窗 -->
+  <DeptSelectForm
+    ref="deptSelectFormRef"
+    :multiple="true"
+    :check-strictly="true"
+    @confirm="handleDeptSelectConfirm"
+  />
 </template>
 
 <script lang="ts" setup>
 import { DICT_TYPE, getBoolDictOptions, getIntDictOptions } from '@/utils/dict'
 import { UserVO } from '@/api/system/user'
+import { DeptVO } from '@/api/system/dept'
 import { CategoryVO } from '@/api/bpm/category'
 
 const props = defineProps({
@@ -142,13 +169,19 @@ const props = defineProps({
   userList: {
     type: Array,
     required: true
+  },
+  deptList: {
+    type: Array,
+    required: true
   }
 })
 
 const formRef = ref()
 const selectedStartUsers = ref<UserVO[]>([])
+const selectedStartDepts = ref<DeptVO[]>([])
 const selectedManagerUsers = ref<UserVO[]>([])
 const userSelectFormRef = ref()
+const deptSelectFormRef = ref()
 const currentSelectType = ref<'start' | 'manager'>('start')
 
 const rules = {
@@ -174,6 +207,13 @@ watch(
     } else {
       selectedStartUsers.value = []
     }
+    if (newVal.startDeptIds?.length) {
+      selectedStartDepts.value = props.deptList.filter((dept: DeptVO) =>
+        newVal.startDeptIds.includes(dept.id)
+      ) as DeptVO[]
+    } else {
+      selectedStartDepts.value = []
+    }
     if (newVal.managerUserIds?.length) {
       selectedManagerUsers.value = props.userList.filter((user: UserVO) =>
         newVal.managerUserIds.includes(user.id)
@@ -193,6 +233,11 @@ const openStartUserSelect = () => {
   userSelectFormRef.value.open(0, selectedStartUsers.value)
 }
 
+/** 打开部门选择 */
+const openStartDeptSelect = () => {
+  deptSelectFormRef.value.open(selectedStartDepts.value)
+}
+
 /** 打开管理员选择 */
 const openManagerUserSelect = () => {
   currentSelectType.value = 'manager'
@@ -214,9 +259,28 @@ const handleUserSelectConfirm = (_, users: UserVO[]) => {
   }
 }
 
+/** 处理部门选择确认 */
+const handleDeptSelectConfirm = (depts: DeptVO[]) => {
+  modelData.value = {
+    ...modelData.value,
+    startDeptIds: depts.map((d) => d.id)
+  }
+}
+
 /** 处理发起人类型变化 */
 const handleStartUserTypeChange = (value: number) => {
-  if (value !== 1) {
+  if (value === 0) {
+    modelData.value = {
+      ...modelData.value,
+      startUserIds: [],
+      startDeptIds: []
+    }
+  } else if (value === 1) {
+    modelData.value = {
+      ...modelData.value,
+      startDeptIds: []
+    }
+  } else if (value === 2) {
     modelData.value = {
       ...modelData.value,
       startUserIds: []
@@ -232,6 +296,14 @@ const handleRemoveStartUser = (user: UserVO) => {
   }
 }
 
+/** 移除部门 */
+const handleRemoveStartDept = (dept: DeptVO) => {
+  modelData.value = {
+    ...modelData.value,
+    startDeptIds: modelData.value.startDeptIds.filter((id: number) => id !== dept.id)
+  }
+}
+
 /** 移除管理员 */
 const handleRemoveManagerUser = (user: UserVO) => {
   modelData.value = {

+ 81 - 5
src/views/bpm/model/form/ExtraSettings.vue

@@ -148,7 +148,7 @@
         <div class="flex">
           <el-switch
             v-model="processBeforeTriggerEnable"
-            @change="handlePreProcessNotifyEnableChange"
+            @change="handleProcessBeforeTriggerEnableChange"
           />
           <div class="ml-80px">流程启动后通知</div>
         </div>
@@ -168,9 +168,9 @@
         <div class="flex">
           <el-switch
             v-model="processAfterTriggerEnable"
-            @change="handlePostProcessNotifyEnableChange"
+            @change="handleProcessAfterTriggerEnableChange"
           />
-          <div class="ml-80px">流程启动后通知</div>
+          <div class="ml-80px">流程结束后通知</div>
         </div>
         <HttpRequestSetting
           v-if="processAfterTriggerEnable"
@@ -180,6 +180,46 @@
         />
       </div>
     </el-form-item>
+    <el-form-item class="mb-20px">
+      <template #label>
+        <el-text size="large" tag="b">任务前置通知</el-text>
+      </template>
+      <div class="flex flex-col w-100%">
+        <div class="flex">
+          <el-switch
+            v-model="taskBeforeTriggerEnable"
+            @change="handleTaskBeforeTriggerEnableChange"
+          />
+          <div class="ml-80px">任务执行时通知</div>
+        </div>
+        <HttpRequestSetting
+          v-if="taskBeforeTriggerEnable"
+          v-model:setting="modelData.taskBeforeTriggerSetting"
+          :responseEnable="true"
+          :formItemPrefix="'taskBeforeTriggerSetting'"
+        />
+      </div>
+    </el-form-item>
+    <el-form-item class="mb-20px">
+      <template #label>
+        <el-text size="large" tag="b">任务后置通知</el-text>
+      </template>
+      <div class="flex flex-col w-100%">
+        <div class="flex">
+          <el-switch
+            v-model="taskAfterTriggerEnable"
+            @change="handleTaskAfterTriggerEnableChange"
+          />
+          <div class="ml-80px">任务结束后通知</div>
+        </div>
+        <HttpRequestSetting
+          v-if="taskAfterTriggerEnable"
+          v-model:setting="modelData.taskAfterTriggerSetting"
+          :responseEnable="true"
+          :formItemPrefix="'taskAfterTriggerSetting'"
+        />
+      </div>
+    </el-form-item>
   </el-form>
 </template>
 
@@ -248,7 +288,7 @@ const numberExample = computed(() => {
 
 /** 是否开启流程前置通知 */
 const processBeforeTriggerEnable = ref(false)
-const handlePreProcessNotifyEnableChange = (val: boolean | string | number) => {
+const handleProcessBeforeTriggerEnableChange = (val: boolean | string | number) => {
   if (val) {
     modelData.value.processBeforeTriggerSetting = {
       url: '',
@@ -263,7 +303,7 @@ const handlePreProcessNotifyEnableChange = (val: boolean | string | number) => {
 
 /** 是否开启流程后置通知 */
 const processAfterTriggerEnable = ref(false)
-const handlePostProcessNotifyEnableChange = (val: boolean | string | number) => {
+const handleProcessAfterTriggerEnableChange = (val: boolean | string | number) => {
   if (val) {
     modelData.value.processAfterTriggerSetting = {
       url: '',
@@ -276,6 +316,36 @@ const handlePostProcessNotifyEnableChange = (val: boolean | string | number) =>
   }
 }
 
+/** 是否开启任务前置通知 */
+const taskBeforeTriggerEnable = ref(false)
+const handleTaskBeforeTriggerEnableChange = (val: boolean | string | number) => {
+  if (val) {
+    modelData.value.taskBeforeTriggerSetting = {
+      url: '',
+      header: [],
+      body: [],
+      response: []
+    }
+  } else {
+    modelData.value.taskBeforeTriggerSetting = null
+  }
+}
+
+/** 是否开启任务后置通知 */
+const taskAfterTriggerEnable = ref(false)
+const handleTaskAfterTriggerEnableChange = (val: boolean | string | number) => {
+  if (val) {
+    modelData.value.taskAfterTriggerSetting = {
+      url: '',
+      header: [],
+      body: [],
+      response: []
+    }
+  } else {
+    modelData.value.taskAfterTriggerSetting = null
+  }
+}
+
 /** 表单选项 */
 const formField = ref<Array<{ field: string; title: string }>>([])
 const formFieldOptions4Title = computed(() => {
@@ -341,6 +411,12 @@ const initData = () => {
   if (modelData.value.processAfterTriggerSetting) {
     processAfterTriggerEnable.value = true
   }
+  if (modelData.value.taskBeforeTriggerSetting) {
+    taskBeforeTriggerEnable.value = true
+  }
+  if (modelData.value.taskAfterTriggerSetting) {
+    taskAfterTriggerEnable.value = true
+  }
 }
 defineExpose({ initData })
 

+ 1 - 0
src/views/bpm/model/form/ProcessDesign.vue

@@ -18,6 +18,7 @@
       :model-key="modelData.key"
       :model-name="modelData.name"
       :start-user-ids="modelData.startUserIds"
+      :start-dept-ids="modelData.startDeptIds"
       @success="handleDesignSuccess"
     />
   </template>

+ 12 - 3
src/views/bpm/model/form/index.vue

@@ -62,6 +62,7 @@
             v-model="formData"
             :categoryList="categoryList"
             :userList="userList"
+            :deptList="deptList"
             ref="basicInfoRef"
           />
         </div>
@@ -92,6 +93,7 @@ import * as ModelApi from '@/api/bpm/model'
 import * as FormApi from '@/api/bpm/form'
 import { CategoryApi, CategoryVO } from '@/api/bpm/category'
 import * as UserApi from '@/api/system/user'
+import * as DeptApi from '@/api/system/dept'
 import * as DefinitionApi from '@/api/bpm/definition'
 import { BpmModelFormType, BpmModelType, BpmAutoApproveType } from '@/utils/constants'
 import BasicInfo from './BasicInfo.vue'
@@ -153,6 +155,7 @@ const formData: any = ref({
   visible: true,
   startUserType: undefined,
   startUserIds: [],
+  startDeptIds: [],
   managerUserIds: [],
   allowCancelRunningProcess: true,
   processIdRule: {
@@ -183,6 +186,7 @@ provide('modelData', formData)
 const formList = ref([])
 const categoryList = ref<CategoryVO[]>([])
 const userList = ref<UserApi.UserVO[]>([])
+const deptList = ref<DeptApi.DeptVO[]>([])
 
 /** 初始化数据 */
 const actionType = route.params.type as string
@@ -200,14 +204,17 @@ const initData = async () => {
       data.simpleModel = JSON.parse(data.simpleModel)
     }
     formData.value = data
-    formData.value.startUserType = formData.value.startUserIds?.length > 0 ? 1 : 0
+    formData.value.startUserType =
+      formData.value.startUserIds?.length > 0 ? 1 : formData.value?.startDeptIds?.length > 0 ? 2 : 0
   } else if (['update', 'copy'].includes(actionType)) {
     // 情况二:修改场景/复制场景
     const modelId = route.params.id as string
     formData.value = await ModelApi.getModel(modelId)
-    formData.value.startUserType = formData.value.startUserIds?.length > 0 ? 1 : 0
+    formData.value.startUserType =
+      formData.value.startUserIds?.length > 0 ? 1 : formData.value?.startDeptIds?.length > 0 ? 2 : 0
+
     // 特殊:复制场景
-    if (actionType === 'copy') {
+    if (route.params.type === 'copy') {
       delete formData.value.id
       formData.value.name += '副本'
       formData.value.key += '_copy'
@@ -225,6 +232,8 @@ const initData = async () => {
   categoryList.value = await CategoryApi.getCategorySimpleList()
   // 获取用户列表
   userList.value = await UserApi.getSimpleUserList()
+  // 获取部门列表
+  deptList.value = await DeptApi.getSimpleDeptList()
 
   // 最终,设置 currentStep 切换到第一步
   currentStep.value = 0

+ 152 - 85
src/views/bpm/oa/leave/create.vue

@@ -1,83 +1,78 @@
 <template>
-  <el-form
-    ref="formRef"
-    v-loading="formLoading"
-    :model="formData"
-    :rules="formRules"
-    label-width="80px"
-  >
-    <el-form-item label="请假类型" prop="type">
-      <el-select v-model="formData.type" clearable placeholder="请选择请假类型">
-        <el-option
-          v-for="dict in getIntDictOptions(DICT_TYPE.BPM_OA_LEAVE_TYPE)"
-          :key="dict.value"
-          :label="dict.label"
-          :value="dict.value"
-        />
-      </el-select>
-    </el-form-item>
-    <el-form-item label="开始时间" prop="startTime">
-      <el-date-picker
-        v-model="formData.startTime"
-        clearable
-        placeholder="请选择开始时间"
-        type="datetime"
-        value-format="x"
-      />
-    </el-form-item>
-    <el-form-item label="结束时间" prop="endTime">
-      <el-date-picker
-        v-model="formData.endTime"
-        clearable
-        placeholder="请选择结束时间"
-        type="datetime"
-        value-format="x"
-      />
-    </el-form-item>
-    <el-form-item label="原因" prop="reason">
-      <el-input v-model="formData.reason" placeholder="请输请假原因" type="textarea" />
-    </el-form-item>
-    <el-col v-if="startUserSelectTasks.length > 0">
-      <el-card class="mb-10px">
-        <template #header>指定审批人</template>
+  <el-row :gutter="20">
+    <el-col :span="16">
+      <ContentWrap title="申请信息">
         <el-form
-          :model="startUserSelectAssignees"
-          :rules="startUserSelectAssigneesFormRules"
-          ref="startUserSelectAssigneesFormRef"
+          ref="formRef"
+          v-loading="formLoading"
+          :model="formData"
+          :rules="formRules"
+          label-width="80px"
         >
-          <el-form-item
-            v-for="userTask in startUserSelectTasks"
-            :key="userTask.id"
-            :label="`任务【${userTask.name}】`"
-            :prop="userTask.id"
-          >
-            <el-select
-              v-model="startUserSelectAssignees[userTask.id]"
-              multiple
-              placeholder="请选择审批人"
-            >
+          <el-form-item label="请假类型" prop="type">
+            <el-select v-model="formData.type" clearable placeholder="请选择请假类型">
               <el-option
-                v-for="user in userList"
-                :key="user.id"
-                :label="user.nickname"
-                :value="user.id"
+                v-for="dict in getIntDictOptions(DICT_TYPE.BPM_OA_LEAVE_TYPE)"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value"
               />
             </el-select>
           </el-form-item>
+          <el-form-item label="开始时间" prop="startTime">
+            <el-date-picker
+              v-model="formData.startTime"
+              clearable
+              placeholder="请选择开始时间"
+              type="datetime"
+              value-format="x"
+            />
+          </el-form-item>
+          <el-form-item label="结束时间" prop="endTime">
+            <el-date-picker
+              v-model="formData.endTime"
+              clearable
+              placeholder="请选择结束时间"
+              type="datetime"
+              value-format="x"
+            />
+          </el-form-item>
+          <el-form-item label="原因" prop="reason">
+            <el-input v-model="formData.reason" placeholder="请输入请假原因" type="textarea" />
+          </el-form-item>
+          <el-form-item>
+            <el-button :disabled="formLoading" type="primary" @click="submitForm">
+              确 定
+            </el-button>
+          </el-form-item>
         </el-form>
-      </el-card>
+      </ContentWrap>
     </el-col>
-    <el-form-item>
-      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
-    </el-form-item>
-  </el-form>
+
+    <!-- 审批相关:流程信息 -->
+    <el-col :span="8">
+      <ContentWrap title="审批流程" :bodyStyle="{ padding: '0 20px 0' }">
+        <ProcessInstanceTimeline
+          ref="timelineRef"
+          :activity-nodes="activityNodes"
+          :show-status-icon="false"
+          @select-user-confirm="selectUserConfirm"
+        />
+      </ContentWrap>
+    </el-col>
+  </el-row>
 </template>
 <script lang="ts" setup>
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import * as LeaveApi from '@/api/bpm/leave'
 import { useTagsViewStore } from '@/store/modules/tagsView'
+
+// 审批相关:import
 import * as DefinitionApi from '@/api/bpm/definition'
-import * as UserApi from '@/api/system/user'
+import ProcessInstanceTimeline from '@/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue'
+import * as ProcessInstanceApi from '@/api/bpm/processInstance'
+import { CandidateStrategy, NodeId } from '@/components/SimpleProcessDesignerV2/src/consts'
+import { ApprovalNodeInfo } from '@/api/bpm/processInstance'
 
 defineOptions({ name: 'BpmOALeaveCreate' })
 
@@ -100,30 +95,37 @@ const formRules = reactive({
 })
 const formRef = ref() // 表单 Ref
 
-// 指定审批人
+// 审批相关:变量
 const processDefineKey = 'oa_leave' // 流程定义 Key
 const startUserSelectTasks = ref([]) // 发起人需要选择审批人的用户任务列表
 const startUserSelectAssignees = ref({}) // 发起人选择审批人的数据
-const startUserSelectAssigneesFormRef = ref() // 发起人选择审批人的表单 Ref
-const startUserSelectAssigneesFormRules = ref({}) // 发起人选择审批人的表单 Rules
-const userList = ref<any[]>([]) // 用户列表
+const tempStartUserSelectAssignees = ref({}) // 历史发起人选择审批人的数据,用于每次表单变更时,临时保存
+const activityNodes = ref<ProcessInstanceApi.ApprovalNodeInfo[]>([]) // 审批节点信息
+const processDefinitionId = ref('')
 
 /** 提交表单 */
 const submitForm = async () => {
-  // 校验表单
+  // 1.1 校验表单
   if (!formRef) return
   const valid = await formRef.value.validate()
   if (!valid) return
-  // 校验指定审批人
+  // 1.2 审批相关:校验指定审批人
   if (startUserSelectTasks.value?.length > 0) {
-    await startUserSelectAssigneesFormRef.value.validate()
+    for (const userTask of startUserSelectTasks.value) {
+      if (
+        Array.isArray(startUserSelectAssignees.value[userTask.id]) &&
+        startUserSelectAssignees.value[userTask.id].length === 0
+      ) {
+        return message.warning(`请选择${userTask.name}的审批人`)
+      }
+    }
   }
 
-  // 提交请求
+  // 2. 提交请求
   formLoading.value = true
   try {
     const data = { ...formData.value } as unknown as LeaveApi.LeaveVO
-    // 设置指定审批人
+    // 审批相关:设置指定审批人
     if (startUserSelectTasks.value?.length > 0) {
       data.startUserSelectAssignees = startUserSelectAssignees.value
     }
@@ -137,28 +139,93 @@ const submitForm = async () => {
   }
 }
 
+/** 审批相关:获取审批详情 */
+const getApprovalDetail = async () => {
+  try {
+    const data = await ProcessInstanceApi.getApprovalDetail({
+      processDefinitionId: processDefinitionId.value,
+      // TODO 小北:可以支持 processDefinitionKey 查询
+      activityId: NodeId.START_USER_NODE_ID,
+      processVariablesStr: JSON.stringify({ day: daysDifference() }) // 解决 GET 无法传递对象的问题,后端 String 再转 JSON
+    })
+
+    if (!data) {
+      message.error('查询不到审批详情信息!')
+      return
+    }
+    // 获取审批节点,显示 Timeline 的数据
+    activityNodes.value = data.activityNodes
+
+    // 获取发起人自选的任务
+    startUserSelectTasks.value = data.activityNodes?.filter(
+      (node: ApprovalNodeInfo) => CandidateStrategy.START_USER_SELECT === node.candidateStrategy
+    )
+    // 恢复之前的选择审批人
+    if (startUserSelectTasks.value?.length > 0) {
+      for (const node of startUserSelectTasks.value) {
+        if (
+          tempStartUserSelectAssignees.value[node.id] &&
+          tempStartUserSelectAssignees.value[node.id].length > 0
+        ) {
+          startUserSelectAssignees.value[node.id] = tempStartUserSelectAssignees.value[node.id]
+        } else {
+          startUserSelectAssignees.value[node.id] = []
+        }
+      }
+    }
+  } finally {
+  }
+}
+
+/** 审批相关:选择发起人 */
+const selectUserConfirm = (id: string, userList: any[]) => {
+  startUserSelectAssignees.value[id] = userList?.map((item: any) => item.id)
+}
+
+// 计算天数差
+// TODO @小北:可以搞到 formatTime 里面去,然后看看 dayjs 里面有没有现成的方法,或者辅助计算的方法。
+const daysDifference = () => {
+  const oneDay = 24 * 60 * 60 * 1000 // 一天的毫秒数
+  const diffTime = Math.abs(Number(formData.value.endTime) - Number(formData.value.startTime))
+  return Math.floor(diffTime / oneDay)
+}
+
 /** 初始化 */
 onMounted(async () => {
+  // TODO @小北:这里可以简化,统一通过 getApprovalDetail 处理么?
   const processDefinitionDetail = await DefinitionApi.getProcessDefinition(
     undefined,
     processDefineKey
   )
+
   if (!processDefinitionDetail) {
     message.error('OA 请假的流程模型未配置,请检查!')
     return
   }
+  processDefinitionId.value = processDefinitionDetail.id
   startUserSelectTasks.value = processDefinitionDetail.startUserSelectTasks
-  // 设置指定审批人
-  if (startUserSelectTasks.value?.length > 0) {
-    // 设置校验规则
-    for (const userTask of startUserSelectTasks.value) {
-      startUserSelectAssignees.value[userTask.id] = []
-      startUserSelectAssigneesFormRules.value[userTask.id] = [
-        { required: true, message: '请选择审批人', trigger: 'blur' }
-      ]
+
+  // 审批相关:加载最新的审批详情,主要用于节点预测
+  await getApprovalDetail()
+})
+
+/** 审批相关:预测流程节点会因为输入的参数值而产生新的预测结果值,所以需重新预测一次, formData.value可改成实际业务中的特定字段 */
+watch(
+  formData.value,
+  (newValue, oldValue) => {
+    if (!oldValue) {
+      return
     }
-    // 加载用户列表
-    userList.value = await UserApi.getSimpleUserList()
+    if (newValue && Object.keys(newValue).length > 0) {
+      // 记录之前的节点审批人
+      tempStartUserSelectAssignees.value = startUserSelectAssignees.value
+      startUserSelectAssignees.value = {}
+      // 加载最新的审批详情,主要用于节点预测
+      getApprovalDetail()
+    }
+  },
+  {
+    immediate: true
   }
-})
+)
 </script>

+ 17 - 0
src/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue

@@ -37,6 +37,11 @@
             {{ getApprovalNodeTime(activity) }}
           </div>
         </div>
+        <div v-if="activity.nodeType === NodeType.CHILD_PROCESS_NODE">
+          <el-button type="primary" plain size="small" @click="handleChildProcess(activity)">
+            查看子流程
+          </el-button>
+        </div>
         <!-- 需要自定义选择审批人 -->
         <div
           class="flex flex-wrap gap2 items-center"
@@ -194,6 +199,7 @@ withDefaults(
     showStatusIcon: true // 默认值为 true
   }
 )
+const { push } = useRouter() // 路由
 
 // 审批节点
 const statusIconMap2 = {
@@ -310,4 +316,15 @@ const handleUserSelectConfirm = (activityId: string, userList: any[]) => {
   customApproveUsers.value[activityId] = userList || []
   emit('selectUserConfirm', activityId, userList)
 }
+
+/** 跳转子流程 */
+const handleChildProcess = (activity: any) => {
+  // TODO @lesan:貌似跳不过去?!
+  push({
+    name: 'BpmProcessInstanceDetail',
+    query: {
+      id: activity.processInstanceId
+    }
+  })
+}
 </script>

+ 2 - 0
src/views/bpm/simple/SimpleModelDesign.vue

@@ -6,6 +6,7 @@
       :model-name="modelName"
       @success="handleSuccess"
       :start-user-ids="startUserIds"
+      :start-dept-ids="startDeptIds"
       ref="designerRef"
     />
   </ContentWrap>
@@ -22,6 +23,7 @@ defineProps<{
   modelKey?: string
   modelName?: string
   startUserIds?: number[]
+  startDeptIds?: number[]
 }>()
 
 const emit = defineEmits(['success'])

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

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

+ 17 - 8
src/views/infra/codegen/ImportTable.vue

@@ -62,7 +62,11 @@
     </el-row>
     <!-- 操作 -->
     <template #footer>
-      <el-button :disabled="tableList.length === 0" type="primary" @click="handleImportTable">
+      <el-button
+        :disabled="tableList.length === 0 || dbTableLoading"
+        type="primary"
+        @click="handleImportTable"
+      >
         导入
       </el-button>
       <el-button @click="close">关闭</el-button>
@@ -139,13 +143,18 @@ const handleSelectionChange = (selection) => {
 
 /** 导入按钮操作 */
 const handleImportTable = async () => {
-  await CodegenApi.createCodegenList({
-    dataSourceConfigId: queryParams.dataSourceConfigId,
-    tableNames: tableList.value
-  })
-  message.success('导入成功')
-  emit('success')
-  close()
+  dbTableLoading.value = true
+  try {
+    await CodegenApi.createCodegenList({
+      dataSourceConfigId: queryParams.dataSourceConfigId,
+      tableNames: tableList.value
+    })
+    message.success('导入成功')
+    emit('success')
+    close()
+  } finally {
+    dbTableLoading.value = false
+  }
 }
 const emit = defineEmits(['success'])
 </script>

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