Files
zjpb.net/templates/detail_new.html
Jowe 2e31d2bfd6 fix: 修复前台获取新闻时验证码验证失败的问题
问题原因:fetch请求缺少credentials选项,导致浏览器不发送session cookie
解决方案:添加credentials: 'same-origin'确保发送session cookie

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-23 21:19:34 +08:00

1794 lines
51 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends 'base_new.html' %}
{% block title %}{{ site.name }} - ZJPB AI工具导航{% endblock %}
{% block extra_head %}
<!-- SEO Meta Tags (v2.4新增) -->
<meta name="description" content="{{ site.short_desc or (site.description[:150] if site.description else '') }}">
<meta name="keywords" content="{{ site.name }},{% for tag in site.tags %}{{ tag.name }},{% endfor %}AI工具,自己品吧,ZJPB">
<link rel="canonical" href="{{ request.url }}">
<!-- Open Graph (v2.4新增) -->
<meta property="og:type" content="website">
<meta property="og:title" content="{{ site.name }}">
<meta property="og:description" content="{{ site.short_desc or (site.description[:150] if site.description else '') }}">
<meta property="og:url" content="{{ request.url }}">
{% if site.logo %}<meta property="og:image" content="{{ request.url_root.rstrip('/') }}{{ site.logo }}">{% endif %}
<!-- Schema.org 结构化数据 (v2.4新增) -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "{{ site.name }}",
"url": "{{ site.url }}",
{% if site.logo %}"image": "{{ request.url_root.rstrip('/') }}{{ site.logo }}",{% endif %}
"description": "{{ site.short_desc or site.description or site.name }}",
{% if site.features %}"featureList": {{ site.features.split('\n') | tojson }},{% endif %}
"applicationCategory": "WebApplication",
"operatingSystem": "Any",
{% if site.tags %}"keywords": "{{ site.tags | map(attribute='name') | join(', ') }}",{% endif %}
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "CNY"
},
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.5",
"ratingCount": "{{ site.view_count or 1 }}"
}
}
</script>
<!-- BreadcrumbList 结构化数据 (v2.4新增) -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "首页",
"item": "{{ request.url_root.rstrip('/') }}"
}{% if site.tags and site.tags|length > 0 %},
{
"@type": "ListItem",
"position": 2,
"name": "{{ site.tags[0].name }}",
"item": "{{ request.url_root.rstrip('/') }}/?tag={{ site.tags[0].slug }}"
}{% endif %},
{
"@type": "ListItem",
"position": {% if site.tags and site.tags|length > 0 %}3{% else %}2{% endif %},
"name": "{{ site.name }}",
"item": "{{ request.url }}"
}
]
}
</script>
{% endblock %}
{% block extra_css %}
<style>
/* v2.4新增: 面包屑导航样式 */
.breadcrumb {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 0;
font-size: 14px;
color: var(--text-secondary);
flex-wrap: nowrap;
white-space: nowrap;
overflow-x: auto;
overflow-y: hidden;
}
.breadcrumb-item {
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 {
color: var(--primary-blue);
}
.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;
}
/* 返回链接 */
.back-link {
display: inline-flex;
align-items: center;
gap: 6px;
color: var(--text-secondary);
text-decoration: none;
font-size: 24px;
font-weight: 500;
transition: color 0.2s;
}
.back-link:hover {
color: var(--text-primary);
}
/* 产品头部区域 */
.product-header-wrapper {
display: flex;
gap: 32px;
margin-bottom: 32px;
align-items: flex-start;
position: relative;
}
.product-main-section {
flex: 1;
background: var(--bg-white);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: 32px;
}
.product-header {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
.product-logo-large {
width: 88px;
height: 88px;
border-radius: var(--radius-lg);
object-fit: cover;
flex-shrink: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.product-main-info {
flex: 1;
}
.product-main-info h1 {
font-size: 32px;
font-weight: 700;
margin-bottom: 8px;
line-height: 1.2;
}
.product-link {
display: inline-flex;
align-items: center;
gap: 4px;
color: var(--primary-blue);
text-decoration: none;
font-size: 14px;
margin-bottom: 16px;
}
.product-link:hover {
text-decoration: underline;
}
.product-meta {
display: flex;
gap: 20px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.meta-item {
display: flex;
align-items: center;
gap: 6px;
color: var(--text-secondary);
font-size: 14px;
}
.product-tags-list {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.product-tag {
padding: 6px 12px;
background: #f1f5f9;
color: #64748b;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
text-decoration: none;
transition: all 0.2s;
}
.product-tag:hover {
background: rgba(14, 165, 233, 0.1);
color: var(--primary-blue);
}
.product-tag.free {
background: rgba(139, 92, 246, 0.1);
color: #8b5cf6;
}
/* Try It Now卡片 - 独立定位 */
.try-now-card {
position: sticky;
top: 88px;
width: 300px;
background: var(--bg-white);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: 24px;
flex-shrink: 0;
}
.try-now-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.try-now-header h3 {
font-size: 16px;
font-weight: 600;
margin: 0;
}
.status-badge {
padding: 4px 10px;
background: rgba(34, 197, 94, 0.1);
color: #059669;
border-radius: 4px;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.visit-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
width: 100%;
padding: 12px;
background: var(--primary-blue);
color: white;
border: none;
border-radius: var(--radius-md);
text-decoration: none;
font-weight: 600;
font-size: 14px;
transition: all 0.2s;
text-transform: uppercase;
letter-spacing: 0.3px;
}
.visit-btn:hover {
background: var(--primary-dark);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(14, 165, 233, 0.3);
}
.visit-hint {
text-align: center;
margin-top: 12px;
font-size: 12px;
color: var(--text-muted);
}
/* 主内容布局 */
.content-layout {
display: grid;
grid-template-columns: 1fr 300px;
gap: 32px;
margin-bottom: 48px;
}
.main-column {
min-width: 0;
}
.sidebar-column {
position: sticky;
top: 88px;
align-self: flex-start;
}
/* 内容块 */
.content-block {
background: var(--bg-white);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: 32px;
margin-bottom: 24px;
}
.content-block h2 {
display: flex;
align-items: center;
gap: 10px;
font-size: 20px;
font-weight: 700;
margin-bottom: 20px;
}
.content-block p {
color: var(--text-secondary);
line-height: 1.7;
margin-bottom: 16px;
}
.content-block ul,
.content-block ol {
margin-left: 20px;
margin-bottom: 16px;
}
.content-block li {
color: var(--text-secondary);
line-height: 1.7;
margin-bottom: 8px;
}
/* 新闻卡片 */
.news-item {
padding: 20px;
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
margin-bottom: 16px;
transition: all 0.2s;
}
.news-item:hover {
border-color: var(--primary-blue);
box-shadow: var(--shadow-md);
}
.news-item:last-child {
margin-bottom: 0;
}
.news-badge {
display: inline-block;
padding: 4px 10px;
background: rgba(139, 92, 246, 0.1);
color: #8b5cf6;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 12px;
}
.news-item h4 {
font-size: 16px;
font-weight: 600;
margin-bottom: 8px;
color: var(--text-primary);
}
.news-item p {
font-size: 14px;
color: var(--text-secondary);
margin-bottom: 8px;
line-height: 1.6;
}
.news-date {
font-size: 12px;
color: var(--text-muted);
}
/* 推荐卡片 */
.recommendations-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
.recommendation-card {
padding: 20px;
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
text-decoration: none;
color: var(--text-primary);
transition: all 0.2s;
display: flex;
gap: 12px;
position: relative;
}
.recommendation-card:hover {
border-color: var(--primary-blue);
box-shadow: var(--shadow-md);
transform: translateY(-2px);
}
.recommendation-card .arrow-icon {
position: absolute;
top: 20px;
right: 20px;
color: #cbd5e1;
font-size: 20px;
transition: all 0.2s;
}
.recommendation-card:hover .arrow-icon {
color: var(--primary-blue);
transform: translate(2px, -2px);
}
.rec-logo {
width: 48px;
height: 48px;
border-radius: var(--radius-md);
object-fit: cover;
flex-shrink: 0;
}
.rec-info {
flex: 1;
padding-right: 24px;
}
.rec-info h4 {
font-size: 14px;
font-weight: 600;
margin-bottom: 4px;
}
.rec-info p {
font-size: 12px;
color: var(--text-secondary);
margin-bottom: 8px;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.rec-tags {
display: flex;
gap: 4px;
flex-wrap: wrap;
}
.rec-tag {
padding: 2px 8px;
background: #f1f5f9;
color: #64748b;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
}
/* Markdown内容样式 */
.markdown-content {
color: var(--text-secondary);
line-height: 1.8;
}
.markdown-content h1,
.markdown-content h2,
.markdown-content h3 {
color: var(--text-primary);
font-weight: 600;
margin-top: 24px;
margin-bottom: 16px;
line-height: 1.4;
}
.markdown-content h1 {
font-size: 24px;
}
.markdown-content h2 {
font-size: 20px;
}
.markdown-content h3 {
font-size: 18px;
}
.markdown-content p {
margin-bottom: 16px;
line-height: 1.8;
}
.markdown-content ul,
.markdown-content ol {
margin: 16px 0;
padding-left: 24px;
}
.markdown-content ul li {
list-style: none;
position: relative;
padding-left: 20px;
margin-bottom: 12px;
line-height: 1.8;
}
.markdown-content ul li:before {
content: "▸";
position: absolute;
left: 0;
color: var(--primary-blue);
font-weight: bold;
}
.markdown-content ol li {
margin-bottom: 12px;
line-height: 1.8;
padding-left: 8px;
}
.markdown-content code {
background: #f1f5f9;
color: #e11d48;
padding: 2px 6px;
border-radius: 4px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 0.9em;
}
.markdown-content pre {
background: #1e293b;
color: #e2e8f0;
padding: 16px;
border-radius: 8px;
overflow-x: auto;
margin: 16px 0;
}
.markdown-content pre code {
background: transparent;
color: inherit;
padding: 0;
border-radius: 0;
}
.markdown-content strong {
font-weight: 600;
color: var(--text-primary);
}
.markdown-content em {
font-style: italic;
}
.markdown-content a {
color: var(--primary-blue);
text-decoration: none;
border-bottom: 1px solid transparent;
transition: border-color 0.2s;
}
.markdown-content a:hover {
border-bottom-color: var(--primary-blue);
}
.markdown-content blockquote {
border-left: 4px solid var(--primary-blue);
padding-left: 16px;
margin: 16px 0;
color: var(--text-secondary);
font-style: italic;
}
.markdown-content hr {
border: none;
border-top: 1px solid var(--border-color);
margin: 24px 0;
}
/* 响应式 */
@media (max-width: 968px) {
.product-header-wrapper {
flex-direction: column;
}
.try-now-card {
width: 100%;
position: static;
}
.content-layout {
grid-template-columns: 1fr;
}
.sidebar-column {
position: static;
}
.recommendations-grid {
grid-template-columns: 1fr;
}
}
</style>
{% endblock %}
{% block content %}
<div class="main-content">
<!-- 顶部空白 -->
<div style="height: 20px;"></div>
<!-- v2.4新增: 面包屑导航 -->
<nav class="breadcrumb" aria-label="breadcrumb">
<div class="breadcrumb-item">
<a href="/">首页</a>
</div>
<span class="breadcrumb-separator"></span>
{% if site.tags and site.tags|length > 0 %}
<div class="breadcrumb-item">
<a href="/?tag={{ site.tags[0].slug }}">{{ site.tags[0].name }}</a>
</div>
<span class="breadcrumb-separator"></span>
{% endif %}
<div class="breadcrumb-item active">
{{ site.name }}
</div>
</nav>
<div style="height: 10px;"></div>
<!-- 返回链接 -->
<a href="/" class="back-link">
<span></span>
返回首页
</a>
<!-- 底部空白 -->
<div style="height: 20px;"></div>
<!-- 产品头部区域 -->
<div class="product-header-wrapper">
<!-- 左侧主内容 -->
<div class="product-main-section">
<div class="product-header">
<!-- Logo -->
{% if site.logo %}
<img src="{{ site.logo }}" alt="{{ site.name }}" class="product-logo-large">
{% else %}
<div class="product-logo-large" style="background: linear-gradient(135deg, #0ea5e9 0%, #8b5cf6 100%);"></div>
{% endif %}
<!-- 产品信息 -->
<div class="product-main-info">
<h1>{{ site.name }}</h1>
<a href="{{ site.url }}" target="_blank" class="product-link">
{{ site.url }}
<span></span>
</a>
<div class="product-meta">
<div class="meta-item">
<span>👁</span>
<span>{{ site.view_count | default(0) }} 次浏览</span>
</div>
<div class="meta-item">
<span>📅</span>
<span>添加于 {{ site.created_at.strftime('%Y年%m月%d日') }}</span>
</div>
</div>
<div class="product-tags-list">
{% for tag in site.tags %}
<a href="/?tag={{ tag.slug }}" class="product-tag">{{ tag.name }}</a>
{% endfor %}
<span class="product-tag free">免费试用</span>
</div>
</div>
</div>
</div>
<!-- Try It Now卡片 -->
<div class="try-now-card">
<div class="try-now-header">
<h3>立即访问</h3>
<span class="status-badge">在线</span>
</div>
<a href="{{ site.url }}" target="_blank" class="visit-btn">
访问网站
<span></span>
</a>
<!-- 收藏按钮 -->
<button type="button" id="collectBtn" class="collect-btn" onclick="toggleCollect()">
<span class="collect-icon" id="collectIcon"></span>
<span class="collect-text" id="collectText">收藏</span>
</button>
<!-- v2.5新增:社媒分享入口 -->
<button type="button" class="share-btn" onclick="openShareModal()">
分享
<span></span>
</button>
<p class="visit-hint">在新标签页打开 • {{ site.url.split('/')[2] if site.url else '' }}</p>
</div>
<!-- v2.5新增:社媒分享弹窗 -->
<div id="shareModal" class="share-modal" style="display:none;">
<div class="share-modal-backdrop" onclick="closeShareModal()"></div>
<div class="share-modal-card" role="dialog" aria-modal="true" aria-label="社媒分享文案">
<div class="share-modal-header">
<div>
<div class="share-modal-title">社媒分享文案</div>
<div class="share-modal-subtitle">{{ site.name }} · 一键生成/一键复制</div>
</div>
<button type="button" class="share-modal-close" onclick="closeShareModal()">×</button>
</div>
<div class="share-platform-tabs" id="shareTabs"></div>
<div class="share-content">
<textarea id="shareText" class="share-textarea" rows="10" readonly placeholder="点击生成后显示内容..."></textarea>
<div class="share-actions">
<button type="button" id="shareGenerateBtn" class="share-action-primary" onclick="generateShare()">生成文案</button>
<button type="button" class="share-action-share" onclick="shareToplatform()">分享</button>
<button type="button" class="share-action-secondary" onclick="copyShare()">复制</button>
</div>
<div id="shareStatus" class="share-status" style="display:none;"></div>
</div>
</div>
</div>
</div>
<!-- 内容布局 -->
<div class="content-layout">
<!-- 主列 -->
<div class="main-column">
<!-- Product Overview -->
<div class="content-block">
<h2>
<span></span>
产品概述
</h2>
<div class="markdown-content">{{ site.description | auto_link(site.id) | markdown | safe }}</div>
</div>
<!-- Detailed Description -->
{% if site.features %}
<div class="content-block">
<h2>
<span>📋</span>
主要功能
</h2>
<div class="markdown-content">{{ site.features | auto_link(site.id) | markdown | safe }}</div>
</div>
{% endif %}
<!-- Related News -->
<div class="content-block">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h2 style="margin: 0;">
<span>📰</span>
相关新闻
</h2>
<button id="refreshNewsBtn" class="refresh-news-btn" onclick="showCaptchaModal('{{ site.code }}')">
<span class="refresh-icon"></span> <span class="btn-text">{% if has_news %}获取最新资讯{% else %}加载资讯{% endif %}</span>
</button>
</div>
<div id="newsContainer">
{% if news_list %}
{% for news in news_list %}
<div class="news-item">
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 12px;">
<span class="news-badge">{{ news.news_type }}</span>
{% if news.source_name %}
<div style="display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text-muted);">
{% if news.source_icon %}
<img src="{{ news.source_icon }}" alt="{{ news.source_name }}" style="width: 16px; height: 16px; border-radius: 2px;">
{% endif %}
<span>{{ news.source_name }}</span>
</div>
{% endif %}
</div>
<h4>
{% if news.url %}
<a href="{{ news.url }}" target="_blank" rel="noopener noreferrer" style="color: var(--text-primary); text-decoration: none;">
{{ news.title }}
</a>
{% else %}
{{ news.title }}
{% endif %}
</h4>
{% if news.content %}
<p>{{ news.content[:200] }}{% if news.content|length > 200 %}...{% endif %}</p>
{% endif %}
<div style="display: flex; justify-content: space-between; align-items: center;">
<div class="news-date">
{% if news.published_at %}
{{ news.published_at.strftime('%Y年%m月%d日') }}
{% else %}
未知日期
{% endif %}
</div>
{% if news.url %}
<a href="{{ news.url }}" target="_blank" rel="noopener noreferrer" style="font-size: 12px; color: var(--primary-blue); text-decoration: none;">
阅读全文 ↗
</a>
{% endif %}
</div>
</div>
{% endfor %}
{% else %}
<div class="news-placeholder" style="text-align: center; padding: 40px 20px; color: var(--text-muted);">
<div style="font-size: 48px; margin-bottom: 16px;">📰</div>
<p style="font-size: 14px;">暂无新闻资讯</p>
<p style="font-size: 12px; margin-top: 8px;">点击右上角"加载资讯"按钮获取最新内容</p>
</div>
{% endif %}
</div><!-- End newsContainer -->
</div>
</div>
<!-- 验证码弹窗 -->
<div id="captchaModal" class="captcha-modal" style="display: none;">
<div class="captcha-modal-content">
<div class="captcha-modal-header">
<h3>安全验证</h3>
<button class="captcha-close-btn" onclick="closeCaptchaModal()">&times;</button>
</div>
<div class="captcha-modal-body">
<p style="color: var(--text-secondary); margin-bottom: 20px; font-size: 14px;">为防止API滥用请完成验证</p>
<div class="captcha-image-container">
<img id="captchaImage" src="" alt="验证码" style="width: 120px; height: 40px; border: 1px solid var(--border-color); border-radius: 4px; cursor: pointer;" onclick="refreshCaptcha()">
<button type="button" onclick="refreshCaptcha()" style="margin-left: 10px; padding: 8px 12px; border: 1px solid var(--border-color); background: white; border-radius: 4px; cursor: pointer;">
<span style="font-size: 16px;"></span>
</button>
</div>
<input type="text" id="captchaInput" placeholder="请输入验证码" maxlength="4" style="width: 100%; padding: 10px; margin-top: 15px; border: 1px solid var(--border-color); border-radius: 6px; font-size: 14px;">
<div id="captchaError" style="color: #ef4444; font-size: 12px; margin-top: 8px; display: none;"></div>
</div>
<div class="captcha-modal-footer">
<button onclick="closeCaptchaModal()" class="captcha-btn captcha-btn-cancel">取消</button>
<button onclick="submitCaptcha()" class="captcha-btn captcha-btn-submit">确定</button>
</div>
</div>
</div>
<!-- 侧边栏 -->
<div class="sidebar-column">
<!-- Similar Recommendations -->
{% if recommended_sites %}
<div class="content-block">
<h2>
<span></span>
相似推荐
</h2>
{% for rec_site in recommended_sites %}
<a href="/site/{{ rec_site.code }}" class="recommendation-card" style="display: flex; gap: 12px; padding: 16px; border: 1px solid var(--border-color); border-radius: 12px; margin-bottom: 12px; text-decoration: none; transition: all 0.2s;">
{% if rec_site.logo %}
<img src="{{ rec_site.logo }}" alt="{{ rec_site.name }}" style="width: 48px; height: 48px; border-radius: 8px; flex-shrink: 0;">
{% else %}
<div style="width: 48px; height: 48px; border-radius: 8px; background: linear-gradient(135deg, #0ea5e9 0%, #8b5cf6 100%); flex-shrink: 0;"></div>
{% endif %}
<div style="flex: 1; min-width: 0;">
<h4 style="font-size: 14px; font-weight: 600; margin: 0 0 4px 0; color: var(--text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">{{ rec_site.name }}</h4>
<p style="font-size: 12px; color: var(--text-secondary); margin: 0; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;">{{ rec_site.short_desc or rec_site.description }}</p>
</div>
</a>
{% endfor %}
</div>
{% endif %}
</div>
</div>
</div>
<style>
.refresh-news-btn {
padding: 8px 16px;
background: linear-gradient(135deg, var(--primary-blue) 0%, var(--primary-dark) 100%);
color: white;
border: none;
border-radius: 8px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
display: flex;
align-items: center;
gap: 6px;
}
.refresh-news-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(14, 165, 233, 0.3);
}
.refresh-news-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.refresh-news-btn.loading .refresh-icon {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.news-status {
padding: 12px;
margin: 16px 0;
border-radius: 8px;
font-size: 14px;
display: none;
}
.news-status.success {
background: rgba(34, 197, 94, 0.1);
border: 1px solid rgba(34, 197, 94, 0.3);
color: #22c55e;
}
.news-status.error {
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.3);
color: #ef4444;
}
</style>
<script>
// v2.6.1优化:所有手动请求都需要验证码
let currentSiteCode = '';
function showCaptchaModal(siteCode) {
currentSiteCode = siteCode;
const modal = document.getElementById('captchaModal');
const input = document.getElementById('captchaInput');
const error = document.getElementById('captchaError');
modal.style.display = 'flex';
input.value = '';
error.style.display = 'none';
// 加载验证码
refreshCaptcha();
}
function closeCaptchaModal() {
document.getElementById('captchaModal').style.display = 'none';
currentSiteCode = '';
}
function refreshCaptcha() {
const img = document.getElementById('captchaImage');
img.src = '/api/captcha?' + new Date().getTime();
}
function submitCaptcha() {
const input = document.getElementById('captchaInput');
const captcha = input.value.trim();
if (!captcha) {
showCaptchaError('请输入验证码');
return;
}
if (captcha.length !== 4) {
showCaptchaError('验证码为4位');
return;
}
// 关闭弹窗,开始加载新闻
closeCaptchaModal();
loadNewsWithCaptcha(currentSiteCode, captcha);
}
function showCaptchaError(message) {
const error = document.getElementById('captchaError');
error.textContent = message;
error.style.display = 'block';
}
function loadNewsWithCaptcha(siteCode, captcha) {
const btn = document.getElementById('refreshNewsBtn');
const newsContainer = document.getElementById('newsContainer');
if (!btn || !newsContainer) return;
// 禁用按钮,显示加载状态
btn.disabled = true;
btn.classList.add('loading');
const originalText = btn.querySelector('.btn-text').textContent;
btn.querySelector('.btn-text').textContent = '加载中...';
// 调用新闻获取API
fetch(`/api/fetch-news/${siteCode}`, {
method: 'POST',
credentials: 'same-origin', // 确保发送session cookie
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ captcha: captcha })
})
.then(response => response.json())
.then(data => {
if (data.success) {
// 更新新闻列表
if (data.news && data.news.length > 0) {
let newsHTML = '';
data.news.forEach(news => {
newsHTML += `
<div class="news-item">
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 12px;">
<span class="news-badge">${news.news_type}</span>
${news.source_name ? `
<div style="display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text-muted);">
${news.source_icon ? `<img src="${news.source_icon}" alt="${news.source_name}" style="width: 16px; height: 16px; border-radius: 2px;">` : ''}
<span>${news.source_name}</span>
</div>
` : ''}
</div>
<h4>
${news.url ? `
<a href="${news.url}" target="_blank" rel="noopener noreferrer" style="color: var(--text-primary); text-decoration: none;">
${news.title}
</a>
` : news.title}
</h4>
${news.content ? `<p>${news.content}${news.content.length >= 200 ? '...' : ''}</p>` : ''}
<div style="display: flex; justify-content: space-between; align-items: center;">
<div class="news-date">${news.published_at}</div>
${news.url ? `
<a href="${news.url}" target="_blank" rel="noopener noreferrer" style="font-size: 12px; color: var(--primary-blue); text-decoration: none;">
阅读全文 ↗
</a>
` : ''}
</div>
</div>
`;
});
newsContainer.innerHTML = newsHTML;
// 显示成功消息
showMessage(data.message, 'success');
} else {
newsContainer.innerHTML = `
<div class="news-placeholder" style="text-align: center; padding: 40px 20px; color: var(--text-muted);">
<div style="font-size: 48px; margin-bottom: 16px;">📰</div>
<p style="font-size: 14px;">暂无新闻资讯</p>
</div>
`;
showMessage('暂无新资讯', 'info');
}
// 恢复按钮
btn.disabled = false;
btn.classList.remove('loading');
btn.querySelector('.btn-text').textContent = '获取最新资讯';
} else {
// 获取失败
showMessage(data.error || '获取失败,请稍后重试', 'error');
btn.disabled = false;
btn.classList.remove('loading');
btn.querySelector('.btn-text').textContent = originalText;
}
})
.catch(error => {
console.error('Error:', error);
showMessage('网络请求失败,请检查网络连接', 'error');
btn.disabled = false;
btn.classList.remove('loading');
btn.querySelector('.btn-text').textContent = originalText;
});
}
function showMessage(message, type = 'info') {
const messageDiv = document.createElement('div');
messageDiv.className = `news-status ${type}`;
messageDiv.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 12px 20px;
border-radius: 8px;
background: ${type === 'success' ? '#10b981' : type === 'error' ? '#ef4444' : '#3b82f6'};
color: white;
font-size: 14px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
z-index: 9999;
animation: slideIn 0.3s ease;
`;
messageDiv.textContent = message;
document.body.appendChild(messageDiv);
setTimeout(() => {
messageDiv.style.animation = 'slideOut 0.3s ease';
setTimeout(() => messageDiv.remove(), 300);
}, 3000);
}
// 支持回车键提交验证码
document.addEventListener('DOMContentLoaded', function() {
const captchaInput = document.getElementById('captchaInput');
if (captchaInput) {
captchaInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
submitCaptcha();
}
});
}
// 点击模态框外部关闭
const modal = document.getElementById('captchaModal');
if (modal) {
modal.addEventListener('click', function(e) {
if (e.target === modal) {
closeCaptchaModal();
}
});
}
});
</script>
<style>
/* 收藏按钮 */
.collect-btn {
margin-top: 12px;
width: 100%;
padding: 12px;
border-radius: var(--radius-md);
border: 1px solid var(--border-color);
background: var(--bg-white);
color: var(--text-secondary);
font-weight: 600;
font-size: 14px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: all 0.2s;
}
.collect-btn:hover {
border-color: var(--primary-blue);
color: var(--primary-blue);
}
.collect-btn.collected {
background: rgba(251, 191, 36, 0.1);
border-color: rgba(251, 191, 36, 0.5);
color: #f59e0b;
}
.collect-btn.collected .collect-icon {
animation: pulse 0.4s ease;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.2); }
}
.share-btn {
margin-top: 12px;
width: 100%;
padding: 12px;
border-radius: var(--radius-md);
border: 1px solid rgba(14, 165, 233, 0.35);
background: rgba(14, 165, 233, 0.08);
color: var(--primary-dark);
font-weight: 700;
font-size: 14px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: all 0.2s;
}
.share-btn:hover {
background: rgba(14, 165, 233, 0.12);
transform: translateY(-1px);
}
.share-modal {
position: fixed;
inset: 0;
z-index: 2000;
}
.share-modal-backdrop {
position: absolute;
inset: 0;
background: rgba(15, 23, 42, 0.55);
}
.share-modal-card {
position: relative;
width: min(720px, calc(100vw - 32px));
margin: 72px auto 0;
background: var(--bg-white);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
overflow: hidden;
}
.share-modal-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
padding: 18px 20px;
border-bottom: 1px solid var(--border-color);
background: linear-gradient(135deg, rgba(14, 165, 233, 0.08) 0%, rgba(139, 92, 246, 0.08) 100%);
}
.share-modal-title {
font-size: 16px;
font-weight: 800;
color: var(--text-primary);
}
.share-modal-subtitle {
font-size: 12px;
color: var(--text-secondary);
margin-top: 2px;
}
.share-modal-close {
width: 32px;
height: 32px;
border-radius: 8px;
border: 1px solid var(--border-color);
background: var(--bg-white);
cursor: pointer;
font-size: 18px;
line-height: 1;
color: var(--text-secondary);
}
.share-platform-tabs {
display: flex;
gap: 8px;
padding: 14px 20px 0;
flex-wrap: wrap;
}
.share-tab {
padding: 8px 12px;
border-radius: 999px;
border: 1px solid var(--border-color);
background: #f8fafc;
color: var(--text-secondary);
font-size: 13px;
cursor: pointer;
transition: all 0.15s;
}
.share-tab.active {
background: rgba(14, 165, 233, 0.12);
border-color: rgba(14, 165, 233, 0.35);
color: var(--primary-dark);
font-weight: 700;
}
.share-content {
padding: 14px 20px 20px;
}
.share-textarea {
width: 100%;
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 12px;
font-size: 14px;
line-height: 1.6;
resize: vertical;
min-height: 180px;
background: var(--bg-page);
}
.share-actions {
display: flex;
gap: 10px;
justify-content: flex-end;
margin-top: 12px;
}
.share-action-primary,
.share-action-share,
.share-action-secondary {
padding: 10px 14px;
border-radius: 10px;
border: 1px solid var(--border-color);
cursor: pointer;
font-weight: 700;
transition: all 0.2s;
}
.share-action-primary {
background: var(--primary-blue);
border-color: rgba(14, 165, 233, 0.6);
color: white;
}
.share-action-primary:disabled {
opacity: 0.65;
cursor: not-allowed;
}
.share-action-share {
background: linear-gradient(135deg, #8b5cf6 0%, #ec4899 100%);
border-color: rgba(139, 92, 246, 0.6);
color: white;
}
.share-action-share:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
}
.share-action-secondary {
background: var(--bg-white);
color: var(--text-primary);
}
.share-status {
margin-top: 10px;
font-size: 13px;
color: var(--text-secondary);
}
.share-status.success { color: #16a34a; }
.share-status.error { color: #dc2626; }
@media (max-width: 520px) {
.share-modal-card { margin-top: 56px; }
.share-actions { flex-direction: column; }
.share-action-primary,
.share-action-share,
.share-action-secondary { width: 100%; }
}
</style>
<script>
const SHARE_PLATFORMS = [
{ key: 'universal', label: '通用', publishUrl: null },
{ key: 'xiaohongshu', label: '小红书', publishUrl: 'https://creator.xiaohongshu.com/publish/publish' },
{ key: 'douyin', label: '抖音', publishUrl: 'https://creator.douyin.com/creator-micro/content/publish' },
{ key: 'bilibili', label: 'B站', publishUrl: 'https://member.bilibili.com/platform/upload/text/edit' },
{ key: 'wechat', label: '公众号', publishUrl: 'https://mp.weixin.qq.com' },
{ key: 'moments', label: '朋友圈', publishUrl: null },
{ key: 'x', label: 'X (Twitter)', publishUrl: 'https://x.com/intent/tweet' },
{ key: 'linkedin', label: 'LinkedIn', publishUrl: 'https://www.linkedin.com/feed/' },
];
let shareActivePlatform = 'universal';
let shareContentCache = {};
function openShareModal() {
const modal = document.getElementById('shareModal');
if (!modal) return;
modal.style.display = 'block';
renderShareTabs();
setSharePlatform(shareActivePlatform);
document.addEventListener('keydown', onShareEsc);
}
function closeShareModal() {
const modal = document.getElementById('shareModal');
if (!modal) return;
modal.style.display = 'none';
document.removeEventListener('keydown', onShareEsc);
}
function onShareEsc(e) {
if (e.key === 'Escape') closeShareModal();
}
function renderShareTabs() {
const tabs = document.getElementById('shareTabs');
if (!tabs) return;
tabs.innerHTML = '';
SHARE_PLATFORMS.forEach(p => {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'share-tab' + (p.key === shareActivePlatform ? ' active' : '');
btn.textContent = p.label;
btn.onclick = () => setSharePlatform(p.key);
tabs.appendChild(btn);
});
}
function setSharePlatform(platformKey) {
shareActivePlatform = platformKey;
renderShareTabs();
const textarea = document.getElementById('shareText');
if (!textarea) return;
textarea.value = shareContentCache[platformKey] || '';
const status = document.getElementById('shareStatus');
if (status) status.style.display = 'none';
}
function setShareStatus(type, message) {
const el = document.getElementById('shareStatus');
if (!el) return;
el.className = 'share-status' + (type ? ' ' + type : '');
el.textContent = message;
el.style.display = 'block';
}
function generateShare() {
const btn = document.getElementById('shareGenerateBtn');
if (btn) {
btn.disabled = true;
btn.textContent = '生成中...';
}
setShareStatus('', '正在生成文案,请稍候...');
const payload = {
site_code: '{{ site.code }}',
platforms: SHARE_PLATFORMS.map(p => p.key)
};
fetch('/api/generate-social-share', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
.then(r => r.json())
.then(data => {
if (!data || !data.success) {
throw new Error((data && data.message) ? data.message : '生成失败');
}
shareContentCache = data.content || {};
setSharePlatform(shareActivePlatform);
setShareStatus('success', '已生成,可直接复制使用。');
})
.catch(err => {
setShareStatus('error', err.message || '网络请求失败');
})
.finally(() => {
if (btn) {
btn.disabled = false;
btn.textContent = '生成文案';
}
});
}
function copyShare() {
const textarea = document.getElementById('shareText');
if (!textarea) return;
const text = textarea.value || '';
if (!text.trim()) {
setShareStatus('error', '没有可复制的内容,请先生成。');
return;
}
const doFallback = () => {
textarea.focus();
textarea.select();
try {
document.execCommand('copy');
setShareStatus('success', '已复制到剪贴板');
} catch (e) {
setShareStatus('error', '复制失败,请手动选择复制');
}
};
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text)
.then(() => setShareStatus('success', '已复制到剪贴板'))
.catch(() => doFallback());
} else {
doFallback();
}
}
function shareToplatform() {
const textarea = document.getElementById('shareText');
if (!textarea) return;
const text = textarea.value || '';
if (!text.trim()) {
setShareStatus('error', '请先生成文案后再分享');
return;
}
// 查找当前平台配置
const platform = SHARE_PLATFORMS.find(p => p.key === shareActivePlatform);
if (!platform) {
setShareStatus('error', '未知平台');
return;
}
// 如果平台没有发布URL提示用户
if (!platform.publishUrl) {
setShareStatus('error', `${platform.label}暂不支持直接跳转,请手动复制后前往平台发布`);
return;
}
// 根据平台处理跳转
let shareUrl = platform.publishUrl;
// X (Twitter) 支持预填充文本
if (platform.key === 'x') {
const encodedText = encodeURIComponent(text);
shareUrl = `${platform.publishUrl}?text=${encodedText}`;
}
// 打开发布页面
window.open(shareUrl, '_blank');
setShareStatus('success', `正在跳转到${platform.label}发布页面...`);
// 自动复制文案到剪贴板
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).catch(() => {});
}
}
// ========== 收藏功能 ==========
const siteCode = '{{ site.code }}';
let isCollected = false;
// 页面加载时检查收藏状态
document.addEventListener('DOMContentLoaded', function() {
checkCollectionStatus();
});
function checkCollectionStatus() {
fetch('/api/auth/status')
.then(r => {
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.json();
})
.then(data => {
if (!data.logged_in || data.user_type !== 'user') {
return; // 未登录或非普通用户,不检查收藏状态
}
// 已登录,检查收藏状态
fetch(`/api/collections/status/${siteCode}`)
.then(r => {
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.json();
})
.then(data => {
isCollected = data.is_collected;
updateCollectButton();
})
.catch(err => console.error('检查收藏状态失败:', err));
})
.catch(err => console.error('检查登录状态失败:', err));
}
function updateCollectButton() {
const btn = document.getElementById('collectBtn');
const text = document.getElementById('collectText');
if (isCollected) {
btn.classList.add('collected');
text.textContent = '已收藏';
} else {
btn.classList.remove('collected');
text.textContent = '收藏';
}
}
function toggleCollect() {
// 先检查登录状态
fetch('/api/auth/status')
.then(r => {
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.json();
})
.then(data => {
if (!data.logged_in) {
// 未登录,跳转到登录页
if (confirm('请先登录后再收藏工具')) {
window.location.href = `/login?next=${encodeURIComponent(window.location.pathname)}`;
}
return;
}
if (data.user_type !== 'user') {
showMessage('管理员账号无法使用收藏功能', 'error');
return;
}
// 已登录,执行收藏/取消收藏
fetch('/api/collections/toggle', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ site_code: siteCode })
})
.then(r => {
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.json();
})
.then(data => {
if (data.success) {
isCollected = data.action === 'added';
updateCollectButton();
showMessage(data.message, 'success');
} else {
showMessage(data.message || '操作失败', 'error');
}
})
.catch(err => {
console.error('收藏操作失败:', err);
showMessage('网络请求失败', 'error');
});
})
.catch(err => {
console.error('检查登录状态失败:', err);
showMessage('网络请求失败', 'error');
});
}
</script>
<style>
/* 验证码弹窗样式 */
.captcha-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
animation: fadeIn 0.2s ease;
}
.captcha-modal-content {
background: white;
border-radius: 12px;
width: 90%;
max-width: 400px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
animation: slideUp 0.3s ease;
}
.captcha-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 24px;
border-bottom: 1px solid var(--border-color);
}
.captcha-modal-header h3 {
margin: 0;
font-size: 18px;
color: var(--text-primary);
}
.captcha-close-btn {
background: none;
border: none;
font-size: 28px;
color: var(--text-muted);
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
transition: color 0.2s;
}
.captcha-close-btn:hover {
color: var(--text-primary);
}
.captcha-modal-body {
padding: 24px;
}
.captcha-image-container {
display: flex;
align-items: center;
justify-content: center;
}
.captcha-modal-footer {
padding: 16px 24px;
border-top: 1px solid var(--border-color);
display: flex;
gap: 12px;
justify-content: flex-end;
}
.captcha-btn {
padding: 10px 24px;
border-radius: 6px;
border: none;
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
}
.captcha-btn-cancel {
background: #f3f4f6;
color: #374151;
}
.captcha-btn-cancel:hover {
background: #e5e7eb;
}
.captcha-btn-submit {
background: #0ea5e9;
color: white;
}
.captcha-btn-submit:hover {
background: #0284c7;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideUp {
from {
transform: translateY(20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
</style>
{% endblock %}