Ver Fonte

Merge remote-tracking branch 'yudao-ui-admin-vue3/dev' into dev

# Conflicts:
#	src/api/ai/writer/index.ts
hhhero há 11 meses atrás
pai
commit
0a48b523b9
71 ficheiros alterados com 515 adições e 324 exclusões
  1. 6 0
      src/api/ai/writer/index.ts
  2. 23 35
      src/components/MarkdownView/index.vue
  3. 6 6
      src/layout/components/AppView.vue
  4. 20 20
      src/router/modules/remaining.ts
  5. 6 1
      src/utils/dict.ts
  6. 21 17
      src/views/ai/image/index/components/other/index.vue
  7. 2 2
      src/views/ai/image/index/index.vue
  8. 10 4
      src/views/ai/music/manager/index.vue
  9. 22 53
      src/views/ai/utils/constants.ts
  10. 238 0
      src/views/ai/write/manager/index.vue
  11. 5 5
      src/views/ai/writer/index/components/Left.vue
  12. 49 49
      src/views/mall/promotion/kefu/components/KeFuConversationList.vue
  13. 55 58
      src/views/mall/promotion/kefu/components/KeFuMessageList.vue
  14. 0 0
      src/views/mall/promotion/kefu/components/asserts/a.png
  15. 0 0
      src/views/mall/promotion/kefu/components/asserts/aini.png
  16. 0 0
      src/views/mall/promotion/kefu/components/asserts/aixin.png
  17. 0 0
      src/views/mall/promotion/kefu/components/asserts/baiyan.png
  18. 0 0
      src/views/mall/promotion/kefu/components/asserts/bizui.png
  19. 0 0
      src/views/mall/promotion/kefu/components/asserts/buhaoyisi.png
  20. 0 0
      src/views/mall/promotion/kefu/components/asserts/bukesiyi.png
  21. 0 0
      src/views/mall/promotion/kefu/components/asserts/dajing.png
  22. 0 0
      src/views/mall/promotion/kefu/components/asserts/danao.png
  23. 0 0
      src/views/mall/promotion/kefu/components/asserts/daxiao.png
  24. 0 0
      src/views/mall/promotion/kefu/components/asserts/dianzan.png
  25. 0 0
      src/views/mall/promotion/kefu/components/asserts/emo.png
  26. 0 0
      src/views/mall/promotion/kefu/components/asserts/esi.png
  27. 0 0
      src/views/mall/promotion/kefu/components/asserts/fadai.png
  28. 0 0
      src/views/mall/promotion/kefu/components/asserts/fankun.png
  29. 0 0
      src/views/mall/promotion/kefu/components/asserts/feiwen.png
  30. 0 0
      src/views/mall/promotion/kefu/components/asserts/fennu.png
  31. 0 0
      src/views/mall/promotion/kefu/components/asserts/ganga.png
  32. 0 0
      src/views/mall/promotion/kefu/components/asserts/ganmao.png
  33. 0 0
      src/views/mall/promotion/kefu/components/asserts/hanyan.png
  34. 0 0
      src/views/mall/promotion/kefu/components/asserts/haochi.png
  35. 0 0
      src/views/mall/promotion/kefu/components/asserts/hongxin.png
  36. 0 0
      src/views/mall/promotion/kefu/components/asserts/huaixiao.png
  37. 0 0
      src/views/mall/promotion/kefu/components/asserts/jingkong.png
  38. 0 0
      src/views/mall/promotion/kefu/components/asserts/jingshu.png
  39. 0 0
      src/views/mall/promotion/kefu/components/asserts/jingya.png
  40. 0 0
      src/views/mall/promotion/kefu/components/asserts/kaixin.png
  41. 0 0
      src/views/mall/promotion/kefu/components/asserts/keai.png
  42. 0 0
      src/views/mall/promotion/kefu/components/asserts/keshui.png
  43. 0 0
      src/views/mall/promotion/kefu/components/asserts/kun.png
  44. 0 0
      src/views/mall/promotion/kefu/components/asserts/lengku.png
  45. 0 0
      src/views/mall/promotion/kefu/components/asserts/liuhan.png
  46. 0 0
      src/views/mall/promotion/kefu/components/asserts/liukoushui.png
  47. 0 0
      src/views/mall/promotion/kefu/components/asserts/liulei.png
  48. 0 0
      src/views/mall/promotion/kefu/components/asserts/mengbi.png
  49. 0 0
      src/views/mall/promotion/kefu/components/asserts/mianwubiaoqing.png
  50. 0 0
      src/views/mall/promotion/kefu/components/asserts/nanguo.png
  51. 0 0
      src/views/mall/promotion/kefu/components/asserts/outu.png
  52. 0 0
      src/views/mall/promotion/kefu/components/asserts/picture.svg
  53. 0 0
      src/views/mall/promotion/kefu/components/asserts/shengqi.png
  54. 0 0
      src/views/mall/promotion/kefu/components/asserts/shuizhuo.png
  55. 0 0
      src/views/mall/promotion/kefu/components/asserts/tianshi.png
  56. 0 0
      src/views/mall/promotion/kefu/components/asserts/xiaodiaoya.png
  57. 0 0
      src/views/mall/promotion/kefu/components/asserts/xiaoku.png
  58. 0 0
      src/views/mall/promotion/kefu/components/asserts/xinsui.png
  59. 0 0
      src/views/mall/promotion/kefu/components/asserts/xiong.png
  60. 0 0
      src/views/mall/promotion/kefu/components/asserts/yiwen.png
  61. 0 0
      src/views/mall/promotion/kefu/components/asserts/yun.png
  62. 0 0
      src/views/mall/promotion/kefu/components/asserts/ziya.png
  63. 3 3
      src/views/mall/promotion/kefu/components/index.ts
  64. 4 11
      src/views/mall/promotion/kefu/components/message/ImageMessageItem.vue
  65. 14 15
      src/views/mall/promotion/kefu/components/message/OrderMessageItem.vue
  66. 9 11
      src/views/mall/promotion/kefu/components/message/ProductItem.vue
  67. 1 1
      src/views/mall/promotion/kefu/components/message/ProductMessageItem.vue
  68. 1 2
      src/views/mall/promotion/kefu/components/tools/EmojiSelectPopover.vue
  69. 2 4
      src/views/mall/promotion/kefu/components/tools/PictureSelectUpload.vue
  70. 10 15
      src/views/mall/promotion/kefu/components/tools/emoji.ts
  71. 8 12
      src/views/mall/promotion/kefu/index.vue

+ 6 - 0
src/api/ai/writer/index.ts

@@ -13,6 +13,12 @@ export interface WriteVO {
   format: number // 格式
   tone: number // 语气
   language: number // 语言
+  userId?: number // 用户编号
+  platform?: string // 平台
+  model?: string // 模型
+  generatedContent?: string // 生成的内容
+  errorMessage: string // 错误信息
+  createTime?: Date // 创建时间
 }
 
 export interface AiWritePageReqVO extends PageParam {

+ 23 - 35
src/components/MarkdownView/index.vue

@@ -1,28 +1,36 @@
-
 <template>
   <div ref="contentRef" class="markdown-view" v-html="contentHtml"></div>
 </template>
 
 <script setup lang="ts">
-import {useClipboard} from "@vueuse/core";
-
-import {marked} from 'marked'
+import { useClipboard } from '@vueuse/core'
+import { marked } from 'marked'
 import 'highlight.js/styles/vs2015.min.css'
 import hljs from 'highlight.js'
-import {ref} from "vue";
 
-const {copy} = useClipboard() // 初始化 copy 到粘贴板
+// 定义组件属性
+const props = defineProps({
+  content: {
+    type: String,
+    required: true
+  }
+})
+
+const message = useMessage() // 消息弹窗
+const { copy } = useClipboard() // 初始化 copy 到粘贴板
 const contentRef = ref()
+const contentHtml = ref<any>() // 渲染的html内容
+const { content } = toRefs(props) // 将 props 变为引用类型
 
 // 代码高亮:https://highlightjs.org/
 // 转换 markdown:marked
 
-// marked 渲染器
+/** marked 渲染器 */
 const renderer = {
   code(code, language, c) {
     let highlightHtml
     try {
-      highlightHtml = hljs.highlight(code, {language: language, ignoreIllegals: true}).value
+      highlightHtml = hljs.highlight(code, { language: language, ignoreIllegals: true }).value
     } catch (e) {
       // skip
     }
@@ -36,50 +44,30 @@ marked.use({
   renderer: renderer
 })
 
-// 渲染的html内容
-const contentHtml = ref<any>()
-
-// 定义组件属性
-const props = defineProps({
-  content: {
-    type: String,
-    required: true
-  }
-})
-
-// 将 props 变为引用类型
-const { content } = toRefs(props)
-
-// 监听 content 变化
+/** 监听 content 变化 */
 watch(content, async (newValue, oldValue) => {
-  await renderMarkdown(newValue);
+  await renderMarkdown(newValue)
 })
 
-// 渲染 markdown
+/** 渲染 markdown */
 const renderMarkdown = async (content: string) => {
   contentHtml.value = await marked(content)
 }
 
-// 组件挂在时
-onMounted(async ()  => {
+/** 初始化 **/
+onMounted(async () => {
   // 解析转换 markdown
-  await renderMarkdown(props.content as string);
-  //
+  await renderMarkdown(props.content as string)
   // 添加 copy 监听
   contentRef.value.addEventListener('click', (e: any) => {
-    console.log(e)
     if (e.target.id === 'copy') {
       copy(e.target?.dataset?.copy)
-      ElMessage({
-        message: '复制成功!',
-        type: 'success'
-      })
+      message.success('复制成功!')
     }
   })
 })
 </script>
 
-
 <style lang="scss">
 .markdown-view {
   font-family: PingFang SC;

+ 6 - 6
src/layout/components/AppView.vue

@@ -38,24 +38,24 @@ provide('reload', reload)
     :class="[
       'p-[var(--app-content-padding)] w-[calc(100%-var(--app-content-padding)-var(--app-content-padding))] bg-[var(--app-content-bg-color)] dark:bg-[var(--el-bg-color)]',
       {
-        '!h-[calc(100%-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height))]':
+        '!min-h-[calc(100%-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height))]':
           (fixedHeader &&
             (layout === 'classic' || layout === 'topLeft' || layout === 'top') &&
             footer) ||
           (!tagsView && layout === 'top' && footer),
-        '!h-[calc(100%-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height)-var(--tags-view-height))]':
+        '!min-h-[calc(100%-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height)-var(--tags-view-height))]':
           tagsView && layout === 'top' && footer,
 
-        '!h-[calc(100%-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-var(--top-tool-height)-var(--app-footer-height))]':
+        '!min-h-[calc(100%-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-var(--top-tool-height)-var(--app-footer-height))]':
           !fixedHeader && layout === 'classic' && footer,
 
-        '!h-[calc(100%-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height))]':
+        '!min-h-[calc(100%-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height))]':
           !fixedHeader && layout === 'topLeft' && footer,
 
-        '!h-[calc(100%-var(--top-tool-height)-var(--app-content-padding)-var(--app-content-padding))]':
+        '!min-h-[calc(100%-var(--top-tool-height)-var(--app-content-padding)-var(--app-content-padding))]':
           fixedHeader && layout === 'cutMenu' && footer,
 
-        '!h-[calc(100%-var(--top-tool-height)-var(--app-content-padding)-var(--app-content-padding)-var(--tags-view-height))]':
+        '!min-h-[calc(100%-var(--top-tool-height)-var(--app-content-padding)-var(--app-content-padding)-var(--tags-view-height))]':
           !fixedHeader && layout === 'cutMenu' && footer
       }
     ]"

+ 20 - 20
src/router/modules/remaining.ts

@@ -70,26 +70,26 @@ 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: '/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,

+ 6 - 1
src/utils/dict.ts

@@ -222,5 +222,10 @@ export enum DICT_TYPE {
   AI_PLATFORM = 'ai_platform', // AI 平台
   AI_IMAGE_STATUS = 'ai_image_status', // AI 图片状态
   AI_MUSIC_STATUS = 'ai_music_status', // AI 音乐状态
-  AI_GENERATE_MODE = 'ai_generate_mode' // AI 生成模式
+  AI_GENERATE_MODE = 'ai_generate_mode', // AI 生成模式
+  AI_WRITE_TYPE = 'ai_write_type', // AI 写作类型
+  AI_WRITE_LENGTH = 'ai_write_length', // AI 写作长度
+  AI_WRITE_FORMAT = 'ai_write_format', // AI 写作格式
+  AI_WRITE_TONE = 'ai_write_tone', // AI 写作语气
+  AI_WRITE_LANGUAGE = 'ai_write_language' // AI 写作语言
 }

+ 21 - 17
src/views/ai/image/index/components/other/index.vue

@@ -36,7 +36,13 @@
       <el-text tag="b">平台</el-text>
     </div>
     <el-space wrap class="group-item-body">
-      <el-select v-model="otherPlatform" placeholder="Select" size="large" class="!w-350px" @change="handlerPlatformChange">
+      <el-select
+        v-model="otherPlatform"
+        placeholder="Select"
+        size="large"
+        class="!w-350px"
+        @change="handlerPlatformChange"
+      >
         <el-option
           v-for="item in OtherPlatformEnum"
           :key="item.key"
@@ -52,12 +58,7 @@
     </div>
     <el-space wrap class="group-item-body">
       <el-select v-model="model" placeholder="Select" size="large" class="!w-350px">
-        <el-option
-          v-for="item in models"
-          :key="item.key"
-          :label="item.name"
-          :value="item.key"
-        />
+        <el-option v-for="item in models" :key="item.key" :label="item.name" :value="item.key" />
       </el-select>
     </el-space>
   </div>
@@ -77,12 +78,14 @@
   </div>
 </template>
 <script setup lang="ts">
-import {ImageApi, ImageDrawReqVO, ImageVO} from '@/api/ai/image'
+import { ImageApi, ImageDrawReqVO, ImageVO } from '@/api/ai/image'
 import {
   AiPlatformEnum,
+  ChatGlmModels,
   ImageHotWords,
   ImageModelVO,
   OtherPlatformEnum,
+  QianFanModels,
   TongYiWanXiangModels
 } from '@/views/ai/utils/constants'
 
@@ -96,10 +99,9 @@ const prompt = ref<string>('') // 提示词
 const width = ref<number>(512) // 图片宽度
 const height = ref<number>(512) // 图片高度
 const otherPlatform = ref<string>(AiPlatformEnum.TONG_YI) // 平台
-const models = ref<ImageModelVO[]>(TongYiWanXiangModels) // 模型
+const models = ref<ImageModelVO[]>(TongYiWanXiangModels) // 模型  TongYiWanXiangModels、QianFanModels
 const model = ref<string>(models.value[0].key) // 模型
 
-
 const emits = defineEmits(['onDrawStart', 'onDrawComplete']) // 定义 emits
 
 /** 选择热词 */
@@ -131,9 +133,8 @@ const handleGenerateImage = async () => {
       prompt: prompt.value, // 提示词
       width: width.value, // 图片宽度
       height: height.value, // 图片高度
-      options: {
-      }
-    } as ImageDrawReqVO
+      options: {}
+    } as unknown as ImageDrawReqVO
     await ImageApi.drawImage(form)
   } finally {
     // 回调
@@ -148,21 +149,24 @@ const settingValues = async (detail: ImageVO) => {
   prompt.value = detail.prompt
   width.value = detail.width
   height.value = detail.height
-
 }
 
 /** 平台切换 */
-const handlerPlatformChange = async (platform) => {
+const handlerPlatformChange = async (platform: string) => {
   // 切换平台,切换模型、风格
-  if (AiPlatformEnum.YI_YAN === platform) {
+  if (AiPlatformEnum.TONG_YI === platform) {
     models.value = TongYiWanXiangModels
+  } else if (AiPlatformEnum.YI_YAN === platform) {
+    models.value = QianFanModels
+  } else if (AiPlatformEnum.ZHI_PU === platform) {
+    models.value = ChatGlmModels
   } else {
     models.value = []
   }
   // 切换平台,默认选择一个风格
   if (models.value.length > 0) {
     model.value = models.value[0].key
-  } else  {
+  } else {
     model.value = ''
   }
 }

+ 2 - 2
src/views/ai/image/index/index.vue

@@ -23,7 +23,6 @@
           ref="otherRef"
           @on-draw-complete="handleDrawComplete"
         />
-
       </div>
     </div>
     <div class="main">
@@ -63,7 +62,7 @@ const platformOptions = [
     value: AiPlatformEnum.STABLE_DIFFUSION
   },
   {
-    label: '其',
+    label: '其',
     value: 'other'
   }
 ]
@@ -89,6 +88,7 @@ const handleRegeneration = async (image: ImageVO) => {
   } else if (image.platform === AiPlatformEnum.STABLE_DIFFUSION) {
     stableDiffusionRef.value.settingValues(image)
   }
+  // TODO @fan:貌似 other 重新设置不行?
 }
 </script>
 

+ 10 - 4
src/views/ai/music/manager/index.vue

@@ -9,13 +9,19 @@
       label-width="68px"
     >
       <el-form-item label="用户编号" prop="userId">
-        <el-input
+        <el-select
           v-model="queryParams.userId"
-          placeholder="请输入用户编号"
           clearable
-          @keyup.enter="handleQuery"
+          placeholder="请输入用户编号"
           class="!w-240px"
-        />
+        >
+          <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="title">
         <el-input

+ 22 - 53
src/views/ai/utils/constants.ts

@@ -20,21 +20,24 @@ export const AiPlatformEnum = {
   Ollama: 'Ollama',
   STABLE_DIFFUSION: 'StableDiffusion', // Stability AI
   MIDJOURNEY: 'Midjourney', // Midjourney
-  SUNO: 'Suno', // Suno AI
+  SUNO: 'Suno' // Suno AI
 }
 
-export const OtherPlatformEnum:ImageModelVO [] = [
+export const OtherPlatformEnum: ImageModelVO[] = [
   {
     key: AiPlatformEnum.TONG_YI,
     name: '通义万相'
   },
   {
     key: AiPlatformEnum.YI_YAN,
-    name: '百度图片'
+    name: '百度千帆'
+  },
+  {
+    key: AiPlatformEnum.ZHI_PU,
+    name: '智谱 AI'
   }
 ]
 
-
 /**
  * AI 图像生成状态的枚举
  */
@@ -207,54 +210,6 @@ export const StableDiffusionStylePresets: ImageModelVO[] = [
   }
 ]
 
-// todo @芋艿 这些是通义的风格,看要不要删除
-export const TongYiWanXiangStylePresets: ImageModelVO[] = [
-  {
-    key: '-1',
-    name: '上传图像风格'
-  },
-  {
-    key: '0',
-    name: '复古漫画'
-  },
-  {
-    key: '1',
-    name: '3D童话'
-  },
-  {
-    key: '2',
-    name: '二次元'
-  },
-  {
-    key: '3',
-    name: '小清新'
-  },
-  {
-    key: '4',
-    name: '未来科技'
-  },
-  {
-    key: '5',
-    name: '国画古风'
-  },
-  {
-    key: '6',
-    name: '将军百战'
-  },
-  {
-    key: '7',
-    name: '炫彩卡通'
-  },
-  {
-    key: '8',
-    name: '清雅国风'
-  },
-  {
-    key: '9',
-    name: '喜迎新年'
-  }
-]
-
 export const TongYiWanXiangModels: ImageModelVO[] = [
   {
     key: 'wanx-v1',
@@ -266,6 +221,20 @@ export const TongYiWanXiangModels: ImageModelVO[] = [
   }
 ]
 
+export const QianFanModels: ImageModelVO[] = [
+  {
+    key: 'sd_xl',
+    name: 'sd_xl'
+  }
+]
+
+export const ChatGlmModels: ImageModelVO[] = [
+  {
+    key: 'cogview-3',
+    name: 'cogview-3'
+  }
+]
+
 export const StableDiffusionClipGuidancePresets: ImageModelVO[] = [
   {
     key: 'NONE',
@@ -325,7 +294,7 @@ export const Dall3StyleList: ImageModelVO[] = [
 
 export interface ImageSizeVO {
   key: string
-  name: string
+  name?: string
   style: string
   width: string
   height: string

+ 238 - 0
src/views/ai/write/manager/index.vue

@@ -0,0 +1,238 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="用户编号" prop="userId">
+        <el-select
+          v-model="queryParams.userId"
+          clearable
+          placeholder="请输入用户编号"
+          class="!w-240px"
+        >
+          <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="type">
+        <el-select
+          v-model="queryParams.type"
+          placeholder="请选择写作类型"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.AI_WRITE_TYPE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="平台" prop="platform">
+        <el-select v-model="queryParams.status" placeholder="请选择平台" clearable class="!w-240px">
+          <el-option
+            v-for="dict in getStrDictOptions(DICT_TYPE.AI_PLATFORM)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+        <el-button
+          type="primary"
+          plain
+          @click="openForm('create')"
+          v-hasPermi="['ai:write:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+        <el-button
+          type="success"
+          plain
+          @click="handleExport"
+          :loading="exportLoading"
+          v-hasPermi="['ai:write:export']"
+        >
+          <Icon icon="ep:download" class="mr-5px" /> 导出
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column label="编号" align="center" prop="id" width="120" fixed="left" />
+      <el-table-column label="用户" align="center" 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 label="写作类型" align="center" prop="type">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.AI_WRITE_TYPE" :value="scope.row.type" />
+        </template>
+      </el-table-column>
+      <el-table-column label="平台" align="center" prop="platform" width="120">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.AI_PLATFORM" :value="scope.row.platform" />
+        </template>
+      </el-table-column>
+      <el-table-column label="模型" align="center" prop="model" width="180" />
+      <el-table-column label="生成内容提示" align="center" prop="prompt" width="180" />
+      <el-table-column label="生成的内容" align="center" prop="generatedContent" width="180" />
+      <el-table-column label="原文" align="center" prop="originalContent" width="180" />
+      <el-table-column label="长度" align="center" prop="length">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.AI_WRITE_LENGTH" :value="scope.row.length" />
+        </template>
+      </el-table-column>
+      <el-table-column label="格式" align="center" prop="format">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.AI_WRITE_FORMAT" :value="scope.row.format" />
+        </template>
+      </el-table-column>
+      <el-table-column label="语气" align="center" prop="tone">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.AI_WRITE_TONE" :value="scope.row.tone" />
+        </template>
+      </el-table-column>
+      <el-table-column label="语言" align="center" prop="language">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.AI_WRITE_LANGUAGE" :value="scope.row.language" />
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="错误信息" align="center" prop="errorMessage" />
+      <el-table-column label="操作" align="center">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['ai:write:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['ai:write:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+// TODO 芋艿:这里应该是 write
+import { WriteApi, WriteVO } from '@/api/ai/writer'
+import * as UserApi from '@/api/system/user'
+
+/** AI 写作列表 */
+defineOptions({ name: 'AiWriteManager' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const list = ref<WriteVO[]>([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  userId: undefined,
+  type: undefined,
+  platform: undefined,
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+const userList = ref<UserApi.UserVO[]>([]) // 用户列表
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await WriteApi.getWritePage(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 WriteApi.deleteWrite(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 初始化 **/
+onMounted(async () => {
+  getList()
+  // 获得用户列表
+  userList.value = await UserApi.getSimpleUserList()
+})
+</script>

+ 5 - 5
src/views/ai/writer/index/components/Left.vue

@@ -83,13 +83,13 @@
         </template>
 
         <ReuseLabel label="长度" />
-        <Tag v-model="formData.length" :tags="getIntDictOptions('ai_write_length')" />
+        <Tag v-model="formData.length" :tags="getIntDictOptions(DICT_TYPE.AI_WRITE_LENGTH)" />
         <ReuseLabel label="格式" />
-        <Tag v-model="formData.format" :tags="getIntDictOptions('ai_write_format')" />
+        <Tag v-model="formData.format" :tags="getIntDictOptions(DICT_TYPE.AI_WRITE_FORMAT)" />
         <ReuseLabel label="语气" />
-        <Tag v-model="formData.tone" :tags="getIntDictOptions('ai_write_tone')" />
+        <Tag v-model="formData.tone" :tags="getIntDictOptions(DICT_TYPE.AI_WRITE_TONE)" />
         <ReuseLabel label="语言" />
-        <Tag v-model="formData.language" :tags="getIntDictOptions('ai_write_language')" />
+        <Tag v-model="formData.language" :tags="getIntDictOptions(DICT_TYPE.AI_WRITE_LANGUAGE)" />
 
         <div class="flex items-center justify-center mt-3">
           <el-button :disabled="isWriting" @click="reset">重置</el-button>
@@ -106,7 +106,7 @@ import { ref } from 'vue'
 import Tag from './Tag.vue'
 import { WriteVO } from '@/api/ai/writer'
 import { omit } from 'lodash-es'
-import { getIntDictOptions } from '@/utils/dict'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import { AiWriteTypeEnum, WriteExample } from '@/views/ai/utils/constants'
 
 type TabType = WriteVO['type']

+ 49 - 49
src/views/mall/promotion/kefu/components/KeFuConversationBox.vue → src/views/mall/promotion/kefu/components/KeFuConversationList.vue

@@ -1,16 +1,15 @@
 <template>
   <div class="kefu">
     <div
-      v-for="(item, index) in conversationList"
+      v-for="item in conversationList"
       :key="item.id"
-      :class="{ active: index === activeConversationIndex, pinned: item.adminPinned }"
+      :class="{ active: item.id === activeConversationId, pinned: item.adminPinned }"
       class="kefu-conversation flex items-center"
-      @click="openRightMessage(item, index)"
+      @click="openRightMessage(item)"
       @contextmenu.prevent="rightClick($event as PointerEvent, item)"
     >
       <div class="flex justify-center items-center w-100%">
-        <!-- TODO style 换成 unocss -->
-        <div class="flex justify-center items-center" style="width: 50px; height: 50px">
+        <div class="flex justify-center items-center w-50px h-50px">
           <!-- 头像 + 未读 -->
           <el-badge
             :hidden="item.adminUnreadMessageCount === 0"
@@ -27,19 +26,13 @@
               {{ formatDate(item.lastMessageTime) }}
             </span>
           </div>
-          <!-- 文本消息 -->
-          <template v-if="KeFuMessageContentTypeEnum.TEXT === item.lastMessageContentType">
-            <div
-              v-dompurify-html="replaceEmoji(item.lastMessageContent)"
-              class="last-message flex items-center color-[#989EA6]"
-            ></div>
-          </template>
-          <!-- 图片消息 -->
-          <template v-else>
-            <div class="last-message flex items-center color-[#989EA6]">
-              {{ getContentType(item.lastMessageContentType) }}
-            </div>
-          </template>
+          <!-- 最后聊天内容 -->
+          <div
+            v-dompurify-html="
+              getConversationDisplayText(item.lastMessageContentType, item.lastMessageContent)
+            "
+            class="last-message flex items-center color-[#989EA6]"
+          ></div>
         </div>
       </div>
     </div>
@@ -47,7 +40,7 @@
     <!-- 右键,进行操作(类似微信) -->
     <ul v-show="showRightMenu" :style="rightMenuStyle" class="right-menu-ul">
       <li
-        v-show="!selectedConversation.adminPinned"
+        v-show="!rightClickConversation.adminPinned"
         class="flex items-center"
         @click.stop="updateConversationPinned(true)"
       >
@@ -55,7 +48,7 @@
         置顶会话
       </li>
       <li
-        v-show="selectedConversation.adminPinned"
+        v-show="rightClickConversation.adminPinned"
         class="flex items-center"
         @click.stop="updateConversationPinned(false)"
       >
@@ -79,18 +72,22 @@ import { KeFuConversationApi, KeFuConversationRespVO } from '@/api/mall/promotio
 import { useEmoji } from './tools/emoji'
 import { formatDate } from '@/utils/formatTime'
 import { KeFuMessageContentTypeEnum } from './tools/constants'
+import { useAppStore } from '@/store/modules/app'
 
-defineOptions({ name: 'KeFuConversationBox' })
+defineOptions({ name: 'KeFuConversationList' })
 
 const message = useMessage() // 消息弹窗
-
+const appStore = useAppStore()
 const { replaceEmoji } = useEmoji()
 const conversationList = ref<KeFuConversationRespVO[]>([]) // 会话列表
-const activeConversationIndex = ref(-1) // 选中的会话 index 位置 TODO @puhui999:这个可以改成 activeConversationId 么?因为一般是选中的对话编号
+const activeConversationId = ref(-1) // 选中的会话
+const collapse = computed(() => appStore.getCollapse) // 折叠菜单
 
 /** 加载会话列表 */
 const getConversationList = async () => {
-  conversationList.value = await KeFuConversationApi.getConversationList()
+  const list = await KeFuConversationApi.getConversationList()
+  list.sort((a: KeFuConversationRespVO, _) => (a.adminPinned ? -1 : 1))
+  conversationList.value = list
 }
 defineExpose({ getConversationList })
 
@@ -98,45 +95,48 @@ defineExpose({ getConversationList })
 const emits = defineEmits<{
   (e: 'change', v: KeFuConversationRespVO): void
 }>()
-const openRightMessage = (item: KeFuConversationRespVO, index: number) => {
-  activeConversationIndex.value = index
+const openRightMessage = (item: KeFuConversationRespVO) => {
+  activeConversationId.value = item.id
   emits('change', item)
 }
 
-// TODO @puhui999:这个,是不是改成 getConversationDisplayText,获取会话的展示文本。然后,把文本消息类型,也统一处理(包括上面的 replaceEmoji)。这样,更统一。
 /** 获得消息类型 */
-const getContentType = computed(() => (lastMessageContentType: number) => {
-  switch (lastMessageContentType) {
-    case KeFuMessageContentTypeEnum.SYSTEM:
-      return '[系统消息]'
-    case KeFuMessageContentTypeEnum.VIDEO:
-      return '[视频消息]'
-    case KeFuMessageContentTypeEnum.IMAGE:
-      return '[图片消息]'
-    case KeFuMessageContentTypeEnum.PRODUCT:
-      return '[商品消息]'
-    case KeFuMessageContentTypeEnum.ORDER:
-      return '[订单消息]'
-    case KeFuMessageContentTypeEnum.VOICE:
-      return '[语音消息]'
-    default:
-      return ''
+const getConversationDisplayText = computed(
+  () => (lastMessageContentType: number, lastMessageContent: string) => {
+    switch (lastMessageContentType) {
+      case KeFuMessageContentTypeEnum.SYSTEM:
+        return '[系统消息]'
+      case KeFuMessageContentTypeEnum.VIDEO:
+        return '[视频消息]'
+      case KeFuMessageContentTypeEnum.IMAGE:
+        return '[图片消息]'
+      case KeFuMessageContentTypeEnum.PRODUCT:
+        return '[商品消息]'
+      case KeFuMessageContentTypeEnum.ORDER:
+        return '[订单消息]'
+      case KeFuMessageContentTypeEnum.VOICE:
+        return '[语音消息]'
+      case KeFuMessageContentTypeEnum.TEXT:
+        return replaceEmoji(lastMessageContent)
+      default:
+        return ''
+    }
   }
-})
+)
 
 //======================= 右键菜单 =======================
 const showRightMenu = ref(false) // 显示右键菜单
 const rightMenuStyle = ref<any>({}) // 右键菜单 Style
-const selectedConversation = ref<KeFuConversationRespVO>({} as KeFuConversationRespVO) // 右键选中的会话对象 TODO puhui999:这个是不是叫 rightClickConversation 会好点。因为 selected 容易和选中的对话,定义上有点重叠
+const rightClickConversation = ref<KeFuConversationRespVO>({} as KeFuConversationRespVO) // 右键选中的会话对象
 
 /** 打开右键菜单 */
 const rightClick = (mouseEvent: PointerEvent, item: KeFuConversationRespVO) => {
-  selectedConversation.value = item
+  rightClickConversation.value = item
   // 显示右键菜单
   showRightMenu.value = true
   rightMenuStyle.value = {
     top: mouseEvent.clientY - 110 + 'px',
-    left: mouseEvent.clientX - 80 + 'px'
+    left: collapse.value ? mouseEvent.clientX - 80 + 'px' : mouseEvent.clientX - 210 + 'px'
   }
 }
 /** 关闭右键菜单 */
@@ -148,7 +148,7 @@ const closeRightMenu = () => {
 const updateConversationPinned = async (adminPinned: boolean) => {
   // 1. 会话置顶/取消置顶
   await KeFuConversationApi.updateConversationPinned({
-    id: selectedConversation.value.id,
+    id: rightClickConversation.value.id,
     adminPinned
   })
   message.notifySuccess(adminPinned ? '置顶成功' : '取消置顶成功')
@@ -161,7 +161,7 @@ const updateConversationPinned = async (adminPinned: boolean) => {
 const deleteConversation = async () => {
   // 1. 删除会话
   await message.confirm('您确定要删除该会话吗?')
-  await KeFuConversationApi.deleteConversation(selectedConversation.value.id)
+  await KeFuConversationApi.deleteConversation(rightClickConversation.value.id)
   // 2. 关闭右键菜单,更新会话列表
   closeRightMenu()
   await getConversationList()

+ 55 - 58
src/views/mall/promotion/kefu/components/KeFuChatBox.vue → src/views/mall/promotion/kefu/components/KeFuMessageList.vue

@@ -1,21 +1,11 @@
 <template>
-  <el-container v-if="showChatBox" class="kefu">
+  <el-container v-if="showKeFuMessageList" class="kefu">
     <el-header>
-      <!-- TODO @puhui999:keFuConversation => conversation -->
-      <div class="kefu-title">{{ keFuConversation.userNickname }}</div>
+      <div class="kefu-title">{{ conversation.userNickname }}</div>
     </el-header>
-    <!-- TODO @puhui999:unocss -->
-    <el-main class="kefu-content" style="overflow: visible">
-      <!-- 加载历史消息 -->
-      <div
-        v-show="loadingMore"
-        class="loadingMore flex justify-center items-center cursor-pointer"
-        @click="handleOldMessage"
-      >
-        加载更多
-      </div>
+    <el-main class="kefu-content overflow-visible">
       <el-scrollbar ref="scrollbarRef" always height="calc(100vh - 495px)" @scroll="handleScroll">
-        <div ref="innerRef" class="w-[100%] pb-3px">
+        <div v-if="refreshContent" ref="innerRef" class="w-[100%] pb-3px">
           <!-- 消息列表 -->
           <div v-for="(item, index) in getMessageList0" :key="item.id" class="w-[100%]">
             <div class="flex justify-center items-center mb-20px">
@@ -48,7 +38,7 @@
             >
               <el-avatar
                 v-if="item.senderType === UserTypeEnum.MEMBER"
-                :src="keFuConversation.userAvatar"
+                :src="conversation.userAvatar"
                 alt="avatar"
               />
               <div
@@ -121,36 +111,48 @@ import relativeTime from 'dayjs/plugin/relativeTime'
 
 dayjs.extend(relativeTime)
 
-defineOptions({ name: 'KeFuMessageBox' })
+defineOptions({ name: 'KeFuMessageList' })
 
 const message = ref('') // 消息弹窗
 
 const messageTool = useMessage()
 const messageList = ref<KeFuMessageRespVO[]>([]) // 消息列表
-const keFuConversation = ref<KeFuConversationRespVO>({} as KeFuConversationRespVO) // 用户会话
+const conversation = ref<KeFuConversationRespVO>({} as KeFuConversationRespVO) // 用户会话
 const showNewMessageTip = ref(false) // 显示有新消息提示
 const queryParams = reactive({
   pageNo: 1,
+  pageSize: 10,
   conversationId: 0
 })
 const total = ref(0) // 消息总条数
-
+const refreshContent = ref(false) // 内容刷新,主要解决会话消息页面高度不一致导致的滚动功能精度失效
 /** 获得消息列表 */
-const getMessageList = async (conversation: KeFuConversationRespVO) => {
-  keFuConversation.value = conversation
-  queryParams.conversationId = conversation.id
-  const messageTotal = messageList.value.length
-  if (total.value > 0 && messageTotal > 0 && messageTotal === total.value) {
-    return
+const getMessageList = async (val: KeFuConversationRespVO, conversationChange: boolean) => {
+  // 会话切换,重置相关参数
+  if (conversationChange) {
+    queryParams.pageNo = 1
+    messageList.value = []
+    total.value = 0
+    loadHistory.value = false
+    refreshContent.value = false
   }
+  conversation.value = val
+  queryParams.conversationId = val.id
   const res = await KeFuMessageApi.getKeFuMessagePage(queryParams)
   total.value = res.total
-  for (const item of res.list) {
-    if (messageList.value.some((val) => val.id === item.id)) {
-      continue
+  // 情况一:加载最新消息
+  if (queryParams.pageNo === 1) {
+    messageList.value = res.list
+  } else {
+    // 情况二:加载历史消息
+    for (const item of res.list) {
+      if (messageList.value.some((val) => val.id === item.id)) {
+        continue
+      }
+      messageList.value.push(item)
     }
-    messageList.value.push(item)
   }
+  refreshContent.value = true
   await scrollToBottom()
 }
 
@@ -162,20 +164,24 @@ const getMessageList0 = computed(() => {
 
 /** 刷新消息列表 */
 const refreshMessageList = async () => {
-  if (!keFuConversation.value) {
+  if (!conversation.value) {
     return
   }
 
   queryParams.pageNo = 1
-  await getMessageList(keFuConversation.value)
+  await getMessageList(conversation.value, false)
   if (loadHistory.value) {
-    // 下角显示有新消息提示
+    // 下角显示有新消息提示
     showNewMessageTip.value = true
   }
 }
 
 defineExpose({ getMessageList, refreshMessageList })
-const showChatBox = computed(() => !isEmpty(keFuConversation.value)) // 是否显示聊天区域
+const showKeFuMessageList = computed(() => !isEmpty(conversation.value)) // 是否显示聊天区域
+const skipGetMessageList = computed(() => {
+  // 已加载到最后一页的话则不触发新的消息获取
+  return total.value > 0 && Math.ceil(total.value / queryParams.pageSize) === queryParams.pageNo
+}) // 跳过消息获取
 
 /** 处理表情选择 */
 const handleEmojiSelect = (item: Emoji) => {
@@ -186,7 +192,7 @@ const handleEmojiSelect = (item: Emoji) => {
 const handleSendPicture = async (picUrl: string) => {
   // 组织发送消息
   const msg = {
-    conversationId: keFuConversation.value.id,
+    conversationId: conversation.value.id,
     contentType: KeFuMessageContentTypeEnum.IMAGE,
     content: picUrl
   }
@@ -202,7 +208,7 @@ const handleSendMessage = async () => {
   }
   // 2. 组织发送消息
   const msg = {
-    conversationId: keFuConversation.value.id,
+    conversationId: conversation.value.id,
     contentType: KeFuMessageContentTypeEnum.TEXT,
     content: message.value
   }
@@ -215,7 +221,7 @@ const sendMessage = async (msg: any) => {
   await KeFuMessageApi.sendKeFuMessage(msg)
   message.value = ''
   // 加载消息列表
-  await getMessageList(keFuConversation.value)
+  await getMessageList(conversation.value, false)
   // 滚动到最新消息处
   await scrollToBottom()
 }
@@ -233,7 +239,7 @@ const scrollToBottom = async () => {
   scrollbarRef.value!.setScrollTop(innerRef.value!.clientHeight)
   showNewMessageTip.value = false
   // 2.2 消息已读
-  await KeFuMessageApi.updateKeFuMessageReadStatus(keFuConversation.value.id)
+  await KeFuMessageApi.updateKeFuMessageReadStatus(conversation.value.id)
 }
 
 /** 查看新消息 */
@@ -243,23 +249,28 @@ const handleToNewMessage = async () => {
 }
 
 /** 加载历史消息 */
-const loadingMore = ref(false) // 滚动到顶部加载更多
 const loadHistory = ref(false) // 加载历史消息
 const handleScroll = async ({ scrollTop }) => {
-  const messageTotal = messageList.value.length
-  if (total.value > 0 && messageTotal > 0 && messageTotal === total.value) {
+  if (skipGetMessageList.value) {
     return
   }
-  // 距顶 20 加载下一页数据
-  loadingMore.value = scrollTop < 20
+  // 触顶自动加载下一页数据
+  if (scrollTop === 0) {
+    await handleOldMessage()
+  }
 }
 const handleOldMessage = async () => {
+  // 记录已有页面高度
+  const oldPageHeight = innerRef.value?.clientHeight
+  if (!oldPageHeight) {
+    return
+  }
   loadHistory.value = true
   // 加载消息列表
   queryParams.pageNo += 1
-  await getMessageList(keFuConversation.value)
-  loadingMore.value = false
-  // TODO puhui999: 等页面加载完后,获得上一页最后一条消息的位置,控制滚动到它所在位置
+  await getMessageList(conversation.value, false)
+  // 等页面加载完后,获得上一页最后一条消息的位置,控制滚动到它所在位置
+  scrollbarRef.value!.setScrollTop(innerRef.value!.clientHeight - oldPageHeight)
 }
 
 /**
@@ -288,20 +299,6 @@ const showTime = computed(() => (item: KeFuMessageRespVO, index: number) => {
   &-content {
     position: relative;
 
-    .loadingMore {
-      position: absolute;
-      top: 0;
-      left: 0;
-      width: 100%;
-      height: 50px;
-      background-color: #eee;
-      color: #666;
-      text-align: center;
-      line-height: 50px;
-      transform: translateY(-100%);
-      transition: transform 0.3s ease-in-out;
-    }
-
     .newMessageTip {
       position: absolute;
       bottom: 35px;

+ 0 - 0
src/views/mall/promotion/kefu/components/images/a.png → src/views/mall/promotion/kefu/components/asserts/a.png


+ 0 - 0
src/views/mall/promotion/kefu/components/images/aini.png → src/views/mall/promotion/kefu/components/asserts/aini.png


+ 0 - 0
src/views/mall/promotion/kefu/components/images/aixin.png → src/views/mall/promotion/kefu/components/asserts/aixin.png


+ 0 - 0
src/views/mall/promotion/kefu/components/images/baiyan.png → src/views/mall/promotion/kefu/components/asserts/baiyan.png


+ 0 - 0
src/views/mall/promotion/kefu/components/images/bizui.png → src/views/mall/promotion/kefu/components/asserts/bizui.png


+ 0 - 0
src/views/mall/promotion/kefu/components/images/buhaoyisi.png → src/views/mall/promotion/kefu/components/asserts/buhaoyisi.png


+ 0 - 0
src/views/mall/promotion/kefu/components/images/bukesiyi.png → src/views/mall/promotion/kefu/components/asserts/bukesiyi.png


+ 0 - 0
src/views/mall/promotion/kefu/components/images/dajing.png → src/views/mall/promotion/kefu/components/asserts/dajing.png


+ 0 - 0
src/views/mall/promotion/kefu/components/images/danao.png → src/views/mall/promotion/kefu/components/asserts/danao.png


+ 0 - 0
src/views/mall/promotion/kefu/components/images/daxiao.png → src/views/mall/promotion/kefu/components/asserts/daxiao.png


+ 0 - 0
src/views/mall/promotion/kefu/components/images/dianzan.png → src/views/mall/promotion/kefu/components/asserts/dianzan.png


+ 0 - 0
src/views/mall/promotion/kefu/components/images/emo.png → src/views/mall/promotion/kefu/components/asserts/emo.png


+ 0 - 0
src/views/mall/promotion/kefu/components/images/esi.png → src/views/mall/promotion/kefu/components/asserts/esi.png


+ 0 - 0
src/views/mall/promotion/kefu/components/images/fadai.png → src/views/mall/promotion/kefu/components/asserts/fadai.png


+ 0 - 0
src/views/mall/promotion/kefu/components/images/fankun.png → src/views/mall/promotion/kefu/components/asserts/fankun.png


+ 0 - 0
src/views/mall/promotion/kefu/components/images/feiwen.png → src/views/mall/promotion/kefu/components/asserts/feiwen.png


+ 0 - 0
src/views/mall/promotion/kefu/components/images/fennu.png → src/views/mall/promotion/kefu/components/asserts/fennu.png


+ 0 - 0
src/views/mall/promotion/kefu/components/images/ganga.png → src/views/mall/promotion/kefu/components/asserts/ganga.png


+ 0 - 0
src/views/mall/promotion/kefu/components/images/ganmao.png → src/views/mall/promotion/kefu/components/asserts/ganmao.png


+ 0 - 0
src/views/mall/promotion/kefu/components/images/hanyan.png → src/views/mall/promotion/kefu/components/asserts/hanyan.png


+ 0 - 0
src/views/mall/promotion/kefu/components/images/haochi.png → src/views/mall/promotion/kefu/components/asserts/haochi.png


+ 0 - 0
src/views/mall/promotion/kefu/components/images/hongxin.png → src/views/mall/promotion/kefu/components/asserts/hongxin.png


+ 0 - 0
src/views/mall/promotion/kefu/components/images/huaixiao.png → src/views/mall/promotion/kefu/components/asserts/huaixiao.png


+ 0 - 0
src/views/mall/promotion/kefu/components/images/jingkong.png → src/views/mall/promotion/kefu/components/asserts/jingkong.png


+ 0 - 0
src/views/mall/promotion/kefu/components/images/jingshu.png → src/views/mall/promotion/kefu/components/asserts/jingshu.png


+ 0 - 0
src/views/mall/promotion/kefu/components/images/jingya.png → src/views/mall/promotion/kefu/components/asserts/jingya.png


+ 0 - 0
src/views/mall/promotion/kefu/components/images/kaixin.png → src/views/mall/promotion/kefu/components/asserts/kaixin.png


+ 0 - 0
src/views/mall/promotion/kefu/components/images/keai.png → src/views/mall/promotion/kefu/components/asserts/keai.png


+ 0 - 0
src/views/mall/promotion/kefu/components/images/keshui.png → src/views/mall/promotion/kefu/components/asserts/keshui.png


+ 0 - 0
src/views/mall/promotion/kefu/components/images/kun.png → src/views/mall/promotion/kefu/components/asserts/kun.png


+ 0 - 0
src/views/mall/promotion/kefu/components/images/lengku.png → src/views/mall/promotion/kefu/components/asserts/lengku.png


+ 0 - 0
src/views/mall/promotion/kefu/components/images/liuhan.png → src/views/mall/promotion/kefu/components/asserts/liuhan.png


+ 0 - 0
src/views/mall/promotion/kefu/components/images/liukoushui.png → src/views/mall/promotion/kefu/components/asserts/liukoushui.png


+ 0 - 0
src/views/mall/promotion/kefu/components/images/liulei.png → src/views/mall/promotion/kefu/components/asserts/liulei.png


+ 0 - 0
src/views/mall/promotion/kefu/components/images/mengbi.png → src/views/mall/promotion/kefu/components/asserts/mengbi.png


+ 0 - 0
src/views/mall/promotion/kefu/components/images/mianwubiaoqing.png → src/views/mall/promotion/kefu/components/asserts/mianwubiaoqing.png


+ 0 - 0
src/views/mall/promotion/kefu/components/images/nanguo.png → src/views/mall/promotion/kefu/components/asserts/nanguo.png


+ 0 - 0
src/views/mall/promotion/kefu/components/images/outu.png → src/views/mall/promotion/kefu/components/asserts/outu.png


+ 0 - 0
src/views/mall/promotion/kefu/components/images/picture.svg → src/views/mall/promotion/kefu/components/asserts/picture.svg


+ 0 - 0
src/views/mall/promotion/kefu/components/images/shengqi.png → src/views/mall/promotion/kefu/components/asserts/shengqi.png


+ 0 - 0
src/views/mall/promotion/kefu/components/images/shuizhuo.png → src/views/mall/promotion/kefu/components/asserts/shuizhuo.png


+ 0 - 0
src/views/mall/promotion/kefu/components/images/tianshi.png → src/views/mall/promotion/kefu/components/asserts/tianshi.png


+ 0 - 0
src/views/mall/promotion/kefu/components/images/xiaodiaoya.png → src/views/mall/promotion/kefu/components/asserts/xiaodiaoya.png


+ 0 - 0
src/views/mall/promotion/kefu/components/images/xiaoku.png → src/views/mall/promotion/kefu/components/asserts/xiaoku.png


+ 0 - 0
src/views/mall/promotion/kefu/components/images/xinsui.png → src/views/mall/promotion/kefu/components/asserts/xinsui.png


+ 0 - 0
src/views/mall/promotion/kefu/components/images/xiong.png → src/views/mall/promotion/kefu/components/asserts/xiong.png


+ 0 - 0
src/views/mall/promotion/kefu/components/images/yiwen.png → src/views/mall/promotion/kefu/components/asserts/yiwen.png


+ 0 - 0
src/views/mall/promotion/kefu/components/images/yun.png → src/views/mall/promotion/kefu/components/asserts/yun.png


+ 0 - 0
src/views/mall/promotion/kefu/components/images/ziya.png → src/views/mall/promotion/kefu/components/asserts/ziya.png


+ 3 - 3
src/views/mall/promotion/kefu/components/index.ts

@@ -1,4 +1,4 @@
-import KeFuConversationBox from './KeFuConversationBox.vue'
-import KeFuChatBox from './KeFuChatBox.vue'
+import KeFuConversationList from './KeFuConversationList.vue'
+import KeFuMessageList from './KeFuMessageList.vue'
 
-export { KeFuConversationBox, KeFuChatBox }
+export { KeFuConversationList, KeFuMessageList }

+ 4 - 11
src/views/mall/promotion/kefu/components/message/ImageMessageItem.vue

@@ -10,12 +10,13 @@
             : ''
       ]"
     >
-      <!-- TODO @puhui999:unocss -->
       <el-image
+        :initial-index="0"
+        :preview-src-list="[message.content]"
         :src="message.content"
+        class="w-200px"
         fit="contain"
-        style="width: 200px"
-        @click="imagePreview(message.content)"
+        preview-teleported
       />
     </div>
   </template>
@@ -25,17 +26,9 @@
 import { KeFuMessageContentTypeEnum } from '../tools/constants'
 import { UserTypeEnum } from '@/utils/constants'
 import { KeFuMessageRespVO } from '@/api/mall/promotion/kefu/message'
-import { createImageViewer } from '@/components/ImageViewer'
 
 defineOptions({ name: 'ImageMessageItem' })
 defineProps<{
   message: KeFuMessageRespVO
 }>()
-
-/** 图预览 */
-const imagePreview = (imgUrl: string) => {
-  createImageViewer({
-    urlList: [imgUrl]
-  })
-}
 </script>

+ 14 - 15
src/views/mall/promotion/kefu/components/message/OrderMessageItem.vue

@@ -18,10 +18,9 @@
           </div>
         </div>
         <div v-for="item in getMessageContent.items" :key="item.id" class="border-bottom">
-          <!-- TODO @puhui999:要不把 img => picUrl 类似这种,搞的更匹配一点 -->
           <ProductItem
-            :img="item.picUrl"
             :num="item.count"
+            :picUrl="item.picUrl"
             :price="item.price"
             :skuText="item.properties.map((property: any) => property.valueName).join(' ')"
             :title="item.spuName"
@@ -61,7 +60,7 @@ const getMessageContent = computed(() => JSON.parse(props.message.content))
  * @param order 订单
  * @return {string} 颜色的 class 名称
  */
-function formatOrderColor(order) {
+function formatOrderColor(order: any) {
   if (order.status === 0) {
     return 'info-color'
   }
@@ -79,7 +78,7 @@ function formatOrderColor(order) {
  *
  * @param order 订单
  */
-function formatOrderStatus(order) {
+function formatOrderStatus(order: any) {
   if (order.status === 0) {
     return '待付款'
   }
@@ -109,23 +108,23 @@ function formatOrderStatus(order) {
   background-color: #e2e2e2;
 
   .order-card-header {
-    height: 80rpx;
+    height: 80px;
 
     .order-no {
-      font-size: 26rpx;
+      font-size: 26px;
       font-weight: 500;
     }
   }
 
   .pay-box {
     .discounts-title {
-      font-size: 24rpx;
+      font-size: 24px;
       line-height: normal;
       color: #999999;
     }
 
     .discounts-money {
-      font-size: 24rpx;
+      font-size: 24px;
       line-height: normal;
       color: #999;
       font-family: OPPOSANS;
@@ -137,29 +136,29 @@ function formatOrderStatus(order) {
   }
 
   .order-card-footer {
-    height: 100rpx;
+    height: 100px;
 
     .more-item-box {
-      padding: 20rpx;
+      padding: 20px;
 
       .more-item {
-        height: 60rpx;
+        height: 60px;
 
         .title {
-          font-size: 26rpx;
+          font-size: 26px;
         }
       }
     }
 
     .more-btn {
       color: #999999;
-      font-size: 24rpx;
+      font-size: 24px;
     }
 
     .content {
-      width: 154rpx;
+      width: 154px;
       color: #333333;
-      font-size: 26rpx;
+      font-size: 26px;
       font-weight: 500;
     }
   }

+ 9 - 11
src/views/mall/promotion/kefu/components/message/ProductItem.vue

@@ -8,7 +8,14 @@
       class="ss-order-card-warp flex items-stretch justify-between bg-white"
     >
       <div class="img-box mr-24px">
-        <el-image :src="img" class="order-img" fit="contain" @click="imagePrediv(img)" />
+        <el-image
+          :initial-index="0"
+          :preview-src-list="[picUrl]"
+          :src="picUrl"
+          class="order-img"
+          fit="contain"
+          preview-teleported
+        />
       </div>
       <div
         :style="[{ width: titleWidth ? titleWidth + 'px' : '' }]"
@@ -44,12 +51,11 @@
 </template>
 
 <script lang="ts" setup>
-import { createImageViewer } from '@/components/ImageViewer'
 import { fenToYuan } from '@/utils'
 
 defineOptions({ name: 'ProductItem' })
 const props = defineProps({
-  img: {
+  picUrl: {
     type: String,
     default: 'https://img1.baidu.com/it/u=1601695551,235775011&fm=26&fmt=auto'
   },
@@ -101,14 +107,6 @@ const skuString = computed(() => {
   }
   return props.skuText
 })
-
-// TODO @puhui999:可以使用 preview-teleported
-/** 图预览 */
-const imagePrediv = (imgUrl: string) => {
-  createImageViewer({
-    urlList: [imgUrl]
-  })
-}
 </script>
 
 <style lang="scss" scoped>

+ 1 - 1
src/views/mall/promotion/kefu/components/message/ProductMessageItem.vue

@@ -11,7 +11,7 @@
       ]"
     >
       <ProductItem
-        :img="getMessageContent.picUrl"
+        :picUrl="getMessageContent.picUrl"
         :price="getMessageContent.price"
         :skuText="getMessageContent.introduction"
         :title="getMessageContent.spuName"

+ 1 - 2
src/views/mall/promotion/kefu/components/tools/EmojiSelectPopover.vue

@@ -17,8 +17,7 @@
           class="icon-item mr-2 mt-1 w-1/10 flex cursor-pointer items-center justify-center border border-solid p-2"
           @click="handleSelect(item)"
         >
-          <!-- TODO @puhui999:换成 unocss -->
-          <img :src="item.url" style="width: 24px; height: 24px" />
+          <img :src="item.url" class="w-24px h-24px" />
         </li>
       </ul>
     </ElScrollbar>

+ 2 - 4
src/views/mall/promotion/kefu/components/tools/PictureSelectUpload.vue

@@ -1,14 +1,12 @@
 <!-- 图片选择 -->
 <template>
   <div>
-    <!-- TODO @puhui999:unocss -->
-    <img :src="Picture" style="width: 35px; height: 35px" @click="selectAndUpload" />
+    <img :src="Picture" class="w-35px h-35px" @click="selectAndUpload" />
   </div>
 </template>
 
 <script lang="ts" setup>
-// TODO @puhui999:images 换成 asserts
-import Picture from '@/views/mall/promotion/kefu/components/images/picture.svg'
+import Picture from '@/views/mall/promotion/kefu/components/asserts/picture.svg'
 import * as FileApi from '@/api/infra/file'
 
 defineOptions({ name: 'PictureSelectUpload' })

+ 10 - 15
src/views/mall/promotion/kefu/components/tools/emoji.ts

@@ -59,12 +59,10 @@ export interface Emoji {
 export const useEmoji = () => {
   const emojiPathList = ref<any[]>([])
 
-  // TODO @puhui999:initStaticEmoji 会不会更好
   /** 加载本地图片 */
-  const getStaticEmojiPath = async () => {
-    // TODO @puhui999:images 改成 asserts 更合适哈。
+  const initStaticEmoji = async () => {
     const pathList = import.meta.glob(
-      '@/views/mall/promotion/kefu/components/images/*.{png,jpg,jpeg,svg}'
+      '@/views/mall/promotion/kefu/components/asserts/*.{png,jpg,jpeg,svg}'
     )
     for (const path in pathList) {
       const imageModule: any = await pathList[path]()
@@ -75,26 +73,24 @@ export const useEmoji = () => {
   /** 初始化 */
   onMounted(async () => {
     if (isEmpty(emojiPathList.value)) {
-      await getStaticEmojiPath()
+      await initStaticEmoji()
     }
   })
 
-  // TODO @puhui999:建议 function 都改成 const 这种来定义哈。保持统一风格
   /**
    * 将文本中的表情替换成图片
    *
-   * @param data 文本 TODO @puhui999:data => content
+   * @param data 文本
    * @return 替换后的文本
    */
-  function replaceEmoji(data: string) {
-    let newData = data
+  const replaceEmoji = (content: string) => {
+    let newData = content
     if (typeof newData !== 'object') {
-      // TODO @puhui999: \] 是不是可以简化成 ]。我看 idea 提示了哈
-      const reg = /\[(.+?)\]/g // [] 中括号
+      const reg = /\[(.+?)]/g // [] 中括号
       const zhEmojiName = newData.match(reg)
       if (zhEmojiName) {
         zhEmojiName.forEach((item) => {
-          const emojiFile = selEmojiFile(item)
+          const emojiFile = getEmojiFileByName(item)
           newData = newData.replace(
             item,
             `<img class="chat-img" style="width: 24px;height: 24px;margin: 0 3px;" src="${emojiFile}"/>`
@@ -112,13 +108,12 @@ export const useEmoji = () => {
    */
   function getEmojiList(): Emoji[] {
     return emojiList.map((item) => ({
-      url: selEmojiFile(item.name),
+      url: getEmojiFileByName(item.name),
       name: item.name
     })) as Emoji[]
   }
 
-  // TODO @puhui999:getEmojiFileByName 会不会更容易理解哈
-  function selEmojiFile(name: string) {
+  function getEmojiFileByName(name: string) {
     for (const emoji of emojiList) {
       if (emoji.name === name) {
         return emojiPathList.value.find((item: string) => item.indexOf(emoji.file) > -1)

+ 8 - 12
src/views/mall/promotion/kefu/index.vue

@@ -1,23 +1,22 @@
 <template>
   <el-row :gutter="10">
-    <!-- TODO @puhui999:KeFuConversationBox => KeFuConversationList ;KeFuChatBox => KeFuMessageList -->
     <!-- 会话列表 -->
     <el-col :span="8">
       <ContentWrap>
-        <KeFuConversationBox ref="keFuConversationRef" @change="handleChange" />
+        <KeFuConversationList ref="keFuConversationRef" @change="handleChange" />
       </ContentWrap>
     </el-col>
     <!-- 会话详情(选中会话的消息列表) -->
     <el-col :span="16">
       <ContentWrap>
-        <KeFuChatBox ref="keFuChatBoxRef" @change="getConversationList" />
+        <KeFuMessageList ref="keFuChatBoxRef" @change="getConversationList" />
       </ContentWrap>
     </el-col>
   </el-row>
 </template>
 
 <script lang="ts" setup>
-import { KeFuChatBox, KeFuConversationBox } from './components'
+import { KeFuConversationList, KeFuMessageList } from './components'
 import { WebSocketMessageTypeConstants } from './components/tools/constants'
 import { KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
 import { getAccessToken } from '@/utils/auth'
@@ -29,14 +28,12 @@ const message = useMessage() // 消息弹窗
 
 // ======================= WebSocket start =======================
 const server = ref(
-  (import.meta.env.VITE_BASE_URL + '/infra/ws/').replace('http', 'ws') +
-    '?token=' +
-    getAccessToken()
+  (import.meta.env.VITE_BASE_URL + '/infra/ws').replace('http', 'ws') + '?token=' + getAccessToken()
 ) // WebSocket 服务地址
 
 /** 发起 WebSocket 连接 */
 const { data, close, open } = useWebSocket(server.value, {
-  autoReconnect: false, // TODO @puhui999:重连要加下
+  autoReconnect: true,
   heartbeat: true
 })
 
@@ -76,17 +73,16 @@ watchEffect(() => {
   }
 })
 // ======================= WebSocket end =======================
-
 /** 加载会话列表 */
-const keFuConversationRef = ref<InstanceType<typeof KeFuConversationBox>>()
+const keFuConversationRef = ref<InstanceType<typeof KeFuConversationList>>()
 const getConversationList = () => {
   keFuConversationRef.value?.getConversationList()
 }
 
 /** 加载指定会话的消息列表 */
-const keFuChatBoxRef = ref<InstanceType<typeof KeFuChatBox>>()
+const keFuChatBoxRef = ref<InstanceType<typeof KeFuMessageList>>()
 const handleChange = (conversation: KeFuConversationRespVO) => {
-  keFuChatBoxRef.value?.getMessageList(conversation)
+  keFuChatBoxRef.value?.getMessageList(conversation, true)
 }
 
 /** 初始化 */