add:markdown转pdf页面

This commit is contained in:
2026-01-19 22:33:12 +08:00
parent 57cd7e7c3e
commit 219576fa7c

View 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>