Browse Source

fix:ai测量素材 和商品优化

super-yimizi 19 hours ago
parent
commit
e4a18275b2

+ 5 - 4
addons/qcloudsms/config.php

@@ -54,7 +54,7 @@ return [
         'title' => '签名',
         'type' => 'string',
         'content' => [],
-        'value' => '量T裁衣',
+        'value' => '宜宾市三人行营销策划',
         'rule' => 'required',
         'msg' => '',
         'tip' => '',
@@ -98,13 +98,14 @@ return [
         'content' => [],
         'value' => [
             'register' => '',
-            'resetpwd' => '',
-            'changepwd' => '',
-            'changemobile' => '',
+            'resetpwd' => '2493760',
+            'changepwd' => '2493760',
+            'changemobile' => '2493762',
             'profile' => '',
             'notice' => '',
             'mobilelogin' => '2493757',
             'bind' => '',
+            'cancelaccount' => '2493769',
         ],
         'rule' => 'required',
         'msg' => '',

+ 233 - 0
application/admin/controller/general/AiMeasureConfig.php

@@ -0,0 +1,233 @@
+<?php
+
+namespace app\admin\controller\general;
+
+use app\common\controller\Backend;
+use app\common\model\Config as ConfigModel;
+use think\Cache;
+use think\Db;
+use think\Exception;
+use think\Validate;
+
+/**
+ * AI测量素材配置
+ *
+ * @icon   fa fa-camera
+ * @remark AI测量功能的素材配置管理,包含自拍模式和帮拍模式的配置项
+ */
+class AiMeasureConfig extends Backend
+{
+
+    /**
+     * @var \app\common\model\Config
+     */
+    protected $model = null;
+    protected $noNeedRight = ['check', 'selectpage'];
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new ConfigModel;
+    }
+
+    /**
+     * 查看
+     */
+    public function index()
+    {
+        // AI测量素材配置分组
+        $aiMeasureGroups = [
+            'ai_measure_selfie' => 'AI测量自拍模式',
+            'ai_measure_helper' => 'AI测量帮拍模式',
+            // 'ai_measure_common' => 'AI测量通用配置'
+        ];
+        
+        $siteList = [];
+        foreach ($aiMeasureGroups as $k => $v) {
+            $siteList[$k]['name'] = $k;
+            $siteList[$k]['title'] = $v;
+            $siteList[$k]['list'] = [];
+        }
+
+        // 获取AI测量相关的配置项
+        $configList = $this->model->where('group', 'in', array_keys($aiMeasureGroups))->select();
+        
+        foreach ($configList as $k => $v) {
+            if (!isset($siteList[$v['group']])) {
+                continue;
+            }
+            $value = $v->toArray();
+            $value['title'] = __($value['title']);
+            if (in_array($value['type'], ['select', 'selects', 'checkbox', 'radio'])) {
+                $value['value'] = explode(',', $value['value']);
+            }
+            $value['content'] = json_decode($value['content'], true);
+            $value['tip'] = htmlspecialchars($value['tip']);
+            $siteList[$v['group']]['list'][] = $value;
+        }
+        
+        $index = 0;
+        foreach ($siteList as $k => &$v) {
+            $v['active'] = !$index ? true : false;
+            $index++;
+        }
+        
+        $this->view->assign('siteList', $siteList);
+        $this->view->assign('typeList', ConfigModel::getTypeList());
+        $this->view->assign('groupList', $aiMeasureGroups);
+        return $this->view->fetch();
+    }
+
+    /**
+     * 添加
+     */
+    public function add()
+    {
+        if (!config('app_debug')) {
+            $this->error(__('Only work at development environment'));
+        }
+        if ($this->request->isPost()) {
+            $this->token();
+            $params = $this->request->post("row/a", [], 'trim');
+            if ($params) {
+                // 限制只能添加AI测量相关的配置组
+                if (!in_array($params['group'], ['ai_measure_selfie', 'ai_measure_helper', 'ai_measure_common'])) {
+                    $this->error(__('Invalid group parameter'));
+                }
+                
+                foreach ($params as $k => &$v) {
+                    $v = is_array($v) && $k !== 'setting' ? implode(',', $v) : $v;
+                }
+                if (in_array($params['type'], ['select', 'selects', 'checkbox', 'radio', 'array'])) {
+                    $params['content'] = json_encode(ConfigModel::decode($params['content']), JSON_UNESCAPED_UNICODE);
+                } else {
+                    $params['content'] = '';
+                }
+                try {
+                    $result = $this->model->create($params);
+                } catch (Exception $e) {
+                    $this->error($e->getMessage());
+                }
+                if ($result !== false) {
+                    try {
+                        ConfigModel::refreshFile();
+                    } catch (Exception $e) {
+                        $this->error($e->getMessage());
+                    }
+                    $this->success();
+                } else {
+                    $this->error($this->model->getError());
+                }
+            }
+            $this->error(__('Parameter %s can not be empty', ''));
+        }
+        return $this->view->fetch();
+    }
+
+    /**
+     * 编辑
+     * @param null $ids
+     */
+    public function edit($ids = null)
+    {
+        if ($this->request->isPost()) {
+            $this->token();
+            $row = $this->request->post("row/a", [], 'trim');
+            if ($row) {
+                $configList = [];
+                // 只处理AI测量相关的配置项
+                $aiMeasureConfigs = $this->model->where('group', 'in', ['ai_measure_selfie', 'ai_measure_helper', 'ai_measure_common'])->select();
+                
+                foreach ($aiMeasureConfigs as $v) {
+                    if (isset($row[$v['name']])) {
+                        $value = $row[$v['name']];
+                        if (is_array($value) && isset($value['field'])) {
+                            $value = json_encode(ConfigModel::getArrayData($value), JSON_UNESCAPED_UNICODE);
+                        } else {
+                            $value = is_array($value) ? implode(',', $value) : $value;
+                        }
+                        $v['value'] = $value;
+                        $configList[] = $v->toArray();
+                    }
+                }
+                try {
+                    $this->model->allowField(true)->saveAll($configList);
+                } catch (Exception $e) {
+                    $this->error($e->getMessage());
+                }
+                try {
+                    ConfigModel::refreshFile();
+                } catch (Exception $e) {
+                    $this->error($e->getMessage());
+                }
+                $this->success();
+            }
+            $this->error(__('Parameter %s can not be empty', ''));
+        }
+    }
+
+    /**
+     * 删除
+     * @param string $ids
+     */
+    public function del($ids = "")
+    {
+        if (!config('app_debug')) {
+            $this->error(__('Only work at development environment'));
+        }
+        $name = $this->request->post('name');
+        $config = ConfigModel::getByName($name);
+        if ($name && $config) {
+            // 检查是否为AI测量配置项
+            if (!in_array($config['group'], ['ai_measure_selfie', 'ai_measure_helper', 'ai_measure_common'])) {
+                $this->error(__('Can only delete AI measurement configuration items'));
+            }
+            
+            try {
+                $config->delete();
+                ConfigModel::refreshFile();
+            } catch (Exception $e) {
+                $this->error($e->getMessage());
+            }
+            $this->success();
+        } else {
+            $this->error(__('Invalid parameters'));
+        }
+    }
+
+    /**
+     * 检测配置项是否存在
+     * @internal
+     */
+    public function check()
+    {
+        $params = $this->request->post("row/a");
+        if ($params) {
+            $config = $this->model->get($params);
+            if (!$config) {
+                $this->success();
+            } else {
+                $this->error(__('Name already exist'));
+            }
+        } else {
+            $this->error(__('Invalid parameters'));
+        }
+    }
+
+    public function selectpage()
+    {
+        $id = $this->request->get("id/d");
+        $config = \app\common\model\Config::get($id);
+        if (!$config) {
+            $this->error(__('Invalid parameters'));
+        }
+        $setting = $config['setting'];
+        //自定义条件
+        $custom = isset($setting['conditions']) ? (array)json_decode($setting['conditions'], true) : [];
+        $custom = array_filter($custom);
+
+        $this->request->request(['showField' => $setting['field'], 'keyField' => $setting['primarykey'], 'custom' => $custom, 'searchField' => [$setting['field'], $setting['primarykey']]]);
+        $this->model = \think\Db::connect()->setTable($setting['table']);
+        return parent::selectpage();
+    }
+}

+ 342 - 143
application/admin/controller/shop/Goods.php

@@ -310,56 +310,60 @@ class Goods extends Backend
         }
     }
 
-    //处理商品SKU(统一处理单规格和多规格)
+    /**
+     * 处理商品SKU(统一处理单规格和多规格)
+     * @param array $params 商品参数
+     * @param int $goods_id 商品ID
+     * @throws \Exception
+     */
     protected function processGoodsSku($params, $goods_id)
     {
         // 统一通过skus和spec字段处理
-        if (isset($params['skus']) && isset($params['spec'])) {
-            $skus = (array)json_decode($params['skus'], true);
-            $spec = (array)json_decode($params['spec'], true);
-            
-            // 判断是单规格还是多规格
-            if (empty($spec) && count($skus) === 1) {
-                // 单规格处理
-                $this->addSingleSpecSku($skus[0], $goods_id);
-            } else {
-                // 多规格处理
-                $this->addMultiSpecSku($skus, $spec, $goods_id);
-            }
-        } else {
+        if (!isset($params['skus']) || !isset($params['spec'])) {
             throw new \Exception('SKU数据格式错误');
         }
+
+        $skus = (array)json_decode($params['skus'], true);
+        $spec = (array)json_decode($params['spec'], true);
+        
+        if (empty($skus)) {
+            throw new \Exception('SKU数据不能为空');
+        }
+        
+        // 判断是单规格还是多规格
+        if (empty($spec) && count($skus) === 1) {
+            // 单规格处理
+            $this->processSingleSpecSku($skus[0], $goods_id);
+        } else {
+            // 多规格处理
+            $this->processMultiSpecSku($skus, $spec, $goods_id);
+        }
     }
 
-    //添加单规格商品SKU
-    protected function addSingleSpecSku($skuData, $goods_id)
+    /**
+     * 处理单规格商品SKU
+     * @param array $skuData SKU数据
+     * @param int $goods_id 商品ID
+     */
+    protected function processSingleSpecSku($skuData, $goods_id)
     {
-        // 如果原来是多规格,先清理多余的SKU记录
+        // 验证单规格SKU数据
+        $this->validateSingleSpecSku($skuData);
+        
+        // 如果原来是多规格,先清理多余的SKU记录和规格关联
         $existingSkuCount = $this->sku_model->where('goods_id', $goods_id)->count();
         if ($existingSkuCount > 1) {
             // 从多规格改为单规格,删除所有旧记录
             $this->sku_model->where('goods_id', $goods_id)->delete();
+            // 删除规格关联数据
+            Db::name('shop_goods_sku_spec')->where('goods_id', $goods_id)->delete();
         }
         
         // 查询现有的SKU记录
         $existingSku = $this->sku_model->where('goods_id', $goods_id)->find();
         
         // 创建单规格SKU数据
-        $newSkuData = [
-            'goods_id'       => $goods_id,
-            'spec_value_ids' => '', // 单规格无规格值ID
-            'sku_attr'       => '', // 单规格无规格属性
-            'sku_sn'         => $skuData['sku_sn'] ?? '',
-            'image'          => $skuData['image'] ?? '',
-            'price'          => $skuData['price'] ?? 0,
-            'lineation_price'=> $skuData['lineation_price'] ?? 0,
-            'cost_price'     => $skuData['cost_price'] ?? 0,
-            'weight'         => $skuData['weight'] ?? 0,
-            'volume'         => $skuData['volume'] ?? 0,
-            'stocks'         => $skuData['stocks'] ?? 0,
-            'status'         => 1,
-            'is_default'     => 1, // 单规格默认为默认SKU
-        ];
+        $newSkuData = $this->buildSingleSkuData($skuData, $goods_id);
         
         $defaultSkuId = 0;
         
@@ -376,156 +380,351 @@ class Goods extends Backend
         }
         
         // 更新商品主表信息
+        $this->updateGoodsForSingleSpec($skuData, $goods_id, $defaultSkuId);
+    }
+    
+    /**
+     * 验证单规格SKU数据
+     * @param array $skuData
+     * @throws \Exception
+     */
+    protected function validateSingleSpecSku($skuData)
+    {
+        if (!isset($skuData['price']) || floatval($skuData['price']) <= 0) {
+            throw new \Exception('单规格商品销售价必须大于0');
+        }
+        
+        if (!isset($skuData['stocks']) || intval($skuData['stocks']) <= 0) {
+            throw new \Exception('单规格商品库存必须大于0');
+        }
+    }
+    
+    /**
+     * 构建单规格SKU数据
+     * @param array $skuData
+     * @param int $goods_id
+     * @return array
+     */
+    protected function buildSingleSkuData($skuData, $goods_id)
+    {
+        return [
+            'goods_id'       => $goods_id,
+            'spec_value_ids' => '', // 单规格无规格值ID
+            'sku_attr'       => '', // 单规格无规格属性
+            'sku_sn'         => $skuData['sku_sn'] ?? '',
+            'image'          => $skuData['image'] ?? '',
+            'price'          => floatval($skuData['price'] ?? 0),
+            'lineation_price'=> floatval($skuData['lineation_price'] ?? 0),
+            'cost_price'     => floatval($skuData['cost_price'] ?? 0),
+            'weight'         => floatval($skuData['weight'] ?? 0),
+            'volume'         => floatval($skuData['volume'] ?? 0),
+            'stocks'         => intval($skuData['stocks'] ?? 0),
+            'status'         => 1,
+            'is_default'     => 1, // 单规格默认为默认SKU
+        ];
+    }
+    
+    /**
+     * 更新商品主表信息(单规格)
+     * @param array $skuData
+     * @param int $goods_id
+     * @param int $defaultSkuId
+     */
+    protected function updateGoodsForSingleSpec($skuData, $goods_id, $defaultSkuId)
+    {
         $updateData = [
             'spec_type'          => 0, // 单规格
-            'stocks'             => $skuData['stocks'] ?? 0,
-            'min_price'          => $skuData['price'] ?? 0,
-            'max_price'          => $skuData['price'] ?? 0,
-            'price'              => $skuData['price'] ?? 0,
-            'min_lineation_price'=> $skuData['lineation_price'] ?? 0,
-            'max_lineation_price'=> $skuData['lineation_price'] ?? 0,
-            'lineation_price'    => $skuData['lineation_price'] ?? 0,
+            'stocks'             => intval($skuData['stocks'] ?? 0),
+            'min_price'          => floatval($skuData['price'] ?? 0),
+            'max_price'          => floatval($skuData['price'] ?? 0),
+            'price'              => floatval($skuData['price'] ?? 0),
+            'min_lineation_price'=> floatval($skuData['lineation_price'] ?? 0),
+            'max_lineation_price'=> floatval($skuData['lineation_price'] ?? 0),
+            'lineation_price'    => floatval($skuData['lineation_price'] ?? 0),
             'default_sku_id'     => $defaultSkuId,
         ];
         
         $this->model->where('id', $goods_id)->update($updateData);
     }
 
-    //添加商品多规格
-    protected function addMultiSpecSku($skus, $spec, $goods_id)
+    /**
+     * 处理多规格商品SKU
+     * @param array $skus SKU数据数组
+     * @param array $spec 规格数据数组
+     * @param int $goods_id 商品ID
+     */
+    protected function processMultiSpecSku($skus, $spec, $goods_id)
     {
-        //属性入库,传递规格类型信息
+        // 验证多规格数据
+        $this->validateMultiSpecSku($skus, $spec);
+        
+        // 规格属性入库,传递规格类型信息
         $specList = Spec::push($spec);
         $newSpec = GoodsSkuSpec::push($specList, $goods_id, $spec);
-        //匹配属性
-        $list = $this->sku_model->where('goods_id', $goods_id)->select();
-        $newData = [];
-        $stocks = 0;
+        
+        // 处理SKU数据
+        $this->processSkuData($skus, $newSpec, $spec, $goods_id);
+        
+        // 处理默认SKU
+        $defaultSkuId = $this->processDefaultSku($skus, $goods_id);
+        
+        // 更新商品主表信息
+        $this->updateGoodsForMultiSpec($skus, $goods_id, $defaultSkuId);
+        
+        // 清理无用的SKU记录
+        $this->cleanupUnusedSkus($goods_id, count($skus));
+    }
+    
+    /**
+     * 验证多规格SKU数据
+     * @param array $skus
+     * @param array $spec
+     * @throws \Exception
+     */
+    protected function validateMultiSpecSku($skus, $spec)
+    {
+        if (empty($spec)) {
+            throw new \Exception('多规格商品必须设置规格信息');
+        }
+        
+        // 验证是否有有效的SKU
+        $hasValidSku = false;
+        foreach ($skus as $sku) {
+            if (isset($sku['status']) && ($sku['status'] == 1 || $sku['status'] === '1')) {
+                $hasValidSku = true;
+                
+                if (!isset($sku['price']) || floatval($sku['price']) <= 0) {
+                    throw new \Exception('SKU销售价必须大于0');
+                }
+                
+                if (!isset($sku['stocks']) || intval($sku['stocks']) <= 0) {
+                    throw new \Exception('SKU库存必须大于0');
+                }
+            }
+        }
+        
+        if (!$hasValidSku) {
+            throw new \Exception('多规格商品至少需要一个有效的SKU');
+        }
+    }
+    
+    /**
+     * 处理SKU数据
+     * @param array $skus
+     * @param array $newSpec
+     * @param array $spec
+     * @param int $goods_id
+     */
+    protected function processSkuData($skus, $newSpec, $spec, $goods_id)
+    {
+        $existingSkus = $this->sku_model->where('goods_id', $goods_id)->select();
+        $newSkuData = [];
+        
         foreach ($skus as $k => $sk) {
             $newSkuId = $this->getSkuId($sk['skus'], $newSpec, $spec);
-            
-            // 生成SKU规格属性JSON字符串
             $skuAttr = $this->generateSkuAttr($sk['skus'], $newSpec, $spec);
             
-            $newSkuData = [
-                'goods_id'     => $goods_id,
-                'spec_value_ids' => $newSkuId,
-                'sku_attr'     => $skuAttr,
-                'sku_sn'       => $sk['sku_sn'] ?? '',
-                'image'        => $sk['image'] ?? '',
-                'price'        => $sk['price'] ?? 0,
-                'lineation_price' => $sk['lineation_price'] ?? 0,
-                'cost_price'   => $sk['cost_price'] ?? 0,
-                'weight'       => $sk['weight'] ?? 0,
-                'volume'       => $sk['volume'] ?? 0,
-                'stocks'       => $sk['stocks'] ?? 0,
-                'status'       => isset($sk['status']) ? intval($sk['status']) : 1,
-                'is_default'   => isset($sk['is_default']) ? intval($sk['is_default']) : 0,
-            ];
-            if (isset($list[$k])) {
-                $row = $list[$k];
-                $oldSkuIdsArr = explode(',', $row['spec_value_ids']);
-                sort($oldSkuIdsArr);
-                $oldSkuId = implode(',', $oldSkuIdsArr);
-
+            $skuData = $this->buildMultiSkuData($sk, $goods_id, $newSkuId, $skuAttr);
+            
+            if (isset($existingSkus[$k])) {
+                $existingSku = $existingSkus[$k];
+                $oldSkuId = $this->normalizeSkuId($existingSku['spec_value_ids']);
+                
                 if ($oldSkuId == $newSkuId) {
-                    //相等的更新
-                    $row->save($newSkuData);
+                    // 规格匹配,更新数据
+                    $skuData['sales'] = $existingSku->sales; // 保持原有销量
+                    $existingSku->save($skuData);
                 } else {
-                    //不等的
-                    $row->save(array_merge($newSkuData, ['sales' => 0]));
+                    // 规格不匹配,重置销量
+                    $skuData['sales'] = 0;
+                    $existingSku->save($skuData);
                 }
-                unset($list[$k]);
-            } else { //多余的
-                $newData[] = array_merge($newSkuData, ['sales' => 0]);
+                unset($existingSkus[$k]);
+            } else {
+                // 新增SKU
+                $skuData['sales'] = 0;
+                $newSkuData[] = $skuData;
             }
-            $stocks = bcadd($stocks, $sk['stocks'] ?? 0);
-        }
-        if (!empty($newData)) {
-            $this->sku_model->saveAll($newData);
         }
         
-        // 处理默认SKU
+        if (!empty($newSkuData)) {
+            $this->sku_model->saveAll($newSkuData);
+        }
+    }
+    
+    /**
+     * 构建多规格SKU数据
+     * @param array $skuData
+     * @param int $goods_id
+     * @param string $specValueIds
+     * @param string $skuAttr
+     * @return array
+     */
+    protected function buildMultiSkuData($skuData, $goods_id, $specValueIds, $skuAttr)
+    {
+        return [
+            'goods_id'       => $goods_id,
+            'spec_value_ids' => $specValueIds,
+            'sku_attr'       => $skuAttr,
+            'sku_sn'         => $skuData['sku_sn'] ?? '',
+            'image'          => $skuData['image'] ?? '',
+            'price'          => floatval($skuData['price'] ?? 0),
+            'lineation_price'=> floatval($skuData['lineation_price'] ?? 0),
+            'cost_price'     => floatval($skuData['cost_price'] ?? 0),
+            'weight'         => floatval($skuData['weight'] ?? 0),
+            'volume'         => floatval($skuData['volume'] ?? 0),
+            'stocks'         => intval($skuData['stocks'] ?? 0),
+            'status'         => isset($skuData['status']) ? intval($skuData['status']) : 1,
+            'is_default'     => isset($skuData['is_default']) ? intval($skuData['is_default']) : 0,
+        ];
+    }
+    
+    /**
+     * 规范化SKU ID字符串
+     * @param string $skuIds
+     * @return string
+     */
+    protected function normalizeSkuId($skuIds)
+    {
+        $idsArray = explode(',', $skuIds);
+        sort($idsArray);
+        return implode(',', $idsArray);
+    }
+    
+    /**
+     * 处理默认SKU
+     * @param array $skus
+     * @param int $goods_id
+     * @return int
+     */
+    protected function processDefaultSku($skus, $goods_id)
+    {
         $defaultSkuId = 0;
         $hasDefault = false;
-        foreach ($skus as $k => $sk) {
-            if (isset($sk['is_default']) && $sk['is_default']) {
+        
+        // 检查是否有明确设置的默认SKU
+        foreach ($skus as $sku) {
+            if (isset($sku['is_default']) && $sku['is_default']) {
                 $hasDefault = true;
                 break;
             }
         }
         
-        // 如果没有明确设置默认SKU,则使用第一个SKU作为默认
         if (!$hasDefault && !empty($skus)) {
-            $firstSku = $this->sku_model->where('goods_id', $goods_id)->order('id', 'asc')->find();
-            if ($firstSku) {
-                $firstSku->save(['is_default' => 1]);
-                $defaultSkuId = $firstSku->id;
+            // 如果没有明确设置默认SKU,则使用第一个有效SKU作为默认
+            $firstValidSku = $this->sku_model->where('goods_id', $goods_id)
+                ->where('status', 1)
+                ->order('id', 'asc')
+                ->find();
+            if ($firstValidSku) {
+                $firstValidSku->save(['is_default' => 1]);
+                $defaultSkuId = $firstValidSku->id;
             }
         } else {
             // 查找默认SKU的ID
-            $defaultSku = $this->sku_model->where('goods_id', $goods_id)->where('is_default', 1)->find();
+            $defaultSku = $this->sku_model->where('goods_id', $goods_id)
+                ->where('is_default', 1)
+                ->find();
             if ($defaultSku) {
                 $defaultSkuId = $defaultSku->id;
             }
         }
         
-        //更新库存和默认SKU,计算价格范围
-        if (!empty($skus)) {
-            $prices = array_column($skus, 'price');
-            $lineationPrices = array_column($skus, 'lineation_price');
-            
-            // 使用bc函数计算平均价格,保证精度
-            $totalPrice = '0';
-            $validPriceCount = 0;
-            foreach ($prices as $price) {
-                if ($price > 0) {
-                    $totalPrice = bcadd($totalPrice, (string)$price, 2);
-                    $validPriceCount++;
-                }
-            }
-            
-            // 计算平均价格
-            $avgPrice = $validPriceCount > 0 ? bcdiv($totalPrice, (string)$validPriceCount, 2) : '0.00';
-            
-            // 使用bc函数计算平均划线价格
-            $totalLineationPrice = '0';
-            $validLineationPriceCount = 0;
-            foreach ($lineationPrices as $lineationPrice) {
-                if ($lineationPrice > 0) {
-                    $totalLineationPrice = bcadd($totalLineationPrice, (string)$lineationPrice, 2);
-                    $validLineationPriceCount++;
-                }
-            }
-            
-            // 计算平均划线价格
-            $avgLineationPrice = $validLineationPriceCount > 0 ? bcdiv($totalLineationPrice, (string)$validLineationPriceCount, 2) : '0.00';
-            
-            // 过滤掉0值的划线价格来计算最大最小值
-            $validLineationPrices = array_filter($lineationPrices, function($price) {
-                return $price > 0;
-            });
-            
-            $updateData = [
-                'stocks' => $stocks, 
-                'spec_type' => 1,
-                'min_price' => min($prices),
-                'max_price' => max($prices),
-                'price' => $avgPrice,
-                'min_lineation_price' => !empty($validLineationPrices) ? min($validLineationPrices) : '0.00',
-                'max_lineation_price' => !empty($validLineationPrices) ? max($validLineationPrices) : '0.00',
-                'lineation_price' => $avgLineationPrice
-            ];
-            if ($defaultSkuId > 0) {
-                $updateData['default_sku_id'] = $defaultSkuId;
-            }
-            $this->model->where('id', $goods_id)->update($updateData);
-        } else {
+        return $defaultSkuId;
+    }
+    
+    /**
+     * 更新商品主表信息(多规格)
+     * @param array $skus
+     * @param int $goods_id
+     * @param int $defaultSkuId
+     */
+    protected function updateGoodsForMultiSpec($skus, $goods_id, $defaultSkuId)
+    {
+        if (empty($skus)) {
             $this->model->where('id', $goods_id)->update(['spec_type' => 0]);
+            return;
         }
-        //原来多的删除
-        foreach ($list as $it) {
-            $it->delete();
+        
+        // 计算价格范围和库存
+        $priceStats = $this->calculatePriceStats($skus);
+        $totalStocks = $this->calculateTotalStocks($skus);
+        
+        $updateData = [
+            'stocks'             => $totalStocks,
+            'spec_type'          => 1,
+            'min_price'          => $priceStats['min_price'],
+            'max_price'          => $priceStats['max_price'],
+            'price'              => $priceStats['avg_price'],
+            'min_lineation_price'=> $priceStats['min_lineation_price'],
+            'max_lineation_price'=> $priceStats['max_lineation_price'],
+            'lineation_price'    => $priceStats['avg_lineation_price'],
+        ];
+        
+        if ($defaultSkuId > 0) {
+            $updateData['default_sku_id'] = $defaultSkuId;
+        }
+        
+        $this->model->where('id', $goods_id)->update($updateData);
+    }
+    
+    /**
+     * 计算价格统计信息
+     * @param array $skus
+     * @return array
+     */
+    protected function calculatePriceStats($skus)
+    {
+        $prices = array_column($skus, 'price');
+        $lineationPrices = array_filter(array_column($skus, 'lineation_price'), function($price) {
+            return $price > 0;
+        });
+        
+        // 计算平均价格
+        $validPrices = array_filter($prices, function($price) { return $price > 0; });
+        $avgPrice = !empty($validPrices) ? array_sum($validPrices) / count($validPrices) : 0;
+        
+        // 计算平均划线价格
+        $avgLineationPrice = !empty($lineationPrices) ? array_sum($lineationPrices) / count($lineationPrices) : 0;
+        
+        return [
+            'min_price' => !empty($prices) ? min($prices) : 0,
+            'max_price' => !empty($prices) ? max($prices) : 0,
+            'avg_price' => round($avgPrice, 2),
+            'min_lineation_price' => !empty($lineationPrices) ? min($lineationPrices) : 0,
+            'max_lineation_price' => !empty($lineationPrices) ? max($lineationPrices) : 0,
+            'avg_lineation_price' => round($avgLineationPrice, 2),
+        ];
+    }
+    
+    /**
+     * 计算总库存
+     * @param array $skus
+     * @return int
+     */
+    protected function calculateTotalStocks($skus)
+    {
+        $totalStocks = 0;
+        foreach ($skus as $sku) {
+            $totalStocks += intval($sku['stocks'] ?? 0);
+        }
+        return $totalStocks;
+    }
+    
+    /**
+     * 清理无用的SKU记录
+     * @param int $goods_id
+     * @param int $currentSkuCount
+     */
+    protected function cleanupUnusedSkus($goods_id, $currentSkuCount)
+    {
+        // 删除多余的SKU记录(保留当前SKU数量)
+        $allSkus = $this->sku_model->where('goods_id', $goods_id)->order('id', 'asc')->select();
+        if (count($allSkus) > $currentSkuCount) {
+            $skusToDelete = array_slice($allSkus->toArray(), $currentSkuCount);
+            foreach ($skusToDelete as $sku) {
+                $this->sku_model->where('id', $sku['id'])->delete();
+            }
         }
     }
 

+ 163 - 59
application/admin/validate/shop/Goods.php

@@ -10,65 +10,80 @@ class Goods extends Validate
      * 验证规则
      */
     protected $rule = [
-        'goods_sn'          => 'require|max:50',
+        // 基础信息
+        'type'              => 'require|in:1,2,3,4',
+        'goods_sn'          => 'require|max:50|unique:shop_goods,goods_sn',
         'title'             => 'require|max:200',
-        'sub_title'         => 'max:200',
+        'subtitle'          => 'max:200',
+        'category_id'       => 'require|integer|gt:0',
         'category_ids'      => 'require',
+        'brand_id'          => 'integer|egt:0',
         'image'             => 'require',
         'images'            => 'require',
-        'weigh'             => 'require|integer|egt:0',
-        'status'            => 'require|in:0,1,2,3',
-        'delivery_type'     => 'require|in:EXPRESS,PICKUP,VIRTUAL',
-        'express_type'      => 'requireIf:delivery_type,EXPRESS|in:1,2,3',
-        'express_freight'   => 'requireIf:express_type,2|float|egt:0',
-        'express_template_id' => 'requireIf:express_type,3|integer|gt:0',
+        'weigh'             => 'integer|egt:0',
+        
+        // 价格库存
         'spec_type'         => 'require|in:0,1',
-        'price'             => 'requireIf:spec_type,0|float|gt:0|checkSinglePrice',
-
+        'price'             => 'requireIf:spec_type,0|float|gt:0',
         'lineation_price'   => 'float|egt:0',
         'cost_price'        => 'float|egt:0',
         'stocks'            => 'requireIf:spec_type,0|integer|gt:0',
         'weight'            => 'float|egt:0',
         'volume'            => 'float|egt:0',
-        'scheduled_online_time'  => 'requireIf:status,3',
+        'sku_sn'            => 'max:100',
+        'single_image'      => '',
+        
+        // 配送设置
+        'delivery_type'     => 'require|in:EXPRESS,PICKUP,VIRTUAL',
+        'express_type'      => 'requireIf:delivery_type,EXPRESS|in:1,2,3',
+        'express_price'     => 'requireIf:express_type,2|float|egt:0',
+        'express_template_id' => 'requireIf:express_type,3|integer|gt:0',
+        
+        // 销售设置
+        'online_type'       => 'require|in:1,2,3',
+        'stock_show_type'   => 'require|in:1,2',
+        'sales_show_type'   => 'require|in:1,2',
+        'scheduled_online_time'  => 'requireIf:online_type,3',
         'scheduled_offline_time' => 'requireIf:is_auto_offline,1',
+        'is_auto_offline'   => 'in:0,1',
+        'keywords'          => 'max:255',
+        'description'       => 'max:500',
+        
+        // 多规格相关
         'spec_data'         => 'checkMultiSpec',
+        'sku_data'          => 'checkSkuData',
     ];
     
     /**
      * 提示消息
      */
     protected $message = [
+        // 基础信息
+        'type.require'              => '请选择商品类型',
+        'type.in'                   => '商品类型值不正确',
         'goods_sn.require'          => '商品编码不能为空',
         'goods_sn.max'              => '商品编码不能超过50个字符',
+        'goods_sn.unique'           => '商品编码已存在',
         'title.require'             => '商品标题不能为空',
         'title.max'                 => '商品标题不能超过200个字符',
-        // 'sub_title.require'         => '商品副标题不能为空',
-        'sub_title.max'             => '商品副标题不能超过200个字符',
+        'subtitle.max'              => '商品副标题不能超过200个字符',
+        'category_id.require'       => '请选择商品分类',
+        'category_id.integer'       => '商品分类ID必须是整数',
+        'category_id.gt'            => '请选择有效的商品分类',
         'category_ids.require'      => '请选择商品分类',
+        'brand_id.integer'          => '品牌ID必须是整数',
+        'brand_id.egt'              => '品牌ID必须大于等于0',
         'image.require'             => '请上传商品主图',
         'images.require'            => '请上传商品轮播图',
-        'weigh.require'             => '排序值不能为空',
         'weigh.integer'             => '排序值必须是整数',
         'weigh.egt'                 => '排序值必须大于等于0',
-        'status.require'            => '请选择商品状态',
-        'status.in'                 => '商品状态值不正确',
-        'delivery_type.require'     => '请选择配送方式',
-        'delivery_type.in'          => '配送方式值不正确',
-        'express_type.requireIf'    => '选择快递配送时必须设置运费方式',
-        'express_type.in'           => '运费方式值不正确',
-        'express_freight.requireIf' => '选择统一运费时必须设置运费金额',
-        'express_freight.float'     => '运费金额必须是数字',
-        'express_freight.egt'       => '运费金额必须大于等于0',
-        'express_template_id.requireIf' => '选择运费模板时必须选择模板',
-        'express_template_id.integer'   => '运费模板ID必须是整数',
-        'express_template_id.gt'        => '运费模板ID必须大于0',
+        
+        // 价格库存
         'spec_type.require'         => '请选择规格类型',
         'spec_type.in'              => '规格类型值不正确',
         'price.requireIf'           => '单规格商品必须设置销售价',
         'price.float'               => '销售价必须是数字',
         'price.gt'                  => '销售价必须大于0',
-
         'lineation_price.float'     => '划线价必须是数字',
         'lineation_price.egt'       => '划线价必须大于等于0',
         'cost_price.float'          => '成本价必须是数字',
@@ -80,16 +95,87 @@ class Goods extends Validate
         'weight.egt'                => '重量必须大于等于0',
         'volume.float'              => '体积必须是数字',
         'volume.egt'                => '体积必须大于等于0',
+        'sku_sn.max'                => 'SKU编码不能超过100个字符',
+        
+        // 配送设置
+        'delivery_type.require'     => '请选择配送方式',
+        'delivery_type.in'          => '配送方式值不正确',
+        'express_type.requireIf'    => '选择快递配送时必须设置运费方式',
+        'express_type.in'           => '运费方式值不正确',
+        'express_price.requireIf'   => '选择统一运费时必须设置运费金额',
+        'express_price.float'       => '运费金额必须是数字',
+        'express_price.egt'         => '运费金额必须大于等于0',
+        'express_template_id.requireIf' => '选择运费模板时必须选择模板',
+        'express_template_id.integer'   => '运费模板ID必须是整数',
+        'express_template_id.gt'        => '运费模板ID必须大于0',
+        
+        // 销售设置
+        'online_type.require'       => '请选择上架方式',
+        'online_type.in'            => '上架方式值不正确',
+        'stock_show_type.require'   => '请选择库存显示方式',
+        'stock_show_type.in'        => '库存显示方式值不正确',
+        'sales_show_type.require'   => '请选择销量显示方式',
+        'sales_show_type.in'        => '销量显示方式值不正确',
         'scheduled_online_time.requireIf' => '定时上架必须设置上架时间',
         'scheduled_offline_time.requireIf' => '自动下架必须设置下架时间',
+        'is_auto_offline.in'        => '自动下架设置值不正确',
+        'keywords.max'              => '关键词不能超过255个字符',
+        'description.max'           => '描述不能超过500个字符',
     ];
     
     /**
      * 验证场景
      */
     protected $scene = [
-        'add'  => [],
-        'edit' => [],
+        'add'  => [
+            'type', 'goods_sn', 'title', 'subtitle', 'category_id', 'category_ids', 'brand_id',
+            'image', 'images', 'weigh', 'spec_type', 'price', 'lineation_price', 'cost_price',
+            'stocks', 'weight', 'volume', 'sku_sn', 'single_image', 'delivery_type',
+            'express_type', 'express_price', 'express_template_id', 'online_type',
+            'stock_show_type', 'sales_show_type', 'scheduled_online_time', 'scheduled_offline_time',
+            'is_auto_offline', 'keywords', 'description', 'spec_data', 'sku_data'
+        ],
+        'edit' => [
+            'type', 'goods_sn', 'title', 'subtitle', 'category_id', 'category_ids', 'brand_id',
+            'image', 'images', 'weigh', 'spec_type', 'price', 'lineation_price', 'cost_price',
+            'stocks', 'weight', 'volume', 'sku_sn', 'single_image', 'delivery_type',
+            'express_type', 'express_price', 'express_template_id', 'online_type',
+            'stock_show_type', 'sales_show_type', 'scheduled_online_time', 'scheduled_offline_time',
+            'is_auto_offline', 'keywords', 'description', 'spec_data', 'sku_data'
+        ],
+    ];
+    
+    /**
+     * 字段描述
+     */
+    protected $field = [
+        'type'              => '商品类型',
+        'goods_sn'          => '商品编码',
+        'title'             => '商品标题',
+        'subtitle'          => '商品副标题',
+        'category_id'       => '商品分类',
+        'category_ids'      => '商品分类',
+        'brand_id'          => '品牌',
+        'image'             => '商品主图',
+        'images'            => '商品轮播图',
+        'spec_type'         => '规格类型',
+        'price'             => '销售价',
+        'lineation_price'   => '划线价',
+        'cost_price'        => '成本价',
+        'stocks'            => '库存',
+        'weight'            => '重量',
+        'volume'            => '体积',
+        'delivery_type'     => '配送方式',
+        'express_type'      => '运费方式',
+        'express_price'     => '运费金额',
+        'express_template_id' => '运费模板',
+        'online_type'       => '上架方式',
+        'stock_show_type'   => '库存显示方式',
+        'sales_show_type'   => '销量显示方式',
+        'scheduled_online_time'  => '定时上架时间',
+        'scheduled_offline_time' => '定时下架时间',
+        'keywords'          => '关键词',
+        'description'       => '描述',
     ];
     
     /**
@@ -103,41 +189,27 @@ class Goods extends Validate
                 return '多规格商品必须设置规格信息';
             }
             
-            if (empty($data['sku_data']) || !is_array($data['sku_data'])) {
-                return '多规格商品必须设置SKU信息';
-            }
-            
             // 验证规格名称不能为空
-            foreach ($data['spec_data'] as $spec) {
+            foreach ($data['spec_data'] as $key => $spec) {
                 if (empty($spec['name'])) {
-                    return '规格名称不能为空';
+                    return "规格#{$key}名称不能为空";
                 }
                 
                 if (empty($spec['value']) || !is_array($spec['value'])) {
-                    return '规格值不能为空';
+                    return "规格「{$spec['name']}」的规格值不能为空";
                 }
                 
-                foreach ($spec['value'] as $value) {
-                    if (empty($value['name'])) {
-                        return '规格值名称不能为空';
+                foreach ($spec['value'] as $idx => $specValue) {
+                    if (empty($specValue['name'])) {
+                        return "规格「{$spec['name']}」的第" . ($idx + 1) . "个规格值名称不能为空";
+                    }
+                    
+                    // 检查定制规格是否上传了图片
+                    if (isset($spec['type']) && $spec['type'] === 'custom') {
+                        if (empty($specValue['image'])) {
+                            return "定制规格「{$spec['name']}」的规格值「{$specValue['name']}」必须上传图片";
+                        }
                     }
-                }
-            }
-            
-            // 验证SKU数据
-            foreach ($data['sku_data'] as $sku) {
-                if (empty($sku['skus']) || !is_array($sku['skus'])) {
-                    return 'SKU规格属性不能为空';
-                }
-                
-                if (!isset($sku['price']) || $sku['price'] <= 0) {
-                    return 'SKU销售价必须大于0';
-                }
-                
-
-                
-                if (!isset($sku['stocks']) || $sku['stocks'] <= 0) {
-                    return 'SKU库存必须大于0';
                 }
             }
         }
@@ -146,11 +218,43 @@ class Goods extends Validate
     }
     
     /**
-     * 自定义验证规则:单规格商品价格验证
+     * 自定义验证规则:SKU数据验证
      */
-    protected function checkSinglePrice($value, $rule, $data)
+    protected function checkSkuData($value, $rule, $data)
     {
-        // 单规格商品价格验证已简化,只验证价格大于0
+        if (isset($data['spec_type']) && $data['spec_type'] == '1') {
+            // 多规格商品需要验证SKU数据
+            if (empty($data['sku_data']) || !is_array($data['sku_data'])) {
+                return '多规格商品必须设置SKU信息';
+            }
+            
+            $hasValidSku = false;
+            
+            // 验证SKU数据
+            foreach ($data['sku_data'] as $index => $sku) {
+                // 检查显示状态的SKU
+                if (isset($sku['status']) && ($sku['status'] == 1 || $sku['status'] === '1' || $sku['status'] === true)) {
+                    $hasValidSku = true;
+                    
+                    if (empty($sku['skus']) || !is_array($sku['skus'])) {
+                        return "第" . ($index + 1) . "个SKU的规格属性不能为空";
+                    }
+                    
+                    if (!isset($sku['price']) || floatval($sku['price']) <= 0) {
+                        return "第" . ($index + 1) . "个SKU的销售价必须大于0";
+                    }
+                    
+                    if (!isset($sku['stocks']) || intval($sku['stocks']) <= 0) {
+                        return "第" . ($index + 1) . "个SKU的库存必须大于0";
+                    }
+                }
+            }
+            
+            if (!$hasValidSku) {
+                return '多规格商品至少需要一个有效的SKU';
+            }
+        }
+        
         return true;
     }
 }

+ 104 - 0
application/admin/view/general/ai_measure_config/add.html

@@ -0,0 +1,104 @@
+<form id="add-form" class="form-horizontal" role="form" data-toggle="validator" method="POST" action="{:url('general.ai_measure_config/add')}">
+    {:token()}
+    
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Group')}:</label>
+        <div class="col-xs-12 col-sm-4">
+            <select name="row[group]" class="form-control selectpicker">
+                <option value="ai_measure_selfie">AI测量自拍模式</option>
+                <option value="ai_measure_helper">AI测量帮拍模式</option>
+                <option value="ai_measure_common">AI测量通用配置</option>
+            </select>
+        </div>
+    </div>
+    
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Type')}:</label>
+        <div class="col-xs-12 col-sm-4">
+            <select name="row[type]" id="c-type" class="form-control selectpicker">
+                <option value="string">字符串</option>
+                <option value="text">多行文本</option>
+                <option value="editor">富文本编辑器</option>
+                <option value="number">数字</option>
+                <option value="switch">开关</option>
+                <option value="radio">单选框</option>
+                <option value="checkbox">复选框</option>
+                <option value="select">下拉选择</option>
+                <option value="image">图片</option>
+                <option value="array">数组</option>
+            </select>
+        </div>
+    </div>
+    
+    <div class="form-group">
+        <label for="name" class="control-label col-xs-12 col-sm-2">{:__('Name')}:</label>
+        <div class="col-xs-12 col-sm-4">
+            <input type="text" class="form-control" id="name" name="row[name]" value="" data-rule="required; length(3~30); remote(general/ai_measure_config/check)" placeholder="ai_measure_selfie_xxx"/>
+            <span class="help-block">建议以 ai_measure_ 开头</span>
+        </div>
+    </div>
+    
+    <div class="form-group">
+        <label for="title" class="control-label col-xs-12 col-sm-2">{:__('Title')}:</label>
+        <div class="col-xs-12 col-sm-4">
+            <input type="text" class="form-control" id="title" name="row[title]" value="" data-rule="required" placeholder="配置项的中文标题"/>
+        </div>
+    </div>
+    
+    <div class="form-group">
+        <label for="value" class="control-label col-xs-12 col-sm-2">{:__('Value')}:</label>
+        <div class="col-xs-12 col-sm-4">
+            <input type="text" class="form-control" id="value" name="row[value]" value="" data-rule="" placeholder="默认值"/>
+        </div>
+    </div>
+    
+    <div class="form-group hide" id="add-content-container">
+        <label for="content" class="control-label col-xs-12 col-sm-2">{:__('Content')}:</label>
+        <div class="col-xs-12 col-sm-4">
+            <textarea name="row[content]" id="content" cols="30" rows="5" class="form-control" data-rule="required(content)" placeholder="value1|title1&#10;value2|title2">value1|title1
+value2|title2</textarea>
+            <span class="help-block">用于单选框、复选框、下拉选择的选项数据</span>
+        </div>
+    </div>
+    
+    <div class="form-group">
+        <label for="tip" class="control-label col-xs-12 col-sm-2">{:__('Tip')}:</label>
+        <div class="col-xs-12 col-sm-4">
+            <input type="text" class="form-control" id="tip" name="row[tip]" value="" data-rule="" placeholder="配置项的说明提示"/>
+        </div>
+    </div>
+    
+    <div class="form-group">
+        <label for="rule" class="control-label col-xs-12 col-sm-2">{:__('Rule')}:</label>
+        <div class="col-xs-12 col-sm-4">
+            <input type="text" class="form-control" id="rule" name="row[rule]" value="" data-tip="{:__('Rule tips')}" placeholder="验证规则,如:required;number;min:1"/>
+        </div>
+    </div>
+    
+    <div class="form-group">
+        <label for="visible" class="control-label col-xs-12 col-sm-2">{:__('Visible condition')}:</label>
+        <div class="col-xs-12 col-sm-4">
+            <input type="text" class="form-control" id="visible" name="row[visible]" value="" data-rule="" placeholder="可见条件,留空表示始终可见"/>
+        </div>
+    </div>
+    
+    <div class="form-group">
+        <label for="extend" class="control-label col-xs-12 col-sm-2">{:__('Extend')}:</label>
+        <div class="col-xs-12 col-sm-4">
+            <textarea name="row[extend]" id="extend" cols="30" rows="5" class="form-control" data-tip="{:__('Extend tips')}" data-rule="required(extend)" data-msg-extend="当类型为自定义时,扩展属性不能为空" placeholder="扩展属性,一般留空"></textarea>
+        </div>
+    </div>
+    
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2"></label>
+        <div class="col-xs-12 col-sm-4">
+            {if !$Think.config.app_debug}
+            <button type="button" class="btn btn-primary disabled">{:__('Only work at development environment')}</button>
+            {else/}
+            <button type="submit" class="btn btn-primary btn-embossed">{:__('OK')}</button>
+            <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
+            {/if}
+        </div>
+    </div>
+
+</form>

+ 312 - 0
application/admin/view/general/ai_measure_config/index.html

@@ -0,0 +1,312 @@
+<style type="text/css">
+    @media (max-width: 375px) {
+        .edit-form tr td input {
+            width: 100%;
+        }
+
+        .edit-form tr th:first-child, .edit-form tr td:first-child {
+            width: 20%;
+        }
+
+        .edit-form tr th:nth-last-of-type(-n+2), .edit-form tr td:nth-last-of-type(-n+2) {
+            display: none;
+        }
+    }
+
+    .edit-form table > tbody > tr td a.btn-delcfg {
+        visibility: hidden;
+    }
+
+    .edit-form table > tbody > tr:hover td a.btn-delcfg {
+        visibility: visible;
+    }
+
+    .ai-measure-tips {
+        background: #f8f9fa;
+        border: 1px solid #e9ecef;
+        border-radius: 4px;
+        padding: 15px;
+        margin-bottom: 20px;
+    }
+
+    .ai-measure-tips h5 {
+        color: #495057;
+        margin-bottom: 10px;
+    }
+
+    .ai-measure-tips ul {
+        margin-bottom: 0;
+        padding-left: 20px;
+    }
+
+    .ai-measure-tips li {
+        margin-bottom: 5px;
+        color: #6c757d;
+    }
+
+</style>
+<div class="panel panel-default panel-intro">
+    <div class="panel-heading">
+        {:build_heading(null, false)}
+        <div class="ai-measure-tips">
+            <h5><i class="fa fa-info-circle"></i> AI测量素材配置说明</h5>
+            <ul>
+                <li><strong>自拍模式:</strong>用户自己拍摄照片进行AI测量的相关配置</li>
+                <li><strong>帮拍模式:</strong>他人协助拍摄照片进行AI测量的相关配置</li>
+                <li>配置项包括:引导图片、提示文案、限制参数等</li>
+            </ul>
+        </div>
+        <ul class="nav nav-tabs">
+            {foreach $siteList as $index=>$vo}
+            <li class="{$vo.active?'active':''}"><a href="#tab-{$vo.name|htmlentities}" data-toggle="tab">{:__(htmlentities($vo.title))}</a></li>
+            {/foreach}
+            {if $Think.config.app_debug}
+            <li data-toggle="tooltip" title="{:__('Add new config')}">
+                <a href="#addcfg" data-toggle="tab"><i class="fa fa-plus"></i></a>
+            </li>
+            {/if}
+        </ul>
+    </div>
+
+    <div class="panel-body">
+        <div id="myTabContent" class="tab-content">
+            <!--@formatter:off-->
+            {foreach $siteList as $index=>$vo}
+            <div class="tab-pane fade {$vo.active ? 'active in' : ''}" id="tab-{$vo.name|htmlentities}">
+                <div class="widget-body no-padding">
+                    <form id="{$vo.name|htmlentities}-form" class="edit-form form-horizontal" role="form" data-toggle="validator" method="POST" action="{:url('general.ai_measure_config/edit')}">
+                        {:token()}
+                        <table class="table table-striped">
+                            <thead>
+                            <tr>
+                                <th width="15%">{:__('Title')}</th>
+                                <th width="68%">{:__('Value')}</th>
+                                <!-- {if $Think.config.app_debug}
+                                <th width="15%">{:__('Name')}</th>
+                                <th width="2%"></th>
+                                {/if} -->
+                            </tr>
+                            </thead>
+                            <tbody>
+                            {foreach $vo.list as $item}
+                            <tr data-favisible="{$item.visible|default=''|htmlentities}" data-name="{$item.name|htmlentities}" class="{if $item.visible??''}hidden{/if}">
+                                <td>{$item.title|htmlentities}</td>
+                                <td>
+                                    <div class="row">
+                                        <div class="col-sm-8 col-xs-12">
+                                            {switch $item.type}
+                                            {case string}
+                                            <input {$item.extend_html|htmlentities} type="text" name="row[{$item.name|htmlentities}]" value="{$item.value|htmlentities}" class="form-control" data-rule="{$item.rule|htmlentities}" data-tip="{$item.tip|htmlentities}"/>
+                                            {/case}
+                                            {case password}
+                                            <input {$item.extend_html|htmlentities} type="password" name="row[{$item.name|htmlentities}]" value="{$item.value|htmlentities}" class="form-control" data-rule="{$item.rule|htmlentities}" data-tip="{$item.tip|htmlentities}"/>
+                                            {/case}
+                                            {case text}
+                                            <textarea {$item.extend_html|htmlentities} name="row[{$item.name|htmlentities}]" class="form-control" data-rule="{$item.rule|htmlentities}" rows="5" data-tip="{$item.tip|htmlentities}">{$item.value|htmlentities}</textarea>
+                                            {/case}
+                                            {case editor}
+                                            <textarea {$item.extend_html|htmlentities} name="row[{$item.name|htmlentities}]" id="editor-{$item.name|htmlentities}" class="form-control editor" data-rule="{$item.rule|htmlentities}" rows="5" data-tip="{$item.tip|htmlentities}">{$item.value|htmlentities}</textarea>
+                                            {/case}
+                                            {case array}
+                                            <dl {$item.extend_html|htmlentities} class="fieldlist" data-name="row[{$item.name|htmlentities}]">
+                                                <dd>
+                                                    <ins>{:isset($item["setting"]["key"])&&$item["setting"]["key"]?$item["setting"]["key"]:__('Array key')}</ins>
+                                                    <ins>{:isset($item["setting"]["value"])&&$item["setting"]["value"]?$item["setting"]["value"]:__('Array value')}</ins>
+                                                </dd>
+                                                <dd><a href="javascript:;" class="btn btn-sm btn-success btn-append"><i class="fa fa-plus"></i> {:__('Append')}</a></dd>
+                                                <textarea name="row[{$item.name|htmlentities}]" class="form-control hide" cols="30" rows="5">{$item.value|htmlentities}</textarea>
+                                            </dl>
+                                            {/case}
+                                            {case date}
+                                            <input {$item.extend_html|htmlentities} type="text" name="row[{$item.name|htmlentities}]" value="{$item.value|htmlentities}" class="form-control datetimepicker" data-date-format="YYYY-MM-DD" data-tip="{$item.tip|htmlentities}" data-rule="{$item.rule|htmlentities}"/>
+                                            {/case}
+                                            {case time}
+                                            <input {$item.extend_html|htmlentities} type="text" name="row[{$item.name|htmlentities}]" value="{$item.value|htmlentities}" class="form-control datetimepicker" data-date-format="HH:mm:ss" data-tip="{$item.tip|htmlentities}" data-rule="{$item.rule|htmlentities}"/>
+                                            {/case}
+                                            {case datetime}
+                                            <input {$item.extend_html|htmlentities} type="text" name="row[{$item.name|htmlentities}]" value="{$item.value|htmlentities}" class="form-control datetimepicker" data-date-format="YYYY-MM-DD HH:mm:ss" data-tip="{$item.tip|htmlentities}" data-rule="{$item.rule|htmlentities}"/>
+                                            {/case}
+                                            {case datetimerange}
+                                            <input {$item.extend_html|htmlentities} type="text" name="row[{$item.name|htmlentities}]" value="{$item.value|htmlentities}" class="form-control datetimerange" data-tip="{$item.tip|htmlentities}" data-rule="{$item.rule|htmlentities}"/>
+                                            {/case}
+                                            {case number}
+                                            <input {$item.extend_html|htmlentities} type="number" name="row[{$item.name|htmlentities}]" value="{$item.value|htmlentities}" class="form-control" data-tip="{$item.tip|htmlentities}" data-rule="{$item.rule|htmlentities}"/>
+                                            {/case}
+                                            {case checkbox}
+                                            <div class="checkbox">
+                                            {foreach name="item.content" item="vo"}
+                                            <label for="row[{$item.name|htmlentities}][]-{$key|htmlentities}"><input id="row[{$item.name|htmlentities}][]-{$key|htmlentities}" name="row[{$item.name|htmlentities}][]" type="checkbox" value="{$key|htmlentities}" data-tip="{$item.tip|htmlentities}" {in name="key" value="$item.value" }checked{/in} /> {$vo|htmlentities}</label>
+                                            {/foreach}
+                                            </div>
+                                            {/case}
+                                            {case radio}
+                                            <div class="radio">
+                                            {foreach name="item.content" item="vo"}
+                                            <label for="row[{$item.name|htmlentities}]-{$key|htmlentities}"><input id="row[{$item.name|htmlentities}]-{$key|htmlentities}" name="row[{$item.name|htmlentities}]" type="radio" value="{$key|htmlentities}" data-tip="{$item.tip|htmlentities}" {in name="key" value="$item.value" }checked{/in} /> {$vo|htmlentities}</label>
+                                            {/foreach}
+                                            </div>
+                                            {/case}
+                                            {case value="select" break="0"}{/case}
+                                            {case value="selects"}
+                                            <select {$item.extend_html|htmlentities} name="row[{$item.name|htmlentities}]{$item.type=='selects'?'[]':''}" class="form-control selectpicker" data-tip="{$item.tip|htmlentities}" {$item.type=='selects'?'multiple':''}>
+                                                {foreach name="item.content" item="vo"}
+                                                <option value="{$key|htmlentities}" {in name="key" value="$item.value" }selected{/in}>{$vo|htmlentities}</option>
+                                                {/foreach}
+                                            </select>
+                                            {/case}
+                                            {case value="image" break="0"}{/case}
+                                            {case value="images"}
+                                            <div class="form-inline">
+                                                <input id="c-{$item.name|htmlentities}" class="form-control" size="50" name="row[{$item.name|htmlentities}]" type="text" value="{$item.value|htmlentities}" data-tip="{$item.tip|htmlentities}">
+                                                <span><button type="button" id="faupload-{$item.name|htmlentities}" class="btn btn-danger faupload" data-input-id="c-{$item.name|htmlentities}" data-mimetype="image/gif,image/jpeg,image/png,image/jpg,image/bmp,image/webp" data-multiple="{$item.type=='image'?'false':'true'}" data-preview-id="p-{$item.name|htmlentities}"><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
+                                                <span><button type="button" id="fachoose-{$item.name|htmlentities}" class="btn btn-primary fachoose" data-input-id="c-{$item.name|htmlentities}" data-mimetype="image/*" data-multiple="{$item.type=='image'?'false':'true'}"><i class="fa fa-list"></i> {:__('Choose')}</button></span>
+                                                <span class="msg-box n-right" for="c-{$item.name|htmlentities}"></span>
+                                                <ul class="row list-inline faupload-preview" id="p-{$item.name|htmlentities}"></ul>
+                                            </div>
+                                            {/case}
+                                            {case value="file" break="0"}{/case}
+                                            {case value="files"}
+                                            <div class="form-inline">
+                                                <input id="c-{$item.name|htmlentities}" class="form-control" size="50" name="row[{$item.name|htmlentities}]" type="text" value="{$item.value|htmlentities}" data-tip="{$item.tip|htmlentities}">
+                                                <span><button type="button" id="faupload-{$item.name|htmlentities}" class="btn btn-danger faupload" data-input-id="c-{$item.name|htmlentities}" data-mimetype="*" data-multiple="{$item.type=='file'?'false':'true'}"><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
+                                                <span><button type="button" id="fachoose-{$item.name|htmlentities}" class="btn btn-primary fachoose" data-input-id="c-{$item.name|htmlentities}" data-multiple="{$item.type=='file'?'false':'true'}"><i class="fa fa-list"></i> {:__('Choose')}</button></span>
+                                                <span class="msg-box n-right" for="c-{$item.name|htmlentities}"></span>
+                                            </div>
+                                            {/case}
+                                            {case switch}
+                                            <input id="c-{$item.name|htmlentities}" name="row[{$item.name|htmlentities}]" type="hidden" value="{:$item.value?1:0}">
+                                            <a href="javascript:;" data-toggle="switcher" class="btn-switcher" data-input-id="c-{$item.name|htmlentities}" data-yes="1" data-no="0">
+                                                <i class="fa fa-toggle-on text-success {if !$item.value}fa-flip-horizontal text-gray{/if} fa-2x"></i>
+                                            </a>
+                                            {/case}
+                                            {case bool}
+                                            <label for="row[{$item.name|htmlentities}]-yes"><input id="row[{$item.name|htmlentities}]-yes" name="row[{$item.name|htmlentities}]" type="radio" value="1" {$item.value?'checked':''} data-tip="{$item.tip|htmlentities}" /> {:__('Yes')}</label>
+                                            <label for="row[{$item.name|htmlentities}]-no"><input id="row[{$item.name|htmlentities}]-no" name="row[{$item.name|htmlentities}]" type="radio" value="0" {$item.value?'':'checked'} data-tip="{$item.tip|htmlentities}" /> {:__('No')}</label>
+                                            {/case}
+                                            {case selectpage}
+                                            <input {$item.extend_html|htmlentities} type="text" name="row[{$item.name|htmlentities}]" id="c-{$item.name|htmlentities}" value="{$item.value|htmlentities}" class="form-control selectpage" data-source="{:url('general.ai_measure_config/selectpage')}?id={$item.id|htmlentities}" data-primary-key="{$item.setting.primarykey|htmlentities}" data-field="{$item.setting.field|htmlentities}" data-multiple="false" data-tip="{$item.tip|htmlentities}" data-rule="{$item.rule|htmlentities}" />
+                                            {/case}
+                                            {case custom}
+                                            {$item.extend_html|htmlentities}
+                                            {/case}
+                                            {/switch}
+                                        </div>
+                                        <div class="col-sm-4"></div>
+                                    </div>
+
+                                </td>
+    
+                            </tr>
+                            {/foreach}
+                            </tbody>
+                            <tfoot>
+                            <tr>
+                                <td></td>
+                                <td>
+                                    <div class="layer-footer">
+                                        <button type="submit" class="btn btn-primary btn-embossed disabled">{:__('OK')}</button>
+                                        <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
+                                    </div>
+                                </td>
+                                {if $Think.config.app_debug}
+                                <td></td>
+                                <td></td>
+                                {/if}
+                            </tr>
+                            </tfoot>
+                        </table>
+                    </form>
+                </div>
+            </div>
+            {/foreach}
+            <div class="tab-pane fade" id="addcfg">
+                <form id="add-form" class="form-horizontal" role="form" data-toggle="validator" method="POST" action="{:url('general.ai_measure_config/add')}">
+                    {:token()}
+                    <div class="form-group">
+                        <label class="control-label col-xs-12 col-sm-2">{:__('Group')}:</label>
+                        <div class="col-xs-12 col-sm-4">
+                            <select name="row[group]" class="form-control selectpicker">
+                                {foreach name="groupList" item="vo"}
+                                <option value="{$key|htmlentities}" {in name="key" value="ai_measure_selfie" }selected{/in}>{$vo|htmlentities}</option>
+                                {/foreach}
+                            </select>
+                        </div>
+                    </div>
+                    <div class="form-group">
+                        <label class="control-label col-xs-12 col-sm-2">{:__('Type')}:</label>
+                        <div class="col-xs-12 col-sm-4">
+                            <select name="row[type]" id="c-type" class="form-control selectpicker">
+                                {foreach name="typeList" item="vo"}
+                                <option value="{$key|htmlentities}" {in name="key" value="string" }selected{/in}>{$vo|htmlentities}</option>
+                                {/foreach}
+                            </select>
+                        </div>
+                    </div>
+                    <div class="form-group">
+                        <label for="name" class="control-label col-xs-12 col-sm-2">{:__('Name')}:</label>
+                        <div class="col-xs-12 col-sm-4">
+                            <input type="text" class="form-control" id="name" name="row[name]" value="" data-rule="required; length(3~30); remote(general/ai_measure_config/check)"/>
+                        </div>
+                    </div>
+                    <div class="form-group">
+                        <label for="title" class="control-label col-xs-12 col-sm-2">{:__('Title')}:</label>
+                        <div class="col-xs-12 col-sm-4">
+                            <input type="text" class="form-control" id="title" name="row[title]" value="" data-rule="required"/>
+                        </div>
+                    </div>
+                    <div class="form-group">
+                        <label for="value" class="control-label col-xs-12 col-sm-2">{:__('Value')}:</label>
+                        <div class="col-xs-12 col-sm-4">
+                            <input type="text" class="form-control" id="value" name="row[value]" value="" data-rule=""/>
+                        </div>
+                    </div>
+                    <div class="form-group hide" id="add-content-container">
+                        <label for="content" class="control-label col-xs-12 col-sm-2">{:__('Content')}:</label>
+                        <div class="col-xs-12 col-sm-4">
+                            <textarea name="row[content]" id="content" cols="30" rows="5" class="form-control" data-rule="required(content)">value1|title1
+value2|title2</textarea>
+                        </div>
+                    </div>
+                    <div class="form-group">
+                        <label for="tip" class="control-label col-xs-12 col-sm-2">{:__('Tip')}:</label>
+                        <div class="col-xs-12 col-sm-4">
+                            <input type="text" class="form-control" id="tip" name="row[tip]" value="" data-rule=""/>
+                        </div>
+                    </div>
+                    <div class="form-group">
+                        <label for="rule" class="control-label col-xs-12 col-sm-2">{:__('Rule')}:</label>
+                        <div class="col-xs-12 col-sm-4">
+                            <input type="text" class="form-control" id="rule" name="row[rule]" value="" data-tip="{:__('Rule tips')}"/>
+                        </div>
+                    </div>
+                    <div class="form-group">
+                        <label for="visible" class="control-label col-xs-12 col-sm-2">{:__('Visible condition')}:</label>
+                        <div class="col-xs-12 col-sm-4">
+                            <input type="text" class="form-control" id="visible" name="row[visible]" value="" data-rule=""/>
+                        </div>
+                    </div>
+                    <div class="form-group">
+                        <label for="extend" class="control-label col-xs-12 col-sm-2">{:__('Extend')}:</label>
+                        <div class="col-xs-12 col-sm-4">
+                            <textarea name="row[extend]" id="extend" cols="30" rows="5" class="form-control" data-tip="{:__('Extend tips')}" data-rule="required(extend)" data-msg-extend="当类型为自定义时,扩展属性不能为空"></textarea>
+                        </div>
+                    </div>
+                    <div class="form-group">
+                        <label class="control-label col-xs-12 col-sm-2"></label>
+                        <div class="col-xs-12 col-sm-4">
+                            {if !$Think.config.app_debug}
+                            <button type="button" class="btn btn-primary disabled">{:__('Only work at development environment')}</button>
+                            {else/}
+                            <button type="submit" class="btn btn-primary btn-embossed">{:__('OK')}</button>
+                            <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
+                            {/if}
+                        </div>
+                    </div>
+
+                </form>
+
+            </div>
+            <!--@formatter:on-->
+        </div>
+    </div>
+</div>

+ 211 - 10
application/admin/view/shop/goods/add.html

@@ -132,16 +132,43 @@
     100% { opacity: 1; }
 }
 
-/* 表单字段错误高亮样式 */
-.error-highlight {
+
+/* Nice-validator错误样式兼容 */
+.n-invalid {
     border-color: #d9534f !important;
-    box-shadow: 0 0 0 0.2rem rgba(217, 83, 79, 0.25) !important;
-    background-color: #fdf2f2 !important;
 }
 
-.error-highlight:focus {
-    border-color: #d9534f !important;
-    box-shadow: 0 0 0 0.2rem rgba(217, 83, 79, 0.25) !important;
+.form-group.has-error .form-control,
+.form-group.has-error .form-control:focus {
+    border-color: #d9534f;
+    box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 6px rgba(217, 83, 79, 0.4);
+}
+
+/* 错误消息框样式 */
+.msg-box {
+    display: block;
+    margin-top: 5px;
+    font-size: 12px;
+    color: #d9534f;
+}
+
+.msg-box.n-right {
+    text-align: left;
+}
+
+/* 表单验证状态样式 */
+.form-group.has-error .control-label {
+    color: #d9534f;
+}
+
+.form-group.has-error .form-control {
+    border-color: #d9534f;
+    box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
+}
+
+.form-group.has-error .form-control:focus {
+    border-color: #d9534f;
+    box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 6px rgba(217, 83, 79, 0.4);
 }
 
 /* 提交按钮禁用状态样式 */
@@ -193,6 +220,144 @@
     color: #5cb85c;
 }
 
+/* 选项卡成功状态样式 */
+.nav-tabs .tab-completed {
+    color: #28a745 !important;
+    position: relative;
+}
+
+.nav-tabs .tab-completed::after {
+    content: "\f00c";
+    font-family: "FontAwesome";
+    position: absolute;
+    top: 50%;
+    right: 8px;
+    transform: translateY(-50%);
+    color: #28a745;
+    font-size: 12px;
+}
+
+.nav-tabs .tab-disabled {
+    color: #6c757d !important;
+    pointer-events: none;
+    opacity: 0.6;
+}
+
+.nav-tabs .tab-current {
+    color: #007bff !important;
+    font-weight: bold;
+}
+
+/* 增强的字段验证状态样式 */
+.field-error {
+    border-color: #dc3545 !important;
+    box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25) !important;
+    background-color: rgba(248, 215, 218, 0.3) !important;
+}
+
+
+
+/* 条件必填字段提示 */
+.conditional-required-hint {
+    font-size: 11px;
+    color: #6c757d;
+    font-style: italic;
+    margin-top: 2px;
+}
+
+/* 分步导航样式 */
+.step-navigation {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 15px 0;
+    border-top: 1px solid #e5e5e5;
+    margin-top: 20px;
+}
+
+.step-buttons {
+    display: flex;
+    gap: 10px;
+    align-items: center;
+}
+
+.step-buttons .btn {
+    min-width: 100px;
+    padding: 8px 16px;
+    font-size: 14px;
+    border-radius: 4px;
+    transition: all 0.3s ease;
+}
+
+.step-buttons .btn-prev {
+    background-color: #6c757d;
+    border-color: #6c757d;
+    color: white;
+}
+
+.step-buttons .btn-prev:hover {
+    background-color: #5a6268;
+    border-color: #545b62;
+}
+
+.step-buttons .btn-next {
+    background-color: #007bff;
+    border-color: #007bff;
+    color: white;
+}
+
+.step-buttons .btn-next:hover {
+    background-color: #0056b3;
+    border-color: #004085;
+}
+
+.step-buttons .btn-submit {
+    background-color: #28a745;
+    border-color: #28a745;
+    color: white;
+    min-width: 120px;
+}
+
+.step-buttons .btn-submit:hover {
+    background-color: #218838;
+    border-color: #1e7e34;
+}
+
+.step-buttons .btn-reset {
+    background-color: #f8f9fa;
+    border-color: #dee2e6;
+    color: #6c757d;
+}
+
+.step-buttons .btn-reset:hover {
+    background-color: #e9ecef;
+    border-color: #adb5bd;
+}
+
+.step-info {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    color: #6c757d;
+    font-size: 14px;
+}
+
+.step-current {
+    font-weight: bold;
+    color: #007bff;
+    font-size: 16px;
+}
+
+.step-total {
+    font-weight: bold;
+}
+
+.step-text {
+    color: #495057;
+    font-weight: 500;
+    margin-left: 8px;
+}
+
 /* 响应式调整 */
 @media (max-width: 768px) {
     .goods-type-card {
@@ -204,6 +369,23 @@
     .goods-type-selection {
         flex-direction: column;
     }
+    
+    .step-navigation {
+        flex-direction: column;
+        gap: 15px;
+        text-align: center;
+    }
+    
+    .step-buttons {
+        order: 2;
+        justify-content: center;
+        flex-wrap: wrap;
+    }
+    
+    .step-info {
+        order: 1;
+        justify-content: center;
+    }
 }
 </style>
 
@@ -280,7 +462,7 @@
                         <label class="control-label col-xs-12 col-sm-2">{:__('Image')}:</label>
                         <div class="col-xs-12 col-sm-8">
                             <div class="input-group">
-                                <input id="c-image" data-rule="required" class="form-control" size="50" name="row[image]" type="text" value="">
+                                <input id="c-image" data-rule="required" class="form-control" size="50" name="row[image]" type="text" value="" data-msg-required="商品主图不能为空" data-msg-image="商品主图格式不正确,仅支持 jpg, jpeg, png, gif, bmp, webp 格式">
                                 <div class="input-group-addon no-border no-padding">
                                     <span><button type="button" id="faupload-image" class="btn btn-danger faupload" data-input-id="c-image" data-mimetype="image/gif,image/jpeg,image/png,image/jpg,image/bmp,image/webp" data-multiple="false" data-preview-id="p-image"><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
                                     <span><button type="button" id="fachoose-image" class="btn btn-primary fachoose" data-input-id="c-image" data-mimetype="image/*" data-multiple="false"><i class="fa fa-list"></i> {:__('Choose')}</button></span>
@@ -563,11 +745,30 @@
 
     <div id="goods-sku"></div>
 
+    <!-- 分步导航按钮 -->
     <div class="form-group layer-footer">
         <label class="control-label col-xs-12 col-sm-2"></label>
         <div class="col-xs-12 col-sm-8">
-            <button type="submit" class="btn btn-primary btn-embossed disabled">{:__('OK')}</button>
-            <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
+            <div class="step-navigation">
+                <div class="step-buttons">
+                    <button type="button" class="btn btn-default btn-prev" style="display: none;">
+                        <i class="fa fa-chevron-left"></i> 上一步
+                    </button>
+                    <button type="button" class="btn btn-primary btn-next">
+                        下一步 <i class="fa fa-chevron-right"></i>
+                    </button>
+                    <button type="submit" class="btn btn-success btn-submit" style="display: none;">
+                        <i class="fa fa-check"></i> 提交
+                    </button>
+                    <button type="reset" class="btn btn-default btn-reset">
+                        <i class="fa fa-refresh"></i> {:__('Reset')}
+                    </button>
+                </div>
+                <div class="step-info">
+                    <span class="step-current">1</span> / <span class="step-total">6</span>
+                    <span class="step-text">基础信息</span>
+                </div>
+            </div>
         </div>
     </div>
 </form>

+ 2 - 2
application/admin/view/shop/goods/add_sku.html

@@ -1001,7 +1001,7 @@
             <label class="control-label col-xs-12 col-sm-2">销售价:</label>
             <div class="col-xs-12 col-sm-10">
                 <div class="input-group">
-                    <input id="c-price" data-rule="required;number;min:0.01" class="form-control" type="number" value="" step="0.01" min="0.01" placeholder="请输入销售价">
+                    <input id="c-price" data-rule="required;number;min:0.01" class="form-control" type="number" value="" step="0.01" min="0.01" placeholder="请输入销售价" data-msg-required="销售价不能为空" data-msg-number="销售价必须为数字" data-msg-min="销售价不能小于0.01元">
                     <span class="input-group-addon">元</span>
                 </div>
             </div>
@@ -1047,7 +1047,7 @@
             <label class="control-label col-xs-12 col-sm-2">库存:</label>
             <div class="col-xs-12 col-sm-10">
                 <div class="input-group">
-                    <input id="c-stocks" data-rule="required;integer;min:1" class="form-control" type="number" value="" min="1" placeholder="请输入库存数量">
+                    <input id="c-stocks" data-rule="required;integer;min:1" class="form-control" type="number" value="" min="1" placeholder="请输入库存数量" data-msg-required="库存数量不能为空" data-msg-integer="库存数量必须为整数" data-msg-min="库存数量不能小于1">
                     <span class="input-group-addon">件</span>
                 </div>
             </div>

+ 183 - 14
application/admin/view/shop/goods/edit.html

@@ -132,17 +132,6 @@
     100% { opacity: 1; }
 }
 
-/* 表单字段错误高亮样式 */
-.error-highlight {
-    border-color: #d9534f !important;
-    box-shadow: 0 0 0 0.2rem rgba(217, 83, 79, 0.25) !important;
-    background-color: #fdf2f2 !important;
-}
-
-.error-highlight:focus {
-    border-color: #d9534f !important;
-    box-shadow: 0 0 0 0.2rem rgba(217, 83, 79, 0.25) !important;
-}
 
 /* 提交按钮禁用状态样式 */
 .btn[type="submit"].disabled {
@@ -193,6 +182,150 @@
     color: #5cb85c;
 }
 
+/* Nice-validator错误样式兼容 */
+.n-invalid {
+    border-color: #d9534f !important;
+}
+
+.form-group.has-error .form-control,
+.form-group.has-error .form-control:focus {
+    border-color: #d9534f;
+    box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 6px rgba(217, 83, 79, 0.4);
+}
+
+/* 错误消息框样式 */
+.msg-box {
+    display: block;
+    margin-top: 5px;
+    font-size: 12px;
+    color: #d9534f;
+}
+
+.msg-box.n-right {
+    text-align: left;
+}
+
+/* 分步导航选项卡状态样式 */
+.nav-tabs .tab-completed {
+    color: #28a745 !important;
+    position: relative;
+}
+
+.nav-tabs .tab-completed::after {
+    content: "\f00c";
+    font-family: "FontAwesome";
+    position: absolute;
+    top: 50%;
+    right: 8px;
+    transform: translateY(-50%);
+    color: #28a745;
+    font-size: 12px;
+}
+
+.nav-tabs .tab-disabled {
+    color: #6c757d !important;
+    pointer-events: none;
+    opacity: 0.6;
+}
+
+.nav-tabs .tab-current {
+    color: #007bff !important;
+    font-weight: bold;
+}
+
+/* 分步导航样式 */
+.step-navigation {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 15px 0;
+    border-top: 1px solid #e5e5e5;
+    margin-top: 20px;
+}
+
+.step-buttons {
+    display: flex;
+    gap: 10px;
+    align-items: center;
+}
+
+.step-buttons .btn {
+    min-width: 100px;
+    padding: 8px 16px;
+    font-size: 14px;
+    border-radius: 4px;
+    transition: all 0.3s ease;
+}
+
+.step-buttons .btn-prev {
+    background-color: #6c757d;
+    border-color: #6c757d;
+    color: white;
+}
+
+.step-buttons .btn-prev:hover {
+    background-color: #5a6268;
+    border-color: #545b62;
+}
+
+.step-buttons .btn-next {
+    background-color: #007bff;
+    border-color: #007bff;
+    color: white;
+}
+
+.step-buttons .btn-next:hover {
+    background-color: #0056b3;
+    border-color: #004085;
+}
+
+.step-buttons .btn-submit {
+    background-color: #28a745;
+    border-color: #28a745;
+    color: white;
+    min-width: 120px;
+}
+
+.step-buttons .btn-submit:hover {
+    background-color: #218838;
+    border-color: #1e7e34;
+}
+
+.step-buttons .btn-reset {
+    background-color: #f8f9fa;
+    border-color: #dee2e6;
+    color: #6c757d;
+}
+
+.step-buttons .btn-reset:hover {
+    background-color: #e9ecef;
+    border-color: #adb5bd;
+}
+
+.step-info {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    color: #6c757d;
+    font-size: 14px;
+}
+
+.step-current {
+    font-weight: bold;
+    color: #007bff;
+    font-size: 16px;
+}
+
+.step-total {
+    font-weight: bold;
+}
+
+.step-text {
+    color: #495057;
+    font-weight: 500;
+    margin-left: 8px;
+}
+
 /* 响应式调整 */
 @media (max-width: 768px) {
     .goods-type-card {
@@ -204,6 +337,23 @@
     .goods-type-selection {
         flex-direction: column;
     }
+    
+    .step-navigation {
+        flex-direction: column;
+        gap: 15px;
+        text-align: center;
+    }
+    
+    .step-buttons {
+        order: 2;
+        justify-content: center;
+        flex-wrap: wrap;
+    }
+    
+    .step-info {
+        order: 1;
+        justify-content: center;
+    }
 }
 </style>
 
@@ -279,7 +429,7 @@
                         <label class="control-label col-xs-12 col-sm-2">{:__('Image')}:</label>
                         <div class="col-xs-12 col-sm-8">
                             <div class="input-group">
-                                <input id="c-image" data-rule="required" class="form-control" size="50" name="row[image]" type="text" value="{$row.image}">
+                                <input id="c-image" data-rule="required" class="form-control" size="50" name="row[image]" type="text" value="{$row.image}" data-msg-required="商品主图不能为空" data-msg-image="商品主图格式不正确,仅支持 jpg, jpeg, png, gif, bmp, webp 格式">
                                 <div class="input-group-addon no-border no-padding">
                                     <span><button type="button" id="faupload-image" class="btn btn-danger faupload" data-input-id="c-image" data-mimetype="image/gif,image/jpeg,image/png,image/jpg,image/bmp,image/webp" data-multiple="false" data-preview-id="p-image"><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
                                     <span><button type="button" id="fachoose-image" class="btn btn-primary fachoose" data-input-id="c-image" data-mimetype="image/*" data-multiple="false"><i class="fa fa-list"></i> {:__('Choose')}</button></span>
@@ -517,11 +667,30 @@
 
     <div id="goods-sku"></div>
 
+    <!-- 分步导航按钮 -->
     <div class="form-group layer-footer">
         <label class="control-label col-xs-12 col-sm-2"></label>
         <div class="col-xs-12 col-sm-8">
-            <button type="submit" class="btn btn-primary btn-embossed disabled">{:__('OK')}</button>
-            <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
+            <div class="step-navigation">
+                <div class="step-buttons">
+                    <button type="button" class="btn btn-default btn-prev" style="display: none;">
+                        <i class="fa fa-chevron-left"></i> 上一步
+                    </button>
+                    <button type="button" class="btn btn-primary btn-next">
+                        下一步 <i class="fa fa-chevron-right"></i>
+                    </button>
+                    <button type="submit" class="btn btn-success btn-submit" style="display: none;">
+                        <i class="fa fa-check"></i> 保存
+                    </button>
+                    <button type="reset" class="btn btn-default btn-reset">
+                        <i class="fa fa-refresh"></i> {:__('Reset')}
+                    </button>
+                </div>
+                <div class="step-info">
+                    <span class="step-current">1</span> / <span class="step-total">6</span>
+                    <span class="step-text">基础信息</span>
+                </div>
+            </div>
         </div>
     </div>
 </form>

+ 240 - 0
application/api/controller/AiMeasurement.php

@@ -274,6 +274,246 @@ class AiMeasurement extends Api
     }
 
     /**
+     * 获取AI测量素材配置
+     * @ApiMethod (GET)
+     * @ApiParams (name="mode", type="string", required=false, description="模式:selfie(自拍)、helper(帮拍)、common(通用)、all(全部)")
+     */
+    public function getMaterialConfig()
+    {
+        $mode = $this->request->get('mode', 'all');
+
+        // try {
+            $config = [];
+            
+            switch ($mode) {
+                case 'selfie':
+                    $config = $this->getSelfieConfig();
+                    break;
+                    
+                case 'helper':
+                    $config = $this->getHelperConfig();
+                    break;
+                    
+                case 'common':
+                    $config = $this->getCommonConfig();
+                    break;
+                    
+                case 'all':
+                default:
+                    $config = [
+                        'selfie' => $this->getSelfieConfig(),
+                        'helper' => $this->getHelperConfig(),
+                        'common' => $this->getCommonConfig()
+                    ];
+                    break;
+            }
+
+            $this->success('获取成功', $config);
+        // } catch (\Exception $e) {
+        //     $this->error($e->getMessage());
+        // }
+    }
+
+    /**
+     * 获取自拍模式配置
+     */
+    private function getSelfieConfig()
+    {
+        return [
+            'enabled' => config('site.ai_measure_selfie_enabled'),
+            
+            // 引导教程
+            'tutorial' => [
+                'images' => $this->parseImages(config('site.ai_measure_selfie_tutorial_images')),
+                'video' => $this->formatFileUrl(config('site.ai_measure_selfie_tutorial_video'))
+            ],
+            
+            // 陀螺仪检测
+            'gyroscope' => [
+                'voice' => $this->formatFileUrl(config('site.ai_measure_selfie_gyro_voice')),
+                'example' => $this->formatFileUrl(config('site.ai_measure_selfie_gyro_example'))
+            ],
+            
+            // 拍摄正面
+            'front_shooting' => [
+                'frame' => $this->formatFileUrl(config('site.ai_measure_selfie_front_frame')),
+                'voice' => $this->formatFileUrl(config('site.ai_measure_selfie_front_voice')),
+                'demo' => $this->formatFileUrl(config('site.ai_measure_selfie_front_demo'))
+            ],
+            
+            // 拍摄侧面
+            'side_shooting' => [
+                'frame' => $this->formatFileUrl(config('site.ai_measure_selfie_side_frame')),
+                'voice' => $this->formatFileUrl(config('site.ai_measure_selfie_side_voice')),
+                'demo' => $this->formatFileUrl(config('site.ai_measure_selfie_side_demo'))
+            ],
+            
+            // 拍摄正面侧平举
+            'arms_shooting' => [
+                'frame' => $this->formatFileUrl(config('site.ai_measure_selfie_arms_frame')),
+                'voice' => $this->formatFileUrl(config('site.ai_measure_selfie_arms_voice')),
+                'demo' => $this->formatFileUrl(config('site.ai_measure_selfie_arms_demo'))
+            ],
+            
+            // 拍摄过程素材
+            'process_materials' => [
+                'countdown_voice' => $this->formatFileUrl(config('site.ai_measure_selfie_countdown_voice')),
+                'timer_sound' => $this->formatFileUrl(config('site.ai_measure_selfie_timer_sound')),
+                'complete_sound' => $this->formatFileUrl(config('site.ai_measure_selfie_complete_sound')),
+                'next_voice' => $this->formatFileUrl(config('site.ai_measure_selfie_next_voice')),
+                'finish_voice' => $this->formatFileUrl(config('site.ai_measure_selfie_finish_voice'))
+            ]
+        ];
+    }
+
+    /**
+     * 获取帮拍模式配置
+     */
+    private function getHelperConfig()
+    {
+        return [
+            'enabled' => config('site.ai_measure_helper_enabled'),
+            
+            // 引导教程
+            'tutorial' => [
+                'images' => $this->parseImages(config('site.ai_measure_helper_tutorial_images')),
+                'video' => $this->formatFileUrl(config('site.ai_measure_helper_tutorial_video'))
+            ],
+            
+            // 陀螺仪检测
+            'gyroscope' => [
+                'voice' => $this->formatFileUrl(config('site.ai_measure_helper_gyro_voice')),
+                'example' => $this->formatFileUrl(config('site.ai_measure_helper_gyro_example'))
+            ],
+            
+            // 拍摄正面
+            'front_shooting' => [
+                'frame' => $this->formatFileUrl(config('site.ai_measure_helper_front_frame')),
+                'voice' => $this->formatFileUrl(config('site.ai_measure_helper_front_voice')),
+                'demo' => $this->formatFileUrl(config('site.ai_measure_helper_front_demo'))
+            ],
+            
+            // 拍摄侧面
+            'side_shooting' => [
+                'frame' => $this->formatFileUrl(config('site.ai_measure_helper_side_frame')),
+                'voice' => $this->formatFileUrl(config('site.ai_measure_helper_side_voice')),
+                'demo' => $this->formatFileUrl(config('site.ai_measure_helper_side_demo'))
+            ],
+            
+            // 拍摄正面侧平举
+            'arms_shooting' => [
+                'frame' => $this->formatFileUrl(config('site.ai_measure_helper_arms_frame')),
+                'voice' => $this->formatFileUrl(config('site.ai_measure_helper_arms_voice')),
+                'demo' => $this->formatFileUrl(config('site.ai_measure_helper_arms_demo'))
+            ],
+            
+            // 拍摄过程素材
+            'process_materials' => [
+                'countdown_voice' => cdnurl(config('site.ai_measure_helper_countdown_voice')),
+                'timer_sound' => $this->formatFileUrl(config('site.ai_measure_helper_timer_sound')),
+                'complete_sound' => $this->formatFileUrl(config('site.ai_measure_helper_complete_sound')),
+                'next_voice' => $this->formatFileUrl(config('site.ai_measure_helper_next_voice')),
+                'finish_voice' => $this->formatFileUrl(config('site.ai_measure_helper_finish_voice'))
+            ]
+        ];
+    }
+
+    /**
+     * 获取通用配置
+     */
+    private function getCommonConfig()
+    {
+        return [
+            'privacy_notice' => config('site.ai_measure_privacy_notice'),
+            'accuracy_disclaimer' => config('site.ai_measure_accuracy_disclaimer'),
+            'measurement_types' => $this->parseMeasurementTypes(config('site.ai_measure_measurement_types')),
+            'ai_model_version' => config('site.ai_measure_ai_model_version'),
+            'feedback_enabled' => config('site.ai_measure_feedback_enabled'),
+            'debug_mode' => config('site.ai_measure_debug_mode'),
+            'log_level' => config('site.ai_measure_log_level')
+        ];
+    }
+
+    /**
+     * 格式化文件URL
+     */
+    private function formatFileUrl($url)
+    {
+        if (empty($url)) {
+            return null;
+        }
+        
+        return cdnurl($url);
+    }
+
+    /**
+     * 解析图片集合
+     */
+    private function parseImages($images)
+    {
+        if (empty($images)) {
+            return [];
+        }
+        
+        // 如果是JSON格式的字符串,解析为数组
+        if (is_string($images)) {
+            $imageArray = json_decode($images, true);
+            if (json_last_error() === JSON_ERROR_NONE && is_array($imageArray)) {
+                $images = $imageArray;
+            } else {
+                // 如果不是JSON,可能是逗号分隔的字符串
+                $images = explode(',', $images);
+            }
+        }
+        
+        if (!is_array($images)) {
+            return [];
+        }
+        
+        // 格式化每个图片URL
+        return array_map(function($url) {
+            return $this->formatFileUrl(trim($url));
+        }, array_filter($images));
+    }
+
+    /**
+     * 解析测量类型
+     */
+    private function parseMeasurementTypes($types)
+    {
+        if (empty($types)) {
+            return [];
+        }
+        
+        $typeLabels = [
+            'bust' => '胸围',
+            'waist' => '腰围', 
+            'hips' => '臀围',
+            'shoulder' => '肩宽',
+            'sleeve' => '袖长',
+            'inseam' => '内缝长',
+            'outseam' => '外缝长',
+            'thigh' => '大腿围',
+            'neck' => '颈围'
+        ];
+        
+        $typeArray = is_string($types) ? explode(',', $types) : $types;
+        $result = [];
+        
+        foreach ($typeArray as $type) {
+            $type = trim($type);
+            if (isset($typeLabels[$type])) {
+                $result[] = [
+                    'key' => $type,
+                    'label' => $typeLabels[$type]
+                ];
+            }
+        }
+        
+        return $result;
+    }
+
+    /**
      * 直接调用第三方AI测量服务
      * @ApiMethod (POST)
      * @ApiParams (name="profile_id", type="integer", required=true, description="档案ID")

+ 93 - 1
application/extra/site.php

@@ -1,7 +1,7 @@
 <?php
 
 return array (
-  'name' => 'T裁衣',
+  'name' => 'T裁衣',
   'beian' => '',
   'cdnurl' => '',
   'version' => '1.0.5',
@@ -50,4 +50,96 @@ return array (
   'prize_redbag' => '/uploads/20250627/216b925d6922617fefb9cd5e69614343.png',
   'prize_code' => '/uploads/20250627/f8ad608f00d07de8d4ccc31ef9d79eaf.png',
   'lottery_guide' => '/uploads/20250627/2fb3539ff2752a411ea97a3c31e1988e.png',
+  'shop.platform' => '',
+  'shop.platform.WechatMiniProgram' => '',
+  'shop.platform.WechatMiniProgram.app_id' => '',
+  'shop.platform.WechatMiniProgram.secret' => '',
+  'shop.platform.WechatMiniProgram.status' => '0',
+  'shop.platform.WechatMiniProgram.auto_login' => '1',
+  'shop.platform.WechatMiniProgram.bind_mobile' => '1',
+  'shop.platform.WechatMiniProgram.payment' => '',
+  'shop.platform.WechatMiniProgram.payment.wechat' => '1',
+  'shop.platform.WechatMiniProgram.payment.methods' => 
+  array (
+    0 => 'wechat',
+    1 => 'balance',
+  ),
+  'shop.platform.WechatMiniProgram.share' => 
+  array (
+  ),
+  'shop.platform.DouyinMiniProgram' => '',
+  'shop.platform.DouyinMiniProgram.app_id' => '',
+  'shop.platform.DouyinMiniProgram.secret' => '',
+  'shop.platform.DouyinMiniProgram.status' => '0',
+  'shop.platform.DouyinMiniProgram.auto_login' => '1',
+  'shop.platform.DouyinMiniProgram.bind_mobile' => '1',
+  'shop.platform.DouyinMiniProgram.payment' => '',
+  'shop.platform.DouyinMiniProgram.payment.wechat' => '0',
+  'shop.platform.DouyinMiniProgram.payment.alipay' => '1',
+  'shop.platform.DouyinMiniProgram.payment.methods' => 
+  array (
+    0 => 'alipay',
+    1 => 'balance',
+  ),
+  'shop.platform.DouyinMiniProgram.share' => 
+  array (
+  ),
+  'ai_measure_selfie_enabled' => '1',
+  'ai_measure_selfie_tutorial_images' => 
+  array (
+    0 => '/uploads/20250908/8bf2111a74a4bd6db079ab615499df89.png',
+  ),
+  'ai_measure_selfie_tutorial_video' => '',
+  'ai_measure_selfie_gyro_voice' => '',
+  'ai_measure_selfie_gyro_example' => '',
+  'ai_measure_selfie_front_frame' => '',
+  'ai_measure_selfie_front_voice' => '',
+  'ai_measure_selfie_front_demo' => '',
+  'ai_measure_selfie_side_frame' => '',
+  'ai_measure_selfie_side_voice' => '',
+  'ai_measure_selfie_side_demo' => '',
+  'ai_measure_selfie_arms_frame' => '',
+  'ai_measure_selfie_arms_voice' => '',
+  'ai_measure_selfie_arms_demo' => '',
+  'ai_measure_selfie_countdown_voice' => '',
+  'ai_measure_selfie_timer_sound' => '',
+  'ai_measure_selfie_complete_sound' => '',
+  'ai_measure_selfie_next_voice' => '',
+  'ai_measure_selfie_finish_voice' => '',
+  'ai_measure_helper_enabled' => '1',
+  'ai_measure_helper_tutorial_images' => 
+  array (
+    0 => '',
+  ),
+  'ai_measure_helper_tutorial_video' => '',
+  'ai_measure_helper_gyro_voice' => '',
+  'ai_measure_helper_gyro_example' => '',
+  'ai_measure_helper_front_frame' => '',
+  'ai_measure_helper_front_voice' => '',
+  'ai_measure_helper_front_demo' => '',
+  'ai_measure_helper_side_frame' => '',
+  'ai_measure_helper_side_voice' => '',
+  'ai_measure_helper_side_demo' => '',
+  'ai_measure_helper_arms_frame' => '',
+  'ai_measure_helper_arms_voice' => '',
+  'ai_measure_helper_arms_demo' => '',
+  'ai_measure_helper_countdown_voice' => '',
+  'ai_measure_helper_timer_sound' => '',
+  'ai_measure_helper_complete_sound' => '',
+  'ai_measure_helper_next_voice' => '',
+  'ai_measure_helper_finish_voice' => '',
+  'ai_measure_privacy_notice' => '<div class="privacy-notice"><h4>隐私保护承诺</h4><ul><li>📸 您的照片仅用于AI测量分析,不会用于其他用途</li><li>🔐 所有图片数据传输均采用SSL加密保护</li><li>⏰ 测量完成后照片将在24小时内自动删除</li><li>🚫 我们不会保存、分享或出售您的个人照片</li><li>✅ 严格遵守相关数据保护法规</li></ul></div>',
+  'ai_measure_accuracy_disclaimer' => '<div class="accuracy-disclaimer"><h4>测量准确性说明</h4><p><strong>准确度范围:</strong>AI测量结果准确度约为85-95%,具体取决于:</p><ul><li>📷 拍摄质量和角度</li><li>👕 服装类型和贴合度</li><li>💡 环境光线条件</li><li>🧍 身体姿势标准度</li></ul><p><strong>误差范围:</strong>测量结果可能存在±1-3cm的误差</p><p><strong>建议:</strong>购买前请参考详细尺码表,建议选择略宽松一号</p></div>',
+  'ai_measure_measurement_types' => 
+  array (
+    0 => 'bust',
+    1 => 'waist',
+    2 => 'hips',
+    3 => 'shoulder',
+    4 => 'sleeve',
+  ),
+  'ai_measure_ai_model_version' => 'v2.1.3',
+  'ai_measure_feedback_enabled' => '1',
+  'ai_measure_debug_mode' => '0',
+  'ai_measure_log_level' => 'info',
 );

+ 146 - 0
public/assets/js/backend/general/ai_measure_config.js

@@ -0,0 +1,146 @@
+define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) {
+
+    var Controller = {
+        index: function () {
+            $("form.edit-form").data("validator-options", {
+                display: function (elem) {
+                    return $(elem).closest('tr').find("td:first").text();
+                }
+            });
+            Form.api.bindevent($("form.edit-form"));
+
+            //不可见的元素不验证
+            $("form#add-form").data("validator-options", {
+                ignore: ':hidden',
+                rules: {
+                    content: function () {
+                        return ['radio', 'checkbox', 'select', 'selects'].indexOf($("#add-form select[name='row[type]']").val()) > -1;
+                    },
+                    extend: function () {
+                        return $("#add-form select[name='row[type]']").val() == 'custom';
+                    }
+                }
+            });
+            Form.api.bindevent($("form#add-form"), function (ret) {
+                setTimeout(function () {
+                    location.reload();
+                }, 1500);
+            });
+
+            //切换显示隐藏变量字典列表
+            $(document).on("change", "form#add-form select[name='row[type]']", function (e) {
+                $("#add-content-container").toggleClass("hide", ['select', 'selects', 'checkbox', 'radio'].indexOf($(this).val()) > -1 ? false : true);
+            });
+
+            //删除配置
+            $(document).on("click", ".btn-delcfg", function () {
+                var that = this;
+                Layer.confirm(__('Are you sure you want to delete this item?'), {
+                    icon: 3,
+                    title: '提示'
+                }, function (index) {
+                    Backend.api.ajax({
+                        url: "general/ai_measure_config/del",
+                        data: {name: $(that).data("name")}
+                    }, function () {
+                        $(that).closest("tr").remove();
+                        Layer.close(index);
+                    });
+                });
+
+            });
+
+            // AI测量配置特有的验证逻辑
+            $(document).on("submit", "form.edit-form", function (e) {
+                // 检查图片类型的配置项是否有有效的图片URL
+                var hasError = false;
+                $(this).find('input[name*="image"], input[name*="picture"]').each(function() {
+                    var value = $(this).val();
+                    if (value && !value.match(/\.(jpg|jpeg|png|gif|webp)$/i)) {
+                        Toastr.error('请上传有效的图片文件');
+                        hasError = true;
+                        return false;
+                    }
+                });
+                
+                if (hasError) {
+                    e.preventDefault();
+                    return false;
+                }
+            });
+
+            // 为特定的AI测量配置项添加提示信息
+            $(document).ready(function() {
+                // 为自拍模式配置添加特殊提示
+                $('#tab-ai_measure_selfie input, #tab-ai_measure_selfie textarea, #tab-ai_measure_selfie select').each(function() {
+                    var name = $(this).attr('name');
+                    if (name && name.indexOf('guide') > -1) {
+                        $(this).attr('data-toggle', 'tooltip');
+                        $(this).attr('title', '引导用户进行自拍的相关配置');
+                    }
+                });
+
+                // 为帮拍模式配置添加特殊提示
+                $('#tab-ai_measure_helper input, #tab-ai_measure_helper textarea, #tab-ai_measure_helper select').each(function() {
+                    var name = $(this).attr('name');
+                    if (name && name.indexOf('guide') > -1) {
+                        $(this).attr('data-toggle', 'tooltip');
+                        $(this).attr('title', '引导他人协助拍摄的相关配置');
+                    }
+                });
+
+                // 初始化提示信息
+                $('[data-toggle="tooltip"]').tooltip();
+            });
+
+            // 图片预览功能增强
+            $(document).on('change', 'input[type="text"][name*="image"], input[type="text"][name*="picture"]', function() {
+                var $input = $(this);
+                var imageUrl = $input.val();
+                var $preview = $input.closest('.form-inline').find('.faupload-preview');
+                
+                if (!$preview.length) {
+                    $preview = $('<div class="image-preview" style="margin-top: 10px;"></div>');
+                    $input.closest('.form-inline').append($preview);
+                }
+                
+                $preview.empty();
+                
+                if (imageUrl && imageUrl.match(/\.(jpg|jpeg|png|gif|webp)$/i)) {
+                    $preview.html('<img src="' + imageUrl + '" style="max-width: 200px; max-height: 150px; border: 1px solid #ddd; border-radius: 4px;" />');
+                }
+            });
+
+            // 配置项分组折叠功能
+            $(document).on('click', '.panel-heading .nav-tabs li a', function() {
+                var target = $(this).attr('href');
+                var groupName = target.replace('#tab-', '');
+                
+                // 记住当前选中的分组
+                localStorage.setItem('ai_measure_config_active_tab', groupName);
+            });
+
+            // 恢复上次选中的分组
+            var lastActiveTab = localStorage.getItem('ai_measure_config_active_tab');
+            if (lastActiveTab) {
+                $('.nav-tabs li').removeClass('active');
+                $('.tab-pane').removeClass('active in');
+                
+                $('.nav-tabs a[href="#tab-' + lastActiveTab + '"]').closest('li').addClass('active');
+                $('#tab-' + lastActiveTab).addClass('active in');
+            }
+        },
+        add: function () {
+            Controller.api.bindevent();
+        },
+        edit: function () {
+            Controller.api.bindevent();
+        },
+        api: {
+            bindevent: function () {
+                Form.api.bindevent($("form[role=form]"));
+            }
+        }
+    };
+    return Controller;
+});

File diff suppressed because it is too large
+ 605 - 236
public/assets/js/backend/shop/goods.js


Some files were not shown because too many files changed in this diff