Files
ToNav/templates/admin/categories.html
OpenClaw Agent 521cd9ba42 docs: add comprehensive README.md and fix bugs
- add README.md with usage and deployment guide
- fix category sync logic in backend
- fix URL overflow in admin services list
- fix data caching issues in front-end and back-end
- add 'View Front-end' button in admin dashboard
2026-02-13 07:00:49 +08:00

494 lines
12 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}分类管理 - ToNav{% endblock %}
{% block content %}
<div class="container admin-layout">
<!-- 顶部导航 -->
<div class="admin-header">
<div class="header-left">
<h1>📂 分类管理</h1>
<a href="/admin" class="back-link">← 返回首页</a>
</div>
<div class="header-right">
<button class="btn btn-outline" onclick="window.open('/', '_blank')" style="margin-right: 10px;">查看前台 ↗</button>
<button class="btn btn-primary" onclick="showCreateModal()">+ 新建分类</button>
</div>
</div>
<!-- 分类列表 -->
<div class="categories-list">
<div class="loading">加载中...</div>
</div>
<!-- 创建/编辑弹窗 -->
<div class="modal" id="categoryModal">
<div class="modal-content">
<div class="modal-header">
<h2 id="modalTitle">新建分类</h2>
<button class="close-btn" onclick="closeModal()">×</button>
</div>
<form id="categoryForm" class="modal-body">
<input type="hidden" id="categoryId">
<div class="form-group">
<label>分类名称 *</label>
<input type="text" id="categoryName" class="input" required>
</div>
<div class="form-group">
<label>排序权重</label>
<input type="number" id="categorySort" class="input" value="0">
</div>
<div class="form-actions">
<button type="button" class="btn" onclick="closeModal()">取消</button>
<button type="submit" class="btn btn-primary">保存</button>
</div>
</form>
</div>
</div>
</div>
<style>
:root {
--main-red: #ff4d4f;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 600px;
margin: 0 auto;
}
.admin-header {
background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%);
color: #fff;
padding: 25px 30px;
border-radius: 20px 20px 0 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.header-left h1 {
font-size: 22px;
font-weight: 700;
}
.back-link {
color: #8c8c8c;
font-size: 13px;
text-decoration: none;
}
.back-link:hover {
color: #bfbfbf;
}
.categories-list {
background: #fff;
padding: 30px;
border-radius: 0 0 20px 20px;
}
.category-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px;
background: #fafafa;
border-radius: 12px;
margin-bottom: 10px;
transition: all 0.3s;
}
.category-item:hover {
background: #f0f0f0;
transform: translateX(5px);
}
.category-info {
display: flex;
align-items: center;
gap: 15px;
}
.category-icon {
font-size: 24px;
}
.category-name {
font-size: 16px;
font-weight: 600;
color: #262626;
}
.category-meta {
font-size: 12px;
color: #999;
margin-left: 10px;
}
.category-actions {
display: flex;
gap: 8px;
}
.action-btn-sm {
padding: 8px 14px;
border: 1px solid #d9d9d9;
background: #fff;
border-radius: 8px;
font-size: 13px;
cursor: pointer;
transition: all 0.3s;
}
.action-btn-sm:hover {
border-color: var(--main-red);
color: var(--main-red);
}
.action-btn-sm.danger:hover {
background: #fff2f0;
border-color: #ff4d4f;
color: #ff4d4f;
}
/* Modal */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.6);
z-index: 1000;
align-items: center;
justify-content: center;
animation: fadeIn 0.3s ease;
}
.modal.active {
display: flex;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.modal-content {
background: #fff;
border-radius: 20px;
max-width: 500px;
width: 90%;
animation: slideUp 0.3s ease;
}
@keyframes slideUp {
from { transform: translateY(50px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.modal-header {
padding: 20px 25px;
background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%);
color: #fff;
display: flex;
justify-content: space-between;
align-items: center;
border-radius: 20px 20px 0 0;
}
.modal-header h2 {
font-size: 18px;
font-weight: 600;
}
.close-btn {
background: none;
border: none;
color: #fff;
font-size: 28px;
cursor: pointer;
padding: 0;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
}
.close-btn:hover {
background: rgba(255,255,255,0.2);
}
.modal-body {
padding: 25px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-size: 14px;
font-weight: 500;
color: #262626;
}
.input {
width: 100%;
padding: 10px 12px;
border: 1px solid #d9d9d9;
border-radius: 8px;
font-size: 14px;
transition: all 0.3s;
}
.input:focus {
outline: none;
border-color: var(--main-red);
box-shadow: 0 0 0 3px rgba(255, 77, 79, 0.1);
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
}
.btn-outline {
background: transparent;
border: 1px solid rgba(255,255,255,0.3);
color: #fff;
}
.btn-outline:hover {
background: rgba(255,255,255,0.1);
border-color: #fff;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.btn-primary {
background: var(--main-red);
color: #fff;
box-shadow: 0 4px 15px rgba(255, 77, 79, 0.4);
}
.btn-primary:hover {
background: #ff7875;
}
.loading {
text-align: center;
padding: 40px;
color: #999;
}
.empty-state {
text-align: center;
padding: 40px;
color: #999;
}
</style>
{% endblock %}
{% block scripts %}
<script>
let allCategories = [];
let allServices = [];
// 初始化
document.addEventListener('DOMContentLoaded', function() {
loadData();
setupForm();
});
// 加载数据
async function loadData() {
try {
const [catResp, svcResp] = await Promise.all([
fetch('/api/admin/categories'),
fetch('/api/admin/services')
]);
if (!catResp.ok) {
window.location.href = '/admin/login';
return;
}
allCategories = await catResp.json();
allServices = await svcResp.json();
renderCategories();
} catch (err) {
console.error('加载失败:', err);
window.location.href = '/admin/login';
}
}
// 渲染分类列表
function renderCategories() {
const container = document.querySelector('.categories-list');
if (allCategories.length === 0) {
container.innerHTML = '<div class="empty-state">暂无分类</div>';
return;
}
let html = '';
allCategories.forEach(category => {
const serviceCount = allServices.filter(s => s.category === category.name).length;
html += `
<div class="category-item">
<div class="category-info">
<span class="category-icon">📂</span>
<div>
<span class="category-name">${category.name}</span>
<span class="category-meta">${serviceCount} 个服务 · 排序 ${category.sort_order}</span>
</div>
</div>
<div class="category-actions">
<button class="action-btn-sm" onclick="editCategory(${category.id})">编辑</button>
<button class="action-btn-sm danger" onclick="deleteCategory(${category.id}, '${category.name}')">删除</button>
</div>
</div>
`;
});
container.innerHTML = html;
}
// 设置表单
function setupForm() {
document.getElementById('categoryForm').addEventListener('submit', function(e) {
e.preventDefault();
saveCategory();
});
}
// 显示创建弹窗
function showCreateModal() {
document.getElementById('modalTitle').textContent = '新建分类';
document.getElementById('categoryId').value = '';
document.getElementById('categoryForm').reset();
document.getElementById('categorySort').value = '0';
document.getElementById('categoryModal').classList.add('active');
}
// 编辑分类
function editCategory(id) {
const category = allCategories.find(c => c.id === id);
if (!category) return;
document.getElementById('modalTitle').textContent = '编辑分类';
document.getElementById('categoryId').value = category.id;
document.getElementById('categoryName').value = category.name;
document.getElementById('categorySort').value = category.sort_order;
document.getElementById('categoryModal').classList.add('active');
}
// 保存分类
async function saveCategory() {
const id = document.getElementById('categoryId').value;
const data = {
name: document.getElementById('categoryName').value.trim(),
sort_order: parseInt(document.getElementById('categorySort').value) || 0
};
if (!data.name) {
alert('分类名称不能为空');
return;
}
try {
const url = id ? `/api/admin/categories/${id}` : '/api/admin/categories';
const method = id ? 'PUT' : 'POST';
const response = await fetch(url, {
method: method,
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
if (response.ok) {
closeModal();
loadData();
} else {
const result = await response.json();
alert(result.error || '保存失败');
}
} catch (err) {
alert('请求失败: ' + err.message);
}
}
// 删除分类
async function deleteCategory(id, name) {
const serviceCount = allServices.filter(s => s.category === name).length;
if (serviceCount > 0) {
alert(`该分类下有 ${serviceCount} 个服务,无法删除`);
return;
}
if (!confirm(`确定要删除分类"${name}"吗?此操作不可恢复。`)) return;
try {
const response = await fetch(`/api/admin/categories/${id}`, {
method: 'DELETE'
});
if (response.ok) {
loadData();
} else {
const result = await response.json();
alert(result.error || '删除失败');
}
} catch (err) {
alert('请求失败: ' + err.message);
}
}
// 关闭弹窗
function closeModal() {
document.getElementById('categoryModal').classList.remove('active');
}
// 点击外部关闭弹窗
document.getElementById('categoryModal').addEventListener('click', function(e) {
if (e.target === this) {
closeModal();
}
});
</script>
{% endblock %}