Authored by yyq

long article

import Layout from './layout';
import LayoutHeader from './layout-header';
import LayoutTitle from './layout-title';
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,
LayoutHeader,
LayoutTitle,
LayoutRecycleList,
RecycleList,
AuthComponent,
... ...
<template>
<div class="layout-title">
<slot>{{title}}</slot>
</div>
</template>
<script>
export default {
name: 'LayoutTitle',
props: {
title: {
type: String,
default: ''
}
}
};
</script>
<style lang="scss" scoped>
.layout-title {
font-size: 32px;
color: #222;
padding-left: 30px;
}
</style>
... ...
... ... @@ -190,7 +190,7 @@ export default {
width: this.itemWidth,
isThumb: true,
placeholder: false
}, i <= lastIndex ? (startCol + i) % this.cols : -1);
}, i < lastIndex ? (startCol + i) % this.cols : -1);
}
this.$nextTick(() => {
... ... @@ -228,26 +228,20 @@ export default {
return true;
});
},
loadItem(item, colIndex) {
loadItem(item, index) {
return new Promise(r => {
let index = colIndex;
if (colIndex < 0) {
if (index < 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();
}
setTimeout(() => {
this.$nextTick(() => {
r();
this.colsHeight[index] = this.$refs['col'+index][0].offsetHeight;
});
}, 100);
});
},
getMinHeightCol() {
... ...
<template>
<div
class="product-item"
:class="{single}">
:class="itemClass">
<div class="product-content" @click="onClick">
<div class="product-image">
<ImageFormat class="image" :lazy="lazy" :src="product.productImage" :width="136" :height="180"></ImageFormat>
... ... @@ -32,7 +32,8 @@ export default {
thumb: Boolean,
posId: Number,
articleId: [String, Number],
index: Number
index: Number,
model: String
},
data() {
return {
... ... @@ -46,6 +47,12 @@ export default {
}
},
computed: {
itemClass() {
return {
single: this.single,
[`product-model-${this.model}`]: this.model
};
},
favClass() {
return {
'btn-is-fav': this.favorite,
... ... @@ -201,4 +208,67 @@ export default {
margin: 0;
}
}
.product-model-2 {
width: 170px;
height: auto;
overflow: visible;
margin-left: 20px;
margin-right: 0;
.product-content {
display: block;
border: 0;
position: relative;
}
.product-image {
width: 100%;
height: 226px;
}
.product-info {
padding: 0;
margin-top: 12px;
.product-name {
height: 66px;
font-size: 25px;
line-height: 1.4;
padding-right: 0;
margin-bottom: 0;
color: #444;
letter-spacing: 1PX;
width: 213px;
transform: translateX(-21px) scale(0.8);
}
.price {
width: 126px;
height: 50px;
color: #fff;
font-size: 25px;
line-height: 1;
font-weight: 300;
position: absolute;
top: 166px;
right: -22px;
background-color: #444;
display: flex;
justify-content: center;
align-items: center;
transform: scale(0.8);
> * {
display: none;
}
> span {
display: inline;
}
}
}
}
</style>
... ...
... ... @@ -8,6 +8,7 @@
:single="single"
:product="product"
:article-id="articleId"
:model="model"
:pos-id="posId"
:lazy="lazy"
:index="index"
... ... @@ -19,7 +20,6 @@
<script>
import ProductGroupItem from './product-group-item';
import {Button} from 'cube-ui';
export default {
name: 'ProductGroup',
... ... @@ -40,29 +40,35 @@ export default {
share: Boolean,
thumb: Boolean,
posId: Number,
index: Number
index: Number,
model: String
},
computed: {
single() {
return this.data.length === 1;
},
},
components: {Button, ProductGroupItem}
components: {ProductGroupItem}
};
</script>
<style lang="scss" scoped>
.product-group {
height: 182px;
margin: 40px 0;
overflow-y: hidden;
&:after {
content: "";
display: block;
margin-top: -79px;
}
}
.product-group-scroll {
width: 100%;
padding-bottom: 80px;
overflow-x: auto;
overflow-y: hidden;
height: 220px;
white-space: nowrap;
-webkit-overflow-scrolling: touch;
}
... ...
<template>
<Layout class="article-detail">
<RecycleScrollReveal :size="5" ref="scroll" @scroll="onScroll" :offset="800" :on-fetch="onFetch" :manual-init="true">
<template v-slot:eternalTop>
<ArticleDeatilLong ref="detailLong" :data="article" :scrollTop="scrollTop"></ArticleDeatilLong>
</template>
<template class="article-item" #item="{ data }">
<ArticleItem2
type="topic"
:index="data.index"
:data="data.data"
:width="colWidthForTwo"
: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>
</Layout>
</template>
<script>
import {get} from 'lodash';
import ArticleDeatilLong from './components/detail/article-long';
import ArticleItem2 from './components/article/article-item2';
import {createNamespacedHelpers} from 'vuex';
const {mapState, mapActions} = createNamespacedHelpers('article');
export default {
name: 'ArticleDetailPage',
data() {
return {
id: 0,
scrollTop: 0,
scrolling: false,
article: {},
share: false,
colWidthForTwo: 370,
posId: 0
};
},
activated() {
if (+this.$route.params.id !== this.id) {
this.id = +this.$route.params.id;
this.init();
}
},
mounted() {
this.colWidthForTwo = Math.floor(this.$el.offsetWidth / 2);
},
computed: {
},
methods: {
...mapActions(['fetchArticleList']),
init() {
this.syncServiceArticleDetail();
},
onScroll({scrollTop}) {
this.scrollTop = scrollTop;
this.scrolling = true;
this._scTimer && clearTimeout(this._scTimer);
this._scTimer = setTimeout(() => {
this.scrolling = false;
}, 400);
},
syncServiceArticleDetail() {
const articleId = parseInt(this.id, 10);
this.fetchArticleList({
articleId,
page: 1,
limit: 1,
columnType: 1002
}).then(res => {
this.article = get(res, 'data.detailList[0]', {});
});
},
onFetch() {
return Promise.resolve([this.article]);
}
},
components: {
ArticleDeatilLong,
ArticleItem2
}
};
</script>
<style lang="scss" scoped>
/deep/ .scroll-reveal-list {
padding: 6px;
background-color: #f0f0f0;
}
</style>
... ...
... ... @@ -11,7 +11,7 @@
</div>
<div class="opts">
<WidgetFollow :class="invisibleClass" v-if="data.hasAttention" :share="share" :author-uid="data.authorUid" :authorType="data.authorType" :follow="data.hasAttention === 'Y'" @on-follow="onFollow" :pos-id="posId"></WidgetFollow>
<i class="iconfont icon-more1" @click="onMore"></i>
<i v-if="more" class="iconfont icon-more1" @click="onMore"></i>
</div>
</div>
</template>
... ... @@ -32,6 +32,10 @@ export default {
type: Boolean,
default: true
},
more: {
type: Boolean,
default: true
},
type: String,
share: Boolean,
thumb: Boolean,
... ...
<template>
<div class="article-item-topics">
<WidgetTopic
:share="share"
:topic="topic.topicName"
@click.native="onTopic(topic)"
v-for="topic in data.topicList"
:key="topic.topicId">
</WidgetTopic>
</div>
</template>
<script>
import YAS from 'utils/yas-constants';
export default {
name: 'ArticleItemTopics',
props: {
data: {
type: Object,
default() {
return {};
}
},
share: Boolean,
posId: {
type: Number,
default() {
return 0;
}
},
},
methods: {
onTopic({topicId, topicName}) {
if (this.share) {
return this.$links.toDownloadApp();
}
if (this.data.articleType === 5) {
return;
}
this.$router.push({
name: 'topic',
params: {
topicId: topicId,
topicName: topicName
}
});
this.reportLabel(topicId);
},
reportLabel(id) {
this.$store.dispatch('reportYas', {
params: {
appop: YAS.eventName.labelClick,
param: {
LABEL_ID: id,
POS_ID: this.posId
}
}
});
}
}
};
</script>
<style lang="scss" scoped>
.article-item-topics {
width: 100%;
margin-top: 32px;
line-height: 60px;
& /deep/ .topic-wrap {
margin-right: 10px;
}
}
</style>
... ...
... ... @@ -5,7 +5,9 @@
<WidgetIconBtn class="item" type="star" :pos-id="sceneId" :text="favoriteCount" :articleId="articleId" :option="optionFav" ></WidgetIconBtn>
<WidgetIconBtn class="item" type="msg" :text="commentCount" :option="optionComment" @click="onComment"></WidgetIconBtn>
</div>
<div class="close ml20" @click="onClose">收起</div>
<div class="close ml20" @click="onClose">
<slot>收起</slot>
</div>
</div>
</template>
... ... @@ -67,6 +69,7 @@ export default {
color: white;
font-size: 32px;
line-height: 100px;
font-weight: 300;
background-color: #d0021b;
text-align: center;
}
... ...
<template>
<div class="fixed-header" :style="headerStyle">
<LayoutHeader theme="transparent">
<template>
<div ref="titleBlock" class="title-block" :style="`transform: translate3d(0, ${blockTranslateY}, 0)`">
<slot></slot>
</div>
</template>
<template v-slot:opts>
<WidgetShare v-if="!share" class="article-share" :option="shareOption" @click.native="onShare"></WidgetShare>
</template>
</LayoutHeader>
</div>
</template>
<script>
import {get} from 'lodash';
import ArticleItemHeader from '../article/article-item-header';
export default {
name: 'ArticleDetailHeader',
data() {
return {
shareOption: {
color: '#fff',
iconBold: true
}
}
},
props: [
'data',
'step',
'titleStep',
'share'
],
computed: {
stepPercent() {
return parseInt(this.step, 10) / 100;
},
headerStyle() {
const color = Math.floor((255 - 68) * (1 - this.stepPercent) + 68);
if (this.$el) {
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})`;
});
}
}
return {
background: `rgba(255,255,255,${this.stepPercent})`
}
},
blockTranslateY() {
return '-' + parseInt(`0${this.titleStep}`, 10) + '%';
}
},
watch: {
},
methods: {
onShare() {
this.$yoho.share({
title: this.data.topicName,
imgUrl: this.data.topicImageUrl,
link: `${location.origin}/grass/topic/share/${this.data.topicId}`,
desc: '我在有货的社区发现一个热门话题。' + this.data.topicDesc,
hideType: ['7', '8', '9']
});
},
onFollow() {
return parseInt(this.step, 10) / 100;
}
},
components: {
ArticleItemHeader,
}
};
</script>
<style scoped>
.fixed-header {
position: fixed;
width: 100%;
z-index: 10;
transition: all 100ms;
/deep/ .title {
overflow: visible!important;
}
.title-block {
height: 100%;
position: absolute;
top: 100%;
left: -60px;
right: -60px;
}
.article-share {
margin-right: 26px;
}
}
</style>
... ...
<template>
<div class="article-detail-long">
<ArticleDetailHeader ref="header" :step="headerAnimateStep" :title-step="headerTitleAnimateStep" :data="authorData">
<div class="title-main">
<div class="title-info" :style="`transform: translate3d(0, ${titleTranslateY}, 0)`">
<ArticleItemHeader class="title-info-author" :share="share" :data="authorData" :lazy="lazy" :more="showMoreOpt" @on-follow="onFollow"></ArticleItemHeader>
<div class="title-info-rec">推荐阅读</div>
</div>
</div>
</ArticleDetailHeader>
<div ref="coverFigure" class="cover-figure">
<img class="cover-img" :src="coverImage.src" :style="`transform: translate3d(0, ${coverTranslateY}px, 0)`">
</div>
<div ref="authorBlock" class="author-block">
<ArticleItemHeader :share="share" :data="authorData" :lazy="lazy" :more="showMoreOpt" @on-follow="onFollow"></ArticleItemHeader>
</div>
<div class="main-detail">
<div class="article-context" style="height: 1000px;background: #ccc;">
</div>
<ArticleItemTopics class="topics-wrap" :data="data" :share="share"></ArticleItemTopics>
<LayoutTitle>推荐商品</LayoutTitle>
<ProductGroup :data="recomendProduct" model="2"></ProductGroup>
<LayoutTitle class="recomend-title">推荐阅读</LayoutTitle>
</div>
<ArticleDetailFooter class="detail-fixed-footer">
文中商品
</ArticleDetailFooter>
</div>
</template>
<script>
import ArticleItemHeader from '../article/article-item-header';
import ArticleItemTopics from '../article/article-item-topics';
import ArticleDetailFooter from './article-footer';
import ArticleDetailHeader from './article-header';
import {mapState} from 'vuex';
import {get} from 'lodash';
export default {
name: 'ArticleDetailLong',
props: {
data: {
type: Object,
default() {
return {};
}
},
scrollTop: Number,
share: Boolean,
},
data() {
return {
coverHeight: 0,
downgrade: false,
showMoreOpt: false,
authorBlock: {},
recomendProduct: [{
"id": 230,
"marketPrice": 390,
"orderBy": 0,
"productId": 142427,
"productImage": "http://img10.static.yhbimg.com/goodsimg/2019/01/16/16/0123e8e8c7243ca83e37e6a11d4a431777.png?imageView2/{mode}/w/{width}/h/{height}",
"productName": "Timberland 插肩印花短袖T恤",
"productSkn": 51085719,
"productType": 1,
"salesPrice": 11
}, {
"id": 232,
"marketPrice": 1290,
"orderBy": 1,
"productId": 142447,
"productImage": "http://img13.static.yhbimg.com/goodsimg/2015/02/13/08/024becd3478789516c2749d5bc0e4e48cc.jpg?imageView2/{mode}/w/{width}/h/{height}",
"productName": "Timberland 女士经典无里衬帆船鞋",
"productSkn": 51085694,
"productType": 1,
"salesPrice": 11
}, {
"id": 234,
"marketPrice": 444,
"orderBy": 2,
"productId": 508376,
"productImage": "http://img13.static.yhbimg.com/goodsimg/2017/03/31/17/021204a24a95b6a5c258e7469e7bbbd22f.jpg?imageView2/{mode}/w/{width}/h/{height}",
"productName": "bigrabbit男手套",
"productSkn": 512587700,
"productType": 1,
"salesPrice": 444
}, {
"id": 236,
"marketPrice": 790,
"orderBy": 3,
"productId": 142413,
"productImage": "http://img10.static.yhbimg.com/goodsimg/2019/01/17/10/01d10b02308ecbfb42176cbf965478181a.jpg?imageView2/{mode}/w/{width}/h/{height}",
"productName": "Timberland 男士休闲短裤116",
"productSkn": 51085737,
"productType": 1,
"salesPrice": 11
}, {
"id": 238,
"marketPrice": 429,
"orderBy": 4,
"productId": 69792,
"productImage": "http://img13.static.yhbimg.com/goodsimg/2014/05/14/03/02e5b2050d3fd73584dd786cadc4eaf68d.jpg?imageView2/{mode}/w/{width}/h/{height}",
"productName": "izzue 碎花拼接T恤",
"productSkn": 51041119,
"productType": 1,
"salesPrice": 11
}],
};
},
mounted() {
let {clientHeight, clientWidth} = document.documentElement;
if (!this.$yoho.isiOS && (clientHeight / clientWidth) < 1.8) {
this.downgrade = true;
}
},
computed: {
...mapState(['yoho']),
coverImage() {
this.$nextTick(() => {
if (this.$refs.coverFigure) {
this.coverHeight = this.$refs.coverFigure.offsetHeight;
}
if (this.$refs.authorBlock) {
this.authorBlock = {
height: this.$refs.authorBlock.offsetHeight,
top: this.$refs.authorBlock.offsetTop
};
}
});
return {
src: '//flv01.static.yhbimg.com/grassImg/2019/05/07/12/03a620a29bb8b3a4f508dc8f86f6974a7a.jpg'
};
},
coverTranslateY() {
let top = this.scrollTop > 0 ? this.scrollTop : 0;
if (top && !this.downgrade && this.coverHeight && top < this.coverHeight) {
return top / 2;
} else {
return 0;
}
},
headerAnimateStep() {
if (this.scrollTop > this.coverHeight) {
return 100;
} else if (this.scrollTop > 0) {
let step = Math.round(this.scrollTop - 10 / (this.coverHeight - 10) * 100);
return Math.max(Math.min(step, 100), 0);
} else {
return 0;
}
},
headerTitleAnimateStep() {
let {height, top} = this.authorBlock;
let scrollTop = this.scrollTop;
if (this.$refs && this.$refs.header) {
scrollTop += this.$refs.header.$el.offsetHeight;
}
if (top && height) {
if (scrollTop >= top + height) {
return 100;
} else if (scrollTop > top) {
let step = Math.round((scrollTop - top) / height * 100);
return Math.max(Math.min(step, 100), 0);
}
}
return 0;
},
titleTranslateY() {
let scrollTop = this.scrollTop;
if (this.$refs && this.$refs.header) {
scrollTop += this.$refs.header.$el.offsetHeight;
return scrollTop > this.$el.offsetHeight ? '-50%' : 0;
} else {
return 0;
}
},
authorData() {
return {
authorName: this.data.authorName,
authorUid: this.data.authorUid,
authorType: this.data.authorType,
authorHeadIco: this.data.authorHeadIco,
hasAttention: this.data.hasAttention,
isAuthor: this.data.isAuthor
};
},
lazy() {
return this.data.lazy;
}
},
methods: {
onClick() {
if (this.share) {
return this.$links.toDownloadApp();
}
},
onFollow(follow) {
this.$emit('on-follow', follow);
},
onTopic() {
}
},
components: {
ArticleDetailHeader,
ArticleItemHeader,
ArticleItemTopics,
ArticleDetailFooter
}
};
</script>
<style scoped>
.title-main {
height: 100%;
color: #444;
overflow: hidden;
.title-info {
height: 200%;
transition: all 300ms;
> * {
height: 50%;
background: none;
}
.title-info-rec {
font-size: 32px;
line-height: 1.2;
display: flex;
justify-content: center;
align-items: center;
}
}
/deep/ .avatar {
padding-left: 0;
}
/deep/ .opts {
padding-right: 10px;
}
}
.cover-img {
width: 100%;
display: block;
position: relative;
z-index: 0;
}
.author-block,
.main-detail {
position: relative;
z-index: 1;
}
.article-context {
padding: 30px;
}
.topics-wrap {
padding-left: 30px;
margin-bottom: 60px;
}
/deep/ .product-item:first-child {
margin-left: 30px;
}
/deep/ .product-item:last-child {
margin-right: 30px;
}
.recomend-title {
padding-top: 18px;
padding-bottom: 24px;
}
.detail-fixed-footer {
width: 100%;
position: fixed;
bottom: 0;
z-index: 10;
}
</style>
... ...
... ... @@ -23,6 +23,14 @@ export default [{
keepAlive: true
}
}, {
path: '/article/detail2/:id',
name: 'article.detail2',
alias: '/article/detail2/:id',
component: () => import(/* webpackChunkName: "article-detail" */ './article-detail2'),
meta: {
keepAlive: true
}
}, {
path: '/article/:id/user/:type/:authorType/:authorUid',
name: 'article.user',
alias: '/article/:id/user/:type/:authorType/:authorUid',
... ...