512 lines
14 KiB
HTML
Executable File
512 lines
14 KiB
HTML
Executable File
{% extends "base.html" %}
|
|
|
|
{% block content %}
|
|
{% include 'admin_nav.html' %}
|
|
|
|
<div class="admin-container">
|
|
<div class="admin-content">
|
|
<div class="admin-card">
|
|
<div class="card-header">
|
|
<div class="header-left">
|
|
<i class="fas fa-th-large"></i>
|
|
<h3>分类管理</h3>
|
|
</div>
|
|
<button onclick="showAddCategoryModal()" class="btn-primary">
|
|
<i class="fas fa-plus"></i> 添加分类
|
|
</button>
|
|
</div>
|
|
|
|
<div class="categories-list">
|
|
<div class="categories-header">
|
|
<div class="sort-handle-header"></div>
|
|
<div>分类名称</div>
|
|
<div>应用数量</div>
|
|
<div>操作</div>
|
|
</div>
|
|
<div id="sortableCategories">
|
|
{% for category in categories %}
|
|
<div class="category-item" data-id="{{ category.id }}">
|
|
<div class="sort-handle">
|
|
<i class="fas fa-grip-vertical"></i>
|
|
</div>
|
|
<div class="category-name">{{ category.name }}</div>
|
|
<div class="app-count">{{ category.app_count }}个应用</div>
|
|
<div class="category-actions">
|
|
<button onclick="editCategory({{ category.id }}, '{{ category.name }}')" class="btn-edit">
|
|
<i class="fas fa-edit"></i>
|
|
</button>
|
|
<button onclick="deleteCategory({{ category.id }})" class="btn-delete">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 添加分类弹窗 -->
|
|
<div id="addCategoryModal" class="modal">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h3>添加分类</h3>
|
|
<span class="close" onclick="closeAddCategoryModal()">×</span>
|
|
</div>
|
|
<form id="addCategoryForm" onsubmit="return addCategory(event)">
|
|
<div class="form-group">
|
|
<label for="categoryName">分类名称</label>
|
|
<input type="text" id="categoryName" name="name" required>
|
|
</div>
|
|
<div class="form-actions">
|
|
<button type="submit" class="btn-primary">确认添加</button>
|
|
<button type="button" onclick="closeAddCategoryModal()" class="btn-secondary">取消</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 编辑分类弹窗 -->
|
|
<div id="editCategoryModal" class="modal">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h3>编辑分类</h3>
|
|
<span class="close" onclick="closeEditCategoryModal()">×</span>
|
|
</div>
|
|
<form id="editCategoryForm" onsubmit="return updateCategory(event)">
|
|
<input type="hidden" id="editCategoryId">
|
|
<div class="form-group">
|
|
<label for="editCategoryName">分类名称</label>
|
|
<input type="text" id="editCategoryName" name="name" required>
|
|
</div>
|
|
<div class="form-actions">
|
|
<button type="submit" class="btn-primary">保存修改</button>
|
|
<button type="button" onclick="closeEditCategoryModal()" class="btn-secondary">取消</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.categories-list {
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.categories-header {
|
|
display: grid;
|
|
grid-template-columns: 50px 1fr 100px 120px;
|
|
padding: 10px;
|
|
background: #f5f5f7;
|
|
border-radius: 8px;
|
|
margin-bottom: 10px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.category-item {
|
|
display: grid;
|
|
grid-template-columns: 50px 1fr 100px 120px;
|
|
align-items: center;
|
|
padding: 12px;
|
|
background: white;
|
|
border-radius: 8px;
|
|
margin-bottom: 8px;
|
|
transition: all 0.3s ease;
|
|
cursor: move;
|
|
}
|
|
|
|
.category-item:hover {
|
|
background: #f8f9fa;
|
|
}
|
|
|
|
.sort-handle {
|
|
color: #999;
|
|
cursor: move;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.sort-handle i {
|
|
font-size: 16px;
|
|
}
|
|
|
|
.category-name {
|
|
font-size: 14px;
|
|
}
|
|
|
|
.category-actions {
|
|
display: flex;
|
|
gap: 8px;
|
|
justify-content: flex-end;
|
|
}
|
|
|
|
.btn-edit, .btn-delete {
|
|
padding: 6px 12px;
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.btn-edit {
|
|
background: #007AFF;
|
|
color: white;
|
|
}
|
|
|
|
.btn-delete {
|
|
background: #ff3b30;
|
|
color: white;
|
|
}
|
|
|
|
.btn-edit:hover {
|
|
background: #0056b3;
|
|
}
|
|
|
|
.btn-delete:hover {
|
|
background: #dc3545;
|
|
}
|
|
|
|
/* 拖拽时的样式 */
|
|
.category-item.dragging {
|
|
opacity: 0.5;
|
|
background: #f0f0f0;
|
|
}
|
|
|
|
.category-item.drag-over {
|
|
border-top: 2px solid #007AFF;
|
|
}
|
|
|
|
/* 添加弹窗样式 */
|
|
.modal {
|
|
display: none;
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background-color: rgba(0, 0, 0, 0.5);
|
|
z-index: 1000;
|
|
}
|
|
|
|
.modal-content {
|
|
position: relative;
|
|
background-color: #fff;
|
|
margin: 10% auto;
|
|
padding: 20px;
|
|
width: 90%;
|
|
max-width: 500px;
|
|
border-radius: 12px;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
}
|
|
|
|
.modal-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 0px;
|
|
}
|
|
|
|
.modal-header h3 {
|
|
margin: 0;
|
|
font-size: 18px;
|
|
color: #333;
|
|
}
|
|
|
|
.close {
|
|
font-size: 24px;
|
|
color: #666;
|
|
cursor: pointer;
|
|
padding: 5px;
|
|
}
|
|
|
|
.close:hover {
|
|
color: #333;
|
|
}
|
|
|
|
.form-group {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.form-group label {
|
|
display: block;
|
|
margin-bottom: 8px;
|
|
color: #333;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.form-group input {
|
|
width: 100%;
|
|
padding: 8px 12px;
|
|
border: 1px solid #ddd;
|
|
border-radius: 6px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.form-group input:focus {
|
|
outline: none;
|
|
border-color: #007AFF;
|
|
}
|
|
|
|
.form-actions {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 10px;
|
|
}
|
|
|
|
.btn-secondary {
|
|
background: #f5f5f7;
|
|
color: #333;
|
|
border: none;
|
|
padding: 8px 16px;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.btn-secondary:hover {
|
|
background: #e5e5e7;
|
|
}
|
|
|
|
.app-count {
|
|
font-size: 13px;
|
|
color: #666;
|
|
text-align: center;
|
|
background: #f5f5f7;
|
|
padding: 4px 8px;
|
|
border-radius: 12px;
|
|
margin-right: 10px;
|
|
}
|
|
</style>
|
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/Sortable/1.14.0/Sortable.min.js"></script>
|
|
<script>
|
|
// 初始化拖拽排序
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
var el = document.getElementById('sortableCategories');
|
|
var sortable = new Sortable(el, {
|
|
animation: 150,
|
|
handle: '.sort-handle',
|
|
ghostClass: 'dragging',
|
|
onEnd: function(evt) {
|
|
// 获取新的排序
|
|
var items = el.getElementsByClassName('category-item');
|
|
var newOrder = Array.from(items).map(item => parseInt(item.dataset.id));
|
|
|
|
console.log('Sending order:', newOrder); // 添加调试日志
|
|
|
|
// 发送到服务器
|
|
fetch('/admin/update_category_order', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-Requested-With': 'XMLHttpRequest'
|
|
},
|
|
credentials: 'same-origin',
|
|
body: JSON.stringify({
|
|
order: newOrder
|
|
})
|
|
})
|
|
.then(response => {
|
|
console.log('Response status:', response.status); // 添加调试日志
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
console.log('Response data:', data); // 添加调试日志
|
|
if (data.success) {
|
|
showNotification('排序已更新', 'success');
|
|
} else {
|
|
throw new Error(data.error || '更新失败');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
showNotification('更新失败,请重试', 'error');
|
|
// 如果更新失败,恢复原始顺序
|
|
setTimeout(() => {
|
|
location.reload();
|
|
}, 2000);
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
function showNotification(message, type = 'success') {
|
|
const notification = document.createElement('div');
|
|
notification.className = `notification ${type}`;
|
|
notification.textContent = message;
|
|
|
|
Object.assign(notification.style, {
|
|
position: 'fixed',
|
|
bottom: '20px',
|
|
right: '20px',
|
|
padding: '10px 20px',
|
|
borderRadius: '4px',
|
|
backgroundColor: type === 'success' ? '#4CAF50' : '#f44336',
|
|
color: 'white',
|
|
zIndex: '1000',
|
|
opacity: '0',
|
|
transform: 'translateY(20px)',
|
|
transition: 'all 0.3s ease',
|
|
boxShadow: '0 2px 5px rgba(0,0,0,0.2)'
|
|
});
|
|
|
|
document.body.appendChild(notification);
|
|
|
|
// 触发重排以应用过渡效果
|
|
notification.offsetHeight;
|
|
|
|
notification.style.opacity = '1';
|
|
notification.style.transform = 'translateY(0)';
|
|
|
|
setTimeout(() => {
|
|
notification.style.opacity = '0';
|
|
notification.style.transform = 'translateY(20px)';
|
|
setTimeout(() => notification.remove(), 300);
|
|
}, 3000);
|
|
}
|
|
|
|
function deleteCategory(categoryId) {
|
|
if (confirm('确定要删除这个分类吗?删除后将同时删除该分类下的所有应用')) {
|
|
fetch(`/delete_category/${categoryId}`, {
|
|
method: 'POST'
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showNotification('分类及其应用已删除', 'success');
|
|
// 延迟刷新页面,让用户看到通知
|
|
setTimeout(() => {
|
|
location.reload();
|
|
}, 1000);
|
|
} else {
|
|
showNotification(data.error || '删除失败', 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
showNotification('删除失败,请重试', 'error');
|
|
});
|
|
}
|
|
}
|
|
|
|
// 添加分类弹窗相关函数
|
|
function showAddCategoryModal() {
|
|
document.getElementById('addCategoryModal').style.display = 'block';
|
|
document.getElementById('categoryName').value = '';
|
|
document.getElementById('categoryName').focus();
|
|
}
|
|
|
|
function closeAddCategoryModal() {
|
|
document.getElementById('addCategoryModal').style.display = 'none';
|
|
}
|
|
|
|
function addCategory(event) {
|
|
event.preventDefault();
|
|
const name = document.getElementById('categoryName').value.trim();
|
|
|
|
if (!name) {
|
|
showNotification('分类名称不能为空', 'error');
|
|
return false;
|
|
}
|
|
|
|
fetch('/add_category', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
},
|
|
body: `name=${encodeURIComponent(name)}`
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showNotification('分类添加成功', 'success');
|
|
closeAddCategoryModal();
|
|
setTimeout(() => location.reload(), 1000);
|
|
} else {
|
|
showNotification(data.error || '添加失败', 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
showNotification('添加失败,请重试', 'error');
|
|
});
|
|
|
|
return false;
|
|
}
|
|
|
|
// 编辑分类弹窗相关函数
|
|
function showEditCategoryModal() {
|
|
document.getElementById('editCategoryModal').style.display = 'block';
|
|
}
|
|
|
|
function closeEditCategoryModal() {
|
|
document.getElementById('editCategoryModal').style.display = 'none';
|
|
}
|
|
|
|
function editCategory(id, name) {
|
|
document.getElementById('editCategoryId').value = id;
|
|
document.getElementById('editCategoryName').value = name;
|
|
showEditCategoryModal();
|
|
}
|
|
|
|
function updateCategory(event) {
|
|
event.preventDefault();
|
|
const id = document.getElementById('editCategoryId').value;
|
|
const name = document.getElementById('editCategoryName').value.trim();
|
|
|
|
if (!name) {
|
|
showNotification('分类名称不能为空', 'error');
|
|
return false;
|
|
}
|
|
|
|
fetch(`/edit_category/${id}`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
},
|
|
body: `name=${encodeURIComponent(name)}`
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showNotification('分类修改成功', 'success');
|
|
closeEditCategoryModal();
|
|
setTimeout(() => location.reload(), 1000);
|
|
} else {
|
|
showNotification(data.error || '修改失败', 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
showNotification('修改失败,请重试', 'error');
|
|
});
|
|
|
|
return false;
|
|
}
|
|
|
|
// 点击弹窗外部关闭
|
|
window.onclick = function(event) {
|
|
const addModal = document.getElementById('addCategoryModal');
|
|
const editModal = document.getElementById('editCategoryModal');
|
|
|
|
if (event.target == addModal) {
|
|
closeAddCategoryModal();
|
|
}
|
|
if (event.target == editModal) {
|
|
closeEditCategoryModal();
|
|
}
|
|
}
|
|
|
|
// 添加 ESC 键关闭弹窗
|
|
document.addEventListener('keydown', function(event) {
|
|
if (event.key === 'Escape') {
|
|
closeAddCategoryModal();
|
|
closeEditCategoryModal();
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock %} |