Pārlūkot izejas kodu

Merge remote-tracking branch 'refs/remotes/yudao/dev' into dev-crm

puhui999 10 mēneši atpakaļ
vecāks
revīzija
8bbfaf2aeb
53 mainītis faili ar 6499 papildinājumiem un 1122 dzēšanām
  1. 0 1
      .gitignore
  2. 11 1
      build/vite/optimize.ts
  3. 7 3
      package.json
  4. 5750 924
      pnpm-lock.yaml
  5. 2 6
      src/api/ai/image/index.ts
  6. 38 0
      src/api/ai/mindmap/index.ts
  7. 1 1
      src/api/bpm/definition/index.ts
  8. 1 0
      src/api/bpm/model/index.ts
  9. 2 11
      src/api/bpm/processInstance/index.ts
  10. 1 1
      src/components/ConfigGlobal/src/ConfigGlobal.vue
  11. 2 2
      src/components/DictTag/src/DictTag.vue
  12. 1 0
      src/components/DiyEditor/components/ComponentLibrary.vue
  13. 49 9
      src/components/Editor/src/Editor.vue
  14. 5 6
      src/components/MarkdownView/index.vue
  15. 1 0
      src/components/UploadFile/src/UploadFile.vue
  16. 9 7
      src/components/bpmnProcessDesigner/package/penal/listeners/ProcessListenerDialog.vue
  17. 10 8
      src/components/bpmnProcessDesigner/package/penal/task/task-components/ProcessExpressionDialog.vue
  18. 3 1
      src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue
  19. 1 1
      src/config/axios/service.ts
  20. 5 0
      src/layout/components/Menu/src/Menu.vue
  21. 1 1
      src/layout/components/UserInfo/src/components/LockPage.vue
  22. 22 21
      src/router/modules/remaining.ts
  23. 0 1
      src/utils/dict.ts
  24. 21 6
      src/utils/download.ts
  25. 5 6
      src/views/ai/chat/index/index.vue
  26. 14 2
      src/views/ai/image/index/components/ImageList.vue
  27. 40 21
      src/views/ai/image/square/index.vue
  28. 78 0
      src/views/ai/mindmap/index/components/Left.vue
  29. 164 0
      src/views/ai/mindmap/index/components/Right.vue
  30. 92 0
      src/views/ai/mindmap/index/index.vue
  31. 0 0
      src/views/ai/music/index/index.vue
  32. 0 0
      src/views/ai/music/index/list/audioBar/index.vue
  33. 0 0
      src/views/ai/music/index/list/index.vue
  34. 0 0
      src/views/ai/music/index/list/songCard/index.vue
  35. 0 0
      src/views/ai/music/index/list/songInfo/index.vue
  36. 0 0
      src/views/ai/music/index/mode/desc.vue
  37. 0 0
      src/views/ai/music/index/mode/index.vue
  38. 0 0
      src/views/ai/music/index/mode/lyric.vue
  39. 0 0
      src/views/ai/music/index/title/index.vue
  40. 66 1
      src/views/ai/utils/constants.ts
  41. 4 4
      src/views/bpm/processInstance/detail/index.vue
  42. 19 5
      src/views/bpm/processInstance/index.vue
  43. 2 1
      src/views/infra/fileConfig/index.vue
  44. 1 1
      src/views/infra/webSocket/index.vue
  45. 19 1
      src/views/mall/product/category/index.vue
  46. 1 1
      src/views/mall/product/spu/form/index.vue
  47. 6 0
      src/views/mall/product/spu/index.vue
  48. 9 48
      src/views/pay/app/components/channel/WeixinChannelForm.vue
  49. 3 1
      src/views/pay/order/OrderDetail.vue
  50. 3 1
      src/views/pay/refund/RefundDetail.vue
  51. 21 14
      src/views/pay/wallet/balance/index.vue
  52. 3 3
      src/views/system/operatelog/index.vue
  53. 6 1
      src/views/system/sms/channel/index.vue

+ 0 - 1
.gitignore

@@ -2,7 +2,6 @@ node_modules
 .DS_Store
 dist
 dist-ssr
-*.local
 /dist*
 pnpm-debug
 auto-*.d.ts

+ 11 - 1
build/vite/optimize.ts

@@ -27,6 +27,12 @@ const include = [
   'echarts-wordcloud',
   '@wangeditor/editor',
   '@wangeditor/editor-for-vue',
+  '@microsoft/fetch-event-source',
+  'markdown-it',
+  'markmap-view',
+  'markmap-lib',
+  'markmap-toolbar',
+  'highlight.js',
   'element-plus',
   'element-plus/es',
   'element-plus/es/locale/lang/zh-cn',
@@ -104,7 +110,11 @@ const include = [
   'element-plus/es/components/collapse/style/css',
   'element-plus/es/components/collapse-item/style/css',
   'element-plus/es/components/button-group/style/css',
-  'element-plus/es/components/text/style/css'
+  'element-plus/es/components/text/style/css',
+  'element-plus/es/components/segmented/style/css',
+  '@element-plus/icons-vue',
+  'element-plus/es/components/footer/style/css',
+  'element-plus/es/components/empty/style/css'
 ]
 
 const exclude = ['@iconify/json']

+ 7 - 3
package.json

@@ -1,6 +1,6 @@
 {
   "name": "yudao-ui-admin-vue3",
-  "version": "2.1.0-snapshot",
+  "version": "2.2.0-snapshot",
   "description": "基于vue3、vite4、element-plus、typesScript",
   "author": "xingyu",
   "private": false,
@@ -52,7 +52,11 @@
     "highlight.js": "^11.9.0",
     "jsencrypt": "^3.3.2",
     "lodash-es": "^4.17.21",
-    "markdown-it": "^13.0.1",
+    "markdown-it": "^14.1.0",
+    "markmap-common": "^0.16.0",
+    "markmap-lib": "^0.16.1",
+    "markmap-toolbar": "^0.17.0",
+    "markmap-view": "^0.16.0",
     "min-dash": "^4.1.1",
     "mitt": "^3.0.1",
     "nprogress": "^0.2.0",
@@ -85,8 +89,8 @@
     "@types/qs": "^6.9.12",
     "@typescript-eslint/eslint-plugin": "^7.1.0",
     "@typescript-eslint/parser": "^7.1.0",
-    "@unocss/transformer-variant-group": "^0.58.5",
     "@unocss/eslint-config": "^0.57.4",
+    "@unocss/transformer-variant-group": "^0.58.5",
     "@vitejs/plugin-legacy": "^5.3.1",
     "@vitejs/plugin-vue": "^5.0.4",
     "@vitejs/plugin-vue-jsx": "^3.1.0",

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 5750 - 924
pnpm-lock.yaml


+ 2 - 6
src/api/ai/image/index.ts

@@ -53,13 +53,9 @@ export interface ImageMidjourneyButtonsVO {
 // AI 图片 API
 export const ImageApi = {
   // 获取【我的】绘图分页
-  getImagePageMy: async (params: PageParam) => {
+  getImagePageMy: async (params: any) => {
     return await request.get({ url: `/ai/image/my-page`, params })
   },
-  // 获取公开的绘图记录
-  getImagePagePublic: async (params) => {
-    return await request.get({ url: `/ai/image/public-page`, params })
-  },
   // 获取【我的】绘图记录
   getImageMy: async (id: number) => {
     return await request.get({ url: `/ai/image/get-my?id=${id}` })
@@ -97,7 +93,7 @@ export const ImageApi = {
 
   // 更新绘画发布状态
   updateImage: async (data: any) => {
-    return await request.put({ url: '/ai/image/update-public-status', data })
+    return await request.put({ url: '/ai/image/update', data })
   },
 
   // 删除绘画

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

@@ -0,0 +1,38 @@
+import { getAccessToken } from '@/utils/auth'
+import { fetchEventSource } from '@microsoft/fetch-event-source'
+import { config } from '@/config/axios/config'
+
+export interface AiMindMapGenerateReqVO {
+  prompt: string
+}
+
+export const AiMindMapApi = {
+  generateMindMap: ({
+    data,
+    onClose,
+    onMessage,
+    onError,
+    ctrl
+  }: {
+    data: AiMindMapGenerateReqVO
+    onMessage?: (res: any) => void
+    onError?: (...args: any[]) => void
+    onClose?: (...args: any[]) => void
+    ctrl: AbortController
+  }) => {
+    const token = getAccessToken()
+    return fetchEventSource(`${config.base_url}/ai/mind-map/generate-stream`, {
+      method: 'post',
+      headers: {
+        'Content-Type': 'application/json',
+        Authorization: `Bearer ${token}`
+      },
+      openWhenHidden: true,
+      body: JSON.stringify(data),
+      onmessage: onMessage,
+      onerror: onError,
+      onclose: onClose,
+      signal: ctrl.signal
+    })
+  }
+}

+ 1 - 1
src/api/bpm/definition/index.ts

@@ -1,6 +1,6 @@
 import request from '@/config/axios'
 
-export const getProcessDefinition = async (id: number, key: string) => {
+export const getProcessDefinition = async (id?: string, key?: string) => {
   return await request.get({
     url: '/bpm/process-definition/get',
     params: { id, key }

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

@@ -5,6 +5,7 @@ export type ProcessDefinitionVO = {
   version: number
   deploymentTIme: string
   suspensionState: number
+  formType?: number
 }
 
 export type ModelVO = {

+ 2 - 11
src/api/bpm/processInstance/index.ts

@@ -1,4 +1,5 @@
 import request from '@/config/axios'
+import { ProcessDefinitionVO } from '@/api/bpm/model'
 
 export type Task = {
   id: string
@@ -18,17 +19,7 @@ export type ProcessInstanceVO = {
   businessKey: string
   createTime: string
   endTime: string
-}
-
-export type ProcessInstanceCopyVO = {
-  type: number
-  taskName: string
-  taskKey: string
-  processInstanceName: string
-  processInstanceKey: string
-  startUserId: string
-  options: string[]
-  reason: string
+  processDefinition?: ProcessDefinitionVO
 }
 
 export const getProcessInstanceMyPage = async (params: any) => {

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

@@ -54,7 +54,7 @@ const currentLocale = computed(() => localeStore.currentLocale)
   <ElConfigProvider
     :namespace="variables.elNamespace"
     :locale="currentLocale.elLocale"
-    :message="{ max: 1 }"
+    :message="{ max: 5 }"
     :size="size"
   >
     <slot></slot>

+ 2 - 2
src/components/DictTag/src/DictTag.vue

@@ -22,8 +22,8 @@ export default defineComponent({
       const dictOptions = getDictOptions(dictType)
       dictOptions.forEach((dict: DictDataType) => {
         if (dict.value === value) {
-          if (dict.colorType + '' === 'primary' || dict.colorType + '' === 'default') {
-            dict.colorType = ''
+          if (dict.colorType + '' === 'default') {
+            dict.colorType = 'info'
           }
           dictData.value = dict
         }

+ 1 - 0
src/components/DiyEditor/components/ComponentLibrary.vue

@@ -95,6 +95,7 @@ const handleCloneComponent = (component: DiyComponent<any>) => {
 .editor-left {
   z-index: 1;
   flex-shrink: 0;
+  user-select: none;
   box-shadow: 8px 0 8px -8px rgb(0 0 0 / 12%);
 
   :deep(.el-collapse) {

+ 49 - 9
src/components/Editor/src/Editor.vue

@@ -96,11 +96,6 @@ const editorConfig = computed((): IEditorConfig => {
           // 选择文件时的类型限制,默认为 ['image/*'] 。如不想限制,则设置为 []
           allowedFileTypes: ['image/*'],
 
-          // 自定义上传参数,例如传递验证的 token 等。参数会被添加到 formData 中,一起上传到服务端。
-          meta: { updateSupport: 0 },
-          // 将 meta 拼接到 url 参数中,默认 false
-          metaWithUrl: true,
-
           // 自定义增加 http  header
           headers: {
             Accept: '*',
@@ -108,9 +103,6 @@ const editorConfig = computed((): IEditorConfig => {
             'tenant-id': getTenantId()
           },
 
-          // 跨域是否传递 cookie ,默认为 false
-          withCredentials: true,
-
           // 超时时间,默认为 10 秒
           timeout: 5 * 1000, // 5 秒
 
@@ -119,7 +111,7 @@ const editorConfig = computed((): IEditorConfig => {
 
           // 上传之前触发
           onBeforeUpload(file: File) {
-            console.log(file)
+            // console.log(file)
             return file
           },
           // 上传进度的回调函数
@@ -142,6 +134,54 @@ const editorConfig = computed((): IEditorConfig => {
           customInsert(res: any, insertFn: InsertFnType) {
             insertFn(res.data, 'image', res.data)
           }
+        },
+        ['uploadVideo']: {
+          server: import.meta.env.VITE_UPLOAD_URL,
+          // 单个文件的最大体积限制,默认为 10M
+          maxFileSize: 10 * 1024 * 1024,
+          // 最多可上传几个文件,默认为 100
+          maxNumberOfFiles: 10,
+          // 选择文件时的类型限制,默认为 ['video/*'] 。如不想限制,则设置为 []
+          allowedFileTypes: ['video/*'],
+
+          // 自定义增加 http  header
+          headers: {
+            Accept: '*',
+            Authorization: 'Bearer ' + getAccessToken(),
+            'tenant-id': getTenantId()
+          },
+
+          // 超时时间,默认为 30 秒
+          timeout: 15 * 1000, // 15 秒
+
+          // form-data fieldName,后端接口参数名称,默认值wangeditor-uploaded-image
+          fieldName: 'file',
+
+          // 上传之前触发
+          onBeforeUpload(file: File) {
+            // console.log(file)
+            return file
+          },
+          // 上传进度的回调函数
+          onProgress(progress: number) {
+            // progress 是 0-100 的数字
+            console.log('progress', progress)
+          },
+          onSuccess(file: File, res: any) {
+            console.log('onSuccess', file, res)
+          },
+          onFailed(file: File, res: any) {
+            alert(res.message)
+            console.log('onFailed', file, res)
+          },
+          onError(file: File, err: any, res: any) {
+            alert(err.message)
+            console.error('onError', file, err, res)
+          },
+          // 自定义插入图片
+          customInsert(res: any, insertFn: InsertFnType) {
+            insertFn(res.data, 'mp4', res.data)
+          }
         }
       },
       uploadImgShowBase64: true

+ 5 - 6
src/components/MarkdownView/index.vue

@@ -1,10 +1,9 @@
 <template>
-<!--  <div ref="contentRef" class="markdown-view" v-html="contentHtml"></div>-->
   <div ref="contentRef" class="markdown-view" v-html="renderedMarkdown"></div>
 </template>
 
 <script setup lang="ts">
-import {useClipboard} from '@vueuse/core'
+import { useClipboard } from '@vueuse/core'
 import MarkdownIt from 'markdown-it'
 import 'highlight.js/styles/vs2015.min.css'
 import hljs from 'highlight.js'
@@ -20,7 +19,6 @@ const props = defineProps({
 const message = useMessage() // 消息弹窗
 const { copy } = useClipboard() // 初始化 copy 到粘贴板
 const contentRef = ref()
-const { content } = toRefs(props) // 将 props 变为引用类型
 
 const md = new MarkdownIt({
   highlight: function (str, lang) {
@@ -32,11 +30,12 @@ const md = new MarkdownIt({
     }
     return ``
   }
-});
+})
 
+/** 渲染 markdown */
 const renderedMarkdown = computed(() => {
-  return md.render(props.content);
-});
+  return md.render(props.content)
+})
 
 /** 初始化 **/
 onMounted(async () => {

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

@@ -32,6 +32,7 @@
           格式为 <b style="color: #f56c6c">{{ fileType.join('/') }}</b> 的文件
         </div>
       </template>
+      <!-- TODO @puhui999:1)表单展示的时候,位置会偏掉,已发微信;2)disable 的时候,应该把【删除】按钮也隐藏掉? -->
       <template #file="row">
         <div class="flex items-center">
           <span>{{ row.file.name }}</span>

+ 9 - 7
src/components/bpmnProcessDesigner/package/penal/listeners/ProcessListenerDialog.vue

@@ -43,9 +43,6 @@ import { CommonStatusEnum } from '@/utils/constants'
 /** BPM 流程 表单 */
 defineOptions({ name: 'ProcessListenerDialog' })
 
-const { t } = useI18n() // 国际化
-const message = useMessage() // 消息弹窗
-
 const dialogVisible = ref(false) // 弹窗的是否展示
 const loading = ref(true) // 列表的加载中
 const list = ref<ProcessListenerVO[]>([]) // 列表的数据
@@ -53,17 +50,23 @@ const total = ref(0) // 列表的总页数
 const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
-  type: undefined,
+  type: '',
   status: CommonStatusEnum.ENABLE
 })
 
 /** 打开弹窗 */
 const open = async (type: string) => {
+  queryParams.pageNo = 1
+  queryParams.type = type
+  getList()
   dialogVisible.value = true
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 查询列表 */
+const getList = async () => {
   loading.value = true
   try {
-    queryParams.pageNo = 1
-    queryParams.type = type
     const data = await ProcessListenerApi.getProcessListenerPage(queryParams)
     list.value = data.list
     total.value = data.total
@@ -71,7 +74,6 @@ const open = async (type: string) => {
     loading.value = false
   }
 }
-defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 
 /** 提交表单 */
 const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调

+ 10 - 8
src/components/bpmnProcessDesigner/package/penal/task/task-components/ProcessExpressionDialog.vue

@@ -28,9 +28,6 @@ import { ProcessExpressionApi, ProcessExpressionVO } from '@/api/bpm/processExpr
 /** BPM 流程 表单 */
 defineOptions({ name: 'ProcessExpressionDialog' })
 
-const { t } = useI18n() // 国际化
-const message = useMessage() // 消息弹窗
-
 const dialogVisible = ref(false) // 弹窗的是否展示
 const loading = ref(true) // 列表的加载中
 const list = ref<ProcessExpressionVO[]>([]) // 列表的数据
@@ -38,17 +35,23 @@ const total = ref(0) // 列表的总页数
 const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
-  type: undefined,
+  type: '',
   status: CommonStatusEnum.ENABLE
 })
 
 /** 打开弹窗 */
-const open = async (type: string) => {
+const open = (type: string) => {
+  queryParams.pageNo = 1
+  queryParams.type = type
+  getList()
   dialogVisible.value = true
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 查询列表 */
+const getList = async () => {
   loading.value = true
   try {
-    queryParams.pageNo = 1
-    queryParams.type = type
     const data = await ProcessExpressionApi.getProcessExpressionPage(queryParams)
     list.value = data.list
     total.value = data.total
@@ -56,7 +59,6 @@ const open = async (type: string) => {
     loading.value = false
   }
 }
-defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 
 /** 提交表单 */
 const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调

+ 3 - 1
src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue

@@ -135,6 +135,7 @@ import * as PostApi from '@/api/system/post'
 import * as UserApi from '@/api/system/user'
 import * as UserGroupApi from '@/api/bpm/userGroup'
 import ProcessExpressionDialog from './ProcessExpressionDialog.vue'
+import { ProcessExpressionVO } from '@/api/bpm/processExpression'
 
 defineOptions({ name: 'UserTask' })
 const props = defineProps({
@@ -197,8 +198,9 @@ const processExpressionDialogRef = ref()
 const openProcessExpressionDialog = async () => {
   processExpressionDialogRef.value.open()
 }
-const selectProcessExpression = (expression) => {
+const selectProcessExpression = (expression: ProcessExpressionVO) => {
   userTaskForm.value.candidateParam = [expression.expression]
+  updateElementTask()
 }
 
 watch(

+ 1 - 1
src/config/axios/service.ts

@@ -81,7 +81,7 @@ service.interceptors.request.use(
   (error: AxiosError) => {
     // Do something with request error
     console.log(error) // for debug
-    Promise.reject(error)
+    return Promise.reject(error)
   }
 )
 

+ 5 - 0
src/layout/components/Menu/src/Menu.vue

@@ -90,6 +90,11 @@ export default defineComponent({
           backgroundColor="var(--left-menu-bg-color)"
           textColor="var(--left-menu-text-color)"
           activeTextColor="var(--left-menu-text-active-color)"
+          popperClass={
+            unref(menuMode) === 'vertical'
+              ? `${prefixCls}-popper--vertical`
+              : `${prefixCls}-popper--horizontal`
+          }
           onSelect={menuSelect}
         >
           {{

+ 1 - 1
src/layout/components/UserInfo/src/components/LockPage.vue

@@ -52,7 +52,7 @@ async function goLogin() {
   // 登出后清理
   deleteUserCache() // 清空用户缓存
   tagsViewStore.delAllViews()
-  resetRouter() // 重置静态路由表
+  // resetRouter() // 重置静态路由表
   lockStore.resetLockInfo()
   replace('/login')
 }

+ 22 - 21
src/router/modules/remaining.ts

@@ -70,26 +70,6 @@ const remainingRouter: AppRouteRecordRaw[] = [
       }
     ]
   },
-  // {
-  //   path: '/ai/music',
-  //   component: Layout,
-  //   redirect: '/index',
-  //   name: 'AIMusic',
-  //   meta: {},
-  //   children: [
-  //     {
-  //       path: 'index',
-  //       component: () => import('@/views/ai/music/components/index.vue'),
-  //       name: 'AIMusicIndex',
-  //       meta: {
-  //         title: 'AI 音乐',
-  //         icon: 'ep:home-filled',
-  //         noCache: false,
-  //         affix: true
-  //       }
-  //     }
-  //   ]
-  // },
   {
     path: '/user',
     component: Layout,
@@ -361,7 +341,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
         component: () => import('@/views/mall/product/spu/form/index.vue'),
         name: 'ProductSpuAdd',
         meta: {
-          noCache: true,
+          noCache: false, // 需要缓存
           hidden: true,
           canTo: true,
           icon: 'ep:edit',
@@ -593,6 +573,27 @@ const remainingRouter: AppRouteRecordRaw[] = [
         component: () => import('@/views/crm/product/detail/index.vue')
       }
     ]
+  },
+  {
+    path: '/ai',
+    component: Layout,
+    name: 'Ai',
+    meta: {
+      hidden: true
+    },
+    children: [
+      {
+        path: 'image/square',
+        component: () => import('@/views/ai/image/square/index.vue'),
+        name: 'AiImageSquare',
+        meta: {
+          title: '绘图作品',
+          icon: 'ep:home-filled',
+          noCache: false,
+          affix: true
+        }
+      }
+    ]
   }
 ]
 

+ 0 - 1
src/utils/dict.ts

@@ -125,7 +125,6 @@ export enum DICT_TYPE {
   SYSTEM_SMS_TEMPLATE_TYPE = 'system_sms_template_type',
   SYSTEM_SMS_SEND_STATUS = 'system_sms_send_status',
   SYSTEM_SMS_RECEIVE_STATUS = 'system_sms_receive_status',
-  SYSTEM_ERROR_CODE_TYPE = 'system_error_code_type',
   SYSTEM_OAUTH2_GRANT_TYPE = 'system_oauth2_grant_type',
   SYSTEM_MAIL_SEND_STATUS = 'system_mail_send_status',
   SYSTEM_NOTIFY_TEMPLATE_TYPE = 'system_notify_template_type',

+ 21 - 6
src/utils/download.ts

@@ -34,16 +34,31 @@ const download = {
     download0(data, fileName, 'text/markdown')
   },
   // 下载图片(允许跨域)
-  image: (url: string) => {
+  image: ({
+    url,
+    canvasWidth,
+    canvasHeight,
+    drawWithImageSize = true
+  }: {
+    url: string
+    canvasWidth?: number // 指定画布宽度
+    canvasHeight?: number // 指定画布高度
+    drawWithImageSize?: boolean // 将图片绘制在画布上时带上图片的宽高值, 默认是要带上的
+  }) => {
     const image = new Image()
-    image.setAttribute('crossOrigin', 'anonymous')
+    // image.setAttribute('crossOrigin', 'anonymous')
     image.src = url
     image.onload = () => {
       const canvas = document.createElement('canvas')
-      canvas.width = image.width
-      canvas.height = image.height
-      const ctx = canvas.getContext('2d') as CanvasDrawImage
-      ctx.drawImage(image, 0, 0, image.width, image.height)
+      canvas.width = canvasWidth || image.width
+      canvas.height = canvasHeight || image.height
+      const ctx = canvas.getContext('2d') as CanvasRenderingContext2D
+      ctx?.clearRect(0, 0, canvas.width, canvas.height)
+      if (drawWithImageSize) {
+        ctx.drawImage(image, 0, 0, image.width, image.height)
+      } else {
+        ctx.drawImage(image, 0, 0)
+      }
       const url = canvas.toDataURL('image/png')
       const a = document.createElement('a')
       a.href = url

+ 5 - 6
src/views/ai/chat/index/index.vue

@@ -4,10 +4,10 @@
     <ConversationList
       :active-id="activeConversationId"
       ref="conversationListRef"
-      @onConversationCreate="handleConversationCreateSuccess"
-      @onConversationClick="handleConversationClick"
-      @onConversationClear="handleConversationClear"
-      @onConversationDelete="handlerConversationDelete"
+      @on-conversation-create="handleConversationCreateSuccess"
+      @on-conversation-click="handleConversationClick"
+      @on-conversation-clear="handleConversationClear"
+      @on-conversation-delete="handlerConversationDelete"
     />
     <!-- 右侧:对话详情 -->
     <el-container class="detail-container">
@@ -27,7 +27,7 @@
           <el-button size="small" class="btn">
             <Icon icon="ep:download" color="#787878" />
           </el-button>
-          <el-button size="small" class="btn" @click="handleGoTopMessage" >
+          <el-button size="small" class="btn" @click="handleGoTopMessage">
             <Icon icon="ep:top" color="#787878" />
           </el-button>
         </div>
@@ -119,7 +119,6 @@ import MessageList from './components/message/MessageList.vue'
 import MessageListEmpty from './components/message/MessageListEmpty.vue'
 import MessageLoading from './components/message/MessageLoading.vue'
 import MessageNewConversation from './components/message/MessageNewConversation.vue'
-import { Download, Top } from '@element-plus/icons-vue'
 
 /** AI 聊天对话 列表 */
 defineOptions({ name: 'AiChat' })

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

@@ -1,6 +1,10 @@
 <template>
   <el-card class="dr-task" body-class="task-card" shadow="never">
-    <template #header>绘画任务</template>
+    <template #header>
+      绘画任务
+      <!-- TODO @fan:看看,怎么优化下这个样子哈。 -->
+      <el-button @click="handleViewPublic">绘画作品</el-button>
+    </template>
     <!-- 图片列表 -->
     <div class="task-image-list" ref="imageListRef">
       <ImageCard
@@ -42,6 +46,7 @@ import { AiImageStatusEnum } from '@/views/ai/utils/constants'
 import download from '@/utils/download'
 
 const message = useMessage() // 消息弹窗
+const router = useRouter() // 路由
 
 // 图片分页相关的参数
 const queryParams = reactive({
@@ -59,6 +64,13 @@ const inProgressTimer = ref<any>() // 生成中的 image 定时器,轮询生
 const isShowImageDetail = ref<boolean>(false) // 图片详情是否展示
 const showImageDetailId = ref<number>(0) // 图片详情的图片编号
 
+/** 处理查看绘图作品 */
+const handleViewPublic = () => {
+  router.push({
+    name: 'AiImageSquare'
+  })
+}
+
 /** 查看图片的详情  */
 const handleDetailOpen = async () => {
   isShowImageDetail.value = true
@@ -138,7 +150,7 @@ const handleImageButtonClick = async (type: string, imageDetail: ImageVO) => {
   }
   // 下载
   if (type === 'download') {
-    await download.image(imageDetail.picUrl)
+    await download.image({ url: imageDetail.picUrl })
     return
   }
   // 重新生成

+ 40 - 21
src/views/ai/image/square/index.vue

@@ -1,48 +1,67 @@
 <template>
   <div class="square-container">
+    <!-- TODO @fan:style 建议换成 unocss -->
+    <!-- TODO @fan:Search 可以换成 Icon 组件么? -->
     <el-input
-      v-model="searchText"
+      v-model="queryParams.prompt"
       style="width: 100%; margin-bottom: 20px"
       size="large"
       placeholder="请输入要搜索的内容"
       :suffix-icon="Search"
-      @keyup.enter="handleSearch"
+      @keyup.enter="handleQuery"
     />
     <div class="gallery">
-      <div v-for="item in publicList" :key="item.id" class="gallery-item">
+      <!-- TODO @fan:这个图片的风格,要不和 ImageCard.vue 界面一致?(只有卡片,没有操作);因为看着更有相框的感觉~~~ -->
+      <div v-for="item in list" :key="item.id" class="gallery-item">
         <img :src="item.picUrl" class="img" />
       </div>
     </div>
+    <!-- TODO @fan:缺少翻页 -->
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
   </div>
 </template>
 <script setup lang="ts">
 import { ImageApi, ImageVO } from '@/api/ai/image'
 import { Search } from '@element-plus/icons-vue'
 
-/** 属性 */
-// TODO @fan:queryParams 里面搞分页哈。
-const pageNo = ref<number>(1)
-const pageSize = ref<number>(20)
-const publicList = ref<ImageVO[]>([])
-const searchText = ref<string>('')
+// TODO @fan:加个 loading 加载中的状态
+const loading = ref(true) // 列表的加载中
+const list = ref<ImageVO[]>([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  publicStatus: true,
+  prompt: undefined
+})
 
-/** 获取数据 */
-const getListData = async () => {
-  const res = await ImageApi.getImagePagePublic({
-    pageNo: pageNo.value,
-    pageSize: pageSize.value,
-    prompt: searchText.value
-  })
-  publicList.value = res.list as ImageVO[]
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await ImageApi.getImagePageMy(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
 }
 
-/** 搜索 */
-const handleSearch = async () => {
-  await getListData()
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
 }
 
+/** 初始化 */
 onMounted(async () => {
-  await getListData()
+  await getList()
 })
 </script>
 <style scoped lang="scss">

+ 78 - 0
src/views/ai/mindmap/index/components/Left.vue

@@ -0,0 +1,78 @@
+<template>
+  <div class="w-[350px] p-5 flex flex-col bg-[#f5f7f9]">
+    <h3 class="w-full h-full h-7 text-5 text-center leading-[28px] title">思维导图创作中心</h3>
+    <!--下面表单部分-->
+    <div class="flex-grow overflow-y-auto">
+      <div class="mt-[30ppx]">
+        <el-text tag="b">您的需求?</el-text>
+        <el-input
+          v-model="formData.prompt"
+          maxlength="1024"
+          rows="5"
+          class="w-100% mt-15px"
+          input-style="border-radius: 7px;"
+          placeholder="请输入提示词,让AI帮你完善"
+          show-word-limit
+          type="textarea"
+        />
+        <el-button
+          class="!w-full mt-[15px]"
+          type="primary"
+          :loading="isGenerating"
+          @click="emits('submit', formData)"
+        >
+          智能生成思维导图
+        </el-button>
+      </div>
+      <div class="mt-[30px]">
+        <el-text tag="b">使用已有内容生成?</el-text>
+        <el-input
+          v-model="generatedContent"
+          maxlength="1024"
+          rows="5"
+          class="w-100% mt-15px"
+          input-style="border-radius: 7px;"
+          placeholder="例如:童话里的小屋应该是什么样子?"
+          show-word-limit
+          type="textarea"
+        />
+        <el-button
+          class="!w-full mt-[15px]"
+          type="primary"
+          @click="emits('directGenerate', generatedContent)"
+          :disabled="isGenerating"
+        >
+          直接生成
+        </el-button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { MindMapContentExample } from '@/views/ai/utils/constants'
+
+const emits = defineEmits(['submit', 'directGenerate'])
+defineProps<{
+  isGenerating: boolean
+}>()
+// 提交的提示词字段
+const formData = reactive({
+  prompt: ''
+})
+
+const generatedContent = ref(MindMapContentExample) // 已有的内容
+
+defineExpose({
+  setGeneratedContent(newContent: string) {
+    // 设置已有的内容,在生成结束的时候将结果赋值给该值
+    generatedContent.value = newContent
+  }
+})
+</script>
+
+<style lang="scss" scoped>
+.title {
+  color: var(--el-color-primary);
+}
+</style>

+ 164 - 0
src/views/ai/mindmap/index/components/Right.vue

@@ -0,0 +1,164 @@
+<template>
+  <el-card class="my-card h-full flex-grow">
+    <template #header>
+      <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">
+          <template #icon>
+            <Icon icon="ph:copy-bold" />
+          </template>
+          下载图片
+        </el-button>
+      </h3>
+    </template>
+
+    <div ref="contentRef" class="hide-scroll-bar h-full box-border">
+      <!--展示 markdown 的容器,最终生成的是 html 字符串,直接用 v-html 嵌入-->
+      <div v-if="isGenerating" ref="mdContainerRef" class="wh-full overflow-y-auto">
+        <div class="flex flex-col items-center justify-center" v-html="html"></div>
+      </div>
+
+      <div ref="mindMapRef" class="wh-full">
+        <svg ref="svgRef" class="w-full" :style="{ height: `${contentAreaHeight}px` }" />
+        <div ref="toolBarRef" class="absolute bottom-[10px] right-5"></div>
+      </div>
+    </div>
+  </el-card>
+</template>
+
+<script setup lang="ts">
+import { Markmap } from 'markmap-view'
+import { Transformer } from 'markmap-lib'
+import { Toolbar } from 'markmap-toolbar'
+import markdownit from 'markdown-it'
+import download from '@/utils/download'
+
+const md = markdownit()
+const message = useMessage() // 消息弹窗
+
+const props = defineProps<{
+  generatedContent: string // 生成结果
+  isEnd: boolean // 是否结束
+  isGenerating: boolean // 是否正在生成
+  isStart: boolean // 开始状态,开始时需要清除 html
+}>()
+const contentRef = ref<HTMLDivElement>() // 右侧出来header以下的区域
+const mdContainerRef = ref<HTMLDivElement>() // markdown 的容器,用来滚动到底下的
+const mindMapRef = ref<HTMLDivElement>() // 思维导图的容器
+const svgRef = ref<SVGElement>() // 思维导图的渲染 svg
+const toolBarRef = ref<HTMLDivElement>() // 思维导图右下角的工具栏,缩放等
+const html = ref('') // 生成过程中的文本
+const contentAreaHeight = ref(0) // 生成区域的高度,出去 header 部分
+let markMap: Markmap | null = null
+const transformer = new Transformer()
+
+onMounted(() => {
+  contentAreaHeight.value = contentRef.value?.clientHeight || 0 // 获取区域高度
+  /** 初始化思维导图 **/
+  try {
+    markMap = Markmap.create(svgRef.value!)
+    const { el } = Toolbar.create(markMap)
+    toolBarRef.value?.append(el)
+    nextTick(update)
+  } catch (e) {
+    message.error('思维导图初始化失败')
+  }
+})
+
+watch(props, ({ generatedContent, isGenerating, isEnd, isStart }) => {
+  // 开始生成的时候清空一下 markdown 的内容
+  if (isStart) {
+    html.value = ''
+  }
+  // 生成内容的时候使用 markdown 来渲染
+  if (isGenerating) {
+    html.value = md.render(generatedContent)
+  }
+  // 生成结束时更新思维导图
+  if (isEnd) {
+    update()
+  }
+})
+
+/** 更新思维导图的展示 */
+const update = () => {
+  try {
+    const { root } = transformer.transform(processContent(props.generatedContent))
+    markMap?.setData(root)
+    markMap?.fit()
+  } catch (e) {
+    console.error(e)
+  }
+}
+
+/** 处理内容 */
+const processContent = (text: string) => {
+  const arr: string[] = []
+  const lines = text.split('\n')
+  for (let line of lines) {
+    if (line.indexOf('```') !== -1) {
+      continue
+    }
+    line = line.replace(/([*_~`>])|(\d+\.)\s/g, '')
+    arr.push(line)
+  }
+  return arr.join('\n')
+}
+
+/** 下载图片 */
+// download SVG to png file
+const downloadImage = () => {
+  const svgElement = mindMapRef.value
+  // 将 SVG 渲染到图片对象
+  const serializer = new XMLSerializer()
+  const source = `<?xml version="1.0" standalone="no"?>\r\n${serializer.serializeToString(svgRef.value!)}`
+  const base64Url = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(source)}`
+  download.image({
+    url: base64Url,
+    canvasWidth: svgElement?.offsetWidth,
+    canvasHeight: svgElement?.offsetHeight,
+    drawWithImageSize: false
+  })
+}
+
+defineExpose({
+  scrollBottom() {
+    mdContainerRef.value?.scrollTo(0, mdContainerRef.value?.scrollHeight)
+  }
+})
+</script>
+<style lang="scss" scoped>
+.hide-scroll-bar {
+  -ms-overflow-style: none;
+  scrollbar-width: none;
+
+  &::-webkit-scrollbar {
+    width: 0;
+    height: 0;
+  }
+}
+.my-card {
+  display: flex;
+  flex-direction: column;
+
+  :deep(.el-card__body) {
+    box-sizing: border-box;
+    flex-grow: 1;
+    overflow-y: auto;
+    padding: 0;
+    @extend .hide-scroll-bar;
+  }
+}
+// markmap的tool样式覆盖
+:deep(.markmap) {
+  width: 100%;
+}
+:deep(.mm-toolbar-brand) {
+  display: none;
+}
+:deep(.mm-toolbar) {
+  display: flex;
+  flex-direction: row;
+}
+</style>

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

@@ -0,0 +1,92 @@
+<template>
+  <div class="absolute top-0 left-0 right-0 bottom-0 flex">
+    <!--表单区域-->
+    <Left
+      ref="leftRef"
+      @submit="submit"
+      @direct-generate="directGenerate"
+      :is-generating="isGenerating"
+    />
+    <!--右边生成思维导图区域-->
+    <Right
+      ref="rightRef"
+      :generatedContent="generatedContent"
+      :isEnd="isEnd"
+      :isGenerating="isGenerating"
+      :isStart="isStart"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+import Left from './components/Left.vue'
+import Right from './components/Right.vue'
+import { AiMindMapApi, AiMindMapGenerateReqVO } from '@/api/ai/mindmap'
+import { MindMapContentExample } from '@/views/ai/utils/constants'
+
+defineOptions({
+  name: 'AiMindMap'
+})
+const ctrl = ref<AbortController>() // 请求控制
+const isGenerating = ref(false) // 是否正在生成思维导图
+const isStart = ref(false) // 开始生成,用来清空思维导图
+const isEnd = ref(true) // 用来判断结束的时候渲染思维导图
+const message = useMessage() // 消息提示
+
+const generatedContent = ref('') // 生成思维导图结果
+
+const leftRef = ref<InstanceType<typeof Left>>() // 左边组件
+const rightRef = ref<InstanceType<typeof Right>>() // 右边组件
+
+/** 使用已有内容直接生成 **/
+const directGenerate = (existPrompt: string) => {
+  isEnd.value = false // 先设置为false再设置为true,让子组建的watch能够监听到
+  generatedContent.value = existPrompt
+  isEnd.value = true
+}
+
+/** 停止 stream 生成 */
+const stopStream = () => {
+  isGenerating.value = false
+  isStart.value = false
+  ctrl.value?.abort()
+}
+
+/** 提交生成 */
+const submit = (data: AiMindMapGenerateReqVO) => {
+  isGenerating.value = true
+  isStart.value = true
+  isEnd.value = false
+  ctrl.value = new AbortController() // 请求控制赋值
+  generatedContent.value = '' // 清空生成数据
+  AiMindMapApi.generateMindMap({
+    data,
+    onMessage: async (res) => {
+      const { code, data, msg } = JSON.parse(res.data)
+      if (code !== 0) {
+        message.alert(`生成思维导图异常! ${msg}`)
+        stopStream()
+        return
+      }
+      generatedContent.value = generatedContent.value + data
+      await nextTick()
+      rightRef.value?.scrollBottom()
+    },
+    onClose() {
+      isEnd.value = true
+      leftRef.value?.setGeneratedContent(generatedContent.value)
+      stopStream()
+    },
+    onError(err) {
+      console.error('生成思维导图失败', err)
+      stopStream()
+    },
+    ctrl: ctrl.value
+  })
+}
+
+/** 初始化 */
+onMounted(() => {
+  generatedContent.value = MindMapContentExample
+})
+</script>

+ 0 - 0
src/views/ai/music/components/index.vue → src/views/ai/music/index/index.vue


+ 0 - 0
src/views/ai/music/components/list/audioBar/index.vue → src/views/ai/music/index/list/audioBar/index.vue


+ 0 - 0
src/views/ai/music/components/list/index.vue → src/views/ai/music/index/list/index.vue


+ 0 - 0
src/views/ai/music/components/list/songCard/index.vue → src/views/ai/music/index/list/songCard/index.vue


+ 0 - 0
src/views/ai/music/components/list/songInfo/index.vue → src/views/ai/music/index/list/songInfo/index.vue


+ 0 - 0
src/views/ai/music/components/mode/desc.vue → src/views/ai/music/index/mode/desc.vue


+ 0 - 0
src/views/ai/music/components/mode/index.vue → src/views/ai/music/index/mode/index.vue


+ 0 - 0
src/views/ai/music/components/mode/lyric.vue → src/views/ai/music/index/mode/lyric.vue


+ 0 - 0
src/views/ai/music/components/title/index.vue → src/views/ai/music/index/title/index.vue


+ 66 - 1
src/views/ai/utils/constants.ts

@@ -67,10 +67,11 @@ export enum AiWriteTypeEnum {
 // 表格展示对照map
 export const AiWriteTypeTableRender = {
   [AiWriteTypeEnum.WRITING]: '撰写',
-  [AiWriteTypeEnum.REPLY]: '回复',
+  [AiWriteTypeEnum.REPLY]: '回复'
 }
 
 // ========== 【图片 UI】相关的枚举 ==========
+
 export const ImageHotWords = [
   '中国旗袍',
   '古装美女',
@@ -414,3 +415,67 @@ export const WriteExample = {
     data: '您的请假申请已收悉,经核实和考虑,暂时无法批准您的请假申请。\n\n如有特殊情况或紧急事务,请及时与我联系。\n\n祝工作顺利。\n\n谢谢。'
   }
 }
+
+// ========== 【思维导图 UI】相关的枚举 ==========
+
+/** 思维导图已有内容生成示例 **/
+export const MindMapContentExample = `# Java 技术栈
+
+## 核心技术
+### Java SE
+### Java EE
+
+## 框架
+### Spring
+#### Spring Boot
+#### Spring MVC
+#### Spring Data
+### Hibernate
+### MyBatis
+
+## 构建工具
+### Maven
+### Gradle
+
+## 版本控制
+### Git
+### SVN
+
+## 测试工具
+### JUnit
+### Mockito
+### Selenium
+
+## 应用服务器
+### Tomcat
+### Jetty
+### WildFly
+
+## 数据库
+### MySQL
+### PostgreSQL
+### Oracle
+### MongoDB
+
+## 消息队列
+### Kafka
+### RabbitMQ
+### ActiveMQ
+
+## 微服务
+### Spring Cloud
+### Dubbo
+
+## 容器化
+### Docker
+### Kubernetes
+
+## 云服务
+### AWS
+### Azure
+### Google Cloud
+
+## 开发工具
+### IntelliJ IDEA
+### Eclipse
+### Visual Studio Code`

+ 4 - 4
src/views/bpm/processInstance/detail/index.vue

@@ -47,10 +47,10 @@
           <el-form-item label="抄送人" prop="copyUserIds">
             <el-select v-model="auditForms[index].copyUserIds" multiple placeholder="请选择抄送人">
               <el-option
-                v-for="item in userOptions"
-                :key="item.id"
-                :label="item.nickname"
-                :value="item.id"
+                v-for="itemx in userOptions"
+                :key="itemx.id"
+                :label="itemx.nickname"
+                :value="itemx.id"
               />
             </el-select>
           </el-form-item>

+ 19 - 5
src/views/bpm/processInstance/index.vue

@@ -76,7 +76,7 @@
           type="primary"
           plain
           v-hasPermi="['bpm:process-instance:query']"
-          @click="handleCreate()"
+          @click="handleCreate(undefined)"
         >
           <Icon icon="ep:plus" class="mr-5px" /> 发起流程
         </el-button>
@@ -146,7 +146,7 @@
           >
             取消
           </el-button>
-          <el-button link type="primary" v-else @click="handleCreate(scope.row.id)">
+          <el-button link type="primary" v-else @click="handleCreate(scope.row)">
             重新发起
           </el-button>
         </template>
@@ -167,6 +167,8 @@ import { dateFormatter, formatPast2 } from '@/utils/formatTime'
 import { ElMessageBox } from 'element-plus'
 import * as ProcessInstanceApi from '@/api/bpm/processInstance'
 import { CategoryApi } from '@/api/bpm/category'
+import { ProcessInstanceVO } from '@/api/bpm/processInstance'
+import * as DefinitionApi from '@/api/bpm/definition'
 
 defineOptions({ name: 'BpmProcessInstanceMy' })
 
@@ -214,10 +216,22 @@ const resetQuery = () => {
 }
 
 /** 发起流程操作 **/
-const handleCreate = (id) => {
-  router.push({
+const handleCreate = async (row?: ProcessInstanceVO) => {
+  // 如果是【业务表单】,不支持重新发起
+  if (row?.id) {
+    const processDefinitionDetail = await DefinitionApi.getProcessDefinition(
+      row.processDefinitionId
+    )
+    debugger
+    if (processDefinitionDetail.formType === 20) {
+      message.error('重新发起流程失败,原因:该流程使用业务表单,不支持重新发起')
+      return
+    }
+  }
+  // 跳转发起流程界面
+  await router.push({
     name: 'BpmProcessInstanceCreate',
-    query: { processInstanceId: id }
+    query: { processInstanceId: row?.id }
   })
 }
 

+ 2 - 1
src/views/infra/fileConfig/index.vue

@@ -206,7 +206,8 @@ const handleMaster = async (id) => {
 const handleTest = async (id) => {
   try {
     const response = await FileConfigApi.testFileConfig(id)
-    message.alert('测试通过,上传文件成功!访问地址:' + response)
+    await message.confirm('是否要访问该文件?', '测试上传成功')
+    window.open(response, '_blank')
   } catch {}
 }
 

+ 1 - 1
src/views/infra/webSocket/index.vue

@@ -86,7 +86,7 @@ const getTagColor = computed(() => (getIsOpen.value ? 'success' : 'red')) // Web
 
 /** 发起 WebSocket 连接 */
 const { status, data, send, close, open } = useWebSocket(server.value, {
-  autoReconnect: false,
+  autoReconnect: true,
   heartbeat: true
 })
 

+ 19 - 1
src/views/mall/product/category/index.vue

@@ -56,7 +56,7 @@
         width="180"
         :formatter="dateFormatter"
       />
-      <el-table-column label="操作" align="center">
+      <el-table-column label="操作" align="center" min-width="180">
         <template #default="scope">
           <el-button
             link
@@ -66,6 +66,15 @@
           >
             编辑
           </el-button>
+          <el-button
+            link
+            type="primary"
+            v-if="scope.row.parentId > 0"
+            @click="handleViewSpu(scope.row.id)"
+            v-hasPermi="['product:spu:query']"
+          >
+            查看商品
+          </el-button>
           <el-button
             link
             type="danger"
@@ -142,6 +151,15 @@ const handleDelete = async (id: number) => {
   } catch {}
 }
 
+/** 查看商品操作 */
+const router = useRouter() // 路由
+const handleViewSpu = (id: number) => {
+  router.push({
+    name: 'ProductSpu',
+    query: { categoryId: id }
+  })
+}
+
 /** 初始化 **/
 onMounted(() => {
   getList()

+ 1 - 1
src/views/mall/product/spu/form/index.vue

@@ -63,7 +63,7 @@ import SkuForm from './SkuForm.vue'
 import DeliveryForm from './DeliveryForm.vue'
 import { convertToInteger, floatToFixed2, formatToFraction } from '@/utils'
 
-defineOptions({ name: 'ProductSpuForm' })
+defineOptions({ name: 'ProductSpuAdd' })
 
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗

+ 6 - 0
src/views/mall/product/spu/index.vue

@@ -244,6 +244,7 @@ import * as ProductCategoryApi from '@/api/mall/product/category'
 defineOptions({ name: 'ProductSpu' })
 
 const message = useMessage() // 消息弹窗
+const route = useRoute() // 路由
 const { t } = useI18n() // 国际化
 const { push } = useRouter() // 路由跳转
 
@@ -431,6 +432,11 @@ onActivated(() => {
 
 /** 初始化 **/
 onMounted(async () => {
+  // 解析路由的 categoryId
+  if (route.query.categoryId) {
+    queryParams.value.categoryId = Number(route.query.categoryId)
+  }
+  // 获得商品信息
   await getTabsCount()
   await getList()
   // 获得分类树

+ 9 - 48
src/views/pay/app/components/channel/WeixinChannelForm.vue

@@ -52,9 +52,6 @@
               v-model="formData.config.mchKey"
               placeholder="请输入商户密钥"
               clearable
-              :style="{ width: '100%' }"
-              type="textarea"
-              :autosize="{ minRows: 8, maxRows: 8 }"
             />
           </el-form-item>
           <el-form-item
@@ -92,9 +89,6 @@
               v-model="formData.config.apiV3Key"
               placeholder="请输入 API V3 密钥"
               clearable
-              :style="{ width: '100%' }"
-              type="textarea"
-              :autosize="{ minRows: 8, maxRows: 8 }"
             />
           </el-form-item>
           <el-form-item
@@ -126,35 +120,13 @@
               </el-button>
             </el-upload>
           </el-form-item>
-          <el-form-item
-            label-width="180px"
-            label="apiclient_cert.pem证书"
-            prop="config.privateCertContent"
-          >
+          <el-form-item label-width="180px" label="证书序列号" prop="config.certSerialNo">
             <el-input
-              v-model="formData.config.privateCertContent"
-              type="textarea"
-              placeholder="请上传apiclient_cert.pem证书"
-              readonly
-              :autosize="{ minRows: 8, maxRows: 8 }"
-              :style="{ width: '100%' }"
+              v-model="formData.config.certSerialNo"
+              placeholder="请输入证书序列号"
+              clearable
             />
           </el-form-item>
-          <el-form-item label-width="180px" label="" prop="privateCertContentFile">
-            <el-upload
-              ref="privateCertContentFile"
-              :limit="1"
-              accept=".pem"
-              action=""
-              :before-upload="pemFileBeforeUpload"
-              :http-request="privateCertContentUpload"
-            >
-              <el-button type="primary">
-                <Icon icon="ep:upload" class="mr-5px" />
-                点击上传
-              </el-button>
-            </el-upload>
-          </el-form-item>
         </div>
         <el-form-item label-width="180px" label="备注" prop="remark">
           <el-input v-model="formData.remark" :style="{ width: '100%' }" />
@@ -193,7 +165,7 @@ const formData = ref<any>({
     mchKey: '',
     keyContent: '',
     privateKeyContent: '',
-    privateCertContent: '',
+    certSerialNo: '',
     apiV3Key: ''
   }
 })
@@ -210,8 +182,8 @@ const formRules = {
   'config.privateKeyContent': [
     { required: true, message: '请上传 apiclient_key.pem 证书', trigger: 'blur' }
   ],
-  'config.privateCertContent': [
-    { required: true, message: '请上传 apiclient_cert.pem证 书', trigger: 'blur' }
+  'config.certSerialNo': [
+    { required: true, message: '请输入证书序列号', trigger: 'blur' }
   ],
   'config.apiV3Key': [{ required: true, message: '请上传 api V3 密钥值', trigger: 'blur' }]
 }
@@ -278,7 +250,7 @@ const resetForm = (appId, code) => {
       mchKey: '',
       keyContent: '',
       privateKeyContent: '',
-      privateCertContent: '',
+      certSerialNo: '',
       apiV3Key: ''
     }
   }
@@ -286,7 +258,7 @@ const resetForm = (appId, code) => {
 }
 
 /**
- * apiclient_cert.p12、apiclient_cert.pem、apiclient_key.pem 上传前的校验
+ * apiclient_cert.p12、apiclient_key.pem 上传前的校验
  */
 const fileBeforeUpload = (file, fileAccept) => {
   let format = '.' + file.name.split('.')[1]
@@ -321,17 +293,6 @@ const privateKeyContentUpload = async (event) => {
   readFile.readAsText(event.file)
 }
 
-/**
- * 读取 apiclient_cert.pem 到 privateCertContent 字段
- */
-const privateCertContentUpload = async (event) => {
-  const readFile = new FileReader()
-  readFile.onload = (e: any) => {
-    formData.value.config.privateCertContent = e.target.result
-  }
-  readFile.readAsText(event.file)
-}
-
 /**
  * 读取 apiclient_cert.p12 到 keyContent 字段
  */

+ 3 - 1
src/views/pay/order/OrderDetail.vue

@@ -62,7 +62,9 @@
     <el-divider />
     <el-descriptions :column="1" label-class-name="desc-label" direction="vertical" border>
       <el-descriptions-item label="支付通道异步回调内容">
-        <el-text>{{ detailData.extension.channelNotifyData }}</el-text>
+        <el-text style="white-space: pre-wrap; word-break: break-word">
+          {{ detailData.extension.channelNotifyData }}
+        </el-text>
       </el-descriptions-item>
     </el-descriptions>
   </Dialog>

+ 3 - 1
src/views/pay/refund/RefundDetail.vue

@@ -62,7 +62,9 @@
     </el-descriptions>
     <el-descriptions :column="1" label-class-name="desc-label" direction="vertical" border>
       <el-descriptions-item label="支付通道异步回调内容">
-        {{ refundDetail.channelNotifyData }}
+        <el-text style="white-space: pre-wrap; word-break: break-word">
+          {{ refundDetail.channelNotifyData }}
+        </el-text>
       </el-descriptions-item>
     </el-descriptions>
   </Dialog>

+ 21 - 14
src/views/pay/wallet/balance/index.vue

@@ -8,15 +8,30 @@
       :inline="true"
       label-width="68px"
     >
-      <el-form-item label="用户昵称" prop="nickname">
+      <el-form-item label="用户编号" prop="userId">
         <el-input
-          v-model="queryParams.nickname"
-          placeholder="请输入用户昵称"
+          v-model="queryParams.userId"
+          placeholder="请输入用户编号"
           clearable
           @keyup.enter="handleQuery"
           class="!w-240px"
         />
       </el-form-item>
+      <el-form-item label="用户类型" prop="userType">
+        <el-select
+          v-model="queryParams.userType"
+          placeholder="请选择用户类型"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.USER_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"
@@ -39,12 +54,7 @@
   <ContentWrap>
     <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
       <el-table-column label="编号" align="center" prop="id" />
-      <el-table-column label="用户昵称" align="center" prop="nickname" />
-      <el-table-column label="头像" align="center" prop="avatar" width="80px">
-        <template #default="scope">
-          <img :src="scope.row.avatar" style="width: 40px" />
-        </template>
-      </el-table-column>
+      <el-table-column label="用户编号" align="center" prop="userId" />
       <el-table-column label="用户类型" align="center" prop="userType">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.USER_TYPE" :value="scope.row.userType" />
@@ -97,20 +107,17 @@ import WalletForm from './WalletForm.vue'
 
 defineOptions({ name: 'WalletBalance' })
 
-const message = useMessage() // 消息弹窗
-const { t } = useI18n() // 国际化
-
 const loading = ref(true) // 列表的加载中
 const total = ref(0) // 列表的总页数
 const list = ref([]) // 列表的数据
 const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
-  nickname: null,
+  userId: null,
+  userType: null,
   createTime: []
 })
 const queryFormRef = ref() // 搜索的表单
-const exportLoading = ref(false) // 导出的加载中
 
 /** 查询列表 */
 const getList = async () => {

+ 3 - 3
src/views/system/operatelog/index.vue

@@ -35,10 +35,10 @@
           class="!w-240px"
         />
       </el-form-item>
-      <el-form-item label="操作模块" prop="subType">
+      <el-form-item label="操作" prop="subType">
         <el-input
           v-model="queryParams.subType"
-          placeholder="请输入操作模块"
+          placeholder="请输入操作"
           clearable
           @keyup.enter="handleQuery"
           class="!w-240px"
@@ -105,7 +105,7 @@
         :formatter="dateFormatter"
       />
       <el-table-column label="业务编号" align="center" prop="bizId" width="120" />
-      <el-table-column label="IP" align="center" prop="userIp" width="120" />
+      <el-table-column label="操作 IP" align="center" prop="userIp" width="120" />
       <el-table-column label="操作" align="center" fixed="right" width="60">
         <template #default="scope">
           <el-button

+ 6 - 1
src/views/system/sms/channel/index.vue

@@ -18,7 +18,12 @@
         />
       </el-form-item>
       <el-form-item label="启用状态" prop="status">
-        <el-select v-model="queryParams.status" placeholder="请选择启用状态" clearable>
+        <el-select
+          v-model="queryParams.status"
+          placeholder="请选择启用状态"
+          class="!w-240px"
+          clearable
+        >
           <el-option
             v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
             :key="dict.value"

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels