|
|
|
|
@ -0,0 +1,861 @@
|
|
|
|
|
<template>
|
|
|
|
|
<div class="review-result-page">
|
|
|
|
|
<!-- 顶部工具栏 -->
|
|
|
|
|
<div class="toolbar">
|
|
|
|
|
<div class="toolbar-left">
|
|
|
|
|
<ElButton :icon="ArrowLeft" @click="goBack">返回</ElButton>
|
|
|
|
|
<span class="task-title">{{ task?.task_name || "审核结果" }}</span>
|
|
|
|
|
<ElTag v-if="task" :type="statusTagType(task.status)" effect="light" size="small">
|
|
|
|
|
{{ statusText(task.status) }}
|
|
|
|
|
</ElTag>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="toolbar-right">
|
|
|
|
|
<ElButton
|
|
|
|
|
:icon="mdPanelCollapsed ? Expand : Fold"
|
|
|
|
|
@click="mdPanelCollapsed = !mdPanelCollapsed"
|
|
|
|
|
>
|
|
|
|
|
{{ mdPanelCollapsed ? "展开结果" : "收起结果" }}
|
|
|
|
|
</ElButton>
|
|
|
|
|
<ElButton type="primary" :icon="Download" @click="handleExport" :loading="exporting">
|
|
|
|
|
导出
|
|
|
|
|
</ElButton>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 主体区域 -->
|
|
|
|
|
<div class="content-area">
|
|
|
|
|
<!-- 左侧文档预览 -->
|
|
|
|
|
<div class="docx-panel" :class="{ 'docx-panel--full': mdPanelCollapsed }">
|
|
|
|
|
<FaDocxPreview
|
|
|
|
|
v-if="task?.file_url"
|
|
|
|
|
:src="task.file_url"
|
|
|
|
|
:readonly="true"
|
|
|
|
|
class="docx-preview"
|
|
|
|
|
/>
|
|
|
|
|
<ElEmpty v-else class="empty-docx" description="暂无文档" />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 右侧 MD 预览 -->
|
|
|
|
|
<div v-show="!mdPanelCollapsed" class="md-panel" :class="{ 'md-outline-collapsed': mdOutlineCollapsed }">
|
|
|
|
|
<!-- MD 大纲(可折叠) -->
|
|
|
|
|
<div class="md-outline-wrap">
|
|
|
|
|
<div class="md-outline-inner" v-show="!mdOutlineCollapsed">
|
|
|
|
|
<div class="md-outline-header">
|
|
|
|
|
<span>大纲</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="md-outline-body" ref="mdOutlineBodyRef">
|
|
|
|
|
<ul v-if="mdHeadings.length" class="md-outline-list">
|
|
|
|
|
<li
|
|
|
|
|
v-for="h in mdHeadings"
|
|
|
|
|
:key="h.id"
|
|
|
|
|
:class="['md-outline-item', `level-${h.level}`, { active: activeMdHeading === h.id }]"
|
|
|
|
|
@click="scrollToMdHeading(h.id)"
|
|
|
|
|
:title="h.text"
|
|
|
|
|
>
|
|
|
|
|
{{ h.text }}
|
|
|
|
|
</li>
|
|
|
|
|
</ul>
|
|
|
|
|
<div v-else class="no-outline">暂无大纲</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class="md-outline-toggle"
|
|
|
|
|
:title="mdOutlineCollapsed ? '展开大纲' : '收起大纲'"
|
|
|
|
|
@click="mdOutlineCollapsed = !mdOutlineCollapsed"
|
|
|
|
|
>
|
|
|
|
|
<ElIcon><ArrowRight v-if="mdOutlineCollapsed" /><ArrowLeft v-else /></ElIcon>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- MD 内容 -->
|
|
|
|
|
<div class="md-content-wrap" ref="mdContentWrapRef">
|
|
|
|
|
<div class="md-content-header">
|
|
|
|
|
<span class="md-content-title">预览</span>
|
|
|
|
|
<ElButton
|
|
|
|
|
link
|
|
|
|
|
:icon="FullScreen"
|
|
|
|
|
title="大窗口预览"
|
|
|
|
|
class="full-preview-btn"
|
|
|
|
|
@click="openFullPreview"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- @scroll="onMdScroll" -->
|
|
|
|
|
<div v-loading="mdLoading" class="md-content-scroll">
|
|
|
|
|
<div v-if="mdContent" class="markdown-body" v-html="renderedMdWithIds"></div>
|
|
|
|
|
<ElEmpty v-else-if="!mdLoading" description="审核结果接口暂未提供,等待后端接入" :image-size="80" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 大窗口预览 -->
|
|
|
|
|
<ElDialog
|
|
|
|
|
v-model="fullPreviewVisible"
|
|
|
|
|
title="审核结果预览"
|
|
|
|
|
width="90%"
|
|
|
|
|
top="3vh"
|
|
|
|
|
append-to-body
|
|
|
|
|
destroy-on-close
|
|
|
|
|
class="full-preview-dialog"
|
|
|
|
|
>
|
|
|
|
|
<div class="full-preview-body">
|
|
|
|
|
<!-- 左侧大纲 -->
|
|
|
|
|
<div class="full-outline-wrap">
|
|
|
|
|
<div class="full-outline-header">大纲</div>
|
|
|
|
|
<div class="full-outline-body">
|
|
|
|
|
<ul v-if="fullHeadings.length" class="full-outline-list">
|
|
|
|
|
<li
|
|
|
|
|
v-for="h in fullHeadings"
|
|
|
|
|
:key="h.id"
|
|
|
|
|
:class="['full-outline-item', `level-${h.level}`, { active: fullActiveHeading === h.id }]"
|
|
|
|
|
:title="h.text"
|
|
|
|
|
@click="scrollToFullHeading(h.id)"
|
|
|
|
|
>
|
|
|
|
|
{{ h.text }}
|
|
|
|
|
</li>
|
|
|
|
|
</ul>
|
|
|
|
|
<div v-else class="no-outline">暂无大纲</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- 右侧内容 -->
|
|
|
|
|
<div class="full-content-wrap" ref="fullContentWrapRef">
|
|
|
|
|
<!-- @scroll="onFullScroll" -->
|
|
|
|
|
<div class="full-content-scroll">
|
|
|
|
|
<div v-if="mdContent" class="markdown-body" v-html="renderedFullMd"></div>
|
|
|
|
|
<ElEmpty v-else description="暂无内容" :image-size="80" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</ElDialog>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { ref, computed, onMounted, watch } from "vue";
|
|
|
|
|
import { useRoute, useRouter } from "vue-router";
|
|
|
|
|
import MarkdownIt from "markdown-it";
|
|
|
|
|
import hljs from "highlight.js";
|
|
|
|
|
import "highlight.js/styles/atom-one-light.css";
|
|
|
|
|
import {
|
|
|
|
|
ArrowLeft,
|
|
|
|
|
ArrowRight,
|
|
|
|
|
Download,
|
|
|
|
|
Fold,
|
|
|
|
|
Expand,
|
|
|
|
|
FullScreen,
|
|
|
|
|
} from "@element-plus/icons-vue";
|
|
|
|
|
import FaDocxPreview from "@/components/business/fa-docx-preview/index.vue";
|
|
|
|
|
import { ReviewTaskAPI } from "@/api/module_report/review";
|
|
|
|
|
import type { ReviewTaskItem } from "@/api/module_report/review";
|
|
|
|
|
import type { ReportType } from "@/api/module_report/report";
|
|
|
|
|
|
|
|
|
|
defineOptions({ name: "ReviewResultPage" });
|
|
|
|
|
|
|
|
|
|
const route = useRoute();
|
|
|
|
|
const router = useRouter();
|
|
|
|
|
|
|
|
|
|
/** 报告类型:1 能源报告 / 2 光伏报告(由路由参数传入) */
|
|
|
|
|
const reportType = computed<ReportType>(() => {
|
|
|
|
|
const t = route.query.report_type;
|
|
|
|
|
return t === "2" ? 2 : 1;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const task = ref<ReviewTaskItem | null>(null);
|
|
|
|
|
const mdLoading = ref(false);
|
|
|
|
|
const exporting = ref(false);
|
|
|
|
|
const mdPanelCollapsed = ref(false);
|
|
|
|
|
const mdOutlineCollapsed = ref(false);
|
|
|
|
|
const fullPreviewVisible = ref(false);
|
|
|
|
|
|
|
|
|
|
const mdContent = ref("");
|
|
|
|
|
const mdHeadings = ref<{ id: string; text: string; level: number }[]>([]);
|
|
|
|
|
const activeMdHeading = ref("");
|
|
|
|
|
|
|
|
|
|
const mdContentWrapRef = ref<HTMLElement | null>(null);
|
|
|
|
|
const fullContentWrapRef = ref<HTMLElement | null>(null);
|
|
|
|
|
const fullActiveHeading = ref("");
|
|
|
|
|
|
|
|
|
|
function escapeHtml(s: string): string {
|
|
|
|
|
return s
|
|
|
|
|
.replace(/&/g, "&")
|
|
|
|
|
.replace(/</g, "<")
|
|
|
|
|
.replace(/>/g, ">")
|
|
|
|
|
.replace(/"/g, """)
|
|
|
|
|
.replace(/'/g, "'");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function highlightCode(str: string, lang: string): string {
|
|
|
|
|
if (lang && hljs.getLanguage(lang)) {
|
|
|
|
|
try {
|
|
|
|
|
return `<pre class="hljs"><code>${hljs.highlight(str, { language: lang, ignoreIllegals: true }).value}</code></pre>`;
|
|
|
|
|
} catch (e) {}
|
|
|
|
|
}
|
|
|
|
|
return `<pre class="hljs"><code>${escapeHtml(str)}</code></pre>`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const mdParser = new MarkdownIt({
|
|
|
|
|
html: true,
|
|
|
|
|
linkify: true,
|
|
|
|
|
typographer: true,
|
|
|
|
|
breaks: true,
|
|
|
|
|
highlight: highlightCode,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
interface MdHeading { id: string; text: string; level: number; }
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 渲染 markdown,同时收集标题并注入 id。
|
|
|
|
|
* 直接操作 markdown-it token 流,确保大纲提取与渲染结果完全一致,
|
|
|
|
|
* 避免正则解析与渲染器输出不匹配(如 setext 标题、内联格式等)的问题。
|
|
|
|
|
*/
|
|
|
|
|
function renderMarkdown(md: string, idPrefix = "md-h-"): { html: string; headings: MdHeading[] } {
|
|
|
|
|
const headings: MdHeading[] = [];
|
|
|
|
|
const env: Record<string, unknown> = {};
|
|
|
|
|
const tokens = mdParser.parse(md, env);
|
|
|
|
|
let counter = 0;
|
|
|
|
|
for (let i = 0; i < tokens.length; i++) {
|
|
|
|
|
const tok = tokens[i];
|
|
|
|
|
if (!tok || tok.type !== "heading_open") continue;
|
|
|
|
|
const level = Number(tok.tag.substring(1));
|
|
|
|
|
const id = `${idPrefix}${counter++}`;
|
|
|
|
|
tok.attrSet("id", id);
|
|
|
|
|
// 从后续 inline token 中提取标题纯文本
|
|
|
|
|
let text = "";
|
|
|
|
|
let j = i + 1;
|
|
|
|
|
while (j < tokens.length) {
|
|
|
|
|
const inner = tokens[j];
|
|
|
|
|
if (!inner || inner.type === "heading_close") break;
|
|
|
|
|
if (inner.type === "inline" && inner.children) {
|
|
|
|
|
for (const child of inner.children) {
|
|
|
|
|
if (child.type === "text" || child.type === "code_inline") {
|
|
|
|
|
text += child.content;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
j++;
|
|
|
|
|
}
|
|
|
|
|
headings.push({ id, text: text.trim(), level });
|
|
|
|
|
}
|
|
|
|
|
const html = mdParser.renderer.render(tokens, mdParser.options, env);
|
|
|
|
|
return { html, headings };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 侧栏预览渲染结果(含标题 id 与大纲列表) */
|
|
|
|
|
const sideRender = computed(() => {
|
|
|
|
|
if (!mdContent.value) return { html: "", headings: [] as MdHeading[] };
|
|
|
|
|
return renderMarkdown(mdContent.value, "md-h-");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const renderedMdWithIds = computed(() => sideRender.value.html);
|
|
|
|
|
|
|
|
|
|
/** 大窗口预览渲染结果(使用不同 id 前缀避免 DOM id 重复) */
|
|
|
|
|
const fullRender = computed(() => {
|
|
|
|
|
if (!mdContent.value) return { html: "", headings: [] as MdHeading[] };
|
|
|
|
|
return renderMarkdown(mdContent.value, "md-fh-");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const renderedFullMd = computed(() => fullRender.value.html);
|
|
|
|
|
const fullHeadings = computed(() => fullRender.value.headings);
|
|
|
|
|
|
|
|
|
|
// 大纲标题随渲染结果更新
|
|
|
|
|
watch(
|
|
|
|
|
() => sideRender.value.headings,
|
|
|
|
|
(val) => {
|
|
|
|
|
mdHeadings.value = val;
|
|
|
|
|
activeMdHeading.value = "";
|
|
|
|
|
},
|
|
|
|
|
{ immediate: true }
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const taskId = computed(() => {
|
|
|
|
|
const id = route.query.id || route.params.id;
|
|
|
|
|
return id ? Number(id) : 0;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
if (taskId.value) {
|
|
|
|
|
loadTask(taskId.value);
|
|
|
|
|
loadResult(taskId.value);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
async function loadTask(id: number) {
|
|
|
|
|
try {
|
|
|
|
|
const resp = await ReviewTaskAPI.detail(id, reportType.value);
|
|
|
|
|
task.value = resp?.data?.data ?? null;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
task.value = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadResult(id: number) {
|
|
|
|
|
mdLoading.value = true;
|
|
|
|
|
try {
|
|
|
|
|
// TODO: 审查结果接口暂未提供,此处预留调用入口
|
|
|
|
|
// const resp = await ReviewTaskAPI.getResult(id);
|
|
|
|
|
// mdContent.value = resp?.data?.data?.md_content || "";
|
|
|
|
|
|
|
|
|
|
// 临时使用 public 目录下的 mock 文件,接口接入后删除
|
|
|
|
|
const resp = await fetch("/mock/审查结果-202605301641.md");
|
|
|
|
|
mdContent.value = await resp.text();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
mdContent.value = "";
|
|
|
|
|
} finally {
|
|
|
|
|
mdLoading.value = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function goBack() {
|
|
|
|
|
router.back();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openFullPreview() {
|
|
|
|
|
fullActiveHeading.value = "";
|
|
|
|
|
fullPreviewVisible.value = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleExport() {
|
|
|
|
|
if (!taskId.value) return;
|
|
|
|
|
exporting.value = true;
|
|
|
|
|
try {
|
|
|
|
|
// TODO: 接口暂未提供,此处预留
|
|
|
|
|
// const resp = await ReviewTaskAPI.exportResult(taskId.value);
|
|
|
|
|
// const blob = new Blob([resp.data]);
|
|
|
|
|
// const url = URL.createObjectURL(blob);
|
|
|
|
|
// const a = document.createElement("a");
|
|
|
|
|
// a.href = url;
|
|
|
|
|
// a.download = `${task.value?.task_name || "审核结果"}.md`;
|
|
|
|
|
// a.click();
|
|
|
|
|
// URL.revokeObjectURL(url);
|
|
|
|
|
|
|
|
|
|
// 临时下载 md 内容
|
|
|
|
|
const blob = new Blob([mdContent.value], { type: "text/markdown;charset=utf-8" });
|
|
|
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
|
const a = document.createElement("a");
|
|
|
|
|
a.href = url;
|
|
|
|
|
a.download = `${task.value?.task_name || "审核结果"}.md`;
|
|
|
|
|
a.click();
|
|
|
|
|
URL.revokeObjectURL(url);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
} finally {
|
|
|
|
|
exporting.value = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function scrollToMdHeading(id: string) {
|
|
|
|
|
const container = mdContentWrapRef.value?.querySelector(".md-content-scroll") as HTMLElement;
|
|
|
|
|
if (!container) return;
|
|
|
|
|
const el = container.querySelector(`[id="${id}"]`) as HTMLElement | null;
|
|
|
|
|
if (!el) return;
|
|
|
|
|
// 使用 getBoundingClientRect 计算相对偏移,避免嵌套元素下 offsetTop 不准
|
|
|
|
|
const containerRect = container.getBoundingClientRect();
|
|
|
|
|
const elRect = el.getBoundingClientRect();
|
|
|
|
|
const offset = elRect.top - containerRect.top + container.scrollTop - 16;
|
|
|
|
|
container.scrollTo({ top: Math.max(0, offset), behavior: "smooth" });
|
|
|
|
|
activeMdHeading.value = id;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onMdScroll(e: Event) {
|
|
|
|
|
const container = e.target as HTMLElement;
|
|
|
|
|
if (!container) return;
|
|
|
|
|
const containerRect = container.getBoundingClientRect();
|
|
|
|
|
const headings = mdHeadings.value;
|
|
|
|
|
let current = "";
|
|
|
|
|
for (const h of headings) {
|
|
|
|
|
const el = container.querySelector(`[id="${h.id}"]`) as HTMLElement | null;
|
|
|
|
|
if (el) {
|
|
|
|
|
const relTop = el.getBoundingClientRect().top - containerRect.top;
|
|
|
|
|
if (relTop - 40 <= 0) {
|
|
|
|
|
current = h.id;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (current) activeMdHeading.value = current;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 大窗口预览:点击大纲定位 */
|
|
|
|
|
function scrollToFullHeading(id: string) {
|
|
|
|
|
const container = fullContentWrapRef.value?.querySelector(".full-content-scroll") as HTMLElement;
|
|
|
|
|
if (!container) return;
|
|
|
|
|
const el = container.querySelector(`[id="${id}"]`) as HTMLElement | null;
|
|
|
|
|
if (!el) return;
|
|
|
|
|
const containerRect = container.getBoundingClientRect();
|
|
|
|
|
const elRect = el.getBoundingClientRect();
|
|
|
|
|
const offset = elRect.top - containerRect.top + container.scrollTop - 16;
|
|
|
|
|
container.scrollTo({ top: Math.max(0, offset), behavior: "smooth" });
|
|
|
|
|
fullActiveHeading.value = id;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 大窗口预览:滚动联动高亮 */
|
|
|
|
|
function onFullScroll(e: Event) {
|
|
|
|
|
const container = e.target as HTMLElement;
|
|
|
|
|
if (!container) return;
|
|
|
|
|
const containerRect = container.getBoundingClientRect();
|
|
|
|
|
const headings = fullHeadings.value;
|
|
|
|
|
let current = "";
|
|
|
|
|
for (const h of headings) {
|
|
|
|
|
const el = container.querySelector(`[id="${h.id}"]`) as HTMLElement | null;
|
|
|
|
|
if (el) {
|
|
|
|
|
const relTop = el.getBoundingClientRect().top - containerRect.top;
|
|
|
|
|
if (relTop - 40 <= 0) {
|
|
|
|
|
current = h.id;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (current) fullActiveHeading.value = current;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function statusText(s: number) {
|
|
|
|
|
return ["未审查", "审查中", "审查完成", "审查失败"][s] ?? "未知";
|
|
|
|
|
}
|
|
|
|
|
function statusTagType(s: number): "info" | "warning" | "success" | "danger" {
|
|
|
|
|
return ["info", "warning", "success", "danger"][s] as any;
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
|
.review-result-page {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
height: 100%;
|
|
|
|
|
background: #f5f7fa;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.toolbar {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
padding: 10px 16px;
|
|
|
|
|
background: #fff;
|
|
|
|
|
border-bottom: 1px solid #e5e7eb;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.toolbar-left {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.task-title {
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: #303133;
|
|
|
|
|
max-width: 400px;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.toolbar-right {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.content-area {
|
|
|
|
|
flex: 1;
|
|
|
|
|
display: flex;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
padding: 12px;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.docx-panel {
|
|
|
|
|
flex: 1.8;
|
|
|
|
|
background: #fff;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
border: 1px solid #e5e7eb;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
|
|
|
|
&--full {
|
|
|
|
|
flex: 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.docx-preview) {
|
|
|
|
|
flex: 1;
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
|
|
|
|
.outline-sidebar {
|
|
|
|
|
width: 200px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.empty-docx {
|
|
|
|
|
flex: 1;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 右侧 MD 面板(动态宽度,与左侧文档区按比例分配) */
|
|
|
|
|
.md-panel {
|
|
|
|
|
flex: 1;
|
|
|
|
|
min-width: 320px;
|
|
|
|
|
background: #fff;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
border: 1px solid #e5e7eb;
|
|
|
|
|
display: flex;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
|
|
|
|
&.md-outline-collapsed {
|
|
|
|
|
.md-outline-wrap { flex: 0 0 20px; min-width: 20px; }
|
|
|
|
|
.md-outline-inner { display: none; }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.md-outline-wrap {
|
|
|
|
|
flex: 0 0 30%;
|
|
|
|
|
min-width: 120px;
|
|
|
|
|
max-width: 240px;
|
|
|
|
|
background: #fafafa;
|
|
|
|
|
border-right: 1px solid #f0f0f0;
|
|
|
|
|
display: flex;
|
|
|
|
|
position: relative;
|
|
|
|
|
transition: flex-basis 0.3s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.md-outline-inner {
|
|
|
|
|
flex: 1;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.md-outline-header {
|
|
|
|
|
padding: 10px 10px 6px;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: #606266;
|
|
|
|
|
border-bottom: 1px solid #f0f0f0;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.md-outline-body {
|
|
|
|
|
flex: 1;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
padding: 6px 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.md-outline-list {
|
|
|
|
|
list-style: none;
|
|
|
|
|
margin: 0;
|
|
|
|
|
padding: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.md-outline-item {
|
|
|
|
|
padding: 5px 10px;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: #606266;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
border-left: 2px solid transparent;
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
background: #f0f5ff;
|
|
|
|
|
color: #409eff;
|
|
|
|
|
}
|
|
|
|
|
&.active {
|
|
|
|
|
color: #409eff;
|
|
|
|
|
background: #ecf5ff;
|
|
|
|
|
border-left-color: #409eff;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
}
|
|
|
|
|
&.level-1 { padding-left: 10px; font-weight: 600; }
|
|
|
|
|
&.level-2 { padding-left: 20px; }
|
|
|
|
|
&.level-3 { padding-left: 30px; font-size: 11px; }
|
|
|
|
|
&.level-4 { padding-left: 40px; font-size: 11px; color: #909399; }
|
|
|
|
|
&.level-5, &.level-6 { padding-left: 50px; font-size: 11px; color: #909399; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.no-outline {
|
|
|
|
|
padding: 16px 8px;
|
|
|
|
|
text-align: center;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: #c0c4cc;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.md-outline-toggle {
|
|
|
|
|
position: absolute;
|
|
|
|
|
right: -1px;
|
|
|
|
|
top: 50%;
|
|
|
|
|
transform: translateY(-50%);
|
|
|
|
|
width: 14px;
|
|
|
|
|
height: 40px;
|
|
|
|
|
border: 1px solid #e5e7eb;
|
|
|
|
|
background: #fff;
|
|
|
|
|
border-radius: 0 4px 4px 0;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
padding: 0;
|
|
|
|
|
color: #909399;
|
|
|
|
|
z-index: 2;
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
color: #409eff;
|
|
|
|
|
background: #f0f5ff;
|
|
|
|
|
}
|
|
|
|
|
.el-icon { font-size: 10px; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.md-content-wrap {
|
|
|
|
|
flex: 1;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.md-content-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
padding: 6px 10px 6px 12px;
|
|
|
|
|
border-bottom: 1px solid #f0f0f0;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
|
|
|
|
.md-content-title {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: #606266;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.full-preview-btn {
|
|
|
|
|
color: #909399;
|
|
|
|
|
padding: 4px;
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
color: #409eff;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.md-content-scroll {
|
|
|
|
|
flex: 1;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
padding: 16px 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* markdown body 样式 */
|
|
|
|
|
:deep(.markdown-body) {
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
line-height: 1.8;
|
|
|
|
|
color: #303133;
|
|
|
|
|
word-break: break-word;
|
|
|
|
|
|
|
|
|
|
h1 {
|
|
|
|
|
font-size: 18px; font-weight: 700; margin: 16px 0 12px; padding-bottom: 8px;
|
|
|
|
|
border-bottom: 1px solid #eaecef; color: #111827;
|
|
|
|
|
}
|
|
|
|
|
h2 {
|
|
|
|
|
font-size: 15px; font-weight: 600; margin: 14px 0 10px; color: #1f2937;
|
|
|
|
|
}
|
|
|
|
|
h3 { font-size: 14px; font-weight: 600; margin: 12px 0 8px; color: #374151; }
|
|
|
|
|
h4,h5,h6 { font-size: 13px; font-weight: 600; margin: 10px 0 6px; color: #4b5563; }
|
|
|
|
|
p { margin: 8px 0; }
|
|
|
|
|
ul, ol { padding-left: 22px; margin: 8px 0; }
|
|
|
|
|
li { margin: 3px 0; }
|
|
|
|
|
table {
|
|
|
|
|
border-collapse: collapse;
|
|
|
|
|
width: 100%;
|
|
|
|
|
margin: 10px 0;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
}
|
|
|
|
|
th, td {
|
|
|
|
|
border: 1px solid #e5e7eb;
|
|
|
|
|
padding: 6px 10px;
|
|
|
|
|
text-align: left;
|
|
|
|
|
}
|
|
|
|
|
th { background: #f9fafb; font-weight: 600; }
|
|
|
|
|
code {
|
|
|
|
|
background: #f3f4f6;
|
|
|
|
|
padding: 1px 5px;
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: #d63384;
|
|
|
|
|
}
|
|
|
|
|
pre.hljs {
|
|
|
|
|
background: #f6f8fa;
|
|
|
|
|
padding: 12px;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
}
|
|
|
|
|
pre code {
|
|
|
|
|
background: transparent;
|
|
|
|
|
padding: 0;
|
|
|
|
|
color: inherit;
|
|
|
|
|
}
|
|
|
|
|
blockquote {
|
|
|
|
|
border-left: 3px solid #409eff;
|
|
|
|
|
padding: 4px 12px;
|
|
|
|
|
margin: 10px 0;
|
|
|
|
|
background: #f0f7ff;
|
|
|
|
|
color: #606266;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
|
|
|
|
|
<!-- 大窗口预览弹窗样式(append-to-body teleport 至 body 外,需使用非 scoped 样式) -->
|
|
|
|
|
<style lang="scss">
|
|
|
|
|
.full-preview-dialog {
|
|
|
|
|
.el-dialog__body {
|
|
|
|
|
padding: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.full-preview-body {
|
|
|
|
|
height: calc(100vh - 120px);
|
|
|
|
|
display: flex;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
background: #fff;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 左侧大纲 */
|
|
|
|
|
.full-outline-wrap {
|
|
|
|
|
flex: 0 0 220px;
|
|
|
|
|
background: #fafafa;
|
|
|
|
|
border-right: 1px solid #f0f0f0;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.full-outline-header {
|
|
|
|
|
padding: 12px 14px 8px;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: #606266;
|
|
|
|
|
border-bottom: 1px solid #f0f0f0;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.full-outline-body {
|
|
|
|
|
flex: 1;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
padding: 6px 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.full-outline-list {
|
|
|
|
|
list-style: none;
|
|
|
|
|
margin: 0;
|
|
|
|
|
padding: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.full-outline-item {
|
|
|
|
|
padding: 6px 14px;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
color: #606266;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
border-left: 2px solid transparent;
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
background: #f0f5ff;
|
|
|
|
|
color: #409eff;
|
|
|
|
|
}
|
|
|
|
|
&.active {
|
|
|
|
|
color: #409eff;
|
|
|
|
|
background: #ecf5ff;
|
|
|
|
|
border-left-color: #409eff;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
}
|
|
|
|
|
&.level-1 { padding-left: 14px; font-weight: 600; }
|
|
|
|
|
&.level-2 { padding-left: 24px; }
|
|
|
|
|
&.level-3 { padding-left: 34px; font-size: 12px; }
|
|
|
|
|
&.level-4 { padding-left: 44px; font-size: 12px; color: #909399; }
|
|
|
|
|
&.level-5, &.level-6 { padding-left: 54px; font-size: 12px; color: #909399; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 右侧内容 */
|
|
|
|
|
.full-content-wrap {
|
|
|
|
|
flex: 1;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.full-content-scroll {
|
|
|
|
|
flex: 1;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
padding: 24px 32px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.markdown-body {
|
|
|
|
|
max-width: 900px;
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
line-height: 1.8;
|
|
|
|
|
color: #303133;
|
|
|
|
|
word-break: break-word;
|
|
|
|
|
|
|
|
|
|
h1 {
|
|
|
|
|
font-size: 20px; font-weight: 700; margin: 18px 0 14px; padding-bottom: 8px;
|
|
|
|
|
border-bottom: 1px solid #eaecef; color: #111827;
|
|
|
|
|
}
|
|
|
|
|
h2 {
|
|
|
|
|
font-size: 17px; font-weight: 600; margin: 16px 0 12px; color: #1f2937;
|
|
|
|
|
}
|
|
|
|
|
h3 { font-size: 15px; font-weight: 600; margin: 14px 0 10px; color: #374151; }
|
|
|
|
|
h4, h5, h6 { font-size: 14px; font-weight: 600; margin: 12px 0 8px; color: #4b5563; }
|
|
|
|
|
p { margin: 10px 0; }
|
|
|
|
|
ul, ol { padding-left: 24px; margin: 10px 0; }
|
|
|
|
|
li { margin: 4px 0; }
|
|
|
|
|
table {
|
|
|
|
|
border-collapse: collapse;
|
|
|
|
|
width: 100%;
|
|
|
|
|
margin: 12px 0;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
}
|
|
|
|
|
th, td {
|
|
|
|
|
border: 1px solid #e5e7eb;
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
text-align: left;
|
|
|
|
|
}
|
|
|
|
|
th { background: #f9fafb; font-weight: 600; }
|
|
|
|
|
code {
|
|
|
|
|
background: #f3f4f6;
|
|
|
|
|
padding: 2px 6px;
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
color: #d63384;
|
|
|
|
|
}
|
|
|
|
|
pre.hljs {
|
|
|
|
|
background: #f6f8fa;
|
|
|
|
|
padding: 14px;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
}
|
|
|
|
|
pre code {
|
|
|
|
|
background: transparent;
|
|
|
|
|
padding: 0;
|
|
|
|
|
color: inherit;
|
|
|
|
|
}
|
|
|
|
|
blockquote {
|
|
|
|
|
border-left: 3px solid #409eff;
|
|
|
|
|
padding: 6px 14px;
|
|
|
|
|
margin: 12px 0;
|
|
|
|
|
background: #f0f7ff;
|
|
|
|
|
color: #606266;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|