feat(思维导图助手):测试优化功能页面
parent
e5d281a7da
commit
a999121ff8
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,28 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>思维导图</title>
|
||||
<style>
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
#app {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
<script type="module" crossorigin src="/assets/index-DnACqG0E.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/vendor-DwrfALIr.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/markdown-_Dy2zPzC.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-HnN589X2.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -12,6 +12,7 @@
|
|||
"build": "^0.1.4",
|
||||
"d3": "^7.9.0",
|
||||
"element-plus": "^2.4.0",
|
||||
"handlebars": "^4.7.9",
|
||||
"markdown-it": "^14.0.0",
|
||||
"markmap-lib": "^0.18.12",
|
||||
"markmap-toolbar": "^0.18.12",
|
||||
|
|
@ -2261,6 +2262,40 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/handlebars": {
|
||||
"version": "4.7.9",
|
||||
"resolved": "https://registry.npmmirror.com/handlebars/-/handlebars-4.7.9.tgz",
|
||||
"integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.5",
|
||||
"neo-async": "^2.6.2",
|
||||
"source-map": "^0.6.1",
|
||||
"wordwrap": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"handlebars": "bin/handlebars"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.7"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"uglify-js": "^3.1.4"
|
||||
}
|
||||
},
|
||||
"node_modules/handlebars/node_modules/uglify-js": {
|
||||
"version": "3.19.3",
|
||||
"resolved": "https://registry.npmmirror.com/uglify-js/-/uglify-js-3.19.3.tgz",
|
||||
"integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==",
|
||||
"license": "BSD-2-Clause",
|
||||
"optional": true,
|
||||
"bin": {
|
||||
"uglifyjs": "bin/uglifyjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
|
|
@ -2684,6 +2719,15 @@
|
|||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz",
|
||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/moo-server": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/moo-server/-/moo-server-1.3.0.tgz",
|
||||
|
|
@ -2723,6 +2767,12 @@
|
|||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/neo-async": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmmirror.com/neo-async/-/neo-async-2.6.2.tgz",
|
||||
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/normalize-wheel-es": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz",
|
||||
|
|
@ -2995,6 +3045,15 @@
|
|||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
|
|
@ -3299,6 +3358,12 @@
|
|||
"node": ">= 12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/wordwrap": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/wordwrap/-/wordwrap-1.0.0.tgz",
|
||||
"integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/wrench": {
|
||||
"version": "1.3.9",
|
||||
"resolved": "https://registry.npmjs.org/wrench/-/wrench-1.3.9.tgz",
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
"build": "^0.1.4",
|
||||
"d3": "^7.9.0",
|
||||
"element-plus": "^2.4.0",
|
||||
"handlebars": "^4.7.9",
|
||||
"markdown-it": "^14.0.0",
|
||||
"markmap-lib": "^0.18.12",
|
||||
"markmap-toolbar": "^0.18.12",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,66 @@
|
|||
<template>
|
||||
<div class="markdown-renderer">
|
||||
<div v-if="content" class="markdown-content" v-html="renderedContent"></div>
|
||||
<template v-if="content">
|
||||
<div class="render-actions">
|
||||
<div class="render-actions-left">
|
||||
<el-segmented
|
||||
v-if="mode === 'markdown'"
|
||||
v-model="activeFlavor"
|
||||
:options="flavorOptions"
|
||||
size="small"
|
||||
/>
|
||||
<el-segmented
|
||||
v-else-if="mode === 'richtext'"
|
||||
v-model="activeRichtextFormat"
|
||||
:options="richtextOptions"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
<el-button type="primary" size="small" class="action-button primary" @click="handleDownload()">
|
||||
<template #icon>
|
||||
<el-icon><Download /></el-icon>
|
||||
</template>
|
||||
下载
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="render-content">
|
||||
<div v-if="mode === 'markdown'" class="render-shell">
|
||||
<div ref="markdownRef" class="markdown-content rendered-html" v-html="renderedContent"></div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="mode === 'html'" class="render-shell">
|
||||
<div ref="htmlRef" class="markdown-content rendered-html" v-html="renderedContent"></div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="mode === 'pdf'" class="render-shell">
|
||||
<div class="pdf-stage">
|
||||
<div ref="pdfRef" class="pdf-preview-pages">
|
||||
<div
|
||||
v-for="(page, index) in pdfPages"
|
||||
:key="`pdf-page-${index}`"
|
||||
class="pdf-page"
|
||||
>
|
||||
<div class="markdown-content pdf-content" v-html="page"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref="pdfMeasureRef"
|
||||
class="pdf-measure markdown-content pdf-content"
|
||||
v-html="renderedContent"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div v-else class="render-shell">
|
||||
<div class="richtext-stage">
|
||||
<article ref="richtextRef" class="richtext-document">
|
||||
<div class="markdown-content richtext-content" v-html="renderedContent"></div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-else class="empty-state">
|
||||
<el-empty :description="$t('results.noResults')" />
|
||||
</div>
|
||||
|
|
@ -8,114 +68,477 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import type { PropType } from 'vue'
|
||||
import { Download } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { loadHtml2Pdf } from '@/utils/wordToPdf'
|
||||
|
||||
type RenderMode = 'markdown' | 'html' | 'pdf' | 'richtext'
|
||||
type FlavorMode = 'commonmark' | 'gfm'
|
||||
type RichtextFormat = 'word' | 'rtf'
|
||||
|
||||
const props = defineProps({
|
||||
content: {
|
||||
type: String as PropType<string>,
|
||||
default: ''
|
||||
},
|
||||
mode: {
|
||||
type: String as PropType<RenderMode>,
|
||||
default: 'markdown'
|
||||
},
|
||||
flavor: {
|
||||
type: String as PropType<FlavorMode>,
|
||||
default: 'gfm'
|
||||
}
|
||||
})
|
||||
|
||||
const md = new MarkdownIt({
|
||||
html: true,
|
||||
linkify: true,
|
||||
typographer: true,
|
||||
breaks: true
|
||||
})
|
||||
const activeFlavor = ref<FlavorMode>(props.flavor)
|
||||
const activeRichtextFormat = ref<RichtextFormat>('word')
|
||||
const markdownRef = ref<HTMLElement | null>(null)
|
||||
const htmlRef = ref<HTMLElement | null>(null)
|
||||
const pdfRef = ref<HTMLElement | null>(null)
|
||||
const pdfMeasureRef = ref<HTMLElement | null>(null)
|
||||
const richtextRef = ref<HTMLElement | null>(null)
|
||||
const pdfPages = ref<string[]>([])
|
||||
|
||||
// 添加LaTeX支持
|
||||
md.use(function (md) {
|
||||
// 行内公式 $$...$$
|
||||
md.inline.ruler.after('escape', 'math_inline', function (state, silent) {
|
||||
let start = state.pos
|
||||
if (state.src.charCodeAt(start) !== 0x24/* $ */) return false
|
||||
if (state.src.charCodeAt(start + 1) !== 0x24/* $ */) return false
|
||||
watch(
|
||||
() => props.flavor,
|
||||
(value) => {
|
||||
activeFlavor.value = value
|
||||
}
|
||||
)
|
||||
|
||||
let pos = start + 2
|
||||
let found = false
|
||||
const flavorOptions = [
|
||||
{ label: 'CommonMark', value: 'commonmark' },
|
||||
{ label: 'GFM', value: 'gfm' }
|
||||
]
|
||||
|
||||
while (pos < state.posMax) {
|
||||
if (state.src.charCodeAt(pos) === 0x24/* $ */ &&
|
||||
state.src.charCodeAt(pos + 1) === 0x24/* $ */) {
|
||||
found = true
|
||||
break
|
||||
const richtextOptions = [
|
||||
{ label: 'Word', value: 'word' },
|
||||
{ label: 'RTF', value: 'rtf' }
|
||||
]
|
||||
|
||||
function applyMathSupport(md: MarkdownIt) {
|
||||
md.use(function (md) {
|
||||
md.inline.ruler.after('escape', 'math_inline', function (state, silent) {
|
||||
const start = state.pos
|
||||
if (state.src.charCodeAt(start) !== 0x24) return false
|
||||
if (state.src.charCodeAt(start + 1) !== 0x24) return false
|
||||
|
||||
let pos = start + 2
|
||||
let found = false
|
||||
while (pos < state.posMax) {
|
||||
if (state.src.charCodeAt(pos) === 0x24 && state.src.charCodeAt(pos + 1) === 0x24) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
pos++
|
||||
}
|
||||
pos++
|
||||
}
|
||||
if (!found) return false
|
||||
|
||||
if (!found) return false
|
||||
if (!silent) {
|
||||
const token = state.push('math_inline', 'span', 0)
|
||||
token.content = state.src.slice(start + 2, pos)
|
||||
token.markup = '$$'
|
||||
}
|
||||
|
||||
if (!silent) {
|
||||
const token = state.push('math_inline', 'span', 0)
|
||||
token.content = state.src.slice(start + 2, pos)
|
||||
token.markup = '$$'
|
||||
}
|
||||
state.pos = pos + 2
|
||||
return true
|
||||
})
|
||||
|
||||
state.pos = pos + 2
|
||||
return true
|
||||
md.inline.ruler.after('escape', 'math_inline_single', function (state, silent) {
|
||||
const start = state.pos
|
||||
if (state.src.charCodeAt(start) !== 0x24) return false
|
||||
if (state.src.charCodeAt(start + 1) === 0x24) return false
|
||||
|
||||
let pos = start + 1
|
||||
let found = false
|
||||
while (pos < state.posMax) {
|
||||
if (state.src.charCodeAt(pos) === 0x24) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
pos++
|
||||
}
|
||||
if (!found) return false
|
||||
|
||||
if (!silent) {
|
||||
const token = state.push('math_inline_single', 'span', 0)
|
||||
token.content = state.src.slice(start + 1, pos)
|
||||
token.markup = '$'
|
||||
}
|
||||
|
||||
state.pos = pos + 1
|
||||
return true
|
||||
})
|
||||
})
|
||||
|
||||
// 行内公式 $...$
|
||||
md.inline.ruler.after('escape', 'math_inline_single', function (state, silent) {
|
||||
let start = state.pos
|
||||
if (state.src.charCodeAt(start) !== 0x24/* $ */) return false
|
||||
if (state.src.charCodeAt(start + 1) === 0x24/* $ */) return false // 避免与$$冲突
|
||||
md.renderer.rules.math_inline = function(tokens, idx) {
|
||||
return `<span class="math-inline">$$${tokens[idx].content}$$</span>`
|
||||
}
|
||||
|
||||
let pos = start + 1
|
||||
let found = false
|
||||
|
||||
while (pos < state.posMax) {
|
||||
if (state.src.charCodeAt(pos) === 0x24/* $ */) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
pos++
|
||||
}
|
||||
|
||||
if (!found) return false
|
||||
|
||||
if (!silent) {
|
||||
const token = state.push('math_inline_single', 'span', 0)
|
||||
token.content = state.src.slice(start + 1, pos)
|
||||
token.markup = '$'
|
||||
}
|
||||
|
||||
state.pos = pos + 1
|
||||
return true
|
||||
})
|
||||
})
|
||||
|
||||
// 自定义渲染器
|
||||
md.renderer.rules.math_inline = function(tokens, idx) {
|
||||
return `<span class="math-inline">$$${tokens[idx].content}$$</span>`
|
||||
md.renderer.rules.math_inline_single = function(tokens, idx) {
|
||||
return `<span class="math-inline">$${tokens[idx].content}$</span>`
|
||||
}
|
||||
}
|
||||
|
||||
md.renderer.rules.math_inline_single = function(tokens, idx) {
|
||||
return `<span class="math-inline">$${tokens[idx].content}$</span>`
|
||||
function createMarkdownRenderer(flavor: FlavorMode) {
|
||||
const md = flavor === 'commonmark'
|
||||
? new MarkdownIt('commonmark', {
|
||||
html: true,
|
||||
linkify: true,
|
||||
typographer: true,
|
||||
breaks: false
|
||||
})
|
||||
: new MarkdownIt({
|
||||
html: true,
|
||||
linkify: true,
|
||||
typographer: true,
|
||||
breaks: true
|
||||
})
|
||||
|
||||
applyMathSupport(md)
|
||||
return md
|
||||
}
|
||||
|
||||
const renderedContent = computed(() => {
|
||||
if (!props.content) return ''
|
||||
return md.render(props.content)
|
||||
const flavor = props.mode === 'markdown' ? activeFlavor.value : 'gfm'
|
||||
const renderer = createMarkdownRenderer(flavor)
|
||||
return renderer.render(props.content)
|
||||
})
|
||||
|
||||
async function recalculatePdfPages() {
|
||||
if (props.mode !== 'pdf') {
|
||||
pdfPages.value = []
|
||||
return
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
|
||||
const measureEl = pdfMeasureRef.value
|
||||
if (!measureEl) {
|
||||
pdfPages.value = renderedContent.value ? [renderedContent.value] : []
|
||||
return
|
||||
}
|
||||
|
||||
const children = Array.from(measureEl.children) as HTMLElement[]
|
||||
if (children.length === 0) {
|
||||
pdfPages.value = renderedContent.value ? [renderedContent.value] : []
|
||||
return
|
||||
}
|
||||
|
||||
const maxPageHeight = 980
|
||||
const pages: string[] = []
|
||||
let currentPageHtml = ''
|
||||
let currentHeight = 0
|
||||
|
||||
children.forEach((child) => {
|
||||
const childHeight = child.offsetHeight || child.scrollHeight || 0
|
||||
const childHtml = child.outerHTML
|
||||
|
||||
if (currentPageHtml && currentHeight + childHeight > maxPageHeight) {
|
||||
pages.push(currentPageHtml)
|
||||
currentPageHtml = childHtml
|
||||
currentHeight = childHeight
|
||||
return
|
||||
}
|
||||
|
||||
currentPageHtml += childHtml
|
||||
currentHeight += childHeight
|
||||
})
|
||||
|
||||
if (currentPageHtml) {
|
||||
pages.push(currentPageHtml)
|
||||
}
|
||||
|
||||
pdfPages.value = pages.length > 0 ? pages : [renderedContent.value]
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.mode, renderedContent.value],
|
||||
() => {
|
||||
void recalculatePdfPages()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
void recalculatePdfPages()
|
||||
})
|
||||
|
||||
const baseFileName = computed(() => `markdown_render_${new Date().getTime()}`)
|
||||
|
||||
function downloadBlob(blob: Blob, fileName: string) {
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = fileName
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
function createFullHtmlDocument(innerHtml: string, title: string) {
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>${title}</title>
|
||||
<style>
|
||||
body { font-family: "Microsoft YaHei", Arial, sans-serif; color: #1f2937; padding: 32px; line-height: 1.75; }
|
||||
h1, h2, h3, h4, h5, h6 { color: #111827; }
|
||||
pre { background: #f5f7fa; padding: 16px; border-radius: 8px; overflow: auto; }
|
||||
code { background: #f5f7fa; padding: 2px 4px; border-radius: 4px; }
|
||||
table { border-collapse: collapse; width: 100%; }
|
||||
th, td { border: 1px solid #dcdfe6; padding: 8px 12px; text-align: left; }
|
||||
blockquote { border-left: 4px solid #dcdfe6; margin: 1em 0; padding-left: 1em; color: #606266; }
|
||||
img { max-width: 100%; }
|
||||
</style>
|
||||
</head>
|
||||
<body>${innerHtml}</body>
|
||||
</html>`
|
||||
}
|
||||
|
||||
function htmlToPlainText(html: string) {
|
||||
const temp = document.createElement('div')
|
||||
temp.innerHTML = html
|
||||
return temp.innerText || temp.textContent || ''
|
||||
}
|
||||
|
||||
function escapeRtf(text: string) {
|
||||
return text
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/\{/g, '\\{')
|
||||
.replace(/\}/g, '\\}')
|
||||
.replace(/\n/g, '\\par\n')
|
||||
}
|
||||
|
||||
async function exportHtml(innerHtml: string, fileName: string) {
|
||||
const html = createFullHtmlDocument(innerHtml, fileName)
|
||||
downloadBlob(new Blob([html], { type: 'text/html;charset=utf-8' }), fileName)
|
||||
}
|
||||
|
||||
async function exportMarkdown(markdown: string, fileName: string) {
|
||||
downloadBlob(new Blob([markdown], { type: 'text/markdown;charset=utf-8' }), fileName)
|
||||
}
|
||||
|
||||
async function exportPdf(element: HTMLElement | null, fileName: string) {
|
||||
if (!element) {
|
||||
ElMessage.error('当前没有可导出的 PDF 内容')
|
||||
return
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
await loadHtml2Pdf()
|
||||
|
||||
const pdfBlob = await window.html2pdf()
|
||||
.set({
|
||||
margin: 10,
|
||||
filename: fileName,
|
||||
image: { type: 'jpeg', quality: 0.98 },
|
||||
html2canvas: { scale: 2, useCORS: true },
|
||||
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
|
||||
})
|
||||
.from(element)
|
||||
.outputPdf('blob')
|
||||
|
||||
downloadBlob(pdfBlob, fileName)
|
||||
}
|
||||
|
||||
async function exportDoc(innerHtml: string, fileName: string) {
|
||||
const docHtml = createFullHtmlDocument(innerHtml, fileName)
|
||||
downloadBlob(new Blob(['\ufeff', docHtml], { type: 'application/msword' }), fileName)
|
||||
}
|
||||
|
||||
async function exportRtf(innerHtml: string, fileName: string) {
|
||||
const plainText = htmlToPlainText(innerHtml)
|
||||
const rtf = `{\\rtf1\\ansi\\deff0 {\\fonttbl{\\f0 Calibri;}}\n\\fs24 ${escapeRtf(plainText)}}`
|
||||
downloadBlob(new Blob([rtf], { type: 'application/rtf' }), fileName)
|
||||
}
|
||||
|
||||
async function handleDownload() {
|
||||
if (props.mode === 'markdown') {
|
||||
const suffix = activeFlavor.value === 'commonmark' ? '_commonmark' : '_gfm'
|
||||
await exportMarkdown(props.content, `${baseFileName.value}${suffix}.md`)
|
||||
return
|
||||
}
|
||||
if (props.mode === 'html') {
|
||||
await exportHtml(renderedContent.value, `${baseFileName.value}.html`)
|
||||
return
|
||||
}
|
||||
if (props.mode === 'pdf') {
|
||||
await exportPdf(pdfRef.value, `${baseFileName.value}.pdf`)
|
||||
return
|
||||
}
|
||||
if (activeRichtextFormat.value === 'word') {
|
||||
await exportDoc(renderedContent.value, `${baseFileName.value}.doc`)
|
||||
return
|
||||
}
|
||||
await exportRtf(renderedContent.value, `${baseFileName.value}.rtf`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.markdown-renderer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #FFFFFF;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.08);
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.render-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 16px;
|
||||
flex-shrink: 0;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
background-color: #FFFFFF;
|
||||
}
|
||||
|
||||
.render-actions-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
border-radius: 6px;
|
||||
transition: all 0.3s ease;
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.action-button.primary {
|
||||
background-color: #165dff;
|
||||
border-color: #165dff;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.action-button.primary:hover {
|
||||
background-color: #0e42d2;
|
||||
border-color: #0e42d2;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(22, 93, 255, 0.3);
|
||||
}
|
||||
|
||||
.render-shell {
|
||||
min-height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.render-content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
|
||||
.render-header {
|
||||
padding: 14px 16px;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(135deg, #f7faff 0%, #eef4ff 100%);
|
||||
border: 1px solid #d7e4ff;
|
||||
}
|
||||
|
||||
.render-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.render-desc {
|
||||
margin-top: 4px;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: #5b6472;
|
||||
}
|
||||
|
||||
.markdown-content {
|
||||
padding: 20px;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
border-radius: 10px;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.rendered-html {
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.pdf-stage {
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
background:
|
||||
radial-gradient(circle at top, rgba(22, 93, 255, 0.08), transparent 30%),
|
||||
#eef2f7;
|
||||
}
|
||||
|
||||
.pdf-preview-pages {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.pdf-page {
|
||||
width: min(100%, 840px);
|
||||
min-height: 1160px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border: 1px solid #d9dee8;
|
||||
box-shadow: 0 18px 45px rgba(15, 23, 42, 0.12);
|
||||
padding: 28px 36px 36px;
|
||||
}
|
||||
|
||||
.pdf-content {
|
||||
padding: 0;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.pdf-measure {
|
||||
position: absolute;
|
||||
left: -99999px;
|
||||
top: 0;
|
||||
width: 768px;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.richtext-stage {
|
||||
padding: 12px;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(180deg, #f3f4f6 0%, #eef2f7 100%);
|
||||
}
|
||||
|
||||
.richtext-document {
|
||||
min-height: 100%;
|
||||
background: white;
|
||||
border: 1px solid #dde3ea;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
.richtext-content {
|
||||
padding: 32px 40px 48px;
|
||||
}
|
||||
|
||||
.markdown-content :deep(h1) {
|
||||
font-size: 2em;
|
||||
margin: 0.67em 0;
|
||||
|
|
@ -136,7 +559,7 @@ const renderedContent = computed(() => {
|
|||
|
||||
.markdown-content :deep(p) {
|
||||
margin: 1em 0;
|
||||
line-height: 1.6;
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.markdown-content :deep(code) {
|
||||
|
|
@ -149,7 +572,7 @@ const renderedContent = computed(() => {
|
|||
.markdown-content :deep(pre) {
|
||||
background-color: #f5f7fa;
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
|
|
@ -200,11 +623,32 @@ const renderedContent = computed(() => {
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 400px;
|
||||
height: 100%;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.math-inline {
|
||||
font-family: 'Cambria Math', 'STIX Two Math', serif;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
</style>
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.render-actions-left {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.pdf-page {
|
||||
padding: 24px 20px 32px;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.richtext-content {
|
||||
padding: 24px 18px 32px;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -71,7 +71,16 @@ const nodeCount = computed(() => {
|
|||
})
|
||||
|
||||
const initMarkmap = async () => {
|
||||
if (!props.content || !svgRef.value) return
|
||||
if (!svgRef.value) return
|
||||
|
||||
if (!props.content) {
|
||||
svgRef.value.innerHTML = ''
|
||||
if (mmInstance && typeof mmInstance.destroy === 'function') {
|
||||
mmInstance.destroy()
|
||||
}
|
||||
mmInstance = null
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('Initializing markmap with content:', props.content.substring(0, 100) + '...')
|
||||
|
|
@ -430,4 +439,4 @@ const handleWheel = (event: WheelEvent) => {
|
|||
gap: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { documentApi, type ParseParams } from '@/api/document'
|
|||
import { convertWordToPdf, isWordFile } from '@/utils/wordToPdf'
|
||||
|
||||
// 根据上一级标题自动补全下一级标题
|
||||
function autoPromoteParagraphsToSubheading(text: string): string {
|
||||
export function autoPromoteParagraphsToSubheading(text: string): string {
|
||||
const lines = text.split('\n')
|
||||
const result: string[] = []
|
||||
let inSection = false
|
||||
|
|
@ -186,6 +186,16 @@ export function useDocumentProcessor() {
|
|||
results.value = null
|
||||
error.value = null
|
||||
}
|
||||
|
||||
// 演示模式:上传文件后由用户手动粘贴 Markdown 源码
|
||||
const initializeManualResult = () => {
|
||||
results.value = {
|
||||
markdown: '',
|
||||
source: '',
|
||||
mindmap: ''
|
||||
}
|
||||
error.value = null
|
||||
}
|
||||
|
||||
// 处理文档转换
|
||||
const processDocument = async () => {
|
||||
|
|
@ -276,9 +286,10 @@ export function useDocumentProcessor() {
|
|||
|
||||
// 方法
|
||||
handleFileUpload,
|
||||
initializeManualResult,
|
||||
clearAll,
|
||||
processDocument,
|
||||
getFormulaLabel,
|
||||
getFormulaInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
import MarkdownIt from 'markdown-it'
|
||||
import type Token from 'markdown-it/lib/token.mjs'
|
||||
|
||||
const md = new MarkdownIt({
|
||||
html: true,
|
||||
linkify: true,
|
||||
typographer: true,
|
||||
breaks: true
|
||||
})
|
||||
|
||||
type AstNode = {
|
||||
type: string
|
||||
tag?: string
|
||||
nesting?: number
|
||||
level?: number
|
||||
content?: string
|
||||
markup?: string
|
||||
info?: string
|
||||
block?: boolean
|
||||
hidden?: boolean
|
||||
map?: number[] | null
|
||||
attrs?: Record<string, string>
|
||||
children?: AstNode[]
|
||||
}
|
||||
|
||||
function tokenToAstNode(token: Token): AstNode {
|
||||
return {
|
||||
type: token.type,
|
||||
tag: token.tag || undefined,
|
||||
nesting: token.nesting,
|
||||
level: token.level,
|
||||
content: token.content || undefined,
|
||||
markup: token.markup || undefined,
|
||||
info: token.info || undefined,
|
||||
block: token.block,
|
||||
hidden: token.hidden,
|
||||
map: token.map || null,
|
||||
attrs: token.attrs ? Object.fromEntries(token.attrs) : undefined,
|
||||
children: token.children?.map(tokenToAstNode)
|
||||
}
|
||||
}
|
||||
|
||||
export function markdownToAstString(markdown: string): string {
|
||||
if (!markdown.trim()) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const tokens = md.parse(markdown, {})
|
||||
const ast = {
|
||||
type: 'root',
|
||||
children: tokens.map(tokenToAstNode)
|
||||
}
|
||||
|
||||
return JSON.stringify(ast, null, 2)
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
export interface ReplacementRule {
|
||||
id: string
|
||||
search: string
|
||||
replace: string
|
||||
}
|
||||
|
||||
export interface MarkdownTemplateRenderResult {
|
||||
markdown: string
|
||||
error: string | null
|
||||
}
|
||||
|
||||
export function createEmptyReplacementRule(): ReplacementRule {
|
||||
return {
|
||||
id: `${Date.now()}_${Math.random().toString(16).slice(2)}`,
|
||||
search: '',
|
||||
replace: ''
|
||||
}
|
||||
}
|
||||
|
||||
export function renderMarkdownTemplate(template: string, rules: ReplacementRule[]): MarkdownTemplateRenderResult {
|
||||
if (!template.trim()) {
|
||||
return {
|
||||
markdown: '',
|
||||
error: null
|
||||
}
|
||||
}
|
||||
|
||||
const activeRules = rules.filter((rule) => rule.search.trim())
|
||||
|
||||
try {
|
||||
const markdown = activeRules.reduce((current, rule) => {
|
||||
return current.split(rule.search).join(rule.replace)
|
||||
}, template)
|
||||
|
||||
return {
|
||||
markdown,
|
||||
error: null
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
markdown: template,
|
||||
error: `替换规则执行失败:${(error as Error).message}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -38,7 +38,7 @@ async function loadMammoth(): Promise<void> {
|
|||
/**
|
||||
* 动态加载 html2pdf 库
|
||||
*/
|
||||
async function loadHtml2Pdf(): Promise<void> {
|
||||
export async function loadHtml2Pdf(): Promise<void> {
|
||||
if (typeof window.html2pdf === 'undefined') {
|
||||
return new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script');
|
||||
|
|
@ -140,4 +140,4 @@ export async function convertWordToPdf(
|
|||
export function isWordFile(file: File): boolean {
|
||||
const fileName = file.name.toLowerCase();
|
||||
return fileName.endsWith('.docx') || fileName.endsWith('.doc');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,25 +1,17 @@
|
|||
<template>
|
||||
<div class="document-processor">
|
||||
<!-- 顶部标题栏 -->
|
||||
<header class="top-header">
|
||||
<h2 class="ellipsis" title="思维导图">思维导图</h2>
|
||||
<el-button
|
||||
type="text"
|
||||
@click="toggleSettings"
|
||||
class="settings-button"
|
||||
>
|
||||
设置
|
||||
</el-button>
|
||||
<el-button type="text" @click="toggleSettings" class="settings-button">
|
||||
设置
|
||||
</el-button>
|
||||
</header>
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
|
||||
<div class="main-content">
|
||||
<!-- 文件上传区域 -->
|
||||
<div class="upload-section">
|
||||
<!-- 拖拽上传区 -->
|
||||
<div
|
||||
<div
|
||||
class="drag-upload-area"
|
||||
:class="{ 'drag-over': isDragging, 'collapsed': isUploadAreaCollapsed }"
|
||||
:class="{ 'drag-over': isDragging, collapsed: isUploadAreaCollapsed }"
|
||||
@drop="handleDrop"
|
||||
@dragover.prevent="isDragging = true"
|
||||
@dragleave="isDragging = false"
|
||||
|
|
@ -28,39 +20,31 @@
|
|||
<div class="upload-content" v-show="!isUploadAreaCollapsed">
|
||||
<el-icon class="upload-icon"><Folder /></el-icon>
|
||||
<div class="upload-text">文件导入</div>
|
||||
<div class="upload-hint">支持PDF、Word、PNG格式文件,点击或拖拽文件至此处上传</div>
|
||||
<div class="upload-hint">支持PDF、Word、PNG格式文件,上传后可手动粘贴 Markdown 源码用于演示</div>
|
||||
</div>
|
||||
|
||||
<!-- 已上传文件列表 -->
|
||||
|
||||
<div class="uploaded-files">
|
||||
<div class="file-item" v-for="(file, index) in uploadedFiles" :key="index">
|
||||
<el-icon class="file-icon"><Document /></el-icon>
|
||||
<span class="file-name">{{ file.name }}</span>
|
||||
<el-button
|
||||
type="text"
|
||||
:icon="Delete"
|
||||
<el-button
|
||||
type="text"
|
||||
:icon="Delete"
|
||||
@click.stop="removeFile(index)"
|
||||
class="remove-button"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- 设置面板 -->
|
||||
<div v-if="showSettings" class="settings-panel">
|
||||
<div class="settings-header">
|
||||
<h3 class="settings-title">设置</h3>
|
||||
<el-button
|
||||
type="text"
|
||||
@click="toggleSettings"
|
||||
class="close-button"
|
||||
>
|
||||
<el-button type="text" @click="toggleSettings" class="close-button">
|
||||
<el-icon><Close /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
<ConfigPanel
|
||||
<ConfigPanel
|
||||
v-model="config"
|
||||
:backend-options="backendOptions"
|
||||
:language-options="languageOptions"
|
||||
|
|
@ -68,61 +52,107 @@
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 结果显示区域 -->
|
||||
|
||||
<div class="result-section">
|
||||
<!-- 标签页 -->
|
||||
<el-tabs v-model="activeTab" class="result-tabs">
|
||||
<el-tab-pane label="Markdown 渲染" name="markdown" />
|
||||
<el-tab-pane label="Markdown 源码" name="source" />
|
||||
<el-tab-pane name="markdown">
|
||||
<template #label>
|
||||
<el-dropdown trigger="hover" @command="handleMarkdownRenderModeChange">
|
||||
<span class="source-tab-label">
|
||||
{{ markdownRenderModeLabel }}
|
||||
<el-icon class="source-tab-arrow"><ArrowDown /></el-icon>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="markdown">Markdown渲染</el-dropdown-item>
|
||||
<el-dropdown-item command="html">HTML渲染</el-dropdown-item>
|
||||
<el-dropdown-item command="pdf">PDF渲染</el-dropdown-item>
|
||||
<el-dropdown-item command="richtext">富文本格式渲染</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane name="source">
|
||||
<template #label>
|
||||
<el-dropdown trigger="hover" @command="handleSourceViewModeChange">
|
||||
<span class="source-tab-label">
|
||||
{{ sourceViewMode === 'markdown' ? 'Markdown 源码' : 'AST抽象语法树' }}
|
||||
<el-icon class="source-tab-arrow"><ArrowDown /></el-icon>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="markdown">Markdown 源码</el-dropdown-item>
|
||||
<el-dropdown-item command="ast">AST抽象语法树</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="思维导图" name="mindmap" />
|
||||
</el-tabs>
|
||||
|
||||
<!-- 结果内容 -->
|
||||
|
||||
<div class="result-content">
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="isProcessing" class="loading-container">
|
||||
<el-icon class="loading-icon"><Loading /></el-icon>
|
||||
<p class="loading-text">正在处理文档...</p>
|
||||
</div>
|
||||
|
||||
<!-- 无结果状态 -->
|
||||
|
||||
<div v-else-if="!results" class="empty-state">
|
||||
<div class="empty-icon"></div>
|
||||
<p class="empty-text">暂无转换结果</p>
|
||||
</div>
|
||||
|
||||
<!-- 结果内容 -->
|
||||
|
||||
<div v-else class="result-tab-content result-fade-in">
|
||||
<!-- Markdown 渲染 -->
|
||||
<div v-show="activeTab === 'markdown'" class="markdown-content">
|
||||
<div class="markdown-box">
|
||||
<MarkdownRenderer :content="results.markdown || ''" />
|
||||
<MarkdownRenderer
|
||||
:content="renderedMarkdownContent"
|
||||
:mode="markdownRenderMode"
|
||||
:flavor="markdownCompatibilityFlavor"
|
||||
@update:mode="handleMarkdownRenderModeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Markdown 源码 -->
|
||||
|
||||
<div v-show="activeTab === 'source'" class="source-content">
|
||||
<el-input
|
||||
v-model="results.source"
|
||||
type="textarea"
|
||||
:rows="20"
|
||||
class="source-textarea"
|
||||
/>
|
||||
<div class="source-toolbar">
|
||||
<el-button
|
||||
:type="hasActiveReplacementRules ? 'primary' : 'default'"
|
||||
class="replacement-config-button"
|
||||
@click="openReplacementDialog"
|
||||
>
|
||||
{{ hasActiveReplacementRules ? `动态模板渲染(已配置${activeReplacementRuleCount}条)` : '动态模板渲染' }}
|
||||
</el-button>
|
||||
<div v-if="templateRenderError" class="template-error-inline">
|
||||
{{ templateRenderError }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="source-panel">
|
||||
<el-input
|
||||
v-model="sourcePanelContent"
|
||||
type="textarea"
|
||||
:rows="20"
|
||||
:readonly="sourceViewMode === 'ast'"
|
||||
:placeholder="sourceViewMode === 'markdown' ? '请在这里粘贴 Markdown 模板源码' : 'AST 内容将根据替换后的 Markdown 自动生成'"
|
||||
class="source-textarea"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 思维导图 -->
|
||||
|
||||
<div v-show="activeTab === 'mindmap'" class="mindmap-content">
|
||||
<div class="mindmap-box">
|
||||
<MindMapRenderer :content="results?.mindmap || ''" />
|
||||
<MindMapRenderer :content="renderedMindmapContent" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 隐藏的文件输入 -->
|
||||
|
||||
<input
|
||||
ref="fileInput"
|
||||
type="file"
|
||||
|
|
@ -131,28 +161,65 @@
|
|||
style="position: absolute; width: 0; height: 0; overflow: hidden;"
|
||||
@change="handleFileInputChange"
|
||||
/>
|
||||
|
||||
<el-dialog
|
||||
v-model="showReplacementDialog"
|
||||
title="动态模板渲染配置"
|
||||
width="720px"
|
||||
:fullscreen="isMobileDialog"
|
||||
destroy-on-close
|
||||
class="replacement-dialog"
|
||||
>
|
||||
<div class="replacement-dialog-body">
|
||||
<div class="replacement-dialog-tip">
|
||||
支持字符替换。保存后会实时作用到 Markdown 预览、AST、思维导图和导出结果。
|
||||
</div>
|
||||
|
||||
<div class="replacement-rule-list">
|
||||
<div v-for="(rule, index) in draftReplacementRules" :key="rule.id" class="replacement-rule-row">
|
||||
<div class="replacement-rule-head">
|
||||
<div class="replacement-rule-index">规则 {{ index + 1 }}</div>
|
||||
</div>
|
||||
<el-input v-model="rule.search" placeholder="被替换字符,例如:{{title}}" class="replacement-rule-input" />
|
||||
<el-input v-model="rule.replace" placeholder="替换成,例如:项目演示标题" class="replacement-rule-input" />
|
||||
<el-button class="replacement-delete-button" @click="removeDraftRule(index)">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-button class="add-rule-button" @click="addDraftRule">新增替换规则</el-button>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="replacement-dialog-footer">
|
||||
<el-button @click="resetDraftRules">重置</el-button>
|
||||
<el-button @click="showReplacementDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="saveReplacementRules">保存生效</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { Delete, Document, Folder, Setting, Close, Loading } from '@element-plus/icons-vue'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { Delete, Document, Folder, Close, Loading, ArrowDown } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import ConfigPanel from '@/components/ConfigPanel.vue'
|
||||
import MarkdownRenderer from '@/components/MarkdownRenderer.vue'
|
||||
import MindMapRenderer from '@/components/MindMapRenderer.vue'
|
||||
import { useDocumentProcessor } from '@/composables/useDocumentProcessor'
|
||||
import { autoPromoteParagraphsToSubheading, useDocumentProcessor } from '@/composables/useDocumentProcessor'
|
||||
import { markdownToAstString } from '@/utils/markdownAst'
|
||||
import { createEmptyReplacementRule, renderMarkdownTemplate, type ReplacementRule } from '@/utils/markdownTemplate'
|
||||
|
||||
// 使用组合式函数
|
||||
const {
|
||||
uploadedFiles,
|
||||
config,
|
||||
results,
|
||||
isProcessing,
|
||||
error,
|
||||
backendOptions,
|
||||
languageOptions,
|
||||
clearAll,
|
||||
processDocument: originalProcessDocument,
|
||||
initializeManualResult,
|
||||
handleFileUpload
|
||||
} = useDocumentProcessor()
|
||||
|
||||
|
|
@ -161,10 +228,54 @@ const fileInput = ref<HTMLInputElement | null>(null)
|
|||
const isDragging = ref(false)
|
||||
const activeTab = ref('markdown')
|
||||
const isUploadAreaCollapsed = ref(false)
|
||||
const markdownRenderMode = ref<'markdown' | 'html' | 'pdf' | 'richtext'>('markdown')
|
||||
const markdownCompatibilityFlavor = ref<'commonmark' | 'gfm'>('gfm')
|
||||
const sourceViewMode = ref<'markdown' | 'ast'>('markdown')
|
||||
const showReplacementDialog = ref(false)
|
||||
const isMobileDialog = ref(false)
|
||||
const replacementRules = ref<ReplacementRule[]>([])
|
||||
const draftReplacementRules = ref<ReplacementRule[]>([])
|
||||
|
||||
const manualMarkdownContent = computed(() => results.value?.source || '')
|
||||
const templateRenderResult = computed(() => renderMarkdownTemplate(manualMarkdownContent.value, replacementRules.value))
|
||||
const renderedMarkdownContent = computed(() => templateRenderResult.value.markdown)
|
||||
const renderedMindmapContent = computed(() => autoPromoteParagraphsToSubheading(renderedMarkdownContent.value))
|
||||
const templateRenderError = computed(() => templateRenderResult.value.error)
|
||||
const activeReplacementRuleCount = computed(() => replacementRules.value.filter((rule) => rule.search.trim()).length)
|
||||
const hasActiveReplacementRules = computed(() => activeReplacementRuleCount.value > 0)
|
||||
|
||||
const markdownRenderModeLabel = computed(() => {
|
||||
const labels = {
|
||||
markdown: 'Markdown渲染',
|
||||
html: 'HTML渲染',
|
||||
pdf: 'PDF渲染',
|
||||
richtext: '富文本格式渲染'
|
||||
}
|
||||
return labels[markdownRenderMode.value]
|
||||
})
|
||||
|
||||
const cloneRules = (rules: ReplacementRule[]) => rules.map((rule) => ({ ...rule }))
|
||||
|
||||
const updateViewportState = () => {
|
||||
isMobileDialog.value = window.innerWidth <= 768
|
||||
}
|
||||
|
||||
const sourcePanelContent = computed({
|
||||
get: () => {
|
||||
if (sourceViewMode.value === 'ast') {
|
||||
return markdownToAstString(renderedMarkdownContent.value)
|
||||
}
|
||||
return manualMarkdownContent.value
|
||||
},
|
||||
set: (value: string) => {
|
||||
if (sourceViewMode.value === 'markdown' && results.value) {
|
||||
results.value.source = value
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const toggleSettings = () => {
|
||||
if (showSettings.value) {
|
||||
// 关闭设置面板,添加动画
|
||||
const settingsPanel = document.querySelector('.settings-panel')
|
||||
if (settingsPanel) {
|
||||
settingsPanel.classList.remove('showing')
|
||||
|
|
@ -172,20 +283,16 @@ const toggleSettings = () => {
|
|||
setTimeout(() => {
|
||||
showSettings.value = false
|
||||
settingsPanel.classList.remove('hiding')
|
||||
// 移除点击外部关闭事件
|
||||
document.removeEventListener('click', handleClickOutside)
|
||||
}, 400)
|
||||
}
|
||||
} else {
|
||||
// 打开设置面板
|
||||
showSettings.value = true
|
||||
// 确保 DOM 更新后添加 showing 类
|
||||
setTimeout(() => {
|
||||
const settingsPanel = document.querySelector('.settings-panel')
|
||||
if (settingsPanel) {
|
||||
settingsPanel.classList.add('showing')
|
||||
}
|
||||
// 添加点击外部关闭事件
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
}, 10)
|
||||
}
|
||||
|
|
@ -194,10 +301,13 @@ const toggleSettings = () => {
|
|||
const handleClickOutside = (event: MouseEvent) => {
|
||||
const settingsPanel = document.querySelector('.settings-panel')
|
||||
const settingsButton = document.querySelector('.settings-button')
|
||||
|
||||
if (settingsPanel && !settingsPanel.contains(event.target as Node) &&
|
||||
settingsButton && !settingsButton.contains(event.target as Node)) {
|
||||
// 关闭设置面板,添加动画
|
||||
|
||||
if (
|
||||
settingsPanel &&
|
||||
!settingsPanel.contains(event.target as Node) &&
|
||||
settingsButton &&
|
||||
!settingsButton.contains(event.target as Node)
|
||||
) {
|
||||
settingsPanel.classList.remove('showing')
|
||||
settingsPanel.classList.add('hiding')
|
||||
setTimeout(() => {
|
||||
|
|
@ -212,24 +322,68 @@ const handleBackendChange = (backend: string) => {
|
|||
console.log('Backend changed to:', backend)
|
||||
}
|
||||
|
||||
const triggerUpload = () => {
|
||||
if (fileInput.value) {
|
||||
fileInput.value.click()
|
||||
const handleMarkdownRenderModeChange = (command: 'markdown' | 'html' | 'pdf' | 'richtext') => {
|
||||
markdownRenderMode.value = command
|
||||
activeTab.value = 'markdown'
|
||||
}
|
||||
|
||||
const handleSourceViewModeChange = (command: string) => {
|
||||
if (command === 'markdown' || command === 'ast') {
|
||||
sourceViewMode.value = command
|
||||
activeTab.value = 'source'
|
||||
}
|
||||
}
|
||||
|
||||
const openReplacementDialog = () => {
|
||||
draftReplacementRules.value = cloneRules(replacementRules.value)
|
||||
if (draftReplacementRules.value.length === 0) {
|
||||
draftReplacementRules.value = [createEmptyReplacementRule()]
|
||||
}
|
||||
showReplacementDialog.value = true
|
||||
}
|
||||
|
||||
const addDraftRule = () => {
|
||||
draftReplacementRules.value.push(createEmptyReplacementRule())
|
||||
}
|
||||
|
||||
const removeDraftRule = (index: number) => {
|
||||
draftReplacementRules.value.splice(index, 1)
|
||||
if (draftReplacementRules.value.length === 0) {
|
||||
draftReplacementRules.value.push(createEmptyReplacementRule())
|
||||
}
|
||||
}
|
||||
|
||||
const resetDraftRules = () => {
|
||||
draftReplacementRules.value = [createEmptyReplacementRule()]
|
||||
}
|
||||
|
||||
const saveReplacementRules = () => {
|
||||
replacementRules.value = cloneRules(draftReplacementRules.value).filter((rule) => rule.search.trim() || rule.replace.trim())
|
||||
showReplacementDialog.value = false
|
||||
ElMessage.success(replacementRules.value.length > 0 ? '替换规则已保存并生效' : '已清空替换规则')
|
||||
}
|
||||
|
||||
const triggerUpload = () => {
|
||||
fileInput.value?.click()
|
||||
}
|
||||
|
||||
const resetReplacementConfig = () => {
|
||||
replacementRules.value = []
|
||||
draftReplacementRules.value = []
|
||||
}
|
||||
|
||||
const handleFileInputChange = async (event: Event) => {
|
||||
const input = event.target as HTMLInputElement
|
||||
if (input.files) {
|
||||
// 使用 handleFileUpload 函数处理文件,包括 Word 转 PDF
|
||||
await handleFileUpload(input.files)
|
||||
// 清空input值,允许重复选择同一个文件
|
||||
input.value = ''
|
||||
// 折叠上传区域
|
||||
if (uploadedFiles.value.length > 0) {
|
||||
isUploadAreaCollapsed.value = true
|
||||
// 自动处理文件
|
||||
processDocument()
|
||||
initializeManualResult()
|
||||
resetReplacementConfig()
|
||||
markdownRenderMode.value = 'markdown'
|
||||
sourceViewMode.value = 'markdown'
|
||||
activeTab.value = 'source'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -237,37 +391,41 @@ const handleFileInputChange = async (event: Event) => {
|
|||
const handleDrop = async (event: DragEvent) => {
|
||||
event.preventDefault()
|
||||
isDragging.value = false
|
||||
|
||||
|
||||
if (event.dataTransfer?.files) {
|
||||
await handleFileUpload(event.dataTransfer.files)
|
||||
// 折叠上传区域
|
||||
if (uploadedFiles.value.length > 0) {
|
||||
isUploadAreaCollapsed.value = true
|
||||
// 自动处理文件
|
||||
processDocument()
|
||||
initializeManualResult()
|
||||
resetReplacementConfig()
|
||||
markdownRenderMode.value = 'markdown'
|
||||
sourceViewMode.value = 'markdown'
|
||||
activeTab.value = 'source'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const removeFile = (index: number) => {
|
||||
uploadedFiles.value.splice(index, 1)
|
||||
// 如果没有文件了,展开上传区域
|
||||
if (uploadedFiles.value.length === 0) {
|
||||
isUploadAreaCollapsed.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const processDocument = async () => {
|
||||
await originalProcessDocument()
|
||||
// 处理完成后切换到思维导图标签
|
||||
activeTab.value = 'mindmap'
|
||||
}
|
||||
|
||||
// 重写clearAll函数,确保清除后显示上传区域
|
||||
const clearAllFiles = () => {
|
||||
clearAll()
|
||||
resetReplacementConfig()
|
||||
isUploadAreaCollapsed.value = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
updateViewportState()
|
||||
window.addEventListener('resize', updateViewportState)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', updateViewportState)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
@ -282,7 +440,6 @@ const clearAllFiles = () => {
|
|||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 顶部标题栏 */
|
||||
.top-header {
|
||||
padding: 16px 24px;
|
||||
display: flex;
|
||||
|
|
@ -313,7 +470,6 @@ const clearAllFiles = () => {
|
|||
color: #165DFF;
|
||||
}
|
||||
|
||||
/* 主内容区域 */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
padding: 24px;
|
||||
|
|
@ -323,20 +479,11 @@ const clearAllFiles = () => {
|
|||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 上传区域 */
|
||||
.upload-section {
|
||||
margin-bottom: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #343A40;
|
||||
margin: 0 0 16px 0;
|
||||
}
|
||||
|
||||
/* 拖拽上传区 */
|
||||
.drag-upload-area {
|
||||
border: 2px dashed #CED4DA;
|
||||
border-radius: 8px;
|
||||
|
|
@ -356,11 +503,7 @@ const clearAllFiles = () => {
|
|||
display: block;
|
||||
}
|
||||
|
||||
.drag-upload-area:hover {
|
||||
border-color: #165DFF;
|
||||
background-color: #F8F9FF;
|
||||
}
|
||||
|
||||
.drag-upload-area:hover,
|
||||
.drag-upload-area.drag-over {
|
||||
border-color: #165DFF;
|
||||
background-color: #F8F9FF;
|
||||
|
|
@ -391,7 +534,6 @@ const clearAllFiles = () => {
|
|||
max-width: 400px;
|
||||
}
|
||||
|
||||
/* 已上传文件 */
|
||||
.uploaded-files {
|
||||
margin-top: 24px;
|
||||
text-align: left;
|
||||
|
|
@ -444,46 +586,6 @@ const clearAllFiles = () => {
|
|||
color: #DC3545;
|
||||
}
|
||||
|
||||
/* 操作按钮 */
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.process-button {
|
||||
flex: 1;
|
||||
background-color: #165DFF;
|
||||
border-color: #165DFF;
|
||||
color: #FFFFFF;
|
||||
border-radius: 6px;
|
||||
height: 40px;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.process-button:hover:not(:disabled) {
|
||||
background-color: #0E42D2;
|
||||
border-color: #0E42D2;
|
||||
}
|
||||
|
||||
.clear-button {
|
||||
flex: 1;
|
||||
background-color: #FFFFFF;
|
||||
border-color: #CED4DA;
|
||||
color: #495057;
|
||||
border-radius: 6px;
|
||||
height: 40px;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.clear-button:hover:not(:disabled) {
|
||||
border-color: #165DFF;
|
||||
color: #165DFF;
|
||||
}
|
||||
|
||||
/* 设置面板 */
|
||||
.settings-panel {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
|
|
@ -504,13 +606,11 @@ const clearAllFiles = () => {
|
|||
.settings-panel.showing {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
transition: transform 0.3s ease, opacity 0.4s ease;
|
||||
}
|
||||
|
||||
.settings-panel.hiding {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
transition: transform 0.3s ease, opacity 0.4s ease;
|
||||
}
|
||||
|
||||
.settings-header {
|
||||
|
|
@ -539,9 +639,6 @@ const clearAllFiles = () => {
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* 结果区域 */
|
||||
.result-section {
|
||||
background-color: #FFFFFF;
|
||||
border: 1px solid #E9ECEF;
|
||||
|
|
@ -553,7 +650,6 @@ const clearAllFiles = () => {
|
|||
min-height: 0;
|
||||
}
|
||||
|
||||
/* 标签页 */
|
||||
.result-tabs {
|
||||
border-bottom: 1px solid #E9ECEF;
|
||||
}
|
||||
|
|
@ -587,7 +683,16 @@ const clearAllFiles = () => {
|
|||
height: 2px;
|
||||
}
|
||||
|
||||
/* 结果内容 */
|
||||
.source-tab-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.source-tab-arrow {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.result-content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
|
|
@ -596,8 +701,8 @@ const clearAllFiles = () => {
|
|||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.loading-container {
|
||||
.loading-container,
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
|
@ -612,7 +717,8 @@ const clearAllFiles = () => {
|
|||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
.loading-text,
|
||||
.empty-text {
|
||||
font-size: 14px;
|
||||
color: #6C757D;
|
||||
margin: 0;
|
||||
|
|
@ -623,16 +729,6 @@ const clearAllFiles = () => {
|
|||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 400px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
|
|
@ -641,13 +737,6 @@ const clearAllFiles = () => {
|
|||
background-position: center;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 14px;
|
||||
color: #6C757D;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 标签内容 */
|
||||
.result-tab-content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
|
|
@ -670,7 +759,8 @@ const clearAllFiles = () => {
|
|||
}
|
||||
}
|
||||
|
||||
.markdown-content {
|
||||
.markdown-content,
|
||||
.mindmap-content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
|
@ -700,6 +790,32 @@ const clearAllFiles = () => {
|
|||
.source-content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.source-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.replacement-config-button {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.template-error-inline {
|
||||
color: #CF1322;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.source-panel {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.source-textarea {
|
||||
|
|
@ -724,56 +840,135 @@ const clearAllFiles = () => {
|
|||
padding: 16px;
|
||||
}
|
||||
|
||||
.mindmap-content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
.replacement-dialog-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.replacement-dialog-tip {
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
background: #F8FAFF;
|
||||
border: 1px solid #DCE7FF;
|
||||
color: #4E5969;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.replacement-rule-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.replacement-rule-row {
|
||||
display: grid;
|
||||
grid-template-columns: 120px 1fr 1fr 72px;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.replacement-rule-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.replacement-rule-index {
|
||||
font-size: 13px;
|
||||
color: #4E5969;
|
||||
}
|
||||
|
||||
.replacement-delete-button {
|
||||
border: 1px solid #D0D5DD;
|
||||
border-radius: 6px;
|
||||
padding: 0 12px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.replacement-rule-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.add-rule-button {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.replacement-dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.main-content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.top-header {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.drag-upload-area {
|
||||
padding: 32px 16px;
|
||||
}
|
||||
|
||||
.upload-icon {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.upload-hint {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.main-content,
|
||||
.result-content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
|
||||
.top-header {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.drag-upload-area {
|
||||
padding: 32px 16px;
|
||||
}
|
||||
|
||||
.upload-icon {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.upload-hint {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.result-tabs :deep(.el-tabs__nav) {
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
|
||||
.result-tabs :deep(.el-tabs__item) {
|
||||
padding: 0 16px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.source-toolbar {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.replacement-dialog-body {
|
||||
max-height: none;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.replacement-rule-row {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.replacement-rule-head {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.replacement-delete-button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.replacement-dialog-footer {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.replacement-dialog-footer :deep(.el-button) {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Reference in New Issue