FastAdmin 框架实战:商品多规格管理功能完整开发方案

在电商类系统开发中,商品多规格管理是核心功能之一,用于实现颜色、尺寸、型号等不同规格组合的商品库存、价格独立管理。FastAdmin 作为基于 ThinkPHP5+Bootstrap 的快速开发框架,凭借其插件化、模块化特性,可高效实现商品多规格的动态添加、编辑、删除、组合生成及数据持久化功能。
本文基于原生 FastAdmin 框架,从前端模板渲染、JS 逻辑交互、后端数据处理、模型配置四个维度,详解商品多规格管理功能的完整实现方案,开箱即用,可直接集成到项目中。

一、功能核心需求梳理

商品多规格管理需满足以下核心业务场景:

  1. 支持单规格 / 多规格自由切换,满足不同商品场景需求;
  2. 动态添加 / 删除规格名称(如颜色、尺寸);
  3. 为每个规格名称动态添加 / 删除规格值(如红色、S 码);
  4. 自动根据规格值生成所有规格组合(如红色 + S、红色 + M);
  5. 为每个规格组合独立配置成本价、售价、库存、状态;
  6. 新增 / 编辑商品时,自动保存 / 更新规格及组合数据;
  7. 编辑页面回显已保存的规格、规格值、规格组合数据。

二、前端实现:模板与交互逻辑

前端基于 FastAdmin 内置的HTML 模板引擎+jQuery 实现动态渲染,核心分为主页面布局模板、子模板定义和JS 交互逻辑三部分,完美兼容单规格 / 多规格切换。

1. 商品价格库存主页面模板

这是商品添加 / 编辑页的核心表单布局,集成了 FastAdmin 的data-favisible条件显示功能,实现单规格 / 多规格表单自动切换:

html 复制代码
<div class="panel panel-default">
    <div class="panel-heading">价格库存</div>
    <div class="panel-body">

        <div class="form-group">
            <label class="control-label col-xs-12 col-sm-2">{:__('规格类型')}:</label>
            <div class="col-xs-12 col-sm-8">
                <div class="col-xs-12 col-sm-8">
                    {:build_radios('row[is_multi]', [1=>'多规格',0=>'单规格'],$row['is_multi'])}
                    <input id="attrs" type="hidden" class="form-control" name="row[attrs]" placeholder="" value="">
                </div>
            </div>
        </div>

        <!-- 单规格表单:仅当is_multi=0时显示 -->
        <div data-favisible="is_multi=0">
            <div class="form-group">
                <label class="control-label col-xs-12 col-sm-2">{:__('Origin_price')}:</label>
                <div class="col-xs-12 col-sm-8">
                    <input id="c-origin_price"  class="form-control" step="0.01" name="row[sku][origin_price]" type="number" value="{$row.sku.origin_price}">
                </div>
            </div>
            <div class="form-group">
                <label class="control-label col-xs-12 col-sm-2">{:__('Cost_price')}:</label>
                <div class="col-xs-12 col-sm-8">
                    <input id="c-cost_price"  class="form-control" step="0.01" name="row[sku][cost_price]" type="number" value="{$row.sku.cost_price}">
                </div>
            </div>
            <div class="form-group">
                <label class="control-label col-xs-12 col-sm-2">{:__('Price')}:</label>
                <div class="col-xs-12 col-sm-8">
                    <input id="c-price" data-rule="required"  class="form-control" step="0.01" name="row[sku][price]" type="number" value="{$row.sku.price}">
                </div>
            </div>
            <div class="form-group">
                <label class="control-label col-xs-12 col-sm-2">{:__('Stock')}:</label>
                <div class="col-xs-12 col-sm-8">
                    <input id="c-stock" data-rule="required"  class="form-control" min="1"  step="1" name="row[sku][stock]" type="number" value="{$row.sku.stock}">
                </div>
            </div>
        </div>

        <!-- 多规格表单:仅当is_multi=1时显示 -->
        <div data-favisible="is_multi=1">
            <!-- 多规格添加入口 -->
            <div class="form-group" >
                <label class="control-label col-xs-12 col-sm-2">{:__('规格名称')}:</label>
                <div class="col-xs-12 col-sm-8 form-inline">
                    <div class="input-group">
                        <input id="attr-key" type="text" class="form-control" placeholder="请输入规格名称(如颜色)" value="">
                        <span class="input-group-btn">
                            <button class="btn btn-success plus-attr-key"  type="button"> <i class="glyphicon glyphicon-plus"></i>添加规格</button>
                        </span>
                    </div>
                </div>
            </div>

            <!-- 规格组渲染容器 -->
            <div class="form-group">
                <label class="control-label col-xs-12 col-sm-2"></label>
                <div class="col-xs-12 col-sm-12 spec-html"></div>
            </div>

            <!-- 规格组合表格渲染容器 -->
            <div class="form-group">
                <label class="control-label col-xs-12 col-sm-2"></label>
                <div class="col-xs-12 col-sm-12 product-list-html"></div>
            </div>
        </div>

    </div>
</div>

2. 前端 HTML 子模板定义

通过<script type="text/html">定义 4 个核心子模板,实现规格、规格值、商品组合列表的动态渲染:

html 复制代码
<!-- 1. 单个规格值输入框模板 -->
<script type="text/html" id="attr-val-tpl">
    <div class="input-group attr-vals" data-attr_key_index="${attr_key_index}" data-attr_val_index="${attr_val_index}">
        <input type="text" class="form-control attr-val" placeholder="请输入规格值" value="${val}">
        <span class="input-group-btn remove-attr-val">
            <button class="btn btn-danger" type="button"><i class="glyphicon glyphicon-remove"></i></button>
        </span>
    </div>
</script>

<!-- 2. 规格组(名称+值)模板 -->
<script type="text/html" id="spec-html-tpl">
    <div class="panel panel-default" data-attr_key_index="${attr_key_index}">
        <div class="panel-heading">
            ${attr_key}
            <i class="glyphicon glyphicon-remove fr remove-attr-key"></i>
        </div>
        <div class="panel-body form-inline">
            ${attr_val_html}
            <button class="btn btn-success plus-attr-val" type="button">追加规格值</button>
        </div>
    </div>
</script>

<!-- 3. 商品规格组合列表容器模板 -->
<script type="text/html" id="products-list-tpl">
    <div class="table-responsive">
        <table class="table table-striped table-bordered table-hover">
            <thead>
                <tr>
                    <th>序号</th>
                    ${sku_keys}
                    <th>封面</th>
                    <th>成本价</th>
                    <th>市场价</th>
                    <th>售价</th>
                    <th>库存</th>
                    <th>状态</th>
                </tr>
            </thead>
            <tbody id="products-list">${rows}</tbody>
        </table>
    </div>
</script>

<!-- 4. 单个规格组合行模板 -->
<script type="text/html" id="products-row-tpl">
    <tr>
        <input class="form-control" name="row[skus][${index}][id]" type="hidden" value="${id}">
        <input class="form-control" name="row[skus][${index}][sku_attrs]" type="hidden" value="${sku_attrs}">
        <td>${num}</td>
        ${sku_vals}
        <td></td>
        <td><input class="form-control" min="0" name="row[skus][${index}][cost_price]" type="number" value="${cost_price}"></td>
        <td><input class="form-control" min="0" name="row[skus][${index}][origin_price]" type="number" value="${origin_price}"></td>
        <td><input class="form-control" min="0"  name="row[skus][${index}][price]" type="number" value="${price}"></td>
        <td><input class="form-control" min="0"  name="row[skus][${index}][stock]" type="number" value="${stock}"></td>
        <td><input type="checkbox" name="row[skus][${index}][status]" value="1" ${checked}></td>
    </tr>
</script>

3. 前端 JS 核心交互逻辑

JS 核心实现规格增删、值增删、自动组合生成、数据回显,基于 FastAdmin 的Form.api.bindevent绑定事件,兼容动态渲染元素:

javascript 复制代码
api: {
    bindevent: function () {
        // 初始化规格数据(编辑页回显/新增页空数据)
        let _attrs = transAttrs(Config.attrs);
        const updateSpecHtml = () => { buildSpecHtml(_attrs); };
        updateSpecHtml();

        // 获取元素绑定的索引值
        const findAttrIndex = ($element, keyAttr = 'attr_key_index') => {
            return parseInt($element.data(keyAttr), 10);
        };

        // 1. 添加规格名称
        $(document).on('click', '.plus-attr-key', function () {
            const attr_key_val = $('#attr-key').val().trim();
            if (!attr_key_val) return Layer.msg('请填写规格名称');
            if (_attrs.some(item => item.name === attr_key_val)) return Layer.msg('规格名称已存在');
            _attrs.push({ name: attr_key_val, value: [] });
            $('#attr-key').val('');
            updateSpecHtml();
        });

        // 2. 删除规格名称
        $(document).on('click', '.remove-attr-key', function () {
            const index = findAttrIndex($(this).closest('[data-attr_key_index]'));
            _attrs.splice(index, 1);
            updateSpecHtml();
        });

        // 3. 添加规格值
        $(document).on('click', '.plus-attr-val', function () {
            const attr_key_index = findAttrIndex($(this).closest('[data-attr_key_index]'));
            _attrs[attr_key_index].value.push('');
            updateSpecHtml();
        });

        // 4. 删除规格值
        $(document).on('click', '.remove-attr-val', function () {
            const $parent = $(this).closest('[data-attr_key_index]');
            const attr_key_index = findAttrIndex($parent);
            const attr_val_index = findAttrIndex($(this).closest('[data-attr_val_index]'), 'attr_val_index');
            _attrs[attr_key_index].value.splice(attr_val_index, 1);
            updateSpecHtml();
        });

        // 5. 监听规格值输入变更
        $(document).on('change', '.attr-val', function () {
            const $parent = $(this).closest('[data-attr_key_index]');
            const attr_key_index = findAttrIndex($parent);
            const attr_val_index = findAttrIndex($(this).closest('[data-attr_val_index]'), 'attr_val_index');
            _attrs[attr_key_index].value[attr_val_index] = $(this).val();
            updateSpecHtml();
        });

        // 绑定FastAdmin表单提交
        Form.api.bindevent($("form[role=form]"));
    }
}

// 数据转换/组合生成工具函数
// 1. 数据库数据转前端规格格式
function transAttrs(attrs) {
    if (!Array.isArray(attrs)) return [];
    const groupedAttrs = attrs.reduce((acc, item) => {
        if (!acc[item.attr_key]) acc[item.attr_key] = [];
        acc[item.attr_key].push(item.attr_value);
        return acc;
    }, {});
    return Object.entries(groupedAttrs).map(([name, value]) => ({ name, value }));
}

// 2. 前端规格转数据库存储格式
function formatDbAttrs(attrs) {
    if (!attrs.length) return [];
    return attrs.flatMap(item => item.value.map(val => ({ attr_key: item.name, attr_value: val })));
}

// 3. 生成所有规格组合
function generateCombinations(attrs) {
    const formattedAttrs = attrs.filter(item => item.value.length).map(item => 
        item.value.map(val => `${item.name}:${val}`)
    );
    return formattedAttrs.reduce((acc, curr) => {
        if (!acc.length) return curr.map(i => [i]);
        let res = [];
        acc.forEach(a => curr.forEach(c => res.push([...a, c])));
        return res;
    }, []).map(i => i.join(','));
}

// 4. 渲染规格+商品组合列表
function buildSpecHtml(attrs) {
    $('#attrs').val(JSON.stringify(formatDbAttrs(attrs)));
    buildProductsHtml(attrs);
    const specHtmlTemplate = $('#spec-html-tpl').html();
    const attrValTemplate = $('#attr-val-tpl').html();
    let html = '';
    attrs.forEach((item,index) => {
        let attr_val_html = ''
        item.value.forEach((valItem,valIndex)=>{
            attr_val_html+= attrValTemplate
                .replace(/\${val}/g,valItem)
                .replace(/\${attr_key_index}/g,index)
                .replace(/\${attr_val_index}/g,valIndex)
        })

        html += specHtmlTemplate
            .replace(/\${attr_key}/g, item.name)
            .replace(/\${attr_key_index}/g, index)
            .replace(/\${attr_val_html}/g, attr_val_html);
    });
    $('.spec-html').html(html);
}

// 5. 渲染规格组合表格
function buildProductsHtml(attrs) {
    if (!attrs || attrs.length === 0) {
        return [];
    }
    const products = buildProducts(attrs)

    const productsListTpl = $('#products-list-tpl').html();
    const productsRowTpl = $('#products-row-tpl').html();

    // 单行数据
    const rows_html = products.map((product, index) => {
        const sku_val_html = attrs.map(item => {
            const skuAttr = product.sku_attrs.find(attr => attr.attr_key === item.name);
            return `<td>${skuAttr ? skuAttr.attr_value : ''}</td>`;
        }).join('');

        const checked = product.status === 1? 'checked' : ''

        return productsRowTpl
            .replace(/\${sku_attrs}/g, JSON.stringify(product.sku_attrs))
            .replace(/\${checked}/g, checked)
            .replace(/\${index}/g, index)
            .replace(/\${num}/g, index + 1)
            .replace(/\${origin_price}/g, product.origin_price)
            .replace(/\${cost_price}/g, product.cost_price)
            .replace(/\${price}/g, product.price)
            .replace(/\${stock}/g, product.stock)
            .replace(/\${id}/g, product.id)
            .replace(/\${sku_vals}/g, sku_val_html);

    }).join('');

    // 整体表格数据
    const sku_keys_html = attrs.map(item => `<th>${item.name}</th>`).join('');
    const products_html = productsListTpl
        .replace(/\${rows}/g, rows_html)
        .replace(/\${sku_keys}/g, sku_keys_html);

    $('.product-list-html').html(products_html);
}

// 构建产品列表结构
function buildProducts(attrs) {
    const dbProducts = Config.skus;
    const combinations = generateCombinations(attrs)
    const products = combinations.map(combo => ({
        sku_attrs: parseSkuAttrs(combo),
        status: 1,
        origin_price: '',
        cost_price: '',
        price: '',
        stock: '',
        id:''
    }));

    if (!dbProducts || dbProducts.length === 0) {
        return products;
    }

    return products.map(product => {
        const matchedDbProduct = dbProducts.find(dbProduct =>
            isSkuAttrsEqual(product.sku_attrs, dbProduct.sku_attrs)
        );

        return matchedDbProduct ? {...product, ...matchedDbProduct} : {...product};
    });
}
// 比较两个sku_attrs
function isSkuAttrsEqual(attrs1, attrs2) {
    if (!Array.isArray(attrs1) || !Array.isArray(attrs2)) {
        return false;
    }

    if (attrs1.length !== attrs2.length) {
        return false;
    }

    const normalizedAttrs1 = normalizeSkuAttrs(attrs1);
    const normalizedAttrs2 = normalizeSkuAttrs(attrs2);

    return JSON.stringify(normalizedAttrs1) === JSON.stringify(normalizedAttrs2);
}
function normalizeSkuAttrs(attrs) {
    return attrs
        .map(attr => `${attr.attr_key}:${attr.attr_value}`)
        .sort()
        .join('|');
}
function parseSkuAttrs(skuStr) {
    return skuStr.split(',').map(item => {
        const [attr_key, attr_value] = item.split(':');
        return { attr_key, attr_value };
    });
}

三、后端实现:控制器 + 服务层 + 模型

后端基于 FastAdmin 的MVC 架构,分离控制器(请求处理)、服务层(业务逻辑)、模型(数据交互),保证代码可维护性。

1. 模型配置(Goods/GoodsSku)

实现JSON 数据自动转换,简化前后端数据交互:

php 复制代码
// app\common\model\Goods.php 商品主模型
class Goods extends \think\Model
{
    // 自动json解析规格字段
    public function getAttrsAttr($value)
    {
        return json_decode($value, true) ?? [];
    }

    // 关联规格组合
    public function skus()
    {
        return $this->hasMany(GoodsSku::class, 'goods_id', 'id');
    }
}

// app\common\model\GoodsSku.php 商品规格组合模型
class GoodsSku extends \think\Model
{
    // 自动json解析规格组合字段
    public function getSkuAttrsAttr($value)
    {
        return json_decode($value, true) ?? [];
    }
}

2. 服务层:核心业务逻辑

封装新增 / 更新商品规格的核心逻辑,实现数据校验、事务控制、增删改查:

php 复制代码
// app\common\service\GoodsService.php
class GoodsService
{
    // 新增商品+规格
    public static function createGoods($params)
    {
        $isMulti = $params['is_multi'] ?? GoodsEnum::MULTI_FALSE;
        $sku_data = $params['sku'] ?? [];
        $skus_data = $params['skus'] ?? [];
        unset($params['sku'], $params['skus']);

        $params = self::buildParams($params);
        Db::startTrans();
        try {
            $goods = Goods::create($params, true);
            // 单规格处理
            if (GoodsEnum::MULTI_FALSE == $isMulti) {
                if (!$sku_data) {
                    throw new ValidateException('请填写规格参数');
                }
                $sku_data['goods_id'] = $goods->id;
                GoodsSku::create($sku_data);
            } else {
                // 多规格处理
                if (!$skus_data) {
                    throw new ValidateException('请选择规格参数');
                }
                foreach ($skus_data as &$item) {
                    (new \app\admin\validate\goods\GoodsSku())->scene('add')->goCheck($item);
                    $item['status'] = !isset($item['status']) ? 0:$item['status'];
                    $item['goods_id'] = $goods->id;
                }
                (new GoodsSku())->allowField(true)->saveAll($skus_data);
            }
            Db::commit();
        } catch (\Throwable $e) {
            Db::rollback();
            throw new ValidateException($e->getMessage());
        }
    }

    // 更新商品+规格
    public static function updateGoods($params)
    {
        $isMulti = $params['is_multi'] ?? GoodsEnum::MULTI_FALSE;
        $sku_data = $params['sku'] ?? [];
        $skus_data = $params['skus'] ?? [];
        unset($params['sku'], $params['skus']);

        $params = self::buildParams($params);
        $info = Goods::where('id', $params['id'])->find();
        Db::startTrans();
        try {
            $info->allowField(true)->save($params);
            // 单规格更新
            if (GoodsEnum::MULTI_FALSE == $isMulti) {
                $sku = GoodsSku::where('goods_id', $info->id)->find();
                $sku->allowField(true)->save($sku_data);
            } else {
                // 多规格增量更新
                $oids = GoodsSku::where('goods_id', $info->id)->column('id');
                $insert = $update = [];
                foreach ($skus_data as $item) {
                    $item['goods_id'] = $info->id;
                    $skuId = $item['id'] ?? null;
                    if ($skuId) {
                        $update[] = $item;
                    } else {
                        $insert[] = $item;
                    }
                }
                [$delIds] = array_diffs($oids, array_column($update, 'id'));
                $insert && GoodsSku::createAll($insert);
                $update && GoodsSku::updateAll($update);
                $delIds && GoodsSku::destroy($delIds);
            }
            Db::commit();
        } catch (\Throwable $e) {
            Db::rollback();
            throw new ValidateException($e->getMessage());
        }
    }

    // 参数校验
    public static function buildParams($params)
    {
        $shipTypes = $params['ship_types'] ?? 'express';
        $freight_template_id = $params['freight_template_id'] ?? 0;
        return $params;
    }
}

3. 控制器:请求入口

处理新增 / 编辑页面渲染和表单提交,兼容数据回显:

php 复制代码
// app\admin\controller\goods\Goods.php
class Goods extends Backend
{
    // 新增商品
    public function add()
    {
        if (false === $this->request->isPost()) {
            $this->assignconfig('attrs',[]);
            $this->assignconfig('skus',[]);
            return $this->view->fetch();
        }
        $params = $this->request->post('row/a');
        Db::startTrans();
        try {
            GoodsService::createGoods($params);
            Db::commit();
        } catch (\Throwable $e) {
            Db::rollback();
            $this->error($e->getMessage());
        }
        $this->success();
    }

    // 编辑商品
    public function edit($ids = null)
    {
        $row = $this->model->get($ids);
        if (false === $this->request->isPost()) {
            $this->view->assign('row', $row);
            $this->assignconfig('attrs',$row->attrs?? []);
            $this->assignconfig('skus',$row->skus?? []);
            return $this->view->fetch();
        }
        $params = $this->request->post('row/a');
        Db::startTrans();
        try {
            $params['id'] = $ids;
            GoodsService::updateGoods($params);
            Db::commit();
        } catch (\Throwable $e) {
            Db::rollback();
            $this->error($e->getMessage());
        }
        $this->success();
    }
}

四、核心功能流程详解

1. 规格切换流程

  • 选择单规格:自动隐藏多规格模块,显示统一的价格、库存表单;
  • 选择多规格:自动显示规格添加、规格组合表格,支持动态配置。

2. 新增商品流程

  1. 前端选择规格类型,动态添加规格名称 + 规格值;
  2. JS 自动生成所有规格组合,渲染价格 / 库存表单;
  3. 表单提交时,规格数据转为 JSON 格式提交到后端;
  4. 服务层开启事务,先保存商品主表,再批量保存规格组合;
  5. 事务提交,完成商品多规格创建。

3. 编辑商品流程

  1. 编辑页加载时,控制器通过assignconfig回显规格 + 组合数据;
  2. JS 解析回显数据,自动渲染规格、规格值、组合列表;
  3. 用户修改规格 / 价格 / 库存后提交表单;
  4. 服务层增量更新规格组合(新增 / 修改 / 删除冗余数据);
  5. 事务提交,完成数据更新。

五、功能优势与扩展说明

1. 核心优势

  1. 单多规格无缝切换:基于 FastAdmin 原生data-favisible实现,无需自定义 JS;
  2. 纯原生实现:基于 FastAdmin 内置组件,无需额外插件,兼容性强;
  3. 动态渲染:规格 / 值增删无刷新,自动生成组合,提升操作效率;
  4. 数据安全:后端事务控制,避免数据不一致,支持增量更新;
  5. 易维护:前后端分离,模板、交互、业务逻辑解耦。

2. 扩展方向

  1. 规格图片:在规格组合模板中添加图片上传字段,实现规格封面管理;
  2. 快速批量赋值:添加批量设置价格 / 库存功能,减少重复操作;
  3. 规格排序:增加规格名称 / 值的拖拽排序功能;
  4. 库存预警:后端添加库存低于阈值的校验与提醒。

六、总结

本文基于 FastAdmin 框架实现的商品多规格管理功能,覆盖了电商系统商品规格管理的全业务场景,完美支持单规格 / 多规格自由切换,从前端动态交互到后端数据持久化,形成了完整的解决方案。代码遵循 FastAdmin 开发规范,结构清晰、易于二次开发,可直接集成到各类电商、分销、零售类 FastAdmin 项目中,大幅降低多规格商品管理的开发成本。
核心亮点在于前端自动组合生成、单多规格无缝切换和后端增量更新,既保证了用户操作的便捷性,又保障了数据的准确性与性能,是 FastAdmin 电商开发的必备功能模块。

相关推荐
JSON_L11 天前
Fastadmin中使用think-queue队列
php·fastadmin
JSON_L11 天前
Fastadmin中使用阿里云oss
php·oss·fastadmin
withoutfear19 天前
fastadmin表格多tab选项卡组合筛选
php·fastadmin·tp5
JSON_L1 个月前
Fastadmin中实现获取名称首字母
php·fastadmin
云游云记2 个月前
在FastAdmin ThinkPHP5环境下 关联查询 软删除未生效
php·fastadmin·软删除
appleคิดถึง2 个月前
fastadmin 生成邀请海报
二维码·fastadmin·tp5·邀请海报
JSON_L2 个月前
Fastadmin中使用GatewayClient
php·fastadmin
JSON_L2 个月前
Fastadmin中使用百度翻译API
php·fastadmin·百度翻译api
JSON_L3 个月前
Fastadmin Excel 导入实现
php·excel·fastadmin