Переглянути джерело

!477 [代码优化]AI: 写作添加注释,增加可读性,调整代码,方便后续调整,添加预览header,调整生成内容区域布局,将右边铺满
Merge pull request !477 from hhhero/dev

芋道源码 9 місяців тому
батько
коміт
e53786e8bd

+ 9 - 32
src/api/ai/writer/index.ts

@@ -3,37 +3,14 @@ import { fetchEventSource } from '@microsoft/fetch-event-source'
 import { getAccessToken } from '@/utils/auth'
 import { config } from '@/config/axios/config'
 
-// TODO @hhhero:可以改成 WriteVO 哈,主要是保持一致
-export interface WriteParams {
-  // TODO @hhhero:注释。每个属性的后面哈。会更简洁一点
-  /**
-   * 1:撰写 2:回复
-   */
-  type: 1 | 2
-  /**
-   * 写作内容提示 1。撰写 2回复
-   */
-  prompt: string
-  /**
-   *  原文
-   */
-  originalContent: string
-  /**
-   * 长度
-   */
-  length: number
-  /**
-   * 格式
-   */
-  format: number
-  /**
-   * 语气
-   */
-  tone: number
-  /**
-   * 语言
-   */
-  language: number
+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 = ({
@@ -43,7 +20,7 @@ export const writeStream = ({
   onError,
   ctrl
 }: {
-  data: WriteParams
+  data: WriteVO
   onMessage?: (res: any) => void
   onError?: (...args: any[]) => void
   onClose?: (...args: any[]) => void

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

@@ -41,6 +41,14 @@ export const AiMusicStatusEnum = {
   FAIL: 30 // 已失败
 }
 
+/**
+ * AI 写作类型的枚举
+ */
+export enum AiWriteTypeEnum {
+  WRITING = 1, // 撰写
+  REPLY // 回复
+}
+
 // ========== 【图片 UI】相关的枚举 ==========
 export const ImageHotWords = [
   '中国旗袍',

Різницю між файлами не показано, бо вона завелика
+ 5 - 0
src/views/ai/utils/utils.ts


+ 50 - 52
src/views/ai/writer/components/Left.vue

@@ -24,7 +24,7 @@
     </h3>
   </DefineLabel>
 
-  <!-- TODO 小屏幕的时候是定位在左边的,大屏是分开的 -->
+  <!-- TODO @hhhero 小屏幕的时候是定位在左边的,大屏是分开的 -->
   <div class="relative" v-bind="$attrs">
     <!-- tab -->
     <div
@@ -32,7 +32,7 @@
     >
       <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 === 2 && 'after:transform after:translate-x-[100%]'"
+        :class="selectedTab === AiWriteTypeEnum.REPLY && 'after:transform after:translate-x-[100%]'"
       >
         <ReuseTab
           v-for="tab in tabs"
@@ -53,7 +53,7 @@
             type="textarea"
             :rows="5"
             :maxlength="500"
-            v-model="writeForm.prompt"
+            v-model="formData.prompt"
             placeholder="请输入写作内容"
             showWordLimit
           />
@@ -65,7 +65,7 @@
             type="textarea"
             :rows="5"
             :maxlength="500"
-            v-model="writeForm.originalContent"
+            v-model="formData.originalContent"
             placeholder="请输入原文"
             showWordLimit
           />
@@ -75,23 +75,23 @@
             type="textarea"
             :rows="5"
             :maxlength="500"
-            v-model="writeForm.prompt"
+            v-model="formData.prompt"
             placeholder="请输入回复内容"
             showWordLimit
           />
         </template>
 
         <ReuseLabel label="长度" />
-        <Tag v-model="writeForm.length" :tags="writeTags.lenTags" />
+        <Tag v-model="formData.length" :tags="getIntDictOptions('ai_write_length')" />
         <ReuseLabel label="格式" />
-        <Tag v-model="writeForm.format" :tags="writeTags.formatTags" />
+        <Tag v-model="formData.format" :tags="getIntDictOptions('ai_write_format')" />
         <ReuseLabel label="语气" />
-        <Tag v-model="writeForm.tone" :tags="writeTags.toneTags" />
+        <Tag v-model="formData.tone" :tags="getIntDictOptions('ai_write_tone')" />
         <ReuseLabel label="语言" />
-        <Tag v-model="writeForm.language" :tags="writeTags.langTags" />
+        <Tag v-model="formData.language" :tags="getIntDictOptions('ai_write_language')" />
 
         <div class="flex items-center justify-center mt-3">
-          <el-button :disabled="isWriting">重置</el-button>
+          <el-button :disabled="isWriting" @click="reset">重置</el-button>
           <el-button :loading="isWriting" @click="submit" color="#846af7">生成</el-button>
         </div>
       </div>
@@ -103,12 +103,13 @@
 import { createReusableTemplate } from '@vueuse/core'
 import { ref } from 'vue'
 import Tag from './Tag.vue'
-import { WriteParams } from '@/api/ai/writer'
+import { WriteVO } from '@/api/ai/writer'
 import { omit } from 'lodash-es'
 import { getIntDictOptions } from '@/utils/dict'
-import dataJson from '../data.json'
+import { WriteExampleDataJson } from '@/views/ai/utils/utils'
+import { AiWriteTypeEnum } from "@/views/ai/utils/constants";
 
-type TabType = WriteParams['type']
+type TabType = WriteVO['type']
 
 const message = useMessage()
 
@@ -117,25 +118,31 @@ defineProps<{
 }>()
 
 const emits = defineEmits<{
-  (e: 'submit', params: Partial<WriteParams>)
+  (e: 'submit', params: Partial<WriteVO>)
   (e: 'example', param: 'write' | 'reply')
+  (e: 'reset')
 }>()
 
+/** 点击示例的时候,将定义好的文章作为示例展示出来 **/
 const example = (type: 'write' | 'reply') => {
-  writeForm.value = {
+  formData.value = {
     ...initData,
-    ...omit(dataJson[type], ['data'])
+    ...omit(WriteExampleDataJson[type], ['data'])
   }
   emits('example', type)
 }
-
-const selectedTab = ref<TabType>(1)
+/** 重置,将表单值作为初选值 **/
+const reset = () => {
+  formData.value = {...initData}
+  emits('reset')
+}
+const selectedTab = ref<TabType>(AiWriteTypeEnum.WRITING)
 const tabs: {
   text: string
   value: TabType
 }[] = [
-  { text: '撰写', value: 1 }, // TODO @hhhero:1、2 这个枚举到 constants 里。方便后续万一要调整
-  { text: '回复', value: 2 }
+  { text: '撰写', value: AiWriteTypeEnum.WRITING },
+  { text: '回复', value: AiWriteTypeEnum.REPLY }
 ]
 const [DefineTab, ReuseTab] = createReusableTemplate<{
   active?: boolean
@@ -143,7 +150,21 @@ const [DefineTab, ReuseTab] = createReusableTemplate<{
   itemClick: () => void
 }>()
 
-const initData: WriteParams = {
+/**
+ * 可以在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<{
+  label: string
+  class?: string
+  hint?: string
+  hintClick?: () => void
+}>()
+
+const initData: WriteVO = {
   type: 1,
   prompt: '',
   originalContent: '',
@@ -152,49 +173,26 @@ const initData: WriteParams = {
   length: 1,
   format: 1
 }
-// TODO @hhhero:这个字段,要不叫 formData,和其他模块保持一致。然后 initData 和它也更好对应上
-const writeForm = ref<WriteParams>({ ...initData })
-
-// TODO @hhhero:这种一次性的变量,要不直接 vue template 直接调用。目的是:让 ts 这块,更专注逻辑哈。
-const writeTags = {
-  // 长度 TODO @hhhero:注释放在和面哈;
-  // TODO @hhhero:一般 length 不用缩写哈。更完整会更容易阅读;
-  lenTags: getIntDictOptions('ai_write_length'),
-  // 格式
-
-  formatTags: getIntDictOptions('ai_write_format'),
-  // 语气
-
-  toneTags: getIntDictOptions('ai_write_tone'),
-  // 语言
-  langTags: getIntDictOptions('ai_write_language')
-  //
-}
-
-// TODO @hhhero:这个写法不错。要不写个简单的注释,我怕很多人不懂哈。
-const [DefineLabel, ReuseLabel] = createReusableTemplate<{
-  label: string
-  class?: string
-  hint?: string
-  hintClick?: () => void
-}>()
-
+const formData = ref<WriteVO>({ ...initData })
+/** 切换tab **/
 const switchTab = (value: TabType) => {
   selectedTab.value = value
-  writeForm.value = { ...initData }
+  formData.value = { ...initData }
 }
 
 const submit = () => {
-  if (selectedTab.value === 2 && !writeForm.value.originalContent) {
+  if (selectedTab.value === 2 && !formData.value.originalContent) {
     message.warning('请输入原文')
     return
   }
-  if (!writeForm.value.prompt) {
+  if (!formData.value.prompt) {
     message.warning(`请输入${selectedTab.value === 1 ? '写作' : '回复'}内容`)
     return
   }
   emits('submit', {
-    ...(selectedTab.value === 1 ? omit(writeForm.value, ['originalContent']) : writeForm.value),
+    /** 撰写的时候没有 originalContent 字段**/
+    ...(selectedTab.value === 1 ? omit(formData.value, ['originalContent']) : formData.value),
+    /** 使用选中tab值覆盖当前的type类型 **/
     type: selectedTab.value
   })
 }

+ 51 - 25
src/views/ai/writer/components/Right.vue

@@ -1,29 +1,41 @@
 <template>
-  <div class="h-full box-border py-6 px-7">
-    <div class="w-full h-full relative bg-white box-border p-3 sm:p-16 pr-0">
+  <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="copyMsg"
-        class="absolute top-2 right-2 copy-btn"
-        :data-clipboard-target="inputId"
+        @click="copyContent"
+        size="small"
       >
+        <template #icon>
+          <Icon icon="ph:copy-bold" />
+        </template>
         复制
       </el-button>
-      <!-- 展示在下面中间的位置 -->
-      <el-button
-        v-show="isWriting"
-        class="absolute bottom-2 left-1/2 -translate-x-1/2"
-        @click="emits('stopStream')"
-      >
-        终止生成
-      </el-button>
-      <div ref="contentRef" class="w-full h-full pr-3 sm:pr-16 overflow-y-auto">
+
+
+    </h3>
+
+    <div ref="contentRef" class="hide-scroll-bar flex-grow 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
+          v-show="isWriting"
+          class="absolute bottom-2 sm:bottom-5 left-1/2 -translate-x-1/2 z-36"
+          @click="emits('stopStream')"
+          size="small"
+        >
+          <template #icon>
+            <Icon icon="material-symbols:stop" />
+          </template>
+          终止生成
+        </el-button>
         <el-input
           id="inputId"
           type="textarea"
-          v-model="compMsg"
+          v-model="compContent"
           autosize
           :input-style="{ boxShadow: 'none' }"
           resize="none"
@@ -41,25 +53,27 @@ const message = useMessage()
 const { copied, copy } = useClipboard()
 
 const props = defineProps({
-  msg: {
+  content: {
+    // 生成的结果
     type: String,
     default: ''
   },
   isWriting: {
+    // 是否正在生成文章
     type: Boolean,
     default: false
   }
 })
 
-const emits = defineEmits(['update:msg', 'stopStream'])
+const emits = defineEmits(['update:content', 'stopStream'])
 
-// TODO @hhhero:是不是 Msg 改成 Content 这种哈。或者 Message。
-const compMsg = computed({
+// 通过计算属性,双向绑定,更改生成的内容,考虑到用户想要更改生成文章的情况
+const compContent = computed({
   get() {
-    return props.msg
+    return props.content
   },
   set(val) {
-    emits('update:msg', val)
+    emits('update:content', val)
   }
 })
 
@@ -72,15 +86,27 @@ defineExpose({
 })
 
 /** 点击复制的时候复制内容 */
-const showCopy = computed(() => props.msg && !props.isWriting) // 是否展示拷贝
-const inputId = computed(() => getCurrentInstance()?.uid) // TODO @hhhero:这个可以写个注释哈
-const copyMsg = () => {
-  copy(props.msg)
+const showCopy = computed(() => props.content && !props.isWriting) // 是否展示复制按钮,在生成内容完成的时候展示
+const copyContent = () => {
+  copy(props.content)
 }
 
+// 复制成功的时候copied.value为true
 watch(copied, (val) => {
   if (val) {
     message.success('复制成功')
   }
 })
 </script>
+
+<style lang="scss" scoped>
+.hide-scroll-bar {
+  -ms-overflow-style: none;
+  scrollbar-width: none;
+
+  &::-webkit-scrollbar {
+    width: 0;
+    height: 0;
+  }
+}
+</style>

Різницю між файлами не показано, бо вона завелика
+ 0 - 3
src/views/ai/writer/data.json


+ 25 - 16
src/views/ai/writer/index.vue → src/views/ai/writer/index/index.vue

@@ -1,26 +1,27 @@
-<!-- TODO @hhhero:挪到 write/index/index.vue 里。因为后续会有 write/manager/index.vue 管理内容 -->
 <template>
   <div class="h-[calc(100vh-var(--top-tool-height)-var(--app-footer-height)-40px)] -m-5 flex">
-    <Left :is-writing="isWriting" class="h-full" @submit="submit" @example="handleExampleClick" />
-    <!-- TODO @hhhero:顶部应该有个预览的 header -->
-    <!-- TODO @hhhero:整个 Right 组件的框,没铺满的感觉? -->
+    <Left
+      :is-writing="isWriting"
+      class="h-full"
+      @submit="submit"
+      @reset="reset"
+      @example="handleExampleClick"
+    />
     <Right
       :is-writing="isWriting"
       @stop-stream="stopStream"
       ref="rightRef"
       class="flex-grow"
-      v-model:msg="writeResult"
+      v-model:content="writeResult"
     />
   </div>
 </template>
 
 <script setup lang="ts">
-import Left from './components/Left.vue'
-import Right from './components/Right.vue'
-// TODO @hhhero:搞成 WriteApi 哈
-import { writeStream } from '@/api/ai/writer'
-// TODO @hhhero:dataJson 放到 ai/utils/utils.ts
-import dataJson from './data.json'
+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'
 
 const message = useMessage()
 
@@ -36,11 +37,11 @@ const stopStream = () => {
 
 /** 执行写作 */
 const rightRef = ref<InstanceType<typeof Right>>()
-const submit = async (data) => {
+const submit = (data) => {
   abortController.value = new AbortController()
   writeResult.value = ''
   isWriting.value = true
-  await writeStream({
+  WriteApi.writeStream({
     data,
     onMessage: async (res) => {
       const { code, data, msg } = JSON.parse(res.data)
@@ -56,12 +57,20 @@ const submit = async (data) => {
     },
     ctrl: abortController.value,
     onClose: stopStream,
-    onError: stopStream // TODO @hhhero: error 的时候,是不是要打印下错误哈
+    onError: (...err) => {
+      console.error('写作异常', ...err)
+      stopStream()
+    }
   })
 }
 
 /** 点击示例触发 */
-const handleExampleClick = (type: keyof typeof dataJson) => {
-  writeResult.value = dataJson[type].data
+const handleExampleClick = (type: keyof typeof WriteExampleDataJson) => {
+  writeResult.value = WriteExampleDataJson[type].data
+}
+
+/** 点击重置的时候清空写作的结果**/
+const reset = () => {
+  writeResult.value = ''
 }
 </script>

Деякі файли не було показано, через те що забагато файлів було змінено