From 5cd7e1eb30a2dc02737ff516188c4a03c64b99af Mon Sep 17 00:00:00 2001 From: renjianbo <18691577328@163.com> Date: Sun, 3 May 2026 09:51:48 +0800 Subject: [PATCH] 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 --- src/flask_prompt_master/__init__.py | 4 + .../routes/export_routes.py | 97 +++++++++++++++++++ vue-app/src/utils/export.ts | 38 ++++++++ vue-app/src/views/GenerateView.vue | 26 +++++ 4 files changed, 165 insertions(+) create mode 100644 src/flask_prompt_master/routes/export_routes.py create mode 100644 vue-app/src/utils/export.ts diff --git a/src/flask_prompt_master/__init__.py b/src/flask_prompt_master/__init__.py index c6ae602..caf7e4e 100644 --- a/src/flask_prompt_master/__init__.py +++ b/src/flask_prompt_master/__init__.py @@ -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 diff --git a/src/flask_prompt_master/routes/export_routes.py b/src/flask_prompt_master/routes/export_routes.py new file mode 100644 index 0000000..95f22be --- /dev/null +++ b/src/flask_prompt_master/routes/export_routes.py @@ -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//', 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', + }, + ) diff --git a/vue-app/src/utils/export.ts b/vue-app/src/utils/export.ts new file mode 100644 index 0000000..11016a8 --- /dev/null +++ b/vue-app/src/utils/export.ts @@ -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(` + + + ${title} + +

${title}

导出时间: ${new Date().toLocaleString('zh-CN')}

${escapeHtml(content)}
+ `) + w.document.close() + setTimeout(() => w.print(), 300) +} + +function escapeHtml(text: string) { + return text.replace(/&/g, '&').replace(//g, '>') +} diff --git a/vue-app/src/views/GenerateView.vue b/vue-app/src/views/GenerateView.vue index f8fa50b..1683f85 100644 --- a/vue-app/src/views/GenerateView.vue +++ b/vue-app/src/views/GenerateView.vue @@ -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 () => { 加入收藏 复制 + + + 导出 + + +