Phase3: 三平台封面图生成

- cover_generator.py: Pillow生成知乎/微信/小红书封面图(渐变背景+标题)
- 知乎 1200×630 蓝调, 微信 900×500 绿调, 小红书 1080×1440 红调
- creator.py 流水线增加 cover_generator 作为最终回退
- db_helper.py 新增 save_cover_to_article
- main.py 挂载 /automation/images 静态目录
- articles.py preview 接口返回 images 字段
- topics.html 预览dialog展示封面图
This commit is contained in:
Yuzhiran Dev
2026-05-20 19:12:58 +08:00
parent 40a77cad2c
commit c8bee712d7
6 changed files with 237 additions and 5 deletions
+1 -1
View File
@@ -138,7 +138,7 @@ def preview_article(topic_id: str, platform: str = "zhihu", current_user: User =
article = db.query(Article).filter(Article.id == article_id).first()
if not article or not article.html_content:
raise HTTPException(status_code=404, detail=f"Article not found for {topic_id} on {platform}")
return {"topic_id": topic_id, "platform": platform, "html": article.html_content}
return {"topic_id": topic_id, "platform": platform, "html": article.html_content, "images": article.images or {}}
@router.put("/{topic_id}/content")
def update_article_content(
+7
View File
@@ -95,6 +95,13 @@ app.include_router(platform_config.router)
app.include_router(collector_mgmt.router)
app.include_router(assistant.router)
# 挂载自动生成的图片(必须先于前端根挂载)
PROJECT_ROOT_DIR = Path(__file__).parent.parent.parent.parent
AUTOMATION_IMAGES_DIR = PROJECT_ROOT_DIR / "automation" / "images"
if AUTOMATION_IMAGES_DIR.exists():
app.mount("/automation/images", StaticFiles(directory=str(AUTOMATION_IMAGES_DIR)), name="images")
logger.info(f"Images mounted at /automation/images from {AUTOMATION_IMAGES_DIR}")
# 挂载前端
FRONTEND_DIR = Path(__file__).parent.parent.parent / "frontend"
if FRONTEND_DIR.exists() and (FRONTEND_DIR / "index.html").exists():
+6 -3
View File
@@ -163,6 +163,9 @@
</div>
</div>
<div style="width:100%;">
<div v-if="!editing && previewImages[previewPlatform] && previewImages[previewPlatform].cover" style="margin-bottom:16px; border:1px solid #ebeef5; border-radius:8px; overflow:hidden; text-align:center;">
<img :src="'/' + previewImages[previewPlatform].cover" alt="封面图" style="max-width:100%; max-height:360px; object-fit:contain;" @error="$event.target.style.display='none'" />
</div>
<div v-if="!editing" class="preview-body" style="border:1px solid #ebeef5; border-radius:8px; background:#fff; padding:20px; width:100%; line-height:1.8;" v-html="currentPreviewBody"></div>
<div v-else ref="editor" contenteditable="true" style="min-height:300px; height:68vh; border:1px solid #409eff; border-radius:8px; background:#fff; padding:20px; overflow-y:auto; width:100%; line-height:1.8; outline:none; box-shadow:0 0 0 2px rgba(64,158,255,0.2);" @input="editContent = $event.target.innerHTML"></div>
</div>
@@ -196,7 +199,7 @@ const TopicsApp = {
stats: { total: 0, pending: 0, review: 0, ready: 0, published: 0, today: 0 },
topics: [], allTopics: [], todayTopics: [], todayCount: 0,
previewVisible: false, previewTopic: null, previewFullscreen: false,
previewPlatform: 'zhihu', platformContents: {}, editing: false, editContent: '',
previewPlatform: 'zhihu', platformContents: {}, previewImages: {}, editing: false, editContent: '',
publishDialogVisible: false, publishTopic: null,
publishPlatforms: { zhihu: true, wechat: true, xiaohongshu: true },
publishing: false,
@@ -317,14 +320,14 @@ const TopicsApp = {
} catch (error) { this.$message.error(`批量审查失败: ${error.message}`); }
},
async openPreview(topic) {
this.previewTopic = topic; this.previewPlatform = 'zhihu'; this.previewVisible = true; this.platformContents = {};
this.previewTopic = topic; this.previewPlatform = 'zhihu'; this.previewVisible = true; this.platformContents = {}; this.previewImages = {};
const token = this.getToken();
if (!token) return;
const platforms = ['zhihu', 'wechat', 'xiaohongshu'];
const names = { zhihu: '知乎', wechat: '微信公众号', xiaohongshu: '小红书' };
await Promise.all(platforms.map(p =>
fetch(`/api/articles/${topic.id}/preview?platform=${p}`, { headers: { 'Authorization': 'Bearer ' + token } })
.then(r => r.ok ? r.json() : null).then(d => { if (d && d.html) this.platformContents[p] = d.html; }).catch(e => { console.error(`加载${p}预览失败:`, e); this.$message.error(`加载${names[p]}预览失败`); })
.then(r => r.ok ? r.json() : null).then(d => { if (d && d.html) { this.platformContents[p] = d.html; if (d.images) this.previewImages[p] = d.images; } }).catch(e => { console.error(`加载${p}预览失败:`, e); this.$message.error(`加载${names[p]}预览失败`); })
));
},
togglePreviewFullscreen() {