全量上传
This commit is contained in:
parent
76dc6c4a85
commit
ccff2bdf98
3
.cursorignore
Normal file
3
.cursorignore
Normal 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
2
.gitignore
vendored
@ -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
183
PAGINATION_README.md
Normal 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
136
README.md
@ -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. 管理员账号需要在数据库中手动创建或通过注册功能创建
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
118
app.js
Normal file
118
app.js
Normal 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}`);
|
||||
});
|
||||
}
|
235
controllers/adminController.js
Normal file
235
controllers/adminController.js
Normal 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
80
controllers/user/auth.js
Normal 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
274
db/blog.js
Normal 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\n(1)芯片问题其实没必要担心。我们单芯片还是落后美国一代,我们用数学补物理、非摩尔补摩尔,用群计算补单芯片,在结果上也能达到实用状况。\n\n(2)软件是卡不住脖子的,那是数学的图形符号、代码,一些尖端的算子、算法垒起来的,没有阻拦索。困难在我们的教育培养、人才梯队的建设。\n\n(3)当我国拥有一定经济实力的时候,要重视理论特别是基础理论的研究。如果不搞基础研究,就没根。即使叶茂,欣欣向荣,风一吹就会倒的。\n\n(4)我们要理解支持搞理论工作的。理论科学家是孤独的,我们要有战略耐心,要理解他们。他们头脑中的符号、公式、思维,世界上能与他们沟通的只有几个人。对理论科学家要尊重,因为我们不懂他的文化,社会要宽容,国家要支持。\n\n(5)买国外的产品很贵,因为价格里面就包含他们在基础研究上的投入。中国搞不搞基础研究,也要付钱的,能不能付给自己搞基础研究的人。\n\n(6)华为一年1800亿投入研发,大概有600亿是做基础理论研究,不考核。1200亿左右投入产品研发,投入是要考核的。没有理论就没有突破,我们就赶不上美国。\n\n(7)人工智能也许是人类社会最后一次技术革命,当然可能还有能源的核聚变。发展人工智能要有电力保障,中国的发电、电网传输都是非常好的,通信网络是世界最发达的,东数西算的理想是可能实现的。\n\n(8)赞声与骂声,都不要在意,而要在乎自己能不能做好。把自己做好,就没有问题。",
|
||||
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
112
db/init.js
Normal 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
103
fix_database.js
Normal 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
16
middleware/auth.js
Normal 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
9
models/Category.js
Normal 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
11
models/Comment.js
Normal 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
12
models/Link.js
Normal 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
49
models/Post.js
Normal 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
13
models/User.js
Normal 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
1909
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
package.json
Normal file
25
package.json
Normal 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
356
routes/admin/index.js
Normal 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
144
routes/user/auth.js
Normal 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
48
test_links.js
Normal 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
55
test_pagination.js
Normal 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();
|
||||
}
|
||||
}
|
71
test_pagination_component.js
Normal file
71
test_pagination_component.js
Normal 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
0
uploads/app.js
Normal file
0
uploads/config.js
Normal file
0
uploads/config.js
Normal file
81
utils/pagination.js
Normal file
81
utils/pagination.js
Normal 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
|
||||
};
|
143
views/admin/categories/list.ejs
Normal file
143
views/admin/categories/list.ejs
Normal 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>
|
67
views/admin/comments/list.ejs
Normal file
67
views/admin/comments/list.ejs
Normal 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
163
views/admin/index.ejs
Normal 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
141
views/admin/links/list.ejs
Normal 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
380
views/admin/posts/list.ejs
Normal 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
102
views/admin/users/list.ejs
Normal 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>
|
139
views/components/pagination.ejs
Normal file
139
views/components/pagination.ejs
Normal 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">«</span>
|
||||
</a>
|
||||
</li>
|
||||
<% } else { %>
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link" aria-label="上一页">
|
||||
<span aria-hidden="true">«</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">»</span>
|
||||
</a>
|
||||
</li>
|
||||
<% } else { %>
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link" aria-label="下一页">
|
||||
<span aria-hidden="true">»</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
304
views/user/index.ejs
Normal 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
348
views/user/info.ejs
Normal 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
138
views/user/login.ejs
Normal 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
146
views/user/register.ejs
Normal 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
122
views/user/userInfo.ejs
Normal 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>
|
Loading…
Reference in New Issue
Block a user