release: v2.0 - 完整功能管理系统

主要功能:
- 完整的Flask-Admin后台管理系统
- 网站/标签/新闻管理功能
- 用户登录认证系统
- 科技感/未来风UI设计
- 标签分类系统(取代传统分类)
- 详情页面展示
- 数据库迁移脚本
- 书签导入解析工具

技术栈:
- Flask + SQLAlchemy
- Flask-Admin管理界面
- Bootstrap 4响应式设计
- 用户认证与权限管理

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Jowe
2025-12-28 19:21:17 +08:00
parent 2fbca6ebc7
commit 9e47ebe749
49 changed files with 6274 additions and 1353 deletions

View File

@@ -0,0 +1,363 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>批量导入 - ZJPB 焦提示词</title>
<!-- 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">
<!-- Google Material Symbols -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Custom Admin Theme -->
<link href="{{ url_for('static', filename='css/admin-sidebar.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/admin-actions.css') }}" rel="stylesheet">
</head>
<body class="admin-sidebar-layout">
<!-- 左侧菜单栏 -->
<aside class="admin-sidebar">
<!-- Logo -->
<div class="sidebar-logo">
<span class="material-symbols-outlined logo-icon">blur_on</span>
<span class="logo-text">ZJPB 焦提示词</span>
</div>
<!-- 主菜单 -->
<nav class="sidebar-nav">
<div class="nav-section">
<div class="nav-section-title">主菜单</div>
<ul class="nav-menu">
<li class="nav-item">
<a href="{{ url_for('admin.index') }}" class="nav-link">
<span class="material-symbols-outlined nav-icon">dashboard</span>
<span class="nav-text">控制台</span>
</a>
</li>
<li class="nav-item">
<a href="{{ url_for('site.index_view') }}" class="nav-link">
<span class="material-symbols-outlined nav-icon">public</span>
<span class="nav-text">网站管理</span>
</a>
</li>
<li class="nav-item">
<a href="{{ url_for('tag.index_view') }}" class="nav-link">
<span class="material-symbols-outlined nav-icon">label</span>
<span class="nav-text">标签管理</span>
</a>
</li>
<li class="nav-item">
<a href="{{ url_for('news.index_view') }}" class="nav-link">
<span class="material-symbols-outlined nav-icon">newspaper</span>
<span class="nav-text">新闻管理</span>
</a>
</li>
<li class="nav-item">
<a href="{{ url_for('admin_users.index_view') }}" class="nav-link">
<span class="material-symbols-outlined nav-icon">admin_panel_settings</span>
<span class="nav-text">管理员</span>
</a>
</li>
</ul>
</div>
<!-- 系统菜单 -->
<div class="nav-section">
<div class="nav-section-title">系统</div>
<ul class="nav-menu">
<li class="nav-item active">
<a href="{{ url_for('batch_import') }}" class="nav-link">
<span class="material-symbols-outlined nav-icon">upload_file</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>
<span class="nav-text">查看网站</span>
</a>
</li>
<li class="nav-item">
<a href="{{ url_for('admin_logout') }}" class="nav-link">
<span class="material-symbols-outlined nav-icon">logout</span>
<span class="nav-text">退出登录</span>
</a>
</li>
</ul>
</div>
</nav>
<!-- 用户信息 -->
<div class="sidebar-user">
<div class="user-avatar">
<span class="material-symbols-outlined">account_circle</span>
</div>
<div class="user-info">
<div class="user-name">{{ current_user.username }}</div>
<div class="user-email">{{ current_user.email or 'admin@zjpb.com' }}</div>
</div>
</div>
</aside>
<!-- 右侧主内容区 -->
<div class="admin-main">
<!-- 顶部导航栏 -->
<header class="admin-header">
<div class="header-breadcrumb">
<a href="{{ url_for('admin.index') }}" class="breadcrumb-link">控制台</a>
<span class="breadcrumb-separator">/</span>
<span class="breadcrumb-current">批量导入</span>
</div>
<div class="header-actions">
<div class="search-box">
<span class="material-symbols-outlined search-icon">search</span>
<input type="text" placeholder="全局搜索..." class="search-input">
</div>
<button class="header-btn">
<span class="material-symbols-outlined">notifications</span>
</button>
<button class="header-btn">
<span class="material-symbols-outlined">settings</span>
</button>
</div>
</header>
<!-- 页面内容 -->
<main class="admin-content">
<div class="page-header">
<div>
<h1 class="page-title">批量导入网站</h1>
<p class="page-description">支持通过URL列表或Chrome书签文件批量导入网站</p>
</div>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'success' if category == 'success' else 'danger' }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="close" data-dismiss="alert">
<span>&times;</span>
</button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="card">
<div class="card-body">
<ul class="nav nav-tabs mb-4" role="tablist">
<li class="nav-item">
<a class="nav-link active" data-toggle="tab" href="#url-list">URL列表导入</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#bookmark-file">Chrome书签导入</a>
</li>
</ul>
<div class="tab-content">
<!-- URL列表导入 -->
<div class="tab-pane fade show active" id="url-list">
<form method="POST" action="{{ url_for('batch_import') }}">
<input type="hidden" name="import_type" value="url_list">
<div class="form-group">
<label for="url-textarea">网站URL列表</label>
<textarea class="form-control" id="url-textarea" name="url_list" rows="10"
placeholder="每行一个URL例如&#10;https://www.example.com&#10;https://www.google.com&#10;https://github.com"></textarea>
<small class="form-text text-muted">
每行输入一个网站URL系统将自动抓取网站名称、描述和Logo
</small>
</div>
<div class="form-group">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="auto-activate" name="auto_activate" checked>
<label class="custom-control-label" for="auto-activate">自动启用导入的网站</label>
</div>
</div>
<button type="submit" class="btn btn-primary">
<span class="material-symbols-outlined" style="font-size: 18px; vertical-align: middle;">upload_file</span>
开始导入
</button>
</form>
</div>
<!-- Chrome书签导入 -->
<div class="tab-pane fade" id="bookmark-file">
<form method="POST" action="{{ url_for('batch_import') }}" enctype="multipart/form-data">
<input type="hidden" name="import_type" value="bookmark_file">
<div class="alert alert-info">
<strong>如何导出Chrome书签</strong>
<ol class="mb-0 mt-2">
<li>打开Chrome浏览器</li>
<li><kbd>Ctrl + Shift + O</kbd> 打开书签管理器</li>
<li>点击右上角的 <strong></strong> 菜单</li>
<li>选择 <strong>导出书签</strong></li>
<li>保存为HTML文件</li>
</ol>
</div>
<div class="form-group">
<label for="bookmark-file-input">选择书签文件</label>
<div class="custom-file">
<input type="file" class="custom-file-input" id="bookmark-file-input"
name="bookmark_file" accept=".html,.htm" required>
<label class="custom-file-label" for="bookmark-file-input">选择文件...</label>
</div>
<small class="form-text text-muted">
仅支持HTML格式的Chrome书签导出文件
</small>
</div>
<div class="form-group">
<label for="folder-filter">筛选文件夹(可选)</label>
<input type="text" class="form-control" id="folder-filter" name="folder_filter"
placeholder="例如AI工具">
<small class="form-text text-muted">
留空则导入所有书签,填写文件夹名称则只导入该文件夹下的书签
</small>
</div>
<div class="form-group">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="auto-activate-2" name="auto_activate" checked>
<label class="custom-control-label" for="auto-activate-2">自动启用导入的网站</label>
</div>
</div>
<button type="submit" class="btn btn-primary">
<span class="material-symbols-outlined" style="font-size: 18px; vertical-align: middle;">upload_file</span>
开始导入
</button>
</form>
</div>
</div>
</div>
</div>
{% if results %}
<div class="card mt-4">
<div class="card-header">
<h5 class="mb-0">导入结果</h5>
</div>
<div class="card-body">
<div class="alert alert-success">
<strong>导入完成!</strong>
成功: {{ results.success_count }},
失败: {{ results.failed_count }},
总计: {{ results.total_count }}
</div>
{% if results.success_list %}
<h6>成功导入 ({{ results.success_count }})</h6>
<ul class="list-group mb-3">
{% for item in results.success_list %}
<li class="list-group-item">
<span class="material-symbols-outlined text-success" style="font-size: 18px; vertical-align: middle;">check_circle</span>
<strong>{{ item.name }}</strong> - {{ item.url }}
</li>
{% endfor %}
</ul>
{% endif %}
{% if results.failed_list %}
<h6 class="text-danger">导入失败 ({{ results.failed_count }})</h6>
<div class="alert alert-warning">
<small><strong>提示:</strong>失败的URL不会影响其他URL的导入您可以稍后手动添加这些网站。</small>
</div>
<div class="table-responsive">
<table class="table table-sm table-hover">
<thead class="thead-light">
<tr>
<th style="width: 40px;">#</th>
<th style="width: 30%;">网站名称</th>
<th style="width: 35%;">URL</th>
<th style="width: 35%;">失败原因</th>
</tr>
</thead>
<tbody>
{% for item in results.failed_list %}
<tr>
<td>
<span class="material-symbols-outlined text-danger" style="font-size: 20px;">cancel</span>
</td>
<td>
<strong>{{ item.name or '未知' }}</strong>
</td>
<td>
<small class="text-muted" style="word-break: break-all;">{{ item.url }}</small>
</td>
<td>
<span class="badge badge-danger">{{ item.error }}</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div>
</div>
{% endif %}
</main>
</div>
<!-- Bootstrap JS -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/js/bootstrap.bundle.min.js"></script>
<style>
.page-title {
font-size: 24px;
font-weight: 600;
margin-bottom: 8px;
}
.page-description {
color: #666;
margin-bottom: 24px;
}
.card {
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
border: 1px solid #e0e0e0;
}
.card-header {
background: #f8f9fa;
border-bottom: 1px solid #dee2e6;
}
kbd {
background-color: #f7f7f7;
border: 1px solid #ccc;
border-radius: 3px;
box-shadow: 0 1px 0 rgba(0,0,0,0.2);
color: #333;
display: inline-block;
font-family: monospace;
font-size: 12px;
padding: 2px 6px;
}
</style>
<script>
// 文件选择器显示文件名
document.querySelector('.custom-file-input').addEventListener('change', function(e) {
var fileName = e.target.files[0].name;
var label = e.target.nextElementSibling;
label.textContent = fileName;
});
</script>
</body>
</html>

View File

@@ -8,10 +8,14 @@
<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">
<!-- Custom Admin Theme -->
<link href="{{ url_for('static', filename='css/admin-theme.css') }}" rel="stylesheet">
<style>
/* 强制应用亮色主题到body */
body {
background: #F3F3F3 !important;
color: #000000 !important;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif !important;
}
</style>
{% endblock %}
{% block body %}
<body class="admin-theme">
{{ super() }}
</body>
{% endblock %}
{% block body_class %}admin-theme{% endblock %}

239
templates/admin/index.html Normal file
View File

@@ -0,0 +1,239 @@
{% extends 'admin/master.html' %}
{% block body %}
<div class="dashboard-container">
<div class="row">
<!-- 统计卡片 -->
<div class="col-md-3 mb-4">
<div class="stat-card">
<div class="stat-icon" style="background: rgba(0, 82, 217, 0.1); color: #0052D9;">
<span class="material-symbols-outlined">public</span>
</div>
<div class="stat-info">
<div class="stat-value">{{ stats.sites_count or 0 }}</div>
<div class="stat-label">AI工具总数</div>
</div>
</div>
</div>
<div class="col-md-3 mb-4">
<div class="stat-card">
<div class="stat-icon" style="background: rgba(0, 168, 112, 0.1); color: #00A870;">
<span class="material-symbols-outlined">label</span>
</div>
<div class="stat-info">
<div class="stat-value">{{ stats.tags_count or 0 }}</div>
<div class="stat-label">标签分类</div>
</div>
</div>
</div>
<div class="col-md-3 mb-4">
<div class="stat-card">
<div class="stat-icon" style="background: rgba(227, 115, 24, 0.1); color: #E37318;">
<span class="material-symbols-outlined">newspaper</span>
</div>
<div class="stat-info">
<div class="stat-value">{{ stats.news_count or 0 }}</div>
<div class="stat-label">新闻动态</div>
</div>
</div>
</div>
<div class="col-md-3 mb-4">
<div class="stat-card">
<div class="stat-icon" style="background: rgba(213, 73, 65, 0.1); color: #D54941;">
<span class="material-symbols-outlined">visibility</span>
</div>
<div class="stat-info">
<div class="stat-value">{{ stats.total_views or 0 }}</div>
<div class="stat-label">总浏览量</div>
</div>
</div>
</div>
</div>
<!-- 快捷操作 -->
<div class="row">
<div class="col-md-12 mb-4">
<div class="card">
<div class="card-header">
<h5 class="mb-0">快捷操作</h5>
</div>
<div class="card-body">
<div class="quick-actions">
<a href="{{ url_for('site.create_view') }}" class="quick-action-btn">
<span class="material-symbols-outlined">add_circle</span>
<span>添加新工具</span>
</a>
<a href="{{ url_for('tag.index_view') }}" class="quick-action-btn">
<span class="material-symbols-outlined">label</span>
<span>管理标签</span>
</a>
<a href="{{ url_for('news.create_view') }}" class="quick-action-btn">
<span class="material-symbols-outlined">post_add</span>
<span>发布新闻</span>
</a>
<a href="{{ url_for('index') }}" class="quick-action-btn" target="_blank">
<span class="material-symbols-outlined">open_in_new</span>
<span>查看网站</span>
</a>
</div>
</div>
</div>
</div>
</div>
<!-- 最近添加的工具 -->
<div class="row">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0">最近添加的工具</h5>
</div>
<div class="card-body">
{% if recent_sites %}
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>名称</th>
<th>URL</th>
<th>浏览量</th>
<th>状态</th>
<th>添加时间</th>
</tr>
</thead>
<tbody>
{% for site in recent_sites %}
<tr>
<td>
<div class="d-flex align-items-center">
{% if site.logo %}
<img src="{{ site.logo }}" alt="{{ site.name }}" style="width: 32px; height: 32px; border-radius: 4px; margin-right: 12px; object-fit: cover;">
{% endif %}
<strong>{{ site.name }}</strong>
</div>
</td>
<td>
<a href="{{ site.url }}" target="_blank" style="color: #0052D9; text-decoration: none;">
{{ site.url[:50] }}...
</a>
</td>
<td>{{ site.view_count }}</td>
<td>
{% if site.is_active %}
<span class="badge badge-success">已启用</span>
{% else %}
<span class="badge badge-secondary">已禁用</span>
{% endif %}
</td>
<td>{{ site.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-muted text-center mb-0">暂无数据</p>
{% endif %}
</div>
</div>
</div>
</div>
</div>
<style>
.dashboard-container {
max-width: 1400px;
}
.stat-card {
background: white;
border: 1px solid #DCDFE6;
border-radius: 6px;
padding: 20px;
display: flex;
align-items: center;
gap: 16px;
box-shadow: 0 1px 4px rgba(0, 0, 0, .05);
}
.stat-icon {
width: 56px;
height: 56px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
}
.stat-icon .material-symbols-outlined {
font-size: 28px;
}
.stat-info {
flex: 1;
}
.stat-value {
font-size: 28px;
font-weight: 600;
color: #000000;
line-height: 1;
margin-bottom: 4px;
}
.stat-label {
font-size: 14px;
color: #606266;
}
.quick-actions {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 12px;
}
.quick-action-btn {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: #F5F7FA;
border: 1px solid #DCDFE6;
border-radius: 6px;
color: #000000;
text-decoration: none;
transition: all 0.2s;
}
.quick-action-btn:hover {
background: #ECF2FE;
border-color: #0052D9;
color: #0052D9;
text-decoration: none;
transform: translateY(-2px);
}
.quick-action-btn .material-symbols-outlined {
font-size: 24px;
}
.badge {
padding: 4px 8px;
font-size: 12px;
border-radius: 4px;
}
.badge-success {
background: rgba(0, 168, 112, 0.1);
color: #00A870;
}
.badge-secondary {
background: #F5F5F5;
color: #606266;
}
</style>
{% endblock %}

176
templates/admin/master.html Normal file
View File

@@ -0,0 +1,176 @@
{% import 'admin/layout.html' as layout with context -%}
{% import 'admin/static.html' as admin_static with context %}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{% if admin_view.category %}{{ admin_view.category }} - {% endif %}{{ admin_view.name }} - {{ admin_view.admin.name }}{% endblock %}</title>
<!-- 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">
<!-- Google Material Symbols -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Custom Admin Theme -->
<link href="{{ url_for('static', filename='css/admin-sidebar.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/admin-actions.css') }}" rel="stylesheet">
<!-- 暂时禁用中文化CSS等待精确调整 -->
<!-- <link href="{{ url_for('static', filename='css/admin-i18n.css') }}" rel="stylesheet"> -->
{% block head_css %}{% endblock %}
</head>
<body class="admin-sidebar-layout">
<!-- 左侧菜单栏 -->
<aside class="admin-sidebar">
<!-- Logo -->
<div class="sidebar-logo">
<span class="material-symbols-outlined logo-icon">blur_on</span>
<span class="logo-text">ZJPB 焦提示词</span>
</div>
<!-- 主菜单 -->
<nav class="sidebar-nav">
<div class="nav-section">
<div class="nav-section-title">主菜单</div>
<ul class="nav-menu">
{% set active_category = admin_view.category if admin_view.category else '' %}
{% set active_name = admin_view.name if admin_view.name else '' %}
{% for item in admin_view.admin._menu %}
{% if item.is_category() %}
{# 分类菜单 #}
{% for child in item.get_children() %}
{% set is_active = (child.name == active_name) %}
<li class="nav-item {% if is_active %}active{% endif %}">
<a href="{{ child.get_url() }}" class="nav-link">
<span class="material-symbols-outlined nav-icon">
{% if '网站' in child.name %}public
{% elif '标签' in child.name %}label
{% elif '新闻' in child.name %}newspaper
{% elif '管理员' in child.name %}admin_panel_settings
{% else %}circle{% endif %}
</span>
<span class="nav-text">{{ child.name }}</span>
</a>
</li>
{% endfor %}
{% else %}
{# 单独菜单项 #}
{% set is_active = (item.name == active_name) %}
<li class="nav-item {% if is_active %}active{% endif %}">
<a href="{{ item.get_url() }}" class="nav-link">
<span class="material-symbols-outlined nav-icon">dashboard</span>
<span class="nav-text">{{ item.name }}</span>
</a>
</li>
{% endif %}
{% endfor %}
</ul>
</div>
<!-- 系统菜单 -->
<div class="nav-section">
<div class="nav-section-title">系统</div>
<ul class="nav-menu">
<li class="nav-item">
<a href="{{ url_for('batch_import') }}" class="nav-link">
<span class="material-symbols-outlined nav-icon">upload_file</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>
<span class="nav-text">查看网站</span>
</a>
</li>
<li class="nav-item">
<a href="{{ url_for('admin_logout') }}" class="nav-link">
<span class="material-symbols-outlined nav-icon">logout</span>
<span class="nav-text">退出登录</span>
</a>
</li>
</ul>
</div>
</nav>
<!-- 用户信息 -->
<div class="sidebar-user">
<div class="user-avatar">
<span class="material-symbols-outlined">account_circle</span>
</div>
<div class="user-info">
<div class="user-name">{{ current_user.username }}</div>
<div class="user-email">{{ current_user.email or 'admin@zjpb.com' }}</div>
</div>
</div>
</aside>
<!-- 右侧主内容区 -->
<div class="admin-main">
<!-- 顶部导航栏 -->
<header class="admin-header">
<div class="header-breadcrumb">
<a href="{{ url_for('admin.index') }}" class="breadcrumb-link">控制台</a>
<span class="breadcrumb-separator">/</span>
<span class="breadcrumb-current">{{ admin_view.name }}</span>
</div>
<div class="header-actions">
<div class="search-box">
<span class="material-symbols-outlined search-icon">search</span>
<input type="text" placeholder="全局搜索..." class="search-input">
</div>
<button class="header-btn">
<span class="material-symbols-outlined">notifications</span>
</button>
<button class="header-btn">
<span class="material-symbols-outlined">settings</span>
</button>
</div>
</header>
<!-- 页面内容 -->
<main class="admin-content">
{% block page_body %}
<div class="page-header">
<div>
<h1 class="page-title">{% block brand %}{{ admin_view.name }}{% endblock %}</h1>
{% if admin_view.name == '网站管理' %}
<p class="page-description">管理和维护AI工具导航平台的所有工具。</p>
{% elif admin_view.name == '标签管理' %}
<p class="page-description">管理工具分类标签,优化内容组织。</p>
{% elif admin_view.name == '新闻管理' %}
<p class="page-description">管理工具相关新闻和更新动态。</p>
{% elif admin_view.name == '管理员' %}
<p class="page-description">管理后台管理员账号和权限。</p>
{% else %}
<p class="page-description">{{ admin_view.category or '' }}</p>
{% endif %}
</div>
{% block page_actions %}{% endblock %}
</div>
{% block messages %}
{{ layout.messages() }}
{% endblock %}
{% block body %}{% endblock %}
{% endblock %}
</main>
</div>
<!-- Bootstrap JS -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/js/bootstrap.bundle.min.js"></script>
{% block tail_js %}{% endblock %}
{% block tail %}{% endblock %}
</body>
</html>

View File

@@ -7,30 +7,40 @@
margin-top: 10px;
margin-bottom: 15px;
}
.generate-tags-btn {
margin-top: 10px;
margin-bottom: 15px;
}
.fetch-status {
margin-top: 10px;
padding: 10px;
border-radius: 8px;
display: none;
}
.fetch-status.success {
.tags-status {
margin-top: 10px;
padding: 10px;
border-radius: 8px;
display: none;
}
.fetch-status.success, .tags-status.success {
background-color: rgba(34, 197, 94, 0.1);
border: 1px solid rgba(34, 197, 94, 0.3);
color: #4ade80;
}
.fetch-status.error {
.fetch-status.error, .tags-status.error {
background-color: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.3);
color: #f87171;
}
.auto-fetch-btn .loading-icon {
.auto-fetch-btn .loading-icon, .generate-tags-btn .loading-icon {
display: none;
}
.auto-fetch-btn.loading .loading-icon {
.auto-fetch-btn.loading .loading-icon, .generate-tags-btn.loading .loading-icon {
display: inline-block;
animation: spin 1s linear infinite;
}
.auto-fetch-btn.loading .normal-icon {
.auto-fetch-btn.loading .normal-icon, .generate-tags-btn.loading .normal-icon {
display: none;
}
@keyframes spin {
@@ -119,6 +129,75 @@ document.addEventListener('DOMContentLoaded', function() {
statusDiv.style.display = 'block';
}
}
// 在标签字段后添加"AI生成标签"按钮
const tagsField = document.querySelector('select[name="tags"]');
if (tagsField) {
const generateBtn = document.createElement('button');
generateBtn.type = 'button';
generateBtn.className = 'btn btn-success generate-tags-btn';
generateBtn.innerHTML = '<span class="normal-icon">✨</span><span class="loading-icon">↻</span> AI生成标签';
const tagsStatusDiv = document.createElement('div');
tagsStatusDiv.className = 'tags-status';
tagsField.parentNode.appendChild(generateBtn);
tagsField.parentNode.appendChild(tagsStatusDiv);
generateBtn.addEventListener('click', function() {
const nameField = document.querySelector('input[name="name"]');
const descriptionField = document.querySelector('textarea[name="description"]');
const name = nameField ? nameField.value.trim() : '';
const description = descriptionField ? descriptionField.value.trim() : '';
if (!name || !description) {
showTagsStatus('请先填写网站名称和描述', 'error');
return;
}
// 显示加载状态
generateBtn.disabled = true;
generateBtn.classList.add('loading');
tagsStatusDiv.style.display = 'none';
// 调用API生成标签
fetch('/api/generate-tags', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: name,
description: description
})
})
.then(response => response.json())
.then(data => {
if (data.success && data.tags && data.tags.length > 0) {
// 显示生成的标签
const tagsText = data.tags.join(', ');
showTagsStatus('✓ AI生成的标签建议: ' + tagsText + '\n请在标签字段中手动选择或创建这些标签', 'success');
} else {
showTagsStatus('✗ ' + (data.message || '标签生成失败'), 'error');
}
})
.catch(error => {
console.error('Error:', error);
showTagsStatus('✗ 网络请求失败,请重试', 'error');
})
.finally(() => {
generateBtn.disabled = false;
generateBtn.classList.remove('loading');
});
});
function showTagsStatus(message, type) {
tagsStatusDiv.textContent = message;
tagsStatusDiv.className = 'tags-status ' + type;
tagsStatusDiv.style.display = 'block';
}
}
});
</script>
{% endblock %}

View File

@@ -7,30 +7,40 @@
margin-top: 10px;
margin-bottom: 15px;
}
.generate-tags-btn {
margin-top: 10px;
margin-bottom: 15px;
}
.fetch-status {
margin-top: 10px;
padding: 10px;
border-radius: 8px;
display: none;
}
.fetch-status.success {
.tags-status {
margin-top: 10px;
padding: 10px;
border-radius: 8px;
display: none;
}
.fetch-status.success, .tags-status.success {
background-color: rgba(34, 197, 94, 0.1);
border: 1px solid rgba(34, 197, 94, 0.3);
color: #4ade80;
}
.fetch-status.error {
.fetch-status.error, .tags-status.error {
background-color: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.3);
color: #f87171;
}
.auto-fetch-btn .loading-icon {
.auto-fetch-btn .loading-icon, .generate-tags-btn .loading-icon {
display: none;
}
.auto-fetch-btn.loading .loading-icon {
.auto-fetch-btn.loading .loading-icon, .generate-tags-btn.loading .loading-icon {
display: inline-block;
animation: spin 1s linear infinite;
}
.auto-fetch-btn.loading .normal-icon {
.auto-fetch-btn.loading .normal-icon, .generate-tags-btn.loading .normal-icon {
display: none;
}
@keyframes spin {
@@ -119,6 +129,75 @@ document.addEventListener('DOMContentLoaded', function() {
statusDiv.style.display = 'block';
}
}
// 在标签字段后添加"AI生成标签"按钮
const tagsField = document.querySelector('select[name="tags"]');
if (tagsField) {
const generateBtn = document.createElement('button');
generateBtn.type = 'button';
generateBtn.className = 'btn btn-success generate-tags-btn';
generateBtn.innerHTML = '<span class="normal-icon">✨</span><span class="loading-icon">↻</span> AI生成标签';
const tagsStatusDiv = document.createElement('div');
tagsStatusDiv.className = 'tags-status';
tagsField.parentNode.appendChild(generateBtn);
tagsField.parentNode.appendChild(tagsStatusDiv);
generateBtn.addEventListener('click', function() {
const nameField = document.querySelector('input[name="name"]');
const descriptionField = document.querySelector('textarea[name="description"]');
const name = nameField ? nameField.value.trim() : '';
const description = descriptionField ? descriptionField.value.trim() : '';
if (!name || !description) {
showTagsStatus('请先填写网站名称和描述', 'error');
return;
}
// 显示加载状态
generateBtn.disabled = true;
generateBtn.classList.add('loading');
tagsStatusDiv.style.display = 'none';
// 调用API生成标签
fetch('/api/generate-tags', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: name,
description: description
})
})
.then(response => response.json())
.then(data => {
if (data.success && data.tags && data.tags.length > 0) {
// 显示生成的标签
const tagsText = data.tags.join(', ');
showTagsStatus('✓ AI生成的标签建议: ' + tagsText + '\n请在标签字段中手动选择或创建这些标签', 'success');
} else {
showTagsStatus('✗ ' + (data.message || '标签生成失败'), 'error');
}
})
.catch(error => {
console.error('Error:', error);
showTagsStatus('✗ 网络请求失败,请重试', 'error');
})
.finally(() => {
generateBtn.disabled = false;
generateBtn.classList.remove('loading');
});
});
function showTagsStatus(message, type) {
tagsStatusDiv.textContent = message;
tagsStatusDiv.className = 'tags-status ' + type;
tagsStatusDiv.style.display = 'block';
}
}
});
</script>
{% endblock %}

View File

@@ -56,6 +56,12 @@
box-shadow: 0 10px 30px -10px rgba(37, 192, 244, 0.15);
border-color: #25c0f4;
}
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>
{% block extra_css %}{% endblock %}
</head>

278
templates/base_new.html Normal file
View File

@@ -0,0 +1,278 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}ZJPB - 焦提示词 | AI工具导航{% endblock %}</title>
<!-- 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=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<!-- Material Symbols -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--primary-blue: #0ea5e9;
--primary-dark: #0284c7;
--text-primary: #1e293b;
--text-secondary: #64748b;
--text-muted: #94a3b8;
--bg-page: #f8fafc;
--bg-white: #ffffff;
--border-color: #e2e8f0;
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
--radius-sm: 6px;
--radius-md: 8px;
--radius-lg: 12px;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: var(--bg-page);
color: var(--text-primary);
line-height: 1.6;
}
/* 导航栏 */
.navbar {
background: var(--bg-white);
border-bottom: 1px solid var(--border-color);
padding: 0;
position: sticky;
top: 0;
z-index: 1000;
box-shadow: var(--shadow-sm);
}
.nav-container {
max-width: 1280px;
margin: 0 auto;
padding: 0 24px;
display: flex;
align-items: center;
justify-content: space-between;
height: 64px;
}
.nav-left {
display: flex;
align-items: center;
gap: 40px;
}
.nav-logo {
display: flex;
align-items: center;
gap: 8px;
text-decoration: none;
color: var(--text-primary);
font-weight: 700;
font-size: 18px;
}
.nav-logo-icon {
width: 32px;
height: 32px;
background: linear-gradient(135deg, #0ea5e9 0%, #0284c7 100%);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
color: white;
}
.nav-links {
display: flex;
align-items: center;
gap: 32px;
list-style: none;
}
.nav-links a {
color: var(--text-secondary);
text-decoration: none;
font-size: 14px;
font-weight: 500;
transition: color 0.2s;
}
.nav-links a:hover {
color: var(--text-primary);
}
.nav-right {
display: flex;
align-items: center;
gap: 16px;
}
.search-box {
position: relative;
display: flex;
align-items: center;
}
.search-box input {
width: 280px;
padding: 8px 12px 8px 36px;
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
font-size: 14px;
background: var(--bg-page);
transition: all 0.2s;
}
.search-box input:focus {
outline: none;
border-color: var(--primary-blue);
background: var(--bg-white);
}
.search-box .material-symbols-outlined {
position: absolute;
left: 10px;
top: 50%;
transform: translateY(-50%);
font-size: 20px;
color: var(--text-muted);
}
.btn {
padding: 8px 16px;
border-radius: var(--radius-sm);
font-size: 14px;
font-weight: 500;
text-decoration: none;
cursor: pointer;
transition: all 0.2s;
border: none;
display: inline-flex;
align-items: center;
gap: 6px;
}
.btn-secondary {
background: transparent;
color: var(--text-secondary);
}
.btn-secondary:hover {
color: var(--text-primary);
}
.btn-primary {
background: var(--primary-blue);
color: white;
}
.btn-primary:hover {
background: var(--primary-dark);
}
/* 主内容区 */
.main-content {
max-width: 1280px;
margin: 0 auto;
padding: 0 24px;
}
/* 页脚 */
.footer {
background: var(--bg-white);
border-top: 1px solid var(--border-color);
margin-top: 80px;
padding: 32px 0;
}
.footer-container {
max-width: 1280px;
margin: 0 auto;
padding: 0 24px;
display: flex;
align-items: center;
justify-content: space-between;
}
.footer-text {
color: var(--text-secondary);
font-size: 14px;
}
.footer-links {
display: flex;
gap: 24px;
}
.footer-links a {
color: var(--text-secondary);
text-decoration: none;
font-size: 14px;
transition: color 0.2s;
}
.footer-links a:hover {
color: var(--text-primary);
}
{% block extra_css %}{% endblock %}
</style>
</head>
<body>
<!-- 导航栏 -->
<nav class="navbar">
<div class="nav-container">
<div class="nav-left">
<a href="/" class="nav-logo">
<div class="nav-logo-icon">
<span class="material-symbols-outlined" style="font-size: 20px;">blur_on</span>
</div>
<span>ZJPB</span>
</a>
<ul class="nav-links">
<li><a href="/">Home</a></li>
<li><a href="/#categories">Categories</a></li>
<li><a href="/admin/login">Admin</a></li>
</ul>
</div>
<div class="nav-right">
<form action="/" method="get" class="search-box">
<span class="material-symbols-outlined">search</span>
<input type="text" name="q" placeholder="搜索 AI 工具..." value="{{ search_query or '' }}">
</form>
<a href="/admin/login" class="btn btn-secondary">登录</a>
<a href="/admin/login" class="btn btn-primary">注册</a>
</div>
</div>
</nav>
<!-- 主内容 -->
{% block content %}{% endblock %}
<!-- 页脚 -->
<footer class="footer">
<div class="footer-container">
<div class="footer-text">
© 2023 ZJPB AI Directory. All rights reserved.
</div>
<div class="footer-links">
<a href="#">Twitter</a>
<a href="#">Discord</a>
<a href="#">Privacy Policy</a>
</div>
</div>
</footer>
{% block extra_js %}{% endblock %}
</body>
</html>

View File

@@ -106,6 +106,82 @@
</div>
</section>
{% endif %}
<!-- Related News -->
{% if news_list %}
<section>
<h3 class="text-xl font-bold text-white mb-4 flex items-center gap-2">
<span class="material-symbols-outlined text-secondary\">newspaper</span>
相关新闻
</h3>
<div class="grid gap-4">
{% for news in news_list %}
<a class="group block p-5 rounded-xl border border-border-dark bg-surface-dark/40 hover:bg-border-dark/60 hover:border-primary/30 transition-all" href="{{ news.url if news.url else '#' }}" {{ 'target=\"_blank\"' if news.url else '' }}>
<div class="flex justify-between items-start mb-2">
{% if news.news_type == 'Product Update' %}
<span class="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold uppercase tracking-wider bg-primary/10 text-primary">{{ news.news_type }}</span>
{% elif news.news_type == 'Industry News' %}
<span class="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold uppercase tracking-wider bg-purple-500/10 text-purple-400">{{ news.news_type }}</span>
{% else %}
<span class="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold uppercase tracking-wider bg-gray-500/10 text-gray-400">{{ news.news_type }}</span>
{% endif %}
<span class="text-xs text-gray-400">
{% set days_ago = (now() - news.published_at).days %}
{% if days_ago == 0 %}
今天
{% elif days_ago == 1 %}
1天前
{% elif days_ago < 7 %}
{{ days_ago }}天前
{% else %}
{{ news.published_at.strftime('%Y年%m月%d日') }}
{% endif %}
</span>
</div>
<h4 class="text-white font-bold text-base group-hover:text-primary transition-colors mb-2">{{ news.title }}</h4>
<p class="text-sm text-gray-400 line-clamp-2">{{ news.content }}</p>
</a>
{% endfor %}
</div>
</section>
{% endif %}
<!-- Similar Recommendations -->
{% if recommended_sites %}
<section>
<h3 class="text-xl font-bold text-white mb-4 flex items-center gap-2">
<span class="material-symbols-outlined text-primary">auto_awesome</span>
同类工具推荐
</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
{% for rec_site in recommended_sites %}
<a class="flex flex-col p-4 rounded-xl border border-border-dark bg-surface-dark hover:border-primary/50 hover:shadow-[0_0_20px_-10px_rgba(37,192,244,0.3)] transition-all group h-full" href="{{ url_for('site_detail', slug=rec_site.slug) }}">
<div class="flex items-start justify-between mb-3">
{% if rec_site.logo %}
<div class="size-10 rounded-lg bg-cover bg-center shadow-lg" style="background-image: url('{{ rec_site.logo }}');"></div>
{% else %}
<div class="size-10 rounded-lg bg-gradient-to-br from-primary to-secondary flex items-center justify-center shadow-lg">
<span class="text-white font-bold text-lg">{{ rec_site.name[0] }}</span>
</div>
{% endif %}
<div class="size-8 rounded-full bg-border-dark/50 flex items-center justify-center text-gray-400 group-hover:text-white group-hover:bg-primary group-hover:scale-110 transition-all">
<span class="material-symbols-outlined text-[18px]">arrow_forward</span>
</div>
</div>
<div>
<h4 class="text-white font-bold mb-1">{{ rec_site.name }}</h4>
<p class="text-xs text-gray-400 line-clamp-2 mb-3">{{ rec_site.short_desc or '暂无描述' }}</p>
<div class="flex gap-2 flex-wrap">
{% for tag in rec_site.tags[:2] %}
<span class="px-2 py-1 rounded bg-border-dark text-[10px] text-gray-400 font-medium">{{ tag.name }}</span>
{% endfor %}
</div>
</div>
</a>
{% endfor %}
</div>
</section>
{% endif %}
</div>
</div>

577
templates/detail_new.html Normal file
View File

@@ -0,0 +1,577 @@
{% extends 'base_new.html' %}
{% block title %}{{ site.name }} - ZJPB AI工具导航{% endblock %}
{% block extra_css %}
<style>
/* 返回链接 */
.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);
}
.back-link .material-symbols-outlined {
font-size: 24px;
line-height: 1;
vertical-align: middle;
margin-top: -2px;
}
/* 产品头部区域 */
.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-link .material-symbols-outlined {
font-size: 16px;
}
.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;
}
.meta-item .material-symbols-outlined {
font-size: 18px;
}
.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-btn .material-symbols-outlined {
font-size: 18px;
}
.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 h2 .material-symbols-outlined {
font-size: 24px;
color: var(--primary-blue);
}
.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;
}
/* 响应式 */
@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: 40px;"></div>
<!-- 返回链接 -->
<a href="/" class="back-link">
<span class="material-symbols-outlined">arrow_back</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 class="material-symbols-outlined">open_in_new</span>
</a>
<div class="product-meta">
<div class="meta-item">
<span class="material-symbols-outlined">visibility</span>
<span>{{ site.view_count | default(0) }} 次浏览</span>
</div>
<div class="meta-item">
<span class="material-symbols-outlined">calendar_today</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 class="material-symbols-outlined">north_east</span>
</a>
<p class="visit-hint">在新标签页打开 • {{ site.url.split('/')[2] if site.url else '' }}</p>
</div>
</div>
<!-- 内容布局 -->
<div class="content-layout">
<!-- 主列 -->
<div class="main-column">
<!-- Product Overview -->
<div class="content-block">
<h2>
<span class="material-symbols-outlined">info</span>
产品概述
</h2>
<p>{{ site.description }}</p>
</div>
<!-- Detailed Description -->
{% if site.features %}
<div class="content-block">
<h2>
<span class="material-symbols-outlined">description</span>
详细描述
</h2>
<div>{{ site.features | safe }}</div>
</div>
{% endif %}
<!-- Related News -->
{% if news_list %}
<div class="content-block">
<h2>
<span class="material-symbols-outlined">newspaper</span>
相关新闻
</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>
{% endfor %}
</div>
{% endif %}
<!-- Similar Recommendations -->
{% if recommended_sites %}
<div class="content-block">
<h2>
<span class="material-symbols-outlined">auto_awesome</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="material-symbols-outlined arrow-icon">north_east</span>
</a>
{% endfor %}
</div>
</div>
{% endif %}
</div>
<!-- 侧边栏 -->
<div class="sidebar-column">
<!-- 预留侧边栏位置,可以后续添加其他模块 -->
</div>
</div>
</div>
{% endblock %}

412
templates/index_new.html Normal file
View File

@@ -0,0 +1,412 @@
{% extends 'base_new.html' %}
{% block title %}ZJPB - 焦提示词 | 发现最新最好用的AI工具和产品{% endblock %}
{% block extra_css %}
<style>
/* Hero区域 */
.hero {
padding-bottom: 40px;
background: var(--bg-page);
}
.hero-content {
max-width: 1280px;
margin: 0 auto;
padding: 0 24px;
}
.hero-subtitle {
font-size: 24px;
font-weight: 400;
color: #475569;
line-height: 1.5;
margin-bottom: 8px;
}
.hero-subtitle-en {
font-size: 16px;
font-weight: 400;
color: #64748b;
line-height: 1.6;
margin-bottom: 0;
}
/* 分类过滤 */
.categories {
padding: 32px 0;
}
.category-tabs {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.category-tab {
padding: 10px 20px;
border: 1px solid var(--border-color);
border-radius: 50px;
background: var(--bg-white);
color: var(--text-secondary);
text-decoration: none;
font-size: 14px;
font-weight: 500;
transition: all 0.2s;
display: inline-flex;
align-items: center;
gap: 8px;
}
.category-tab:hover {
border-color: var(--primary-blue);
color: var(--primary-blue);
}
.category-tab.active {
background: var(--primary-blue);
border-color: var(--primary-blue);
color: white;
}
.category-tab .material-symbols-outlined {
font-size: 18px;
}
/* 工具网格 */
.tools-section {
padding: 32px 0 48px;
}
.tools-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
margin-bottom: 48px;
}
.tool-card {
background: var(--bg-white);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 24px;
text-decoration: none;
color: var(--text-primary);
transition: all 0.2s;
position: relative;
display: flex;
flex-direction: column;
}
.tool-card:hover {
border-color: var(--primary-blue);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
transform: translateY(-4px);
}
.tool-card-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 12px;
}
.tool-logo {
width: 48px;
height: 48px;
border-radius: 10px;
object-fit: cover;
flex-shrink: 0;
}
.tool-link-icon {
color: #cbd5e1;
transition: all 0.2s;
font-size: 20px;
}
.tool-card:hover .tool-link-icon {
color: var(--primary-blue);
transform: translate(2px, -2px);
}
.tool-name {
font-size: 18px;
font-weight: 700;
margin-bottom: 8px;
margin-top: 0;
color: var(--text-primary);
}
.tool-description {
font-size: 14px;
color: var(--text-secondary);
line-height: 1.6;
margin-bottom: 16px;
flex: 1;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.tool-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding-top: 16px;
border-top: 1px solid var(--border-color);
}
.tool-tags {
display: flex;
gap: 6px;
flex-wrap: wrap;
flex: 1;
min-width: 0;
}
.tool-tag {
padding: 4px 10px;
background: #f1f5f9;
color: #64748b;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.3px;
}
.tool-views {
display: flex;
align-items: center;
gap: 4px;
color: #94a3b8;
font-size: 13px;
white-space: nowrap;
}
.tool-views .material-symbols-outlined {
font-size: 16px;
}
/* 分页 */
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
}
.pagination a,
.pagination span {
min-width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
background: var(--bg-white);
color: var(--text-secondary);
text-decoration: none;
font-size: 14px;
font-weight: 500;
transition: all 0.2s;
}
.pagination a:hover {
border-color: var(--primary-blue);
color: var(--primary-blue);
}
.pagination .active {
background: var(--primary-blue);
border-color: var(--primary-blue);
color: white;
}
.pagination .disabled {
opacity: 0.5;
cursor: not-allowed;
}
.pagination .disabled:hover {
border-color: var(--border-color);
color: var(--text-secondary);
}
/* 空状态 */
.empty-state {
text-align: center;
padding: 80px 20px;
}
.empty-state .material-symbols-outlined {
font-size: 64px;
color: var(--text-muted);
margin-bottom: 16px;
}
.empty-state h3 {
font-size: 20px;
color: var(--text-primary);
margin-bottom: 8px;
}
.empty-state p {
color: var(--text-secondary);
}
/* 响应式 */
@media (max-width: 768px) {
.hero-title {
font-size: 40px;
}
.tools-grid {
grid-template-columns: 1fr;
}
}
</style>
{% endblock %}
{% block content %}
<!-- Hero区域 -->
<div class="hero">
<div style="height: 40px;"></div>
<div class="hero-content">
<p class="hero-subtitle">发现最新最好用的AI工具和产品</p>
<p class="hero-subtitle-en">Discover the best AI tools tailored for your workflow</p>
</div>
</div>
<div class="main-content">
<!-- 分类过滤 -->
<div class="categories" id="categories">
<div class="category-tabs">
<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 %}
</span>
{{ tag.name }}
</a>
{% endfor %}
</div>
</div>
<!-- 工具网格 -->
<div class="tools-section">
{% 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>
搜索 "{{ search_query }}" 的结果:找到 {{ pagination.total if pagination else sites|length }} 个工具
<a href="/" style="margin-left: 12px; color: #0ea5e9; text-decoration: none;">清除搜索</a>
</p>
</div>
{% endif %}
{% if sites %}
<div class="tools-grid">
{% for site in sites %}
<a href="/site/{{ site.code }}" class="tool-card">
<div class="tool-card-header">
{% if site.logo %}
<img src="{{ site.logo }}" alt="{{ site.name }}" class="tool-logo">
{% 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>
</div>
<h3 class="tool-name">{{ site.name }}</h3>
<p class="tool-description">{{ site.short_desc or site.description }}</p>
<div class="tool-footer">
<div class="tool-tags">
{% for tag in site.tags[:2] %}
<span class="tool-tag">{{ tag.name }}</span>
{% endfor %}
</div>
<div class="tool-views">
<span class="material-symbols-outlined">visibility</span>
<span>{% if site.view_count >= 1000 %}{{ (site.view_count / 1000) | round(1) }}k{% else %}{{ site.view_count | default(0) }}{% endif %}</span>
</div>
</div>
</a>
{% endfor %}
</div>
<!-- 分页 -->
{% if pagination and pagination.pages > 1 %}
<div class="pagination">
<!-- 上一页 -->
{% 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>
</a>
{% else %}
<a href="#" class="disabled">
<span class="material-symbols-outlined">chevron_left</span>
</a>
{% endif %}
<!-- 页码显示 -->
{% set start_page = [1, pagination.page - 2]|max %}
{% set end_page = [pagination.pages, pagination.page + 2]|min %}
{% if start_page > 1 %}
<a href="?page=1{% if selected_tag %}&tag={{ selected_tag.slug }}{% endif %}{% if search_query %}&q={{ search_query }}{% endif %}">1</a>
{% if start_page > 2 %}
<span>...</span>
{% endif %}
{% endif %}
{% for page_num in range(start_page, end_page + 1) %}
{% if page_num == pagination.page %}
<span class="active">{{ page_num }}</span>
{% else %}
<a href="?page={{ page_num }}{% if selected_tag %}&tag={{ selected_tag.slug }}{% endif %}{% if search_query %}&q={{ search_query }}{% endif %}">{{ page_num }}</a>
{% endif %}
{% endfor %}
{% if end_page < pagination.pages %}
{% if end_page < pagination.pages - 1 %}
<span>...</span>
{% endif %}
<a href="?page={{ pagination.pages }}{% if selected_tag %}&tag={{ selected_tag.slug }}{% endif %}{% if search_query %}&q={{ search_query }}{% endif %}">{{ pagination.pages }}</a>
{% endif %}
<!-- 下一页 -->
{% 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>
</a>
{% else %}
<a href="#" class="disabled">
<span class="material-symbols-outlined">chevron_right</span>
</a>
{% endif %}
</div>
{% endif %}
{% else %}
<div class="empty-state">
<span class="material-symbols-outlined">search_off</span>
<h3>暂无工具</h3>
<p>{% if selected_tag %}该分类下还没有工具{% else %}还没有添加任何工具{% endif %}</p>
</div>
{% endif %}
</div>
</div>
{% endblock %}