Authored by htoooth

Merge branch 'feature/cart-product-info' of http://git.yoho.cn/fe/yohobuy-node

  1 +/**
  2 + * Created by TaoHuang on 2016/10/19.
  3 + */
  4 +
  5 +'use strict';
  6 +
  7 +const service = require('../models/cart-service');
  8 +
  9 +const getProductInfo = (req, res, next) => {
  10 + let pid = req.query.productId || '';
  11 +
  12 + service.getProductInfoAsync(pid).then((result) => {
  13 + return res.render('goods-detail', Object.assign({
  14 + layout: false
  15 + }, result));
  16 + }).catch(next);
  17 +};
  18 +
  19 +
  20 +module.exports = {
  21 + getProductInfo
  22 +};
  1 +/**
  2 + * sub app cart
  3 + * @author: htoooth<ht.anglenx@gmail.com>
  4 + * @date: 2016/10/19
  5 + */
  6 +
  7 +'use strict';
  8 +
  9 +var express = require('express'),
  10 + path = require('path'),
  11 + hbs = require('express-handlebars');
  12 +
  13 +var app = express();
  14 +
  15 +// set view engin
  16 +var doraemon = path.join(__dirname, '../../doraemon/views'); //parent view root
  17 +
  18 +app.on('mount', function(parent) {
  19 + delete parent.locals.settings; // 不继承父 App 的设置
  20 + Object.assign(app.locals, parent.locals);
  21 +});
  22 +
  23 +app.set('views', path.join(__dirname, 'views/action'));
  24 +app.engine('.hbs', hbs({
  25 + extname: '.hbs',
  26 + defaultLayout: 'layout',
  27 + layoutsDir: doraemon,
  28 + partialsDir: [path.join(__dirname, 'views/partial'), `${doraemon}/partial`],
  29 + helpers: global.yoho.helpers
  30 +}));
  31 +
  32 +// router
  33 +app.use(require('./router'));
  34 +
  35 +module.exports = app;
  1 +/**
  2 + * 商品详情models
  3 + * @author: xuqi<qi.xu@yoho.cn>
  4 + * @date: 2016/5/6
  5 + */
  6 +
  7 +'use strict';
  8 +
  9 +const Promise = require('bluebird');
  10 +const co = Promise.coroutine;
  11 +const _ = require('lodash');
  12 +const helpers = global.yoho.helpers;
  13 +
  14 +const productAPI = require('./product-api');
  15 +
  16 +const _getProductIntroAsync = (productSkn) => {
  17 + return co(function * () {
  18 + let result = yield Promise.props({
  19 + sizeInfo: productAPI.sizeInfoAsync(productSkn)
  20 + });
  21 +
  22 + return result;
  23 + })();
  24 +};
  25 +
  26 +/**
  27 + * 获得sku商品数据
  28 + */
  29 +const _getSkuDataByProductBaseInfo = (data) => {
  30 + let totalStorageNum = 0;
  31 + let skuGoods = null;// sku商品
  32 + let defaultImage = '';// 默认图
  33 + let chooseSkuFlag = false; // 选中状态
  34 +
  35 + if (_.isEmpty(_.get(data, 'goods_list', []))) {
  36 + return {
  37 + totalStorageNum,
  38 + skuGoods,
  39 + defaultImage
  40 + };
  41 + }
  42 +
  43 + skuGoods = _.get(data, 'goods_list', []).reduce((acc, cur, pos)=> {
  44 +
  45 + // 如果status为0,即skc下架时就跳过该商品$value['status'] === 0
  46 + let goodsGroup = {};
  47 +
  48 + if (_.isEmpty(cur.color_image)) {
  49 + return acc;
  50 + }
  51 +
  52 + if (cur.images_list) {
  53 + // 商品列表
  54 + goodsGroup.productSkc = cur.product_skc;
  55 + goodsGroup.src = helpers.image(cur.color_image, 40, 40);
  56 + goodsGroup.title = `${_.trim(data.product_name)} ${cur.color_name}`;
  57 + goodsGroup.name = cur.color_name;
  58 + goodsGroup.focus = false;
  59 + goodsGroup.total = 0;
  60 + goodsGroup.thumbs = [];
  61 + goodsGroup.size = [];
  62 + }
  63 +
  64 + _.get(cur, 'images_list', []).forEach((good) => {
  65 + if (good.image_url) {
  66 + goodsGroup.thumbs.push({
  67 + url: '',
  68 + shower: helpers.image(good.image_url, 420, 560),
  69 + img: helpers.image(good.image_url, 75, 100)
  70 + });
  71 + }
  72 + });
  73 +
  74 + // 缩略图空,不显示
  75 + if (_.isEmpty(goodsGroup.thumbs)) {
  76 + return acc;
  77 + }
  78 +
  79 + // 默认第一张图片
  80 + if (pos === 0) {
  81 + defaultImage = helpers.image(cur.color_image, 420, 560);
  82 + }
  83 +
  84 + // 商品的尺码列表
  85 + _.get(cur, 'size_list', []).forEach((size) => {
  86 + if (data.attribute === 3) {
  87 + // 虚拟商品,门票默认最大为4,
  88 + size.storage_number = size.storage_number > 4 ? 4 : size.storage_number;
  89 + }
  90 +
  91 + // 如果status为0,即skc下架时就跳过该商品
  92 + if (cur.status === 0) {
  93 + size.storage_number = 0;
  94 + }
  95 +
  96 + goodsGroup.size.push({
  97 + name: size.size_name,
  98 + sku: size.product_sku,
  99 + num: _.parseInt(size.storage_number),
  100 + goodsId: size.size_id
  101 + });
  102 +
  103 + // 单个sku商品的总数
  104 + goodsGroup.total += _.parseInt(size.storage_number);
  105 +
  106 + if (goodsGroup.total > 0 && !chooseSkuFlag) { // 默认选中该sku商品
  107 + goodsGroup.focus = true;
  108 + chooseSkuFlag = true;// 选中sku商品
  109 + }
  110 +
  111 + totalStorageNum += _.parseInt(size.storage_number);
  112 +
  113 + });
  114 +
  115 + acc.push(goodsGroup);
  116 + return acc;
  117 + }, []);
  118 +
  119 + if (!_.isEmpty(skuGoods) && !chooseSkuFlag) { // 没有选中一个sku商品,默认选中第一个sku商品
  120 + _.head(skuGoods).focus = true;
  121 + }
  122 +
  123 + return {
  124 + defaultImage: defaultImage,
  125 + skuGoods: skuGoods,
  126 + totalStorageNum: totalStorageNum
  127 + };
  128 +};
  129 +
  130 +/**
  131 + * 使sizeBoList id以 sizeAttributeBos id顺序一样
  132 + * @param sizeInfoBo
  133 + */
  134 +const _sizeInfoBoSort = (sizeInfoBo) => {
  135 + if (!sizeInfoBo.sizeBoList || !sizeInfoBo.sizeAttributeBos) {
  136 + return {};
  137 + }
  138 +
  139 + _.get(sizeInfoBo, 'sizeBoList', []).forEach((sizeBoList, sizek)=> {
  140 + let sortAttr = {};
  141 +
  142 + sizeBoList.sortAttributes.forEach(sortAttributes => {
  143 + sortAttr[sortAttributes.id] = sortAttributes;
  144 + });
  145 +
  146 + sizeInfoBo.sizeBoList[sizek].sortAttributes = sortAttr;
  147 + });
  148 +
  149 + _.get(sizeInfoBo, 'sizeBoList', []).forEach((sizeBoList, sizek)=> {
  150 + let sortAttr = [];
  151 +
  152 + sizeInfoBo.sizeAttributeBos.forEach(val => {
  153 + if (sizeBoList.sortAttributes[val.id]) {
  154 + sortAttr.push(sizeBoList.sortAttributes[val.id]);
  155 + }
  156 + });
  157 +
  158 + sizeInfoBo.sizeBoList[sizek].sortAttributes = sortAttr;
  159 +
  160 + });
  161 +
  162 + return sizeInfoBo;
  163 +};
  164 +
  165 +/**
  166 + * 获取尺寸信息
  167 + * @param sizeInfo
  168 + * @returns {{}}
  169 + */
  170 +const _getSizeData = (sizeInfo) => {
  171 +
  172 + // 尺码信息
  173 + if (!_.has(sizeInfo, 'sizeInfoBo')) {
  174 + return {};
  175 + }
  176 +
  177 + sizeInfo.sizeInfoBo = _sizeInfoBoSort(sizeInfo.sizeInfoBo);
  178 +
  179 + let boyReference = _.get(sizeInfo, 'productExtra.boyReference', false);
  180 + let girlReference = _.get(sizeInfo, 'productExtra.girlReference', false);
  181 + let gender = _.get(sizeInfo, 'productDescBo.gender', 3);
  182 + let referenceName = (function() {
  183 + if (gender === 3 && boyReference) {
  184 + return '参考尺码(男)';
  185 + } else if (gender === 3 && girlReference) {
  186 + return '参考尺码(女)';
  187 + } else {
  188 + return '参考尺码';
  189 + }
  190 + }());
  191 +
  192 + // 判断是否显示参考尺码
  193 + let showReference = (boyReference && _.get(sizeInfo, 'sizeInfoBo.sizeBoList[0].boyReferSize', false)) ||
  194 + (girlReference && _.get(sizeInfo, 'sizeInfoBo.sizeBoList[0].girlReferSize', false));
  195 +
  196 + if (!_.has(sizeInfo, 'sizeInfoBo.sizeAttributeBos')) {
  197 + return {};
  198 + }
  199 +
  200 + // 尺码信息头部
  201 + let size = {
  202 + thead: [{name: '吊牌尺码', id: ''}],
  203 + tbody: []
  204 + };
  205 +
  206 + // 显示参考尺码
  207 + if (showReference) {
  208 + size.thead[1] = {name: referenceName, id: ''};
  209 + }
  210 +
  211 + _.get(sizeInfo, 'sizeInfoBo.sizeAttributeBos', []).forEach((value) => {
  212 + size.thead.push({
  213 + name: value.attributeName || ' ',
  214 + id: value.id
  215 + });
  216 + });
  217 +
  218 + _.get(sizeInfo, 'sizeInfoBo.sizeBoList', []).forEach((value) => {
  219 + let sizes = [];
  220 +
  221 + // 吊牌尺码
  222 + sizes.push(value.sizeName);
  223 +
  224 + // 判断是否显示参考尺码
  225 + if (boyReference && (gender === 1 || gender === 3) && showReference) {
  226 + sizes.push(_.get(value, 'boyReferSize.referenceName', ' '));
  227 + } else if (girlReference && (gender === 2 || gender === 3) && showReference) {
  228 + sizes.push(_.get(value, 'girlReferSize.referenceName', ' '));
  229 + } else {
  230 + if (size.thead[1] && showReference) {
  231 + size.thead[1] = {};
  232 + }
  233 + }
  234 +
  235 + // 其他尺码信息
  236 + _.get(value, 'sortAttributes', []).forEach(attr => {
  237 + sizes.push(_.get(attr, 'sizeValue', ' '));
  238 + });
  239 +
  240 + // 尺码信息
  241 + size.tbody.push(sizes);
  242 + });
  243 +
  244 + // 参考尺码为空
  245 + if (_.isEmpty(size.thead[1]) && showReference) {
  246 + // 移除这个值
  247 + size.thead.splice(1, 1);
  248 + }
  249 +
  250 + // 测量方式
  251 + if (sizeInfo.sizeImage) {
  252 + size.sizeImg = sizeInfo.sizeImage;
  253 + }
  254 +
  255 + return size;
  256 +};
  257 +
  258 +/**
  259 + * 商品尺码信息
  260 + *
  261 + * @param productSkn
  262 + * @param maxSortId
  263 + * @return object
  264 + */
  265 +const _getIntroInfo = (productSkn, additionalData)=> {
  266 + if (!productSkn) {
  267 + return {};
  268 + }
  269 +
  270 + let sizeInfo = additionalData.sizeInfo;
  271 +
  272 + if (_.isEmpty(sizeInfo)) {
  273 + return {};
  274 + }
  275 +
  276 + let result = {};
  277 +
  278 + // 尺寸数据
  279 + result.size = _getSizeData(sizeInfo);
  280 +
  281 + return result;
  282 +};
  283 +
  284 +/**
  285 + * 详情页数据格式化
  286 + * @param origin Object 原始数据
  287 + * @return result Object 格式化数据
  288 + */
  289 +const _detailDataPkg = (origin) => {
  290 + return co(function*() {
  291 + if (_.isEmpty(origin) || _.isEmpty(origin)) {
  292 + return {};
  293 + }
  294 +
  295 + let result = {};
  296 +
  297 + let propOrigin = _.partial(_.get, origin);
  298 +
  299 + // 商品名称
  300 + if (!propOrigin('product_name')) {
  301 + return result;
  302 + }
  303 +
  304 + result.name = propOrigin('product_name');
  305 + result.skn = propOrigin('product_skn');
  306 + result.productId = propOrigin('product_id');
  307 +
  308 + // 商品价格
  309 + result.marketPrice = propOrigin('format_market_price');
  310 + result.salePrice = propOrigin('format_sales_price');
  311 + result.hasOtherPrice = true;
  312 +
  313 + if (result.salePrice === '0') {
  314 + delete result.salePrice;
  315 + result.hasOtherPrice = false;
  316 + }
  317 +
  318 + // 上市期
  319 + if (propOrigin('expect_arrival_time')) {
  320 + result.arrivalDate = `${propOrigin('expect_arrival_time')}月`;
  321 + result.presalePrice = propOrigin('format_sales_price');
  322 + delete result.salePrice;
  323 + result.hasOtherPrice = false;
  324 + }
  325 +
  326 + // sku商品信息
  327 + let skuData = _getSkuDataByProductBaseInfo(origin);
  328 +
  329 + // 商品购买状态
  330 + let soldOut = !!(propOrigin('status') === 0 || skuData.totalStorageNum === 0);
  331 + let notForSale = propOrigin('attribute') === 2; // 非卖品
  332 + let virtualGoods = propOrigin('attribute') === 3; // 虚拟商品
  333 +
  334 + if (!soldOut && !notForSale && !virtualGoods) {
  335 + result.addToCart = 1;
  336 + }
  337 +
  338 + result.colors = skuData.skuGoods;
  339 +
  340 + return result;
  341 + })();
  342 +};
  343 +
  344 +/**
  345 + * 获取某一个商品详情主页面
  346 + */
  347 +const getProductInfoAsync = (pid) => {
  348 + return co(function * () {
  349 + if (!pid) {
  350 + return {};
  351 + }
  352 +
  353 + // 获取商品基本信息
  354 + let productData = yield productAPI.getProductAsync(pid);
  355 +
  356 + if (_.isEmpty(productData.data)) {
  357 + return Promise.reject({
  358 + code: 404,
  359 + message: 'app.product.data api wrong'
  360 + });
  361 + }
  362 +
  363 + let productSkn = _.get(productData, 'data.product_skn');
  364 +
  365 + let requestData = yield Promise.all([
  366 + _getProductIntroAsync(productSkn), // 商品详细介绍
  367 + _detailDataPkg(productData.data) // 商品详细价格
  368 + ]);
  369 +
  370 + let productDescription = requestData[0];
  371 + let productInfo = requestData[1];
  372 +
  373 + let intro = _getIntroInfo(productSkn, productDescription);
  374 +
  375 + return Object.assign(productInfo, intro);
  376 + })();
  377 +};
  378 +
  379 +module.exports = {
  380 + getProductInfoAsync // 获取某一个商品详情主页面
  381 +};
  1 +/**
  2 + * Created by TaoHuang on 2016/10/19.
  3 + */
  4 +
  5 +const api = global.yoho.API;
  6 +
  7 +const sizeInfoAsync = skn => {
  8 + return api.get('', {
  9 + method: 'h5.product.intro',
  10 + productskn: skn
  11 + });
  12 +
  13 +};
  14 +
  15 +/**
  16 + * 获得产品信息
  17 + * @param pid
  18 + * @returns {Promise.<type>}
  19 + */
  20 +const getProductAsync = (pid) => {
  21 +
  22 + return api.get('', {
  23 + method: 'app.product.data',
  24 + product_id: pid
  25 + });
  26 +};
  27 +
  28 +module.exports = {
  29 + sizeInfoAsync,
  30 + getProductAsync
  31 +};
  1 +/**
  2 + * router of sub app cart
  3 + * @author: htoooth<ht.anglenx@gmail.com>
  4 + * @date: 2016/10/19
  5 + */
  6 +
  7 +'use strict';
  8 +
  9 +const router = require('express').Router(); // eslint-disable-line
  10 +const cRoot = './controllers';
  11 +
  12 +const cart = require(`${cRoot}/cart`);
  13 +
  14 +router.get('/index/getProductInfo', cart.getProductInfo);
  15 +
  16 +// Your controller here
  17 +
  18 +module.exports = router;
  1 +<div class="detail-header">
  2 + <span class="colse">X关闭</span>
  3 +</div>
  4 +<div class="detail-body">
  5 + <span class="magnify"></span>
  6 + {{#colors}}
  7 + <div class="detail-bigpic {{#unless focus}}none{{/unless}}">
  8 + {{#thumbs}}
  9 + <div class="bigpic">
  10 + <img src="{{shower}}">
  11 + </div>
  12 + {{/thumbs}}
  13 + <div class="piclist">
  14 + <span class="pre"></span>
  15 + <div class="con">
  16 + <ul>
  17 + {{#thumbs}}
  18 + <li><img src="{{img}}"></li>
  19 + {{/thumbs}}
  20 + </ul>
  21 + </div>
  22 + <span class="next"></span>
  23 + </div>
  24 + </div>
  25 + {{/colors}}
  26 + <div class="detail-info">
  27 + <div class="title">
  28 + <h2>{{name}}</h2>
  29 + </div>
  30 + <div class="type">
  31 + <span class="type-s">新品</span>
  32 + </div>
  33 + <div class="price">
  34 +
  35 + {{#if salePrice}}
  36 + <span class="oldprice">原价:<del>¥{{marketPrice}}</del></span>
  37 + <span class="newprice">现价:<b class="promotion-price">¥{{salePrice}}</b></span>
  38 + {{^}}
  39 + <span class="newprice {{#presalePrice}}none{{/presalePrice}}">原价:<b class="promotion-price">¥{{marketPrice}}</b></span>
  40 + {{/if}}
  41 +
  42 + {{#if presalePrice}}
  43 + <span class="oldprice">原价:<del>¥{{marketPrice}}</del></span>
  44 + <span class="newprice">预售价:<b class="promotion-price">¥{{presalePrice}}</b></span>
  45 + {{/if}}
  46 + {{#arrivalDate}}
  47 + <span class="arrivalDate">上市期:{{arrivalDate}}</span>
  48 + {{/arrivalDate}}
  49 + </div>
  50 + <div class="order">
  51 + <dl>
  52 + <dd class="colorBox">选颜色:</dd>
  53 + <dt>
  54 + <div class="colorBox">
  55 + <ul>
  56 + {{#colors}}
  57 + <li class="color">
  58 + <p class="{{#if focus}}atcive{{/if}}"><span></span><img src="{{src}}"></p>
  59 + <span>{{name}}</span>
  60 + </li>
  61 + {{/colors}}
  62 + </ul>
  63 + </div>
  64 + </dt>
  65 + <dd class="">选尺码:</dd>
  66 + <dt>
  67 + {{#colors}}
  68 + <div class="showSizeBox {{#unless focus}}none{{/unless}}">
  69 + {{#size}}
  70 + <span data-sku="{{sku}}" data-num="{{num}}">{{name}}</span>
  71 + {{/size}}
  72 + </div>
  73 + {{/colors}}
  74 + </dt>
  75 + <dd>选件数:</dd>
  76 + <dt>
  77 + <div class="amount_wrapper">
  78 + <i class="amount cut"></i>
  79 + <input type="text" id="mnum" class="mnum" value="1" readonly="readonly">
  80 + <i class="amount add"></i>
  81 +
  82 + </div>
  83 + </dt>
  84 + </dl>
  85 + </div>
  86 + <div class="submit">
  87 + <input class="addcart" type="button">
  88 + <input class="btn_pre_sale none" type="button">
  89 + <input class="btn_sellout none" type="button">
  90 + <input class="fav_count" type="button">
  91 + </div>
  92 + </div>
  93 +
  94 + <div class="detail-size">
  95 + <h3>尺码信息<span>(单位:厘米)</span></h3>
  96 + {{# size}}
  97 + <table>
  98 + <thead>
  99 + <tr>
  100 + {{# thead}}
  101 + <td width="{{width}}">{{name}}</td>
  102 + {{/ thead}}
  103 + </tr>
  104 + </thead>
  105 + <tbody>
  106 + {{# tbody}}
  107 + <tr>
  108 + {{#each .}}
  109 + <td>{{.}}</td>
  110 + {{/each}}
  111 + </tr>
  112 + {{/ tbody}}
  113 + </tbody>
  114 + </table>
  115 + {{/ size}}
  116 + <div class="size-info">
  117 + ※ 以上尺寸为实物实际测量,因测量方式不同会有略微误差,相关数据仅作参考,以收到实物为准。
  118 + </div>
  119 + </div>
  120 +</div>
  121 +<input value="{{addToCart}}" id="addToCart" type="hidden" />
@@ -178,7 +178,7 @@ const local = { @@ -178,7 +178,7 @@ const local = {
178 return `${config.siteUrl}/home`; 178 return `${config.siteUrl}/home`;
179 } 179 }
180 }()); 180 }());
181 - 181 + console.log(user.uid);
182 AuthHelper.syncUserSession(user.uid, req, res).then(() => { 182 AuthHelper.syncUserSession(user.uid, req, res).then(() => {
183 res.json({ 183 res.json({
184 code: 200, 184 code: 200,
@@ -16,4 +16,5 @@ module.exports = app => { @@ -16,4 +16,5 @@ module.exports = app => {
16 app.use('/home', require('./apps/home')); // 会员中心 16 app.use('/home', require('./apps/home')); // 会员中心
17 app.use('/brands', require('./apps/brands')); 17 app.use('/brands', require('./apps/brands'));
18 app.use('/guang', require('./apps/guang')); 18 app.use('/guang', require('./apps/guang'));
  19 + app.use('/cart', require('./apps/cart'));// 购物车
19 }; 20 };