Commit 34c40969 authored by 肖燊's avatar 肖燊
Browse files

:alarm_clock: v2.0.4 版本更新

Showing with 12773 additions and 222 deletions
+12773 -222
# DoraCMS 2.0.3
# DoraCMS 2.0.4
![DoraCMS](http://7xkrk4.com1.z0.glb.clouddn.com/doracms2.jpg "DoraCMS")
## 2.0.3版本更新
## 2.0.4版本更新
1、上传缩略图支持七牛云存储
1、添加系统支持redis缓存,通过开关控制,并添加通过redis缓存数据中间件。要知道redis在存储性能方面MongoDB要好很多(主要存储session数据)
2、取消在后台首页显示用户敏感信息,提高安全性
2、重新整理了样式,组件样式全部单独提取,提高可维护性。
3、管理员登录md5加密 ![#87](https://github.com/doramart/DoraCMS/pull/87 "#87")
3、文档详情页添加了“喜欢”功能。
4、修复描述信息不是必填项,但是也验证了 ![#91](https://github.com/doramart/DoraCMS/issues/91 "#91")
4、修复了某些场景下批量删除留言异常的bug。
5、站点地图域名可配置
5、添加了二维码分享功能。
6、统一端口号配置
6、添加了回到顶部按钮。
7、修复管理员在留言管理中 对某个会员 回复信息, 然后再给自己(doramart)回复信息后,进入系统主页浏览器报错的问题![#93](https://github.com/doramart/DoraCMS/issues/93 "#93")
7、优化了包括 最新文档、近期文档、推荐文档等模块的代码结构。
8、修复修改管理员信息没有改手机号却提示手机号格式不正确的问题 ![#92](https://github.com/doramart/DoraCMS/pull/92 "#92")
8、优化了用户中心的界面和交互。
9、前台后台添加404页面
9、修复页面跳转后滚动条不置顶的问题。
10、后台没有权限的菜单不显示
10、修复了某些场景下通过标签查询,分页异常的问题。
11、优化了cms在移动端的显示。
12、修复了一些明显bug
11、修复了一些样式问题
## 更新方法:
1、checkout 最新 2.0.3 代码
1、checkout 最新 2.0.4 代码
2、删除 node_modules,重新安装依赖包
......
......@@ -26,6 +26,7 @@ const config = {
],
alias: {
'@': path.join(__dirname, '..', 'src'),
'front_public': '@/index/assets/css/public.scss',
'scss_vars': '@/manage/assets/styles/vars.scss',
'~src': path.resolve(__dirname, '../src'),
'~api': path.resolve(__dirname, '../src/api/index-client'),
......
......@@ -32,6 +32,8 @@ var config = merge(base, {
},
resolve: {
alias: {
'@': path.join(__dirname, '..', 'src'),
'front_public': '@/index/assets/css/public.scss',
'~api': path.resolve(__dirname, '../src/api/index-server'),
'~server': path.resolve(__dirname, '../server'),
'api-config': path.resolve(__dirname, '../src/api/config-server'),
......
This diff is collapsed.
{
"name": "doracms",
"version": "2.0.3",
"version": "2.0.4",
"description": "基于nodejs,express,vue2 内容管理系统.",
"keywords": [
"vue",
......@@ -28,6 +28,7 @@
"body-parser": "^1.18.2",
"compression": "^1.7.1",
"connect-mongo": "^1.3.2",
"connect-redis": "^3.3.2",
"cookie-parser": "^1.4.3",
"cross-env": "^5.1.1",
"crypto": "0.0.3",
......@@ -51,6 +52,7 @@
"nodemailer": "^4.4.0",
"nprogress": "^0.2.0",
"qiniu": "^7.1.1",
"qr-image": "^3.2.0",
"serve-favicon": "^2.4.5",
"shelljs": "^0.7.8",
"shortid": "^2.2.8",
......@@ -114,4 +116,4 @@
"engines": {
"node": "8.x"
}
}
\ No newline at end of file
}
......@@ -9,6 +9,7 @@ const favicon = require('serve-favicon')
const express = require('express')
const session = require('express-session');
const MongoStore = require('connect-mongo')(session);
const RedisStore = require('connect-redis')(session);
const compression = require('compression')
const lurCache = require('lru-cache')
const ueditor = require("ueditor")
......@@ -96,21 +97,36 @@ app.use(bodyParser.urlencoded({ extended: true }))
// cookie 解析中间件
app.use(cookieParser(settings.session_secret));
// session配置
app.use(session({ //session持久化配置
secret: settings.encrypt_key,
// key: "kvkenskey",
cookie: {
maxAge: 1000 * 60 * 60 * 24 * 1
},
resave: false,
saveUninitialized: true,
store: new MongoStore({
db: "session",
host: "localhost",
port: 27017,
url: !isProd ? settings.URL : 'mongodb://' + settings.USERNAME + ':' + settings.PASSWORD + '@' + settings.HOST + ':' + settings.PORT + '/' + settings.DB + ''
})
}));
let sessionConfig = {};
if (settings.openRedis) {
sessionConfig = {
secret: settings.session_secret,
store: new RedisStore({
port: settings.redis_port,
host: settings.redis_host,
pass: settings.redis_psd,
ttl: 1800 // 过期时间
}),
resave: true,
saveUninitialized: true
}
} else {
sessionConfig = {
secret: settings.encrypt_key,
cookie: {
maxAge: 1000 * 60 * 10
},
resave: false,
saveUninitialized: true,
store: new MongoStore({
db: "session",
host: "localhost",
port: 27017,
url: !isProd ? settings.URL : 'mongodb://' + settings.USERNAME + ':' + settings.PASSWORD + '@' + settings.HOST + ':' + settings.PORT + '/' + settings.DB + ''
})
}
}
app.use(session(sessionConfig));
// 鉴权用户
app.use(authUser.auth);
// 初始化日志目录
......
......@@ -6,7 +6,7 @@ const formidable = require('formidable');
const { service, settings, validatorUtil, logUtil, siteFunc } = require('../../../utils');
const shortid = require('shortid');
const validator = require('validator')
const _ = require('lodash')
function checkFormData(req, res, fields) {
let errMsg = '';
if (fields._id && !siteFunc.checkCurrentId(fields._id)) {
......@@ -63,9 +63,12 @@ class Content {
}
if (sortby) {
for (const item of sortby) {
sortObj[item] = -1
}
// for (const item of sortby) {
// sortObj[item] = -1
// }
delete sortObj.date;
sortObj[sortby] = -1;
}
if (state) {
......@@ -169,7 +172,7 @@ class Content {
content && (content.commentNum = commentNum);
// 推荐文章查询
const totalContents = await ContentModel.count({});
const randomArticles = await ContentModel.find({}, 'stitle sImg').skip(Math.floor(totalContents * Math.random())).limit(4);
const randomArticles = await ContentModel.find({}, 'stitle sImg').skip(Math.floor(totalContents * Math.random())).limit(6);
res.send({
state: 'success',
doc: content || {},
......@@ -187,6 +190,33 @@ class Content {
}
}
async updateLikeNum(req, res, next) {
let targetId = req.query.contentId;
let userId = req.session.user._id;
try {
let oldContent = await ContentModel.findOne({ _id: targetId });
if (!_.isEmpty(oldContent) && (oldContent.likeUserIds).indexOf(userId) > -1) {
res.send({
state: 'error',
type: 'ERROR_IN_UPDATE_DATA',
message: '不可重复提交',
})
} else {
let newContent = await ContentModel.findOneAndUpdate({ _id: targetId }, { '$inc': { 'likeNum': 1 }, '$push': { 'likeUserIds': userId } });
res.send({
state: 'success',
likeNum: newContent.likeNum + 1
});
}
} catch (error) {
res.send({
state: 'error',
type: 'ERROR_IN_SAVE_DATA',
message: '更新数据失败:' + error,
})
}
}
async addContent(req, res, next) {
const form = new formidable.IncomingForm();
form.parse(req, async (err, fields, files) => {
......@@ -216,7 +246,8 @@ class Content {
isTop: fields.isTop,
from: fields.from,
discription: fields.discription,
comments: fields.comments
comments: fields.comments,
likeUserIds: []
}
const newContent = new ContentModel(groupObj);
......
......@@ -183,12 +183,10 @@ class Message {
message: errMsg,
})
}
let contentIdArr = [];
for (let i = 0; i < targetIds.length; i++) {
let msgObj = await MessageModel.findOne({ _id: targetIds[i] });
if (msgObj && contentIdArr.indexOf(msgObj.contentId) == -1) {
// 避免重复删除
contentIdArr.push(msgObj.contentId);
if (msgObj) {
await ContentModel.findOneAndUpdate({ _id: msgObj.contentId }, { '$inc': { 'commentNum': -1 } })
}
}
......
......@@ -66,7 +66,7 @@ class User {
queryObj.userName = { $regex: reKey }
}
const Users = await UserModel.find(queryObj, { password: 0}).sort({ date: -1 }).skip(Number(pageSize) * (Number(current) - 1)).limit(Number(pageSize));
const Users = await UserModel.find(queryObj, { password: 0 }).sort({ date: -1 }).skip(Number(pageSize) * (Number(current) - 1)).limit(Number(pageSize));
const totalItems = await UserModel.count(queryObj);
res.send({
state: 'success',
......@@ -113,10 +113,12 @@ class User {
email: fields.email,
logo: fields.logo,
phoneNum: fields.phoneNum || '',
password: service.encrypt(fields.password, settings.encrypt_key),
confirm: fields.confirm,
group: fields.group
}
if (fields.password) {
userObj.password = service.encrypt(fields.password, settings.encrypt_key);
}
const item_id = fields._id;
try {
......
......@@ -14,7 +14,7 @@ var AdminUser = require('./AdminUser');
var ContentSchema = new Schema({
_id: {
type: String,
'default': shortid.generate
},
title: String,
......@@ -35,7 +35,7 @@ var ContentSchema = new Schema({
comments: String,
commentNum: { type: Number, default: 0 }, // 评论数
likeNum: { type: Number, default: 0 }, // 喜欢数
likeUserIds: String, // 喜欢该文章的用户ID集合
likeUserIds: [{ type: String, default: [] }], // 喜欢该文章的用户ID集合
from: { type: String, default: '1' } // 来源 1为原创 2为转载
});
......@@ -48,7 +48,7 @@ ContentSchema.path('date').get(function (v) {
return moment(v).startOf('hour').fromNow();
});
ContentSchema.path('updateDate').get(function (v) {
return moment(v).format("YYYY-MM-DD HH:mm");
return moment(v).format("YYYY-MM-DD");
});
var Content = mongoose.model("Content", ContentSchema);
......
......@@ -15,6 +15,7 @@ const authUser = require('../../utils/middleware/authUser');
const { AdminUser, ContentCategory, Content, ContentTag, User, Message, SystemConfig, UserNotify, Ads } = require('../lib/controller');
const _ = require('lodash');
const qr = require('qr-image')
function checkUserSession(req, res, next) {
if (!_.isEmpty(req.session.user)) {
......@@ -55,6 +56,22 @@ router.get('/content/getSimpleListByParams', (req, res, next) => { req.query.sta
// 查询文档详情
router.get('/content/getContent', Content.getOneContent)
// 更新喜欢文档
router.get('/content/updateLikeNum', checkUserSession, Content.updateLikeNum)
//文章二维码生成
router.get('/qrImg', (req, res, next) => {
let detailLink = req.query.detailLink;
try {
let img = qr.image(detailLink, { size: 10 });
res.writeHead(200, { 'Content-Type': 'image/png' });
img.pipe(res);
} catch (e) {
res.writeHead(414, { 'Content-Type': 'text/html' });
res.end('<h1>414 Request-URI Too Large</h1>');
}
});
// 用户登录
router.post('/users/doLogin', User.loginAction);
......
......@@ -2,7 +2,6 @@ import Vue from 'vue'
import { createApp } from './app'
import ProgressBar from './index/components/ProgressBar.vue'
import "./index/assets/base.css"
import '../node_modules/element-ui/lib/theme-chalk/index.css'
import '../node_modules/element-ui/lib/theme-chalk/display.css';
import '../node_modules/font-awesome/css/font-awesome.min.css'
......@@ -22,9 +21,6 @@ router.beforeResolve((to, from, next) => {
const matched = router.getMatchedComponents(to)
const prevMatched = router.getMatchedComponents(from)
// [a, b]
// [a, b, c, d]
// => [c, d]
let diffed = false
const activated = matched.filter((c, i) => diffed || (diffed = prevMatched[i] !== c))
......
<style lang="scss">
@import "~front_public";
</style>
<template>
<div id="app">
......
html,
body {
padding: 0;
margin: 0;
background: #ffffff;
-webkit-font-smoothing: antialiased;
color: rgb(51, 51, 51);
font-family: -apple-system, SF UI Text, Arial, PingFang SC, Hiragino Sans GB, Microsoft YaHei, WenQuanYi Micro Hei, sans-serif;
}
.fade-enter-active,
.fade-leave-active {
transition: all .2s ease;
}
.fade-enter,
.fade-leave-active {
opacity: 0;
}
a:link,
a:active,
a:visited {
text-decoration: none;
color: #333333;
}
a:hover {
color: #409EFF
}
.content-main a:link,
.content-main a:active,
.content-main a:visited {
color: #409EFF;
}
ul {
margin: 0;
padding: 0;
}
li {
list-style-type: none;
}
.h1,
.h2,
.h3,
.h4,
.h5,
.h6,
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: inherit;
font-weight: 500;
line-height: 1.1;
color: inherit;
}
h2 {
font-size: 18px;
}
h3 {
font-size: 16px;
}
pre {
display: block;
padding: 15px;
margin: 0 0 10px;
font-size: 13px;
line-height: 1.42857143;
color: #657b83;
word-break: break-all;
word-wrap: break-word;
background-color: #f5f5f5;
border: 1px solid #ccc;
border-radius: 3px;
overflow: auto;
font-family: Monaco, Menlo, Consolas, "Courier New", monospace;
}
.contentContainer {
margin-top: 30px;
}
.content-item:last-child {
border: none !important;
}
.normaltitle,
.catetitle {
font-size: 14px;
color: #fff;
text-align: left;
font-weight: normal;
margin: 0;
margin-bottom: 10px;
}
.normaltitle span {
background-color: #409EFF;
height: 30px;
line-height: 30px;
padding: 2px 12px;
display: inline-block;
}
.catetitle {
color: #333333;
}
.login-form {
margin: 0 auto;
padding-top: 6%;
padding-bottom: 100px;
}
.login-form .submit-btn {
text-align: left;
}
.login-form .login-container {
background-clip: padding-box;
padding: 25px 35px 10px 35px;
background: #fff;
box-shadow: 0 0 8px rgba(0, 0, 0, .1);
border-radius: 4px;
}
.login-form .login-container .title {
margin: 0px auto 40px auto;
text-align: center;
color: #505458;
}
.login-form .login-container.remember {
margin: 0px 0px 35px 0px;
}
@media screen and (max-width:768px) {
.header .header-main {
padding: 5px 0 !important;
}
.header .header-main .header-logo {
width: 150px !important;
margin: 0 auto;
}
.header .header-main .header-nav {
border-left: none !important;
margin-left: 0 !important;
}
.footer .sitemap {
display: none !important;
}
.content-item {
padding-bottom: 15px !important;
}
}
@media screen and (max-width:1200px) {
.search-pannel {
display: none !important;
}
}
\ No newline at end of file
.admin-logo-title {
h3 {
color: #99a9bf;
font-size: 35px;
text-align: center;
font-weight: normal;
}
}
.admin-login-form {
margin: 0 auto;
margin-top: 70px;
margin-bottom: 100px;
input {
border-top-right-radius: 4px !important;
border-bottom-right-radius: 4px !important;
}
.el-input-group__prepend {
padding: 0 10px;
}
.submit-btn {
text-align: center;
.el-button {
width: 100%;
}
}
.imageCode {
width: 8rem;
height: 2rem;
float: right;
}
.login-container {
-webkit-border-radius: 5px;
border-radius: 5px;
-moz-border-radius: 5px;
background-clip: padding-box;
background: transparent;
border: 1px solid #eaeaea;
box-shadow: 0 0 20px #cac6c6;
h3 {
margin: 0;
border-bottom: 1px solid #e9eaec;
padding: 14px 16px;
line-height: 1;
font-weight: 500;
i {
display: inline-block;
margin-right: 10px;
}
}
.loginForm {
padding: 14px 16px;
}
.el-form-item {
margin-bottom: 15px;
.el-form-item__error {
left: 2rem;
}
}
.title {
margin: 0px auto 40px auto;
text-align: center;
color: #505458;
}
.remember {
margin: 0px 0px 35px 0px;
}
}
}
\ No newline at end of file
// 回到顶部组件
.page-component-up {
background-color: #fff;
position: fixed;
right: 50px;
bottom: 150px;
width: 40px;
height: 40px;
border-radius: 20px;
cursor: pointer;
transition: 0.3s;
box-shadow: 0 0 6px rgba(0, 0, 0, 0.12);
i {
color: $color-primary;
display: block;
line-height: 40px;
text-align: center;
font-size: 18px;
}
}
\ No newline at end of file
// 右侧分类
.catesMenu {
font-size: 14px;
.parent-name {
font-weight: 700;
height: 30px;
line-height: 30px;
padding-left: 30px;
margin-top: 15px;
}
.cate-list {
padding-left: 40px;
.active a:link,
.active a:visited {
color: #3ca5f6;
}
}
.cate-list li {
font-weight: normal;
height: 30px;
line-height: 30px;
}
}
\ No newline at end of file
// 轮播图
.content-ads {
.el-carousel__item h3 {
color: #475669;
font-size: 18px;
opacity: 0.75;
margin: 0;
}
.el-carousel__item:nth-child(2n) {
background-color: #99a9bf;
}
.el-carousel__item:nth-child(2n + 1) {
background-color: #d3dce6;
}
.img-pannel {
img {
width: 100%;
}
}
.text-pannel ul li {
display: inline-block;
margin-right: 10px;
}
.case-title {
color: #b4bccc;
margin: 15px auto;
font-size: 13px;
}
.case-item {
margin-bottom: 20px;
}
}
\ No newline at end of file
.footer {
font-size: 14px;
padding: 35px 0;
color: #5f676f;
background: #2d3237;
ul {
li {
text-align: center;
line-height: 35px;
padding: 0 10px;
a:link,
a:visited {
color: #76818c;
}
}
}
}
\ No newline at end of file
// header组件
.drop-menu {
width: 96%;
}
.header {
position: fixed;
left: 0;
top: 0;
z-index: 999;
width: 100%;
border: 0;
background: #fff;
-webkit-box-shadow: 0 0 3px 1px rgba(0, 0, 0, 0.1);
box-shadow: 0 0 3px 1px rgba(0, 0, 0, 0.1);
.header-main {
margin: 0 auto;
overflow: hidden;
.header-logo {
a {
text-decoration: none;
color: #333333;
height: 62px;
display: table-cell;
vertical-align: middle;
}
img {
max-height: 38px;
width: auto;
margin-top: 8px;
}
}
.header-nav {
font-size: 15px;
float: left;
width: 100%;
.el-dropdown {
font-size: 15px;
color: #333333;
}
ul {
li {
display: inline-block;
a {
padding: 0 20px;
line-height: 62px;
color: #333333;
}
}
li.active {
a:link,
a:visited {
color: $color-primary;
}
}
}
}
.right-pannel {
margin-top: 10px;
}
}
.toggle-menu,
.toggle-search {
color: #aaaaaa;
font-size: 18px;
padding-left: 0.6rem;
padding-right: 0.6rem;
border: none;
margin-top: 0.4rem;
}
.toggle-search {
float: right;
}
} // loginPannle
.login-pannel {
float: right;
text-align: right;
ul {
li {
cursor: pointer;
color: $color-primary;
height: 40px;
line-height: 40px;
display: inline-block;
font-size: 14px;
i {
font-size: 12px;
}
.logo {
width: 1.8rem;
height: 1.8rem;
display: inline-block;
vertical-align: middle;
margin-right: 5px;
img {
width: 100%;
border-radius: 50%;
}
}
}
.login-txt {
a:first-child {
margin-right: 10px;
}
}
}
} // searchPannel
.search-pannel {
width: 100%;
display: inline-block;
margin-top: 5px;
text-align: right;
.input-area {
display: inline-block;
position: relative;
i {
display: inline-block;
cursor: pointer;
font-weight: 700px;
color: #cccccc;
position: absolute;
top: 5px;
right: 14px;
font-size: 20px;
}
width: 50px;
height: 30px;
background: #fff;
border-left: 1px solid #eee;
border-right: 1px solid #eee;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
vertical-align: top;
-webkit-transition: all 0.3s ease-out 0s;
-o-transition: all 0.3s ease-out 0s;
transition: all 0.3s ease-out 0s;
input {
border: none;
width: 0;
}
}
.input-area.active {
width: 184px;
-webkit-transition: all 0.3s ease-out 0s;
-o-transition: all 0.3s ease-out 0s;
transition: all 0.3s ease-out 0s;
input {
width: 155px;
padding: 0 10px;
-webkit-transition: all 0.3s ease-out 0s;
-o-transition: all 0.3s ease-out 0s;
transition: all 0.3s ease-out 0s;
}
}
.el-form-item {
margin-bottom: 0;
}
}
\ No newline at end of file
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment