1. 文章管理 2. 评论管理

This commit is contained in:
ronger 2021-06-28 08:08:24 +08:00
parent ca7e8e5e97
commit 25b2b81147
8 changed files with 642 additions and 62 deletions

0
assets/weixinStore.jpg Normal file
View File

View File

@ -10,6 +10,14 @@
<i class="el-icon-s-data"></i>
<span slot="title">Dashboard</span>
</el-menu-item>
<el-menu-item index="admin-articles">
<i class="el-icon-s-custom"></i>
<span slot="title">文章管理</span>
</el-menu-item>
<el-menu-item index="admin-comments">
<i class="el-icon-s-custom"></i>
<span slot="title">评论管理</span>
</el-menu-item>
<el-menu-item index="admin-users">
<i class="el-icon-s-custom"></i>
<span slot="title">用户管理</span>

213
pages/admin/articles.vue Normal file
View File

@ -0,0 +1,213 @@
<template>
<el-row style="margin-top: 20px;">
<el-col style="margin-bottom: 1rem;">
<el-breadcrumb separator-class="el-icon-arrow-right">
<el-breadcrumb-item :to="{ path: '/admin/dashboard' }">首页</el-breadcrumb-item>
<el-breadcrumb-item>文章管理</el-breadcrumb-item>
</el-breadcrumb>
</el-col>
<el-col style="margin-bottom: 1rem;">
<el-pagination
:hide-on-single-page="true"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="pagination.currentPage"
:page-sizes="[10, 20, 50, 100]"
:page-size="pagination.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="pagination.total">
</el-pagination>
</el-col>
<el-col>
<el-table
:data="articles"
style="width: 100%">
<el-table-column
label="#"
width="40"
prop="idArticle">
</el-table-column>
<el-table-column
label="标题"
prop="articleTitle">
<template slot-scope="scope">
<el-button type="text" @click="showArticleDetail(scope.row.articlePermalink)">{{ scope.row.articleTitle }}</el-button>
</template>
</el-table-column>
<el-table-column
label="标签"
prop="articleTitle">
<template slot-scope="scope">
<el-tag
style="margin-left: 0.5rem;"
v-for="tag in scope.row.tags"
:key="tag.idTag"
size="mini"
effect="plain">
# {{ tag.tagTitle }}
</el-tag>
</template>
</el-table-column>
<el-table-column
label="作者"
width="100"
prop="articleAuthorName">
</el-table-column>
<el-table-column
label="最后更新时间"
width="110"
prop="updatedTime">
</el-table-column>
<el-table-column label="操作">
<template slot-scope="scope">
<el-button v-if="scope.row.articlePerfect === '1'" size="mini" @click="cancelPreference(scope.$index, scope.row.idArticle)" plain>取消优选</el-button>
<el-button v-else size="mini" @click="setPreference(scope.$index, scope.row.idArticle)" plain>设为优选</el-button>
<el-button size="mini" type="primary"
@click="updateTags(scope.$index, scope.row)" plain>编辑标签
</el-button>
<el-button v-if="scope.row.articleStatus === '0'" size="mini" type="danger"
@click="toggleStatus(scope.$index, scope.row)" plain>下架
</el-button>
<el-button v-else size="mini" type="success"
@click="toggleStatus(scope.$index, scope.row)" plain>上架
</el-button>
</template>
</el-table-column>
</el-table>
</el-col>
<el-col>
<el-pagination
:hide-on-single-page="true"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="pagination.currentPage"
:page-sizes="[10, 20, 50, 100]"
:page-size="pagination.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="pagination.total">
</el-pagination>
</el-col>
<el-col>
<el-dialog :visible.sync="dialogVisible">
<edit-tags
:idArticle="idArticle"
:tags="articleTags"
@closeDialog="closeTagsDialog">
</edit-tags>
</el-dialog>
</el-col>
</el-row>
</template>
<script>
import {mapState} from 'vuex';
import EditTags from '~/components/widget/tags';
export default {
name: "articles",
components: {
EditTags
},
fetch({store, params, error}) {
return Promise.all([
store
.dispatch('admin/fetchArticles', params)
.catch(err => error({statusCode: 404}))
])
},
computed: {
...mapState({
articles: state => state.admin.article.articles,
pagination: state => state.admin.article.pagination
})
},
data() {
return {
order: 'desc',
idRole: 0,
idUser: 0,
dialogVisible: false,
index: Number,
idArticle: Number,
articleTags: ''
}
},
methods: {
handleSizeChange(pageSize) {
let _ts = this;
_ts.$store.dispatch('admin/fetchArticles', {
page: _ts.pagination.currentPage,
rows: pageSize
})
},
handleCurrentChange(page) {
let _ts = this;
_ts.$store.dispatch('admin/fetchArticles', {
page: page,
rows: _ts.pagination.pageSize
})
},
toggleStatus() {},
setPreference(index, idArticle) {
let _ts = this;
_ts.$axios.$patch("/api/article/update-perfect", {
idArticle: idArticle,
articlePerfect: '1'
}).then(function (res) {
if (res) {
if (res.success) {
_ts.$store.commit('admin/updateArticlePreference', {
index: index,
idArticle: idArticle,
articlePerfect: '1'
})
_ts.$message.success("设置成功!");
} else {
_ts.$message.error(_ts.message);
}
}
})
},
cancelPreference(index, idArticle) {
let _ts = this;
_ts.$axios.$patch("/api/article/update-perfect", {
idArticle: idArticle,
articlePerfect: '0'
}).then(function (res) {
if (res) {
if (res.success) {
_ts.$store.commit('admin/updateArticlePreference', {
index: index,
idArticle: idArticle,
articlePerfect: '0'
})
_ts.$message.success("取消成功!");
} else {
_ts.$message.error(_ts.message);
}
}
})
},
updateTags(index, article) {
let _ts = this
_ts.$set(_ts, 'index', index);
_ts.$set(_ts, 'idArticle', article.idArticle);
_ts.$set(_ts, 'articleTags', article.articleTags);
_ts.$set(_ts, 'dialogVisible', true);
},
closeTagsDialog() {
this.$set(this, 'dialogVisible', false);
},
showArticleDetail(articlePermalink) {
window.open(articlePermalink);
}
},
mounted() {
this.$store.commit("setActiveMenu", "admin-articles");
}
}
</script>
<style scoped>
</style>

164
pages/admin/comments.vue Normal file
View File

@ -0,0 +1,164 @@
<template>
<el-row class="article__wrapper" style="margin-top: 20px;">
<el-col style="margin-bottom: 1rem;">
<el-breadcrumb separator-class="el-icon-arrow-right">
<el-breadcrumb-item :to="{ path: '/admin/dashboard' }">首页</el-breadcrumb-item>
<el-breadcrumb-item>评论管理</el-breadcrumb-item>
</el-breadcrumb>
</el-col>
<el-col>
<el-pagination
:hide-on-single-page="true"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="pagination.currentPage"
:page-sizes="[10, 20, 50, 100]"
:page-size="pagination.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="pagination.total">
</el-pagination>
</el-col>
<el-col>
<el-col v-for="comment in comments" :key="comment.idComment">
<el-card style="margin-top: 1rem;" :id="'comment-' + comment.idComment">
<el-col :xs="3" :sm="1" :xl="1">
<el-avatar v-show="comment.commenter.userAvatarURL" :src="comment.commenter.userAvatarURL"></el-avatar>
<el-avatar v-show="!comment.commenter.userAvatarURL"
src="https://static.rymcu.com/article/1578475481946.png"></el-avatar>
</el-col>
<el-col :xs="21" :sm="23" :xl="23">
<el-col style="margin-left: 1rem;">
<el-col v-show="comment.commentOriginalCommentId">
<el-col :span="16">
<el-link rel="nofollow" @click="onRouter('user', comment.commenter.userAccount)" :underline="false"
class="text-default">{{ comment.commenter.userNickname }}
</el-link>
<small class="text-default" style="margin: 0 0.2rem">回复了</small><span style="font-weight: bold;"> {{comment.commentOriginalAuthorNickname}}</span>
</el-col>
<el-col :span="8" class="text-right" style="padding-right: 1rem;">
<el-link rel="nofollow" :underline="false" title="查看原评论"
@click.native="toggleShowOriginalComment(comment.commentOriginalCommentId)"><i
class="el-icon-reading"></i> 查看原评论</el-link>
</el-col>
</el-col>
<el-col v-show="!comment.commentOriginalCommentId">
<el-col :span="16">
<el-link rel="nofollow" @click="onRouter('user', comment.commenter.userAccount)" :underline="false"
class="text-default">{{ comment.commenter.userNickname }}
</el-link>
</el-col>
</el-col>
</el-col>
<el-col style="padding: 1rem;">
<el-col>
<div class="vditor-reset comment-content" v-html="comment.commentContent"></div>
</el-col>
</el-col>
<el-col :span="16" style="padding-left: 1rem;">
<el-link rel="nofollow" :underline="false" class="text-default">{{ comment.timeAgo }}</el-link>
</el-col>
</el-col>
</el-card>
<el-col :id="'original-' + comment.commentOriginalCommentId" style="background-color: #d9d9d9;padding-left: 1.5rem;
margin-top: 0.3rem;border-radius: 0.5rem;cursor: pointer;display: none;">
<el-col v-show="comment.commentOriginalCommentId" :span="2">
<p>
<span>{{comment.commentOriginalAuthorNickname}} :</span>
</p>
</el-col>
<el-col v-show="comment.commentOriginalCommentId" :span="20">
<div class="vditor-reset comment-content" v-html="comment.commentOriginalContent"></div>
</el-col>
</el-col>
</el-col>
</el-col>
<el-col>
<el-pagination
:hide-on-single-page="true"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="pagination.currentPage"
:page-sizes="[10, 20, 50, 100]"
:page-size="pagination.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="pagination.total">
</el-pagination>
</el-col>
</el-row>
</template>
<script>
import {mapState} from 'vuex';
export default {
name: "comments",
fetch({store, params, error}) {
return Promise.all([
store
.dispatch('admin/fetchComments', params)
.catch(err => error({statusCode: 404}))
])
},
computed: {
...mapState({
comments: state => state.admin.comment.comments,
pagination: state => state.admin.comment.pagination
})
},
data() {
return {
order: 'desc',
idRole: 0,
idUser: 0,
dialogVisible: false
}
},
methods: {
onRouter(name, data) {
this.$router.push(
{
path: '/user/' + data
}
)
},
toggleShowOriginalComment(commentId) {
let ele = document.getElementById('original-' + commentId);
if (ele.style.display === 'none') {
ele.style.display = 'block';
} else {
ele.style.display = 'none';
}
},
handleSizeChange(pageSize) {
let _ts = this;
_ts.$store.dispatch('admin/fetchComments', {
page: _ts.pagination.currentPage,
rows: pageSize
})
},
handleCurrentChange(page) {
let _ts = this;
_ts.$store.dispatch('admin/fetchComments', {
page: page,
rows: _ts.pagination.pageSize
})
},
toggleStatus() {}
},
mounted() {
this.$store.commit("setActiveMenu", "admin-comments");
}
}
</script>
<style lang="scss">
@import "~vditor/src/assets/scss/index.scss";
.article__wrapper {
margin: 20px auto;
display: block;
padding-left: 1rem;
padding-right: 1rem;
box-sizing: border-box;
}
</style>

View File

@ -21,18 +21,64 @@
<el-input v-model="topic.topicUri"></el-input>
</el-form-item>
<el-form-item label="图标">
<el-upload
class="avatar-uploader"
:action="tokenURL.URL"
:multiple="true"
:with-credentials="true"
:headers="uploadHeaders"
:show-file-list="false"
:on-success="handleAvatarSuccess"
:before-upload="beforeAvatarUpload">
<img v-if="topicIconPath" class="topic-brand-img" :src="topicIconPath">
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>
<el-row>
<el-col :span="24">
<vue-cropper
ref="cropper"
:aspect-ratio="1 / 1"
:src="topicIconPath"
:checkCrossOrigin="false"
:checkOrientation="false"
:imgStyle="{width: '480px', height: '480px'}"
:autoCropArea="1"
:autoCrop="autoCrop"
preview=".preview"
/>
</el-col>
<el-col :span="24" style="margin-top: 2rem;">
<el-col :span="8">
<el-card>
<div class="card-body d-flex flex-column">
<el-col :span="4" style="text-align: right;">
<div v-if="topicIconPath" class="preview preview-large topic-brand-img"/>
<el-image v-else class="topic-brand-img" />
</el-col>
<el-col :span="20">
<el-col>
<el-col>
<el-link rel="nofollow" :underline="false">
<h4>{{ topic.topicTitle }}</h4>
</el-link>
</el-col>
<el-col>
<div class="text-muted article-summary-md">{{ topic.topicDescription }}</div>
</el-col>
</el-col>
</el-col>
</div>
</el-card>
</el-col>
</el-col>
<el-col :span="24" style="margin-top: 2rem;">
<el-upload
class="avatar-uploader"
action=""
:multiple="true"
:show-file-list="false"
:on-success="handleAvatarSuccess"
:before-upload="beforeAvatarUpload">
<div>
<el-button type="primary" round plain>上传</el-button>
</div>
</el-upload>
<el-button style="margin-top: 1rem;" type="primary" round plain @click.prevent="reset">重置</el-button>
<el-button type="primary" round plain @click.prevent="cropImage">裁剪</el-button>
<el-col>
<span style="color: red;padding-right: 5px;">*</span>
<span>上传图片调整至最佳效果后,请点击裁剪按钮截取</span>
</el-col>
</el-col>
</el-row>
</el-form-item>
<el-form-item label="导航主题">
<el-switch
@ -71,14 +117,21 @@
<script>
import Vue from 'vue';
import {mapState} from 'vuex';
import VueCropper from 'vue-cropperjs';
import 'cropperjs/dist/cropper.css';
export default {
name: "adminTopicPost",
components: {
VueCropper
},
computed: {
uploadHeaders() {
let token = this.$store.getters.uploadHeaders;
return {'X-Upload-Token': token}
}
...mapState({
uploadHeaders: state => {
return {'X-Upload-Token': state.uploadHeaders}
}
})
},
data() {
return {
@ -107,6 +160,7 @@ export default {
},
topicIconPath: '',
isEdit: false,
autoCrop: true,
notificationFlag: true
}
},
@ -114,44 +168,44 @@ export default {
_initEditor(data) {
let _ts = this;
let toolbar = [
'emoji',
'headings',
'bold',
'italic',
'strike',
'link',
'|',
'list',
'ordered-list',
'check',
'outdent',
'indent',
'|',
'quote',
'line',
'code',
'inline-code',
'insert-before',
'insert-after',
'|',
'upload',
// 'record',
'table',
'|',
'undo',
'redo',
'|',
'edit-mode',
{
name: 'more',
toolbar: [
'fullscreen',
'both',
'preview',
'info'
],
}]
let toolbar = [
'emoji',
'headings',
'bold',
'italic',
'strike',
'link',
'|',
'list',
'ordered-list',
'check',
'outdent',
'indent',
'|',
'quote',
'line',
'code',
'inline-code',
'insert-before',
'insert-after',
'|',
'upload',
// 'record',
'table',
'|',
'undo',
'redo',
'|',
'edit-mode',
{
name: 'more',
toolbar: [
'fullscreen',
'both',
'preview',
'info'
],
}]
return new Vue.Vditor(data.id, {
toolbar,
mode: 'sv',
@ -191,9 +245,7 @@ export default {
url: this.tokenURL.URL,
linkToImgUrl: this.tokenURL.linkToImageURL,
token: this.tokenURL.token,
filename: name => name.replace(/[^(a-zA-Z0-9\u4e00-\u9fa5\.)]/g, '').
replace(/[\?\\/:|<>\*\[\]\(\)\$%\{\}@~]/g, '').
replace('/\\s/g', '')
filename: name => name.replace(/[^(a-zA-Z0-9\u4e00-\u9fa5\.)]/g, '').replace(/[\?\\/:|<>\*\[\]\(\)\$%\{\}@~]/g, '').replace('/\\s/g', '')
},
height: data.height,
counter: 102400,
@ -222,11 +274,24 @@ export default {
if (!(isJPG || isPNG)) {
this.$message.error('上传图标只能是 JPG 或者 PNG 格式!');
return false;
}
if (!isLt2M) {
this.$message.error('上传图标大小不能超过 2MB!');
return false;
}
this.fileToBase64(file);
return false;
},
fileToBase64(file) {
let _ts = this;
let reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = function () {
_ts.$set(_ts, 'topicIconPath', this.result);
_ts.$refs.cropper.replace(this.result);
}
return (isJPG || isPNG) && isLt2M;
},
async updateTopic() {
let _ts = this;
@ -254,6 +319,24 @@ export default {
})
}
})
},
reset() {
this.$refs.cropper.reset();
},
// get image data for post processing, e.g. upload or setting image src
cropImage() {
let _ts = this;
try {
_ts.cropImg = _ts.$refs.cropper.getCroppedCanvas().toDataURL();
let topic = _ts.topic;
topic.topicIconPath = _ts.cropImg;
_ts.$set(_ts, 'topic', topic);
_ts.$set(_ts, 'topicIconPath', _ts.cropImg);
_ts.$message.success('已裁剪 !');
} catch (e) {
_ts.$message.error('图片获取失败 !');
return;
}
}
},
beforeRouteLeave(to, from, next) {
@ -306,7 +389,7 @@ export default {
const responseData = await _ts.$axios.$get('/api/admin/topic/detail/' + _ts.$route.params.topic_id);
_ts.$set(_ts, 'topic', responseData);
if (responseData.topicIconPath) {
_ts.$set(_ts,'topicIconPath',responseData.topicIconPath);
_ts.$set(_ts, 'topicIconPath', responseData.topicIconPath);
}
} else {
_ts.$set(_ts, 'isEdit', false);
@ -329,5 +412,41 @@ export default {
</script>
<style lang="scss">
@import "~vditor/src/assets/scss/index.scss";
@import "~vditor/src/assets/scss/index.scss";
.preview-area {
width: 16rem;
}
.preview-area p {
font-size: 1.25rem;
}
.preview-area p:last-of-type {
margin-top: 1rem;
}
.crop-placeholder {
width: 36px;
height: 36px;
background: #ccc;
}
.cropped-image img {
max-width: 100%;
}
.img-cropper {
width: 480px;
min-height: 480px;
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC);
}
.preview-large {
width: 100%;
height: 144px;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
background-color: #ffffff;
overflow: hidden;
}
</style>

View File

@ -97,7 +97,6 @@
<script>
import Vue from 'vue';
import {mapState} from 'vuex';
import saveSvg from 'save-svg-as-png';
import VueCropper from 'vue-cropperjs';
import 'cropperjs/dist/cropper.css';

0
pages/tag/_tag_uri.vue Normal file
View File

View File

@ -14,11 +14,27 @@ const getDefaultRolesData = () => {
}
}
const getDefaultArticlesData = () => {
return {
articles: [],
pagination: {}
}
}
const getDefaultCommentsData = () => {
return {
comments: [],
pagination: {}
}
}
export const state = () => {
return {
fetching: false,
user: getDefaultUsersData(),
role: getDefaultRolesData()
role: getDefaultRolesData(),
article: getDefaultArticlesData(),
comment: getDefaultCommentsData()
}
}
@ -33,6 +49,20 @@ export const mutations = {
updateRolesData(state, action) {
state.role.roles = action.roles
state.role.pagination = action.pagination
},
updateArticlesData(state, action) {
state.article.articles = action.articles
state.article.pagination = action.pagination
},
updateCommentsData(state, action) {
state.comment.comments = action.comments
state.comment.pagination = action.pagination
},
updateArticlePreference(state, action) {
let article = state.article.articles[action.index]
if (article.idArticle === action.idArticle) {
article.articlePerfect = action.articlePerfect
}
}
}
@ -82,5 +112,52 @@ export const actions = {
console.log(error);
commit('updateFetching', false);
});
},
fetchArticles({commit}, params = {}) {
// 清空已有数据
commit('updateArticlesData', getDefaultArticlesData())
commit('updateFetching', true)
let data = {
page: params.page || 1,
rows: params.rows || 10,
topicUri: 'news'
}
return this.$axios
.$get(`${ADMIN_API_PATH}/articles`, {
params: data
})
.then(response => {
commit('updateFetching', false);
commit('updateArticlesData', response);
})
.catch(error => {
console.log(error);
commit('updateFetching', false);
});
},
fetchComments({commit}, params = {}) {
// 清空已有数据
commit('updateCommentsData', getDefaultArticlesData())
commit('updateFetching', true)
let data = {
page: params.page || 1,
rows: params.rows || 10
}
return this.$axios
.$get(`${ADMIN_API_PATH}/comments`, {
params: data
})
.then(response => {
commit('updateFetching', false);
commit('updateCommentsData', response);
})
.catch(error => {
console.log(error);
commit('updateFetching', false);
});
}
}