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