Authored by yyq

topic

<template>
<div v-if="data.topicImageUrl" class="topic-head" :style="headStyle">
<div class="center">
<p class="desc">{{data.topicDesc}}</p>
<div v-if="showAttAmount" class="att-amount">
<label>{{data.attAmount}}人已关注</label>
</div>
<div v-if="+data.allowAttention" class="att-topic-btn" :class="attClass" @click="onClick">
<WidgetFollow v-if="!share" class="att-click-wrap" :topic-id="data.topicId" :follow="data.hasAttention" @on-follow="onFollow"></WidgetFollow>
<template v-if="data.hasAttention">
已关注
</template>
<template v-else>
<span class="iconfont icon-plus"></span>
关注话题
</template>
<div class="topic-banner" :style="headStyle">
<div class="banner-main">
<div class="topic-name">{{data.topicName}}</div>
<p class="topic-desc">{{data.topicDesc}}</p>
<div class="topic-extra">
<label class="att-amount">{{data.articleAmount}}人参与讨论</label>
<WidgetFollow v-if="+data.allowAttention" class="att-click-wrap" :topic-id="data.topicId" :follow="data.hasAttention" :share="share" @on-follow="onFollow"></WidgetFollow>
</div>
</div>
</div>
<div v-else class="topic-head-empty" :style="headStyle"></div>
</template>
<script>
... ... @@ -44,22 +35,9 @@ export default {
}
return style;
},
showAttAmount() {
return this.data && +this.data.showAttAmount > 0;
},
attClass() {
return {
active: this.data.hasAttention
};
}
},
methods: {
onClick() {
if (this.share) {
return this.$links.toDownloadApp();
}
},
onFollow(follow) {
this.$emit('on-follow', follow);
},
... ... @@ -68,98 +46,70 @@ export default {
</script>
<style scoped>
.topic-head {
min-height: 360px;
background-size: 100%;
box-sizing: border-box;
position: relative;
.topic-banner {
background-size: cover;
display: flex;
align-items: center;
background-repeat: no-repeat;
background-color: #eee;
color: #fff;
position: relative;
&:before {
content: "";
height: 1px;
position: absolute;
top: -1px;
left: 0;
right: 0;
z-index: 1;
box-shadow: 0 0 140px 40px #000;
.banner-main {
height: 240px;
}
.center {
width: 100%;
position: relative;
z-index: 2;
text-align: center;
padding: 40px 100px;
box-sizing: border-box;
.topic-name {
height: 50px;
padding: 0 30px;
margin: 30px 0 18px;
font-size: 36px;
letter-spacing: 0.09PX;
font-weight: 800;
display: flex;
align-items: center;
}
.desc {
.topic-desc {
padding: 0 30px;
height: 60px;
font-size: 24px;
color: #fff;
line-height: 1.42;
word-break: break-all;
font-weight: 300;
line-height: 30px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
$attColor: #b0b0b0;
.att-amount {
margin-top: 20px;
color: $attColor;
display: flex;
justify-content: center;
label {
display: inline-block;
position: relative;
line-height: 1;
&:before,
&:after {
content: "";
width: 60px;
height: 2px;
background-color: $attColor;
display: block;
position: absolute;
top: 50%;
margin-top: -1px;
}
&:before {
right: calc(100% + 20px);
}
&:after {
left: calc(100% + 20px);
}
}
}
.att-topic-btn {
width: 200px;
height: 50px;
font-size: 26px;
border-radius: 28px;
line-height: 1;
.topic-extra {
width: 100%;
height: 36px;
margin: 34px 0 40px;
display: flex;
align-items: center;
justify-content: center;
background-color: #d90025;
color: #fff;
margin: 36px auto 0;
position: relative;
justify-content: space-between;
.iconfont {
font-weight: bold;
margin-right: 10px;
.att-amount {
font-size: 26px;
font-weight: 700px;
margin-left: 30px;
}
&.active {
background-color: #e48a8a;
.att-click-wrap {
width: 190px;
height: 60px;
padding-right: 36px;
margin-right: -32px;
border-radius: 31px;
border-color: #ff5660;
background-color: #ff5660;
color: #fff;
font-size: 32px;
&.follow {
background: none;
border-color: #fff;
}
}
}
}
... ...
<template>
<div class="fixed-header" :style="`background: rgba(64,64,64,${stepPercent});`">
<div class="fixed-header" :class="{'full-header': step >= 100}" :style="`background: rgba(255,255,255,${stepPercent});`">
<LayoutHeader theme="transparent">
<template>
<div ref="titleBlock" class="title-block">
<span :style="titleStyle">#{{title}}</span>
</div>
<div v-if="!share && +data.allowAttention" class="att-topic-btn" :class="attClass">
<WidgetFollow class="att-click-wrap" :topic-id="data.topicId" :follow="data.hasAttention" :share="share" @on-follow="onFollow"></WidgetFollow>
<span>{{data.hasAttention ? '已关注' : '关注'}}</span>
</div>
<div ref="titleBlock" class="title-block" :style="titleStyle">#{{title}}</div>
</template>
<template v-slot:opts>
<WidgetShare v-if="!share" class="topic-share" :option="shareOption" @click.native="onShare"></WidgetShare>
</template>
</LayoutHeader>
<div class="suction-top-block">
<slot name="suctionTop"></slot>
</div>
</div>
</template>
... ... @@ -39,41 +36,42 @@ export default {
],
computed: {
stepPercent() {
return parseInt(this.step, 10) / 100;
let percent = parseInt(this.step, 10) / 100;
this.setIconColor(Math.floor((255 - 34) * (1 - percent) + 34));
return percent;
},
titleStyle() {
let style = {};
let stepPercent = this.stepPercent;
if (!this.offsetLeft) {
this.updateTitleOffset();
}
const criticalPercent = 0.3;
const style = {
opacity: 0
};
if (get(this, '$yoho.isiOS')) {
style.transform = `translate3d(${-stepPercent * this.offsetLeft}px, 0, 0)`;
} else {
style.transform = `translate3d(${this.stepPercent > 0.86 ? -this.offsetLeft : 0}px, 0, 0)`;
if (this.stepPercent > criticalPercent) {
style.opacity = ((this.stepPercent - criticalPercent) / (1 - criticalPercent)).toFixed(2);
}
return style;
},
attClass() {
return {
active: this.data.hasAttention,
show: this.stepPercent > 0.86
};
}
},
watch: {
title() {
this.offsetLeft = 0;
}
mounted() {
this._iconColor && this.setIconColor(this._iconColor);
},
methods: {
updateTitleOffset() {
if (this.$refs && this.$refs.titleBlock) {
this.$nextTick(() => {
this.offsetLeft = this.$refs.titleBlock.offsetLeft + 40;
setIconColor(color) {
if (!this.$el) {
this._iconColor = color;
return;
}
if (!this.icons) {
this.icons = [...this.$el.querySelectorAll('.back .iconfont'), ...this.$el.querySelectorAll('.opts .iconfont')];
}
if (this.icons && this.icons.length) {
this.icons.forEach(dom => {
dom.style.color = `rgb(${color},${color},${color})`;
});
}
},
... ... @@ -89,30 +87,34 @@ export default {
<style scoped>
.fixed-header {
position: fixed;
position: absolute;
width: 100%;
z-index: 10;
transition: all 500ms;
transition: background 50ms;
&.full-header:after {
content: "";
background-color: #fff;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: -1;
}
/deep/ .title {
overflow: visible!important;
}
.title-block {
font-size: 0;
> span {
max-width: 400px;
line-height: 1.2;
font-size: 36px;
display: inline-block;
vertical-align: super;
overflow: hidden;
text-overflow:ellipsis;
white-space: nowrap;
position: relative;
transition: all 116ms;
}
color: #222;
line-height: 1.2;
font-size: 36px;
overflow: hidden;
text-overflow:ellipsis;
white-space: nowrap;
transition: opacity 100ms;
}
.att-topic-btn {
... ... @@ -153,5 +155,11 @@ export default {
.topic-share {
margin-right: 26px;
}
.suction-top-block {
width: 100%;
position: absolute;
top: 100%;
}
}
</style>
... ...
<template>
<div class="tab-block">
<div v-for="(tab, index) in tabList" :key="index" :ref="'tab-item' + index" class="tab-item" :class="{active: index == activeIndex}" @click="changeType(index)">
<div class="name">{{tab.name}}</div>
<div v-if="index == statusBlockIndex" ref="statusBlock" class="tab-status-block" :style="statusBlockStyle"></div>
</div>
</div>
</template>
<script>
import {findIndex} from 'lodash';
export default {
props: {
activeType: {
type: Number,
default: 0
}
},
data() {
let tabList = [
{name: '最新', type: 2},
{name: '热门', type: 1},
];
let index = Math.max(findIndex(tabList, {type: this.activeType}), 0);
return {
tabList,
statusBlockIndex: index,
activeIndex: index,
statusBlockStyle: {}
};
},
watch: {
activeType(val) {
let index = findIndex(this.tabList, {type: val});
if (index > -1) {
this.activeIndex = index;
}
},
activeIndex() {
this.changeStatusBlockStyle();
}
},
methods: {
changeType(index) {
this.activeIndex = index;
this.$emit('on-change-tab', {...this.tabList[index]});
},
changeStatusBlockStyle() {
const baseDom = this.$refs['tab-item' + this.statusBlockIndex];
if (baseDom) {
const baseLeft = baseDom[0].offsetLeft;
const {offsetLeft, offsetWidth} = this.$refs['tab-item' + this.activeIndex][0] || {};
this.statusBlockStyle = {
transform: `translate3d(${offsetLeft - baseLeft}px, 0, 0)`,
width: offsetWidth + 'px'
};
}
}
}
};
</script>
<style scoped>
.tab-block {
height: 100px;
background-color: #fff;
display: flex;
justify-content: center;
font-size: 32px;
.tab-item {
margin: 0 38px;
padding-top: 20px;
color: #b0b0b0;
position: relative;
&.active {
color: #222;
font-weight: 500;
}
.name {
margin-bottom: 4px;
}
}
.tab-status-block {
width: 100%;
height: 10px;
background: #d0021b;
box-shadow: 0 4px 8px 0 rgba(210,0,13,0.34);
position: absolute;
transition: all 500ms;
}
}
</style>
... ...
<template>
<div>
<Layout class="article">
<TopicHeader ref="header" :title="topicTitle" :step="headerAnimateStep" :data="topicSimpleInfo" :share="share" @on-follow="onFollowTopic"></TopicHeader>
<TopicHeader ref="header" :title="topicTitle" :step="headerAnimateStep" :data="topicSimpleInfo" :share="share" @on-follow="onFollowTopic">
<template v-if="tabBlockSuctionTop" v-slot:suctionTop>
<TopicTabBlock :active-type="activeTopicListType" @on-change-tab="onChangeTab"></TopicTabBlock>
</template>
</TopicHeader>
<RecycleScrollReveal :size="5" :thumbs="thumbs" ref="scroll" @scroll="onScroll" :offset="800" :on-fetch="onFetch" :manual-init="true"
@on-inited="onInited">
<template v-slot:eternalTop>
<TopicBanner ref="topicHead" :data="topicSimpleInfo" :share="share" @on-follow="onFollowTopic"></TopicBanner>
<TopicBanner ref="topicBanner" :data="topicSimpleInfo" :share="share" @on-follow="onFollowTopic"></TopicBanner>
<TopicTabBlock :active-type="activeTopicListType" @on-change-tab="onChangeTab"></TopicTabBlock>
</template>
<template class="article-item" #item="{ data }">
<ArticleItem2
... ... @@ -31,17 +36,6 @@
立即参与
</a>
</Layout>
<ArticleActionSheet v-if="showArticleDetailAction" ref="actionSheet"></ArticleActionSheet>
<YohoActionSheet transfer v-if="showCommentAction" ref="commentAction" :full="true">
<Comment ref="comment"
:destId="articleId"
:popup="true"
:article-id="articleId"
:pos-id="posId"
@on-close="onClose"
@on-comment="onActionComment"></Comment>
</YohoActionSheet>
<MoreActionSheet transfer ref="moreAction" @on-follow="onFollow" @on-delete="onDelete"></MoreActionSheet>
</div>
</template>
... ... @@ -55,6 +49,7 @@ import ArticleActionSheet from './components/detail/article-action-sheet';
import MoreActionSheet from './components/detail/more-action-sheet';
import TopicHeader from './components/topic/header';
import TopicBanner from './components/topic/banner';
import TopicTabBlock from './components/topic/tab-block';
import {mapState as mapYohoState, mapActions as mapYohoActions, mapMutations as mapYohoMutations, createNamespacedHelpers} from 'vuex';
const {mapState, mapMutations, mapActions} = createNamespacedHelpers('article');
... ... @@ -70,14 +65,8 @@ export default {
topicId: 0,
topicName: '',
headerAnimateStep: 0,
articleId: 0,
articleIndex: -1,
showCommentAction: false,
showCommentActioning: false,
showArticleDetailAction: false,
showMoreAction: true,
scrollTop: 0,
currentId: 0,
activeTopicListType: 2,
colWidthForTwo: 0,
scrolling: false
};
... ... @@ -140,11 +129,25 @@ export default {
return this.topicInfo[this.topicId] || {};
},
publishUrl() {
return this.topicId ? `?openby:yohobuy={"action":"go.grasspublish","params":{"topicId":"${this.topicId}","topicName":"${this.topicName}"}}` : '';
let params = {};
if (this.topicId) {
params.topicId = this.topicId;
params.topicName = this.topicName;
if (+this.topicSimpleInfo.isReward === 1) {
params.rewardNotice = this.topicSimpleInfo.rewardNotice;
}
}
return `?openby:yohobuy={"action":"go.grasspublish","params":${JSON.stringify(params)}}`;
},
topicTitle() {
return this.topicName || this.topicSimpleInfo.topicName || '';
},
tabBlockSuctionTop() {
return this.scrollTop >= (this._topicHeaderHeight - this._headerHeight);
},
headerBgOpacity() {
return this.headerAnimateStep / 100;
}
... ... @@ -152,78 +155,38 @@ export default {
methods: {
...mapYohoActions(['fetchUserProfile']),
...mapYohoMutations(['SET_STATUS_BAR_COLOR']),
...mapActions(['fetchTopicSimpleInfo', 'fetchTopicRelatedArticles', 'fetchArticleProductFavs']),
...mapMutations(['ASYNC_ARTICLE_COMMENT', 'CHANGE_AUTHOR_FOLLOW', 'CHANGE_TOPIC_FOLLOW']),
onShowComment({articleId, index}) {
this.articleId = articleId;
this.articleIndex = index;
this.showCommentAction = true;
this.$nextTick(() => {
if (this.showCommentActioning) {
return;
}
this.showCommentActioning = true;
this.$refs.comment.init();
this.$refs.commentAction.show();
setTimeout(() => {
this.showCommentActioning = false;
}, 300);
});
},
onShowMore({article, index}) {
this.$refs.moreAction.show(article, this.userUid, index);
},
onDelete(index) {
this.$refs.scroll.delete(index);
},
onPageReady({success}) {
if (success && this.showCommentAction) {
this.$refs.commentAction.show();
setTimeout(() => {
this.showCommentActioning = false;
}, 300);
} else {
this.showCommentActioning = false;
}
},
onUnlockHeight(params) {
this.$refs.scroll.unlockHight(params);
},
onClose() {
this.$refs.commentAction.hide();
},
onActionComment() {
this.ASYNC_ARTICLE_COMMENT({articleId: this.articleId, type: this.type});
this.onResize(this.articleIndex);
...mapActions(['fetchTopicSimpleInfo', 'fetchTopicRelatedArticles']),
...mapMutations(['CHANGE_AUTHOR_FOLLOW', 'CHANGE_TOPIC_FOLLOW']),
onChangeTab({type}) {
this.page = 1;
this.activeTopicListType = type;
this.$refs.scroll && this.$refs.scroll.init();
},
onScroll(params) {
onScroll({item, scrollTop, startIndex, items}) {
this.scrolling = true;
this._scTimer && clearTimeout(this._scTimer);
this._scTimer = setTimeout(() => {
this.scrolling = false;
}, 400);
this.scrollEvent(params);
this._ttTimer && clearTimeout(this._ttTimer);
this._ttTimer = setTimeout(() => {
this.scrollEvent(params);
}, throttleTime);
this.updateHeadAnimateStep(scrollTop);
this.scrollTop = scrollTop || 0;
this.scrollEvent({item, scrollTop, startIndex, items});
},
onDounceScroll({item, scrollTop, startIndex, items}) {
updateHeadAnimateStep(scrollTop) {
if (this.scrollTop === scrollTop) {
return;
}
this.scrollTop = scrollTop || 0;
this._headerHeight = this._headerHeight || this.$refs.header.$el.offsetHeight;
this._topicHeaderHeight = this._topicHeaderHeight || this.$refs.topicHead.$el.offsetHeight;
let step = Math.round((scrollTop - 10) / (this._topicHeaderHeight - this._headerHeight) * 100);
this._topicHeaderHeight = this._topicHeaderHeight || this.$refs.topicBanner.$el.offsetHeight;
if (step && this.topicSimpleInfo.topicImageUrl) {
let step = Math.round((scrollTop - 4) / (this._topicHeaderHeight - this._headerHeight - 10) * 100);
if (step) {
this.headerAnimateStep = Math.max(Math.min(step, 100), 0);
}
},
onDounceScroll({items}) {
this.reportArticleShow(items);
},
init() {
... ... @@ -254,9 +217,13 @@ export default {
this.headerAnimateStep = 100;
}
}),
this.fetchTopicRelatedArticles({topicId: this.topicId, page: 1}).then(res => {
this.fetchTopicRelatedArticles({
topicId: this.topicId,
type: this.activeTopicListType,
page: 1
}).then(res => {
if (res.code === 200) {
this.listPreCache[`${this.topicId}-1`] = res;
this.listPreCache[`${this.topicId}-${this.activeTopicListType}-1`] = res;
}
})
]);
... ... @@ -271,12 +238,15 @@ export default {
this.fetching = true;
this.page = this.page || 1;
let result = get(this.listPreCache, `${this.topicId}-${this.page}`);
let result = get(this.listPreCache, `${this.topicId}-${this.activeTopicListType}-${this.page}`);
if (!result) {
if (result) {
this.listPreCache = {};
} else {
result = await this.fetchTopicRelatedArticles({
topicId,
page: this.page
page: this.page,
type: this.activeTopicListType
});
}
... ... @@ -304,24 +274,10 @@ export default {
onInited(items) {
this.reportArticleShow(items);
},
onFollow(data, follow) {
this.CHANGE_AUTHOR_FOLLOW({authorUid: data.authorUid, authorType: data.authorType, follow, type: this.type});
},
onFollowTopic(follow) {
this.CHANGE_TOPIC_FOLLOW({topicId: this.topicId, follow});
follow && this.reportTopicFollow();
},
onResize(index) {
this.$nextTick(() => {
this.$refs.scroll.resize(index);
});
},
onShowGuang(params) {
this.showArticleDetailAction = true;
this.$nextTick(() => {
this.$refs.actionSheet.show(params);
});
},
setPageShareInfo({topicName, topicImageUrl, topicDesc}) {
if (this.share) {
document && (document.title = topicName || '话题');
... ... @@ -392,75 +348,62 @@ export default {
}
},
components: {
ArticleItem,
ArticleItem2,
ArticleActionSheet,
MoreActionSheet,
TopicHeader,
TopicBanner
TopicBanner,
TopicTabBlock
}
};
</script>
<style scoped>
/deep/ .recycle-scroll-reveal-main {
background-color: #f7f7f7;
/deep/ .recycle-scroll-reveal-main {
background-color: #f7f7f7;
.scroll-reveal-list {
padding: 5px;
}
.scroll-reveal-list {
padding: 5px;
}
}
/deep/ .att-click-wrap {
opacity: 0;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 1;
}
/deep/ .cube-recycle-list-items {
padding-top: 54PX;
/deep/ .cube-recycle-list-items {
padding-top: 54PX;
.cube-recycle-list-item:first-child {
margin-top: -54PX;
}
.cube-recycle-list-item:first-child {
margin-top: -54PX;
}
}
.publish-btn {
width: 260px;
height: 80px;
border-radius: 42px;
font-size: 30px;
color: #fff;
background: #D0021B;
position: absolute;
bottom: 100px;
left: calc(50% - 130px);
z-index: 1;
display: flex;
align-items: center;
transition: all 600ms ease-in-out;
&.scroll-opacity {
opacity: 0.3;
}
.publish-btn {
width: 260px;
height: 80px;
border-radius: 42px;
font-size: 30px;
color: #fff;
background: #D0021B;
position: absolute;
bottom: 100px;
left: calc(50% - 130px);
z-index: 1;
display: flex;
align-items: center;
transition: all 600ms ease-in-out;
&.scroll-opacity {
opacity: 0.3;
}
.avatar-block {
width: 52px;
height: 52px;
margin: 14px 30px 14px 14px;
border-radius: 50%;
overflow: hidden;
}
.avatar {
display: block;
width: 100%;
height: 100%;
}
.avatar-block {
width: 52px;
height: 52px;
margin: 14px 30px 14px 14px;
border-radius: 50%;
overflow: hidden;
}
.avatar {
display: block;
width: 100%;
height: 100%;
}
}
</style>
... ...
... ... @@ -302,11 +302,12 @@ export default {
return result;
},
async fetchTopicRelatedArticles({ commit, state }, {topicId, page, lastedTime}) {
async fetchTopicRelatedArticles({ commit, state }, {topicId, page, type}) {
commit(Types.FETCH_ARTICLE_TOPIC_REQUEST, { page });
const result = await this.$api.post('/api/grass/topicRelatedArticles', {
topicId,
page,
type,
limit: 6,
lastedTime: state.articleLastedTimeByTopic || void 0
});
... ...
... ... @@ -13,6 +13,7 @@ module.exports = {
topicId: {type: Number, require: true},
limit: {type: Number},
page: {type: Number},
type: {type: Number},
lastedTime: {type: Number}
}
},
... ...