Authored by yyq

topic

... ... @@ -3,6 +3,8 @@ import LayoutHeader from './layout-header';
import LayoutRecycleList from './layout-recycle-list';
import RecycleList from './recycle-list';
import AuthComponent from './auth-component';
import RecycleScrollReveal from './recycle-scroll-reveal';
export default [
Layout,
... ... @@ -10,4 +12,5 @@ export default [
LayoutRecycleList,
RecycleList,
AuthComponent,
RecycleScrollReveal
];
... ...
<template>
<div class="recycle-scroll-reveal" v-bind="$attrs" v-on="$listeners">
<div class="recycle-scroll-reveal-main">
<div ref="eternal" class="eternal-top">
<slot name="eternalTop"></slot>
</div>
<div class="scroll-reveal-list">
<div
v-for="(items, col) in visibleItems"
:key="col"
class="scroll-reveal-col"
:ref="'col'+ col"
:style="colStyle">
<div
v-for="item in items"
:key="item.index"
class="scroll-reveal-item"
:ref="'items' + item.index"
:style="getItemStyle(item)">
<div v-if="!item.placeholder">
<slot name="item" :data="item"></slot>
</div>
</div>
</div>
</div>
<div class="loading">
<p v-if="noMore" class="load-text">没有更多了</p>
<Loading v-else :size="20"></Loading>
</div>
</div>
</div>
</template>
<script>
import {throttle, get} from 'lodash';
import {Loading} from 'cube-ui';
const EVENT_SCROLL = 'scroll';
export default {
name: 'RecycleScrollReveal',
data() {
let data = {
colPrefix: 'sr_col',
visibleItems: [],
colsHeight: [],
items: [],
itemWidth: 0,
currentItems: [],
noMore: false
};
for (let i = 0; i < this.cols; i++) {
data[data.colPrefix + i] = [];
data.visibleItems[i] = data[data.colPrefix + i];
data.colsHeight[i] = 0;
}
return data;
},
props: {
infinite: {
type: Boolean,
default: false
},
size: {
type: Number,
default: 20
},
offset: {
type: Number,
default: 100
},
onFetch: {
type: Function,
required: true
},
thumbs: {
type: Array,
default() {
return [];
}
},
cols: {
type: Number,
default: 2
}
},
computed: {
colStyle() {
return {
width: `${100 / this.cols}%`
};
}
},
watch: {
items(newList, oldList) {
let list = newList.slice(oldList.length, newList.length);
this.loadItems(list, oldList.length);
}
},
mounted() {
this.updateList = throttle(this._updateList.bind(this), 100);
let supportsPassive = false;
try {
const opts = Object.defineProperty({}, 'passive', {
get() {
supportsPassive = true;
return true;
}
});
window.addEventListener('test', null, opts);
} catch (e) {} //eslint-disable-line
this.$el.addEventListener(EVENT_SCROLL, this.onScroll, supportsPassive ? { passive: true } : false);
this.init();
},
beforeDestroy() {
this.$el.removeEventListener(EVENT_SCROLL, this.onScroll);
},
methods: {
resize(index) {
this.$refs.scroll.resize(index);
},
init() {
this.currentItems = [];
this.colsHeight = [];
this.itemWidth = Math.floor(this.$el.offsetWidth / this.cols);
for (let i = 0; i < this.cols; i++) {
this[this.colPrefix + i].length = 0;
this.visibleItems[i] = this[this.colPrefix + i];
this.colsHeight[i] = 0;
}
this.load();
},
load(reload) {
if (!reload && (this.loading || this.noMore)) {
return;
}
this.loading = true;
this.onFetch().then(res => {
this.loading = false;
if (!res) {
this.noMore = true;
} else {
this.items = this.items.concat(res);
}
});
},
async loadItems(list, start = 0) {
if (!list.length) {
return;
}
let lastIndex = list.length;
if (this.cols > 1) {
lastIndex = Math.max.apply(null, [1, list.length - Math.floor(this.cols * 2)]);
}
let startCol = this.getMinHeightCol();
for (let i = 0; i < list.length; i++) {
await this.loadItem({
data: list[i],
index: i + start,
width: this.itemWidth,
isThumb: true,
placeholder: false
}, i <= lastIndex ? (startCol + i) % this.cols : -1);
}
for (let i = 0; i < this.cols; i++) {
let loop = true;
let col = this[this.colPrefix + i];
let k = col.length - 1;
while (loop) {
let cur = col[k];
if (!cur || cur.height) {
loop = false;
continue;
}
let dom = this.$refs[`items${cur.index}`];
try {
if (dom && dom[0]) {
cur.height = dom[0].offsetHeight;
cur.top = dom[0].offsetTop;
}
} catch (error) {
const message = `cur_${typeof cur}, dom_ ${typeof dom}, column_${i}, index_ ${index}, length_ ${this.items.length}, ${error ? error.message : ''}`;
throw new Error(message);
}
this.$set(this[this.colPrefix + i], k, cur);
k--;
}
}
},
loadItem(item, colIndex) {
return new Promise(r => {
let index = colIndex;
if (colIndex < 0) {
index = this.getMinHeightCol();
}
this[this.colPrefix + index].push(item);
if (colIndex < 0) {
setTimeout(() => {
this.$nextTick(() => {
r();
this.colsHeight[index] = this.$refs['col'+index][0].offsetHeight;
});
}, 100);
} else {
r();
}
});
},
getMinHeightCol() {
return this.colsHeight.indexOf(Math.min.apply(null, this.colsHeight));
},
getItemStyle(item) {
const style = {};
if (item.height) {
if (!item.unlockHight) {
style.height = `${item.height}px`;
}
} else if (item.willchange) {
style.transition = 'height 300ms cubic-bezier(0.165, 0.84, 0.44, 1)';
style['will-change'] = 'height';
style.height = `${item.height}px`;
style.opacity = 0;
} else if (!item.isThumb) {
style.position = 'absolute';
style.top = `${-1000}px`;
style.visibility = 'hidden';
}
return style;
},
updateCurrentItems(scrollTop) {
let top = scrollTop - this.$refs.eternal.offsetHeight;
let arr = [];
for (let i = 0; i < this.cols; i++) {
arr.push(this.updateColumnCurrentItems (i, top));
}
this.currentItems = arr;
},
updateColumnCurrentItems (index, top) {
let col = this[this.colPrefix + index];
let startIndex = Math.max(0, Math.floor(col.length * top / this.$refs['col'+index][0].offsetHeight) - this.size);
let endIndex = col.length;
while (startIndex < endIndex) {
if (col[startIndex].top > top) {
endIndex = startIndex - 1;
}
startIndex++;
}
for (let i = Math.max(0, Math.floor(endIndex - this.size * 2.5)); i < Math.min(this.size * 5, col.length); i++) {
let placeholder = Math.abs(i - endIndex) > this.size;
if (col[i].placeholder !== placeholder) {
this.$set(col[i], 'placeholder', placeholder);
}
}
return col[endIndex];
},
_updateList() {
const scrollTop = this.$el.scrollTop;
const heights = this.$el.offsetHeight;
// trigger load
if (scrollTop + this.$el.offsetHeight > heights - this.offset) {
this.load();
}
this.updateCurrentItems(scrollTop);
},
onScroll() {
const scrollTop = this.$el.scrollTop;
this.updateList();
this.$emit('scroll', {scrollTop});
},
},
components: {
Loading
}
};
</script>
<style lang="scss" scoped>
.recycle-scroll-reveal {
position: relative;
height: 100%;
overflow-x: hidden;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.recycle-scroll-reveal-main {
min-height: 100%;
}
.scroll-reveal-list {
display: flex;
align-items: flex-start;
.scroll-reveal-col {
flex-grow: 1;
position: relative;
}
}
.loading {
padding: 20px 0;
text-align: center;
/deep/ .cube-loading-spinners {
margin: auto;
}
}
</style>
... ...
<template>
<ArticleResource v-if="data.dataType == 2" :data="data"></ArticleResource>
<div v-else class="article-item">
<div class="article-item">
<slot>
<ArticleItemHeader :pos-id="posId" :type="type" :thumb="thumb" :share="share" :data="headerData" :lazy="lazy" @on-follow="onFollow" @on-show-more="onShowMore"></ArticleItemHeader>
<ArticleItemSlide :type="type" :thumb="thumb" :share="share" :data="slideData" :slide-index="data.blockIndex" :lazy="lazy" @on-praise="onPraise"></ArticleItemSlide>
... ... @@ -9,8 +8,8 @@
:article-id="articleId" :pos-id="posId">
</ArticleItemIntro>
<ArticleItemComment :thumb="thumb" :type="type" :share="share" :data="commentData" @on-show-comment="onShowComment" @on-resize="onResize" :pos-id="posId" :article-id="data.articleId" ></ArticleItemComment>
<div class="line"></div>
</slot>
<div class="line"></div>
</div>
</template>
... ... @@ -20,7 +19,6 @@ import ArticleItemHeader from './article-item-header';
import ArticleItemSlide from './article-item-slide';
import ArticleItemIntro from './article-item-intro';
import ArticleItemComment from './article-item-comment';
import ArticleResource from './article-resource-item';
import YAS from 'utils/yas-constants';
import dayjs from 'utils/day';
... ... @@ -159,7 +157,7 @@ export default {
});
}
},
components: {ArticleItemHeader, ArticleItemSlide, ArticleItemIntro, ArticleItemComment, ArticleResource}
components: {ArticleItemHeader, ArticleItemSlide, ArticleItemIntro, ArticleItemComment}
};
</script>
... ...
<template>
<div class="article-item">
<slot>
<div class="article-item-main">
<div class="layer-image" :style="`height: ${width * coverImage.scale}px`">
<ImageFormat :mode="1" :src="coverImage.contentData" :width="imgWidth" :height="Math.floor(coverImage.scale * imgWidth)"></ImageFormat>
</div>
<div v-if="intro" class="description">{{intro}}</div>
<div class="attribution">
<div class="auther">
<WidgetAvatar class="avatar" :src="data.authorHeadIco" :width="70" :height="70"></WidgetAvatar>
<span class="name">{{data.authorName}}</span>
</div>
<div class="fav">
<WidgetFav :articleId="data.articleId" :num="data.praiseCount" :option="favOption"></WidgetFav>
</div>
</div>
</div>
</slot>
</div>
</template>
<script>
import {get, first} from 'lodash';
import YAS from 'utils/yas-constants';
import {createNamespacedHelpers} from 'vuex';
const {mapState} = createNamespacedHelpers('article');
export default {
name: 'ArticleItem',
data() {
return {
imgWidth: 370
}
},
props: {
data: {
type: Object,
default() {
return {};
}
},
width: Number,
index: Number,
share: Boolean,
type: String,
thumb: Boolean,
posId: Number,
articleId: Number
},
computed: {
...mapState(['articleStates']),
articleState() {
return this.articleStates[this.data.articleId] || this.data;
},
coverImage() {
let img = get(this.data, 'blockList', []).filter(block => block.templateKey === 'image')[0] || {};
img.scale = img.height / img.width;
return img;
},
intro() {
return get(get(this.data, 'blockList', []).filter(block => block.templateKey === 'text'), '[0].contentData', '')
},
favOption() {
return {
selected: this.articleState.hasPraised === 'Y',
iconBold: true,
iconFontSize: 30,
textAlign: 'normal'
};
}
},
methods: {
onFollow(follow) {
this.$emit('on-follow', follow);
},
onPraise() {
this.$refs.articleIntro.onPraise();
},
reportClickArticle() {
this.$store.dispatch('reportYas', {
params: {
appop: YAS.eventName.articleExpand,
param: {
I_INDEX: this.index,
ARTICLE_ID: this.articleId,
PRD_SKN: this.productListData.map(i => i.productSkn).join(','),
POS_ID: this.posId
}
}
});
}
}
};
</script>
<style scoped>
.article-item {
width: 100%;
padding: 5px;
.article-item-main {
background-color: #fff;
border-radius: 2PX;
overflow: hidden;
}
.layer-image {
img {
width: 100%;
height: 100%;
display: block;
}
}
.description {
margin: 14px 20px;
font-size: 24px;
line-height: 36px;
max-height: 72px;
word-break: break-all;
text-overflow: -o-ellipsis-lastline;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
}
.attribution {
margin: 20px;
display: flex;
justify-content: space-between;
}
.auther {
display: flex;
align-items: center;
}
.avatar {
width: 40px;
height: 40px;
margin-right: 12px;
}
.name {
max-width: 170px;
display: inline-block;
vertical-align: middle;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.line {
height: 10PX;
width: 100%;
background-color: #f0f0f0;
}
}
</style>
... ...
<template>
<div class="article-item">
<div class="article-resource-item">
<a :href="data.resourceUrl" class="resource-block" :style="blockStyle">
<ImageFormat :lazy="true" class="resource-image" :src="data.resourceSrc" :width="width" :height="height"></ImageFormat>
</a>
<div class="line"></div>
</div>
</template>
... ... @@ -16,14 +15,21 @@ export default {
default() {
return {};
}
},
part: {
type: Number,
default: 1
}
},
data() {
return {
width: 750
defaultWidth: 750
};
},
computed: {
width() {
return this.defaultWidth / this.part;
},
height() {
return this.width * this.data.imageHeight / this.data.imageWidth;
},
... ... @@ -36,7 +42,7 @@ export default {
<style lang="scss" scoped>
.article-item {
.article-resource-item {
width: 100%;
background-color: #fff;
... ... @@ -50,11 +56,5 @@ export default {
height: 100%;
}
}
.line {
height: 10PX;
width: 100%;
background-color: #f0f0f0;
}
}
</style>
... ...
... ... @@ -27,7 +27,11 @@
@on-unlock-height="onUnlockHeight"
@on-show-guang="onShowGuang"
@on-show-comment="onShowComment"
@on-show-more="onShowMore"></ArticleItem>
@on-show-more="onShowMore">
<template v-if="data.data.dataType == 2">
<ArticleResource :data="data.data"></ArticleResource>
</template>
</ArticleItem>
</template>
</LayoutRecycleList>
</Layout>
... ... @@ -51,6 +55,7 @@ import {throttle} from 'lodash';
import YAS from 'utils/yas-constants';
import ArticleItem from './article-item';
import ArticleActionSheet from '../detail/article-action-sheet';
import ArticleResource from './article-resource-item';
import MoreActionSheet from '../detail/more-action-sheet';
import {mapState, createNamespacedHelpers} from 'vuex';
const {mapMutations} = createNamespacedHelpers('article');
... ... @@ -281,7 +286,8 @@ export default {
components: {
ArticleItem,
ArticleActionSheet,
MoreActionSheet
MoreActionSheet,
ArticleResource
}
};
</script>
... ...
... ... @@ -2,7 +2,28 @@
<div>
<Layout class="article">
<TopicHeader ref="header" :title="topicTitle" :step="headerAnimateStep" :data="topicInfo" @on-follow="onFollowTopic"></TopicHeader>
<LayoutRecycleList :size="10" :thumbs="thumbs" ref="scroll" @scroll="onScroll" :offset="2000" :on-fetch="onFetch"
<RecycleScrollReveal v-if="topicInfo.viewModel == 2" :size="20" :thumbs="thumbs" ref="scroll" @scroll="onScroll" :offset="2000" :on-fetch="onFetch"
@on-inited="onInited">
<template v-slot:eternalTop>
<TopicBanner ref="topicHead" :data="topicInfo" @on-follow="onFollowTopic"></TopicBanner>
</template>
<template class="article-item" #item="{ data }">
<ArticleItem2
:type="type"
:index="data.index"
:data="data.data"
:share="share"
:article-id="data.data.articleId"
:pos-id="posId">
<template v-if="data.data.dataType == 2">
<ArticleResource :data="data.data"></ArticleResource>
</template>
</ArticleItem2>
</template>
</RecycleScrollReveal>
<LayoutRecycleList v-if="topicInfo.viewModel == 1" :size="10" :thumbs="thumbs" ref="scroll" @scroll="onScroll" :offset="2000" :on-fetch="onFetch"
@on-inited="onInited">
<template class="article-item" #item="{ data }">
<TopicBanner v-if="data.data.topicHead" ref="topicHead" :data="topicInfo" @on-follow="onFollowTopic"></TopicBanner>
... ... @@ -20,9 +41,13 @@
@on-show-guang="onShowGuang"
@on-show-comment="onShowComment"
@on-show-more="onShowMore">
<template v-if="data.data.dataType == 2">
<ArticleResource :data="data.data"></ArticleResource>
</template>
</ArticleItem>
</template>
</LayoutRecycleList>
</Layout>
<ArticleActionSheet v-if="showArticleDetailAction" ref="actionSheet"></ArticleActionSheet>
<YohoActionSheet transfer v-if="showCommentAction" ref="commentAction" :full="true">
... ... @@ -42,6 +67,7 @@
import {throttle, get} from 'lodash';
import YAS from 'utils/yas-constants';
import ArticleItem from './components/article/article-item';
import ArticleItem2 from './components/article/article-item2';
import ArticleActionSheet from './components/detail/article-action-sheet';
import MoreActionSheet from './components/detail/more-action-sheet';
import TopicHeader from './components/topic/header';
... ... @@ -54,10 +80,8 @@ const throttleTime = 40;
export default {
name: 'TopicPage',
props: {
share: Boolean,
type: String,
noHeader: Boolean,
thumbs: {
type: Array,
default() {
... ... @@ -219,8 +243,10 @@ export default {
},
init() {
this.fetchTopicSimpleInfo({topicId: this.topicId});
this.$refs.scroll.$el.scrollTop = 0;
this.$refs.scroll.init();
if (this.$refs.scroll) {
this.$refs.scroll.$el.scrollTop = 0;
this.$refs.scroll.init();
}
},
async onFetch() {
const topicId = parseInt(this.topicId, 10);
... ... @@ -260,7 +286,7 @@ export default {
}
return new Promise(resolve => {
if (this.page <= 2) {
if (this.topicInfo.viewModel === 1 && this.page <= 2) {
list = [{topicHead: true}, ...list];
}
... ... @@ -344,6 +370,7 @@ export default {
},
components: {
ArticleItem,
ArticleItem2,
ArticleActionSheet,
MoreActionSheet,
TopicHeader,
... ... @@ -353,6 +380,14 @@ export default {
</script>
<style scoped>
/deep/ .recycle-scroll-reveal-main {
background-color: #f7f7f7;
.scroll-reveal-list {
padding: 5px;
}
}
/deep/ .att-click-wrap {
opacity: 0;
position: absolute;
... ...