Browse Source

完善报告撰写功能

1. 章节内容传入子组件
2. 章节内容编辑后保存到对象
3. 标题支持编辑
Kevin Jiang 2 years ago
parent
commit
b929c7ef03

+ 5 - 0
src/client/company.client.ts

@@ -6,6 +6,11 @@ function _url(path: string) {
   return `/gw/user${path}`;
 }
 
+export async function get(id: number): Promise<Company> {
+  const resp = await httpClient.get<Response<Company>>(_url(`/company/${id}`));
+  return resp.data.data;
+}
+
 export async function create(name: string): Promise<Company> {
   const resp = await httpClient.put<Response<Company>>(_url("/company"), { name });
   return resp.data.data;

+ 5 - 0
src/client/report.client.ts

@@ -23,3 +23,8 @@ export async function create(): Promise<number> {
   const resp = await httpClient.put<Response<number>>(_url("/report"));
   return resp.data.data;
 }
+
+export async function save(reportSaveRequest: ReportSaveRequest) {
+  const resp = await httpClient.post<Response<ReportSaveRequest>>(_url("/report"), reportSaveRequest);
+  return resp.data.data;
+}

+ 16 - 8
src/helpers/log.helper.ts

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

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

@@ -29,6 +29,10 @@ export class ChapterManager {
     this.chapters[index] = data;
   }
 
+  public setChapters(chapters: Chapter[]) {
+    this.chapters = chapters;
+  }
+
   public getChapters(): Chapter[] {
     return this.chapters.map((chapter, index) => {
       const fullTitle = Nzh.cn.encodeS(index+1) + '、' + chapter.title;

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

@@ -1,5 +1,9 @@
 import * as companyClient from "@/client/company.client";
 
+export async function get(id: number) {
+  return companyClient.get(id);
+}
+
 export async function create(name: string) {
   return companyClient.create(name);
 }

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

@@ -20,3 +20,7 @@ export async function create(userId: number): Promise<ReportSaveRequest> {
 
   return report;
 }
+
+export async function save(reportSaveRequest: ReportSaveRequest) {
+  return reportClient.save(reportSaveRequest);
+}

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

@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import { onMounted, ref, type Ref } from "vue";
+import { computed, onMounted, ref, type Ref } from "vue";
 import { useRoute } from "vue-router";
 import { PlusOutlined } from "@ant-design/icons-vue";
 import PageHeader from '@/components/PageHeader.vue';
@@ -12,6 +12,13 @@ import type { Chapter, ChapterType, ReportSaveRequest } from "@/types/report.typ
 import { ChapterManager } from "@/models/report.model";
 import ChapterEditor from "./components/ChapterEditor.vue";
 import * as reportService from "@/services/report.service";
+import { CompLog } from "@/helpers/log.helper";
+
+const route = useRoute();
+
+const report: Ref<ReportSaveRequest | null> = ref(null);
+
+const companyId = computed(() => report.value?.companyId || 0)
 
 // 报告元数据
 const metadata: Ref<any[]> = ref([]);
@@ -30,20 +37,37 @@ function onChapterTypeSelected(type: ChapterType) {
 
 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};
+  }
 }
 
-const route = useRoute();
+function onCompanyChange(companyId: number) {
+  if (report.value) {
+    report.value.companyId = companyId;
+  }
+}
 
-const report: Ref<ReportSaveRequest | null> = ref(null);
+const saveLoading = ref(false);
+
+function onSave() {
+  if (report.value) {
+    saveLoading.value = true;
+    reportService.save(report.value).then(() => {
+      saveLoading.value = false;
+    });
+  }
+}
 
 onMounted(() => {
   const id = parseInt(route.params.id as string);
   if (id && id > 0) {
     reportService.get(id).then((data) => {
       report.value = data;
-    }).catch((err) => {
-      console.log(err);
-    });
+      const chapters = report.value.chapters;
+      chapterManager.value.setChapters(chapters);
+    }).catch(CompLog.logErr('ReportEditorView'));
   }
 });
 </script>
@@ -53,7 +77,7 @@ onMounted(() => {
   <a-row :gutter="24">
     <a-col :span="12">
       <div class="metadata-wrap">
-        <RelationCompanyInput />
+        <RelationCompanyInput v-model:id="companyId" @change="onCompanyChange" />
         <ReportCategory />
         <DynamicMeta @change="onMetaChange" />
       </div>
@@ -79,7 +103,7 @@ onMounted(() => {
   </div>
   <div class="bottom-button-group">
     <a-space>
-      <a-button type="primary">保存</a-button>
+      <a-button type="primary" @click="onSave" :loading="saveLoading">保存</a-button>
       <a-button>预览</a-button>
       <a-button>导出</a-button>
       <a-button>保存为模板</a-button>

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

@@ -1,7 +1,8 @@
 <!-- 章节编辑器 -->
 
 <script setup lang="ts">
-import { ref, type PropType } from 'vue';
+import { ref, watch, type PropType } from 'vue';
+import { EditOutlined } 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";
@@ -13,18 +14,43 @@ const props = defineProps({
   }
 });
 
-const referenceDraw = ref<InstanceType<typeof ReferenceDraw> | null>(null);
+const titleEditor = ref<HTMLInputElement | null>(null);
+
+const titleEditing = ref(false);
 
-const content = ref('');
+const fullTitle = ref('');
+
+const referenceDraw = ref<InstanceType<typeof ReferenceDraw> | null>(null);
 
 const emits = defineEmits<{
   (e: "update:data", data: Chapter): void;
   (e: "change", data: Chapter): void;
 }>();
 
-function onContentChange() {
-  console.log('parent content', content.value);
-  emits("change", {...props.data, content: content.value});
+watch(
+  () => titleEditor.value,
+  (el: HTMLInputElement | null) => {
+    el?.focus();
+  }
+);
+
+function onEditTitle() {
+  fullTitle.value = props.data.title;
+  titleEditing.value = true;
+}
+
+function onTitleChange() {
+  titleEditing.value = false;
+  emits("change", {...props.data, title: fullTitle.value})
+}
+
+function onTitleChangeCancel() {
+  fullTitle.value = props.data.title;
+  titleEditing.value = false;
+}
+
+function onContentChange(value: string) {
+  emits("change", {...props.data, content: value});
 }
 
 function search() {
@@ -34,9 +60,27 @@ function search() {
 
 <template>
   <div class="chapter-item">
-    <h1>{{ data.fullTitle }}</h1>
+    <a-row>
+      <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-col>
+      <a-col>
+        <a-button type="text" @click="onEditTitle"><edit-outlined /></a-button>
+      </a-col>
+    </a-row>
     <p>
-      <ContentEditor v-model:data="content" @change="onContentChange" />
+      <ContentEditor :data="data.content" @change="onContentChange" />
     </p>
     <div class="chapter-button-group">
       <a-space>
@@ -52,6 +96,10 @@ function search() {
 .chapter-item {
   margin-top: 2em;
 
+  .title-editor {
+    width: 400px;
+  }
+
   .chapter-button-group {
     text-align: right;
   }

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

@@ -13,7 +13,7 @@ const content = ref("");
 
 const toolbar = [
   'undo redo',
-  // 'blocks',
+  'blocks',
   'bold italic',
   'alignleft aligncenter alignright alignjustify',
   'indent outdent',

+ 26 - 1
src/views/report/components/report-create/RelationCompanyInput.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import { ref, type Ref } from "vue";
+import { ref, watch, type Ref } from "vue";
 import { debounce } from "lodash";
 import { PlusOutlined } from '@ant-design/icons-vue';
 import * as companyService from "@/services/company.service";
@@ -9,12 +9,24 @@ import { CompLog } from "@/helpers/log.helper";
 const name: Ref<string> = ref('');
 const companyId: Ref<number | null> = ref(null);
 
+const props = defineProps({
+  id: {
+    type: Number,
+    required: true,
+    default: 0,
+  }
+});
+
 const options = ref<SelectProps['options']>([]);
 
 const newName = ref('');
 
 const visible = ref(false);
 
+const emits = defineEmits<{
+  (e: "change", companyId: number): void;
+}>();
+
 const fetchCompany = debounce((val) => {
   companyService.query(val, 10).then((companies) => {
     const data = companies.map((company) => ({ value: company.id, label: company.name }));
@@ -28,6 +40,7 @@ function onFocus() {
 
 function onChange(val: number) {
   companyId.value = val;
+  emits("change", companyId.value);
 }
 
 function onModalOk() {
@@ -35,8 +48,20 @@ function onModalOk() {
   companyService.create(newName.value).then((company) => {
     name.value = company.name;
     companyId.value = company.id;
+    emits("change", companyId.value);
   }).catch(CompLog.logError);
 }
+
+watch(
+  () => props.id,
+  (id: number) => {
+  if (id > 0) {
+    companyService.get(id).then((company) => {
+      companyId.value = id;
+      name.value = company.name;
+    }).catch(CompLog.logError);
+  }
+});
 </script>
 
 <template>