Просмотр исходного кода

Merge branch 'yudaocode:master' into master

rohit 10 месяцев назад
Родитель
Сommit
6289bbd3e1
73 измененных файлов с 923 добавлено и 2068 удалено
  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. 243 1803
      pnpm-lock.yaml
  7. BIN
      public/home.png
  8. 2 2
      src/api/ai/workflow/index.ts
  9. 0 15
      src/api/infra/codegen/index.ts
  10. 1 7
      src/api/infra/file/index.ts
  11. 1 0
      src/api/infra/fileConfig/index.ts
  12. 0 2
      src/api/login/index.ts
  13. 5 0
      src/api/system/tenant/index.ts
  14. 2 7
      src/api/system/user/index.ts
  15. 5 9
      src/api/system/user/profile.ts
  16. 2 1
      src/components/Cropper/src/CopperModal.vue
  17. 1 1
      src/components/Dialog/src/Dialog.vue
  18. 2 2
      src/components/DiyEditor/components/mobile/CouponCard/component.tsx
  19. 10 3
      src/components/DiyEditor/components/mobile/CouponCard/index.vue
  20. 10 7
      src/components/DiyEditor/components/mobile/MagicCube/index.vue
  21. 2 2
      src/components/DiyEditor/components/mobile/MenuSwiper/index.vue
  22. 8 3
      src/components/DiyEditor/components/mobile/NavigationBar/property.vue
  23. 2 2
      src/components/DiyEditor/components/mobile/ProductCard/config.ts
  24. 4 1
      src/components/DiyEditor/components/mobile/ProductCard/index.vue
  25. 15 1
      src/components/DiyEditor/components/mobile/SearchBar/property.vue
  26. 7 1
      src/components/DiyEditor/components/mobile/TitleBar/config.ts
  27. 14 20
      src/components/DiyEditor/components/mobile/TitleBar/index.vue
  28. 45 22
      src/components/DiyEditor/components/mobile/TitleBar/property.vue
  29. 4 2
      src/components/Draggable/index.vue
  30. 8 1
      src/components/FormCreate/src/components/useApiSelect.tsx
  31. 1 0
      src/components/FormCreate/src/config/useSelectRule.ts
  32. 2 1
      src/components/FormCreate/src/type/index.ts
  33. 2 1
      src/components/FormCreate/src/useFormCreateDesigner.ts
  34. 2 2
      src/components/MagicCubeEditor/index.vue
  35. 2 1
      src/components/bpmnProcessDesigner/package/penal/form/ElementForm.vue
  36. 0 1
      src/components/bpmnProcessDesigner/package/penal/listeners/ElementListeners.vue
  37. 0 1
      src/components/bpmnProcessDesigner/package/utils.ts
  38. 14 2
      src/config/axios/service.ts
  39. 2 0
      src/hooks/web/useCache.ts
  40. 18 1
      src/hooks/web/useWatermark.ts
  41. 46 0
      src/layout/components/TenantVisit/index.vue
  42. 6 0
      src/layout/components/ToolHeader.vue
  43. 6 6
      src/locales/zh-CN.ts
  44. 4 4
      src/router/modules/remaining.ts
  45. 5 0
      src/store/modules/user.ts
  46. 10 2
      src/utils/auth.ts
  47. 2 2
      src/utils/constants.ts
  48. 2 2
      src/utils/index.ts
  49. 83 57
      src/views/Home/Index.vue
  50. 2 0
      src/views/Home/types.ts
  51. 2 2
      src/views/Login/components/LoginForm.vue
  52. 1 5
      src/views/Profile/Index.vue
  53. 13 5
      src/views/Profile/components/UserAvatar.vue
  54. 2 0
      src/views/ai/chat/index/index.vue
  55. 2 0
      src/views/ai/mindmap/index/index.vue
  56. 5 2
      src/views/ai/model/model/ModelForm.vue
  57. 202 1
      src/views/ai/workflow/form/WorkflowDesign.vue
  58. 16 11
      src/views/ai/workflow/form/index.vue
  59. 4 2
      src/views/ai/write/index/index.vue
  60. 0 1
      src/views/bpm/model/CategoryDraggableModel.vue
  61. 17 8
      src/views/infra/codegen/ImportTable.vue
  62. 14 1
      src/views/infra/fileConfig/FileConfigForm.vue
  63. 0 1
      src/views/iot/thingmodel/ThingModelForm.vue
  64. 2 9
      src/views/mall/promotion/diy/template/index.vue
  65. 2 2
      src/views/mall/promotion/kefu/index.vue
  66. 1 1
      src/views/mall/trade/order/components/OrderTableColumn.vue
  67. 2 2
      src/views/mp/material/components/upload.ts
  68. 5 1
      src/views/report/goview/index.vue
  69. 15 0
      src/views/report/jmreport/bi.vue
  70. 1 0
      src/views/system/menu/MenuForm.vue
  71. 1 1
      src/views/system/role/RoleDataPermissionForm.vue
  72. 2 2
      src/views/system/user/index.vue
  73. 0 1
      uno.config.ts

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.4.2-snapshot",
   "description": "基于vue3、vite4、element-plus、typesScript",
   "author": "xingyu",
   "private": false,
@@ -133,7 +133,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"

Разница между файлами не показана из-за своего большого размера
+ 243 - 1803
pnpm-lock.yaml


BIN
public/home.png


+ 2 - 2
src/api/ai/workflow/index.ts

@@ -20,6 +20,6 @@ export const deleteWorkflow = async (id) => {
   return await request.delete({ url: '/ai/workflow/delete?id=' + id })
 }
 
-export const updateWorkflowModel = async (data) => {
-  return await request.put({ url: '/ai/workflow/updateWorkflowModel', data })
+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 })

+ 1 - 7
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 {
   // 文件配置编号
@@ -17,7 +11,7 @@ export interface FilePresignedUrlRespVO {
 }
 
 // 查询文件列表
-export const getFilePage = (params: FilePageReqVO) => {
+export const getFilePage = (params: PageParam) => {
   return request.get({ url: '/infra/file/page', params })
 }
 

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

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

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

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

@@ -91,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>

+ 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 字段

+ 14 - 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 请求缓存

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

+ 6 - 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,9 @@ const locale = computed(() => appStore.getLocale)
 // 消息图标
 const message = computed(() => appStore.getMessage)
 
+// 租户切换权限
+const hasTenantVisitPermission = computed(() => checkPermi(['system:tenant:visit']))
+
 export default defineComponent({
   name: 'ToolHeader',
   setup() {
@@ -62,6 +67,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: '请输入用户昵称',

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

+ 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 // 仅针对连续审批的节点自动通过
 }

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

+ 83 - 57
src/views/Home/Index.vue

@@ -83,12 +83,16 @@
               :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 +135,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>
@@ -180,10 +184,12 @@ import { useUserStore } from '@/store/modules/user'
 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' })
 
 const { t } = useI18n()
+const router = useRouter()
 const userStore = useUserStore()
 const { setWatermark } = useWatermark()
 const loading = ref(true)
@@ -212,45 +218,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 +274,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 +306,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 +405,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?' +

+ 1 - 5
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>
@@ -9,11 +10,6 @@
       <ProfileUser />
     </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">

+ 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 密钥列表

+ 202 - 1
src/views/ai/workflow/form/WorkflowDesign.vue

@@ -13,11 +13,58 @@
         测试
       </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
@@ -25,9 +72,149 @@ defineProps<{
 
 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 = () => {
-  // TODO @lesan 测试
+  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}`)
+  }
 }
 
 /** 表单校验 */
@@ -47,3 +234,17 @@ 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>

+ 16 - 11
src/views/ai/workflow/form/index.vue

@@ -59,7 +59,7 @@
         <WorkflowDesign
           v-if="currentStep === 1"
           v-model="formData"
-          :provider="provider"
+          :provider="llmProvider"
           ref="workflowDesignRef"
         />
       </div>
@@ -73,7 +73,8 @@ import { CommonStatusEnum } from '@/utils/constants'
 import * as WorkflowApi from '@/api/ai/workflow'
 import BasicInfo from './BasicInfo.vue'
 import WorkflowDesign from './WorkflowDesign.vue'
-import { ApiKeyApi } from '@/api/ai/model/apiKey'
+import { ModelApi } from '@/api/ai/model/model'
+import { AiModelTypeEnum } from '@/views/ai/utils/constants'
 
 const router = useRouter()
 const { delView } = useTagsViewStore()
@@ -104,31 +105,35 @@ const formData: any = ref({
   graph: '',
   status: CommonStatusEnum.ENABLE
 })
-// TODO @lesan:待接入
-const provider = ref<any>()
+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 apiKeys = await ApiKeyApi.getApiKeySimpleList()
-  provider.value = {
+  // 加载模型列表
+  const models = await ModelApi.getModelSimpleList(AiModelTypeEnum.CHAT)
+  llmProvider.value = {
     llm: () =>
-      apiKeys.map(({ id, name }) => ({
+      models.map(({ id, name }) => ({
         value: id,
         label: name
       })),
     knowledge: () => [],
     internal: () => []
   }
+  // TODO @lesan:知识库(可以看下 knowledge)
+  // TODO @lesan:搜索引擎(这个之前有个 pr 搞了,,,可能来接下)
 
+  // 设置当前步骤
   currentStep.value = 0
 }
 
@@ -164,17 +169,17 @@ const handleSave = async () => {
 
     // 更新表单数据
     const data = {
-      ...formData.value
+      ...formData.value,
+      graph: JSON.stringify(workflowData.value)
     }
-
-    data.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) {

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

+ 0 - 1
src/views/bpm/model/CategoryDraggableModel.vue

@@ -449,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)

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

+ 14 - 1
src/views/infra/fileConfig/FileConfigForm.vue

@@ -5,7 +5,7 @@
       v-loading="formLoading"
       :model="formData"
       :rules="formRules"
-      label-width="120px"
+      label-width="130px"
     >
       <el-form-item label="配置名" prop="name">
         <el-input v-model="formData.name" placeholder="请输入配置名" />
@@ -83,6 +83,16 @@
       <el-form-item v-if="formData.storage === 20" label="accessSecret" prop="config.accessSecret">
         <el-input v-model="formData.config.accessSecret" placeholder="请输入 accessSecret" />
       </el-form-item>
+      <el-form-item
+        v-if="formData.storage === 20"
+        label="是否 Path Style"
+        prop="config.enablePathStyleAccess"
+      >
+        <el-radio-group v-model="formData.config.enablePathStyleAccess">
+          <el-radio key="true" :value="true">启用</el-radio>
+          <el-radio key="false" :value="false">禁用</el-radio>
+        </el-radio-group>
+      </el-form-item>
       <!-- 通用 -->
       <el-form-item v-if="formData.storage === 20" label="自定义域名">
         <!-- 无需参数校验,所以去掉 prop -->
@@ -133,6 +143,9 @@ const formRules = reactive<FormRules>({
     bucket: [{ required: true, message: '存储 bucket 不能为空', trigger: 'blur' }],
     accessKey: [{ required: true, message: 'accessKey 不能为空', trigger: 'blur' }],
     accessSecret: [{ required: true, message: 'accessSecret 不能为空', trigger: 'blur' }],
+    enablePathStyleAccess: [
+      { required: true, message: '是否 PathStyle 访问不能为空', trigger: 'change' }
+    ],
     domain: [{ required: true, message: '自定义域名不能为空', trigger: 'blur' }]
   } as FormRules
 })

+ 0 - 1
src/views/iot/thingmodel/ThingModelForm.vue

@@ -132,7 +132,6 @@ defineExpose({ open, close: () => (dialogVisible.value = false) })
 /** 提交表单 */
 const emit = defineEmits(['success'])
 const submitForm = async () => {
-  debugger
   await formRef.value.validate()
   formLoading.value = true
   try {

+ 2 - 9
src/views/mall/promotion/diy/template/index.vue

@@ -62,20 +62,13 @@
           />
         </template>
       </el-table-column>
-      <el-table-column label="模板名称" align="center" prop="name" />
+      <el-table-column label="模板名称" align="center" prop="name" min-width="180" />
       <el-table-column label="是否使用" align="center" prop="used">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.used" />
         </template>
       </el-table-column>
-      <el-table-column
-        label="使用时间"
-        align="center"
-        prop="usedTime"
-        :formatter="dateFormatter"
-        width="180px"
-      />
-      <el-table-column label="备注" align="center" prop="remark" />
+      <el-table-column label="备注" align="center" prop="remark" min-width="180" />
       <el-table-column
         label="创建时间"
         align="center"

+ 2 - 2
src/views/mall/promotion/kefu/index.vue

@@ -16,7 +16,6 @@ import { KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
 import { getRefreshToken } from '@/utils/auth'
 import { useWebSocket } from '@vueuse/core'
 import { useMallKefuStore } from '@/store/modules/mall/kefu'
-import { jsonParse } from '@/utils'
 
 defineOptions({ name: 'KeFu' })
 
@@ -66,7 +65,8 @@ watch(
       // 2.3 消息类型:KEFU_MESSAGE_ADMIN_READ
       if (type === WebSocketMessageTypeConstants.KEFU_MESSAGE_ADMIN_READ) {
         // 更新会话已读
-        kefuStore.updateConversationStatus(jsonParse(jsonMessage.content))
+        const message = JSON.parse(jsonMessage.content)
+        kefuStore.updateConversationStatus(message.conversationId)
       }
     } catch (error) {
       console.error(error)

+ 1 - 1
src/views/mall/trade/order/components/OrderTableColumn.vue

@@ -120,7 +120,7 @@
               v-if="scope.row.deliveryType === DeliveryTypeEnum.EXPRESS.type"
               class="flex flex-col"
             >
-              <span>买家:{{ scope.row.user.nickname }}</span>
+              <span>买家:{{ scope.row.user?.nickname }}</span>
               <span>
                 收货人:{{ scope.row.receiverName }} {{ scope.row.receiverMobile }}
                 {{ scope.row.receiverAreaName }} {{ scope.row.receiverDetailAddress }}

+ 2 - 2
src/views/mp/material/components/upload.ts

@@ -1,8 +1,8 @@
 import type { UploadProps, UploadRawFile } from 'element-plus'
-import { getAccessToken } from '@/utils/auth'
+import { getRefreshToken } from '@/utils/auth'
 import { UploadType, useBeforeUpload } from '@/views/mp/hooks/useUpload'
 
-const HEADERS = { Authorization: 'Bearer ' + getAccessToken() } // 请求头
+const HEADERS = { Authorization: 'Bearer ' + getRefreshToken() } // 请求头(解决 el-upload 上传过程中,无法刷新令牌的问题)
 const UPLOAD_URL = import.meta.env.VITE_BASE_URL + '/admin-api/mp/material/upload-permanent' // 上传地址
 
 interface UploadData {

+ 5 - 1
src/views/report/goview/index.vue

@@ -6,7 +6,11 @@
   </ContentWrap>
 </template>
 <script lang="ts" setup>
+import { getAccessToken, getRefreshToken } from '@/utils/auth'
+
 defineOptions({ name: 'GoView' })
 
-const src = ref(import.meta.env.VITE_GOVIEW_URL)
+const src = ref(
+  `${import.meta.env.VITE_GOVIEW_URL}?accessToken=${getAccessToken()}&refreshToken=${getRefreshToken()}`
+)
 </script>

+ 15 - 0
src/views/report/jmreport/bi.vue

@@ -0,0 +1,15 @@
+<template>
+  <doc-alert title="大屏设计器" url="https://doc.iocoder.cn/screen/" />
+
+  <ContentWrap :bodyStyle="{ padding: '0px' }" class="!mb-0">
+    <IFrame :src="src" />
+  </ContentWrap>
+</template>
+<script lang="ts" setup>
+import { getRefreshToken } from '@/utils/auth'
+
+defineOptions({ name: 'JimuBI' })
+
+// 使用 getRefreshToken() 方法,而不使用 getAccessToken() 方法的原因:积木报表无法方便的刷新访问令牌
+const src = ref(import.meta.env.VITE_BASE_URL + '/drag/list?token=' + getRefreshToken())
+</script>

+ 1 - 0
src/views/system/menu/MenuForm.vue

@@ -147,6 +147,7 @@ const formData = ref({
 })
 const formRules = reactive({
   name: [{ required: true, message: '菜单名称不能为空', trigger: 'blur' }],
+  type: [{ required: true, message: '菜单类型不能为空', trigger: 'blur' }],
   sort: [{ required: true, message: '菜单顺序不能为空', trigger: 'blur' }],
   path: [{ required: true, message: '路由地址不能为空', trigger: 'blur' }],
   status: [{ required: true, message: '状态不能为空', trigger: 'blur' }]

+ 1 - 1
src/views/system/role/RoleDataPermissionForm.vue

@@ -20,7 +20,7 @@
     </el-form>
     <el-form-item
       v-if="formData.dataScope === SystemDataScopeEnum.DEPT_CUSTOM"
-      label="权限范围"
+      label="部门范围"
       label-width="80px"
     >
       <el-card class="w-full h-400px !overflow-y-scroll" shadow="never">

+ 2 - 2
src/views/system/user/index.vue

@@ -41,7 +41,7 @@
           <el-form-item label="状态" prop="status">
             <el-select
               v-model="queryParams.status"
-              placeholder="用户状态"
+              placeholder="请选择用户状态"
               clearable
               class="!w-240px"
             >
@@ -345,7 +345,7 @@ const handleResetPwd = async (row: UserApi.UserVO) => {
     )
     const password = result.value
     // 发起重置
-    await UserApi.resetUserPwd(row.id, password)
+    await UserApi.resetUserPassword(row.id, password)
     message.success('修改成功,新密码是:' + password)
   } catch {}
 }

+ 0 - 1
uno.config.ts

@@ -37,7 +37,6 @@ ${selector}:before {
   position: absolute;
   top: 0;
   left: 0;
-  width: 1px;
   height: 100%;
   background-color: var(--el-border-color);
   z-index: 3;