release: v2.2.0 - 博查新闻搜索功能 (生产环境部署版)

核心功能:
  - 集成博查Web Search API自动获取网站相关新闻
  - 智能新闻更新机制(每日首次访问触发)
  - 精确新闻搜索(使用引号强制匹配网站名称)
  - News模型扩展(source_name, source_icon字段)
  - 网站详情页新闻展示模块
  - 新闻来源网站信息展示
  - 自动去重防止重复新闻

  技术实现:
  - NewsSearcher工具类封装博查API
  - 数据库迁移脚本migrate_news_fields.py
  - 测试脚本test_news_feature.py
  - 定期任务脚本fetch_news_cron.py
  - API路由:/api/fetch-site-news, /api/fetch-all-news

  配置优化:
  - 修复manage.sh路径和启动命令
  - 博查API配置(BOCHA_API_KEY, BOCHA_BASE_URL)
  - 新闻搜索参数配置

  界面优化:
  - 详情页新闻模块(左侧主栏)
  - 相似推荐模块(右侧边栏)
  - 首页标签图标修复
  - 后台添加修改密码功能
  - 登录页面优化

  部署信息:
  - 部署日期: 2025-12-30
  - 部署方式: 手动上传文件
  - 数据库: 已迁移(添加source_name和source_icon字段)
This commit is contained in:
ZJPB Admin
2025-12-30 23:44:27 +08:00
parent 9f5d006090
commit b00e52e1e0
12 changed files with 1255 additions and 107 deletions

View File

@@ -85,6 +85,12 @@
<span class="nav-text">批量导入</span>
</a>
</li>
<li class="nav-item">
<a href="{{ url_for('change_password') }}" class="nav-link">
<span class="material-symbols-outlined nav-icon">lock_reset</span>
<span class="nav-text">修改密码</span>
</a>
</li>
<li class="nav-item">
<a href="{{ url_for('index') }}" class="nav-link" target="_blank">
<span class="material-symbols-outlined nav-icon">open_in_new</span>

View File

@@ -1,67 +1,97 @@
{% extends "base.html" %}
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>管理员登录 - ZJPB 焦提示词</title>
{% block title %}管理员登录 - ZJPB 焦提示词{% endblock %}
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=Noto+Sans:wght@400;500;700&display=swap" rel="stylesheet">
{% block extra_css %}
<style>
.glass-panel {
background: rgba(22, 33, 37, 0.7);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
}
.tech-bg-grid {
background-image: radial-gradient(circle at center, rgba(37, 192, 244, 0.05) 0%, transparent 70%),
linear-gradient(rgba(37, 192, 244, 0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(37, 192, 244, 0.03) 1px, transparent 1px);
background-size: 100% 100%, 40px 40px, 40px 40px;
}
</style>
{% endblock %}
<!-- Material Symbols -->
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet">
{% block content %}
<!-- Override default layout -->
</div> <!-- Close main wrapper -->
<body class="bg-background-dark font-display text-white min-h-screen flex flex-col items-center justify-center relative overflow-hidden selection:bg-primary/30">
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
"primary": "#0ea5e9", // Sky 500
"primary-dark": "#0284c7", // Sky 600
"background": "#f8fafc", // Slate 50
"surface": "#ffffff",
"input-bg": "#ffffff",
"input-border": "#e2e8f0", // Slate 200
"text-main": "#0f172a", // Slate 900
"text-secondary": "#334155", // Slate 700
"text-muted": "#64748b", // Slate 500
},
fontFamily: {
"display": ["Space Grotesk", "sans-serif"],
"body": ["Noto Sans", "sans-serif"],
},
},
},
}
</script>
<style>
.glass-panel {
background: rgba(255, 255, 255, 0.75);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
}
.tech-bg-grid {
background-image: radial-gradient(circle at center, rgba(14, 165, 233, 0.04) 0%, transparent 60%),
linear-gradient(rgba(14, 165, 233, 0.05) 1px, transparent 1px),
linear-gradient(90deg, rgba(14, 165, 233, 0.05) 1px, transparent 1px);
background-size: 100% 100%, 40px 40px, 40px 40px;
}
</style>
</head>
<body class="bg-[#f8fafc] font-display text-[#0f172a] min-h-screen flex flex-col items-center justify-center relative overflow-hidden selection:bg-sky-500/20 selection:text-sky-700">
<!-- Ambient Background -->
<div class="absolute inset-0 z-0 tech-bg-grid pointer-events-none"></div>
<div class="absolute top-[-20%] right-[-10%] w-[600px] h-[600px] bg-primary/10 rounded-full blur-[120px] pointer-events-none"></div>
<div class="absolute bottom-[-10%] left-[-10%] w-[500px] h-[500px] bg-purple-600/10 rounded-full blur-[100px] pointer-events-none"></div>
<div class="absolute top-[-20%] right-[-10%] w-[600px] h-[600px] bg-sky-200/40 rounded-full blur-[120px] pointer-events-none mix-blend-multiply"></div>
<div class="absolute bottom-[-10%] left-[-10%] w-[500px] h-[500px] bg-indigo-100/60 rounded-full blur-[100px] pointer-events-none mix-blend-multiply"></div>
<!-- Main Content Wrapper -->
<div class="w-full max-w-[480px] px-4 z-10 flex flex-col gap-6">
<!-- Back Navigation -->
<a class="group flex items-center gap-2 text-gray-400 hover:text-white transition-colors w-fit" href="{{ url_for('index') }}">
<div class="flex items-center justify-center w-8 h-8 rounded-full border border-border-dark bg-surface-dark group-hover:border-primary/50 transition-colors">
<a class="group flex items-center gap-2 text-[#64748b] hover:text-[#0f172a] transition-colors w-fit" href="{{ url_for('index') }}">
<div class="flex items-center justify-center w-8 h-8 rounded-full border border-[#e2e8f0] bg-white group-hover:border-sky-500/50 group-hover:bg-sky-500/5 transition-all shadow-sm">
<span class="material-symbols-outlined text-sm">arrow_back</span>
</div>
<span class="text-sm font-medium">返回首页</span>
</a>
<!-- Login Card -->
<div class="glass-panel border border-white/5 dark:border-border-dark/50 rounded-xl shadow-2xl p-8 md:p-10 relative overflow-hidden">
<div class="glass-panel border border-white rounded-2xl shadow-[0_4px_30px_rgba(0,0,0,0.03)] p-8 md:p-12 relative overflow-hidden ring-1 ring-black/5">
<!-- Decorative Accent -->
<div class="absolute top-0 left-0 w-full h-[2px] bg-gradient-to-r from-transparent via-primary to-transparent opacity-70"></div>
<div class="absolute top-0 left-0 w-full h-[2px] bg-gradient-to-r from-transparent via-sky-500/50 to-transparent"></div>
<!-- Header -->
<div class="flex flex-col gap-2 mb-8">
<div class="flex items-center gap-3 mb-2">
<div class="p-2 rounded-lg bg-primary/10 text-primary">
<div class="p-2 rounded-lg bg-sky-500/10 text-sky-500">
<span class="material-symbols-outlined text-2xl">shield_person</span>
</div>
<span class="text-xs font-bold tracking-widest uppercase text-primary/80">System Access</span>
<span class="text-xs font-bold tracking-widest uppercase text-sky-500">System Access</span>
</div>
<h1 class="text-3xl font-black tracking-tight text-white leading-tight">管理员登录</h1>
<p class="text-gray-400 text-base font-normal">输入您的登录凭据以访问后台管理系统</p>
<h1 class="text-3xl font-black tracking-tight text-[#0f172a] leading-tight">管理员登录</h1>
<p class="text-[#64748b] text-base font-normal">输入您的登录凭据以访问后台管理系统</p>
</div>
<!-- Error Messages -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="mb-6 p-3 rounded-lg bg-red-500/10 border border-red-500/20 flex items-start gap-3">
<span class="material-symbols-outlined text-red-400 text-sm mt-0.5">error</span>
<p class="text-red-400 text-sm">{{ message }}</p>
<div class="mb-6 p-3 rounded-lg bg-red-50 border border-red-200 flex items-start gap-3">
<span class="material-symbols-outlined text-red-500 text-sm mt-0.5">error</span>
<p class="text-red-600 text-sm">{{ message }}</p>
</div>
{% endfor %}
{% endif %}
@@ -71,16 +101,16 @@
<form method="POST" action="{{ url_for('admin_login') }}" class="flex flex-col gap-5">
<!-- Username Field -->
<div class="space-y-2">
<label class="text-white text-sm font-medium leading-normal" for="username">用户名</label>
<label class="text-[#334155] text-sm font-semibold leading-normal" for="username">用户名</label>
<div class="relative group">
<input class="form-input flex w-full h-14 pl-12 pr-4 rounded-lg text-white focus:outline-0 focus:ring-0 border border-border-dark bg-surface-dark focus:border-primary placeholder:text-gray-500 text-base font-normal leading-normal transition-colors"
<input class="form-input flex w-full h-12 pl-11 pr-4 rounded-lg text-[#0f172a] focus:outline-0 focus:ring-2 focus:ring-sky-500/20 border border-[#e2e8f0] bg-white focus:border-sky-500 placeholder:text-slate-400 text-base font-normal leading-normal transition-all shadow-sm"
id="username"
name="username"
placeholder="输入您的用户名或邮箱"
type="text"
required
autofocus/>
<div class="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 group-focus-within:text-primary transition-colors flex items-center justify-center pointer-events-none">
<div class="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400 group-focus-within:text-sky-500 transition-colors flex items-center justify-center pointer-events-none">
<span class="material-symbols-outlined text-[20px]">person</span>
</div>
</div>
@@ -88,34 +118,56 @@
<!-- Password Field -->
<div class="space-y-2">
<label class="text-white text-sm font-medium leading-normal" for="password">密码</label>
<div class="flex justify-between items-end">
<label class="text-[#334155] text-sm font-semibold leading-normal" for="password">密码</label>
</div>
<div class="flex w-full items-stretch rounded-lg group relative">
<input class="form-input flex w-full h-14 pl-12 pr-12 rounded-lg text-white focus:outline-0 focus:ring-0 border border-border-dark bg-surface-dark focus:border-primary placeholder:text-gray-500 text-base font-normal leading-normal transition-colors z-10"
<input class="form-input flex w-full h-12 pl-11 pr-11 rounded-lg text-[#0f172a] focus:outline-0 focus:ring-2 focus:ring-sky-500/20 border border-[#e2e8f0] bg-white focus:border-sky-500 placeholder:text-slate-400 text-base font-normal leading-normal transition-all shadow-sm z-10"
id="password"
name="password"
placeholder="输入您的密码"
type="password"
required/>
<div class="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 group-focus-within:text-primary transition-colors flex items-center justify-center pointer-events-none z-20">
<div class="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400 group-focus-within:text-sky-500 transition-colors flex items-center justify-center pointer-events-none z-20">
<span class="material-symbols-outlined text-[20px]">lock</span>
</div>
<button class="absolute right-0 top-0 h-full px-3 text-slate-400 hover:text-[#334155] transition-colors flex items-center justify-center z-20 focus:outline-none"
type="button"
onclick="togglePassword()">
<span class="material-symbols-outlined text-[20px]" id="toggleIcon">visibility_off</span>
</button>
</div>
</div>
<!-- Login Button -->
<button class="mt-4 flex w-full h-12 cursor-pointer items-center justify-center overflow-hidden rounded-lg bg-primary text-black text-base font-bold leading-normal tracking-[0.015em] hover:bg-primary/90 transition-all active:scale-[0.98] shadow-[0_0_20px_rgba(37,192,244,0.3)] hover:shadow-[0_0_30px_rgba(37,192,244,0.5)]"
<button class="mt-4 flex w-full h-12 cursor-pointer items-center justify-center overflow-hidden rounded-lg bg-sky-500 text-white text-base font-bold leading-normal tracking-wide hover:bg-sky-600 transition-all active:scale-[0.98] shadow-lg shadow-sky-500/25 hover:shadow-sky-500/40"
type="submit">
<span class="truncate">登录</span>
</button>
</form>
<!-- Footer Meta -->
<div class="mt-8 flex justify-center gap-6 border-t border-border-dark/30 pt-6">
<p class="text-gray-400 text-xs text-center">
<div class="mt-8 flex justify-center gap-6 border-t border-slate-100 pt-6">
<p class="text-[#64748b] text-xs text-center font-medium">
ZJPB - 焦提示词 管理系统
</p>
</div>
</div>
</div>
<script>
function togglePassword() {
const passwordInput = document.getElementById('password');
const toggleIcon = document.getElementById('toggleIcon');
if (passwordInput.type === 'password') {
passwordInput.type = 'text';
toggleIcon.textContent = 'visibility';
} else {
passwordInput.type = 'password';
toggleIcon.textContent = 'visibility_off';
}
}
</script>
</body>
{% endblock %}
</html>

View File

@@ -628,15 +628,51 @@
</h2>
{% for news in news_list %}
<div class="news-item">
<span class="news-badge">{{ news.news_type }}</span>
<h4>{{ news.title }}</h4>
<p>{{ news.content[:200] }}...</p>
<div class="news-date">{{ news.published_at.strftime('%b %d, %Y') }}</div>
<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 %}
</div>
{% endif %}
</div>
<!-- 侧边栏 -->
<div class="sidebar-column">
<!-- Similar Recommendations -->
{% if recommended_sites %}
<div class="content-block">
@@ -644,35 +680,22 @@
<span></span>
相似推荐
</h2>
<div class="recommendations-grid">
{% for rec_site in recommended_sites %}
<a href="/site/{{ rec_site.code }}" class="recommendation-card">
{% if rec_site.logo %}
<img src="{{ rec_site.logo }}" alt="{{ rec_site.name }}" class="rec-logo">
{% else %}
<div class="rec-logo" style="background: linear-gradient(135deg, #0ea5e9 0%, #8b5cf6 100%);"></div>
{% endif %}
<div class="rec-info">
<h4>{{ rec_site.name }}</h4>
<p>{{ rec_site.short_desc or rec_site.description }}</p>
<div class="rec-tags">
{% for tag in rec_site.tags[:2] %}
<span class="rec-tag">{{ tag.name }}</span>
{% endfor %}
</div>
</div>
<span class="arrow-icon"></span>
</a>
{% endfor %}
</div>
{% 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 class="sidebar-column">
<!-- 预留侧边栏位置,可以后续添加其他模块 -->
</div>
</div>
</div>
{% endblock %}

View File

@@ -41,6 +41,7 @@
display: flex;
gap: 8px;
flex-wrap: wrap;
position: relative;
}
.category-tab {
@@ -58,6 +59,74 @@
gap: 8px;
}
.category-tab .tag-count {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 20px;
height: 20px;
padding: 0 6px;
background: rgba(100, 116, 139, 0.1);
color: var(--text-secondary);
font-size: 12px;
font-weight: 600;
border-radius: 10px;
transition: all 0.2s;
}
.category-tab.active .tag-count {
background: rgba(255, 255, 255, 0.25);
color: white;
}
.category-tab:hover .tag-count {
background: rgba(59, 130, 246, 0.15);
color: var(--primary-blue);
}
.category-tab.active:hover .tag-count {
background: rgba(255, 255, 255, 0.35);
color: white;
}
.category-tab.hidden-tag {
display: none;
}
.category-tab.show-all .hidden-tag {
display: inline-flex;
}
.more-tags-btn {
padding: 10px 20px;
border: 1px solid var(--border-color);
border-radius: 50px;
background: var(--bg-white);
color: var(--primary-blue);
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
display: inline-flex;
align-items: center;
gap: 8px;
user-select: none;
}
.more-tags-btn:hover {
border-color: var(--primary-blue);
background: rgba(59, 130, 246, 0.05);
}
.more-tags-btn .expand-icon {
font-size: 14px;
transition: transform 0.2s;
}
.more-tags-btn.expanded .expand-icon {
transform: rotate(180deg);
}
.category-tab:hover {
border-color: var(--primary-blue);
color: var(--primary-blue);
@@ -69,8 +138,8 @@
color: white;
}
.category-tab .material-symbols-outlined {
font-size: 18px;
.category-tab .icon {
font-size: 16px;
}
/* 工具网格 */
@@ -187,10 +256,6 @@
white-space: nowrap;
}
.tool-views .material-symbols-outlined {
font-size: 16px;
}
/* 分页 */
.pagination {
display: flex;
@@ -243,7 +308,7 @@
padding: 80px 20px;
}
.empty-state .material-symbols-outlined {
.empty-state .icon {
font-size: 64px;
color: var(--text-muted);
margin-bottom: 16px;
@@ -286,25 +351,32 @@
<!-- 分类过滤 -->
<div class="categories" id="categories">
<div class="category-tabs">
<div class="category-tabs" id="categoryTabs">
<a href="/" class="category-tab {% if not selected_tag %}active{% endif %}">
All Tools
</a>
{% for tag in tags %}
<a href="/?tag={{ tag.slug }}" class="category-tab {% if selected_tag and selected_tag.id == tag.id %}active{% endif %}">
<span class="material-symbols-outlined">
{% if 'Chat' in tag.name or 'GPT' in tag.name %}chat
{% elif 'Image' in tag.name or '图' in tag.name %}image
{% elif 'Video' in tag.name or '视频' in tag.name %}videocam
{% elif 'Writing' in tag.name or '写作' in tag.name %}edit_note
{% elif 'Coding' in tag.name or '代码' in tag.name %}code
{% elif 'Audio' in tag.name or '音频' in tag.name %}graphic_eq
{% elif '3D' in tag.name %}view_in_ar
{% else %}label{% endif %}
<a href="/?tag={{ tag.slug }}" class="category-tab {% if selected_tag and selected_tag.id == tag.id %}active{% endif %} {% if loop.index > 10 %}hidden-tag{% endif %}" data-tag-index="{{ loop.index }}">
<span>
{% if 'Chat' in tag.name or 'GPT' in tag.name or '对话' in tag.name %}💬
{% elif 'Image' in tag.name or '图' in tag.name %}🖼️
{% elif 'Video' in tag.name or '视频' in tag.name %}🎬
{% elif 'Writing' in tag.name or '写作' in tag.name %}✍️
{% elif 'Coding' in tag.name or '代码' in tag.name %}💻
{% elif 'Audio' in tag.name or '音频' in tag.name %}🎵
{% elif '3D' in tag.name %}🎨
{% else %}🏷️{% endif %}
</span>
{{ tag.name }}
<span class="tag-count">{{ tag_counts.get(tag.id, 0) }}</span>
</a>
{% endfor %}
{% if tags|length > 10 %}
<div class="more-tags-btn" id="moreTagsBtn">
<span>更多</span>
<span class="expand-icon"></span>
</div>
{% endif %}
</div>
</div>
@@ -313,7 +385,7 @@
{% if search_query %}
<div style="margin-bottom: 24px; padding: 16px; background: #f1f5f9; border-radius: 8px;">
<p style="margin: 0; color: #64748b; font-size: 14px;">
<span class="material-symbols-outlined" style="font-size: 18px; vertical-align: middle;">search</span>
<span style="font-size: 18px; vertical-align: middle;">🔍</span>
搜索 "{{ search_query }}" 的结果:找到 {{ pagination.total if pagination else sites|length }} 个工具
<a href="/" style="margin-left: 12px; color: #0ea5e9; text-decoration: none;">清除搜索</a>
</p>
@@ -329,7 +401,7 @@
{% else %}
<div class="tool-logo" style="background: linear-gradient(135deg, #0ea5e9 0%, #8b5cf6 100%);"></div>
{% endif %}
<span class="material-symbols-outlined tool-link-icon">north_east</span>
<span class="tool-link-icon"></span>
</div>
<h3 class="tool-name">{{ site.name }}</h3>
<p class="tool-description">{{ site.short_desc or site.description }}</p>
@@ -340,7 +412,7 @@
{% endfor %}
</div>
<div class="tool-views">
<span class="material-symbols-outlined">visibility</span>
<span>👁</span>
<span>{% if site.view_count >= 1000 %}{{ (site.view_count / 1000) | round(1) }}k{% else %}{{ site.view_count | default(0) }}{% endif %}</span>
</div>
</div>
@@ -354,11 +426,11 @@
<!-- 上一页 -->
{% if pagination.has_prev %}
<a href="?page={{ pagination.prev_num }}{% if selected_tag %}&tag={{ selected_tag.slug }}{% endif %}{% if search_query %}&q={{ search_query }}{% endif %}">
<span class="material-symbols-outlined">chevron_left</span>
<span></span>
</a>
{% else %}
<a href="#" class="disabled">
<span class="material-symbols-outlined">chevron_left</span>
<span></span>
</a>
{% endif %}
@@ -391,22 +463,52 @@
<!-- 下一页 -->
{% if pagination.has_next %}
<a href="?page={{ pagination.next_num }}{% if selected_tag %}&tag={{ selected_tag.slug }}{% endif %}{% if search_query %}&q={{ search_query }}{% endif %}">
<span class="material-symbols-outlined">chevron_right</span>
<span></span>
</a>
{% else %}
<a href="#" class="disabled">
<span class="material-symbols-outlined">chevron_right</span>
<span></span>
</a>
{% endif %}
</div>
{% endif %}
{% else %}
<div class="empty-state">
<span class="material-symbols-outlined">search_off</span>
<span class="icon">🔍</span>
<h3>暂无工具</h3>
<p>{% if selected_tag %}该分类下还没有工具{% else %}还没有添加任何工具{% endif %}</p>
</div>
{% endif %}
</div>
</div>
<script>
// 标签展开/收起功能
document.addEventListener('DOMContentLoaded', function() {
const moreTagsBtn = document.getElementById('moreTagsBtn');
if (moreTagsBtn) {
moreTagsBtn.addEventListener('click', function() {
const categoryTabs = document.getElementById('categoryTabs');
const hiddenTags = categoryTabs.querySelectorAll('.hidden-tag');
const isExpanded = moreTagsBtn.classList.contains('expanded');
if (isExpanded) {
// 收起
hiddenTags.forEach(tag => {
tag.style.display = 'none';
});
moreTagsBtn.classList.remove('expanded');
moreTagsBtn.querySelector('span:first-child').textContent = '更多';
} else {
// 展开
hiddenTags.forEach(tag => {
tag.style.display = 'inline-flex';
});
moreTagsBtn.classList.add('expanded');
moreTagsBtn.querySelector('span:first-child').textContent = '收起';
}
});
}
});
</script>
{% endblock %}