|
@@ -0,0 +1,372 @@
|
|
|
+<script setup lang="ts">
|
|
|
+import { onMounted, onUnmounted, ref, type Ref } from "vue";
|
|
|
+import { useRoute } from "vue-router";
|
|
|
+import { PlusOutlined } from "@ant-design/icons-vue";
|
|
|
+import PageHeader from '@/components/PageHeader.vue';
|
|
|
+import RelationCompanyInput from './components/report-create/RelationCompanyInput.vue';
|
|
|
+import ReportField from "./components/report-create/ReportField.vue"
|
|
|
+import DynamicMeta from './components/dynamic-meta/DynamicMeta.vue';
|
|
|
+import ChapterTypeSelect from "./components/ChapterTypeSelect.vue";
|
|
|
+import LogoComponent from "./components/LogoComponent.vue";
|
|
|
+import type { Chapter, ChapterType, ReportMetadataItem, ReportSaveRequest, ReportTemplateSave, DuplicateCheckRequest, ReportChapterRequest } from "@/types/report.types";
|
|
|
+import { ChapterManager } from "@/models/report.model";
|
|
|
+import ChapterEditor from "./components/ChapterEditor.vue";
|
|
|
+import * as reportService from "@/services/report.service";
|
|
|
+import * as reportTemplateService from "@/services/reportTemplate.service"
|
|
|
+import * as logoService from "@/services/logo.service"
|
|
|
+import { CompLog } from "@/libs/log.lib";
|
|
|
+import { message } from "ant-design-vue";
|
|
|
+import TemplateSaveModal from "./components/TemplateSaveModal.vue";
|
|
|
+import type { ReportTemplateModal } from "./reportComponent.types";
|
|
|
+import { useReportExport } from "@/libs/reportExport.lib";
|
|
|
+import { routeToReportPreview } from "@/router";
|
|
|
+import { useAuthStore } from "@/stores/auth.store";
|
|
|
+
|
|
|
+const _componentName = 'ReportEditorView'
|
|
|
+const logError = CompLog.logErr(_componentName)
|
|
|
+
|
|
|
+// 当前路由
|
|
|
+const route = useRoute();
|
|
|
+// 报告下载函数库
|
|
|
+const reportExport = useReportExport()
|
|
|
+// 权限store
|
|
|
+const authStore = useAuthStore()
|
|
|
+
|
|
|
+const chapterEditors = ref<any | null>([])
|
|
|
+
|
|
|
+const report: Ref<ReportSaveRequest | null> = ref(null);
|
|
|
+const relationCompanyEl = ref<HTMLInputElement | null>(null);
|
|
|
+const reportCategoryEl = ref<HTMLInputElement | null>(null);
|
|
|
+const reportNameEl = ref<HTMLInputElement | null>(null);
|
|
|
+const supervisorEl = ref<HTMLInputElement | null>(null);
|
|
|
+const keywordsEl = ref<HTMLInputElement | null>(null);
|
|
|
+const duplicateCheckRequest: Ref<DuplicateCheckRequest> = ref({
|
|
|
+ reportId: "",
|
|
|
+ contents: []
|
|
|
+});
|
|
|
+
|
|
|
+const placeholders = ref(["请输入报告类型","请输入项目名称","请输入项目负责人","请输入报告关键词"])
|
|
|
+
|
|
|
+// 报告元数据
|
|
|
+const metadata: Ref<ReportMetadataItem[]> = ref([]);
|
|
|
+// 章节类型选择器可见状态
|
|
|
+const chapterTypeSelectVisible = ref(false);
|
|
|
+// 章节管理对象
|
|
|
+const chapterManager = new ChapterManager();
|
|
|
+// logo url
|
|
|
+const logoUrl = ref<string | undefined>(undefined)
|
|
|
+// 报告导出按钮是否在加载的状态
|
|
|
+const exporting = ref(false)
|
|
|
+// 定时保存handler
|
|
|
+const timeIntervalHandler = ref<number | null>(null)
|
|
|
+//报告状态
|
|
|
+const reportStatus = ref<number>(0)
|
|
|
+
|
|
|
+function onMetaChange(data: ReportMetadataItem[]) {
|
|
|
+ metadata.value = data;
|
|
|
+ if (report.value) {
|
|
|
+ report.value.metadata = metadata.value;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Logo组件上传处理函数
|
|
|
+ * @param file 图片文件
|
|
|
+ */
|
|
|
+function onLogoChange(file: File) {
|
|
|
+ if (!report.value) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const formData = new FormData();
|
|
|
+ formData.append("file", file)
|
|
|
+ formData.append("reportId", report.value.id.toString())
|
|
|
+
|
|
|
+ logoService.upload(formData).then((resp) => {
|
|
|
+ if (resp && report.value && authStore.token) {
|
|
|
+ report.value.logoPath = resp;
|
|
|
+ logoUrl.value = logoService.link(report.value.id, authStore.token)
|
|
|
+ onSave()
|
|
|
+ }
|
|
|
+ }).catch(logError)
|
|
|
+}
|
|
|
+
|
|
|
+function onLogoDelete() {
|
|
|
+ if (!report.value || !report.value.logoPath) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ logoService.remove(report.value.id).then((resp) => {
|
|
|
+ if (resp) {
|
|
|
+ if (report.value) {
|
|
|
+ report.value.logoPath = ''
|
|
|
+ }
|
|
|
+ logoUrl.value = undefined
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+function onChapterTypeSelected(types: ChapterType[]) {
|
|
|
+ chapterManager.addChapters(types);
|
|
|
+}
|
|
|
+
|
|
|
+function onChapterChange(index: number, data: Chapter) {
|
|
|
+ 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) {
|
|
|
+ if (report.value) {
|
|
|
+ report.value.companyId = companyId;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const saveLoading = ref(false);
|
|
|
+
|
|
|
+function validateString(text: string, msg: string, el: Ref<HTMLInputElement | null>) {
|
|
|
+ if ((text || '').trim() == '') {
|
|
|
+ message.warning(msg);
|
|
|
+ el.value?.focus()
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+function onSave() {
|
|
|
+ if (report.value) {
|
|
|
+ if (!report.value.companyId) {
|
|
|
+ message.warning("请填写关联企业!")
|
|
|
+ relationCompanyEl.value?.focus();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const validtions: [string, string, Ref<HTMLInputElement | null>][] = [
|
|
|
+ [report.value.name, "请填写报告类型!", reportCategoryEl],
|
|
|
+ [report.value.reportName, "请填写项目名称!", reportNameEl],
|
|
|
+ [report.value.supervisor, '请填写负责人!', supervisorEl],
|
|
|
+ [report.value.keywords, "请填写关键词!", keywordsEl],
|
|
|
+ ];
|
|
|
+ for (const [text, msg, el] of validtions) {
|
|
|
+ if (!validateString(text, msg, el)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (report.value.metadata) {
|
|
|
+ for (const item of report.value.metadata) {
|
|
|
+ if (item.name == '' || item.value == '') {
|
|
|
+ message.warning("有动态字段的名称或值为空,请填写!")
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ saveLoading.value = true;
|
|
|
+ reportService.save(report.value).then(() => {
|
|
|
+ saveLoading.value = false;
|
|
|
+ message.success("保存成功")
|
|
|
+ }).catch((err) => {
|
|
|
+ saveLoading.value = false;
|
|
|
+ logError(err);
|
|
|
+ });
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 导出报告
|
|
|
+ */
|
|
|
+function onExport() {
|
|
|
+ exporting.value = true
|
|
|
+ if (report.value) {
|
|
|
+ reportExport.download(report.value.name, report.value.id)
|
|
|
+ setTimeout(() => exporting.value = false, 5000)
|
|
|
+ } else {
|
|
|
+ message.warning("暂无数据可以导出")
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 报告预览
|
|
|
+ */
|
|
|
+function onPreview() {
|
|
|
+ if (report.value) {
|
|
|
+ routeToReportPreview(report.value.id)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const templateSaveModal = ref<InstanceType<typeof TemplateSaveModal> | null>(null);
|
|
|
+
|
|
|
+function showTemplateSaveModal() {
|
|
|
+ templateSaveModal.value?.show();
|
|
|
+}
|
|
|
+
|
|
|
+function onSaveTemplate(modalData: ReportTemplateModal) {
|
|
|
+ let chapters = report.value?.chapters.map((chapter) => {
|
|
|
+ return {
|
|
|
+ title: chapter.title,
|
|
|
+ keywords: chapter.keywords,
|
|
|
+ } as Chapter
|
|
|
+ })
|
|
|
+ const reportTemplateSave: ReportTemplateSave = {
|
|
|
+ id: 0,
|
|
|
+ companyId: report.value?.companyId || 0,
|
|
|
+ name: modalData.templateName,
|
|
|
+ reportName: report.value?.reportName || '',
|
|
|
+ logoPath: report.value?.logoPath || '',
|
|
|
+ metadata: report.value?.metadata || [],
|
|
|
+ chapters: chapters || [],
|
|
|
+ tags: modalData.tags,
|
|
|
+ }
|
|
|
+
|
|
|
+ reportTemplateService.save(reportTemplateSave).then(() => {
|
|
|
+ templateSaveModal.value?.complete()
|
|
|
+ message.success("模板保存成功")
|
|
|
+ }).catch((err) => {
|
|
|
+ logError(err)
|
|
|
+ templateSaveModal.value?.complete()
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+function updateStatus() {
|
|
|
+ if (report.value) {
|
|
|
+ reportService.updateStatus(report.value.id)
|
|
|
+ getStatus();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function getStatus(){
|
|
|
+ reportStatus.value = reportStatus.value == 0 ? 1 : 0;
|
|
|
+}
|
|
|
+
|
|
|
+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);
|
|
|
+ }
|
|
|
+ index += 1;
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function extract(chapters: ReportChapterRequest[], id: string) {
|
|
|
+ duplicateCheckRequest.value.contents = []
|
|
|
+ for (var i = 0; i < chapters.length; i ++) {
|
|
|
+ duplicateCheckRequest.value?.contents.push(chapters[i].content)
|
|
|
+ }
|
|
|
+ if (duplicateCheckRequest?.value) {
|
|
|
+ duplicateCheckRequest.value.reportId = id;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ const id = parseInt(route.params.id as string);
|
|
|
+ if (id && id > 0) {
|
|
|
+ reportService.get(id).then((data) => {
|
|
|
+ report.value = data;
|
|
|
+ reportStatus.value = data.status
|
|
|
+ const chapters = report.value.chapters;
|
|
|
+ chapterManager.setChapters(chapters);
|
|
|
+ // 设置Logo信息
|
|
|
+ if (report.value.id && authStore.token) {
|
|
|
+ logoUrl.value = logoService.link(report.value.id, authStore.token)
|
|
|
+ }
|
|
|
+ // 数据加载成功后,设置自动保存
|
|
|
+ timeIntervalHandler.value = window.setInterval(() => {
|
|
|
+ onSave();
|
|
|
+ }, 60000);
|
|
|
+ }).catch(logError);
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+onUnmounted(() => {
|
|
|
+ if (timeIntervalHandler.value) {
|
|
|
+ window.clearInterval(timeIntervalHandler.value)
|
|
|
+ }
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<template>
|
|
|
+ <PageHeader title="报告撰写" />
|
|
|
+ <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" />
|
|
|
+ <DynamicMeta v-model:data="report.metadata" @change="onMetaChange" />
|
|
|
+ </div>
|
|
|
+ </a-col>
|
|
|
+ <a-col :span="12">
|
|
|
+ <LogoComponent @change="onLogoChange" @delete="onLogoDelete" :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)"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div class="add-chapter-wrap">
|
|
|
+ <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">
|
|
|
+ <span v-if="reportStatus == 0">未完成</span>
|
|
|
+ <span v-if="reportStatus == 1">已完成</span>
|
|
|
+ </a-checkbox>
|
|
|
+ <a-button type="primary" @click="onSave" :loading="saveLoading">保存</a-button>
|
|
|
+ <a-button @click="onPreview">预览</a-button>
|
|
|
+ <a-button @click="onExport" :loading="exporting">导出</a-button>
|
|
|
+ <a-button @click="showTemplateSaveModal">另保存为模板</a-button>
|
|
|
+ <a-button @click="duplicateCheck">查重</a-button>
|
|
|
+ </a-space>
|
|
|
+ </div>
|
|
|
+ <ChapterTypeSelect @ok="onChapterTypeSelected" v-model:visible="chapterTypeSelectVisible" />
|
|
|
+ <TemplateSaveModal ref="templateSaveModal" @ok="onSaveTemplate" />
|
|
|
+</template>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+.metadata-wrap {
|
|
|
+ > * {
|
|
|
+ margin-top: 0.5em;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.add-chapter-wrap {
|
|
|
+ margin-top: 3em;
|
|
|
+}
|
|
|
+
|
|
|
+.bottom-button-group {
|
|
|
+ margin-top: 5em;
|
|
|
+}
|
|
|
+</style>
|