Authored by 郭成尧

Merge remote-tracking branch 'remotes/origin/develop' into feature/brand

  1 +/**
  2 + *
  3 + * @author: Aiden Xu<aiden.xu@yoho.cn>
  4 + * @date: 2016/07/19
  5 + */
  6 +'use strict';
  7 +
  8 +// const _ = require('lodash');
  9 +
  10 +// const helpers = global.yoho.helpers;
  11 +const api = global.yoho.API;
  12 +const _ = require('lodash');
  13 +
  14 +/**
  15 + * 商品详情
  16 + */
  17 +const component = {
  18 + index(req, res) {
  19 + const pid = req.params[0], goodsId = req.params[1];
  20 +
  21 + res.render('detail', {
  22 + module: 'product',
  23 + page: 'detail',
  24 + pid: pid,
  25 + goodsId: goodsId
  26 + });
  27 + },
  28 + product(req, res, next) {
  29 + const pid = req.params[0];// , goodsId = req.params[1];
  30 +
  31 + let params = {
  32 + productId: _.toString(pid),
  33 + method: 'h5.product.data' // TODO replace this to 'app.product.data'
  34 +
  35 + };
  36 +
  37 + api.get('', params).then(result => {
  38 + res.json(result);
  39 + }).catch(next);
  40 + },
  41 + intro(req, res, next) {
  42 + let params = {
  43 + method: 'h5.product.intro', // TODO replace this to 'app.product.intro'
  44 + productskn: req.query.skn,
  45 + udid: 'f528764d624db129b32c21fbca0cb8d6'
  46 + };
  47 +
  48 + api.get('', params).then(result => {
  49 + res.json(result);
  50 + }).catch(next);
  51 + },
  52 +
  53 + /**
  54 + * 加入购物车接口
  55 + *
  56 + */
  57 + addToCart(req, res, next) {
  58 + let params = {
  59 + method: 'app.Shopping.add',
  60 + product_sku: req.body.productSku, // 商品SKU
  61 + buy_number: req.body.buyNumber, // 购买数量
  62 + goods_type: req.body.goodsType || 0, // 商品类型,0表示普通商品,1表示加价购商品
  63 + edit_product_sku: req.body.isEdit || 0, // 是否是编辑商品SKU,0表示不是编辑
  64 + selected: 'Y',
  65 + promotion_id: req.body.promotionId || null, // 促销id,默认null(加价购有关)
  66 + uid: req.user.uid || null, // TODO: fix uid
  67 + shopping_key: global.yoho.cookie.getShoppingKey(req)
  68 + };
  69 +
  70 + api.get('', params).then(result => {
  71 + res.json(result);
  72 + }).catch(next);
  73 + },
  74 +
  75 + getFavorite(req, res, next) {
  76 + api.get('', {}).then(result => {
  77 + res.json(result);
  78 + }).catch(next);
  79 + },
  80 +
  81 + /**
  82 + * 收藏
  83 + *
  84 + * @param req
  85 + * @param res
  86 + * @param next
  87 + */
  88 + addFavorite(req, res, next) {
  89 + let params = {
  90 + method: 'app.Shopping.addfavorite',
  91 + product_sku_list: req.body.sku,
  92 + uid: req.user.uid || 8050378 // TODO: fix this hard coded uid
  93 + };
  94 +
  95 + api.get('', params).then(result => {
  96 + res.json(result);
  97 + }).catch(next);
  98 + },
  99 +
  100 + /**
  101 + * 获取购物车数量
  102 + *
  103 + * @param req
  104 + * @param res
  105 + * @param next
  106 + */
  107 + getCartCount: (req, res, next) => {
  108 + let params = {
  109 + method: 'app.Shopping.count',
  110 + shopping_key: global.yoho.cookie.getShoppingKey(req),
  111 + uid: req.user.uid || 0 // TODO fix uid
  112 + };
  113 +
  114 + api.get('', params).then(result => {
  115 + res.json(result);
  116 + }).catch(next);
  117 + }
  118 +};
  119 +
  120 +module.exports = component;
1 /** 1 /**
2 - * sub app product  
3 - * @author: chen xuan<xuan.chen@yoho.cn>  
4 - * @date: 2016/07/19 2 + * sub app
  3 + * @author: Bi Kai<kai.bi@yoho.cn>
  4 + * @date: 2016/05/09
5 */ 5 */
6 6
7 -var express = require('express'),  
8 - path = require('path'),  
9 - hbs = require('express-handlebars'); 7 +const express = require('express');
  8 +const path = require('path');
  9 +const hbs = require('express-handlebars');
10 10
11 -var app = express(); 11 +const app = express();
12 12
13 -// set view engin  
14 -var doraemon = path.join(__dirname, '../../doraemon/views'); // parent view root 13 +// set view engine
  14 +const doraemon = path.join(__dirname, '../../doraemon/views'); // parent view root
15 15
16 app.on('mount', function(parent) { 16 app.on('mount', function(parent) {
17 delete parent.locals.settings; // 不继承父 App 的设置 17 delete parent.locals.settings; // 不继承父 App 的设置
1 /** 1 /**
2 * router of sub app product 2 * router of sub app product
  3 + * @author: Aiden Xu<aiden.xu@yoho.cn>
3 * @author: chen xuan<xuan.chen@yoho.cn> 4 * @author: chen xuan<xuan.chen@yoho.cn>
4 * @date: 2016/07/19 5 * @date: 2016/07/19
5 */ 6 */
6 7
7 'use strict'; 8 'use strict';
8 9
9 -const Router = require('express').Router; 10 +const expressRouter = require('express').Router;
10 const cRoot = './controllers'; 11 const cRoot = './controllers';
11 const productList = require(`${cRoot}/list`); 12 const productList = require(`${cRoot}/list`);
12 13
13 -const router = Router(); 14 +const router = expressRouter();
14 15
15 // 商品列表 16 // 商品列表
16 router.use('/list', (req, res, next) => { 17 router.use('/list', (req, res, next) => {
@@ -21,4 +22,14 @@ router.use('/list', (req, res, next) => { @@ -21,4 +22,14 @@ router.use('/list', (req, res, next) => {
21 router.get('/list', productList.index); 22 router.get('/list', productList.index);
22 router.post('/list', productList.getProducts); 23 router.post('/list', productList.getProducts);
23 24
  25 +// 商品详情controller
  26 +const detail = require(`${cRoot}/detail`);
  27 +
  28 +router.get(/\/pro_([\d]+)_([\d]+)\/(.*)/, detail.index); // 商品详情routers
  29 +router.get(/\/product_([\d]+)_([\d]+)\.json/, detail.product);
  30 +router.get(/\/intro\.json/, detail.intro);
  31 +router.post(/cart.json/, detail.addToCart);
  32 +router.get(/favorite.json/, detail.getFavorite);
  33 +router.post(/favorite.json/, detail.addFavorite);
  34 +router.get(/cart-count.json/, detail.getCartCount);
24 module.exports = router; 35 module.exports = router;
  1 +<div id="app" class="product-page" data-pid="{{pid}}" data-goods-id="{{goodsId}}">
  2 + <app/>
  3 +</div>
@@ -13,20 +13,20 @@ module.exports = { @@ -13,20 +13,20 @@ module.exports = {
13 app: 'h5', 13 app: 'h5',
14 appVersion: '4.6.0', // 调用api的版本 14 appVersion: '4.6.0', // 调用api的版本
15 port: 6004, 15 port: 6004,
16 - siteUrl: '//m.yohobuy.com', 16 + siteUrl: '//m.yohoblk.com',
17 domains: { 17 domains: {
18 api: 'http://devapi.yoho.cn:58078/', 18 api: 'http://devapi.yoho.cn:58078/',
19 - service: 'http://192.168.102.202:8080/gateway/' 19 + service: 'http://devservice.yoho.cn:58077/'
20 }, 20 },
21 subDomains: { 21 subDomains: {
22 - host: '.m.yohobuy.com',  
23 - default: '//m.yohobuy.com',  
24 - guang: '//guang.m.yohobuy.com',  
25 - list: '//list.m.yohobuy.com',  
26 - search: '//search.m.yohobuy.com',  
27 - huodong: '//huodong.m.yohobuy.com',  
28 - activity: '//activity.yohobuy.com',  
29 - index: '//m.yohobuy.com' 22 + host: '.m.yohoblk.com',
  23 + default: '//m.yohoblk.com',
  24 + guang: '//guang.m.yohoblk.com',
  25 + list: '//list.m.yohoblk.com',
  26 + search: '//search.m.yohoblk.com',
  27 + huodong: '//huodong.m.yohoblk.com',
  28 + activity: '//activity.yohoblk.com',
  29 + index: '//m.yohoblk.com'
30 }, 30 },
31 useOneapm: false, 31 useOneapm: false,
32 useCache: false, 32 useCache: false,
1 /** 1 /**
2 * 路由分发 2 * 路由分发
3 - * @author: xuqi<qi.xu@yoho.cn> 3 + *
  4 + * @author: Aiden Xu<aiden.xu@yoho.cn>
4 * @date: 2016/4/27 5 * @date: 2016/4/27
5 */ 6 */
6 7
@@ -13,4 +14,6 @@ module.exports = app => { @@ -13,4 +14,6 @@ module.exports = app => {
13 if (app.locals.devEnv) { 14 if (app.locals.devEnv) {
14 app.use('/example', require('./apps/example')); 15 app.use('/example', require('./apps/example'));
15 } 16 }
  17 +
  18 + app.use('/product', require('./apps/product'));
16 }; 19 };
@@ -107,6 +107,8 @@ class Overlay { @@ -107,6 +107,8 @@ class Overlay {
107 overflow: 'auto' 107 overflow: 'auto'
108 }); 108 });
109 } 109 }
  110 +
  111 + this.settings.onClose();
110 } 112 }
111 } 113 }
112 114
@@ -5,18 +5,42 @@ let Vue = require('yoho-vue'); @@ -5,18 +5,42 @@ let Vue = require('yoho-vue');
5 * 替换参数 5 * 替换参数
6 * 6 *
7 * @example 7 * @example
8 - * value = /{width}/{height}/{model} 8 + * value = /{width}/{height}/{mode}
9 * 9 *
10 * {value | resize 100 200 2} ==> /100/200/2 10 * {value | resize 100 200 2} ==> /100/200/2
11 */ 11 */
12 -Vue.filter('resize', (value, width, height, model)=>{  
13 - return value.replace(/({width}|{height}|{mode})/g, function($0) { 12 +Vue.filter('resize', (value, width, height, mode)=> {
  13 + return value ? value.replace(/(\{width}|\{height}|\{mode})/g, function($0) {
14 const dict = { 14 const dict = {
15 '{width}': width, 15 '{width}': width,
16 '{height}': height, 16 '{height}': height,
17 - '{mode}': model || 2 17 + '{mode}': mode || 2
18 }; 18 };
19 19
20 return dict[$0]; 20 return dict[$0];
21 - }); 21 + }) : '';
  22 +});
  23 +
  24 +/**
  25 + * 性别款式
  26 + *
  27 + * @example
  28 + *
  29 + * {value | gender}
  30 + */
  31 +Vue.filter('clothingGenderIdentity', (value)=> {
  32 + let ret = null;
  33 +
  34 + switch (value) {
  35 + case 1:
  36 + ret = '男款';
  37 + break;
  38 + case 2:
  39 + ret = '女款';
  40 + break;
  41 + default:
  42 + ret = '通用';
  43 + }
  44 +
  45 + return ret;
22 }); 46 });
  1 +const Vue = require('yoho-vue');
  2 +const app = require('product/detail.vue');
  3 +
  4 +require('../common/overlay');
  5 +
  6 +new Vue({
  7 + el: '#app',
  8 + components: {
  9 + app: app
  10 + }
  11 +});
@@ -12,6 +12,11 @@ @@ -12,6 +12,11 @@
12 &.button-solid { 12 &.button-solid {
13 background: $black; 13 background: $black;
14 color: $white; 14 color: $white;
  15 +
  16 + &:disabled {
  17 + background: $grey;
  18 + color: $white;
  19 + }
15 } 20 }
16 21
17 &.button-round { 22 &.button-round {
1 @charset "utf-8"; 1 @charset "utf-8";
2 @import "common/index"; 2 @import "common/index";
3 @import "example/index"; 3 @import "example/index";
  4 +@import "product/index";
4 @import "channel/index"; 5 @import "channel/index";
5 @import "home/index"; 6 @import "home/index";
6 @import "brand/index"; 7 @import "brand/index";
  1 +.product-page {
  2 + background: #f6f6f6;
  3 +}
  1 +.feature-options {
  2 + display: inline-block;
  3 +}
  4 +
  5 +.feature-button {
  6 + min-width: 88px;
  7 + height: 88px;
  8 + min-height: inherit;
  9 + margin-right: 20px;
  10 +
  11 + &:disabled::before {
  12 + }
  13 +}
  1 +.feature-selector {
  2 + background: #FFFFFF;
  3 + width: 100%;
  4 + height: 608px;
  5 + bottom: 0;
  6 + position: fixed;
  7 + padding: 20px 30px 30px 30px;
  8 + z-index: 1001;
  9 + transform: translate3d(0, 100%, 0);
  10 + transition: all 0.1s ease-in-out;
  11 +
  12 + .header {
  13 + height: 120px;
  14 +
  15 + h3 {
  16 + margin: 0;
  17 + max-height: 60px;
  18 + font-weight: 300;
  19 + }
  20 + h4 {
  21 + color: #b0b0b0;
  22 + font-weight: 200;
  23 + font-size: 30px;
  24 + margin-top: 32px;
  25 + margin-bottom: 0;
  26 + }
  27 + .image-box {
  28 + width: 90px;
  29 + height: 120px;
  30 + display: inline-block;
  31 + }
  32 + .text-box {
  33 + display: inline-block;
  34 + margin-left: 24px;
  35 + max-width: 512px;
  36 + }
  37 + }
  38 +
  39 + hr {
  40 + border: none;
  41 + border-top: 1px solid #F0F0F0;
  42 + margin-top: 30px;
  43 + margin-bottom: 20px;
  44 + }
  45 +
  46 + ul {
  47 + list-style: none;
  48 + border: none;
  49 + margin-left: 18px;
  50 + margin-top: 30px;
  51 + margin-bottom: 0;
  52 + padding: 0;
  53 + }
  54 +
  55 + li {
  56 + display: inline-block;
  57 + }
  58 +
  59 + section {
  60 + h4 {
  61 + margin: 0;
  62 + font-size: 25px;
  63 + line-height: 80px;
  64 + display: inline-block;
  65 + }
  66 + }
  67 +
  68 + .add-to-cart {
  69 + width: 100%;
  70 + margin-top: 50px;
  71 + font-size: 27px;
  72 + }
  73 +
  74 + &.slide-in {
  75 + transform: translate3d(0, 0, 0);
  76 + }
  77 +}
  1 +<template>
  2 + <ul class="feature-options">
  3 + <li v-for="item in options">
  4 + <button :class="{ 'button-solid': value && value === item.value}"
  5 + :disabled="item.disabled"
  6 + @click="selectOption(item.value)"
  7 + class="button feature-button">
  8 + {{item.text}}
  9 + </button>
  10 + </li>
  11 + </ul>
  12 +</template>
  13 +<style src="./css/feature-options.css"></style>
  14 +<script src="./js/feature-options.js"></script>
  1 +<template>
  2 + <div class="feature-selector" :class="{ 'slide-in': isVisible }">
  3 + <div class="header">
  4 + <div class="image-box">
  5 + <img :src="selection.thumbnail | resize 45 60"/>
  6 + </div>
  7 + <div class="text-box">
  8 + <h3>{{entity.productName}}</h3>
  9 + <h4>{{entity.productPriceBo.formatSalesPrice}}</h4>
  10 + </div>
  11 + </div>
  12 +
  13 + <hr>
  14 +
  15 + <div>
  16 + <section>
  17 + <h4>颜色</h4>
  18 + <feature-options name="color" :options="colors" :selection="selection.color"></feature-options>
  19 + </section>
  20 + <section>
  21 + <h4>尺码</h4>
  22 + <feature-options name="size" :options="sizes" :selection="selection.size"></feature-options>
  23 + </section>
  24 + <button @click="addToCart()"
  25 + class="button button-solid add-to-cart">加入购物袋
  26 + </button>
  27 + </div>
  28 + </div>
  29 +</template>
  30 +<style src="./css/feature-selector.css"></style>
  31 +<script src="./js/feature-selector.js"></script>
  1 +module.exports = {
  2 + props: {
  3 + options: Array,
  4 + name: String,
  5 + selection: null
  6 + },
  7 + data() {
  8 + return {
  9 + value: ''
  10 + };
  11 + },
  12 + watch: {
  13 + selection() {
  14 + this.value = this.selection;
  15 + }
  16 + },
  17 + methods: {
  18 + selectOption: function(opt) {
  19 + this.value = opt;
  20 + this.$parent.$emit(`feature:${this.name}.select`, opt);
  21 + }
  22 + }
  23 +};
  1 +module.exports = {
  2 + init() {
  3 + },
  4 + props: {
  5 + isVisible: Boolean,
  6 + entity: Object,
  7 + onAddToCart: Function
  8 + },
  9 + watch: {
  10 + isVisible() {
  11 + const self = this;
  12 +
  13 + if (this.isVisible) {
  14 + this.overlay = $.overlay({
  15 + onClose: function() {
  16 + self.isVisible = false;
  17 + }
  18 + });
  19 +
  20 + this.overlay.show();
  21 + } else {
  22 + this.overlay.hide();
  23 + this.$parent.$emit('featureselector.close');
  24 + }
  25 + },
  26 + entity() {
  27 + const thumbnails = {};
  28 + const selection = {};
  29 + const colorSizes = {};
  30 + const stocks = {};
  31 +
  32 + // 更新颜色
  33 + this.colors = this.entity.goodsList.filter((goods)=> {
  34 + // 确保商品启用
  35 + return goods.status !== 0;
  36 + }).map((goods)=> {
  37 + // 缩略图
  38 + thumbnails[goods.colorId] = goods.colorImage;
  39 +
  40 + // 更新颜色对应尺码
  41 + colorSizes[goods.colorId] = goods.goodsSizeBoList.map((size)=> {
  42 + if (!stocks[goods.colorId]) {
  43 + stocks[goods.colorId] = 0;
  44 + }
  45 +
  46 + // 默认选中有库存的第一个颜色尺码
  47 + if (size.goodsSizeStorageNum > 0) {
  48 + if (!selection.color) {
  49 + selection.color = goods.colorId;
  50 + }
  51 +
  52 + if (!selection.size && size.goodsSizeStorageNum > 0) {
  53 + selection.size = size.goodsSizeSkuId;
  54 + }
  55 +
  56 + // 计算所有尺码的库存
  57 + stocks[goods.colorId] += size.goodsSizeStorageNum;
  58 + }
  59 +
  60 + return {
  61 + text: size.sizeName,
  62 + value: size.goodsSizeSkuId,
  63 + disabled: size.goodsSizeStorageNum === 0
  64 + };
  65 + });
  66 +
  67 + return {
  68 + text: goods.colorName,
  69 + value: goods.colorId,
  70 + disabled: stocks[goods.colorId] === 0 // 是否售完
  71 + };
  72 + });
  73 +
  74 + this.sizes = colorSizes[selection.color];
  75 + this.colorSizes = colorSizes;
  76 + this.thumbnails = thumbnails;
  77 +
  78 + // 选择默认值
  79 + this.$emit('feature:color.select', selection.color);
  80 + this.$emit('feature:size.select', selection.size);
  81 + }
  82 + },
  83 + data() {
  84 + return {
  85 + colors: [],
  86 + sizes: [],
  87 + colorSizes: {},
  88 + thumbnails: {},
  89 + selection: {
  90 + color: null,
  91 + size: null,
  92 + thumbnail: ''
  93 + }
  94 + };
  95 + },
  96 + components: {
  97 + featureOptions: require('../feature-options.vue')
  98 + },
  99 + created() {
  100 + // 选择颜色
  101 + this.$on('feature:color.select', (opt)=> {
  102 + const selection = {
  103 + color: opt,
  104 + size: ((color, size)=> {
  105 + // 切换颜色后选择匹配的尺码
  106 + const sizes = this.colorSizes[color];
  107 +
  108 + if (sizes && sizes.length > 0) {
  109 + const oldSizes = sizes.filter((item) => {
  110 + return item.value === size;
  111 + });
  112 +
  113 + if (oldSizes && oldSizes.length > 0) {
  114 + const newSizes = this.colorSizes[opt];
  115 +
  116 + const matchedSize = newSizes.filter((item)=> {
  117 + return !item.disabled && item.text === oldSizes[0].text;
  118 + });
  119 +
  120 + if (matchedSize && matchedSize.length > 0) {
  121 + return matchedSize[0].value;
  122 + }
  123 + }
  124 + }
  125 +
  126 + return null;
  127 + })(this.selection.color, this.selection.size),
  128 + thumbnail: this.thumbnails[opt]
  129 + };
  130 +
  131 + this.sizes = this.colorSizes[opt];
  132 + Object.assign(this.selection, selection);
  133 + });
  134 +
  135 + // 选择尺码
  136 + this.$on('feature:size.select', (opt)=> {
  137 + const selection = {
  138 + size: opt
  139 + };
  140 +
  141 + Object.assign(this.selection, selection);
  142 + });
  143 + },
  144 + methods: {
  145 + /**
  146 + * 将当前选择添加到购物车
  147 + */
  148 + addToCart() {
  149 + // console.log(`${this.selection.color}:${this.selection.size}`);
  150 + const sku = this.selection.size;
  151 +
  152 + $.post('/product/cart.json', {
  153 + productSku: sku,
  154 + buyNumber: 1
  155 + }).then((result)=> {
  156 + this.onAddToCart(result);
  157 + });
  158 + }
  159 + }
  160 +};
  1 +.show-box .brand {
  2 + max-height: 108px;
  3 + line-height: 48px;
  4 + overflow: hidden;
  5 +
  6 + img {
  7 + vertical-align: middle;
  8 + }
  9 +
  10 + h2 {
  11 + font-size: 28px;
  12 + vertical-align: middle;
  13 + margin-left: 30px;
  14 + }
  15 +
  16 + a {
  17 + float: right;
  18 + margin-top: 12px;
  19 + font-size: 28px;
  20 + color: #b0b0b0;
  21 + display: inline-block;
  22 + vertical-align: middle;
  23 + }
  24 +}
  25 +
  26 +.separator {
  27 + text-align: center;
  28 + color: #c4c4c4;
  29 + height: 110px;
  30 + line-height: 110px;
  31 + margin-bottom: -20px;
  32 +
  33 + span {
  34 + background: #f6f6f6;
  35 + padding: 0 15px;
  36 + }
  37 +
  38 + hr {
  39 + max-width: 512px;
  40 + margin-top: -55px;
  41 + border: none;
  42 + border-top: 1px solid #eeeeee;
  43 + }
  44 +}
  45 +
  46 +i.info {
  47 + font-style: normal;
  48 + color: #b0b0b0;
  49 + margin-top: 24px;
  50 + display: block;
  51 + font-size: 18px;
  52 +}
  53 +
  54 +.image-box {
  55 + background: #ffffff;
  56 +}
  57 +
  58 +.title-box {
  59 + text-align: center;
  60 + margin-bottom: 50px;
  61 + max-height: 195px;
  62 +
  63 + h1 {
  64 + text-align: center;
  65 + font-size: 30px;
  66 + line-height: 48px;
  67 + font-weight: normal;
  68 + max-width: 580px;
  69 + margin: 30px auto 30px auto;
  70 + }
  71 +
  72 + i.price {
  73 + color: #b0b0b0;
  74 + font-size: 32px;
  75 + font-weight: lighter;
  76 + font-style: normal;
  77 +
  78 + &.strike-through {
  79 + text-decoration: line-through;
  80 + }
  81 +
  82 + &.highlight {
  83 + color: #d0021b;
  84 + }
  85 + }
  86 +}
  87 +
  88 +.control-box {
  89 + display: flex;
  90 + flex-direction: row;
  91 + justify-content: space-around;
  92 + align-items: stretch;
  93 + position: fixed;
  94 + width: 100%;
  95 + height: 99px;
  96 + bottom: 0;
  97 +
  98 + .control-button {
  99 + min-width: 100px;
  100 + border: none;
  101 + border-top: 1px solid #CCC;
  102 +
  103 + .icon {
  104 + font-size: 40px;
  105 + }
  106 +
  107 + }
  108 +
  109 + .control-button:first-child {
  110 + border-right: 1px solid #CCC;
  111 + }
  112 +
  113 + .button-solid {
  114 + font-size: 26px;
  115 + }
  116 +
  117 +}
  118 +
  119 +.horizon-wrapper {
  120 + overflow-x: scroll;
  121 +}
  122 +
  123 +.table {
  124 + border-collapse: collapse;
  125 +
  126 + th {
  127 + background: #f6f6f6;
  128 + }
  129 +
  130 + th, td {
  131 + border: 1px solid #eeeeee;
  132 + min-width: 170px;
  133 + line-height: 66px;
  134 + text-align: center;
  135 + vertical-align: middle;
  136 + }
  137 +}
  138 +
  139 +.wash-condition {
  140 + display: flex;
  141 + justify-content: space-around;
  142 +}
  143 +
  144 +.wash-condition-item {
  145 + flex: 1;
  146 + text-align: center;
  147 +}
  148 +
  149 +.description {
  150 + font-size: 0;
  151 +
  152 + li {
  153 + font-size: 24px;
  154 + width: 325px;
  155 + line-height: 40px;
  156 + display: inline-block;
  157 + }
  158 +
  159 + .desc-caption {
  160 + color: #c7c7c7;
  161 + min-width: 100px;
  162 + }
  163 +}
  164 +
  165 +.model-avatar {
  166 + vertical-align: middle;
  167 + border-radius: 100%;
  168 +}
  169 +
  170 +.model-name {
  171 + width: 100px;
  172 + display: inline-block;
  173 +}
  174 +
  175 +.badge-tr {
  176 + margin-top: -10px;
  177 +}
  1 +.image-carousel {
  2 + width: 100%;
  3 + height: 1000px;
  4 +
  5 + .swipe {
  6 + height: 100%;
  7 + }
  8 +
  9 + .swipe-indicators {
  10 + left: auto;
  11 + right: 32px;
  12 + }
  13 +
  14 + .swipe-indicator {
  15 + width: 8px;
  16 + height: 8px;
  17 + line-height: 12px;
  18 + display: inline-block;
  19 +
  20 + &.active {
  21 + width: 12px;
  22 + height: 12px;
  23 + background: #000;
  24 + opacity: 0.6;
  25 + margin: -2px 5px;
  26 + }
  27 + }
  28 +}
  1 +.show-box {
  2 + margin-top: 20px;
  3 + background: #ffffff;
  4 + border-top: 1px solid #eeeeee;
  5 + border-bottom: 1px solid #eeeeee;
  6 + padding: 30px;
  7 +
  8 + img {
  9 + max-width: 100%;
  10 + height: auto !important;
  11 + }
  12 +
  13 + p {
  14 + color: #808080;
  15 + font-size: 24px;
  16 + line-height: 48px;
  17 + }
  18 +
  19 + hr {
  20 + border: none;
  21 + border-bottom: 1px solid #eeeeee;
  22 + margin: 32px 0 20px 0;
  23 + }
  24 +
  25 + h2 {
  26 + margin: 0;
  27 + font-size: 32px;
  28 + text-align: left;
  29 + color: #000000;
  30 + font-weight: normal;
  31 + display: inline-block;
  32 +
  33 + + i {
  34 + font-size: 14px;
  35 + font-style: normal;
  36 + color: #b0b0b0;
  37 + font-weight: normal;
  38 + }
  39 + }
  40 +
  41 + ul {
  42 + list-style: none;
  43 + padding: 0;
  44 + }
  45 +
  46 + .image-box {
  47 + float: left;
  48 + }
  49 +
  50 + .text-box {
  51 + float: left;
  52 + margin-left: 20px;
  53 + text-align: center;
  54 + line-height: 36px;
  55 + }
  56 +
  57 + .clear-fix {
  58 + clear: both;
  59 + }
  60 +
  61 + &.first-box {
  62 + margin-top: 0;
  63 + padding: 0;
  64 + }
  65 +
  66 + &.last-box {
  67 + margin-bottom: 99px;
  68 + }
  69 +}
  1 +<template>
  2 + <show-box :is-first="true">
  3 + <image-carousel :goods="entity.goodsList"></image-carousel>
  4 + <div class="title-box">
  5 + <h1>{{entity.productName}}</h1>
  6 +
  7 + <i class="price" :class="{'strike-through': entity.productPriceBo.salesPrice > 0}">{{entity.productPriceBo.formatMarketPrice}}</i>
  8 +
  9 + <i v-if="entity.productPriceBo.salesPrice > 0" class="price highlight">
  10 + {{entity.productPriceBo.formatSalesPrice}}
  11 + </i>
  12 + </div>
  13 + </show-box>
  14 +
  15 + <show-box>
  16 + <div class="brand">
  17 + <img :src="entity.brand.brandIco | resize 110 68" width="55" height="34"/>
  18 +
  19 + <h2>{{entity.brand.brandName}}</h2>
  20 + <a href="#">
  21 + 进入店铺
  22 + <span class="icon icon-right"></span>
  23 + </a>
  24 + </div>
  25 + </show-box>
  26 +
  27 + <div class="separator"><span>继续拖动,查看商品信息</span>
  28 + <hr/>
  29 + </div>
  30 +
  31 + <show-box v-if="intro.productDescBo">
  32 + <h2>商品信息</h2>
  33 + <i>DESCRIPTION</i>
  34 + <hr>
  35 +
  36 + <ul class="description">
  37 + <li>
  38 + <span class="desc-caption">编号:</span>
  39 + <span>{{intro.productDescBo.erpProductId}}</span>
  40 + </li>
  41 + <li>
  42 + <span class="desc-caption">颜色:</span>
  43 + <span>{{intro.productDescBo.colorName}}</span>
  44 + </li>
  45 + <li>
  46 + <span class="desc-caption">性别:</span>
  47 + <span>{{intro.productDescBo.gender | clothingGenderIdentity}}</span>
  48 + </li>
  49 + <li v-for="item in intro.productDescBo.standardBos">
  50 + <span class="desc-caption">{{item.standardName}}:</span> <span>{{item.standardVal}}</span>
  51 + </li>
  52 + </ul>
  53 + </show-box>
  54 +
  55 +
  56 + <show-box v-if="intro.sizeInfoBo">
  57 + <h2>尺码信息</h2>
  58 + <i>SIZE INFO</i>
  59 + <hr>
  60 + <div class="horizon-wrapper">
  61 + <table class="table">
  62 + <thead>
  63 + <th>吊牌尺码</th>
  64 + <th v-for="header in intro.sizeInfoBo.sizeAttributeBos">{{header.attributeName}}</th>
  65 + </thead>
  66 + <tbody>
  67 +
  68 + <tr v-for="size in intro.sizeInfoBo.sizeBoList">
  69 + <td>{{size.sizeName}}</td>
  70 + <td v-for="item in size.sortAttributes">{{item.sizeValue}}</td>
  71 + </tr>
  72 + </tbody>
  73 + </table>
  74 + </div>
  75 +
  76 + <i class="info">提示:左滑查看完整表格信息</i>
  77 + </show-box>
  78 +
  79 + <show-box v-if="intro.sizeImage">
  80 + <h2>测量方式</h2>
  81 + <i>MEASUREMENT METHOD</i>
  82 + <hr>
  83 + <img v-if="intro.sizeImage" :src="intro.sizeImage"/>
  84 + </show-box>
  85 +
  86 + <show-box v-if="intro.modelBos && intro.modelBos.length > 0">
  87 + <h2>模特试穿</h2>
  88 + <i>REFERENCE</i>
  89 + <hr>
  90 + <div class="horizon-wrapper">
  91 + <table class="table">
  92 + <thead>
  93 + <tr>
  94 + <th>模特</th>
  95 + <th>身高</th>
  96 + <th>体重</th>
  97 + <th>三围</th>
  98 + <th>吊牌尺码</th>
  99 + <th>试穿描述</th>
  100 + </tr>
  101 + </thead>
  102 + <tbody>
  103 + <tr v-for="item in intro.modelBos">
  104 + <td>
  105 + <img class="model-avatar" :src="item.avatar"/>
  106 + <span class="model-name">{{item.modelName}}</span>
  107 + </td>
  108 + <td>{{item.height}}</td>
  109 + <td>{{item.weight}}</td>
  110 + <td>{{item.vitalStatistics}}</td>
  111 + <td>{{item.fitModelBo.fit_size}}</td>
  112 + <td>{{item.fitModelBo.feel}}</td>
  113 + </tr>
  114 + </tbody>
  115 + </table>
  116 + </div>
  117 + <i class="info">提示:左滑查看完整表格信息</i>
  118 + </show-box>
  119 +
  120 + <show-box>
  121 + <div v-if="intro.productMaterialList && intro.productMaterialList.length > 0">
  122 + <h2>商品材质</h2>
  123 + <i>MATERIALS</i>
  124 + <hr>
  125 + </div>
  126 +
  127 + <div v-if="intro.productMaterialList">
  128 + <ul v-for="item in intro.productMaterialList">
  129 + <div>
  130 + <div class="image-box">
  131 + <img :src="item.imageUrl" width="86" height="35"/>
  132 + </div>
  133 + <div class="text-box">
  134 + <div>{{item.caption}}</div>
  135 + <div>{{item.encaption}}</div>
  136 + </div>
  137 + <div class="clear-fix"></div>
  138 + </div>
  139 + <p>
  140 + {{item.remark}}
  141 + </p>
  142 +
  143 + <hr/>
  144 + </ul>
  145 + </div>
  146 +
  147 + <ul class="wash-condition">
  148 + <li class="wash-condition-item" v-for="item in intro.washTipsBoList">
  149 + <img :src="item.img" width="25" height="25"/>
  150 + <div>{{item.caption}}</div>
  151 + </li>
  152 + </ul>
  153 + </show-box>
  154 +
  155 + <show-box :is-last="true">
  156 + <h2>商品详情</h2>
  157 + <i>DETAILS</i>
  158 + <p>
  159 + {{{entity.brand.brandIntro}}}
  160 + </p>
  161 +
  162 + <p v-if="intro.productIntroBo">
  163 + {{{intro.productIntroBo.productIntro}}}
  164 + </p>
  165 + </show-box>
  166 +
  167 + <div class="control-box">
  168 + <button class="button control-button" style="flex: 1">
  169 + <a style="position:relative">
  170 + <i class="icon icon-bag"></i>
  171 + <span v-if="cartCount > 0" class="badge badge-tr">{{cartCount}}</span>
  172 + </a>
  173 + </button>
  174 + <button class="button control-button" style="flex: 1">
  175 + <span class="icon icon-love"></span>
  176 + </button>
  177 + <button class="button button-solid" style="flex: 2"
  178 + @click="showAddToCart()"
  179 + :disabled="isSoldOut">
  180 + <span v-if="isSoldOut">
  181 + 已售完
  182 + </span>
  183 + <span v-else="">
  184 + 加入购物袋
  185 + </span>
  186 + </button>
  187 + </div>
  188 +
  189 + <feature-selector :is-visible="showFeatureSelector" :entity="entity"
  190 + :on-add-to-cart="onAddToCart"></feature-selector>
  191 +</template>
  192 +<style src="./css/detail.css"></style>
  193 +<script src="./js/detail.js"></script>
  1 +<style src="./css/image-carousel.css"></style>
  2 +<template>
  3 + <div class="image-carousel">
  4 + <swipe>
  5 + <swipe-item v-for="item in goods">
  6 + <a href="#" title="{{item.title}}">
  7 + <img :src="item.colorImage | resize 750 1000" width="375" height="500" alt="">
  8 + </a>
  9 + </swipe-item>
  10 + </swipe>
  11 + </div>
  12 +</template>
  13 +<script>
  14 + const swipe = require('vue-swipe');
  15 +
  16 + require('common/vue-filter');
  17 +
  18 + module.exports = {
  19 + props: {
  20 + goods: [Object]
  21 + },
  22 + data() {
  23 + return {};
  24 + },
  25 + components: {
  26 + swipe: swipe.Swipe,
  27 + swipeItem: swipe.SwipeItem
  28 + }
  29 + };
  30 +</script>
  1 +const app = $('#app');
  2 +const tip = require('common/tip');
  3 +
  4 +require('vue-swipe/dist/vue-swipe.css');
  5 +
  6 +module.exports = {
  7 + data() {
  8 + return {
  9 + intro: {},
  10 + entity: {
  11 + brand: {
  12 + brandName: '',
  13 + brandIco: ''
  14 + },
  15 + productPriceBo: {
  16 + formatMarketPrice: ''
  17 + }
  18 + },
  19 + showFeatureSelector: false,
  20 + cartCount: 0,
  21 +
  22 + /**
  23 + * 加入购物车回调
  24 + *
  25 + * @param result
  26 + */
  27 + onAddToCart: (result)=> {
  28 + // TODO: 库存不足
  29 + // TODO: 商品已下架
  30 + if (result.code === 200) {
  31 + this.cartCount = result.data.goods_count;
  32 + this.showFeatureSelector = false;
  33 + } else {
  34 + this.showFeatureSelector = false;
  35 + tip('系统异常,请稍后重试');
  36 + }
  37 + }
  38 + };
  39 + },
  40 + computed: {
  41 + isSoldOut: function() {
  42 + return this.entity.storage === 0;
  43 + }
  44 + },
  45 + components: {
  46 + imageCarousel: require('../image-carousel.vue'),
  47 + featureSelector: require('component/product/feature-selector.vue'),
  48 + showBox: require('../show-box.vue')
  49 + },
  50 + methods: {
  51 + showAddToCart: function() {
  52 + this.showFeatureSelector = true;
  53 + }
  54 + },
  55 + created() {
  56 + const self = this;
  57 +
  58 + // 显示商品特征选择组件
  59 + this.$on('featureselector.close', function() {
  60 + self.showFeatureSelector = false;
  61 + });
  62 +
  63 + // 读取基础数据
  64 + $.get(`/product/product_${app.data('pid')}_${app.data('goodsId')}.json`).then(result=> {
  65 + this.entity = result;
  66 + return result;
  67 + }).then((result)=> {
  68 + // 读取商品详情
  69 + return $.get('/product/product/intro.json', {skn: result.productPriceBo.productSkn});
  70 + }).then(result => {
  71 + this.intro = result;
  72 + });
  73 +
  74 + // 读取购物车数量
  75 + $.get('/product/cart-count.json', {}).then(result=> {
  76 + if (result.code === 200) {
  77 + this.cartCount = result.data.cart_goods_count;
  78 + }
  79 + });
  80 + }
  81 +};
  1 +<template>
  2 + <div class="show-box" :class="{ 'first-box': isFirst, 'last-box': isLast }">
  3 + <slot></slot>
  4 + </div>
  5 +</template>
  6 +<style src="./css/show-box.css"></style>
  7 +<script>
  8 + module.exports = {
  9 + props: {
  10 + isFirst: [Boolean],
  11 + isLast: [Boolean]
  12 + },
  13 + data() {
  14 + return {};
  15 + }
  16 + };
  17 +</script>