feat(S3): content export - Markdown download and PDF print
- Backend: GET /api/export/:type/:id returns .md file download - Backend: POST /api/export/text for direct text-to-md export - Frontend: downloadMarkdown() utility for client-side .md download - Frontend: printAsPdf() utility via browser print dialog - GenerateView: export dropdown button (MD / PDF) on result card Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -76,6 +76,10 @@ def create_app(config_class=None):
|
||||
from src.flask_prompt_master.routes.conversation_routes import conversation_bp
|
||||
app.register_blueprint(conversation_bp)
|
||||
|
||||
# 内容导出(Markdown 下载)
|
||||
from src.flask_prompt_master.routes.export_routes import export_bp
|
||||
app.register_blueprint(export_bp)
|
||||
|
||||
# 提示词结构化质量评价(多段文本 + 模型 JSON 评价 + 历史)
|
||||
from src.flask_prompt_master.models import prompt_quality_models # noqa: F401
|
||||
from src.flask_prompt_master.routes.prompt_quality_routes import prompt_quality_bp
|
||||
|
||||
97
src/flask_prompt_master/routes/export_routes.py
Normal file
97
src/flask_prompt_master/routes/export_routes.py
Normal file
@@ -0,0 +1,97 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
内容导出路由:Markdown 文件下载
|
||||
"""
|
||||
from flask import Blueprint, request, jsonify, Response
|
||||
from src.flask_prompt_master.models.models import Prompt, MealPlan, WeeklyReport, TravelPlan, MeetingMinutes, ResumeOptimization
|
||||
from src.flask_prompt_master.models.history_models import PromptHistory
|
||||
from src.flask_prompt_master.user_context import get_current_user_id
|
||||
from datetime import datetime
|
||||
|
||||
export_bp = Blueprint('export', __name__)
|
||||
|
||||
CONTENT_MODELS = {
|
||||
'prompt': Prompt,
|
||||
'meal': MealPlan,
|
||||
'report': WeeklyReport,
|
||||
'travel': TravelPlan,
|
||||
'meeting': MeetingMinutes,
|
||||
'resume': ResumeOptimization,
|
||||
'history': PromptHistory,
|
||||
}
|
||||
|
||||
FIELD_MAP = {
|
||||
'prompt': ('input_text', 'generated_text'),
|
||||
'meal': ('meal_plan_content',),
|
||||
'report': ('report_content',),
|
||||
'travel': ('plan_content',),
|
||||
'meeting': ('summary_content',),
|
||||
'resume': ('optimized_content',),
|
||||
'history': ('generated_prompt',),
|
||||
}
|
||||
|
||||
TITLE_MAP = {
|
||||
'prompt': '提示词生成结果',
|
||||
'meal': '饭菜规划',
|
||||
'report': '周报/日报',
|
||||
'travel': '旅行攻略',
|
||||
'meeting': '会议纪要',
|
||||
'resume': '简历优化',
|
||||
'history': '历史记录',
|
||||
}
|
||||
|
||||
|
||||
@export_bp.route('/api/export/<content_type>/<int:content_id>', methods=['GET'])
|
||||
def export_content(content_type, content_id):
|
||||
"""导出内容为 Markdown 文件下载"""
|
||||
if content_type not in CONTENT_MODELS:
|
||||
return jsonify({'success': False, 'message': f'不支持的内容类型: {content_type}'}), 400
|
||||
|
||||
model = CONTENT_MODELS[content_type]
|
||||
record = model.query.get(content_id)
|
||||
if not record:
|
||||
return jsonify({'success': False, 'message': '内容不存在'}), 404
|
||||
|
||||
fields = FIELD_MAP.get(content_type, [])
|
||||
title = TITLE_MAP.get(content_type, '导出内容')
|
||||
|
||||
# 组装 Markdown 内容
|
||||
lines = [f'# {title}', '', f'> 导出时间: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}', '']
|
||||
for field in fields:
|
||||
val = getattr(record, field, None) or ''
|
||||
lines.append(val)
|
||||
lines.append('')
|
||||
|
||||
markdown = '\n'.join(lines)
|
||||
|
||||
filename = f'{content_type}_{content_id}_{datetime.now().strftime("%Y%m%d%H%M%S")}.md'
|
||||
return Response(
|
||||
markdown,
|
||||
mimetype='text/markdown; charset=utf-8',
|
||||
headers={
|
||||
'Content-Disposition': f'attachment; filename="{filename}"',
|
||||
'X-Content-Type-Options': 'nosniff',
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@export_bp.route('/api/export/text', methods=['POST'])
|
||||
def export_text():
|
||||
"""直接导出文本内容为 Markdown(无需存储记录)"""
|
||||
data = request.get_json(silent=True) or {}
|
||||
content = (data.get('content') or '').strip()
|
||||
title = (data.get('title') or '导出内容').strip()
|
||||
|
||||
if not content:
|
||||
return jsonify({'success': False, 'message': '内容为空'}), 400
|
||||
|
||||
markdown = f'# {title}\n\n> 导出时间: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}\n\n{content}\n'
|
||||
filename = f'export_{datetime.now().strftime("%Y%m%d%H%M%S")}.md'
|
||||
return Response(
|
||||
markdown,
|
||||
mimetype='text/markdown; charset=utf-8',
|
||||
headers={
|
||||
'Content-Disposition': f'attachment; filename="{filename}"',
|
||||
'X-Content-Type-Options': 'nosniff',
|
||||
},
|
||||
)
|
||||
38
vue-app/src/utils/export.ts
Normal file
38
vue-app/src/utils/export.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* 导出文本为 Markdown / PDF
|
||||
*/
|
||||
|
||||
export function downloadMarkdown(content: string, title = '导出内容') {
|
||||
const now = new Date().toISOString().slice(0, 19).replace(/[:T]/g, '-')
|
||||
const markdown = `# ${title}\n\n> 导出时间: ${new Date().toLocaleString('zh-CN')}\n\n${content}\n`
|
||||
const blob = new Blob([markdown], { type: 'text/markdown; charset=utf-8' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `${title.replace(/\s+/g, '_')}_${now}.md`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
export function printAsPdf(content: string, title = '导出内容') {
|
||||
const w = window.open('', '_blank', 'width=800,height=600')
|
||||
if (!w) return
|
||||
w.document.write(`
|
||||
<!DOCTYPE html>
|
||||
<html><head><meta charset="utf-8">
|
||||
<title>${title}</title>
|
||||
<style>
|
||||
body { font-family: system-ui, sans-serif; max-width: 800px; margin: 0 auto; padding: 2rem; line-height: 1.7; }
|
||||
pre { white-space: pre-wrap; background: #f5f5f5; padding: 1rem; border-radius: 6px; }
|
||||
</style></head>
|
||||
<body><h1>${title}</h1><p><em>导出时间: ${new Date().toLocaleString('zh-CN')}</em></p><pre>${escapeHtml(content)}</pre></body></html>
|
||||
`)
|
||||
w.document.close()
|
||||
setTimeout(() => w.print(), 300)
|
||||
}
|
||||
|
||||
function escapeHtml(text: string) {
|
||||
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import { useRouter } from 'vue-router'
|
||||
import { fetchGenerateMeta, fetchTemplatesByCategory, generatePrompt, continuePrompt } from '@/api/modules/prompt'
|
||||
import { quickAddFavorite } from '@/api/modules/favorite'
|
||||
import type { PromptTemplateItem } from '@/api/types/template'
|
||||
import { ArrowDownBold } from '@element-plus/icons-vue'
|
||||
import { downloadMarkdown, printAsPdf } from '@/utils/export'
|
||||
|
||||
const metaLoading = ref(true)
|
||||
const templatesLoading = ref(false)
|
||||
@@ -279,6 +281,19 @@ function resetConversation() {
|
||||
ElMessage.info('已开始新对话')
|
||||
}
|
||||
|
||||
function exportMarkdown() {
|
||||
if (!result.value) return
|
||||
const tpl = selectedTemplate.value
|
||||
downloadMarkdown(result.value.generated_text, tpl?.name || '提示词生成结果')
|
||||
ElMessage.success('Markdown 已下载')
|
||||
}
|
||||
|
||||
function exportPdf() {
|
||||
if (!result.value) return
|
||||
const tpl = selectedTemplate.value
|
||||
printAsPdf(result.value.generated_text, tpl?.name || '提示词生成结果')
|
||||
}
|
||||
|
||||
async function addToFavorites() {
|
||||
if (!result.value || selectedTemplateId.value == null) return
|
||||
const tpl = selectedTemplate.value
|
||||
@@ -439,6 +454,17 @@ onMounted(async () => {
|
||||
加入收藏
|
||||
</el-button>
|
||||
<el-button type="success" plain size="small" @click="copyResult">复制</el-button>
|
||||
<el-dropdown trigger="click" @command="(cmd: string) => cmd === 'md' ? exportMarkdown() : exportPdf()">
|
||||
<el-button type="info" plain size="small">
|
||||
导出 <el-icon class="el-icon--right"><ArrowDownBold /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="md">Markdown (.md)</el-dropdown-item>
|
||||
<el-dropdown-item command="pdf">PDF (打印)</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</el-space>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user