Authored by htoooth

Merge remote-tracking branch 'origin/master'

# Conflicts:
#	apps/store/article/types.js
@@ -15,6 +15,7 @@ @@ -15,6 +15,7 @@
15 ], 15 ],
16 "rules": { 16 "rules": {
17 "camelcase": "off", 17 "camelcase": "off",
18 - "max-len": "off" 18 + "max-len": "off",
  19 + "new-cap": "off"
19 } 20 }
20 } 21 }
@@ -8,13 +8,17 @@ import titleMixin from './mixins/title'; @@ -8,13 +8,17 @@ import titleMixin from './mixins/title';
8 import pluginCore from './plugins/core'; 8 import pluginCore from './plugins/core';
9 import lazyload from 'vue-lazyload'; 9 import lazyload from 'vue-lazyload';
10 import reportError from 'report-error'; 10 import reportError from 'report-error';
  11 +import dayjs from 'dayjs';
  12 +import 'dayjs/locale/zh-cn';
  13 +import relativeTime from 'dayjs/plugin/relativeTime';
11 14
12 Vue.use(lazyload, { 15 Vue.use(lazyload, {
13 preLoad: 2 16 preLoad: 2
14 }); 17 });
15 Vue.use(pluginCore); 18 Vue.use(pluginCore);
16 Vue.mixin(titleMixin); 19 Vue.mixin(titleMixin);
17 - 20 +dayjs.locale('zh-cn');
  21 +dayjs.extend(relativeTime);
18 22
19 export function createApp(context) { 23 export function createApp(context) {
20 const router = createRouter(); 24 const router = createRouter();
@@ -3,6 +3,11 @@ @@ -3,6 +3,11 @@
3 <template v-slot:item="{ data }"> 3 <template v-slot:item="{ data }">
4 <slot name="item" :data="data"></slot> 4 <slot name="item" :data="data"></slot>
5 </template> 5 </template>
  6 + <template v-slot:noMore>
  7 + <p class="">
  8 + 没有更多了
  9 + </p>
  10 + </template>
6 </RecycleList> 11 </RecycleList>
7 </template> 12 </template>
8 13
@@ -42,18 +42,15 @@ @@ -42,18 +42,15 @@
42 </div> 42 </div>
43 <div 43 <div
44 v-if="!infinite" 44 v-if="!infinite"
45 - class="cube-recycle-list-loading"  
46 - :style="{visibility: loading ? 'visible' : 'hidden'}"  
47 - > 45 + class="cube-recycle-list-loading">
48 <slot name="spinner"> 46 <slot name="spinner">
49 - <div class="cube-recycle-list-loading-content"> 47 + <div class="cube-recycle-list-loading-content" v-show="!noMore" :style="{visibility: loading ? 'visible' : 'hidden'}">
50 <cube-loading class="spinner"></cube-loading> 48 <cube-loading class="spinner"></cube-loading>
51 </div> 49 </div>
52 </slot> 50 </slot>
53 - </div>  
54 -  
55 - <div v-show="noMore" class="cube-recycle-list-noMore">  
56 - <slot name="noMore" /> 51 + <div v-show="noMore" class="cube-recycle-list-noMore">
  52 + <slot name="noMore" />
  53 + </div>
57 </div> 54 </div>
58 </div> 55 </div>
59 <div class="cube-recycle-list-fake"></div> 56 <div class="cube-recycle-list-fake"></div>
@@ -153,7 +150,7 @@ export default { @@ -153,7 +150,7 @@ export default {
153 // increase capacity of items to display tombstone 150 // increase capacity of items to display tombstone
154 this.items.length += this.size; 151 this.items.length += this.size;
155 this.loadItems(); 152 this.loadItems();
156 - } else if (!this.loading) { 153 + } else if (!this.loading && !this.noMore) {
157 this.getItems(); 154 this.getItems();
158 } 155 }
159 }, 156 },
@@ -6,10 +6,10 @@ @@ -6,10 +6,10 @@
6 v-for="(product, inx) in data" 6 v-for="(product, inx) in data"
7 :key="inx"> 7 :key="inx">
8 <div class="product-content"> 8 <div class="product-content">
9 - <ImageFormat class="product-image" :src="product.src"></ImageFormat> 9 + <ImageFormat :lazy="lazy" class="product-image" :src="product.productImage" :width="136" :height="180"></ImageFormat>
10 <div class="product-info"> 10 <div class="product-info">
11 - <p class="product-name">{{product.name}}</p>  
12 - <p class="price">¥{{product.price}}</p> 11 + <p class="product-name">{{product.productName}}</p>
  12 + <p class="price">¥{{product.salesPrice}}</p>
13 </div> 13 </div>
14 </div> 14 </div>
15 <div class="btn-fav hover-opacity" v-if="!product.isFav">收藏</div> 15 <div class="btn-fav hover-opacity" v-if="!product.isFav">收藏</div>
@@ -29,6 +29,10 @@ export default { @@ -29,6 +29,10 @@ export default {
29 default() { 29 default() {
30 return []; 30 return [];
31 } 31 }
  32 + },
  33 + lazy: {
  34 + type: Boolean,
  35 + default: true
32 } 36 }
33 }, 37 },
34 computed: { 38 computed: {
@@ -94,6 +98,8 @@ export default { @@ -94,6 +98,8 @@ export default {
94 color: #9b9b9b; 98 color: #9b9b9b;
95 letter-spacing: -0.25PX; 99 letter-spacing: -0.25PX;
96 height: 104px; 100 height: 104px;
  101 + display: flex;
  102 + align-content: center;
97 } 103 }
98 104
99 .price { 105 .price {
@@ -5,6 +5,7 @@ import WidgetLike from './widget-like'; @@ -5,6 +5,7 @@ import WidgetLike from './widget-like';
5 import WidgetShare from './widget-share'; 5 import WidgetShare from './widget-share';
6 import WidgetTopic from './widget-topic'; 6 import WidgetTopic from './widget-topic';
7 import WidgetAvatar from './widget-avatar'; 7 import WidgetAvatar from './widget-avatar';
  8 +import WidgetFollow from './widget-follow';
8 9
9 export default [ 10 export default [
10 WidgetAvatarGroup, 11 WidgetAvatarGroup,
@@ -13,5 +14,6 @@ export default [ @@ -13,5 +14,6 @@ export default [
13 WidgetLike, 14 WidgetLike,
14 WidgetShare, 15 WidgetShare,
15 WidgetTopic, 16 WidgetTopic,
16 - WidgetAvatar 17 + WidgetAvatar,
  18 + WidgetFollow
17 ]; 19 ];
1 <template> 1 <template>
2 - <ImageFormat class="img-avatar" :src="src" :width="width" :height="height"></ImageFormat> 2 + <ImageFormat :lazy="lazy" class="img-avatar" :src="src" :width="width" :height="height"></ImageFormat>
3 </template> 3 </template>
4 4
5 <script> 5 <script>
@@ -17,6 +17,10 @@ export default { @@ -17,6 +17,10 @@ export default {
17 height: { 17 height: {
18 type: Number, 18 type: Number,
19 default: 35 19 default: 35
  20 + },
  21 + lazy: {
  22 + type: Boolean,
  23 + default: false
20 } 24 }
21 }, 25 },
22 computed: { 26 computed: {
@@ -6,7 +6,7 @@ @@ -6,7 +6,7 @@
6 export default { 6 export default {
7 name: 'WidgetFav', 7 name: 'WidgetFav',
8 props: { 8 props: {
9 - num: String, 9 + num: [String, Number],
10 option: Object 10 option: Object
11 } 11 }
12 }; 12 };
  1 +<template>
  2 + <button class="btn-follow hover-opacity" :class="followClass" @click="onFollow">{{followText}}</button>
  3 +</template>
  4 +
  5 +<script>
  6 +import {createNamespacedHelpers} from 'vuex';
  7 +const {mapActions} = createNamespacedHelpers('user');
  8 +
  9 +export default {
  10 + name: 'WidgetFollow',
  11 + props: {
  12 + authorUid: Number,
  13 + follow: Boolean
  14 + },
  15 + data() {
  16 + return {
  17 + loading: false,
  18 + followStatus: this.follow
  19 + };
  20 + },
  21 + watch: {
  22 + follow(val) {
  23 + this.followStatus = val;
  24 + }
  25 + },
  26 + computed: {
  27 + followClass() {
  28 + return {
  29 + loading: this.loading,
  30 + follow: this.followStatus
  31 + };
  32 + },
  33 + followText() {
  34 + return this.followStatus ? '已关注' : '关注';
  35 + }
  36 + },
  37 + methods: {
  38 + ...mapActions(['followUser']),
  39 + async onFollow() {
  40 + if (this.loading) {
  41 + return;
  42 + }
  43 + this.loading = true;
  44 + const result = await this.followUser({followUid: this.authorUid, status: this.followStatus ? 1 : 0});
  45 +
  46 + this.loading = false;
  47 + if (result.code === 200) {
  48 + this.followStatus = !this.followStatus;
  49 + this.$emit('on-follow', this.followStatus);
  50 + } else {
  51 + this.$createToast && this.$createToast({
  52 + txt: result.message || '服务器开小差了',
  53 + type: 'warn',
  54 + time: 1000
  55 + }).show();
  56 + }
  57 + }
  58 + }
  59 +};
  60 +</script>
  61 +
  62 +<style lang="scss" scoped>
  63 +.btn-follow {
  64 + width: 120px;
  65 + height: 50px;
  66 + padding: 0;
  67 + font-size: 26px;
  68 + border-radius: 3PX;
  69 + background-color: #222;
  70 + color: #fff;
  71 + display: flex;
  72 + align-items: center;
  73 + justify-content: center;
  74 +
  75 + &.follow {
  76 + border: solid 1px #4a4a4a;
  77 + background-color: #fff;
  78 + color: #000;
  79 + }
  80 +}
  81 +</style>
@@ -6,7 +6,7 @@ @@ -6,7 +6,7 @@
6 export default { 6 export default {
7 name: 'WidgetLike', 7 name: 'WidgetLike',
8 props: { 8 props: {
9 - num: String, 9 + num: [String, Number],
10 option: Object 10 option: Object
11 } 11 }
12 }; 12 };
@@ -6,7 +6,7 @@ @@ -6,7 +6,7 @@
6 export default { 6 export default {
7 name: 'WidgetShare', 7 name: 'WidgetShare',
8 props: { 8 props: {
9 - num: String, 9 + num: [String, Number],
10 option: Object 10 option: Object
11 } 11 }
12 }; 12 };
1 <template> 1 <template>
2 <Article :on-fetch="onFetch"> 2 <Article :on-fetch="onFetch">
3 <template v-slot:thumb> 3 <template v-slot:thumb>
4 - <ArticleItem v-for="data in articleCurrentList" :key="data.articleId" :data="data"></ArticleItem> 4 + <ArticleItem v-for="data in currentList" :key="data.articleId" :data="data"></ArticleItem>
5 </template> 5 </template>
6 </Article> 6 </Article>
7 </template> 7 </template>
8 8
9 <script> 9 <script>
  10 +import {get} from 'lodash';
10 import Article from './components/article/article'; 11 import Article from './components/article/article';
11 import ArticleItem from './components/article/article-item'; 12 import ArticleItem from './components/article/article-item';
12 import {createNamespacedHelpers} from 'vuex'; 13 import {createNamespacedHelpers} from 'vuex';
@@ -23,24 +24,39 @@ export default { @@ -23,24 +24,39 @@ export default {
23 return this.onFetch(); 24 return this.onFetch();
24 }, 25 },
25 computed: { 26 computed: {
26 - ...mapState(['articleCurrentList']) 27 + ...mapState(['articleList']),
  28 + currentList() {
  29 + if (this.articleList.length > 2) {
  30 + return this.articleList.slice(0, 2);
  31 + }
  32 + return this.articleList;
  33 + }
27 }, 34 },
28 methods: { 35 methods: {
29 ...mapActions(['fetchArticleList']), 36 ...mapActions(['fetchArticleList']),
30 async onFetch() { 37 async onFetch() {
31 - if (this.page === 1 && this.articleCurrentList.length) {  
32 - return this.articleCurrentList; 38 + if (this.page === 1 && this.articleList.length) {
  39 + this.page++;
  40 + return this.articleList;
  41 + }
  42 + const articleId = parseInt(this.$route.params.id, 10);
  43 +
  44 + if (!articleId) {
  45 + return;
33 } 46 }
34 const result = await this.fetchArticleList({ 47 const result = await this.fetchArticleList({
35 - articleId: this.$route.params.id, 48 + articleId: parseInt(this.$route.params.id, 10),
36 page: this.page 49 page: this.page
37 }); 50 });
38 51
39 if (result.code === 200) { 52 if (result.code === 200) {
40 - this.page++;  
41 - return Promise.resolve(result.data.detailList); 53 + if (get(result, 'data.detailList', []).length) {
  54 + this.page++;
  55 + return Promise.resolve(result.data.detailList);
  56 + }
  57 + return Promise.resolve(false);
42 } else { 58 } else {
43 - this.$createToast({ 59 + this.$createToast && this.$createToast({
44 txt: result.message || '服务器开小差了', 60 txt: result.message || '服务器开小差了',
45 type: 'warn', 61 type: 'warn',
46 time: 1000 62 time: 1000
1 <template> 1 <template>
2 <div class="article-item-comment"> 2 <div class="article-item-comment">
3 - <p class="comment-item" v-for="(comment, inx) in comments" :key="inx">  
4 - <span class="user-name">{{comment.name}}:</span> 3 + <p class="comment-item" v-for="(comment, inx) in data.comments" :key="inx">
  4 + <span class="user-name">{{comment.userName}}:</span>
5 <span class="comment-content">{{comment.content}}</span> 5 <span class="comment-content">{{comment.content}}</span>
6 </p> 6 </p>
7 <div class="comment"> 7 <div class="comment">
8 <div class="comment-input hover-opacity">添加回复:赞美是一种美德</div> 8 <div class="comment-input hover-opacity">添加回复:赞美是一种美德</div>
9 </div> 9 </div>
10 <div class="total-comment"> 10 <div class="total-comment">
11 - <div class="total hover-opacity">查看{{count}}条评论</div>  
12 - <div class="last-time">{{date}}</div> 11 + <div class="total hover-opacity">查看{{data.commentCount}}条评论</div>
  12 + <div class="last-time">{{data.date}}</div>
13 </div> 13 </div>
14 </div> 14 </div>
15 </template> 15 </template>
@@ -19,20 +19,12 @@ import {Input} from 'cube-ui'; @@ -19,20 +19,12 @@ import {Input} from 'cube-ui';
19 export default { 19 export default {
20 name: 'ArticleItemComment', 20 name: 'ArticleItemComment',
21 props: { 21 props: {
22 - comments: {  
23 - type: Array, 22 + data: {
  23 + type: Object,
24 default() { 24 default() {
25 - return []; 25 + return {};
26 } 26 }
27 }, 27 },
28 - count: {  
29 - type: Number,  
30 - default: 0  
31 - },  
32 - date: {  
33 - type: String,  
34 - default: 0  
35 - }  
36 }, 28 },
37 components: {CubeInput: Input} 29 components: {CubeInput: Input}
38 }; 30 };
1 <template> 1 <template>
2 <div class="article-item-header"> 2 <div class="article-item-header">
3 <div class="avatar"> 3 <div class="avatar">
4 - <WidgetAvatar class="widget-avatar" :src="data.authorHeadIco" :width="70" :height="70"></WidgetAvatar> 4 + <WidgetAvatar :lazy="lazy" class="widget-avatar" :src="data.authorHeadIco" :width="70" :height="70"></WidgetAvatar>
5 <span class="name">{{data.authorName}}</span> 5 <span class="name">{{data.authorName}}</span>
6 </div> 6 </div>
7 <div class="opts"> 7 <div class="opts">
8 - <button class="btn-follow hover-opacity" v-if="true">关注</button>  
9 - <button class="btn-follow followed hover-opacity" v-else>已关注</button> 8 + <WidgetFollow :author-uid="data.authorUid" :follow="data.hasAttention === 'Y'" @on-follow="onFollow"></WidgetFollow>
10 <i class="iconfont icon-more1" @click="onMore"></i> 9 <i class="iconfont icon-more1" @click="onMore"></i>
11 </div> 10 </div>
12 </div> 11 </div>
@@ -21,6 +20,10 @@ export default { @@ -21,6 +20,10 @@ export default {
21 default() { 20 default() {
22 return {}; 21 return {};
23 } 22 }
  23 + },
  24 + lazy: {
  25 + type: Boolean,
  26 + default: true
24 } 27 }
25 }, 28 },
26 methods: { 29 methods: {
@@ -37,6 +40,9 @@ export default { @@ -37,6 +40,9 @@ export default {
37 }).show(); 40 }).show();
38 } 41 }
39 }).show(); 42 }).show();
  43 + },
  44 + onFollow(follow) {
  45 + this.$emit('on-follow', follow);
40 } 46 }
41 } 47 }
42 }; 48 };
@@ -75,25 +81,6 @@ export default { @@ -75,25 +81,6 @@ export default {
75 align-items: center; 81 align-items: center;
76 justify-content: flex-end; 82 justify-content: flex-end;
77 83
78 - .btn-follow {  
79 - width: 120px;  
80 - height: 50px;  
81 - padding: 0;  
82 - font-size: 26px;  
83 - border-radius: 3PX;  
84 - background-color: #222;  
85 - color: #fff;  
86 - display: flex;  
87 - align-items: center;  
88 - justify-content: center;  
89 -  
90 - &.followed {  
91 - border: solid 1px #4a4a4a;  
92 - background-color: #fff;  
93 - color: #000;  
94 - }  
95 - }  
96 -  
97 .icon-more1 { 84 .icon-more1 {
98 font-size: 40px; 85 font-size: 40px;
99 margin-left: 30px; 86 margin-left: 30px;
@@ -2,22 +2,25 @@ @@ -2,22 +2,25 @@
2 <div class="article-item-intro"> 2 <div class="article-item-intro">
3 <div ref="intro" class="intro hover-opacity" :class="introClass" :style="introStyle" @click="onExpand"> 3 <div ref="intro" class="intro hover-opacity" :class="introClass" :style="introStyle" @click="onExpand">
4 {{intro}} 4 {{intro}}
5 - <span class="expand" v-if="!isExpand">…展开</span>  
6 - <span class="expand collapse" v-else>收起</span> 5 + <span class="expand" v-if="!isExpand && isEllipsis">…展开</span>
  6 + <span class="expand collapse" v-if="isExpand && isEllipsis">收起</span>
7 </div> 7 </div>
8 <div class="topics"> 8 <div class="topics">
9 - <WidgetTopic topic="种草1" @click.native="onTopic"></WidgetTopic>  
10 - <WidgetTopic topic="种草2" @click.native="onTopic"></WidgetTopic>  
11 - <WidgetTopic topic="种草3" @click.native="onTopic"></WidgetTopic> 9 + <WidgetTopic
  10 + :topic="label.labelName"
  11 + @click.native="onTopic(label)"
  12 + v-for="label in data.labelList"
  13 + :key="label.labelId">
  14 + </WidgetTopic>
12 </div> 15 </div>
13 <div class="widgets"> 16 <div class="widgets">
14 <div class="share"> 17 <div class="share">
15 <WidgetShare></WidgetShare> 18 <WidgetShare></WidgetShare>
16 </div> 19 </div>
17 <div class="opts"> 20 <div class="opts">
18 - <WidgetFav num="99"></WidgetFav>  
19 - <WidgetLike num="91"></WidgetLike>  
20 - <WidgetFav num="99"></WidgetFav> 21 + <WidgetFav :num="data.favoriteCount" :option="favoriteOption"></WidgetFav>
  22 + <WidgetLike :num="data.praiseCount" :option="praiseOption"></WidgetLike>
  23 + <WidgetFav :num="data.commentCount"></WidgetFav>
21 </div> 24 </div>
22 </div> 25 </div>
23 </div> 26 </div>
@@ -45,12 +48,16 @@ export default { @@ -45,12 +48,16 @@ export default {
45 introHeight: 0 48 introHeight: 0
46 }; 49 };
47 }, 50 },
  51 + created() {
  52 + if (this.data.intro.length < 66) {
  53 + this.isEllipsis = false;
  54 + }
  55 + },
48 computed: { 56 computed: {
49 intro() { 57 intro() {
50 - if (!this.isEllipsis) { 58 + if (this.isExpand || this.data.intro.length < 66) {
51 return this.data.intro; 59 return this.data.intro;
52 - }  
53 - if (this.data.intro.length > 66) { 60 + } else {
54 return this.data.intro.substring(0, 66); 61 return this.data.intro.substring(0, 66);
55 } 62 }
56 }, 63 },
@@ -63,7 +70,17 @@ export default { @@ -63,7 +70,17 @@ export default {
63 return { 70 return {
64 height: this.introHeight ? `${this.introHeight}px` : void 0 71 height: this.introHeight ? `${this.introHeight}px` : void 0
65 }; 72 };
66 - } 73 + },
  74 + favoriteOption() {
  75 + return {
  76 + selected: this.data.hasFavor === 'Y'
  77 + };
  78 + },
  79 + praiseOption() {
  80 + return {
  81 + selected: this.data.hasPraise === 'Y'
  82 + };
  83 + },
67 }, 84 },
68 mounted() { 85 mounted() {
69 this.introCollapseHeight = this.$refs.intro.scrollHeight; 86 this.introCollapseHeight = this.$refs.intro.scrollHeight;
@@ -79,8 +96,10 @@ export default { @@ -79,8 +96,10 @@ export default {
79 }); 96 });
80 }, 97 },
81 onExpand() { 98 onExpand() {
  99 + if (!this.isEllipsis) {
  100 + return;
  101 + }
82 this.isExpand = !this.isExpand; 102 this.isExpand = !this.isExpand;
83 - this.isEllipsis = false;  
84 this.$nextTick(() => { 103 this.$nextTick(() => {
85 if (this.isExpand) { 104 if (this.isExpand) {
86 this.introHeight = this.$refs.intro.scrollHeight; 105 this.introHeight = this.$refs.intro.scrollHeight;
@@ -89,9 +108,6 @@ export default { @@ -89,9 +108,6 @@ export default {
89 } 108 }
90 this.isExpanding = true; 109 this.isExpanding = true;
91 this.$emit('on-resizeing'); 110 this.$emit('on-resizeing');
92 - if (!this.isExpand) {  
93 - this.isEllipsis = true;  
94 - }  
95 this.resizeDebounce(); 111 this.resizeDebounce();
96 }); 112 });
97 }, 113 },
@@ -2,7 +2,7 @@ @@ -2,7 +2,7 @@
2 <div class="article-item-slide"> 2 <div class="article-item-slide">
3 <Slide :data="data.blockList" :threshold="0.2" :auto-play="false" :loop="false" :options="scrollOption" @change="onChange"> 3 <Slide :data="data.blockList" :threshold="0.2" :auto-play="false" :loop="false" :options="scrollOption" @change="onChange">
4 <SlideItem v-for="(item, inx) in data.blockList" :key="inx"> 4 <SlideItem v-for="(item, inx) in data.blockList" :key="inx">
5 - <ImageFormat :lazy="data.index > 0" class="image-slide-item" :src="item.contentData" :width="item.width" :height="item.height"></ImageFormat> 5 + <ImageFormat :lazy="lazy" class="image-slide-item" :src="item.contentData" :width="item.width" :height="item.height"></ImageFormat>
6 </SlideItem> 6 </SlideItem>
7 <template slot="dots" slot-scope="props"> 7 <template slot="dots" slot-scope="props">
8 <span class="slide-dot" 8 <span class="slide-dot"
@@ -29,6 +29,10 @@ export default { @@ -29,6 +29,10 @@ export default {
29 default() { 29 default() {
30 return {}; 30 return {};
31 } 31 }
  32 + },
  33 + lazy: {
  34 + type: Boolean,
  35 + default: true
32 } 36 }
33 }, 37 },
34 data() { 38 data() {
1 <template> 1 <template>
2 <div class="article-item"> 2 <div class="article-item">
3 - <ArticleItemHeader :data="headerData"></ArticleItemHeader>  
4 - <ArticleItemSlide :data="slideData"></ArticleItemSlide>  
5 - <ProductGroup :data="productListData"></ProductGroup> 3 + <ArticleItemHeader :data="headerData" :lazy="lazy" @on-follow="onFollow"></ArticleItemHeader>
  4 + <ArticleItemSlide :data="slideData" :lazy="lazy"></ArticleItemSlide>
  5 + <ProductGroup :data="productListData" :lazy="lazy"></ProductGroup>
6 <ArticleItemIntro :data="introData" @on-resize="onResize" @on-resizeing="onResizeing"></ArticleItemIntro> 6 <ArticleItemIntro :data="introData" @on-resize="onResize" @on-resizeing="onResizeing"></ArticleItemIntro>
7 - <ArticleItemComment :comments="commentData" :count="12" :date="'1天前'"></ArticleItemComment> 7 + <ArticleItemComment :data="commentData"></ArticleItemComment>
8 <div class="line"></div> 8 <div class="line"></div>
9 </div> 9 </div>
10 </template> 10 </template>
11 11
12 <script> 12 <script>
  13 +import {get} from 'lodash';
13 import ArticleItemHeader from './article-item-header'; 14 import ArticleItemHeader from './article-item-header';
14 import ArticleItemSlide from './article-item-slide'; 15 import ArticleItemSlide from './article-item-slide';
15 import ArticleItemIntro from './article-item-intro'; 16 import ArticleItemIntro from './article-item-intro';
16 import ArticleItemComment from './article-item-comment'; 17 import ArticleItemComment from './article-item-comment';
  18 +import dayjs from 'dayjs';
17 19
18 export default { 20 export default {
19 name: 'ArticleItem', 21 name: 'ArticleItem',
@@ -29,47 +31,40 @@ export default { @@ -29,47 +31,40 @@ export default {
29 headerData() { 31 headerData() {
30 return { 32 return {
31 authorName: this.data.authorName, 33 authorName: this.data.authorName,
32 - articleId: this.data.articleId,  
33 - authorHeadIco: this.data.authorHeadIco 34 + authorUid: this.data.authorUid,
  35 + authorHeadIco: this.data.authorHeadIco,
  36 + hasAttention: this.data.hasAttention,
34 }; 37 };
35 }, 38 },
36 slideData() { 39 slideData() {
37 return { 40 return {
38 - blockList: this.data.blockList,  
39 - index: this.data.index 41 + blockList: get(this.data, 'blockList', []).filter(block => block.templateKey === 'image'),
40 }; 42 };
41 }, 43 },
42 introData() { 44 introData() {
43 return { 45 return {
44 - intro: '旗下大热鞋款旗下大热鞋款旗下大热鞋款一直以来在街头造型当中的能见度都算高,凭藉其舒适脚感与百搭外型旗下大热鞋款也轻松成为许多鞋迷的心头好。近日,旗下旗下大热鞋款旗下大热鞋款旗下大热鞋款一直以来在街头造型当中的能见度都算高,凭藉其舒适脚感与百搭外型旗下大热鞋款也轻松成为许多鞋迷的心头好。近日,旗下大热鞋款旗下大热鞋款旗下大热鞋款旗下大热鞋款旗下大热鞋款一直以来在街头造型当中的能见度都算高,凭藉其舒适脚感与百搭外型旗下大热鞋款也轻松成为许多鞋迷的心头好。近日,旗下大热鞋款旗下大热鞋款大热鞋款旗下大热鞋款 Air Max 95 一直以来在街头造型当中的能见度都算高,凭藉其舒适脚感与百搭外型 Air Max 95 也轻松成为许多鞋迷的心头好。' 46 + intro: get(get(this.data, 'blockList', []).filter(block => block.templateKey === 'text'), '[0].contentData'),
  47 + labelList: this.data.labelList,
  48 + hasFavor: this.data.hasFavor,
  49 + hasPraise: this.data.hasPraise,
  50 + commentCount: this.data.commentCount,
  51 + praiseCount: this.data.praiseCount,
  52 + favoriteCount: this.data.favoriteCount
45 }; 53 };
46 }, 54 },
47 commentData() { 55 commentData() {
48 - return [{  
49 - name: 'NIKE后援团',  
50 - content: '好期待,一定要抢一双👟!',  
51 - }, {  
52 - name: 'NIKE后援团',  
53 - content: '表情表情!!!这双仔细看好好看!!哈哈哈哈哈哈😄✌️'  
54 - }]; 56 + return {
  57 + comments: this.data.comments,
  58 + commentCount: this.data.commentCount,
  59 + publishTime: this.data.publishTime,
  60 + date: dayjs(this.data.publishTime).fromNow()
  61 + };
55 }, 62 },
56 productListData() { 63 productListData() {
57 - return [{  
58 - src: '//img12.static.yhbimg.com/goodsimg/2019/01/29/15/022a23864f68c66a6e1ef398ce7bd82efc.jpg?imageView2/2/w/640/h/640/q/60',  
59 - name: 'Off-White™ x Nike Air Max 90 全新「Desert Ore」配色曝光',  
60 - price: 3299,  
61 - isFav: false  
62 - }, {  
63 - src: '//img12.static.yhbimg.com/goodsimg/2019/01/29/15/022a23864f68c66a6e1ef398ce7bd82efc.jpg?imageView2/2/w/640/h/640/q/60',  
64 - name: 'Off-White™ x Nike Air Max 90 全新「Desert Ore」配色曝光',  
65 - price: 2299,  
66 - isFav: true  
67 - }, {  
68 - src: '//img12.static.yhbimg.com/goodsimg/2019/01/29/15/022a23864f68c66a6e1ef398ce7bd82efc.jpg?imageView2/2/w/640/h/640/q/60',  
69 - name: 'Off-White™ x Nike Air Max 90 全新「Desert Ore」配色曝光',  
70 - price: 1299,  
71 - isFav: false  
72 - }]; 64 + return this.data.productList || [];
  65 + },
  66 + lazy() {
  67 + return this.data.index > 1;
73 } 68 }
74 }, 69 },
75 methods: { 70 methods: {
@@ -78,6 +73,9 @@ export default { @@ -78,6 +73,9 @@ export default {
78 }, 73 },
79 onResizeing() { 74 onResizeing() {
80 this.$emit('on-resizeing'); 75 this.$emit('on-resizeing');
  76 + },
  77 + onFollow(follow) {
  78 + this.$emit('on-follow', follow);
81 } 79 }
82 }, 80 },
83 components: {ArticleItemHeader, ArticleItemSlide, ArticleItemIntro, ArticleItemComment} 81 components: {ArticleItemHeader, ArticleItemSlide, ArticleItemIntro, ArticleItemComment}
@@ -6,14 +6,18 @@ @@ -6,14 +6,18 @@
6 <span class="user-name">{{currentAuthor.authorName}}</span> 6 <span class="user-name">{{currentAuthor.authorName}}</span>
7 </template> 7 </template>
8 <template v-if="showHeader" v-slot:opts> 8 <template v-if="showHeader" v-slot:opts>
9 - <button class="btn-follow hover-opacity" v-if="true">关注</button>  
10 - <button class="btn-follow followed hover-opacity" v-else>已关注</button> 9 + <WidgetFollow class="widget-follow" :author-uid="currentAuthor.authorUid" :follow="currentAuthor.hasAttention === 'Y'" @on-follow="follow => onFollow(currentAuthor, follow)"></WidgetFollow>
11 </template> 10 </template>
12 </LayoutHeader> 11 </LayoutHeader>
13 <LayoutScroll v-if="isMounted" ref="scroll" @scroll="onScroll" :offset="1000" :on-fetch="onFetch"> 12 <LayoutScroll v-if="isMounted" ref="scroll" @scroll="onScroll" :offset="1000" :on-fetch="onFetch">
14 <template class="article-item" v-slot:item="{ data }"> 13 <template class="article-item" v-slot:item="{ data }">
15 - <ArticleItem :id="`item${data.id}`" :data="data" :data-id="data.id" @on-resize="onResize(data)" @on-resizeing="onResizeing(data)"></ArticleItem>  
16 - <div :id="`ph${data.id}`"></div> 14 + <ArticleItem
  15 + :id="`item${data.index}`"
  16 + :data="data" :data-id="data.index"
  17 + @on-resize="onResize(data)"
  18 + @on-resizeing="onResizeing(data)"
  19 + @on-follow="follow => onFollow(data, follow)"></ArticleItem>
  20 + <div :id="`ph${data.index}`"></div>
17 </template> 21 </template>
18 </LayoutScroll> 22 </LayoutScroll>
19 <slot name="thumb" v-else></slot> 23 <slot name="thumb" v-else></slot>
@@ -22,6 +26,8 @@ @@ -22,6 +26,8 @@
22 26
23 <script> 27 <script>
24 import ArticleItem from './article-item'; 28 import ArticleItem from './article-item';
  29 +import {createNamespacedHelpers} from 'vuex';
  30 +const {mapMutations} = createNamespacedHelpers('article');
25 31
26 export default { 32 export default {
27 name: 'Article', 33 name: 'Article',
@@ -33,7 +39,9 @@ export default { @@ -33,7 +39,9 @@ export default {
33 onFetch: Function 39 onFetch: Function
34 }, 40 },
35 mounted() { 41 mounted() {
36 - this.isMounted = true; 42 + this.$nextTick(() => {
  43 + this.isMounted = true;
  44 + })
37 }, 45 },
38 data() { 46 data() {
39 return { 47 return {
@@ -43,8 +51,10 @@ export default { @@ -43,8 +51,10 @@ export default {
43 showHeader: false, 51 showHeader: false,
44 isMounted: false, 52 isMounted: false,
45 currentAuthor: { 53 currentAuthor: {
  54 + authorUid: 0,
46 authorName: '', 55 authorName: '',
47 authorHeadIco: '', 56 authorHeadIco: '',
  57 + hasAttention: 'N',
48 opacity: 1, 58 opacity: 1,
49 isShare: false 59 isShare: false
50 } 60 }
@@ -56,14 +66,17 @@ export default { @@ -56,14 +66,17 @@ export default {
56 } 66 }
57 }, 67 },
58 methods: { 68 methods: {
  69 + ...mapMutations(['CHANGE_AUTHOR_FOLLOW']),
59 onScroll({item, scrollTop}) { 70 onScroll({item, scrollTop}) {
60 this.scrollTop = scrollTop; 71 this.scrollTop = scrollTop;
61 if (scrollTop === 0) { 72 if (scrollTop === 0) {
62 this.currentAuthor.opacity = 1; 73 this.currentAuthor.opacity = 1;
63 this.showHeader = false; 74 this.showHeader = false;
64 } else { 75 } else {
  76 + this.currentAuthor.authorUid = item.data.authorUid;
65 this.currentAuthor.authorName = item.data.authorName; 77 this.currentAuthor.authorName = item.data.authorName;
66 this.currentAuthor.authorHeadIco = item.data.authorHeadIco; 78 this.currentAuthor.authorHeadIco = item.data.authorHeadIco;
  79 + this.currentAuthor.hasAttention = item.data.hasAttention;
67 this.showHeader = true; 80 this.showHeader = true;
68 const offsetTop = scrollTop - item.top; 81 const offsetTop = scrollTop - item.top;
69 82
@@ -83,26 +96,32 @@ export default { @@ -83,26 +96,32 @@ export default {
83 this.$refs.scroll.init(); 96 this.$refs.scroll.init();
84 }, 97 },
85 onResize(data) { 98 onResize(data) {
86 - const $phItem = document.getElementById(`ph${data.id}`); 99 + const $phItem = document.getElementById(`ph${data.index}`);
87 100
88 $phItem.innerHTML = ''; 101 $phItem.innerHTML = '';
89 $phItem.status = 0; 102 $phItem.status = 0;
90 this.$refs.scroll.resize(); 103 this.$refs.scroll.resize();
91 }, 104 },
92 onResizeing(data) { 105 onResizeing(data) {
93 - const $phItem = document.getElementById(`ph${data.id}`); 106 + const $phItem = document.getElementById(`ph${data.index}`);
94 107
95 if ($phItem.status === 1) { 108 if ($phItem.status === 1) {
96 return; 109 return;
97 } 110 }
98 111
99 - const $nextItem = document.getElementById(`item${data.id + 1}`); 112 + const $nextItem = document.getElementById(`item${data.index + 1}`);
100 const html = $nextItem.outerHTML; 113 const html = $nextItem.outerHTML;
101 114
102 $phItem.innerHTML = html; 115 $phItem.innerHTML = html;
103 $phItem.style.zIndex = 999; 116 $phItem.style.zIndex = 999;
104 $phItem.status = 1; 117 $phItem.status = 1;
105 }, 118 },
  119 + onFollow(data, follow) {
  120 + this.CHANGE_AUTHOR_FOLLOW({authorUid: data.authorUid, follow});
  121 + if (data.authorUid === this.currentAuthor.authorUid) {
  122 + this.currentAuthor.hasAttention = follow ? 'Y' : 'N';
  123 + }
  124 + }
106 }, 125 },
107 components: { 126 components: {
108 ArticleItem 127 ArticleItem
@@ -120,24 +139,8 @@ export default { @@ -120,24 +139,8 @@ export default {
120 height: 52px; 139 height: 52px;
121 } 140 }
122 141
123 -.btn-follow {  
124 - width: 120px;  
125 - height: 50px;  
126 - padding: 0;  
127 - font-size: 26px;  
128 - border-radius: 3PX;  
129 - background-color: #222;  
130 - color: #fff;  
131 - display: flex;  
132 - align-items: center;  
133 - justify-content: center; 142 +.widget-follow {
134 margin-right: 30px; 143 margin-right: 30px;
135 -  
136 - &.followed {  
137 - border: solid 1px #4a4a4a;  
138 - background-color: #fff;  
139 - color: #222;  
140 - }  
141 } 144 }
142 145
143 .user-name { 146 .user-name {
@@ -684,6 +684,10 @@ img[lazy=loaded] { @@ -684,6 +684,10 @@ img[lazy=loaded] {
684 &:active { 684 &:active {
685 opacity: 0.8; 685 opacity: 0.8;
686 } 686 }
  687 +
  688 + &.loading {
  689 + opacity: 0.8;
  690 + }
687 } 691 }
688 692
689 button { 693 button {
@@ -14,12 +14,12 @@ export default { @@ -14,12 +14,12 @@ export default {
14 }); 14 });
15 15
16 if (result && result.code === 200) { 16 if (result && result.code === 200) {
17 - result.data.detailList = result.data.detailList.map((item, inx) => {  
18 - item.index = (page - 1) * limit + inx;  
19 - return item;  
20 - }); 17 + if (!result.data.detailList) {
  18 + result.data.detailList = [];
  19 + }
21 commit(Types.FETCH_ARTICLE_DETAIL_SUCCESS, { 20 commit(Types.FETCH_ARTICLE_DETAIL_SUCCESS, {
22 data: result.data.detailList, 21 data: result.data.detailList,
  22 + page
23 }); 23 });
24 } else { 24 } else {
25 commit(Types.FETCH_ARTICLE_DETAIL_FAILD); 25 commit(Types.FETCH_ARTICLE_DETAIL_FAILD);
@@ -6,7 +6,7 @@ export default function() { @@ -6,7 +6,7 @@ export default function() {
6 namespaced: true, 6 namespaced: true,
7 state: { 7 state: {
8 fetchArticleList: false, 8 fetchArticleList: false,
9 - articleCurrentList: [], 9 + articleList: [],
10 articleDetail: null, 10 articleDetail: null,
11 }, 11 },
12 actions, 12 actions,
@@ -4,14 +4,28 @@ export default { @@ -4,14 +4,28 @@ export default {
4 [Types.FETCH_ARTICLE_DETAIL_REQUEST](state) { 4 [Types.FETCH_ARTICLE_DETAIL_REQUEST](state) {
5 state.fetchArticleList = true; 5 state.fetchArticleList = true;
6 }, 6 },
7 - [Types.FETCH_ARTICLE_DETAIL_SUCCESS](state, {data}) { 7 + [Types.FETCH_ARTICLE_DETAIL_SUCCESS](state, {data, page}) {
8 state.fetchArticleList = false; 8 state.fetchArticleList = false;
9 - state.articleCurrentList = data; 9 + if (page === 1) {
  10 + state.articleList = [];
  11 + }
  12 + state.articleList = state.articleList.concat(data);
  13 +
  14 + state.articleList.forEach((item, index) => {
  15 + item.index = index;
  16 + });
10 }, 17 },
11 [Types.FETCH_ARTICLE_DETAIL_FAILD](state) { 18 [Types.FETCH_ARTICLE_DETAIL_FAILD](state) {
12 state.fetchArticleList = false; 19 state.fetchArticleList = false;
13 }, 20 },
14 [Types.FETCH_GUANG_SUCCESS](state, data) { 21 [Types.FETCH_GUANG_SUCCESS](state, data) {
15 state.articleDetail = data; 22 state.articleDetail = data;
  23 + },
  24 + [Types.CHANGE_AUTHOR_FOLLOW](state, {authorUid, follow}) {
  25 + state.articleList.forEach(article => {
  26 + if (article.authorUid === authorUid) {
  27 + article.hasAttention = follow ? 'Y' : 'N';
  28 + }
  29 + });
16 } 30 }
17 }; 31 };
1 export const FETCH_ARTICLE_DETAIL_REQUEST = 'FETCH_ARTICLE_DETAIL_REQUEST'; 1 export const FETCH_ARTICLE_DETAIL_REQUEST = 'FETCH_ARTICLE_DETAIL_REQUEST';
2 export const FETCH_ARTICLE_DETAIL_FAILD = 'FETCH_ARTICLE_DETAIL_FAILD'; 2 export const FETCH_ARTICLE_DETAIL_FAILD = 'FETCH_ARTICLE_DETAIL_FAILD';
3 export const FETCH_ARTICLE_DETAIL_SUCCESS = 'FETCH_ARTICLE_DETAIL_SUCCESS'; 3 export const FETCH_ARTICLE_DETAIL_SUCCESS = 'FETCH_ARTICLE_DETAIL_SUCCESS';
  4 +
4 export const FETCH_GUANG_REQUEST = 'FETCH_GUANG_REQUEST'; 5 export const FETCH_GUANG_REQUEST = 'FETCH_GUANG_REQUEST';
5 export const FETCH_GUANG_FAILED = 'FETCH_GUANG_FAILED'; 6 export const FETCH_GUANG_FAILED = 'FETCH_GUANG_FAILED';
6 export const FETCH_GUANG_SUCCESS = 'FETCH_GUANG_SUCCESS'; 7 export const FETCH_GUANG_SUCCESS = 'FETCH_GUANG_SUCCESS';
7 export const FETCH_FAV_SUCCESS = 'FETCH_FAV_SUCCESS'; 8 export const FETCH_FAV_SUCCESS = 'FETCH_FAV_SUCCESS';
8 export const FETCH_ZAN_SUCCESS = 'FETCH_ZAN_SUCCESS'; 9 export const FETCH_ZAN_SUCCESS = 'FETCH_ZAN_SUCCESS';
  10 +
  11 +export const CHANGE_AUTHOR_FOLLOW = 'CHANGE_AUTHOR_FOLLOW';
@@ -3,6 +3,7 @@ import Vuex from 'vuex'; @@ -3,6 +3,7 @@ import Vuex from 'vuex';
3 import {createApi} from 'create-api'; 3 import {createApi} from 'create-api';
4 import storeYoho from './yoho'; 4 import storeYoho from './yoho';
5 import storeArticle from './article'; 5 import storeArticle from './article';
  6 +import storeUser from './user';
6 import plugin from './plugin'; 7 import plugin from './plugin';
7 8
8 Vue.use(Vuex); 9 Vue.use(Vuex);
@@ -12,7 +13,8 @@ export function createStore(context) { @@ -12,7 +13,8 @@ export function createStore(context) {
12 namespaced: true, 13 namespaced: true,
13 modules: { 14 modules: {
14 yoho: storeYoho(), 15 yoho: storeYoho(),
15 - article: storeArticle() 16 + article: storeArticle(),
  17 + user: storeUser()
16 }, 18 },
17 strict: process.env.NODE_ENV !== 'production', 19 strict: process.env.NODE_ENV !== 'production',
18 plugins: [plugin] 20 plugins: [plugin]
  1 +export default {
  2 + async followUser(actions, {followUid, status}) {
  3 + const result = await this.$api.get('/api/grass/updateAttention', {
  4 + followUid,
  5 + status,
  6 + attentionType: 1
  7 + });
  8 +
  9 + return result;
  10 + },
  11 + async followTopic(actions, {topicId, status}) {
  12 + const result = await this.$api.get('/api/grass/updateAttention', {
  13 + topicId,
  14 + status,
  15 + attentionType: 0
  16 + });
  17 +
  18 + return result;
  19 + },
  20 +};
  1 +import actions from './actions';
  2 +import mutations from './mutations';
  3 +
  4 +export default function() {
  5 + return {
  6 + namespaced: true,
  7 + state: {
  8 + },
  9 + actions,
  10 + mutations
  11 + };
  12 +}
  1 +import * as Types from './types';
  2 +
  3 +export default {
  4 +};
  1 +export const FETCH_ARTICLE_DETAIL_REQUEST = 'FETCH_ARTICLE_DETAIL_REQUEST';
  2 +export const FETCH_ARTICLE_DETAIL_FAILD = 'FETCH_ARTICLE_DETAIL_FAILD';
  3 +export const FETCH_ARTICLE_DETAIL_SUCCESS = 'FETCH_ARTICLE_DETAIL_SUCCESS';
@@ -9,11 +9,19 @@ module.exports = { @@ -9,11 +9,19 @@ module.exports = {
9 params: { 9 params: {
10 page: {type: Number, require: false}, 10 page: {type: Number, require: false},
11 limit: {type: Number, require: false}, 11 limit: {type: Number, require: false},
12 - uid: {type: Number, require: false},  
13 articleId: {type: Number}, 12 articleId: {type: Number},
14 columnType: {type: Number} 13 columnType: {type: Number}
15 } 14 }
16 }, 15 },
  16 + '/api/grass/updateAttention': {
  17 + api: 'app.grass.updateAttention',
  18 + params: {
  19 + topicId: {type: Number, require: false},
  20 + followUid: {type: Number, require: false},
  21 + status: {type: Number},
  22 + attentionType: {type: Number}
  23 + }
  24 + },
17 '/api/guang/article/detail': { 25 '/api/guang/article/detail': {
18 service: true, 26 service: true,
19 api: URI_PACKAGE_ARTICLE, 27 api: URI_PACKAGE_ARTICLE,
@@ -39,6 +39,7 @@ @@ -39,6 +39,7 @@
39 "connect-redis": "^3.4.0", 39 "connect-redis": "^3.4.0",
40 "cookie-parser": "^1.4.3", 40 "cookie-parser": "^1.4.3",
41 "cube-ui": "^1.12.6", 41 "cube-ui": "^1.12.6",
  42 + "dayjs": "^1.8.5",
42 "express": "^4.16.4", 43 "express": "^4.16.4",
43 "express-session": "^1.15.6", 44 "express-session": "^1.15.6",
44 "fastclick": "^1.0.6", 45 "fastclick": "^1.0.6",
@@ -2057,6 +2057,10 @@ date-now@^0.1.4: @@ -2057,6 +2057,10 @@ date-now@^0.1.4:
2057 version "0.1.4" 2057 version "0.1.4"
2058 resolved "http://npm.yohops.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" 2058 resolved "http://npm.yohops.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
2059 2059
  2060 +dayjs@^1.8.5:
  2061 + version "1.8.5"
  2062 + resolved "http://npm.yohops.com/dayjs/-/dayjs-1.8.5.tgz#0b066770f89a20022218544989f3d23e5e8db29a"
  2063 +
2060 de-indent@^1.0.2: 2064 de-indent@^1.0.2:
2061 version "1.0.2" 2065 version "1.0.2"
2062 resolved "http://npm.yohops.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d" 2066 resolved "http://npm.yohops.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"