Authored by yyq

Merge branch 'feature/userPage'

<template>
<Layout class="author-page">
<LayoutHeader slot='header' theme="white">
<div ref="headerAuthor" class="header-author">
<div class="h-name flex">{{baseData.nickName}}</div>
<div class="h-more">
<div class="flex">
<WidgetAvatar v-if="baseData.headIco" class="h-headico" :src="baseData.headIco" :width="100" :height="100"></WidgetAvatar>
</div>
<div class="h-follow flex">
<WidgetFollow class="widget-follow" :author-uid="autherInfo.authorUid" :follow="isAttention" @on-follow="follow => onFollow(follow)"></WidgetFollow>
</div>
</div>
</div>
<template v-slot:opts>
<WidgetShare class="header-share"></WidgetShare>
</template>
</LayoutHeader>
<cube-sticky :pos="scrollY">
<cube-scroll
class="main-container"
:scroll-events="scrollEvents"
@scroll="scrollHandler">
<div ref="authorProfile" class="author-profile">
<span class="avatar-box">
<WidgetAvatar v-if="baseData.headIco" :src="baseData.headIco" :width="100" :height="100"></WidgetAvatar>
</span>
<div class="author-section">
<ul class="author-fans">
<li>
<span class="num">{{baseData.attCount}}</span>
<p class="name">
<span>关注</span>
</p>
</li>
<li>
<span class="num">{{baseData.fansCount}}</span>
<p class="name">
<span>粉丝</span>
</p>
</li>
<li>
<span class="num">{{baseData.praiseAndfavorite}}</span>
<p class="name">
<span>获赞与收藏</span>
</p>
</li>
</ul>
<div class="operate-wrap">
<label v-if="isOwner" class="operate-btn btn-user-edit">编辑个人资料</label>
<WidgetFollow v-else class="operate-btn" :author-uid="autherInfo.authorUid" :follow="isAttention" @on-follow="follow => onFollow(follow)"></WidgetFollow>
</div>
</div>
</div>
<p v-if="baseData.signature" class="author-desc">{{baseData.signature}}</p>
<cube-sticky-ele ele-key="11">
<FavTabBlock :tabs-num="tabsNum" :active-index="activeIndex" @change="changeTab"></FavTabBlock>
</cube-sticky-ele>
<div class="contant-list">
<WaterFall class="pannel-wrap" :list="list" :pos="scrollY"></WaterFall>
</div>
<div v-if="loadStatus" class="loading">
<Loading v-if="loadStatus === 1" class="load-icon" :size="20"></Loading>
<p v-else class="load-text">没有更多了</p>
</div>
</cube-scroll>
<template slot="fixed" slot-scope="props">
<FavTabBlock :tabs-num="tabsNum" :active-index="activeIndex" @change="changeTab"></FavTabBlock>
</template>
</cube-sticky>
</Layout>
</template>
<script>
import {assign, get} from 'lodash';
import {Scroll, Sticky, Loading} from 'cube-ui';
import CubeStickyEle from 'cube-ui/src/components/sticky/sticky-ele.vue';
import FavTabBlock from './components/fav-tab-block';
import WaterFall from './components/water-fall';
import {createNamespacedHelpers} from 'vuex';
const {mapActions} = createNamespacedHelpers('user');
export default {
name: 'userpage',
data() {
return {
autherInfo: {},
scrollEvents: ['scroll'],
scrollY: 0,
baseData: {},
isAttention: false,
isOwner: false,
tabsNum: [10, 0],
activeIndex: 0,
fetchInfo: {},
loadStatus: ''
}
},
created() {
this.autherInfo = {
authorUid: +this.$route.params.id,
authorType: this.$route.params.type
};
this.fetchBaseInfo();
this.fetchList();
},
mounted() {
let $dom = this.$refs.headerAuthor;
if ($dom.offsetHeight) {
this._animeDuration = 300;
import('animejs').then(({default: anime}) => {
this._animeEl = anime({
targets: $dom,
translateY: -$dom.offsetHeight,
easing: 'easeInOutSine',
duration: this._animeDuration,
autoplay: false
});
});
}
},
watch: {
scrollY(top) {
let animePlayed = false;
if (top > this.$refs.authorProfile.offsetHeight) {
animePlayed = true;
}
if (!this._animePlayed === !animePlayed) {
return;
}
let start;
let self = this;
function step(timestamp) {
if (!start) {
start = timestamp
};
let progress = Math.floor(timestamp - start);
self._animeEl.seek(animePlayed ? progress : self._animeDuration - progress);
if (progress < self._animeDuration) {
window.requestAnimationFrame(step);
}
};
window.requestAnimationFrame(step);
this._animePlayed = animePlayed;
}
},
computed: {
list() {
return get(this.fetchInfo, `${this.activeIndex}.list`) || [];
}
},
methods: {
...mapActions(['autherBaseInfo', 'autherAritcleNum', 'autherPubList', 'autherFavList']),
scrollHandler({ y }) {
this.scrollY = -y;
if (this.scrollY + 1000 > this.$el.offsetHeight) {
this._listTimer && clearTimeout(this._listTimer);
this._listTimer = setTimeout(() => {
this.fetchList();
}, 100);
}
},
changeTab(index) {
if (this.activeIndex !== index) {
this.activeIndex = index;
this.fetchList();
}
},
fetchBaseInfo() {
this.autherBaseInfo(this.autherInfo).then(res => {
if (res.code === 200) {
this.baseData = res.data;
this.isOwner = res.data.isOwner;
this.isAttention = res.data.isAttention === 'Y';
}
});
this.autherAritcleNum(this.autherInfo).then(res => {
this.tabsNum = [get(res, 'data.articleCount'), get(res, 'data.favoriteCount')];
});
},
async fetchList() {
this.fetchInfo = this.fetchInfo || [];
if (this.syncing) {
return;
}
let info = this.fetchInfo[this.activeIndex] || {};
let result;
info.page = info.page || 1;
if (info.page >= info.totalPage) {
return;
}
let syncServiceName;
if (this.activeIndex === 1) {
syncServiceName = 'autherFavList';
} else {
syncServiceName = 'autherPubList';
}
if (this[syncServiceName]) {
this.syncing = true;
result = await this[syncServiceName](assign({
page: info.page,
lastedTime: info.lastedTime || ''
}, this.autherInfo));
this.syncing = false;
}
if (result.code === 200) {
info.list = (info.list || []).concat(result.data.list);
info.page++;
info.totalPage = 10 || result.data.totalPage;
info.lastedTime = result.data.lastedTime;
}
if (info.page > info.totalPage) {
this.loadStatus = 2;
} else {
this.loadStatus = 1;
}
this.fetchInfo[this.activeIndex] = info;
this.fetchInfo = {...this.fetchInfo};
},
onFollow(follow) {
this.isAttention = follow;
}
},
components: {
CubeScroll: Scroll,
CubeSticky: Sticky,
CubeStickyEle,
Loading,
FavTabBlock,
WaterFall
}
};
</script>
<style lang="scss">
.author-page {
box-sizing: border-box;
color: #4a4a4a;
}
.header-author {
width: 100%;
height: 100%;
position: relative;
.flex {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.h-name {
font-size: 36px;
font-weight: 500;
}
.h-more {
width: 100%;
height: 100%;
position: absolute;
top: 100%;
left: 0;
}
.h-headico {
width: 60px;
height: 60px;
}
.h-follow {
position: absolute;
top: 0;
right: -50px;
}
}
.header-share {
margin-right: 26px;
color: #222;
font-weight: bold;
}
.author-profile {
padding: 24px 30px;
display: flex;
justify-content: space-between;
.avatar-box {
width: 150px;
height: 150px;
overflow: hidden;
border-radius: 50%;
> img {
width: 100%;
height: 100%;
display: block;
}
}
}
.author-section {
display: flex;
flex-direction: column;
justify-content: space-between;
}
.author-fans {
display: flex;
justify-content: flex-end;
padding-top: 4px;
padding-right: 54px;
li {
margin-left: 140px;
position: relative;
&:first-child {
margin-left: 0;
}
.num {
font-size: 28px;
font-weight: 500;
padding-bottom: 6px;
display: block;
}
.name {
position: absolute;
word-break: keep-all;
font-size: 20px;
font-weight: 300;
color: #9b9b9b;
margin-left: 50%;
> * {
position: relative;
left: -50%;
}
}
}
}
.operate-wrap {
text-align: right;
.operate-btn {
width: calc(100% - 20px);
font-size: 23px;
line-height: 48px;
display: inline-block;
}
.btn-user-edit {
color: #222;
border: 1px solid #4a4a4a;
border-radius: 8px;
text-align: center;
}
}
.author-desc {
margin: 20px 30px;
font-size: 24px;
font-weight: 300;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.loading {
padding: 20px 0;
.load-icon > span {
margin: auto;
}
.load-text {
text-align: center;
}
}
</style>
... ...
<template>
<div class="tabs-wrap">
<ul class="tabs-list">
<li v-for="(item, index) in tabList" :key="index" :class="{'active': active === index}" @click="changeType(index, true)">
{{item.name}}
<span v-if="item.num" class="t-num">({{item.num}})</span>
</li>
</ul>
</div>
</template>
<script>
import {find} from 'lodash';
export default {
props: {
tabsNum: Array,
activeIndex: Number
},
data() {
return {
tabList: [
{name: '内容', type: 1},
{name: '收藏', type: 2},
],
active: ''
};
},
created() {
this.changeType(this.activeIndex);
this.computetabsNum();
},
methods: {
changeType(index, isClick) {
if (!this.tabList[index]) {
index = 0;
}
this.active = index;
this.$emit('change', index);
},
computetabsNum(a) {
let tabList = this.tabList;
for (let i = this.tabList.length - 1; i >= 0; i--) {
let num = '';
if (this.tabsNum[i] > 0) {
num = this.tabsNum[i];
}
tabList[i].num = num;
}
this.tabList = [...tabList];
},
computeCurrentTab() {
this.changeType(this.activeIndex || 0);
}
},
watch: {
tabsNum: 'computetabsNum',
activeIndex: 'computeCurrentTab'
}
};
</script>
<style>
.tabs-wrap {
padding: 20px 30px 30px;
background-color: #fff;
display: flex;
justify-content: space-between;
align-items: center;
.tabs-list {
display: flex;
li {
font-size: 32px;
color: #b0b0b0;
font-weight: 300;
margin-right: 40px;
}
.active {
color: #222;
font-weight: 500;
position: relative;
&:after {
content: '';
position: absolute;
left: 0;
bottom: -10px;
width: 100%;
height: 8px;
background-color: #d90025;
box-shadow: 0 2px 4px 0 rgba(210, 0, 13, 0.34);
}
}
.t-num {
color: #b0b0b0;
font-size: 24px;
zoom: 0.9;
margin-left: -8px;
}
}
}
</style>
... ...
<template>
<div class="wf-list" :style="{'height': listHeight + 'px'}">
<div
v-for="i in viewList"
:key="`${i._temporary ? '_' : ''}${i.articleId}`"
class="wf-item"
:class="{'wf-item-default': i._default, 'wf-item-temp': i._temporary}"
:style="`width: ${100 / cols}%;transform: translate(${i.left}px, ${i.top}px)`">
<div class="wf-item-mid">
<router-link :to="'/article/' + i.articleId">
<div class="layer-image" :style="{'height': i.coverHeight + 'px'}">
<ImageFormat v-if="!i._temporary" :src="i[srcKey]" :width="coverImageWidth" :height="i.coverHeight"></ImageFormat>
</div>
<div class="description">{{i.content}}</div>
</router-link>
<div class="attribution">
<router-link :to="'/article/' + i.articleId" class="auther">
<span class="avatar">
<WidgetAvatar v-if="!i._temporary" :src="i.authorHeadIco" :width="70" :height="70"></WidgetAvatar>
</span>
<span class="name">{{i.authorName}}</span>
</router-link>
<div class="fav">
<WidgetFav :articleId="i.articleId" :num="i.praiseCount" :option="favOption"></WidgetFav>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import {assign} from 'lodash';
export default {
data() {
return {
viewList: [{
_default: true
}],
coverImageWidth: 0,
colWidthPer: 0,
calcIndex: 0,
loadedIndex: 0,
colsHeight: [],
favOption: {
iconFontSize: 26,
textAlign: 'normal'
}
}
},
props: {
pos: {
type: Number,
default: 0
},
list: {
type: Array,
default: []
},
srcKey: {
type: String,
default: 'coverImage'
},
cols: {
type: Number,
default: 2
},
space: {
type: Number,
default: 14
}
},
mounted() {
this.$on('calced', (nlist) => {
this.viewList = this.viewList.concat(nlist);
this.$nextTick(() => {
this.calcLayout();
})
});
this.reset();
this.clacCoverSize();
},
watch: {
pos() {
this.timer && clearTimeout(this.timer);
this.timer = setTimeout(this.resetViewList, 0);
},
list(newList, oldList) {
if (oldList.length > newList.length) {
this.reset();
}
this.clacCoverSize();
}
},
computed: {
listHeight() {
return Math.max.apply(null, this.colsHeight);
},
colWidth() {
return this.$el.offsetWidth / this.cols;
}
},
methods: {
clacCoverSize() {
let nlist = [];
for (let i = this.calcIndex; i < this.list.length; i++) {
let item = this.list[i];
item.coverHeight = Math.floor(item.imageHeight / item.imageWidth * this.coverImageWidth);
nlist.push(assign({_temporary: true}, item));
};
this.$emit('calced', nlist);
},
calcCoverImgHeight(w, h) {
return h / w * this.coverImageWidth;
},
calcLayout() {
let $item = this.$el.getElementsByClassName('wf-item-temp');
if (!$item || !$item.length) {
return;
}
if (!this.loadedIndex || !this.colsHeight) {
this.colsHeight = [];
}
for (let i = this.loadedIndex; i < this.list.length; i++) {
let $elem = $item[i - this.loadedIndex];
if (!$elem) {
return;
}
let height = $elem.offsetHeight;
let top, left, leftPer;
let item = this.list[i];
item.height = height;
if (i < this.cols) {
this.colsHeight[i] = height;
top = 0
left = i * this.colWidth;
leftPer = i * this.colWidthPer;
} else {
let minHeight = Math.min.apply(null, this.colsHeight);
let minIndex = this.colsHeight.indexOf(minHeight);
top = minHeight;
left = minIndex * this.colWidth;
leftPer = minIndex * this.colWidthPer;
this.colsHeight[minIndex] = minHeight + height;
}
item.left = left;
item.leftPer = leftPer;
item.top = top;
item.bottom = top + height;
}
this.loadedIndex = this.list.length;
this.resetViewList();
},
setImgWidth() {
let imgWidth = this.$el.offsetWidth / this.cols;
let $item = this.$el.getElementsByClassName('wf-item');
if ($item && $item.length) {
let _w = $item[0].offsetWidth;
let $img = $item[0].getElementsByClassName('layer-image');
(_w > 0) && (imgWidth = _w);
if ($img && $img.length) {
_w = $img[0].offsetWidth;
(_w > 0) && (imgWidth = _w);
}
}
this.coverImageWidth = imgWidth;
},
reset() {
this.offsetTop = this.$el.offsetTop;
this.clientHeight = document.body.clientHeight;
this.colWidthPer = 100 / this.cols;
this.loadedIndex = 0;
this.calcIndex = 0;
this.viewList = [];
this.lastPos = 0;
this.viewIndex = 0;
this.setImgWidth();
},
resetViewList() {
this.viewIndex = this.viewIndex || {};
let i, step;
let startPos = this.pos - this.clientHeight * 2;
let endPos = this.pos + this.clientHeight * 2;
let list = [];
let indexArr = [];
if (this.pos < this.lastPos) {
i = Math.min.apply(null, [(this.viewIndex['end'] || 0), this.list.length - 1]);
step = -1;
} else {
i = Math.max.apply(null, [(this.viewIndex['start'] || 0), 0]);
step = 1;
}
let loop = true;
while (loop)
{
if (i < 0 || i >= this.list.length) {
loop = false;
continue;
}
let item = this.list[i];
if (item) {
if (item.top > endPos - this.offsetTop) {
if (step > 0) {
loop = false;
}
} else if (item.bottom < startPos - this.offsetTop) {
if (step < 0) {
loop = false;
}
} else {
indexArr.push(i);
list.push(item);
}
}
i += step;
}
if (!indexArr.length) {
return;
}
indexArr = indexArr.sort(function(a, b) {
return a - b;
});
let viewIndex = {
start: indexArr[0],
end: indexArr[indexArr.length - 1]
};
if (this.viewIndex.start !== viewIndex.start || this.viewIndex.end !== viewIndex.end) {
this.viewList = list;
this.lastPos = this.pos;
this.viewIndex = viewIndex;
}
}
}
};
</script>
<style lang="css">
.wf-list {
margin: 0 20px;
font-size: 0;
position: relative;
}
.wf-item {
padding: 10px;
font-size: 24px;
overflow: hidden;
position: absolute;
.wf-item-mid {
border-radius: 4px;
box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.15);
overflow: hidden;
}
.layer-image {
background-color: #f4f4f4;
min-height: 100px;
> img {
width: 100%;
height: 100%;
display: block;
}
}
.description {
line-height: 1.5;
padding: 10px 20px;
}
.attribution {
display: flex;
justify-content: space-between;
padding: 20px;
}
.avatar {
width: 60px;
height: 60px;
border-radius: 50%;
display: inline-block;
vertical-align: middle;
overflow: hidden;
> img {
width: 100%;
height: 100%;
}
}
.name {
display: inline-block;
vertical-align: middle;
}
.fav {
line-height: 60px;
}
}
.wf-item-default,
.wf-item-temp {
opacity: 0;
margin-left: -100%;
}
</style>
... ...
export default [{
path: '/xxx',
name: 'xxx',
// component: () => import(/* webpackChunkName: "xxx" */ './xxx')
path: '/author/:type/:id',
name: 'author',
component: () => import(/* webpackChunkName: "author" */ './author')
}];
... ...
... ... @@ -41,4 +41,40 @@ export default {
return result;
},
async autherBaseInfo(actions, {authorUid, authorType}) {
const result = await this.$api.get('/api/grass/getGrassUserBaseInfo', {
authorUid,
authorType
});
return result;
},
async autherAritcleNum(actions, {authorUid, authorType}) {
const result = await this.$api.get('/api/grass/getGrassPubAndFavorNum', {
authorUid,
authorType
});
return result;
},
async autherPubList(actions, {authorUid, authorType, page, lastedTime}) {
const result = await this.$api.get('/api/grass/userPublishedArticleList', {
authorUid,
authorType,
page,
lastedTime
});
return result;
},
async autherFavList(actions, {authorUid, authorType, page, lastedTime}) {
const result = await this.$api.get('/api/grass/userFavouriteArticleList', {
authorUid,
authorType,
page,
lastedTime
});
return result;
},
};
... ...
... ... @@ -2,7 +2,10 @@ const URI_PACKAGE_ARTICLE = 'guang/service/v2/article/';
const URI_PACKAGE_AUTHOR = 'guang/service/v1/author/';
const URI_PACKAGE_PRAISE = 'guang/api/v1/article/';
const userPageApis = require('./api-map/userpage');
module.exports = {
...userPageApis,
'/api/grass/labelRealtedArticleDetail': {
api: 'app.grass.labelRealtedArticleDetail',
cache: true,
... ...
module.exports = {
'/api/grass/getGrassUserBaseInfo': {
api: 'app.grass.getGrassUserBaseInfo',
cache: true,
params: {
authorUid: {type: Number, require: true},
authorType: {type: Number, require: true}
}
},
'/api/grass/getGrassPubAndFavorNum': {
api: 'app.grass.getGrassPubAndFavorNum',
cache: true,
params: {
authorUid: {type: Number, require: true},
authorType: {type: Number, require: true}
}
},
'/api/grass/userPublishedArticleList': {
api: 'app.grass.userPublishedArticleList',
cache: true,
params: {
authorUid: {type: Number, require: true},
authorType: {type: Number, require: true},
page: {type: Number},
lastedTime: {type: Number}
}
},
'/api/grass/userFavouriteArticleList': {
api: 'app.grass.userFavouriteArticleList',
cache: true,
params: {
authorUid: {type: Number, require: true},
authorType: {type: Number, require: true},
page: {type: Number},
lastedTime: {type: Number}
}
},
}
... ...