# 一、商品sku选购信息表 goods_sku

具体查看,4. 商品表goods的选购sku表goods_sku[商品选购sku表]字段设计

# 二、创建商品sku选购信息

# 1. 路由

app/router/admin/shop.js

    ...
    //商品购物车选购goods_sku
    router.get('/shop/admin/goods-/:goods_id/createGoodsSku', controller.admin.goods.createGoodsSku);
    router.post('/shop/admin/goods-/:goods_id/saveGoodsSku', controller.admin.goods.saveGoodsSku);
    router.get('/shop/admin/goods-/:goods_id/indexGoodsSku', controller.admin.goods.indexGoodsSku);
    router.get('/shop/admin/goods-/:id/deleteGoodsSku', controller.admin.goods.deleteGoodsSku);
    //商品参数goods_param处理
    //选择一个商品分类下的skus添加到商品参数中
    ...

# 2. 新建模型

app/model/goods_sku.js

'use strict';


module.exports = app => {
    const { INTEGER, STRING, DATE, ENUM, TEXT, BIGINT,DECIMAL } = app.Sequelize;

    const GoodsSku = app.model.define('goods_sku', {
        id: {
            type: INTEGER(20).UNSIGNED,
            primaryKey: true,
            autoIncrement: true,
            comment: '主键id'
        },
        goods_id: {
            type: INTEGER(20).UNSIGNED,
            allowNull: false,
            // defaultValue:0,
            comment: '商品id',
            references: { //关联关系
                model: 'goods', //关联的表
                key: 'id' //关联表的主键
            },
            onDelete: 'cascade', //删除时操作
            onUpdate: 'restrict', // 更新时操作
        },
        name: {
            type: STRING(255),
            allowNull: false,
            defaultValue: '',
            comment: '选购组合名称'
        },
        namestr: { 
            type: STRING(255), 
            allowNull: false, 
            defaultValue: '', 
            comment: '选购组合名称纯文字'
        },
        cover: {
            type: STRING(2000),
            allowNull: true,
            defaultValue: '',
            comment: '选购组合封面图'
        },
        stock: {
            type: INTEGER,//不限定长度.默认int(11)
            allowNull: true,
            defaultValue: 0,
            comment: '商品选购组合库存数量'
        },
        price: {
            type: DECIMAL(10, 2),
            allowNull: true,
            comment: '商品选购组合价格'
        },
        special_price: {
            type: DECIMAL(10, 2),
            allowNull: true,
            comment: '商品选购组合优惠价格'
        },
        order: {
            type: INTEGER,//不限定长度.默认int(11)
            allowNull: true,
            defaultValue: 50,
            comment: '排序,默认50'
        },
        status: {
            type: INTEGER(1),
            allowNull: false,
            defaultValue: 1,
            comment: '状态:1:启用,0:禁用'
        },
        // sex: { type: ENUM, values: ['男','女','保密'], allowNull: true, defaultValue: '保密', comment: '留言用户性别'},
        create_time: {
            type: DATE,
            allowNull: false,
            defaultValue: app.Sequelize.fn('NOW'),
            get() {
                return app.formatTime(this.getDataValue('create_time'));
            }
        },
        update_time: { type: DATE, allowNull: false, defaultValue: app.Sequelize.fn('NOW') }
    });


    // 模型关联关系
    GoodsSku.associate = function (models) {
        // 关联商品 反向一对多
        GoodsSku.belongsTo(app.model.Goods, {
            foreignKey: 'goods_id', // 关联外键
        });
    }

    return GoodsSku;
}

# 3. 模版

# 1. _form.html模版代码

app/view/admin/layout/_form.html

{# 定义一个可递归调用的子模板 #}
{% macro renderMenu(items,keyname,type) %}
{% for item in items %}
<span class="dropdown-item" style="margin-left: {{item.level * 20}}px;
cursor: pointer;background:none;"
    onmouseover="this.style.color = '#000000';this.style.fontWeight = 800;"
    onmouseout="this.style.color = '#002222';this.style.fontWeight = 'normal';"
    @click="dropdownItemClick('{{keyname}}','{{item.name}}','{{item.id}}')">
    {% if type == 'treeDataSelect' %}
    <input type="checkbox" name="{{keyname}}" style="margin-right: 10px;"
        data="{{item.id}}" class="checkbox-{{keyname}}"
        @click.stop="treeDataSelectClick('{{keyname}}','{{item.name}}','{{item.id}}','checkbox-{{keyname}}',$event)">
    {% endif %}
    {{item.name}}
</span>
{# 检查是否存在子菜单项 #}
{% if item.children and item.children.length %}
<div class="dropdown-submenu">
    {% call renderMenu(item.children,keyname,type) %}
    {# 这里通过调用macro自身实现了递归 #}
    {% endcall %}
</div>
{% endif %}
{% endfor %}
{% endmacro %}

<div class="card">
    <div class="card-body">
        <div class="mb-4">
            <div class="bg-light">
                <button class="btn btn-default btn-sm" onclick="history.back()"
                title="点击返回上一页"><span class="fa fa-arrow-left"></span> 返回上一页</button>
            </div>
        </div>
        {% if form %}
        <form action="{{form.action}}" method="post">
            {% for item in form.fields %}
            <div class="form-group row">
                <label class="col-form-label col-md-2" 
                style="display: {% if item.hidekeyData %}none{% else %}block{% endif %};">{{item.label}}</label>
                <div class="col-md-10">
                    {# 如果是文件类型 #}
                    {% if item.type == 'file' %}
                    <input class="form-control" type="file" name="{{item.name}}"
                        @change="uploadFile($event,'{{item.name}}')">
                     <img :src="form.{{item.name}}" v-if="form.{{item.name}}"
                        class="mt-2 p-1 rounded border avatar-lg"> 

                    {# 获取字段值 #}
                    {# 可打印 {{form | dump}} #}
                    {% set fieldValue = form.data[item.name] | safe %}
                    {# 当字段值存在时显示图片 #}
                    {# {% if fieldValue and fieldValue.length > 0 %}
                        {# 通过 split 切割判断文件后缀 #}
                        {# {% set fileExtension = fieldValue.split('.').pop() | lower %} #}
                        {# 如果是 excel 文件则使用固定地址 #}
                        {# <img 
                        src="{% if fileExtension == 'xlsx' %}/public/admin/assets/img/excel.png{% else %}{{ fieldValue }}{% endif %}"
                        class="mt-2 p-1 rounded border avatar-lg"> #}
                    {# {% endif %} #} 
                    
                    {# 如果是文件上传到oss云存储里面 #}
                    {% elif item.type == 'fileoss' %}
                    <input class="form-control" type="file" name="{{item.name}}"
                        @change="uploadFileoss($event,'{{item.name}}')">
                     <img :src="form.{{item.name}}" v-if="form.{{item.name}}"
                        class="mt-2 p-1 rounded border avatar-lg"> 

                    {# 如果是下拉框类型 #}
                    {% elif item.type == 'dropdown' %}
                    {# {{ item.ortherdata | safe }} #}
                    <div class="dropdown dropdown-maindiv" ortherdata="{{ item.ortherdata }}">
                        <button type="button"
                            class="btn dropdown-toggle dropdown-{{item.name}}"
                            data-toggle="dropdown" id="dropdownData">
                            {{item.placeholder if item.placeholder else '一级分类'}}
                        </button>
                        <div class="dropdown-menu" style="height: 400px;overflow: auto;">
                            <h5 class="dropdown-header">请选择要放在哪个分类下</h5>
                            <div class="dropdown-divider"></div>
                            {# 使用定义好的子模板递归渲染菜单 #}
                            <!-- 在上面的代码块之前或之外的合适位置,提前解析item.default为JSON对象 -->
                            {% set itemDefaultParsed = item.default | safe |
                            fromJson %}
                            {# {{item.callback | dump}} #}
                            {% call
                            renderMenu(itemDefaultParsed,item.name,item.type) %}
                            {% endcall %}
                        </div>
                    </div>
                    {% elif item.type == 'btncheck' %}
                    {# 如果是按钮组选择类型 #}
                    <div class="btn-group btn-group-{{item.name}}">
                        {% set itemDefaultParsed = item.default | safe |
                        fromJson %}
                        {% for btn in itemDefaultParsed %}
                        <button type="button"
                            class="btn {{'btn-success'  if btn.checked else 'btn-light'}}"
                            @click="changeBtnStatus('{{item.name}}','btn-group-{{item.name}}','{{btn.value}}','{{ loop.index }}')">{{btn.name}}</button>
                        {% endfor %}

                    </div>
                    {# 如果是文本域类型 #}
                    {% elif item.type == 'textarea' %}
                    <textarea class="form-control" rows="5" name="{{item.name}}"
                        placeholder="{{item.placeholder}}..."
                        v-model="form.{{item.name}}"></textarea>
                    {# 如果是自定义类型组件 #}
                    {% elif item.type == 'diy' %}
                    {% if item.render %}
                    {{ item.render(item) | safe }}
                    {% endif %}
                    {# 如果是富文本编辑器 #}
                    {% elif item.type == 'editor' %}
                    {% include "admin/layout/_editor.html" %}
                    {# 如果循环遍历数组数据到表单 #}
                    {% elif item.type == 'for_form' %}
                    {# {{item.default}} #}
                    {% set forformParsed = item.default | safe | fromJson %}
                    {# {{ forformParsed | dump }} #}
                    {% if forformParsed.length == 0 %}
                        {% set ortherdataParsed = item.ortherdata | safe | fromJson %}
                        {# {{ ortherdataParsed | dump }} #}
                        {# 如果有html字段 #}
                        {% if ortherdataParsed.html %}
                            {{ ortherdataParsed.html | safe }}
                        {% endif %}
                    {% else %}
                        {% if item.id %}
                            <div class="param-groups">
                                {% for dataItem in forformParsed %}
                                <div class="param-group" data-id="{{ dataItem.id }}">
                                    {# 参数{{ loop.index }} #}
                                    <h5 class="mb-3">{{ dataItem.title }} 参数设置</h5>
                                    {% set params = dataItem.data %}
                                    <div class="row">
                                        {% for param in params %}
                                        {# param的值{{ param }} #}
                                        <div class="col-lg-3 col-md-4 mb-3">
                                            <div class="form-group">
                                                <label data="{{ param.name | trim }}">{{ param.name | trim }}</label>
                                                <input type="text"  inputId="{{item.id}}"
                                                    class="form-control"
                                                    value="{{ param.value | trim }}"
                                                    placeholder="请输入{{ param.name | trim }}">
                                            </div>
                                        </div>
                                        {% endfor %}
                                    </div>
                                </div>
                                {% if not loop.last %}<hr>{% endif %}
                                {% endfor %}
                            </div>
                        {% else %}
                            <div class="param-groups">
                                {% for dataItem in forformParsed %}
                                <div class="param-group" data-id="{{ dataItem.id }}">
                                    {# 参数{{ loop.index }} #}
                                    <h5 class="mb-3">{{ dataItem.name }} 参数设置</h5>
                                    {% set params = dataItem.default.split(',') %}
                                    <div class="row">
                                        {% for param in params %}
                                        <div class="col-lg-3 col-md-4 mb-3">
                                            <div class="form-group">
                                                {# {{ param | trim }}--{{ dataItem.id }}-{{ loop.index0 }} #}
                                                <label data="{{ param | trim }}">{{ param | trim }}</label>
                                                <input type="text"
                                                    class="form-control"
                                                    placeholder="请输入{{ param | trim }}">
                                            </div>
                                        </div>
                                        {% endfor %}
                                    </div>
                                </div>
                                {% if not loop.last %}<hr>{% endif %}
                                {% endfor %}
                            </div>
                        {% endif %}
                    {% endif %}
                       
                    {# 商品sku动态表单 #}
                    {% elif item.type == 'for_form_goodsSku' %}
                    {% include "admin/layout/_for_form_goodsSku.html" %}

                    {# 如果是树形结构数据选择 #}
                    {% elif item.type == 'treeDataSelect' %}
                        {# 使用定义好的子模板递归渲染菜单 #}
                        <!-- 在上面的代码块之前或之外的合适位置,提前解析item.default为JSON对象 -->
                        {% set itemDefaultParsed = item.default | safe | fromJson %}
                        {% call renderMenu(itemDefaultParsed,item.name,item.type) %}
                        {% endcall %}
                    {% else %}
                        <input 
                        style="display: {% if item.hidekeyData %}none{% else %}block{% endif %};"
                        type="{{item.type}}" class="form-control"
                            name="{{item.name}}"  {% if item.disabled %}disabled="disabled"{% else %}{% endif %}
                            placeholder="{{item.placeholder}}..."
                            v-model="form.{{item.name}}">
                    {% endif %}
                </div>
            </div>
            {% endfor %}

            {% if not form.hideSubmit %}
            <div class="text-right mt-3">
                <button type="submit" class="btn btn-primary"
                    @click.stop.prevent="submit">提 交</button>
            </div>
            {% endif %}
            
        </form>
        {% endif %}
    </div>
</div>

<script>
    Vueapp = new Vue({
        el:'#vueapp',
        data(){
           return {
              form:{
                {% for item in form.fields %}
                   {{item.name}} : `{{ (form.data[item.name]|safe) if form.data[item.name] else  item.default  }}`,
                {% endfor %}
              },
              uploadedFiles: {}, //存储每个item.name对应的已上传文件路径
           }
        },
        mounted(){
            console.log('form',this.form);
            console.log('btn',typeof this.form.status);

            $('.form-group input').on('input',function(){
                // console.log($(this).val());
                let val = $(this).val();
                $(this).attr('datavalue',val);
            });
        },
        methods:{
            getparaminfo(){
                let arr = [];
                $('.param-groups .param-group').each(function(index,element){
                    let arr_eve = {
                        title:  $(element).find('h5').text(), // 获取标题
                        data:[], // 获取参数
                    };
                    let paramGroupId = $(element).attr('data-id');
                    let formGroups = $(element).find('.row').find('.form-group');
                    formGroups.each(function(index1,element1){
                        arr_eve.data.push({
                            name: $(element1).find('label').attr('data'),
                            value: $(element1).find('input').attr('datavalue') || $(element1).find('input').val(),
                        });
                    });
                    arr.push(arr_eve);
                });
                return arr;
            },
            submit(){
                // console.log('提交成功',this.form);
                // console.log(JSON.parse(this.form.pid.replaceAll('&quot;','"')));
                // console.log('下拉框第一项', JSON.parse(this.form.pid.replaceAll('&quot;','"'))[0]);
                // console.log('按钮组第一项', JSON.parse(this.form.status.replaceAll('&quot;','"'))[0]);
                for(const key in this.form){
                   const value = this.form[key];
                   console.log('value:',value);
                    if(typeof value == 'string' && value.length > 0  && value.indexOf('&quot;')>-1){
                      if(key == 'paraminfo'){
                        this.form[key] = JSON.stringify(this.getparaminfo());
                      }else{
                        this.form[key] = JSON.parse(value.replaceAll('&quot;','"'))[0].value;
                      }
                    }else{
                        // goods_param表 paraminfo修改的时候单独处理
                        if(key == 'paraminfo'){
                            // 针对 for-form 类型 写入paraminfo数据
                            console.log('for-form 类型的paraminfo',this.getparaminfo());
                            // 针对dropdown类型,写入paraminfo数据
                            console.log('dropdown 类型的paraminfo',this.form[key]);
                            if(this.getparaminfo().length){
                                this.form[key] = JSON.stringify(this.getparaminfo());
                            }
                        }
                    }
                }
                //提交之前,针对商品多参数的处理,如 paraminfo的处理
                // console.log(this.getparaminfo());;
                // console.log('提交成功1',this.form);
                // return;
                $.ajax({
                    type: 'POST', 
                    url: "{{form.action}}?_csrf={{ctx.csrf|safe}}",
                    // contentType:'application/x-www-form-urlencoded',
                    // data: {
                    //     username:123,
                    //     sex:'女'
                    // },
                    contentType:'application/json;charset=UTF-8;',
                    data:JSON.stringify(this.form),
                    success: function (response, stutas, xhr) {
                        console.log(response)
                        Vueapp.$refs.toast.show({
                            // msg:response.data || "{{'修改'  if id else '新建' }}成功",
                            msg:"{{'修改'  if id else '新建' }}成功",
                            type:'success',
                            delay:1000,
                            success:function(){
                                // 查询当前网址是否有gourl参数
                                let gourl = window.location.search.match(/gourl=([^&]*)/);
                                gourl = decodeURIComponent(gourl).split(',');
                                if(gourl && gourl[1]){
                                    // 跳转到gourl对应的页面
                                    window.location.href = gourl[1];
                                } else{
                                    // 跳转到某个页面
                                    window.location.href = "{{successUrl}}";
                                }
                            }
                        });
                    },
                    error:function(e){
                        console.log(e)
                        Vueapp.$refs.toast.show({
                            msg:e.responseJSON.data,
                            type:'danger',
                            delay:3000
                        });
                    }
                });
            },
            //如果是文件上传到本地服务器
            uploadFile(e,name,callback=null){
            //    console.log('e:', e);
            //    console.log('name:',name);
               let file = e.target.files[0];
            //    console.log(file);
               let formData = new FormData();
               formData.append('file', file);
               $.ajax({
                    type: 'POST', 
                    url: "/uploadStreamSingleToServerDiy/adminImg?_csrf={{ctx.csrf|safe}}",
                    processData: false,  // 告诉jQuery不要去处理发送的数据
                    data: formData,
                    contentType: false,   // 告诉jQuery不要去设置Content-Type请求头
                    success:  (response, stutas, xhr)=> {
                        console.log(response)
                        this.form[name] = response.data.url;
                        Vueapp.$refs.toast.show({
                            msg:"上传图片成功",
                            type:'success',
                            delay:1000,
                            success:function(){}
                        });

                        // 使用$set将上传成功的文件URL添加到uploadedFiles对象
                        this.$set(this.uploadedFiles,name,response.data.url);
                        if(callback && typeof callback === 'function'){
                            callback({
                               event: e,
                               name:name,
                               data:response.data
                            });
                        }

                    },
                    error:function(e){
                        console.log(e)
                        Vueapp.$refs.toast.show({
                            msg:e.responseJSON.data,
                            type:'danger',
                            delay:3000
                        });
                    }
                });
            },
            //如果是文件上传到oss云存储里面
            uploadFileoss(e,name,callback=null){
            //    console.log('e:', e);
            //    console.log('name:',name);
               let file = e.target.files[0];
            //    console.log(file);
               let formData = new FormData();
               formData.append('img', file);
               $.ajax({
                    type: 'POST', 
                    url: "/shop/admin/image/uploadAliyun?_csrf={{ctx.csrf|safe}}",
                    processData: false,  // 告诉jQuery不要去处理发送的数据
                    data: formData,
                    contentType: false,   // 告诉jQuery不要去设置Content-Type请求头
                    success:  (response, stutas, xhr)=> {
                        console.log(response)
                        this.form[name] = response.data[0].url;
                        Vueapp.$refs.toast.show({
                            msg:"上传图片成功",
                            type:'success',
                            delay:1000,
                            success:function(){}
                        });

                        // 使用$set将上传成功的文件URL添加到uploadedFiles对象
                        this.$set(this.uploadedFiles,name,response.data.url);
                        if(callback && typeof callback === 'function'){
                            callback({
                               event: e,
                               name:name,
                               data:response.data
                            });
                        }

                    },
                    error:function(e){
                        console.log(e)
                        Vueapp.$refs.toast.show({
                            msg:e.responseJSON.data,
                            type:'danger',
                            delay:3000
                        });
                    }
                });
            },
            //下拉菜单选中项
            dropdownItemClick(keyname,name,id){
                console.log('keyname', keyname);
                console.log('name', name);
                console.log('id', id);
                // $('#dropdownData').text(name);
                $('.dropdown-' + keyname).text(name);
                this.form[keyname] = id;
                let _vue_this = this;
                
                // 如果下拉框有额外值的情况,比如:app/controller/admin/goods.js中的selectGoodsParamBytree()方法
                if($('.dropdown-maindiv').attr('ortherdata')){
                    $('.dropdown-maindiv').parent().find('.ortherdata').remove();
                    //如果有额外值,根据情况读取这些额外值
                    let ortherdata = JSON.parse($('.dropdown-maindiv').attr('ortherdata'));
                    console.log('额外值',ortherdata);
                    let skus = ortherdata.find(v=> v.id == id);
                    skus = skus.skus;
                    console.log('skus',skus);
                    let str = ``;
                    for(let i=0;i<skus.length;i++){
                       // let evedata = [];
                       // evedata.push(encodeURIComponent(JSON.stringify([{
                       //    title: skus[i].name,
                       //    data: skus[i].default,
                       // }])));
                       let evedata = encodeURIComponent(JSON.stringify([{
                          title: skus[i].name,
                          data: skus[i].default,
                          id:skus[i].id,
                       }]));
                       let _str = `<div class="evedata" 
                       evedata = "${evedata}"
                       style="margin-top:20px;font-size:12px;display:flex;">`;
                        _str += `
                            <div style="margin-right:10px;">
                              <input type="checkbox" checked class="evedata-checkbox" data-id="${skus[i].id}" />
                            </div>
                            <div>
                               <div style="margin-bottom:10px;"><span>规格名称</span>:<span>${skus[i].name}</span></div>
                               <div><span>规格值</span>:<span>${skus[i].default}</span></div>
                            </div>
                        `;
                       _str += `</div>`;
                       str += _str;
                    }
                    str = `<div class="ortherdata">` + str + `</div>`;
                    $('.dropdown-maindiv').parent().append(str);
                    //重新给表单字段赋值
                    getcheck();
                    $('.evedata-checkbox').on('click',getcheck);
                    function getcheck(){
                        let arr = [];
                        $('.dropdown-maindiv').parent().find('.ortherdata').find('.evedata').each(function(index,ele){
                            let input = $(ele).find('input[type="checkbox"]');
                            //如果input为选中状态
                            // console.log('input的个数'+index+'的选中状态',input.prop('checked'));
                            if(input.prop('checked')){
                                let evedata = $(ele).attr('evedata');
                                evedata = JSON.parse(decodeURIComponent(evedata));
                                // console.log('evedata',evedata[0]);
                                arr.push(evedata[0]);
                                // console.log('写进paraminfo的数据',arr);
                            }else{
                                let removeid = input.attr('data-id');
                                arr.forEach(function(v,i){
                                    if(v.id == removeid){
                                        arr.splice(i,1);
                                    }
                                });
                                // console.log('移除之后写进paraminfo的数据',arr);
                            }
                            //最终写进paraminfo的数据
                            console.log('最终写进paraminfo的数据',arr);
                            // 需要符合paraminfo的数据特性,在改造一下
                            let newarr = [];
                            for(let i=0;i<arr.length;i++){
                                let _string = arr[i].data;
                                // console.log('看一下data',typeof arr[i].data);
                                let _data = _string.split(/[,,]/); 
                                // console.log('看一下_data',_data);
                                let _arr = [];
                                for(let j=0;j<_data.length;j++){
                                    let obj = {
                                       name:_data[j],
                                       value:"",
                                    };
                                    _arr.push(obj);
                                }
                                // console.log('看一下_arr',_arr);
                                // console.log('看一下其中的data',arr[i]['data']);
                                // arr[i]['data'] = _arr;
                                let _obj = {
                                    title:arr[i].title,
                                    data:_arr,
                                    id:arr[i].id,
                                };
                                newarr.push(_obj);
                            }
                            console.log('看一下写入值',newarr);
                            _vue_this.form[keyname] = JSON.stringify(newarr);
                        });
                    }
                    
                    
                }
            },
            //按钮组选中项
            changeBtnStatus(keyname,classname,btnValue,index){
                console.log(keyname,classname,btnValue,index);
                $('.' + classname).find('button').eq(index-1).addClass('btn-success')
                .siblings().removeClass('btn-success').addClass('btn-light');
                this.form[keyname] = btnValue;
            },
            //自定义的按钮组点击效果
            diyBtnGroup(clickbtn_parentClassname,classname,index){
               $('.' + classname).children().eq(index).show().siblings().hide();
               $('.' + clickbtn_parentClassname).children().eq(index)
               .addClass('btn-success').siblings().removeClass('btn-success').addClass('btn-light');
            },
            treeDataSelectClick(keyname,name,id,classname,e){
                // console.log('keyname', keyname);
                // console.log('name', name);
                // console.log('id', id);
                // console.log('classname', classname);
                // console.log('事件对象', e);

                // 选中上一级下一级所有都选中
                // console.log('父级class', $(e.target).parent().parent().attr('class'));
                if($(e.target).parent().parent().attr('class') != 'dropdown-submenu'){
                   let checkbox = $(e.target).parent().next().find('input[type="checkbox"]'); 
                   if($(e.target).is(':checked')){
                       checkbox.prop('checked',true).attr('select',1);
                   }else{
                       checkbox.prop('checked',false).attr('select',0);
                   }
                   //设置自己
                   if($(e.target).is(':checked')){
                     $(e.target).attr('select',1);
                   }else{
                    $(e.target).attr('select',0);
                   }
                }else{
                    //每个级别下的所有多选框数量
                    let all = $(e.target).parent().parent().find('input[type="checkbox"]').length;
                    //当前这个级别下面选了多少个框了
                    let len = $(e.target).parent().parent().find('input[type="checkbox"]:checked').length;
                    // console.log('当前级别总共多少框',all);
                    // console.log('当前级别选了多少框',len);
                    if(all == len){
                        $(e.target).parent().parent().prev().find('input[type="checkbox"]')
                        .prop('checked',true).attr('select',1);
                    }else{
                        $(e.target).parent().parent().prev().find('input[type="checkbox"]')
                        .prop('checked',false).attr('select',1);
                    }
                    //设置自己
                    if($(e.target).is(':checked')){
                        $(e.target).attr('select',1);
                    }else{
                        $(e.target).attr('select',0);
                    }
                    //一个都没有选
                    if(len == 0){
                        $(e.target).parent().parent().prev().find('input[type="checkbox"]')
                        .prop('checked',false).attr('select',0);
                    }
                }

                //选取选中的值
                // console.log('选取框的长度', $(`input[name=${keyname}]:checked`).length);
                let arr = [];
                // $(`input[name=${keyname}]:checked`).each(function(index,element){
                //      console.log($(element).attr('data'));
                //      arr.push(parseInt($(element).attr('data')));
                // });
                $(`input[name=${keyname}]`).each(function(index,element){
                     if($(element).attr('select') == 1){
                        arr.push(parseInt($(element).attr('data')));
                     }
                });

                // console.log('获取权限的id值集合数据',arr);
                console.log('写进数据库数据',arr.join(','));
                this.form[keyname] = arr.join(',');

            }
        }
    });
</script>


# 2. 新建 _for_form_goodsSku.html 模版

app/view/admin/layout/_for_form_goodsSku.html

<div id="goodsSkudata">
    {# 添加sku区 #}
    <div class="sku-form">
        <span class="btn btn-primary mb-2" style="width: 200px;cursor: pointer;" id="addGoodsSkuInput">再加一项</span>
        <div style="display: flex;flex-wrap: nowrap;" class="sku-item mt-2">
            <input type="text" 
            placeholder="选购项名称如:手机颜色" 
            class="form-control sku-name" style="width: 200px;" />
            <input type="text"  
            placeholder="选购项值如:白色,黑色(用逗号隔开)" 
            class="form-control ml-2 sku-values" />
            <span class="btn btn-danger ml-2 delete-btn" style="width: 100px;cursor: pointer;">删除</span>
        </div>
    </div>
    <div>
        <span class="btn btn-primary my-2" style="width: 200px;cursor: pointer;" id="addGoodsSku">生成选购组合</span>
    </div>
    {# sku动态生成组合区 #}
    <div class="sku-table" style="margin-top: 20px;">
        <!-- 动态表格将在这里生成 -->
    </div>

    {# 确认添加信息 #}
    <span class="btn btn-success mt-5" style="width: 200px;cursor: pointer;" 
    id="addGoodsSkuSubmit">提交选购信息</span>
</div>

<script>
    $(function(){
        // 支持中英文逗号分隔
        function splitValues(str) {
            return str.trim().split(/[,,]\s*/);
        }

        // 生成笛卡尔积函数
        function cartesianProduct(arr) {
            return arr.reduce((a, b) => 
                a.flatMap(x => b.map(y => [...x, y])), 
                [[]]
            );
        }

        // 生成SKU表格
        function generateSkuTable() {
            const skuItems = [];
            $('.sku-item').each(function() {
                const name = $(this).find('.sku-name').val().trim();
                const values = splitValues($(this).find('.sku-values').val()); // 使用新分割方法
                if (name && values.length > 0) {
                    skuItems.push({ name, values });
                }
            });

            if (skuItems.length === 0) {
                $('.sku-table').html('<div class="alert alert-info">请先添加选购项</div>');
                return;
            }

            // 生成表头
            const headers = [
                ...skuItems.map(item => item.name),
                '图片', '库存', '价格', '优惠价', '操作'
            ];

            // 生成表体
            const combinations = cartesianProduct(skuItems.map(item => item.values));
            const $table = $('<table class="table table-bordered"></table>');
            const $thead = $('<thead></thead>');
            const $tbody = $('<tbody></tbody>');

            // 构建表头
            $thead.append($('<tr></tr>').append(
                headers.map(header => $('<th></th>').text(header))
            ));

            // 构建表体
            combinations.forEach(comb => {
                const $tr = $('<tr></tr>');
                
                // 属性列
                comb.forEach(value => {
                    $tr.append($('<td></td>').text(value));
                });

                // 图片上传列
                const $uploadTd = $('<td></td>');
                const $uploadBtn = $('<span class="btn btn-sm btn-secondary">上传</span>');
                const $imgContainer = $('<div class="mt-2"></div>');
                
                $uploadBtn.on('click', function() {
                    const $input = $('<input type="file" hidden/>');
                    $input.on('change', function(e) {
                        const file = e.target.files[0];
                        if (file) {
                            // 这里添加实际的上传逻辑
                            let formData = new FormData();
                            formData.append('file', file);
                            $.ajax({
                                type: 'POST', 
                                url: "/uploadStreamSingleToServerDiy/adminImg?_csrf={{ctx.csrf|safe}}",
                                processData: false,  // 告诉jQuery不要去处理发送的数据
                                data: formData,
                                contentType: false,   // 告诉jQuery不要去设置Content-Type请求头
                                success:  (response, stutas, xhr)=> {
                                    // console.log(response.data.url) 
                                    $imgContainer.html(
                                        `<img src="${response.data.url}" 
                                        style="max-width:50px;max-height:50px;"/>`
                                    );  
                                },
                                error:function(e){
                                    console.log(e)
                                }
                            });
                            //读图片
                            /*
                            const reader = new FileReader();
                            reader.onload = function(e) {
                                $imgContainer.html(
                                    `<img src="${e.target.result}" 
                                     style="max-width:50px;max-height:50px;"/>`
                                );
                            };
                            reader.readAsDataURL(file);
                            */
                        }
                    });
                    $input.click();
                });
                
                $uploadTd.append($uploadBtn, $imgContainer);
                $tr.append($uploadTd);

                // 输入列
                $tr.append(
                    $('<td><input type="number" class="form-control stock" value="0" min="0"/></td>'),
                    $('<td><input type="number" class="form-control price" value="0" min="0" step="0.01"/></td>'),
                    $('<td><input type="number" class="form-control special-price" value="0" min="0" step="0.01"/></td>'),
                    $('<td><button class="btn btn-danger btn-sm delete-row">删除</button></td>')
                );

                $tbody.append($tr);
            });

            // 组装表格
            $table.append($thead, $tbody);
            $('.sku-table').html($table);
        }

        // 事件绑定
        $('#addGoodsSku').click(generateSkuTable);

        $('#addGoodsSkuInput').click(function() {
            const $clone = $('.sku-item:first').clone();
            $clone.find('input').val('');
            $('.sku-form').append($clone);
        });

        $('#goodsSkudata').on('click', '.delete-btn', function() {
            $(this).closest('.sku-item').remove();
            generateSkuTable();
        });

        $('.sku-table').on('click', '.delete-row', function() {
            $(this).closest('tr').remove();
        });

        $('#addGoodsSkuSubmit').click(function() {
            //  获取选项及属性值
            let skuItems = [];
            $(this).parent().find('.sku-form .sku-item').each(function() {
                const name = $(this).find('.sku-name').val();
                const values = $(this).find('.sku-values').val().trim().split(/[,,]\s*/);
                skuItems.push({
                    name:name,
                    values:values
                });
            });
            // console.log('选项及属性值:', skuItems);return;
            //  获取SKU数据
            const skuData = [];
            $('.sku-table tbody tr').each(function() {
                const $row = $(this);
                const data = {
                    attributes: {},
                    // 修改点2:字段名替换为数据库字段
                    cover: $row.find('img').attr('src') || '',
                    stock: parseInt($row.find('.stock').val()) || 0,
                    price: parseFloat($row.find('.price').val()) || 0,
                    special_price: parseFloat($row.find('.special-price').val()) || 0
                };

                // 动态属性收集(保持中文键)
                $row.find('td').each(function(index) {
                    const header = $('.sku-table th').eq(index).text();
                    const excludeHeaders = ['图片', '库存', '价格', '优惠价', '操作'];
                    if (!excludeHeaders.includes(header)) {
                        data.attributes[header] = $(this).text();
                    }
                });

                data.name = data.attributes;

                // 输出attributes值的纯文字信息
                let resultValue = '';
                for (const key in data.attributes) {
                    if (data.attributes.hasOwnProperty(key)) {
                        resultValue += data.attributes[key];
                    }
                }
                data.namestr = resultValue;

                skuData.push(data);
            });

            // console.log('SKU数据:', skuData);
            // console.log('选项及属性值:', skuItems);return;
            // alert('数据已输出到控制台,请查看');
            // alert('跳转到哪个页面:' + '{{successUrl|safe}}');
            // alert('提交到哪个页面:' + '{{form.action|safe}}');
            if(skuData.length){
                $.ajax({
                    type: 'POST', 
                    url: "{{form.action}}?_csrf={{ctx.csrf|safe}}",
                    // contentType:'application/x-www-form-urlencoded',
                    // data: {
                    //     username:123,
                    //     sex:'女'
                    // },
                    contentType:'application/json;charset=UTF-8;',
                    data:JSON.stringify({skuData:skuData, skuItems:skuItems}),
                    success: function (response, stutas, xhr) {
                        console.log(response)
                        // 查询当前网址是否有gourl参数
                        let gourl = window.location.search.match(/gourl=([^&]*)/);
                        gourl = decodeURIComponent(gourl).split(',');
                        if(gourl && gourl[1]){
                            // 跳转到gourl对应的页面
                            window.location.href = gourl[1];
                        } else{
                            // 跳转到某个页面
                            window.location.href = "{{successUrl}}";
                        }
                    },
                    error:function(e){
                        console.log(e)
                    }
                });
            }else{
                $(this).removeClass('btn-success').addClass('btn-danger').text('请先: 生成选购组合');
                setTimeout(() => {
                    $(this).removeClass('btn-danger').addClass('btn-success').text('提交选购信息');
                }, 1500);
            }
        });
    });
</script>

# 4. 控制器

app/controller/admin/goods.js

    // 动态生成商品sku列表
    async createGoodsSku(){
        const { ctx, app } = this;
        //1.参数验证
        this.ctx.validate({
            goods_id: {
                type: 'int',
                required: true,
                desc: '商品id',
                // defValue: 0,
                range:{
                    min:1,
                }
            },
        });
        // 参数
        const goods_id = ctx.params.goods_id;
        let data = await app.model.Goods.findOne({ where: { id:goods_id } });
        if (!data) {
            return ctx.apiFail('该商品不存在');
        }
        await ctx.renderTemplate({
            title: '创建商品Sku选购信息',//现在网页title,面包屑导航title,页面标题
            tempType: 'form', //模板类型:table表格模板 ,form表单模板,for_form循环模版
            form: {
                //提交地址
                action: "/shop/admin/goods-/"+goods_id+"/saveGoodsSku",
                //  字段
                fields: [
                    {
                        label: '商品选购信息',
                        type: 'for_form_goodsSku', //商品sku动态表单
                    },
                    
                ],
                // 隐藏提交按钮,在模版自行完成提交
                hideSubmit:true, 
            },
            //新增成功之后跳转到哪个页面
            successUrl: '/shop/admin/goods-/'+goods_id+'/indexGoodsSku',
        });
    }
    // 提交商品sku列表
    async saveGoodsSku(){
        const { ctx, app } = this;
        //1.参数验证
        this.ctx.validate({
            goods_id: {
                type: 'int',
                required: true,
                desc: '商品id',
                // defValue: 0,
                range:{
                    min:1,
                }
            },
            skuData: {
                type: 'string',
                required: true,
                desc: '商品sku信息',
                // defValue: 0,
            },
        });
        // 参数
        const goods_id = ctx.params.goods_id;
        let data = await app.model.Goods.findOne({ where: { id:goods_id } });
        if (!data) {
            return ctx.apiFail('该商品不存在');
        }
        let {skuData} = ctx.request.body;
        skuData = JSON.parse(skuData);
        // 给skuData的每个元素加一个goods_id
        skuData.forEach(element => {
            element.goods_id = goods_id;
            element.name = JSON.stringify(element.name);
        });
        // ctx.body = skuData;return;
        //批量写入
        await app.model.GoodsSku.bulkCreate(skuData);
        ctx.apiSuccess('创建成功');
    }
    // 商品sku列表
    async indexGoodsSku(){

    }

# 三、 商品sku列表和删除

# 1. 控制器

app/controller/admin/goods.js

    // 商品sku列表
    async indexGoodsSku(){
        const { ctx, app } = this;
        //1.参数验证
        this.ctx.validate({
            goods_id: {
                type: 'int',
                required: true,
                desc: '商品id',
                // defValue: 0,
                range:{
                    min:1,
                }
            },
            page: {
                type: 'int',
                required: false,
                desc: '页码',
                defValue: 1,
            },
            limit: {
                type: 'int',
                required: false,
                desc: '每页条数',
                defValue: 10,
            },
        });
        // 参数
        const goods_id = ctx.params.goods_id;
        let Goodsdata = await app.model.Goods.findOne({ where: { id:goods_id } });
        if (!Goodsdata) {
            return ctx.apiFail('该商品不存在');
        }
        // let GoodsClassdata = await app.model.GoodsClass.findOne({
        //     where:{
        //         id:Goodsdata.goods_class_id
        //     },
        //     include: [
        //         { 
        //             model: app.model.Skus, 
        //             // as: 'children' 
        //         },
        //     ],
        // });
        // ctx.body = GoodsClassdata; return;
        //分页:可以提炼成一个公共方法page(模型名称,where条件,其他参数options)
        let data = await ctx.page('GoodsSku',{
            goods_id:goods_id
        },{
            order:[
                ['order','asc'],
                ['id','asc']
            ],
        });
        // let data = await ctx.service.goodsClass.datalist({ limit: 10000 });
        // console.log('分类数据', data);
        // ctx.body = data;
        // return;
        // data = data.rules;
        //渲染公共模版
        await ctx.renderTemplate({
            title: '商品:' + Goodsdata.name + ' 选购sku列表',//现在网页title,面包屑导航title,页面标题
            data,
            tempType: 'table', //模板类型:table表格模板 ,form表单模板
            table: {
                //表格上方按钮,没有不要填buttons
                buttons: [
                    {
                        url: '/shop/admin/goods-/'+goods_id+'/createGoodsSku',//新增路径
                        desc: '创建商品选购sku',//新增 //按钮名称
                        // icon: 'fa fa-plus fa-lg',//按钮图标
                    },
                    // {
                    //     url: '/shop/admin/goods/create',//新增路径
                    //     desc: '上传商品',//新增 //按钮名称
                    //     // icon: 'fa fa-plus fa-lg',//按钮图标
                    // }
                ],
                //表头
                columns: [
                    {
                        title: '选购组合名称',
                        // key: 'name',
                        class: 'text-left',//可选
                        render(item) { //树形数据
                            let name = JSON.parse(item.name);
                            // 遍历对象属性值
                            let result = '';
                            for (let key in name) {
                                if (name.hasOwnProperty(key)) {
                                    result += `<span style="background-color: #f0f0f0; 
                                    margin: 5px; padding: 2px 5px;font-size: 14px;">${name[key]}</span>`;
                                }
                            }
                            return result;
                        }
                    },
                    {
                        title: '选购图片',
                        // key: 'cover',
                        class: 'text-left',//可选
                        render(item) { //树形数据
                            let str = '无';
                            if(item.cover){
                                str = `<img src="${item.cover}" style="width:100px;height:100px;" />`;
                            }
                            return `${str}`;
                        }
                    },
                    // {
                    //     title: '是否是导航栏栏目',
                    //     key: 'isnav',
                    //     width: 200,//可选
                    //     class: 'text-center',//可选
                    //     hidekeyData: true,//是否隐藏key对应的数据
                    //     render(item) {
                    //         console.log('可用状态里面每个item', item);
                    //         let arr = [
                    //             { value: 1, name: '是' },
                    //             { value: 0, name: '否' },
                    //         ];
                    //         let str = `<div class="btn-group btn-group-${item.id}">`;
                    //         for (let i = 0; i < arr.length; i++) {
                    //             str += `<button type="button" class="btn btn-light" data="${item.isnav}"
                    //             value="${arr[i].value}"
                    //             @click="changeBtnStatus('isnav','btn-group-${item.id}',${arr[i].value},${i},${item.id},'category','Category')">${arr[i].name}</button>`;
                    //         }
                    //         str += `</div>`;
                    //         return str;
                    //     }
                    // },
                    {
                        title: '库存',
                        key: 'stock',
                        class: 'text-center',//可选
                    },
                    {
                        title: '价格',
                        key: 'price',
                        class: 'text-center',//可选
                    },
                    {
                        title: '优惠价',
                        key: 'special_price',
                        class: 'text-center',//可选
                    },
                    {
                        title: '排序',
                        key: 'order',
                        class: 'text-center',//可选
                    },
                    {
                        title: '可用状态',
                        key: 'status',
                        width: 200,//可选
                        class: 'text-center',//可选
                        hidekeyData: true,//是否隐藏key对应的数据
                        render(item) {
                            // console.log('可用状态里面每个item', item);
                            let arr = [
                                { value: 1, name: '可用' },
                                { value: 0, name: '不可用' },
                            ];
                            let str = `<div class="btn-group btn-group-${item.id}">`;
                            for (let i = 0; i < arr.length; i++) {
                                str += `<button type="button" class="btn btn-light" data="${item.status}"
                                value="${arr[i].value}"
                                @click="changeBtnStatus('status','btn-group-${item.id}',${arr[i].value},${i},${item.id},'/shop/admin/goods-/${goods_id}/indexGoodsSku','GoodsSku')">${arr[i].name}</button>`;
                            }
                            str += `</div>`;
                            return str;
                        }
                    },
                    {
                        title: '操作',
                        class: 'text-right',//可选
                        action: {
                            //修改
                            // edit: function (id) {
                            //     return `/shop/admin/goods-/${id}/editGoodsSku`;
                            // },
                            //删除
                            delete: function (id) {
                                return `/shop/admin/goods-/${id}/deleteGoodsSku`;
                            }
                        }
                    },
                ],
            },
        });
    }

    // 删除sku
    async deleteGoodsSku(){
        const { ctx, app } = this;
        const id = ctx.params.id;

        let data = await app.model.GoodsSku.findOne({ where: { id } });
        if (!data) {
            return ctx.apiFail('该商品sku不存在');
        }
        await app.model.GoodsSku.destroy({
            where: {
                id
            }
        });
        //提示
        ctx.toast('商品sku删除成功', 'success');
        //跳转
        ctx.redirect('/shop/admin/goods-/'+data.goods_id+'/indexGoodsSku');
    }

# 四、最后补充商品sku选购项展示

在控制器 app/controller/admin/goods.js

    // 动态生成商品sku选购信息提交数据
    async saveGoodsSku(){
        const { ctx, app } = this;
        //1.参数验证
        this.ctx.validate({
            goods_id: {
                type: 'int',
                required: true,
                desc: '商品id',
                // defValue: 0,
                range:{
                    min:1,
                }
            },
            skuData: {
                type: 'string',
                required: true,
                desc: '商品sku信息',
                // defValue: 0,
                range:{
                    // min:1,
                }
            },
            skuItems: {
                type: 'string',
                required: true,
                desc: '商品sku项目',
                // defValue: 0,
                range:{
                    // min:1,
                }
            },
        });
        // 参数
        const goods_id = ctx.params.goods_id;
        let data = await app.model.Goods.findOne({ where: { id:goods_id } });
        if (!data) {
            return ctx.apiFail('该商品不存在');
        }
        let {skuData,skuItems} = ctx.request.body;
        skuData = JSON.parse(skuData);
        //给skuData添加goods_id
        skuData.forEach(item=>{
            item.goods_id = goods_id;
            item.name = JSON.stringify(item.name);
        })
        //保存数据
        await app.model.GoodsSku.bulkCreate(skuData);
        // 更新skuItems到goods表的sku_value字段
        data.sku_value = skuItems;
        await data.save();
        ctx.apiSuccess('创建成功');
    }

    // 商品列表页面
    async index() {
        const { ctx, app } = this;
        //分页:可以提炼成一个公共方法page(模型名称,where条件,其他参数options)
        let keyword = ctx.query.keyword || '';
        let data = await ctx.page('Goods',{
            name:{
                [this.app.Sequelize.Op.like]: '%' + keyword + '%',
            }
        },{
            include:[{
                model:app.model.GoodsClass,
                attributes:['id','name'],
            }],
        });
        // let data = await ctx.service.goodsClass.datalist({ limit: 10000 });
        // console.log('数据', data);
        // ctx.body = data;
        // return;
        // data = data.rules;
        //渲染公共模版
        await ctx.renderTemplate({
            title: '商品列表',//现在网页title,面包屑导航title,页面标题
            data,
            tempType: 'table', //模板类型:table表格模板 ,form表单模板
            table: {
                //表格上方按钮,没有不要填buttons
                buttons: [
                    {
                        url: '/shop/admin/goods-/create',//新增路径
                        desc: '创建商品',//新增 //按钮名称
                        // icon: 'fa fa-plus fa-lg',//按钮图标
                    },
                    // {
                    //     url: '/shop/admin/goods/create',//新增路径
                    //     desc: '上传商品',//新增 //按钮名称
                    //     // icon: 'fa fa-plus fa-lg',//按钮图标
                    // }
                ],
                //表头
                columns: [
                    {
                        title: '商品分类和商品名称等',
                        // key: 'name',
                        class: 'text-left',//可选
                        width:150,
                        render(item) { 
                            // console.log('每个item',item);
                            return `<div style="font-size:12px;">
                                 <p style="color:#999000;"><span>商品分类:</span><span>${item.goods_class.name}</span></p>
                                 <div style="display:flex;">
                                     <img src="${item.cover}" style="width:50px;height:50px;margin-right:10px;">
                                     <p style="width:90px;display:flex;
                                     flex-wrap:wrap;white-space:wrap;"><span>${item.name}</span></p>
                                 </div>
                                 <p><a href="/shop/admin/goods-/${item.id}/indexGoodsBanner"
                                 style="color:green;">商品图片管理</a></p>
                                 <p><a href="/shop/admin/goods-/${item.id}/indexGoodsParam"
                                 style="color:green;">商品参数管理</a></p>
                                 <p><a href="/shop/admin/goods-/${item.id}/indexGoodsSku"
                                 style="color:green;">商品选购sku管理</a></p>
                            </div>`;
                        }
                    },
                    {
                        title: '商品选购项',
                        // key: 'sku_value',
                        class: 'text-left',//可选
                        width:300,
                        render(item) { 
                            // console.log('每个item',item);
                            let str = ``;
                            if(item.sku_value){
                                let sku_value = JSON.parse(item.sku_value);
                                for(let i=0;i<sku_value.length;i++){
                                    str += `<div style="color:#333333;margin-bottom:15px;">`;
                                    str += `<div style="margin-bottom:10px;font-weight:bold;">${sku_value[i].name}</div>`;
                                    let str_arr = sku_value[i].values;
                                    for(let j=0;j<str_arr.length;j++){
                                        str += `<span style="padding:2px 5px;background-color:#f0f0f0;margin-right:5px;">${str_arr[j]}</span>`;
                                    }
                                    str += `</div>`;
                               }
                            }
                            return `<div style="font-size:12px;">
                                 ${str}
                            </div>`;
                        }
                    },
                    {
                        title: '价格相关',
                        // key: 'name',
                        class: 'text-left',//可选
                        render(item) { 
                            // console.log('每个item',item);
                            return `<div style="font-size:12px;">
                                 <p style="color:#999000;"><span>起售价(最低售价,多少元起):</span>
                                 <span>${item.min_price?item.min_price:'暂无'}</span></p>
                                 <p style="color:#999000;"><span>历史最低价:</span>
                                 <span>${item.history_min_price?item.history_min_price:'暂无'}</span></p>
                                 <p style="color:#999000;"><span>券后价:</span>
                                 <span>${item.coupon_price?item.coupon_price:'暂无'}</span></p>
                                 <p style="color:#999000;"><span>折后价:</span>
                                 <span>${item.discount_price?item.discount_price:'暂无'}</span></p>
                                 <p style="color:#999000;"><span>秒杀价:</span>
                                 <span>${item.spike_price?item.spike_price:'暂无'}</span></p>
                                 <p style="color:#999000;"><span>其它设置价(如:首件价,新客价,30天低价等等):</span>
                                 <span>${item.other_price?item.other_price:'暂无'}</span></p>
                                 <p><a href="/shop/admin/goods-/${item.id}/editPrice"
                                 style="color:green;">修改商品价格相关信息</a></p>
                            </div>`;
                        }
                    },
                    {
                        title: '库存相关',
                        // key: 'name',
                        class: 'text-left',//可选
                        render(item) { 
                            // console.log('每个item',item);
                            return `<div style="font-size:12px;">
                                 <p style="color:#999000;"><span>单位(默认:件):</span>
                                 <span>${item.unit?item.unit:'件'}</span></p>
                                 <p style="color:#999000;"><span>库存:</span>
                                 <span>${item.stock?item.stock:'暂无'}</span></p>
                                 <p style="color:#999000;"><span>库存预警:</span>
                                 <span>${item.min_stock?item.min_stock:'暂无'}</span></p>
                                 <p><a href="/shop/admin/goods-/${item.id}/editStock"
                                 style="color:green;">修改商品库存相关信息</a></p>
                            </div>`;
                        }
                    },
                    {
                        title: '库存是否显示',
                        key: 'stock_display',
                        // width: 200,//可选
                        class: 'text-left',//可选
                        hidekeyData: true,//是否隐藏key对应的数据
                        render(item) {
                            // console.log('可用状态里面每个item', item);
                            let arr = [
                                { value: 1, name: '显示' },
                                { value: 0, name: '隐藏' },
                            ];
                            let str = `<div class="btn-group btn-group-${item.id}">`;
                            for (let i = 0; i < arr.length; i++) {
                                str += `<button type="button" class="btn btn-light" data="${item.stock_display}"
                                value="${arr[i].value}"
                                @click="changeBtnStatus('stock_display','btn-group-${item.id}',${arr[i].value},${i},${item.id},'/shop/admin/goods-','Goods')">${arr[i].name}</button>`;
                            }
                            str += `</div>`;
                            return str;
                        }
                    },
                    {
                        title: '统计相关',
                        // key: 'name',
                        class: 'text-left',//可选
                        render(item) { 
                            // console.log('每个item',item);
                            return `<div style="font-size:12px;">
                                 <p style="color:#999000;"><span>商品评分:</span>
                                 <span>${item.rating?item.rating:5.0}</span></p>
                                 <p style="color:#999000;"><span>总销量:</span>
                                 <span>${item.sale_count?item.sale_count:'暂无'}</span></p>
                                 <p style="color:#999000;"><span>商品评论数量:</span>
                                 <span>${item.review_count?item.review_count:'暂无'}</span></p>
                                 <p style="color:#999000;"><span>商品收藏量:</span>
                                 <span>${item.love_count?item.love_count:'暂无'}</span></p>
                                 <p style="color:#999000;"><span>商品推荐量:</span>
                                 <span>${item.recommend_count?item.recommend_count:'暂无'}</span></p>
                                 <p><a href="/shop/admin/goods-/${item.id}/editTotalinfo"
                                 style="color:green;">修改商品统计相关信息</a></p>
                            </div>`;
                        }
                    },
                    {
                        title: '商品标签',
                        // key: 'goods_tags',
                        class: 'text-left',//可选
                        render(item) { 
                            // console.log('每个item',item);
                            let str = ``;
                            if(item.goods_tags){
                                let arr = item.goods_tags.split(/[,,]/);
                                for(let i=0;i<arr.length;i++){
                                    str += `<span style="border:1px solid #909090;padding:2px;margin:2px;">${arr[i]}</span>`;
                                }
                            }
                            return `<div style="font-size:12px;width:100px;display:flex;flex-wrap:wrap;">
                                 ${str}
                                 <p style="margin-top:10px;"><a href="/shop/admin/goods-/${item.id}/editTags"
                                 style="color:green;">修改商品标签信息</a></p>
                            </div>`;
                        }
                    },
                    {
                        title: '审核状态',
                        key: 'ischeck',
                        // width: 200,//可选
                        class: 'text-left',//可选
                        hidekeyData: true,//是否隐藏key对应的数据
                        render(item) {
                            // console.log('可用状态里面每个item', item);
                            let arr = [
                                { value: 1, name: '通过' },
                                { value: 0, name: '审核中' },
                                { value: 2, name: '拒绝' },
                            ];
                            let str = `<div class="btn-group btn-group-${item.id}">`;
                            for (let i = 0; i < arr.length; i++) {
                                str += `<button type="button" class="btn btn-light" data="${item.ischeck}"
                                value="${arr[i].value}"
                                @click="changeBtnStatus('ischeck','btn-group-${item.id}',${arr[i].value},${i},${item.id},'/shop/admin/goods-','Goods')">${arr[i].name}</button>`;
                            }
                            str += `</div>`;
                            return str;
                        }
                    },
                    {
                        title: '商品状态',
                        key: 'goods_status',
                        // width: 200,//可选
                        class: 'text-left',//可选
                        hidekeyData: true,//是否隐藏key对应的数据
                        render(item) {
                            // console.log('可用状态里面每个item', item);
                            let arr = [
                                { value: 1, name: '上架' },
                                { value: 0, name: '仓库' },
                                { value: 2, name: '下架' },
                                { value: 3, name: '违规下架' },
                                { value: 4, name: '回收站' },
                            ];
                            let str = `<div class="btn-group btn-group-${item.id}">`;
                            for (let i = 0; i < arr.length; i++) {
                                str += `<button type="button" class="btn btn-light" data="${item.goods_status}"
                                value="${arr[i].value}"
                                @click="changeBtnStatus('goods_status','btn-group-${item.id}',${arr[i].value},${i},${item.id},'/shop/admin/goods-','Goods')">${arr[i].name}</button>`;
                            }
                            str += `</div>`;
                            return str;
                        }
                    },
                    // {
                    //     title: '分类下的商品',
                    //     // key: 'name',
                    //     class: 'text-left',//可选
                    //     render(item) { //树形数据
                    //         // console.log('每个item',item);
                    //         // if (item.level) {
                    //         //     let w = item.level * 40;
                    //         //     return `<span style="display:inline-block;width:${w}px"></span>`;
                    //         // }
                    //         return `<a href="/shop/admin/imageclass/${item.id}/imgList">${item.images.length}张</a>`;
                    //     }
                    // },
                    // {
                    //     title: '是否是导航栏栏目',
                    //     key: 'isnav',
                    //     width: 200,//可选
                    //     class: 'text-center',//可选
                    //     hidekeyData: true,//是否隐藏key对应的数据
                    //     render(item) {
                    //         console.log('可用状态里面每个item', item);
                    //         let arr = [
                    //             { value: 1, name: '是' },
                    //             { value: 0, name: '否' },
                    //         ];
                    //         let str = `<div class="btn-group btn-group-${item.id}">`;
                    //         for (let i = 0; i < arr.length; i++) {
                    //             str += `<button type="button" class="btn btn-light" data="${item.isnav}"
                    //             value="${arr[i].value}"
                    //             @click="changeBtnStatus('isnav','btn-group-${item.id}',${arr[i].value},${i},${item.id},'category','Category')">${arr[i].name}</button>`;
                    //         }
                    //         str += `</div>`;
                    //         return str;
                    //     }
                    // },
                    {
                        title: '排序',
                        key: 'order',
                        class: 'text-center',//可选
                    },
                    {
                        title: '状态',
                        key: 'status',
                        width: 200,//可选
                        class: 'text-center',//可选
                        hidekeyData: true,//是否隐藏key对应的数据
                        render(item) {
                            // console.log('可用状态里面每个item', item);
                            let arr = [
                                { value: 1, name: '可用' },
                                { value: 0, name: '不可用' },
                            ];
                            let str = `<div class="btn-group btn-group-${item.id}">`;
                            for (let i = 0; i < arr.length; i++) {
                                str += `<button type="button" class="btn btn-light" data="${item.status}"
                                value="${arr[i].value}"
                                @click="changeBtnStatus('status','btn-group-${item.id}',${arr[i].value},${i},${item.id},'/shop/admin/goods-','Goods')">${arr[i].name}</button>`;
                            }
                            str += `</div>`;
                            return str;
                        }
                    },
                    {
                        title: '操作',
                        class: 'text-right',//可选
                        action: {
                            //修改
                            edit: function (id) {
                                return `/shop/admin/goods-/edit/${id}`;
                            },
                            //删除
                            delete: function (id) {
                                return `/shop/admin/goods-/${id}/delete`;
                            }
                        }
                    },
                ],
            },
        });
    }

# 五、(选修补充)修改商品sku选购信息及新增商品sku选购信息时候删除之前的选购信息

# 1. 路由

app/router/admin/shop.js

    ...
    // 商品购物车sku选购 goods_sku
    router.get('/shop/admin/goods-/:goods_id/createGoodsSku', controller.admin.goods.createGoodsSku);
    router.post('/shop/admin/goods-/:goods_id/saveGoodsSku', controller.admin.goods.saveGoodsSku);
    router.get('/shop/admin/goods-/:goods_id/indexGoodsSku', controller.admin.goods.indexGoodsSku);
    router.get('/shop/admin/goods-/:id/editGoodsSku', controller.admin.goods.editGoodsSku);
    router.post('/shop/admin/goods-/:id/updateGoodsSku', controller.admin.goods.updateGoodsSku);
    router.get('/shop/admin/goods-/:id/deleteGoodsSku', controller.admin.goods.deleteGoodsSku);
    //商品参数goods_param处理
    //选择一个商品分类下的skus添加到商品参数中
    ...

# 2. 控制器

app/controller/admin/goods.js

    // 动态生成商品sku选购信息
    async createGoodsSku(){
        const { ctx, app } = this;
        //1.参数验证
        this.ctx.validate({
            goods_id: {
                type: 'int',
                required: true,
                desc: '商品id',
                // defValue: 0,
                range:{
                    min:1,
                }
            },
        });
        // 参数
        const goods_id = ctx.params.goods_id;
        let data = await app.model.Goods.findOne({ where: { id:goods_id } });
        if (!data) {
            return ctx.apiFail('该商品不存在');
        }
        await ctx.renderTemplate({
            title: '创建商品sku选购信息',//现在网页title,面包屑导航title,页面标题
            tempType: 'form', //模板类型:table表格模板 ,form表单模板,for_form循环模版
            form: {
                //提交地址
                action: "/shop/admin/goods-/"+goods_id+"/saveGoodsSku",
                //  字段
                fields: [
                    {
                        label: '商品选购信息',
                        type: 'for_form_goodsSku', //商品动态sku表单
                    },
                ],
                //隐藏提交按钮,用模版里面的提交按钮
                hideSubmit:true,
            },
            //新增成功之后跳转到哪个页面
            successUrl: '/shop/admin/goods-/'+goods_id+'/indexGoodsSku',
        });
    }
    // 动态生成商品sku选购信息提交数据
    async saveGoodsSku(){
        const { ctx, app } = this;
        //1.参数验证
        this.ctx.validate({
            goods_id: {
                type: 'int',
                required: true,
                desc: '商品id',
                // defValue: 0,
                range:{
                    min:1,
                }
            },
            skuData: {
                type: 'string',
                required: true,
                desc: '商品sku信息',
                // defValue: 0,
                range:{
                    // min:1,
                }
            },
            skuItems: {
                type: 'string',
                required: true,
                desc: '商品sku选项',
                // defValue: 0,
                range:{
                    // min:1,
                }
            },
        });
        // 参数
        const goods_id = ctx.params.goods_id;
        let data = await app.model.Goods.findOne({ where: { id:goods_id } });
        if (!data) {
            return ctx.apiFail('该商品不存在');
        }
        //删除已存在的sku信息
        await app.model.GoodsSku.destroy({ where: { goods_id:goods_id } });

        let {skuData,skuItems} = ctx.request.body;
        skuData = JSON.parse(skuData);
        //给skuData添加goods_id
        skuData.forEach(item=>{
            item.goods_id = goods_id;
            item.name = JSON.stringify(item.name);
        })
        //保存数据
        await app.model.GoodsSku.bulkCreate(skuData);
        // 更新skuItems到goods表的sku_value字段
        data.sku_value = skuItems;
        await data.save();
        ctx.apiSuccess('创建成功');
    }
    
    // 商品sku列表
    async indexGoodsSku(){
        const { ctx, app } = this;
        //1.参数验证
        this.ctx.validate({
            goods_id: {
                type: 'int',
                required: true,
                desc: '商品id',
                // defValue: 0,
                range:{
                    min:1,
                }
            },
            page: {
                type: 'int',
                required: false,
                desc: '页码',
                defValue: 1,
                range:{
                    min:1,
                }
            },
            limit: {
                type: 'int',
                required: false,
                desc: '每页显示数量',
                defValue: 10,
                range:{
                    min:1,
                }
            },
        });
        // 参数
        const goods_id = ctx.params.goods_id;
        let Goodsdata = await app.model.Goods.findOne({ where: { id:goods_id } });
        if (!Goodsdata) {
            return ctx.apiFail('该商品不存在');
        }
        // let GoodsClassdata = await app.model.GoodsClass.findOne({
        //     where:{
        //         id:Goodsdata.goods_class_id
        //     },
        //     include: [
        //         { 
        //             model: app.model.Skus, 
        //             // as: 'children' 
        //         },
        //     ],
        // });
        // ctx.body = GoodsClassdata; return;
        //分页:可以提炼成一个公共方法page(模型名称,where条件,其他参数options)
        let data = await ctx.page('GoodsSku',{
            goods_id:goods_id
        },{
            order:[
                ['order', 'asc'],
                ['id', 'asc'],
            ],
        });
        // let data = await ctx.service.goodsClass.datalist({ limit: 10000 });
        // console.log('分类数据', data);
        // ctx.body = data;
        // return;
        // data = data.rules;
        //渲染公共模版
        await ctx.renderTemplate({
            title: '商品:' + Goodsdata.name + ' 选购sku列表',//现在网页title,面包屑导航title,页面标题
            data,
            tempType: 'table', //模板类型:table表格模板 ,form表单模板
            table: {
                //表格上方按钮,没有不要填buttons
                buttons: [
                    {
                        url: '/shop/admin/goods-/'+goods_id+'/createGoodsSku',//新增路径
                        desc: '创建商品选购sku',//新增 //按钮名称
                        // icon: 'fa fa-plus fa-lg',//按钮图标
                    },
                    // {
                    //     url: '/shop/admin/goods/create',//新增路径
                    //     desc: '上传商品',//新增 //按钮名称
                    //     // icon: 'fa fa-plus fa-lg',//按钮图标
                    // }
                ],
                //表头
                columns: [
                    {
                        title: '选购的组合名称',
                        // key: 'name',
                        class: 'text-center',//可选
                        render(item) { //树形数据
                            let name = JSON.parse(item.name);
                            //遍历对象属性值
                            let result = '';
                            for (let key in name) {
                                if(name.hasOwnProperty(key)){
                                    result += `
                                    <span style="background:#f0f0f0;padding:2px 5px;margin:5px;
                                    border-radius:5px;display:inline-block;font-size:14px;">${name[key]}</span>`;
                                }
                            }
                            return result;
                        }
                    },
                    {
                        title: '选购的组合图片',
                        // key: 'cover',
                        class: 'text-center',//可选
                        render(item) { //树形数据
                            // console.log('每个item',item);
                            // if (item.level) {
                            //     let w = item.level * 40;
                            //     return `<span style="display:inline-block;width:${w}px"></span>`;
                            // }
                            let str = ``;
                            if(item.cover){
                                str = `<img src="${item.cover}" style="width:100px;height:100px;">`;
                            }
                            return `${str}`;
                        }
                    },
                    // {
                    //     title: '是否是导航栏栏目',
                    //     key: 'isnav',
                    //     width: 200,//可选
                    //     class: 'text-center',//可选
                    //     hidekeyData: true,//是否隐藏key对应的数据
                    //     render(item) {
                    //         console.log('可用状态里面每个item', item);
                    //         let arr = [
                    //             { value: 1, name: '是' },
                    //             { value: 0, name: '否' },
                    //         ];
                    //         let str = `<div class="btn-group btn-group-${item.id}">`;
                    //         for (let i = 0; i < arr.length; i++) {
                    //             str += `<button type="button" class="btn btn-light" data="${item.isnav}"
                    //             value="${arr[i].value}"
                    //             @click="changeBtnStatus('isnav','btn-group-${item.id}',${arr[i].value},${i},${item.id},'category','Category')">${arr[i].name}</button>`;
                    //         }
                    //         str += `</div>`;
                    //         return str;
                    //     }
                    // },
                    {
                        title: '库存',
                        key: 'stock',
                        class: 'text-center',//可选
                    },
                    {
                        title: '价格',
                        key: 'price',
                        class: 'text-center',//可选
                    },
                    {
                        title: '优惠价',
                        key: 'special_price',
                        class: 'text-center',//可选
                    },
                    {
                        title: '排序',
                        key: 'order',
                        class: 'text-center',//可选
                    },
                    {
                        title: '可用状态',
                        key: 'status',
                        width: 200,//可选
                        class: 'text-center',//可选
                        hidekeyData: true,//是否隐藏key对应的数据
                        render(item) {
                            // console.log('可用状态里面每个item', item);
                            let arr = [
                                { value: 1, name: '可用' },
                                { value: 0, name: '不可用' },
                            ];
                            let str = `<div class="btn-group btn-group-${item.id}">`;
                            for (let i = 0; i < arr.length; i++) {
                                str += `<button type="button" class="btn btn-light" data="${item.status}"
                                value="${arr[i].value}"
                                @click="changeBtnStatus('status','btn-group-${item.id}',${arr[i].value},${i},${item.id},'/shop/admin/goods-/${goods_id}/indexGoodsSku','GoodsSku')">${arr[i].name}</button>`;
                            }
                            str += `</div>`;
                            return str;
                        }
                    },
                    {
                        title: '操作',
                        class: 'text-right',//可选
                        action: {
                            //修改
                            edit: function (id) {
                                return `/shop/admin/goods-/${id}/editGoodsSku`;
                            },
                            //删除
                            delete: function (id) {
                                return `/shop/admin/goods-/${id}/deleteGoodsSku`;
                            }
                        }
                    },
                ],
            },
        });
    }
    // 删除商品sku
    async deleteGoodsSku(){
        const { ctx, app } = this;
        const id = ctx.params.id;

        let data = await app.model.GoodsSku.findOne({ where: { id } });
        if (!data) {
            return ctx.apiFail('该商品sku不存在');
        }
        await app.model.GoodsSku.destroy({
            where: {
                id
            }
        });
        //提示
        ctx.toast('商品sku数据删除成功', 'success');
        //跳转
        ctx.redirect('/shop/admin/goods-/'+data.goods_id+'/indexGoodsSku');
    }
    // 修改商品sku选购信息界面
    async editGoodsSku() {
        const { ctx, app } = this;
        const id = ctx.params.id;
        let currentdata = await app.model.GoodsSku.findOne({
            where: {
                id,
                // status:1
            }
        });
        if (!currentdata) {
            return ctx.apiFail('该商品sku信息不存在');
        }
        currentdata = JSON.parse(JSON.stringify(currentdata));
        // console.log('当前商品分类数据', currentdata);
        // return;

        let Goodsdata = await app.model.Goods.findOne({
            where: {
                id: currentdata.goods_id,
                // status:1
            }
        });

        // 渲染模版前先拿到所有分类
        // let data = await ctx.service.goodsClass.dropdown_goodsclass_list();
        // console.log('下拉框显示的所有分类', JSON.stringify(data));
        // return;

        //渲染公共模版
        await ctx.renderTemplate({
            id,
            title: '修改商品:' + Goodsdata.name + '的sku信息',//现在网页title,面包屑导航title,页面标题
            tempType: 'form', //模板类型:table表格模板 ,form表单模板
            form: {
                //修改提交地址
                action: '/shop/admin/goods-/' + id + '/updateGoodsSku',
                //  字段
                fields: [
                    // {
                    //     label: '放在哪个商品分类里面',
                    //     type: 'dropdown', //下拉框
                    //     name: 'goods_class_id',
                    //     default: JSON.stringify(data),
                    //     placeholder: '不调整(如需调整请选择)',
                    // },
                    // {
                    //     label: '商品参数信息',
                    //     type: 'for_form', 
                    //     name: 'paraminfo',
                    //     placeholder: '',
                    //     default:currentdata.paraminfo, //新增时候默认值,可选
                    //     id:id, //加一个id,代表修改,不加id代表新增
                    // },
                    {
                        label: '选购组合',
                        type: 'text', 
                        name: 'namestr',
                        placeholder: '选购组合不可修改',
                        // default:50,
                        disabled:true, //禁用
                    },
                    {
                        label: '选购组合封面图',
                        type: 'file', // fileoss|file
                        name: 'cover',
                        placeholder: '请输入选购组合封面图,选填',
                        // default:50,
                    },
                    {
                        label: '商品选购组合库存数量',
                        type: 'number',
                        name: 'stock',
                        placeholder: '请输入商品选购组合库存数量,选填',
                        // default:50,
                    },
                    {
                        label: '商品选购组合价格',
                        type: 'number',
                        name: 'price',
                        placeholder: '请输入商品选购组合价格,选填',
                        // default:50,
                    },
                    {
                        label: '商品选购组合优惠价格',
                        type: 'number',
                        name: 'special_price',
                        placeholder: '请输入商品选购组合优惠价格,选填',
                        // default:50,
                    },
                    {
                        label: '排序',
                        type: 'number',
                        name: 'order',
                        placeholder: '请输入排序,选填',
                        // default:50,
                    },
                    {
                        label: '可用状态',
                        type: 'btncheck', //按钮组选择
                        name: 'status',
                        default: JSON.stringify([
                            { value: 1, name: '可用', checked: currentdata.status === 1 },
                            { value: 0, name: '不可用', checked: currentdata.status === 0 },
                        ]),
                        placeholder: '状态 0不可用 1可用 等等状态',
                    },
                ],
                //修改内容默认值
                data:currentdata,
            },
            //修改成功之后跳转到哪个页面
            successUrl: '/shop/admin/goods-/'+currentdata.goods_id+'/indexGoodsSku',
        });
    }
    // 修改商品sku选购信息提交数据
    async updateGoodsSku() {
        const { ctx, app } = this;
        //1.参数验证
        this.ctx.validate({
            id: {
                type: 'int',
                required: true,
                desc: '商品sku信息id',
                // defValue: 0,
                range:{
                    min:1,
                }
            },
            cover: {
                type: 'string',
                required: false,
                desc: '商品选购封面',
                // defValue: 0,
                range:{
                    max:2000
                }
            },
            stock: {
                type: 'number',
                required: false,
                desc: '商品选购组合库存数量',
                defValue: 0,
                range:{
                    // max:2000
                }
            },
            price: {
                type: 'number',
                required: false,
                desc: '商品选购组合价格',
                // defValue: 0,
                range:{
                    // max:2000
                }
            },
            special_price: {
                type: 'number',
                required: false,
                desc: '商品选购组合优惠价格',
                // defValue: 0,
                range:{
                    // max:2000
                }
            },
            status: {
                type: 'int',
                required: false,
                defValue: 1,
                desc: '状态 0不可用 1可用 等等状态',
                range:{
                    in:[0,1]
                }
            },
            order: {
                type: 'int',
                required: false,
                defValue: 50,
                desc: '排序'
            },
        });
        // 参数
        const id = ctx.params.id;
        // 先看一下是否存在
        let data = await app.model.GoodsSku.findOne({ where: { id } });
        if (!data) {
            return ctx.apiFail('该商品SKU信息不存在');
        }
        const {  cover,stock,price,special_price, order, status } = ctx.request.body;
        let goods_id = data.goods_id;
        // 修改数据
        data.cover = cover;
        data.stock = stock;
        data.price = price;
        data.special_price = special_price;
        data.status = status;
        // data.goods_id = goods_id;
        data.order = order;
        await data.save();
        // 给一个反馈
        ctx.apiSuccess('修改商品SKU成功');
    }
更新时间: 2025年5月8日星期四下午3点16分