Authored by 陈峰

commit

... ... @@ -8,13 +8,17 @@ import titleMixin from './mixins/title';
import pluginCore from './plugins/core';
import lazyload from 'vue-lazyload';
import reportError from 'report-error';
import dayjs from 'dayjs';
import 'dayjs/locale/zh-cn';
import relativeTime from 'dayjs/plugin/relativeTime';
Vue.use(lazyload, {
preLoad: 2
});
Vue.use(pluginCore);
Vue.mixin(titleMixin);
dayjs.locale('zh-cn');
dayjs.extend(relativeTime);
export function createApp(context) {
const router = createRouter();
... ...
... ... @@ -3,6 +3,11 @@
<template v-slot:item="{ data }">
<slot name="item" :data="data"></slot>
</template>
<template v-slot:noMore>
<p class="">
没有更多了
</p>
</template>
</RecycleList>
</template>
... ...
... ... @@ -42,18 +42,15 @@
</div>
<div
v-if="!infinite"
class="cube-recycle-list-loading"
:style="{visibility: loading ? 'visible' : 'hidden'}"
>
class="cube-recycle-list-loading">
<slot name="spinner">
<div class="cube-recycle-list-loading-content">
<div class="cube-recycle-list-loading-content" v-show="!noMore" :style="{visibility: loading ? 'visible' : 'hidden'}">
<cube-loading class="spinner"></cube-loading>
</div>
</slot>
</div>
<div v-show="noMore" class="cube-recycle-list-noMore">
<slot name="noMore" />
<div v-show="noMore" class="cube-recycle-list-noMore">
<slot name="noMore" />
</div>
</div>
</div>
<div class="cube-recycle-list-fake"></div>
... ... @@ -153,7 +150,7 @@ export default {
// increase capacity of items to display tombstone
this.items.length += this.size;
this.loadItems();
} else if (!this.loading) {
} else if (!this.loading && !this.noMore) {
this.getItems();
}
},
... ...
... ... @@ -6,10 +6,10 @@
v-for="(product, inx) in data"
:key="inx">
<div class="product-content">
<ImageFormat class="product-image" :src="product.src"></ImageFormat>
<ImageFormat :lazy="lazy" class="product-image" :src="product.productImage" :width="136" :height="180"></ImageFormat>
<div class="product-info">
<p class="product-name">{{product.name}}</p>
<p class="price">¥{{product.price}}</p>
<p class="product-name">{{product.productName}}</p>
<p class="price">¥{{product.salesPrice}}</p>
</div>
</div>
<div class="btn-fav hover-opacity" v-if="!product.isFav">收藏</div>
... ... @@ -29,6 +29,10 @@ export default {
default() {
return [];
}
},
lazy: {
type: Boolean,
default: true
}
},
computed: {
... ... @@ -94,6 +98,8 @@ export default {
color: #9b9b9b;
letter-spacing: -0.25PX;
height: 104px;
display: flex;
align-content: center;
}
.price {
... ...
... ... @@ -5,6 +5,7 @@ import WidgetLike from './widget-like';
import WidgetShare from './widget-share';
import WidgetTopic from './widget-topic';
import WidgetAvatar from './widget-avatar';
import WidgetFollow from './widget-follow';
export default [
WidgetAvatarGroup,
... ... @@ -13,5 +14,6 @@ export default [
WidgetLike,
WidgetShare,
WidgetTopic,
WidgetAvatar
WidgetAvatar,
WidgetFollow
];
... ...
<template>
<ImageFormat class="img-avatar" :src="src" :width="width" :height="height"></ImageFormat>
<ImageFormat :lazy="lazy" class="img-avatar" :src="src" :width="width" :height="height"></ImageFormat>
</template>
<script>
... ... @@ -17,6 +17,10 @@ export default {
height: {
type: Number,
default: 35
},
lazy: {
type: Boolean,
default: false
}
},
computed: {
... ...
... ... @@ -6,7 +6,7 @@
export default {
name: 'WidgetFav',
props: {
num: String,
num: [String, Number],
option: Object
}
};
... ...
<template>
<button class="btn-follow hover-opacity" v-if="!followed">关注</button>
<button class="btn-follow followed hover-opacity" v-else>已关注</button>
</template>
<script>
export default {
name: 'WidgetFollow',
props: {
authorUid: Number,
followed: Boolean
}
};
</script>
<style lang="scss" scoped>
.btn-follow {
width: 120px;
height: 50px;
padding: 0;
font-size: 26px;
border-radius: 3PX;
background-color: #222;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
&.followed {
border: solid 1px #4a4a4a;
background-color: #fff;
color: #000;
}
}
</style>
... ...
... ... @@ -6,7 +6,7 @@
export default {
name: 'WidgetLike',
props: {
num: String,
num: [String, Number],
option: Object
}
};
... ...
... ... @@ -6,7 +6,7 @@
export default {
name: 'WidgetShare',
props: {
num: String,
num: [String, Number],
option: Object
}
};
... ...
<template>
<Article :on-fetch="onFetch">
<template v-slot:thumb>
<ArticleItem v-for="data in articleCurrentList" :key="data.articleId" :data="data"></ArticleItem>
<ArticleItem v-for="data in currentList" :key="data.articleId" :data="data"></ArticleItem>
</template>
</Article>
</template>
<script>
import {get} from 'lodash';
import Article from './components/article/article';
import ArticleItem from './components/article/article-item';
import {createNamespacedHelpers} from 'vuex';
... ... @@ -20,27 +21,43 @@ export default {
};
},
serverPrefetch() {
console.log('serverPrefetch')
return this.onFetch();
},
computed: {
...mapState(['articleCurrentList'])
...mapState(['articleCurrentList']),
currentList() {
if (this.articleCurrentList.length > 2) {
return this.articleCurrentList.slice(0, 2);
}
return this.articleCurrentList;
}
},
methods: {
...mapActions(['fetchArticleList']),
async onFetch() {
if (this.page === 1 && this.articleCurrentList.length) {
this.page++;
return this.articleCurrentList;
}
const articleId = parseInt(this.$route.params.id, 10);
if (!articleId) {
return;
}
const result = await this.fetchArticleList({
articleId: this.$route.params.id,
articleId: parseInt(this.$route.params.id, 10),
page: this.page
});
if (result.code === 200) {
this.page++;
return Promise.resolve(result.data.detailList);
if (get(result, 'data.detailList', []).length) {
this.page++;
return Promise.resolve(result.data.detailList);
}
return Promise.resolve(false);
} else {
this.$createToast({
this.$createToast && this.$createToast({
txt: result.message || '服务器开小差了',
type: 'warn',
time: 1000
... ...
<template>
<div class="article-item-comment">
<p class="comment-item" v-for="(comment, inx) in comments" :key="inx">
<span class="user-name">{{comment.name}}:</span>
<p class="comment-item" v-for="(comment, inx) in data.comments" :key="inx">
<span class="user-name">{{comment.userName}}:</span>
<span class="comment-content">{{comment.content}}</span>
</p>
<div class="comment">
<div class="comment-input hover-opacity">添加回复:赞美是一种美德</div>
</div>
<div class="total-comment">
<div class="total hover-opacity">查看{{count}}条评论</div>
<div class="last-time">{{date}}</div>
<div class="total hover-opacity">查看{{data.commentCount}}条评论</div>
<div class="last-time">{{data.date}}</div>
</div>
</div>
</template>
... ... @@ -19,20 +19,12 @@ import {Input} from 'cube-ui';
export default {
name: 'ArticleItemComment',
props: {
comments: {
type: Array,
data: {
type: Object,
default() {
return [];
return {};
}
},
count: {
type: Number,
default: 0
},
date: {
type: String,
default: 0
}
},
components: {CubeInput: Input}
};
... ...
<template>
<div class="article-item-header">
<div class="avatar">
<WidgetAvatar class="widget-avatar" :src="data.authorHeadIco" :width="70" :height="70"></WidgetAvatar>
<WidgetAvatar :lazy="lazy" class="widget-avatar" :src="data.authorHeadIco" :width="70" :height="70"></WidgetAvatar>
<span class="name">{{data.authorName}}</span>
</div>
<div class="opts">
<button class="btn-follow hover-opacity" v-if="true">关注</button>
<button class="btn-follow followed hover-opacity" v-else>已关注</button>
<WidgetFollow :article-id="data.authorUid" :followed="data.hasAttention === 'Y'"></WidgetFollow>
<i class="iconfont icon-more1" @click="onMore"></i>
</div>
</div>
... ... @@ -21,6 +20,10 @@ export default {
default() {
return {};
}
},
lazy: {
type: Boolean,
default: true
}
},
methods: {
... ... @@ -75,25 +78,6 @@ export default {
align-items: center;
justify-content: flex-end;
.btn-follow {
width: 120px;
height: 50px;
padding: 0;
font-size: 26px;
border-radius: 3PX;
background-color: #222;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
&.followed {
border: solid 1px #4a4a4a;
background-color: #fff;
color: #000;
}
}
.icon-more1 {
font-size: 40px;
margin-left: 30px;
... ...
... ... @@ -2,22 +2,25 @@
<div class="article-item-intro">
<div ref="intro" class="intro hover-opacity" :class="introClass" :style="introStyle" @click="onExpand">
{{intro}}
<span class="expand" v-if="!isExpand">…展开</span>
<span class="expand collapse" v-else>收起</span>
<span class="expand" v-if="!isExpand && isEllipsis">…展开</span>
<span class="expand collapse" v-if="isExpand && isEllipsis">收起</span>
</div>
<div class="topics">
<WidgetTopic topic="种草1" @click.native="onTopic"></WidgetTopic>
<WidgetTopic topic="种草2" @click.native="onTopic"></WidgetTopic>
<WidgetTopic topic="种草3" @click.native="onTopic"></WidgetTopic>
<WidgetTopic
:topic="label.labelName"
@click.native="onTopic(label)"
v-for="label in data.labelList"
:key="label.labelId">
</WidgetTopic>
</div>
<div class="widgets">
<div class="share">
<WidgetShare></WidgetShare>
</div>
<div class="opts">
<WidgetFav num="99"></WidgetFav>
<WidgetLike num="91"></WidgetLike>
<WidgetFav num="99"></WidgetFav>
<WidgetFav :num="data.favoriteCount" :option="favoriteOption"></WidgetFav>
<WidgetLike :num="data.praiseCount" :option="praiseOption"></WidgetLike>
<WidgetFav :num="data.commentCount"></WidgetFav>
</div>
</div>
</div>
... ... @@ -45,12 +48,16 @@ export default {
introHeight: 0
};
},
created() {
if (this.data.intro.length < 66) {
this.isEllipsis = false;
}
},
computed: {
intro() {
if (!this.isEllipsis) {
if (this.isExpand || this.data.intro.length < 66) {
return this.data.intro;
}
if (this.data.intro.length > 66) {
} else {
return this.data.intro.substring(0, 66);
}
},
... ... @@ -63,7 +70,17 @@ export default {
return {
height: this.introHeight ? `${this.introHeight}px` : void 0
};
}
},
favoriteOption() {
return {
selected: this.data.hasFavor === 'Y'
};
},
praiseOption() {
return {
selected: this.data.hasPraise === 'Y'
};
},
},
mounted() {
this.introCollapseHeight = this.$refs.intro.scrollHeight;
... ... @@ -79,8 +96,10 @@ export default {
});
},
onExpand() {
if (!this.isEllipsis) {
return;
}
this.isExpand = !this.isExpand;
this.isEllipsis = false;
this.$nextTick(() => {
if (this.isExpand) {
this.introHeight = this.$refs.intro.scrollHeight;
... ... @@ -89,9 +108,6 @@ export default {
}
this.isExpanding = true;
this.$emit('on-resizeing');
if (!this.isExpand) {
this.isEllipsis = true;
}
this.resizeDebounce();
});
},
... ...
... ... @@ -2,7 +2,7 @@
<div class="article-item-slide">
<Slide :data="data.blockList" :threshold="0.2" :auto-play="false" :loop="false" :options="scrollOption" @change="onChange">
<SlideItem v-for="(item, inx) in data.blockList" :key="inx">
<ImageFormat :lazy="data.index > 0" class="image-slide-item" :src="item.contentData" :width="item.width" :height="item.height"></ImageFormat>
<ImageFormat :lazy="lazy" class="image-slide-item" :src="item.contentData" :width="item.width" :height="item.height"></ImageFormat>
</SlideItem>
<template slot="dots" slot-scope="props">
<span class="slide-dot"
... ... @@ -29,6 +29,10 @@ export default {
default() {
return {};
}
},
lazy: {
type: Boolean,
default: true
}
},
data() {
... ...
<template>
<div class="article-item">
<ArticleItemHeader :data="headerData"></ArticleItemHeader>
<ArticleItemSlide :data="slideData"></ArticleItemSlide>
<ProductGroup :data="productListData"></ProductGroup>
<ArticleItemHeader :data="headerData" :lazy="lazy"></ArticleItemHeader>
<ArticleItemSlide :data="slideData" :lazy="lazy"></ArticleItemSlide>
<ProductGroup :data="productListData" :lazy="lazy"></ProductGroup>
<ArticleItemIntro :data="introData" @on-resize="onResize" @on-resizeing="onResizeing"></ArticleItemIntro>
<ArticleItemComment :comments="commentData" :count="12" :date="'1天前'"></ArticleItemComment>
<ArticleItemComment :data="commentData"></ArticleItemComment>
<div class="line"></div>
</div>
</template>
<script>
import {get} from 'lodash';
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 dayjs from 'dayjs';
export default {
name: 'ArticleItem',
... ... @@ -29,47 +31,40 @@ export default {
headerData() {
return {
authorName: this.data.authorName,
articleId: this.data.articleId,
authorHeadIco: this.data.authorHeadIco
authorUid: this.data.authorUid,
authorHeadIco: this.data.authorHeadIco,
hasAttention: this.data.hasAttention,
};
},
slideData() {
return {
blockList: this.data.blockList,
index: this.data.index
blockList: get(this.data, 'blockList', []).filter(block => block.templateKey === 'image'),
};
},
introData() {
return {
intro: '旗下大热鞋款旗下大热鞋款旗下大热鞋款一直以来在街头造型当中的能见度都算高,凭藉其舒适脚感与百搭外型旗下大热鞋款也轻松成为许多鞋迷的心头好。近日,旗下旗下大热鞋款旗下大热鞋款旗下大热鞋款一直以来在街头造型当中的能见度都算高,凭藉其舒适脚感与百搭外型旗下大热鞋款也轻松成为许多鞋迷的心头好。近日,旗下大热鞋款旗下大热鞋款旗下大热鞋款旗下大热鞋款旗下大热鞋款一直以来在街头造型当中的能见度都算高,凭藉其舒适脚感与百搭外型旗下大热鞋款也轻松成为许多鞋迷的心头好。近日,旗下大热鞋款旗下大热鞋款大热鞋款旗下大热鞋款 Air Max 95 一直以来在街头造型当中的能见度都算高,凭藉其舒适脚感与百搭外型 Air Max 95 也轻松成为许多鞋迷的心头好。'
intro: get(get(this.data, 'blockList', []).filter(block => block.templateKey === 'text'), '[0].contentData'),
labelList: this.data.labelList,
hasFavor: this.data.hasFavor,
hasPraise: this.data.hasPraise,
commentCount: this.data.commentCount,
praiseCount: this.data.praiseCount,
favoriteCount: this.data.favoriteCount
};
},
commentData() {
return [{
name: 'NIKE后援团',
content: '好期待,一定要抢一双👟!',
}, {
name: 'NIKE后援团',
content: '表情表情!!!这双仔细看好好看!!哈哈哈哈哈哈😄✌️'
}];
return {
comments: this.data.comments,
commentCount: this.data.commentCount,
publishTime: this.data.publishTime,
date: dayjs(this.data.publishTime).fromNow()
};
},
productListData() {
return [{
src: '//img12.static.yhbimg.com/goodsimg/2019/01/29/15/022a23864f68c66a6e1ef398ce7bd82efc.jpg?imageView2/2/w/640/h/640/q/60',
name: 'Off-White™ x Nike Air Max 90 全新「Desert Ore」配色曝光',
price: 3299,
isFav: false
}, {
src: '//img12.static.yhbimg.com/goodsimg/2019/01/29/15/022a23864f68c66a6e1ef398ce7bd82efc.jpg?imageView2/2/w/640/h/640/q/60',
name: 'Off-White™ x Nike Air Max 90 全新「Desert Ore」配色曝光',
price: 2299,
isFav: true
}, {
src: '//img12.static.yhbimg.com/goodsimg/2019/01/29/15/022a23864f68c66a6e1ef398ce7bd82efc.jpg?imageView2/2/w/640/h/640/q/60',
name: 'Off-White™ x Nike Air Max 90 全新「Desert Ore」配色曝光',
price: 1299,
isFav: false
}];
return this.data.productList || [];
},
lazy() {
return this.data.index > 1;
}
},
methods: {
... ...
... ... @@ -6,14 +6,13 @@
<span class="user-name">{{currentAuthor.authorName}}</span>
</template>
<template v-if="showHeader" v-slot:opts>
<button class="btn-follow hover-opacity" v-if="true">关注</button>
<button class="btn-follow followed hover-opacity" v-else>已关注</button>
<WidgetFollow :article-id="currentAuthor.authorUid" :followed="currentAuthor.hasAttention === 'Y'"></WidgetFollow>
</template>
</LayoutHeader>
<LayoutScroll v-if="isMounted" ref="scroll" @scroll="onScroll" :offset="1000" :on-fetch="onFetch">
<template class="article-item" v-slot:item="{ data }">
<ArticleItem :id="`item${data.id}`" :data="data" :data-id="data.id" @on-resize="onResize(data)" @on-resizeing="onResizeing(data)"></ArticleItem>
<div :id="`ph${data.id}`"></div>
<ArticleItem :id="`item${data.index}`" :data="data" :data-id="data.index" @on-resize="onResize(data)" @on-resizeing="onResizeing(data)"></ArticleItem>
<div :id="`ph${data.index}`"></div>
</template>
</LayoutScroll>
<slot name="thumb" v-else></slot>
... ... @@ -33,7 +32,9 @@ export default {
onFetch: Function
},
mounted() {
this.isMounted = true;
this.$nextTick(() => {
this.isMounted = true;
})
},
data() {
return {
... ... @@ -43,8 +44,10 @@ export default {
showHeader: false,
isMounted: false,
currentAuthor: {
authorUid: 0,
authorName: '',
authorHeadIco: '',
hasAttention: 'N',
opacity: 1,
isShare: false
}
... ... @@ -62,8 +65,10 @@ export default {
this.currentAuthor.opacity = 1;
this.showHeader = false;
} else {
this.currentAuthor.authorUid = item.data.authorUid;
this.currentAuthor.authorName = item.data.authorName;
this.currentAuthor.authorHeadIco = item.data.authorHeadIco;
this.currentAuthor.hasAttention = item.data.hasAttention;
this.showHeader = true;
const offsetTop = scrollTop - item.top;
... ... @@ -83,20 +88,20 @@ export default {
this.$refs.scroll.init();
},
onResize(data) {
const $phItem = document.getElementById(`ph${data.id}`);
const $phItem = document.getElementById(`ph${data.index}`);
$phItem.innerHTML = '';
$phItem.status = 0;
this.$refs.scroll.resize();
},
onResizeing(data) {
const $phItem = document.getElementById(`ph${data.id}`);
const $phItem = document.getElementById(`ph${data.index}`);
if ($phItem.status === 1) {
return;
}
const $nextItem = document.getElementById(`item${data.id + 1}`);
const $nextItem = document.getElementById(`item${data.index + 1}`);
const html = $nextItem.outerHTML;
$phItem.innerHTML = html;
... ...
... ... @@ -11,6 +11,9 @@ export default {
});
if (result && result.code === 200) {
if (!result.data.detailList) {
result.data.detailList = [];
}
result.data.detailList = result.data.detailList.map((item, inx) => {
item.index = (page - 1) * limit + inx;
return item;
... ...
... ... @@ -39,6 +39,7 @@
"connect-redis": "^3.4.0",
"cookie-parser": "^1.4.3",
"cube-ui": "^1.12.6",
"dayjs": "^1.8.5",
"express": "^4.16.4",
"express-session": "^1.15.6",
"fastclick": "^1.0.6",
... ...
... ... @@ -2057,6 +2057,10 @@ date-now@^0.1.4:
version "0.1.4"
resolved "http://npm.yohops.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
dayjs@^1.8.5:
version "1.8.5"
resolved "http://npm.yohops.com/dayjs/-/dayjs-1.8.5.tgz#0b066770f89a20022218544989f3d23e5e8db29a"
de-indent@^1.0.2:
version "1.0.2"
resolved "http://npm.yohops.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
... ...