Эх сурвалжийг харах

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

puhui999 10 сар өмнө
parent
commit
1ad5de57f9

BIN
.image/common/ai-feature.png


BIN
.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-preview.png)
 
-_前端基于 crmeb uniapp 经过授权重构,优化代码实现,接入芋道快速开发平台_
-
-演示地址:<https://doc.iocoder.cn/mall-preview/>
-
 ### ERP 系统
 
-![功能图](/.image/common/erp-feature.png)
-
 演示地址:<https://doc.iocoder.cn/erp-preview/>
 
-### CRM 系统
+![功能图](/.image/common/erp-feature.png)
 
-![功能图](/.image/common/crm-feature.png)
+### CRM 系统
 
 演示地址:<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) => {
     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) => {
     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 - 43
src/api/ai/writer/index.ts

@@ -1,43 +0,0 @@
-import { fetchEventSource } from '@microsoft/fetch-event-source'
-
-import { getAccessToken } from '@/utils/auth'
-import { config } from '@/config/axios/config'
-
-export interface WriteVO {
-  type: 1 | 2 // 1:撰写 2:回复
-  prompt: string // 写作内容提示 1。撰写 2回复
-  originalContent: string // 原文
-  length: number // 长度
-  format: number // 格式
-  tone: number // 语气
-  language: number // 语言
-}
-
-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
-  })
-}

+ 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 写作语言
 }

+ 4 - 8
src/views/ai/image/index/components/ImageList.vue

@@ -185,9 +185,11 @@ onUnmounted(async () => {
   }
 })
 </script>
-
-<!-- TODO fan:这 2 个 scss 可以合并么? -->
 <style lang="scss">
+.dr-task {
+  width: 100%;
+  height: 100%;
+}
 .task-card {
   margin: 0;
   padding: 0;
@@ -229,9 +231,3 @@ onUnmounted(async () => {
   align-items: center;
 }
 </style>
-<style scoped lang="scss">
-.dr-task {
-  width: 100%;
-  height: 100%;
-}
-</style>

+ 216 - 0
src/views/ai/image/index/components/other/index.vue

@@ -0,0 +1,216 @@
+<!-- dall3 -->
+<template>
+  <div class="prompt">
+    <el-text tag="b">画面描述</el-text>
+    <el-text tag="p">建议使用“形容词+动词+风格”的格式,使用“,”隔开</el-text>
+    <el-input
+      v-model="prompt"
+      maxlength="1024"
+      rows="5"
+      class="w-100% mt-15px"
+      input-style="border-radius: 7px;"
+      placeholder="例如:童话里的小屋应该是什么样子?"
+      show-word-limit
+      type="textarea"
+    />
+  </div>
+  <div class="hot-words">
+    <div>
+      <el-text tag="b">随机热词</el-text>
+    </div>
+    <el-space wrap class="word-list">
+      <el-button
+        round
+        class="btn"
+        :type="selectHotWord === hotWord ? 'primary' : 'default'"
+        v-for="hotWord in ImageHotWords"
+        :key="hotWord"
+        @click="handleHotWordClick(hotWord)"
+      >
+        {{ hotWord }}
+      </el-button>
+    </el-space>
+  </div>
+  <div class="group-item">
+    <div>
+      <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-option
+          v-for="item in OtherPlatformEnum"
+          :key="item.key"
+          :label="item.name"
+          :value="item.key"
+        />
+      </el-select>
+    </el-space>
+  </div>
+  <div class="group-item">
+    <div>
+      <el-text tag="b">模型</el-text>
+    </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-select>
+    </el-space>
+  </div>
+  <div class="group-item">
+    <div>
+      <el-text tag="b">图片尺寸</el-text>
+    </div>
+    <el-space wrap class="group-item-body">
+      <el-input v-model="width" type="number" class="w-170px" placeholder="图片宽度" />
+      <el-input v-model="height" type="number" class="w-170px" placeholder="图片高度" />
+    </el-space>
+  </div>
+  <div class="btns">
+    <el-button type="primary" size="large" round :loading="drawIn" @click="handleGenerateImage">
+      {{ drawIn ? '生成中' : '生成内容' }}
+    </el-button>
+  </div>
+</template>
+<script setup lang="ts">
+import { ImageApi, ImageDrawReqVO, ImageVO } from '@/api/ai/image'
+import {
+  AiPlatformEnum,
+  ChatGlmModels,
+  ImageHotWords,
+  ImageModelVO,
+  OtherPlatformEnum,
+  QianFanModels,
+  TongYiWanXiangModels
+} from '@/views/ai/utils/constants'
+
+const message = useMessage() // 消息弹窗
+
+// 定义属性
+const drawIn = ref<boolean>(false) // 生成中
+const selectHotWord = ref<string>('') // 选中的热词
+// 表单
+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) // 模型  TongYiWanXiangModels、QianFanModels
+const model = ref<string>(models.value[0].key) // 模型
+
+const emits = defineEmits(['onDrawStart', 'onDrawComplete']) // 定义 emits
+
+/** 选择热词 */
+const handleHotWordClick = async (hotWord: string) => {
+  // 情况一:取消选中
+  if (selectHotWord.value == hotWord) {
+    selectHotWord.value = ''
+    return
+  }
+
+  // 情况二:选中
+  selectHotWord.value = hotWord // 选中
+  prompt.value = hotWord // 替换提示词
+}
+
+/** 图片生成 */
+const handleGenerateImage = async () => {
+  // 二次确认
+  await message.confirm(`确认生成内容?`)
+  try {
+    // 加载中
+    drawIn.value = true
+    // 回调
+    emits('onDrawStart', AiPlatformEnum.STABLE_DIFFUSION)
+    // 发送请求
+    const form = {
+      platform: otherPlatform.value,
+      model: model.value, // 模型
+      prompt: prompt.value, // 提示词
+      width: width.value, // 图片宽度
+      height: height.value, // 图片高度
+      options: {}
+    } as unknown as ImageDrawReqVO
+    await ImageApi.drawImage(form)
+  } finally {
+    // 回调
+    emits('onDrawComplete', AiPlatformEnum.STABLE_DIFFUSION)
+    // 加载结束
+    drawIn.value = false
+  }
+}
+
+/** 填充值 */
+const settingValues = async (detail: ImageVO) => {
+  prompt.value = detail.prompt
+  width.value = detail.width
+  height.value = detail.height
+}
+
+/** 平台切换 */
+const handlerPlatformChange = async (platform: string) => {
+  // 切换平台,切换模型、风格
+  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 {
+    model.value = ''
+  }
+}
+
+/** 暴露组件方法 */
+defineExpose({ settingValues })
+</script>
+<style scoped lang="scss">
+// 提示词
+.prompt {
+}
+
+// 热词
+.hot-words {
+  display: flex;
+  flex-direction: column;
+  margin-top: 30px;
+
+  .word-list {
+    display: flex;
+    flex-direction: row;
+    flex-wrap: wrap;
+    justify-content: start;
+    margin-top: 15px;
+
+    .btn {
+      margin: 0;
+    }
+  }
+}
+
+// 模型
+.group-item {
+  margin-top: 30px;
+
+  .group-item-body {
+    margin-top: 15px;
+    width: 100%;
+  }
+}
+
+.btns {
+  display: flex;
+  justify-content: center;
+  margin-top: 50px;
+}
+</style>

+ 12 - 0
src/views/ai/image/index/index.vue

@@ -18,6 +18,11 @@
           ref="stableDiffusionRef"
           @on-draw-complete="handleDrawComplete"
         />
+        <Other
+          v-if="selectPlatform === 'other'"
+          ref="otherRef"
+          @on-draw-complete="handleDrawComplete"
+        />
       </div>
     </div>
     <div class="main">
@@ -33,11 +38,13 @@ import { ImageVO } from '@/api/ai/image'
 import Dall3 from './components/dall3/index.vue'
 import Midjourney from './components/midjourney/index.vue'
 import StableDiffusion from './components/stableDiffusion/index.vue'
+import Other from './components/other/index.vue'
 
 const imageListRef = ref<any>() // image 列表 ref
 const dall3Ref = ref<any>() // dall3(openai) ref
 const midjourneyRef = ref<any>() // midjourney ref
 const stableDiffusionRef = ref<any>() // stable diffusion ref
+const otherRef = ref<any>() // stable diffusion ref
 
 // 定义属性
 const selectPlatform = ref(AiPlatformEnum.MIDJOURNEY)
@@ -53,6 +60,10 @@ const platformOptions = [
   {
     label: 'Stable Diffusion',
     value: AiPlatformEnum.STABLE_DIFFUSION
+  },
+  {
+    label: '其它',
+    value: 'other'
   }
 ]
 
@@ -77,6 +88,7 @@ const handleRegeneration = async (image: ImageVO) => {
   } else if (image.platform === AiPlatformEnum.STABLE_DIFFUSION) {
     stableDiffusionRef.value.settingValues(image)
   }
+  // TODO @fan:貌似 other 重新设置不行?
 }
 </script>
 

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

@@ -0,0 +1,62 @@
+<template>
+  <div class="gallery">
+    <div v-for="item in publicList" :key="item" class="gallery-item">
+      <img :src="item.picUrl" class="img"/>
+    </div>
+  </div>
+</template>
+<script setup lang="ts">
+import { ImageApi, ImageVO, ImageMidjourneyButtonsVO } from '@/api/ai/image'
+
+/** 属性 */
+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">
+
+.gallery {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
+  gap: 10px;
+  //max-width: 1000px;
+  padding: 20px;
+  background-color: #fff;
+  box-shadow: 0 0 10px rgba(0,0,0,0.1);
+}
+
+.gallery-item {
+  position: relative;
+  overflow: hidden;
+  background: #f0f0f0;
+  cursor: pointer;
+  transition: transform 0.3s;
+}
+
+.gallery-item img {
+  width: 100%;
+  height: auto;
+  display: block;
+  transition: transform 0.3s;
+}
+
+.gallery-item:hover img {
+  transform: scale(1.1);
+}
+
+.gallery-item:hover {
+  transform: scale(1.05);
+}
+
+</style>

+ 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: 根据信息生成音乐

+ 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

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 54 - 1
src/views/ai/utils/constants.ts


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 5
src/views/ai/utils/utils.ts


+ 49 - 35
src/views/ai/writer/components/Left.vue → src/views/ai/write/index/components/Left.vue

@@ -24,27 +24,28 @@
     </h3>
   </DefineLabel>
 
-  <!-- TODO @hhhero 小屏幕的时候是定位在左边的,大屏是分开的 -->
-  <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">
@@ -82,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>
@@ -103,15 +104,14 @@
 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 { getIntDictOptions } from '@/utils/dict'
-import { WriteExampleDataJson } from '@/views/ai/utils/utils'
-import { AiWriteTypeEnum } from "@/views/ai/utils/constants";
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { AiWriteTypeEnum, WriteExample } from '@/views/ai/utils/constants'
 
 type TabType = WriteVO['type']
 
-const message = useMessage()
+const message = useMessage() // 消息弹窗
 
 defineProps<{
   isWriting: boolean
@@ -127,15 +127,17 @@ const emits = defineEmits<{
 const example = (type: 'write' | 'reply') => {
   formData.value = {
     ...initData,
-    ...omit(WriteExampleDataJson[type], ['data'])
+    ...omit(WriteExample[type], ['data'])
   }
   emits('example', type)
 }
+
 /** 重置,将表单值作为初选值 **/
 const reset = () => {
-  formData.value = {...initData}
+  formData.value = { ...initData }
   emits('reset')
 }
+
 const selectedTab = ref<TabType>(AiWriteTypeEnum.WRITING)
 const tabs: {
   text: string
@@ -151,10 +153,12 @@ const [DefineTab, ReuseTab] = createReusableTemplate<{
 }>()
 
 /**
- * 可以在template里边定义可复用的组件,DefineLabel,ReuseLabel是采用的解构赋值,都是Vue组件
- * 直接通过组件的形式使用,<DefineLabel v-slot="{ label, hint, hintClick }">中间是需要复用的组件代码</DefineLabel>,通过<ReuseLabel />来使用定义的组件
- * DefineLabel里边的v-slot="{ label, hint, hintClick }“相当于是解构了组件的prop,需要注意的是boolean类型,需要显式的赋值比如 <ReuseLabel :flag="true" />
- * 事件也得以prop形式传入,不能是@event的形式,比如下面的hintClick需要<ReuseLabel :hintClick="() => { doSomething }"/>
+ * 可以在 template 里边定义可复用的组件,DefineLabel,ReuseLabel 是采用的解构赋值,都是 Vue 组件
+ *
+ * 直接通过组件的形式使用,<DefineLabel v-slot="{ label, hint, hintClick }"> 中间是需要复用的组件代码 <DefineLabel />,通过 <ReuseLabel /> 来使用定义的组件
+ * DefineLabel 里边的 v-slot="{ label, hint, hintClick }"相当于是解构了组件的 prop,需要注意的是 boolean 类型,需要显式的赋值比如 <ReuseLabel :flag="true" />
+ * 事件也得以 prop 形式传入,不能是 @event的形式,比如下面的 hintClick 需要<ReuseLabel :hintClick="() => { doSomething }"/>
+ *
  * @see https://vueuse.org/createReusableTemplate
  */
 const [DefineLabel, ReuseLabel] = createReusableTemplate<{
@@ -174,12 +178,22 @@ const initData: WriteVO = {
   format: 1
 }
 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] }
+  }
 }
 
+/** 提交写作 */
 const submit = () => {
   if (selectedTab.value === 2 && !formData.value.originalContent) {
     message.warning('请输入原文')
@@ -192,7 +206,7 @@ const submit = () => {
   emits('submit', {
     /** 撰写的时候没有 originalContent 字段**/
     ...(selectedTab.value === 1 ? omit(formData.value, ['originalContent']) : formData.value),
-    /** 使用选中tab值覆盖当前的type类型 **/
+    /** 使用选中 tab 值覆盖当前的 type 类型 **/
     type: selectedTab.value
   })
 }

+ 32 - 24
src/views/ai/writer/components/Right.vue → src/views/ai/write/index/components/Right.vue

@@ -1,24 +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
@@ -43,14 +38,14 @@
         />
       </div>
     </div>
-  </div>
+  </el-card>
 </template>
 
 <script setup lang="ts">
 import { useClipboard } from '@vueuse/core'
 
-const message = useMessage()
-const { copied, copy } = useClipboard()
+const message = useMessage() // 消息弹窗
+const { copied, copy } = useClipboard() // 粘贴板
 
 const props = defineProps({
   content: {
@@ -67,7 +62,7 @@ const props = defineProps({
 
 const emits = defineEmits(['update:content', 'stopStream'])
 
-// 通过计算属性,双向绑定,更改生成的内容,考虑到用户想要更改生成文章的情况
+/** 通过计算属性,双向绑定,更改生成的内容,考虑到用户想要更改生成文章的情况 */
 const compContent = computed({
   get() {
     return props.content
@@ -91,7 +86,7 @@ const copyContent = () => {
   copy(props.content)
 }
 
-// 复制成功的时候copied.value为true
+/** 复制成功的时候 copied.value 为 true */
 watch(copied, (val) => {
   if (val) {
     message.success('复制成功')
@@ -109,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>

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

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

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

@@ -1,5 +1,5 @@
 <template>
-  <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"
@@ -18,10 +18,10 @@
 </template>
 
 <script setup lang="ts">
-import Left from '../components/Left.vue'
-import Right from '../components/Right.vue'
-import * as WriteApi from '@/api/ai/writer'
-import { WriteExampleDataJson } from '@/views/ai/utils/utils'
+import Left from './components/Left.vue'
+import Right from './components/Right.vue'
+import { WriteApi, WriteVO } from '@/api/ai/write'
+import { WriteExample } from '@/views/ai/utils/constants'
 
 const message = useMessage()
 
@@ -37,7 +37,7 @@ const stopStream = () => {
 
 /** 执行写作 */
 const rightRef = ref<InstanceType<typeof Right>>()
-const submit = (data) => {
+const submit = (data: WriteVO) => {
   abortController.value = new AbortController()
   writeResult.value = ''
   isWriting.value = true
@@ -51,9 +51,9 @@ const submit = (data) => {
         return
       }
       writeResult.value = writeResult.value + data
-      nextTick(() => {
-        rightRef.value?.scrollToBottom()
-      })
+      // 滚动到底部
+      await nextTick()
+      rightRef.value?.scrollToBottom()
     },
     ctrl: abortController.value,
     onClose: stopStream,
@@ -65,8 +65,8 @@ const submit = (data) => {
 }
 
 /** 点击示例触发 */
-const handleExampleClick = (type: keyof typeof WriteExampleDataJson) => {
-  writeResult.value = WriteExampleDataJson[type].data
+const handleExampleClick = (type: keyof typeof WriteExample) => {
+  writeResult.value = WriteExample[type].data
 }
 
 /** 点击重置的时候清空写作的结果**/

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

@@ -0,0 +1,256 @@
+<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.platform" 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>
+        <!-- TODO @YunaiV  目前没有导出接口,需要导出吗 -->
+        <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"
+        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">
+        <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">
+<!--          TODO @YunaiV 目前没有修改接口,写作要可以更改吗-->
+          <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'
+import { useRouter } from 'vue-router'
+import { WriteApi, AiWritePageReqVO, AiWriteRespVo } from '@/api/ai/write'
+import * as UserApi from '@/api/system/user'
+
+/** AI 写作列表 */
+defineOptions({ name: 'AiWriteManager' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+const router = useRouter() // 路由
+
+const loading = ref(true) // 列表的加载中
+const list = ref<AiWriteRespVo[]>([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive<AiWritePageReqVO>({
+  pageNo: 1,
+  pageSize: 10,
+  userId: undefined,
+  type: undefined,
+  platform: undefined,
+  createTime: undefined
+})
+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 openForm = (type: string, id?: number) => {
+  switch (type) {
+    case 'create':
+      router.push('/ai/write')
+      break
+  }
+}
+
+/** 删除按钮操作 */
+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 - 0
types/global.d.ts

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

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно