Authored by htoooth

Merge branch 'release/1.0' of http://git.yoho.cn/fe/yoho-app-web into feature/ufo

# Conflicts:
#	config/ssr-routes.js
@@ -133,6 +133,10 @@ export default { @@ -133,6 +133,10 @@ export default {
133 }, 133 },
134 zoom: { 134 zoom: {
135 default: false 135 default: false
  136 + },
  137 + observeDom: {
  138 + type: Boolean,
  139 + default: true
136 } 140 }
137 }, 141 },
138 data() { 142 data() {
@@ -185,7 +189,8 @@ export default { @@ -185,7 +189,8 @@ export default {
185 freeScroll: this.freeScroll, 189 freeScroll: this.freeScroll,
186 mouseWheel: this.mouseWheel, 190 mouseWheel: this.mouseWheel,
187 bounce: this.bounce, 191 bounce: this.bounce,
188 - zoom: this.zoom 192 + zoom: this.zoom,
  193 + observeDOM: this.observeDom,
189 }; 194 };
190 195
191 this.scroll = new BetterScroll(this.$refs.wrapper, options); 196 this.scroll = new BetterScroll(this.$refs.wrapper, options);
@@ -11,8 +11,6 @@ import 'statics/font/ufofont.css'; @@ -11,8 +11,6 @@ import 'statics/font/ufofont.css';
11 11
12 const {app, router, store} = createApp(); 12 const {app, router, store} = createApp();
13 13
14 -window.newBlk = true;  
15 -  
16 if (window.__INITIAL_STATE__) { 14 if (window.__INITIAL_STATE__) {
17 store.replaceState(window.__INITIAL_STATE__); 15 store.replaceState(window.__INITIAL_STATE__);
18 } 16 }
1 <template> 1 <template>
2 - <Modal :value="value" @input="onInput" ref="modal" :transfer="true" @on-sure="onSure"> 2 + <Modal class="ufo-font" :value="value" @input="onInput" ref="modal" :transfer="true" @on-sure="onSure">
3 <div class="change-price-modal"> 3 <div class="change-price-modal">
4 <p class="modal-title">当前42码最低售价:¥1999.00</p> 4 <p class="modal-title">当前42码最低售价:¥1999.00</p>
5 <Inputx :maxlength="8" class="input-number"> 5 <Inputx :maxlength="8" class="input-number">
1 <template> 1 <template>
2 - <Modal :value="value" @input="onInput" ref="modal" :transfer="true" @on-sure="onSure"> 2 + <Modal class="ufo-font" :value="value" @input="onInput" ref="modal" :transfer="true" @on-sure="onSure">
3 <div class="change-price-modal"> 3 <div class="change-price-modal">
4 <p class="modal-title">选择你要下架的数量</p> 4 <p class="modal-title">选择你要下架的数量</p>
5 <Inputx v-model="stockNum" :maxlength="8" :readonly="true" class="input-number"> 5 <Inputx v-model="stockNum" :maxlength="8" :readonly="true" class="input-number">
  1 +<template>
  2 + <div class="product-item" :class="{['has-tip']: value.tip}">
  3 + <div class="item-content" :style="itemStyle" @touchstart="onTouchStart" @touchmove="onTouchMove" @touchend="onTouchEnd">
  4 + <div class="tip" v-if="value.tip">超出建议售价将被限制超出建议售价将被限制展示</div>
  5 + <div class="info">
  6 + <div class="left">
  7 + <span class="size">{{value.size}}</span>
  8 + <span class="l-size">1/3</span>
  9 + <span class="unit">码</span>
  10 + </div>
  11 + <div class="middle">
  12 + <p class="size-store">¥{{value.price}},12个库存</p>
  13 + <p class="low-price">当前最低价¥{{value.price}}</p>
  14 + </div>
  15 + <div class="right">
  16 + <Button class="chg-price" @click="onChgPrice">调 价</Button>
  17 + </div>
  18 + </div>
  19 + </div>
  20 + <div ref="options" class="item-options">
  21 + <Button class="btn-no-sale" @click="onNoSale">不卖了</Button>
  22 + </div>
  23 + </div>
  24 +</template>
  25 +
  26 +<script>
  27 +import {Button} from 'cube-ui';
  28 +
  29 +export default {
  30 + name: 'ProductItem',
  31 + data() {
  32 + return {
  33 + distance: 0,
  34 + startX: 0,
  35 + startY: 0,
  36 + move: false,
  37 + transition: true
  38 + };
  39 + },
  40 + computed: {
  41 + itemStyle() {
  42 + return {
  43 + transition: this.transition ? void 0 : 'none 0s ease 0s',
  44 + transform: this.move ? `translate3d(${this.distance}px, 0px, 0px)` : void 0
  45 + };
  46 + }
  47 + },
  48 + props: {
  49 + value: Object,
  50 + slideValue: Object
  51 + },
  52 + mounted() {
  53 +
  54 + },
  55 + watch: {
  56 + slideValue(val) {
  57 + if (this.distance !== 0 && this.value !== val) {
  58 + this.distance = 0;
  59 + this.timeout = setTimeout(() => {
  60 + this.move = false;
  61 + }, 500);
  62 + }
  63 + }
  64 + },
  65 + methods: {
  66 + onNoSale() {
  67 + this.$emit('on-no-sale');
  68 + },
  69 + onChgPrice() {
  70 + this.$emit('on-chg-price');
  71 + },
  72 + onTouchStart(evt) {
  73 + const {clientX, clientY} = evt.touches[0];
  74 +
  75 + this.startX = clientX - this.distance;
  76 + this.startY = clientY;
  77 + if (this.timeout) {
  78 + clearTimeout(this.timeout);
  79 + }
  80 + },
  81 + onTouchMove(evt) {
  82 + this.transition = false;
  83 + const {clientX, clientY} = evt.touches[0];
  84 + let distance = clientX - this.startX;
  85 +
  86 + if (Math.abs(clientY - this.startY) > 20 && this.distance === 0) {
  87 + this.startX = 0;
  88 + return;
  89 + }
  90 +
  91 + if (0 - distance > 20 && !this.move) {
  92 + this.$emit('on-slide', this.value);
  93 + this.move = true;
  94 + }
  95 +
  96 + if (this.distance + distance > 0) {
  97 + return;
  98 + }
  99 + if (this.move) {
  100 + this.distance = distance;
  101 + }
  102 + },
  103 + onTouchEnd() {
  104 + const optionsWidth = this.$refs.options.clientWidth;
  105 +
  106 + if (0 - this.distance > optionsWidth) {
  107 + this.transition = true;
  108 + this.distance = 0 - optionsWidth;
  109 + } else if (0 - this.distance < optionsWidth) {
  110 + this.transition = true;
  111 + this.distance = 0;
  112 + }
  113 + if (this.distance === 0) {
  114 + this.timeout = setTimeout(() => {
  115 + this.move = false;
  116 + }, 500);
  117 + }
  118 + }
  119 + },
  120 + components: {Button}
  121 +};
  122 +</script>
  123 +
  124 +<style lang="scss" scoped>
  125 +.product-item {
  126 + width: 100%;
  127 + border-bottom: 1px solid #eee;
  128 + position: relative;
  129 +}
  130 +
  131 +.item-content {
  132 + padding-left: 40px;
  133 + padding-right: 40px;
  134 + position: relative;
  135 + z-index: 2;
  136 + background-color: #fff;
  137 + padding-top: 20px;
  138 + padding-bottom: 40px;
  139 + transition: transform 0.5s cubic-bezier(0.36, 0.66, 0.04, 1);
  140 +}
  141 +
  142 +.item-options {
  143 + position: absolute;
  144 + top: 0;
  145 + right: 0;
  146 + bottom: 0;
  147 + z-index: 1;
  148 +
  149 + .cube-btn {
  150 + height: 100%;
  151 + line-height: 1;
  152 + background-color: #eee;
  153 + width: 160px;
  154 + font-size: 28px;
  155 + color: #000;
  156 +
  157 + &:active {
  158 + opacity: 0.7;
  159 + }
  160 + }
  161 +}
  162 +
  163 +.tip {
  164 + color: #d0021b;
  165 +}
  166 +
  167 +.info {
  168 + width: 100%;
  169 + display: flex;
  170 + padding-top: 20px;
  171 +}
  172 +
  173 +.left {
  174 + width: 160px;
  175 + display: flex;
  176 + height: 56px;
  177 + align-items: flex-end;
  178 +
  179 + .size {
  180 + font-size: 56px;
  181 + line-height: 56px;
  182 + margin-right: 6px;
  183 + }
  184 +
  185 + .l-size {
  186 + align-self: flex-end;
  187 + margin-right: 6px;
  188 + }
  189 +
  190 + .unit {
  191 + align-self: flex-start;
  192 + margin-right: 6px;
  193 + }
  194 +}
  195 +
  196 +.middle {
  197 + flex: 1;
  198 +
  199 + .size-store {
  200 + font-size: 28px;
  201 + }
  202 +
  203 + .low-price {
  204 + color: #999;
  205 + margin-top: 6px;
  206 + }
  207 +}
  208 +
  209 +.right {
  210 + width: 130px;
  211 + text-align: right;
  212 + display: flex;
  213 + align-items: center;
  214 +
  215 + .chg-price {
  216 + width: 130px;
  217 + height: 60px;
  218 + line-height: 60px;
  219 + padding-top: 0;
  220 + padding-bottom: 0;
  221 + background-color: #08314d;
  222 + font-size: 28px;
  223 +
  224 + &:active {
  225 + opacity: 0.7;
  226 + }
  227 + }
  228 +}
  229 +</style>
  1 +<template>
  2 + <div class="product-group">
  3 + <ProductItem
  4 + v-for="(skc, i) in skcs"
  5 + :key="i"
  6 + :value="skc"
  7 + :slideValue="slideSkc"
  8 + @on-chg-price="onChgPrice"
  9 + @on-no-sale="onNoSale"
  10 + @on-slide="onSlide"></ProductItem>
  11 + <ModalPrice
  12 + v-if="modalLoad"
  13 + ref="modalPrice"
  14 + v-model="showModalPrice"
  15 + @on-sure="onPriceSure">
  16 + </ModalPrice>
  17 + <ModalUnstock
  18 + v-if="modalLoad"
  19 + ref="modalUnstock"
  20 + v-model="showModalUnstock"
  21 + @on-sure="onUnstockSure">
  22 + </ModalUnstock>
  23 + </div>
  24 +</template>
  25 +
  26 +<script>
  27 +import ModalPrice from './modal-price';
  28 +import ModalUnstock from './modal-unstock';
  29 +import ProductItem from './product-item';
  30 +
  31 +export default {
  32 + name: 'ProductList',
  33 + props: {
  34 + skcs: Array
  35 + },
  36 + data() {
  37 + return {
  38 + modalLoad: false,
  39 + showModalPrice: false,
  40 + showTips: false,
  41 + showModalUnstock: false,
  42 + slideSkc: {}
  43 + };
  44 + },
  45 + mounted() {
  46 + this.modalLoad = true;
  47 + },
  48 + methods: {
  49 + onPriceSure() {
  50 + this.showTips = !this.showTips;
  51 + },
  52 + onUnstockSure() {
  53 + this.showTips = !this.showTips;
  54 + },
  55 + onChgPrice() {
  56 + this.showModalPrice = true;
  57 + },
  58 + onNoSale() {
  59 + this.showModalUnstock = true;
  60 + },
  61 + onSlide(val) {
  62 + console.log('onSlide')
  63 + this.slideSkc = val;
  64 + }
  65 + },
  66 + components: {ModalPrice, ModalUnstock, ProductItem}
  67 +};
  68 +</script>
1 export default [{ 1 export default [{
2 path: '/ufo/order/:orderId(\\d+)', 2 path: '/ufo/order/:orderId(\\d+)',
3 name: 'order', 3 name: 'order',
4 - component: () => import(/* webpackChunkName: "order" */ './order') 4 + component: () => import('./order')
5 }]; 5 }];
1 <template> 1 <template>
2 <LayoutApp class="ufo-font" :class="classes"> 2 <LayoutApp class="ufo-font" :class="classes">
3 - <ScrollView ref="scroll" :pull-up-load="true" :pull-down-refresh="true" @pullingUp="onPullingUp" @pullingDown="onPullingDown"> 3 + <ScrollView ref="scroll" :observe-dom="false" :pull-up-load="true" :pull-down-refresh="true" @pullingUp="onPullingUp" @pullingDown="onPullingDown">
4 <div class="order-page"> 4 <div class="order-page">
5 <div class="title">出售中</div> 5 <div class="title">出售中</div>
6 <div class="product"> 6 <div class="product">
@@ -10,48 +10,19 @@ @@ -10,48 +10,19 @@
10 <p class="stock-info">5个尺码,39个商品库存</p> 10 <p class="stock-info">5个尺码,39个商品库存</p>
11 </div> 11 </div>
12 </div> 12 </div>
13 - <p class="arrival-time"><i class="iconfont icon-info"></i>最新上架时间:2018.10.27 00:16:41</p>  
14 - <div class="product-group">  
15 - <div class="product-item" :class="{['has-tip']: skc.tip}" v-for="(skc, i) in orderDetail.skcs" :key="i" @click="onClick">  
16 - <div class="tip" v-if="skc.tip">超出建议售价将被限制超出建议售价将被限制展示</div>  
17 - <div class="info">  
18 - <div class="left">  
19 - <span class="size">{{skc.size}}</span>  
20 - <span class="l-size">1/3</span>  
21 - <span class="unit">码</span>  
22 - </div>  
23 - <div class="middle">  
24 - <p class="size-store">¥{{skc.price}},12个库存</p>  
25 - <p class="low-price">当前最低价¥{{skc.price}}</p>  
26 - </div>  
27 - <div class="right">  
28 - <Button class="chg-price" @click="onChgPrice">调价</Button>  
29 - </div>  
30 - </div>  
31 - </div> 13 + <div class="arrival">
  14 + <p class="arrival-time"><i class="iconfont icon-info"></i><span>尺码列表左滑选择 不卖了 ,下架当前尺码商品</span></p>
32 </div> 15 </div>
  16 + <ProductList :skcs="orderDetail.skcs"></ProductList>
33 </div> 17 </div>
34 </ScrollView> 18 </ScrollView>
35 - <ModalPrice  
36 - v-if="modalLoad"  
37 - ref="modalPrice"  
38 - v-model="showModalPrice"  
39 - @on-sure="onPriceSure">  
40 - </ModalPrice>  
41 - <ModalUnstock  
42 - v-if="modalLoad"  
43 - ref="modalUnstock"  
44 - v-model="showModalUnstock"  
45 - @on-sure="onUnstockSure">  
46 - </ModalUnstock>  
47 </LayoutApp> 19 </LayoutApp>
48 </template> 20 </template>
49 21
50 <script> 22 <script>
51 import {Button} from 'cube-ui'; 23 import {Button} from 'cube-ui';
52 -import ModalPrice from './components/modal-price';  
53 -import ModalUnstock from './components/modal-unstock';  
54 -import ScrollView from 'components/scroll-view.vue'; 24 +import ScrollView from 'components/scroll-view';
  25 +import ProductList from './components/product-list';
55 import {createNamespacedHelpers} from 'vuex'; 26 import {createNamespacedHelpers} from 'vuex';
56 27
57 28
@@ -74,11 +45,8 @@ export default { @@ -74,11 +45,8 @@ export default {
74 asyncData({store, router}) { 45 asyncData({store, router}) {
75 return store.dispatch('ufo/order/fetchProduct', {orderId: router.params.orderId}); 46 return store.dispatch('ufo/order/fetchProduct', {orderId: router.params.orderId});
76 }, 47 },
77 - created() {  
78 - },  
79 mounted() { 48 mounted() {
80 this.fetchOrderDetail({orderId: this.$route.params.orderId}); 49 this.fetchOrderDetail({orderId: this.$route.params.orderId});
81 - this.modalLoad = true;  
82 }, 50 },
83 methods: { 51 methods: {
84 ...mapActions(['fetchOrderDetail']), 52 ...mapActions(['fetchOrderDetail']),
@@ -95,34 +63,25 @@ export default { @@ -95,34 +63,25 @@ export default {
95 onClick() { 63 onClick() {
96 this.showModalUnstock = true; 64 this.showModalUnstock = true;
97 }, 65 },
98 - onPriceSure() {  
99 - this.showTips = !this.showTips;  
100 - },  
101 - onUnstockSure() {  
102 - this.showTips = !this.showTips;  
103 - },  
104 - onNoSale() {  
105 - this.showModalUnstock = true;  
106 - },  
107 - onChgPrice() {  
108 - this.showModalPrice = true;  
109 - }  
110 }, 66 },
111 - components: {ModalPrice, ModalUnstock, Button, ScrollView} 67 + components: {Button, ScrollView, ProductList}
112 }; 68 };
113 </script> 69 </script>
114 70
115 <style lang="scss" scoped> 71 <style lang="scss" scoped>
116 .order-page { 72 .order-page {
117 - padding: 24px 40px;  
118 -  
119 & > .title { 73 & > .title {
120 font-size: 82px; 74 font-size: 82px;
121 font-weight: bold; 75 font-weight: bold;
  76 + padding-top: 24px;
  77 + padding-left: 40px;
  78 + padding-right: 40px;
122 } 79 }
123 80
124 .product { 81 .product {
125 width: 100%; 82 width: 100%;
  83 + padding-left: 40px;
  84 + padding-right: 40px;
126 height: 192px; 85 height: 192px;
127 display: flex; 86 display: flex;
128 87
@@ -152,95 +111,29 @@ export default { @@ -152,95 +111,29 @@ export default {
152 } 111 }
153 } 112 }
154 113
155 - .arrival-time { 114 + .arrival {
156 margin-top: 20px; 115 margin-top: 20px;
157 margin-bottom: 20px; 116 margin-bottom: 20px;
  117 + padding-left: 40px;
  118 + padding-right: 40px;
158 width: 100%; 119 width: 100%;
159 - height: 56px;  
160 - line-height: 56px;  
161 - color: #999;  
162 - font-size: 24px;  
163 - padding-left: 14px;  
164 - padding-right: 14px;  
165 - background-color: #f0f0f0;  
166 - display: flex;  
167 -  
168 - i {  
169 - font-size: 30px;  
170 - margin-right: 10px;  
171 - align-items: center;  
172 - }  
173 - }  
174 -  
175 - .product-group {  
176 - .product-item {  
177 - width: 100%;  
178 - border-bottom: 1px solid #eee;  
179 - padding-top: 20px;  
180 - padding-bottom: 40px;  
181 - }  
182 -  
183 - .tip {  
184 - color: #d0021b;  
185 - }  
186 120
187 - .info {  
188 - width: 100%;  
189 - display: flex;  
190 - padding-top: 20px;  
191 - }  
192 -  
193 - .left {  
194 - width: 160px;  
195 - display: flex; 121 + .arrival-time {
196 height: 56px; 122 height: 56px;
197 - align-items: flex-end;  
198 -  
199 - .size {  
200 - font-size: 56px;  
201 - line-height: 56px;  
202 - margin-right: 6px;  
203 - }  
204 -  
205 - .l-size {  
206 - align-self: flex-end;  
207 - margin-right: 6px;  
208 - }  
209 -  
210 - .unit {  
211 - align-self: flex-start;  
212 - margin-right: 6px;  
213 - }  
214 - }  
215 -  
216 - .middle {  
217 - flex: 1;  
218 -  
219 - .size-store {  
220 - font-size: 28px;  
221 - }  
222 -  
223 - .low-price {  
224 - color: #999;  
225 - margin-top: 6px;  
226 - }  
227 - }  
228 -  
229 - .right {  
230 - width: 130px;  
231 - text-align: right; 123 + color: #999;
  124 + font-size: 24px;
  125 + padding-left: 14px;
  126 + padding-right: 14px;
  127 + background-color: #f0f0f0;
  128 + display: flex;
  129 + align-items: center;
232 130
233 - .chg-price {  
234 - width: 130px;  
235 - height: 60px;  
236 - line-height: 60px;  
237 - padding-top: 0;  
238 - padding-bottom: 0;  
239 - background-color: #002b47;  
240 - font-size: 28px; 131 + i {
  132 + font-size: 30px;
  133 + margin-right: 10px;
  134 + align-items: center;
241 } 135 }
242 } 136 }
243 } 137 }
244 } 138 }
245 -  
246 </style> 139 </style>
@@ -664,4 +664,4 @@ img[lazy=loaded] { @@ -664,4 +664,4 @@ img[lazy=loaded] {
664 664
665 .pointer-events { 665 .pointer-events {
666 pointer-events: none; 666 pointer-events: none;
667 -}  
  667 +}
1 -const dispatchTap = (evt) => {  
2 - const clickEvent = document.createEvent('MouseEvents');  
3 - const touch = evt.changedTouches[0] || {};  
4 -  
5 - clickEvent.initMouseEvent('e-click', true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null);  
6 - clickEvent.forwardedTouchEvent = true;  
7 - evt.target.dispatchEvent(clickEvent);  
8 -};  
9 -  
10 -const onTouchstart = function() {  
11 - setTimeout(() => { // ios webview中scroll和touchmove事件在滚动时会被阻塞,判断滑动状态放入下一次Event loop  
12 - if (this.state.yoho.touchStatus !== 'scrolling' || // 额外判断滚动状态中止100ms后可以触发tap事件  
13 - Date.now() - this.state.yoho.scrollTime > 100) {  
14 - this.commit('SET_TOUCH_STATUS', {touchStatus: 'touchstart'});  
15 - }  
16 - }, 0);  
17 -};  
18 -const onTouchend = function(evt) {  
19 - setTimeout(() => { // 同样放入下一次Event loop否则会导致touchend优先于touchstart事件触发  
20 - if (this.state.yoho.touchStatus === 'touchstart') {  
21 - dispatchTap(evt); // 触发自定义e-click事件  
22 - this.commit('SET_TOUCH_STATUS', {touchStatus: ''});  
23 - }  
24 - }, 0);  
25 -  
26 - if (evt.cancelable) { // ios webview中如果不阻止默认事件会导致:点击后迅速滑动不能及时响应  
27 - evt.preventDefault();  
28 - }  
29 -};  
30 -const onTouchmove = function() {  
31 - this.commit('SET_TOUCH_STATUS', {touchStatus: 'scrolling', time: Date.now()});  
32 -};  
33 -const onScroll = function() {  
34 - console.log('onScroll')  
35 - this.commit('SET_TOUCH_STATUS', {touchStatus: 'scrolling', time: Date.now()});  
36 -};  
37 -  
38 -  
39 export default store => { 1 export default store => {
40 - // if (process.env.VUE_ENV === 'client') {  
41 - // // 自定义点击事件,解决ioswebview中点击延迟和滑动中误点击响应的问题。  
42 - // let supportsPassive = false;  
43 -  
44 - // try {  
45 - // const opts = Object.defineProperty({}, 'passive', {  
46 - // get: function() {  
47 - // supportsPassive = true;  
48 - // return false;  
49 - // }  
50 - // });  
51 -  
52 - // window.addEventListener('test', null, opts);  
53 - // } catch (e) {} //eslint-disable-line  
54 - // store.commit('SET_SUPPORTS_PASSIVE', supportsPassive);  
55 -  
56 - // document.addEventListener('touchstart', onTouchstart.bind(store));  
57 - // document.addEventListener('touchend', onTouchend.bind(store));  
58 - // document.addEventListener('scroll', onScroll.bind(store), supportsPassive ? { passive: true } : false);  
59 - // document.addEventListener('touchmove', onTouchmove.bind(store), supportsPassive ? { passive: true } : false);  
60 - // } 2 +
61 }; 3 };
  1 +export const calcPx = (px) => {
  2 + const clientWidth = document.body.clientWidth;
  3 + let fz;
  4 +
  5 + if (clientWidth > 750) {
  6 + fz = 1;
  7 + } else {
  8 + fz = (clientWidth / 750);
  9 + }
  10 + return px * fz;
  11 +};
@@ -14,7 +14,6 @@ let webpackConfig = merge(baseConfig, { @@ -14,7 +14,6 @@ let webpackConfig = merge(baseConfig, {
14 target: 'node', 14 target: 'node',
15 resolve: { 15 resolve: {
16 alias: { 16 alias: {
17 - hammerjs$: 'vue-touch/dist/hammer-ssr.js',  
18 'create-api': 'common/create-api-server.js' 17 'create-api': 'common/create-api-server.js'
19 } 18 }
20 }, 19 },
@@ -22,7 +21,7 @@ let webpackConfig = merge(baseConfig, { @@ -22,7 +21,7 @@ let webpackConfig = merge(baseConfig, {
22 libraryTarget: 'commonjs2', 21 libraryTarget: 'commonjs2',
23 }, 22 },
24 externals: nodeExternals({ 23 externals: nodeExternals({
25 - whitelist: [/cube-ui/] 24 + whitelist: [/\.css$/, /cube-ui/]
26 }), 25 }),
27 plugins: [ 26 plugins: [
28 new VueSSRServerPlugin({ 27 new VueSSRServerPlugin({
@@ -57,7 +57,7 @@ module.exports = { @@ -57,7 +57,7 @@ module.exports = {
57 activity: '//activity.yohobuy.com', 57 activity: '//activity.yohobuy.com',
58 index: '//m.yohobuy.com' 58 index: '//m.yohobuy.com'
59 }, 59 },
60 - useCache: false, 60 + useCache: true,
61 loggers: { 61 loggers: {
62 infoFile: { 62 infoFile: {
63 close: true, 63 close: true,
1 module.exports = [ 1 module.exports = [
2 { 2 {
3 route: /channel/, 3 route: /channel/,
4 - cache: true, 4 + cache: true
5 }, 5 },
6 { 6 {
7 route: /ufo\/order/, 7 route: /ufo\/order/,
8 cache: true, 8 cache: true,
9 - }, 9 + cackeKey: '$url$params',
  10 + cacheTime: 10
  11 + }
10 { 12 {
11 route: /coupon\/ufo/, 13 route: /coupon\/ufo/,
12 cache: true 14 cache: true
1 const fs = require('fs'); 1 const fs = require('fs');
2 const path = require('path'); 2 const path = require('path');
3 -const LRU = require('lru-cache');  
4 const url = require('url'); 3 const url = require('url');
  4 +const sourceMap = require('source-map');
5 const _ = require('lodash'); 5 const _ = require('lodash');
  6 +const md5 = require('yoho-md5');
6 const pkg = require('../../package.json'); 7 const pkg = require('../../package.json');
7 const routes = require('../../config/ssr-routes'); 8 const routes = require('../../config/ssr-routes');
  9 +const redis = require('../../utils/redis');
8 const {createBundleRenderer} = require('vue-server-renderer'); 10 const {createBundleRenderer} = require('vue-server-renderer');
  11 +const logger = global.yoho.logger;
  12 +const config = global.yoho.config;
9 13
10 -const microCache = LRU({ // eslint-disable-line  
11 - max: 1000,  
12 - maxAge: 2000  
13 -}); 14 +const REG_STACK = /at ([^:]+):(\d+):(\d+)/;
14 15
15 const isDev = process.env.NODE_ENV === 'development' || !process.env.NODE_ENV; 16 const isDev = process.env.NODE_ENV === 'development' || !process.env.NODE_ENV;
16 17
17 let renderer; 18 let renderer;
  19 +let serverBundle;
18 20
19 if (!isDev) { 21 if (!isDev) {
20 const template = fs.readFileSync(path.join(__dirname, '../../apps/index.html'), 'utf-8'); 22 const template = fs.readFileSync(path.join(__dirname, '../../apps/index.html'), 'utf-8');
21 - const serverBundle = require(`../../dist/yohoblk-wap/bundle/yoho-ssr-server-${pkg.version}.json`); 23 +
  24 + serverBundle = require(`../../dist/yohoblk-wap/bundle/yoho-ssr-server-${pkg.version}.json`);
22 const clientManifest = require(`../../dist/yohoblk-wap/bundle/yoho-ssr-client-${pkg.version}.json`); 25 const clientManifest = require(`../../dist/yohoblk-wap/bundle/yoho-ssr-client-${pkg.version}.json`);
23 26
24 renderer = createBundleRenderer(serverBundle, { 27 renderer = createBundleRenderer(serverBundle, {
@@ -43,32 +46,85 @@ const getContext = (req) => { @@ -43,32 +46,85 @@ const getContext = (req) => {
43 }; 46 };
44 }; 47 };
45 48
46 -const render = ({cache, cacheRule}) => {  
47 - return (req, res, next) => {  
48 - const reqUrl = url.parse(req.url); 49 +const getCacheKey = (urlPath, cackeKey = '') => {
  50 + const urlObj = url.parse(urlPath);
  51 +
  52 + return md5(cackeKey
  53 + .replace('$url', urlObj.pathname)
  54 + .replace('$params', urlObj.query));
  55 +};
  56 +
  57 +const parseError = async({stack = ''}) => {
  58 + try {
  59 + const splits = stack.split('\n');
  60 + const lastError = splits.map(str => {
  61 + const match = str.match(REG_STACK);
49 62
50 - if (cache && reqUrl[cacheRule]) {  
51 - const html = microCache.get(reqUrl[cacheRule]); 63 + if (match) {
  64 + return {file: match[1], line: parseInt(match[2], 10), column: parseInt(match[3], 10)};
  65 + }
  66 + return false;
  67 + }).find(match => match);
  68 +
  69 + if (lastError && lastError.file) {
  70 + const consumer = await new sourceMap.SourceMapConsumer(serverBundle.maps[lastError.file]);
  71 +
  72 + const origin = consumer.originalPositionFor({
  73 + line: lastError.line,
  74 + column: 342
  75 + });
  76 +
  77 + console.log(origin);
  78 + }
  79 + } catch (error) {
  80 + logger.error(error);
  81 + }
  82 +};
  83 +
  84 +const render = (route) => {
  85 + return async(req, res, next) => {
  86 + res.setHeader('X-YOHO-Version', pkg.version);
  87 + const ck = getCacheKey(req.url, route.cackeKey);
  88 +
  89 + if (config.useCache && route.cache && ck) {
  90 + const html = await redis.getAsync(ck);
52 91
53 if (html) { 92 if (html) {
  93 + logger.debug(`cached ${req.url}`);
  94 + res.setHeader('X-YOHO-Cached', 'HIT');
54 return res.send(html); 95 return res.send(html);
55 } 96 }
  97 + res.setHeader('X-YOHO-Cached', 'MISS');
56 } 98 }
57 let context = getContext(req); 99 let context = getContext(req);
58 100
59 renderer.renderToString(context, (err, html) => { 101 renderer.renderToString(context, (err, html) => {
60 if (err) { 102 if (err) {
61 - return next(err); 103 + parseError(err);
  104 + return next(err.message);
62 } 105 }
63 - if (cache && reqUrl[cacheRule]) {  
64 - microCache.set(reqUrl[cacheRule], html); 106 + if (config.useCache && route.cache && ck) {
  107 + redis.setex(ck, route.cacheTime || 60, html);
65 } 108 }
66 return res.send(html); 109 return res.send(html);
67 }); 110 });
68 }; 111 };
69 }; 112 };
70 -const devRender = () => {  
71 - return (req, res, next) => { 113 +const devRender = (route) => {
  114 + return async(req, res, next) => {
  115 + res.setHeader('X-YOHO-Version', pkg.version);
  116 + const ck = getCacheKey(req.url, route.cackeKey);
  117 +
  118 + if (config.useCache && route.cache && ck) {
  119 + const html = await redis.getAsync(ck);
  120 +
  121 + if (html) {
  122 + logger.debug(`cached ${req.url}`);
  123 + res.setHeader('X-YOHO-Cached', 'HIT');
  124 + return res.send(html);
  125 + }
  126 + res.setHeader('X-YOHO-Cached', 'MISS');
  127 + }
72 let context = getContext(req); 128 let context = getContext(req);
73 129
74 process.send({action: 'ssr_request', context}); 130 process.send({action: 'ssr_request', context});
@@ -90,6 +146,9 @@ const devRender = () => { @@ -90,6 +146,9 @@ const devRender = () => {
90 }); 146 });
91 } 147 }
92 } 148 }
  149 + if (config.useCache && route.cache && ck) {
  150 + redis.setex(ck, route.cacheTime || 60, msg.html);
  151 + }
93 return res.end(msg.html); 152 return res.end(msg.html);
94 } 153 }
95 }; 154 };
@@ -2,7 +2,21 @@ const _ = require('lodash'); @@ -2,7 +2,21 @@ const _ = require('lodash');
2 const config = global.yoho.config; 2 const config = global.yoho.config;
3 3
4 module.exports = (req, res, next) => { 4 module.exports = (req, res, next) => {
5 - if (!req.user.uid && 5 + if (!req.yoho.isApp) {
  6 + if (req.session && _.isNumber(req.session.LOGIN_UID_)) {
  7 + // 调用接口传参时切勿使用toString获得字符串
  8 + req.user.uid = {
  9 + toString: () => {
  10 + return _.parseInt(req.session.LOGIN_UID_);
  11 + },
  12 + sessionKey: req.session.SESSION_KEY,
  13 + appSessionType: req.session.SESSION_TYPE
  14 + };
  15 + let userData = _.get(req.session, 'USER', {});
  16 +
  17 + _.merge(req.user, userData);
  18 + }
  19 + } else if (!req.user.uid &&
6 req.cookies.app_uid && 20 req.cookies.app_uid &&
7 req.cookies.app_uid !== '0' && 21 req.cookies.app_uid !== '0' &&
8 req.cookies.app_session_key && 22 req.cookies.app_session_key &&
@@ -9,6 +9,7 @@ @@ -9,6 +9,7 @@
9 }, 9 },
10 "scripts": { 10 "scripts": {
11 "start": "NODE_ENV=production node app.js", 11 "start": "NODE_ENV=production node app.js",
  12 + "test": "NODE_ENV=test3 node app.js",
12 "dev": "node app-dev.js", 13 "dev": "node app-dev.js",
13 "client": "NODE_ENV=production webpack --config ./build/webpack.client.conf.js", 14 "client": "NODE_ENV=production webpack --config ./build/webpack.client.conf.js",
14 "server": "NODE_ENV=production webpack --config ./build/webpack.server.conf.js", 15 "server": "NODE_ENV=production webpack --config ./build/webpack.server.conf.js",
@@ -25,24 +26,21 @@ @@ -25,24 +26,21 @@
25 } 26 }
26 }, 27 },
27 "dependencies": { 28 "dependencies": {
  29 + "axios": "^0.18.0",
28 "body-parser": "^1.18.3", 30 "body-parser": "^1.18.3",
29 "client-sessions": "^0.8.0", 31 "client-sessions": "^0.8.0",
30 "connect-multiparty": "^2.2.0", 32 "connect-multiparty": "^2.2.0",
31 "connect-redis": "^3.4.0", 33 "connect-redis": "^3.4.0",
32 "cookie-parser": "^1.4.3", 34 "cookie-parser": "^1.4.3",
  35 + "cube-ui": "^1.12.6",
33 "express": "^4.16.4", 36 "express": "^4.16.4",
34 "express-session": "^1.15.6", 37 "express-session": "^1.15.6",
  38 + "fastclick": "^1.0.6",
35 "lodash": "^4.17.11", 39 "lodash": "^4.17.11",
36 "request-promise": "^4.2.2", 40 "request-promise": "^4.2.2",
37 "serve-favicon": "^2.5.0", 41 "serve-favicon": "^2.5.0",
  42 + "source-map": "^0.7.3",
38 "uuid": "^3.3.2", 43 "uuid": "^3.3.2",
39 - "winston": "^3.1.0",  
40 - "yoho-cookie": "^1.2.0",  
41 - "yoho-express-session": "^2.0.0",  
42 - "yoho-md5": "^2.1.0",  
43 - "yoho-node-lib": "=0.6.41",  
44 - "yoho-qs": "^1.0.1",  
45 - "yoho-store": "^1.3.20",  
46 "vue": "^2.5.20", 44 "vue": "^2.5.20",
47 "vue-awesome-swiper": "^3.1.3", 45 "vue-awesome-swiper": "^3.1.3",
48 "vue-infinite-scroll": "^2.0.2", 46 "vue-infinite-scroll": "^2.0.2",
@@ -54,10 +52,14 @@ @@ -54,10 +52,14 @@
54 "vue-template-compiler": "^2.5.20", 52 "vue-template-compiler": "^2.5.20",
55 "vue-touch": "^1.1.0", 53 "vue-touch": "^1.1.0",
56 "vue-virtual-scroll-list": "^1.2.8", 54 "vue-virtual-scroll-list": "^1.2.8",
57 - "cube-ui": "^1.12.6",  
58 - "axios": "^0.18.0",  
59 - "fastclick": "^1.0.6",  
60 - "vuex": "^3.0.1" 55 + "vuex": "^3.0.1",
  56 + "winston": "^3.1.0",
  57 + "yoho-cookie": "^1.2.0",
  58 + "yoho-express-session": "^2.0.0",
  59 + "yoho-md5": "^2.1.0",
  60 + "yoho-node-lib": "=0.6.41",
  61 + "yoho-qs": "^1.0.1",
  62 + "yoho-store": "^1.3.20"
61 }, 63 },
62 "devDependencies": { 64 "devDependencies": {
63 "@babel/core": "^7.2.0", 65 "@babel/core": "^7.2.0",
1 -/**  
2 - * 格式化 后台 返回的 filters 数据  
3 - * @author chenxuan <xuan.chen@yoho.cn>  
4 - */  
5 -'use strict';  
6 -  
7 -const _ = require('lodash');  
8 -  
9 -/**  
10 - * 处理 以风格的数据  
11 - * 风格1:  
12 - * [  
13 - * {filter_attribute: value}  
14 - * ]  
15 - * 风格2:  
16 - * {filter_attribute: value}  
17 - *  
18 - * 处理结果  
19 - * [  
20 - * {attribute:value,......}  
21 - * ]  
22 - *  
23 - */  
24 -let verboseAttrHandler = (filterField, dataArr) => {  
25 - let result = [];  
26 - let re = new RegExp(`^${filterField}_`);  
27 -  
28 - // 处理 {filter_attribute: value}  
29 - function handlerAttrObj(obj) {  
30 - let item = {};  
31 - let keys = Object.keys(obj);  
32 -  
33 - keys.forEach(key => {  
34 - let newKey = key.replace(re, '');  
35 -  
36 - item[newKey] = obj[key];  
37 - });  
38 - return item;  
39 - }  
40 -  
41 - if (_.isPlainObject(dataArr)) {  
42 - // 风格2  
43 - let attr = handlerAttrObj(dataArr);  
44 -  
45 - result.push(attr);  
46 - } else {  
47 - // 风格1  
48 - dataArr.forEach(obj => {  
49 - let attr = handlerAttrObj(obj);  
50 -  
51 - result.push(attr);  
52 - });  
53 - }  
54 -  
55 - return result;  
56 -};  
57 -  
58 -/*  
59 - * 处理 以下风格:  
60 - * {  
61 - "340,99999": "¥339以上",  
62 - "0,149": "¥0-149",  
63 - "150,179": "¥150-179",  
64 - "180,339": "¥180-339"  
65 - * },  
66 - *  
67 - * 处理结果:  
68 - * [  
69 - * {id: '0,149', value: '0,149', name: '¥0-149'},  
70 - * {id: '150, 179', value: '150,179', name: '¥150-179'}  
71 - * .....  
72 - * ]  
73 - * 按照字符串顺序  
74 - */  
75 -let keyIdHandler = (filterField, obj) => {  
76 - const result = [];  
77 - const keys = Object.keys(obj);  
78 -  
79 - keys.sort((a, b) => {  
80 - let v1 = Number.parseFloat(a.split(',')[0]);  
81 - let v2 = Number.parseFloat(b.split(',')[0]);  
82 -  
83 - return v1 - v2;  
84 - });  
85 -  
86 - keys.forEach(key => {  
87 - let item = {};  
88 -  
89 - item.id = key;  
90 -  
91 - let t = obj[key];  
92 - let isObject = Object.prototype.toString.apply(t) === '[object Object]';  
93 -  
94 - if (isObject) {  
95 - item = Object.assign(item, t);  
96 - } else {  
97 - item.name = t;  
98 - }  
99 -  
100 - result.push(item);  
101 - });  
102 -  
103 - return result;  
104 -};  
105 -  
106 -function prettyFilter(filters) {  
107 - // Warn!!!  
108 - delete filters.ageLevel;  
109 -  
110 - let keys = _.keys(filters);  
111 -  
112 - _.forEach(keys, key => {  
113 - let process;  
114 -  
115 - // 相同规律的 使用 相同的处理规则  
116 - switch (key) {  
117 - case 'color':  
118 - case 'size':  
119 - case 'brand':  
120 - case 'style':  
121 - process = verboseAttrHandler;  
122 - break;  
123 -  
124 - case 'gender':  
125 - case 'priceRange':  
126 - case 'discount':  
127 - process = keyIdHandler;  
128 - break;  
129 -  
130 - // 其他不做处理  
131 - default:  
132 - process = _.noop;  
133 - }  
134 - filters[key] = process(key, filters[key]) || filters[key];  
135 - });  
136 -}  
137 -  
138 -module.exports = prettyFilter;  
139 -  
140 -  
141 -/*  
142 -filter Object经过 prettyFilter, camelCase 后的 数据格式:  
143 -{  
144 - color: [{id,name,value, code}],  
145 - gender: [{id,name}],  
146 - size: [id,name],  
147 - discount: [{id,name,count}],  
148 - priceRange: [{id, name}],  
149 - ageLevel: [{id,name, productCount}]  
150 - brand: [{id, name,domain,alif,ico,keyword,hotKeyword,isHot, nameEn, nameCn}]  
151 - groupSort,  
152 -}  
153 -*/  
1 -'use strict';  
2 -const path = require('path');  
3 -const _ = require('lodash');  
4 -const helpers = global.yoho.helpers;  
5 -const utilsPath = path.join(global.utils, '/constant');  
6 -const genderMap = require(utilsPath).genderMap;  
7 -  
8 -/**  
9 - * 根据性别来决定 默认图片获取字段 如果是 2、3  
10 - */  
11 -const _procProductImg = (product, genderVal) => {  
12 - let defaultImages;  
13 -  
14 - switch (genderVal) {  
15 - case genderMap.men:  
16 - defaultImages = product.cover_1 || product.images_url;  
17 - break;  
18 - case genderMap.women:  
19 - defaultImages = product.cover_2 || product.images_url;  
20 - break;  
21 - default:  
22 - defaultImages = product.images_url || product.cover_1 || product.cover_2;  
23 - break;  
24 - }  
25 -  
26 - defaultImages || (defaultImages = '');  
27 -  
28 - return defaultImages;  
29 -};  
30 -  
31 -/**  
32 - * 商品搜索商品数据处理  
33 - */  
34 -module.exports = (list, options) => {  
35 - options = options || {};  
36 -  
37 - const pruductList = [];  
38 -  
39 - if (!options.gender) {  
40 - options.gender = '';  
41 - }  
42 -  
43 - options = Object.assign({  
44 - showTags: true,  
45 - showNew: true,  
46 - showSale: true,  
47 - width: 290,  
48 - height: 388,  
49 - isApp: false,  
50 - showPoint: true,  
51 - gender: '',  
52 - yhChannel: ''  
53 - }, options);  
54 -  
55 - if (Array.isArray(options.gender)) {  
56 - options.gender = options.gender[0];  
57 - }  
58 -  
59 - let genderVal = options.gender.split(',');  
60 -  
61 - if (genderVal.indexOf(genderMap.men) && genderVal.indexOf(genderMap.women)) { // 男女 通吃  
62 - genderVal = '';  
63 - } else {  
64 - genderVal = genderVal[0];  
65 - }  
66 -  
67 - _.forEach(list, (product) => {  
68 - // 商品信息有问题,则不显示  
69 - if (!product || !product.product_id) {  
70 - return;  
71 - }  
72 -  
73 - // 如果库存为0,显示已抢完  
74 - if (product.storage_num === 0) {  
75 - product.no_storage = true;  
76 - }  
77 -  
78 - // 市场价和售价一样,则不显示市场价  
79 - if (product.market_price === product.sales_price) {  
80 - product.market_price = false;  
81 - }  
82 -  
83 - product.is_soon_sold_out = product.is_soon_sold_out === 'Y';  
84 - product.url = helpers.urlFormat(`/product/pro_${product.product_id}_${_.get(product, 'goods_list[0].goods_id', '')}/${product.cn_alphabet}.html`); // eslint-disable-line  
85 -  
86 - // APP访问需要加附加的参数  
87 - // 备注:如果以后APP的接口太多,可以把这边参数提取出来,变成一个公共的方法来生成,便于以后管理维护  
88 - if (options.isApp) {  
89 - product.url += `?openby:yohobuy={"action":"go.productDetail","params":{"product_skn":'${product.product_id}'}}`; // eslint-disable-line  
90 - }  
91 -  
92 - if (options.showTags) {  
93 - product.tags = {};  
94 -  
95 - product.tags.is_new = options.showNew && product.is_new === 'Y'; // 新品  
96 - product.tags.is_discount = options.showSale && product.is_discount === 'Y'; // 在售  
97 - product.tags.is_limited = product.is_limited === 'Y'; // 限量  
98 - product.tags.is_yohood = product.is_yohood === 'Y'; // YOHOOD  
99 - product.tags.mid_year = product.mid_year === 'Y'; // 年中  
100 - product.tags.year_end = product.year_end === 'Y'; // 年末  
101 - product.tags.is_advance = product.is_advance === 'Y'; // 再到着  
102 -  
103 - // 打折与即将售完组合显示打折  
104 - if (product.isSoonSoldOut && product.tags.is_discount) {  
105 - product.tags.is_new = false;  
106 - } else if (product.tags.is_discount &&  
107 - (product.tags.is_new || product.tags.is_limited || product.tags.is_yohood || product.tags.is_advance)) {  
108 - // 打折与其它组合则隐藏打折  
109 - product.tags.is_discount = false;  
110 - } else if (product.tags.is_yohood && product.tags.is_new) {  
111 - // YOHOOD和新品组合显示YOHOOD  
112 - product.tags.is_new = false;  
113 - }  
114 - }  
115 -  
116 - pruductList.push(product);  
117 - });  
118 -  
119 - return pruductList;  
120 -};  
1 -const _ = require('lodash');  
2 -  
3 -/**  
4 - * 处理楼层数据  
5 - * @param {[array]} list  
6 - * @return {[array]}  
7 - */  
8 -module.exports = (list) => {  
9 - const formatData = [];  
10 -  
11 - list = list || [];  
12 -  
13 - _.forEach(list, (floor) => {  
14 - floor[_.camelCase(floor.template_name)] = true;  
15 -  
16 - // 特殊资源位处理  
17 -  
18 - formatData.push(floor);  
19 - });  
20 -  
21 -  
22 - return formatData;  
23 -};  
  1 +
  2 +const _ = require('lodash');
  3 +const redis = require('redis');
  4 +const bluebird = require('bluebird');
  5 +const config = require('../config/common');
  6 +let client;
  7 +const timeout = 200; // redis 操作超时时间
  8 +
  9 +try {
  10 + client = redis.createClient(config.redis.connect);
  11 +
  12 + bluebird.promisifyAll(redis.RedisClient.prototype);
  13 + bluebird.promisifyAll(redis.Multi.prototype);
  14 +
  15 +
  16 + client.all = args => {
  17 + if (!client.ready) {
  18 + if (Array.isArray(args)) {
  19 + return Promise.resolve(_.fill(args, false));
  20 + } else {
  21 + return Promise.resolve(false);
  22 + }
  23 + }
  24 +
  25 + return client.multi.call(client, args).execAsync().timeout(timeout).then(res => {
  26 + return res;
  27 + }).catch(() => {
  28 + return false;
  29 + });
  30 + };
  31 +
  32 + client.on('error', function() {
  33 + global.yoho.redis = '';
  34 + });
  35 +
  36 + client.on('connect', function() {
  37 + global.yoho.redis = client;
  38 + });
  39 +} catch (e) {
  40 + global.yoho.redis = '';
  41 +}
  42 +
  43 +
  44 +
  45 +module.exports = client;
@@ -7162,6 +7162,10 @@ source-map@^0.6.1, source-map@~0.6.1: @@ -7162,6 +7162,10 @@ source-map@^0.6.1, source-map@~0.6.1:
7162 resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" 7162 resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
7163 integrity sha1-dHIq8y6WFOnCh6jQu95IteLxomM= 7163 integrity sha1-dHIq8y6WFOnCh6jQu95IteLxomM=
7164 7164
  7165 +source-map@^0.7.3:
  7166 + version "0.7.3"
  7167 + resolved "http://npm.yohops.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
  7168 +
7165 spdx-correct@^3.0.0: 7169 spdx-correct@^3.0.0:
7166 version "3.1.0" 7170 version "3.1.0"
7167 resolved "http://npm.yohops.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4" 7171 resolved "http://npm.yohops.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4"