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:
363
templates/admin/batch_import.html
Normal file
363
templates/admin/batch_import.html
Normal 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>×</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,例如: https://www.example.com https://www.google.com 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>
|
||||
@@ -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
239
templates/admin/index.html
Normal 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
176
templates/admin/master.html
Normal 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>
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
Reference in New Issue
Block a user