瀏覽代碼

完善报告编辑页面

Kevin Jiang 2 年之前
父節點
當前提交
a7307c512a

+ 11 - 0
src/client/chapterSearch.client.ts

@@ -0,0 +1,11 @@
+import httpClient from "@/services/httpClient";
+import type { Response } from "@/types/response.types"
+import type { ChapterSearchReqeust, ChapterSearchResponse } from "@/types/search.types";
+import * as urlHelper from "@/helpers/url.helper"
+
+const _url = urlHelper.withPrefix("/gw/chapter/search")
+
+export async function search(request: ChapterSearchReqeust): Promise<ChapterSearchResponse> {
+  const resp = await httpClient.post<Response<ChapterSearchResponse>>(_url(), request);
+  return resp.data.data;
+}

+ 4 - 8
src/client/report.client.ts

@@ -3,11 +3,7 @@ import type { ReportSaveRequest } from "@/types/report.types";
 import type { Response } from "@/types/response.types";
 import * as urlHelper from "@/helpers/url.helper"
 
-// function _url(path: string) {
-//   return `/gw/user${path}`;
-// }
-
-const _url = urlHelper.withPrefix('/gw/user');
+const _url = urlHelper.withPrefix('/gw/user/report');
 
 /**
  * 通过报告ID获取报告数据
@@ -15,7 +11,7 @@ const _url = urlHelper.withPrefix('/gw/user');
  * @returns Promise
  */
 export async function get(id: number): Promise<ReportSaveRequest> {
-  const resp = await httpClient.get<Response<ReportSaveRequest>>(_url(`/report/${id}`))
+  const resp = await httpClient.get<Response<ReportSaveRequest>>(_url(`/${id}`))
   return resp.data.data;
 }
 
@@ -23,11 +19,11 @@ export async function get(id: number): Promise<ReportSaveRequest> {
  * 请求后端服务创建一个新报告
  */
 export async function create(): Promise<number> {
-  const resp = await httpClient.put<Response<number>>(_url("/report"));
+  const resp = await httpClient.put<Response<number>>(_url());
   return resp.data.data;
 }
 
 export async function save(reportSaveRequest: ReportSaveRequest) {
-  const resp = await httpClient.post<Response<ReportSaveRequest>>(_url("/report"), reportSaveRequest);
+  const resp = await httpClient.post<Response<ReportSaveRequest>>(_url(), reportSaveRequest);
   return resp.data.data;
 }

+ 1 - 6
src/helpers/log.helper.ts

@@ -1,14 +1,9 @@
-import { getCurrentInstance } from "vue";
 
 export class CompLog {
   /**
-   * 输出错误信息,带上组件的名称
+   * 输出错误信息,带上tag信息
    * @param args 输出数据
    */
-  public static logError(...args: any) {
-    console.error(getCurrentInstance()?.type.__name, ...args);
-  }
-
   public static logErr(tag: string) {
     return (...args: any) => {
       console.error(tag, ...args);

+ 41 - 4
src/models/report.model.ts

@@ -1,4 +1,4 @@
-import { ChapterTypeLabels, type Chapter, type ChapterType } from "@/types/report.types";
+import { ChapterTypeKeywords, ChapterTypeLabels, type Chapter, type ChapterType } from "@/types/report.types";
 import Nzh from "nzh";
 
 /**
@@ -13,10 +13,14 @@ export class ChapterManager {
 
   public addChapter(chapterType: ChapterType) {
     const item = ChapterTypeLabels.find((item) => item.key == chapterType);
+    const keywordsItem = ChapterTypeKeywords.find((item) => item.key == chapterType);
     if (item) {
       this.chapters.push({
         title: item.label,
+        fullTitle: this.getTitleWithChineseNum(this.chapters.length, item.label),
         content: '',
+        chapterType: chapterType,
+        keywords: keywordsItem?.keywords || [],
       });
     }
   }
@@ -25,8 +29,17 @@ export class ChapterManager {
     return this.chapters[index];
   }
 
+  private getTitleWithChineseNum(index: number, title: string) {
+    return Nzh.cn.encodeS(index+1) + '、' + title
+  }
+
   public setChapter(index: number, data: Chapter) {
-    this.chapters[index] = data;
+    const chapter = this.chapters[index]
+    this.chapters[index] = {
+      ...chapter,
+      ...data,
+      fullTitle: this.getTitleWithChineseNum(index, chapter.title)
+    };
   }
 
   public setChapters(chapters: Chapter[]) {
@@ -35,8 +48,32 @@ export class ChapterManager {
 
   public getChapters(): Chapter[] {
     return this.chapters.map((chapter, index) => {
-      const fullTitle = Nzh.cn.encodeS(index+1) + '、' + chapter.title;
+      const fullTitle = this.getTitleWithChineseNum(index, chapter.title)
       return { ...chapter, fullTitle };
     });
   }
-}
+
+  public moveUp(index: number) {
+    if (index < 1) {
+      return;
+    }
+    const temp = this.chapters[index-1];
+    this.chapters[index-1] = this.chapters[index]
+    this.chapters[index] = temp
+  }
+
+  public moveDown(index: number) {
+    if (index >= this.chapters.length-1) {
+      return;
+    }
+    const temp = this.chapters[index+1];
+    this.chapters[index+1] = this.chapters[index]
+    this.chapters[index] = temp
+  }
+
+  public remove(index: number) {
+    if (index >= 0 && index < this.chapters.length) {
+      this.chapters.splice(index, 1)
+    }
+  }
+}

+ 6 - 0
src/services/chapterSearch.service.ts

@@ -0,0 +1,6 @@
+import * as chapterSearchClient from "@/client/chapterSearch.client"
+import type { ChapterSearchReqeust, ChapterSearchResponse } from "@/types/search.types"
+
+export async function search(request: ChapterSearchReqeust): Promise<ChapterSearchResponse> {
+  return chapterSearchClient.search(request);
+}

+ 34 - 0
src/types/report.types.ts

@@ -38,6 +38,38 @@ export const ChapterTypeLabels = [
   },
 ]
 
+export const ChapterTypeKeywords = [
+  {
+    key: ChapterType.BACKGROUND,
+    keywords: ['背景', '意义'],
+  },
+  {
+    key: ChapterType.RESEARCH_STATUS,
+    keywords: ['研究现状', '发展趋势'],
+  },
+  {
+    key: ChapterType.RESEARCH,
+    keywords: ['研究内容', '实施内容'],
+  },
+  {
+    key: ChapterType.INNOVATION,
+    keywords: ['关键技术', '创新点', '解决的问题', '解决的关键问题'],
+  },
+  {
+    key: ChapterType.SOLUTION,
+    keywords: ['实施方案'],
+  },
+  {
+    key: ChapterType.USER_DEFINED,
+    keywords: []
+  },
+]
+
+export interface SelectedChapterType {
+  type: ChapterType;
+  keywords: string[];
+}
+
 export interface ReportMetadataItem {
   name: string;
   value: string;
@@ -48,6 +80,8 @@ export interface Chapter {
   title: string;
   fullTitle?: string;
   content: string;
+  chapterType: ChapterType;
+  keywords: string[]
 }
 
 export interface ReportChapterRequest extends Chapter {

+ 22 - 0
src/types/search.types.ts

@@ -30,3 +30,25 @@ export interface Pagination {
 export type Sort = 'Relevance' | 'DateDesc' | 'DateAsc';
 
 export type ResultMode = 'brief' | 'detail';
+
+export interface ChapterSearchReqeust {
+  from: number;
+  size: number;
+  keywords: string[];
+}
+
+export interface ChapterSearchDocResponse {
+  id: string;
+  score: number;
+  filename: string;
+  section: string;
+  parentSearch: string;
+  content: string;
+  page: number;
+  orderNum: number;
+}
+
+export interface ChapterSearchResponse {
+  total: number;
+  items: ChapterSearchDocResponse[];
+}

+ 26 - 12
src/views/report/ReportEditorView.vue

@@ -18,6 +18,9 @@ import { message } from "ant-design-vue";
 import TemplateSaveModal from "./components/TemplateSaveModal.vue";
 import type { ReportTemplateModal } from "./reportComponent.types";
 
+const _componentName = 'ReportEditorView'
+const logError = CompLog.logErr(_componentName)
+
 const route = useRoute();
 
 const report: Ref<ReportSaveRequest | null> = ref(null);
@@ -28,7 +31,7 @@ const metadata: Ref<ReportMetadataItem[]> = ref([]);
 // 章节类型选择器可见状态
 const chapterTypeSelectVisible = ref(false);
 // 章节管理对象
-const chapterManager = ref(new ChapterManager());
+const chapterManager = new ChapterManager();
 
 function onMetaChange(data: ReportMetadataItem[]) {
   metadata.value = data;
@@ -38,15 +41,23 @@ function onMetaChange(data: ReportMetadataItem[]) {
 }
 
 function onChapterTypeSelected(type: ChapterType) {
-  chapterManager.value.addChapter(type);
+  chapterManager.addChapter(type);
 }
 
 function onChapterChange(index: number, data: Chapter) {
-  chapterManager.value.setChapter(index, data);
-  if (report.value) {
-    const chapter = report.value.chapters[index];
-    report.value.chapters[index] = {...chapter, ...data};
-  }
+  chapterManager.setChapter(index, data);
+}
+
+function onChapterMoveUp(index: number) {
+  chapterManager.moveUp(index)
+}
+
+function onChapterMoveDown(index: number) {
+  chapterManager.moveDown(index)
+}
+
+function onChapterRemove(index: number) {
+  chapterManager.remove(index)
 }
 
 function onCompanyChange(companyId: number) {
@@ -71,7 +82,7 @@ function onSave() {
       message.success("保存成功")
     }).catch((err) => {
       saveLoading.value = false;
-      CompLog.logErr(err);
+      logError(err);
     });
   }
 }
@@ -99,7 +110,7 @@ function onSaveTemplate(modalData: ReportTemplateModal) {
   reportTemplateService.save(reportTemplateSave).then(() => {
     templateSaveModal.value?.complete()
   }).catch((err) => {
-    CompLog.logErr(err)
+    logError(err)
     templateSaveModal.value?.complete()
   })
 }
@@ -110,8 +121,8 @@ onMounted(() => {
     reportService.get(id).then((data) => {
       report.value = data;
       const chapters = report.value.chapters;
-      chapterManager.value.setChapters(chapters);
-    }).catch(CompLog.logErr('ReportEditorView'));
+      chapterManager.setChapters(chapters);
+    }).catch(logError);
   }
 });
 </script>
@@ -131,12 +142,15 @@ onMounted(() => {
     </a-col>
   </a-row>
   <a-divider />
-  <div class="chapter-wrap">
+  <div class="chapter-wrap" v-if="report">
     <ChapterEditor
       v-for="(item, index) in chapterManager.getChapters()"
       :key="index"
       :data="item"
       @change="onChapterChange(index, $event)"
+      @move-up="onChapterMoveUp(index)"
+      @move-down="onChapterMoveDown(index)"
+      @remove="onChapterRemove(index)"
     />
   </div>
   <div class="add-chapter-wrap">

+ 5 - 2
src/views/report/ReportListView.vue

@@ -13,6 +13,9 @@ import type { ReportBasic } from "@/types/report.types"
 import { routeToReportEditor } from "@/router";
 import SpinComponent from "@/components/SpinComponent.vue";
 
+const _componentName = 'ReportListView'
+const logError = CompLog.logErr(_componentName)
+
 const currentPage = ref(1);
 
 interface CompanyAggProp {
@@ -43,7 +46,7 @@ function fetchReports(companyId?: number, page?: number, field?: string, keyword
     reports.value = resp.reports;
     spinning.value = false;
   }).catch((err) => {
-    CompLog.logErr(err);
+    logError(err);
     spinning.value = false;
   })
 }
@@ -88,7 +91,7 @@ onMounted(() => {
         checked: false,
       }
     });
-  }).catch(CompLog.logErr)
+  }).catch(logError)
 });
 </script>
 

+ 56 - 18
src/views/report/components/ChapterEditor.vue

@@ -2,7 +2,7 @@
 
 <script setup lang="ts">
 import { ref, watch, type PropType } from 'vue';
-import { EditOutlined } from '@ant-design/icons-vue';
+import { EditOutlined, DownOutlined } from '@ant-design/icons-vue';
 import type { Chapter } from '@/types/report.types';
 import ContentEditor from "../components/ContentEditor.vue";
 import ReferenceDraw from "../components/reference/ReferenceDraw.vue";
@@ -14,6 +14,8 @@ const props = defineProps({
   }
 });
 
+const keywords = ref<string[]>([])
+
 const titleEditor = ref<HTMLInputElement | null>(null);
 
 const titleEditing = ref(false);
@@ -25,6 +27,9 @@ const referenceDraw = ref<InstanceType<typeof ReferenceDraw> | null>(null);
 const emits = defineEmits<{
   (e: "update:data", data: Chapter): void;
   (e: "change", data: Chapter): void;
+  (e: "moveUp"): void;
+  (e: "moveDown"): void;
+  (e: "remove"): void;
 }>();
 
 watch(
@@ -53,30 +58,59 @@ function onContentChange(value: string) {
   emits("change", {...props.data, content: value});
 }
 
-function search() {
+function onMenuClick(e: { key: string }) {
+  switch (e.key) {
+    case "UP":
+      emits("moveUp");
+      break;
+    case "DOWN":
+      emits("moveDown");
+      break;
+    case "REMOVE":
+      emits("remove")
+      break;
+  }
+}
+
+function searchReference() {
   referenceDraw.value?.show();
 }
 </script>
 
 <template>
   <div class="chapter-item">
-    <a-row>
+    <a-row type="flex" justify="space-between">
       <a-col>
-        <a-input
-          v-if="titleEditing"
-          v-model:value="fullTitle"
-          @blur="onTitleChange"
-          @pressEnter="onTitleChange"
-          @keyup.esc="onTitleChangeCancel"
-          ref="titleEditor"
-          class="title-editor"
-        />
-        <h1 v-else>
-          <span>{{ data.fullTitle }}</span>
-        </h1>
+        <a-space>
+          <a-input
+            v-if="titleEditing"
+            v-model:value="fullTitle"
+            @blur="onTitleChange"
+            @pressEnter="onTitleChange"
+            @keyup.esc="onTitleChangeCancel"
+            ref="titleEditor"
+            class="title-editor"
+          />
+          <h1 v-else>
+            <span>{{ data.fullTitle }}</span>
+          </h1>
+          <a-button type="text" @click="onEditTitle" class="edit-btn"><edit-outlined /></a-button>
+        </a-space>
       </a-col>
       <a-col>
-        <a-button type="text" @click="onEditTitle"><edit-outlined /></a-button>
+        <a-dropdown>
+          <template #overlay>
+            <a-menu @click="onMenuClick">
+              <a-menu-item key="UP">上移</a-menu-item>
+              <a-menu-item key="DOWN">下移</a-menu-item>
+              <a-menu-item key="REMOVE">删除</a-menu-item>
+            </a-menu>
+          </template>
+          <a-button type="link" size="small">
+            操作
+            <DownOutlined />
+          </a-button>
+        </a-dropdown>
       </a-col>
     </a-row>
     <p>
@@ -84,12 +118,12 @@ function search() {
     </p>
     <div class="chapter-button-group">
       <a-space>
-        <a-button @click="search">查找参考文献</a-button>
+        <a-button @click="searchReference">查找参考文献</a-button>
         <a-button>自动生成</a-button>
       </a-space>
     </div>
   </div>
-  <ReferenceDraw ref="referenceDraw" />
+  <ReferenceDraw :keywords="keywords" ref="referenceDraw" />
 </template>
 
 <style scoped lang="scss">
@@ -100,6 +134,10 @@ function search() {
     width: 400px;
   }
 
+  .edit-btn {
+    margin-left: 1em;
+  }
+
   .chapter-button-group {
     text-align: right;
   }

+ 8 - 1
src/views/report/components/ContentEditor.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import { onMounted, ref } from "vue";
+import { onMounted, ref, watch } from "vue";
 import Editor from "@tinymce/tinymce-vue";
 
 const props = defineProps({
@@ -72,6 +72,13 @@ function onChange() {
 
 onMounted(() => {
   content.value = props.data;
+
+  watch(
+    () => props.data,
+    (newData: string) => {
+      content.value = newData
+    }
+  );
 });
 </script>
 

+ 3 - 1
src/views/report/components/TemplateSaveModal.vue

@@ -5,6 +5,8 @@ import * as reportTemplateService from "@/services/reportTemplate.service"
 import { CompLog } from "@/helpers/log.helper";
 import type { SelectProps } from "ant-design-vue";
 
+const logError = CompLog.logErr("TemplateSaveModal")
+
 const visible = ref(false)
 
 const saving = ref(false);
@@ -32,7 +34,7 @@ function onOk() {
 onMounted(() => {
   reportTemplateService.tags().then((tags) => {
     tagsOptions.value = tags.map((tag) => ({ value: tag }))
-  }).catch(CompLog.logErr)
+  }).catch(logError)
 });
 
 function hide() {

+ 7 - 0
src/views/report/components/reference/ReferenceDraw.vue

@@ -6,6 +6,13 @@ import ReferenceSearchBox from "./ReferenceSearchBox.vue";
 import ReferenceSearchResultItem from "./ReferenceSearchResultItem.vue";
 import type { RefereceSearchResultItem } from "./reference.type";
 
+defineProps({
+  keywords: {
+    type: Array<String>,
+    required: true,
+  }
+});
+
 const visible = ref<boolean>(false);
 
 const referenceSearchResult: Ref<RefereceSearchResultItem[]> = ref([

+ 5 - 28
src/views/report/components/report-create/RelationCompanyInput.vue

@@ -6,6 +6,8 @@ import * as companyService from "@/services/company.service";
 import type { SelectProps } from "ant-design-vue";
 import { CompLog } from "@/helpers/log.helper";
 
+const logError = CompLog.logErr("RelationCompanyInput")
+
 const name: Ref<string> = ref('');
 const companyId: Ref<number | null> = ref(null);
 
@@ -30,7 +32,7 @@ const fetchCompany = debounce((val) => {
   companyService.query(val, 10).then((companies) => {
     const data = companies.map((company) => ({ value: company.id, label: company.name }));
     options.value = data;
-  }).catch(CompLog.logError);
+  }).catch(logError);
 }, 300);
 
 function onFocus() {
@@ -48,42 +50,17 @@ function onModalOk() {
     name.value = company.name;
     companyId.value = company.id;
     emits("change", companyId.value);
-  }).catch(CompLog.logError);
+  }).catch(logError);
 }
 
-// watch(
-//   () => props.id,
-//   (id: number) => {
-//     console.log('id', id);
-//     if (id > 0) {
-//       companyService.get(id).then((company) => {
-//         companyId.value = id;
-//         name.value = company.name;
-//       }).catch(CompLog.logError);
-//     }
-//   }
-// );
-
 onMounted(() => {
   const id = props.id;
   if (id > 0) {
     companyService.get(id).then((company) => {
       companyId.value = id;
       name.value = company.name;
-    }).catch(CompLog.logError);
+    }).catch(logError);
   }
-  // watch(
-  //   () => props.id,
-  //   (id: number) => {
-  //     console.log('id', id);
-  //     if (id > 0) {
-  //       companyService.get(id).then((company) => {
-  //         companyId.value = id;
-  //         name.value = company.name;
-  //       }).catch(CompLog.logError);
-  //     }
-  //   }
-  // );
 });
 </script>
 

+ 4 - 2
src/views/reportTemplate/ReportTemplateListView.vue

@@ -9,6 +9,8 @@ import { CompLog } from "@/helpers/log.helper";
 import SpinComponent from "@/components/SpinComponent.vue";
 import { message } from "ant-design-vue";
 
+const logError = CompLog.logErr("ReportTemplateListView")
+
 // 报告模板总数
 const total = ref(0);
 // 报告模板列表
@@ -30,7 +32,7 @@ function fetchTemplates(field?: string, keyword?: string, page?: number) {
     loading.value = false
   }).catch((err) => {
     loading.value = false
-    CompLog.logErr(err)
+    logError(err)
   })
 }
 
@@ -49,7 +51,7 @@ function onDelete(index: number) {
       if (deleted) {
         fetchTemplates()
       } else {
-        CompLog.logErr('删除报告失败:' + template.id)
+        logError('删除报告失败:' + template.id)
       }
     })
   } else {