Authored by 陈轩

Merge remote-tracking branch 'origin/develop' into develop

  1 +/**
  2 + * 微信分享签名
  3 + * Bi Kai <kai.bi@yoho.cn>
  4 + */
  5 +'use strict';
  6 +const wechatModel = require('../models/wechat');
  7 +
  8 +exports.shareToken = (req, res, next) => {
  9 + wechatModel.calcSignature({
  10 + url: req.query.url || 'http://www.yohoblk.com/'
  11 + }).then((result) => {
  12 + res.jsonp(result);
  13 + }).catch(next);
  14 +};
  1 +'use strict';
  2 +
  3 +/*
  4 + * 生成微信分享所需的签名
  5 + * bikai <kai.bi@yoho.cn>
  6 + * 2016.6.15
  7 + */
  8 +const request = require('request-promise');
  9 +const Promise = require('bluebird');
  10 +const crypto = require('crypto');
  11 +const logger = global.yoho.logger;
  12 +const cache = global.yoho.cache;
  13 +
  14 +// 此处请勿使用有货公众号的 appId, 此处使用的是 女生志 的appId
  15 +const appId = 'wxb52ec6a352f0b090';
  16 +const secret = '9fe6bedb0b7f30986a168c7fc44f34c0';
  17 +
  18 +const sha1 = (str) => {
  19 + const generator = crypto.createHash('sha1');
  20 +
  21 + generator.update(str);
  22 + return generator.digest('hex');
  23 +};
  24 +
  25 +const accessTokenCacheKey = 'wechatShare:accessToken';
  26 +const ticketCacheKey = 'wechatShare:ticket';
  27 +
  28 +// 微信 JS 接口签名校验工具 http://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=jsapisign
  29 +
  30 +let _getAccessToken = Promise.coroutine(function* () {
  31 + let accessToken = yield cache.get(accessTokenCacheKey);
  32 +
  33 + if (accessToken) {
  34 + return accessToken;
  35 + }
  36 +
  37 + logger.info('get accessToken from wechat API');
  38 + return request({
  39 + url: 'https://api.weixin.qq.com/cgi-bin/token',
  40 + qs: {
  41 + grant_type: 'client_credential',
  42 + appid: appId,
  43 + secret: secret
  44 + },
  45 + json: true
  46 + }).then((res) => {
  47 +
  48 + // accessToken 有效期 7200s,缓存 7100s
  49 + cache.set(accessTokenCacheKey, res.access_token, 7100).catch((err) => {
  50 + logger.error('set wechat accessToken cache error', JSON.stringify(err));
  51 + });
  52 + return res.access_token;
  53 + }).catch((err) => {
  54 + logger.error('get accessToken from wechat API error', JSON.stringify(err));
  55 + });
  56 +});
  57 +
  58 +let _getTicket = Promise.coroutine(function* () {
  59 + let ticket = yield cache.get(ticketCacheKey);
  60 +
  61 + if (ticket) {
  62 + return ticket;
  63 + }
  64 +
  65 + logger.info('get ticket from wechat API');
  66 + return request({
  67 + url: 'https://api.weixin.qq.com/cgi-bin/ticket/getticket',
  68 + qs: {
  69 + access_token: yield _getAccessToken(),
  70 + type: 'jsapi'
  71 + },
  72 + json: true
  73 + }).then(res => {
  74 +
  75 + // ticket 有效期 7200s,缓存 7100s
  76 + cache.set(ticketCacheKey, res.ticket, 7100).catch((err) => {
  77 + logger.error('set wechat Token cache error', JSON.stringify(err));
  78 + });
  79 + return res.ticket;
  80 + }).catch((err) => {
  81 + logger.error('get ticket from wechat API error', JSON.stringify(err));
  82 + });
  83 +});
  84 +
  85 +let calcSignature = Promise.coroutine(function* (data) {
  86 + data = Object.assign({
  87 + nonceStr: Math.random().toString(36).substr(2, 15),
  88 + timestamp: Math.floor(Date.now() / 1000) + '',
  89 + ticket: yield _getTicket(),
  90 + appId: appId
  91 + }, data);
  92 +
  93 + const str = `jsapi_ticket=${data.ticket}&noncestr=${data.nonceStr}&timestamp=${data.timestamp}&url=${data.url}`;
  94 +
  95 + data.signature = sha1(str);
  96 + return data;
  97 +});
  98 +
  99 +// 测试
  100 +// calcSignature({
  101 +// url: 'http://www.yohobuy.com/'
  102 +// }).then(console.log);
  103 +
  104 +module.exports = {
  105 + calcSignature
  106 +};
@@ -11,8 +11,11 @@ const router = require('express').Router(); // eslint-disable-line @@ -11,8 +11,11 @@ const router = require('express').Router(); // eslint-disable-line
11 var multipart = require('connect-multiparty'); 11 var multipart = require('connect-multiparty');
12 var multipartMiddleware = multipart(); 12 var multipartMiddleware = multipart();
13 13
14 -const uploadApi = require('./upload/upload.js'); 14 +const uploadApi = require('./controllers/upload.js');
  15 +const wechat = require('./controllers/wechat.js');
15 16
16 router.post('/upload/image', multipartMiddleware, uploadApi.uploadImg); 17 router.post('/upload/image', multipartMiddleware, uploadApi.uploadImg);
17 18
  19 +router.get('/wechat/share/token', wechat.shareToken);
  20 +
18 module.exports = router; 21 module.exports = router;
@@ -101,11 +101,11 @@ const getCateListData = params => { @@ -101,11 +101,11 @@ const getCateListData = params => {
101 sub: [] 101 sub: []
102 }; 102 };
103 if (_.isEmpty(cate.sub)) { 103 if (_.isEmpty(cate.sub)) {
104 - item.url = helpers.urlFormat('/', { 104 + item.url = helpers.urlFormat('/product/list', {
105 sort: item.sort, 105 sort: item.sort,
106 sort_name: item.name, 106 sort_name: item.name,
107 gender: genderArr[categorykey] 107 gender: genderArr[categorykey]
108 - }, 'list'); 108 + });
109 oneClass.ca.push(item); 109 oneClass.ca.push(item);
110 return true;// equal continue; 110 return true;// equal continue;
111 } 111 }
@@ -115,11 +115,11 @@ const getCateListData = params => { @@ -115,11 +115,11 @@ const getCateListData = params => {
115 id: item.id, 115 id: item.id,
116 name: '全部' + item.name, 116 name: '全部' + item.name,
117 sort: item.sort, 117 sort: item.sort,
118 - url: helpers.urlFormat('/', { 118 + url: helpers.urlFormat('/product/list', {
119 sort: item.sort, 119 sort: item.sort,
120 sort_name: item.name, 120 sort_name: item.name,
121 gender: genderArr[categorykey] 121 gender: genderArr[categorykey]
122 - }, 'list'), 122 + }),
123 sub: [] 123 sub: []
124 }); 124 });
125 125
@@ -130,11 +130,11 @@ const getCateListData = params => { @@ -130,11 +130,11 @@ const getCateListData = params => {
130 sort: sub.relation_parameter.sort, 130 sort: sub.relation_parameter.sort,
131 url: '' 131 url: ''
132 }; 132 };
133 - subitem.url = helpers.urlFormat('/', { 133 + subitem.url = helpers.urlFormat('/product/list', {
134 sort: subitem.sort, 134 sort: subitem.sort,
135 sort_name: subitem.name, 135 sort_name: subitem.name,
136 gender: genderArr[categorykey] 136 gender: genderArr[categorykey]
137 - }, 'list'); 137 + });
138 item.sub.push(subitem); 138 item.sub.push(subitem);
139 }); 139 });
140 140
@@ -6,11 +6,11 @@ @@ -6,11 +6,11 @@
6 */ 6 */
7 7
8 module.exports = app => { 8 module.exports = app => {
9 - app.use('/', require('./apps/channel'));  
10 - app.use('/api', require('./apps/api'));  
11 - app.use('/product', require('./apps/product'));  
12 - app.use('/home', require('./apps/home'));  
13 - app.use('/news', require('./apps/news')); 9 + app.use('/', require('./apps/channel')); // 一级频道模块
  10 + app.use('/api', require('./apps/api')); // 各模块公有 API
  11 + app.use('/product', require('./apps/product')); // 商品模块
  12 + app.use('/home', require('./apps/home')); // 个人中心
  13 + app.use('/news', require('./apps/news')); // 资讯
14 14
15 // 组件示例 15 // 组件示例
16 if (!app.locals.proEnv) { 16 if (!app.locals.proEnv) {
@@ -19,7 +19,7 @@ const titleMap = { @@ -19,7 +19,7 @@ const titleMap = {
19 action: '' 19 action: ''
20 }, 20 },
21 title: { 21 title: {
22 - des: '标题1', 22 + des: 'BLK',
23 action: '' 23 action: ''
24 } 24 }
25 }, 25 },
@@ -29,11 +29,11 @@ const titleMap = { @@ -29,11 +29,11 @@ const titleMap = {
29 action: '' 29 action: ''
30 }, 30 },
31 title: { 31 title: {
32 - des: '标题2', 32 + des: 'BLK',
33 action: '' 33 action: ''
34 }, 34 },
35 right: { 35 right: {
36 - action: '' 36 + action: origin + '/home'
37 } 37 }
38 }, 38 },
39 3: { 39 3: {
@@ -93,19 +93,13 @@ const matchHeader = (url) => { @@ -93,19 +93,13 @@ const matchHeader = (url) => {
93 headerid: '-1' // 默认不显示头部 93 headerid: '-1' // 默认不显示头部
94 }; 94 };
95 95
96 - if (/\/product\/new/.test(url)) {  
97 - header = titleMap[1];  
98 -  
99 - // header.xxx = '111';// 匹配到头类型以后,可修改里边的内容  
100 - return header;  
101 - }  
102 -  
103 if (/\/brands/.test(url) || /\/cate/.test(url)) { 96 if (/\/brands/.test(url) || /\/cate/.test(url)) {
104 - header = titleMap[4]; 97 + header = titleMap[2];
  98 + alert(JSON.stringify(header));
105 return header; 99 return header;
106 } 100 }
107 101
108 - if (/\/home\/mydetails$/.test(url)) { 102 + if (/\/home\/mydetails/.test(url)) {
109 header = titleMap[1]; 103 header = titleMap[1];
110 header.title.des = '个人信息'; 104 header.title.des = '个人信息';
111 return header; 105 return header;
@@ -148,15 +142,15 @@ const matchHeader = (url) => { @@ -148,15 +142,15 @@ const matchHeader = (url) => {
148 header.ltitle = { 142 header.ltitle = {
149 des: '商品', 143 des: '商品',
150 action: origin + '/home/favorite' 144 action: origin + '/home/favorite'
151 - } 145 + };
152 header.rtitle = { 146 header.rtitle = {
153 des: '品牌', 147 des: '品牌',
154 action: origin + '/home/favorite?tab=brand' 148 action: origin + '/home/favorite?tab=brand'
155 - } 149 + };
156 header.right = { 150 header.right = {
157 des: '编辑', 151 des: '编辑',
158 action: 'editModel' 152 action: 'editModel'
159 - } 153 + };
160 return header; 154 return header;
161 } 155 }
162 156
@@ -184,7 +178,7 @@ const matchHeader = (url) => { @@ -184,7 +178,7 @@ const matchHeader = (url) => {
184 header.right = { 178 header.right = {
185 des: '提交', 179 des: '提交',
186 action: 'saveFeedback' 180 action: 'saveFeedback'
187 - } 181 + };
188 return header; 182 return header;
189 } 183 }
190 184
@@ -200,13 +194,20 @@ const matchHeader = (url) => { @@ -200,13 +194,20 @@ const matchHeader = (url) => {
200 194
201 module.exports = (url) => { 195 module.exports = (url) => {
202 if (yoho.isApp) { 196 if (yoho.isApp) {
203 - let data = { 197 + // 品牌 品类
  198 + // if (/\/brands/.test(url) || /\/cate/.test(url)) {
  199 + // return yoho.goTab({index: 1});
  200 + // }
  201 +
  202 + // 个人中心首页
  203 + if (/\/home$/.test(url)) {
  204 + return yoho.goTab({index: 4});
  205 + }
  206 +
  207 + yoho.goNewPage({
204 header: matchHeader(url), 208 header: matchHeader(url),
205 url: /^(https?:)?\/\//i.test(url) ? url : origin + url 209 url: /^(https?:)?\/\//i.test(url) ? url : origin + url
206 - };  
207 -  
208 - // console.log(data);  
209 - yoho.goNewPage(data); 210 + });
210 } else { 211 } else {
211 location.href = url; 212 location.href = url;
212 } 213 }
@@ -18,9 +18,11 @@ const $ = require('yoho-jquery'); @@ -18,9 +18,11 @@ const $ = require('yoho-jquery');
18 const interceptClick = require('common/intercept-click'); 18 const interceptClick = require('common/intercept-click');
19 19
20 $(() => { 20 $(() => {
21 - $('body').on('click', 'a[href]:not(".no-intercept")', function() { 21 + $('body').on('click', 'a[href]', function() {
22 // 拦截跳转 22 // 拦截跳转
23 - interceptClick($(this).attr('href'));  
24 - return false; 23 + if (!$(this).hasClass('no-intercept')) {
  24 + interceptClick($(this).attr('href'));
  25 + return false;
  26 + }
25 }); 27 });
26 }); 28 });
@@ -34,7 +34,7 @@ const yoho = { @@ -34,7 +34,7 @@ const yoho = {
34 * @param success 调用成功的回调方法 34 * @param success 调用成功的回调方法
35 * @param fail 调用失败的回调方法 35 * @param fail 调用失败的回调方法
36 */ 36 */
37 - goTap(args, success, fail) { 37 + goTab(args, success, fail) {
38 if (this.isApp) { 38 if (this.isApp) {
39 window.yohoInterface.triggerEvent(success || nullFun, fail || nullFun, { 39 window.yohoInterface.triggerEvent(success || nullFun, fail || nullFun, {
40 method: 'go.tab', 40 method: 'go.tab',
1 <template> 1 <template>
2 - <div class="cate-tab-fixed">  
3 - <tab v-bind:page="page"></tab>  
4 - </div>  
5 <div class="cate-page" id='cate-page'> 2 <div class="cate-page" id='cate-page'>
6 <div class="cate-nav clearfix"> 3 <div class="cate-nav clearfix">
7 <ul> 4 <ul>
@@ -29,12 +26,6 @@ @@ -29,12 +26,6 @@
29 </div> 26 </div>
30 </template> 27 </template>
31 <style> 28 <style>
32 -.cate-tab-fixed {  
33 - position: fixed;  
34 - top: 0;  
35 - left: 0;  
36 -}  
37 -  
38 .cate-page { 29 .cate-page {
39 font-size: 36px; 30 font-size: 36px;
40 font-family: helvetica, Arial, "黑体"; 31 font-family: helvetica, Arial, "黑体";
1 <template> 1 <template>
2 <tab v-bind:page="page"></tab> 2 <tab v-bind:page="page"></tab>
  3 + <brand-search></brand-search>
3 <resources v-bind:content-code.sync="contentCode"></resources> 4 <resources v-bind:content-code.sync="contentCode"></resources>
4 <brand-list v-bind:channel="channel"></brand-list> 5 <brand-list v-bind:channel="channel"></brand-list>
5 </template> 6 </template>
@@ -10,6 +11,7 @@ @@ -10,6 +11,7 @@
10 const tab = require('channel/tab.vue'); 11 const tab = require('channel/tab.vue');
11 const resources = require('component/resources/index.vue'); 12 const resources = require('component/resources/index.vue');
12 const brandList = require('channel/brand-list.vue'); 13 const brandList = require('channel/brand-list.vue');
  14 + const brandSearch = require('channel/brand-search.vue');
13 15
14 16
15 module.exports = { 17 module.exports = {
@@ -21,6 +23,7 @@ @@ -21,6 +23,7 @@
21 }, 23 },
22 components: { 24 components: {
23 tab, 25 tab,
  26 + brandSearch,
24 resources, 27 resources,
25 brandList 28 brandList
26 } 29 }
  1 +<template>
  2 + <div class="search">
  3 + <input v-if="showInput" type="text" name="">
  4 + <div v-else class="input" @click="changeToInput()">
  5 + <span class="icon icon-search"></span> Search
  6 + </div>
  7 + </div>
  8 +</template>
  9 +<style>
  10 + .search {
  11 + width: 100%;
  12 + height: 85px;
  13 + padding: 15px 0;
  14 + background-color: #f6f6f6;
  15 + text-align: center;
  16 +
  17 + .input {
  18 + margin-left: auto;
  19 + margin-right: auto;
  20 + background-color: #fff;
  21 + text-align: center;
  22 + color: #b0b0b0;
  23 + height: 55px;
  24 + width: 92%;
  25 + font-size: 28px;
  26 + padding: 5px 0;
  27 + }
  28 +
  29 + input {
  30 + width: 92%;
  31 + height: 55px;
  32 + padding: 10px;
  33 + }
  34 + }
  35 +</style>
  36 +<script>
  37 + module.exports = {
  38 + data() {
  39 + return {
  40 + showInput: false
  41 + };
  42 + },
  43 + methods: {
  44 + changeToInput() {
  45 + this.showInput = true;
  46 + }
  47 + }
  48 + };
  49 +</script>
1 <template> 1 <template>
2 - <tab v-bind:page="page"></tab>  
3 - <resources v-bind:content-code.sync="contentCode"></resources> 2 + <div class="tab-top-fixed">
  3 + <tab v-bind:page="page"></tab>
  4 + </div>
  5 + <div class="resources">
  6 + <resources v-bind:content-code.sync="contentCode"></resources>
  7 + </div>
4 </template> 8 </template>
  9 +<style>
  10 + .tab-top-fixed {
  11 + position: fixed;
  12 + top: 0;
  13 + left: 0;
  14 + width: 100%;
  15 + z-index: 99;
  16 + }
  17 +
  18 + .resources {
  19 + margin-top: 100px;
  20 + }
  21 +</style>
5 <script> 22 <script>
6 const contentCode = require('content-code'); 23 const contentCode = require('content-code');
7 const qs = require('yoho-qs'); 24 const qs = require('yoho-qs');
@@ -33,8 +33,13 @@ @@ -33,8 +33,13 @@
33 <style> 33 <style>
34 @import "../../scss/common/color"; 34 @import "../../scss/common/color";
35 35
  36 + html {
  37 + font-size: 60px !important;
  38 + }
  39 +
36 .sidebar { 40 .sidebar {
37 width: 100%; 41 width: 100%;
  42 + margin-top: 40px;
38 background: $white; 43 background: $white;
39 overflow-x: hidden; 44 overflow-x: hidden;
40 45
@@ -61,9 +61,9 @@ @@ -61,9 +61,9 @@
61 methods: { 61 methods: {
62 showcase: function() { 62 showcase: function() {
63 const opts = { 63 const opts = {
64 - images: this.goods.map((item)=> { 64 + images: this.goods.map((item) => {
65 return item.colorImage; 65 return item.colorImage;
66 - }), 66 + }).filter(image => image),
67 index: this.$refs.swipe.index 67 index: this.$refs.swipe.index
68 }; 68 };
69 69
@@ -168,7 +168,7 @@ @@ -168,7 +168,7 @@
168 168
169 </show-box> 169 </show-box>
170 170
171 - <div class="control-box"> 171 + <div class="control-box" v-if="isApp">
172 <button class="button control-button"> 172 <button class="button control-button">
173 <span @click="yoho.goShopingCart()" style="position: relative;"> 173 <span @click="yoho.goShopingCart()" style="position: relative;">
174 <i class="icon icon-bag"></i> 174 <i class="icon icon-bag"></i>
@@ -191,6 +191,10 @@ @@ -191,6 +191,10 @@
191 </button> 191 </button>
192 </div> 192 </div>
193 193
  194 + <div v-if="!isApp">
  195 + <share-bottom></share-bottom>
  196 + </div>
  197 +
194 <feature-selector :is-visible="showFeatureSelector" :entity="entity" 198 <feature-selector :is-visible="showFeatureSelector" :entity="entity"
195 :on-add-to-cart="onAddToCart"></feature-selector> 199 :on-add-to-cart="onAddToCart"></feature-selector>
196 </template> 200 </template>
@@ -293,7 +297,7 @@ @@ -293,7 +297,7 @@
293 justify-content: space-around; 297 justify-content: space-around;
294 align-items: stretch; 298 align-items: stretch;
295 position: fixed; 299 position: fixed;
296 - width: 100%; 300 + width: 750px;
297 height: 99px; 301 height: 99px;
298 bottom: 0; 302 bottom: 0;
299 303
@@ -453,6 +457,7 @@ @@ -453,6 +457,7 @@
453 featureSelector: require('component/product/feature-selector.vue'), 457 featureSelector: require('component/product/feature-selector.vue'),
454 showBox: require('./show-box.vue'), 458 showBox: require('./show-box.vue'),
455 topNav: require('./top-nav.vue'), 459 topNav: require('./top-nav.vue'),
  460 + shareBottom: require('component/tool/share-bottom.vue'),
456 }, 461 },
457 methods: { 462 methods: {
458 /** 463 /**
@@ -476,7 +481,11 @@ @@ -476,7 +481,11 @@
476 this.entity.isCollect = 'N'; 481 this.entity.isCollect = 'N';
477 } else if (result.code === 403) { 482 } else if (result.code === 403) {
478 // 未登陆 483 // 未登陆
479 - yoho.goLogin(); 484 + yoho.goLogin('', function() {
  485 +
  486 + }, function() {
  487 +
  488 + });
480 } 489 }
481 }); 490 });
482 } else { 491 } else {
@@ -13,9 +13,10 @@ @@ -13,9 +13,10 @@
13 .top-nav { 13 .top-nav {
14 position: fixed; 14 position: fixed;
15 z-index: 10; 15 z-index: 10;
16 - font-size: 40px; 16 + font-size: 50px;
17 padding: 30px; 17 padding: 30px;
18 width: 100%; 18 width: 100%;
  19 + top: 40px;
19 20
20 .left { 21 .left {
21 float: left; 22 float: left;
1 <template> 1 <template>
2 <div class="brand-share"> 2 <div class="brand-share">
3 - <div class="brand-top-box" v-bind:style="{ 'background-image': `url(${shopInfo.brandBg})` }"></div> 3 + <img class="brand-top-box" v-bind:src="shopInfo.brandBg | resize 750 478">
4 <div class="brand-title">{{ shopInfo.brandName }}</div> 4 <div class="brand-title">{{ shopInfo.brandName }}</div>
5 <div class="brand-intro">{{ shopInfo.brandIntro }}</div> 5 <div class="brand-intro">{{ shopInfo.brandIntro }}</div>
6 <div class="tip">进入 BLK 选购潮品</div> 6 <div class="tip">进入 BLK 选购潮品</div>
@@ -13,10 +13,6 @@ @@ -13,10 +13,6 @@
13 .brand-share { 13 .brand-share {
14 .brand-top-box { 14 .brand-top-box {
15 width: 100%; 15 width: 100%;
16 - height: 468px;  
17 - color: #fff;  
18 - background-color: #ccc;  
19 - position: relative;  
20 } 16 }
21 17
22 .brand-title { 18 .brand-title {