From e71230ca96d0022bfc40f87e1739a3e06480c0ab Mon Sep 17 00:00:00 2001 From: Jowe <123822645+Selei1983@users.noreply.github.com> Date: Sat, 10 Jan 2026 18:04:42 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20v2.5=20=E7=A4=BE=E5=AA=92=E5=88=86?= =?UTF-8?q?=E4=BA=AB=E5=8A=9F=E8=83=BD=20+=20=E9=9D=A2=E5=8C=85=E5=B1=91?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增功能: - 社媒分享:一键生成多平台分享文案 - 支持8个平台:通用、小红书、抖音、B站、微信公众号、朋友圈、X (Twitter)、LinkedIn - 智能生成适配各平台特点的文案(字数限制、风格、内容丰富度) - 集成DeepSeek AI自动生成 + 规则模板回退 - "分享"按钮支持直接跳转到平台发布后台 - X (Twitter)支持预填充文案 - 自动复制文案到剪贴板 优化改进: - 面包屑导航:修复折行问题,确保始终单行显示 - 添加 white-space: nowrap 和 flex-shrink: 0 - 长标题自动省略显示 - 社媒分享按钮文案:从"一键生成社媒分享"改为"分享" - 更新Twitter为X,使用x.com域名 技术实现: - 前端:detail_new.html 新增分享弹窗和交互逻辑 - 后端:app.py 新增 /api/generate-social-share 接口 - 支持按平台定制文案内容和格式 Co-Authored-By: Claude Sonnet 4.5 --- app.py | 198 ++++++++++++++++++ templates/detail_new.html | 416 +++++++++++++++++++++++++++++++++++++- 2 files changed, 612 insertions(+), 2 deletions(-) diff --git a/app.py b/app.py index ccc446f..3445a7f 100644 --- a/app.py +++ b/app.py @@ -239,6 +239,204 @@ def create_app(config_name='default'): return render_template('detail_new.html', site=site, news_list=news_list, recommended_sites=recommended_sites) + # ========== 社媒营销路由 (v2.5新增) ========== + @app.route('/api/generate-social-share', methods=['POST']) + def generate_social_share(): + """一键生成社媒分享文案(前台可访问)""" + try: + data = request.get_json() or {} + site_code = (data.get('site_code') or '').strip() + platforms = data.get('platforms') or [] + + if not site_code: + return jsonify({'success': False, 'message': '请提供网站编码'}), 400 + + site = Site.query.filter_by(code=site_code, is_active=True).first() + if not site: + return jsonify({'success': False, 'message': '网站不存在或未启用'}), 404 + + # 允许的平台列表(前端也会限制,这里再做一次兜底) + allowed_platforms = { + 'universal', + 'xiaohongshu', + 'douyin', + 'bilibili', + 'wechat', + 'moments', + 'x', + 'linkedin' + } + + if not isinstance(platforms, list): + platforms = [] + platforms = [p for p in platforms if isinstance(p, str)] + platforms = [p.strip().lower() for p in platforms if p.strip()] + platforms = [p for p in platforms if p in allowed_platforms] + + # 默认生成通用模板 + if not platforms: + platforms = ['universal'] + + # 组织站点信息 + site_info = { + 'name': site.name, + 'url': site.url, + 'short_desc': site.short_desc or '', + 'description': (site.description or '')[:1200], + 'features': (site.features or '')[:1200], + 'tags': [t.name for t in (site.tags or [])] + } + + # 尝试使用PromptTemplate + DeepSeek生成 + from utils.tag_generator import TagGenerator + generator = TagGenerator() + prompt = PromptTemplate.query.filter_by(key='social_share', is_active=True).first() + + generated = {} + + if prompt and generator.client: + # 构造提示词变量 + tags_text = ','.join(site_info['tags']) + user_prompt = prompt.user_prompt_template.format( + name=site_info['name'], + url=site_info['url'], + short_desc=site_info['short_desc'], + description=site_info['description'], + features=site_info['features'], + tags=tags_text, + platforms=','.join(platforms) + ) + + try: + resp = generator.client.chat.completions.create( + model='deepseek-chat', + messages=[ + {'role': 'system', 'content': prompt.system_prompt}, + {'role': 'user', 'content': user_prompt} + ], + temperature=0.7, + max_tokens=1200 + ) + text = (resp.choices[0].message.content or '').strip() + # 约定:模型返回JSON字符串;若不是JSON则回退到纯文本放到universal + import json as _json + try: + parsed = _json.loads(text) + if isinstance(parsed, dict): + for k, v in parsed.items(): + if isinstance(k, str) and isinstance(v, str) and k in allowed_platforms: + generated[k] = v.strip() + except Exception: + generated['universal'] = text + except Exception as e: + # AI失败不影响整体,回退模板 + print(f"社媒文案AI生成失败:{str(e)}") + + # 回退:规则模板(根据各平台字数限制优化) + def build_fallback(p): + name = site_info['name'] + url = site_info['url'] + short_desc = site_info['short_desc'] or '' + full_desc = site_info['description'] or '' + features = site_info['features'] or '' + tags = site_info['tags'][:6] + tags_line = ' ' + ' '.join([f"#{t}" for t in tags]) if tags else '' + + # X (Twitter) - 280字符限制,简洁为主 + if p == 'x': + desc = (short_desc or full_desc)[:100] + base = f"🔥 {name}\n\n{desc}\n\n🔗 {url}{tags_line}" + return base[:280] + + # LinkedIn - 3000字限制,专业风格 + if p == 'linkedin': + desc = (full_desc or short_desc)[:500] + content = f"💡 推荐一个实用工具:{name}\n\n📝 简介:\n{desc}\n\n" + if features: + features_list = features.split('\n')[:3] + content += f"✨ 主要功能:\n" + '\n'.join([f"• {f.strip()}" for f in features_list if f.strip()][:3]) + "\n\n" + content += f"🔗 官网:{url}\n\n" + if tags: + content += f"📌 适用场景:{', '.join(tags)}" + return content[:3000] + + # 小红书 - 2000字限制,图文风格 + if p == 'xiaohongshu': + desc = (short_desc or full_desc)[:300] + content = f"✨ {name} | 我最近在用的宝藏工具\n\n" + content += f"📌 一句话介绍:\n{desc}\n\n" + if features: + features_list = features.split('\n')[:5] + content += f"🎯 核心功能:\n" + '\n'.join([f"✓ {f.strip()}" for f in features_list if f.strip()][:5]) + "\n\n" + content += f"🔗 体验入口:{url}\n\n{tags_line}" + return content[:2000] + + # 抖音 - 2000字限制,短视频文案风格 + if p == 'douyin': + desc = (short_desc or full_desc)[:200] + content = f"🔥 发现一个超好用的工具:{name}\n\n" + content += f"💡 {desc}\n\n" + if features: + features_list = features.split('\n')[:3] + content += "✨ 亮点功能:\n" + '\n'.join([f"👉 {f.strip()}" for f in features_list if f.strip()][:3]) + "\n\n" + content += f"🔗 想试试戳:{url}\n\n{tags_line}" + return content[:2000] + + # B站 - 20000字限制,专栏风格 + if p == 'bilibili': + desc = (full_desc or short_desc)[:800] + content = f"【分享】{name} - 值得一试的优质工具\n\n" + content += f"## 📖 工具简介\n{desc}\n\n" + if features: + content += f"## ✨ 主要功能\n{features}\n\n" + content += f"## 🔗 体验地址\n{url}\n\n" + if tags: + content += f"## 🏷️ 相关标签\n{' / '.join(tags)}" + return content[:20000] + + # 微信公众号 - 基本无限制,正式风格 + if p == 'wechat': + desc = (full_desc or short_desc)[:600] + content = f"今天给大家分享一个实用工具:{name}\n\n" + content += f"📝 工具介绍\n{desc}\n\n" + if features: + content += f"✨ 核心功能\n{features}\n\n" + content += f"🔗 官方网站\n{url}\n\n" + if tags: + content += f"如果你也在关注「{' · '.join(tags)}」相关的工具,不妨收藏一下这个实用的选择。" + return content + + # 朋友圈 - 简短为主 + if p == 'moments': + desc = (short_desc or full_desc)[:80] + return f"💡 {name}\n{desc}\n🔗 {url}{tags_line}".strip() + + # 通用模板 + desc = (full_desc or short_desc)[:300] + return f"{name}\n\n{desc}\n\n{url}{tags_line}".strip() + + results = {} + for p in platforms: + results[p] = generated.get(p) or build_fallback(p) + + # 统一补充一个通用版本 + if 'universal' not in results: + results['universal'] = generated.get('universal') or build_fallback('universal') + + return jsonify({ + 'success': True, + 'site': { + 'code': site.code, + 'name': site.name, + 'url': site.url + }, + 'platforms': platforms, + 'content': results + }) + + except Exception as e: + return jsonify({'success': False, 'message': f'生成失败: {str(e)}'}), 500 + # ========== 后台登录路由 ========== @app.route('/admin/login', methods=['GET', 'POST']) def admin_login(): diff --git a/templates/detail_new.html b/templates/detail_new.html index dcf4a76..c82d55a 100644 --- a/templates/detail_new.html +++ b/templates/detail_new.html @@ -80,19 +80,25 @@ padding: 12px 0; font-size: 14px; color: var(--text-secondary); - flex-wrap: wrap; + flex-wrap: nowrap; + white-space: nowrap; + overflow-x: auto; + overflow-y: hidden; } .breadcrumb-item { - display: flex; + display: inline-flex; align-items: center; gap: 8px; + flex-shrink: 0; + white-space: nowrap; } .breadcrumb-item a { color: var(--text-secondary); text-decoration: none; transition: color 0.2s; + white-space: nowrap; } .breadcrumb-item a:hover { @@ -102,11 +108,16 @@ .breadcrumb-item.active { color: var(--text-primary); font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 300px; } .breadcrumb-separator { color: var(--text-muted); user-select: none; + flex-shrink: 0; } /* 返回链接 */ @@ -715,8 +726,41 @@ 访问网站 + + + +

在新标签页打开 • {{ site.url.split('/')[2] if site.url else '' }}

+ + + @@ -952,4 +996,372 @@ function refreshNews(siteCode) { } + + + + {% endblock %}