qoder生成项目
This commit is contained in:
560
static/admin/app.js
Normal file
560
static/admin/app.js
Normal file
@@ -0,0 +1,560 @@
|
||||
// API 基础地址
|
||||
const API_BASE = '/api';
|
||||
|
||||
// 当前登录用户
|
||||
let currentUser = null;
|
||||
|
||||
// 页面初始化
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
currentUser = JSON.parse(localStorage.getItem('user') || '{}');
|
||||
document.getElementById('username').textContent = currentUser.nickname || currentUser.username;
|
||||
showSidebar();
|
||||
showPage('dashboard');
|
||||
} else {
|
||||
// 未登录状态:隐藏所有页面,只显示登录页
|
||||
document.querySelectorAll('.page').forEach(p => p.classList.add('hidden'));
|
||||
const loginPage = document.getElementById('login-page');
|
||||
if (loginPage) {
|
||||
loginPage.classList.remove('hidden');
|
||||
}
|
||||
hideSidebar();
|
||||
}
|
||||
|
||||
// 绑定菜单点击事件
|
||||
document.querySelectorAll('.menu-item').forEach(item => {
|
||||
item.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const page = item.dataset.page;
|
||||
showPage(page);
|
||||
|
||||
document.querySelectorAll('.menu-item').forEach(i => i.classList.remove('active'));
|
||||
item.classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
// 登录表单
|
||||
document.getElementById('login-form').addEventListener('submit', handleLogin);
|
||||
|
||||
// 文章表单
|
||||
document.getElementById('post-form').addEventListener('submit', handlePostSubmit);
|
||||
|
||||
// 模态框表单
|
||||
document.getElementById('modal-form').addEventListener('submit', handleModalSubmit);
|
||||
});
|
||||
|
||||
// 显示登录页
|
||||
function showLogin() {
|
||||
hideSidebar();
|
||||
document.querySelectorAll('.page').forEach(p => p.classList.add('hidden'));
|
||||
const loginPage = document.getElementById('login-page');
|
||||
if (loginPage) {
|
||||
loginPage.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// 显示侧边栏
|
||||
function showSidebar() {
|
||||
document.querySelector('.sidebar').style.display = 'flex';
|
||||
}
|
||||
|
||||
// 隐藏侧边栏
|
||||
function hideSidebar() {
|
||||
document.querySelector('.sidebar').style.display = 'none';
|
||||
}
|
||||
|
||||
// 切换页面
|
||||
function showPage(pageName) {
|
||||
document.querySelectorAll('.page').forEach(p => p.classList.add('hidden'));
|
||||
const targetPage = document.getElementById(pageName + '-page');
|
||||
if (targetPage) {
|
||||
targetPage.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// 加载对应数据
|
||||
switch(pageName) {
|
||||
case 'dashboard':
|
||||
loadDashboard();
|
||||
break;
|
||||
case 'posts':
|
||||
loadPosts();
|
||||
break;
|
||||
case 'categories':
|
||||
loadCategories();
|
||||
break;
|
||||
case 'tags':
|
||||
loadTags();
|
||||
break;
|
||||
case 'pages':
|
||||
loadPages();
|
||||
break;
|
||||
case 'comments':
|
||||
loadComments();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 登录
|
||||
async function handleLogin(e) {
|
||||
e.preventDefault();
|
||||
const username = document.getElementById('login-username').value;
|
||||
const password = document.getElementById('login-password').value;
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password })
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
localStorage.setItem('token', data.token);
|
||||
localStorage.setItem('user', JSON.stringify(data.user));
|
||||
currentUser = data.user;
|
||||
document.getElementById('username').textContent = data.user.nickname || data.user.username;
|
||||
showSidebar();
|
||||
showPage('dashboard');
|
||||
} else {
|
||||
alert(data.error || '登录失败');
|
||||
}
|
||||
} catch (err) {
|
||||
alert('网络错误');
|
||||
}
|
||||
}
|
||||
|
||||
// 退出登录
|
||||
function logout() {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
currentUser = null;
|
||||
showLogin();
|
||||
}
|
||||
|
||||
// 获取请求头
|
||||
function getHeaders() {
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
||||
};
|
||||
}
|
||||
|
||||
// 加载仪表盘
|
||||
async function loadDashboard() {
|
||||
try {
|
||||
const [posts, categories, tags, comments] = await Promise.all([
|
||||
fetch(`${API_BASE}/posts?page_size=1`).then(r => r.json()),
|
||||
fetch(`${API_BASE}/categories`).then(r => r.json()),
|
||||
fetch(`${API_BASE}/tags`).then(r => r.json()),
|
||||
fetch(`${API_BASE}/comments?status=pending`).then(r => r.json())
|
||||
]);
|
||||
|
||||
document.getElementById('stat-posts').textContent = posts.total || 0;
|
||||
document.getElementById('stat-categories').textContent = categories.data?.length || 0;
|
||||
document.getElementById('stat-tags').textContent = tags.data?.length || 0;
|
||||
document.getElementById('stat-comments').textContent = comments.data?.length || 0;
|
||||
} catch (err) {
|
||||
console.error('加载仪表盘失败:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// 加载文章列表
|
||||
async function loadPosts() {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/posts?page_size=100`, { headers: getHeaders() });
|
||||
const data = await res.json();
|
||||
|
||||
const tbody = document.getElementById('posts-list');
|
||||
tbody.innerHTML = data.data?.map(post => `
|
||||
<tr>
|
||||
<td>${post.title}</td>
|
||||
<td>${post.category?.name || '-'}</td>
|
||||
<td><span class="status-badge status-${post.status}">${getStatusText(post.status)}</span></td>
|
||||
<td>${post.views}</td>
|
||||
<td>${post.published_at ? new Date(post.published_at).toLocaleDateString() : '-'}</td>
|
||||
<td>
|
||||
<button class="btn-success" onclick="editPost(${post.id})">编辑</button>
|
||||
<button class="btn-danger" onclick="deletePost(${post.id})">删除</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('') || '<tr><td colspan="6" style="text-align:center">暂无文章</td></tr>';
|
||||
} catch (err) {
|
||||
console.error('加载文章失败:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// 显示文章表单
|
||||
async function showPostForm(isEdit = false) {
|
||||
document.getElementById('post-form-title').textContent = isEdit ? '编辑文章' : '新建文章';
|
||||
document.getElementById('post-form').reset();
|
||||
document.getElementById('post-id').value = '';
|
||||
|
||||
// 加载分类选项
|
||||
const res = await fetch(`${API_BASE}/categories`);
|
||||
const data = await res.json();
|
||||
const select = document.getElementById('post-category');
|
||||
select.innerHTML = data.data?.map(c => `<option value="${c.id}">${c.name}</option>`).join('') || '';
|
||||
|
||||
showPage('post-form');
|
||||
}
|
||||
|
||||
// 编辑文章
|
||||
async function editPost(id) {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/posts/${id}`, { headers: getHeaders() });
|
||||
const data = await res.json();
|
||||
const post = data.data;
|
||||
|
||||
document.getElementById('post-id').value = post.id;
|
||||
document.getElementById('post-title').value = post.title;
|
||||
document.getElementById('post-content').value = post.content;
|
||||
document.getElementById('post-summary').value = post.summary || '';
|
||||
document.getElementById('post-cover').value = post.cover || '';
|
||||
document.getElementById('post-status').value = post.status;
|
||||
document.getElementById('post-istop').checked = post.is_top;
|
||||
document.getElementById('post-tags').value = post.tags?.map(t => t.name).join(', ') || '';
|
||||
|
||||
// 加载分类并设置选中
|
||||
const catRes = await fetch(`${API_BASE}/categories`);
|
||||
const catData = await catRes.json();
|
||||
const select = document.getElementById('post-category');
|
||||
select.innerHTML = catData.data?.map(c =>
|
||||
`<option value="${c.id}" ${c.id === post.category_id ? 'selected' : ''}>${c.name}</option>`
|
||||
).join('') || '';
|
||||
|
||||
document.getElementById('post-form-title').textContent = '编辑文章';
|
||||
showPage('post-form');
|
||||
} catch (err) {
|
||||
alert('加载文章失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 提交文章
|
||||
async function handlePostSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const id = document.getElementById('post-id').value;
|
||||
const tagsStr = document.getElementById('post-tags').value;
|
||||
const tags = tagsStr ? tagsStr.split(',').map(t => t.trim()).filter(t => t) : [];
|
||||
|
||||
const data = {
|
||||
title: document.getElementById('post-title').value,
|
||||
content: document.getElementById('post-content').value,
|
||||
summary: document.getElementById('post-summary').value,
|
||||
cover: document.getElementById('post-cover').value,
|
||||
category_id: parseInt(document.getElementById('post-category').value),
|
||||
status: document.getElementById('post-status').value,
|
||||
is_top: document.getElementById('post-istop').checked,
|
||||
tags: tags
|
||||
};
|
||||
|
||||
try {
|
||||
const url = id ? `${API_BASE}/admin/posts/${id}` : `${API_BASE}/admin/posts`;
|
||||
const method = id ? 'PUT' : 'POST';
|
||||
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers: getHeaders(),
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
alert(id ? '更新成功' : '创建成功');
|
||||
showPage('posts');
|
||||
loadPosts();
|
||||
} else {
|
||||
const err = await res.json();
|
||||
alert(err.error || '操作失败');
|
||||
}
|
||||
} catch (err) {
|
||||
alert('网络错误');
|
||||
}
|
||||
}
|
||||
|
||||
// 删除文章
|
||||
async function deletePost(id) {
|
||||
if (!confirm('确定要删除这篇文章吗?')) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/admin/posts/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: getHeaders()
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
loadPosts();
|
||||
} else {
|
||||
alert('删除失败');
|
||||
}
|
||||
} catch (err) {
|
||||
alert('网络错误');
|
||||
}
|
||||
}
|
||||
|
||||
// 加载分类
|
||||
async function loadCategories() {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/categories`);
|
||||
const data = await res.json();
|
||||
|
||||
const tbody = document.getElementById('categories-list');
|
||||
tbody.innerHTML = data.data?.map(cat => `
|
||||
<tr>
|
||||
<td>${cat.name}</td>
|
||||
<td>${cat.slug}</td>
|
||||
<td>${cat.post_count}</td>
|
||||
<td>
|
||||
<button class="btn-success" onclick="editCategory(${cat.id}, '${cat.name}', '${cat.slug}')">编辑</button>
|
||||
<button class="btn-danger" onclick="deleteCategory(${cat.id})">删除</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('') || '<tr><td colspan="4" style="text-align:center">暂无分类</td></tr>';
|
||||
} catch (err) {
|
||||
console.error('加载分类失败:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// 加载标签
|
||||
async function loadTags() {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/tags`);
|
||||
const data = await res.json();
|
||||
|
||||
const tbody = document.getElementById('tags-list');
|
||||
tbody.innerHTML = data.data?.map(tag => `
|
||||
<tr>
|
||||
<td>${tag.name}</td>
|
||||
<td>${tag.slug}</td>
|
||||
<td>${tag.post_count}</td>
|
||||
<td>
|
||||
<button class="btn-success" onclick="editTag(${tag.id}, '${tag.name}', '${tag.slug}')">编辑</button>
|
||||
<button class="btn-danger" onclick="deleteTag(${tag.id})">删除</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('') || '<tr><td colspan="4" style="text-align:center">暂无标签</td></tr>';
|
||||
} catch (err) {
|
||||
console.error('加载标签失败:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// 加载页面
|
||||
async function loadPages() {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/pages`);
|
||||
const data = await res.json();
|
||||
|
||||
const tbody = document.getElementById('pages-list');
|
||||
tbody.innerHTML = data.data?.map(page => `
|
||||
<tr>
|
||||
<td>${page.title}</td>
|
||||
<td>${page.slug}</td>
|
||||
<td><span class="status-badge status-${page.status}">${getStatusText(page.status)}</span></td>
|
||||
<td>${page.order}</td>
|
||||
<td>
|
||||
<button class="btn-success" onclick="editPage(${page.id})">编辑</button>
|
||||
<button class="btn-danger" onclick="deletePage(${page.id})">删除</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('') || '<tr><td colspan="5" style="text-align:center">暂无页面</td></tr>';
|
||||
} catch (err) {
|
||||
console.error('加载页面失败:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// 加载评论
|
||||
async function loadComments() {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/comments`, { headers: getHeaders() });
|
||||
const data = await res.json();
|
||||
|
||||
const tbody = document.getElementById('comments-list');
|
||||
tbody.innerHTML = data.data?.map(c => `
|
||||
<tr>
|
||||
<td>${c.author}</td>
|
||||
<td>${c.content.substring(0, 50)}${c.content.length > 50 ? '...' : ''}</td>
|
||||
<td>文章 #${c.post_id}</td>
|
||||
<td><span class="status-badge status-${c.status}">${c.status}</span></td>
|
||||
<td>${new Date(c.created_at).toLocaleString()}</td>
|
||||
<td>
|
||||
${c.status === 'pending' ? `<button class="btn-success" onclick="approveComment(${c.id})">通过</button>` : ''}
|
||||
<button class="btn-danger" onclick="deleteComment(${c.id})">删除</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('') || '<tr><td colspan="6" style="text-align:center">暂无评论</td></tr>';
|
||||
} catch (err) {
|
||||
console.error('加载评论失败:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// 显示分类表单
|
||||
function showCategoryForm() {
|
||||
showModal('category', '新建分类');
|
||||
}
|
||||
|
||||
// 显示标签表单
|
||||
function showTagForm() {
|
||||
showModal('tag', '新建标签');
|
||||
}
|
||||
|
||||
// 显示页面表单
|
||||
function showPageForm() {
|
||||
showModal('page', '新建页面', true);
|
||||
}
|
||||
|
||||
// 显示模态框
|
||||
function showModal(type, title, hasContent = false) {
|
||||
document.getElementById('modal-type').value = type;
|
||||
document.getElementById('modal-id').value = '';
|
||||
document.getElementById('modal-title').textContent = title;
|
||||
document.getElementById('modal-name').value = '';
|
||||
document.getElementById('modal-slug').value = '';
|
||||
|
||||
const extra = document.querySelector('.modal-extra');
|
||||
if (hasContent) {
|
||||
extra.innerHTML = `
|
||||
<label>内容</label>
|
||||
<textarea id="modal-content" rows="5"></textarea>
|
||||
<label>排序</label>
|
||||
<input type="number" id="modal-order" value="0">
|
||||
`;
|
||||
} else {
|
||||
extra.innerHTML = '';
|
||||
}
|
||||
|
||||
document.getElementById('modal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
// 隐藏模态框
|
||||
function hideModal() {
|
||||
document.getElementById('modal').classList.add('hidden');
|
||||
}
|
||||
|
||||
// 提交模态框表单
|
||||
async function handleModalSubmit(e) {
|
||||
e.preventDefault();
|
||||
const type = document.getElementById('modal-type').value;
|
||||
const id = document.getElementById('modal-id').value;
|
||||
|
||||
const data = {
|
||||
name: document.getElementById('modal-name').value,
|
||||
slug: document.getElementById('modal-slug').value
|
||||
};
|
||||
|
||||
if (type === 'page') {
|
||||
data.content = document.getElementById('modal-content').value;
|
||||
data.order = parseInt(document.getElementById('modal-order').value) || 0;
|
||||
data.status = 'published';
|
||||
}
|
||||
|
||||
try {
|
||||
// 处理复数形式
|
||||
const typePlural = type === 'category' ? 'categories' :
|
||||
type === 'tag' ? 'tags' :
|
||||
type === 'page' ? 'pages' : type + 's';
|
||||
const url = id ? `${API_BASE}/admin/${typePlural}/${id}` : `${API_BASE}/admin/${typePlural}`;
|
||||
const method = id ? 'PUT' : 'POST';
|
||||
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers: getHeaders(),
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
hideModal();
|
||||
// 处理复数形式的页面名称
|
||||
const pageName = type === 'category' ? 'categories' :
|
||||
type === 'tag' ? 'tags' :
|
||||
type === 'page' ? 'pages' : type + 's';
|
||||
showPage(pageName);
|
||||
} else {
|
||||
const errorData = await res.json().catch(() => ({}));
|
||||
alert(errorData.error || '操作失败');
|
||||
}
|
||||
} catch (err) {
|
||||
alert('网络错误');
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑分类
|
||||
function editCategory(id, name, slug) {
|
||||
document.getElementById('modal-type').value = 'category';
|
||||
document.getElementById('modal-id').value = id;
|
||||
document.getElementById('modal-title').textContent = '编辑分类';
|
||||
document.getElementById('modal-name').value = name;
|
||||
document.getElementById('modal-slug').value = slug;
|
||||
document.querySelector('.modal-extra').innerHTML = '';
|
||||
document.getElementById('modal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
// 编辑标签
|
||||
function editTag(id, name, slug) {
|
||||
document.getElementById('modal-type').value = 'tag';
|
||||
document.getElementById('modal-id').value = id;
|
||||
document.getElementById('modal-title').textContent = '编辑标签';
|
||||
document.getElementById('modal-name').value = name;
|
||||
document.getElementById('modal-slug').value = slug;
|
||||
document.querySelector('.modal-extra').innerHTML = '';
|
||||
document.getElementById('modal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
// 删除分类
|
||||
async function deleteCategory(id) {
|
||||
if (!confirm('确定要删除这个分类吗?')) return;
|
||||
await fetch(`${API_BASE}/admin/categories/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: getHeaders()
|
||||
});
|
||||
loadCategories();
|
||||
}
|
||||
|
||||
// 删除标签
|
||||
async function deleteTag(id) {
|
||||
if (!confirm('确定要删除这个标签吗?')) return;
|
||||
await fetch(`${API_BASE}/admin/tags/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: getHeaders()
|
||||
});
|
||||
loadTags();
|
||||
}
|
||||
|
||||
// 删除页面
|
||||
async function deletePage(id) {
|
||||
if (!confirm('确定要删除这个页面吗?')) return;
|
||||
await fetch(`${API_BASE}/admin/pages/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: getHeaders()
|
||||
});
|
||||
loadPages();
|
||||
}
|
||||
|
||||
// 通过评论
|
||||
async function approveComment(id) {
|
||||
await fetch(`${API_BASE}/admin/comments/${id}/approve`, {
|
||||
method: 'PUT',
|
||||
headers: getHeaders()
|
||||
});
|
||||
loadComments();
|
||||
}
|
||||
|
||||
// 删除评论
|
||||
async function deleteComment(id) {
|
||||
if (!confirm('确定要删除这条评论吗?')) return;
|
||||
await fetch(`${API_BASE}/admin/comments/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: getHeaders()
|
||||
});
|
||||
loadComments();
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
function getStatusText(status) {
|
||||
const map = {
|
||||
'published': '已发布',
|
||||
'draft': '草稿',
|
||||
'private': '私密',
|
||||
'pending': '待审核'
|
||||
};
|
||||
return map[status] || status;
|
||||
}
|
||||
252
static/admin/index.html
Normal file
252
static/admin/index.html
Normal file
@@ -0,0 +1,252 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>后台管理 - GoBlog</title>
|
||||
<link rel="stylesheet" href="/static/admin/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<aside class="sidebar">
|
||||
<div class="logo">
|
||||
<h2>GoBlog</h2>
|
||||
<p>后台管理</p>
|
||||
</div>
|
||||
<nav class="menu">
|
||||
<a href="#dashboard" class="menu-item active" data-page="dashboard">仪表盘</a>
|
||||
<a href="#posts" class="menu-item" data-page="posts">文章管理</a>
|
||||
<a href="#categories" class="menu-item" data-page="categories">分类管理</a>
|
||||
<a href="#tags" class="menu-item" data-page="tags">标签管理</a>
|
||||
<a href="#pages" class="menu-item" data-page="pages">页面管理</a>
|
||||
<a href="#comments" class="menu-item" data-page="comments">评论管理</a>
|
||||
</nav>
|
||||
<div class="user-info">
|
||||
<span id="username">Admin</span>
|
||||
<button onclick="logout()">退出</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="main-content">
|
||||
<!-- 登录页面 -->
|
||||
<div id="login-page" class="page">
|
||||
<div class="login-box">
|
||||
<h2>管理员登录</h2>
|
||||
<form id="login-form">
|
||||
<div class="form-group">
|
||||
<label>用户名</label>
|
||||
<input type="text" id="login-username" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>密码</label>
|
||||
<input type="password" id="login-password" required>
|
||||
</div>
|
||||
<button type="submit">登录</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 仪表盘 -->
|
||||
<div id="dashboard-page" class="page hidden">
|
||||
<h1>仪表盘</h1>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<h3>文章总数</h3>
|
||||
<p class="stat-number" id="stat-posts">0</p>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>分类总数</h3>
|
||||
<p class="stat-number" id="stat-categories">0</p>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>标签总数</h3>
|
||||
<p class="stat-number" id="stat-tags">0</p>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>待审核评论</h3>
|
||||
<p class="stat-number" id="stat-comments">0</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文章管理 -->
|
||||
<div id="posts-page" class="page hidden">
|
||||
<div class="page-header">
|
||||
<h1>文章管理</h1>
|
||||
<button class="btn-primary" onclick="showPostForm()">新建文章</button>
|
||||
</div>
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>标题</th>
|
||||
<th>分类</th>
|
||||
<th>状态</th>
|
||||
<th>浏览量</th>
|
||||
<th>发布时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="posts-list"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 文章表单 -->
|
||||
<div id="post-form-page" class="page hidden">
|
||||
<div class="page-header">
|
||||
<h1 id="post-form-title">新建文章</h1>
|
||||
<button class="btn-secondary" onclick="showPage('posts')">返回</button>
|
||||
</div>
|
||||
<form id="post-form">
|
||||
<input type="hidden" id="post-id">
|
||||
<div class="form-group">
|
||||
<label>标题</label>
|
||||
<input type="text" id="post-title" required>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>分类</label>
|
||||
<select id="post-category" required></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>状态</label>
|
||||
<select id="post-status">
|
||||
<option value="draft">草稿</option>
|
||||
<option value="published">已发布</option>
|
||||
<option value="private">私密</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>标签(用逗号分隔)</label>
|
||||
<input type="text" id="post-tags" placeholder="标签1, 标签2, 标签3">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>封面图 URL</label>
|
||||
<input type="text" id="post-cover" placeholder="https://example.com/image.jpg">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>摘要</label>
|
||||
<textarea id="post-summary" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>内容 (支持 HTML)</label>
|
||||
<textarea id="post-content" rows="15" required></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox" id="post-istop"> 置顶文章
|
||||
</label>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary">保存</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 分类管理 -->
|
||||
<div id="categories-page" class="page hidden">
|
||||
<div class="page-header">
|
||||
<h1>分类管理</h1>
|
||||
<button class="btn-primary" onclick="showCategoryForm()">新建分类</button>
|
||||
</div>
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>名称</th>
|
||||
<th>别名</th>
|
||||
<th>文章数</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="categories-list"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 标签管理 -->
|
||||
<div id="tags-page" class="page hidden">
|
||||
<div class="page-header">
|
||||
<h1>标签管理</h1>
|
||||
<button class="btn-primary" onclick="showTagForm()">新建标签</button>
|
||||
</div>
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>名称</th>
|
||||
<th>别名</th>
|
||||
<th>文章数</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tags-list"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 页面管理 -->
|
||||
<div id="pages-page" class="page hidden">
|
||||
<div class="page-header">
|
||||
<h1>页面管理</h1>
|
||||
<button class="btn-primary" onclick="showPageForm()">新建页面</button>
|
||||
</div>
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>标题</th>
|
||||
<th>别名</th>
|
||||
<th>状态</th>
|
||||
<th>排序</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="pages-list"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 评论管理 -->
|
||||
<div id="comments-page" class="page hidden">
|
||||
<div class="page-header">
|
||||
<h1>评论管理</h1>
|
||||
</div>
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>作者</th>
|
||||
<th>内容</th>
|
||||
<th>文章</th>
|
||||
<th>状态</th>
|
||||
<th>时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="comments-list"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- 分类/标签/页面 表单弹窗 -->
|
||||
<div id="modal" class="modal hidden">
|
||||
<div class="modal-content">
|
||||
<h3 id="modal-title">标题</h3>
|
||||
<form id="modal-form">
|
||||
<input type="hidden" id="modal-id">
|
||||
<input type="hidden" id="modal-type">
|
||||
<div class="form-group">
|
||||
<label>名称</label>
|
||||
<input type="text" id="modal-name" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>别名(可选,用于 URL)</label>
|
||||
<input type="text" id="modal-slug">
|
||||
</div>
|
||||
<div class="form-group modal-extra">
|
||||
<!-- 动态内容 -->
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button type="button" class="btn-secondary" onclick="hideModal()">取消</button>
|
||||
<button type="submit" class="btn-primary">保存</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/admin/app.js?v=3"></script>
|
||||
</body>
|
||||
</html>
|
||||
360
static/admin/style.css
Normal file
360
static/admin/style.css
Normal file
@@ -0,0 +1,360 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
background: #f0f2f5;
|
||||
}
|
||||
|
||||
#app {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* 侧边栏 */
|
||||
.sidebar {
|
||||
width: 220px;
|
||||
background: #001529;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.logo {
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.logo h2 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.logo p {
|
||||
font-size: 12px;
|
||||
color: rgba(255,255,255,0.6);
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.menu {
|
||||
flex: 1;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: block;
|
||||
padding: 12px 20px;
|
||||
color: rgba(255,255,255,0.7);
|
||||
text-decoration: none;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.menu-item:hover,
|
||||
.menu-item.active {
|
||||
color: #fff;
|
||||
background: #1890ff;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
padding: 15px 20px;
|
||||
border-top: 1px solid rgba(255,255,255,0.1);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user-info button {
|
||||
background: transparent;
|
||||
border: 1px solid rgba(255,255,255,0.3);
|
||||
color: #fff;
|
||||
padding: 5px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.user-info button:hover {
|
||||
background: rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
/* 主内容区 */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.page.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 按钮 */
|
||||
.btn-primary {
|
||||
background: #1890ff;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #40a9ff;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #fff;
|
||||
color: #333;
|
||||
border: 1px solid #d9d9d9;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
border-color: #1890ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #ff4d4f;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #52c41a;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
/* 统计卡片 */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: #fff;
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
.stat-card h3 {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
color: #1890ff;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* 数据表格 */
|
||||
.data-table {
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.data-table th,
|
||||
.data-table td {
|
||||
padding: 12px 16px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.data-table th {
|
||||
background: #fafafa;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.data-table tr:hover {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
/* 表单 */
|
||||
.form-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group select:focus,
|
||||
.form-group textarea:focus {
|
||||
outline: none;
|
||||
border-color: #1890ff;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
/* 登录页 */
|
||||
#login-page {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
background: #f0f2f5;
|
||||
}
|
||||
|
||||
#login-page.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.login-box {
|
||||
background: #fff;
|
||||
padding: 40px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
width: 360px;
|
||||
}
|
||||
|
||||
.login-box h2 {
|
||||
text-align: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.login-box button {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* 弹窗 */
|
||||
.modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0,0,0,0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: #fff;
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
width: 400px;
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.modal-content h3 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* 状态标签 */
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.status-published {
|
||||
background: #f6ffed;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.status-draft {
|
||||
background: #fff7e6;
|
||||
color: #fa8c16;
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
background: #fff2f0;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.status-approved {
|
||||
background: #f6ffed;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
.logo h2,
|
||||
.logo p,
|
||||
.menu-item {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user