瀏覽代碼

!479 AI 大模型的草稿提交(code review 代码)
Merge pull request !479 from 芋道源码/dev

芋道源码 9 月之前
父節點
當前提交
21c7c17e8f

二進制
.image/common/ai-feature.png


二進制
.image/common/ai-preview.gif


+ 6 - 8
README.md

@@ -191,26 +191,24 @@ ps:核心功能已经实现,正在对接微信小程序中...
 
 
 ### 商城系统
 ### 商城系统
 
 
+演示地址:<https://doc.iocoder.cn/mall-preview/>
+
 ![功能图](/.image/common/mall-feature.png)
 ![功能图](/.image/common/mall-feature.png)
 
 
 ![功能图](/.image/common/mall-preview.png)
 ![功能图](/.image/common/mall-preview.png)
 
 
-_前端基于 crmeb uniapp 经过授权重构,优化代码实现,接入芋道快速开发平台_
-
-演示地址:<https://doc.iocoder.cn/mall-preview/>
-
 ### ERP 系统
 ### ERP 系统
 
 
-![功能图](/.image/common/erp-feature.png)
-
 演示地址:<https://doc.iocoder.cn/erp-preview/>
 演示地址:<https://doc.iocoder.cn/erp-preview/>
 
 
-### CRM 系统
+![功能图](/.image/common/erp-feature.png)
 
 
-![功能图](/.image/common/crm-feature.png)
+### CRM 系统
 
 
 演示地址:<https://doc.iocoder.cn/crm-preview/>
 演示地址:<https://doc.iocoder.cn/crm-preview/>
 
 
+![功能图](/.image/common/crm-feature.png)
+
 ## 🐷 演示图
 ## 🐷 演示图
 
 
 ### 系统功能
 ### 系统功能

+ 4 - 0
src/api/ai/image/index.ts

@@ -56,6 +56,10 @@ export const ImageApi = {
   getImagePageMy: async (params: PageParam) => {
   getImagePageMy: async (params: PageParam) => {
     return await request.get({ url: `/ai/image/my-page`, params })
     return await request.get({ url: `/ai/image/my-page`, params })
   },
   },
+  // 获取公开的绘图记录
+  getImagePagePublic: async (params: PageParam) => {
+    return await request.get({ url: `/ai/image/public-page`, params })
+  },
   // 获取【我的】绘图记录
   // 获取【我的】绘图记录
   getImageMy: async (id: number) => {
   getImageMy: async (id: number) => {
     return await request.get({ url: `/ai/image/get-my?id=${id}` })
     return await request.get({ url: `/ai/image/get-my?id=${id}` })

+ 85 - 0
src/api/ai/write/index.ts

@@ -0,0 +1,85 @@
+import { fetchEventSource } from '@microsoft/fetch-event-source'
+
+import { getAccessToken } from '@/utils/auth'
+import { config } from '@/config/axios/config'
+import { AiWriteTypeEnum } from '@/views/ai/utils/constants'
+import request from '@/config/axios'
+
+export interface WriteVO {
+  type: AiWriteTypeEnum.WRITING | AiWriteTypeEnum.REPLY // 1:撰写 2:回复
+  prompt: string // 写作内容提示 1。撰写 2回复
+  originalContent: string // 原文
+  length: number // 长度
+  format: number // 格式
+  tone: number // 语气
+  language: number // 语言
+  userId?: number // 用户编号
+  platform?: string // 平台
+  model?: string // 模型
+  generatedContent?: string // 生成的内容
+  errorMessage?: string // 错误信息
+  createTime?: Date // 创建时间
+}
+
+export interface AiWritePageReqVO extends PageParam {
+  userId?: number // 用户编号
+  type?: AiWriteTypeEnum //  写作类型
+  platform?: string // 平台
+  createTime?: [string, string] // 创建时间
+}
+
+export interface AiWriteRespVo {
+  id: number
+  userId: number
+  type: number
+  platform: string
+  model: string
+  prompt: string
+  generatedContent: string
+  originalContent: string
+  length: number
+  format: number
+  tone: number
+  language: number
+  errorMessage: string
+  createTime: string
+}
+
+export const WriteApi = {
+  writeStream: ({
+    data,
+    onClose,
+    onMessage,
+    onError,
+    ctrl
+  }: {
+    data: WriteVO
+    onMessage?: (res: any) => void
+    onError?: (...args: any[]) => void
+    onClose?: (...args: any[]) => void
+    ctrl: AbortController
+  }) => {
+    const token = getAccessToken()
+    return fetchEventSource(`${config.base_url}/ai/write/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
+    })
+  },
+  // 获取写作列表
+  getWritePage: (params: AiWritePageReqVO) => {
+    return request.get<PageResult<AiWriteRespVo[]>>({ url: `/ai/write/page`, params })
+  },
+  // 删除写作
+  deleteWrite(id: number) {
+    return request.delete({ url: `/ai/write/delete`, params: { id } })
+  }
+}

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

@@ -1,65 +0,0 @@
-import { fetchEventSource } from '@microsoft/fetch-event-source'
-import request from '@/config/axios'
-
-import { getAccessToken } from '@/utils/auth'
-import { config } from '@/config/axios/config'
-import { AiWriteTypeEnum } from '@/views/ai/utils/constants'
-
-export interface WriteVO {
-  type: AiWriteTypeEnum.WRITING | AiWriteTypeEnum.REPLY // 1:撰写 2:回复
-  prompt: string // 写作内容提示 1。撰写 2回复
-  originalContent: string // 原文
-  length: number // 长度
-  format: number // 格式
-  tone: number // 语气
-  language: number // 语言
-  userId?: number // 用户编号
-  platform?: string // 平台
-  model?: string // 模型
-  generatedContent?: string // 生成的内容
-  errorMessage: string // 错误信息
-  createTime?: Date // 创建时间
-}
-
-// TODO @hhero:搞成 WriteApi,类似 ConversationApi 一样。这样更有类的概念,后续引入某个 Api,然后调用它的方法就可以了。
-export const writeStream = ({
-  data,
-  onClose,
-  onMessage,
-  onError,
-  ctrl
-}: {
-  data: WriteVO
-  onMessage?: (res: any) => void
-  onError?: (...args: any[]) => void
-  onClose?: (...args: any[]) => void
-  ctrl: AbortController
-}) => {
-  const token = getAccessToken()
-  return fetchEventSource(`${config.base_url}/ai/write/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
-  })
-}
-
-// AI 写作 API
-export const WriteApi = {
-  // 查询AI 写作分页
-  getWritePage: async (params: any) => {
-    return await request.get({ url: `/ai/write/page`, params })
-  },
-
-  // 删除AI 写作
-  deleteWrite: async (id: number) => {
-    return await request.delete({ url: `/ai/write/delete?id=` + id })
-  }
-}

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

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

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

@@ -62,7 +62,7 @@ const platformOptions = [
     value: AiPlatformEnum.STABLE_DIFFUSION
     value: AiPlatformEnum.STABLE_DIFFUSION
   },
   },
   {
   {
-    label: '其',
+    label: '其',
     value: 'other'
     value: 'other'
   }
   }
 ]
 ]
@@ -88,6 +88,7 @@ const handleRegeneration = async (image: ImageVO) => {
   } else if (image.platform === AiPlatformEnum.STABLE_DIFFUSION) {
   } else if (image.platform === AiPlatformEnum.STABLE_DIFFUSION) {
     stableDiffusionRef.value.settingValues(image)
     stableDiffusionRef.value.settingValues(image)
   }
   }
+  // TODO @fan:貌似 other 重新设置不行?
 }
 }
 </script>
 </script>
 
 

+ 41 - 0
src/views/ai/image/square/index.vue

@@ -0,0 +1,41 @@
+<template>
+  <div class="card-list">
+    <div v-for="item in publicList" :key="item.id" class="card">
+      <img :src="item.picUrl" class="img" />
+    </div>
+  </div>
+</template>
+<script setup lang="ts">
+import { ImageApi, ImageVO } from '@/api/ai/image'
+
+/** 属性 */
+// TODO @fan:queryParams 里面搞分页哈。
+const pageNo = ref<number>(1)
+const pageSize = ref<number>(20)
+const publicList = ref<ImageVO[]>([])
+
+/** 获取数据 */
+const getListData = async () => {
+  const res = await ImageApi.getImagePagePublic({ pageNo: pageNo.value, pageSize: pageSize.value })
+  publicList.value = res.list as ImageVO[]
+  console.log('publicList.value', publicList.value)
+}
+
+onMounted(async () => {
+  await getListData()
+})
+</script>
+<style scoped lang="scss">
+.card-list {
+  //display: flex;
+  //flex-direction: column;
+  column-count: 4;
+  column-gap: 3px;
+}
+
+.card {
+  .img {
+    width: 50%;
+  }
+}
+</style>

+ 2 - 2
src/views/ai/music/components/index.vue

@@ -1,5 +1,5 @@
 <template>
 <template>
-  <div class="flex h-1/1">
+  <div class="flex ">
     <!-- 模式 -->
     <!-- 模式 -->
     <Mode class="flex-none" @generate-music="generateMusic"/>
     <Mode class="flex-none" @generate-music="generateMusic"/>
     <!-- 音频列表 -->
     <!-- 音频列表 -->
@@ -13,7 +13,7 @@ import List from './list/index.vue'
 
 
 defineOptions({ name: 'Index' })
 defineOptions({ name: 'Index' })
 
 
-const listRef = ref<{generateMusic: (...args) => void} | null>(null)
+const listRef = ref<Nullable<{generateMusic: (...args) => void}>>(null)
 
 
 function generateMusic (args: {formData: Recordable}) {
 function generateMusic (args: {formData: Recordable}) {
  unref(listRef)?.generateMusic(args.formData)
  unref(listRef)?.generateMusic(args.formData)

+ 60 - 1
src/views/ai/music/components/list/audioBar/index.vue

@@ -1,9 +1,68 @@
 <template>
 <template>
-  <div class="h-72px bg-[var(--el-bg-color-overlay)] b-solid b-1 b-[var(--el-border-color)] b-l-none">播放器</div>
+  <div class="flex items-center justify-between px-2 h-72px bg-[var(--el-bg-color-overlay)] b-solid b-1 b-[var(--el-border-color)] b-l-none">
+    <!-- 歌曲信息 -->
+    <div class="flex gap-[10px]">
+      <el-image src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png" class="w-[45px]"/>
+      <div>
+        <div>我很好</div>
+        <div class="text-[12px] text-gray-400">刘大壮</div>
+      </div>
+    </div>
+      
+    <!-- 音频controls -->
+    <div class="flex gap-[12px] items-center">
+      <Icon icon="majesticons:back-circle" :size="20" class="text-gray-300 cursor-pointer"/>
+      <Icon :icon="audioProps.paused ? 'mdi:arrow-right-drop-circle' : 'solar:pause-circle-bold'" :size="30" class=" cursor-pointer" @click="toggleStatus('paused')"/>
+      <Icon icon="majesticons:next-circle" :size="20" class="text-gray-300 cursor-pointer"/>
+      <div class="flex gap-[16px] items-center">
+        <span>{{audioProps.currentTime}}</span>
+        <el-slider v-model="audioProps.duration" color="#409eff" class="w-[160px!important] "/>
+        <span>{{ audioProps.duration }}</span>
+      </div>
+      <!-- 音频 -->
+      <audio v-bind="audioProps" ref="audioRef" controls v-show="!audioProps" @timeupdate="audioTimeUpdate">
+        <source :src="response"/>
+      </audio>
+    </div>
+
+    <!-- 音量控制器 -->
+    <div class="flex gap-[16px] items-center">
+      <Icon :icon="audioProps.muted ? 'tabler:volume-off' : 'tabler:volume'" :size="20" class="cursor-pointer" @click="toggleStatus('muted')"/>
+      <el-slider v-model="audioProps.volume" color="#409eff" class="w-[160px!important] "/>
+    </div>
+  </div>
 </template>
 </template>
 
 
 <script lang="ts" setup>
 <script lang="ts" setup>
+import { formatPast } from '@/utils/formatTime'
+import response from '@/assets/audio/response.mp3'
 
 
 defineOptions({ name: 'Index' })
 defineOptions({ name: 'Index' })
 
 
+  const audioRef = ref<Nullable<HTMLElement>>(null)
+    // 音频相关属性https://www.runoob.com/tags/ref-av-dom.html
+  const audioProps = reactive({
+    autoplay: true,
+    paused: false,
+    currentTime: '00:00',
+    duration: '00:00',
+    muted:  false,
+    volume: 50,
+  })
+
+  function toggleStatus (type: string) {
+    audioProps[type] = !audioProps[type]
+    if (type === 'paused' && audioRef.value) {
+      if (audioProps[type]) {
+        audioRef.value.pause()
+      } else {
+        audioRef.value.play()
+      }
+    }
+  }
+
+  // 更新播放位置
+  function audioTimeUpdate (args) {
+    audioProps.currentTime = formatPast(new Date(args.timeStamp), 'mm:ss')
+  }
 </script>
 </script>

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

@@ -1,5 +1,5 @@
 <template>
 <template>
-  <div class="flex flex-col h-full">
+  <div class="flex flex-col h-[600px]">
     <div class="flex-auto flex overflow-hidden">
     <div class="flex-auto flex overflow-hidden">
       <el-tabs v-model="currentType" class="flex-auto px-[var(--app-content-padding)]">
       <el-tabs v-model="currentType" class="flex-auto px-[var(--app-content-padding)]">
         <!-- 我的创作 -->
         <!-- 我的创作 -->

+ 2 - 5
src/views/ai/music/components/mode/index.vue

@@ -1,5 +1,5 @@
 <template>
 <template>
-  <ContentWrap class="w-300px h-full">
+  <ContentWrap class="w-300px h-full mb-[0!important]">
     <el-radio-group v-model="generateMode" class="mb-15px">
     <el-radio-group v-model="generateMode" class="mb-15px">
       <el-radio-button label="desc">
       <el-radio-button label="desc">
         描述模式
         描述模式
@@ -28,10 +28,7 @@ const emits = defineEmits(['generate-music'])
 
 
 const generateMode = ref('lyric')
 const generateMode = ref('lyric')
 
 
-interface ModeRef {
-  formData: Recordable
-}
-const modeRef = ref<ModeRef | null>(null)
+const modeRef = ref<Nullable<{ formData: Recordable }>>(null)
 
 
 /*
 /*
  *@Description: 根据信息生成音乐
  *@Description: 根据信息生成音乐

+ 28 - 52
src/views/ai/utils/constants.ts

@@ -20,17 +20,21 @@ export const AiPlatformEnum = {
   Ollama: 'Ollama',
   Ollama: 'Ollama',
   STABLE_DIFFUSION: 'StableDiffusion', // Stability AI
   STABLE_DIFFUSION: 'StableDiffusion', // Stability AI
   MIDJOURNEY: 'Midjourney', // Midjourney
   MIDJOURNEY: 'Midjourney', // Midjourney
-  SUNO: 'Suno', // Suno AI
+  SUNO: 'Suno' // Suno AI
 }
 }
 
 
-export const OtherPlatformEnum:ImageModelVO [] = [
+export const OtherPlatformEnum: ImageModelVO[] = [
   {
   {
     key: AiPlatformEnum.TONG_YI,
     key: AiPlatformEnum.TONG_YI,
     name: '通义万相'
     name: '通义万相'
   },
   },
   {
   {
     key: AiPlatformEnum.YI_YAN,
     key: AiPlatformEnum.YI_YAN,
-    name: '百度图片'
+    name: '百度千帆'
+  },
+  {
+    key: AiPlatformEnum.ZHI_PU,
+    name: '智谱 AI'
   }
   }
 ]
 ]
 
 
@@ -60,6 +64,12 @@ export enum AiWriteTypeEnum {
   REPLY // 回复
   REPLY // 回复
 }
 }
 
 
+// 表格展示对照map
+export const AiWriteTypeTableRender = {
+  [AiWriteTypeEnum.WRITING]: '撰写',
+  [AiWriteTypeEnum.REPLY]: '回复',
+}
+
 // ========== 【图片 UI】相关的枚举 ==========
 // ========== 【图片 UI】相关的枚举 ==========
 export const ImageHotWords = [
 export const ImageHotWords = [
   '中国旗袍',
   '中国旗袍',
@@ -200,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[] = [
 export const TongYiWanXiangModels: ImageModelVO[] = [
   {
   {
     key: 'wanx-v1',
     key: 'wanx-v1',
@@ -259,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[] = [
 export const StableDiffusionClipGuidancePresets: ImageModelVO[] = [
   {
   {
     key: 'NONE',
     key: 'NONE',
@@ -318,7 +294,7 @@ export const Dall3StyleList: ImageModelVO[] = [
 
 
 export interface ImageSizeVO {
 export interface ImageSizeVO {
   key: string
   key: string
-  name: string
+  name?: string
   style: string
   style: string
   width: string
   width: string
   height: string
   height: string

+ 29 - 19
src/views/ai/writer/index/components/Left.vue → src/views/ai/write/index/components/Left.vue

@@ -24,26 +24,28 @@
     </h3>
     </h3>
   </DefineLabel>
   </DefineLabel>
 
 
-  <div class="relative" v-bind="$attrs">
+  <div class="flex flex-col" v-bind="$attrs">
     <!-- tab -->
     <!-- tab -->
-    <div
-      class="absolute left-1/2 top-2 -translate-x-1/2 w-[303px] rounded-full bg-[#DDDFE3] p-1 z-10"
-    >
-      <div
-        class="flex items-center relative after:content-[''] after:block after:bg-white after:h-[30px] after:w-1/2 after:absolute after:top-0 after:left-0 after:transition-transform after:rounded-full"
-        :class="selectedTab === AiWriteTypeEnum.REPLY && 'after:transform after:translate-x-[100%]'"
-      >
-        <ReuseTab
-          v-for="tab in tabs"
-          :key="tab.value"
-          :text="tab.text"
-          :active="tab.value === selectedTab"
-          :itemClick="() => switchTab(tab.value)"
-        />
+    <div class="w-full pt-2 bg-[#f5f7f9] flex justify-center">
+      <div class="w-[303px] rounded-full bg-[#DDDFE3] p-1 z-10">
+        <div
+          class="flex items-center relative after:content-[''] after:block after:bg-white after:h-[30px] after:w-1/2 after:absolute after:top-0 after:left-0 after:transition-transform after:rounded-full"
+          :class="
+            selectedTab === AiWriteTypeEnum.REPLY && 'after:transform after:translate-x-[100%]'
+          "
+        >
+          <ReuseTab
+            v-for="tab in tabs"
+            :key="tab.value"
+            :text="tab.text"
+            :active="tab.value === selectedTab"
+            :itemClick="() => switchTab(tab.value)"
+          />
+        </div>
       </div>
       </div>
     </div>
     </div>
     <div
     <div
-      class="px-7 pb-2 pt-[46px] overflow-y-auto lg:block w-[380px] box-border bg-[#ECEDEF] h-full"
+      class="px-7 pb-2 flex-grow overflow-y-auto lg:block w-[380px] box-border bg-[#f5f7f9] h-full"
     >
     >
       <div>
       <div>
         <template v-if="selectedTab === 1">
         <template v-if="selectedTab === 1">
@@ -102,7 +104,7 @@
 import { createReusableTemplate } from '@vueuse/core'
 import { createReusableTemplate } from '@vueuse/core'
 import { ref } from 'vue'
 import { ref } from 'vue'
 import Tag from './Tag.vue'
 import Tag from './Tag.vue'
-import { WriteVO } from '@/api/ai/writer'
+import { WriteVO } from 'src/api/ai/write'
 import { omit } from 'lodash-es'
 import { omit } from 'lodash-es'
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import { AiWriteTypeEnum, WriteExample } from '@/views/ai/utils/constants'
 import { AiWriteTypeEnum, WriteExample } from '@/views/ai/utils/constants'
@@ -177,10 +179,18 @@ const initData: WriteVO = {
 }
 }
 const formData = ref<WriteVO>({ ...initData })
 const formData = ref<WriteVO>({ ...initData })
 
 
+/** 用来记录切换之前所填写的数据,切换的时候给赋值回来 **/
+const recordFormData = {} as Record<AiWriteTypeEnum, WriteVO>
+
 /** 切换tab **/
 /** 切换tab **/
 const switchTab = (value: TabType) => {
 const switchTab = (value: TabType) => {
-  selectedTab.value = value
-  formData.value = { ...initData }
+  if (value !== selectedTab.value) {
+    // 保存之前的久数据
+    recordFormData[selectedTab.value] = formData.value
+    selectedTab.value = value
+    // 将之前的旧数据赋值回来
+    formData.value = { ...initData, ...recordFormData[value] }
+  }
 }
 }
 
 
 /** 提交写作 */
 /** 提交写作 */

+ 28 - 13
src/views/ai/writer/index/components/Right.vue → src/views/ai/write/index/components/Right.vue

@@ -1,17 +1,19 @@
 <template>
 <template>
-  <div class="h-full box-border flex flex-col px-7">
-    <h3 class="m-0 h-14 -mx-7 px-7 shrink-0 flex items-center justify-between bg-[#ecedef]">
-      <span>预览</span>
-      <!-- 展示在右上角 -->
-      <el-button color="#846af7" v-show="showCopy" @click="copyContent" size="small">
-        <template #icon>
-          <Icon icon="ph:copy-bold" />
-        </template>
-        复制
-      </el-button>
-    </h3>
+  <el-card class="my-card h-full">
+    <template #header>
+      <h3 class="m-0 px-7 shrink-0 flex items-center justify-between">
+        <span>预览</span>
+        <!-- 展示在右上角 -->
+        <el-button color="#846af7" v-show="showCopy" @click="copyContent" size="small">
+          <template #icon>
+            <Icon icon="ph:copy-bold" />
+          </template>
+          复制
+        </el-button>
+      </h3>
+    </template>
 
 
-    <div ref="contentRef" class="hide-scroll-bar flex-grow box-border overflow-y-auto">
+    <div ref="contentRef" class="hide-scroll-bar h-full box-border overflow-y-auto">
       <div class="w-full min-h-full relative flex-grow bg-white box-border p-3 sm:p-7">
       <div class="w-full min-h-full relative flex-grow bg-white box-border p-3 sm:p-7">
         <!-- 终止生成内容的按钮 -->
         <!-- 终止生成内容的按钮 -->
         <el-button
         <el-button
@@ -36,7 +38,7 @@
         />
         />
       </div>
       </div>
     </div>
     </div>
-  </div>
+  </el-card>
 </template>
 </template>
 
 
 <script setup lang="ts">
 <script setup lang="ts">
@@ -102,4 +104,17 @@ watch(copied, (val) => {
     height: 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;
+  }
+}
 </style>
 </style>

+ 1 - 0
src/views/ai/writer/index/components/Tag.vue → src/views/ai/write/index/components/Tag.vue

@@ -1,3 +1,4 @@
+<!-- 标签选项 -->
 <template>
 <template>
   <div class="flex flex-wrap gap-[8px]">
   <div class="flex flex-wrap gap-[8px]">
     <span
     <span

+ 4 - 5
src/views/ai/writer/index/index.vue → src/views/ai/write/index/index.vue

@@ -1,6 +1,5 @@
 <template>
 <template>
-  <!-- TODO @hhhero:整体没啥问题了。感觉整体框框的样子可以优化下,可以参考下绘图界面。例如说:1)写作的“预览”和绘图的“绘图任务”的 header;2)左右的边界,有个竖线之类的。 -->
-  <div class="h-[calc(100vh-var(--top-tool-height)-var(--app-footer-height)-40px)] -m-5 flex">
+  <div class="absolute top-0 left-0 right-0 bottom-0 flex">
     <Left
     <Left
       :is-writing="isWriting"
       :is-writing="isWriting"
       class="h-full"
       class="h-full"
@@ -21,7 +20,7 @@
 <script setup lang="ts">
 <script setup lang="ts">
 import Left from './components/Left.vue'
 import Left from './components/Left.vue'
 import Right from './components/Right.vue'
 import Right from './components/Right.vue'
-import * as WriteApi from '@/api/ai/writer'
+import { WriteApi, WriteVO } from '@/api/ai/write'
 import { WriteExample } from '@/views/ai/utils/constants'
 import { WriteExample } from '@/views/ai/utils/constants'
 
 
 const message = useMessage()
 const message = useMessage()
@@ -38,7 +37,7 @@ const stopStream = () => {
 
 
 /** 执行写作 */
 /** 执行写作 */
 const rightRef = ref<InstanceType<typeof Right>>()
 const rightRef = ref<InstanceType<typeof Right>>()
-const submit = (data) => {
+const submit = (data: WriteVO) => {
   abortController.value = new AbortController()
   abortController.value = new AbortController()
   writeResult.value = ''
   writeResult.value = ''
   isWriting.value = true
   isWriting.value = true
@@ -66,7 +65,7 @@ const submit = (data) => {
 }
 }
 
 
 /** 点击示例触发 */
 /** 点击示例触发 */
-const handleExampleClick = (type: keyof typeof WriteExampleDataJson) => {
+const handleExampleClick = (type: keyof typeof WriteExample) => {
   writeResult.value = WriteExample[type].data
   writeResult.value = WriteExample[type].data
 }
 }
 
 

+ 25 - 7
src/views/ai/write/manager/index.vue

@@ -39,7 +39,7 @@
         </el-select>
         </el-select>
       </el-form-item>
       </el-form-item>
       <el-form-item label="平台" prop="platform">
       <el-form-item label="平台" prop="platform">
-        <el-select v-model="queryParams.status" placeholder="请选择平台" clearable class="!w-240px">
+        <el-select v-model="queryParams.platform" placeholder="请选择平台" clearable class="!w-240px">
           <el-option
           <el-option
             v-for="dict in getStrDictOptions(DICT_TYPE.AI_PLATFORM)"
             v-for="dict in getStrDictOptions(DICT_TYPE.AI_PLATFORM)"
             :key="dict.value"
             :key="dict.value"
@@ -70,6 +70,7 @@
         >
         >
           <Icon icon="ep:plus" class="mr-5px" /> 新增
           <Icon icon="ep:plus" class="mr-5px" /> 新增
         </el-button>
         </el-button>
+        <!-- TODO @YunaiV  目前没有导出接口,需要导出吗 -->
         <el-button
         <el-button
           type="success"
           type="success"
           plain
           plain
@@ -103,7 +104,13 @@
         </template>
         </template>
       </el-table-column>
       </el-table-column>
       <el-table-column label="模型" align="center" prop="model" width="180" />
       <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="prompt"
+        width="180"
+        show-overflow-tooltip
+      />
       <el-table-column label="生成的内容" align="center" prop="generatedContent" 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="originalContent" width="180" />
       <el-table-column label="长度" align="center" prop="length">
       <el-table-column label="长度" align="center" prop="length">
@@ -136,6 +143,7 @@
       <el-table-column label="错误信息" align="center" prop="errorMessage" />
       <el-table-column label="错误信息" align="center" prop="errorMessage" />
       <el-table-column label="操作" align="center">
       <el-table-column label="操作" align="center">
         <template #default="scope">
         <template #default="scope">
+<!--          TODO @YunaiV 目前没有修改接口,写作要可以更改吗-->
           <el-button
           <el-button
             link
             link
             type="primary"
             type="primary"
@@ -168,8 +176,8 @@
 <script setup lang="ts">
 <script setup lang="ts">
 import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
 import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
 import { dateFormatter } from '@/utils/formatTime'
 import { dateFormatter } from '@/utils/formatTime'
-// TODO 芋艿:这里应该是 write
-import { WriteApi, WriteVO } from '@/api/ai/writer'
+import { useRouter } from 'vue-router'
+import { WriteApi, AiWritePageReqVO, AiWriteRespVo } from '@/api/ai/write'
 import * as UserApi from '@/api/system/user'
 import * as UserApi from '@/api/system/user'
 
 
 /** AI 写作列表 */
 /** AI 写作列表 */
@@ -177,17 +185,18 @@ defineOptions({ name: 'AiWriteManager' })
 
 
 const message = useMessage() // 消息弹窗
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
 const { t } = useI18n() // 国际化
+const router = useRouter() // 路由
 
 
 const loading = ref(true) // 列表的加载中
 const loading = ref(true) // 列表的加载中
-const list = ref<WriteVO[]>([]) // 列表的数据
+const list = ref<AiWriteRespVo[]>([]) // 列表的数据
 const total = ref(0) // 列表的总页数
 const total = ref(0) // 列表的总页数
-const queryParams = reactive({
+const queryParams = reactive<AiWritePageReqVO>({
   pageNo: 1,
   pageNo: 1,
   pageSize: 10,
   pageSize: 10,
   userId: undefined,
   userId: undefined,
   type: undefined,
   type: undefined,
   platform: undefined,
   platform: undefined,
-  createTime: []
+  createTime: undefined
 })
 })
 const queryFormRef = ref() // 搜索的表单
 const queryFormRef = ref() // 搜索的表单
 const userList = ref<UserApi.UserVO[]>([]) // 用户列表
 const userList = ref<UserApi.UserVO[]>([]) // 用户列表
@@ -216,6 +225,15 @@ const resetQuery = () => {
   handleQuery()
   handleQuery()
 }
 }
 
 
+/** 新增方法,跳转到写作页面 **/
+const openForm = (type: string, id?: number) => {
+  switch (type) {
+    case 'create':
+      router.push('/ai/write')
+      break
+  }
+}
+
 /** 删除按钮操作 */
 /** 删除按钮操作 */
 const handleDelete = async (id: number) => {
 const handleDelete = async (id: number) => {
   try {
   try {

+ 1 - 3
src/views/mall/promotion/kefu/index.vue

@@ -28,9 +28,7 @@ const message = useMessage() // 消息弹窗
 
 
 // ======================= WebSocket start =======================
 // ======================= WebSocket start =======================
 const server = ref(
 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 服务地址
 
 
 /** 发起 WebSocket 连接 */
 /** 发起 WebSocket 连接 */

+ 5 - 0
types/global.d.ts

@@ -50,4 +50,9 @@ declare global {
     name: string
     name: string
     children?: Tree[] | any[]
     children?: Tree[] | any[]
   }
   }
+  // 分页数据公共返回
+  interface PageResult<T> {
+    list: T // 数据
+    total: number // 总量
+  }
 }
 }