在电商类系统开发中,商品多规格管理是核心功能之一,用于实现颜色、尺寸、型号等不同规格组合的商品库存、价格独立管理。FastAdmin 作为基于 ThinkPHP5+Bootstrap 的快速开发框架,凭借其插件化、模块化特性,可高效实现商品多规格的动态添加、编辑、删除、组合生成及数据持久化功能。
本文基于原生 FastAdmin 框架,从前端模板渲染、JS 逻辑交互、后端数据处理、模型配置四个维度,详解商品多规格管理功能的完整实现方案,开箱即用,可直接集成到项目中。
一、功能核心需求梳理
商品多规格管理需满足以下核心业务场景:
- 支持单规格 / 多规格自由切换,满足不同商品场景需求;
- 动态添加 / 删除规格名称(如颜色、尺寸);
- 为每个规格名称动态添加 / 删除规格值(如红色、S 码);
- 自动根据规格值生成所有规格组合(如红色 + S、红色 + M);
- 为每个规格组合独立配置成本价、售价、库存、状态;
- 新增 / 编辑商品时,自动保存 / 更新规格及组合数据;
- 编辑页面回显已保存的规格、规格值、规格组合数据。
二、前端实现:模板与交互逻辑
前端基于 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. 新增商品流程
- 前端选择规格类型,动态添加规格名称 + 规格值;
- JS 自动生成所有规格组合,渲染价格 / 库存表单;
- 表单提交时,规格数据转为 JSON 格式提交到后端;
- 服务层开启事务,先保存商品主表,再批量保存规格组合;
- 事务提交,完成商品多规格创建。
3. 编辑商品流程
- 编辑页加载时,控制器通过assignconfig回显规格 + 组合数据;
- JS 解析回显数据,自动渲染规格、规格值、组合列表;
- 用户修改规格 / 价格 / 库存后提交表单;
- 服务层增量更新规格组合(新增 / 修改 / 删除冗余数据);
- 事务提交,完成数据更新。
五、功能优势与扩展说明
1. 核心优势
- 单多规格无缝切换:基于 FastAdmin 原生data-favisible实现,无需自定义 JS;
- 纯原生实现:基于 FastAdmin 内置组件,无需额外插件,兼容性强;
- 动态渲染:规格 / 值增删无刷新,自动生成组合,提升操作效率;
- 数据安全:后端事务控制,避免数据不一致,支持增量更新;
- 易维护:前后端分离,模板、交互、业务逻辑解耦。
2. 扩展方向
- 规格图片:在规格组合模板中添加图片上传字段,实现规格封面管理;
- 快速批量赋值:添加批量设置价格 / 库存功能,减少重复操作;
- 规格排序:增加规格名称 / 值的拖拽排序功能;
- 库存预警:后端添加库存低于阈值的校验与提醒。
六、总结
本文基于 FastAdmin 框架实现的商品多规格管理功能,覆盖了电商系统商品规格管理的全业务场景,完美支持单规格 / 多规格自由切换,从前端动态交互到后端数据持久化,形成了完整的解决方案。代码遵循 FastAdmin 开发规范,结构清晰、易于二次开发,可直接集成到各类电商、分销、零售类 FastAdmin 项目中,大幅降低多规格商品管理的开发成本。
核心亮点在于前端自动组合生成、单多规格无缝切换和后端增量更新,既保证了用户操作的便捷性,又保障了数据的准确性与性能,是 FastAdmin 电商开发的必备功能模块。