Bladeren bron

AI一键生成

anderx 9 maanden geleden
bovenliggende
commit
249f6c8ee7

+ 6 - 0
src/client/ai.client.ts

@@ -9,3 +9,9 @@ export async function generate(projectName: string, keywords: string, chapterTit
   const resp = await httpClient.post<Response<string>>(_url("ai/generate"), params);
   return resp.data.data;
 }
+
+export async function generatePlus(projectName: string, chapterTitle: string) {
+  const params = { projectName, chapterTitle }
+  const resp = await httpClient.post<Response<string>>(_url("ai/generatePlus"), params);
+  return resp.data.data;
+}

+ 4 - 0
src/services/ai.service.ts

@@ -3,3 +3,7 @@ import * as aiClient from "@/client/ai.client";
 export async function generate(projectName: string, keywords: string, chapterTitle: string) {
   return aiClient.generate(projectName, keywords, chapterTitle)
 }
+
+export async function generatePlus(projectName: string, chapterTitle: string) {
+  return aiClient.generatePlus(projectName, chapterTitle)
+}

+ 78 - 32
src/views/report/ReportEditorView.vue

@@ -112,6 +112,17 @@ function onChapterChange(index: number, data: Chapter) {
   chapterManager.setChapter(index, data);
 }
 
+function onNewChapterChange(data: Chapter) {
+  const list = chapterManager.getChapters()
+  let index:number = 0
+  for(var i = 0; i < list.length; i++){
+    if(list[i].title==data.title){
+      index = i
+    }
+  }
+  chapterManager.setChapter(index,data);
+}
+
 function onChapterMoveUp(index: number) {
   chapterManager.moveUp(index)
 }
@@ -248,19 +259,19 @@ function duplicateCheck() {
   if (report.value) {
     extract(report.value.chapters, report.value.id +"")
   }
- if (duplicateCheckRequest?.value) {
-  reportService.duplicateCheck(duplicateCheckRequest.value).then((data: any) => {
-    let index = 0;
-    for (let item of data.repetitiveRate) {
-      if (item && item > 0) {
-        chapterEditors.value[index]?.notifyRepetitiveWarning(true);
-      } else {
-        chapterEditors.value[index]?.notifyRepetitiveWarning(false);
+  if (duplicateCheckRequest?.value) {
+    reportService.duplicateCheck(duplicateCheckRequest.value).then((data: any) => {
+      let index = 0;
+      for (let item of data.repetitiveRate) {
+        if (item && item > 0) {
+          chapterEditors.value[index]?.notifyRepetitiveWarning(true);
+        } else {
+          chapterEditors.value[index]?.notifyRepetitiveWarning(false);
+        }
+        index += 1;
       }
-      index += 1;
-    }
-  })
- }
+    })
+  }
 }
 
 function extract(chapters: ReportChapterRequest[], id: string) {
@@ -269,7 +280,7 @@ function extract(chapters: ReportChapterRequest[], id: string) {
     duplicateCheckRequest.value?.contents.push(chapters[i].content)
   }
   if (duplicateCheckRequest?.value) {
-      duplicateCheckRequest.value.reportId = id;
+    duplicateCheckRequest.value.reportId = id;
   }
 }
 
@@ -305,42 +316,74 @@ onUnmounted(() => {
   <a-row :gutter="24" v-if="report">
     <a-col :span="12">
       <div class="metadata-wrap">
-        <RelationCompanyInput v-model:id="report.companyId" @change="onCompanyChange" ref="relationCompanyEl" />
-        <ReportField name="报告类型" v-model="report.name" :placeholder="placeholders[0]" ref="reportCategoryEl" />
-        <ReportField name="项目名称" v-model="report.reportName" :placeholder="placeholders[1]" ref="reportNameEl"/>
-        <ReportField name="项目负责人" v-model="report.supervisor" :placeholder="placeholders[2]" ref="supervisorEl" />
-        <ReportField name="关键词" v-model="report.keywords" :placeholder="placeholders[3]" ref="keywordsEl" />
+        <RelationCompanyInput
+            v-model:id="report.companyId"
+            @change="onCompanyChange"
+            ref="relationCompanyEl"
+        />
+        <ReportField
+            name="报告类型"
+            v-model="report.name"
+            :placeholder="placeholders[0]"
+            ref="reportCategoryEl"
+        />
+        <ReportField
+            name="项目名称"
+            v-model="report.reportName"
+            :placeholder="placeholders[1]"
+            ref="reportNameEl"
+        />
+        <ReportField
+            name="项目负责人"
+            v-model="report.supervisor"
+            :placeholder="placeholders[2]"
+            ref="supervisorEl"
+        />
+        <ReportField
+            name="关键词"
+            v-model="report.keywords"
+            :placeholder="placeholders[3]"
+            ref="keywordsEl"
+        />
         <DynamicMeta v-model:data="report.metadata" @change="onMetaChange" />
       </div>
     </a-col>
     <a-col :span="12">
-      <LogoComponent @change="onLogoChange" @delete="onLogoDelete" :url="logoUrl" />
+      <LogoComponent
+          :title="report.reportName"
+          :list="chapterManager.getChapters()"
+          @change="onLogoChange"
+          @delete="onLogoDelete"
+          @ok="onChapterTypeSelected"
+          @one-touch="onNewChapterChange($event)"
+          :url="logoUrl"
+      />
     </a-col>
   </a-row>
   <a-divider />
   <div class="chapter-wrap" v-if="report">
     <ChapterEditor
-      v-for="(item, index) in chapterManager.getChapters()"
-      :key="index"
-      :title="report.reportName"
-      :data="item"
-      ref="chapterEditors"
-      :reportKeywords="report.keywords"
-      @change="onChapterChange(index, $event)"
-      @move-up="onChapterMoveUp(index)"
-      @move-down="onChapterMoveDown(index)"
-      @remove="onChapterRemove(index)"
+        v-for="(item, index) in chapterManager.getChapters()"
+        :key="index"
+        :title="report.reportName"
+        :data="item"
+        ref="chapterEditors"
+        :reportKeywords="report.keywords"
+        @change="onChapterChange(index, $event)"
+        @move-up="onChapterMoveUp(index)"
+        @move-down="onChapterMoveDown(index)"
+        @remove="onChapterRemove(index)"
     />
   </div>
   <div class="add-chapter-wrap">
-    <a-button @click="() => chapterTypeSelectVisible = true">
+    <a-button @click="() => (chapterTypeSelectVisible = true)">
       <plus-outlined />
       添加章节
     </a-button>
   </div>
   <div class="bottom-button-group">
     <a-space v-if="report">
-      <a-checkbox @change="updateStatus()"  :checked="reportStatus == 1">
+      <a-checkbox @change="updateStatus()" :checked="reportStatus == 1">
         <span v-if="reportStatus == 0">未完成</span>
         <span v-if="reportStatus == 1">已完成</span>
       </a-checkbox>
@@ -351,7 +394,10 @@ onUnmounted(() => {
       <a-button @click="duplicateCheck">查重</a-button>
     </a-space>
   </div>
-  <ChapterTypeSelect @ok="onChapterTypeSelected" v-model:visible="chapterTypeSelectVisible" />
+  <ChapterTypeSelect
+      @ok="onChapterTypeSelected"
+      v-model:visible="chapterTypeSelectVisible"
+  />
   <TemplateSaveModal ref="templateSaveModal" @ok="onSaveTemplate" />
 </template>
 

+ 10 - 10
src/views/report/components/ChapterTypeSelect.vue

@@ -7,7 +7,7 @@ defineProps({
     type: Boolean,
     required: true,
     default: false,
-  }
+  },
 });
 
 const emits = defineEmits<{
@@ -17,16 +17,16 @@ const emits = defineEmits<{
 
 const categoryOptions = ChapterTypeLabels.map((item) => ({
   label: item.label,
-  value: item.key
-}))
+  value: item.key,
+}));
 
 const checkedCategories = ref<ChapterType[]>([]);
 
 function onOk() {
   let values: ChapterType[] = [];
   for (let item of categoryOptions) {
-    if (checkedCategories.value.find(x => x == item.value)) {
-      values.push(item.value)
+    if (checkedCategories.value.find((x) => x == item.value)) {
+      values.push(item.value);
     }
   }
   emits("ok", values);
@@ -46,11 +46,11 @@ function reset() {
 
 <template>
   <a-modal
-    :visible="visible"
-    title="选择章节类型"
-    :ok-button-props="{ disabled: checkedCategories.length == 0 }"
-    @ok="onOk"
-    @cancel="onCancel"
+      :visible="visible"
+      title="选择章节类型"
+      :ok-button-props="{ disabled: checkedCategories.length == 0 }"
+      @ok="onOk"
+      @cancel="onCancel"
   >
     <a-checkbox-group v-model:value="checkedCategories" name="checkboxgroup">
       <div v-for="(item, index) in categoryOptions" :key="index" class="category-item">

+ 171 - 34
src/views/report/components/LogoComponent.vue

@@ -1,58 +1,87 @@
 <script setup lang="ts">
-import { ref } from 'vue';
-import { PlusOutlined, LoadingOutlined, DeleteOutlined } from '@ant-design/icons-vue';
-import { message } from 'ant-design-vue';
-import type { UploadChangeParam } from 'ant-design-vue';
+import { ChapterType, ChapterTypeLabels } from "@/types/report.types";
+import { ref } from "vue";
+import type { PropType } from "vue";
+import { PlusOutlined, LoadingOutlined, DeleteOutlined } from "@ant-design/icons-vue";
+import * as aiService from "@/services/ai.service";
+import { marked } from "marked";
+import { message } from "ant-design-vue";
+import type { UploadChangeParam } from "ant-design-vue";
+import { Item } from "ant-design-vue/lib/menu";
+import { iteratee } from "lodash";
+import type { Chapter } from "@/types/report.types";
 
-defineProps({
+const props = defineProps({
   url: {
     type: String,
     required: false,
-  }
-})
+  },
+  title: {
+    type: String,
+    required: true,
+  },
+  list: {
+    type: Array as PropType<Chapter[]>,
+    default() {
+      return [];
+    },
+  },
+});
 
 const emits = defineEmits<{
   (e: "change", file: File): void;
   (e: "delete"): void;
+  (e: "ok", vaule: ChapterType[]): void;
+  (e: "one-touch", data: Chapter): void;
 }>();
 
+const categoryOptions = ChapterTypeLabels.map((item) => ({
+  label: item.label,
+  value: item.key,
+}));
+
 function getBase64(img: Blob, callback: (base64Url: string) => void) {
   const reader = new FileReader();
-  reader.addEventListener('load', () => callback(reader.result as string));
+  reader.addEventListener("load", () => callback(reader.result as string));
   reader.readAsDataURL(img);
 }
 
 const fileList = ref([]);
 const loading = ref<boolean>(false);
-const imageUrl = ref<string>('');
+const imageUrl = ref<string>("");
+const checkedCategories = ref<ChapterType[]>([
+  ChapterType.BACKGROUND,
+  ChapterType.RESEARCH_STATUS,
+  ChapterType.RESEARCH,
+]);
 
 const handleChange = (info: UploadChangeParam) => {
-  if (info.file.status === 'uploading') {
+  if (info.file.status === "uploading") {
     loading.value = true;
     return;
   }
-  if (info.file.status === 'done') {
+  if (info.file.status === "done") {
     // Get this url from response in real world.
     getBase64(info.file.originFileObj as Blob, (base64Url: string) => {
       imageUrl.value = base64Url;
       loading.value = false;
     });
   }
-  if (info.file.status === 'error') {
+  if (info.file.status === "error") {
     loading.value = false;
-    message.error('upload error');
+    message.error("upload error");
   }
 };
 
 const beforeUpload = (file: File) => {
-  const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
+  const isJpgOrPng = file.type === "image/jpeg" || file.type === "image/png";
   if (!isJpgOrPng) {
-    message.error('仅支持JPG和PNG格式的图片');
+    message.error("仅支持JPG和PNG格式的图片");
   }
 
   const isLt2M = file.size / 1024 / 1024 < 2;
   if (!isLt2M) {
-    message.error('仅支持小于2MB的图片');
+    message.error("仅支持小于2MB的图片");
   }
   if (!isJpgOrPng || !isLt2M) {
     return false;
@@ -64,7 +93,105 @@ const beforeUpload = (file: File) => {
 };
 
 function onDelete() {
-  emits("delete")
+  emits("delete");
+}
+
+function onContentChange(data: any) {
+  emits("one-touch", data);
+}
+
+function test1() {
+  let values: ChapterType[] = [];
+  let chapterTitle: string = "";
+  if(!props.title){
+    message.warning("项目名称不能为空")
+    return
+  }
+  if (props.list.length == 0) {
+    for (let item of categoryOptions) {
+      if (checkedCategories.value.find((x) => x == item.value)) {
+        values.push(item.value);
+      }
+    }
+    emits("ok", values);
+  } else {
+    let arr = [];
+    let dataList = props.list;
+    for (let item of dataList) {
+      if(item.title.includes(',')){
+        message.warning("章节类型中不能存在英文逗号!!!")
+        return
+      }
+      arr.push(item.title);
+    }
+    chapterTitle = arr.toString();
+  }
+  const loadingHandler = message.loading({
+    content: "AI正在撰写章节内容,请稍等……",
+    duration: 0,
+  });
+  aiService
+      .generatePlus(props.title, chapterTitle)
+      .then((resp) => {
+        // return;
+        const obj: any = resp;
+        if (props.list.length == 0) {
+          let list: any = [
+            {
+              title: "立项背景与意义",
+              fullTitle: "",
+              content: "",
+              chapterType: "BACKGROUND",
+              keywords: [],
+            },
+            {
+              title: "国内外研究现状与发展趋势",
+              fullTitle: "",
+              content: "",
+              chapterType: "RESEARCH_STATUS",
+              keywords: [],
+            },
+            {
+              title: "项目主要研究内容",
+              fullTitle: "",
+              content: "",
+              chapterType: "RESEARCH",
+              keywords: [],
+            },
+          ];
+          for (let item of list) {
+            const key: string = item.title;
+            onContentChange({
+              title: "",
+              fullTitle: "",
+              content: marked.parse(obj[key]),
+              chapterType: item.chapterType,
+              keywords: [],
+            });
+          }
+        } else {
+          for (let item of props.list) {
+            const key: string = item.title;
+            onContentChange({
+              title: item.title,
+              fullTitle: item.fullTitle,
+              content: marked.parse(obj[key]),
+              chapterType: item.chapterType,
+              keywords: item.keywords,
+            });
+          }
+        }
+        // onContentChange(marked.parse(resp));
+        if (loadingHandler) {
+          loadingHandler();
+        }
+      })
+      .catch((err) => {
+        console.log("err", err);
+        if (loadingHandler) {
+          loadingHandler();
+        }
+      });
 }
 </script>
 
@@ -72,14 +199,14 @@ function onDelete() {
   <a-row>
     <a-col>
       <a-upload
-        v-model:file-list="fileList"
-        name="avatar"
-        list-type="picture-card"
-        class="logo-uploader"
-        :show-upload-list="false"
-        action="/"
-        :before-upload="beforeUpload"
-        @change="handleChange"
+          v-model:file-list="fileList"
+          name="avatar"
+          list-type="picture-card"
+          class="logo-uploader"
+          :show-upload-list="false"
+          action="/"
+          :before-upload="beforeUpload"
+          @change="handleChange"
       >
         <img width="104" v-if="url" :src="url" alt="企业Logo" />
         <div v-else>
@@ -91,16 +218,14 @@ function onDelete() {
     </a-col>
     <a-col class="description-wrap">
       <div class="description">
-        <div style="flex: 1;">
-          请按需上传企业logo<br />仅支持 jpg.  png.  格式图片
-        </div>
+        <div style="flex: 1">请按需上传企业logo<br />仅支持 jpg. png. 格式图片</div>
         <div>
           <a-popconfirm
-            title="确定删除此Logoo?"
-            ok-text="确定"
-            cancel-text="取消"
-            @confirm="onDelete"
-            placement="topRight"
+              title="确定删除此Logoo?"
+              ok-text="确定"
+              cancel-text="取消"
+              @confirm="onDelete"
+              placement="topRight"
           >
             <a-button :disabled="!url"><delete-outlined /></a-button>
           </a-popconfirm>
@@ -108,6 +233,13 @@ function onDelete() {
       </div>
     </a-col>
   </a-row>
+  <a-row>
+    <a-col>
+      <a-button class="generate-project" type="primary" @click="test1">
+        一键生成
+      </a-button>
+    </a-col>
+  </a-row>
 </template>
 
 <style scoped lang="scss">
@@ -129,4 +261,9 @@ function onDelete() {
     align-content: space-between;
   }
 }
-</style>
+
+.generate-project {
+  margin: 43px 0 0 20px;
+  background: green;
+}
+</style>