精华内容
下载资源
问答
  • 保存SKU表数据 在保存数据之前我们需要先获取三级分类信息、SPU表的名称信息、当前SPU商品的规格选项信息加载到页面中 1、获取三级分类信息 接口分析 请求方式: GET/meiduo_admin/skus/categories/ # ----...

    保存SKU表数据

    在保存数据之前我们需要先获取三级分类信息、SPU表的名称信息、当前SPU商品的规格选项信息加载到页面中

    1、获取三级分类信息

    接口分析

    请求方式: GET /meiduo_admin/skus/categories/

        # -------获取三级分类信息--------
        url(r'skus/categories/$', skus.SKUCategorieView.as_view()),

     【图片中重写的方法名,作用相同于下面写的类方法】

    请求参数: 通过请求头传递jwt token数据。

    返回数据: JSON

    [
            {
                "id": "商品分类id",
                "name": "商品分类名称"
            },
            ...
        ]
    
    返回值类型是否必须说明
    Idint商品分类id
    name数组商品分类名称

    后端实现

    from rest_framework.viewsets import ModelViewSet
    from meiduo_admin.serializers.skus import SKUGoodsSerializer
    from meiduo_admin.utils import UserPageNum
    from goods.models import SKU
    from rest_framework.generics import ListAPIView
    from meiduo_admin.serializers.skus import SKUCategorieSerializer
    from goods.models import GoodsCategory
    
    
    class SKUCategorieView(ListAPIView):
    
        serializer_class = SKUCategorieSerializer
        # 根据数据存储规律parent_id大于37为三级分类信息,查询条件为parent_id__gt=37
        queryset = GoodsCategory.objects.filter(parent_id__gt=37)
    

    序列化器的定义

    from rest_framework import serializers
    from goods.models import SKU,GoodsCategory
    from goods.models import SKUSpecification
    from goods.models import GoodsCategory
    
    
    class SKUCategorieSerializer(serializers.ModelSerializer):
        """
          商品分类序列化器
        """
        class Meta:
            model = GoodsCategory
            fields = "__all__"
    

    2、获取spu表名称数据

    接口分析

    请求方式: GET /meiduo_admin/goods/simple/

        # -------获取spu表名称数据--------
        url(r'goods/simple/$', skus.SPUSimpleView.as_view()),  # 此行代码和上面规格路由表的作用是一样的
    

    请求参数: 通过请求头传递jwt token数据。

    返回数据: JSON

    [
            {
                "id": "商品SPU ID",
                "name": "SPU名称"
            },
            ...
        ]
    
    返回值类型是否必须说明
    Idint商品SPU ID
    name数组SPU名称

    后端实现

    class SPUSimpleView(ListAPIView):
        serializer_class = SPUSimpleSerializer
        queryset = SPU.objects.all()
    

    定义序列化器

    class SPUSimpleSerializer(serializers.ModelSerializer):
        """
            商品SPU表序列化器
        """
        class Meta:
            model = GoodsCategory
            fields = ('id', 'name')

    3、获取SPU商品规格信息

    接口分析

    请求方式: GET meiduo_admin/goods/(?P<pk>\d+)/specs/

        # -------获取spu商品规格信息--------
        url(r'goods/(?P<pk>\d+)/specs/$', skus.SPUSpecView.as_view()),

    请求参数: 通过请求头传递jwt token数据。

    在路径中传递当前SPU商品id

    返回数据: JSON

     [
            {
                "id": "规格id",
                "name": "规格名称",
                "spu": "SPU商品名称",
                "spu_id": "SPU商品id",
                "options": [
                    {
                        "id": "选项id",
                        "name": "选项名称"
                    },
                    ...
                ]
            },
            ...
       ]
    
    返回值类型是否必须说明
    Idint规格id
    nameStr规格名称
    SupstrSpu商品名称
    Spu_idIntspu商品id
    options关联的规格选项

    后端实现

    class SPUSpecView(ListAPIView):
    
        serializer_class = SPUSpecSerialzier
        # 因为我们继承的是ListAPIView,在拓展类中是通过get_queryset获取数据,但是我们现在要获取的是规格信息,所以重写get_queryset
        def get_queryset(self):
              # 获取spuid值
                 pk=self.kwargs['pk']
            # 根据spu的id值关联过滤查询出规格信息
            return SPUSpecification.objects.filter(spu_id=self.kwargs['pk'])
    

    定义序列化器

    from goods.models import GoodsCategory, SpecificationOption, SPUSpecification
    
    class SPUOptineSerializer(serializers.ModelSerializer):
        """
            规格选项序列化器
        """
    
        class Meta:
            model = SpecificationOption
            fields = ('id', 'value')
    
    
    class SPUSpecSerialzier(serializers.ModelSerializer):
        """
            规格序列化器
        """
        # 关联序列化返回SPU表数据
    
        spu = serializers.StringRelatedField(read_only=True)
        spu_id = serializers.IntegerField(read_only=True)
        # 关联序列化返回 规格选项信息
        options = SPUOptineSerializer(read_only=True, many=True)  # 使用规格选项序列化器
    
        class Meta:
            model = SPUSpecification  # SPUSpecification中的外键spu关联了SPU商品表
            fields = "__all__"
    

    4、保存SKU数据

    【保存数据时若不显示,则刷新一下】

    接口分析

    请求方式: POST meiduo_admin/skus/

    #  sku表查询路由****************************
    router = DefaultRouter()
    router.register('skus', skus.SKUGoodsView, base_name='sku')
    urlpatterns += router.urls

    请求参数: 通过请求头传递jwt token数据。

    参数类型是否必须说明
    namestr商品SKU名称
    spu_idint商品SPU ID
    captionstr商品副标题
    category_idint三级分类ID
    priceint价格
    cost_priceint进价
    market_priceint市场价
    stockint库存
    is_launchedboole上下架

    返回数据: JSON

     {
            "id": "商品SKU ID",
            "name": "商品SKU名称",
            "goods": "商品SPU名称",
            "goods_id": "商品SPU ID",
            "caption": "商品副标题",
            "category_id": "三级分类id",
            "category": "三级分类名称",
            "price": "价格",
            "cost_price": "进价",
            "market_price": "市场价",
            "stock": "库存",
            "sales": "销量",
            "is_launched": "上下架",
            "specs": [
                {
                    "spec_id": "规格id",
                    "option_id": "选项id"
                },
                ...
            ]
        }
    
    参数类型是否必须说明
    namestr商品SKU名称
    spu_idint商品SPU ID
    captionstr商品副标题
    category_idint三级分类ID
    priceint价格
    cost_priceint进价
    market_priceint市场价
    stockint库存
    is_launchedboole上下架

    后端实现:

    在后端实现中,我们需要异步生成详情页静态页面,同时涉及到多张表操作,我们还需要使用事务

    # SKUGoodsView继承的是ModelViewSet 所以保存逻辑还是使用同一个类视图
    class SKUGoodsView(ModelViewSet):
    
        serializer_class =SKUGoodsSerializer
        pagination_class = PageNum
    
        def get_queryset(self):
            keyword=self.request.query_params.get('keyword')
            if keyword == '' or keyword is None:
                return SKU.objects.all()
    
            else:
                return SKU.objects.filter(name=keyword)
    

    序列化器的定义

    from django.db import transaction
    from celery_tasks.static_file.tasks import get_detail_html
    
    class SKUSerializer(serializers.ModelSerializer):
        """
            SKU表数据
        """
        # 返回关联spu表的名称和关联的分类表的名称
        spu = serializers.StringRelatedField(read_only=True)
        category = serializers.StringRelatedField(read_only=True)
    
        # 返回模型类类的spu_id和category_id
        spu_id = serializers.IntegerField()
        category_id = serializers.IntegerField()
    
        # 返回商品的规格信息 ,在商品规格详情表(SKUSpecification)中有个外键sku关了当前的SKU表
        specs = SKUSpecificationSerializer(many=True)
    
        class Meta:
            model = SKU
            fields = "__all__"
    
        def create(self, validated_data):
          # self指的是当前序列化器对象,在self下面有个context属性保存了请求对象
          specs=self.context['request'].data.get('specs')
          # specs = validated_data['specs']
          # 因为sku表中没有specs字段,所以在保存的时候需要删除validated_data中specs数据
          del validated_data['specs']
          with transaction.atomic():
            # 开启事务
            sid = transaction.savepoint()
            try:
              # 1、保存sku表
              sku = SKU.objects.create(**validated_data)
              # 2、保存SKU具体规格
              for spec in specs:
                SKUSpecification.objects.create(sku=sku, spec_id=spec['spec_id'], option_id=spec['option_id'])
            except:
              # 捕获异常,说明数据库操作失败,进行回滚
              transaction.savepoint_rollback(sid)
              return serializers.ValidationError('数据库错误')
            else:
              # 没有捕获异常,数据库操作成功,进行提交
              transaction.savepoint_commit(sid)
              # 执行异步任务生成新的静态页面
              get_detail_html.delay(sku.id)
              return sku
    

    异步任务:

    import os
    
    from django.conf import settings
    from django.shortcuts import render
    
    from goods.models import SKU
    from meiduo_mall.utils.breadcrumb import get_breadcrumb
    from meiduo_mall.utils.categories import get_categories
    from celery_tasks.main import app
    
    
    @app.task(name='get_detail_html')
    def get_detail_html(sku_id):
        # 获取当前sku对象
        sku=SKU.objects.get(id=sku_id)
        # 分类数据
        categories = get_categories()
    
        # 获取面包屑导航
        breadcrumb = get_breadcrumb(sku.category)
    
        # 获取spu
        spu = sku.spu
    
        # 获取规格信息:sku===>spu==>specs
        specs = spu.specs.order_by('id')
    
        # 查询所有的sku,如华为P10的所有库存商品
        skus = spu.skus.order_by('id')
        '''
        {
            选项:sku_id
        }
        说明:键的元组中,规格的索引是固定的
        示例数据如下:
        {
            (1,3):1,
            (2,3):2,
            (1,4):3,
            (2,4):4
        }
        '''
        sku_options = {}
        sku_option = []
        for sku1 in skus:
            infos = sku1.specs.order_by('spec_id')
            option_key = []
            for info in infos:
                option_key.append(info.option_id)
                # 获取当前商品的规格信息
                if sku.id == sku1.id:
                    sku_option.append(info.option_id)
            sku_options[tuple(option_key)] = sku1.id
    
        # 遍历当前spu所有的规格
        specs_list = []
        for index, spec in enumerate(specs):
            option_list = []
            for option in spec.options.all():
                # 如果当前商品为蓝、64,则列表为[2,3]
                sku_option_temp = sku_option[:]
                # 替换对应索引的元素:规格的索引是固定的[1,3]
                sku_option_temp[index] = option.id
                # 为选项添加sku_id属性,用于在html中输出链接
                option.sku_id = sku_options.get(tuple(sku_option_temp), 0)
                # 添加选项对象
                option_list.append(option)
            # 为规格对象添加选项列表
            spec.option_list = option_list
            # 重新构造规格数据
            specs_list.append(spec)
    
        context = {
            'sku': sku,
            'categories': categories,
            'breadcrumb': breadcrumb,
            'category_id': sku.category_id,
            'spu': spu,
            'specs': specs_list
        }
        response = render(None, 'detail.html', context)
        file_name = os.path.join(settings.BASE_DIR, 'static/detail/%d.html' % sku.id)
        # 写文件
        with open(file_name, 'w') as f1:
            f1.write(response.content.decode())

    展开全文
  • 效果图 代码做了大量的优化跟精简 尤其是在嵌套...div class="sku-container"> <div class="ub ubv-s"> <el-autocomplete v-model="newAttr" style="width: 350px;margin-bottom: 20px;" :fetch-sug

    效果图

    在这里插入图片描述

    代码做了大量的优化跟精简 尤其是在嵌套循环上 比原组件减少了大约一半的代码

    组件代码

    <template>
      <div class="sku-container">
        <div class="ub ubv-s">
          <el-autocomplete
            v-model="newAttr"
            style="width: 350px;margin-bottom: 20px;"
            :fetch-suggestions="querySearchAsync"
            value-key="name"
            class="m-right1"
            clearable
            placeholder="添加新属性 如:颜色"
            @keyup.enter.native="addAttr"
            @select="item => selectNewAttr = item"
          >
            <el-button slot="append" type="primary" icon="el-icon-plus" @click="addAttr">添加</el-button>
          </el-autocomplete>
          <el-alert
            title="属性数量增减会影响之前sku,sku变动后 之前的依然会被保留但不在显示,恢复相同属性即可恢复"
            type="warning"
            :closable="false"
            show-icon
          />
        </div>
        <div v-if="!disabled" class="sku-check">
          <div v-if="theme == 1" class="theme-1">
            <el-card v-for="(item, index) in attr" :key="index" class="item" shadow="never">
              <div slot="header" class="ub ubv-c ub-sb">
                <div class="uf1">{{ item.name }}</div>
                <div class="text-gray m-right1">是否SKU:</div>
                <el-switch
                  v-model="item.is_sku"
                  class="m-right1"
                />
                <el-popconfirm :title="`删除 ${item.name} 属性删除吗?`" @onConfirm="delSourceAttr(index)">
                  <i slot="reference" class="el-icon-delete" />
                </el-popconfirm>
              </div>
    
              <el-checkbox v-for="(item2) in item.item" :key="item2.name" v-model="item2.checked" :label="item2.name" size="small" />
              <el-autocomplete
                v-if="item.canAddAttribute"
                v-model="item.addAttribute"
                :fetch-suggestions="querySearchAttrValAsync"
                value-key="name"
                clearable
                class="add-attr"
                size="small"
                placeholder="新增一个规格"
                @focus="addAttrId = index"
                @keyup.enter.native="onAddAttribute(index)"
                @select="item => selectNewAttrVal = item"
              >
                <el-button slot="append" type="primary" icon="el-icon-plus" @click="onAddAttribute(index)">添加</el-button>
              </el-autocomplete>
            </el-card>
          </div>
          <el-table v-else :data="attr" :show-header="false" class="theme-2">
            <el-table-column prop="name" width="120" :resizable="false" />
            <el-table-column>
              <template slot-scope="scope">
                <el-checkbox v-for="(item2, index2) in scope.row.item" :key="index2" v-model="item2.checked" :label="item2.name" size="small" />
              </template>
            </el-table-column>
            <el-table-column width="250">
              <template slot-scope="scope">
                <el-input v-model="scope.row.addAttribute" size="small" placeholder="新增一个规格" class="add-attr" @keyup.enter.native="onAddAttribute(scope.$index)">
                  <el-button slot="append" size="small" icon="el-icon-plus" @click="onAddAttribute(scope.$index)">添加</el-button>
                </el-input>
              </template>
            </el-table-column>
          </el-table>
        </div>
        <div class="sku-list">
          <el-form ref="form" :model="form" status-icon inline-message>
            <el-table :data="form.skuData" row-key="sku" stripe border highlight-current-row max-height="600" :span-method="arraySpanMethod">
              <!-- 考虑到异步加载的情况,如果 attribute 数据先加载完成,则表头会立马展示,效果不理想,故使用emitAttribute 数据,该数据为计算属性,通过 attr 生成,结构与 attribute 一致 -->
              <el-table-column v-if="attribute.length > 0" type="index" width="50" align="center" :resizable="false" />
    
              <el-table-column v-for="(eAttr, index) in emitAttribute" :key="`attribute-${index}`" :label="eAttr.name" :prop="eAttr.attr_id" :width="staticWidth" align="center" :resizable="false" sortable />
              <el-table-column v-for="(item, index) in structure" :key="`structure-${index}`" :label="item.label" :prop="item.name" align="center" :resizable="false" :min-width="staticWidth+'px'">
                <!-- 自定义表头 -->
                <template slot="header">
                  <span :class="{'required_title': item.required}">
                    {{ item.label }}
                  </span>
                  <el-tooltip v-if="item.tip" effect="dark" :content="item.tip" placement="top">
                    <i class="el-icon-info" />
                  </el-tooltip>
                </template>
                <!-- 自定义表格内部展示 -->
                <template slot-scope="scope">
                  <!-- 增加是 key 是为了保证异步验证不会出现 skuData 数据变化后无法验证的 bug -->
                  <el-form-item v-if="item.type == 'input'" :key="`structure-input-${index}-${scope.row.sku}`" :prop="'skuData.' + scope.$index + '.' + item.name" :rules="rules[item.name]">
                    <el-input v-model="scope.row[item.name]" :type="item.input_type||''" clearable :placeholder="`请输入${item.label}`" size="small" />
                  </el-form-item>
                  <el-form-item v-else-if="item.type == 'slot'" :key="`structure-input-${index}-${scope.row.sku}`" :prop="'skuData.' + scope.$index + '.' + item.name" :rules="rules[item.name]">
                    <slot :name="item.name" :$index="scope.$index" :row="scope.row" :column="scope.column" />
                  </el-form-item>
                </template>
              </el-table-column>
              <!-- 批量设置,当 sku 数超过 2 个时出现 -->
              <template v-if="isBatch && form.skuData.length > 2" slot="append">
                <el-table :data="[{}]" :show-header="false">
                  <el-table-column :width="attribute.length * staticWidth + 50" align="center" :resizable="false">批量设置(Enter确定)</el-table-column>
                  <el-table-column v-for="(item, index) in structure" :key="`batch-structure-${index}`" align="center" :resizable="false" min-width="120px">
                    <el-input v-if="item.type == 'input' && item.batch != false" v-model="batch[item.name]" clearable :type="item.input_type||''" :placeholder="`填写一个${item.label}`" size="small" @keyup.enter.native="onBatchSet(item.name)" @change="onBatchSet(item.name)" />
                  </el-table-column>
                </el-table>
              </template>
            </el-table>
          </el-form>
        </div>
      </div>
    </template>
    
    <script>
    import R from '@/api/Base'
    const ATTR = new R('Attr', '', 'Index')
    const ATTR_VAL = new R('AttrVal', '', 'Index')
    import { sortObjByField } from '@/utils/index'
    export default {
      name: 'SkuForm',
      props: {
        sourceAttribute: {
          type: Object,
          default: () => {}
        },
        attribute: {
          type: Array,
          default: () => []
        },
        sku: {
          type: Array,
          default: () => []
        },
        structure: {
          type: Array,
          default: () => [
            { name: 'price', type: 'input', label: '价格' },
            { name: 'stock', type: 'input', label: '库存' }
          ]
        },
        // sku 字段分隔符
        separator: {
          type: String,
          default: ';'
        },
        // 无规格的 sku
        emptySku: {
          type: String,
          default: ''
        },
        // 是否显示 sku 选择栏
        disabled: {
          type: Boolean,
          default: false
        },
        // 主题风格
        theme: {
          type: Number,
          default: 1
        },
        // 是否开启异步加载
        async: {
          type: Boolean,
          default: false
        }
      },
      data() {
        return {
          newAttr: '',
          isInit: false,
          editorNameIndex: -1,
          attr: {},
          staticWidth: 90,
          form: {
            skuData: []
          },
          batch: {},
          selectNewAttr: null,
          selectNewAttrVal: null,
          addAttrId: 0
        }
      },
      computed: {
        rules() {
          // 重新生成验证规则
          const rules = {}
          this.structure.forEach(v => {
            if (v.type === 'input') {
              rules[v.name] = []
              if (v.required) {
                rules[v.name].push({ required: true, message: `${v.label}不能为空`, trigger: 'blur' })
              }
              if (v.validate) {
                rules[v.name].push({ validator: this.customizeValidate, trigger: 'blur' })
              }
            } else if (v.type === 'slot') {
              rules[v.name] = []
              if (v.required) {
                rules[v.name].push({ required: true, message: `${v.label}不能为空`, trigger: ['change', 'blur'] })
              }
              if (v.validate) {
                rules[v.name].push({ validator: this.customizeValidate, trigger: ['change', 'blur'] })
              }
            }
          })
          return rules
        },
        isBatch() {
          return this.structure.some(item => {
            return item.type === 'input' && item.batch !== false
          })
        },
        // 将 attr 数据还原会 attribute 数据的结构,用于更新 attribute
        emitAttribute() {
          const attribute = []
          for (const attr_id in this.attr) {
            const v1 = this.attr[attr_id]
            if (!v1.is_sku) continue
            const obj = {
              name: v1.name,
              item: {},
              attr_id
            }
            let len = 0
            for (const val_id in v1.item) {
              const v2 = v1.item[val_id]
              v2.checked && (obj.item['' + val_id] = { name: v2.name }) && len++
            }
    
            len && (obj['len'] = len) && (attribute.push(obj))
          }
          return attribute.sort(sortObjByField('len'))
        },
        rowSpan() {
          const obj = {}
          this.attribute.forEach(({ attr_id }, index) => {
            let rowSpan = 1
            for (let i = index + 1; i < this.attribute.length; i++) rowSpan *= this.attribute[i].len
            obj[attr_id] = rowSpan
          })
          return obj
        }
      },
      watch: {
        attr: {
          handler(n) {
            if (!this.isInit) {
              // 更新父组件
              this.$emit('update:attribute', this.emitAttribute)
              this.$emit('update:sourceAttribute', this.attr)
              this.$emit('change')
            }
            // 解决通过 $emit 更新后无法拿到 attribute 最新数据的问题
            this.$nextTick(() => {
              if (this.attribute.length !== 0) {
                this.combinationAttribute()
              } else {
                this.form.skuData = []
                const obj = {
                  sku: this.emptySku
                }
                this.structure.forEach(v => {
                  if (!(v.type === 'slot' && v.skuProperty === false)) {
                    obj[v.name] = typeof v.defaultValue !== 'undefined' ? v.defaultValue : ''
                  }
                })
                this.form.skuData.push(obj)
              }
              this.clearValidate()
            })
          },
          deep: true
        },
        'form.skuData': {
          handler(newValue, oldValue) {
            if (!this.isInit || (newValue.length === 1 && newValue[0].sku === this.emptySku)) {
              // 如果有老数据,或者 sku 数据为空,则更新父级 sku 数据
              if (oldValue.length || !this.sku.length) {
                // 更新父组件
                const arr = []
                newValue.forEach(v1 => {
                  const obj = { ...v1 }
                  this.structure.forEach(v2 => {
                    if (!(v2.type === 'slot' && v2.skuProperty === false)) {
                      obj[v2.name] = v1[v2.name] || (typeof v2.defaultValue !== 'undefined' ? v2.defaultValue : '')
                    }
                  })
                  arr.push(obj)
                })
                this.$emit('update:sku', arr)
                // this.$emit('change')
              }
            }
          },
          deep: true
        }
      },
      mounted() {
        !this.async && this.init()
      },
      methods: {
        arraySpanMethod({ column: { property: attr_id }, rowIndex }) {
          if (this.rowSpan[attr_id]) {
            const rowSpan = rowIndex % this.rowSpan[attr_id] === 0 ? this.rowSpan[attr_id] : 0
            return [rowSpan, 1]
          }
          return [1, 1]
        },
        querySearchAsync(name, cb) {
          ATTR.getList({ name }).then(({ data }) => {
            cb(data.filter(({ _id }) => !this.attr[_id]))
          }).catch(err => console.log('err', err))
        },
        querySearchAttrValAsync(name, cb) {
          ATTR_VAL.getList({ name, attr_id: this.addAttrId }).then(({ data }) => {
            cb(data.filter(({ _id }) => !this.attr[this.addAttrId].item[_id]))
          }).catch(err => console.log('err', err))
        },
        init() {
          this.isInit = true
          this.attr = this.sourceAttribute
          // 通过 sku 更新 skuData,但因为 skuData 是实时监听 attr 变化并自动生成,而 watch 是在 methods 后执行,所以增加 setTimeout 方法,确保 skuData 生成后在执行下面的代码
          setTimeout(() => {
            this.sku.forEach(skuItem => {
              // skuItem.sku = this.attribute.map(item => skuItem[item.name]).join(this.separator)
              this.form.skuData.forEach(skuDataItem => {
                if (skuItem.sku === skuDataItem.sku) {
                  skuDataItem['_id'] = skuItem._id
                  this.structure.forEach(structureItem => {
                    skuDataItem[structureItem.name] = skuItem[structureItem.name]
                  })
                }
              })
            })
            this.isInit = false
          }, 50)
        },
        editorName() {
          this.editorNameIndex = -1
        },
        // 根据 attribute 进行排列组合,生成 skuData 数据
        combinationAttribute(index = 0, dataTemp = []) {
          const arrt_id = this.attribute[index]?.attr_id ?? null
          if (arrt_id === null) return
          const attr = this.attribute[index]
          if (index === 0) {
            for (const val_id in attr.item) {
              const obj = {
                [arrt_id]: attr.item[val_id].name,
                attr: {}
              }
              obj['attr'][arrt_id] = +val_id
              obj['sku'] = Object.values(obj['attr']).sort().join(this.separator)
              this.structure.forEach(v => {
                if (!(v.type === 'slot' && v.skuProperty === false)) {
                  obj[v.name] = typeof v.defaultValue !== 'undefined' ? v.defaultValue : ''
                }
              })
              dataTemp.push(obj)
            }
          } else {
            const temp = []
            for (let i = 0; i < dataTemp.length; i++) {
              for (const val_id in attr.item) {
                temp.push({ ...dataTemp[i] })
                temp[temp.length - 1][arrt_id] = attr.item[val_id].name
                const tmpAttr = { ...temp[temp.length - 1]['attr'] }
                tmpAttr[arrt_id] = +val_id
                temp[temp.length - 1]['attr'] = tmpAttr
                temp[temp.length - 1]['sku'] = Object.values(tmpAttr).sort((v1, v2) => v1 - v2).join(this.separator)
              }
            }
            dataTemp = temp
          }
    
          if (index !== this.attribute.length - 1) {
            this.combinationAttribute(index + 1, dataTemp)
          } else {
            if (!this.isInit || this.async) {
              // 将原有的 sku 数据和新的 sku 数据比较,相同的 sku 则把原有的 sku 数据覆盖到新的 sku 数据里
              for (let i = 0; i < this.form.skuData.length; i++) {
                for (let j = 0; j < dataTemp.length; j++) {
                  if (this.form.skuData[i].sku === dataTemp[j].sku) {
                    dataTemp[j] = this.form.skuData[i]
                  }
                }
              }
            }
            this.form.skuData = dataTemp
          }
        },
        // 新增一个规格
        async onAddAttribute(index) {
          this.attr[index].addAttribute = this.attr[index].addAttribute.trim()
          if (this.attr[index].addAttribute !== '') {
            if (!this.attr[index].addAttribute.includes(this.separator)) {
              const data = { name: this.attr[index].addAttribute, checked: true }
    
              if (this.newAttr === this?.selectNewAttrVal?.name) {
                this.$set(this.attr[index].item, '' + this.selectNewAttrVal._id, data)
                this.newAttr = ''
                return
              }
              if (this.loading) return
              // 不然就需要把这个新属性加入到数据库中
              this.loading = true
              const res = await ATTR_VAL.addOne({ name: this.attr[index].addAttribute, attr_id: index })
                .then(r => r.data)
                .catch(err => { console.log(err) })
                .finally(() => {
                  this.loading = false
                })
              if (!res) return
              this.$set(this.attr[index].item, '' + res._id, data)
              this.attr[index].addAttribute = ''
            } else {
              this.$message({
                type: 'warning',
                message: `规格里不允许出现「 ${this.separator} 」字符,请检查后重新添加`
              })
            }
          }
        },
        onBatchSet(type) {
          if (this.batch[type] !== '') {
            this.form.skuData.forEach(v => {
              v[type] = this.batch[type]
            })
            this.batch[type] = ''
            // 批量设置完成后,触发一次当前列的验证
            this.validateFieldByColumns([type], () => {})
          }
        },
        // 自定义输入框验证,通过调用 structure 里的 validate 方法实现,重点是 callback 要带过去
        customizeValidate(rule, value, callback) {
          const [model, index, name] = rule.field.split('.')
          this.structure.forEach(v => {
            if (v.name === name) {
              v.validate(this.form[model], index, callback)
            }
          })
        },
        // sku 表单验证
        validate(callback) {
          this.$refs['form'].validate(valid => {
            callback(valid)
          })
        },
        validateFieldByColumns(colums, callback) {
          const props = []
          this.form.skuData.forEach((v, i) => {
            colums.forEach(v => {
              props.push(`skuData.${i}.${v}`)
            })
          })
          this.$refs['form'].validateField(props, valid => {
            callback(valid)
          })
        },
        validateFieldByRows(index, prop, callback) {
          this.$refs['form'].validateField([`skuData.${index}.${prop}`], valid => {
            callback(valid)
          })
        },
        clearValidate() {
          this.$refs['form'].clearValidate()
        },
        delSourceAttr(id) {
          this.$delete(this.attr, id)
        },
        async addAttr() {
          this.newAttr = this.newAttr.replace(/\s/g, '')
          if (!this.newAttr) return
          // 如果新加的 跟 select选中的name相等 那么就直接加入
          const data = { name: this.newAttr, is_sku: false, item: {}, canAddAttribute: true, addAttribute: '' }
    
          if (this.newAttr === this?.selectNewAttr?.name) {
            this.$set(this.attr, '' + this.selectNewAttr._id, data)
            this.newAttr = ''
            return
          }
          if (this.loading) return
          // 不然就需要把这个新属性加入到数据库中
          this.loading = true
          const res = await ATTR.addOne({ name: this.newAttr })
            .then(r => r.data)
            .catch(err => { console.log(err) })
            .finally(() => {
              this.loading = false
            })
          if (!res) return
          this.$set(this.attr, '' + res._id, data)
          this.newAttr = ''
        }
      }
    }
    </script>
    
    <style lang="scss" scoped>
    .sku-container {
        ::v-deep .el-card {
            margin: 10px 0;
            .el-card__header {
                line-height: initial;
                padding: 10px 20px;
            }
            .el-card__body {
                padding: 10px 20px 20px;
            }
        }
        .sku-check {
            .theme-1 {
                display: flex;
                flex-wrap: wrap;
                justify-content: space-between;
                margin-bottom: 10px;
                .item {
                    width: 32%;
                    &:last-child:nth-child(3n - 1) {
                        margin-right: calc(100% - 32% * 2 - 4% / 2) !important;
                    }
                    .add-attr {
                        width: 100%;
                        margin-top: 10px;
                    }
                }
            }
            .theme-2 {
                border: 1px solid #ebeef5;
                border-bottom: 0;
                margin-bottom: 20px;
            }
        }
        .sku-name {
            text-align: right;
        }
        .batch-set {
            width: 100%;
            margin-top: 5px;
        }
        .sku-list {
            line-height: initial;
            ::v-deep .el-input__inner {
                text-align: center;
            }
            ::v-deep .el-table__append-wrapper {
                overflow: initial;
                .el-table {
                    overflow: initial;
                    .el-table__body-wrapper {
                        overflow: initial;
                    }
                }
            }
            ::v-deep .el-form-item {
                margin-bottom: 0;
                .el-form-item__content {
                    line-height: initial;
                    .el-form-item__error {
                        margin-left: 0;
                    }
                }
            }
            .required_title::before {
                content: '*';
                color: #f56c6c;
            }
        }
    }
      .el-icon-delete{
        cursor: pointer;
        transition: color 0.3s;
        &:hover {
          color: red;
        }
      }
    </style>
    
    
    展开全文
  • 前端SKU实现

    千次阅读 2020-12-22 16:56:12
    公司项目做商品模块 本人实现了商品sku生成部分 在sku生成后可以选择禁用其中部分sku ( 实际场景部分商品没有sku中的一些规格的时候需要禁用部分sku ) 所以在前台选择sku的时候会出现部分sku无法选中的情况 ...

    一、 效果图

    二、 前言

    公司项目做商品模块 本人实现了商品sku生成部分 在sku生成后可以选择禁用其中部分sku ( 实际场景部分商品没有sku中的一些规格的时候需要禁用部分sku ) 所以在前台选择sku的时候会出现部分sku无法选中的情况

    最开始也抱着百度的方案写这一块功能 但是目前百度的大部分都是 矩形阵图算法 但是亲测都有bug 所以最后只能自己写了 无奈自己愚笨 写不出来 最后求助公司其他大佬 和在大佬的帮助下完成此功能

    三、 总体实现思路

    根据选中的规格和当前需要被验证的规格组成一个对象 然后在循环skuList判断 只要有一条sku中的规格满足这个对象中的每一项即可
    具体实现过程看下面代码实现过程

    四、 实现代码

    1、 先看数据 (这里是全数据 虽然有点长但是方便调试 测试)

        const specList = [
        	{ title: "颜色", list: ["红色", "紫色", "白色", "黑色"] },
            { title: "套餐", list: ["套餐一", "套餐二", "套餐三", "套餐四"] },
            { title: "内存", list: ["64G", "128G", "256G"] }
        ]
        
        const skuList = [
            // { id: 1608188117177, specs: ["红色", "套餐一", "64G"] },
            { id: 1608188117178, specs: ["红色", "套餐一", "128G"] },
            { id: 1608188117179, specs: ["红色", "套餐一", "256G"] },
            { id: 1608188117180, specs: ["红色", "套餐二", "64G"] },
            { id: 1608188117181, specs: ["红色", "套餐二", "128G"] },
            { id: 1608188117182, specs: ["红色", "套餐二", "256G"] },
            { id: 1608188117183, specs: ["红色", "套餐三", "64G"] },
            { id: 1608188117184, specs: ["红色", "套餐三", "128G"] },
            { id: 1608188117185, specs: ["红色", "套餐三", "256G"] },
            // { id: 1608188117186, specs: ["红色", "套餐四", "64G"] },
            // { id: 1608188117187, specs: ["红色", "套餐四", "128G"] },
            // { id: 1608188117188, specs: ["红色", "套餐四", "256G"] },
            // { id: 1608188117189, specs: ["紫色", "套餐一", "64G"] },
            // { id: 1608188117190, specs: ["紫色", "套餐一", "128G"] },
            // { id: 1608188117191, specs: ["紫色", "套餐一", "256G"] },
            { id: 1608188117192, specs: ["紫色", "套餐二", "64G"] },
            { id: 1608188117193, specs: ["紫色", "套餐二", "128G"] },
            { id: 1608188117194, specs: ["紫色", "套餐二", "256G"] },
            { id: 1608188117195, specs: ["紫色", "套餐三", "64G"] },
            { id: 1608188117196, specs: ["紫色", "套餐三", "128G"] },
            { id: 1608188117197, specs: ["紫色", "套餐三", "256G"] },
            { id: 1608188117198, specs: ["紫色", "套餐四", "64G"] },
            { id: 1608188117199, specs: ["紫色", "套餐四", "128G"] },
            { id: 1608188117200, specs: ["紫色", "套餐四", "256G"] },
            { id: 1608188117201, specs: ["白色", "套餐一", "64G"] },
            { id: 1608188117202, specs: ["白色", "套餐一", "128G"] },
            { id: 1608188117203, specs: ["白色", "套餐一", "256G"] },
            { id: 1608188117204, specs: ["白色", "套餐二", "64G"] },
            // { id: 1608188117205, specs: ["白色", "套餐二", "128G"] },
            // { id: 1608188117206, specs: ["白色", "套餐二", "256G"] },
            // { id: 1608188117207, specs: ["白色", "套餐三", "64G"] },
            // { id: 1608188117208, specs: ["白色", "套餐三", "128G"] },
            // { id: 1608188117209, specs: ["白色", "套餐三", "256G"] },
            // { id: 1608188117210, specs: ["白色", "套餐四", "64G"] },
            { id: 1608188117211, specs: ["白色", "套餐四", "128G"] },
            { id: 1608188117212, specs: ["白色", "套餐四", "256G"] },
            { id: 1608188117213, specs: ["黑色", "套餐一", "64G"] },
            { id: 1608188117214, specs: ["黑色", "套餐一", "128G"] },
            { id: 1608188117215, specs: ["黑色", "套餐一", "256G"] },
            { id: 1608188117216, specs: ["黑色", "套餐二", "64G"] },
            // { id: 1608188117217, specs: ["黑色", "套餐二", "128G"] },
            // { id: 1608188117218, specs: ["黑色", "套餐二", "256G"] },
            // { id: 1608188117219, specs: ["黑色", "套餐三", "64G"] },
            // { id: 1608188117220, specs: ["黑色", "套餐三", "128G"] },
            // { id: 1608188117221, specs: ["黑色", "套餐三", "256G"] },
            // { id: 1608188117222, specs: ["黑色", "套餐四", "64G"] },
            { id: 1608188117223, specs: ["黑色", "套餐四", "128G"] },
            { id: 1608188117224, specs: ["黑色", "套餐四", "256G"] }
        ]
    
    

    2、 页面代码

    <div id="app">
        <h3>前端SKU实现</h3>
        <div v-for="(item,index) in specList" :key="index">
          <div class='title'>{{item.title}}</div>
          <div class='spec'>
            <div class='spec-item' v-for="(its,ins) in item.list" :key="its.name + ins">
              <span @click="changeSpec(item.title, its.name, its.able)" :class="[selectSpec[item.title] === its.name ? 'active' : '' , its.able? '' : 'disabled']">{{its.name}}</span>
            </div>
          </div>
        </div>
      </div>
      
     <style>
    .spec-item{
      display: inline-block;
      margin-right: 10px;
    }
    .spec-item span {
      border:1px solid #eee;
      cursor: pointer;
      padding:5px 10px;
    }
    .spec-item .active{
      border: 1px solid red;
      background-color: red;
      color:#fff;
    }
    .spec-item .disabled{
      color: #c0c4cc;
      cursor: not-allowed;
      background-image: none;
      background-color: #fff;
      border-color: #ebeef5;
    }
    </style>
    

    3、 核心JS逻辑

    // 第一步引入数据    这里引入的数据就是上面的 specList  skuList
    import {specList, skuList} from './data'
    
    export default {
      name: 'App',
      data() {
        return {
          specList:[],
          skuList:[],
          selectSpec:{},  // 选择数据的对象 将已选的数据放在这个对象里面记录下来  用对象的好处在下面深拷贝处就能体验到了 
        }
      },
     
      created() {
        //  第二步  处理数据
        this.skuList = skuList;
        // 初始化选择数据的对象 
        specList.forEach(item => {
          this.$set(this.selectSpec, item.title, "");
        })
        // 将规格数据处理成我们视图所需要的数据类型 
        this.specList = specList.map(item => {
          return {
            title:item.title,
            list: item.list.map(its => {
              return {
                name:its,
                //  判断是否可以选择
                //  这里相当于init 初始化数据  this.isAble() 核心判断逻辑  
                able: this.isAble(item.title, its)  // 注释的调试看逻辑代码 false 
              }
            })
          }
        })
        // 注释的调试看逻辑代码
        // this.selectSpec = {
        //   '颜色':'',
        //   '套餐':'套餐一',
        //   '内存':'64G'
        // }
        // this.isAble('颜色', '红色')
      },
      methods:{
        // 核心判断逻辑 
        // 判断规格是否可以被选择  核心函数 key当前的规格的title   value规格值
        isAble(key, value){
          // 深拷贝 避免被影响  
          var copySelectSpec = JSON.parse(JSON.stringify(this.selectSpec));
          // 用对象的好处就在这了 直接赋值当前验证项
          copySelectSpec[key] = value;
          // 用数组的 some 方法 效率高 符合条件直接退出循环
          let flag = this.skuList.some(item => {
            // 条件判断 核心逻辑判断
            // console.log(item)
            var i = 0 ; 
            // 这个for in 循环的逻辑就对底子不深的人来说就看不懂了 原理就是循环已经选中的 和 正在当前对比的数据 和 所有的sku对比 只有当前验证的所有项满足sku中的规格或者其他规格为空时 即满足条件 稍微有点复杂 把注释的调试代码打开就调试下就可以看懂了
            for(let k in copySelectSpec) {
              //  console.log(copySelectSpec[k])  // 注释的调试看逻辑代码
              if(copySelectSpec[k] != "" && item.specs.includes(copySelectSpec[k])){
                // console.log(item)
                i++
              }else if (copySelectSpec[k] == "") {
                i++;
              }
            }
            // 符合下面条件就退出了 不符合会一直循环知道循环结束没有符合的条件就 return false 了 
            // console.log(i) // 注释的调试看逻辑代码
            return i == specList.length
          })
          console.log(flag)
          return flag
        },
        // 点击事件
        changeSpec(key,value,able){
          if(!able) return
          if(this.selectSpec[key] === value){
            this.selectSpec[key] = ''
          }else {
            this.selectSpec[key] = value
          }
          // forEach循环改变原数组 
          this.specList.forEach(item => {
            item.list.forEach(its => {
              its.able = this.isAble(item.title, its.name);
              console.log(its.name, its.able);
            });
          });
        }
      },
    }
    

    五、 总结

    created处理数据的时候将 specList 的数据处理成我们逻辑和视图结合需要的数据 ( 不要在意数据一不一样 我们后台给的数据也一样 需要前端自己二次处理 ) 此处我用的 map 方法处理 用其他循环方法处理也可以只要能得到我们最终的数据就行了

    // 处理后的数据
    this.specList = [
        {
            "title":"颜色",
            "list":[
                {
                    "name":"红色",
                    "able":true
                },
                {
                    "name":"紫色",
                    "able":true
                },
                {
                    "name":"白色",
                    "able":true
                },
                {
                    "name":"黑色",
                    "able":true
                }
            ]
        },
        .....
    ] 
    
    

    当在created处理able数据时 当前没用选中数据 当前的selectSpec 初始化完成 应该是这个样子的

    this.selectSpec = {"颜色":"","套餐":"","内存":""}
    

    循环处理this.specList的时候 在处理每个规格是否可以被选中的时候 假如循环到 红色 时 此时的 this.selectSpec = {"颜色":"红色","套餐":"","内存":""} 在看下面的这段代码

       isAble(key, value){  //此时 key = 颜色    value = 红色
          var copySelectSpec = JSON.parse(JSON.stringify(this.selectSpec));
          copySelectSpec[key] = value;  // 此时 copySelectSpec = {"颜色":"红色","套餐":"","内存":""}
          let flag = this.skuList.some(item => {
          	// 按照上面的skuList数据 此时第一次循环 item = { id: 1608188117178, specs: ["红色", "套餐一", "128G"] }
            var i = 0 ; 
            for(let k in copySelectSpec) {  //  for in  懂吧?
              // copySelectSpec[k]  依次是 '红色' \ '' \ ''
              if(copySelectSpec[k] != "" && item.specs.includes(copySelectSpec[k])){  // item中有红色 i++    
                i++
              }else if (copySelectSpec[k] == "") {   // ''后面规格是空字符串 不管 也执行 i++
                i++;
              }
            }
            // 此时 红色满足条件 return true  此时红色规格校验完毕 并会退出当选skuList循环
            return i == specList.length
          })
          return flag
        },
    

    上述代码模拟程序 在初始化红色规格时侯的逻辑 其余规格也相同

    由上述代码运行逻辑可见 当选择规格后 就是这样的
    假如选择了 红色 此时 this.selectSpec = {"颜色":"红色","套餐":"","内存":""}
    验证颜色时 会被这段代码覆盖 copySelectSpec[key] = value
    验证其他规格时 假如分别验证 套餐一 或者 套餐四

    	// 验证套餐一     
            let flag = this.skuList.some(item => {
          	// 按照上面的skuList数据 此时第一次循环 item = { id: 1608188117178, specs: ["红色", "套餐一", "128G"] }
            var i = 0 ; 
            for(let k in copySelectSpec) { 
              // copySelectSpec[k]  依次是 '红色' \ '套餐一' \ ''
              if(copySelectSpec[k] != "" && item.specs.includes(copySelectSpec[k])){  // item中有红色 i++    item中有套餐一 i++  
                i++
              }else if (copySelectSpec[k] == "") {   // ''后面规格是空字符串 不管 也执行 i++
                i++;
              }
            }
            // 此时套餐一满足条件 return true  此时套餐一规格校验完毕 并会退出当选skuList循环
            return i == specList.length
            
            
            
            // 验证套餐四
            let flag = this.skuList.some(item => {
          	// 按照上面的skuList数据 此时第一次循环 item = { id: 1608188117178, specs: ["红色", "套餐一", "128G"] }
    	// 直至循环到最后 { id: 1608188117224, specs: ["黑色", "套餐四", "256G"] }  都没有出现一条sku 同时出现套餐四和红色规格的sku  
            // 所以i 最多等于2  specList.length等于3  return false
            var i = 0 ; 
            for(let k in copySelectSpec) {  
              // copySelectSpec[k]  依次是 '红色' \ '套餐四' \ ''
              if(copySelectSpec[k] != "" && item.specs.includes(copySelectSpec[k])){ 
                // item中有红色的就没用套餐四 有套餐四的就没用红色规格 所以 i=1
                i++
              }else if (copySelectSpec[k] == "") {   // '' 后面规格是空字符串 不管 也执行 i++
                i++;
              }
            }
            // 最终i = 2 return false
            return i == specList.length
    
    

    由此可见 不管怎么选择都可以验证所有的规格是否可以被选

    下面附上demo地址 gitHub:前端商品sku-demo

    记得给我小星星 😊

    展开全文
  • Android 实现SKU选择通用方式

    千次阅读 2021-11-18 11:25:15
    实现sku的方式一般采用在获取到数据后拆分所有条件的可能性,实现方式参考js的实现,代码如下: SkuHelp.kt /** 不考虑服务端的格式类型,将对应格式翻译成如下格式: *[ * { "颜色": "红", "尺码": "大", "型号": ...

    效果如下:

    在这里插入图片描述

    说明:

    实现sku的方式一般采用在获取到数据后拆分所有条件的可能性,实现方式参考js的实现,代码如下:

    SkuHelp.kt

    /**
    不考虑服务端的格式类型,将对应格式翻译成如下格式:
         *[
         * { "颜色": "红", "尺码": "大", "型号": "A", "skuId": "3158055" },
         * { "颜色": "白", "尺码": "大", "型号": "A", "skuId": "3158054" },
         * { "颜色": "白", "尺码": "中", "型号": "B", "skuId": "3133859" },
         * { "颜色": "蓝", "尺码": "小", "型号": "C", "skuId": "3516833" }
         *]
         (注:skuId为数据id)
    **/
    class SkuHelp {
    
        private val skuArray = "skuArray"
    
        // 将数据翻译为sku可识别的通用数据,此处为特殊处理方式
        fun transformToBean(goodsSpecArray: List<SpecificationsBean>): Pair<Map<String, String>, List<Map<String, String>>> {
            val rootArray = mutableListOf<MutableMap<String, String>>()
            val tagImgMap = mutableMapOf<String, String>()
    
            goodsSpecArray.forEach { bean ->
                if (bean.id != null && bean.goodsSpecValue != null) {
                    val valueMap = bean.goodsSpecValue.mapFromJson<String, String>()
                    valueMap["id"] = bean.id
    
                    // 处理数据中有图片的现象
                    valueMap.forEach { (k, v) ->
                        // 判断value上是否有图片存在
                        if (v.contains(",")) {
                            val splitArray = v.split(',')
                            valueMap[k] = splitArray[0]
                            tagImgMap[splitArray[0]] = splitArray[1]
                        }
                    }
                    rootArray.add(valueMap)
                }
            }
    
            return Pair(tagImgMap, rootArray)
        }
    
        /**
         * rootArray 格式为:
         *[
         * { "颜色": "红", "尺码": "大", "型号": "A", "skuId": "3158055" },
         * { "颜色": "白", "尺码": "大", "型号": "A", "skuId": "3158054" },
         * { "颜色": "白", "尺码": "中", "型号": "B", "skuId": "3133859" },
         * { "颜色": "蓝", "尺码": "小", "型号": "C", "skuId": "3516833" }
         *]
         */
        fun initData(rootArray: List<Map<String, String>>): Triple<Map<String, List<String>>, Map<String, Map<String, List<String>>>, List<String>> {
            val keyArray = getSkuKey(rootArray)
            val (allKeyArray, resultMap) = combineAttr(rootArray, keyArray)
            val conditionMap = buildResult(allKeyArray)
    
            return Triple(resultMap, conditionMap, keyArray)
        }
    
        /**
         * 处理合并后的条件会有多个特殊字符
         */
        fun trimSplit(trim: String): String {
            // ⊙abc⊙ => abc
            // ⊙a⊙⊙b⊙c⊙ => a⊙b⊙c
            val reLeft = Regex("^$spliter+")
            val reRight = Regex("$spliter+\$")
            val reSplit = Regex("$spliter+")
    
            return trim.replace(reLeft, "")
                .replace(reRight, "")
                .replace(reSplit, spliter)
        }
    
        // 获取条件sku
        fun getSkuArray(condition: String, conditionMap: Map<String, Map<String, List<String>>>): List<String>? {
            val newMap = conditionMap[condition]
            return newMap?.get(skuArray)
        }
    
        // 获取key, 如:颜色,品牌,尺码
        private fun getSkuKey(rootArray: List<Map<String, String>>): List<String> {
            val keyArray = mutableListOf<String>()
            if (rootArray.isNotEmpty()) {
                val valueMap = rootArray[0]
                valueMap.forEach { (k, _) ->
                    // 过滤数据为id的项
                    if (k == "id") {
                        return@forEach
                    }
                    keyArray.add(k)
                }
            }
            return keyArray
        }
    
        val spliter = "\u2299"
    
        // 计算组合数据
        private fun combineAttr(
            rootArray: List<Map<String, String>>,
            keyArray: List<String>
        ): Pair<List<MutableMap<String, String>>, MutableMap<String, MutableList<String>>> {
            // 将数据转换成可视化数据,通过id查找对应数据
            /**
             * 为了通用,此处将数据打包成如下格式:
            [
            { "颜色": "红", "尺码": "大", "型号": "A", "skuId": "3158055" },
            { "颜色": "白", "尺码": "大", "型号": "A", "skuId": "3158054" },
            { "颜色": "白", "尺码": "中", "型号": "B", "skuId": "3133859" },
            { "颜色": "蓝", "尺码": "小", "型号": "C", "skuId": "3516833" }
            ]
             */
    //        val beanArray = mutableListOf<MutableMap<String, String>>()
    //        goodsSpecArray.forEach { bean ->
    //            if (bean.id != null && bean.goodsSpecValue != null) {
    //                val valueMap = bean.goodsSpecValue.mapFromJson<String, String>()
    //                valueMap["id"] = bean.id
    //                beanArray.add(valueMap)
    //            }
    //        }
    
            // 将界面展示数据分离如: {"颜色":["红","白","蓝"],"尺码":["大","中","小"],"型号":["A","B","C"]}
            val resultMap = mutableMapOf<String, MutableList<String>>()
            // 将条件与id整合起来如:[{{path=红⊙大⊙A, sku=3158055}...}]
            val allKeyArray = mutableListOf<MutableMap<String, String>>()
    
            for (itemMap in rootArray) {
                val valueArray = mutableListOf<String>()
                for (key in keyArray) {
                    val array = mutableListOf<String>()
                    if (resultMap[key] != null) {
                        array.addAll(resultMap[key]!!)
                    }
                    if (!array.contains(itemMap[key])) {
                        array.add(itemMap[key]!!)
                    }
                    resultMap[key] = array
                    valueArray.add(itemMap[key]!!)
                }
                allKeyArray.add(
                    mutableMapOf(
                        "path" to valueArray.joinToString(separator = spliter),
                        "sku" to itemMap["id"]!!
                    )
                )
            }
    
            return Pair(allKeyArray, resultMap)
        }
    
        // 合并所有条件
        private fun getAllKeys(allKeyArray: List<Map<String, String>>): List<String> {
            // 如: ["红⊙大⊙A",...]
            val keyArray = mutableListOf<String>()
            allKeyArray.forEach { keyMap ->
                keyArray.add(keyMap["path"]!!)
            }
            return keyArray
        }
    
        // 生成所有子集是否可选、库存状态 map
        private fun buildResult(allKeyArray: List<Map<String, String>>): MutableMap<String, MutableMap<String, MutableList<String>>> {
            // 将条件整合成一个key的List
            val allKeys = getAllKeys(allKeyArray)
            // 获取所有数据的可能性如: {"蓝色⊙X"={skuArray=[1, 2, 3]}...}
            val resMap = mutableMapOf<String, MutableMap<String, MutableList<String>>>()
    
            allKeys.forEachIndexed { index, allKey ->
                val sku = allKeyArray[index]["sku"]
                val values = allKey.split(spliter)
    
                val allSets = powerSet(values)
                // 每个组合的子集
                allSets.forEachIndexed { _, set ->
                    val key = set.joinToString(separator = spliter)
    
                    if (resMap[key] != null) {
                        resMap[key]?.get(skuArray)?.add(sku!!)
                    } else {
                        resMap[key] = mutableMapOf(
                            skuArray to mutableListOf(sku!!)
                        )
                    }
                }
            }
    
            return resMap
        }
    
        /**
         * 取得集合的所有子集「幂集」
        arr = [1,2,3]
    
        i = 0, ps = [[]]:
        j = 0; j < ps.length => j < 1:
        i=0, j=0 ps.push(ps[0].concat(arr[0])) => ps.push([].concat(1)) => [1]
        ps = [[], [1]]
    
        i = 1, ps = [[], [1]] :
        j = 0; j < ps.length => j < 2
        i=1, j=0 ps.push(ps[0].concat(arr[1])) => ps.push([].concat(2))  => [2]
        i=1, j=1 ps.push(ps[1].concat(arr[1])) => ps.push([1].concat(2)) => [1,2]
        ps = [[], [1], [2], [1,2]]
    
        i = 2, ps = [[], [1], [2], [1,2]]
        j = 0; j < ps.length => j < 4
        i=2, j=0 ps.push(ps[0].concat(arr[2])) => ps.push([3])    => [3]
        i=2, j=1 ps.push(ps[1].concat(arr[2])) => ps.push([1, 3]) => [1, 3]
        i=2, j=2 ps.push(ps[2].concat(arr[2])) => ps.push([2, 3]) => [2, 3]
        i=2, j=3 ps.push(ps[3].concat(arr[2])) => ps.push([2, 3]) => [1, 2, 3]
        ps = [[], [1], [2], [1,2], [3], [1, 3], [2, 3], [1, 2, 3]]
         */
        private fun powerSet(set: List<String>): List<List<String>> {
            //已知所求集合的幂集会有2^n个元素
            val size = 2 shl set.size
            val powerSet: MutableList<List<String>> = ArrayList(size)
            //首先空集肯定是集合的幂集
            powerSet.add(Collections.emptyList())
            for (element in set) {
                //计算当前元素与已存在幂集的组合
                val preSize = powerSet.size
                for (i in 0 until preSize) {
                    val combineSubset: MutableList<String> = ArrayList(powerSet[i])
                    combineSubset.add(element)
                    powerSet.add(combineSubset)
                }
            }
            return powerSet
        }
    }
    

    SpecificationsDialog.kt

    /**
     * 商品规格选择sku
     */
    class SpecificationsDialog(private val contextX: Context) :
        AlertDialog(contextX, R.style.DialogWindowStyle_Shop_bg) {
        private val binding: PdDialogLayoutSpecificationsBinding =
            PdDialogLayoutSpecificationsBinding.inflate(layoutInflater)
    
        private val adapter: SpecificationsDialogAdapter = SpecificationsDialogAdapter()
    
        // 存储有库存,以id建立的字典方便为了获取具体数据
        private val mapModel = mutableMapOf<String, SpecificationsBean>()
    
        // 需要满足的条件总数
        private var totalConditions = 0
    
        // adapter数据源
        private val skuBeanArray = mutableListOf<ShowSKUBean>()
    
        // 缓存之前存储的条件 - 选中条件,key是(如:品牌:x1), value是goodsSpecValue
        private val cacheValueMap = LinkedHashMap<String, String>()
    
        // =
        private val cacheConditionMap: MutableMap<String, Map<String, List<String>>> = mutableMapOf()
    
        // sku 列的名称,如:品牌,..
        private val keysArray: MutableList<String> = mutableListOf()
    
        // 具体sku数据,用于提交
        private var cacheModel: SpecificationsBean? = null
    
        private val skuHelp = SkuHelp()
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(binding.root)
            initDefWindows()
            initView()
    
            initTestData()
            binding.pdTvStock.text = "1000"
        }
    
        // 重置所有属性
        private fun resetData() {
            cacheModel = null
            totalConditions = 0
            keysArray.clear()
            cacheConditionMap.clear()
            cacheValueMap.clear()
            skuBeanArray.clear()
            mapModel.clear()
        }
    
        // 初始化数据
        fun initData(beanArray: List<SpecificationsBean>) {
            resetData()
    
            // 将数据转换成map,用id对应数据方便查找
            beanArray.forEachIndexed { index, bean ->
                if (index == 0) {
                    cacheModel = bean
                }
                mapModel[bean.id!!] = bean
            }
    
            val (tagImgMap, goodsSpecMap) = skuHelp.transformToBean(beanArray)
            val (uiMap, conditionMap, keysArray) = skuHelp.initData(goodsSpecMap)
    
            this.keysArray.addAll(keysArray)
    
            // 需要满足的条件总数
            totalConditions = uiMap.size
            uiMap.forEach { (k, vList) ->
                val dvList = vList.map { v ->
                    if (tagImgMap[v] == null) {
                        DialogSpecsValue(value = v)
                    } else {
                        DialogSpecsValue(value = v, img = tagImgMap[v])
                    }
                }
                skuBeanArray.add(ShowSKUBean(k, dvList))
            }
    
            cacheConditionMap.putAll(conditionMap)
            defSelected()
            adapter.setList(skuBeanArray)
        }
    
        // 默认选中数据中第一条
        private fun defSelected() {
            cacheModel?.let { bean ->
                if (bean.goodsSpecValue == null) return@let
                val valueMap = bean.goodsSpecValue.mapFromJson<String, String>()
                keysArray.forEach { key ->
                    val value = valueMap[key] ?: return@forEach
                    if (value.contains(",")) {
                        val splitArray = value.split(',')
                        cacheValueMap[key] = splitArray[0]
                    } else {
                        cacheValueMap[key] = value
                    }
                }
    
                defSettingSelected()
            }
        }
    
        // TODO 暂时用于测试,部分逻辑可以通用
        // 模拟数据
        private fun initTestData() {
            val goodsSpecArray = mutableListOf(
                SpecificationsBean(
                    "1",
                    "0",
                    goodsDefaluePrice = 100f,
                    goodsPrice = 85.6f,
                    goodsNums = 10000,
                    goodsSpecValue = "{\"颜色\":\"蓝色\",\"尺码\":\"X\",\"风格\":\"时尚\"}"
                ),
                SpecificationsBean(
                    "2",
                    "0",
                    goodsDefaluePrice = 100f,
                    goodsPrice = 87.2f,
                    goodsNums = 10000,
                    goodsSpecValue = "{\"颜色\":\"蓝色\",\"尺码\":\"X\",\"风格\":\"简约\"}"
                ),
                SpecificationsBean(
                    "3",
                    "0",
                    goodsDefaluePrice = 100f,
                    goodsPrice = 87.2f,
                    goodsNums = 10000,
                    goodsSpecValue = "{\"颜色\":\"蓝色\",\"尺码\":\"X\",\"风格\":\"欧式\"}"
                ),
                SpecificationsBean(
                    "4",
                    "0",
                    goodsDefaluePrice = 100f,
                    goodsPrice = 67.2f,
                    goodsNums = 10000,
                    goodsSpecValue = "{\"颜色\":\"蓝色\",\"尺码\":\"M\",\"风格\":\"简约\"}"
                ),
                SpecificationsBean(
                    "5",
                    "0",
                    goodsDefaluePrice = 100f,
                    goodsPrice = 57.2f,
                    goodsNums = 10000,
                    goodsSpecValue = "{\"颜色\":\"蓝色\",\"尺码\":\"M\",\"风格\":\"欧式\"}"
                ),
                SpecificationsBean(
                    "6",
                    "0",
                    goodsDefaluePrice = 100f,
                    goodsPrice = 87.2f,
                    goodsNums = 10000,
                    goodsSpecValue = "{\"颜色\":\"红色\",\"尺码\":\"X\",\"风格\":\"时尚\"}"
                ),
                SpecificationsBean(
                    "7",
                    "0",
                    goodsDefaluePrice = 100f,
                    goodsPrice = 83.2f,
                    goodsNums = 10000,
                    goodsSpecValue = "{\"颜色\":\"红色\",\"尺码\":\"M\",\"风格\":\"时尚\"}"
                ),
            )
    
            resetData()
    
            // 将数据转换成map,用id对应数据方便查找
            goodsSpecArray.forEachIndexed { index, bean ->
                if (index == 0) {
                    cacheModel = bean
                }
                mapModel[bean.id!!] = bean
            }
    
            val (tagImgMap, goodsSpecMap) = skuHelp.transformToBean(goodsSpecArray)
            val (uiMap, conditionMap, keysArray) = skuHelp.initData(goodsSpecMap)
    
            this.keysArray.addAll(keysArray)
    
            // 需要满足的条件总数
            totalConditions = uiMap.size
            uiMap.forEach { (k, vList) ->
                val dvList = vList.map { v ->
                    if (tagImgMap[v] == null) {
                        DialogSpecsValue(value = v)
                    } else {
                        DialogSpecsValue(value = v, img = tagImgMap[v])
                    }
                }
                skuBeanArray.add(ShowSKUBean(k, dvList))
            }
    
            cacheConditionMap.putAll(conditionMap)
            defSelected()
            adapter.setList(skuBeanArray)
        }
    
        private fun initView() {
            binding.pdItvBack.setOnClickListener { dismiss() }
            binding.pdRvSpecs.adapter = adapter
            val layoutManager = LinearLayoutManager(contextX)
            layoutManager.orientation = LinearLayoutManager.VERTICAL
            binding.pdRvSpecs.layoutManager = layoutManager
    
            adapter.setItemClick { holder, p, dialogSpecsValue, key ->
                // 将选中条件缓存
                if (!dialogSpecsValue.isSelect) {
                    cacheValueMap[key] = dialogSpecsValue.value
                } else {
                    cacheValueMap.remove(key)
                }
    
                settingSelected(key, dialogSpecsValue)
                adapter.notifyDataSetChanged()
    
                // 判断是否需要给出价格
                if (this.totalConditions == cacheValueMap.size) {
                    val synthesisKey = obtainConditionSplicing()
                    val valueArray = skuHelp.getSkuArray(synthesisKey, cacheConditionMap)
                    var id = ""
                    if (valueArray.isNullOrEmpty()) {
                        "无此属性搭配".showToast()
                        return@setItemClick
                    } else {
                        id = valueArray[0]
                    }
                    cacheModel = mapModel[id]
                    Log.e("fyc", " bean >>>> $cacheModel")
                } else {
                    cacheModel = null
                }
            }
        }
    
        private fun initDefWindows() {
            setCanceledOnTouchOutside(true)
            setCancelable(true)
    
            val localWindow = this.window
            localWindow?.setWindowAnimations(R.style.DialogWindowStyle)
            localWindow?.setGravity(Gravity.BOTTOM)
            localWindow?.setBackgroundDrawableResource(android.R.color.transparent)
            val lp = localWindow!!.attributes
            val wh =
                intArrayOf(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
            lp.width = wh[0]
            lp.height = wh[1]
            localWindow.attributes = lp
        }
    
        // 更新所有属性状态
        private fun updateStatus() {
    
            val cacheValueArray = mutableListOf<String>()
            cacheValueMap.forEach { (_, v) ->
                cacheValueArray.add(v)
            }
    
            skuBeanArray.forEachIndexed { i, bean ->
                val copyArray = Array(totalConditions){""}
                for (z in 0..totalConditions) {
                    if (z < cacheValueArray.size) {
                        copyArray[z] = cacheValueArray[z]
                    }
                }
    
                bean.valueList.forEach valueTag@ { valueBean ->
                    // 选中项,忽略
                    if (cacheValueMap[bean.key] == valueBean.value) {
                        // continue
                        return@valueTag
                    }
    
                    Log.e("fyc", "${bean.key} :  ${valueBean.value}")
                    copyArray[i] = valueBean.value
    
                    // 合并成条件
                    val conditionKey = skuHelp.trimSplit(copyArray.joinToString(separator = skuHelp.spliter))
                    // 获取合适的条件
                    if (cacheConditionMap[conditionKey] == null) {
                        valueBean.isCanSelect = false
                        valueBean.isSelect = false
                    } else {
                        valueBean.isCanSelect = true
                        valueBean.isSelect = false
                    }
                }
            }
        }
    
        // 设置选中项 - item点击事件
        private fun settingSelected(key: String, bean: DialogSpecsValue) {
            // 遍历现有数据判断可选项
            skuBeanArray.forEach { model ->
                // 判断当前项目是否具备选中项
                val selectionVal = cacheValueMap[model.key]
                model.valueList.forEach { dialogSpecsValue ->
                    // 表示当前项目被选中
                    if (selectionVal == dialogSpecsValue.value) {
                        dialogSpecsValue.isSelect = true
                    }
                    // 当前选中项,是无效选项触发
                    if (selectionVal == dialogSpecsValue.value && !dialogSpecsValue.isCanSelect) {
                        cacheValueMap.clear()
                        cacheValueMap[key] = bean.value
                        dialogSpecsValue.isSelect = true
                        dialogSpecsValue.isCanSelect = true
                    }
                }
            }
    
            // 对当前条件进行排序,防止出现,选中组合混乱
            val selectMap = LinkedHashMap<String, String>()
            keysArray.forEach { dataKey ->
                if (cacheValueMap[dataKey] != null) {
                    selectMap[dataKey] = cacheValueMap[dataKey]!!
                }
            }
            cacheValueMap.clear()
            cacheValueMap.putAll(selectMap)
    
            updateStatus()
        }
    
        // 设置选中项
        private fun defSettingSelected() {
            // 遍历现有数据判断可选项
            skuBeanArray.forEach { model ->
                // 判断当前项目是否具备选中项
                val selectionVal = cacheValueMap[model.key]
                model.valueList.forEach { dialogSpecsValue ->
                    // 表示当前项目被选中
                    if (selectionVal == dialogSpecsValue.value) {
                        dialogSpecsValue.isSelect = true
                    }
                }
            }
    
            updateStatus()
        }
    
        // 将选中的条件组成和实际可选条件获取key
        private fun obtainConditionSplicing(): String {
            val splicingArray = mutableListOf<String>()
            cacheValueMap.forEach { (_, v) ->
                splicingArray.add(v)
            }
            return splicingArray.joinToString(separator = skuHelp.spliter)
        }
    }
    

    SpecificationsBean.kt

    /**
     * 商铺规格
     */
    @JsonClass(generateAdapter=true)
    data class SpecificationsBean(
        // 商品规格id
        val id: String?,
        // 商品id
        val goodsId: String?,
        // 商品规格值
        val goodsSpecValue: String?,
        // 商品价格
        val goodsPrice: Float?,
        // 商品原价
        val goodsDefaluePrice: Float?,
        // 商品数量 - 库存
        val goodsNums: Int?
    )
    

    ShowSKUBean.kt

    /**
     * 将map数据转换成rv可识别的数据
     */
    data class ShowSKUBean(
        // 如:品牌
        val key: String,
        // 如:x1,x2...
        val valueList: List<DialogSpecsValue>
    )
    

    DialogSpecsValue.kt

    /**
     * dialog-sku展示项
     */
    data class DialogSpecsValue(
        // 是否能选择
        var isCanSelect: Boolean = true,
        // 是否能选中
        var isSelect: Boolean = false,
        var value: String = "",
        // 对应数据图片,没有可为空
        var img: String? = null
    ) 
    

    总结:代码比较多,需要给位耐心看完,可以实现图片中一样的效果,懒得废话太多,sku实现的思路并不复杂,麻烦的是如何写算法处理,此处只展示一种sku实现方式,这种方式的弊端是吃内存,但至少效果可以实现,一般来讲后端会对应作出限制,理论上内存这块不用太过于担心,由于我的数据中如:‘颜色:蓝色,/static/x/x.jpg’ value可能携带图片此处处理方式是针对于需要图片的方式,如果不需要处理图片,直接将图片处理的代码删除即可,希望本文对大家有所帮助。

    展开全文
  • // 这个for in 循环的逻辑就对底子不深的人来说就看不懂了 原理就是循环已经选中的 和 正在当前对比的数据 和 所有的sku对比 只有当前验证的所有项满足sku中的规格或者其他规格为空时 即满足条件 稍微有点复杂 把...
  • 不啰嗦了,直接上代码 <!... <... <head>...Sku 多维属性状态判断</title> <script src="http://misc.360buyimg.com/jdf/lib/jquery-1.6.4.js"></script> <style>
  • 2-2 SPU和SKU详解   商城系统中的商品信息肯定避免不了SPU和SKU这两个概念,本节就给大家详细介绍下这块的内容 1、掌握SKU和SPU关系 SPU = Standard Product Unit (标准化产品单元) SPU是商品信息聚合的最小单位,...
  • 更新SKU表数据 1、 获取修改商品的详情信息 点就修改按钮时,我们需要先获取要修改的商品详情信息 接口分析 请求方式: GET/meiduo_admin/skus/(?P<pk>\d+)/ 请求参数: 通过请求头传递jwt token数据。 ...
  • SKU(Stock Keeping Unit)库存量单位,即库存进出计量的单位, 可以是以件、盒、托盘等为单位。SKU是物理上不可分割的最小存货单元。在使用时要根据不同业态,不同管理模式来处理。 区别: SPU:属性值 特性...
  • 前面步骤一到二是上传图片,第三步才是商品sku模板的开始。1.下图是我们看到模板的第一步(添加规格类型),就是不论什么规格都是自己拟定义image.png2.下图是我们看到的第二步,增加规格的名称image.pngimage...
  • 简介 一般情况下我们使用5张表就可以...spu表和sku表实现不同商品的存储:spu表使用attribute_list字段保存属性集合,查询时使用product_id和product_specs去sku表中获取的具体的单品信息。 spu表中可以增加一些商品的
  • }, 删除属性的操作 删除属性是比较简单的 只要删除 sku_arr 的数据后,在重新生成表头和表格数据即可 删除属性值的操作 需要先找到goodsSpecs中对应规格值删除, 然后通过循环表格数据table_data 把含有对应规格...
  • SKU数据库设计

    2020-12-31 12:39:54
    ## 方法二 ### 京东的商品 SKU 设计 举个例子:博世的空调滤清器,再商品列表页空调滤清器分类博世品牌下全是同一 SPU 不同 SKU 的热销商品,进入到单个商品页面里,也可以选择其他的 SKU ,选择其他 SKU 就跳转到...
  • SKU表管理 在sku表中我们需要对SKU表数据进行增删改查操作,这时候我们可以借助于视图集中的ModelViewset来完成相应的操作 查询获取sku表列表数据 【商品的详细信息,要引入事务的管理,对于多张表的处理需要...
  • 另一个是当修改了商品信息之后,商品SKU的ID会发生变化,由于购物车表和订单商品表都关联了商品SKU的ID,这样就会导致匹配不上。最近对这两个问题做了点优化,下面来聊聊优化的思路。商品的SPU和SKU首先我...
  • “店铺券”, “textColor”: “#FD5F20”, “type”: “default” } ] }, “rootCategoryId”: “50013886”, “sku”: { “skuBase”: { “props”: [ { “name”: “颜色分类”, “pid”: “1627207”, “values...
  • 一、item_sku-获取淘宝商品sku详细信息接口接入说明: 点击注册获取key和secret测试账号 接口说明:可以先注册测试账号,然后通过商品详情ID,和sku_ID,以及是否需要获取优惠价is_promotion=0 二、建议使用场景 1、...
  • SKU商品规格选择

    2021-04-29 01:22:13
    SKU商品规格选择在线demo地址Github代码地址可选属性实现思路当用户点击某选项(如"黑色")时,拿到此时所有规格的选项( { color: "黑色" })根据商品数据、用户当前选择规格,查找匹配商品。如果找到了,就返回商品...
  • 比如我们买衣服、鞋子这类物件,一般都需要我们选择合适的颜色、尺码等属性color_size先了解一下 SKU 的学术概念吧最小库存管理单元(Stock Keeping Unit, SKU)是一个会计学名词,定义为库存管理中的最小可用单元,...
  • spu和sku 搜索服务 文章目录spu和sku 搜索服务流程分析表结构分析查询所有SKU后端实现:JavaBean后端实现:搭建环境后端实现:查询 流程分析 搜索服务:从elasticsearch中搜索数据,默认情况下es中没有数据 sku查询...
  • 第一章SKU核心思路

    2021-04-25 09:19:30
    SKU购物车控件系列文章目录第一章SKU核心思路 提示:这系列文章使用js实现,其他语言思路类似只是代码语法不同也能参考 文章目录 系列文章目录 前言 一、业务模型分析 二、数据模型分析 1.引入库 2.读入...
  • 商品sku处理

    2021-02-04 20:32:28
    一般情况来说,一个商品 goods_id 对应多个 sku_id ;设计表时我们会采取这样的方式:产品表 --- 产品sku表 --- 产品基本属性表(产品属性名称表---产品属性值表)就目前我所接触到得项目而言:一个商品id既是一个...
  • 在连锁零售门店中有时称单品为一个SKU(中文译为最小存货单位,英文全称为Stock Keeping Unit,简称SKU,定义为保存库存控制的最小可用单位,例如纺织品中一个SKU通常表示规格、颜色、款式)。 通俗一点就是如下图所...
  • 先来一张图快速了解下组件形态快速上手因为 vue-sku-form 基于 ElementUI 开发,安装使用前确保项目中已正确安装 ElementUI# 推荐使用 yarn 进行安装yarn add vue-sku-form# 或者使用 npm 也无伤大雅npm install vue...
  • vue的vant组件中sku使用

    2021-11-05 20:15:03
    <template> <div class="info"> <van-button type="primary" @click="show=true">显示商品</van-button>...van-button type="warning" @click="callBackMainPage...van-sku v-model="show" :s
  • Spu和Sku表结构设计

    2021-09-04 16:17:53
    Spu和Sku表结构设计spusku商品类目表品牌表分类品牌关系表分类属性表属性表 spu CREATE TABLE `spu` ( `id` varchar(60) NOT NULL COMMENT '主键', `name` varchar(100) DEFAULT NULL COMMENT 'SPU名', `intro` ...
  • 如何设计SKU表结构

    2020-12-31 12:39:56
    spu和sku这里拿iphone6s举例,它身上有很多的属性和值, 比如毛重: 420.00 g产地: 中国大陆容量: 16G, 64G, 128G颜色: 银, 白, 玫瑰金spu 指的是商品(iphone6s),spu属性就是不会影响到库存和价格的属性, 又叫关键...
  • 订单sku规格选择 参考 https://juejin.cn/post/6914163217124032525 /* 重新实现笛卡尔积 传入的数组 '为空', '长度为1', '长度大于1' 三种情况 分别处理 入参数组格式: [ { attr:"颜色", valueList:["黑","白"] }...
  • 以前刚开始接触到这种 sku 选择器,还以为只是简简单单的几个 tab 组合后传给后端,但是等到实际开发才知道这种 sku 选择器,是后端告诉你有什么规格,比如颜色有几种,码数有几种,然后再告诉你有几种组.

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 38,848
精华内容 15,539
关键字:

sku