Răsfoiți Sursa

【代码优化】分支合并

puhui999 8 luni în urmă
părinte
comite
ffdd3325dd
48 a modificat fișierele cu 1314 adăugiri și 460 ștergeri
  1. 2 1
      .vscode/settings.json
  2. 1 1
      package.json
  3. 22 0
      src/api/ai/mindmap/index.ts
  4. 10 14
      src/api/mall/product/property.ts
  5. 2 0
      src/api/mall/product/spu.ts
  6. 1 0
      src/api/mall/promotion/seckill/seckillActivity.ts
  7. 1 0
      src/api/pay/app/index.ts
  8. 60 30
      src/components/DictTag/src/DictTag.vue
  9. 22 12
      src/components/DiyEditor/components/mobile/PromotionCombination/index.vue
  10. 22 12
      src/components/DiyEditor/components/mobile/PromotionSeckill/index.vue
  11. 2 0
      src/layout/components/Setting/src/Setting.vue
  12. 12 0
      src/layout/components/Setting/src/components/InterfaceDisplay.vue
  13. 80 33
      src/layout/components/TagsView/src/TagsView.vue
  14. 1 0
      src/locales/zh-CN.ts
  15. 11 2
      src/router/modules/remaining.ts
  16. 9 1
      src/store/modules/app.ts
  17. 3 1
      src/store/modules/permission.ts
  18. 1 0
      src/styles/index.scss
  19. 11 0
      src/styles/theme.scss
  20. 6 4
      src/views/Login/Login.vue
  21. 15 12
      src/views/Login/SocialLogin.vue
  22. 5 4
      src/views/Login/components/LoginForm.vue
  23. 1 1
      src/views/Login/components/QrCodeForm.vue
  24. 1 1
      src/views/Login/components/RegisterForm.vue
  25. 9 6
      src/views/ai/mindmap/index/components/Right.vue
  26. 3 3
      src/views/ai/mindmap/index/index.vue
  27. 195 0
      src/views/ai/mindmap/manager/index.vue
  28. 18 18
      src/views/ai/write/index/components/Left.vue
  29. 21 6
      src/views/mall/home/components/OperationDataCard.vue
  30. 3 3
      src/views/mall/product/spu/components/SkuList.vue
  31. 44 7
      src/views/mall/product/spu/form/ProductAttributes.vue
  32. 53 2
      src/views/mall/product/spu/form/ProductPropertyAddForm.vue
  33. 9 2
      src/views/mall/product/spu/form/SkuForm.vue
  34. 1 1
      src/views/mall/promotion/kefu/components/history/MemberBrowsingHistory.vue
  35. 8 11
      src/views/mall/trade/afterSale/index.vue
  36. 10 34
      src/views/member/user/detail/UserAccountInfo.vue
  37. 276 0
      src/views/member/user/detail/UserAftersaleList.vue
  38. 68 0
      src/views/member/user/detail/UserBalanceList.vue
  39. 25 5
      src/views/member/user/detail/index.vue
  40. 11 8
      src/views/pay/app/components/AppForm.vue
  41. 80 55
      src/views/pay/app/components/channel/AlipayChannelForm.vue
  42. 4 4
      src/views/pay/app/components/channel/MockChannelForm.vue
  43. 4 4
      src/views/pay/app/components/channel/WalletChannelForm.vue
  44. 35 41
      src/views/pay/app/components/channel/WeixinChannelForm.vue
  45. 51 43
      src/views/pay/app/index.vue
  46. 5 5
      stylelint.config.js
  47. 5 5
      tsconfig.json
  48. 75 68
      vite.config.ts

+ 2 - 1
.vscode/settings.json

@@ -83,7 +83,8 @@
     "editor.defaultFormatter": "esbenp.prettier-vscode"
   },
   "editor.codeActionsOnSave": {
-    "source.fixAll.eslint": "explicit"
+    "source.fixAll.eslint": "explicit",
+    "source.fixAll.stylelint": "explicit"
   },
   "[vue]": {
     "editor.defaultFormatter": "rvest.vs-code-prettier-eslint"

+ 1 - 1
package.json

@@ -47,7 +47,7 @@
     "driver.js": "^1.3.1",
     "echarts": "^5.5.0",
     "echarts-wordcloud": "^2.1.0",
-    "element-plus": "2.7.0",
+    "element-plus": "2.8.0",
     "fast-xml-parser": "^4.3.2",
     "highlight.js": "^11.9.0",
     "jsencrypt": "^3.3.2",

+ 22 - 0
src/api/ai/mindmap/index.ts

@@ -1,7 +1,20 @@
 import { getAccessToken } from '@/utils/auth'
 import { fetchEventSource } from '@microsoft/fetch-event-source'
 import { config } from '@/config/axios/config'
+import request from '@/config/axios' // AI 思维导图 VO
 
+// AI 思维导图 VO
+export interface MindMapVO {
+  id: number // 编号
+  userId: number // 用户编号
+  prompt: string // 生成内容提示
+  generatedContent: string // 生成的思维导图内容
+  platform: string // 平台
+  model: string // 模型
+  errorMessage: string // 错误信息
+}
+
+// AI 思维导图生成 VO
 export interface AiMindMapGenerateReqVO {
   prompt: string
 }
@@ -34,5 +47,14 @@ export const AiMindMapApi = {
       onclose: onClose,
       signal: ctrl.signal
     })
+  },
+
+  // 查询思维导图分页
+  getMindMapPage: async (params: any) => {
+    return await request.get({ url: `/ai/mind-map/page`, params })
+  },
+  // 删除思维导图
+  deleteMindMap: async (id: number) => {
+    return await request.delete({ url: `/ai/mind-map/delete?id=` + id })
   }
 }

+ 10 - 14
src/api/mall/product/property.ts

@@ -24,20 +24,6 @@ export interface PropertyValueVO {
   remark?: string
 }
 
-/**
- * 商品属性值的明细
- */
-export interface PropertyValueDetailVO {
-  /** 属性项的编号 */
-  propertyId: number // 属性的编号
-  /** 属性的名称 */
-  propertyName: string
-  /** 属性值的编号 */
-  valueId: number
-  /** 属性值的名称 */
-  valueName: string
-}
-
 // ------------------------ 属性项 -------------------
 
 // 创建属性项
@@ -65,6 +51,11 @@ export const getPropertyPage = (params: PageParam) => {
   return request.get({ url: '/product/property/page', params })
 }
 
+// 获得属性项精简列表
+export const getPropertySimpleList = (): Promise<PropertyVO[]> => {
+  return request.get({ url: '/product/property/simple-list' })
+}
+
 // ------------------------ 属性值 -------------------
 
 // 获得属性值分页
@@ -91,3 +82,8 @@ export const updatePropertyValue = (data: PropertyValueVO) => {
 export const deletePropertyValue = (id: number) => {
   return request.delete({ url: `/product/property/value/delete?id=${id}` })
 }
+
+// 获得属性值精简列表
+export const getPropertyValueSimpleList = (propertyId: number): Promise<PropertyValueVO[]> => {
+  return request.get({ url: '/product/property/value/simple-list', params: { propertyId } })
+}

+ 2 - 0
src/api/mall/product/spu.ts

@@ -50,6 +50,8 @@ export interface Spu {
   giveIntegral?: number // 赠送积分
   virtualSalesCount?: number // 虚拟销量
   price?: number // 商品价格
+  combinationPrice?: number // 商品拼团价格
+  seckillPrice?: number // 商品秒杀价格
   salesCount?: number // 商品销量
   marketPrice?: number // 市场价
   costPrice?: number // 成本价

+ 1 - 0
src/api/mall/promotion/seckill/seckillActivity.ts

@@ -24,6 +24,7 @@ export interface SeckillActivityVO {
 // 秒杀活动所需属性
 export interface SeckillProductVO {
   skuId: number
+  spuId: number
   seckillPrice: number
   stock: number
 }

+ 1 - 0
src/api/pay/app/index.ts

@@ -2,6 +2,7 @@ import request from '@/config/axios'
 
 export interface AppVO {
   id: number
+  appKey: string
   name: string
   status: number
   remark: string

+ 60 - 30
src/components/DictTag/src/DictTag.vue

@@ -1,8 +1,9 @@
 <script lang="tsx">
-import { defineComponent, PropType, ref } from 'vue'
+import { computed, defineComponent, PropType } from 'vue'
 import { isHexColor } from '@/utils/color'
 import { ElTag } from 'element-plus'
 import { DictDataType, getDictOptions } from '@/utils/dict'
+import { isArray, isBoolean, isNumber, isString } from '@/utils/is'
 
 export default defineComponent({
   name: 'DictTag',
@@ -12,49 +13,78 @@ export default defineComponent({
       required: true
     },
     value: {
-      type: [String, Number, Boolean] as PropType<string | number | boolean>,
+      type: [String, Number, Boolean, Array],
       required: true
+    },
+    // 字符串分隔符 只有当 props.value 传入值为字符串时有效
+    separator: {
+      type: String as PropType<string>,
+      default: ','
+    },
+    // 每个 tag 之间的间隔,默认为 5px,参考的 el-row 的 gutter
+    gutter: {
+      type: String as PropType<string>,
+      default: '5px'
     }
   },
   setup(props) {
-    const dictData = ref<DictDataType>()
-    const getDictObj = (dictType: string, value: string) => {
-      const dictOptions = getDictOptions(dictType)
-      dictOptions.forEach((dict: DictDataType) => {
-        if (dict.value === value) {
-          if (dict.colorType + '' === 'default') {
-            dict.colorType = 'info'
-          }
-          dictData.value = dict
-        }
-      })
-    }
-    const rederDictTag = () => {
+    const valueArr: any = computed(() => {
+      // 1. 是 Number 类型和 Boolean 类型的情况
+      if (isNumber(props.value) || isBoolean(props.value)) {
+        return [String(props.value)]
+      }
+      // 2. 是字符串(进一步判断是否有包含分隔符号 -> props.sepSymbol )
+      else if (isString(props.value)) {
+        return props.value.split(props.separator)
+      }
+      // 3. 数组
+      else if (isArray(props.value)) {
+        return props.value.map(String)
+      }
+      return []
+    })
+    const renderDictTag = () => {
       if (!props.type) {
         return null
       }
       // 解决自定义字典标签值为零时标签不渲染的问题
-      if (props.value === undefined || props.value === null) {
+      if (props.value === undefined || props.value === null || props.value === '') {
         return null
       }
-      getDictObj(props.type, props.value.toString())
-      // 添加标签的文字颜色为白色,解决自定义背景颜色时标签文字看不清的问题
+      const dictOptions = getDictOptions(props.type)
+
       return (
-        <ElTag
-          style={dictData.value?.cssClass ? 'color: #fff' : ''}
-          type={dictData.value?.colorType}
-          color={
-            dictData.value?.cssClass && isHexColor(dictData.value?.cssClass)
-              ? dictData.value?.cssClass
-              : ''
-          }
-          disableTransitions={true}
+        <div
+          class="dict-tag"
+          style={{
+            display: 'inline-flex',
+            gap: props.gutter,
+            justifyContent: 'center',
+            alignItems: 'center'
+          }}
         >
-          {dictData.value?.label}
-        </ElTag>
+          {dictOptions.map((dict: DictDataType) => {
+            if (valueArr.value.includes(dict.value)) {
+              if (dict.colorType + '' === 'primary' || dict.colorType + '' === 'default') {
+                dict.colorType = ''
+              }
+              return (
+                // 添加标签的文字颜色为白色,解决自定义背景颜色时标签文字看不清的问题
+                <ElTag
+                  style={dict?.cssClass ? 'color: #fff' : ''}
+                  type={dict?.colorType || null}
+                  color={dict?.cssClass && isHexColor(dict?.cssClass) ? dict?.cssClass : ''}
+                  disableTransitions={true}
+                >
+                  {dict?.label}
+                </ElTag>
+              )
+            }
+          })}
+        </div>
       )
     }
-    return () => rederDictTag()
+    return () => renderDictTag()
   }
 })
 </script>

+ 22 - 12
src/components/DiyEditor/components/mobile/PromotionCombination/index.vue

@@ -1,35 +1,35 @@
 <template>
-  <el-scrollbar class="z-1 min-h-30px" wrap-class="w-full" ref="containerRef">
+  <el-scrollbar ref="containerRef" class="z-1 min-h-30px" wrap-class="w-full">
     <!-- 商品网格 -->
     <div
-      class="grid overflow-x-auto"
       :style="{
         gridGap: `${property.space}px`,
         gridTemplateColumns,
         width: scrollbarWidth
       }"
+      class="grid overflow-x-auto"
     >
       <!-- 商品 -->
       <div
-        class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white"
+        v-for="(spu, index) in spuList"
+        :key="index"
         :style="{
           borderTopLeftRadius: `${property.borderRadiusTop}px`,
           borderTopRightRadius: `${property.borderRadiusTop}px`,
           borderBottomLeftRadius: `${property.borderRadiusBottom}px`,
           borderBottomRightRadius: `${property.borderRadiusBottom}px`
         }"
-        v-for="(spu, index) in spuList"
-        :key="index"
+        class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white"
       >
         <!-- 角标 -->
         <div
           v-if="property.badge.show"
           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" />
+          <el-image :src="property.badge.imgUrl" class="h-26px w-38px" fit="cover" />
         </div>
         <!-- 商品封面图 -->
-        <el-image fit="cover" :src="spu.picUrl" :style="{ width: imageSize, height: imageSize }" />
+        <el-image :src="spu.picUrl" :style="{ width: imageSize, height: imageSize }" fit="cover" />
         <div
           :class="[
             'flex flex-col gap-8px p-8px box-border',
@@ -42,8 +42,8 @@
           <!-- 商品名称 -->
           <div
             v-if="property.fields.name.show"
-            class="truncate text-12px"
             :style="{ color: property.fields.name.color }"
+            class="truncate text-12px"
           >
             {{ spu.name }}
           </div>
@@ -51,10 +51,10 @@
             <!-- 商品价格 -->
             <span
               v-if="property.fields.price.show"
-              class="text-12px"
               :style="{ color: property.fields.price.color }"
+              class="text-12px"
             >
-              ¥{{ spu.price }}
+              ¥{{ fenToYuan(spu.combinationPrice || spu.price || 0) }}
             </span>
           </div>
         </div>
@@ -62,10 +62,13 @@
     </div>
   </el-scrollbar>
 </template>
-<script setup lang="ts">
+<script lang="ts" setup>
 import { PromotionCombinationProperty } from './config'
 import * as ProductSpuApi from '@/api/mall/product/spu'
+import { Spu } from '@/api/mall/product/spu'
 import * as CombinationActivityApi from '@/api/mall/promotion/combination/combinationActivity'
+import { CombinationProductVO } from '@/api/mall/promotion/combination/combinationActivity'
+import { fenToYuan } from '@/utils'
 
 /** 拼团 */
 defineOptions({ name: 'PromotionCombination' })
@@ -80,6 +83,13 @@ watch(
     const activity = await CombinationActivityApi.getCombinationActivity(props.property.activityId)
     if (!activity?.spuId) return
     spuList.value = [await ProductSpuApi.getSpu(activity.spuId)]
+    // 循环活动信息,赋值拼团价格
+    activity.products.forEach((product: CombinationProductVO) => {
+      spuList.value.forEach((spu: Spu) => {
+        // 商品原售价和拼团价,哪个便宜就赋值哪个
+        spu.combinationPrice = Math.min(spu.combinationPrice || Infinity, product.combinationPrice) // 设置 SPU 的最低价格
+      })
+    })
   },
   {
     immediate: true,
@@ -122,4 +132,4 @@ onMounted(() => {
 })
 </script>
 
-<style scoped lang="scss"></style>
+<style lang="scss" scoped></style>

+ 22 - 12
src/components/DiyEditor/components/mobile/PromotionSeckill/index.vue

@@ -1,35 +1,35 @@
 <template>
-  <el-scrollbar class="z-1 min-h-30px" wrap-class="w-full" ref="containerRef">
+  <el-scrollbar ref="containerRef" class="z-1 min-h-30px" wrap-class="w-full">
     <!-- 商品网格 -->
     <div
-      class="grid overflow-x-auto"
       :style="{
         gridGap: `${property.space}px`,
         gridTemplateColumns,
         width: scrollbarWidth
       }"
+      class="grid overflow-x-auto"
     >
       <!-- 商品 -->
       <div
-        class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white"
+        v-for="(spu, index) in spuList"
+        :key="index"
         :style="{
           borderTopLeftRadius: `${property.borderRadiusTop}px`,
           borderTopRightRadius: `${property.borderRadiusTop}px`,
           borderBottomLeftRadius: `${property.borderRadiusBottom}px`,
           borderBottomRightRadius: `${property.borderRadiusBottom}px`
         }"
-        v-for="(spu, index) in spuList"
-        :key="index"
+        class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white"
       >
         <!-- 角标 -->
         <div
           v-if="property.badge.show"
           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" />
+          <el-image :src="property.badge.imgUrl" class="h-26px w-38px" fit="cover" />
         </div>
         <!-- 商品封面图 -->
-        <el-image fit="cover" :src="spu.picUrl" :style="{ width: imageSize, height: imageSize }" />
+        <el-image :src="spu.picUrl" :style="{ width: imageSize, height: imageSize }" fit="cover" />
         <div
           :class="[
             'flex flex-col gap-8px p-8px box-border',
@@ -42,8 +42,8 @@
           <!-- 商品名称 -->
           <div
             v-if="property.fields.name.show"
-            class="truncate text-12px"
             :style="{ color: property.fields.name.color }"
+            class="truncate text-12px"
           >
             {{ spu.name }}
           </div>
@@ -51,10 +51,10 @@
             <!-- 商品价格 -->
             <span
               v-if="property.fields.price.show"
-              class="text-12px"
               :style="{ color: property.fields.price.color }"
+              class="text-12px"
             >
-              ¥{{ spu.price }}
+              ¥{{ fenToYuan(spu.seckillPrice || spu.price || 0) }}
             </span>
           </div>
         </div>
@@ -62,10 +62,13 @@
     </div>
   </el-scrollbar>
 </template>
-<script setup lang="ts">
+<script lang="ts" setup>
 import { PromotionSeckillProperty } from './config'
 import * as ProductSpuApi from '@/api/mall/product/spu'
+import { Spu } from '@/api/mall/product/spu'
 import * as SeckillActivityApi from '@/api/mall/promotion/seckill/seckillActivity'
+import { SeckillProductVO } from '@/api/mall/promotion/seckill/seckillActivity'
+import { fenToYuan } from '@/utils'
 
 /** 秒杀 */
 defineOptions({ name: 'PromotionSeckill' })
@@ -80,6 +83,13 @@ watch(
     const activity = await SeckillActivityApi.getSeckillActivity(props.property.activityId)
     if (!activity?.spuId) return
     spuList.value = [await ProductSpuApi.getSpu(activity.spuId)]
+    spuList.value = [await ProductSpuApi.getSpu(activity.spuId)]
+    // 循环活动信息,赋值秒杀最低价格
+    activity.products.forEach((product: SeckillProductVO) => {
+      spuList.value.forEach((spu: Spu) => {
+        spu.seckillPrice = Math.min(spu.seckillPrice || Infinity, product.seckillPrice) // 设置 SPU 的最低价格
+      })
+    })
   },
   {
     immediate: true,
@@ -122,4 +132,4 @@ onMounted(() => {
 })
 </script>
 
-<style scoped lang="scss"></style>
+<style lang="scss" scoped></style>

+ 2 - 0
src/layout/components/Setting/src/Setting.vue

@@ -126,6 +126,8 @@ const copyConfig = async () => {
       message: ${appStore.getMessage},
       // 标签页
       tagsView: ${appStore.getTagsView},
+      // 标签页
+      tagsViewImmerse: ${appStore.getTagsViewImmerse},
       // 标签页图标
       getTagsViewIcon: ${appStore.getTagsViewIcon},
       // logo

+ 12 - 0
src/layout/components/Setting/src/components/InterfaceDisplay.vue

@@ -73,6 +73,13 @@ const tagsViewChange = (show: boolean) => {
   appStore.setTagsView(show)
 }
 
+// 标签页沉浸
+const tagsViewImmerse = ref(appStore.getTagsViewImmerse)
+
+const tagsViewImmerseChange = (immerse: boolean) => {
+  appStore.setTagsViewImmerse(immerse)
+}
+
 // 标签页图标
 const tagsViewIcon = ref(appStore.getTagsViewIcon)
 
@@ -181,6 +188,11 @@ watch(
       <ElSwitch v-model="tagsView" @change="tagsViewChange" />
     </div>
 
+    <div class="flex items-center justify-between">
+      <span class="text-14px">{{ t('setting.tagsViewImmerse') }}</span>
+      <ElSwitch v-model="tagsViewImmerse" @change="tagsViewImmerseChange" />
+    </div>
+
     <div class="flex items-center justify-between">
       <span class="text-14px">{{ t('setting.tagsViewIcon') }}</span>
       <ElSwitch v-model="tagsViewIcon" @change="tagsViewIconChange" />

+ 80 - 33
src/layout/components/TagsView/src/TagsView.vue

@@ -1,7 +1,7 @@
 <script lang="ts" setup>
-import { onMounted, watch, computed, unref, ref, nextTick } from 'vue'
-import { useRouter } from 'vue-router'
+import { computed, nextTick, onMounted, ref, unref, watch } from 'vue'
 import type { RouteLocationNormalizedLoaded, RouterLinkProps } from 'vue-router'
+import { useRouter } from 'vue-router'
 import { usePermissionStore } from '@/store/modules/permission'
 import { useTagsViewStore } from '@/store/modules/tagsView'
 import { useAppStore } from '@/store/modules/app'
@@ -33,6 +33,8 @@ const affixTagArr = ref<RouteLocationNormalizedLoaded[]>([])
 
 const appStore = useAppStore()
 
+const tagsViewImmerse = computed(() => appStore.getTagsViewImmerse)
+
 const tagsViewIcon = computed(() => appStore.getTagsViewIcon)
 
 const isDark = computed(() => appStore.getIsDark)
@@ -266,21 +268,33 @@ watch(
     class="relative w-full flex bg-[#fff] dark:bg-[var(--el-bg-color)]"
   >
     <span
-      :class="`${prefixCls}__tool ${prefixCls}__tool--first`"
+      :class="tagsViewImmerse ? '' : `${prefixCls}__tool ${prefixCls}__tool--first`"
       class="h-[var(--tags-view-height)] w-[var(--tags-view-height)] flex cursor-pointer items-center justify-center"
       @click="move(-200)"
     >
       <Icon
-        icon="ep:d-arrow-left"
-        color="var(--el-text-color-placeholder)"
         :hover-color="isDark ? '#fff' : 'var(--el-color-black)'"
+        color="var(--el-text-color-placeholder)"
+        icon="ep:d-arrow-left"
       />
     </span>
     <div class="flex-1 overflow-hidden">
       <ElScrollbar ref="scrollbarRef" class="h-full" @scroll="scroll">
         <div class="h-full flex">
           <ContextMenu
+            v-for="item in visitedViews"
+            :key="item.fullPath"
             :ref="itemRefs.set"
+            :class="[
+              `${prefixCls}__item`,
+              tagsViewImmerse ? `${prefixCls}__item--immerse` : '',
+              tagsViewIcon ? `${prefixCls}__item--icon` : '',
+              tagsViewImmerse && tagsViewIcon ? `${prefixCls}__item--immerse--icon` : '',
+              item?.meta?.affix ? `${prefixCls}__item--affix` : '',
+              {
+                'is-active': isActive(item)
+              }
+            ]"
             :schema="[
               {
                 icon: 'ep:refresh',
@@ -338,41 +352,33 @@ watch(
                 }
               }
             ]"
-            v-for="item in visitedViews"
-            :key="item.fullPath"
             :tag-item="item"
-            :class="[
-              `${prefixCls}__item`,
-              item?.meta?.affix ? `${prefixCls}__item--affix` : '',
-              {
-                'is-active': isActive(item)
-              }
-            ]"
             @visible-change="visibleChange"
           >
             <div>
-              <router-link :ref="tagLinksRefs.set" :to="{ ...item }" custom v-slot="{ navigate }">
+              <router-link :ref="tagLinksRefs.set" v-slot="{ navigate }" :to="{ ...item }" custom>
                 <div
+                  :class="`h-full flex items-center justify-center whitespace-nowrap pl-15px ${prefixCls}__item--label`"
                   @click="navigate"
-                  class="h-full flex items-center justify-center whitespace-nowrap pl-15px"
                 >
                   <Icon
                     v-if="
-                      item?.matched &&
-                      item?.matched[1] &&
-                      item?.matched[1]?.meta?.icon &&
-                      tagsViewIcon
+                      tagsViewIcon &&
+                      (item?.meta?.icon ||
+                        (item?.matched &&
+                          item.matched[0] &&
+                          item.matched[item.matched.length - 1].meta?.icon))
                     "
-                    :icon="item?.matched[1]?.meta?.icon"
+                    :icon="item?.meta?.icon || item.matched[item.matched.length - 1].meta.icon"
                     :size="12"
                     class="mr-5px"
                   />
                   {{ t(item?.meta?.title as string) }}
                   <Icon
                     :class="`${prefixCls}__item--close`"
+                    :size="12"
                     color="#333"
                     icon="ep:close"
-                    :size="12"
                     @click.prevent.stop="closeSelectedTag(item)"
                   />
                 </div>
@@ -383,29 +389,28 @@ watch(
       </ElScrollbar>
     </div>
     <span
-      :class="`${prefixCls}__tool`"
+      :class="tagsViewImmerse ? '' : `${prefixCls}__tool`"
       class="h-[var(--tags-view-height)] w-[var(--tags-view-height)] flex cursor-pointer items-center justify-center"
       @click="move(200)"
     >
       <Icon
-        icon="ep:d-arrow-right"
-        color="var(--el-text-color-placeholder)"
         :hover-color="isDark ? '#fff' : 'var(--el-color-black)'"
+        color="var(--el-text-color-placeholder)"
+        icon="ep:d-arrow-right"
       />
     </span>
     <span
-      :class="`${prefixCls}__tool`"
+      :class="tagsViewImmerse ? '' : `${prefixCls}__tool`"
       class="h-[var(--tags-view-height)] w-[var(--tags-view-height)] flex cursor-pointer items-center justify-center"
       @click="refreshSelectedTag(selectedTag)"
     >
       <Icon
-        icon="ep:refresh-right"
-        color="var(--el-text-color-placeholder)"
         :hover-color="isDark ? '#fff' : 'var(--el-color-black)'"
+        color="var(--el-text-color-placeholder)"
+        icon="ep:refresh-right"
       />
     </span>
     <ContextMenu
-      trigger="click"
       :schema="[
         {
           icon: 'ep:refresh',
@@ -457,15 +462,16 @@ watch(
           }
         }
       ]"
+      trigger="click"
     >
       <span
-        :class="`${prefixCls}__tool`"
+        :class="tagsViewImmerse ? '' : `${prefixCls}__tool`"
         class="block h-[var(--tags-view-height)] w-[var(--tags-view-height)] flex cursor-pointer items-center justify-center"
       >
         <Icon
-          icon="ep:menu"
-          color="var(--el-text-color-placeholder)"
           :hover-color="isDark ? '#fff' : 'var(--el-color-black)'"
+          color="var(--el-text-color-placeholder)"
+          icon="ep:menu"
         />
       </span>
     </ContextMenu>
@@ -511,7 +517,7 @@ $prefix-cls: #{$namespace}-tags-view;
     position: relative;
     top: 2px;
     height: calc(100% - 6px);
-    padding-right: 25px;
+    padding-right: 15px;
     margin-left: 4px;
     font-size: 12px;
     cursor: pointer;
@@ -525,6 +531,7 @@ $prefix-cls: #{$namespace}-tags-view;
       display: none;
       transform: translate(0, -50%);
     }
+
     &:not(.#{$prefix-cls}__item--affix):hover {
       .#{$prefix-cls}__item--close {
         display: block;
@@ -532,6 +539,10 @@ $prefix-cls: #{$namespace}-tags-view;
     }
   }
 
+  &__item--icon {
+    padding-right: 20px;
+  }
+
   &__item:not(.is-active) {
     &:hover {
       color: var(--el-color-primary);
@@ -542,12 +553,47 @@ $prefix-cls: #{$namespace}-tags-view;
     color: var(--el-color-white);
     background-color: var(--el-color-primary);
     border: 1px solid var(--el-color-primary);
+
     .#{$prefix-cls}__item--close {
       :deep(span) {
         color: var(--el-color-white) !important;
       }
     }
   }
+
+  &__item--immerse {
+    top: 3px;
+    padding-right: 35px;
+    margin: 0 -10px;
+    border: 1px solid transparent;
+    -webkit-mask-box-image: url("data:image/svg+xml,%3Csvg width='68' height='34' viewBox='0 0 68 34' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='m27,0c-7.99582,0 -11.95105,0.00205 -12,12l0,6c0,8.284 -0.48549,16.49691 -8.76949,16.49691l54.37857,-0.11145c-8.284,0 -8.60908,-8.10146 -8.60908,-16.38546l0,-6c0.11145,-12.08445 -4.38441,-12 -12,-12l-13,0z' fill='%23409eff'/%3E%3C/svg%3E")
+      12 27 15;
+
+    .#{$prefix-cls}__item--label {
+      padding-left: 35px;
+    }
+
+    .#{$prefix-cls}__item--close {
+      right: 20px;
+    }
+  }
+
+  &__item--immerse--icon {
+    padding-right: 35px;
+  }
+
+  &__item--immerse:not(.is-active) {
+    &:hover {
+      color: var(--el-color-white);
+      background-color: var(--el-color-primary);
+
+      .#{$prefix-cls}__item--close {
+        :deep(span) {
+          color: var(--el-color-white) !important;
+        }
+      }
+    }
+  }
 }
 
 .dark {
@@ -574,6 +620,7 @@ $prefix-cls: #{$namespace}-tags-view;
       color: var(--el-color-white);
       background-color: var(--el-color-primary);
       border: 1px solid var(--el-color-primary);
+
       .#{$prefix-cls}__item--close {
         :deep(span) {
           color: var(--el-color-white) !important;

+ 1 - 0
src/locales/zh-CN.ts

@@ -92,6 +92,7 @@ export default {
     localeIcon: '多语言图标',
     messageIcon: '消息图标',
     tagsView: '标签页',
+    tagsViewImmerse: '标签页沉浸',
     logo: '标志',
     greyMode: '灰色模式',
     fixedHeader: '固定头部',

+ 11 - 2
src/router/modules/remaining.ts

@@ -589,11 +589,20 @@ const remainingRouter: AppRouteRecordRaw[] = [
         meta: {
           title: '绘图作品',
           icon: 'ep:home-filled',
-          noCache: false,
-          affix: true
+          noCache: false
         }
       }
     ]
+  },
+  {
+    path: '/:pathMatch(.*)*',
+    component: () => import('@/views/Error/404.vue'),
+    name: '',
+    meta: {
+      title: '404',
+      hidden: true,
+      breadcrumb: false
+    }
   }
 ]
 

+ 9 - 1
src/store/modules/app.ts

@@ -1,6 +1,6 @@
 import { defineStore } from 'pinia'
 import { store } from '../index'
-import { setCssVar, humpToUnderline } from '@/utils'
+import { humpToUnderline, setCssVar } from '@/utils'
 import { ElMessage } from 'element-plus'
 import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
 import { ElementPlusSize } from '@/types/elementPlus'
@@ -21,6 +21,7 @@ interface AppState {
   locale: boolean
   message: boolean
   tagsView: boolean
+  tagsViewImmerse: boolean
   tagsViewIcon: boolean
   logo: boolean
   fixedHeader: boolean
@@ -58,6 +59,7 @@ export const useAppStore = defineStore('app', {
       locale: true, // 多语言图标
       message: true, // 消息图标
       tagsView: true, // 标签页
+      tagsViewImmerse: false, // 标签页沉浸
       tagsViewIcon: true, // 是否显示标签图标
       logo: true, // logo
       fixedHeader: true, // 固定toolheader
@@ -131,6 +133,9 @@ export const useAppStore = defineStore('app', {
     getTagsView(): boolean {
       return this.tagsView
     },
+    getTagsViewImmerse(): boolean {
+      return this.tagsViewImmerse
+    },
     getTagsViewIcon(): boolean {
       return this.tagsViewIcon
     },
@@ -208,6 +213,9 @@ export const useAppStore = defineStore('app', {
     setTagsView(tagsView: boolean) {
       this.tagsView = tagsView
     },
+    setTagsViewImmerse(tagsViewImmerse: boolean) {
+      this.tagsViewImmerse = tagsViewImmerse
+    },
     setTagsViewIcon(tagsViewIcon: boolean) {
       this.tagsViewIcon = tagsViewIcon
     },

+ 3 - 1
src/store/modules/permission.ts

@@ -40,10 +40,12 @@ export const usePermissionStore = defineStore('permission', {
         }
         const routerMap: AppRouteRecordRaw[] = generateRoute(res)
         // 动态路由,404一定要放到最后面
+        // preschooler:vue-router@4以后已支持静态404路由,此处可不再追加
         this.addRouters = routerMap.concat([
           {
             path: '/:path(.*)*',
-            redirect: '/404',
+            // redirect: '/404',
+            component: () => import('@/views/Error/404.vue'),
             name: '404Page',
             meta: {
               hidden: true,

+ 1 - 0
src/styles/index.scss

@@ -1,5 +1,6 @@
 @import './var.css';
 @import './FormCreate/index.scss';
+@import './theme.scss';
 @import 'element-plus/theme-chalk/dark/css-vars.css';
 
 .reset-margin [class*='el-icon'] + span {

+ 11 - 0
src/styles/theme.scss

@@ -4,3 +4,14 @@
 // .dark .dark\:text-color {
 //   color: rgba(255, 255, 255, var(--dark-text-color));
 // }
+
+// 登录页
+.dark .login-form {
+  .el-divider__text {
+    background-color: var(--login-bg-color);
+  }
+
+  .el-card {
+    background-color: var(--login-bg-color);
+  }
+}

+ 6 - 4
src/views/Login/Login.vue

@@ -5,7 +5,7 @@
   >
     <div class="relative mx-auto h-full flex">
       <div
-        :class="`${prefixCls}__left flex-1 bg-gray-500 bg-opacity-20 relative p-30px lt-xl:hidden`"
+        :class="`${prefixCls}__left flex-1 bg-gray-500 bg-opacity-20 relative p-30px lt-xl:hidden overflow-x-hidden overflow-y-auto`"
       >
         <!-- 左上角的 logo + 系统标题 -->
         <div class="relative flex items-center text-white">
@@ -27,7 +27,9 @@
           </TransitionGroup>
         </div>
       </div>
-      <div class="relative flex-1 p-30px dark:bg-[var(--login-bg-color)] lt-sm:p-10px">
+      <div
+        class="relative flex-1 p-30px dark:bg-[var(--login-bg-color)] lt-sm:p-10px overflow-x-hidden overflow-y-auto"
+      >
         <!-- 右上角的主题、语言选择 -->
         <div
           class="flex items-center justify-between text-white at-2xl:justify-end at-xl:justify-end"
@@ -36,7 +38,7 @@
             <img alt="" class="mr-10px h-48px w-48px" src="@/assets/imgs/logo.png" />
             <span class="text-20px font-bold">{{ underlineToHump(appStore.getTitle) }}</span>
           </div>
-          <div class="flex items-center justify-end space-x-10px">
+          <div class="flex items-center justify-end space-x-10px h-48px">
             <ThemeSwitch />
             <LocaleDropdown class="dark:text-white lt-xl:text-white" />
           </div>
@@ -44,7 +46,7 @@
         <!-- 右边的登录界面 -->
         <Transition appear enter-active-class="animate__animated animate__bounceInRight">
           <div
-            class="m-auto h-full w-[100%] flex items-center at-2xl:max-w-500px at-lg:max-w-500px at-md:max-w-500px at-xl:max-w-500px"
+            class="m-auto h-[calc(100%-60px)] w-[100%] flex items-center at-2xl:max-w-500px at-lg:max-w-500px at-md:max-w-500px at-xl:max-w-500px"
           >
             <!-- 账号登录 -->
             <LoginForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />

+ 15 - 12
src/views/Login/SocialLogin.vue

@@ -1,11 +1,11 @@
 <template>
   <div
     :class="prefixCls"
-    class="relative h-[100%] lt-xl:bg-[var(--login-bg-color)] lt-md:px-10px lt-sm:px-10px lt-xl:px-10px"
+    class="relative h-[100%] lt-md:px-10px lt-sm:px-10px lt-xl:px-10px lt-xl:px-10px"
   >
     <div class="relative mx-auto h-full flex">
       <div
-        :class="`${prefixCls}__left flex-1 bg-gray-500 bg-opacity-20 relative p-30px lt-xl:hidden`"
+        :class="`${prefixCls}__left flex-1 bg-gray-500 bg-opacity-20 relative p-30px lt-xl:hidden overflow-x-hidden overflow-y-auto`"
       >
         <!-- 左上角的 logo + 系统标题 -->
         <div class="relative flex items-center text-white">
@@ -27,7 +27,9 @@
           </TransitionGroup>
         </div>
       </div>
-      <div class="relative flex-1 p-30px dark:bg-[var(--login-bg-color)] lt-sm:p-10px">
+      <div
+        class="relative flex-1 p-30px dark:bg-[var(--login-bg-color)] lt-sm:p-10px overflow-x-hidden overflow-y-auto"
+      >
         <!-- 右上角的主题、语言选择 -->
         <div
           class="flex items-center justify-between text-white at-2xl:justify-end at-xl:justify-end"
@@ -36,7 +38,7 @@
             <img alt="" class="mr-10px h-48px w-48px" src="@/assets/imgs/logo.png" />
             <span class="text-20px font-bold">{{ underlineToHump(appStore.getTitle) }}</span>
           </div>
-          <div class="flex items-center justify-end space-x-10px">
+          <div class="flex items-center justify-end space-x-10px h-48px">
             <ThemeSwitch />
             <LocaleDropdown class="dark:text-white lt-xl:text-white" />
           </div>
@@ -44,7 +46,7 @@
         <!-- 右边的登录界面 -->
         <Transition appear enter-active-class="animate__animated animate__bounceInRight">
           <div
-            class="m-auto h-full w-[100%] flex items-center at-2xl:max-w-500px at-lg:max-w-500px at-md:max-w-500px at-xl:max-w-500px"
+            class="m-auto h-[calc(100%-60px)] w-[100%] flex items-center at-2xl:max-w-500px at-lg:max-w-500px at-md:max-w-500px at-xl:max-w-500px"
           >
             <!-- 账号登录 -->
             <el-form
@@ -112,9 +114,9 @@
                         </el-checkbox>
                       </el-col>
                       <el-col :offset="6" :span="12">
-                        <el-link style="float: right" type="primary">{{
-                          t('login.forgetPassword')
-                        }}</el-link>
+                        <el-link style="float: right" type="primary"
+                          >{{ t('login.forgetPassword') }}
+                        </el-link>
                       </el-col>
                     </el-row>
                   </el-form-item>
@@ -274,10 +276,11 @@ const handleLogin = async (params) => {
     const code = route?.query?.code as string
     const state = route?.query?.state as string
 
+    const loginDataLoginForm = { ...loginData.loginForm }
     const res = await LoginApi.login({
       // 账号密码登录
-      username: loginData.loginForm.username,
-      password: loginData.loginForm.password,
+      username: loginDataLoginForm.username,
+      password: loginDataLoginForm.password,
       captchaVerification: params.captchaVerification,
       // 社交登录
       socialCode: code,
@@ -292,8 +295,8 @@ const handleLogin = async (params) => {
       text: '正在加载系统中...',
       background: 'rgba(0, 0, 0, 0.7)'
     })
-    if (loginData.loginForm.rememberMe) {
-      authUtil.setLoginForm(loginData.loginForm)
+    if (loginDataLoginForm.rememberMe) {
+      authUtil.setLoginForm(loginDataLoginForm)
     } else {
       authUtil.removeLoginForm()
     }

+ 5 - 4
src/views/Login/components/LoginForm.vue

@@ -249,8 +249,9 @@ const handleLogin = async (params) => {
     if (!data) {
       return
     }
-    loginData.loginForm.captchaVerification = params.captchaVerification
-    const res = await LoginApi.login(loginData.loginForm)
+    const loginDataLoginForm = { ...loginData.loginForm }
+    loginDataLoginForm.captchaVerification = params.captchaVerification
+    const res = await LoginApi.login(loginDataLoginForm)
     if (!res) {
       return
     }
@@ -259,8 +260,8 @@ const handleLogin = async (params) => {
       text: '正在加载系统中...',
       background: 'rgba(0, 0, 0, 0.7)'
     })
-    if (loginData.loginForm.rememberMe) {
-      authUtil.setLoginForm(loginData.loginForm)
+    if (loginDataLoginForm.rememberMe) {
+      authUtil.setLoginForm(loginDataLoginForm)
     } else {
       authUtil.removeLoginForm()
     }

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

@@ -1,5 +1,5 @@
 <template>
-  <el-row v-show="getShow" style="margin-right: -10px; margin-left: -10px">
+  <el-row v-show="getShow" class="login-form" style="margin-right: -10px; margin-left: -10px">
     <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
       <LoginFormTitle style="width: 100%" />
     </el-col>

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

@@ -3,7 +3,7 @@
     v-show="getShow"
     :rules="rules"
     :schema="schema"
-    class="dark:(border-1 border-[var(--el-border-color)] border-solid)"
+    class="w-[100%] dark:(border-1 border-[var(--el-border-color)] border-solid)"
     hide-required-asterisk
     label-position="top"
     size="large"

+ 9 - 6
src/views/ai/mindmap/index/components/Right.vue

@@ -4,7 +4,7 @@
       <h3 class="m-0 px-7 shrink-0 flex items-center justify-between">
         <span>思维导图预览</span>
         <!-- 展示在右上角 -->
-        <el-button type="primary" v-show="isEnd" @click="downloadImage" size="small">
+        <el-button v-show="isEnd" size="small" type="primary" @click="downloadImage">
           <template #icon>
             <Icon icon="ph:copy-bold" />
           </template>
@@ -20,14 +20,14 @@
       </div>
 
       <div ref="mindMapRef" class="wh-full">
-        <svg ref="svgRef" class="w-full" :style="{ height: `${contentAreaHeight}px` }" />
+        <svg ref="svgRef" :style="{ height: `${contentAreaHeight}px` }" class="w-full" />
         <div ref="toolBarRef" class="absolute bottom-[10px] right-5"></div>
       </div>
     </div>
   </el-card>
 </template>
 
-<script setup lang="ts">
+<script lang="ts" setup>
 import { Markmap } from 'markmap-view'
 import { Transformer } from 'markmap-lib'
 import { Toolbar } from 'markmap-toolbar'
@@ -43,7 +43,7 @@ const props = defineProps<{
   isGenerating: boolean // 是否正在生成
   isStart: boolean // 开始状态,开始时需要清除 html
 }>()
-const contentRef = ref<HTMLDivElement>() // 右侧出来header以下的区域
+const contentRef = ref<HTMLDivElement>() // 右侧出来 header 以下的区域
 const mdContainerRef = ref<HTMLDivElement>() // markdown 的容器,用来滚动到底下的
 const mindMapRef = ref<HTMLDivElement>() // 思维导图的容器
 const svgRef = ref<SVGElement>() // 思维导图的渲染 svg
@@ -106,8 +106,7 @@ const processContent = (text: string) => {
   return arr.join('\n')
 }
 
-/** 下载图片 */
-// download SVG to png file
+/** 下载图片:download SVG to png file */
 const downloadImage = () => {
   const svgElement = mindMapRef.value
   // 将 SVG 渲染到图片对象
@@ -138,6 +137,7 @@ defineExpose({
     height: 0;
   }
 }
+
 .my-card {
   display: flex;
   flex-direction: column;
@@ -150,13 +150,16 @@ defineExpose({
     @extend .hide-scroll-bar;
   }
 }
+
 // markmap的tool样式覆盖
 :deep(.markmap) {
   width: 100%;
 }
+
 :deep(.mm-toolbar-brand) {
   display: none;
 }
+
 :deep(.mm-toolbar) {
   display: flex;
   flex-direction: row;

+ 3 - 3
src/views/ai/mindmap/index/index.vue

@@ -3,9 +3,9 @@
     <!--表单区域-->
     <Left
       ref="leftRef"
+      :is-generating="isGenerating"
       @submit="submit"
       @direct-generate="directGenerate"
-      :is-generating="isGenerating"
     />
     <!--右边生成思维导图区域-->
     <Right
@@ -18,7 +18,7 @@
   </div>
 </template>
 
-<script setup lang="ts">
+<script lang="ts" setup>
 import Left from './components/Left.vue'
 import Right from './components/Right.vue'
 import { AiMindMapApi, AiMindMapGenerateReqVO } from '@/api/ai/mindmap'
@@ -40,7 +40,7 @@ const rightRef = ref<InstanceType<typeof Right>>() // 右边组件
 
 /** 使用已有内容直接生成 **/
 const directGenerate = (existPrompt: string) => {
-  isEnd.value = false // 先设置为false再设置为true,让子组建的watch能够监听到
+  isEnd.value = false // 先设置为 false 再设置为 true,让子组建的 watch 能够监听到
   generatedContent.value = existPrompt
   isEnd.value = true
 }

+ 195 - 0
src/views/ai/mindmap/manager/index.vue

@@ -0,0 +1,195 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      ref="queryFormRef"
+      :inline="true"
+      :model="queryParams"
+      class="-mb-15px"
+      label-width="68px"
+    >
+      <el-form-item label="用户编号" prop="userId">
+        <el-select
+          v-model="queryParams.userId"
+          class="!w-240px"
+          clearable
+          placeholder="请输入用户编号"
+        >
+          <el-option
+            v-for="item in userList"
+            :key="item.id"
+            :label="item.nickname"
+            :value="item.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="提示词" prop="prompt">
+        <el-input
+          v-model="queryParams.prompt"
+          class="!w-240px"
+          clearable
+          placeholder="请输入提示词"
+          @keyup.enter="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-220px"
+          end-placeholder="结束日期"
+          start-placeholder="开始日期"
+          type="daterange"
+          value-format="YYYY-MM-DD HH:mm:ss"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery">
+          <Icon class="mr-5px" icon="ep:search" />
+          搜索
+        </el-button>
+        <el-button @click="resetQuery">
+          <Icon class="mr-5px" icon="ep:refresh" />
+          重置
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
+      <el-table-column align="center" fixed="left" label="编号" prop="id" width="180" />
+      <el-table-column align="center" label="用户" prop="userId" width="180">
+        <template #default="scope">
+          <span>{{ userList.find((item) => item.id === scope.row.userId)?.nickname }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column align="center" label="提示词" prop="prompt" width="180" />
+      <el-table-column align="center" label="思维导图" min-width="300" prop="generatedContent" />
+      <el-table-column align="center" label="模型" prop="model" width="180" />
+      <el-table-column
+        :formatter="dateFormatter"
+        align="center"
+        label="创建时间"
+        prop="createTime"
+        width="180px"
+      />
+      <el-table-column align="center" label="错误信息" prop="errorMessage" />
+      <el-table-column align="center" fixed="right" label="操作" width="120">
+        <template #default="scope">
+          <el-button link type="primary" @click="openPreview(scope.row)"> 预览</el-button>
+          <el-button
+            v-hasPermi="['ai:mind-map:delete']"
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      v-model:limit="queryParams.pageSize"
+      v-model:page="queryParams.pageNo"
+      :total="total"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 思维导图的预览 -->
+  <el-drawer v-model="previewVisible" :with-header="false" size="800px">
+    <Right
+      v-if="previewVisible2"
+      :generatedContent="previewContent"
+      :isEnd="true"
+      :isGenerating="false"
+      :isStart="false"
+    />
+  </el-drawer>
+</template>
+
+<script lang="ts" setup>
+import { dateFormatter } from '@/utils/formatTime'
+import { AiMindMapApi, MindMapVO } from '@/api/ai/mindmap'
+import * as UserApi from '@/api/system/user'
+import Right from '@/views/ai/mindmap/index/components/Right.vue'
+
+/** AI 思维导图 列表 */
+defineOptions({ name: 'AiMindMapManager' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const list = ref<MindMapVO[]>([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  userId: undefined,
+  prompt: undefined,
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+const userList = ref<UserApi.UserVO[]>([]) // 用户列表
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await AiMindMapApi.getMindMapPage(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 AiMindMapApi.deleteMindMap(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 预览操作按钮 */
+const previewVisible = ref(false) // drawer 的显示隐藏
+const previewVisible2 = ref(false) // right 的显示隐藏
+const previewContent = ref('')
+const openPreview = async (row: MindMapVO) => {
+  previewVisible2.value = false
+  previewVisible.value = true
+  // 在 drawer 渲染完后,再渲染 right 预览,不然会报错,需要保证 width 宽度先出来
+  await nextTick()
+  previewVisible2.value = true
+  previewContent.value = row.generatedContent
+}
+
+/** 初始化 **/
+onMounted(async () => {
+  getList()
+  // 获得用户列表
+  userList.value = await UserApi.getSimpleUserList()
+})
+</script>

+ 18 - 18
src/views/ai/write/index/components/Left.vue

@@ -2,8 +2,8 @@
   <!-- 定义 tab 组件:撰写/回复等 -->
   <DefineTab v-slot="{ active, text, itemClick }">
     <span
-      class="inline-block w-1/2 rounded-full cursor-pointer text-center leading-[30px] relative z-1 text-[5C6370] hover:text-black"
       :class="active ? 'text-black shadow-md' : 'hover:bg-[#DDDFE3]'"
+      class="inline-block w-1/2 rounded-full cursor-pointer text-center leading-[30px] relative z-1 text-[5C6370] hover:text-black"
       @click="itemClick"
     >
       {{ text }}
@@ -14,9 +14,9 @@
     <h3 class="mt-5 mb-3 flex items-center justify-between text-[14px]">
       <span>{{ label }}</span>
       <span
-        @click="hintClick"
         v-if="hint"
         class="flex items-center text-[12px] text-[#846af7] cursor-pointer select-none"
+        @click="hintClick"
       >
         <Icon icon="ep:question-filled" />
         {{ hint }}
@@ -29,17 +29,17 @@
     <div class="w-full pt-2 bg-[#f5f7f9] flex justify-center">
       <div class="w-[303px] rounded-full bg-[#DDDFE3] p-1 z-10">
         <div
-          class="flex items-center relative after:content-[''] after:block after:bg-white after:h-[30px] after:w-1/2 after:absolute after:top-0 after:left-0 after:transition-transform after:rounded-full"
           :class="
             selectedTab === AiWriteTypeEnum.REPLY && 'after:transform after:translate-x-[100%]'
           "
+          class="flex items-center relative after:content-[''] after:block after:bg-white after:h-[30px] after:w-1/2 after:absolute after:top-0 after:left-0 after:transition-transform after:rounded-full"
         >
           <ReuseTab
             v-for="tab in tabs"
             :key="tab.value"
-            :text="tab.text"
             :active="tab.value === selectedTab"
             :itemClick="() => switchTab(tab.value)"
+            :text="tab.text"
           />
         </div>
       </div>
@@ -49,36 +49,36 @@
     >
       <div>
         <template v-if="selectedTab === 1">
-          <ReuseLabel label="写作内容" hint="示例" :hint-click="() => example('write')" />
+          <ReuseLabel :hint-click="() => example('write')" hint="示例" label="写作内容" />
           <el-input
-            type="textarea"
-            :rows="5"
-            :maxlength="500"
             v-model="formData.prompt"
+            :maxlength="500"
+            :rows="5"
             placeholder="请输入写作内容"
             showWordLimit
+            type="textarea"
           />
         </template>
 
         <template v-else>
-          <ReuseLabel label="原文" hint="示例" :hint-click="() => example('reply')" />
+          <ReuseLabel :hint-click="() => example('reply')" hint="示例" label="原文" />
           <el-input
-            type="textarea"
-            :rows="5"
-            :maxlength="500"
             v-model="formData.originalContent"
+            :maxlength="500"
+            :rows="5"
             placeholder="请输入原文"
             showWordLimit
+            type="textarea"
           />
 
           <ReuseLabel label="回复内容" />
           <el-input
-            type="textarea"
-            :rows="5"
-            :maxlength="500"
             v-model="formData.prompt"
+            :maxlength="500"
+            :rows="5"
             placeholder="请输入回复内容"
             showWordLimit
+            type="textarea"
           />
         </template>
 
@@ -93,18 +93,18 @@
 
         <div class="flex items-center justify-center mt-3">
           <el-button :disabled="isWriting" @click="reset">重置</el-button>
-          <el-button :loading="isWriting" @click="submit" color="#846af7">生成</el-button>
+          <el-button :loading="isWriting" color="#846af7" @click="submit">生成</el-button>
         </div>
       </div>
     </div>
   </div>
 </template>
 
-<script setup lang="ts">
+<script lang="ts" setup>
 import { createReusableTemplate } from '@vueuse/core'
 import { ref } from 'vue'
 import Tag from './Tag.vue'
-import { WriteVO } from 'src/api/ai/write'
+import { WriteVO } from '@/api/ai/write'
 import { omit } from 'lodash-es'
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import { AiWriteTypeEnum, WriteExample } from '@/views/ai/utils/constants'

+ 21 - 6
src/views/mall/home/components/OperationDataCard.vue

@@ -11,9 +11,9 @@
         @click="handleClick(item.routerName)"
       >
         <CountTo
-          :prefix="item.prefix"
-          :end-val="item.value"
           :decimals="item.decimals"
+          :end-val="item.value"
+          :prefix="item.prefix"
           class="text-3xl"
         />
         <span class="text-center">{{ item.name }}</span>
@@ -53,10 +53,18 @@ const data = reactive({
 /** 查询订单数据 */
 const getOrderData = async () => {
   const orderCount = await TradeStatisticsApi.getOrderCount()
-  data.orderUndelivered.value = orderCount.undelivered
-  data.orderAfterSaleApply.value = orderCount.afterSaleApply
-  data.orderWaitePickUp.value = orderCount.pickUp
-  data.withdrawAuditing.value = orderCount.auditingWithdraw
+  if (orderCount.undelivered != null) {
+    data.orderUndelivered.value = orderCount.undelivered
+  }
+  if (orderCount.afterSaleApply != null) {
+    data.orderAfterSaleApply.value = orderCount.afterSaleApply
+  }
+  if (orderCount.pickUp != null) {
+    data.orderWaitePickUp.value = orderCount.pickUp
+  }
+  if (orderCount.auditingWithdraw != null) {
+    data.withdrawAuditing.value = orderCount.auditingWithdraw
+  }
 }
 
 /** 查询商品数据 */
@@ -83,6 +91,13 @@ const handleClick = (routerName: string) => {
   router.push({ name: routerName })
 }
 
+/** 激活时 */
+onActivated(() => {
+  getOrderData()
+  getProductData()
+  getWalletRechargeData()
+})
+
 /** 初始化 **/
 onMounted(() => {
   getOrderData()

+ 3 - 3
src/views/mall/product/spu/components/SkuList.vue

@@ -24,7 +24,7 @@
       >
         <template #default="{ row }">
           <span style="font-weight: bold; color: #40aaff">
-            {{ row.properties[index]?.valueName }}
+            {{ row.properties?.[index]?.valueName }}
           </span>
         </template>
       </el-table-column>
@@ -168,7 +168,7 @@
       >
         <template #default="{ row }">
           <span style="font-weight: bold; color: #40aaff">
-            {{ row.properties[index]?.valueName }}
+            {{ row.properties?.[index]?.valueName }}
           </span>
         </template>
       </el-table-column>
@@ -248,7 +248,7 @@
       >
         <template #default="{ row }">
           <span style="font-weight: bold; color: #40aaff">
-            {{ row.properties[index]?.valueName }}
+            {{ row.properties?.[index]?.valueName }}
           </span>
         </template>
       </el-table-column>

+ 44 - 7
src/views/mall/product/spu/form/ProductAttributes.vue

@@ -18,16 +18,28 @@
       >
         {{ value.name }}
       </el-tag>
-      <el-input
+      <el-select
         v-show="inputVisible(index)"
         :id="`input${index}`"
         :ref="setInputRef"
         v-model="inputValue"
-        class="!w-20"
+        :reserve-keyword="false"
+        allow-create
+        class="!w-30"
+        default-first-option
+        filterable
         size="small"
         @blur="handleInputConfirm(index, item.id)"
+        @change="handleInputConfirm(index, item.id)"
         @keyup.enter="handleInputConfirm(index, item.id)"
-      />
+      >
+        <el-option
+          v-for="item2 in attributeOptions"
+          :key="item2.id"
+          :label="item2.name"
+          :value="item2.name"
+        />
+      </el-select>
       <el-button
         v-show="!inputVisible(index)"
         class="button-new-tag ml-1"
@@ -42,7 +54,6 @@
 </template>
 
 <script lang="ts" setup>
-import { ElInput } from 'element-plus'
 import * as PropertyApi from '@/api/mall/product/property'
 import { PropertyAndValues } from '@/views/mall/product/spu/components'
 import { propTypes } from '@/utils/propTypes'
@@ -63,11 +74,12 @@ const inputRef = ref<any[]>([]) //标签输入框Ref
 const setInputRef = (el: any) => {
   if (el === null || typeof el === 'undefined') return
   // 如果不存在 id 相同的元素才添加
-  if (!inputRef.value.some((item) => item.input?.attributes.id === el.input?.attributes.id)) {
+  if (!inputRef.value.some((item) => item.inputRef?.attributes.id === el.inputRef?.attributes.id)) {
     inputRef.value.push(el)
   }
 }
 const attributeList = ref<PropertyAndValues[]>([]) // 商品属性列表
+const attributeOptions = ref([] as PropertyApi.PropertyValueVO[]) // 商品属性名称下拉框
 const props = defineProps({
   propertyList: {
     type: Array,
@@ -100,16 +112,36 @@ const handleCloseProperty = (index: number) => {
 }
 
 /** 显示输入框并获取焦点 */
-const showInput = async (index) => {
+const showInput = async (index: number) => {
   attributeIndex.value = index
   inputRef.value[index].focus()
+  // 获取属性下拉选项
+  await getAttributeOptions(attributeList.value[index].id)
 }
 
 /** 输入框失去焦点或点击回车时触发 */
 const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
 const handleInputConfirm = async (index: number, propertyId: number) => {
   if (inputValue.value) {
-    // 保存属性值
+    // 1. 重复添加校验
+    if (attributeList.value[index].values.find((item) => item.name === inputValue.value)) {
+      message.warning('已存在相同属性值,请重试')
+      attributeIndex.value = null
+      inputValue.value = ''
+      return
+    }
+
+    // 2.1 情况一:属性值已存在,则直接使用并结束
+    const existValue = attributeOptions.value.find((item) => item.name === inputValue.value)
+    if (existValue) {
+      attributeIndex.value = null
+      inputValue.value = ''
+      attributeList.value[index].values.push({ id: existValue.id, name: existValue.name })
+      emit('success', attributeList.value)
+      return
+    }
+
+    // 2.2 情况二:新属性值,则进行保存
     try {
       const id = await PropertyApi.createPropertyValue({ propertyId, name: inputValue.value })
       attributeList.value[index].values.push({ id, name: inputValue.value })
@@ -122,4 +154,9 @@ const handleInputConfirm = async (index: number, propertyId: number) => {
   attributeIndex.value = null
   inputValue.value = ''
 }
+
+/** 获取商品属性下拉选项 */
+const getAttributeOptions = async (propertyId: number) => {
+  attributeOptions.value = await PropertyApi.getPropertyValueSimpleList(propertyId)
+}
 </script>

+ 53 - 2
src/views/mall/product/spu/form/ProductPropertyAddForm.vue

@@ -10,7 +10,22 @@
       @keydown.enter.prevent="submitForm"
     >
       <el-form-item label="属性名称" prop="name">
-        <el-input v-model="formData.name" placeholder="请输入名称" />
+        <el-select
+          v-model="formData.name"
+          :reserve-keyword="false"
+          allow-create
+          class="!w-360px"
+          default-first-option
+          filterable
+          placeholder="请选择属性名称。如果不存在,可手动输入选择"
+        >
+          <el-option
+            v-for="item in attributeOptions"
+            :key="item.id"
+            :label="item.name"
+            :value="item.name"
+          />
+        </el-select>
       </el-form-item>
     </el-form>
     <template #footer>
@@ -37,6 +52,7 @@ const formRules = reactive({
 })
 const formRef = ref() // 表单 Ref
 const attributeList = ref([]) // 商品属性列表
+const attributeOptions = ref([] as PropertyApi.PropertyVO[]) // 商品属性名称下拉框
 const props = defineProps({
   propertyList: {
     type: Array,
@@ -60,15 +76,39 @@ watch(
 const open = async () => {
   dialogVisible.value = true
   resetForm()
+  // 加载列表
+  await getAttributeOptions()
 }
 defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 
 /** 提交表单 */
 const submitForm = async () => {
-  // 校验表单
+  // 1.1 重复添加校验
+  for (const attrItem of attributeList.value) {
+    if (attrItem.name === formData.value.name) {
+      return message.error('该属性已存在,请勿重复添加')
+    }
+  }
+  // 1.2 校验表单
   if (!formRef) return
   const valid = await formRef.value.validate()
   if (!valid) return
+
+  // 2.1 情况一:属性名已存在,则直接使用并结束
+  const existProperty = attributeOptions.value.find((item) => item.name === formData.value.name)
+  if (existProperty) {
+    // 添加到属性列表
+    attributeList.value.push({
+      id: existProperty.id,
+      ...formData.value,
+      values: []
+    })
+    // 关闭弹窗
+    dialogVisible.value = false
+    return
+  }
+
+  // 2.2 情况二:如果是不存在的属性,则需要执行新增
   // 提交请求
   formLoading.value = true
   try {
@@ -80,6 +120,7 @@ const submitForm = async () => {
       ...formData.value,
       values: []
     })
+    // 关闭弹窗
     message.success(t('common.createSuccess'))
     dialogVisible.value = false
   } finally {
@@ -94,4 +135,14 @@ const resetForm = () => {
   }
   formRef.value?.resetFields()
 }
+
+/** 获取商品属性下拉选项 */
+const getAttributeOptions = async () => {
+  formLoading.value = true
+  try {
+    attributeOptions.value = await PropertyApi.getPropertySimpleList()
+  } finally {
+    formLoading.value = false
+  }
+}
 </script>

+ 9 - 2
src/views/mall/product/spu/form/SkuForm.vue

@@ -1,6 +1,13 @@
 <!-- 商品发布 - 库存价格 -->
 <template>
-  <el-form ref="formRef" :disabled="isDetail" :model="formData" :rules="rules" label-width="120px">
+  <el-form
+    ref="formRef"
+    v-loading="formLoading"
+    :disabled="isDetail"
+    :model="formData"
+    :rules="rules"
+    label-width="120px"
+  >
     <el-form-item label="分销类型" props="subCommissionType">
       <el-radio-group
         v-model="formData.subCommissionType"
@@ -94,7 +101,7 @@ const ruleConfig: RuleConfig[] = [
 ]
 
 const message = useMessage() // 消息弹窗
-
+const formLoading = ref(false)
 const props = defineProps({
   propFormData: {
     type: Object as PropType<Spu>,

+ 1 - 1
src/views/mall/promotion/kefu/components/history/MemberBrowsingHistory.vue

@@ -25,7 +25,7 @@ import OrderBrowsingHistory from './OrderBrowsingHistory.vue'
 import { KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
 import { isEmpty } from '@/utils/is'
 import { debounce } from 'lodash-es'
-import { ElScrollbar as ElScrollbarType } from 'element-plus/es/components/scrollbar'
+import { ElScrollbar as ElScrollbarType } from 'element-plus/es/components/scrollbar/index'
 
 defineOptions({ name: 'MemberBrowsingHistory' })
 

+ 8 - 11
src/views/mall/trade/afterSale/index.vue

@@ -135,7 +135,7 @@
           </div>
         </template>
       </el-table-column>
-      <el-table-column align="center" label="订单金额" prop="refundPrice">
+      <el-table-column align="center" label="订单金额" min-width="120" prop="refundPrice">
         <template #default="scope">
           <span>{{ fenToYuan(scope.row.refundPrice) }} 元</span>
         </template>
@@ -156,7 +156,7 @@
           <dict-tag :type="DICT_TYPE.TRADE_AFTER_SALE_WAY" :value="scope.row.way" />
         </template>
       </el-table-column>
-      <el-table-column align="center" fixed="right" label="操作" width="120">
+      <el-table-column align="center" fixed="right" label="操作" width="160">
         <template #default="{ row }">
           <el-button link type="primary" @click="openAfterSaleDetail(row.id)">处理退款</el-button>
         </template>
@@ -181,9 +181,6 @@ import { cloneDeep } from 'lodash-es'
 import { fenToYuan } from '@/utils'
 
 defineOptions({ name: 'TradeAfterSale' })
-const props = defineProps<{
-  userId?: number
-}>()
 
 const { push } = useRouter() // 路由跳转
 
@@ -207,9 +204,9 @@ const queryParams = reactive({
   spuName: null,
   createTime: [],
   way: null,
-  type: null,
-  userId: null
+  type: null
 })
+
 /** 查询列表 */
 const getList = async () => {
   loading.value = true
@@ -219,27 +216,27 @@ const getList = async () => {
     if (data.status === '0') {
       delete data.status
     }
-    if (props.userId) {
-      data.userId = props.userId
-    }
     // 执行查询
     const res = await AfterSaleApi.getAfterSalePage(data)
-    list.value = res.list
+    list.value = res.list as AfterSaleApi.TradeAfterSaleVO[]
     total.value = res.total
   } finally {
     loading.value = false
   }
 }
+
 /** 搜索按钮操作 */
 const handleQuery = async () => {
   queryParams.pageNo = 1
   await getList()
 }
+
 /** 重置按钮操作 */
 const resetQuery = () => {
   queryFormRef.value?.resetFields()
   handleQuery()
 }
+
 /** tab 切换 */
 const tabClick = async (tab: TabsPaneContext) => {
   queryParams.status = tab.paneName

+ 10 - 34
src/views/member/user/detail/UserAccountInfo.vue

@@ -2,81 +2,57 @@
   <el-descriptions :column="2">
     <el-descriptions-item>
       <template #label>
-        <descriptions-item-label label=" 等级 " icon="svg-icon:member_level" />
+        <descriptions-item-label icon="svg-icon:member_level" label=" 等级 " />
       </template>
       {{ user.levelName || '无' }}
     </el-descriptions-item>
     <el-descriptions-item>
       <template #label>
-        <descriptions-item-label label=" 成长值 " icon="ep:suitcase" />
+        <descriptions-item-label icon="ep:suitcase" label=" 成长值 " />
       </template>
       {{ user.experience || 0 }}
     </el-descriptions-item>
     <el-descriptions-item>
       <template #label>
-        <descriptions-item-label label=" 当前积分 " icon="ep:coin" />
+        <descriptions-item-label icon="ep:coin" label=" 当前积分 " />
       </template>
       {{ user.point || 0 }}
     </el-descriptions-item>
     <el-descriptions-item>
       <template #label>
-        <descriptions-item-label label=" 总积分 " icon="ep:coin" />
+        <descriptions-item-label icon="ep:coin" label=" 总积分 " />
       </template>
       {{ user.totalPoint || 0 }}
     </el-descriptions-item>
     <el-descriptions-item>
       <template #label>
-        <descriptions-item-label label=" 当前余额 " icon="svg-icon:member_balance" />
+        <descriptions-item-label icon="svg-icon:member_balance" label=" 当前余额 " />
       </template>
       {{ fenToYuan(wallet.balance || 0) }}
     </el-descriptions-item>
     <el-descriptions-item>
       <template #label>
-        <descriptions-item-label label=" 支出金额 " icon="svg-icon:member_expenditure_balance" />
+        <descriptions-item-label icon="svg-icon:member_expenditure_balance" label=" 支出金额 " />
       </template>
       {{ fenToYuan(wallet.totalExpense || 0) }}
     </el-descriptions-item>
     <el-descriptions-item>
       <template #label>
-        <descriptions-item-label label=" 充值金额 " icon="svg-icon:member_recharge_balance" />
+        <descriptions-item-label icon="svg-icon:member_recharge_balance" label=" 充值金额 " />
       </template>
       {{ fenToYuan(wallet.totalRecharge || 0) }}
     </el-descriptions-item>
   </el-descriptions>
 </template>
-<script setup lang="ts">
+<script lang="ts" setup>
 import { DescriptionsItemLabel } from '@/components/Descriptions'
 import * as UserApi from '@/api/member/user'
 import * as WalletApi from '@/api/pay/wallet/balance'
-import { UserTypeEnum } from '@/utils/constants'
 import { fenToYuan } from '@/utils'
 
-const props = defineProps<{ user: UserApi.UserVO }>() // 用户信息
-const WALLET_INIT_DATA = {
-  balance: 0,
-  totalExpense: 0,
-  totalRecharge: 0
-} as WalletApi.WalletVO // 钱包初始化数据
-const wallet = ref<WalletApi.WalletVO>(WALLET_INIT_DATA) // 钱包信息
-
-/** 查询用户钱包信息 */
-const getUserWallet = async () => {
-  if (!props.user.id) {
-    wallet.value = WALLET_INIT_DATA
-    return
-  }
-  const params = { userId: props.user.id }
-  wallet.value = (await WalletApi.getWallet(params)) || WALLET_INIT_DATA
-}
-
-/** 监听用户编号变化 */
-watch(
-  () => props.user.id,
-  () => getUserWallet(),
-  { immediate: true }
-)
+defineProps<{ user: UserApi.UserVO; wallet: WalletApi.WalletVO }>() // 用户信息
 </script>
-<style scoped lang="scss">
+<style lang="scss" scoped>
 .cell-item {
   display: inline;
 }

+ 276 - 0
src/views/member/user/detail/UserAftersaleList.vue

@@ -0,0 +1,276 @@
+<template>
+  <!-- 搜索 -->
+  <ContentWrap>
+    <el-form ref="queryFormRef" :inline="true" :model="queryParams" label-width="68px">
+      <el-form-item label="商品名称" prop="spuName">
+        <el-input
+          v-model="queryParams.spuName"
+          class="!w-280px"
+          clearable
+          placeholder="请输入商品 SPU 名称"
+          @keyup.enter="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="退款编号" prop="no">
+        <el-input
+          v-model="queryParams.no"
+          class="!w-280px"
+          clearable
+          placeholder="请输入退款编号"
+          @keyup.enter="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="订单编号" prop="orderNo">
+        <el-input
+          v-model="queryParams.orderNo"
+          class="!w-280px"
+          clearable
+          placeholder="请输入订单编号"
+          @keyup.enter="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="售后状态" prop="status">
+        <el-select
+          v-model="queryParams.status"
+          class="!w-280px"
+          clearable
+          placeholder="请选择售后状态"
+        >
+          <el-option label="全部" value="0" />
+          <el-option
+            v-for="dict in getDictOptions(DICT_TYPE.TRADE_AFTER_SALE_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="售后方式" prop="way">
+        <el-select
+          v-model="queryParams.way"
+          class="!w-280px"
+          clearable
+          placeholder="请选择售后方式"
+        >
+          <el-option
+            v-for="dict in getDictOptions(DICT_TYPE.TRADE_AFTER_SALE_WAY)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="售后类型" prop="type">
+        <el-select
+          v-model="queryParams.type"
+          class="!w-280px"
+          clearable
+          placeholder="请选择售后类型"
+        >
+          <el-option
+            v-for="dict in getDictOptions(DICT_TYPE.TRADE_AFTER_SALE_TYPE)"
+            :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"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-260px"
+          end-placeholder="自定义时间"
+          start-placeholder="自定义时间"
+          type="daterange"
+          value-format="YYYY-MM-DD HH:mm:ss"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery">
+          <Icon class="mr-5px" icon="ep:search" />
+          搜索
+        </el-button>
+        <el-button @click="resetQuery">
+          <Icon class="mr-5px" icon="ep:refresh" />
+          重置
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <ContentWrap>
+    <el-tabs v-model="queryParams.status" @tab-click="tabClick">
+      <el-tab-pane
+        v-for="item in statusTabs"
+        :key="item.label"
+        :label="item.label"
+        :name="item.value"
+      />
+    </el-tabs>
+    <!-- 列表 -->
+    <el-table v-loading="loading" :data="list">
+      <el-table-column align="center" label="退款编号" min-width="200" prop="no" />
+      <el-table-column align="center" label="订单编号" min-width="200" prop="orderNo">
+        <template #default="{ row }">
+          <el-button link type="primary" @click="openOrderDetail(row.orderId)">
+            {{ row.orderNo }}
+          </el-button>
+        </template>
+      </el-table-column>
+      <el-table-column label="商品信息" min-width="600" prop="spuName">
+        <template #default="{ row }">
+          <div class="flex items-center">
+            <el-image
+              :src="row.picUrl"
+              class="mr-10px h-30px w-30px"
+              @click="imagePreview(row.picUrl)"
+            />
+            <span class="mr-10px">{{ row.spuName }}</span>
+            <el-tag v-for="property in row.properties" :key="property.propertyId" class="mr-10px">
+              {{ property.propertyName }}: {{ property.valueName }}
+            </el-tag>
+          </div>
+        </template>
+      </el-table-column>
+      <el-table-column align="center" label="订单金额" min-width="120" prop="refundPrice">
+        <template #default="scope">
+          <span>{{ fenToYuan(scope.row.refundPrice) }} 元</span>
+        </template>
+      </el-table-column>
+      <el-table-column align="center" label="申请时间" prop="createTime" width="180">
+        <template #default="scope">
+          <span>{{ formatDate(scope.row.createTime) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column align="center" label="售后状态" width="100">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.TRADE_AFTER_SALE_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column align="center" label="售后方式">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.TRADE_AFTER_SALE_WAY" :value="scope.row.way" />
+        </template>
+      </el-table-column>
+      <el-table-column align="center" fixed="right" label="操作" width="120">
+        <template #default="{ row }">
+          <el-button link type="primary" @click="openAfterSaleDetail(row.id)">处理退款</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      v-model:limit="queryParams.pageSize"
+      v-model:page="queryParams.pageNo"
+      :total="total"
+      @pagination="getList"
+    />
+  </ContentWrap>
+</template>
+<script lang="ts" setup>
+import * as AfterSaleApi from '@/api/mall/trade/afterSale/index'
+import { DICT_TYPE, getDictOptions } from '@/utils/dict'
+import { formatDate } from '@/utils/formatTime'
+import { createImageViewer } from '@/components/ImageViewer'
+import { TabsPaneContext } from 'element-plus'
+import { cloneDeep } from 'lodash-es'
+import { fenToYuan } from '@/utils'
+
+defineOptions({ name: 'UserAfterSaleList' })
+
+const { push } = useRouter() // 路由跳转
+const props = defineProps<{
+  userId: number
+}>()
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref<AfterSaleApi.TradeAfterSaleVO[]>([]) // 列表的数据
+const statusTabs = ref([
+  {
+    label: '全部',
+    value: '0'
+  }
+])
+const queryFormRef = ref() // 搜索的表单
+// 查询参数
+const queryParams = ref({
+  pageNo: 1,
+  pageSize: 10,
+  no: null,
+  status: '0',
+  orderNo: null,
+  spuName: null,
+  createTime: [],
+  way: null,
+  type: null,
+  userId: null
+})
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = cloneDeep(queryParams.value)
+    // 处理掉全部的状态,不传就是全部
+    if (data.status === '0') {
+      delete data.status
+    }
+    // 执行查询
+    if (props.userId) {
+      data.userId = props.userId as any
+    }
+    const res = await AfterSaleApi.getAfterSalePage(data)
+    list.value = res.list as AfterSaleApi.TradeAfterSaleVO[]
+    total.value = res.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = async () => {
+  queryParams.value.pageNo = 1
+  await getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields()
+  handleQuery()
+}
+
+/** tab 切换 */
+const tabClick = async (tab: TabsPaneContext) => {
+  queryParams.value.status = tab.paneName as any
+  await getList()
+}
+
+/** 处理退款 */
+const openAfterSaleDetail = (id: number) => {
+  push({ name: 'TradeAfterSaleDetail', params: { id } })
+}
+
+/** 查看订单详情 */
+const openOrderDetail = (id: number) => {
+  push({ name: 'TradeOrderDetail', params: { id } })
+}
+
+/** 商品图预览 */
+const imagePreview = (imgUrl: string) => {
+  createImageViewer({
+    urlList: [imgUrl]
+  })
+}
+
+onMounted(async () => {
+  await getList()
+  // 设置 statuses 过滤
+  for (const dict of getDictOptions(DICT_TYPE.TRADE_AFTER_SALE_STATUS)) {
+    statusTabs.value.push({
+      label: dict.label,
+      value: dict.value
+    })
+  }
+})
+</script>

+ 68 - 0
src/views/member/user/detail/UserBalanceList.vue

@@ -0,0 +1,68 @@
+<template>
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
+      <el-table-column align="center" label="编号" prop="id" />
+      <el-table-column align="center" label="钱包编号" prop="walletId" />
+      <el-table-column align="center" label="关联业务标题" prop="title" />
+      <el-table-column align="center" label="交易金额" prop="price">
+        <template #default="{ row }"> {{ fenToYuan(row.price) }} 元</template>
+      </el-table-column>
+      <el-table-column align="center" label="钱包余额" prop="balance">
+        <template #default="{ row }"> {{ fenToYuan(row.balance) }} 元</template>
+      </el-table-column>
+      <el-table-column
+        :formatter="dateFormatter"
+        align="center"
+        label="交易时间"
+        prop="createTime"
+        width="180px"
+      />
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      v-model:limit="queryParams.pageSize"
+      v-model:page="queryParams.pageNo"
+      :total="total"
+      @pagination="getList"
+    />
+  </ContentWrap>
+</template>
+
+<script lang="ts" setup>
+import { dateFormatter } from '@/utils/formatTime'
+import * as WalletTransactionApi from '@/api/pay/wallet/transaction'
+import { fenToYuan } from '@/utils'
+
+defineOptions({ name: 'UserBalanceList' })
+const props = defineProps({
+  walletId: {
+    type: Number,
+    required: false
+  }
+})
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  walletId: null
+})
+const list = ref([]) // 列表的数据
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    queryParams.walletId = props.walletId as any
+    const data = await WalletTransactionApi.getWalletTransactionPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>

+ 25 - 5
src/views/member/user/detail/index.vue

@@ -20,7 +20,7 @@
           <template #header>
             <CardTitle title="账户信息" />
           </template>
-          <UserAccountInfo :user="user" />
+          <UserAccountInfo :user="user" :wallet="wallet" />
         </el-card>
       </el-col>
       <!-- 下边:账户明细 -->
@@ -40,7 +40,7 @@
             <UserExperienceRecordList :user-id="id" />
           </el-tab-pane>
           <el-tab-pane label="余额" lazy>
-            <WalletTransactionList :user-id="id" />
+            <UserBalanceList :wallet-id="wallet.id" />
           </el-tab-pane>
           <el-tab-pane label="收货地址" lazy>
             <UserAddressList :user-id="id" />
@@ -49,7 +49,7 @@
             <UserOrderList :user-id="id" />
           </el-tab-pane>
           <el-tab-pane label="售后管理" lazy>
-            <TradeAfterSale :user-id="id" />
+            <UserAfterSaleList :user-id="id" />
           </el-tab-pane>
           <el-tab-pane label="收藏记录" lazy>
             <UserFavoriteList :user-id="id" />
@@ -69,6 +69,7 @@
   <UserForm ref="formRef" @success="getUserData(id)" />
 </template>
 <script lang="ts" setup>
+import * as WalletApi from '@/api/pay/wallet/balance'
 import * as UserApi from '@/api/member/user'
 import { useTagsViewStore } from '@/store/modules/tagsView'
 import UserForm from '@/views/member/user/UserForm.vue'
@@ -82,8 +83,8 @@ import UserOrderList from './UserOrderList.vue'
 import UserPointList from './UserPointList.vue'
 import UserSignList from './UserSignList.vue'
 import UserFavoriteList from './UserFavoriteList.vue'
-import WalletTransactionList from '@/views/pay/wallet/transaction/WalletTransactionList.vue'
-import TradeAfterSale from '@/views/mall/trade/afterSale/index.vue'
+import UserAfterSaleList from './UserAftersaleList.vue'
+import UserBalanceList from './UserBalanceList.vue'
 import { CardTitle } from '@/components/Card/index'
 import { ElMessage } from 'element-plus'
 
@@ -113,6 +114,24 @@ const { currentRoute } = useRouter() // 路由
 const { delView } = useTagsViewStore() // 视图操作
 const route = useRoute()
 const id = Number(route.params.id)
+/* 用户钱包相关信息 */
+const WALLET_INIT_DATA = {
+  balance: 0,
+  totalExpense: 0,
+  totalRecharge: 0
+} as WalletApi.WalletVO // 钱包初始化数据
+const wallet = ref<WalletApi.WalletVO>(WALLET_INIT_DATA) // 钱包信息
+
+/** 查询用户钱包信息 */
+const getUserWallet = async () => {
+  if (!id) {
+    wallet.value = WALLET_INIT_DATA
+    return
+  }
+  const params = { userId: id }
+  wallet.value = (await WalletApi.getWallet(params)) || WALLET_INIT_DATA
+}
+
 onMounted(() => {
   if (!id) {
     ElMessage.warning('参数错误,会员编号不能为空!')
@@ -120,6 +139,7 @@ onMounted(() => {
     return
   }
   getUserData(id)
+  getUserWallet()
 })
 </script>
 <style lang="css" scoped>

+ 11 - 8
src/views/pay/app/components/AppForm.vue

@@ -10,6 +10,9 @@
       <el-form-item label="应用名" prop="name">
         <el-input v-model="formData.name" placeholder="请输入应用名" />
       </el-form-item>
+      <el-form-item label="应用标识" prop="name">
+        <el-input v-model="formData.appKey" placeholder="请输入应用标识" />
+      </el-form-item>
       <el-form-item label="开启状态" prop="status">
         <el-radio-group v-model="formData.status">
           <el-radio
@@ -55,16 +58,15 @@ const formType = ref('') // 表单的类型:create - 新增;update - 修改
 const formData = ref({
   id: undefined,
   name: undefined,
-  packageId: undefined,
-  contactName: undefined,
-  contactMobile: undefined,
-  accountCount: undefined,
-  expireTime: undefined,
-  domain: undefined,
-  status: CommonStatusEnum.ENABLE
+  appKey: undefined,
+  status: CommonStatusEnum.ENABLE,
+  remark: undefined,
+  orderNotifyUrl: undefined,
+  refundNotifyUrl: undefined
 })
 const formRules = reactive({
   name: [{ required: true, message: '应用名不能为空', trigger: 'blur' }],
+  appKey: [{ required: true, message: '应用标识不能为空', trigger: 'blur' }],
   status: [{ required: true, message: '开启状态不能为空', trigger: 'blur' }],
   orderNotifyUrl: [{ required: true, message: '支付结果的回调地址不能为空', trigger: 'blur' }],
   refundNotifyUrl: [{ required: true, message: '退款结果的回调地址不能为空', trigger: 'blur' }]
@@ -123,7 +125,8 @@ const resetForm = () => {
     status: CommonStatusEnum.ENABLE,
     remark: undefined,
     orderNotifyUrl: undefined,
-    refundNotifyUrl: undefined
+    refundNotifyUrl: undefined,
+    appKey: undefined
   }
   formRef.value?.resetFields()
 }

+ 80 - 55
src/views/pay/app/components/channel/AlipayChannelForm.vue

@@ -1,22 +1,22 @@
 <template>
   <div>
-    <Dialog v-model="dialogVisible" :title="dialogTitle" @closed="close" width="830px">
+    <Dialog v-model="dialogVisible" :title="dialogTitle" width="830px" @closed="close">
       <el-form
         ref="formRef"
+        v-loading="formLoading"
         :model="formData"
-        :formRules="formRules"
+        :rules="formRules"
         label-width="100px"
-        v-loading="formLoading"
       >
-        <el-form-item label-width="180px" label="渠道费率" prop="feeRate">
-          <el-input v-model="formData.feeRate" placeholder="请输入渠道费率" clearable>
+        <el-form-item label="渠道费率" label-width="180px" prop="feeRate">
+          <el-input v-model="formData.feeRate" clearable placeholder="请输入渠道费率">
             <template #append>%</template>
           </el-input>
         </el-form-item>
-        <el-form-item label-width="180px" label="开放平台 APPID" prop="config.appId">
-          <el-input v-model="formData.config.appId" placeholder="请输入开放平台 APPID" clearable />
+        <el-form-item label="开放平台 APPID" label-width="180px" prop="config.appId">
+          <el-input v-model="formData.config.appId" clearable placeholder="请输入开放平台 APPID" />
         </el-form-item>
-        <el-form-item label-width="180px" label="渠道状态" prop="status">
+        <el-form-item label="渠道状态" label-width="180px" prop="status">
           <el-radio-group v-model="formData.status">
             <el-radio
               v-for="dict in getDictOptions(DICT_TYPE.COMMON_STATUS)"
@@ -27,7 +27,7 @@
             </el-radio>
           </el-radio-group>
         </el-form-item>
-        <el-form-item label-width="180px" label="网关地址" prop="config.serverUrl">
+        <el-form-item label="网关地址" label-width="180px" prop="config.serverUrl">
           <el-radio-group v-model="formData.config.serverUrl">
             <el-radio label="https://openapi.alipay.com/gateway.do">线上环境</el-radio>
             <el-radio label="https://openapi-sandbox.dl.alipaydev.com/gateway.do">
@@ -35,128 +35,148 @@
             </el-radio>
           </el-radio-group>
         </el-form-item>
-        <el-form-item label-width="180px" label="算法类型" prop="config.signType">
+        <el-form-item label="算法类型" label-width="180px" prop="config.signType">
           <el-radio-group v-model="formData.config.signType">
             <el-radio key="RSA2" label="RSA2">RSA2</el-radio>
           </el-radio-group>
         </el-form-item>
-        <el-form-item label-width="180px" label="公钥类型" prop="config.mode">
+        <el-form-item label="公钥类型" label-width="180px" prop="config.mode">
           <el-radio-group v-model="formData.config.mode">
             <el-radio key="公钥模式" :label="1">公钥模式</el-radio>
             <el-radio key="证书模式" :label="2">证书模式</el-radio>
           </el-radio-group>
         </el-form-item>
         <div v-if="formData.config.mode === 1">
-          <el-form-item label-width="180px" label="应用私钥" prop="config.privateKey">
+          <el-form-item label="应用私钥" label-width="180px" prop="config.privateKey">
             <el-input
-              type="textarea"
-              :autosize="{ minRows: 8, maxRows: 8 }"
               v-model="formData.config.privateKey"
-              placeholder="请输入应用私钥"
-              clearable
+              :autosize="{ minRows: 8, maxRows: 8 }"
               :style="{ width: '100%' }"
+              clearable
+              placeholder="请输入应用私钥"
+              type="textarea"
             />
           </el-form-item>
-          <el-form-item label-width="180px" label="支付宝公钥" prop="config.alipayPublicKey">
+          <el-form-item label="支付宝公钥" label-width="180px" prop="config.alipayPublicKey">
             <el-input
-              type="textarea"
-              :autosize="{ minRows: 8, maxRows: 8 }"
               v-model="formData.config.alipayPublicKey"
-              placeholder="请输入支付宝公钥"
-              clearable
+              :autosize="{ minRows: 8, maxRows: 8 }"
               :style="{ width: '100%' }"
+              clearable
+              placeholder="请输入支付宝公钥"
+              type="textarea"
             />
           </el-form-item>
         </div>
         <div v-if="formData.config.mode === 2">
-          <el-form-item label-width="180px" label="应用私钥" prop="config.privateKey">
+          <el-form-item label="应用私钥" label-width="180px" prop="config.privateKey">
             <el-input
-              type="textarea"
-              :autosize="{ minRows: 8, maxRows: 8 }"
               v-model="formData.config.privateKey"
-              placeholder="请输入应用私钥"
-              clearable
+              :autosize="{ minRows: 8, maxRows: 8 }"
               :style="{ width: '100%' }"
+              clearable
+              placeholder="请输入应用私钥"
+              type="textarea"
             />
           </el-form-item>
-          <el-form-item label-width="180px" label="商户公钥应用证书" prop="config.appCertContent">
+          <el-form-item label="商户公钥应用证书" label-width="180px" prop="config.appCertContent">
             <el-input
               v-model="formData.config.appCertContent"
-              type="textarea"
-              placeholder="请上传商户公钥应用证书"
-              readonly
               :autosize="{ minRows: 8, maxRows: 8 }"
               :style="{ width: '100%' }"
+              placeholder="请上传商户公钥应用证书"
+              readonly
+              type="textarea"
             />
           </el-form-item>
-          <el-form-item label-width="180px" label="">
+          <el-form-item label="" label-width="180px">
             <el-upload
-              action=""
               ref="privateKeyContentFile"
-              :limit="1"
               :accept="fileAccept"
-              :http-request="appCertUpload"
               :before-upload="fileBeforeUpload"
+              :http-request="appCertUpload"
+              :limit="1"
+              action=""
             >
               <el-button type="primary">
-                <Icon icon="ep:upload" class="mr-5px" /> 点击上传
+                <Icon class="mr-5px" icon="ep:upload" />
+                点击上传
               </el-button>
             </el-upload>
           </el-form-item>
           <el-form-item
-            label-width="180px"
             label="支付宝公钥证书"
+            label-width="180px"
             prop="config.alipayPublicCertContent"
           >
             <el-input
               v-model="formData.config.alipayPublicCertContent"
-              type="textarea"
-              placeholder="请上传支付宝公钥证书"
-              readonly
               :autosize="{ minRows: 8, maxRows: 8 }"
               :style="{ width: '100%' }"
+              placeholder="请上传支付宝公钥证书"
+              readonly
+              type="textarea"
             />
           </el-form-item>
-          <el-form-item label-width="180px" label="">
+          <el-form-item label="" label-width="180px">
             <el-upload
               ref="privateCertContentFile"
-              action=""
-              :limit="1"
               :accept="fileAccept"
               :before-upload="fileBeforeUpload"
               :http-request="alipayPublicCertUpload"
+              :limit="1"
+              action=""
             >
               <el-button type="primary">
-                <Icon icon="ep:upload" class="mr-5px" /> 点击上传
+                <Icon class="mr-5px" icon="ep:upload" />
+                点击上传
               </el-button>
             </el-upload>
           </el-form-item>
-          <el-form-item label-width="180px" label="根证书" prop="config.rootCertContent">
+          <el-form-item label="根证书" label-width="180px" prop="config.rootCertContent">
             <el-input
               v-model="formData.config.rootCertContent"
-              type="textarea"
-              placeholder="请上传根证书"
-              readonly
               :autosize="{ minRows: 8, maxRows: 8 }"
               :style="{ width: '100%' }"
+              placeholder="请上传根证书"
+              readonly
+              type="textarea"
             />
           </el-form-item>
-          <el-form-item label-width="180px" label="">
+          <el-form-item label="" label-width="180px">
             <el-upload
               ref="privateCertContentFile"
-              :limit="1"
               :accept="fileAccept"
-              action=""
               :before-upload="fileBeforeUpload"
               :http-request="rootCertUpload"
+              :limit="1"
+              action=""
             >
               <el-button type="primary">
-                <Icon icon="ep:upload" class="mr-5px" /> 点击上传
+                <Icon class="mr-5px" icon="ep:upload" />
+                点击上传
               </el-button>
             </el-upload>
           </el-form-item>
         </div>
-        <el-form-item label-width="180px" label="备注" prop="remark">
+
+        <el-form-item label="接口内容加密方式" label-width="180px" prop="config.encryptType">
+          <el-radio-group v-model="formData.config.encryptType">
+            <el-radio key="NONE" label="">无加密</el-radio>
+            <el-radio key="AES" label="AES">AES</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <div v-if="formData.config.encryptType === 'AES'">
+          <el-form-item label="接口内容加密密钥" label-width="180px" prop="config.encryptKey">
+            <el-input
+              v-model="formData.config.encryptKey"
+              clearable
+              placeholder="请输入接口内容加密密钥"
+            />
+          </el-form-item>
+        </div>
+
+        <el-form-item label="备注" label-width="180px" prop="remark">
           <el-input v-model="formData.remark" :style="{ width: '100%' }" />
         </el-form-item>
       </el-form>
@@ -195,7 +215,9 @@ const formData = ref<any>({
     alipayPublicKey: '',
     appCertContent: '',
     alipayPublicCertContent: '',
-    rootCertContent: ''
+    rootCertContent: '',
+    encryptType: '',
+    encryptKey: ''
   }
 })
 const formRules = {
@@ -213,7 +235,8 @@ const formRules = {
   'config.alipayPublicCertContent': [
     { required: true, message: '请上传支付宝公钥证书', trigger: 'blur' }
   ],
-  'config.rootCertContent': [{ required: true, message: '请上传指定根证书', trigger: 'blur' }]
+  'config.rootCertContent': [{ required: true, message: '请上传指定根证书', trigger: 'blur' }],
+  'config.encryptKey': [{ required: true, message: '请输入接口内容加密密钥', trigger: 'blur' }]
 }
 const fileAccept = '.crt'
 const formRef = ref() // 表单 Ref
@@ -281,7 +304,9 @@ const resetForm = (appId, code) => {
       alipayPublicKey: '',
       appCertContent: '',
       alipayPublicCertContent: '',
-      rootCertContent: ''
+      rootCertContent: '',
+      encryptType: '',
+      encryptKey: ''
     }
   }
   formRef.value?.resetFields()

+ 4 - 4
src/views/pay/app/components/channel/MockChannelForm.vue

@@ -1,14 +1,14 @@
 <template>
   <div>
-    <Dialog v-model="dialogVisible" :title="dialogTitle" @closed="close" width="800px">
+    <Dialog v-model="dialogVisible" :title="dialogTitle" width="800px">
       <el-form
         ref="formRef"
+        v-loading="formLoading"
         :model="formData"
         :rules="formRules"
         label-width="100px"
-        v-loading="formLoading"
       >
-        <el-form-item label-width="180px" label="渠道状态" prop="status">
+        <el-form-item label="渠道状态" label-width="180px" prop="status">
           <el-radio-group v-model="formData.status">
             <el-radio
               v-for="dict in getDictOptions(DICT_TYPE.COMMON_STATUS)"
@@ -19,7 +19,7 @@
             </el-radio>
           </el-radio-group>
         </el-form-item>
-        <el-form-item label-width="180px" label="备注" prop="remark">
+        <el-form-item label="备注" label-width="180px" prop="remark">
           <el-input v-model="formData.remark" :style="{ width: '100%' }" />
         </el-form-item>
       </el-form>

+ 4 - 4
src/views/pay/app/components/channel/WalletChannelForm.vue

@@ -1,14 +1,14 @@
 <template>
   <div>
-    <Dialog v-model="dialogVisible" :title="dialogTitle" @closed="close" width="800px">
+    <Dialog v-model="dialogVisible" :title="dialogTitle" width="800px">
       <el-form
         ref="formRef"
+        v-loading="formLoading"
         :model="formData"
         :rules="formRules"
         label-width="100px"
-        v-loading="formLoading"
       >
-        <el-form-item label-width="180px" label="渠道状态" prop="status">
+        <el-form-item label="渠道状态" label-width="180px" prop="status">
           <el-radio-group v-model="formData.status">
             <el-radio
               v-for="dict in getDictOptions(DICT_TYPE.COMMON_STATUS)"
@@ -19,7 +19,7 @@
             </el-radio>
           </el-radio-group>
         </el-form-item>
-        <el-form-item label-width="180px" label="备注" prop="remark">
+        <el-form-item label="备注" label-width="180px" prop="remark">
           <el-input v-model="formData.remark" :style="{ width: '100%' }" />
         </el-form-item>
       </el-form>

+ 35 - 41
src/views/pay/app/components/channel/WeixinChannelForm.vue

@@ -1,35 +1,35 @@
 <template>
   <div>
-    <Dialog v-model="dialogVisible" :title="dialogTitle" @close="close" width="800px">
+    <Dialog v-model="dialogVisible" :title="dialogTitle" width="800px">
       <el-form
         ref="formRef"
+        v-loading="formLoading"
         :model="formData"
         :rules="formRules"
         label-width="120px"
-        v-loading="formLoading"
       >
-        <el-form-item label-width="180px" label="渠道费率" prop="feeRate">
+        <el-form-item label="渠道费率" label-width="180px" prop="feeRate">
           <el-input
             v-model="formData.feeRate"
-            placeholder="请输入渠道费率"
-            clearable
             :style="{ width: '100%' }"
+            clearable
+            placeholder="请输入渠道费率"
           >
             <template #append>%</template>
           </el-input>
         </el-form-item>
-        <el-form-item label-width="180px" label="微信 APPID" prop="config.appId">
+        <el-form-item label="微信 APPID" label-width="180px" prop="config.appId">
           <el-input
             v-model="formData.config.appId"
-            placeholder="请输入微信 APPID"
-            clearable
             :style="{ width: '100%' }"
+            clearable
+            placeholder="请输入微信 APPID"
           />
         </el-form-item>
-        <el-form-item label-width="180px" label="商户号" prop="config.mchId">
+        <el-form-item label="商户号" label-width="180px" prop="config.mchId">
           <el-input v-model="formData.config.mchId" :style="{ width: '100%' }" />
         </el-form-item>
-        <el-form-item label-width="180px" label="渠道状态" prop="status">
+        <el-form-item label="渠道状态" label-width="180px" prop="status">
           <el-radio-group v-model="formData.status">
             <el-radio
               v-for="dict in getDictOptions(DICT_TYPE.COMMON_STATUS)"
@@ -40,95 +40,91 @@
             </el-radio>
           </el-radio-group>
         </el-form-item>
-        <el-form-item label-width="180px" label="API 版本" prop="config.apiVersion">
+        <el-form-item label="API 版本" label-width="180px" prop="config.apiVersion">
           <el-radio-group v-model="formData.config.apiVersion">
             <el-radio label="v2">v2</el-radio>
             <el-radio label="v3">v3</el-radio>
           </el-radio-group>
         </el-form-item>
         <div v-if="formData.config.apiVersion === 'v2'">
-          <el-form-item label-width="180px" label="商户密钥" prop="config.mchKey">
-            <el-input
-              v-model="formData.config.mchKey"
-              placeholder="请输入商户密钥"
-              clearable
-            />
+          <el-form-item label="商户密钥" label-width="180px" prop="config.mchKey">
+            <el-input v-model="formData.config.mchKey" clearable placeholder="请输入商户密钥" />
           </el-form-item>
           <el-form-item
-            label-width="180px"
             label="apiclient_cert.p12 证书"
+            label-width="180px"
             prop="config.keyContent"
           >
             <el-input
               v-model="formData.config.keyContent"
-              type="textarea"
-              placeholder="请上传 apiclient_cert.p12 证书"
-              readonly
               :autosize="{ minRows: 8, maxRows: 8 }"
               :style="{ width: '100%' }"
+              placeholder="请上传 apiclient_cert.p12 证书"
+              readonly
+              type="textarea"
             />
           </el-form-item>
-          <el-form-item label-width="180px" label="">
+          <el-form-item label="" label-width="180px">
             <el-upload
+              :before-upload="p12FileBeforeUpload"
+              :http-request="keyContentUpload"
               :limit="1"
               accept=".p12"
               action=""
-              :before-upload="p12FileBeforeUpload"
-              :http-request="keyContentUpload"
             >
               <el-button type="primary">
-                <Icon icon="ep:upload" class="mr-5px" />
+                <Icon class="mr-5px" icon="ep:upload" />
                 点击上传
               </el-button>
             </el-upload>
           </el-form-item>
         </div>
         <div v-if="formData.config.apiVersion === 'v3'">
-          <el-form-item label-width="180px" label="API V3 密钥" prop="config.apiV3Key">
+          <el-form-item label="API V3 密钥" label-width="180px" prop="config.apiV3Key">
             <el-input
               v-model="formData.config.apiV3Key"
-              placeholder="请输入 API V3 密钥"
               clearable
+              placeholder="请输入 API V3 密钥"
             />
           </el-form-item>
           <el-form-item
-            label-width="180px"
             label="apiclient_key.pem 证书"
+            label-width="180px"
             prop="config.privateKeyContent"
           >
             <el-input
               v-model="formData.config.privateKeyContent"
-              type="textarea"
-              placeholder="请上传 apiclient_key.pem 证书"
-              readonly
               :autosize="{ minRows: 8, maxRows: 8 }"
               :style="{ width: '100%' }"
+              placeholder="请上传 apiclient_key.pem 证书"
+              readonly
+              type="textarea"
             />
           </el-form-item>
-          <el-form-item label-width="180px" label="" prop="privateKeyContentFile">
+          <el-form-item label="" label-width="180px" prop="privateKeyContentFile">
             <el-upload
               ref="privateKeyContentFile"
+              :before-upload="pemFileBeforeUpload"
+              :http-request="privateKeyContentUpload"
               :limit="1"
               accept=".pem"
               action=""
-              :before-upload="pemFileBeforeUpload"
-              :http-request="privateKeyContentUpload"
             >
               <el-button type="primary">
-                <Icon icon="ep:upload" class="mr-5px" />
+                <Icon class="mr-5px" icon="ep:upload" />
                 点击上传
               </el-button>
             </el-upload>
           </el-form-item>
-          <el-form-item label-width="180px" label="证书序列号" prop="config.certSerialNo">
+          <el-form-item label="证书序列号" label-width="180px" prop="config.certSerialNo">
             <el-input
               v-model="formData.config.certSerialNo"
-              placeholder="请输入证书序列号"
               clearable
+              placeholder="请输入证书序列号"
             />
           </el-form-item>
         </div>
-        <el-form-item label-width="180px" label="备注" prop="remark">
+        <el-form-item label="备注" label-width="180px" prop="remark">
           <el-input v-model="formData.remark" :style="{ width: '100%' }" />
         </el-form-item>
       </el-form>
@@ -182,9 +178,7 @@ const formRules = {
   'config.privateKeyContent': [
     { required: true, message: '请上传 apiclient_key.pem 证书', trigger: 'blur' }
   ],
-  'config.certSerialNo': [
-    { required: true, message: '请输入证书序列号', trigger: 'blur' }
-  ],
+  'config.certSerialNo': [{ required: true, message: '请输入证书序列号', trigger: 'blur' }],
   'config.apiV3Key': [{ required: true, message: '请上传 api V3 密钥值', trigger: 'blur' }]
 }
 const formRef = ref() // 表单 Ref

+ 51 - 43
src/views/pay/app/index.vue

@@ -3,27 +3,27 @@
   <!-- 搜索 -->
   <ContentWrap>
     <el-form
-      class="-mb-15px"
-      :model="queryParams"
       ref="queryFormRef"
       :inline="true"
+      :model="queryParams"
+      class="-mb-15px"
       label-width="68px"
     >
       <el-form-item label="应用名" prop="name">
         <el-input
           v-model="queryParams.name"
-          placeholder="请输入应用名"
+          class="!w-240px"
           clearable
+          placeholder="请输入应用名"
           @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"
+          clearable
+          placeholder="请选择开启状态"
         >
           <el-option
             v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
@@ -36,25 +36,25 @@
       <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"
+          end-placeholder="结束日期"
+          start-placeholder="开始日期"
+          type="daterange"
+          value-format="YYYY-MM-DD HH:mm:ss"
         />
       </el-form-item>
       <el-form-item>
         <el-button @click="handleQuery">
-          <Icon icon="ep:search" class="mr-5px" />
+          <Icon class="mr-5px" icon="ep:search" />
           搜索
         </el-button>
         <el-button @click="resetQuery">
-          <Icon icon="ep:refresh" class="mr-5px" />
+          <Icon class="mr-5px" icon="ep:refresh" />
           重置
         </el-button>
-        <el-button type="primary" plain @click="openForm('create')" v-hasPermi="['pay:app:create']">
-          <Icon icon="ep:plus" class="mr-5px" />
+        <el-button v-hasPermi="['pay:app:create']" plain type="primary" @click="openForm('create')">
+          <Icon class="mr-5px" icon="ep:plus" />
           新增
         </el-button>
       </el-form-item>
@@ -64,9 +64,9 @@
   <!-- 列表 -->
   <ContentWrap>
     <el-table v-loading="loading" :data="list">
-      <el-table-column label="应用编号" align="center" prop="id" />
-      <el-table-column label="应用名" align="center" prop="name" />
-      <el-table-column label="开启状态" align="center" prop="status">
+      <el-table-column align="center" label="应用标识" prop="appKey" />
+      <el-table-column align="center" label="应用名" min-width="90" prop="name" />
+      <el-table-column align="center" label="开启状态" prop="status">
         <template #default="scope">
           <el-switch
             v-model="scope.row.status"
@@ -76,26 +76,28 @@
           />
         </template>
       </el-table-column>
-      <el-table-column label="支付宝配置" align="center">
+      <el-table-column align="center" label="支付宝配置">
         <el-table-column
-          :label="channel.name"
-          align="center"
           v-for="channel in alipayChannels"
           :key="channel.code"
+          :label="channel.name.replace('支付宝', '')"
+          align="center"
         >
           <template #default="scope">
             <el-button
-              type="success"
               v-if="isChannelExists(scope.row.channelCodes, channel.code)"
-              @click="openChannelForm(scope.row, channel.code)"
               circle
+              size="small"
+              type="success"
+              @click="openChannelForm(scope.row, channel.code)"
             >
               <Icon icon="ep:check" />
             </el-button>
             <el-button
               v-else
-              type="danger"
               circle
+              size="small"
+              type="danger"
               @click="openChannelForm(scope.row, channel.code)"
             >
               <Icon icon="ep:close" />
@@ -103,26 +105,28 @@
           </template>
         </el-table-column>
       </el-table-column>
-      <el-table-column label="微信配置" align="center">
+      <el-table-column align="center" label="微信配置">
         <el-table-column
-          :label="channel.name"
-          align="center"
           v-for="channel in wxChannels"
           :key="channel.code"
+          :label="channel.name.replace('微信', '')"
+          align="center"
         >
           <template #default="scope">
             <el-button
-              type="success"
               v-if="isChannelExists(scope.row.channelCodes, channel.code)"
-              @click="openChannelForm(scope.row, channel.code)"
               circle
+              size="small"
+              type="success"
+              @click="openChannelForm(scope.row, channel.code)"
             >
               <Icon icon="ep:check" />
             </el-button>
             <el-button
               v-else
-              type="danger"
               circle
+              size="small"
+              type="danger"
               @click="openChannelForm(scope.row, channel.code)"
             >
               <Icon icon="ep:close" />
@@ -130,21 +134,23 @@
           </template>
         </el-table-column>
       </el-table-column>
-      <el-table-column label="钱包支付配置" align="center">
+      <el-table-column align="center" label="钱包支付配置">
         <el-table-column :label="PayChannelEnum.WALLET.name" align="center">
           <template #default="scope">
             <el-button
-              type="success"
-              circle
               v-if="isChannelExists(scope.row.channelCodes, PayChannelEnum.WALLET.code)"
+              circle
+              size="small"
+              type="success"
               @click="openChannelForm(scope.row, PayChannelEnum.WALLET.code)"
             >
               <Icon icon="ep:check" />
             </el-button>
             <el-button
               v-else
-              type="danger"
               circle
+              size="small"
+              type="danger"
               @click="openChannelForm(scope.row, PayChannelEnum.WALLET.code)"
             >
               <Icon icon="ep:close" />
@@ -152,21 +158,23 @@
           </template>
         </el-table-column>
       </el-table-column>
-      <el-table-column label="模拟支付配置" align="center">
+      <el-table-column align="center" label="模拟支付配置">
         <el-table-column :label="PayChannelEnum.MOCK.name" align="center">
           <template #default="scope">
             <el-button
-              type="success"
-              circle
               v-if="isChannelExists(scope.row.channelCodes, PayChannelEnum.MOCK.code)"
+              circle
+              size="small"
+              type="success"
               @click="openChannelForm(scope.row, PayChannelEnum.MOCK.code)"
             >
               <Icon icon="ep:check" />
             </el-button>
             <el-button
               v-else
-              type="danger"
               circle
+              size="small"
+              type="danger"
               @click="openChannelForm(scope.row, PayChannelEnum.MOCK.code)"
             >
               <Icon icon="ep:close" />
@@ -174,21 +182,21 @@
           </template>
         </el-table-column>
       </el-table-column>
-      <el-table-column label="操作" align="center" min-width="110" fixed="right">
+      <el-table-column align="center" fixed="right" label="操作" min-width="110">
         <template #default="scope">
           <el-button
+            v-hasPermi="['pay:app:update']"
             link
             type="primary"
             @click="openForm('update', scope.row.id)"
-            v-hasPermi="['pay:app:update']"
           >
             编辑
           </el-button>
           <el-button
+            v-hasPermi="['pay:app:delete']"
             link
             type="danger"
             @click="handleDelete(scope.row.id)"
-            v-hasPermi="['pay:app:delete']"
           >
             删除
           </el-button>
@@ -197,9 +205,9 @@
     </el-table>
     <!-- 分页 -->
     <Pagination
-      :total="total"
-      v-model:page="queryParams.pageNo"
       v-model:limit="queryParams.pageSize"
+      v-model:page="queryParams.pageNo"
+      :total="total"
       @pagination="getList"
     />
   </ContentWrap>
@@ -255,7 +263,7 @@ const wxChannels = [
   PayChannelEnum.WX_APP,
   PayChannelEnum.WX_NATIVE,
   PayChannelEnum.WX_WAP,
-  PayChannelEnum.WX_BAR,
+  PayChannelEnum.WX_BAR
 ]
 
 /** 查询列表 */

+ 5 - 5
stylelint.config.js

@@ -13,19 +13,19 @@ module.exports = {
     'at-rule-no-unknown': [
       true,
       {
-        ignoreAtRules: ['function', 'if', 'each', 'include', 'mixin']
+        ignoreAtRules: ['function', 'if', 'each', 'include', 'mixin', 'extend']
       }
     ],
     'media-query-no-invalid': null,
     'function-no-unknown': null,
     'no-empty-source': null,
     'named-grid-areas-no-invalid': null,
-    'unicode-bom': 'never',
+    // 'unicode-bom': 'never',
     'no-descending-specificity': null,
     'font-family-no-missing-generic-family-keyword': null,
-    'declaration-colon-space-after': 'always-single-line',
-    'declaration-colon-space-before': 'never',
-    'declaration-block-trailing-semicolon': null,
+    // 'declaration-colon-space-after': 'always-single-line',
+    // 'declaration-colon-space-before': 'never',
+    // 'declaration-block-trailing-semicolon': null,
     'rule-empty-line-before': [
       'always',
       {

+ 5 - 5
tsconfig.json

@@ -24,11 +24,11 @@
       "@/*": ["src/*"]
     },
     "types": [
-      "@intlify/unplugin-vue-i18n/types",
-      "vite/client",
-      "element-plus/global",
-      "@types/qrcode",
-      "vite-plugin-svg-icons/client"
+      // "@intlify/unplugin-vue-i18n/types",
+      "vite/client"
+      // "element-plus/global",
+      // "@types/qrcode",
+      // "vite-plugin-svg-icons/client"
     ],
     "outDir": "target", // 请保留这个属性,防止tsconfig.json文件报错
     "typeRoots": ["./node_modules/@types/", "./types"]

+ 75 - 68
vite.config.ts

@@ -1,78 +1,85 @@
-import { resolve } from 'path'
-import { loadEnv } from 'vite'
-import type { UserConfig, ConfigEnv } from 'vite'
-import { createVitePlugins } from './build/vite'
-import { include, exclude } from "./build/vite/optimize"
+import {resolve} from 'path'
+import type {ConfigEnv, UserConfig} from 'vite'
+import {loadEnv} from 'vite'
+import {createVitePlugins} from './build/vite'
+import {exclude, include} from "./build/vite/optimize"
 // 当前执行node命令时文件夹的地址(工作目录)
 const root = process.cwd()
 
 // 路径查找
 function pathResolve(dir: string) {
-  return resolve(root, '.', dir)
+    return resolve(root, '.', dir)
 }
 
 // https://vitejs.dev/config/
-export default ({ command, mode }: ConfigEnv): UserConfig => {
-  let env = {} as any
-  const isBuild = command === 'build'
-  if (!isBuild) {
-    env = loadEnv((process.argv[3] === '--mode' ? process.argv[4] : process.argv[3]), root)
-  } else {
-    env = loadEnv(mode, root)
-  }
-  return {
-    base: env.VITE_BASE_PATH,
-    root: root,
-    // 服务端渲染
-    server: {
-      port: env.VITE_PORT, // 端口号
-      host: "0.0.0.0",
-      open: env.VITE_OPEN === 'true',
-      // 本地跨域代理. 目前注释的原因:暂时没有用途,server 端已经支持跨域
-      // proxy: {
-      //   ['/admin-api']: {
-      //     target: env.VITE_BASE_URL,
-      //     ws: false,
-      //     changeOrigin: true,
-      //     rewrite: (path) => path.replace(new RegExp(`^/admin-api`), ''),
-      //   },
-      // },
-    },
-    // 项目使用的vite插件。 单独提取到build/vite/plugin中管理
-    plugins: createVitePlugins(),
-    css: {
-      preprocessorOptions: {
-        scss: {
-          additionalData: '@import "./src/styles/variables.scss";',
-          javascriptEnabled: true
-        }
-      }
-    },
-    resolve: {
-      extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.scss', '.css'],
-      alias: [
-        {
-          find: 'vue-i18n',
-          replacement: 'vue-i18n/dist/vue-i18n.cjs.js'
+export default ({command, mode}: ConfigEnv): UserConfig => {
+    let env = {} as any
+    const isBuild = command === 'build'
+    if (!isBuild) {
+        env = loadEnv((process.argv[3] === '--mode' ? process.argv[4] : process.argv[3]), root)
+    } else {
+        env = loadEnv(mode, root)
+    }
+    return {
+        base: env.VITE_BASE_PATH,
+        root: root,
+        // 服务端渲染
+        server: {
+            port: env.VITE_PORT, // 端口号
+            host: "0.0.0.0",
+            open: env.VITE_OPEN === 'true',
+            // 本地跨域代理. 目前注释的原因:暂时没有用途,server 端已经支持跨域
+            // proxy: {
+            //   ['/admin-api']: {
+            //     target: env.VITE_BASE_URL,
+            //     ws: false,
+            //     changeOrigin: true,
+            //     rewrite: (path) => path.replace(new RegExp(`^/admin-api`), ''),
+            //   },
+            // },
         },
-        {
-          find: /\@\//,
-          replacement: `${pathResolve('src')}/`
-        }
-      ]
-    },
-    build: {
-      minify: 'terser',
-      outDir: env.VITE_OUT_DIR || 'dist',
-      sourcemap: env.VITE_SOURCEMAP === 'true' ? 'inline' : false,
-      // brotliSize: false,
-      terserOptions: {
-        compress: {
-          drop_debugger: env.VITE_DROP_DEBUGGER === 'true',
-          drop_console: env.VITE_DROP_CONSOLE === 'true'
-        }
-      }
-    },
-    optimizeDeps: { include, exclude }
-  }
+        // 项目使用的vite插件。 单独提取到build/vite/plugin中管理
+        plugins: createVitePlugins(),
+        css: {
+            preprocessorOptions: {
+                scss: {
+                    additionalData: '@import "./src/styles/variables.scss";',
+                    javascriptEnabled: true
+                }
+            }
+        },
+        resolve: {
+            extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.scss', '.css'],
+            alias: [
+                {
+                    find: 'vue-i18n',
+                    replacement: 'vue-i18n/dist/vue-i18n.cjs.js'
+                },
+                {
+                    find: /\@\//,
+                    replacement: `${pathResolve('src')}/`
+                }
+            ]
+        },
+        build: {
+            minify: 'terser',
+            outDir: env.VITE_OUT_DIR || 'dist',
+            sourcemap: env.VITE_SOURCEMAP === 'true' ? 'inline' : false,
+            // brotliSize: false,
+            terserOptions: {
+                compress: {
+                    drop_debugger: env.VITE_DROP_DEBUGGER === 'true',
+                    drop_console: env.VITE_DROP_CONSOLE === 'true'
+                }
+            },
+            rollupOptions: {
+                output: {
+                    manualChunks: {
+                        echarts: ['echarts'] // 将 echarts 单独打包,参考 https://gitee.com/yudaocode/yudao-ui-admin-vue3/issues/IAB1SX 讨论
+                    }
+                },
+            },
+        },
+        optimizeDeps: {include, exclude}
+    }
 }