08-12-周三_14-20-35

This commit is contained in:
AaronXu
2026-08-12 14:20:36 +08:00
parent 977f7eec96
commit 56dd5b5adf
16 changed files with 1867 additions and 4 deletions
@@ -0,0 +1,47 @@
{
"models": [
{
"id": "kimi-for-coding",
"name": "kimi-for-coding",
"vendor": "user",
"url": "https://api.kimi.com/coding/v1",
"apiKey": "sk-kimi-OLoO20UJ8D3hPtywZqitU6zQO6edvWXhrRcWynoQFWBsmf38slzCG3ZbhvhLZOeS",
"supportsToolCall": true,
"supportsImages": true,
"supportsReasoning": false,
"temperature": 1
},
{
"id": "kimicode",
"name": "kimicode",
"vendor": "user",
"url": "https://api.kimi.com/coding/v1",
"apiKey": "sk-kimi-1sl2dMlwHcDAM5epuy4EHXhlehH38tcZxzqyOXAK7Og81HofBAcmYnW86KOd9Hdr",
"supportsToolCall": true,
"supportsImages": true,
"supportsReasoning": true,
"temperature": 1
},
{
"id": "mimo-v2.5-pro",
"name": "mimo-v2.5-pro",
"vendor": "user",
"url": "https://token-plan-cn.xiaomimimo.com/v1",
"apiKey": "tpqqgst8uxezjxw070kyv57vo4ayjm97go9jqk7fh",
"supportsToolCall": true,
"supportsImages": true,
"supportsReasoning": false
},
{
"id": "deepseek-v4-pro",
"name": "DeepSeek-V4 Pro",
"vendor": "user",
"url": "https://api.deepseek.com/chat/completions",
"apiKey": "sk-4456a067934f4a2a9582d0f579c7b231",
"supportsToolCall": true,
"supportsImages": false,
"supportsReasoning": true
}
]
}
@@ -0,0 +1,249 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>后台管理 - 笔记小站</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<nav class="navbar">
<a class="navbar-brand" href="index.html"><span class="logo-icon">📝</span> 笔记小站</a>
<div class="navbar-actions" id="navbarActions"></div>
</nav>
<div class="container" id="app">
<div class="admin-header">
<h1>⚙️ 后台管理</h1>
</div>
<div class="admin-tabs">
<button class="admin-tab" data-tab="categories">📂 分类管理</button>
<button class="admin-tab" data-tab="notes">📝 笔记管理</button>
<button class="admin-tab" data-tab="comments">💬 评论管理</button>
<button class="admin-tab" data-tab="users">👥 用户管理</button>
</div>
<div id="tabContent"></div>
</div>
<div class="toast-container" id="toastContainer"></div>
<script src="js/data.js"></script>
<script src="js/common.js"></script>
<script>
(function () {
var activeTab = getUrlParam('tab') || 'categories';
var tabs = document.querySelectorAll('.admin-tab');
tabs.forEach(function (t) {
if (t.dataset.tab === activeTab) t.classList.add('active');
t.addEventListener('click', function () {
history.replaceState(null, '', '?tab=' + this.dataset.tab);
activeTab = this.dataset.tab;
tabs.forEach(function (x) { x.classList.remove('active'); });
this.classList.add('active');
renderTab();
});
});
renderTab();
function renderTab() {
var content = document.getElementById('tabContent');
if (activeTab === 'categories') content.innerHTML = renderCategories();
else if (activeTab === 'notes') content.innerHTML = renderNotes();
else if (activeTab === 'comments') content.innerHTML = renderComments();
else if (activeTab === 'users') content.innerHTML = renderUsers();
}
/* ============ Categories ============ */
function renderCategories() {
var h = '<div class="section-header">';
h += '<h2>分类列表</h2>';
h += '<button class="btn btn-primary" onclick="openCategoryModal()">+ 新增分类</button>';
h += '</div>';
h += '<div class="data-table">';
h += '<table><thead><tr><th>ID</th><th>图标</th><th>名称</th><th>颜色</th><th>操作</th></tr></thead><tbody>';
DATA.categories.forEach(function (c) {
h += '<tr><td>' + c.id + '</td><td>' + c.icon + '</td><td>' + c.name + '</td>';
h += '<td><span class="color-dot" style="background:' + c.color + '"></span> ' + c.color + '</td>';
h += '<td><button class="btn btn-sm" onclick="openCategoryModal(' + c.id + ')">编辑</button> ';
h += '<button class="btn btn-sm btn-danger" onclick="deleteCategory(' + c.id + ')">删除</button></td></tr>';
});
h += '</tbody></table></div>';
return h;
}
window.openCategoryModal = function (id) {
var cat = id ? findById(DATA.categories, id) : null;
var title = cat ? '编辑分类' : '新增分类';
var h = '<div class="form-group"><label>名称</label><input type="text" id="fname" value="' + (cat ? cat.name : '') + '"></div>';
h += '<div class="form-group"><label>图标</label><input type="text" id="ficon" value="' + (cat ? cat.icon : '📁') + '"></div>';
h += '<div class="form-group"><label>颜色</label><input type="color" id="fcolor" value="' + (cat ? cat.color : '#6366f1') + '"></div>';
h += '<div class="form-actions"><button class="btn" onclick="closeModal()">取消</button>';
h += '<button class="btn btn-primary" onclick="saveCategory(' + (cat ? cat.id : 0) + ')">保存</button></div>';
renderModal(title, h);
};
window.saveCategory = function (id) {
var name = document.getElementById('fname').value.trim();
var icon = document.getElementById('ficon').value.trim();
var color = document.getElementById('fcolor').value.trim();
if (!name) { showToast('请输入名称', 'error'); return; }
if (id) {
var c = findById(DATA.categories, id);
if (c) { c.name = name; c.icon = icon; c.color = color; }
} else {
DATA.categories.push({ id: DATA.categories.length + 1, name: name, icon: icon, color: color });
}
closeModal(); renderTab(); showToast('保存成功', 'success');
};
window.deleteCategory = function (id) {
DATA.categories = DATA.categories.filter(function (c) { return c.id !== id; });
renderTab(); showToast('已删除', 'info');
};
/* ============ Notes ============ */
function renderNotes() {
var h = '<div class="section-header">';
h += '<h2>笔记列表</h2>';
h += '<button class="btn btn-primary" onclick="openNoteModal()">+ 新增笔记</button>';
h += '</div><div class="data-table"><table><thead><tr><th>ID</th><th>标题</th><th>分类</th><th>作者</th><th>时间</th><th>操作</th></tr></thead><tbody>';
DATA.notes.forEach(function (n) {
var cat = findById(DATA.categories, n.categoryId);
var author = findById(DATA.users, n.userId);
h += '<tr><td>' + n.id + '</td><td>' + n.title + '</td>';
h += '<td>' + (cat ? cat.icon + ' ' + cat.name : '-') + '</td>';
h += '<td>' + (author ? author.nickname : '-') + '</td><td>' + n.createdAt + '</td>';
h += '<td><button class="btn btn-sm" onclick="openNoteModal(' + n.id + ')">编辑</button> ';
h += '<button class="btn btn-sm btn-danger" onclick="deleteNote(' + n.id + ')">删除</button></td></tr>';
});
h += '</tbody></table></div>';
return h;
}
window.openNoteModal = function (id) {
var note = id ? findById(DATA.notes, id) : null;
var title = note ? '编辑笔记' : '新增笔记';
var h = '<div class="form-group"><label>标题</label><input type="text" id="ftitle" value="' + (note ? note.title : '') + '"></div>';
h += '<div class="form-group"><label>分类</label><select id="fcat">';
DATA.categories.forEach(function (c) {
h += '<option value="' + c.id + '"' + (note && note.categoryId === c.id ? ' selected' : '') + '>' + c.icon + ' ' + c.name + '</option>';
});
h += '</select></div>';
h += '<div class="form-group"><label>内容</label><textarea id="fcontent" rows="6">' + (note ? note.content : '') + '</textarea></div>';
h += '<div class="form-actions"><button class="btn" onclick="closeModal()">取消</button>';
h += '<button class="btn btn-primary" onclick="saveNote(' + (note ? note.id : 0) + ')">保存</button></div>';
renderModal(title, h);
};
window.saveNote = function (id) {
var title = document.getElementById('ftitle').value.trim();
var catId = parseInt(document.getElementById('fcat').value);
var content = document.getElementById('fcontent').value.trim();
if (!title || !content) { showToast('请填写标题和内容', 'error'); return; }
if (id) {
var n = findById(DATA.notes, id);
if (n) { n.title = title; n.categoryId = catId; n.content = content; }
} else {
DATA.notes.push({
id: DATA.notes.length + 1,
title: title,
categoryId: catId,
content: content,
userId: 1,
createdAt: new Date().toISOString().slice(0, 10),
summary: content.slice(0, 100) + (content.length > 100 ? '...' : '')
});
}
closeModal(); renderTab(); showToast('保存成功', 'success');
};
window.deleteNote = function (id) {
DATA.notes = DATA.notes.filter(function (n) { return n.id !== id; });
DATA.comments = DATA.comments.filter(function (c) { return c.noteId !== id; });
renderTab(); showToast('已删除', 'info');
};
/* ============ Comments ============ */
function renderComments() {
var h = '<div class="section-header"><h2>评论列表</h2></div><div class="data-table"><table><thead><tr><th>ID</th><th>笔记</th><th>用户</th><th>内容</th><th>时间</th><th>操作</th></tr></thead><tbody>';
DATA.comments.forEach(function (c) {
var note = findById(DATA.notes, c.noteId);
var user = findById(DATA.users, c.userId);
h += '<tr><td>' + c.id + '</td><td>' + (note ? note.title : '已删除') + '</td>';
h += '<td>' + (user ? user.nickname : (c.author || '游客')) + '</td><td>' + c.content + '</td><td>' + c.createdAt + '</td>';
h += '<td><button class="btn btn-sm btn-danger" onclick="deleteComment(' + c.id + ')">删除</button></td></tr>';
});
h += '</tbody></table></div>';
return h;
}
window.deleteComment = function (id) {
DATA.comments = DATA.comments.filter(function (c) { return c.id !== id; });
renderTab(); showToast('已删除', 'info');
};
/* ============ Users ============ */
function renderUsers() {
var h = '<div class="section-header">';
h += '<h2>用户列表</h2>';
h += '<button class="btn btn-primary" onclick="openUserModal()">+ 新增用户</button>';
h += '</div><div class="data-table"><table><thead><tr><th>ID</th><th>头像</th><th>用户名</th><th>昵称</th><th>角色</th><th>时间</th><th>操作</th></tr></thead><tbody>';
DATA.users.forEach(function (u) {
h += '<tr><td>' + u.id + '</td><td>' + u.avatar + '</td><td>' + u.username + '</td><td>' + u.nickname + '</td>';
h += '<td><span class="badge badge-' + u.role + '">' + (u.role === 'admin' ? '管理员' : '普通用户') + '</span></td>';
h += '<td>' + u.createdAt + '</td>';
h += '<td><button class="btn btn-sm" onclick="openUserModal(' + u.id + ')">编辑</button> ';
h += '<button class="btn btn-sm btn-danger" onclick="deleteUser(' + u.id + ')">删除</button></td></tr>';
});
h += '</tbody></table></div>';
return h;
}
window.openUserModal = function (id) {
var u = id ? findById(DATA.users, id) : null;
var title = u ? '编辑用户' : '新增用户';
var h = '<div class="form-group"><label>用户名</label><input type="text" id="fname" value="' + (u ? u.username : '') + '"></div>';
h += '<div class="form-group"><label>昵称</label><input type="text" id="fnick" value="' + (u ? u.nickname : '') + '"></div>';
h += '<div class="form-group"><label>角色</label><select id="frole">';
h += '<option value="user"' + (u && u.role === 'user' ? ' selected' : '') + '>普通用户</option>';
h += '<option value="admin"' + (u && u.role === 'admin' ? ' selected' : '') + '>管理员</option>';
h += '</select></div>';
h += '<div class="form-actions"><button class="btn" onclick="closeModal()">取消</button>';
h += '<button class="btn btn-primary" onclick="saveUser(' + (u ? u.id : 0) + ')">保存</button></div>';
renderModal(title, h);
};
window.saveUser = function (id) {
var username = document.getElementById('fname').value.trim();
var nickname = document.getElementById('fnick').value.trim();
var role = document.getElementById('frole').value;
if (!username || !nickname) { showToast('请填写用户名和昵称', 'error'); return; }
if (id) {
var u = findById(DATA.users, id);
if (u) { u.username = username; u.nickname = nickname; u.role = role; }
} else {
DATA.users.push({
id: DATA.users.length + 1,
username: username, password: '123456',
nickname: nickname, avatar: '😊', role: role,
createdAt: new Date().toISOString().slice(0, 10)
});
}
closeModal(); renderTab(); showToast('保存成功', 'success');
};
window.deleteUser = function (id) {
DATA.users = DATA.users.filter(function (u) { return u.id !== id; });
renderTab(); showToast('已删除', 'info');
};
/* Helper */
function findById(arr, id) {
for (var i = 0; i < arr.length; i++) { if (arr[i].id === id) return arr[i]; }
return null;
}
})();
</script>
</body>
</html>
@@ -0,0 +1,761 @@
/* ==================== CSS Variables & Reset ==================== */
:root {
--primary: #6366f1;
--primary-hover: #4f46e5;
--primary-light: #eef2ff;
--primary-soft: #e0e7ff;
--success: #10b981;
--warning: #f59e0b;
--danger: #ef4444;
--danger-hover: #dc2626;
--bg: #f8fafc;
--bg-card: #ffffff;
--bg-hover: #f1f5f9;
--text: #0f172a;
--text-secondary: #64748b;
--text-muted: #94a3b8;
--border: #e2e8f0;
--border-light: #f1f5f9;
--shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
--shadow: 0 1px 3px rgba(0,0,0,0.1), 0 1px 2px rgba(0,0,0,0.06);
--shadow-md: 0 4px 6px rgba(0,0,0,0.07), 0 2px 4px rgba(0,0,0,0.06);
--shadow-lg: 0 10px 15px rgba(0,0,0,0.1), 0 4px 6px rgba(0,0,0,0.05);
--shadow-xl: 0 20px 25px rgba(0,0,0,0.1), 0 10px 10px rgba(0,0,0,0.04);
--radius-sm: 8px;
--radius: 12px;
--radius-lg: 16px;
--radius-xl: 20px;
--transition: 0.2s cubic-bezier(0.4, 0, 0.2, 1);
--transition-slow: 0.35s cubic-bezier(0.4, 0, 0.2, 1);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.6;
min-height: 100vh;
-webkit-font-smoothing: antialiased;
}
a { text-decoration: none; color: inherit; }
button { cursor: pointer; font-family: inherit; border: none; outline: none; }
input, textarea, select { font-family: inherit; outline: none; }
/* ==================== Animations ==================== */
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(24px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes fadeInScale {
from { opacity: 0; transform: scale(0.95); }
to { opacity: 1; transform: scale(1); }
}
@keyframes slideInLeft {
from { opacity: 0; transform: translateX(-20px); }
to { opacity: 1; transform: translateX(0); }
}
@keyframes slideInRight {
from { opacity: 0; transform: translateX(20px); }
to { opacity: 1; transform: translateX(0); }
}
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
.anim-fade-up { animation: fadeInUp 0.6s cubic-bezier(0.4,0,0.2,1) both; }
.anim-fade-up-d1 { animation-delay: 0.05s; }
.anim-fade-up-d2 { animation-delay: 0.1s; }
.anim-fade-up-d3 { animation-delay: 0.15s; }
.anim-fade-up-d4 { animation-delay: 0.2s; }
.anim-fade-up-d5 { animation-delay: 0.25s; }
.anim-fade-up-d6 { animation-delay: 0.3s; }
.anim-scale-in { animation: fadeInScale 0.3s cubic-bezier(0.4,0,0.2,1) both; }
/* ==================== Navbar ==================== */
.navbar {
position: sticky; top: 0; z-index: 100;
background: rgba(255,255,255,0.85);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-bottom: 1px solid var(--border);
padding: 0 2rem;
height: 64px;
display: flex;
align-items: center;
justify-content: space-between;
}
.navbar-brand {
font-size: 1.5rem; font-weight: 800;
background: linear-gradient(135deg, var(--primary), #8b5cf6);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
display: flex; align-items: center; gap: 0.5rem;
}
.logo-icon {
width: 36px; height: 36px;
background: linear-gradient(135deg, var(--primary), #8b5cf6);
border-radius: var(--radius-sm);
display: flex; align-items: center; justify-content: center;
font-size: 1.2rem;
-webkit-text-fill-color: white;
}
.navbar-actions {
display: flex; align-items: center; gap: 0.75rem;
}
/* ==================== Buttons ==================== */
.btn {
padding: 0.5rem 1.25rem; border-radius: var(--radius-sm);
font-size: 0.9rem; font-weight: 600;
transition: all var(--transition);
display: inline-flex; align-items: center; gap: 0.4rem;
}
.btn-primary {
background: linear-gradient(135deg, var(--primary), #8b5cf6);
color: white;
box-shadow: 0 2px 8px rgba(99,102,241,0.3);
}
.btn-primary:hover {
transform: translateY(-1px);
box-shadow: 0 4px 14px rgba(99,102,241,0.4);
}
.btn-outline {
background: transparent; color: var(--primary);
border: 2px solid var(--primary-soft);
}
.btn-outline:hover {
background: var(--primary-light); border-color: var(--primary);
}
.btn-ghost {
background: transparent; color: var(--text-secondary);
}
.btn-ghost:hover { background: var(--bg-hover); color: var(--text); }
.btn-sm { padding: 0.35rem 0.85rem; font-size: 0.8rem; }
.btn-danger { background: var(--danger); color: white; }
.btn-danger:hover { background: var(--danger-hover); transform: translateY(-1px); }
.btn-success { background: var(--success); color: white; }
.btn-success:hover { opacity: 0.9; transform: translateY(-1px); }
.btn-warning { background: var(--warning); color: white; }
/* ==================== User Menu ==================== */
.user-avatar {
width: 36px; height: 36px; border-radius: 50%;
background: linear-gradient(135deg, var(--primary), #8b5cf6);
color: white;
display: flex; align-items: center; justify-content: center;
font-weight: 700; font-size: 0.9rem;
}
.user-menu {
display: flex; align-items: center; gap: 0.5rem;
padding: 0.3rem 0.85rem; border-radius: 100px;
cursor: pointer; transition: var(--transition); position: relative;
}
.user-menu:hover { background: var(--bg-hover); }
.user-dropdown {
position: absolute; top: calc(100% + 8px); right: 0;
background: var(--bg-card); border-radius: var(--radius);
box-shadow: var(--shadow-xl); border: 1px solid var(--border);
min-width: 180px; padding: 0.5rem;
animation: fadeInScale 0.2s ease both;
display: none; z-index: 50;
}
.user-dropdown.show { display: block; }
.user-dropdown a {
display: flex; align-items: center; gap: 0.5rem;
padding: 0.6rem 0.85rem; border-radius: var(--radius-sm);
font-size: 0.9rem; transition: var(--transition);
}
.user-dropdown a:hover { background: var(--primary-light); color: var(--primary); }
.user-dropdown .divider { height: 1px; background: var(--border); margin: 0.25rem 0; }
/* ==================== Layout ==================== */
.container { max-width: 1280px; margin: 0 auto; padding: 2rem; }
.main-content { display: flex; gap: 2rem; }
/* ==================== Sidebar ==================== */
.sidebar {
width: 260px; flex-shrink: 0;
position: sticky; top: 88px; height: fit-content;
animation: slideInLeft 0.5s ease both;
}
.category-panel {
background: var(--bg-card); border-radius: var(--radius-lg);
box-shadow: var(--shadow); border: 1px solid var(--border);
overflow: hidden;
}
.category-panel-header {
padding: 1.25rem 1.25rem 0.75rem;
font-weight: 700; font-size: 1rem;
display: flex; align-items: center; gap: 0.5rem;
}
.category-list {
list-style: none; padding: 0 0.5rem 0.75rem;
}
.category-item { margin: 0.15rem 0; }
.category-item a {
display: flex; align-items: center; justify-content: space-between;
padding: 0.65rem 0.85rem; border-radius: var(--radius-sm);
font-size: 0.9rem; transition: all var(--transition);
color: var(--text-secondary); cursor: pointer;
}
.category-item a:hover,
.category-item a.active {
background: var(--primary-light); color: var(--primary); font-weight: 600;
}
.category-item .count {
background: var(--border-light); padding: 0.15rem 0.55rem;
border-radius: 100px; font-size: 0.75rem; font-weight: 600;
color: var(--text-muted);
}
.category-item a.active .count {
background: var(--primary-soft); color: var(--primary);
}
/* ==================== Content Area ==================== */
.content-area { flex: 1; min-width: 0; }
.content-header {
display: flex; align-items: center; justify-content: space-between;
margin-bottom: 1.5rem;
animation: fadeInUp 0.5s ease both;
}
.content-header h2 { font-size: 1.5rem; font-weight: 700; }
.search-box { position: relative; }
.search-box input {
padding: 0.55rem 1rem 0.55rem 2.5rem;
border: 2px solid var(--border); border-radius: 100px;
font-size: 0.9rem; width: 260px;
transition: all var(--transition); background: var(--bg-card);
}
.search-box input:focus {
border-color: var(--primary); box-shadow: 0 0 0 3px rgba(99,102,241,0.1);
width: 300px;
}
.search-box .search-icon {
position: absolute; left: 0.85rem; top: 50%;
transform: translateY(-50%);
color: var(--text-muted); font-size: 0.9rem;
}
/* ==================== Notes Grid ==================== */
.notes-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 1.25rem;
}
.note-card {
display: block;
background: var(--bg-card); border-radius: var(--radius-lg);
border: 1px solid var(--border); padding: 1.5rem;
transition: all var(--transition-slow);
cursor: pointer; position: relative; overflow: hidden;
animation: fadeInUp 0.5s ease both;
color: inherit;
text-decoration: none;
}
.note-card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-xl);
border-color: var(--primary-soft);
}
.note-card::before {
content: ''; position: absolute; top: 0; left: 0; right: 0;
height: 3px;
background: linear-gradient(90deg, var(--primary), #8b5cf6, #a78bfa);
transform: scaleX(0); transform-origin: left;
transition: transform var(--transition-slow);
}
.note-card:hover::before { transform: scaleX(1); }
.note-category {
display: inline-flex; align-items: center; gap: 0.3rem;
padding: 0.2rem 0.7rem; border-radius: 100px;
font-size: 0.75rem; font-weight: 600; margin-bottom: 0.75rem;
}
.note-title {
font-size: 1.1rem; font-weight: 700; margin-bottom: 0.5rem;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.note-excerpt {
color: var(--text-secondary); font-size: 0.88rem;
line-height: 1.6; margin-bottom: 1rem;
display: -webkit-box;
-webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
}
.note-meta {
display: flex; align-items: center; justify-content: space-between;
font-size: 0.8rem; color: var(--text-muted);
padding-top: 0.75rem; border-top: 1px solid var(--border-light);
}
.note-meta span { display: flex; align-items: center; gap: 0.3rem; }
/* ==================== Note Detail Modal ==================== */
.note-detail-overlay {
position: fixed; inset: 0;
background: rgba(15,23,42,0.5); backdrop-filter: blur(4px);
z-index: 200;
display: flex; align-items: center; justify-content: center;
animation: fadeIn 0.2s ease both;
}
.note-detail {
background: var(--bg-card); border-radius: var(--radius-xl);
width: 90%; max-width: 750px; max-height: 90vh; overflow-y: auto;
box-shadow: var(--shadow-xl);
animation: fadeInScale 0.3s ease both;
}
.note-detail-header {
padding: 1.5rem 2rem; border-bottom: 1px solid var(--border);
display: flex; align-items: center; justify-content: space-between;
position: sticky; top: 0; background: var(--bg-card); z-index: 1;
border-radius: var(--radius-xl) var(--radius-xl) 0 0;
}
.note-detail-header h3 { font-size: 1.3rem; }
.note-detail-close {
width: 36px; height: 36px; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
background: var(--bg-hover); font-size: 1.2rem;
transition: var(--transition); cursor: pointer;
}
.note-detail-close:hover { background: var(--border); }
.note-detail-body { padding: 2rem; }
.note-full-content {
font-size: 1rem; line-height: 1.8; white-space: pre-wrap;
}
.note-detail-info {
display: flex; gap: 1.5rem; margin: 1.25rem 0;
padding: 1rem; background: var(--bg);
border-radius: var(--radius); font-size: 0.85rem;
color: var(--text-secondary);
}
/* ==================== Comments ==================== */
.comments-section { padding: 0 2rem 2rem; }
.comments-section h4 {
font-size: 1.1rem; margin-bottom: 1rem;
display: flex; align-items: center; gap: 0.5rem;
}
.comment-form {
display: flex; gap: 0.75rem; margin-bottom: 1.5rem;
}
.comment-form input {
flex: 1; padding: 0.65rem 1rem;
border: 2px solid var(--border); border-radius: 100px;
font-size: 0.9rem; transition: var(--transition);
}
.comment-form input:focus {
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(99,102,241,0.1);
}
.comment-form button {
padding: 0.6rem 1.5rem; border-radius: 100px;
background: var(--primary); color: white; font-weight: 600;
transition: var(--transition);
}
.comment-form button:hover { background: var(--primary-hover); }
.comment-list { list-style: none; display: flex; flex-direction: column; gap: 0.75rem; }
.comment-item {
background: var(--bg); padding: 1rem 1.25rem;
border-radius: var(--radius);
animation: fadeInUp 0.4s ease both;
display: flex; gap: 0.75rem;
}
.comment-avatar {
width: 36px; height: 36px; border-radius: 50%;
background: linear-gradient(135deg, #a78bfa, #6366f1);
color: white;
display: flex; align-items: center; justify-content: center;
font-weight: 700; font-size: 0.8rem; flex-shrink: 0;
}
.comment-body { flex: 1; }
.comment-author { font-weight: 600; font-size: 0.9rem; }
.comment-time { font-size: 0.75rem; color: var(--text-muted); margin-left: 0.5rem; }
.comment-text { font-size: 0.88rem; color: var(--text-secondary); margin-top: 0.25rem; }
.comment-actions { display: flex; gap: 0.5rem; margin-top: 0.4rem; }
.comment-actions button {
font-size: 0.75rem; color: var(--text-muted); background: none;
padding: 0.2rem 0.5rem; border-radius: 4px; transition: var(--transition);
}
.comment-actions button:hover { color: var(--danger); background: #fef2f2; }
/* ==================== Modals ==================== */
.modal-overlay {
position: fixed; inset: 0;
background: rgba(15,23,42,0.5); backdrop-filter: blur(4px);
z-index: 300;
display: flex; align-items: center; justify-content: center;
animation: fadeIn 0.2s ease both;
}
.login-modal {
background: var(--bg-card); border-radius: var(--radius-xl);
padding: 2.5rem; width: 90%; max-width: 420px;
box-shadow: var(--shadow-xl);
animation: fadeInScale 0.3s ease both;
}
.login-modal h2 { font-size: 1.5rem; text-align: center; margin-bottom: 0.5rem; }
.login-modal .subtitle {
text-align: center; color: var(--text-secondary);
font-size: 0.9rem; margin-bottom: 2rem;
}
.form-group { margin-bottom: 1.25rem; }
.form-group label {
display: block; font-size: 0.85rem; font-weight: 600;
margin-bottom: 0.4rem; color: var(--text);
}
.form-group input,
.form-group select,
.form-group textarea {
width: 100%; padding: 0.7rem 1rem;
border: 2px solid var(--border); border-radius: var(--radius-sm);
font-size: 0.9rem; transition: all var(--transition);
background: var(--bg);
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(99,102,241,0.1);
}
.form-group textarea { resize: vertical; min-height: 100px; }
.login-modal .btn-primary { width: 100%; justify-content: center; padding: 0.75rem; }
.login-hint {
text-align: center; margin-top: 1rem;
font-size: 0.8rem; color: var(--text-muted);
}
.form-modal {
background: var(--bg-card); border-radius: var(--radius-xl);
padding: 2rem; width: 90%; max-width: 520px;
box-shadow: var(--shadow-xl);
animation: fadeInScale 0.3s ease both;
max-height: 85vh; overflow-y: auto;
}
.form-modal h3 { margin-bottom: 1.5rem; font-size: 1.2rem; }
.form-actions { display: flex; gap: 0.75rem; justify-content: flex-end; margin-top: 1.5rem; }
/* ==================== Admin ==================== */
.admin-layout { display: flex; min-height: calc(100vh - 64px); }
.admin-sidebar {
width: 240px; background: var(--bg-card);
border-right: 1px solid var(--border);
padding: 1.5rem 0;
display: flex; flex-direction: column;
animation: slideInLeft 0.4s ease both;
}
.admin-nav-item {
display: flex; align-items: center; gap: 0.75rem;
padding: 0.75rem 1.5rem; font-size: 0.9rem; font-weight: 500;
color: var(--text-secondary); transition: var(--transition);
border-left: 3px solid transparent; cursor: pointer;
}
.admin-nav-item:hover { background: var(--bg-hover); color: var(--text); }
.admin-nav-item.active {
color: var(--primary); background: var(--primary-light);
border-left-color: var(--primary); font-weight: 600;
}
.admin-main {
flex: 1; padding: 2rem; overflow-y: auto;
animation: fadeInUp 0.5s ease both;
}
.admin-panel {
background: var(--bg-card); border-radius: var(--radius-lg);
box-shadow: var(--shadow); border: 1px solid var(--border);
overflow: hidden;
}
.admin-panel-header {
padding: 1.25rem 1.5rem; border-bottom: 1px solid var(--border);
display: flex; align-items: center; justify-content: space-between;
}
.admin-panel-header h3 { font-size: 1.1rem; }
.admin-panel-body { padding: 1.5rem; }
.admin-table { width: 100%; border-collapse: collapse; }
.admin-table th {
text-align: left; padding: 0.75rem 1rem;
font-size: 0.8rem; font-weight: 700; color: var(--text-muted);
text-transform: uppercase; letter-spacing: 0.05em;
background: var(--bg); border-bottom: 2px solid var(--border);
}
.admin-table td {
padding: 0.85rem 1rem; font-size: 0.9rem;
border-bottom: 1px solid var(--border-light);
}
.admin-table tbody tr { transition: var(--transition); }
.admin-table tbody tr:hover { background: var(--bg-hover); }
.admin-table .actions { display: flex; gap: 0.4rem; }
.badge {
display: inline-flex; padding: 0.2rem 0.6rem;
border-radius: 100px; font-size: 0.75rem; font-weight: 600;
}
.badge-admin { background: #fef3c7; color: #d97706; }
.badge-user { background: #e0e7ff; color: #4f46e5; }
.badge-category { background: #d1fae5; color: #059669; }
/* ==================== Admin Tabs ==================== */
.admin-header {
text-align: center; padding: 2rem 0 1.5rem;
animation: fadeInUp 0.5s ease both;
}
.admin-header h1 { font-size: 1.8rem; font-weight: 800; }
.admin-tabs {
display: flex; gap: 0.5rem; margin-bottom: 2rem;
background: var(--bg-card); padding: 0.4rem;
border-radius: var(--radius); border: 1px solid var(--border);
animation: fadeInUp 0.5s 0.05s ease both;
}
.admin-tab {
flex: 1; padding: 0.7rem 1rem; border-radius: var(--radius-sm);
font-size: 0.88rem; font-weight: 600; color: var(--text-secondary);
background: transparent; transition: all var(--transition);
cursor: pointer; border: none; text-align: center;
}
.admin-tab:hover { color: var(--text); background: var(--bg-hover); }
.admin-tab.active {
color: white;
background: linear-gradient(135deg, var(--primary), #8b5cf6);
box-shadow: var(--shadow-sm);
}
/* ==================== Admin Section ==================== */
.section-header {
display: flex; align-items: center; justify-content: space-between;
margin-bottom: 1.25rem;
}
.section-header h2 { font-size: 1.2rem; font-weight: 700; }
.data-table {
background: var(--bg-card); border-radius: var(--radius-lg);
box-shadow: var(--shadow); border: 1px solid var(--border);
overflow: hidden;
animation: fadeInUp 0.4s ease both;
}
.data-table table { width: 100%; border-collapse: collapse; }
.data-table thead { background: var(--bg); }
.data-table th {
text-align: left; padding: 0.75rem 1rem;
font-size: 0.8rem; font-weight: 700; color: var(--text-muted);
text-transform: uppercase; letter-spacing: 0.05em;
border-bottom: 2px solid var(--border);
}
.data-table td {
padding: 0.85rem 1rem; font-size: 0.9rem;
border-bottom: 1px solid var(--border-light);
color: var(--text);
}
.data-table tbody tr { transition: var(--transition); }
.data-table tbody tr:hover { background: var(--bg-hover); }
.data-table tbody tr:last-child td { border-bottom: none; }
.data-table .btn { white-space: nowrap; }
.color-dot {
display: inline-block; width: 14px; height: 14px;
border-radius: 50%; vertical-align: middle; margin-right: 4px;
border: 2px solid rgba(0,0,0,0.05);
}
/* ==================== Back Link ==================== */
.back-link {
display: inline-flex; align-items: center; gap: 0.3rem;
color: var(--primary); font-weight: 600; font-size: 0.9rem;
margin-bottom: 1.5rem; transition: var(--transition);
}
.back-link:hover { color: var(--primary-hover); transform: translateX(-3px); }
/* ==================== Note Detail Page ==================== */
.note-detail-header {
background: var(--bg-card); border-radius: var(--radius-lg);
padding: 2rem; border: 1px solid var(--border);
animation: fadeInUp 0.5s ease both;
}
.note-detail-header h1 { font-size: 1.6rem; font-weight: 800; margin-bottom: 1rem; }
.note-detail-meta {
display: flex; flex-wrap: wrap; gap: 1rem; align-items: center;
font-size: 0.85rem; color: var(--text-secondary);
}
.note-detail-meta span {
display: flex; align-items: center; gap: 0.3rem;
}
.note-detail-body {
background: var(--bg-card); border-radius: var(--radius-lg);
padding: 2rem; margin-top: 1rem;
border: 1px solid var(--border);
animation: fadeInUp 0.5s 0.1s ease both;
}
.note-detail-body p {
font-size: 1rem; line-height: 1.9; white-space: pre-wrap;
color: var(--text);
}
/* ==================== Comment Section (Page) ==================== */
.comment-section {
margin-top: 1.5rem;
background: var(--bg-card); border-radius: var(--radius-lg);
padding: 2rem; border: 1px solid var(--border);
animation: fadeInUp 0.5s 0.15s ease both;
}
.comment-section h3 {
font-size: 1.1rem; margin-bottom: 1rem;
display: flex; align-items: center; gap: 0.5rem;
}
.comment-section .comment-item {
background: var(--bg); margin-bottom: 0.75rem;
}
.comment-section .comment-item:last-child { margin-bottom: 0; }
.comment-meta { margin-bottom: 0.2rem; }
.comment-meta strong { font-size: 0.9rem; margin-right: 0.5rem; }
.comment-meta span { font-size: 0.75rem; color: var(--text-muted); }
.comment-form-wrapper {
margin-top: 1.5rem; padding-top: 1.5rem;
border-top: 1px solid var(--border);
}
.comment-form-wrapper h4 {
font-size: 1rem; margin-bottom: 0.75rem;
display: flex; align-items: center; gap: 0.4rem;
}
.comment-form-wrapper .comment-form {
display: flex; flex-direction: column; gap: 0.75rem;
}
.comment-form-wrapper .form-group { margin-bottom: 0; }
.comment-form-wrapper .form-group input,
.comment-form-wrapper .form-group textarea {
width: 100%; padding: 0.7rem 1rem;
border: 2px solid var(--border); border-radius: var(--radius-sm);
font-size: 0.9rem; transition: all var(--transition);
background: var(--bg);
}
.comment-form-wrapper .form-group input:focus,
.comment-form-wrapper .form-group textarea:focus {
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(99,102,241,0.1);
}
.comment-form-wrapper .btn-primary { align-self: flex-start; }
/* ==================== Toast ==================== */
.toast-container {
position: fixed; top: 1.5rem; right: 1.5rem; z-index: 9999;
display: flex; flex-direction: column; gap: 0.5rem;
}
.toast {
padding: 0.85rem 1.25rem; background: var(--bg-card);
border-radius: var(--radius); box-shadow: var(--shadow-lg);
font-size: 0.9rem; font-weight: 500;
animation: slideInRight 0.3s ease both;
display: flex; align-items: center; gap: 0.5rem;
border-left: 4px solid;
}
.toast-success { border-left-color: var(--success); }
.toast-error { border-left-color: var(--danger); }
.toast-info { border-left-color: var(--primary); }
/* ==================== Empty State ==================== */
.empty-state {
text-align: center; padding: 3rem 2rem; color: var(--text-muted);
}
.empty-state .empty-icon { font-size: 3.5rem; margin-bottom: 1rem; animation: float 3s ease-in-out infinite; }
.empty-state p { font-size: 0.95rem; }
/* ==================== Hero Section ==================== */
.hero {
text-align: center; padding: 3rem 2rem;
background: linear-gradient(135deg, #eef2ff 0%, #faf5ff 50%, #f0f9ff 100%);
border-radius: var(--radius-xl); margin-bottom: 2rem;
animation: fadeInUp 0.6s ease both;
}
.hero h1 {
font-size: 2.2rem; font-weight: 800;
background: linear-gradient(135deg, var(--primary), #8b5cf6);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
margin-bottom: 0.75rem;
}
.hero p { color: var(--text-secondary); font-size: 1.05rem; max-width: 500px; margin: 0 auto; }
/* ==================== Responsive ==================== */
@media (max-width: 768px) {
.main-content { flex-direction: column; }
.sidebar { width: 100%; position: static; }
.category-list { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.category-item a { padding: 0.4rem 0.7rem; font-size: 0.8rem; }
.notes-grid { grid-template-columns: 1fr; }
.admin-layout { flex-direction: column; }
.admin-sidebar {
width: 100%; flex-direction: row; overflow-x: auto;
padding: 0.75rem; border-right: none; border-bottom: 1px solid var(--border);
}
.admin-nav-item {
border-left: none; border-bottom: 2px solid transparent;
white-space: nowrap; padding: 0.5rem 1rem;
}
.admin-nav-item.active { border-left-color: transparent; border-bottom-color: var(--primary); }
.admin-main { padding: 1rem; }
.navbar { padding: 0 1rem; }
.container { padding: 1rem; }
.hero { padding: 2rem 1rem; }
.hero h1 { font-size: 1.6rem; }
.search-box input { width: 180px; }
.search-box input:focus { width: 220px; }
}
/* Scrollbar */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 100px; }
::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
@@ -0,0 +1,10 @@
<?php
// 数据库连接配置
$dbhost = "127.0.0.1";
$dbuser = "root";
$dbpass = "123456";
$dbname = "site03";
$dbport = "3306";
$dbcharset = "utf8";
$conn = mysqli_connect($dbhost, $dbuser, $dbpass, $dbname, $dbport, $dbcharset) or exit('连接失败');
?>
@@ -0,0 +1,57 @@
<?php
// 接受数据,并且提取出action
$data = json_decode(file_get_contents('php://input'), true);
$action = $data['action'] ?? '';
// 处理与用户相关的操作
include 'db.php';
// 如果数据库连接失败,则返回错误信息
/** @var mysqli $conn */
if (!isset($conn)) {
echo json_encode(['code' => 10004, 'status' => 'error', 'message' => '数据库连接失败']);
exit;
}
// 测试用户名是否已存在
function testUsername(mysqli $conn, string $username): bool
{
$sql = "select * from users where username = '$username'"; // sql注入风险
$result = mysqli_query($conn, $sql);
if (mysqli_num_rows($result) > 0) {
return true;
} else {
return false;
}
}
// 如果action是register,则注册用户
if ($action == 'register') {
$username = $data['username'];
$password = $data['password'];
$email = $data['email'];
if (testUsername($conn, $username)) {
echo json_encode(['code' => 10001, 'status' => 'error', 'message' => '用户名已存在']);
} else {
$sql = "insert into users (username, password, email) values ('$username', '$password', '$email')";
$result = mysqli_query($conn, $sql);
if ($result) {
echo json_encode(['code' => 0, 'status' => 'success', 'message' => '注册成功']);
} else {
echo json_encode(['code' => 10002, 'status' => 'error', 'message' => '注册失败']);
}
}
}
// login的逻辑
if ($action == 'login') {
$username = $data['username'];
$password = $data['password'];
$sql = "select * from users where username = '$username' and password = '$password'";
$result = mysqli_query($conn, $sql);
if (mysqli_num_rows($result) > 0) {
echo json_encode(['code' => 0, 'status' => 'success', 'message' => '登录成功']);
} else {
echo json_encode(['code' => 10003, 'status' => 'error', 'message' => '用户名或密码错误']);
}
}
@@ -0,0 +1,126 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>笔记小站 - 发现精彩内容</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<nav class="navbar">
<a class="navbar-brand" href="index.html">
<span class="logo-icon">📝</span>
笔记小站
</a>
<div class="navbar-actions" id="navbarActions"></div>
</nav>
<div class="container">
<!-- Hero -->
<div class="hero">
<h1>📝 发现精彩内容</h1>
<p>分享技术、记录生活、沉淀思考——这里有你想看的一切</p>
</div>
<div class="main-content">
<!-- Sidebar: Categories -->
<aside class="sidebar">
<div class="category-panel">
<div class="category-panel-header">📂 笔记分类</div>
<ul class="category-list" id="categoryList"></ul>
</div>
</aside>
<!-- Content: Notes Grid -->
<main class="content-area">
<div class="content-header">
<h2 id="sectionTitle">📋 全部笔记</h2>
<div class="search-box">
<span class="search-icon">🔍</span>
<input type="text" id="searchInput" placeholder="搜索笔记..." value="">
</div>
</div>
<div class="notes-grid" id="notesGrid"></div>
</main>
</div>
</div>
<div class="toast-container" id="toastContainer"></div>
<script src="js/data.js"></script>
<script src="js/common.js"></script>
<script>
(function () {
var catId = getUrlParam('cat') ? parseInt(getUrlParam('cat')) : null;
function renderCategoryList() {
var counts = {};
DATA.notes.forEach(function (n) { counts[n.categoryId] = (counts[n.categoryId] || 0) + 1; });
function buildLink(id, icon, name, count) {
var active = catId === id ? ' active' : '';
return '<li class="category-item"><a class="' + active + '" href="index.html' + (id ? '?cat=' + id : '') + '">' +
'<span>' + icon + ' ' + name + '</span><span class="count">' + count + '</span></a></li>';
}
var html = buildLink(null, '📋', '全部笔记', DATA.notes.length);
DATA.categories.forEach(function (c) {
html += buildLink(c.id, c.icon, c.name, counts[c.id] || 0);
});
document.getElementById('categoryList').innerHTML = html;
}
function renderNotes() {
var cat = catId ? DATA.categories.find(function (c) { return c.id === catId; }) : null;
var q = (document.getElementById('searchInput').value || '').toLowerCase();
var notes = DATA.notes.filter(function (n) {
if (catId && n.categoryId !== catId) return false;
if (q && n.title.toLowerCase().indexOf(q) === -1 && n.excerpt.toLowerCase().indexOf(q) === -1 && n.content.toLowerCase().indexOf(q) === -1) return false;
return true;
});
document.getElementById('sectionTitle').textContent = cat ? cat.icon + ' ' + cat.name : '📋 全部笔记';
if (notes.length === 0) {
document.getElementById('notesGrid').innerHTML = emptyState('📭', '暂无笔记内容');
return;
}
var delays = ['anim-fade-up-d1', 'anim-fade-up-d2', 'anim-fade-up-d3', 'anim-fade-up-d4', 'anim-fade-up-d5', 'anim-fade-up-d6'];
document.getElementById('notesGrid').innerHTML = notes.map(function (n, i) {
var c = DATA.categories.find(function (x) { return x.id === n.categoryId; });
var cc = DATA.comments.filter(function (x) { return x.noteId === n.id; }).length;
return '<a href="note.html?id=' + n.id + '" class="note-card anim-fade-up ' + delays[i % 6] + '">' +
categoryBadge(c) +
'<h3 class="note-title">' + n.title + '</h3>' +
'<p class="note-excerpt">' + n.excerpt + '</p>' +
'<div class="note-meta"><span>👤 ' + n.authorName + '</span><span>👁️ ' + n.views + '</span><span>💬 ' + cc + '</span><span>❤️ ' + n.likes + '</span></div>' +
'</a>';
}).join('');
}
renderCategoryList();
renderNotes();
var searchInput = document.getElementById('searchInput');
if (searchInput) {
var timer;
searchInput.addEventListener('input', function () {
clearTimeout(timer);
timer = setTimeout(renderNotes, 200);
});
searchInput.addEventListener('keydown', function (e) {
if (e.key === 'Escape') { this.value = ''; renderNotes(); }
});
}
})();
</script>
</body>
</html>
@@ -0,0 +1,61 @@
/* ==================== Shared Utilities ==================== */
/* URL parameter helper */
function getUrlParam(name) {
const url = new URL(window.location.href);
return url.searchParams.get(name);
}
/* Toast notification */
function showToast(msg, type) {
type = type || 'info';
var icons = { success: '✅', error: '❌', info: '️' };
var container = document.getElementById('toastContainer');
if (!container) { container = document.createElement('div'); container.id = 'toastContainer'; container.className = 'toast-container'; document.body.appendChild(container); }
var t = document.createElement('div');
t.className = 'toast toast-' + type;
t.innerHTML = '<span>' + (icons[type] || '') + '</span> ' + msg;
container.appendChild(t);
setTimeout(function () { t.remove(); }, 3000);
}
/* Render shared navbar */
function renderNavbar() {
var nav = document.getElementById('navbarActions');
if (!nav) return;
nav.innerHTML =
'<a href="index.html" class="btn btn-ghost">🏠 首页</a>' +
'<a href="admin.html" class="btn btn-outline btn-sm">⚙️ 后台管理</a>' +
'<a href="register.html" class="btn btn-ghost btn-sm">✍️ 注册</a>' +
'<a href="login.html" class="btn btn-primary btn-sm">🔑 登录</a>';
}
/* Category badge helper */
function categoryBadge(cat) {
if (!cat) return '';
return '<span class="note-category" style="background:' + cat.color + '15;color:' + cat.color + '">' + cat.icon + ' ' + cat.name + '</span>';
}
/* Empty state helper */
function emptyState(icon, text) {
return '<div class="empty-state"><div class="empty-icon">' + icon + '</div><p>' + text + '</p></div>';
}
/* Modal helpers */
function renderModal(title, content) {
var div = document.createElement('div');
div.className = 'modal-overlay';
div.onclick = function (e) { if (e.target === div) closeModal(); };
div.innerHTML = '<div class="form-modal" onclick="event.stopPropagation()"><h3>' + title + '</h3>' + content + '</div>';
document.body.appendChild(div);
}
function closeModal() {
var o = document.querySelector('.modal-overlay');
if (o) o.remove();
}
/* ==================== Bootstrap on every page ==================== */
document.addEventListener('DOMContentLoaded', function () {
renderNavbar();
});
@@ -0,0 +1,37 @@
/* ==================== Mock Data Store ==================== */
const DATA = {
users: [
{ id: 1, username: 'admin', password: 'admin123', nickname: '管理员', role: 'admin', avatar: '👑', createdAt: '2024-01-01' },
{ id: 2, username: 'xiaoming', password: '123456', nickname: '小明', role: 'user', avatar: '😊', createdAt: '2024-03-15' },
{ id: 3, username: 'xiaohong', password: '123456', nickname: '小红', role: 'user', avatar: '🌸', createdAt: '2024-05-20' },
],
categories: [
{ id: 1, name: '技术笔记', icon: '💻', color: '#6366f1' },
{ id: 2, name: '生活随笔', icon: '🌿', color: '#10b981' },
{ id: 3, name: '读书心得', icon: '📚', color: '#f59e0b' },
{ id: 4, name: '旅行记录', icon: '✈️', color: '#06b6d4' },
],
notes: [
{ id: 1, categoryId: 1, title: 'JavaScript 异步编程完全指南', excerpt: '掌握 Promise、async/await 和异步错误处理,让你的 JavaScript 代码更加优雅高效。', content: '在现代 JavaScript 开发中,异步编程是每个开发者必须掌握的技能。\n\n## Promise\nPromise 是 ES6 引入的一种异步编程解决方案。它代表一个异步操作的最终完成或失败。\n\n## async/await\nasync/await 是 ES2017 引入的语法糖,让异步代码看起来像同步代码。使用 try/catch 来处理异步错误比 .catch() 链更清晰易读。\n\n## 并发控制\n使用 Promise.all() 可以同时发起多个请求,显著提升性能。但需要注意控制并发数量。', authorId: 1, authorName: '管理员', views: 1520, likes: 89, createdAt: '2025-08-10' },
{ id: 2, categoryId: 1, title: 'CSS Grid 布局实战技巧', excerpt: '从基础到进阶,深入了解 CSS Grid 布局系统,掌握现代网页布局的核心技术。', content: 'CSS Grid 是二维布局系统,让我们能同时控制行和列。\n\n## 容器属性\n- grid-template-columns:定义列\n- grid-template-rows:定义行\n- gap:行列间距\n- grid-template-areas:命名区域\n\n## 项目属性\n- grid-column:控制列跨度\n- grid-row:控制行跨度\n\n使用 Grid 可以轻松实现自适应布局,告别传统 hack 方案。', authorId: 1, authorName: '管理员', views: 980, likes: 56, createdAt: '2025-08-08' },
{ id: 3, categoryId: 2, title: '周末烘焙日记:自制法式可颂', excerpt: '金黄酥脆、层次分明的法式可颂,一次成功的家庭烘焙体验全记录。', content: '这周末终于尝试了心心念念的法式可颂!\n\n## 材料准备\n- 高筋面粉 250g\n- 黄油 150g(裹入用)\n- 酵母 5g / 糖 25g / 盐 5g / 温水 130ml\n\n## 步骤\n1. 和面:将面粉、糖、盐、酵母混合,加入温水揉成光滑面团\n2. 一次发酵:室温发酵 1 小时至 2 倍大\n3. 裹黄油:将面团擀开,放入黄油片,进行四折\n4. 冷藏松弛:每次折叠后冷藏 30 分钟\n5. 成型烘烤:切成三角形卷起,200°C 烤 18 分钟\n\n成品金黄酥脆,层次分明,配上一杯拿铁,完美的周末下午!', authorId: 2, authorName: '小明', views: 756, likes: 134, createdAt: '2025-08-06' },
{ id: 4, categoryId: 3, title: '《人类简史》读书笔记', excerpt: '认知革命、农业陷阱与科学革命——重新审视人类文明的演进与代价。', content: '尤瓦尔·赫拉利的《人类简史》是一本让人重新思考人类文明的书。\n\n## 认知革命\n大约 7 万年前,智人出现了新的思维和沟通方式。语言不仅能描述现实,还能创造虚构的故事,让大规模合作成为可能。\n\n## 农业革命\n农业革命并非人类的进步,而是一个陷阱。农民的生活比狩猎采集者更辛苦,但人口增长让人无法回头。\n\n## 科学革命\n现代科学与帝国、资本主义的结合,创造了今天的世界。科学的核心是"承认无知",正是这种谦逊推动了进步。\n\n## 感想\n我们所谓的进步,到底让谁受益?这是个值得深思的问题。', authorId: 3, authorName: '小红', views: 2340, likes: 267, createdAt: '2025-08-04' },
{ id: 5, categoryId: 1, title: 'React Hooks 深入解析', excerpt: '从 useState 到自定义 Hook,全面解析 React Hooks 的使用场景与最佳实践。', content: 'React Hooks 是 React 16.8 引入的革命性特性。\n\n## useState\n最基础的 Hook,用于在函数组件中添加状态。\n\n## useEffect\n处理副作用的神器。可以用来做数据获取、订阅、DOM 操作等。记得返回清理函数。\n\n## useCallback & useMemo\n性能优化的关键。useCallback 缓存函数,useMemo 缓存计算结果。\n\n## 自定义 Hook\n封装可复用的逻辑,是 Hooks 最强大的特性之一。', authorId: 1, authorName: '管理员', views: 1890, likes: 145, createdAt: '2025-08-02' },
{ id: 6, categoryId: 4, title: '京都红叶季旅行攻略', excerpt: '岚山竹林、清水寺夜枫、永观堂红叶——京都最美的秋日体验全记录。', content: '十一月的京都,是一年中最美的季节。\n\n## 推荐赏枫地点\n### 岚山\n天龙寺的庭院和竹林小径是最佳拍照地,建议清晨前往避开人群。\n\n### 清水寺\n站在清水舞台上俯瞰京都市区,红叶与古建筑交相辉映。\n\n### 永观堂\n被誉为"红叶的永观堂",夜枫尤其惊艳。\n\n## 交通建议\n购买京都巴士一日券,500日元即可无限次乘坐。\n\n## 美食推荐\n- 抹茶甜品:中村藤吉\n- 汤豆腐:南禅寺顺正\n- 拉面:一兰拉面', authorId: 2, authorName: '小明', views: 2100, likes: 320, createdAt: '2025-07-28' },
{ id: 7, categoryId: 2, title: '阳台花园养成记', excerpt: '在城市阳台打造迷你花园,多肉、薄荷、月季——用绿植治愈生活。', content: '在城市的钢筋水泥中,拥有一个属于自己的小花园,是治愈生活最好的方式。\n\n## 适合阳台的植物\n- 多肉植物:耐旱好养,品种丰富\n- 薄荷:清香怡人,还能泡茶\n- 月季:阳台也能开花不断\n- 绿萝:垂吊种植,不占空间\n\n## 养护心得\n每天早晚浇水时,观察植物的变化是最治愈的时刻。看着新芽冒出、花苞绽放,生活的小确幸就在这些细微之处。', authorId: 3, authorName: '小红', views: 645, likes: 89, createdAt: '2025-07-25' },
{ id: 8, categoryId: 3, title: '《设计心理学》核心要点', excerpt: '可视性、反馈、映射——六个核心原则,打造用户友好的产品设计。', content: '唐纳德·诺曼的《设计心理学》是每个产品设计师的必读书。\n\n## 六大核心原则\n1. 可视性 - 功能要能被看见\n2. 反馈 - 每个操作都要有反馈\n3. 约束 - 物理、文化、逻辑约束\n4. 映射 - 控制与效果的关系\n5. 一致性 - 统一的设计语言\n6. 容错性 - 允许犯错并方便恢复\n\n## 人本设计\n好的设计应该是"不可见"的——它如此自然,以至于用户感觉不到设计的存在。', authorId: 1, authorName: '管理员', views: 1200, likes: 178, createdAt: '2025-07-20' },
],
comments: [
{ id: 1, noteId: 1, userId: 2, userName: '小明', userAvatar: '😊', content: '写得太清晰了!Promise 那部分解决了我的疑惑。', createdAt: '2025-08-11 14:30' },
{ id: 2, noteId: 1, userId: 3, userName: '小红', userAvatar: '🌸', content: 'async/await 示例很好理解,期待更多这类文章!', createdAt: '2025-08-11 16:00' },
{ id: 3, noteId: 1, userId: 1, userName: '管理员', userAvatar: '👑', content: '感谢支持!后续会出更多异步相关的文章。', createdAt: '2025-08-11 17:20' },
{ id: 4, noteId: 3, userId: 1, userName: '管理员', userAvatar: '👑', content: '看着就很好吃,周末我也要试试!', createdAt: '2025-08-07 09:00' },
{ id: 5, noteId: 3, userId: 3, userName: '小红', userAvatar: '🌸', content: '烤箱温度有什么需要注意的吗?', createdAt: '2025-08-07 10:30' },
{ id: 6, noteId: 3, userId: 2, userName: '小明', userAvatar: '😊', content: '每个烤箱脾气不同,建议买个烤箱温度计。', createdAt: '2025-08-07 11:00' },
{ id: 7, noteId: 4, userId: 2, userName: '小明', userAvatar: '😊', content: '这本书我去年也读了,农业革命那章确实颠覆认知。', createdAt: '2025-08-05 15:00' },
{ id: 8, noteId: 4, userId: 3, userName: '小红', userAvatar: '🌸', content: '接着看《未来简史》吧,同样精彩!', createdAt: '2025-08-05 16:30' },
{ id: 9, noteId: 6, userId: 3, userName: '小红', userAvatar: '🌸', content: '太美了!今年秋天一定要去一次。', createdAt: '2025-07-30 20:00' },
{ id: 10, noteId: 6, userId: 1, userName: '管理员', userAvatar: '👑', content: '岚山的竹林确实值得早起去,体验完全不一样。', createdAt: '2025-07-31 08:00' },
{ id: 11, noteId: 8, userId: 2, userName: '小明', userAvatar: '😊', content: '总结得很到位,设计心理学确实值得反复读。', createdAt: '2025-07-22 11:00' },
],
};
@@ -0,0 +1,83 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登录 - 笔记小站</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<nav class="navbar">
<a class="navbar-brand" href="index.html">
<span class="logo-icon">📝</span>
笔记小站
</a>
<div class="navbar-actions" id="navbarActions"></div>
</nav>
<div class="modal-overlay"
style="position:relative;background:var(--bg);min-height:calc(100vh - 64px);display:flex;align-items:center;justify-content:center;">
<div class="login-modal anim-scale-in" style="position:relative;">
<h2>🔑 欢迎登录</h2>
<p class="subtitle">登录后即可使用完整功能</p>
<div class="form-group">
<label>用户名</label>
<input type="text" id="loginUsername" placeholder="请输入用户名" autofocus>
</div>
<div class="form-group">
<label>密码</label>
<input type="password" id="loginPassword" placeholder="请输入密码">
</div>
<button class="btn btn-primary" onclick="doLogin()">登录</button>
<div class="login-hint" style="margin-top:0.5rem;">
没有账号?<a href="register.html" style="color:var(--primary);text-decoration:none;font-weight:600;">立即注册</a>
</div>
</div>
</div>
<div class="toast-container" id="toastContainer"></div>
<script src="js/common.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function () { renderNavbar(); });
window.doLogin = function () {
var username = document.getElementById('loginUsername').value.trim();
var password = document.getElementById('loginPassword').value.trim();
if (!username || !password) {
showToast('请填写用户名和密码', 'error'); return;
}
fetch('/function/user.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'login',
username: username,
password: password
})
}).then(res => res.json()).then(data => {
if (data.code == 0) {
showToast('登录成功', 'success');
setTimeout(() => {
window.location.href = '/';
}, 1000);
return
}
showToast(data.message, 'error'); return;
}).catch(err => {
showToast(err.message, 'error');
});
};
['loginUsername', 'loginPassword'].forEach(function (id) {
var el = document.getElementById(id);
if (el) el.addEventListener('keydown', function (e) { if (e.key === 'Enter') doLogin(); });
});
</script>
</body>
</html>
@@ -0,0 +1,106 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>笔记详情 - 笔记小站</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<nav class="navbar">
<a class="navbar-brand" href="index.html"><span class="logo-icon">📝</span> 笔记小站</a>
<div class="navbar-actions" id="navbarActions"></div>
</nav>
<div class="container" id="app">
<a href="index.html" class="back-link">&larr; 返回笔记列表</a>
<div id="noteDetail"></div>
</div>
<div class="toast-container" id="toastContainer"></div>
<script src="js/data.js"></script>
<script src="js/common.js"></script>
<script>
(function () {
var id = parseInt(getUrlParam('id'));
var note = null;
for (var i = 0; i < DATA.notes.length; i++) {
if (DATA.notes[i].id === id) { note = DATA.notes[i]; break; }
}
if (!note) {
document.getElementById('noteDetail').innerHTML = emptyState('😕', '笔记未找到');
return;
}
document.title = note.title + ' - 笔记小站';
var cat = findById(DATA.categories, note.categoryId);
var author = findById(DATA.users, note.userId);
var comments = DATA.comments.filter(function (c) { return c.noteId === id; });
var h = '';
h += '<div class="note-detail-header">';
h += '<h1>' + note.title + '</h1>';
h += '<div class="note-detail-meta">';
h += categoryBadge(cat);
h += '<span>👤 ' + (author ? author.nickname : '未知作者') + '</span>';
h += '<span>📅 ' + note.createdAt + '</span>';
h += '</div>';
h += '</div>';
h += '<div class="note-detail-body"><p>' + note.content + '</p></div>';
h += '<div class="comment-section">';
h += '<h3>💬 评论(' + comments.length + '</h3>';
for (var j = 0; j < comments.length; j++) {
var c = comments[j];
var cu = findById(DATA.users, c.userId);
h += '<div class="comment-item">';
h += '<div class="comment-avatar">' + (cu ? cu.avatar : '👤') + '</div>';
h += '<div class="comment-body">';
h += '<div class="comment-meta"><strong>' + (cu ? cu.nickname : c.author || '游客') + '</strong> <span>' + c.createdAt + '</span></div>';
h += '<p>' + c.content + '</p>';
h += '</div></div>';
}
h += '<div class="comment-form-wrapper">';
h += '<h4>📝 发表评论</h4>';
h += '<div class="comment-form">';
h += '<div class="form-group"><input type="text" id="commentNick" placeholder="你的昵称" maxlength="20"></div>';
h += '<div class="form-group"><textarea id="commentContent" placeholder="写下你的想法..." rows="3"></textarea></div>';
h += '<button class="btn btn-primary" onclick="submitComment()">发布评论</button>';
h += '</div></div>';
h += '</div>';
document.getElementById('noteDetail').innerHTML = h;
window.submitComment = function () {
var nick = document.getElementById('commentNick').value.trim();
var content = document.getElementById('commentContent').value.trim();
if (!nick) { showToast('请填写昵称', 'error'); return; }
if (!content) { showToast('请输入评论内容', 'error'); return; }
var guestUser = { id: 99, nickname: nick, avatar: '💬', username: 'guest' };
DATA.comments.push({
id: DATA.comments.length + 1,
noteId: note.id,
userId: 99,
author: nick,
content: content,
createdAt: new Date().toISOString().slice(0, 16).replace('T', ' ')
});
showToast('评论发表成功', 'success');
document.getElementById('commentContent').value = '';
document.getElementById('commentNick').value = '';
setTimeout(function () { window.location.reload(); }, 600);
};
function findById(arr, id) {
for (var i = 0; i < arr.length; i++) { if (arr[i].id === id) return arr[i]; }
return null;
}
})();
</script>
</body>
</html>
@@ -0,0 +1,104 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>注册 - 笔记小站</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<nav class="navbar">
<a class="navbar-brand" href="index.html">
<span class="logo-icon">📝</span>
笔记小站
</a>
<div class="navbar-actions" id="navbarActions"></div>
</nav>
<div class="modal-overlay" style="position:relative;background:var(--bg);min-height:calc(100vh - 64px);display:flex;align-items:center;justify-content:center;">
<div class="login-modal anim-scale-in" style="position:relative;max-width:440px;">
<h2>✍️ 注册账号</h2>
<p class="subtitle">创建账号,开始你的笔记之旅</p>
<div class="form-group">
<label>用户名</label>
<input type="text" id="regUsername" placeholder="请输入用户名" autofocus>
</div>
<div class="form-group">
<label>邮箱</label>
<input type="email" id="regEmail" placeholder="请输入邮箱地址">
</div>
<div class="form-group">
<label>密码</label>
<input type="password" id="regPassword" placeholder="请输入密码(至少6位)">
</div>
<div class="form-group">
<label>确认密码</label>
<input type="password" id="regPassword2" placeholder="请再次输入密码">
</div>
<button class="btn btn-primary" onclick="doRegister()">注册</button>
<div class="login-hint">
已有账号?<a href="login.html" style="color:var(--primary);text-decoration:none;font-weight:600;">立即登录</a>
</div>
</div>
</div>
<div class="toast-container" id="toastContainer"></div>
<script src="js/common.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function () { renderNavbar(); });
window.doRegister = function () {
var username = document.getElementById('regUsername').value.trim();
var email = document.getElementById('regEmail').value.trim();
var password = document.getElementById('regPassword').value.trim();
var password2 = document.getElementById('regPassword2').value.trim();
if (!username || !email || !password || !password2) {
showToast('请填写所有字段', 'error');
return;
}
if (password.length < 6) {
showToast('密码至少需要 6 位', 'error');
return;
}
if (password !== password2) {
showToast('两次密码输入不一致', 'error');
return;
}
// fetch是一个异步函数,用于发送HTTP请求
fetch('/function/user.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
'action': 'register',
'username': username,
'email': email,
'password': password
})
})
.then(res => res.json())
.then(data => {
if (data.code === 0) {
showToast('注册成功', 'success');
setTimeout(() => {
location.href = 'login.html';
}, 1000);
} else {
showToast(data.message, 'error');
}
})
.catch(err => {
showToast(err.message, 'error');
});
};
['regUsername','regEmail','regPassword','regPassword2'].forEach(function (id) {
var el = document.getElementById(id);
if (el) el.addEventListener('keydown', function (e) { if (e.key === 'Enter') doRegister(); });
});
</script>
</body>
</html>
@@ -0,0 +1,220 @@
- 技术栈
- 原生HTML+CSS+JS
- php8.0
- Mysql5.7
- 界面设计
- 站点主页无需登录,就可以进行内容的浏览
- 点击登录,出现登录页面,管理员登录后进入后台管理,普通用户继续浏览主页
- 用户管理
- 需要支持用户登录,用户分为管理员和普通用户
- 管理员支持对笔记、分类、评论、用户有完整的权限
- 普通用户只有查看笔记、分类、评论和发表评论的权限
- 分类管理
- 笔记管理
- 评论管理
---
## 数据库设计
### 数据库信息
- 数据库名: `site03`
- 字符集: `utf8mb4`
- 排序规则: `utf8mb4_unicode_ci`
```sql
CREATE DATABASE IF NOT EXISTS `site03`
DEFAULT CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE `site03`;
```
---
### 表结构
#### 1. users — 用户表
| 字段 | 类型 | 必填 | 说明 |
| ---------- | ---------------- | ---- | --------------------------------- |
| id | INT UNSIGNED | 是 | 主键,自增 |
| username | VARCHAR(50) | 是 | 用户名,唯一 |
| password | VARCHAR(255) | 是 | 密码哈希(password_hash |
| role | ENUM('admin','user') | 是 | 角色:管理员 / 普通用户,默认 user |
| email | VARCHAR(100) | 否 | 邮箱 |
| avatar | VARCHAR(255) | 否 | 头像路径 |
| status | TINYINT(1) | 是 | 状态:0=禁用, 1=启用,默认 1 |
| created_at | DATETIME | 是 | 创建时间 |
| updated_at | DATETIME | 是 | 更新时间 |
```sql
CREATE TABLE `users` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`username` VARCHAR(50) NOT NULL,
`password` VARCHAR(255) NOT NULL,
`role` ENUM('admin','user') NOT NULL DEFAULT 'user',
`email` VARCHAR(100) DEFAULT NULL,
`avatar` VARCHAR(255) DEFAULT NULL,
`status` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '0=禁用 1=启用',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```
---
#### 2. categories — 分类表
| 字段 | 类型 | 必填 | 说明 |
| ----------- | ---------------- | ---- | ---------------------------- |
| id | INT UNSIGNED | 是 | 主键,自增 |
| name | VARCHAR(50) | 是 | 分类名称,唯一 |
| slug | VARCHAR(50) | 是 | URL 别名,唯一 |
| description | TEXT | 否 | 分类描述 |
| sort_order | INT UNSIGNED | 是 | 排序权重,默认 0 |
| created_at | DATETIME | 是 | 创建时间 |
| updated_at | DATETIME | 是 | 更新时间 |
```sql
CREATE TABLE `categories` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`name` VARCHAR(50) NOT NULL,
`slug` VARCHAR(50) NOT NULL,
`description` TEXT DEFAULT NULL,
`sort_order` INT UNSIGNED NOT NULL DEFAULT 0,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_name` (`name`),
UNIQUE KEY `uk_slug` (`slug`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```
---
#### 3. notes — 笔记表
| 字段 | 类型 | 必填 | 说明 |
| ----------- | --------------------- | ---- | ----------------------------------- |
| id | INT UNSIGNED | 是 | 主键,自增 |
| title | VARCHAR(255) | 是 | 笔记标题 |
| content | LONGTEXT | 是 | 笔记正文 |
| summary | VARCHAR(500) | 否 | 摘要 |
| category_id | INT UNSIGNED | 是 | 所属分类,外键 |
| user_id | INT UNSIGNED | 是 | 作者,外键 |
| status | ENUM('draft','published') | 是 | 状态:草稿 / 已发布,默认 draft |
| view_count | INT UNSIGNED | 是 | 浏览次数,默认 0 |
| created_at | DATETIME | 是 | 创建时间 |
| updated_at | DATETIME | 是 | 更新时间 |
```sql
CREATE TABLE `notes` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`title` VARCHAR(255) NOT NULL,
`content` LONGTEXT NOT NULL,
`summary` VARCHAR(500) DEFAULT NULL,
`category_id` INT UNSIGNED NOT NULL,
`user_id` INT UNSIGNED NOT NULL,
`status` ENUM('draft','published') NOT NULL DEFAULT 'draft',
`view_count` INT UNSIGNED NOT NULL DEFAULT 0,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_category_id` (`category_id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_status` (`status`),
CONSTRAINT `fk_notes_category` FOREIGN KEY (`category_id`) REFERENCES `categories`(`id`) ON DELETE RESTRICT,
CONSTRAINT `fk_notes_user` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```
---
#### 4. comments — 评论表
| 字段 | 类型 | 必填 | 说明 |
| ---------- | ----------------------------------- | ---- | --------------------------------- |
| id | INT UNSIGNED | 是 | 主键,自增 |
| note_id | INT UNSIGNED | 是 | 所属笔记,外键 |
| user_id | INT UNSIGNED | 是 | 评论人,外键 |
| parent_id | INT UNSIGNED | 否 | 父评论 ID(支持回复),外键 |
| content | TEXT | 是 | 评论内容 |
| status | ENUM('pending','approved','rejected') | 是 | 审核状态,默认 pending |
| created_at | DATETIME | 是 | 创建时间 |
| updated_at | DATETIME | 是 | 更新时间 |
```sql
CREATE TABLE `comments` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`note_id` INT UNSIGNED NOT NULL,
`user_id` INT UNSIGNED NOT NULL,
`parent_id` INT UNSIGNED DEFAULT NULL,
`content` TEXT NOT NULL,
`status` ENUM('pending','approved','rejected') NOT NULL DEFAULT 'pending',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_note_id` (`note_id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_parent_id` (`parent_id`),
CONSTRAINT `fk_comments_note` FOREIGN KEY (`note_id`) REFERENCES `notes`(`id`) ON DELETE CASCADE,
CONSTRAINT `fk_comments_user` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE RESTRICT,
CONSTRAINT `fk_comments_parent` FOREIGN KEY (`parent_id`) REFERENCES `comments`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```
---
### 索引说明
| 表 | 索引名 | 字段 | 类型 | 用途 |
| ---------- | ------------------- | ----------- | ------ | -------------------------- |
| users | PRIMARY | id | 主键 | 用户唯一标识 |
| users | uk_username | username | 唯一 | 用户名唯一约束 |
| categories | PRIMARY | id | 主键 | 分类唯一标识 |
| categories | uk_name | name | 唯一 | 分类名唯一约束 |
| categories | uk_slug | slug | 唯一 | URL 别名唯一约束 |
| notes | PRIMARY | id | 主键 | 笔记唯一标识 |
| notes | idx_category_id | category_id | 普通 | 按分类查询笔记 |
| notes | idx_user_id | user_id | 普通 | 按作者查询笔记 |
| notes | idx_status | status | 普通 | 按发布状态筛选 |
| comments | PRIMARY | id | 主键 | 评论唯一标识 |
| comments | idx_note_id | note_id | 普通 | 按笔记查询评论 |
| comments | idx_user_id | user_id | 普通 | 按用户查询评论 |
| comments | idx_parent_id | parent_id | 普通 | 查询回复链 |
---
### 外键约束说明
- `notes.category_id``categories.id`: RESTRICT,存在笔记时不可删除分类
- `notes.user_id``users.id`: RESTRICT,存在笔记时不可删除用户
- `comments.note_id``notes.id`: CASCADE,删除笔记时级联删除评论
- `comments.user_id``users.id`: RESTRICT,存在评论时不可删除用户
- `comments.parent_id``comments.id`: CASCADE,删除父评论时级联删除子回复
---
### 种子数据
```sql
-- 默认管理员: admin / admin123
INSERT INTO `users` (`username`, `password`, `role`) VALUES
('admin', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'admin');
-- 示例分类
INSERT INTO `categories` (`name`, `slug`, `description`, `sort_order`) VALUES
('技术笔记', 'tech', '编程与技术相关笔记', 1),
('生活随笔', 'life', '日常生活与感悟', 2),
('读书笔记', 'reading', '书籍阅读与书评', 3);
```
> 密码 `admin123` 使用 PHP `password_hash('admin123', PASSWORD_DEFAULT)` 生成,实际部署时请更换。
> 详细搭建说明参见 [spec.md](#) 附录。