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>