Selaa lähdekoodia

Merge remote-tracking branch 'origin/dev' into dev

cherishsince 9 kuukautta sitten
vanhempi
commit
9b647cc19e

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

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

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

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

@@ -1,9 +1,68 @@
 <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>
 
 <script lang="ts" setup>
+import { formatPast } from '@/utils/formatTime'
+import response from '@/assets/audio/response.mp3'
 
 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>

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

@@ -1,5 +1,5 @@
 <template>
-  <div class="flex flex-col h-full">
+  <div class="flex flex-col h-[600px]">
     <div class="flex-auto flex overflow-hidden">
       <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>
-  <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-button label="desc">
         描述模式
@@ -28,10 +28,7 @@ const emits = defineEmits(['generate-music'])
 
 const generateMode = ref('lyric')
 
-interface ModeRef {
-  formData: Recordable
-}
-const modeRef = ref<ModeRef | null>(null)
+const modeRef = ref<Nullable<{ formData: Recordable }>>(null)
 
 /*
  *@Description: 根据信息生成音乐

+ 6 - 0
src/views/ai/utils/constants.ts

@@ -64,6 +64,12 @@ export enum AiWriteTypeEnum {
   REPLY // 回复
 }
 
+// 表格展示对照map
+export const AiWriteTypeTableRender = {
+  [AiWriteTypeEnum.WRITING]: '撰写',
+  [AiWriteTypeEnum.REPLY]: '回复',
+}
+
 // ========== 【图片 UI】相关的枚举 ==========
 export const ImageHotWords = [
   '中国旗袍',

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

@@ -24,26 +24,28 @@
     </h3>
   </DefineLabel>
 
-  <div class="relative" v-bind="$attrs">
+  <div class="flex flex-col" v-bind="$attrs">
     <!-- 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
-      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>
         <template v-if="selectedTab === 1">
@@ -102,7 +104,7 @@
 import { createReusableTemplate } from '@vueuse/core'
 import { ref } from '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 { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import { AiWriteTypeEnum, WriteExample } from '@/views/ai/utils/constants'
@@ -177,10 +179,18 @@ const initData: WriteVO = {
 }
 const formData = ref<WriteVO>({ ...initData })
 
+/** 用来记录切换之前所填写的数据,切换的时候给赋值回来 **/
+const recordFormData = {} as Record<AiWriteTypeEnum, WriteVO>
+
 /** 切换tab **/
 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>
-  <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">
         <!-- 终止生成内容的按钮 -->
         <el-button
@@ -36,7 +38,7 @@
         />
       </div>
     </div>
-  </div>
+  </el-card>
 </template>
 
 <script setup lang="ts">
@@ -102,4 +104,17 @@ watch(copied, (val) => {
     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>

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


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

@@ -1,6 +1,5 @@
 <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
       :is-writing="isWriting"
       class="h-full"
@@ -21,7 +20,7 @@
 <script setup lang="ts">
 import Left from './components/Left.vue'
 import Right from './components/Right.vue'
-import * as WriteApi from '@/api/ai/writer'
+import { WriteApi } from '@/api/ai/write'
 import { WriteExample } from '@/views/ai/utils/constants'
 
 const message = useMessage()
@@ -66,7 +65,7 @@ const submit = (data) => {
 }
 
 /** 点击示例触发 */
-const handleExampleClick = (type: keyof typeof WriteExampleDataJson) => {
+const handleExampleClick = (type: keyof typeof WriteExample) => {
   writeResult.value = WriteExample[type].data
 }
 

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

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

+ 5 - 0
types/global.d.ts

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