Browse Source

feat[litemall-admin, litemall-admin-api]:商品上架页面。
在这一个页面同时添加商品相关的四个表,包括商品基本信息、商品规格、商品货品、商品参数。

Junling Bu 7 years ago
parent
commit
ad82b7808c

+ 47 - 0
litemall-admin-api/src/main/java/org/linlinjava/litemall/admin/dao/GoodsAllinone.java

@@ -0,0 +1,47 @@
+package org.linlinjava.litemall.admin.dao;
+
+import org.linlinjava.litemall.db.domain.LitemallGoods;
+import org.linlinjava.litemall.db.domain.LitemallGoodsAttribute;
+import org.linlinjava.litemall.db.domain.LitemallGoodsSpecification;
+import org.linlinjava.litemall.db.domain.LitemallProduct;
+
+public class GoodsAllinone {
+    LitemallGoods goods;
+    LitemallGoodsSpecification[] specifications;
+    LitemallGoodsAttribute[] attributes;
+    // 这里采用 Product 再转换到 LitemallProduct
+    Product[] products;
+
+    public LitemallGoods getGoods() {
+        return goods;
+    }
+
+    public Product[] getProducts() {
+        return products;
+    }
+
+    public void setProducts(Product[] products) {
+        this.products = products;
+    }
+
+    public void setGoods(LitemallGoods goods) {
+        this.goods = goods;
+    }
+
+    public LitemallGoodsSpecification[] getSpecifications() {
+        return specifications;
+    }
+
+    public void setSpecifications(LitemallGoodsSpecification[] specifications) {
+        this.specifications = specifications;
+    }
+
+    public LitemallGoodsAttribute[] getAttributes() {
+        return attributes;
+    }
+
+    public void setAttributes(LitemallGoodsAttribute[] attributes) {
+        this.attributes = attributes;
+    }
+
+}

+ 42 - 0
litemall-admin-api/src/main/java/org/linlinjava/litemall/admin/dao/Product.java

@@ -0,0 +1,42 @@
+package org.linlinjava.litemall.admin.dao;
+
+import java.math.BigDecimal;
+
+public class Product {
+    String[] specifications;
+    BigDecimal price;
+    Integer number;
+    String url;
+
+    public String[] getSpecifications() {
+        return specifications;
+    }
+
+    public void setSpecifications(String[] specifications) {
+        this.specifications = specifications;
+    }
+
+    public BigDecimal getPrice() {
+        return price;
+    }
+
+    public void setPrice(BigDecimal price) {
+        this.price = price;
+    }
+
+    public Integer getNumber() {
+        return number;
+    }
+
+    public void setNumber(Integer number) {
+        this.number = number;
+    }
+
+    public String getUrl() {
+        return url;
+    }
+
+    public void setUrl(String url) {
+        this.url = url;
+    }
+}

+ 33 - 0
litemall-admin-api/src/main/java/org/linlinjava/litemall/admin/util/CatVo.java

@@ -0,0 +1,33 @@
+package org.linlinjava.litemall.admin.util;
+
+import java.util.List;
+
+public class CatVo {
+    private Integer value = null;
+    private String label = null;
+    private List children = null;
+
+    public Integer getValue() {
+        return value;
+    }
+
+    public void setValue(Integer value) {
+        this.value = value;
+    }
+
+    public String getLabel() {
+        return label;
+    }
+
+    public void setLabel(String label) {
+        this.label = label;
+    }
+
+    public List getChildren() {
+        return children;
+    }
+
+    public void setChildren(List children) {
+        this.children = children;
+    }
+}

+ 33 - 0
litemall-admin-api/src/main/java/org/linlinjava/litemall/admin/web/AdminCategoryController.java

@@ -3,6 +3,7 @@ package org.linlinjava.litemall.admin.web;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 import org.linlinjava.litemall.admin.annotation.LoginAdmin;
+import org.linlinjava.litemall.admin.util.CatVo;
 import org.linlinjava.litemall.db.domain.LitemallCategory;
 import org.linlinjava.litemall.db.service.LitemallCategoryService;
 import org.linlinjava.litemall.core.util.ResponseUtil;
@@ -10,6 +11,7 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
 import java.time.LocalDateTime;
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -98,4 +100,35 @@ public class AdminCategoryController {
         return ResponseUtil.ok(data);
     }
 
+
+
+    @GetMapping("/list2")
+    public Object list2(@LoginAdmin Integer adminId) {
+        if (adminId == null) {
+            return ResponseUtil.unlogin();
+        }
+
+        List<LitemallCategory> l1CatList = categoryService.queryL1();
+        List<CatVo> list = new ArrayList<>(l1CatList.size());
+
+        for(LitemallCategory l1 : l1CatList){
+            CatVo l1CatVo = new CatVo();
+            l1CatVo.setValue(l1.getId());
+            l1CatVo.setLabel(l1.getName());
+
+            List<LitemallCategory> l2CatList = categoryService.queryByPid(l1.getId());
+            List<CatVo> children = new ArrayList<>(l2CatList.size());
+            for(LitemallCategory l2 : l2CatList) {
+                CatVo l2CatVo = new CatVo();
+                l2CatVo.setValue(l2.getId());
+                l2CatVo.setLabel(l2.getName());
+                children.add(l2CatVo);
+            }
+            l1CatVo.setChildren(children);
+
+            list.add(l1CatVo);
+        }
+        return ResponseUtil.ok(list);
+    }
+
 }

+ 94 - 0
litemall-admin-api/src/main/java/org/linlinjava/litemall/admin/web/AdminGoodsController.java

@@ -1,14 +1,30 @@
 package org.linlinjava.litemall.admin.web;
 
+import io.swagger.models.auth.In;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 import org.linlinjava.litemall.admin.annotation.LoginAdmin;
+import org.linlinjava.litemall.admin.dao.GoodsAllinone;
+import org.linlinjava.litemall.admin.dao.Product;
 import org.linlinjava.litemall.db.domain.LitemallGoods;
+import org.linlinjava.litemall.db.domain.LitemallGoodsAttribute;
+import org.linlinjava.litemall.db.domain.LitemallGoodsSpecification;
+import org.linlinjava.litemall.db.domain.LitemallProduct;
+import org.linlinjava.litemall.db.service.LitemallGoodsAttributeService;
 import org.linlinjava.litemall.db.service.LitemallGoodsService;
 import org.linlinjava.litemall.core.util.ResponseUtil;
+import org.linlinjava.litemall.db.service.LitemallGoodsSpecificationService;
+import org.linlinjava.litemall.db.service.LitemallProductService;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.transaction.PlatformTransactionManager;
+import org.springframework.transaction.TransactionDefinition;
+import org.springframework.transaction.TransactionStatus;
+import org.springframework.transaction.support.DefaultTransactionDefinition;
 import org.springframework.web.bind.annotation.*;
 
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -19,7 +35,16 @@ public class AdminGoodsController {
     private final Log logger = LogFactory.getLog(AdminGoodsController.class);
 
     @Autowired
+    private PlatformTransactionManager txManager;
+
+    @Autowired
     private LitemallGoodsService goodsService;
+    @Autowired
+    private LitemallGoodsSpecificationService specificationService;
+    @Autowired
+    private LitemallGoodsAttributeService attributeService;
+    @Autowired
+    private LitemallProductService productService;
 
     @GetMapping("/list")
     public Object list(@LoginAdmin Integer adminId,
@@ -81,4 +106,73 @@ public class AdminGoodsController {
         return ResponseUtil.ok();
     }
 
+    @PostMapping("/publish")
+    public Object publish(@LoginAdmin Integer adminId, @RequestBody GoodsAllinone goodsAllinone){
+        if(adminId == null){
+            return ResponseUtil.unlogin();
+        }
+
+        LitemallGoods goods = goodsAllinone.getGoods();
+        LitemallGoodsAttribute[] attributes = goodsAllinone.getAttributes();
+        LitemallGoodsSpecification[] specifications = goodsAllinone.getSpecifications();
+        Product[] products = goodsAllinone.getProducts();
+
+        String name = goods.getName();
+        if(goodsService.checkExistByName(name)){
+            return ResponseUtil.fail(403, "商品名已经存在");
+        }
+
+        // 开启事务管理
+        DefaultTransactionDefinition def = new DefaultTransactionDefinition();
+        def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
+        TransactionStatus status = txManager.getTransaction(def);
+        try {
+
+            // 商品基本信息表litemall_goods
+            goods.setAddTime(LocalDateTime.now());
+            goodsService.add(goods);
+
+            // 商品规格表litemall_goods_specification
+            Map<String, Integer> specIds = new HashMap<>();
+            for(LitemallGoodsSpecification specification : specifications){
+                specification.setGoodsId(goods.getId());
+                specification.setAddTime(LocalDateTime.now());
+                specificationService.add(specification);
+                specIds.put(specification.getValue(), specification.getId());
+            }
+
+            // 商品参数表litemall_goods_attribute
+            for(LitemallGoodsAttribute attribute : attributes){
+                attribute.setGoodsId(goods.getId());
+                attribute.setAddTime(LocalDateTime.now());
+                attributeService.add(attribute);
+            }
+
+            // 商品货品表litemall_product
+            for(Product product : products){
+                LitemallProduct litemallProduct = new LitemallProduct();
+                litemallProduct.setRetailPrice(product.getPrice());
+                litemallProduct.setGoodsNumber(product.getNumber());
+                litemallProduct.setUrl(product.getUrl());
+                litemallProduct.setGoodsId(goods.getId());
+                litemallProduct.setAddTime(LocalDateTime.now());
+
+                String[] values = product.getSpecifications();
+                Integer[] ids = new Integer[values.length];
+                for(int i = 0; i < values.length; i++){
+                    ids[i] = specIds.get(values[i]);
+                }
+                Arrays.sort(ids);
+                litemallProduct.setGoodsSpecificationIds(ids);
+
+                productService.add(litemallProduct);
+            }
+        } catch (Exception ex) {
+            txManager.rollback(status);
+            logger.error("系统内部错误", ex);
+        }
+        txManager.commit(status);
+
+        return ResponseUtil.ok();
+    }
 }

+ 8 - 0
litemall-admin/src/api/category.js

@@ -46,3 +46,11 @@ export function deleteCategory(data) {
     data
   })
 }
+
+export function listCategory2(query) {
+  return request({
+    url: '/category/list2',
+    method: 'get',
+    params: query
+  })
+}

+ 8 - 0
litemall-admin/src/api/goods.js

@@ -39,3 +39,11 @@ export function deleteGoods(data) {
     data
   })
 }
+
+export function publishGoods(data) {
+  return request({
+    url: '/goods/publish',
+    method: 'post',
+    data
+  })
+}

+ 3 - 0
litemall-admin/src/api/storage.js

@@ -64,3 +64,6 @@ export function deleteStorage(data) {
 export function getUploadApi(data) {
   return process.env.OS_API + '/storage/create'
 }
+
+const uploadPath = process.env.OS_API + '/storage/create'
+export { uploadPath }

+ 1 - 0
litemall-admin/src/router/index.js

@@ -98,6 +98,7 @@ export const asyncRouterMap = [
       icon: 'chart'
     },
     children: [
+      { path: 'publish', component: _import('goods/publish'), name: 'publish', meta: { title: '商品上架', noCache: true }},
       { path: 'goods', component: _import('goods/goods'), name: 'goods', meta: { title: '商品管理', noCache: true }},
       { path: 'attribute', component: _import('goods/attribute'), name: 'attribute', meta: { title: '商品参数', noCache: true }},
       { path: 'specification', component: _import('goods/specification'), name: 'specification', meta: { title: '商品规格', noCache: true }},

+ 2 - 1
litemall-admin/src/utils/request.js

@@ -50,7 +50,8 @@ service.interceptors.response.use(
       })
       return Promise.reject('error')
     } else if (res.errno !== 0) {
-      return Promise.reject('error')
+      // 非5xx的错误属于业务错误,留给具体页面处理
+      return Promise.reject(response)
     } else {
       return response
     }

+ 3 - 25
litemall-admin/src/views/goods/goods.vue

@@ -181,8 +181,7 @@
       </el-form>
       <div slot="footer" class="dialog-footer">
         <el-button @click="dialogFormVisible = false">取消</el-button>
-        <el-button v-if="dialogStatus=='create'" type="primary" @click="createData">确定</el-button>
-        <el-button v-else type="primary" @click="updateData">确定</el-button>
+        <el-button type="primary" @click="updateData">确定</el-button>
       </div>
     </el-dialog>
 
@@ -208,7 +207,7 @@
 </style>
 
 <script>
-import { listGoods, createGoods, updateGoods, deleteGoods } from '@/api/goods'
+import { listGoods, updateGoods, deleteGoods } from '@/api/goods'
 import { createStorage, getUploadApi } from '@/api/storage'
 import waves from '@/directive/waves' // 水波纹指令
 import BackToTop from '@/components/BackToTop'
@@ -359,28 +358,7 @@ export default {
       }
     },
     handleCreate() {
-      this.resetForm()
-      this.dialogStatus = 'create'
-      this.dialogFormVisible = true
-      this.$nextTick(() => {
-        this.$refs['dataForm'].clearValidate()
-      })
-    },
-    createData() {
-      this.$refs['dataForm'].validate((valid) => {
-        if (valid) {
-          createGoods(this.dataForm).then(response => {
-            this.list.unshift(response.data.data)
-            this.dialogFormVisible = false
-            this.$notify({
-              title: '成功',
-              message: '创建成功',
-              type: 'success',
-              duration: 2000
-            })
-          })
-        }
-      })
+      this.$router.push({ path: '/goods/publish' })
     },
     handleUpdate(row) {
       this.dataForm = Object.assign({}, row)

+ 2 - 52
litemall-admin/src/views/goods/product.vue

@@ -6,7 +6,6 @@
       <el-input clearable class="filter-item" style="width: 200px;" placeholder="请输入商品ID" v-model="listQuery.goodsId">
       </el-input>
       <el-button class="filter-item" type="primary" v-waves icon="el-icon-search" @click="handleFilter">查找</el-button>
-      <el-button class="filter-item" type="primary" icon="el-icon-edit" @click="handleCreate">添加</el-button>
       <el-button class="filter-item" type="primary" v-waves icon="el-icon-download" @click="handleDownload" :loading="downloadLoading">导出</el-button>
     </div>
 
@@ -42,7 +41,6 @@
       <el-table-column align="center" label="操作" width="250" class-name="small-padding fixed-width">
         <template slot-scope="scope">
           <el-button type="primary" size="mini" @click="handleUpdate(scope.row)">编辑</el-button>
-          <el-button type="danger" size="mini"  @click="handleDelete(scope.row)">删除</el-button>
         </template>
       </el-table-column>
     </el-table>
@@ -54,20 +52,8 @@
       </el-pagination>
     </div>
 
-    <el-dialog title="添加货品" :visible.sync="createDialogFormVisible">
-      <el-form :rules="rules" ref="dataForm" :model="dataForm" status-icon label-position="left" label-width="100px" style='width: 400px; margin-left:50px;'>
-        <el-form-item label="商品ID" prop="goodsId">
-          <el-input v-model="dataForm.goodsId"></el-input>
-        </el-form-item>
-      </el-form>
-      <div slot="footer" class="dialog-footer">
-        <el-button @click="createDialogFormVisible = false">取消</el-button>
-        <el-button type="primary" @click="createData">确定</el-button>
-      </div>
-    </el-dialog>
-
     <!-- 修改对话框 -->
-    <el-dialog title="修改货品" :visible.sync="editDialogFormVisible">
+    <el-dialog title="编辑商品货品" :visible.sync="editDialogFormVisible">
       <el-form :rules="rules" ref="dataForm" :model="dataForm" status-icon label-position="left" label-width="100px" style='width: 400px; margin-left:50px;'>
         <el-form-item label="商品ID" prop="goodsId">
           <el-input v-model="dataForm.goodsId" :disabled="true"></el-input>
@@ -112,7 +98,7 @@
 </style>
 
 <script>
-import { listProduct, createProduct, updateProduct, deleteProduct } from '@/api/product'
+import { listProduct, updateProduct } from '@/api/product'
 import { createStorage } from '@/api/storage'
 
 import waves from '@/directive/waves' // 水波纹指令
@@ -133,7 +119,6 @@ export default {
         goodsId: undefined,
         sort: '+id'
       },
-      createDialogFormVisible: false,
       editDialogFormVisible: false,
       dataForm: {
         id: undefined,
@@ -197,29 +182,6 @@ export default {
         this.$message.error('上传失败,请重新上传')
       })
     },
-    handleCreate() {
-      this.resetForm()
-      this.createDialogFormVisible = true
-      this.$nextTick(() => {
-        this.$refs['dataForm'].clearValidate()
-      })
-    },
-    createData() {
-      this.$refs['dataForm'].validate((valid) => {
-        if (valid) {
-          createProduct(this.dataForm).then(response => {
-            this.getList()
-            this.createDialogFormVisible = false
-            this.$notify({
-              title: '成功',
-              message: '创建成功',
-              type: 'success',
-              duration: 2000
-            })
-          })
-        }
-      })
-    },
     handleUpdate(row) {
       this.dataForm = Object.assign({}, row)
       this.editDialogFormVisible = true
@@ -249,18 +211,6 @@ export default {
         }
       })
     },
-    handleDelete(row) {
-      deleteProduct(row).then(response => {
-        this.$notify({
-          title: '成功',
-          message: '删除成功',
-          type: 'success',
-          duration: 2000
-        })
-        const index = this.list.indexOf(row)
-        this.list.splice(index, 1)
-      })
-    },
     handleDownload() {
       this.downloadLoading = true
       import('@/vendor/Export2Excel').then(excel => {

+ 567 - 0
litemall-admin/src/views/goods/publish.vue

@@ -0,0 +1,567 @@
+<template>
+  <div class="app-container calendar-list-container">
+
+  <el-card class="box-card">
+    <h3>商品介绍</h3>
+    <el-form :rules="rules" ref="goods" :model="goods" label-width="150px">
+        <el-form-item label="商品编号" prop="goodsSn">
+          <el-input v-model="goods.goodsSn"></el-input>
+        </el-form-item>
+        <el-form-item label="商品名称" prop="name">
+          <el-input v-model="goods.name"></el-input>
+        </el-form-item>        
+        <el-form-item label="专柜价格" prop="counterPrice">
+          <el-input v-model="goods.counterPrice" placeholder="0.00">
+            <template slot="append">元</template>
+          </el-input>
+        </el-form-item>
+        <el-form-item label="当前价格" prop="retailPrice">
+          <el-input v-model="goods.retailPrice" placeholder="0.00">
+            <template slot="append">元</template>
+          </el-input>
+        </el-form-item>                
+        <el-form-item label="是否新品" prop="isNew">
+          <el-radio-group v-model="goods.isNew">
+            <el-radio :label="true">新品</el-radio>
+            <el-radio :label="false">非新品</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="是否热卖" prop="isHot">
+          <el-radio-group v-model="goods.isHot">
+            <el-radio :label="false">普通</el-radio>
+            <el-radio :label="true">热卖</el-radio>
+          </el-radio-group>
+        </el-form-item>                
+        <el-form-item label="是否在售" prop="isOnSale">
+          <el-radio-group v-model="goods.isOnSale">
+            <el-radio :label="true">在售</el-radio>
+            <el-radio :label="false">未售</el-radio>
+          </el-radio-group>
+        </el-form-item>
+            
+        <el-form-item label="首页主图">
+          <el-upload class="avatar-uploader" :action='uploadPath' list-type="picture-card" :show-file-list="false" accept=".jpg,.jpeg,.png,.gif" :on-success="uploadListPicUrl">
+			      <img v-if="goods.listPicUrl" :src="goods.listPicUrl" class="avatar">
+						<i v-else class="el-icon-plus avatar-uploader-icon"></i>
+          </el-upload>
+        </el-form-item>
+        
+        <el-form-item label="商品页主图">
+          <el-upload class="avatar-uploader" :action='uploadPath' list-type="picture-card" :show-file-list="false" accept=".jpg,.jpeg,.png,.gif" :on-success="uploadPrimaryPicUrl">
+			      <img v-if="goods.primaryPicUrl" :src="goods.primaryPicUrl" class="avatar">
+						<i v-else class="el-icon-plus avatar-uploader-icon"></i>
+          </el-upload>
+        </el-form-item>
+
+        <el-form-item label="宣传画廊">
+          <el-upload :action='uploadPath' :limit='5' multiple accept=".jpg,.jpeg,.png,.gif" :file-list="galleryFileList" list-type="picture-card" :on-exceed='uploadOverrun' :on-success="handleGalleryUrl" :on-remove="handleRemove">
+             <i class="el-icon-plus"></i>
+          </el-upload>
+        </el-form-item>        
+            
+        <el-form-item label="商品单位">
+          <el-input v-model="goods.goodsUnit" placeholder="件 / 个 / 盒"></el-input>
+        </el-form-item>
+            
+        <el-form-item label="关键字">
+          <el-tag :key="tag" v-for="tag in keywords" closable type="primary" @close="handleClose(tag)">
+            {{tag}}
+          </el-tag>
+          <el-input class="input-new-keyword" v-if="newKeywordVisible" v-model="newKeyword" ref="newKeywordInput" size="small" @keyup.enter.native="handleInputConfirm" @blur="handleInputConfirm">
+          </el-input>
+          <el-button v-else class="button-new-keyword" size="small" type="primary" @click="showInput">+ 增加</el-button>
+        </el-form-item>
+            
+        <el-form-item label="所属分类">
+          <el-cascader expand-trigger="hover" :options="categoryList" @change="handleCategoryChange"></el-cascader>
+        </el-form-item>
+        
+        <el-form-item label="所属品牌商">
+          <el-input v-model="goods.brandId"></el-input>              
+        </el-form-item>
+
+        <el-form-item label="商品简介">
+          <el-input v-model="goods.goodsBrief"></el-input>
+        </el-form-item> 
+            
+        <el-form-item label="商品详细介绍">
+          <editor :init="editorInit" v-model="goods.goodsDesc"></editor>
+        </el-form-item>    
+    </el-form>
+  </el-card>
+
+  <el-card class="box-card">
+      <h3>商品规格</h3>
+      <el-row type="flex" align="middle" :gutter="20" style="padding:20px 0;">
+        <el-col :span="10" >
+          <el-radio-group v-model="multipleSpec" @change="specChanged">
+            <el-radio-button :label="false" >默认标准规格</el-radio-button>
+            <el-radio-button :label="true">多规格支持</el-radio-button>
+          </el-radio-group>
+        </el-col>
+        <el-col :span="10" v-if="multipleSpec">
+          <el-button :plain="true" @click="handleSpecificationShow" type="primary">添加</el-button>
+        </el-col>
+      </el-row>
+
+      <el-table :data="specifications">
+        <el-table-column property="specification" label="规格名" ></el-table-column>
+        <el-table-column property="value" label="规格值" >
+          <template slot-scope="scope">
+            <el-tag type="primary">
+              {{scope.row.value}}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column property="picUrl" label="规格图片">
+          <template slot-scope="scope">
+            <img :src="scope.row.picUrl" width="40" v-if="scope.row.picUrl"/>
+          </template>
+        </el-table-column>
+        <el-table-column align="center" label="操作" width="250" class-name="small-padding fixed-width" v-if="multipleSpec">
+          <template slot-scope="scope">
+            <el-button type="danger" size="mini"  @click="handleSpecificationDelete(scope.row)">删除</el-button>
+          </template>
+      </el-table-column>
+      </el-table>
+
+      <el-dialog title="设置规格" :visible.sync="specVisiable">
+      <el-form :rules="rules" ref="specForm" :model="specForm" status-icon label-position="left" label-width="100px" style='width: 400px; margin-left:50px;'>
+        <el-form-item label="规格名" prop="specification">
+          <el-input v-model="specForm.specification"></el-input>
+        </el-form-item>
+        <el-form-item label="规格值" prop="value">
+          <el-input v-model="specForm.value"></el-input>
+        </el-form-item>
+        <el-form-item label="规格图片" prop="picUrl">
+          <el-upload class="avatar-uploader" :action='uploadPath' list-type="picture-card" :show-file-list="false" accept=".jpg,.jpeg,.png,.gif" :on-success="uploadSpecPicUrl">
+			      <img v-if="specForm.picUrl" :src="specForm.picUrl" class="avatar">
+						<i v-else class="el-icon-plus avatar-uploader-icon"></i>
+          </el-upload>
+        </el-form-item>        
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="specVisiable = false">取消</el-button>
+        <el-button type="primary" @click="handleSpecificationAdd">确定</el-button>
+      </div>
+      </el-dialog>
+    </el-card>
+    
+    <el-card class="box-card">
+      <h3>商品库存</h3>
+       <el-table :data="products">
+        <el-table-column property="value"  label="货品规格" >
+          <template slot-scope="scope">
+            <el-tag :key="tag" v-for="tag in scope.row.specifications">
+              {{tag}}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column property="price" width="100" label="货品售价">
+        </el-table-column>
+        <el-table-column property="number" width="100" label="货品数量">
+        </el-table-column>
+        <el-table-column property="price" width="100" label="货品图片">
+          <template slot-scope="scope">
+            <img :src="scope.row.url" width="40" v-if="scope.row.url"/>
+          </template>
+        </el-table-column>
+        <el-table-column align="center" label="操作" width="100" class-name="small-padding fixed-width">
+          <template slot-scope="scope">
+            <el-button type="primary" size="mini" @click="handleProductShow(scope.row)">设置</el-button>
+          </template>
+      </el-table-column>
+      </el-table>
+
+      <el-dialog title="设置货品" :visible.sync="productVisiable">
+      <el-form ref="productForm" :model="productForm" status-icon label-position="left" label-width="100px" style='width: 400px; margin-left:50px;'>
+        <el-form-item label="货品规格列" prop="specifications">
+          <el-tag :key="tag" v-for="tag in productForm.specifications">
+            {{tag}}
+          </el-tag>
+        </el-form-item>
+        <el-form-item label="货品售价" prop="price">
+          <el-input v-model="productForm.price"></el-input>
+        </el-form-item>
+        <el-form-item label="货品数量" prop="number">
+          <el-input v-model="productForm.number"></el-input>
+        </el-form-item>
+        <el-form-item label="货品图片" prop="picUrl">
+          <el-upload class="avatar-uploader" :action='uploadPath' list-type="picture-card" :show-file-list="false" accept=".jpg,.jpeg,.png,.gif" :on-success="uploadProductUrl">
+			      <img v-if="productForm.url" :src="productForm.url" class="avatar">
+						<i v-else class="el-icon-plus avatar-uploader-icon"></i>
+          </el-upload>
+        </el-form-item>        
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="productVisiable = false">取消</el-button>
+        <el-button type="primary" @click="handleProductEdit">确定</el-button>
+      </div>
+      </el-dialog>
+    </el-card>
+
+    <el-card class="box-card">
+      <h3>商品参数</h3>
+      <el-button :plain="true" @click="handleAttributeShow" type="primary">添加</el-button>
+      <el-table :data="attributes">
+        <el-table-column property="attribute" label="商品参数名称">
+        </el-table-column>
+        <el-table-column property="value" label="商品参数值">
+        </el-table-column>
+        <el-table-column align="center" label="操作" width="100" class-name="small-padding fixed-width">
+          <template slot-scope="scope">
+            <el-button type="danger" size="mini"  @click="handleAttributeDelete(scope.row)">删除</el-button>
+          </template>
+      </el-table-column>
+      </el-table>
+
+      <el-dialog title="设置商品参数" :visible.sync="attributeVisiable">
+      <el-form ref="attributeForm" :model="attributeForm" status-icon label-position="left" label-width="100px" style='width: 400px; margin-left:50px;'>
+        <el-form-item label="商品参数名称" prop="attribute">
+          <el-input v-model="attributeForm.attribute"></el-input>
+        </el-form-item>
+        <el-form-item label="商品参数值" prop="value">
+          <el-input v-model="attributeForm.value"></el-input>
+        </el-form-item>      
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="attributeVisiable = false">取消</el-button>
+        <el-button type="primary" @click="handleAttributeAdd">确定</el-button>
+      </div>
+      </el-dialog>
+    </el-card>
+
+    <div class="op-container">
+      <el-button @click="handleCancel">取消</el-button>
+      <el-button @click="handlePublish" type="primary">上架</el-button>
+    </div>
+
+  </div>
+</template>
+
+<style>
+  .el-card {
+    margin-bottom: 10px;
+  }
+  .el-tag + .el-tag {
+    margin-left: 10px;
+  }
+  .input-new-keyword {
+    width: 90px;
+    margin-left: 10px;
+    vertical-align: bottom;
+  }
+  .avatar-uploader .el-upload {
+	  border: 1px dashed #d9d9d9;
+	  border-radius: 6px;
+	  cursor: pointer;
+	  position: relative;
+	  overflow: hidden;
+	}
+	.avatar-uploader .el-upload:hover {
+	  border-color: #20a0ff;
+	}
+	.avatar-uploader-icon {
+	    font-size: 28px;
+	    color: #8c939d;
+	    width: 120px;
+	    height: 120px;
+	    line-height: 120px;
+	    text-align: center;
+	}
+	.avatar {
+	    width: 120px;
+	    height: 120px;
+	    display: block;
+	}
+</style>
+
+<script>
+import { publishGoods } from '@/api/goods'
+import { createStorage, uploadPath } from '@/api/storage'
+import { listCategory2 } from '@/api/category'
+import Editor from '@tinymce/tinymce-vue'
+import { MessageBox } from 'element-ui'
+
+export default {
+  name: 'GoodsAdd',
+  components: { Editor },
+  data() {
+    return {
+      uploadPath,
+      newKeywordVisible: false,
+      newKeyword: '',
+      keywords: [],
+      galleryFileList: [],
+      categoryList: [],
+      goods: {
+        id: undefined,
+        goodsSn: undefined,
+        name: undefined,
+        counterPrice: undefined,
+        retailPrice: undefined,
+        isHot: false,
+        isNew: true,
+        isOnSale: true,
+        listPicUrl: undefined,
+        primaryPicUrl: undefined,
+        goodsBrief: undefined,
+        goodsDesc: '',
+        keywords: '',
+        gallery: [],
+        categoryId: undefined,
+        brandId: undefined
+      },
+      specVisiable: false,
+      specForm: { specification: '', value: '', picUrl: '' },
+      multipleSpec: false,
+      specifications: [{ specification: '规格', value: '标准', picUrl: '' }],
+      productVisiable: false,
+      productForm: { id: 0, specifications: [], price: 0.00, number: 0, url: '' },
+      products: [{ id: 0, specifications: ['标准'], price: 0.00, number: 0, url: '' }],
+      attributeVisiable: false,
+      attributeForm: { attribute: '', value: '' },
+      attributes: [],
+      rules: {
+        goodsSn: [{ required: true, message: '商品编号不能为空', trigger: 'blur' }],
+        name: [{ required: true, message: '商品名称不能为空', trigger: 'blur' }]
+      },
+      editorInit: {
+        language: 'zh_CN',
+        plugins: ['advlist anchor autolink autoresize autosave emoticons fullscreen hr image imagetools importcss insertdatetime legacyoutput link lists media nonbreaking noneditable pagebreak paste preview print save searchreplace tabfocus table template textcolor textpattern visualblocks visualchars wordcount'],
+        toolbar: ['bold italic underline strikethrough alignleft aligncenter alignright outdent indent  blockquote undo redo removeformat subscript superscript ', 'hr bullist numlist link image charmap preview anchor pagebreak fullscreen media table emoticons forecolor backcolor'],
+        images_upload_handler: function(blobInfo, success, failure) {
+          const formData = new FormData()
+          formData.append('file', blobInfo.blob())
+          createStorage(formData).then(res => {
+            success(res.data.data.url)
+          }).catch(() => {
+            failure('上传失败,请重新上传')
+          })
+        }
+      }
+    }
+  },
+  created() {
+    this.getCatList()
+  },
+  methods: {
+    getCatList: function() {
+      listCategory2().then(response => {
+        this.categoryList = response.data.data
+      })
+    },
+    handleCategoryChange(value) {
+      this.goods.categoryId = value[value.length - 1]
+    },
+    handleCancel: function() {
+      this.$router.push({ path: '/goods/goods' })
+    },
+    handlePublish: function() {
+      const finalGoods = {
+        goods: this.goods,
+        specifications: this.specifications,
+        products: this.products,
+        attributes: this.attributes
+      }
+      publishGoods(finalGoods).then(response => {
+        this.$notify({
+          title: '成功',
+          message: '创建成功',
+          type: 'success',
+          duration: 2000
+        })
+        this.$router.push({ path: '/goods/goods' })
+      }).catch(response => {
+        MessageBox.alert('业务错误:' + response.data.errmsg, '警告', {
+          confirmButtonText: '确定',
+          type: 'error'
+        })
+      })
+    },
+    handleClose(tag) {
+      this.keywords.splice(this.keywords.indexOf(tag), 1)
+      this.goods.keywords = this.keywords.toString()
+    },
+    showInput() {
+      this.newKeywordVisible = true
+      this.$nextTick(_ => {
+        this.$refs.newKeywordInput.$refs.input.focus()
+      })
+    },
+    handleInputConfirm() {
+      const newKeyword = this.newKeyword
+      if (newKeyword) {
+        this.keywords.push(newKeyword)
+        this.goods.keywords = this.keywords.toString()
+      }
+      this.newKeywordVisible = false
+      this.newKeyword = ''
+    },
+    uploadPrimaryPicUrl: function(response) {
+      this.goods.primaryPicUrl = response.data.url
+    },
+    uploadListPicUrl: function(response) {
+      this.goods.listPicUrl = response.data.url
+    },
+    uploadOverrun: function() {
+      this.$message({
+        type: 'error',
+        message: '上传文件个数超出限制!最多上传5张图片!'
+      })
+    },
+    handleGalleryUrl(response, file, fileList) {
+      if (response.errno === 0) {
+        this.goods.gallery.push(response.data.url)
+      }
+    },
+    handleRemove: function(file, fileList) {
+      for (var i = 0; i < this.goods.gallery.length; i++) {
+        // 这里存在两种情况
+        // 1. 如果所删除图片是刚刚上传的图片,那么图片地址是file.response.data.url
+        //    此时的file.url虽然存在,但是是本机地址,而不是远程地址。
+        // 2. 如果所删除图片是后台返回的已有图片,那么图片地址是file.url
+        var url
+        if (file.response === undefined) {
+          url = file.url
+        } else {
+          url = file.response.data.url
+        }
+
+        if (this.goods.gallery[i] === url) {
+          this.goods.gallery.splice(i, 1)
+        }
+      }
+    },
+    specChanged: function(label) {
+      if (label === false) {
+        this.specifications = [{ specification: '规格', value: '标准', picUrl: '' }]
+        this.products = [{ id: 0, specifications: ['标准'], price: 0.00, number: 0, url: '' }]
+      } else {
+        this.specifications = []
+        this.products = []
+      }
+    },
+    uploadSpecPicUrl: function(response) {
+      this.specForm.picUrl = response.data.url
+    },
+    handleSpecificationShow() {
+      this.specForm = {}
+      this.specVisiable = true
+    },
+    handleSpecificationAdd() {
+      var index = this.specifications.length - 1
+      for (var i = 0; i < this.specifications.length; i++) {
+        const v = this.specifications[i]
+        if (v.specification === this.specForm.specification) {
+          index = i
+        }
+      }
+
+      this.specifications.splice(index + 1, 0, this.specForm)
+      this.specVisiable = false
+
+      this.specToProduct()
+    },
+    handleSpecificationDelete(row) {
+      const index = this.specifications.indexOf(row)
+      this.specifications.splice(index, 1)
+      this.specToProduct()
+    },
+    specToProduct() {
+      if (this.specifications.length === 0) {
+        return
+      }
+      // 根据specifications创建临时规格列表
+      var specValues = []
+      var spec = this.specifications[0].specification
+      var values = []
+      values.push(0)
+
+      for (var i = 1; i < this.specifications.length; i++) {
+        const aspec = this.specifications[i].specification
+
+        if (aspec === spec) {
+          values.push(i)
+        } else {
+          specValues.push(values)
+          spec = aspec
+          values = []
+          values.push(i)
+        }
+      }
+      specValues.push(values)
+
+      // 根据临时规格列表生产货品规格
+      // 算法基于 https://blog.csdn.net/tyhj_sf/article/details/53893125
+      var productsIndex = 0
+      var products = []
+      var combination = []
+      var n = specValues.length
+      for (var s = 0; s < n; s++) {
+        combination[s] = 0
+      }
+      var index = 0
+      var isContinue = false
+      do {
+        var specifications = []
+        for (var x = 0; x < n; x++) {
+          var z = specValues[x][combination[x]]
+          specifications.push(this.specifications[z].value)
+        }
+        products[productsIndex] = { id: productsIndex, specifications: specifications, price: 0.00, number: 0, url: '' }
+        productsIndex++
+
+        index++
+        combination[n - 1] = index
+        for (var j = n - 1; j >= 0; j--) {
+          if (combination[j] >= specValues[j].length) {
+            combination[j] = 0
+            index = 0
+            if (j - 1 >= 0) {
+              combination[j - 1] = combination[j - 1] + 1
+            }
+          }
+        }
+        isContinue = false
+        for (var p = 0; p < n; p++) {
+          if (combination[p] !== 0) {
+            isContinue = true
+          }
+        }
+      } while (isContinue)
+
+      this.products = products
+    },
+    handleProductShow(row) {
+      this.productForm = Object.assign({}, row)
+      this.productVisiable = true
+    },
+    uploadProductUrl: function(response) {
+      this.productForm.url = response.data.url
+    },
+    handleProductEdit() {
+      for (var i = 0; i < this.products.length; i++) {
+        const v = this.products[i]
+        if (v.id === this.productForm.id) {
+          this.products.splice(i, 1, this.productForm)
+          break
+        }
+      }
+      this.productVisiable = false
+    },
+    handleAttributeShow() {
+      this.attributeForm = {}
+      this.attributeVisiable = true
+    },
+    handleAttributeAdd() {
+      this.attributes.unshift(this.attributeForm)
+      this.attributeVisiable = false
+    },
+    handleAttributeDelete(row) {
+      const index = this.attributes.indexOf(row)
+      this.attributes.splice(index, 1)
+    }
+  }
+}
+</script>

+ 4 - 47
litemall-admin/src/views/goods/specification.vue

@@ -6,7 +6,6 @@
       <el-input clearable class="filter-item" style="width: 200px;" placeholder="请输入商品ID" v-model="listQuery.goodsId">
       </el-input>
       <el-button class="filter-item" type="primary" v-waves icon="el-icon-search" @click="handleFilter">查找</el-button>
-      <el-button class="filter-item" type="primary" @click="handleCreate" icon="el-icon-edit">添加</el-button>
       <el-button class="filter-item" type="primary" :loading="downloadLoading" v-waves icon="el-icon-download" @click="handleDownload">导出</el-button>
     </div>
 
@@ -30,7 +29,6 @@
       <el-table-column align="center" label="操作" width="250" class-name="small-padding fixed-width">
         <template slot-scope="scope">
           <el-button type="primary" size="mini" @click="handleUpdate(scope.row)">编辑</el-button>
-          <el-button type="danger" size="mini"  @click="handleDelete(scope.row)">删除</el-button>
         </template>
       </el-table-column>
     </el-table>
@@ -42,8 +40,8 @@
       </el-pagination>
     </div>
 
-    <!-- 添加或修改对话框 -->
-    <el-dialog :title="textMap[dialogStatus]" :visible.sync="dialogFormVisible">
+    <!-- 修改对话框 -->
+    <el-dialog title="编辑商品规格" :visible.sync="dialogFormVisible">
       <el-form :rules="rules" ref="dataForm" :model="dataForm" status-icon label-position="left" label-width="100px" style='width: 400px; margin-left:50px;'>
         <el-form-item label="商品ID" prop="goodsId">
           <el-input v-model="dataForm.goodsId"></el-input>
@@ -60,8 +58,7 @@
       </el-form>
       <div slot="footer" class="dialog-footer">
         <el-button @click="dialogFormVisible = false">取消</el-button>
-        <el-button v-if="dialogStatus=='create'" type="primary" @click="createData">确定</el-button>
-        <el-button v-else type="primary" @click="updateData">确定</el-button>
+        <el-button type="primary" @click="updateData">确定</el-button>
       </div>
     </el-dialog>
 
@@ -69,7 +66,7 @@
 </template>
 
 <script>
-import { listGoodsSpecification, createGoodsSpecification, updateGoodsSpecification, deleteGoodsSpecification } from '@/api/goods-specification'
+import { listGoodsSpecification, updateGoodsSpecification } from '@/api/goods-specification'
 import waves from '@/directive/waves' // 水波纹指令
 
 export default {
@@ -97,10 +94,6 @@ export default {
       },
       dialogFormVisible: false,
       dialogStatus: '',
-      textMap: {
-        update: '编辑',
-        create: '创建'
-      },
       rules: {
         goodsId: [{ required: true, message: '商品ID不能为空', trigger: 'blur' }],
         specification: [{ required: true, message: '商品规格名称不能为空', trigger: 'blur' }],
@@ -146,30 +139,6 @@ export default {
         picUrl: undefined
       }
     },
-    handleCreate() {
-      this.resetForm()
-      this.dialogStatus = 'create'
-      this.dialogFormVisible = true
-      this.$nextTick(() => {
-        this.$refs['dataForm'].clearValidate()
-      })
-    },
-    createData() {
-      this.$refs['dataForm'].validate((valid) => {
-        if (valid) {
-          createGoodsSpecification(this.dataForm).then(response => {
-            this.list.unshift(response.data.data)
-            this.dialogFormVisible = false
-            this.$notify({
-              title: '成功',
-              message: '创建成功',
-              type: 'success',
-              duration: 2000
-            })
-          })
-        }
-      })
-    },
     handleUpdate(row) {
       this.dataForm = Object.assign({}, row)
       this.dialogStatus = 'update'
@@ -200,18 +169,6 @@ export default {
         }
       })
     },
-    handleDelete(row) {
-      deleteGoodsSpecification(row).then(response => {
-        this.$notify({
-          title: '成功',
-          message: '删除成功',
-          type: 'success',
-          duration: 2000
-        })
-        const index = this.list.indexOf(row)
-        this.list.splice(index, 1)
-      })
-    },
     handleDownload() {
       this.downloadLoading = true
       import('@/vendor/Export2Excel').then(excel => {

+ 6 - 0
litemall-db/src/main/java/org/linlinjava/litemall/db/service/LitemallGoodsService.java

@@ -207,4 +207,10 @@ public class LitemallGoodsService {
         }
         return cats;
     }
+
+    public boolean checkExistByName(String name) {
+        LitemallGoodsExample example = new LitemallGoodsExample();
+        example.or().andNameEqualTo(name).andDeletedEqualTo(false);
+        return goodsMapper.countByExample(example) != 0;
+    }
 }