全量上传

This commit is contained in:
18796357645 2025-06-24 11:42:12 +08:00
parent 76dc6c4a85
commit ccff2bdf98
40 changed files with 6243 additions and 3 deletions

3
.cursorignore Normal file
View File

@ -0,0 +1,3 @@
# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv)
.idea/
node_modules/

2
.gitignore vendored
View File

@ -7,7 +7,7 @@ yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
.idea
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

183
PAGINATION_README.md Normal file
View File

@ -0,0 +1,183 @@
# 分页功能说明
## 概述
博客系统已集成完整的分页功能,支持以下页面的分页显示:
- **首页博客文章列表** - 每页显示5篇文章
- **管理员后台文章管理** - 每页显示10篇文章
- **管理员后台用户管理** - 每页显示10个用户
- **管理员后台评论管理** - 每页显示10条评论
## 功能特性
### 1. 通用分页组件
- **文件位置**: `views/components/pagination.ejs`
- **功能**: 可复用的分页UI组件
- **特性**:
- 响应式设计
- 智能页码显示最多显示5个页码
- 上一页/下一页导航
- 首页/末页快速跳转
- 省略号显示
- 当前页高亮
- 分页信息显示
### 2. 分页工具函数
- **文件位置**: `utils/pagination.js`
- **主要函数**:
- `getPaginationInfo()` - 计算分页信息
- `paginateQuery()` - 执行分页查询
- `addPaginationToQuery()` - 为查询添加分页
### 3. 分页样式
- **设计风格**: 现代化Bootstrap风格
- **颜色主题**: 蓝色主题(#3498db
- **交互效果**: 悬停动画、过渡效果
- **响应式**: 支持移动端适配
## 使用方法
### 1. 后端路由中使用分页
```javascript
const { paginateQuery } = require('./utils/pagination');
// 在路由中使用
router.get('/posts', async (req, res) => {
const page = parseInt(req.query.page) || 1;
const limit = 10;
const result = await paginateQuery(
Post, // Mongoose模型
{}, // 查询条件
{ sort: { createdAt: -1 } }, // 查询选项
page, // 当前页码
limit // 每页数量
);
res.render('template', {
posts: result.data,
pagination: result.pagination,
baseUrl: '/posts',
query: {}
});
});
```
### 2. 前端模板中使用分页组件
```ejs
<!-- 在模板中引入分页组件 -->
<%- include('components/pagination', {
currentPage: pagination.currentPage,
totalPages: pagination.totalPages,
baseUrl: baseUrl,
query: query
}) %>
```
### 3. 分页参数说明
- `currentPage`: 当前页码
- `totalPages`: 总页数
- `baseUrl`: 基础URL用于生成分页链接
- `query`: 查询参数对象(用于保持其他查询参数)
## 分页配置
### 首页文章列表
- **每页数量**: 5篇文章
- **排序**: 按创建时间倒序
- **URL**: `/`
### 管理员后台
- **文章管理**: 每页10篇文章
- **用户管理**: 每页10个用户
- **评论管理**: 每页10条评论
## 分页组件特性
### 1. 智能页码显示
- 当前页前后各显示2页
- 最多显示5个页码
- 自动显示首页和末页
- 使用省略号表示跳过的页码
### 2. 导航功能
- 上一页/下一页按钮
- 首页/末页快速跳转
- 页码直接跳转
- 禁用状态处理
### 3. 查询参数保持
- 自动保持URL中的查询参数
- 支持搜索、筛选等功能
- 分页不影响其他查询条件
### 4. 响应式设计
- 移动端适配
- 触摸友好的按钮大小
- 清晰的视觉层次
## 样式定制
分页组件的样式可以通过修改 `views/components/pagination.ejs` 中的CSS来自定义
```css
.pagination-wrapper {
margin-top: 30px;
}
.page-link {
color: #3498db;
border-color: #dee2e6;
transition: all 0.3s ease;
}
.page-item.active .page-link {
background-color: #3498db;
border-color: #3498db;
color: white;
}
```
## 测试
运行分页功能测试:
```bash
node test_pagination.js
```
## 注意事项
1. **数据库性能**: 分页查询使用 `skip``limit`,大数据量时建议使用索引优化
2. **内存使用**: 分页组件会计算总记录数,确保数据库连接稳定
3. **URL参数**: 分页参数使用 `page` 参数,避免与其他参数冲突
4. **错误处理**: 分页组件包含完整的错误处理机制
## 扩展功能
### 1. 添加搜索功能
```javascript
// 在查询条件中添加搜索
const searchQuery = req.query.search ? { title: { $regex: req.query.search, $options: 'i' } } : {};
const result = await paginateQuery(Post, searchQuery, options, page, limit);
```
### 2. 添加筛选功能
```javascript
// 添加分类筛选
const categoryFilter = req.query.category ? { category: req.query.category } : {};
const result = await paginateQuery(Post, categoryFilter, options, page, limit);
```
### 3. 自定义每页数量
```javascript
// 允许用户选择每页显示数量
const limit = parseInt(req.query.limit) || 10;
const result = await paginateQuery(Post, {}, options, page, limit);
```
分页功能已完全集成到博客系统中,提供了良好的用户体验和性能优化。

136
README.md
View File

@ -1,3 +1,135 @@
# node-blog
# 陈立龙博客系统
基于 Node.js + Express + MongoDB + EJS 的博客系统。
一个基于 Node.js + Express + MongoDB + EJS 的博客系统。
## 功能特性
- 用户注册和登录
- 博客文章管理
- 友情链接管理
- 响应式设计
- 管理员后台
## 技术栈
- **后端**: Node.js + Express
- **数据库**: MongoDB + Mongoose
- **模板引擎**: EJS
- **会话管理**: express-session + connect-mongo
- **样式**: CSS3 + Grid布局
## 项目结构
```
ChenLilong_Blog/
├── app.js # 主应用文件
├── models/ # 数据模型
│ ├── Post.js # 博客文章模型
│ ├── User.js # 用户模型
│ ├── Link.js # 友情链接模型
│ └── ...
├── routes/ # 路由文件
│ ├── user/ # 用户相关路由
│ └── admin/ # 管理员路由
├── views/ # 视图模板
│ ├── user/ # 用户页面
│ └── admin/ # 管理员页面
├── middleware/ # 中间件
├── db/ # 数据库相关
└── uploads/ # 上传文件
```
## 安装和运行
### 1. 安装依赖
```bash
npm install
```
### 2. 启动MongoDB数据库
双击运行 `数据库启动.bat` 或在命令行中运行:
```bash
mongod --dbpath C:\data\db
```
### 3. 初始化测试数据(可选)
双击运行 `初始化数据.bat` 或在命令行中运行:
```bash
node db/init.js
```
### 4. 启动应用
双击运行 `项目启动.bat` 或在命令行中运行:
```bash
node app.js
```
### 5. 访问应用
打开浏览器访问http://localhost:3001
## 首页功能
### 左侧:博客文章列表
- 显示最新的10篇博客文章
- 每篇文章显示标题、作者、分类、发布时间
- 文章内容预览限制150字符
- 阅读全文链接
### 右侧:友情链接
- 显示所有友情链接
- 点击链接在新标签页中打开
- 悬停效果和动画
## 管理员功能
访问 http://localhost:3001/admin 进入管理员后台:
- 用户管理
- 文章管理
- 分类管理
- 评论管理
- 友情链接管理
## 数据库配置
默认数据库配置:
- 数据库名blog
- 连接地址mongodb://127.0.0.1:27017/blog
## 注意事项
1. 确保MongoDB已正确安装并运行
2. 首次运行建议先执行数据初始化脚本
3. 管理员账号需要在数据库中手动创建或通过注册功能创建
![image-20250624113519953](https://xy-md-assets.oss-cn-hangzhou.aliyuncs.com/image-20250624113519953.png)
![image-20250624113527404](https://xy-md-assets.oss-cn-hangzhou.aliyuncs.com/image-20250624113527404.png)
![image-20250624113549797](https://xy-md-assets.oss-cn-hangzhou.aliyuncs.com/image-20250624113549797.png)
![image-20250624113559568](https://xy-md-assets.oss-cn-hangzhou.aliyuncs.com/image-20250624113559568.png)
![image-20250624113612329](https://xy-md-assets.oss-cn-hangzhou.aliyuncs.com/image-20250624113612329.png)
![image-20250624113623177](https://xy-md-assets.oss-cn-hangzhou.aliyuncs.com/image-20250624113623177.png)
![image-20250624113628972](https://xy-md-assets.oss-cn-hangzhou.aliyuncs.com/image-20250624113628972.png)
![image-20250624113638050](https://xy-md-assets.oss-cn-hangzhou.aliyuncs.com/image-20250624113638050.png)
![image-20250624113644382](https://xy-md-assets.oss-cn-hangzhou.aliyuncs.com/image-20250624113644382.png)
![image-20250624113652652](https://xy-md-assets.oss-cn-hangzhou.aliyuncs.com/image-20250624113652652.png)
![image-20250624113704419](https://xy-md-assets.oss-cn-hangzhou.aliyuncs.com/image-20250624113704419.png)
![image-20250624113714538](https://xy-md-assets.oss-cn-hangzhou.aliyuncs.com/image-20250624113714538.png)

118
app.js Normal file
View File

@ -0,0 +1,118 @@
const express = require('express');
const mongoose = require('mongoose');
const session = require('express-session');
const MongoStore = require('connect-mongo');
const bodyParser = require('body-parser');
const cookieParser = require('cookie-parser');
const path = require('path');
const { paginateQuery } = require('./utils/pagination');
const app = express();
// 增强的 MongoDB 连接配置
mongoose.connect('mongodb://127.0.0.1:27017/blog', { // 使用127.0.0.1而不是localhost
useNewUrlParser: true,
useUnifiedTopology: true,
serverSelectionTimeoutMS: 5000, // 添加服务器选择超时
socketTimeoutMS: 45000, // 添加socket超时
family: 4 // 强制使用IPv4
})
.then(() => {
console.log('MongoDB 已经连接');
// 连接成功后启动服务器
startServer();
})
.catch(err => {
console.error('MongoDB 连接错误:', err);
process.exit(1); // 如果数据库连接失败,退出应用
});
// 添加Mongoose连接状态监听
mongoose.connection.on('connecting', () => console.log('正在连接MongoDB...'));
mongoose.connection.on('disconnected', () => console.log('MongoDB 连接断开'));
mongoose.connection.on('reconnected', () => console.log('MongoDB 重新连接'));
// 其他中间件配置
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
app.use(express.static(path.join(__dirname, 'public')));
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
app.use(cookieParser());
// 增强的session配置
app.use(session({
secret: 'chenlilong_blog_secret',
resave: false,
saveUninitialized: false,
store: MongoStore.create({
mongoUrl: 'mongodb://127.0.0.1:27017/blog',
ttl: 24 * 60 * 60, // 1天
autoRemove: 'native' // 自动清理过期session
}),
cookie: {
maxAge: 1000 * 60 * 60 * 24,
httpOnly: true,
secure: process.env.NODE_ENV === 'production'
}
}));
// 路由配置
app.get('/', async (req, res) => {
try {
const Post = require('./models/Post');
const Link = require('./models/Link');
const { paginateQuery } = require('./utils/pagination');
// 获取分页参数
const page = parseInt(req.query.page) || 1;
const limit = 5; // 每页显示5篇文章
// 获取博客文章(带分页,只获取已发布的文章)
const postsResult = await paginateQuery(
Post,
{ isPublished: true }, // 只显示已发布的文章
{ sort: { isTop: -1, createdAt: -1 } }, // 置顶文章优先,然后按时间倒序
page,
limit
);
// 获取所有友情链接
const links = await Link.find().sort({ order: 1, createdAt: -1 });
res.render('user/index', {
user: req.session.user || null,
posts: postsResult.data,
links: links,
pagination: postsResult.pagination,
baseUrl: '/',
query: {}
});
} catch (error) {
console.error('获取首页数据失败:', error);
res.render('user/index', {
user: req.session.user || null,
posts: [],
links: [],
pagination: { currentPage: 1, totalPages: 1, total: 0 },
baseUrl: '/',
query: {}
});
}
});
const userAuthRouter = require('./routes/user/auth');
app.use('/', userAuthRouter);
const adminRouter = require('./routes/admin/index');
app.use('/admin', adminRouter);
app.use((req, res) => {
res.status(404).send('404 Not Found');
});
// 将服务器启动封装为函数
function startServer() {
const PORT = process.env.PORT || 3001;
app.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}`);
});
}

View File

@ -0,0 +1,235 @@
const User = require('../models/User');
const Category = require('../models/Category');
const Post = require('../models/Post');
const Comment = require('../models/Comment');
const Link = require('../models/Link');
// 用户管理
exports.userList = async (req, res) => {
try {
const users = await User.find().sort({ createdAt: -1 });
res.render('admin/users/list', { users });
} catch (err) {
console.error(err);
res.status(500).send('服务器错误');
}
};
exports.updateUserStatus = async (req, res) => {
try {
const user = await User.findByIdAndUpdate(
req.params.id,
{ isActive: req.body.isActive },
{ new: true }
);
if (!user) return res.status(404).json({ error: '用户不存在' });
res.json({ success: true });
} catch (err) {
console.error(err);
res.status(500).json({ error: '服务器错误' });
}
};
exports.resetPassword = async (req, res) => {
try {
const user = await User.findById(req.params.id);
if (!user) return res.status(404).json({ error: '用户不存在' });
user.password = req.body.newPassword;
await user.save();
res.json({ success: true });
} catch (err) {
console.error(err);
res.status(500).json({ error: '服务器错误' });
}
};
// 分类管理
exports.categoryList = async (req, res) => {
try {
const categories = await Category.find().sort({ order: 1 });
res.render('admin/categories/list', { categories });
} catch (err) {
console.error(err);
res.status(500).send('服务器错误');
}
};
exports.createCategory = async (req, res) => {
try {
const { name, slug, order, isTop } = req.body;
const category = new Category({ name, slug, order, isTop });
await category.save();
res.redirect('/admin/categories');
} catch (err) {
console.error(err);
res.status(500).send('服务器错误');
}
};
exports.updateCategoryTopStatus = async (req, res) => {
try {
const category = await Category.findByIdAndUpdate(
req.params.id,
{ isTop: req.body.isTop },
{ new: true }
);
if (!category) return res.status(404).json({ error: '分类不存在' });
res.json({ success: true });
} catch (err) {
console.error(err);
res.status(500).json({ error: '服务器错误' });
}
};
exports.deleteCategory = async (req, res) => {
try {
await Category.findByIdAndDelete(req.params.id);
res.json({ success: true });
} catch (err) {
console.error(err);
res.status(500).json({ error: '服务器错误' });
}
};
// 文章管理
exports.postList = async (req, res) => {
try {
const posts = await Post.find().sort({ createdAt: -1 });
res.render('admin/posts/list', { posts });
} catch (err) {
console.error(err);
res.status(500).send('服务器错误');
}
};
exports.updatePostTopStatus = async (req, res) => {
try {
const post = await Post.findByIdAndUpdate(
req.params.id,
{ isTop: req.body.isTop },
{ new: true }
);
if (!post) return res.status(404).json({ error: '文章不存在' });
res.json({ success: true });
} catch (err) {
console.error(err);
res.status(500).json({ error: '服务器错误' });
}
};
exports.updatePostPublishStatus = async (req, res) => {
try {
const post = await Post.findByIdAndUpdate(
req.params.id,
{ isPublished: req.body.isPublished },
{ new: true }
);
if (!post) return res.status(404).json({ error: '文章不存在' });
res.json({ success: true });
} catch (err) {
console.error(err);
res.status(500).json({ error: '服务器错误' });
}
};
exports.deletePost = async (req, res) => {
try {
await Post.findByIdAndDelete(req.params.id);
res.json({ success: true });
} catch (err) {
console.error(err);
res.status(500).json({ error: '服务器错误' });
}
};
exports.createPost = async (req, res) => {
try {
const { title, content, category, isPublished, isTop } = req.body;
const author = req.user._id;
const post = new Post({
title,
content,
category,
author,
isPublished: isPublished || false,
isTop: isTop || false
});
await post.save();
res.json({ success: true, post });
} catch (err) {
console.error(err);
res.status(500).json({ error: '服务器错误' });
}
};
exports.updatePost = async (req, res) => {
try {
const { title, content, category, isPublished, isTop } = req.body;
const post = await Post.findByIdAndUpdate(
req.params.id,
{ title, content, category, isPublished, isTop },
{ new: true }
);
if (!post) return res.status(404).json({ error: '文章不存在' });
res.json({ success: true, post });
} catch (err) {
console.error(err);
res.status(500).json({ error: '服务器错误' });
}
};
// 评论管理
exports.commentList = async (req, res) => {
try {
const comments = await Comment.find().populate('user post').sort({ createdAt: -1 });
res.render('admin/comments/list', { comments });
} catch (err) {
console.error(err);
res.status(500).send('服务器错误');
}
};
exports.deleteComment = async (req, res) => {
try {
await Comment.findByIdAndDelete(req.params.id);
res.json({ success: true });
} catch (err) {
console.error(err);
res.status(500).json({ error: '服务器错误' });
}
};
// 友情链接管理
exports.linkList = async (req, res) => {
try {
const links = await Link.find().sort({ order: 1 });
res.render('admin/links/list', { links });
} catch (err) {
console.error(err);
res.status(500).send('服务器错误');
}
};
exports.createLink = async (req, res) => {
try {
const { name, url, description, order } = req.body;
const link = new Link({ name, url, description, order });
await link.save();
res.redirect('/admin/links');
} catch (err) {
console.error(err);
res.status(500).send('服务器错误');
}
};
exports.deleteLink = async (req, res) => {
try {
await Link.findByIdAndDelete(req.params.id);
res.json({ success: true });
} catch (err) {
console.error(err);
res.status(500).json({ error: '服务器错误' });
}
};

80
controllers/user/auth.js Normal file
View File

@ -0,0 +1,80 @@
const User = require('../../models/User');
const bcrypt = require('bcryptjs');
// 注册
exports.register = async (req, res) => {
const { username, password } = req.body;
if (!username || !password) {
return res.render('user/register', { error: '用户名和密码不能为空' });
}
const exist = await User.findOne({ username });
if (exist) {
return res.render('user/register', { error: '用户名已存在' });
}
const hash = await bcrypt.hash(password, 10);
const user = new User({ username, password: hash });
await user.save();
res.redirect('/login');
};
// 登录
exports.login = async (req, res) => {
const { username, password } = req.body;
const user = await User.findOne({ username });
if (!user) {
return res.render('user/login', { error: '用户不存在' });
}
if (user.status === 'frozen') {
return res.render('user/login', { error: '账号已被冻结' });
}
const match = await bcrypt.compare(password, user.password);
if (!match) {
return res.render('user/login', { error: '密码错误' });
}
req.session.user = {
_id: user._id,
username: user.username,
role: user.role
};
if (username === 'admin') {
// 跳转到后台管理页面
return res.redirect('/admin');
}
res.redirect('/');
};
// 登出
exports.logout = (req, res) => {
req.session.destroy(() => {
res.redirect('/login');
});
};
// 修改密码
exports.changePassword = async (req, res) => {
if (!req.session.user) {
return res.status(401).json({ success: false, message: '请先登录' });
}
const { oldPassword, newPassword } = req.body;
if (!oldPassword || !newPassword) {
return res.json({ success: false, message: '参数不完整' });
}
try {
const user = await User.findById(req.session.user._id);
if (!user) {
return res.json({ success: false, message: '用户不存在' });
}
const match = await bcrypt.compare(oldPassword, user.password);
if (!match) {
return res.json({ success: false, message: '原密码错误' });
}
const hash = await bcrypt.hash(newPassword, 10);
user.password = hash;
await user.save();
res.json({ success: true });
} catch (err) {
console.error('修改密码失败:', err);
res.status(500).json({ success: false, message: '服务器错误' });
}
};

274
db/blog.js Normal file
View File

@ -0,0 +1,274 @@
/*
Navicat Premium Data Transfer
Source Server : localhost_27017
Source Server Type : MongoDB
Source Server Version : 80010 (8.0.10)
Source Host : localhost:27017
Source Schema : blog
Target Server Type : MongoDB
Target Server Version : 80010 (8.0.10)
File Encoding : 65001
Date: 24/06/2025 11:40:35
*/
// ----------------------------
// Collection structure for categories
// ----------------------------
db.getCollection("categories").drop();
db.createCollection("categories");
db.getCollection("categories").createIndex({
name: NumberInt("1")
}, {
name: "name_1",
background: true,
unique: true
});
// ----------------------------
// Documents of categories
// ----------------------------
db.getCollection("categories").insert([ {
_id: ObjectId("68591bca984303d931013db2")
} ]);
// ----------------------------
// Collection structure for comments
// ----------------------------
db.getCollection("comments").drop();
db.createCollection("comments");
// ----------------------------
// Documents of comments
// ----------------------------
db.getCollection("comments").insert([ {
_id: ObjectId("685a19e7109455f150256912"),
content: "不错的讲话,中国加油",
author: ObjectId("68590fed4ef1dd3c6a957339"),
post: ObjectId("685a15b95ce698838bef9269"),
createdAt: ISODate("2025-06-24T03:22:15.371Z"),
__v: NumberInt("0")
} ]);
// ----------------------------
// Collection structure for links
// ----------------------------
db.getCollection("links").drop();
db.createCollection("links");
// ----------------------------
// Documents of links
// ----------------------------
db.getCollection("links").insert([ {
_id: ObjectId("685a0430cf497e2b12337750"),
name: "百度",
url: "https://www.baidu.com/?tn=68018901_16_pg",
description: "百度搜索",
order: NumberInt("1"),
createdAt: ISODate("2025-06-24T01:49:36.942Z"),
__v: NumberInt("0")
} ]);
db.getCollection("links").insert([ {
_id: ObjectId("685a0430cf497e2b12337751"),
name: "谷歌",
url: "https://www.google.com",
description: "全球最大的搜索引擎",
order: NumberInt("2"),
createdAt: ISODate("2025-06-24T01:50:00.000Z"),
__v: NumberInt("0")
} ]);
db.getCollection("links").insert([ {
_id: ObjectId("685a0430cf497e2b12337752"),
name: "必应",
url: "https://www.bing.com",
description: "微软推出的搜索引擎",
order: NumberInt("3"),
createdAt: ISODate("2025-06-24T01:51:00.000Z"),
__v: NumberInt("0")
} ]);
db.getCollection("links").insert([ {
_id: ObjectId("685a0430cf497e2b12337753"),
name: "GitHub",
url: "https://github.com",
description: "全球最大的代码托管平台",
order: NumberInt("4"),
createdAt: ISODate("2025-06-24T01:52:00.000Z"),
__v: NumberInt("0")
} ]);
db.getCollection("links").insert([ {
_id: ObjectId("685a0430cf497e2b12337754"),
name: "Stack Overflow",
url: "https://stackoverflow.com",
description: "程序员问答社区",
order: NumberInt("5"),
createdAt: ISODate("2025-06-24T01:53:00.000Z"),
__v: NumberInt("0")
} ]);
db.getCollection("links").insert([ {
_id: ObjectId("685a0430cf497e2b12337755"),
name: "知乎",
url: "https://www.zhihu.com",
description: "中文问答社区",
order: NumberInt("6"),
createdAt: ISODate("2025-06-24T01:54:00.000Z"),
__v: NumberInt("0")
} ]);
db.getCollection("links").insert([ {
_id: ObjectId("685a0430cf497e2b12337756"),
name: "微博",
url: "https://weibo.com",
description: "中文社交媒体平台",
order: NumberInt("7"),
createdAt: ISODate("2025-06-24T01:55:00.000Z"),
__v: NumberInt("0")
} ]);
db.getCollection("links").insert([ {
_id: ObjectId("685a0430cf497e2b12337757"),
name: "CSDN",
url: "https://www.csdn.net",
description: "中文IT技术社区",
order: NumberInt("8"),
createdAt: ISODate("2025-06-24T01:56:00.000Z"),
__v: NumberInt("0")
} ]);
db.getCollection("links").insert([ {
_id: ObjectId("685a0430cf497e2b12337758"),
name: "掘金",
url: "https://juejin.cn",
description: "开发者技术社区",
order: NumberInt("9"),
createdAt: ISODate("2025-06-24T01:57:00.000Z"),
__v: NumberInt("0")
} ]);
db.getCollection("links").insert([ {
_id: ObjectId("685a0430cf497e2b12337759"),
name: "哔哩哔哩",
url: "https://www.bilibili.com",
description: "视频分享网站",
order: NumberInt("10"),
createdAt: ISODate("2025-06-24T01:58:00.000Z"),
__v: NumberInt("0")
} ]);
db.getCollection("links").insert([ {
_id: ObjectId("685a0430cf497e2b1233775a"),
name: "豆瓣",
url: "https://www.douban.com",
description: "图书、电影、音乐评论网站",
order: NumberInt("11"),
createdAt: ISODate("2025-06-24T01:59:00.000Z"),
__v: NumberInt("0")
} ]);
// ----------------------------
// Collection structure for posts
// ----------------------------
db.getCollection("posts").drop();
db.createCollection("posts");
// ----------------------------
// Documents of posts
// ----------------------------
db.getCollection("posts").insert([ {
_id: ObjectId("685a15b95ce698838bef9269"),
title: "任正非专访",
content: "本周,《人民日报》头版刊登任正非专访。\n\n下面是一些摘录。\n\n1芯片问题其实没必要担心。我们单芯片还是落后美国一代我们用数学补物理、非摩尔补摩尔用群计算补单芯片在结果上也能达到实用状况。\n\n2软件是卡不住脖子的那是数学的图形符号、代码一些尖端的算子、算法垒起来的没有阻拦索。困难在我们的教育培养、人才梯队的建设。\n\n3当我国拥有一定经济实力的时候要重视理论特别是基础理论的研究。如果不搞基础研究就没根。即使叶茂欣欣向荣风一吹就会倒的。\n\n4我们要理解支持搞理论工作的。理论科学家是孤独的我们要有战略耐心要理解他们。他们头脑中的符号、公式、思维世界上能与他们沟通的只有几个人。对理论科学家要尊重因为我们不懂他的文化社会要宽容国家要支持。\n\n5买国外的产品很贵因为价格里面就包含他们在基础研究上的投入。中国搞不搞基础研究也要付钱的能不能付给自己搞基础研究的人。\n\n6华为一年1800亿投入研发大概有600亿是做基础理论研究不考核。1200亿左右投入产品研发投入是要考核的。没有理论就没有突破我们就赶不上美国。\n\n7人工智能也许是人类社会最后一次技术革命当然可能还有能源的核聚变。发展人工智能要有电力保障中国的发电、电网传输都是非常好的通信网络是世界最发达的东数西算的理想是可能实现的。\n\n8赞声与骂声都不要在意而要在乎自己能不能做好。把自己做好就没有问题。",
author: "人民日报",
category: "技术",
isPublished: true,
isTop: true,
createdAt: ISODate("2025-06-24T03:04:25.725Z"),
updatedAt: ISODate("2025-06-24T03:04:25.729Z"),
__v: NumberInt("0")
} ]);
db.getCollection("posts").insert([ {
_id: ObjectId("685a15ff5ce698838bef9285"),
title: "程序员常用的六大技术博客类",
content: "一CSDN\n\n网址http://www.csdn.net/\n\n介绍CSDN深度IT技术博客,移动开发博客,Web前端博客,企业架构博客,编程语言博客,互联网博客,数据库博客,系统运维博客,云计算博客,研发管理博客但是csdn最近感觉访问速度比较慢博客还好些下载是有时候真慢真卡以前的CSDN还可以最近两年商业化似乎越来越严重。\n\n\n\n\n二博客园\n\n网址http://www.cnblogs.com\n\n介绍博客园是一个面向开发者的知识分享社区。自创建以来,博客园一直致力并专注于为开发者打造一个纯净的技术交流社区,推动并帮助开发者通过互联网分享知识,从而让更多的技术者交流从用博客园以来发现有一个小小的缺点就是UI设计的太古板一直都没有在设计上有所突破在这个扁平化趋势越来越成为主流的网络上没有能让人眼前一亮的感觉。\n\n\n三掘金\n\n网址 https://juejin.im\n\n介绍掘金是一个帮助开发者成长的社区,是给开发者用的 Hacker News,给设计师用的 Designer News,和给产品经理用的 Medium。掘金的技术文章由稀土上聚集的技术大牛所写 ,文章技术含量很高,但在宣传上似乎有欠缺,在百度搜索关键字,关于掘金的信息也特别的少,或许很多人都不知道吧。很喜欢掘金的页面布局,给人的感觉就是简单大方,相比其他的博客,显得不拥不挤条条有理。\n\n\n\n\n四github\n\n网址 https://github.com/\n\n介绍这个就不多说了所有的程序员都拥有这样一个只属于自己的代码托管平台吧gitHub是一个面向开源及私有软件项目的托管平台因为只支持git 作为唯一的版本库格式进行托管故名gitHub。gitHub于2008年4月10日正式上线除了git代码仓库托管及基本的 Web管理界面以外还提供了订阅、讨论组、文本渲染、在线文件编辑等功能方便实用。平时没事的时候可以多上传一些自己写的项目demo代码等等在面试的时候还能给自己涨不少的分。\n\n\n\n\n五segmentfault\n\n网址https://segmentfault.com/\n\n介绍SegmentFault是中国领先的开发者技术社区。 为编程爱好者提供一个纯粹、高质的技术交流的平台, 与开发者一起学习、交流与成长,关于前端这一块的技术交流者也是非常多的,每次有问题我都会发出来,就会有很多同行帮忙解答,在这里,也有很多脑洞大开的工程师,每次逛完论坛,就像是走在铺满各种各样贝壳的沙滩上,有太多惊喜和闪闪发光值得收藏的技术点。\n\n\n\n\n六开源中国\n\n网址 https://my.oschina.net\n\n介绍开源中国是目前中国最大的开源技术社区。我们传播开源的理念,推广开源项目,为 IT 开发者提供了一个发现、使用、并交流开源技术的平台。主要有开源软件库、代码分享、资讯、协作翻译、码云、众包、招聘等几大模块,虽然首页信息量很多,但对于个人空间管理,无论是提问还是发表文章,记录笔记等,页面是很整洁简约的。\n\n作者祈澈菇凉\n链接https://www.jianshu.com/p/d1614f890282\n来源简书\n著作权归作者所有。商业转载请联系作者获得授权非商业转载请注明出处。",
author: "CSDN",
category: "技术",
isPublished: true,
isTop: false,
createdAt: ISODate("2025-06-24T03:05:35.067Z"),
updatedAt: ISODate("2025-06-24T03:05:35.068Z"),
__v: NumberInt("0")
} ]);
db.getCollection("posts").insert([ {
_id: ObjectId("685a16255ce698838bef928d"),
title: "精选技术博客推荐",
content: "程序员进阶指南:精选技术博客推荐\n大家好今天给大家推荐一些我私藏的技术博客和实用网站希望能帮到正在努力提升自己的程序员们。不过有些平台实在是不敢在帖子里发链接怕哪天号被封了所以大家可以自行上网搜索哦~\n\n🌟 博客Jack Franklin\nJack是谷歌Chrome团队的开发者之一他的博客风格非常程序员化走的是极简路线体验非常棒。他的技术博客主要关注React、Webpack和各种JavaScript技巧还有一些实战经验。特别推荐他的VSCode指南系列真的是非常实用。除此之外他还会时不时分享一些经验总结和个人体悟我特别喜欢他关于从错误中学习和对开发者职场发展的思考。\n\n🌟 博客James Ward\nJames是Google Cloud的倡导者他的讲座和视频比博客本身更出名。他的技术涵盖面非常广毕竟是混迹大厂职场的人包括Adobe和Salesforce。不过他的前端技术相对较浅主要集中在Java、Salesforce以及云和微服务方面。\n\n🌟 网站usethekeyboard.com\n这是一个汇总了各种软件快捷键的cheatsheet收藏网站。我是在搜Notion快捷键时无意中发现的。作为程序员平时使用IDE和Markdown的时候快捷键真的是救命稻草。特别是最近从Evernote转到Notion这个网站帮了我大忙。里面常用的快捷键包括VSCode、Notion、Unity、Xcode还有一些职场常用软件如Slack、Jira、Gitlab等。希望有空的时候能加上IntelliJ的快捷键。\n\n🌟 写码小练习typescript-exercises.github.io\n这个webapp特别适合想入门TypeScript但又找不到练习上手的朋友们。一共有16个小练习每一节都在之前的基础上拓展基本涵盖了TypeScript的重要知识点。写不出来还可以参考答案。作为一个教了三年函数式编程的老师这个练习居然有柯里化的内容简直太亲切了。\n\n希望这些推荐对大家有帮助无论是职场还是求职程序员的提升之路永远没有终点。加油",
author: "百度",
category: "随笔",
isPublished: true,
isTop: false,
createdAt: ISODate("2025-06-24T03:06:13.624Z"),
updatedAt: ISODate("2025-06-24T03:06:13.624Z"),
__v: NumberInt("0")
} ]);
// ----------------------------
// Collection structure for sessions
// ----------------------------
db.getCollection("sessions").drop();
db.createCollection("sessions");
db.getCollection("sessions").createIndex({
expires: NumberInt("1")
}, {
name: "expires_1",
background: true
});
// ----------------------------
// Documents of sessions
// ----------------------------
db.getCollection("sessions").insert([ {
_id: "DQULrlwQkkF9Sm96awZ8jD5dFu0qML08",
expires: ISODate("2025-06-24T15:24:12.519Z"),
session: "{\"cookie\":{\"originalMaxAge\":86400000,\"expires\":\"2025-06-24T09:22:53.530Z\",\"secure\":false,\"httpOnly\":true,\"path\":\"/\"},\"user\":{\"_id\":\"68590fed4ef1dd3c6a957339\",\"username\":\"admin\",\"role\":\"user\"}}"
} ]);
db.getCollection("sessions").insert([ {
_id: "nocdVsYg0XuMYkq9Og5kWuG9nUefstHk",
expires: ISODate("2025-06-25T03:37:10.417Z"),
session: "{\"cookie\":{\"originalMaxAge\":86400000,\"expires\":\"2025-06-25T03:35:44.536Z\",\"secure\":false,\"httpOnly\":true,\"path\":\"/\"},\"user\":{\"_id\":\"68590fed4ef1dd3c6a957339\",\"username\":\"admin\",\"role\":\"user\"}}"
} ]);
// ----------------------------
// Collection structure for users
// ----------------------------
db.getCollection("users").drop();
db.createCollection("users");
db.getCollection("users").createIndex({
username: NumberInt("1")
}, {
name: "username_1",
background: true,
unique: true
});
// ----------------------------
// Documents of users
// ----------------------------
db.getCollection("users").insert([ {
_id: ObjectId("68590fed4ef1dd3c6a957339"),
username: "admin",
password: "$2a$10$5Egc33FsjLyY7mHYp34RsOXM3ZWehZutDEWq4OYy5tcXHXz9ra4ca",
role: "user",
status: "active",
createdAt: ISODate("2025-06-23T08:27:25.734Z"),
__v: NumberInt("0"),
favorites: [
ObjectId("685a15b95ce698838bef9269")
]
} ]);
db.getCollection("users").insert([ {
_id: ObjectId("685a1c4a064b039cc10ee9ea"),
username: "123456",
password: "$2a$10$mK3wHu9wu/r8zJnmuBup7.kWj0OmGsfnivVB/2YugdUxEk3ScL2fO",
role: "user",
status: "active",
favorites: [ ],
createdAt: ISODate("2025-06-24T03:32:26.089Z"),
__v: NumberInt("0")
} ]);

112
db/init.js Normal file
View File

@ -0,0 +1,112 @@
const mongoose = require('mongoose');
const Post = require('../models/Post');
const Link = require('../models/Link');
// 连接数据库
mongoose.connect('mongodb://127.0.0.1:27017/blog', {
useNewUrlParser: true,
useUnifiedTopology: true
})
.then(() => {
console.log('数据库连接成功');
initData();
})
.catch(err => {
console.error('数据库连接失败:', err);
process.exit(1);
});
async function initData() {
try {
// 清空现有数据
await Post.deleteMany({});
await Link.deleteMany({});
// 添加测试博客文章
const posts = [
{
title: '欢迎来到我的博客',
content: '这是我的第一篇博客文章。在这里,我将分享我的技术心得、学习笔记和生活感悟。希望这个博客能够成为我们交流的平台,也希望能够通过写作来提升自己的表达能力。',
author: '陈立龙',
category: '随笔'
},
{
title: 'JavaScript 异步编程详解',
content: 'JavaScript 的异步编程是前端开发中的重要概念。本文将详细介绍 Promise、async/await 和回调函数的使用方法,以及它们之间的区别和最佳实践。通过实际的代码示例,帮助读者更好地理解异步编程的核心概念。',
author: '陈立龙',
category: '技术'
},
{
title: 'Node.js 开发环境搭建',
content: 'Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时。本文将详细介绍如何在 Windows、Mac 和 Linux 系统上搭建 Node.js 开发环境,包括安装 Node.js、配置 npm 镜像源、安装常用开发工具等步骤。',
author: '陈立龙',
category: '教程'
},
{
title: 'MongoDB 数据库操作指南',
content: 'MongoDB 是一个基于分布式文件存储的数据库。本文将介绍 MongoDB 的基本概念、安装配置、常用操作命令,以及在 Node.js 中使用 Mongoose 进行数据库操作的方法。',
author: '陈立龙',
category: '数据库'
},
{
title: '前端开发工具推荐',
content: '工欲善其事,必先利其器。本文将推荐一些前端开发中常用的工具,包括代码编辑器、浏览器开发者工具、包管理器、构建工具等,帮助开发者提高开发效率。',
author: '陈立龙',
category: '工具'
}
];
// 添加测试友情链接
const links = [
{
name: 'GitHub',
url: 'https://github.com',
description: '全球最大的代码托管平台',
order: 1
},
{
name: 'Stack Overflow',
url: 'https://stackoverflow.com',
description: '程序员问答社区',
order: 2
},
{
name: 'MDN Web Docs',
url: 'https://developer.mozilla.org',
description: 'Web开发技术文档',
order: 3
},
{
name: 'W3Schools',
url: 'https://www.w3schools.com',
description: 'Web技术学习网站',
order: 4
},
{
name: 'CSS-Tricks',
url: 'https://css-tricks.com',
description: 'CSS技巧和教程',
order: 5
},
{
name: 'JavaScript.info',
url: 'https://javascript.info',
description: '现代JavaScript教程',
order: 6
}
];
// 插入数据
await Post.insertMany(posts);
await Link.insertMany(links);
console.log('测试数据初始化完成!');
console.log(`添加了 ${posts.length} 篇博客文章`);
console.log(`添加了 ${links.length} 个友情链接`);
mongoose.connection.close();
} catch (error) {
console.error('初始化数据失败:', error);
mongoose.connection.close();
}
}

103
fix_database.js Normal file
View File

@ -0,0 +1,103 @@
const mongoose = require('mongoose');
const Post = require('./models/Post');
// 连接数据库
mongoose.connect('mongodb://127.0.0.1:27017/blog', {
useNewUrlParser: true,
useUnifiedTopology: true
})
.then(() => {
console.log('数据库连接成功');
fixDatabase();
})
.catch(err => {
console.error('数据库连接失败:', err);
process.exit(1);
});
async function fixDatabase() {
try {
console.log('开始检查和修复数据库...\n');
// 查找所有文章
const posts = await Post.find({});
console.log(`找到 ${posts.length} 篇文章`);
let fixedCount = 0;
for (let post of posts) {
let needsUpdate = false;
let updates = {};
// 检查并修复title字段
if (!post.title || post.title.trim() === '') {
updates.title = '无标题';
needsUpdate = true;
console.log(`修复文章 ${post._id} 的标题`);
}
// 检查并修复content字段
if (!post.content || post.content.trim() === '') {
updates.content = '暂无内容';
needsUpdate = true;
console.log(`修复文章 ${post._id} 的内容`);
}
// 检查并修复author字段
if (!post.author || post.author.trim() === '') {
updates.author = '未知作者';
needsUpdate = true;
console.log(`修复文章 ${post._id} 的作者`);
}
// 检查并修复category字段
if (!post.category || post.category.trim() === '') {
updates.category = '未分类';
needsUpdate = true;
console.log(`修复文章 ${post._id} 的分类`);
}
// 添加缺失的字段
if (post.isPublished === undefined) {
updates.isPublished = true;
needsUpdate = true;
console.log(`为文章 ${post._id} 添加发布状态`);
}
if (post.isTop === undefined) {
updates.isTop = false;
needsUpdate = true;
console.log(`为文章 ${post._id} 添加置顶状态`);
}
if (post.updatedAt === undefined) {
updates.updatedAt = post.createdAt || new Date();
needsUpdate = true;
console.log(`为文章 ${post._id} 添加更新时间`);
}
// 更新数据库
if (needsUpdate) {
await Post.findByIdAndUpdate(post._id, updates);
fixedCount++;
}
}
console.log(`\n修复完成!共修复了 ${fixedCount} 篇文章`);
// 显示修复后的统计信息
const totalPosts = await Post.countDocuments();
const publishedPosts = await Post.countDocuments({ isPublished: true });
const topPosts = await Post.countDocuments({ isTop: true });
console.log('\n数据库统计信息:');
console.log(`- 总文章数: ${totalPosts}`);
console.log(`- 已发布: ${publishedPosts}`);
console.log(`- 置顶文章: ${topPosts}`);
mongoose.connection.close();
} catch (error) {
console.error('修复数据库失败:', error);
mongoose.connection.close();
}
}

16
middleware/auth.js Normal file
View File

@ -0,0 +1,16 @@
module.exports = {
ensureAuthenticated: (req, res, next) => {
if (req.session && req.session.user) {
return next();
}
console.log( '请先登录');
res.redirect('/login');
},
ensureAdmin: (req, res, next) => {
if ( req.session.user.username === 'admin') {
return next();
}
console.log( '您没有管理员权限');
res.redirect('/');
}
};

9
models/Category.js Normal file
View File

@ -0,0 +1,9 @@
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const CategorySchema = new Schema({
name: { type: String, required: true, unique: true },
createdAt: { type: Date, default: Date.now }
});
module.exports = mongoose.model('Category', CategorySchema);

11
models/Comment.js Normal file
View File

@ -0,0 +1,11 @@
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const CommentSchema = new Schema({
content: { type: String, required: true },
author: { type: Schema.Types.ObjectId, ref: 'User', required: true },
post: { type: Schema.Types.ObjectId, ref: 'Post', required: true },
createdAt: { type: Date, default: Date.now }
});
module.exports = mongoose.model('Comment', CommentSchema);

12
models/Link.js Normal file
View File

@ -0,0 +1,12 @@
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const LinkSchema = new Schema({
name: { type: String, required: true },
url: { type: String, required: true },
description: { type: String, default: '' },
order: { type: Number, default: 0 },
createdAt: { type: Date, default: Date.now }
});
module.exports = mongoose.model('Link', LinkSchema);

49
models/Post.js Normal file
View File

@ -0,0 +1,49 @@
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const PostSchema = new Schema({
title: {
type: String,
required: true,
default: '无标题'
},
content: {
type: String,
required: true,
default: ''
},
author: {
type: String,
required: true,
default: '未知作者'
},
category: {
type: String,
required: true,
default: '未分类'
},
isPublished: {
type: Boolean,
default: true
},
isTop: {
type: Boolean,
default: false
},
createdAt: {
type: Date,
default: Date.now
},
updatedAt: {
type: Date,
default: Date.now
}
});
// 更新时自动设置updatedAt
PostSchema.pre('save', function(next) {
this.updatedAt = new Date();
next();
});
module.exports = mongoose.model('Post', PostSchema);

13
models/User.js Normal file
View File

@ -0,0 +1,13 @@
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const UserSchema = new Schema({
username: { type: String, required: true, unique: true },
password: { type: String, required: true },
role: { type: String, enum: ['admin', 'user'], default: 'user' },
status: { type: String, enum: ['active', 'frozen'], default: 'active' },
createdAt: { type: Date, default: Date.now },
favorites: [{ type: Schema.Types.ObjectId, ref: 'Post' }]
});
module.exports = mongoose.model('User', UserSchema);

1909
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

25
package.json Normal file
View File

@ -0,0 +1,25 @@
{
"name": "chenlilong_blog",
"version": "1.0.0",
"description": "通用基础内容管理系统CMS- Node.js + MongoDB",
"main": "app.js",
"scripts": {
"start": "node app.js",
"dev": "nodemon app.js"
},
"dependencies": {
"bcryptjs": "^2.4.3",
"body-parser": "^1.20.2",
"connect-mongo": "^5.0.0",
"cookie-parser": "^1.4.6",
"ejs": "^3.1.9",
"express": "^4.18.2",
"express-session": "^1.17.3",
"mongoose": "^7.6.1"
},
"devDependencies": {
"nodemon": "^3.1.10"
},
"author": "Chen Lilong",
"license": "MIT"
}

356
routes/admin/index.js Normal file
View File

@ -0,0 +1,356 @@
const express = require('express');
const router = express.Router();
const { ensureAuthenticated, ensureAdmin } = require('../../middleware/auth');
const User = require('../../models/User');
const Category = require('../../models/Category');
const Post = require('../../models/Post');
const Comment = require('../../models/Comment');
const Link = require('../../models/Link');
const { paginateQuery } = require('../../utils/pagination');
// 后台管理首页
router.get('/', ensureAuthenticated, ensureAdmin, (req, res) => {
res.render('admin/index', {
user: req.user,
template: 'index'
});
});
// 用户管理路由
router.get('/users', ensureAuthenticated, ensureAdmin, async (req, res) => {
try {
// 获取分页参数
const page = parseInt(req.query.page) || 1;
const limit = 10; // 每页显示10个用户
// 获取用户(带分页)
const usersResult = await paginateQuery(
User,
{},
{ sort: { createdAt: -1 } },
page,
limit
);
res.render('admin/index', {
user: req.user,
template: 'users/list',
users: usersResult.data,
pagination: usersResult.pagination,
baseUrl: '/admin/users',
query: {}
});
} catch (error) {
console.error('获取用户列表失败:', error);
res.status(500).send('服务器错误');
}
});
// 分类管理路由
router.get('/categories', ensureAuthenticated, ensureAdmin, async (req, res) => {
const categories = await Category.find().sort({ order: 1 });
res.render('admin/index', {
user: req.user,
template: 'categories/list',
categories
});
});
// 获取分类列表API - 供前端使用
router.get('/api/categories', ensureAuthenticated, ensureAdmin, async (req, res) => {
try {
const categories = await Category.find().sort({ order: 1 });
res.json(categories);
} catch (err) {
console.error(err);
res.status(500).json({ error: '获取分类失败' });
}
});
// 文章管理路由
router.get('/posts', ensureAuthenticated, ensureAdmin, async (req, res) => {
try {
// 获取分页参数
const page = parseInt(req.query.page) || 1;
const limit = 10; // 每页显示10篇文章
// 获取文章(带分页)
const postsResult = await paginateQuery(
Post,
{},
{
populate: 'category author',
sort: { createdAt: -1 }
},
page,
limit
);
res.render('admin/index', {
user: req.user,
template: 'posts/list',
posts: postsResult.data,
pagination: postsResult.pagination,
baseUrl: '/admin/posts',
query: {}
});
} catch (error) {
console.error('获取文章列表失败:', error);
res.status(500).send('服务器错误');
}
});
// 创建文章 - POST
router.post('/posts', ensureAuthenticated, ensureAdmin, async (req, res) => {
try {
const { title, content, category, author, isPublished, isTop } = req.body;
const newPost = new Post({
title,
content,
category,
author,
isPublished: isPublished === 'on' || isPublished === true,
isTop: isTop === 'on' || isTop === true
});
await newPost.save();
res.json({ success: true, post: newPost });
} catch (err) {
console.error(err);
res.status(500).json({ success: false, message: '创建文章失败' });
}
});
// 更新文章 - PUT
router.put('/posts/:id', ensureAuthenticated, ensureAdmin, async (req, res) => {
try {
const { title, content, category, author, isPublished, isTop } = req.body;
const updatedPost = await Post.findByIdAndUpdate(
req.params.id,
{
title,
content,
category,
author,
isPublished: isPublished === 'on' || isPublished === true,
isTop: isTop === 'on' || isTop === true,
updatedAt: Date.now()
},
{ new: true }
);
if (!updatedPost) {
return res.status(404).json({ success: false, message: '文章未找到' });
}
res.json({ success: true, post: updatedPost });
} catch (err) {
console.error(err);
res.status(500).json({ success: false, message: '更新文章失败' });
}
});
// 删除文章 - DELETE
router.delete('/posts/:id', ensureAuthenticated, ensureAdmin, async (req, res) => {
try {
const deletedPost = await Post.findByIdAndDelete(req.params.id);
if (!deletedPost) {
return res.status(404).json({ success: false, message: '文章未找到' });
}
// 同时删除相关评论
await Comment.deleteMany({ post: req.params.id });
res.json({ success: true });
} catch (err) {
console.error(err);
res.status(500).json({ success: false, message: '删除文章失败' });
}
});
// 设置/取消置顶 - PUT
router.put('/posts/:id/top', ensureAuthenticated, ensureAdmin, async (req, res) => {
try {
const { isTop } = req.body;
const updatedPost = await Post.findByIdAndUpdate(
req.params.id,
{ isTop },
{ new: true }
);
if (!updatedPost) {
return res.status(404).json({ success: false, message: '文章未找到' });
}
res.json({ success: true, post: updatedPost });
} catch (err) {
console.error(err);
res.status(500).json({ success: false, message: '操作失败' });
}
});
// 发布/取消发布 - PUT
router.put('/posts/:id/publish', ensureAuthenticated, ensureAdmin, async (req, res) => {
try {
const { isPublished } = req.body;
const updatedPost = await Post.findByIdAndUpdate(
req.params.id,
{ isPublished },
{ new: true }
);
if (!updatedPost) {
return res.status(404).json({ success: false, message: '文章未找到' });
}
res.json({ success: true, post: updatedPost });
} catch (err) {
console.error(err);
res.status(500).json({ success: false, message: '操作失败' });
}
});
// 获取单篇文章详情 - GET
router.get('/posts/:id', ensureAuthenticated, ensureAdmin, async (req, res) => {
try {
const post = await Post.findById(req.params.id);
if (!post) {
return res.status(404).json({ success: false, message: '文章未找到' });
}
res.json(post);
} catch (err) {
console.error(err);
res.status(500).json({ success: false, message: '获取文章信息失败' });
}
});
// 评论管理路由
router.get('/comments', ensureAuthenticated, ensureAdmin, async (req, res) => {
try {
// 获取分页参数
const page = parseInt(req.query.page) || 1;
const limit = 10; // 每页显示10条评论
// 获取评论(带分页)
const commentsResult = await paginateQuery(
Comment,
{},
{
populate: 'author post',
sort: { createdAt: -1 }
},
page,
limit
);
res.render('admin/index', {
user: req.user,
template: 'comments/list',
comments: commentsResult.data,
pagination: commentsResult.pagination,
baseUrl: '/admin/comments',
query: {}
});
} catch (error) {
console.error('获取评论列表失败:', error);
res.status(500).send('服务器错误');
}
});
// 友情链接路由
router.get('/links', ensureAuthenticated, ensureAdmin, async (req, res) => {
try {
// 获取分页参数
const page = parseInt(req.query.page) || 1;
const limit = 10; // 每页显示10条链接
// 获取友情链接(带分页)
const linksResult = await paginateQuery(
Link,
{},
{
sort: { order: 1 }
},
page,
limit
);
res.render('admin/index', {
user: req.user,
template: 'links/list',
links: linksResult.data,
pagination: linksResult.pagination,
baseUrl: '/admin/links',
query: {}
});
} catch (error) {
console.error('获取友情链接列表失败:', error);
res.status(500).send('服务器错误');
}
});
// 添加友情链接 - POST
router.post('/links', ensureAuthenticated, ensureAdmin, async (req, res) => {
try {
const { name, url, description, order } = req.body;
const newLink = new Link({
name,
url,
description: description || '',
order: order || 0
});
await newLink.save();
res.json({ success: true, link: newLink });
} catch (err) {
console.error(err);
res.status(500).json({ success: false, message: '添加友情链接失败' });
}
});
// 更新友情链接 - PUT
router.put('/links/:id', ensureAuthenticated, ensureAdmin, async (req, res) => {
try {
const { name, url, description, order } = req.body;
const updatedLink = await Link.findByIdAndUpdate(
req.params.id,
{
name,
url,
description: description || '',
order: order || 0
},
{ new: true }
);
if (!updatedLink) {
return res.status(404).json({ success: false, message: '友情链接未找到' });
}
res.json({ success: true, link: updatedLink });
} catch (err) {
console.error(err);
res.status(500).json({ success: false, message: '更新友情链接失败' });
}
});
// 删除友情链接 - DELETE
router.delete('/links/:id', ensureAuthenticated, ensureAdmin, async (req, res) => {
try {
const deletedLink = await Link.findByIdAndDelete(req.params.id);
if (!deletedLink) {
return res.status(404).json({ success: false, message: '友情链接未找到' });
}
res.json({ success: true });
} catch (err) {
console.error(err);
res.status(500).json({ success: false, message: '删除友情链接失败' });
}
});
module.exports = router;

144
routes/user/auth.js Normal file
View File

@ -0,0 +1,144 @@
const express = require('express');
const router = express.Router();
const authController = require('../../controllers/user/auth');
const Post = require('../../models/Post');
const Comment = require('../../models/Comment');
const User = require('../../models/User');
// 文章详情页
// 文章详情页
router.get('/posts/:id', async (req, res) => {
try {
const post = await Post.findOne({ _id: req.params.id, isPublished: true });
if (!post) {
return res.status(404).render('user/info', {
post: null,
user: req.session.user || null,
message: '文章不存在或已被删除'
});
}
res.render('user/info', {
post: post, // 确保传递的是查询到的文章
user: req.session.user || null
});
} catch (err) {
console.error('获取文章详情出错:', err);
res.status(500).render('user/info', {
post: null,
user: req.session.user || null,
message: '服务器错误,请稍后再试'
});
}
});
// 注册页面
router.get('/register', (req, res) => {
res.render('user/register', { error: null });
});
// 注册提交
router.post('/register', authController.register);
// 登录页面
router.get('/login', (req, res) => {
res.render('user/login', { error: null });
});
// 登录提交
router.post('/login', authController.login);
// 登出
router.get('/logout', authController.logout);
// 获取用户资料(包括收藏列表)
router.get('/user/profile', async (req, res) => {
if (!req.session.user) {
return res.status(401).json({ success: false, message: '请先登录' });
}
try {
const user = await User.findById(req.session.user._id).populate('favorites');
res.json({
success: true,
user: {
_id: user._id,
username: user.username,
role: user.role
},
favorites: user.favorites || []
});
} catch (err) {
console.error('获取用户资料失败:', err);
res.status(500).json({ success: false, message: '获取用户资料失败' });
}
});
// 收藏文章
router.post('/posts/:id/favorite', async (req, res) => {
if (!req.session.user) return res.status(401).json({ success: false, message: '请先登录' });
try {
await User.findByIdAndUpdate(req.session.user._id, { $addToSet: { favorites: req.params.id } });
res.json({ success: true });
} catch (err) {
res.status(500).json({ success: false, message: '收藏失败' });
}
});
// 取消收藏
router.post('/posts/:id/unfavorite', async (req, res) => {
if (!req.session.user) return res.status(401).json({ success: false, message: '请先登录' });
try {
await User.findByIdAndUpdate(req.session.user._id, { $pull: { favorites: req.params.id } });
res.json({ success: true });
} catch (err) {
res.status(500).json({ success: false, message: '取消收藏失败' });
}
});
// 获取评论列表
router.get('/posts/:id/comments', async (req, res) => {
try {
const comments = await Comment.find({ post: req.params.id }).populate('author', 'username').sort({ createdAt: -1 });
res.json({ success: true, comments });
} catch (err) {
res.status(500).json({ success: false, message: '获取评论失败' });
}
});
// 发表评论
router.post('/posts/:id/comments', async (req, res) => {
if (!req.session.user) return res.status(401).json({ success: false, message: '请先登录' });
const { content } = req.body;
if (!content || !content.trim()) return res.status(400).json({ success: false, message: '评论内容不能为空' });
try {
const comment = new Comment({
content,
author: req.session.user._id,
post: req.params.id
});
await comment.save();
res.json({ success: true, comment });
} catch (err) {
res.status(500).json({ success: false, message: '发表评论失败' });
}
});
// 用户个人中心页面
router.get('/userInfo', async (req, res) => {
if (!req.session.user) {
return res.redirect('/login');
}
try {
const user = await User.findById(req.session.user._id).populate('favorites');
// 获取用户所有评论
const comments = await Comment.find({ author: req.session.user._id }).populate('post', 'title');
res.render('user/userInfo', {
user,
favorites: user.favorites || [],
comments: comments || []
});
} catch (err) {
console.error('获取个人中心信息失败:', err);
res.status(500).render('user/userInfo', { user: null, favorites: [], comments: [], error: '服务器错误' });
}
});
// 修改密码
router.post('/user/changePassword', authController.changePassword);
module.exports = router;

48
test_links.js Normal file
View File

@ -0,0 +1,48 @@
const fetch = require('node-fetch');
// 测试友情链接API
async function testLinksAPI() {
const baseURL = 'http://localhost:3001';
console.log('开始测试友情链接API...\n');
try {
// 测试添加友情链接
console.log('1. 测试添加友情链接...');
const addResponse = await fetch(`${baseURL}/admin/links`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: '测试链接',
url: 'https://example.com',
description: '这是一个测试链接',
order: 1
})
});
if (addResponse.ok) {
const addResult = await addResponse.json();
console.log('✅ 添加成功:', addResult);
} else {
console.log('❌ 添加失败:', addResponse.status, addResponse.statusText);
}
// 测试获取友情链接列表
console.log('\n2. 测试获取友情链接列表...');
const getResponse = await fetch(`${baseURL}/admin/links`);
if (getResponse.ok) {
console.log('✅ 获取成功');
} else {
console.log('❌ 获取失败:', getResponse.status, getResponse.statusText);
}
} catch (error) {
console.error('❌ 测试失败:', error.message);
}
}
// 运行测试
testLinksAPI();

55
test_pagination.js Normal file
View File

@ -0,0 +1,55 @@
const { paginateQuery } = require('./utils/pagination');
const mongoose = require('mongoose');
const Post = require('./models/Post');
// 连接数据库
mongoose.connect('mongodb://127.0.0.1:27017/blog', {
useNewUrlParser: true,
useUnifiedTopology: true
})
.then(() => {
console.log('数据库连接成功');
testPagination();
})
.catch(err => {
console.error('数据库连接失败:', err);
process.exit(1);
});
async function testPagination() {
try {
console.log('开始测试分页功能...\n');
// 测试分页查询
const page = 1;
const limit = 5;
console.log(`测试参数: 页码=${page}, 每页数量=${limit}`);
const result = await paginateQuery(
Post,
{},
{ sort: { createdAt: -1 } },
page,
limit
);
console.log('分页结果:');
console.log('- 当前页:', result.pagination.currentPage);
console.log('- 总页数:', result.pagination.totalPages);
console.log('- 总记录数:', result.pagination.total);
console.log('- 当前页记录数:', result.data.length);
console.log('- 是否有下一页:', result.pagination.hasNext);
console.log('- 是否有上一页:', result.pagination.hasPrev);
console.log('\n文章列表:');
result.data.forEach((post, index) => {
console.log(`${index + 1}. ${post.title} (${post.author})`);
});
mongoose.connection.close();
} catch (error) {
console.error('测试失败:', error);
mongoose.connection.close();
}
}

View File

@ -0,0 +1,71 @@
const ejs = require('ejs');
const path = require('path');
const fs = require('fs');
// 测试分页组件是否能正确渲染
function testPaginationComponent() {
console.log('测试分页组件...\n');
try {
// 读取分页组件文件
const paginationPath = path.join(__dirname, 'views', 'components', 'pagination.ejs');
if (fs.existsSync(paginationPath)) {
console.log('✅ 分页组件文件存在');
// 读取组件内容
const componentContent = fs.readFileSync(paginationPath, 'utf8');
console.log('✅ 分页组件文件读取成功');
// 测试数据
const testData = {
currentPage: 3,
totalPages: 10,
baseUrl: '/admin/posts',
query: { search: 'test' }
};
// 尝试渲染组件
const rendered = ejs.render(componentContent, testData);
console.log('✅ 分页组件渲染成功');
// 检查渲染结果
if (rendered.includes('pagination')) {
console.log('✅ 分页组件包含正确的HTML结构');
}
console.log('\n分页组件测试通过');
} else {
console.log('❌ 分页组件文件不存在');
}
} catch (error) {
console.error('❌ 测试失败:', error.message);
}
}
// 测试路径解析
function testPathResolution() {
console.log('\n测试路径解析...\n');
const paths = [
'views/components/pagination.ejs',
'views/admin/users/list.ejs',
'views/admin/posts/list.ejs',
'views/admin/comments/list.ejs'
];
paths.forEach(filePath => {
const fullPath = path.join(__dirname, filePath);
if (fs.existsSync(fullPath)) {
console.log(`${filePath} 存在`);
} else {
console.log(`${filePath} 不存在`);
}
});
}
// 运行测试
testPaginationComponent();
testPathResolution();

0
uploads/app.js Normal file
View File

0
uploads/config.js Normal file
View File

81
utils/pagination.js Normal file
View File

@ -0,0 +1,81 @@
/**
* 分页工具函数
*/
/**
* 计算分页信息
* @param {number} total - 总记录数
* @param {number} page - 当前页码
* @param {number} limit - 每页记录数
* @returns {object} 分页信息对象
*/
function getPaginationInfo(total, page = 1, limit = 10) {
const totalPages = Math.ceil(total / limit);
const currentPage = Math.max(1, Math.min(page, totalPages));
const skip = (currentPage - 1) * limit;
return {
currentPage,
totalPages,
limit,
skip,
total,
hasNext: currentPage < totalPages,
hasPrev: currentPage > 1
};
}
/**
* 为MongoDB查询添加分页
* @param {object} query - MongoDB查询对象
* @param {number} page - 当前页码
* @param {number} limit - 每页记录数
* @returns {object} 包含分页信息的查询对象
*/
function addPaginationToQuery(query, page = 1, limit = 10) {
const paginationInfo = getPaginationInfo(query.total || 0, page, limit);
return {
...query,
skip: paginationInfo.skip,
limit: paginationInfo.limit,
pagination: paginationInfo
};
}
/**
* 执行分页查询
* @param {object} model - Mongoose模型
* @param {object} filter - 查询条件
* @param {object} options - 查询选项
* @param {number} page - 当前页码
* @param {number} limit - 每页记录数
* @returns {Promise<object>} 包含数据和分页信息的对象
*/
async function paginateQuery(model, filter = {}, options = {}, page = 1, limit = 10) {
const paginationInfo = getPaginationInfo(0, page, limit);
// 获取总记录数
const total = await model.countDocuments(filter);
// 更新分页信息
const updatedPagination = getPaginationInfo(total, page, limit);
// 执行查询
const data = await model.find(filter, null, {
...options,
skip: updatedPagination.skip,
limit: updatedPagination.limit
});
return {
data,
pagination: updatedPagination
};
}
module.exports = {
getPaginationInfo,
addPaginationToQuery,
paginateQuery
};

View File

@ -0,0 +1,143 @@
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">分类管理</h5>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addCategoryModal">
<i class="bi bi-plus"></i> 添加分类
</button>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>排序</th>
<th>分类名称</th>
<th>别名</th>
<th>文章数</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<% categories.forEach(category => { %>
<tr data-id="<%= category._id %>">
<td><%= category.order %></td>
<td>
<% if (category.isTop) { %>
<span class="badge bg-danger me-1">置顶</span>
<% } %>
<%= category.name %>
</td>
<td><%= category.slug %></td>
<td><%= category.postCount || 0 %></td>
<td>
<% if (category.isActive) { %>
<span class="badge bg-success">启用</span>
<% } else { %>
<span class="badge bg-secondary">禁用</span>
<% } %>
</td>
<td class="action-btns">
<button class="btn btn-sm btn-primary edit-btn" data-id="<%= category._id %>">编辑</button>
<button class="btn btn-sm btn-danger delete-btn" data-id="<%= category._id %>">删除</button>
<% if (category.isTop) { %>
<button class="btn btn-sm btn-secondary cancel-top-btn" data-id="<%= category._id %>">取消置顶</button>
<% } else { %>
<button class="btn btn-sm btn-warning set-top-btn" data-id="<%= category._id %>">设为置顶</button>
<% } %>
</td>
</tr>
<% }); %>
</tbody>
</table>
</div>
</div>
</div>
<!-- 添加分类模态框 -->
<div class="modal fade" id="addCategoryModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">添加分类</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form id="addCategoryForm" action="/admin/categories" method="POST">
<div class="modal-body">
<div class="mb-3">
<label class="form-label">分类名称</label>
<input type="text" class="form-control" name="name" required>
</div>
<div class="mb-3">
<label class="form-label">别名</label>
<input type="text" class="form-control" name="slug" required>
</div>
<div class="mb-3">
<label class="form-label">排序值</label>
<input type="number" class="form-control" name="order" value="0">
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" name="isTop" id="isTop">
<label class="form-check-label" for="isTop">置顶分类</label>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="submit" class="btn btn-primary">保存</button>
</div>
</form>
</div>
</div>
</div>
<script>
// 分类置顶/取消置顶
document.querySelectorAll('.set-top-btn, .cancel-top-btn').forEach(btn => {
btn.addEventListener('click', async function() {
const categoryId = this.getAttribute('data-id');
const isTop = this.classList.contains('set-top-btn');
try {
const response = await fetch(`/admin/categories/${categoryId}/top`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ isTop })
});
if (response.ok) {
location.reload();
} else {
alert('操作失败');
}
} catch (error) {
console.error(error);
alert('操作失败');
}
});
});
// 删除分类
document.querySelectorAll('.delete-btn').forEach(btn => {
btn.addEventListener('click', function() {
const categoryId = this.getAttribute('data-id');
if (confirm('确定要删除这个分类吗?')) {
fetch(`/admin/categories/${categoryId}`, {
method: 'DELETE'
})
.then(response => {
if (response.ok) {
location.reload();
} else {
alert('删除失败');
}
})
.catch(error => {
console.error(error);
alert('删除失败');
});
}
});
});
</script>

View File

@ -0,0 +1,67 @@
<div class="card">
<div class="card-header">
<h5 class="mb-0">评论管理</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>评论内容</th>
<th>用户</th>
<th>文章</th>
<th>评论时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<% comments.forEach(comment => { %>
<tr>
<td><%= comment.content %></td>
<td><%= comment.author ? comment.author.username : '未知用户' %></td>
<td><%= comment.post ? comment.post.title : '未知文章' %></td>
<td><%= new Date(comment.createdAt).toLocaleString() %></td>
<td>
<button class="btn btn-sm btn-danger delete-comment-btn" data-id="<%= comment._id %>">删除</button>
</td>
</tr>
<% }); %>
</tbody>
</table>
</div>
<!-- 分页组件 -->
<%- include('../../components/pagination', {
currentPage: pagination.currentPage,
totalPages: pagination.totalPages,
baseUrl: baseUrl,
query: query
}) %>
</div>
</div>
<script>
// 删除评论
document.querySelectorAll('.delete-comment-btn').forEach(btn => {
btn.addEventListener('click', function() {
const commentId = this.getAttribute('data-id');
if (confirm('确定要删除这条评论吗?')) {
fetch(`/admin/comments/${commentId}`, {
method: 'DELETE'
})
.then(response => response.json())
.then(result => {
if (result.success) {
location.reload();
} else {
alert('删除失败');
}
})
.catch(error => {
console.error(error);
alert('删除失败');
});
}
});
});
</script>

163
views/admin/index.ejs Normal file
View File

@ -0,0 +1,163 @@
<!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 href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.bootcdn.net/ajax/libs/bootstrap-icons/1.10.0/font/bootstrap-icons.min.css" rel="stylesheet">
<style>
body {
font-family: 'Microsoft YaHei', sans-serif;
background-color: #f8f9fa;
}
.sidebar {
width: 250px;
min-height: 100vh;
background: #343a40;
color: white;
position: fixed;
transition: all 0.3s;
}
.sidebar-header {
padding: 20px;
background: #212529;
}
.sidebar-menu {
padding: 0;
list-style: none;
}
.sidebar-menu li {
padding: 10px 20px;
border-bottom: 1px solid #4b545c;
}
.sidebar-menu li a {
color: #adb5bd;
text-decoration: none;
display: block;
}
.sidebar-menu li a:hover, .sidebar-menu li.active a {
color: white;
}
.sidebar-menu li a i {
margin-right: 10px;
}
.main-content {
margin-left: 250px;
padding: 20px;
transition: all 0.3s;
}
</style>
</head>
<body>
<div class="sidebar">
<div class="sidebar-header">
<h4>博客后台管理系统</h4>
<p class="mb-0">欢迎, <%= user %></p>
</div>
<ul class="sidebar-menu">
<li class="active">
<a href="/admin"><i class="bi bi-speedometer2"></i> 控制面板</a>
</li>
<li>
<a href="/admin/users"><i class="bi bi-people"></i> 用户管理</a>
</li>
<!-- <li>-->
<!-- <a href="/admin/categories"><i class="bi bi-tags"></i> 分类管理</a>-->
<!-- </li>-->
<li>
<a href="/admin/posts"><i class="bi bi-file-earmark-text"></i> 文章管理</a>
</li>
<li>
<a href="/admin/comments"><i class="bi bi-chat-left-text"></i> 评论管理</a>
</li>
<li>
<a href="/admin/links"><i class="bi bi-link-45deg"></i> 友情链接</a>
</li>
<li>
<a href="/logout"><i class="bi bi-box-arrow-right"></i> 退出登录</a>
</li>
</ul>
</div>
<div class="main-content">
<% if (template !== 'index') { %>
<%- include(`./${template}`) %>
<% } else { %>
<div class="container mt-5">
<div class="p-5 mb-4 bg-light rounded-3">
<div class="container-fluid py-5">
<h1 class="display-5 fw-bold">欢迎来到博客后台管理系统!</h1>
<p class="col-md-8 fs-4">在这里你可以高效地管理用户、文章、评论、分类和友情链接。</p>
</div>
</div>
<div class="row g-4">
<div class="col-md-3">
<a href="/admin/users" class="text-decoration-none">
<div class="card text-center h-100">
<div class="card-body">
<i class="bi bi-people display-4"></i>
<h5 class="card-title mt-2">用户管理</h5>
</div>
</div>
</a>
</div>
<div class="col-md-3">
<a href="/admin/posts" class="text-decoration-none">
<div class="card text-center h-100">
<div class="card-body">
<i class="bi bi-file-earmark-text display-4"></i>
<h5 class="card-title mt-2">文章管理</h5>
</div>
</div>
</a>
</div>
<div class="col-md-3">
<a href="/admin/comments" class="text-decoration-none">
<div class="card text-center h-100">
<div class="card-body">
<i class="bi bi-chat-left-text display-4"></i>
<h5 class="card-title mt-2">评论管理</h5>
</div>
</div>
</a>
</div>
<div class="col-md-3">
<a href="/admin/links" class="text-decoration-none">
<div class="card text-center h-100">
<div class="card-body">
<i class="bi bi-link-45deg display-4"></i>
<h5 class="card-title mt-2">友情链接</h5>
</div>
</div>
</a>
</div>
</div>
</div>
<% } %>
</div>
<script src="https://cdn.bootcdn.net/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script>
<script>
// 当前菜单高亮
document.addEventListener('DOMContentLoaded', function () {
const currentUrl = window.location.pathname;
const menuItems = document.querySelectorAll('.sidebar-menu li a');
menuItems.forEach(item => {
if (item.getAttribute('href') === currentUrl) {
item.parentElement.classList.add('active');
}
});
});
</script>
</body>
</html>

141
views/admin/links/list.ejs Normal file
View File

@ -0,0 +1,141 @@
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">友情链接管理</h5>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addLinkModal">
<i class="bi bi-plus"></i> 添加链接
</button>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>排序</th>
<th>网站名称</th>
<th>URL</th>
<th>描述</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<% links.forEach(link => { %>
<tr data-id="<%= link._id %>">
<td><%= link.order || 0 %></td>
<td><%= link.name %></td>
<td><a href="<%= link.url %>" target="_blank"><%= link.url %></a></td>
<td><%= link.description || '' %></td>
<td class="action-btns">
<button class="btn btn-sm btn-primary edit-link-btn" data-id="<%= link._id %>">编辑</button>
<button class="btn btn-sm btn-danger delete-link-btn" data-id="<%= link._id %>">删除</button>
</td>
</tr>
<% }); %>
</tbody>
</table>
</div>
<!-- 分页组件 -->
<%- include('../../components/pagination', {
currentPage: pagination.currentPage,
totalPages: pagination.totalPages,
baseUrl: baseUrl,
query: query
}) %>
</div>
</div>
<!-- 添加链接模态框 -->
<div class="modal fade" id="addLinkModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">添加友情链接</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form id="addLinkForm">
<div class="modal-body">
<div class="mb-3">
<label class="form-label">网站名称</label>
<input type="text" class="form-control" name="name" required>
</div>
<div class="mb-3">
<label class="form-label">URL</label>
<input type="url" class="form-control" name="url" required>
</div>
<div class="mb-3">
<label class="form-label">描述</label>
<textarea class="form-control" name="description" rows="3"></textarea>
</div>
<div class="mb-3">
<label class="form-label">排序值</label>
<input type="number" class="form-control" name="order" value="0">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="submit" class="btn btn-primary">保存</button>
</div>
</form>
</div>
</div>
</div>
<script>
// 添加友情链接
document.getElementById('addLinkForm').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
const data = {
name: formData.get('name'),
url: formData.get('url'),
description: formData.get('description'),
order: parseInt(formData.get('order')) || 0
};
fetch('/admin/links', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(result => {
if (result.success) {
alert('添加成功!');
location.reload();
} else {
alert('添加失败:' + result.message);
}
})
.catch(error => {
console.error(error);
alert('添加失败');
});
});
// 删除链接
document.querySelectorAll('.delete-link-btn').forEach(btn => {
btn.addEventListener('click', function() {
const linkId = this.getAttribute('data-id');
if (confirm('确定要删除这个链接吗?')) {
fetch(`/admin/links/${linkId}`, {
method: 'DELETE'
})
.then(response => response.json())
.then(result => {
if (result.success) {
alert('删除成功!');
location.reload();
} else {
alert('删除失败:' + result.message);
}
})
.catch(error => {
console.error(error);
alert('删除失败');
});
}
});
});
</script>

380
views/admin/posts/list.ejs Normal file
View File

@ -0,0 +1,380 @@
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">文章管理</h5>
<div>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addPostModal">
<i class="bi bi-plus"></i> 添加文章
</button>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>标题</th>
<th>分类</th>
<th>作者</th>
<th>发布时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<% posts.forEach(post => { %>
<tr>
<td>
<% if (post.isTop) { %>
<span class="badge bg-danger me-1">置顶</span>
<% } %>
<%= post.title %>
</td>
<td><%= post.category %></td>
<td><%= post.author %></td>
<td><%= new Date(post.createdAt).toLocaleString() %></td>
<td class="action-btns">
<button class="btn btn-sm btn-primary edit-post-btn" data-id="<%= post._id %>">编辑</button>
<button class="btn btn-sm btn-danger delete-post-btn" data-id="<%= post._id %>">删除
</button>
<% if (post.isTop) { %>
<button class="btn btn-sm btn-secondary cancel-post-top-btn" data-id="<%= post._id %>">
取消置顶
</button>
<% } else { %>
<button class="btn btn-sm btn-warning set-post-top-btn" data-id="<%= post._id %>">
设为置顶
</button>
<% } %>
<% if (post.isPublished) { %>
<button class="btn btn-sm btn-secondary cancel-publish-btn" data-id="<%= post._id %>">
取消发布
</button>
<% } else { %>
<button class="btn btn-sm btn-success publish-btn" data-id="<%= post._id %>">发布
</button>
<% } %>
</td>
</tr>
<% }); %>
</tbody>
</table>
</div>
<!-- 分页组件 -->
<%- include('../../components/pagination', {
currentPage: pagination.currentPage,
totalPages: pagination.totalPages,
baseUrl: baseUrl,
query: query
}) %>
</div>
</div>
<!-- 添加文章模态框 -->
<div class="modal fade" id="addPostModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">添加文章</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form id="addPostForm">
<div class="modal-body">
<div class="mb-3">
<label class="form-label">文章标题</label>
<input type="text" class="form-control" name="title" required>
</div>
<div class="mb-3">
<label class="form-label">文章内容</label>
<textarea class="form-control" name="content" rows="10" required></textarea>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">分类</label>
<select class="form-control" name="category" required>
<option value="">请选择分类</option>
<option value="技术">技术</option>
<option value="随笔">随笔</option>
<option value="教程">教程</option>
<option value="数据库">数据库</option>
<option value="工具">工具</option>
<option value="未分类">未分类</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">作者</label>
<input type="text" class="form-control" name="author"
value="<%= user ? user.username : '' %>" required>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" name="isPublished" id="isPublished"
checked>
<label class="form-check-label" for="isPublished">立即发布</label>
</div>
</div>
<div class="col-md-6">
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" name="isTop" id="isTop">
<label class="form-check-label" for="isTop">置顶文章</label>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="submit" class="btn btn-primary">保存</button>
</div>
</form>
</div>
</div>
</div>
<!-- 编辑文章模态框 -->
<div class="modal fade" id="editPostModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">编辑文章</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form id="editPostForm">
<input type="hidden" name="postId" id="editPostId">
<div class="modal-body">
<div class="mb-3">
<label class="form-label">文章标题</label>
<input type="text" class="form-control" name="title" id="editTitle" required>
</div>
<div class="mb-3">
<label class="form-label">文章内容</label>
<textarea class="form-control" name="content" id="editContent" rows="10" required></textarea>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">分类</label>
<select class="form-control" name="category" id="editCategory" required>
<option value="">请选择分类</option>
<option value="技术">技术</option>
<option value="随笔">随笔</option>
<option value="教程">教程</option>
<option value="数据库">数据库</option>
<option value="工具">工具</option>
<option value="未分类">未分类</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">作者</label>
<input type="text" class="form-control" name="author" id="editAuthor" required>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" name="isPublished" id="editIsPublished">
<label class="form-check-label" for="editIsPublished">发布文章</label>
</div>
</div>
<div class="col-md-6">
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" name="isTop" id="editIsTop">
<label class="form-check-label" for="editIsTop">置顶文章</label>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="submit" class="btn btn-primary">保存</button>
</div>
</form>
</div>
</div>
</div>
<script>
// 添加文章
document.getElementById('addPostForm').addEventListener('submit', function (e) {
e.preventDefault();
const formData = new FormData(this);
const data = {
title: formData.get('title'),
content: formData.get('content'),
category: formData.get('category'),
author: formData.get('author'),
isPublished: formData.get('isPublished') === 'on',
isTop: formData.get('isTop') === 'on'
};
fetch('/admin/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(result => {
if (result.success) {
alert('添加成功!');
location.reload();
} else {
alert('添加失败:' + result.message);
}
})
.catch(error => {
console.error(error);
alert('添加失败');
});
});
// 编辑文章
document.querySelectorAll('.edit-post-btn').forEach(btn => {
btn.addEventListener('click', function () {
const postId = this.getAttribute('data-id');
// 获取文章数据
fetch(`/admin/posts/${postId}`)
.then(response => response.json())
.then(post => {
// 填充表单
document.getElementById('editPostId').value = post._id;
document.getElementById('editTitle').value = post.title;
document.getElementById('editContent').value = post.content;
document.getElementById('editCategory').value = post.category;
document.getElementById('editAuthor').value = post.author;
document.getElementById('editIsPublished').checked = post.isPublished;
document.getElementById('editIsTop').checked = post.isTop;
// 显示模态框
const editModal = new bootstrap.Modal(document.getElementById('editPostModal'));
editModal.show();
})
.catch(error => {
console.error(error);
alert('获取文章信息失败');
});
});
});
// 提交编辑表单
document.getElementById('editPostForm').addEventListener('submit', function (e) {
e.preventDefault();
const formData = new FormData(this);
const postId = formData.get('postId');
const data = {
title: formData.get('title'),
content: formData.get('content'),
category: formData.get('category'),
author: formData.get('author'),
isPublished: formData.get('isPublished') === 'on',
isTop: formData.get('isTop') === 'on'
};
fetch(`/admin/posts/${postId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(result => {
if (result.success) {
alert('更新成功!');
location.reload();
} else {
alert('更新失败:' + result.message);
}
})
.catch(error => {
console.error(error);
alert('更新失败');
});
});
// 删除文章
document.querySelectorAll('.delete-post-btn').forEach(btn => {
btn.addEventListener('click', function () {
const postId = this.getAttribute('data-id');
if (confirm('确定要删除这篇文章吗?')) {
fetch(`/admin/posts/${postId}`, {
method: 'DELETE'
})
.then(response => response.json())
.then(result => {
if (result.success) {
location.reload();
} else {
alert('删除失败');
}
})
.catch(error => {
console.error(error);
alert('删除失败');
});
}
});
});
// 设置/取消置顶
document.querySelectorAll('.set-post-top-btn, .cancel-post-top-btn').forEach(btn => {
btn.addEventListener('click', function () {
const postId = this.getAttribute('data-id');
const isTop = this.classList.contains('set-post-top-btn');
fetch(`/admin/posts/${postId}/top`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({isTop})
})
.then(response => response.json())
.then(result => {
if (result.success) {
location.reload();
} else {
alert('操作失败');
}
})
.catch(error => {
console.error(error);
alert('操作失败');
});
});
});
// 发布/取消发布
document.querySelectorAll('.publish-btn, .cancel-publish-btn').forEach(btn => {
btn.addEventListener('click', function () {
const postId = this.getAttribute('data-id');
const isPublished = this.classList.contains('publish-btn');
fetch(`/admin/posts/${postId}/publish`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({isPublished})
})
.then(response => response.json())
.then(result => {
if (result.success) {
location.reload();
} else {
alert('操作失败');
}
})
.catch(error => {
console.error(error);
alert('操作失败');
});
});
});
</script>

102
views/admin/users/list.ejs Normal file
View File

@ -0,0 +1,102 @@
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">用户列表</h5>
<div>
<input type="text" class="form-control" placeholder="搜索用户..." id="searchUser">
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>ID</th>
<th>用户名</th>
<th>邮箱</th>
<th>注册时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<% users.forEach(user => { %>
<tr>
<td><%= user._id %></td>
<td><%= user.username %></td>
<td><%= user.email %></td>
<td><%= new Date(user.createdAt).toLocaleString() %></td>
<td class="action-btns">
<button class="btn btn-sm btn-info reset-pwd-btn" data-id="<%= user._id %>">重置密码</button>
</td>
</tr>
<% }); %>
</tbody>
</table>
</div>
<!-- 分页组件 -->
<%- include('../../components/pagination', {
currentPage: pagination.currentPage,
totalPages: pagination.totalPages,
baseUrl: baseUrl,
query: query
}) %>
</div>
</div>
<script>
// 冻结/解冻用户
document.querySelectorAll('.freeze-btn, .activate-btn').forEach(btn => {
btn.addEventListener('click', async function() {
const userId = this.getAttribute('data-id');
const isFreeze = this.classList.contains('freeze-btn');
try {
const response = await fetch(`/admin/users/${userId}/status`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ isActive: !isFreeze })
});
if (response.ok) {
location.reload();
} else {
alert('操作失败');
}
} catch (error) {
console.error(error);
alert('操作失败');
}
});
});
// 重置密码
document.querySelectorAll('.reset-pwd-btn').forEach(btn => {
btn.addEventListener('click', function() {
const userId = this.getAttribute('data-id');
const newPassword = prompt('请输入新密码:');
if (newPassword) {
fetch(`/admin/users/${userId}/password`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ newPassword })
})
.then(response => response.json())
.then(result => {
if (result.success) {
alert('密码重置成功');
} else {
alert('密码重置失败');
}
})
.catch(error => {
console.error(error);
alert('密码重置失败');
});
}
});
});
</script>

View File

@ -0,0 +1,139 @@
<%
// 分页组件参数
// currentPage: 当前页码
// totalPages: 总页数
// baseUrl: 基础URL
// query: 查询参数对象
%>
<div class="pagination-wrapper">
<nav aria-label="分页导航">
<ul class="pagination justify-content-center">
<!-- 上一页 -->
<% if (currentPage > 1) { %>
<li class="page-item">
<a class="page-link" href="<%= baseUrl %>?page=<%= currentPage - 1 %><%= query ? '&' + Object.keys(query).map(key => key + '=' + query[key]).join('&') : '' %>" aria-label="上一页">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
<% } else { %>
<li class="page-item disabled">
<span class="page-link" aria-label="上一页">
<span aria-hidden="true">&laquo;</span>
</span>
</li>
<% } %>
<!-- 页码 -->
<%
let startPage = Math.max(1, currentPage - 2);
let endPage = Math.min(totalPages, currentPage + 2);
// 确保显示5个页码
if (endPage - startPage < 4) {
if (startPage === 1) {
endPage = Math.min(totalPages, startPage + 4);
} else {
startPage = Math.max(1, endPage - 4);
}
}
%>
<!-- 第一页 -->
<% if (startPage > 1) { %>
<li class="page-item">
<a class="page-link" href="<%= baseUrl %>?page=1<%= query ? '&' + Object.keys(query).map(key => key + '=' + query[key]).join('&') : '' %>">1</a>
</li>
<% if (startPage > 2) { %>
<li class="page-item disabled">
<span class="page-link">...</span>
</li>
<% } %>
<% } %>
<!-- 中间页码 -->
<% for (let i = startPage; i <= endPage; i++) { %>
<% if (i === currentPage) { %>
<li class="page-item active">
<span class="page-link"><%= i %></span>
</li>
<% } else { %>
<li class="page-item">
<a class="page-link" href="<%= baseUrl %>?page=<%= i %><%= query ? '&' + Object.keys(query).map(key => key + '=' + query[key]).join('&') : '' %>"><%= i %></a>
</li>
<% } %>
<% } %>
<!-- 最后一页 -->
<% if (endPage < totalPages) { %>
<% if (endPage < totalPages - 1) { %>
<li class="page-item disabled">
<span class="page-link">...</span>
</li>
<% } %>
<li class="page-item">
<a class="page-link" href="<%= baseUrl %>?page=<%= totalPages %><%= query ? '&' + Object.keys(query).map(key => key + '=' + query[key]).join('&') : '' %>"><%= totalPages %></a>
</li>
<% } %>
<!-- 下一页 -->
<% if (currentPage < totalPages) { %>
<li class="page-item">
<a class="page-link" href="<%= baseUrl %>?page=<%= currentPage + 1 %><%= query ? '&' + Object.keys(query).map(key => key + '=' + query[key]).join('&') : '' %>" aria-label="下一页">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
<% } else { %>
<li class="page-item disabled">
<span class="page-link" aria-label="下一页">
<span aria-hidden="true">&raquo;</span>
</span>
</li>
<% } %>
</ul>
</nav>
<!-- 分页信息 -->
<div class="pagination-info text-center text-muted mt-2">
第 <%= currentPage %> 页,共 <%= totalPages %> 页
</div>
</div>
<style>
.pagination-wrapper {
margin-top: 30px;
}
.pagination {
margin-bottom: 0;
}
.page-link {
color: #3498db;
border-color: #dee2e6;
transition: all 0.3s ease;
}
.page-link:hover {
color: #2980b9;
background-color: #f8f9fa;
border-color: #3498db;
}
.page-item.active .page-link {
background-color: #3498db;
border-color: #3498db;
color: white;
}
.page-item.disabled .page-link {
color: #6c757d;
background-color: #fff;
border-color: #dee2e6;
}
.pagination-info {
font-size: 14px;
}
</style>

304
views/user/index.ejs Normal file
View File

@ -0,0 +1,304 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>我的博客 - 首页</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
}
body {
background-color: #f8f9fa;
color: #333;
line-height: 1.6;
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 0;
border-bottom: 1px solid #eaeaea;
margin-bottom: 30px;
}
h2 {
color: #2c3e50;
font-size: 28px;
font-weight: 600;
}
.auth-links {
display: flex;
gap: 15px;
}
.auth-links a {
color: #3498db;
text-decoration: none;
font-weight: 500;
padding: 8px 12px;
border-radius: 4px;
transition: all 0.3s ease;
}
.auth-links a:hover {
background-color: #f0f7fd;
}
.welcome-message {
display: flex;
align-items: center;
gap: 10px;
color: #2c3e50;
font-weight: 500;
}
.welcome-message a {
color: #e74c3c;
text-decoration: none;
font-size: 14px;
padding: 5px 10px;
border-radius: 4px;
transition: all 0.3s ease;
}
.welcome-message a:hover {
background-color: #fdeaea;
}
.main-content {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 30px;
margin-top: 30px;
}
.posts-section {
background: white;
border-radius: 8px;
padding: 25px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.posts-section h3 {
color: #2c3e50;
font-size: 24px;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #3498db;
}
.post-item {
margin-bottom: 25px;
padding-bottom: 20px;
border-bottom: 1px solid #ecf0f1;
}
.post-item:last-child {
border-bottom: none;
margin-bottom: 0;
}
.post-title {
font-size: 18px;
font-weight: 600;
color: #2c3e50;
margin-bottom: 8px;
text-decoration: none;
display: block;
transition: color 0.3s ease;
}
.post-title:hover {
color: #3498db;
}
.post-meta {
font-size: 14px;
color: #7f8c8d;
margin-bottom: 10px;
display: flex;
gap: 15px;
}
.post-content {
color: #555;
line-height: 1.6;
margin-bottom: 10px;
}
.post-excerpt {
color: #666;
font-size: 14px;
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.links-section {
background: white;
border-radius: 8px;
padding: 25px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
height: fit-content;
}
.links-section h3 {
color: #2c3e50;
font-size: 20px;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #e74c3c;
}
.link-list {
list-style: none;
}
.link-item {
margin-bottom: 12px;
}
.link-item a {
color: #3498db;
text-decoration: none;
font-size: 14px;
padding: 8px 12px;
display: block;
border-radius: 4px;
transition: all 0.3s ease;
border: 1px solid transparent;
}
.link-item a:hover {
background-color: #f0f7fd;
border-color: #3498db;
transform: translateX(5px);
}
.no-content {
text-align: center;
color: #7f8c8d;
font-style: italic;
padding: 40px 20px;
}
.read-more {
color: #3498db;
text-decoration: none;
font-size: 14px;
font-weight: 500;
transition: color 0.3s ease;
}
.read-more:hover {
color: #2980b9;
}
@media (max-width: 768px) {
header {
flex-direction: column;
align-items: flex-start;
gap: 15px;
}
.main-content {
grid-template-columns: 1fr;
gap: 20px;
}
.posts-section,
.links-section {
padding: 20px;
}
}
</style>
</head>
<body>
<%
// 安全地获取变量,提供默认值
var posts = locals.posts || [];
var links = locals.links || [];
var user = locals.user || null;
%>
<header>
<h2>欢迎来到博客系统</h2>
<% if (user) { %>
<div class="welcome-message">
<span>欢迎,<%= user.username %></span>
<a href="/userInfo">个人中心</a>
<a href="/logout">退出登录</a>
</div>
<% } else { %>
<div class="auth-links">
<a href="/login">登录</a>
<a href="/register">注册</a>
</div>
<% } %>
</header>
<div class="main-content">
<!-- 左侧:博客文章列表 -->
<div class="posts-section">
<h3>最新文章</h3>
<% if (posts && posts.length > 0) { %>
<% posts.forEach(function(post) { %>
<div class="post-item">
<a href="#" class="post-title"><%= post.title || '无标题' %></a>
<div class="post-meta">
<span>作者:<%= post.author || '未知作者' %></span>
<span>分类:<%= post.category || '未分类' %></span>
<span>发布时间:<%= new Date(post.createdAt).toLocaleDateString('zh-CN') %></span>
</div>
<div class="post-content">
<div class="post-excerpt">
<%
var content = post.content || '';
var excerpt = content.length > 150 ? content.substring(0, 150) + '...' : content;
%>
<%= excerpt %>
</div>
</div>
<a href="/posts/<%= post.id %>" class="read-more">阅读全文 →</a>
</div>
<% }); %>
<% } else { %>
<div class="no-content">
<p>暂无文章</p>
</div>
<% } %>
</div>
<!-- 右侧:友情链接 -->
<div class="links-section">
<h3>友情链接</h3>
<% if (links && links.length > 0) { %>
<ul class="link-list">
<% links.forEach(function(link) { %>
<li class="link-item">
<a href="<%= link.url %>" target="_blank" rel="noopener noreferrer">
<%= link.name %>
</a>
</li>
<% }); %>
</ul>
<% } else { %>
<div class="no-content">
<p>暂无友情链接</p>
</div>
<% } %>
</div>
</div>
</body>
</html>

348
views/user/info.ejs Normal file
View File

@ -0,0 +1,348 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>博客详情</title>
<style>
.post-detail {
max-width: 800px;
margin: 0 auto;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.post-header {
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #eee;
}
.post-title {
font-size: 24px;
color: #2c3e50;
margin-bottom: 10px;
}
.post-meta {
color: #7f8c8d;
font-size: 14px;
}
.post-content {
line-height: 1.8;
color: #333;
margin-bottom: 30px;
}
.back-link {
display: inline-block;
margin-top: 20px;
color: #3498db;
text-decoration: none;
}
/* 新增的收藏按钮样式 */
.action-buttons {
display: flex;
gap: 10px;
margin: 20px 0;
align-items: center;
}
.favorite-btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 5px;
}
.favorite-btn:hover {
transform: translateY(-2px);
}
.favorite-btn.favorited {
background-color: #f8d7da;
color: #721c24;
}
.favorite-btn.not-favorited {
background-color: #e2e3e5;
color: #383d41;
}
.favorite-btn i {
font-size: 14px;
}
/* 评论区域样式优化 */
#comments-section {
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid #eee;
}
#comments-section h3 {
font-size: 20px;
color: #2c3e50;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
}
.comment-item {
border-bottom: 1px solid #f0f0f0;
padding: 15px 0;
margin-bottom: 10px;
}
.comment-author {
font-weight: bold;
color: #3498db;
margin-right: 8px;
}
.comment-content {
margin: 8px 0;
line-height: 1.6;
color: #333;
}
.comment-meta {
font-size: 12px;
color: #95a5a6;
display: flex;
justify-content: space-between;
align-items: center;
}
.no-comments {
color: #95a5a6;
font-style: italic;
padding: 20px 0;
text-align: center;
}
/* 评论表单样式 */
#commentForm {
margin-top: 30px;
}
#commentContent {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 4px;
min-height: 100px;
font-family: inherit;
resize: vertical;
transition: border-color 0.3s;
}
#commentContent:focus {
border-color: #3498db;
outline: none;
box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
}
.submit-comment {
background-color: #3498db;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
transition: background-color 0.3s;
margin-top: 10px;
}
.submit-comment:hover {
background-color: #2980b9;
}
.login-prompt {
color: #7f8c8d;
text-align: center;
margin: 20px 0;
}
.login-prompt a {
color: #3498db;
text-decoration: none;
}
.login-prompt a:hover {
text-decoration: underline;
}
</style>
<!-- 引入Font Awesome图标库 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
</head>
<body>
<div class="post-detail">
<div class="post-header">
<h1 class="post-title"><%= post.title %></h1>
<div class="post-meta">
<span>作者:<%= post.author %></span> |
<span>分类:<%= post.category %></span> |
<span>发布时间:<%= new Date(post.createdAt).toLocaleDateString('zh-CN') %></span>
</div>
</div>
<div class="post-content">
<%= post.content %>
</div>
<div class="action-buttons">
<% if (user) { %>
<button id="favoriteBtn" class="favorite-btn">
<i class="far fa-heart"></i>
<span>收藏</span>
</button>
<% } %>
<a href="/" class="back-link">← 返回首页</a>
</div>
<div id="comments-section">
<h3><i class="far fa-comments"></i> 评论区</h3>
<div id="comments-list">加载中...</div>
<% if (user) { %>
<form id="commentForm">
<textarea id="commentContent" placeholder="写下你的评论..."></textarea>
<button type="submit" class="submit-comment">发表评论</button>
</form>
<% } else { %>
<div class="login-prompt">
请 <a href="/login">登录</a> 后发表评论
</div>
<% } %>
</div>
<script>
const postId = "<%= post._id %>";
const userId = "<%= user ? user._id : '' %>";
const isUser = Boolean(<%= user ? 1 : 0 %>);
// 收藏按钮逻辑
if (isUser) {
fetch('/user/profile')
.then(res => res.json())
.then(data => {
if (data.success) {
const isFav = data.favorites && data.favorites.some(fav => fav._id === postId);
const btn = document.getElementById('favoriteBtn');
btn.innerHTML = `<i class="${isFav ? 'fas' : 'far'} fa-heart"></i> <span>${isFav ? '已收藏' : '收藏'}</span>`;
btn.className = isFav ? 'favorite-btn favorited' : 'favorite-btn not-favorited';
btn.onclick = function() {
fetch(`/posts/${postId}/${isFav ? 'unfavorite' : 'favorite'}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(res => res.json())
.then(r => {
if (r.success) {
// 更新按钮状态而不刷新页面
const newIsFav = !isFav;
btn.innerHTML = `<i class="${newIsFav ? 'fas' : 'far'} fa-heart"></i> <span>${newIsFav ? '已收藏' : '收藏'}</span>`;
btn.className = newIsFav ? 'favorite-btn favorited' : 'favorite-btn not-favorited';
} else {
alert(r.message || '操作失败');
}
})
.catch(err => {
console.error('Error:', err);
alert('网络错误,请重试');
});
};
} else {
console.error('获取用户资料失败:', data.message);
document.getElementById('favoriteBtn').textContent = '收藏功能暂不可用';
}
})
.catch(err => {
console.error('Error fetching user profile:', err);
document.getElementById('favoriteBtn').textContent = '收藏功能暂不可用';
});
}
// 加载评论
function loadComments() {
fetch(`/posts/${postId}/comments`)
.then(res => res.json())
.then(data => {
if (data.success) {
if (data.comments.length === 0) {
document.getElementById('comments-list').innerHTML = '<div class="no-comments">暂无评论,快来发表第一条评论吧~</div>';
return;
}
const list = data.comments.map(c => `
<div class="comment-item">
<div>
<span class="comment-author">${c.author.username}</span>
<span class="comment-content">${c.content}</span>
</div>
<div class="comment-meta">
<span>${new Date(c.createdAt).toLocaleString('zh-CN')}</span>
</div>
</div>
`).join('');
document.getElementById('comments-list').innerHTML = list;
} else {
document.getElementById('comments-list').innerHTML = '<div class="no-comments">评论加载失败,请刷新重试</div>';
}
})
.catch(err => {
console.error('Error loading comments:', err);
document.getElementById('comments-list').innerHTML = '<div class="no-comments">评论加载失败,请检查网络</div>';
});
}
loadComments();
// 发表评论
if (isUser) {
const commentForm = document.getElementById('commentForm');
const commentContent = document.getElementById('commentContent');
commentForm.onsubmit = function(e) {
e.preventDefault();
const content = commentContent.value.trim();
if (!content) {
alert('评论内容不能为空');
commentContent.focus();
return;
}
// 禁用提交按钮防止重复提交
const submitBtn = commentForm.querySelector('button[type="submit"]');
submitBtn.disabled = true;
submitBtn.textContent = '提交中...';
fetch(`/posts/${postId}/comments`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ content })
})
.then(res => res.json())
.then(r => {
submitBtn.disabled = false;
submitBtn.textContent = '发表评论';
if (r.success) {
commentContent.value = '';
loadComments();
// 滚动到新评论位置
setTimeout(() => {
const commentsList = document.getElementById('comments-list');
commentsList.scrollTop = commentsList.scrollHeight;
}, 100);
} else {
alert(r.message || '发表评论失败');
}
})
.catch(err => {
console.error('Error submitting comment:', err);
submitBtn.disabled = false;
submitBtn.textContent = '发表评论';
alert('网络错误,请重试');
});
};
}
</script>
</div>
</body>
</html>

138
views/user/login.ejs Normal file
View File

@ -0,0 +1,138 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>内容管理系统 - 用户登录</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
}
body {
background-color: #f5f5f5;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background-image: linear-gradient(120deg, #f6d365 0%, #fda085 100%);
}
.login-container {
background-color: white;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
padding: 40px;
width: 100%;
max-width: 400px;
transition: all 0.3s ease;
}
.login-container:hover {
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.15);
}
h2 {
color: #333;
text-align: center;
margin-bottom: 30px;
font-weight: 600;
}
.error-message {
color: #e74c3c;
background-color: #fadbd8;
padding: 10px 15px;
border-radius: 5px;
margin-bottom: 20px;
font-size: 14px;
text-align: center;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
color: #555;
font-weight: 500;
}
input {
width: 100%;
padding: 12px 15px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 16px;
transition: border 0.3s;
}
input:focus {
border-color: #3498db;
outline: none;
box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
}
button {
width: 100%;
padding: 12px;
background-color: #3498db;
color: white;
border: none;
border-radius: 5px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.3s;
}
button:hover {
background-color: #2980b9;
}
.register-link {
text-align: center;
margin-top: 20px;
color: #555;
}
.register-link a {
color: #3498db;
text-decoration: none;
transition: color 0.3s;
}
.register-link a:hover {
color: #2980b9;
text-decoration: underline;
}
</style>
</head>
<body>
<div class="login-container">
<h2>内容管理系统登录</h2>
<% if (error) { %>
<div class="error-message"><%= error %></div>
<% } %>
<form method="post" action="/login">
<div class="form-group">
<label for="username">用户名</label>
<input type="text" id="username" name="username" required placeholder="请输入用户名">
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" id="password" name="password" required placeholder="请输入密码">
</div>
<button type="submit">登 录</button>
</form>
<div class="register-link">
没有账号?<a href="/register">立即注册</a>
</div>
</div>
</body>
</html>

146
views/user/register.ejs Normal file
View File

@ -0,0 +1,146 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>内容管理系统 - 用户注册</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
}
body {
background-color: #f5f5f5;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background-image: linear-gradient(120deg, #84fab0 0%, #8fd3f4 100%);
}
.register-container {
background-color: white;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
padding: 40px;
width: 100%;
max-width: 450px;
transition: all 0.3s ease;
}
.register-container:hover {
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.15);
}
h2 {
color: #333;
text-align: center;
margin-bottom: 30px;
font-weight: 600;
}
.error-message {
color: #e74c3c;
background-color: #fadbd8;
padding: 10px 15px;
border-radius: 5px;
margin-bottom: 20px;
font-size: 14px;
text-align: center;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
color: #555;
font-weight: 500;
}
input {
width: 100%;
padding: 12px 15px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 16px;
transition: border 0.3s;
}
input:focus {
border-color: #2ecc71;
outline: none;
box-shadow: 0 0 0 2px rgba(46, 204, 113, 0.2);
}
button {
width: 100%;
padding: 12px;
background-color: #2ecc71;
color: white;
border: none;
border-radius: 5px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.3s;
margin-top: 10px;
}
button:hover {
background-color: #27ae60;
}
.login-link {
text-align: center;
margin-top: 25px;
color: #555;
}
.login-link a {
color: #2ecc71;
text-decoration: none;
transition: color 0.3s;
}
.login-link a:hover {
color: #27ae60;
text-decoration: underline;
}
.password-hint {
font-size: 12px;
color: #777;
margin-top: 5px;
}
</style>
</head>
<body>
<div class="register-container">
<h2>创建新账户</h2>
<% if (error) { %>
<div class="error-message"><%= error %></div>
<% } %>
<form method="post" action="/register">
<div class="form-group">
<label for="username">用户名</label>
<input type="text" id="username" name="username" required placeholder="设置您的用户名">
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" id="password" name="password" required placeholder="设置登录密码">
<div class="password-hint">建议使用8位以上包含字母和数字的组合</div>
</div>
<button type="submit">立即注册</button>
</form>
<div class="login-link">
已有账号?<a href="/login">直接登录</a>
</div>
</div>
</body>
</html>

122
views/user/userInfo.ejs Normal file
View File

@ -0,0 +1,122 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>个人中心</title>
<style>
body { background: #f5f6fa; font-family: 'PingFang SC', 'Microsoft YaHei', Arial, sans-serif; }
.container { max-width: 900px; margin: 40px auto; background: #fff; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.08); padding: 30px; }
h2 { color: #2c3e50; margin-bottom: 20px; }
.section { margin-bottom: 40px; }
.section-title { font-size: 20px; color: #2980b9; margin-bottom: 15px; border-left: 4px solid #2980b9; padding-left: 10px; }
.form-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 6px; color: #555; }
input[type="password"] { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; }
.btn { background: #2980b9; color: #fff; border: none; padding: 8px 18px; border-radius: 4px; cursor: pointer; transition: background 0.2s; }
.btn:hover { background: #1a5a8a; }
.favorites-list, .comments-list { list-style: none; padding: 0; }
.favorites-list li, .comments-list li { background: #f8f9fa; margin-bottom: 10px; padding: 12px 16px; border-radius: 4px; display: flex; justify-content: space-between; align-items: center; }
.favorites-list .post-title, .comments-list .post-title { color: #2980b9; text-decoration: none; font-weight: 500; }
.favorites-list .remove-btn { background: #e74c3c; color: #fff; border: none; padding: 4px 10px; border-radius: 3px; cursor: pointer; }
.favorites-list .remove-btn:hover { background: #c0392b; }
.comments-list .comment-content { color: #333; margin-right: 10px; }
.comments-list .comment-meta { color: #888; font-size: 13px; }
.error-msg { color: #e74c3c; margin-bottom: 10px; }
.success-msg { color: #27ae60; margin-bottom: 10px; }
</style>
</head>
<body>
<div class="container">
<h2>个人中心</h2>
<!-- 密码修改 -->
<div class="section">
<div class="section-title">修改密码</div>
<form id="changePwdForm" method="post" action="/user/changePassword">
<div class="form-group">
<label for="oldPassword">原密码</label>
<input type="password" id="oldPassword" name="oldPassword" required>
</div>
<div class="form-group">
<label for="newPassword">新密码</label>
<input type="password" id="newPassword" name="newPassword" required>
</div>
<div class="form-group">
<label for="confirmPassword">确认新密码</label>
<input type="password" id="confirmPassword" name="confirmPassword" required>
</div>
<button class="btn" type="submit">修改密码</button>
</form>
<div id="pwdMsg"></div>
</div>
<!-- 收藏管理 -->
<div class="section">
<div class="section-title">我的收藏</div>
<% if (favorites && favorites.length > 0) { %>
<ul class="favorites-list">
<% favorites.forEach(function(post) { %>
<li>
<a class="post-title" href="/posts/<%= post._id %>"><%= post.title %></a>
<form method="post" action="/posts/<%= post._id %>/unfavorite" style="display:inline;">
<button class="remove-btn" type="submit">取消收藏</button>
</form>
</li>
<% }); %>
</ul>
<% } else { %>
<div>暂无收藏的博文</div>
<% } %>
</div>
<!-- 评论管理 -->
<div class="section">
<div class="section-title">我的评论</div>
<% if (comments && comments.length > 0) { %>
<ul class="comments-list">
<% comments.forEach(function(comment) { %>
<li>
<span class="comment-content"><%= comment.content %></span>
<span class="comment-meta">于 <a class="post-title" href="/posts/<%= comment.post._id %>"><%= comment.post.title %></a> • <%= new Date(comment.createdAt).toLocaleString('zh-CN') %></span>
</li>
<% }); %>
</ul>
<% } else { %>
<div>暂无评论</div>
<% } %>
</div>
</div>
<script>
// 密码修改表单前端校验和异步提交
document.getElementById('changePwdForm').addEventListener('submit', async function(e) {
e.preventDefault();
const oldPassword = document.getElementById('oldPassword').value;
const newPassword = document.getElementById('newPassword').value;
const confirmPassword = document.getElementById('confirmPassword').value;
const msgDiv = document.getElementById('pwdMsg');
msgDiv.textContent = '';
if (newPassword !== confirmPassword) {
msgDiv.textContent = '两次新密码输入不一致';
msgDiv.className = 'error-msg';
return;
}
try {
const res = await fetch('/user/changePassword', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ oldPassword, newPassword })
});
const data = await res.json();
if (data.success) {
msgDiv.textContent = '密码修改成功';
msgDiv.className = 'success-msg';
document.getElementById('changePwdForm').reset();
} else {
msgDiv.textContent = data.message || '密码修改失败';
msgDiv.className = 'error-msg';
}
} catch (err) {
msgDiv.textContent = '请求失败,请稍后再试';
msgDiv.className = 'error-msg';
}
});
</script>
</body>
</html>

4
初始化数据.bat Normal file
View File

@ -0,0 +1,4 @@
@echo off
echo 正在初始化数据库...
node db/init.js
pause

4
项目启动.bat Normal file
View File

@ -0,0 +1,4 @@
@echo off
echo 正在启动博客系统...
node app.js
pause