作品集模块功能开发

This commit is contained in:
x ronger 2020-08-02 00:25:44 +08:00
parent 3535025547
commit 754637f7c6
18 changed files with 1191 additions and 497 deletions

View File

@ -18,17 +18,22 @@
<div class="text-muted article-summary-md">{{ article.articlePreviewContent }}</div> <div class="text-muted article-summary-md">{{ article.articlePreviewContent }}</div>
<el-row class="pt-5"> <el-row class="pt-5">
<el-col :xs="3" :sm="1" :xl="1" class="mr-3"> <el-col :xs="3" :sm="1" :xl="1" class="mr-3">
<el-avatar v-if="article.articleAuthorAvatarUrl" size="medium" :src="article.articleAuthorAvatarUrl"></el-avatar> <el-avatar v-if="article.articleAuthorAvatarUrl" size="medium"
<el-avatar v-else size="medium" src="https://rymcu.com/vertical/article/1578475481946.png"></el-avatar> :src="article.articleAuthorAvatarUrl"></el-avatar>
<el-avatar v-else size="medium"
src="https://rymcu.com/vertical/article/1578475481946.png"></el-avatar>
</el-col> </el-col>
<el-col :xs="20" :sm="20" :xl="20"> <el-col :xs="20" :sm="20" :xl="20">
<div> <div>
<el-link @click="onRouter('user', article.articleAuthorName)" :underline="false" class="text-default">{{ article.articleAuthorName }}</el-link> <el-link @click="onRouter('user', article.articleAuthorName)" :underline="false" class="text-default">
{{ article.articleAuthorName }}
</el-link>
<small class="d-block text-muted">{{ article.timeAgo }}</small> <small class="d-block text-muted">{{ article.timeAgo }}</small>
</div> </div>
</el-col> </el-col>
<el-col class="text-right"> <el-col class="text-right">
<el-link :underline="false" title="总浏览数"><i class="el-icon-s-data"></i><span style="color: red;">{{ article.articleViewCount }}</span></el-link> <el-link :underline="false" title="总浏览数"><i class="el-icon-s-data"></i><span style="color: red;">{{ article.articleViewCount }}</span>
</el-link>
</el-col> </el-col>
</el-row> </el-row>
</div> </div>

View File

@ -1,10 +1,7 @@
<template> <template>
<el-row class="pt-5"> <el-row class="pt-5">
<el-col v-if="isFetching"> <el-col>
加载中 <el-col v-show="user" style="margin-top: 1rem;">
</el-col>
<el-col v-else>
<el-col v-if="user" style="margin-top: 1rem;">
<el-col :xs="2" :sm="1" :xl="1"> <el-col :xs="2" :sm="1" :xl="1">
<el-avatar :src="avatar"></el-avatar> <el-avatar :src="avatar"></el-avatar>
</el-col> </el-col>
@ -18,7 +15,7 @@
size="40%"> size="40%">
<el-col slot="title"> <el-col slot="title">
<el-col> <el-col>
<el-avatar v-if="commentAuthorAvatar" :src="commentAuthorAvatar"></el-avatar> <el-avatar v-show="commentAuthorAvatar" :src="commentAuthorAvatar"></el-avatar>
<span class="text-default" style="padding-left: 1rem;">{{ commentTitle }}</span> <span class="text-default" style="padding-left: 1rem;">{{ commentTitle }}</span>
</el-col> </el-col>
</el-col> </el-col>
@ -31,7 +28,7 @@
</el-drawer> </el-drawer>
</el-col> </el-col>
</el-col> </el-col>
<el-col v-else class="text-center" style="margin-top: 1rem;"> <el-col v-show="!user" class="text-center" style="margin-top: 1rem;">
<el-button type="primary" size="medium" @click="gotoLogin">登录</el-button> <el-button type="primary" size="medium" @click="gotoLogin">登录</el-button>
后发布评论 后发布评论
</el-col> </el-col>
@ -39,18 +36,18 @@
<el-col v-for="comment in comment.data" :key="comment.idComment"> <el-col v-for="comment in comment.data" :key="comment.idComment">
<el-card style="margin-bottom: 0.5rem;"> <el-card style="margin-bottom: 0.5rem;">
<el-col :xs="3" :sm="1" :xl="1"> <el-col :xs="3" :sm="1" :xl="1">
<el-avatar v-if="comment.commenter.userAvatarURL" :src="comment.commenter.userAvatarURL"></el-avatar> <el-avatar v-show="comment.commenter.userAvatarURL" :src="comment.commenter.userAvatarURL"></el-avatar>
<el-avatar v-else src="https://rymcu.com/vertical/article/1578475481946.png"></el-avatar> <el-avatar v-show="!comment.commenter.userAvatarURL" src="https://rymcu.com/vertical/article/1578475481946.png"></el-avatar>
</el-col> </el-col>
<el-col :xs="21" :sm="23" :xl="23"> <el-col :xs="21" :sm="23" :xl="23">
<el-col style="margin-left: 1rem;"> <el-col style="margin-left: 1rem;">
<el-col v-if="comment.commentOriginalCommentId"> <el-col v-show="comment.commentOriginalCommentId">
<el-link @click="onRouter('user', comment.commenter.userNickname)" :underline="false" <el-link @click="onRouter('user', comment.commenter.userNickname)" :underline="false"
class="text-default">{{ comment.commenter.userNickname }} class="text-default">{{ comment.commenter.userNickname }}
</el-link> </el-link>
<small class="text-default" style="margin: 0 0.2rem">回复了</small><span style="font-weight: bold;"> {{comment.commentOriginalAuthorNickname}}</span> <small class="text-default" style="margin: 0 0.2rem">回复了</small><span style="font-weight: bold;"> {{comment.commentOriginalAuthorNickname}}</span>
</el-col> </el-col>
<el-col v-else> <el-col v-show="!comment.commentOriginalCommentId">
<el-link @click="onRouter('user', comment.commenter.userNickname)" :underline="false" <el-link @click="onRouter('user', comment.commenter.userNickname)" :underline="false"
class="text-default">{{ comment.commenter.userNickname }} class="text-default">{{ comment.commenter.userNickname }}
</el-link> </el-link>
@ -147,6 +144,7 @@
) )
}, },
_initEditor (data) { _initEditor (data) {
let _ts = this;
let toolbar; let toolbar;
if (window.innerWidth < 768) { if (window.innerWidth < 768) {
toolbar = [ toolbar = [
@ -200,6 +198,9 @@
enable: this.postId ? false : true, enable: this.postId ? false : true,
id: this.postId ? this.postId : '', id: this.postId ? this.postId : '',
}, },
after() {
_ts.contentEditor.setValue(data.value ? data.value : '');
},
preview: { preview: {
markdown: { markdown: {
toc: true, toc: true,
@ -212,7 +213,7 @@
return return
} }
// LazyLoadImage(); // LazyLoadImage();
Vue.Vditor.highlightRender({style:'github'}, element, document); // Vue.Vditor.highlightRender({style:'github'}, element, document);
} }
}, },
upload: { upload: {
@ -242,8 +243,8 @@
height: 200, height: 200,
placeholder: '', //this.$t('inputContent', this.$store.state.locale) placeholder: '', //this.$t('inputContent', this.$store.state.locale)
resize: false, resize: false,
value: ''
}); });
_ts.contentEditor.setValue('');
}, 500); }, 500);
} }
}, },
@ -311,7 +312,6 @@
}, },
watch: { watch: {
isFetching(isFetching) { isFetching(isFetching) {
console.log(isFetching)
if (isFetching) { if (isFetching) {
this.cancelCommentReply() this.cancelCommentReply()
} }

View File

@ -5,10 +5,10 @@
<el-col class="card"> <el-col class="card">
<el-col class="card-body d-flex flex-column"> <el-col class="card-body d-flex flex-column">
<el-col :span="4" class="mr-3"> <el-col :span="4" class="mr-3">
<a v-if="portfolio.headImgUrl"> <a v-show="portfolio.headImgUrl">
<el-image class="card-img-top" style="height: 10rem;" :src="portfolio.headImgUrl"></el-image> <el-image class="card-img-top" style="height: 10rem;" :src="portfolio.headImgUrl"></el-image>
</a> </a>
<a v-else> <a v-show="!portfolio.headImgUrl">
<el-image class="card-img-top" style="height: 10rem;" <el-image class="card-img-top" style="height: 10rem;"
src="https://rymcu.com/vertical/article/1574441170152.jpg"></el-image> src="https://rymcu.com/vertical/article/1574441170152.jpg"></el-image>
</a> </a>
@ -28,7 +28,7 @@
</el-col> </el-col>
</el-col> </el-col>
</el-col> </el-col>
<el-col v-if="!portfolios" class="text-center"> <el-col v-show="!portfolios" class="text-center">
这里什么都没有! 这里什么都没有!
</el-col> </el-col>
<el-col> <el-col>

View File

@ -33,51 +33,54 @@
/> />
</el-col> </el-col>
<!--<el-col v-if="user" :xs="0" :sm="8" :xl="6">--> <!--<el-col v-if="user" :xs="0" :sm="8" :xl="6">-->
<el-col v-if="user"> <client-only>
<el-link :underline="false" style="padding-left: 10px;padding-right: 10px;" href="/post-portfolio">创建作品集 <el-col v-if="user">
</el-link> <el-link :underline="false" style="padding-left: 10px;padding-right: 10px;" href="/portfolio/post">创建作品集
<el-link :underline="false" style="padding-left: 10px;padding-right: 10px;" href="/post-article">发帖</el-link> </el-link>
<el-link :underline="false" style="padding-left: 10px;padding-right: 10px;"> <el-link :underline="false" style="padding-left: 10px;padding-right: 10px;" href="/article/post">发帖
<el-dropdown trigger="click" @command="handleCommand"> </el-link>
<el-badge :value="notificationNumbers" class="item"> <el-link :underline="false" style="padding-left: 10px;padding-right: 10px;">
<el-link :underline="false" style="font-size: 1.4rem;" class="el-icon-bell"></el-link> <el-dropdown trigger="click" @command="handleCommand">
</el-badge> <el-badge :value="notificationNumbers" class="item">
<el-dropdown-menu slot="dropdown"> <el-link :underline="false" style="font-size: 1.4rem;" class="el-icon-bell"></el-link>
<el-dropdown-item v-for="notification in notifications" :key="notification.idNotification" </el-badge>
command="notification">{{ notification.dataSummary }} <el-dropdown-menu slot="dropdown">
</el-dropdown-item> <el-dropdown-item v-for="notification in notifications" :key="notification.idNotification"
<el-dropdown-item command="notification">查看所有消息</el-dropdown-item> command="notification">{{ notification.dataSummary }}
</el-dropdown-menu> </el-dropdown-item>
</el-dropdown> <el-dropdown-item command="notification">查看所有消息</el-dropdown-item>
</el-link> </el-dropdown-menu>
<el-link :underline="false" style="margin-left: 10px;"> </el-dropdown>
<el-dropdown trigger="click" @command="handleCommand"> </el-link>
<el-avatar v-if="avatarURL" size="small" :src="avatarURL"></el-avatar> <el-link :underline="false" style="margin-left: 10px;">
<el-avatar v-else size="small" src="https://rymcu.com/vertical/article/1578475481946.png"></el-avatar> <el-dropdown trigger="click" @command="handleCommand">
<el-dropdown-menu slot="dropdown"> <el-avatar v-if="avatarURL" size="small" :src="avatarURL"></el-avatar>
<el-dropdown-item command="user" style="align-items: center;"> <el-avatar v-else size="small" src="https://rymcu.com/vertical/article/1578475481946.png"></el-avatar>
<el-avatar class="mr-3" v-if="avatarURL" size="small" style="margin-top: 1rem;" <el-dropdown-menu slot="dropdown">
:src="avatarURL"></el-avatar> <el-dropdown-item command="user" style="align-items: center;">
<el-avatar class="mr-3" v-else size="small" style="margin-top: 1rem;" <el-avatar class="mr-3" v-if="avatarURL" size="small" style="margin-top: 1rem;"
src="https://rymcu.com/vertical/article/1578475481946.png"></el-avatar> :src="avatarURL"></el-avatar>
<el-link :underline="false" style="margin-left: 10px;margin-bottom: 1rem;">{{ nickname }}</el-link> <el-avatar class="mr-3" v-else size="small" style="margin-top: 1rem;"
</el-dropdown-item> src="https://rymcu.com/vertical/article/1578475481946.png"></el-avatar>
<el-dropdown-item v-show="hasPermissions" command="admin-dashboard">系统管理</el-dropdown-item> <el-link :underline="false" style="margin-left: 10px;margin-bottom: 1rem;">{{ nickname }}</el-link>
<el-dropdown-item command="user-info">资料与账号</el-dropdown-item> </el-dropdown-item>
<el-dropdown-item command="drafts">我的草稿</el-dropdown-item> <el-dropdown-item v-if="hasPermissions" command="admin-dashboard">系统管理</el-dropdown-item>
<el-dropdown-item command="logout" divided>退出登录</el-dropdown-item> <el-dropdown-item command="user-info">资料与账号</el-dropdown-item>
</el-dropdown-menu> <el-dropdown-item command="drafts">我的草稿</el-dropdown-item>
</el-dropdown> <el-dropdown-item command="logout" divided>退出登录</el-dropdown-item>
</el-link> </el-dropdown-menu>
</el-col> </el-dropdown>
<el-col v-else> </el-link>
<nuxt-link to="/login"> </el-col>
<el-link :underline="false" style="margin-left: 10px;">登录</el-link> <el-col v-else>
</nuxt-link> <nuxt-link to="/login">
<nuxt-link to="/register"> <el-link :underline="false" style="margin-left: 10px;">登录</el-link>
<el-link :underline="false" style="margin-left: 10px;">注册</el-link> </nuxt-link>
</nuxt-link> <nuxt-link to="/register">
</el-col> <el-link :underline="false" style="margin-left: 10px;">注册</el-link>
</nuxt-link>
</el-col>
</client-only>
</el-col> </el-col>
</el-col> </el-col>
</el-row> </el-row>

View File

@ -3,7 +3,7 @@ import {NODE_ENV} from '../environment'
const apisMap = { const apisMap = {
development: { development: {
FE: 'http://localhost:3000', FE: 'http://localhost:3000',
BASE: 'https://rymcu.com/vertical-console', BASE: 'http://localhost:8099/vertical',
CDN: '', CDN: '',
PROXY: '/proxy', PROXY: '/proxy',
SOCKET: 'http://localhost:3000', SOCKET: 'http://localhost:3000',
@ -11,7 +11,7 @@ const apisMap = {
}, },
production: { production: {
FE: 'https://rymcu.com', FE: 'https://rymcu.com',
BASE: 'https://api.rymcu.com', BASE: 'https://rymcu.com',
CDN: 'https://cdn.rymcu.com', CDN: 'https://cdn.rymcu.com',
PROXY: 'https://cdn.rymcu.com/proxy', PROXY: 'https://cdn.rymcu.com/proxy',
SOCKET: 'https://rymcu.com', SOCKET: 'https://rymcu.com',

View File

@ -75,7 +75,7 @@ export default {
proxy: [ //proxy配置 proxy: [ //proxy配置
['/api', { ['/api', {
target: apiConfig.BASE, //api请求路径 target: apiConfig.BASE, //api请求路径
pathRewrite: { '^/api' : '/api/v1' } //重定向请求路径防止路由、api路径的冲突 pathRewrite: {'^/api': isDevMode ? '/api/v1' : '/api'} //重定向请求路径防止路由、api路径的冲突
}] }]
], ],
/* /*

784
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -16,7 +16,7 @@
"express": "^4.17.1", "express": "^4.17.1",
"js-cookie": "^2.2.1", "js-cookie": "^2.2.1",
"nuxt": "^2.14.0", "nuxt": "^2.14.0",
"vditor": "^3.4.0" "vditor": "^3.4.1"
}, },
"devDependencies": { "devDependencies": {
"@nuxtjs/proxy": "^2.0.1", "@nuxtjs/proxy": "^2.0.1",

View File

@ -190,10 +190,7 @@
let _ts = this; let _ts = this;
if (item === 'edit') { if (item === 'edit') {
_ts.$router.push({ _ts.$router.push({
name: 'post-article', path: `/article/post/${_ts.article.idArticle}`
query: {
id: _ts.article.idArticle
}
}) })
} else { } else {
_ts.$axios.$get('/api/article/' + _ts.article.idArticle + '/share').then(function (res) { _ts.$axios.$get('/api/article/' + _ts.article.idArticle + '/share').then(function (res) {

View File

@ -0,0 +1,362 @@
<template>
<el-row class="articles">
<el-col>
<el-input
v-model="articleTitle"
class="article-title"
placeholder="请输入标题"
@change="setLocalstorage('title',articleTitle)">
</el-input>
</el-col>
<el-col>
<div id="contentEditor"></div>
</el-col>
<el-col style="margin-top: 1rem;">
<el-select
style="width: 100%;"
v-model="articleTags"
multiple
filterable
allow-create
default-first-option
remote
:remote-method="remoteMethod"
placeholder="请选择文章标签"
:loading="loading"
@change="setLocalstorage('tags',articleTags)">
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</el-col>
<el-col v-if="!isEdit" style="margin-top: 1rem;padding-right:3rem;text-align: right;">
<el-button :loading="doLoading" @click="saveArticle">保存草稿</el-button>
<el-button type="primary" :loading="doLoading" @click="postArticle">发布</el-button>
</el-col>
<el-col v-else style="margin-top: 1rem;padding-right:3rem;text-align: right;">
<el-button type="danger" :loading="doLoading" @click="deleteArticle">删除</el-button>
<el-button v-if="articleStatus === '1'" :loading="doLoading" @click="saveArticle">保存草稿</el-button>
<el-button v-if="articleStatus === '0'" :loading="doLoading" type="primary" @click="postArticle">更新</el-button>
<el-button v-else type="primary" :loading="doLoading" @click="postArticle">发布</el-button>
</el-col>
</el-row>
</template>
<script>
import Vue from 'vue';
import {mapState} from 'vuex';
export default {
name: "PostArticle",
validate({params, store}) {
if (typeof params.article_id === 'undefined') {
return true;
}
return params.article_id && !isNaN(Number(params.article_id))
},
fetch({store, params, error}) {
return Promise.all([
store.dispatch('article/fetchPostDetail', params)
.catch(err => error({statusCode: 404}))
])
},
computed: {
...mapState({
article: state => state.article.detail.data
})
},
data() {
return {
contentEditor: null,
tokenURL: {},
idArticle: 0,
articleTitle: '',
articleContent: '',
articleType: 0,
articleTags: [],
articleStatus: '0',
options: [],
list: [],
loading: false,
doLoading: false,
isEdit: false
}
},
methods: {
_initEditor(data) {
let _ts = this;
let toolbar;
if (window.innerWidth < 768) {
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',
'content-theme',
'code-theme',
{
name: 'more',
toolbar: [
'fullscreen',
'both',
'format',
'preview',
'info',
'help',
],
}]
}
return new Vue.Vditor(data.id, {
toolbar,
mode: 'sv',
tab: '\t',
cache: {
enable: this.$route.params.article_id ? false : true,
id: this.$route.params.article_id ? this.$route.params.article_id : '',
},
after() {
_ts.contentEditor.setValue(data.value ? data.value : '');
},
preview: {
markdown: {
toc: true,
},
math: {
inlineDigit: true
},
delay: 500,
mode: data.mode,
/*url: `${process.env.Server}/api/console/markdown`,*/
parse: (element) => {
if (element.style.display === 'none') {
return
}
// LazyLoadImage();
// Vue.Vditor.highlightRender({style: 'github'}, element, this.contentEditor);
}
},
upload: {
max: 10 * 1024 * 1024,
url: this.tokenURL.URL,
linkToImgUrl: this.tokenURL.URL,
token: this.tokenURL.token,
filename: name => name.replace(/\?|\\|\/|:|\||<|>|\*|\[|\]|\s+/g, '-')
},
height: data.height,
counter: 102400,
resize: {
enable: data.resize,
},
lang: this.$store.state.locale,
placeholder: data.placeholder,
})
},
setLocalstorage(type) {
if (typeof arguments[0] === 'object') {
localStorage.setItem('articleTags', arguments[1]);
return
}
switch (type) {
case 'title': {
localStorage.setItem('article-title', arguments[1])
break;
}
case 'tags': {
localStorage.setItem('article-tags', arguments[1]);
break;
}
}
},
remoteMethod(query) {
if (query !== '') {
this.loading = true;
setTimeout(() => {
this.loading = false;
this.options = this.list.filter(item => {
return item.label.toLowerCase()
.indexOf(query.toLowerCase()) > -1;
});
}, 200);
} else {
this.options = [];
}
},
deleteArticle() {
let _ts = this;
_ts.doLoading = true;
this.$confirm('确定删除吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
let id = _ts.$route.params.article_id;
_ts.$axios.$delete('/api/article/delete/' + id).then(function (res) {
if (res && res.message) {
_ts.$message(res.message);
return false;
}
localStorage.removeItem('article-title');
localStorage.removeItem('article-tags');
_ts.contentEditor.setValue('');
_ts.$router.push({
name: 'home'
})
})
}).catch(() => {
_ts.doLoading = false;
});
},
async postArticle() {
let _ts = this;
_ts.doLoading = true;
let id = _ts.$route.params.article_id;
let articleContent = _ts.contentEditor.getValue();
let articleContentHtml = await _ts.contentEditor.getHTML();
if (!(_ts.articleTitle && articleContent)) {
_ts.$message("标题/正文不能为空!");
_ts.doLoading = false;
return false;
}
let article = {
idArticle: _ts.idArticle,
articleTitle: _ts.articleTitle,
articleContent: articleContent,
articleContentHtml: articleContentHtml,
articleTags: _ts.articleTags.join(","),
articleStatus: 0
};
_ts.$axios[id ? '$put' : '$post']('/api/article/post', article).then(function (res) {
if (res) {
if (res.message) {
_ts.$message(res.message);
_ts.doLoading = false;
return false;
}
localStorage.removeItem('article-title');
localStorage.removeItem('article-tags');
_ts.contentEditor.setValue('');
_ts.$router.push({
path: `/article/${res.id}`
})
}
})
},
async saveArticle() {
let _ts = this;
_ts.doLoading = true;
let id = _ts.$route.params.article_id;
let articleContent = _ts.contentEditor.getValue();
let articleContentHtml = await _ts.contentEditor.getHTML();
if (!(_ts.articleTitle && articleContent)) {
_ts.$message("标题/正文不能为空!");
_ts.doLoading = false;
return false;
}
let article = {
idArticle: _ts.idArticle,
articleTitle: _ts.articleTitle,
articleContent: articleContent,
articleContentHtml: articleContentHtml,
articleTags: _ts.articleTags.join(","),
articleStatus: 1
};
_ts.$axios[id ? '$put' : '$post']('/api/article/post', article).then(function (res) {
if (res) {
if (res.message) {
_ts.$message(res.message);
return false;
}
localStorage.removeItem('article-title');
localStorage.removeItem('article-tags');
_ts.contentEditor.setValue('');
_ts.$router.push({
name: 'draft',
params: {
id: res.id
}
})
}
})
},
getTags() {
let _ts = this;
_ts.$axios.$get('/api/tag/tags').then(function (res) {
if (res) {
_ts.$set(_ts, 'list', res);
}
})
}
},
async mounted() {
let _ts = this;
_ts.$store.commit('setActiveMenu', 'post-article');
const responseData = await _ts.$axios.$get('/api/upload/token');
if (responseData) {
_ts.$set(_ts, 'tokenURL', {
token: responseData.uploadToken || '',
URL: responseData.uploadURL || '',
})
}
_ts.getTags();
Vue.nextTick(() => {
let articleContent = '';
if (_ts.$route.params.article_id) {
_ts.$set(_ts, 'isEdit', true);
let article = _ts.article;
_ts.$set(_ts, 'idArticle', article.idArticle);
_ts.$set(_ts, 'articleTitle', article.articleTitle);
_ts.$set(_ts, 'articleContent', article.articleContent);
_ts.$set(_ts, 'articleStatus', article.articleStatus);
_ts.$set(_ts, 'articleTags', (article.articleTags).split(','));
localStorage.setItem("article-title", article.articleTitle);
localStorage.setItem("article-tags", (article.articleTags).split(','));
articleContent = article.articleContent
} else {
_ts.$set(_ts, 'isEdit', false);
}
_ts.contentEditor = _ts._initEditor({
id: 'contentEditor',
mode: 'both',
height: 480,
placeholder: '', //_ts.$t('inputContent', _ts.$store.state.locale)
resize: false,
value: articleContent
});
})
}
}
</script>
<style lang="scss">
@import "~vditor/src/assets/scss/index.scss";
</style>

View File

@ -5,21 +5,20 @@
</template> </template>
<script> <script>
import ArticleList from '~/components/archive/list' import ArticleList from '~/components/archive/list'
import {mapState} from 'vuex';
export default { export default {
name: 'Index', name: 'Index',
fetch({store}) { fetch() {
return Promise.all([ return this.$store.dispatch('article/fetchList', {page: 1})
store.dispatch('article/fetchList')
])
}, },
components: { components: {
ArticleList ArticleList
}, },
computed: { computed: {
articles() { ...mapState({
return this.$store.state.article.list.data articles: state => state.article.list.data
} })
}, },
methods: { methods: {
currentChangeArticle(page) { currentChangeArticle(page) {

View File

@ -0,0 +1,148 @@
<template>
<el-row class="wrapper">
<el-col>
<el-col :xs="3" :sm="3" :xl="3" class="mr-3">
<el-image :src="portfolio.headImgUrl"></el-image>
</el-col>
<el-col :xs="20" :sm="20" :xl="20">
<el-col>
<h1>{{ portfolio.portfolioTitle }}</h1>
</el-col>
<el-col style="margin-bottom: .5rem;">
<span class="text-default" style="padding-right: 1rem;">作者</span>
<el-link @click="onRouter('user', portfolio.portfolioAuthorName)" :underline="false" class="text-default">
<el-avatar :src="portfolio.portfolioAuthorAvatarUrl" :size="16"></el-avatar>
{{ portfolio.portfolioAuthorName }}
</el-link>
</el-col>
<el-col style="margin-bottom: .5rem;">
<span class="text-default" style="padding-right: 1rem;">文章</span> {{portfolio.articleNumber}}
</el-col>
<el-col style="margin-bottom: .5rem;" v-html="portfolio.portfolioDescription">
</el-col>
</el-col>
</el-col>
<el-col>
<el-col v-if="isAuthor" style="text-align: right;">
<el-col>
<el-link @click="managerPortfolio(portfolio.idPortfolio)" :underline="false" class="text-default">管理</el-link>
</el-col>
</el-col>
<el-divider><i class="el-icon-loading"></i></el-divider>
<el-col>
<article-list :articles="articles" @currentChange="currentChangeArticle"></article-list>
</el-col>
</el-col>
</el-row>
</template>
<script>
import {mapState} from 'vuex';
import ArticleList from "../../components/archive/list";
export default {
name: "PortfolioDetail",
components: {ArticleList},
validate({params, store}) {
return params.portfolio_id && !isNaN(Number(params.portfolio_id))
},
fetch({store, params, error}) {
return Promise.all([
store
.dispatch('portfolio/fetchDetail', params)
.catch(err => error({statusCode: 404})),
store.dispatch('portfolio/fetchArticleList', params)
])
},
computed: {
...mapState({
portfolio: state => state.portfolio.detail.data,
articles: state => state.portfolio.articles,
isFetching: state => state.portfolio.detail.fetching,
isMobile: state => state.global.isMobile,
user: state => state.oauth,
avatar: state => state.oauth?.avatarURL
}),
isAuthor() {
let account = this.$store.state.oauth?.nickname;
if (account) {
if (account === this.portfolio.portfolioAuthor.userNickname) {
return true;
}
}
return false;
},
routePortfolioId() {
return Number(this.$route.params.portfolio_id)
}
},
head() {
return {
title: this.portfolio.portfolioTitle || 'RYMCU - 嵌入式知识学习交流平台',
meta: [
{
name: 'keywords',
content: this.portfolio.portfolioTags || 'RYMCU'
},
{
name: 'description',
content: this.portfolio.portfolioPreviewContent
},
{
name: 'site_name',
content: 'RYMCU'
},
{
name: 'url',
content: this.portfolio.portfolioPermalink
},
{
name: 'og:title',
content: this.portfolio.portfolioTitle + ' - RYMCU'
},
{
name: 'og:description',
content: this.portfolio.portfolioPreviewContent
},
{
name: 'og:site_name',
content: 'RYMCU'
},
{
name: 'og:url',
content: this.portfolio.portfolioPermalink
}
]
}
},
methods: {
onRouter(name, data) {
if (name === 'article') {
this.$router.push({
path: data
})
} else {
this.$router.push(
{
path: '/user/' + data
}
)
}
},
managerPortfolio(id) {
this.$router.push(
{
path: `/portfolio/manager/${id}`
}
)
},
currentChangeArticle(page) {
this.$store.dispatch('portfolio/fetchArticleList', {page: page, portfolio_id: this.routePortfolioId})
}
}
}
</script>
<style scoped>
</style>

View File

@ -7,13 +7,14 @@
<script> <script>
import ArticleList from '~/components/archive/list' import ArticleList from '~/components/archive/list'
import {mapState} from 'vuex';
export default { export default {
name: "topicArticleList", name: "topicArticleList",
components: { components: {
ArticleList ArticleList
}, },
validate({ params, store }) { validate({params, store}) {
if (params.topic_uri === 'news') { if (params.topic_uri === 'news') {
return true; return true;
} }
@ -21,13 +22,13 @@
topic => topic.topicUri === params.topic_uri topic => topic.topicUri === params.topic_uri
) )
}, },
fetch({ store, params }) { fetch({store, params}) {
return store.dispatch('article/fetchList', params) return store.dispatch('article/fetchList', params)
}, },
computed: { computed: {
articles() { ...mapState({
return this.$store.state.article.list.data articles: state => state.article.list.data
}, }),
currentTopic() { currentTopic() {
if (this.$route.params.topic_uri === 'news') { if (this.$route.params.topic_uri === 'news') {
return true; return true;
@ -44,12 +45,11 @@
}, },
methods: { methods: {
currentChangeArticle(page) { currentChangeArticle(page) {
this.$store.dispatch('article/fetchList', {page: page,topic: this.currentTopic}) this.$store.dispatch('article/fetchList', {page: page, topic_uri: this.defaultParams.topic_uri})
} }
}, },
mounted() { mounted() {
this.$store.commit('setActiveMenu', 'topic'); this.$store.commit('setActiveMenu', 'topic');
console.log(this.defaultParams)
}, },
created() { created() {
if (!this.currentTopic) { if (!this.currentTopic) {

View File

@ -42,7 +42,7 @@ export default function ({app, $axios, store, redirect}) {
if (code === 400) { if (code === 400) {
redirect('/400'); redirect('/400');
} else { } else {
console.log(error.data); console.log(error, error.data);
} }
}) })
} }

View File

@ -1,7 +1,8 @@
import Vue from 'vue'; import Vue from 'vue';
import { isBrowser } from '~/environment'; import { isBrowser } from '~/environment';
export const ARTICLE_API_PATH = '/api/console' export const ARTICLE_API_PATH = '/api/article'
export const BASE_API_PATH = '/api/console'
const getDefaultListData = () => { const getDefaultListData = () => {
return { return {
@ -43,6 +44,9 @@ export const mutations = {
updateDetailData(state, action) { updateDetailData(state, action) {
state.detail.data = action.article state.detail.data = action.article
}, },
clearDetailData(state, action) {
state.detail.data = {}
},
// 更新文章阅读全文状态 // 更新文章阅读全文状态
updateDetailRenderedState(state, action) { updateDetailRenderedState(state, action) {
@ -57,29 +61,29 @@ export const mutations = {
export const actions = { export const actions = {
// 获取文章列表 // 获取文章列表
fetchList({commit}, params = {}) { fetchList({commit}, params = {}) {
// 清空已有数据 // 清空已有数据
commit('updateListData', getDefaultListData()) commit('updateListData', getDefaultListData())
commit('updateListFetching', true) commit('updateListFetching', true)
let data = { let data = {
page: params.page, page: params.page || 1,
topicUri: params.topic_uri topicUri: params.topic_uri || ''
} }
console.log('data:', data)
return this.$axios return this.$axios
.$get(`${ARTICLE_API_PATH}/articles`, { .$get(`${BASE_API_PATH}/articles`, {
params: data params: data
}) })
.then(response => { .then(response => {
commit('updateListFetching', false) console.log('response', response);
commit('updateListData', response) commit('updateListFetching', false);
if (isBrowser) { commit('updateListData', response);
Vue.nextTick(() => {
window.scrollTo(0,0);
})
}
}) })
.catch(error => commit('updateListFetching', false)) .catch(error => {
console.log(error);
commit('updateListFetching', false);
});
}, },
// 获取文章详情 // 获取文章详情
@ -95,7 +99,42 @@ export const actions = {
commit('updateDetailFetching', true) commit('updateDetailFetching', true)
// commit('updateDetailData', {}) // commit('updateDetailData', {})
return this.$axios return this.$axios
.$get(`${ARTICLE_API_PATH}/article/${params.article_id}`) .$get(`${BASE_API_PATH}/article/${params.article_id}`)
.then(response => {
return new Promise(resolve => {
commit('updateDetailData', response)
commit('updateDetailFetching', false)
resolve(response)
// delay(() => {
// resolve(response)
// })
})
})
.catch(error => {
commit('updateDetailFetching', false)
return Promise.reject(error)
})
},
// 获取文章详情
fetchPostDetail({ commit }, params = {}) {
// const delay = fetchDelay(
// isBrowser
// )
// if (isBrowser) {
// Vue.nextTick(() => {
// window.scrollTo(0, 300);
// })
// }
if (typeof params.article_id === 'undefined') {
commit('updateDetailData', getDefaultListData())
return;
}
commit('updateDetailFetching', true)
// commit('updateDetailData', {})
return this.$axios
.$get(`${ARTICLE_API_PATH}/detail/${params.article_id}`)
.then(response => { .then(response => {
return new Promise(resolve => { return new Promise(resolve => {
commit('updateDetailData', response) commit('updateDetailData', response)

View File

@ -27,14 +27,17 @@ export const actions = {
auth = JSON.parse(parsed.auth) auth = JSON.parse(parsed.auth)
} catch (err) { } catch (err) {
// No valid cookie found // No valid cookie found
console.log(err);
} }
store.commit('setAuth', auth) store.commit('setAuth', auth)
} }
const initFetchAppData = [ const initFetchAppData = [
// 内容数据 // 内容数据
store.dispatch('topic/fetchList'), store.dispatch('topic/fetchList'),
store.dispatch('article/fetchList') store.dispatch('article/fetchList')
] ]
return Promise.all(initFetchAppData) return Promise.all(initFetchAppData)
} }
} }

138
store/portfolio.js Normal file
View File

@ -0,0 +1,138 @@
import Vue from 'vue';
import { isBrowser } from '~/environment';
export const PORTFOLIO_API_PATH = '/api/console'
const getDefaultListData = () => {
return {
portfolios: [],
pagination: {}
}
}
export const state = () => {
return {
list: {
fetching: false,
data: getDefaultListData()
},
detail: {
fetching: false,
data: {}
},
articles: {
articles: [],
pagination: {}
}
}
}
export const mutations = {
// 作品集列表
updateListFetching(state, action) {
state.list.fetching = action
},
updateListData(state, action) {
state.list.data = action
},
updateExistingListData(state, action) {
state.list.data.data.push(...action.data)
state.list.data.pagination = action.pagination
},
// 作品集详情
updateDetailFetching(state, action) {
state.detail.fetching = action
},
updateDetailData(state, action) {
state.detail.data = action.portfolio
},
updateArticleList(state, action) {
state.articles.articles = action.articles
state.articles.pagination = action.pagination
},
// 更新作品集阅读全文状态
updateDetailRenderedState(state, action) {
Vue.set(
state.detail.data,
'isRenderedFullContent',
action == null ? true : action
)
}
}
export const actions = {
// 获取作品集列表
fetchList({commit}, params = {}) {
// 清空已有数据
commit('updateListData', getDefaultListData())
commit('updateListFetching', true)
let data = {
page: params.page,
topicUri: params.topic_uri
}
return this.$axios
.$get(`${PORTFOLIO_API_PATH}/portfolios`, {
params: data
})
.then(response => {
commit('updateListFetching', false)
commit('updateListData', response)
if (isBrowser) {
Vue.nextTick(() => {
window.scrollTo(0,0);
})
}
})
.catch(error => commit('updateListFetching', false))
},
// 获取作品集详情
fetchDetail({ commit }, params = {}) {
// const delay = fetchDelay(
// isBrowser
// )
// if (isBrowser) {
// Vue.nextTick(() => {
// window.scrollTo(0, 300);
// })
// }
commit('updateDetailFetching', true)
// commit('updateDetailData', {})
return this.$axios
.$get(`${PORTFOLIO_API_PATH}/portfolio/${params.portfolio_id}`)
.then(response => {
return new Promise(resolve => {
commit('updateDetailData', response)
commit('updateDetailFetching', false)
resolve(response)
// delay(() => {
// resolve(response)
// })
})
})
.catch(error => {
commit('updateDetailFetching', false)
return Promise.reject(error)
})
},
fetchArticleList({commit}, params) {
commit('updateDetailFetching', true)
return this.$axios
.$get(`${PORTFOLIO_API_PATH}/portfolio/${params.portfolio_id}/articles`, {
params: {
page: params.page
}
})
.then(response => {
commit('updateArticleList', response)
commit('updateDetailFetching', false)
})
.catch(error => {
commit('updateDetailFetching', false)
})
}
}

View File

@ -11,12 +11,10 @@ export const state = () => {
fetching: false, fetching: false,
data: [], data: [],
articles: { articles: {
fetching: false,
articles: [], articles: [],
pagination: {} pagination: {}
}, },
portfolios: { portfolios: {
fetching: false,
portfolios: [], portfolios: [],
pagination: {} pagination: {}
} }
@ -52,6 +50,7 @@ export const actions = {
}) })
}, },
fetchArticleList({commit}, params) { fetchArticleList({commit}, params) {
commit('updateFetching', true);
return this.$axios return this.$axios
.$get(`${USER_API_PATH}/${params.nickname}/articles`, { .$get(`${USER_API_PATH}/${params.nickname}/articles`, {
params: { params: {
@ -67,6 +66,7 @@ export const actions = {
}) })
}, },
fetchPortfolioList({commit}, params) { fetchPortfolioList({commit}, params) {
commit('updateFetching', true);
return this.$axios return this.$axios
.$get(`${USER_API_PATH}/${params.nickname}/portfolios`, { .$get(`${USER_API_PATH}/${params.nickname}/portfolios`, {
params: { params: {