add:markdown转pdf页面
This commit is contained in:
360
frontend/src/components/MdToPdf.vue
Normal file
360
frontend/src/components/MdToPdf.vue
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { convertMarkdownToPdf } from '../services/api'
|
||||||
|
|
||||||
|
const mode = ref<'text' | 'file'>('text')
|
||||||
|
const mdText = ref('')
|
||||||
|
const file = ref<File | null>(null)
|
||||||
|
|
||||||
|
// Advanced options
|
||||||
|
const showAdvanced = ref(false)
|
||||||
|
const toc = ref(true)
|
||||||
|
const headerText = ref('')
|
||||||
|
const footerText = ref('')
|
||||||
|
const cssName = ref('default')
|
||||||
|
const cssText = ref('')
|
||||||
|
const logoUrl = ref('')
|
||||||
|
const logoFile = ref<File | null>(null)
|
||||||
|
const coverUrl = ref('')
|
||||||
|
const coverFile = ref<File | null>(null)
|
||||||
|
const productName = ref('')
|
||||||
|
const documentName = ref('')
|
||||||
|
const productVersion = ref('')
|
||||||
|
const documentVersion = ref('')
|
||||||
|
const copyrightText = ref('')
|
||||||
|
const filenameText = ref('')
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const error = ref('')
|
||||||
|
const downloadUrl = ref('')
|
||||||
|
|
||||||
|
function onFileChange(e: Event) {
|
||||||
|
const target = e.target as HTMLInputElement
|
||||||
|
const selectedFile = target.files?.[0]
|
||||||
|
if (selectedFile) {
|
||||||
|
file.value = selectedFile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLogoFileChange(e: Event) {
|
||||||
|
const target = e.target as HTMLInputElement
|
||||||
|
const selectedFile = target.files?.[0]
|
||||||
|
if (selectedFile) {
|
||||||
|
logoFile.value = selectedFile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCoverFileChange(e: Event) {
|
||||||
|
const target = e.target as HTMLInputElement
|
||||||
|
const selectedFile = target.files?.[0]
|
||||||
|
if (selectedFile) {
|
||||||
|
coverFile.value = selectedFile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleConvert() {
|
||||||
|
loading.value = true
|
||||||
|
error.value = ''
|
||||||
|
downloadUrl.value = ''
|
||||||
|
|
||||||
|
const fd = new FormData()
|
||||||
|
|
||||||
|
if (mode.value === 'text') {
|
||||||
|
if (!mdText.value.trim()) {
|
||||||
|
error.value = '请输入 Markdown 文本'
|
||||||
|
loading.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fd.append('markdown_content', mdText.value)
|
||||||
|
} else {
|
||||||
|
if (!file.value) {
|
||||||
|
error.value = '请选择文件'
|
||||||
|
loading.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fd.append('file', file.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fd.append('download', 'true')
|
||||||
|
|
||||||
|
// Advanced params
|
||||||
|
if (showAdvanced.value) {
|
||||||
|
fd.append('toc', String(toc.value))
|
||||||
|
if (headerText.value) fd.append('header_text', headerText.value)
|
||||||
|
if (footerText.value) fd.append('footer_text', footerText.value)
|
||||||
|
if (cssName.value) fd.append('css_name', cssName.value)
|
||||||
|
if (cssText.value) fd.append('css_text', cssText.value)
|
||||||
|
|
||||||
|
if (logoUrl.value) fd.append('logo_url', logoUrl.value)
|
||||||
|
if (logoFile.value) fd.append('logo_file', logoFile.value)
|
||||||
|
|
||||||
|
if (coverUrl.value) fd.append('cover_url', coverUrl.value)
|
||||||
|
if (coverFile.value) fd.append('cover_file', coverFile.value)
|
||||||
|
|
||||||
|
if (productName.value) fd.append('product_name', productName.value)
|
||||||
|
if (documentName.value) fd.append('document_name', documentName.value)
|
||||||
|
if (productVersion.value) fd.append('product_version', productVersion.value)
|
||||||
|
if (documentVersion.value) fd.append('document_version', documentVersion.value)
|
||||||
|
|
||||||
|
if (copyrightText.value) fd.append('copyright_text', copyrightText.value)
|
||||||
|
if (filenameText.value) fd.append('filename_text', filenameText.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await convertMarkdownToPdf(fd)
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`HTTP ${res.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentType = res.headers.get('content-type') || ''
|
||||||
|
|
||||||
|
if (contentType.includes('application/json')) {
|
||||||
|
const json = await res.json()
|
||||||
|
if (json.code !== 0) {
|
||||||
|
throw new Error(json.msg || '转换失败')
|
||||||
|
}
|
||||||
|
error.value = '服务器返回了 JSON 响应而不是 PDF 文件'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = await res.blob()
|
||||||
|
if (blob.type !== 'application/pdf') {
|
||||||
|
throw new Error('服务器返回的不是 PDF 文件')
|
||||||
|
}
|
||||||
|
downloadUrl.value = URL.createObjectURL(blob)
|
||||||
|
} catch (e) {
|
||||||
|
error.value = '转换失败:' + String(e)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="card">
|
||||||
|
<h1>Markdown 转 PDF</h1>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<label>输入方式</label>
|
||||||
|
<div class="radio-group">
|
||||||
|
<label><input type="radio" v-model="mode" value="text"> 文本</label>
|
||||||
|
<label><input type="radio" v-model="mode" value="file"> 文件</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row" v-if="mode === 'text'">
|
||||||
|
<label>Markdown 内容</label>
|
||||||
|
<textarea v-model="mdText" rows="10" placeholder="# 在此输入 Markdown 内容..."></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row" v-if="mode === 'file'">
|
||||||
|
<label>文件</label>
|
||||||
|
<input type="file" accept=".md,.markdown" @change="onFileChange" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="toggle-section">
|
||||||
|
<label class="toggle-label">
|
||||||
|
<input type="checkbox" v-model="showAdvanced" />
|
||||||
|
<span>显示高级选项</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="advanced-section" v-if="showAdvanced">
|
||||||
|
<div class="row">
|
||||||
|
<label>目录 (TOC)</label>
|
||||||
|
<select v-model="toc">
|
||||||
|
<option :value="true">开启</option>
|
||||||
|
<option :value="false">关闭</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<label>CSS 模板</label>
|
||||||
|
<select v-model="cssName">
|
||||||
|
<option value="default">Default</option>
|
||||||
|
<option value="">None</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<label>自定义 CSS</label>
|
||||||
|
<textarea v-model="cssText" rows="4" placeholder="/* Enter custom CSS here */"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<label>页眉文本</label>
|
||||||
|
<input type="text" v-model="headerText" placeholder="e.g. Internal Document" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<label>页脚文本</label>
|
||||||
|
<input type="text" v-model="footerText" placeholder="e.g. Confidential" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<label>Logo</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" v-model="logoUrl" placeholder="http(s)://..." />
|
||||||
|
<input type="file" accept="image/png,image/jpeg,image/svg+xml,image/webp" @change="onLogoFileChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<label>封面图片</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" v-model="coverUrl" placeholder="http(s)://..." />
|
||||||
|
<input type="file" accept="image/png,image/jpeg,image/svg+xml,image/webp" @change="onCoverFileChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<label>封面文字</label>
|
||||||
|
<div class="grid-group">
|
||||||
|
<input type="text" v-model="productName" placeholder="Product Name" />
|
||||||
|
<input type="text" v-model="documentName" placeholder="Document Name" />
|
||||||
|
<input type="text" v-model="productVersion" placeholder="Product Version" />
|
||||||
|
<input type="text" v-model="documentVersion" placeholder="Document Version" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<label>版权信息</label>
|
||||||
|
<input type="text" v-model="copyrightText" placeholder="© Company Name" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<label>文件名</label>
|
||||||
|
<input type="text" v-model="filenameText" placeholder="document" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<button @click="handleConvert" :disabled="loading">
|
||||||
|
{{ loading ? '正在转换...' : '开始转换' }}
|
||||||
|
</button>
|
||||||
|
<a v-if="downloadUrl" :href="downloadUrl" class="download-btn" download="document.pdf">下载 PDF</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="error" class="error-msg">{{ error }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.card {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 20px;
|
||||||
|
margin: 0 0 12px;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
margin: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
min-width: 120px;
|
||||||
|
color: #374151;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"], select, textarea {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
font-family: monospace;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-section {
|
||||||
|
margin: 16px 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-label input[type="checkbox"] {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanced-section {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button, .download-btn {
|
||||||
|
background: #2563eb;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
background: #9ca3af;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-btn {
|
||||||
|
background: #059669;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-msg {
|
||||||
|
color: #dc2626;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-group {
|
||||||
|
flex: 1;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user