qoder生成项目

This commit is contained in:
2026-02-13 10:53:54 +08:00
commit 4cab051f9c
24 changed files with 3627 additions and 0 deletions

560
static/admin/app.js Normal file
View 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
View 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
View 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;
}
}

376
static/css/style.css Normal file
View File

@@ -0,0 +1,376 @@
/* 基础样式重置 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-size: 16px;
line-height: 1.6;
color: #333;
background-color: #f5f5f5;
}
a {
color: #1890ff;
text-decoration: none;
}
a:hover {
color: #40a9ff;
}
.container {
max-width: 900px;
margin: 0 auto;
padding: 0 20px;
}
/* 头部样式 */
.header {
background: #fff;
padding: 30px 0;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
margin-bottom: 30px;
}
.site-title {
font-size: 28px;
font-weight: 600;
}
.site-title a {
color: #333;
}
.site-desc {
color: #666;
margin-top: 8px;
}
.main-nav {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #eee;
}
.main-nav a {
margin-right: 20px;
color: #666;
font-size: 15px;
}
.main-nav a:hover {
color: #1890ff;
}
/* 主内容区 */
.main {
min-height: 500px;
}
/* 文章列表 */
.post-list {
display: flex;
flex-direction: column;
gap: 25px;
}
.post-item {
background: #fff;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
.post-cover img {
width: 100%;
height: 200px;
object-fit: cover;
}
.post-content {
padding: 25px;
}
.post-title {
font-size: 22px;
margin-bottom: 12px;
}
.post-title a {
color: #333;
}
.post-title a:hover {
color: #1890ff;
}
.top-badge {
display: inline-block;
background: #ff4d4f;
color: #fff;
font-size: 12px;
padding: 2px 8px;
border-radius: 4px;
margin-left: 8px;
vertical-align: middle;
}
.post-meta {
color: #999;
font-size: 14px;
margin-bottom: 15px;
}
.post-meta span {
margin-right: 15px;
}
.post-meta a {
color: #666;
}
.tag {
display: inline-block;
background: #f0f0f0;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
margin-right: 5px;
}
.post-summary {
color: #666;
line-height: 1.8;
}
.read-more {
display: inline-block;
margin-top: 15px;
color: #1890ff;
}
/* 文章详情 */
.post-detail {
background: #fff;
border-radius: 8px;
padding: 40px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
.post-header {
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 1px solid #eee;
}
.post-detail .post-title {
font-size: 28px;
margin-bottom: 15px;
}
.post-body {
line-height: 1.8;
color: #444;
}
.post-body h2,
.post-body h3,
.post-body h4 {
margin: 30px 0 15px;
}
.post-body p {
margin-bottom: 15px;
}
.post-body img {
max-width: 100%;
border-radius: 4px;
}
.post-body pre {
background: #f6f8fa;
padding: 16px;
border-radius: 6px;
overflow-x: auto;
}
.post-body code {
background: #f6f8fa;
padding: 2px 6px;
border-radius: 3px;
font-family: monospace;
}
/* 页面 */
.page-detail {
background: #fff;
border-radius: 8px;
padding: 40px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
.page-title {
font-size: 28px;
margin-bottom: 20px;
}
/* 分页 */
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 20px;
margin-top: 40px;
padding: 20px;
}
.pagination a {
padding: 8px 16px;
background: #fff;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.page-info {
color: #666;
}
/* 评论区 */
.comments-section {
background: #fff;
border-radius: 8px;
padding: 30px;
margin-top: 30px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
.comments-section h3 {
margin-bottom: 20px;
}
.comment-form {
margin-bottom: 30px;
padding-bottom: 30px;
border-bottom: 1px solid #eee;
}
.form-group {
margin-bottom: 15px;
}
.form-group input,
.form-group textarea {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.form-group textarea {
min-height: 100px;
resize: vertical;
}
.comment-form button {
background: #1890ff;
color: #fff;
border: none;
padding: 10px 24px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.comment-form button:hover {
background: #40a9ff;
}
.comments-list {
display: flex;
flex-direction: column;
gap: 20px;
}
.comment-item {
padding: 15px;
background: #f9f9f9;
border-radius: 6px;
}
.comment-item.child {
margin-left: 30px;
margin-top: 10px;
background: #f0f0f0;
}
.comment-header {
margin-bottom: 10px;
font-size: 14px;
}
.comment-author {
font-weight: 600;
color: #333;
}
.comment-date {
color: #999;
margin-left: 10px;
}
.comment-content {
color: #555;
line-height: 1.6;
}
.no-comments {
color: #999;
text-align: center;
padding: 30px;
}
/* 空状态 */
.empty {
text-align: center;
padding: 60px;
color: #999;
background: #fff;
border-radius: 8px;
}
/* 底部 */
.footer {
background: #fff;
padding: 30px 0;
margin-top: 50px;
text-align: center;
color: #999;
border-top: 1px solid #eee;
}
/* 响应式 */
@media (max-width: 768px) {
.site-title {
font-size: 24px;
}
.post-detail,
.page-detail {
padding: 20px;
}
.post-detail .post-title {
font-size: 22px;
}
.post-meta span {
display: block;
margin: 5px 0;
}
}