Browse Source

fix:后台

super-yimizi 2 months ago
parent
commit
b267e4e491

+ 132 - 9
application/admin/controller/shop/Goods.php

@@ -11,6 +11,8 @@ use app\admin\model\shop\GoodsAttr;
 use app\admin\model\shop\Spec;
 use app\admin\model\shop\GoodsSkuSpec;
 use app\common\Enum\GoodsEnum;
+use app\common\Service\SkuSpec as SkuSpecService;
+
 /**
  * 商品管理
  *
@@ -193,6 +195,60 @@ class Goods extends Backend
         return implode(',', $arr);
     }
 
+    /**
+     * 根据上下架时间计算商品状态
+     * @param array $params 商品参数
+     * @return int 商品状态
+     */
+    protected function calculateGoodsStatus($params)
+    {
+        $currentTime = time();
+        
+        // 获取上架时间设置 - 前端传递的上架类型字段(使用status字段)
+        $onlineType = isset($params['status']) ? intval($params['status']) : GoodsEnum::ONLINE_TYPE_NOT_NOW;
+        $scheduledOnlineTime = isset($params['scheduled_online_time']) ? strtotime($params['scheduled_online_time']) : 0;
+        
+        // 获取下架时间设置
+        $isAutoOffline = isset($params['is_auto_offline']) ? intval($params['is_auto_offline']) : 0;
+        $scheduledOfflineTime = isset($params['scheduled_offline_time']) ? strtotime($params['scheduled_offline_time']) : 0;
+        
+        // 根据上架类型判断状态
+        switch ($onlineType) {
+            case GoodsEnum::ONLINE_TYPE_IMMEDIATE: // 立即上架
+                // 检查是否需要自动下架
+                if ($isAutoOffline && $scheduledOfflineTime > 0) {
+                    if ($currentTime >= $scheduledOfflineTime) {
+                        return GoodsEnum::STATUS_OFF_SALE; // 已下架
+                    }
+                }
+                return GoodsEnum::STATUS_ON_SALE; // 销售中
+                
+            case GoodsEnum::ONLINE_TYPE_NOT_NOW: // 暂不上架
+                return GoodsEnum::STATUS_IN_STORAGE; // 仓库中
+                
+            case GoodsEnum::ONLINE_TYPE_SCHEDULED: // 定时上架
+                if ($scheduledOnlineTime <= 0) {
+                    return GoodsEnum::STATUS_IN_STORAGE; // 未设置上架时间,保持仓库中
+                }
+                
+                if ($currentTime < $scheduledOnlineTime) {
+                    return GoodsEnum::STATUS_IN_STORAGE; // 未到上架时间,仓库中
+                }
+                
+                // 已到上架时间,检查下架时间
+                if ($isAutoOffline && $scheduledOfflineTime > 0) {
+                    if ($currentTime >= $scheduledOfflineTime) {
+                        return GoodsEnum::STATUS_OFF_SALE; // 已下架
+                    }
+                }
+                
+                return GoodsEnum::STATUS_ON_SALE; // 销售中
+                
+            default:
+                return GoodsEnum::STATUS_IN_STORAGE; // 默认仓库中
+        }
+    }
+
     //处理商品SKU(统一处理单规格和多规格)
     protected function processGoodsSku($params, $goods_id)
     {
@@ -435,7 +491,31 @@ class Goods extends Backend
                         
                         $this->model->validateFailException(true)->validate($validate);
                     }
+                    
+                    // 备份上架类型,因为status字段将被覆盖为最终状态
+                    $originalOnlineType = $params['online_type'] ??  GoodsEnum::ONLINE_TYPE_NOT_NOW;
+                    
+                    // 将online_type映射到status字段用于计算
+                    if (isset($params['online_type'])) {
+                        $params['status'] = $params['online_type'];
+                        unset($params['online_type']);
+                    }
+                    
+                    // 先保存商品基础信息(暂时不保存status字段)
+                    $statusBackup = $params['status'] ?? null;
+                    unset($params['status']); // 临时移除status避免冲突
                     $result = $this->model->allowField(true)->save($params);
+                    
+                    // 根据上下架时间自动计算商品状态并更新
+                    $params['status'] = $statusBackup; // 恢复用于计算
+                    $calculatedStatus = $this->calculateGoodsStatus($params);
+                    $this->model->where('id', $this->model->id)->update([
+                        'status' => $calculatedStatus,
+                        'online_type' => $originalOnlineType  // 保存原始上架类型用于编辑时回显
+                    ]);
+                    
+                    // 调试输出
+                    \think\Log::write('商品添加 - 上架类型: ' . $originalOnlineType . ', 计算状态: ' . $calculatedStatus, 'info');
                     //商品规格处理
                     if (isset($params['skus']) && isset($params['spec'])) {
                         $skus = (array)json_decode($params['skus'], true);
@@ -513,7 +593,31 @@ class Goods extends Backend
                         
                         $row->validateFailException(true)->validate($validate);
                     }
+                    
+                    // 备份上架类型,因为status字段将被覆盖为最终状态
+                    $originalOnlineType = $params['online_type'] ??  GoodsEnum::ONLINE_TYPE_NOT_NOW;
+                    
+                    // 将online_type映射到status字段用于计算
+                    if (isset($params['online_type'])) {
+                        $params['status'] = $params['online_type'];
+                        unset($params['online_type']);
+                    }
+                    
+                    // 先保存商品基础信息(暂时不保存status字段)
+                    $statusBackup = $params['status'] ?? null;
+                    unset($params['status']); // 临时移除status避免冲突
                     $result = $row->allowField(true)->save($params);
+                    
+                    // 根据上下架时间自动计算商品状态并更新
+                    $params['status'] = $statusBackup; // 恢复用于计算
+                    $calculatedStatus = $this->calculateGoodsStatus($params);
+                    $this->model->where('id', $row->id)->update([
+                        'status' => $calculatedStatus,
+                        'online_type' => $originalOnlineType  // 保存原始上架类型用于编辑时回显
+                    ]);
+                    
+                    // 调试输出
+                    \think\Log::write('商品编辑 - 上架类型: ' . $originalOnlineType . ', 计算状态: ' . $calculatedStatus, 'info');
                     //商品规格处理
                     if (isset($params['skus']) && isset($params['spec'])) {
                         $skus = (array)json_decode($params['skus'], true);
@@ -552,18 +656,37 @@ class Goods extends Backend
             }
             $this->error(__('Parameter %s can not be empty', ''));
         }
-        //查询属性输出 - 包含所有SKU字段和market_price
-        $list = $this->sku_model->field("sku.id,sku.goods_id,sku.sku_sn,sku.spec_value_ids,sku.image,sku.price,sku.market_price,sku.lineation_price,sku.cost_price,sku.stocks,sku.sales,sku.weight,sku.volume,sku.weigh,sku.is_default,sku.status,sku.createtime,sku.updatetime,GROUP_CONCAT(sp.name,':',sv.value ORDER BY sp.id asc) sku_attr")
-            ->alias('sku')
-            ->where('sku.goods_id', $row->id)
-            ->join('shop_goods_sku_spec p', "FIND_IN_SET(p.id,sku.spec_value_ids)", 'LEFT')
-            ->join('shop_spec sp', 'sp.id=p.spec_id', 'LEFT')
-            ->join('shop_spec_value sv', 'sv.id=p.spec_value_id', 'LEFT')
-            ->group('sku.id')
-            ->select();
+            
+        
+        // 查询SKU数据,包含规格属性字符串
+        $list = $this->sku_model->field("sku.*,GROUP_CONCAT(sp.name,':',sv.value ORDER BY sp.id asc) sku_attr")
+        ->alias('sku')
+        ->where('sku.goods_id', $row->id)
+        ->join('shop_goods_sku_spec p', "FIND_IN_SET(p.id,sku.spec_value_ids)", 'LEFT')
+        ->join('shop_spec sp', 'sp.id=p.spec_id', 'LEFT')
+        ->join('shop_spec_value sv', 'sv.id=p.spec_value_id', 'LEFT')
+        ->group('sku.id')
+        ->select();
+        
+        // 查询规格值的详细信息(包含图片等扩展字段)
+        $spec_values = [];
+        if ($row->spec_type == 1) { // 多规格商品
+            $spec_values = Db::name('shop_goods_sku_spec')
+                ->alias('gss')
+                ->field('sp.name as spec_name, sv.value, sv.image, sv.desc')
+                ->join('shop_spec sp', 'sp.id = gss.spec_id', 'LEFT')
+                ->join('shop_spec_value sv', 'sv.id = gss.spec_value_id', 'LEFT')
+                ->where('gss.goods_id', $row->id)
+                ->group('gss.spec_id, gss.spec_value_id')
+                ->order('sp.id asc, sv.id asc')
+                ->select();
+        }
+        
         $this->view->assign("row", $row);
         $this->assignconfig('goods', $row);
         $this->assignconfig('goods_skus', $list);
+        $this->assignconfig('spec_values', $spec_values); // 传递规格值详细信息
+        
         return $this->view->fetch();
     }
 

+ 65 - 35
application/admin/model/shop/Goods.php

@@ -24,10 +24,11 @@ class Goods extends Model
 
     // 追加属性
     protected $append = [
-        'flag_text',
-        'status_text',
+        // 'status_text',
+        'scheduled_online_time_text',
+        'scheduled_offline_time_text',
     ];
-    
+
     // 规格类型字段映射 - 实际数据库字段就是spec_type
     // 这些方法已经不需要了,因为字段名是一致的
 
@@ -70,50 +71,79 @@ class Goods extends Model
         });
     }
 
-    public function setContentAttr($value)
-    {
-        return $value;
-        //替换卡片信息
-        return \addons\shop\library\Service::replaceSourceTpl($value);
-    }
 
-    public function getContentAttr($value, $data)
+    // 计划上架时间的设置器:将前端datetime转为时间戳
+    public function setScheduledOnlineTimeAttr($value)
     {
-        //组装卡片信息
-        return \addons\shop\library\Service::formatSourceTpl($value);
+        if (empty($value)) {
+            return 0;
+        }
+        
+        // 如果已经是时间戳,直接返回
+        if (is_numeric($value) && (int)$value == $value) {
+            return $value;
+        }
+        
+        // 如果是日期时间字符串,转换为时间戳
+        return strtotime($value);
     }
-
-    public function getFlagList()
+    
+    // 计划上架时间的获取器:将时间戳转为datetime格式
+    public function getScheduledOnlineTimeAttr($value)
     {
-        $config = get_addon_config('shop');
-        return $config['flagtype'] ?? [];
+        if (empty($value) || $value == 0) {
+            return '';
+        }
+        
+        return date('Y-m-d H:i:s', $value);
     }
-
-    public function getStatusList()
+    
+    // 计划上架时间的文本属性
+    public function getScheduledOnlineTimeTextAttr($value, $data)
     {
-        return ['normal' => __('Normal'), 'hidden' => __('Hidden'), 'soldout' => __('Soldout')];
+        $time = $data['scheduled_online_time'] ?? 0;
+        if (empty($time) || $time == 0) {
+            return '';
+        }
+        
+        return date('Y-m-d H:i:s', $time);
     }
-
-
-    public function getFlagTextAttr($value, $data)
+    
+    // 计划下架时间的设置器:将前端datetime转为时间戳
+    public function setScheduledOfflineTimeAttr($value)
     {
-        $value = $value ?: ($data['flag'] ?? '');
-        $valueArr = explode(',', $value);
-        $list = $this->getFlagList();
-        return implode(',', array_intersect_key($list, array_flip($valueArr)));
+        if (empty($value)) {
+            return 0;
+        }
+        
+        // 如果已经是时间戳,直接返回
+        if (is_numeric($value) && (int)$value == $value) {
+            return $value;
+        }
+        
+        // 如果是日期时间字符串,转换为时间戳
+        return strtotime($value);
     }
-
-
-    public function getStatusTextAttr($value, $data)
+    
+    // 计划下架时间的获取器:将时间戳转为datetime格式
+    public function getScheduledOfflineTimeAttr($value)
     {
-        $value = $value ?: ($data['status'] ?? '');
-        $list = $this->getStatusList();
-        return $list[$value] ?? '';
+        if (empty($value) || $value == 0) {
+            return '';
+        }
+        
+        return date('Y-m-d H:i:s', $value);
     }
-
-    protected function setFlagAttr($value)
+    
+    // 计划下架时间的文本属性
+    public function getScheduledOfflineTimeTextAttr($value, $data)
     {
-        return is_array($value) ? implode(',', $value) : $value;
+        $time = $data['scheduled_offline_time'] ?? 0;
+        if (empty($time) || $time == 0) {
+            return '';
+        }
+        
+        return date('Y-m-d H:i:s', $time);
     }
 
     protected function setAttributeIdsAttr($value)

+ 7 - 9
application/admin/validate/shop/Goods.php

@@ -17,7 +17,7 @@ class Goods extends Validate
         'image'             => 'require',
         'images'            => 'require',
         'weigh'             => 'require|integer|egt:0',
-        'status'            => 'require|in:0,1,2',
+        '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',
@@ -27,11 +27,11 @@ class Goods extends Validate
 
         'lineation_price'   => 'float|egt:0',
         'cost_price'        => 'float|egt:0',
-        'stocks'            => 'requireIf:spec_type,0|integer|egt:0',
+        'stocks'            => 'requireIf:spec_type,0|integer|gt:0',
         'weight'            => 'float|egt:0',
         'volume'            => 'float|egt:0',
-        'scheduled_online_time'  => 'requireIf:status,2|dateFormat:Y-m-d H:i:s',
-        'scheduled_offline_time' => 'requireIf:is_auto_offline,1|dateFormat:Y-m-d H:i:s',
+        'scheduled_online_time'  => 'requireIf:status,3',
+        'scheduled_offline_time' => 'requireIf:is_auto_offline,1',
         'spec_data'         => 'checkMultiSpec',
     ];
     
@@ -75,15 +75,13 @@ class Goods extends Validate
         'cost_price.egt'            => '成本价必须大于等于0',
         'stocks.requireIf'          => '单规格商品必须设置库存',
         'stocks.integer'            => '库存必须是整数',
-        'stocks.egt'                => '库存必须大于等于0',
+        'stocks.gt'                 => '库存必须大于0',
         'weight.float'              => '重量必须是数字',
         'weight.egt'                => '重量必须大于等于0',
         'volume.float'              => '体积必须是数字',
         'volume.egt'                => '体积必须大于等于0',
         'scheduled_online_time.requireIf' => '定时上架必须设置上架时间',
-        'scheduled_online_time.dateFormat' => '上架时间格式不正确',
         'scheduled_offline_time.requireIf' => '自动下架必须设置下架时间',
-        'scheduled_offline_time.dateFormat' => '下架时间格式不正确',
     ];
     
     /**
@@ -138,8 +136,8 @@ class Goods extends Validate
                 
 
                 
-                if (!isset($sku['stocks']) || $sku['stocks'] < 0) {
-                    return 'SKU库存必须大于等于0';
+                if (!isset($sku['stocks']) || $sku['stocks'] <= 0) {
+                    return 'SKU库存必须大于0';
                 }
             }
         }

+ 3 - 3
application/admin/view/shop/goods/add.html

@@ -272,7 +272,7 @@
                     <div class="form-group">
                         <label class="control-label col-xs-12 col-sm-2">{:__('Category_ids')}:</label>
                         <div class="col-xs-12 col-sm-8">
-                            <input id="c-category_id" data-rule="required" data-multiple="true"  data-source="shop/category/index/noKeyField/100" data-format-item="{spacer} {name}" class="form-control selectpage" name="row[category_ids]" type="text" value="">
+                            <input id="c-category_ids" data-rule="required" data-multiple="true"  data-source="shop/category/index/noKeyField/100" data-format-item="{spacer} {name}" class="form-control selectpage" name="row[category_ids]" type="text" value="">
                         </div>
                     </div>
 
@@ -338,12 +338,12 @@
                         </div>
                     </div>
                     <div class="form-group">
-                        <label class="control-label col-xs-12 col-sm-2">{:__('Status')}:</label>
+                        <label class="control-label col-xs-12 col-sm-2">{:__('上架类型')}:</label>
                         <div class="col-xs-12 col-sm-8">
 
                             <div class="radio">
                             {foreach name="onlineTypeList" item="vo"}
-                            <label for="row[status]-{$key|htmlentities}"><input id="row[status]-{$key|htmlentities}" name="row[status]" type="radio" value="{$key|htmlentities}" {in name="key" value="1"}checked{/in} /> {$vo|htmlentities}</label>
+                            <label for="row[online_type]-{$key|htmlentities}"><input id="row[online_type]-{$key|htmlentities}" name="row[online_type]" type="radio" value="{$key|htmlentities}" {in name="key" value="1"}checked{/in} /> {$vo|htmlentities}</label>
                             {/foreach}
                             </div>
 

+ 30 - 412
application/admin/view/shop/goods/add_sku.html

@@ -214,243 +214,23 @@
         display: none;
     }
     
-    /* 紧凑模式样式 */
-    .compact-mode .spec-block {
-        margin-bottom: 4px;
-        padding: 6px;
-        border-radius: 4px;
-        border: 1px solid #e8e8e8;
-        background: #fafafa;
-    }
-    
-    .compact-mode .spec-header {
-        margin-bottom: 4px;
-        padding: 3px 6px;
-        cursor: pointer;
-        transition: background-color 0.2s;
-        border-radius: 3px;
-    }
-    
-    .compact-mode .spec-header:hover {
-        background-color: #f0f0f0;
+    /* 隐藏状态的SKU行样式 */
+    .text-muted {
+        opacity: 0.6;
+        background-color: #f8f9fa !important;
     }
     
-    .compact-mode .spec-title {
-        font-size: 12px;
-        font-weight: normal;
-        color: #555;
+    .text-muted input,
+    .text-muted button {
+        opacity: 0.6;
     }
     
-    .compact-mode .spec-type-badge {
-        padding: 1px 4px;
-        font-size: 9px;
-        margin-left: 4px;
-        margin-right: 2px;
-        border-radius: 8px;
-    }
-    
-    .compact-mode .spec-info {
-        font-size: 10px;
-        color: #888;
-        margin-left: 4px;
-    }
+
     
     /* 规格折叠图标 - 普通模式 */
-    .spec-collapse-icon {
-        font-size: 11px;
-        transition: transform 0.3s ease;
-        color: #999;
-        cursor: pointer;
-        margin-left: 8px;
-        opacity: 0.7;
-    }
-    
-    .spec-collapse-icon:hover {
-        color: #666;
-        opacity: 1;
-    }
-    
-    .spec-collapse-icon.collapsed {
-        transform: rotate(-90deg);
-    }
-    
-    .spec-content {
-        overflow: hidden;
-        transition: max-height 0.3s ease;
-    }
-    
-    .spec-content.collapsed {
-        max-height: 0;
-        padding-top: 0;
-        padding-bottom: 0;
-        margin-top: 0;
-    }
-    
-    /* 紧凑模式下的折叠图标 */
-    .compact-mode .spec-collapse-icon {
-        font-size: 9px;
-        margin-left: 6px;
-    }
-    
-    .compact-mode .spec-name-section {
-        margin-bottom: 6px;
-    }
-    
-    .compact-mode .spec-label,
-    .compact-mode .spec-values-label {
-        font-size: 10px;
-        min-width: 50px;
-        margin-right: 6px;
-        color: #666;
-    }
-    
-    .compact-mode .spec-name-input .input-group {
-        max-width: 150px;
-    }
-    
-    .compact-mode .spec-name-input .form-control {
-        height: 24px;
-        font-size: 10px;
-        padding: 2px 4px;
-    }
-    
-    .compact-mode .spec-type-input {
-        margin-top: 4px !important;
-    }
-    
-    .compact-mode .spec-type-input .radio-inline {
-        font-size: 10px;
-        margin-right: 10px;
-    }
-    
-    .compact-mode .spec-values-container {
-        padding: 8px 12px 0;
-    }
-    
-    .compact-mode .spec-values-container::before {
-        width: 10px;
-        height: 16px;
-        left: 6px;
-    }
-    
-    .compact-mode .spec-values-row {
-        gap: 4px;
-        display: grid;
-        grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
-        align-items: start;
-    }
-    
-    .compact-mode .spec-value-item {
-        min-width: 160px;
-        margin-bottom: 4px;
-    }
-    
-    .compact-mode .spec-value-card {
-        padding: 4px;
-        box-shadow: 0 1px 2px rgba(0,0,0,0.05);
-        border: 1px solid #e0e0e0;
-        border-radius: 3px;
-        background: white;
-    }
-    
-    .compact-mode .spec-value-name {
-        margin-bottom: 2px;
-    }
-    
-    .compact-mode .spec-value-name input {
-        height: 20px;
-        font-size: 10px;
-        padding: 2px 4px;
-    }
-    
-    .compact-mode .spec-value-name .btn {
-        height: 20px;
-        font-size: 8px;
-        padding: 1px 3px;
-        line-height: 1;
-    }
-    
-    .compact-mode .spec-value-image {
-        margin-top: 2px;
-    }
-    
-    .compact-mode .spec-value-image .image-preview {
-        min-height: 12px;
-        margin-bottom: 2px;
-    }
-    
-    .compact-mode .spec-value-image img {
-        width: 12px;
-        height: 12px;
-    }
-    
-    .compact-mode .spec-value-image .input-group {
-        font-size: 8px;
-    }
-    
-    .compact-mode .spec-value-image .form-control {
-        height: 18px;
-        font-size: 8px;
-        padding: 1px 3px;
-    }
-    
-    .compact-mode .spec-value-image .btn {
-        height: 18px;
-        font-size: 7px;
-        padding: 1px 3px;
-        line-height: 1;
-    }
-    
-    .compact-mode .spec-value-desc {
-        margin-top: 2px;
-    }
-    
-    .compact-mode .spec-value-desc textarea {
-        font-size: 8px;
-        padding: 2px 3px;
-        resize: vertical;
-        min-height: 24px;
-        line-height: 1.2;
-    }
-    
-    .compact-mode .btn-add-value {
-        height: 20px;
-        font-size: 9px;
-        padding: 2px 6px;
-        min-width: 60px;
-        grid-column: span 1;
-        justify-self: start;
-        align-self: start;
-        margin-top: 2px;
-    }
-    
-    .compact-mode .btn-remove-spec {
-        font-size: 12px;
-        padding: 2px;
-        width: 20px;
-        height: 20px;
-        line-height: 1;
-    }
-    
-    .compact-mode .add-spec-btn-container {
-        padding: 10px;
-        margin-bottom: 8px;
-    }
-    
-    .compact-mode .btn-add-spec {
-        padding: 4px 12px;
-        font-size: 11px;
-    }
-    
-    .compact-mode .help-text {
-        font-size: 10px;
-        margin-top: 4px;
-    }
+
     
-    .compact-mode .add-more-spec .btn-add-spec {
-        padding: 4px 10px;
-        font-size: 10px;
-    }
+
     
     /* 图片操作菜单样式 */
     .image-options-layer {
@@ -490,59 +270,7 @@
         text-align: center;
     }
     
-    /* 紧凑模式切换按钮 */
-    .compact-toggle {
-        position: absolute;
-        top: 15px;
-        right: 15px;
-        z-index: 5;
-    }
-    
-    .compact-toggle .btn {
-        padding: 4px 8px;
-        font-size: 11px;
-        border-radius: 3px;
-    }
-    
-    .compact-toggle .btn-group .btn {
-        border-radius: 0;
-    }
-    
-    .compact-toggle .btn-group .btn:first-child {
-        border-top-left-radius: 3px;
-        border-bottom-left-radius: 3px;
-    }
-    
-    .compact-toggle .btn-group .btn:last-child {
-        border-top-right-radius: 3px;
-        border-bottom-right-radius: 3px;
-    }
-    
-    /* 折叠状态的规格块,只显示标题行 */
-    .compact-mode .spec-block.collapsed-block {
-        padding: 3px 6px;
-        margin-bottom: 2px;
-        background: #f8f8f8;
-        border-left: 3px solid #007bff;
-    }
-    
-    .compact-mode .spec-block.collapsed-block .spec-header {
-        margin-bottom: 0;
-        padding: 2px 0;
-    }
-    
-    .compact-mode .spec-block.collapsed-block .spec-title {
-        font-size: 11px;
-    }
-    
-    .compact-mode .spec-block.collapsed-block .spec-type-badge {
-        padding: 0px 3px;
-        font-size: 8px;
-    }
-    
-    .compact-mode .spec-block.collapsed-block .spec-info {
-        font-size: 9px;
-    }
+
     
     /* 规格值表格样式 */
     .spec-values-table {
@@ -700,20 +428,7 @@
         font-size: 8px;
     }
     
-    /* 紧凑模式下的按钮 */
-    .compact-mode .spec-col-image .input-group {
-        margin-top: 2px;
-    }
-    
-    .compact-mode .spec-col-image .btn-xs {
-        padding: 1px 3px;
-        font-size: 8px;
-        margin-right: 1px;
-    }
-    
-    .compact-mode .spec-col-image .btn-xs i {
-        font-size: 7px;
-    }
+
     
     .upload-hint {
         font-size: 8px;
@@ -748,87 +463,7 @@
         padding: 10px 0;
     }
     
-    /* 紧凑模式下的表格样式 */
-    .compact-mode .spec-values-table {
-        font-size: 10px;
-        margin-bottom: 6px;
-    }
-    
-    .compact-mode .spec-values-header {
-        font-size: 10px;
-        padding: 4px 0;
-    }
-    
-    .compact-mode .spec-value-row {
-        min-height: 36px;
-    }
-    
-    .compact-mode .spec-col-name,
-    .compact-mode .spec-col-image,
-    .compact-mode .spec-col-desc,
-    .compact-mode .spec-col-action {
-        padding: 4px 6px;
-    }
-    
-    .compact-mode .spec-col-image {
-        flex: 0 0 160px;
-        padding: 4px 6px;
-    }
-    
-    .compact-mode .spec-input {
-        padding: 3px 5px;
-        font-size: 10px;
-    }
-    
-    .compact-mode .spec-textarea {
-        padding: 3px 5px;
-        font-size: 9px;
-        min-height: 24px;
-    }
-    
-    .compact-mode .spec-image-container {
-        gap: 6px;
-    }
-    
-    .compact-mode .spec-col-image .image-thumb {
-        width: 28px;
-        height: 28px;
-    }
-    
-    .compact-mode .spec-col-image .spec-upload-btn,
-    .compact-mode .spec-col-image .spec-choose-btn {
-        padding: 2px 6px;
-        font-size: 10px;
-        margin-right: 2px;
-    }
-    
-    .compact-mode .spec-col-image .spec-upload-btn i,
-    .compact-mode .spec-col-image .spec-choose-btn i {
-        margin-right: 2px;
-        font-size: 8px;
-    }
-    
-    .compact-mode .image-preview-clickable {
-        width: 28px;
-        height: 28px;
-    }
-    
-    .compact-mode .spec-thumb-placeholder {
-        font-size: 10px;
-    }
-    
-    .compact-mode .upload-hint {
-        font-size: 6px;
-        margin-top: 1px;
-    }
-    
-    .compact-mode .image-overlay {
-        font-size: 10px;
-    }
-    
-    .compact-mode .spec-add-row {
-        padding: 6px 0;
-    }
+
     
     /* 单规格表单样式优化 */
     #single-spec-form .form-group {
@@ -1341,7 +976,7 @@
         <div class="col-xs-12 col-sm-10">
             <div class="radio">
             {foreach name="specTypeList" item="vo"}
-            <label for="row[spec_type]-{$key|htmlentities}" style="margin-right: 20px;"><input id="row[spec_type]-{$key|htmlentities}" name="row[spec_type]" type="radio" value="{$key|htmlentities}" {in name="key" value="0"}checked{/in} /> {$vo|htmlentities}</label>
+            <label for="row[spec_type]-{$key|htmlentities}" style="margin-right: 20px;"><input id="row[spec_type]-{$key|htmlentities}" name="row[spec_type]" type="radio" value="{$key|htmlentities}" {if condition="isset($row) && $row.spec_type == $key"}checked{elseif condition="!isset($row) && $key == 0" /}checked{/if} /> {$vo|htmlentities}</label>
             {/foreach}
             </div>
         </div>
@@ -1426,23 +1061,7 @@
     </div>
     
     <!-- 多规格表单 - 使用Favisible动态显示 -->
-    <div id="multi-spec-form" data-favisible="spec_type=1" :class="{ 'compact-mode': compactMode }">
-        <!-- 紧凑模式切换按钮 -->
-        <div class="compact-toggle">
-            <div class="btn-group" role="group">
-                <button type="button" class="btn btn-sm" :class="compactMode ? 'btn-success' : 'btn-default'" 
-                        @click="toggleCompactMode" title="切换紧凑模式">
-                    <i class="fa" :class="compactMode ? 'fa-compress' : 'fa-expand'"></i>
-                    {{ compactMode ? '紧凑' : '展开' }}
-                </button>
-                <button type="button" class="btn btn-sm btn-info" 
-                        @click="toggleAllSpecs" v-if="compactMode && specList.length > 1" 
-                        title="全部展开/折叠">
-                    <i class="fa" :class="allCollapsed ? 'fa-eye' : 'fa-eye-slash'"></i>
-                    {{ allCollapsed ? '全展开' : '全折叠' }}
-                </button>
-            </div>
-        </div>
+    <div id="multi-spec-form" data-favisible="spec_type=1">
         
         <!-- <div class="form-group">
             <label class="control-label col-xs-12 col-sm-2">规格模板:</label>
@@ -1471,17 +1090,15 @@
                 
                 <!-- 规格容器 -->
                 <div class="spec-container" v-if="specList.length > 0">
-                    <div class="spec-block" :class="{ 'collapsed-block': compactMode && spec.collapsed }" v-for="(spec,key) in specList" :key="key">
+                    <div class="spec-block" v-for="(spec,key) in specList" :key="key">
                         <!-- 规格头部 -->
-                        <div class="spec-header" @click="toggleSpecCollapse(key)">
+                        <div class="spec-header">
                             <div>
                                 <span class="spec-title">{{ spec.name || '未命名规格' }}</span>
                                 <span class="spec-type-badge" :class="spec.type === 'custom' ? 'badge-custom' : 'badge-basic'">
                                     {{ spec.type === 'custom' ? '定制风格' : '基础规格' }}
                                 </span>
                                 <span class="spec-info">({{ spec.value.length }}个规格值)</span>
-                                <i class="fa fa-chevron-down spec-collapse-icon" 
-                                   :class="{ 'collapsed': spec.collapsed }"></i>
                             </div>
                             <button type="button" class="btn-remove-spec" @click.stop="removeSpec(key)" title="删除整个规格">
                                 <i class="fa fa-trash"></i>
@@ -1489,7 +1106,7 @@
                         </div>
                         
                         <!-- 规格内容区域 -->
-                        <div class="spec-content" :class="{ 'collapsed': spec.collapsed }">
+                        <div class="spec-content">
                             <!-- 规格名称区域 -->
                             <div class="spec-name-section">
                             <div class="spec-name-input">
@@ -1548,7 +1165,7 @@
                                                            class="form-control spec-image-input" 
                                                            :data-spec-key="key"
                                                            :data-value-index="index"
-                                                           :value="specList[key].value[index].image || ''"/>
+                                                           v-model="specList[key].value[index].image"/>
                                                     <div class="input-group-addon no-border no-padding">
                                                         <span>
                                                             <button type="button"
@@ -1670,7 +1287,7 @@
                 </tr>
                 </thead>
                 <tbody>
-                <tr v-for="(item,index) in tableData" :key="index">
+                <tr v-for="(item,index) in tableData" :key="index" :class="{ 'text-muted': tableData[index].status == 0 }">
                     <th scope="row" class="fixed-left">{{index+1}}</th>
                     <td v-for="(res,ik) in specList" :key="ik" style="text-align: center;">
                         {{specValueText(item.skus,ik)}}
@@ -1684,6 +1301,7 @@
                             <div class="input-group-addon no-border no-padding">
                                     <span>
                                         <button type="button"
+                                                :disabled="tableData[index].status == 0"
                                                 :id="'faupload-image-'+index"
                                                 class="btn btn-danger faupload"
                                                 :data-input-id="'c-image-'+index" data-mimetype="image/gif,image/jpeg,image/png,image/jpg,image/bmp,image/webp"
@@ -1693,7 +1311,7 @@
                                         </button>
                                     </span>
                                 <span>
-                                        <button type="button" :id="'fachoose-image-'+index" class="btn btn-primary fachoose"
+                                        <button type="button" :disabled="tableData[index].status == 0" :id="'fachoose-image-'+index" class="btn btn-primary fachoose"
                                                 :data-input-id="'c-image-'+index" data-mimetype="image/*" data-multiple="false">
                                             <i class="fa fa-list"></i>
                                             {:__('Choose')}
@@ -1703,26 +1321,26 @@
                         </div>
                     </td>
                     <td class="scrollable-columns">
-                        <input class="form-control" id="a" :id="'sku_sn'+index" type="text" v-model="tableData[index].sku_sn">
+                        <input class="form-control" :disabled="tableData[index].status == 0" id="a" :id="'sku_sn'+index" type="text" v-model="tableData[index].sku_sn">
                     </td>
                     <td class="scrollable-columns">
-                        <input class="form-control" id="a" :id="'price'+index" type="text" v-model="tableData[index].price">
+                        <input class="form-control" :disabled="tableData[index].status == 0" id="a" :id="'price'+index" type="text" v-model="tableData[index].price">
                     </td>
 
                     <td class="scrollable-columns">
-                        <input class="form-control" id="a" :id="'lineation_price'+index" type="text" v-model="tableData[index].lineation_price">
+                        <input class="form-control" :disabled="tableData[index].status == 0" id="a" :id="'lineation_price'+index" type="text" v-model="tableData[index].lineation_price">
                     </td>
                     <td class="scrollable-columns">
-                        <input class="form-control" id="a" :id="'cost_price'+index" type="text" v-model="tableData[index].cost_price">
+                        <input class="form-control" :disabled="tableData[index].status == 0" id="a" :id="'cost_price'+index" type="text" v-model="tableData[index].cost_price">
                     </td>
                     <td class="scrollable-columns">
-                        <input class="form-control" id="a" :id="'weight'+index" type="text" v-model="tableData[index].weight">
+                        <input class="form-control" :disabled="tableData[index].status == 0" id="a" :id="'weight'+index" type="text" v-model="tableData[index].weight">
                     </td>
                     <td class="scrollable-columns">
-                        <input class="form-control" id="a" :id="'volume'+index" type="text" v-model="tableData[index].volume">
+                        <input class="form-control" :disabled="tableData[index].status == 0" id="a" :id="'volume'+index" type="text" v-model="tableData[index].volume">
                     </td>
                     <td class="scrollable-columns">
-                        <input class="form-control" id="a" :id="'stocks'+index" type="text" v-model="tableData[index].stocks">
+                        <input class="form-control" :disabled="tableData[index].status == 0" id="a" :id="'stocks'+index" type="text" v-model="tableData[index].stocks">
                     </td>
                     <td class="fixed-right" style="text-align: center;">
                         <label class="switch" style="margin: 0;">
@@ -1732,7 +1350,7 @@
                     </td>
                     <td class="fixed-right" style="text-align: center;">
                         <label style="margin: 0;">
-                            <input type="radio" :name="'default_spec'" :value="index" v-model="defaultSpecIndex">
+                            <input type="radio" :disabled="tableData[index].status == 0" :name="'default_spec'" :value="index" v-model="defaultSpecIndex">
                             <span style="margin-left: 5px;">默认</span>
                         </label>
                     </td>

+ 507 - 0
application/admin/view/shop/goods/edit.html

@@ -0,0 +1,507 @@
+<style>
+/* 商品类型选择卡片样式 */
+.goods-type-selection {
+    display: flex;
+    gap: 0;
+    flex-wrap: wrap;
+    align-items: center;
+}
+
+.goods-type-card {
+    position: relative;
+    display: block;
+    width: 150px;
+    height: 34px;
+    border: 1px solid #ddd;
+    border-radius: 4px;
+    background: #fff;
+    cursor: pointer;
+    transition: all 0.3s ease;
+    overflow: hidden;
+    margin-bottom: 0;
+    margin-right: 15px;
+}
+
+.goods-type-card:hover {
+    border-color: #4a90e2;
+    box-shadow: 0 1px 3px rgba(74, 144, 226, 0.1);
+}
+
+.goods-type-card input[type="radio"] {
+    display: none;
+}
+
+.goods-type-card input[type="radio"]:checked + .card-content {
+    background: #f0f7ff;
+}
+
+.goods-type-card input[type="radio"]:checked ~ .check-icon {
+    opacity: 1;
+    transform: scale(1);
+}
+
+.goods-type-card input[type="radio"]:checked {
+    & ~ * {
+        border-color: #4a90e2;
+    }
+}
+
+.goods-type-card input[type="radio"]:checked ~ .card-content ~ .goods-type-card {
+    border-color: #4a90e2;
+}
+
+.goods-type-card:has(input[type="radio"]:checked) {
+    border-color: #4a90e2;
+    background: #f0f7ff;
+}
+
+.card-content {
+    padding: 8px 12px;
+    height: 100%;
+    display: flex;
+    flex-direction: row;
+    justify-content: flex-start;
+    align-items: center;
+    text-align: left;
+    transition: background 0.3s ease;
+}
+
+.type-title {
+    font-size: 13px;
+    font-weight: normal;
+    color: #333;
+    margin-right: 6px;
+}
+
+.type-subtitle {
+    font-size: 11px;
+    color: #999;
+}
+
+.check-icon {
+    position: absolute;
+    bottom: 2px;
+    right: 2px;
+    width: 14px;
+    height: 14px;
+    background: #4a90e2;
+    border-radius: 50%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    opacity: 0;
+    transform: scale(0);
+    transition: all 0.3s ease;
+}
+
+.check-icon i {
+    color: white;
+    font-size: 8px;
+}
+
+/* 兼容性样式 - 当浏览器不支持:has()时的备用方案 */
+.goods-type-card.selected {
+    border-color: #4a90e2;
+    background: #f0f7ff;
+}
+
+.goods-type-card.selected .check-icon {
+    opacity: 1;
+    transform: scale(1);
+}
+
+/* 选项卡错误状态样式 */
+.nav-tabs .tab-error {
+    color: #d9534f !important;
+    border-bottom-color: #d9534f !important;
+}
+
+.nav-tabs .tab-error:hover,
+.nav-tabs .tab-error:focus {
+    color: #c9302c !important;
+    border-bottom-color: #c9302c !important;
+}
+
+.nav-tabs .tab-error .error-icon {
+    animation: pulse 1.5s infinite;
+}
+
+@keyframes pulse {
+    0% { opacity: 1; }
+    50% { opacity: 0.5; }
+    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 {
+    pointer-events: none;
+    opacity: 0.6;
+}
+
+/* 选项卡内容区域的错误提示样式 */
+.tab-error-message {
+    background-color: #f2dede;
+    border: 1px solid #ebccd1;
+    color: #a94442;
+    padding: 10px;
+    border-radius: 4px;
+    margin-bottom: 15px;
+}
+
+.tab-error-message .error-list {
+    margin: 0;
+    padding-left: 20px;
+}
+
+.tab-error-message .error-list li {
+    margin-bottom: 5px;
+}
+
+/* 表单验证状态样式 */
+.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);
+}
+
+/* 选项卡成功状态样式 */
+.nav-tabs .tab-success {
+    color: #5cb85c !important;
+}
+
+.nav-tabs .tab-success .success-icon {
+    color: #5cb85c;
+}
+
+/* 响应式调整 */
+@media (max-width: 768px) {
+    .goods-type-card {
+        width: 100%;
+        margin-right: 0;
+        margin-bottom: 10px;
+    }
+    
+    .goods-type-selection {
+        flex-direction: column;
+    }
+}
+</style>
+
+<form id="edit-form" class="form-horizontal" role="form" data-toggle="validator" method="POST" action="">
+
+    <div class="panel panel-default panel-intro">
+        <div class="panel-heading">
+            <ul class="nav nav-tabs nav-group">
+                <li class="active"><a href="#basics" data-toggle="tab">基础信息</a></li>
+                <li><a href="#skus" data-toggle="tab">价格库存</a></li>
+                <li><a href="#delivery" data-toggle="tab">配送设置</a></li>
+                <li><a href="#detail" data-toggle="tab">商品详情</a></li>
+                <li><a href="#params" data-toggle="tab">商品参数</a></li>
+                <li><a href="#sales" data-toggle="tab">销售设置</a></li>
+            </ul>
+        </div>
+        <div class="panel-body">
+            <div id="myTabContent" class="tab-content">
+                <div class="tab-pane fade active in" id="basics">
+                    <div class="form-group">
+                        <label class="control-label col-xs-12 col-sm-2">{:__('Type')}:</label>
+                        <div class="col-xs-12 col-sm-8">
+                            <div class="goods-type-selection">
+                            {foreach name="goodsTypeList" item="vo"}
+                            <label class="goods-type-card" for="row[type]-{$key|htmlentities}">
+                                <input id="row[type]-{$key|htmlentities}" name="row[type]" type="radio" value="{$key|htmlentities}" {in name="key" value="$row.type"}checked{/in} data-rule="required" />
+                                <div class="card-content">
+                                    <div class="type-title">{$vo|htmlentities}</div>
+                                    <div class="type-subtitle">
+                                        {switch name="$key"}
+                                        {case value="1"}(物流发货){/case}
+                                        {case value="2"}(自动发货){/case}
+                                        {case value="3"}(虚拟发货){/case}
+                                        {case value="4"}(到店核销){/case}
+                                        {default /}
+                                        {/switch}
+                                    </div>
+                                </div>
+                                <div class="check-icon">
+                                    <i class="fa fa-check"></i>
+                                </div>
+                            </label>
+                            {/foreach}
+                            </div>
+                        </div>
+                    </div>
+                    <div class="form-group">
+                        <label class="control-label col-xs-12 col-sm-2">{:__('Goods_sn')}:</label>
+                        <div class="col-xs-12 col-sm-8">
+                            <input id="c-goods_sn" data-rule="required" class="form-control" name="row[goods_sn]" type="text" value="{$row.goods_sn|htmlentities}" placeholder="请输入商品编码">
+                        </div>
+                    </div>
+                    <div class="form-group">
+                        <label class="control-label col-xs-12 col-sm-2">{:__('Title')}:</label>
+                        <div class="col-xs-12 col-sm-8">
+                            <input id="c-title" data-rule="required" class="form-control" name="row[title]" type="text" value="{$row.title|htmlentities}">
+                        </div>
+                    </div>
+                    <div class="form-group">
+                        <label class="control-label col-xs-12 col-sm-2">{:__('Subtitle')}:</label>
+                        <div class="col-xs-12 col-sm-8">
+                            <input id="c-sub_title" data-rule="max:200" class="form-control" name="row[sub_title]" type="text" value="{$row.sub_title|htmlentities}">
+                        </div>
+                    </div>
+                    <div class="form-group">
+                        <label class="control-label col-xs-12 col-sm-2">{:__('Category_ids')}:</label>
+                        <div class="col-xs-12 col-sm-8">
+                            <input id="c-category_ids" data-rule="required" data-multiple="true"  data-source="shop/category/index/noKeyField/100" data-format-item="{spacer} {name}" class="form-control selectpage" name="row[category_ids]" type="text" value="{$row.category_ids}">
+                        </div>
+                    </div>
+
+                    <div class="form-group">
+                        <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}">
+                                <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>
+                                </div>
+                                <span class="msg-box n-right" for="c-image"></span>
+                            </div>
+                            <ul class="row list-inline faupload-preview" id="p-image"></ul>
+                        </div>
+                    </div>
+                    <div class="form-group">
+                        <label class="control-label col-xs-12 col-sm-2">{:__('Images')}:</label>
+                        <div class="col-xs-12 col-sm-8">
+                            <div class="input-group">
+                                <input id="c-images" data-rule="required" class="form-control" size="50" name="row[images]" type="text" value="{$row.images}">
+                                <div class="input-group-addon no-border no-padding">
+                                    <span><button type="button" id="faupload-images" class="btn btn-danger faupload" data-input-id="c-images" data-mimetype="image/gif,image/jpeg,image/png,image/jpg,image/bmp,image/webp" data-multiple="true" data-preview-id="p-images"><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
+                                    <span><button type="button" id="fachoose-images" class="btn btn-primary fachoose" data-input-id="c-images" data-mimetype="image/*" data-multiple="true"><i class="fa fa-list"></i> {:__('Choose')}</button></span>
+                                </div>
+                                <span class="msg-box n-right" for="c-images"></span>
+                            </div>
+                            <ul class="row list-inline faupload-preview" id="p-images"></ul>
+                        </div>
+                    </div>
+                    <div id="attributes"></div>
+                    <div class="form-group">
+                        <label class="control-label col-xs-12 col-sm-2">{:__('Brand')}:</label>
+                        <div class="col-xs-12 col-sm-8">
+                            <input id="c-brand_id" data-source="shop/brand/index" class="form-control selectpage" name="row[brand_id]" type="text" value="{$row.brand_id}">
+                        </div>
+                    </div>
+                    <div class="form-group">
+                        <label class="control-label col-xs-12 col-sm-2">{:__('Guarantee')}:</label>
+                        <div class="col-xs-12 col-sm-8">
+                            <input id="c-guarantee_ids" data-source="shop/guarantee/index" data-multiple="true" class="form-control selectpage" name="row[guarantee_ids]" type="text" value="{$row.guarantee_ids}">
+                        </div>
+                    </div>
+                    <div class="form-group">
+                        <label class="control-label col-xs-12 col-sm-2">{:__('Unit')}:</label>
+                        <div class="col-xs-12 col-sm-8">
+                            <input id="c-unit_id" data-source="shop/brand/index" class="form-control selectpage" name="row[unit_id]" type="text" value="{$row.unit_id}">
+                        </div>
+                    </div>
+                    <div class="form-group">
+                        <label class="control-label col-xs-12 col-sm-2">{:__('Supplier')}:</label>
+                        <div class="col-xs-12 col-sm-8">
+                            <input id="c-supplier_id" data-source="shop/brand/index" class="form-control selectpage" name="row[supplier_id]" type="text" value="{$row.supplier_id}">
+                        </div>
+                    </div>
+                 
+                
+                    <div class="form-group">
+                        <label class="control-label col-xs-12 col-sm-2">{:__('Weigh')}:</label>
+                        <div class="col-xs-12 col-sm-8">
+                            <input id="c-weigh" data-rule="required|integer|min:0" class="form-control" name="row[weigh]" type="number" value="{$row.weigh}" min="0">
+                        </div>
+                    </div>
+                    <div class="form-group">
+                        <label class="control-label col-xs-12 col-sm-2">{:__('上架类型')}:</label>
+                        <div class="col-xs-12 col-sm-8">
+
+                            <div class="radio">
+                            {foreach name="onlineTypeList" item="vo"}
+                            <label for="row[online_type]-{$key|htmlentities}"><input id="row[online_type]-{$key|htmlentities}" name="row[online_type]" type="radio" value="{$key|htmlentities}" {in name="key" value="$row.online_type"}checked{/in} /> {$vo|htmlentities}</label>
+                            {/foreach}
+                            </div>
+
+                        </div>
+                    </div>
+                    
+                    <div class="form-group" id="scheduled-online-time" style="display:none;">
+                        <label class="control-label col-xs-12 col-sm-2">{:__('上架时间')}:</label>
+                        <div class="col-xs-12 col-sm-8">
+                            <input id="c-scheduled_online_time" class="form-control datetimepicker" data-date-format="YYYY-MM-DD HH:mm:ss" data-use-current="true" name="row[scheduled_online_time]" type="text" value="{$row.scheduled_online_time}">
+                        </div>
+                    </div>
+                    
+                    <div class="form-group" id="offline-time-setting" style="display:none;">
+                        <label class="control-label col-xs-12 col-sm-2">{:__('定时下架')}:</label>
+                        <div class="col-xs-12 col-sm-8">
+                            <div class="checkbox">
+                                <label for="row[is_auto_offline]">
+                                    <input id="c-is_auto_offline" name="row[is_auto_offline]" type="checkbox" value="1" {in name="1" value="$row.is_auto_offline"}checked{/in}> 
+                                    {:__('自动下架')}
+                                </label>
+                            </div>
+                            <div id="scheduled-offline-time" style="display:none; margin-top:10px;">
+                                <input id="c-scheduled_offline_time" class="form-control datetimepicker" data-date-format="YYYY-MM-DD HH:mm:ss" data-use-current="true" name="row[scheduled_offline_time]" type="text" value="{$row.scheduled_offline_time}">
+                            </div>
+                        </div>
+                    </div>
+                </div>
+                <div class="tab-pane fade" id="skus">
+                    {include file="shop/goods/add_sku" /}
+                </div>
+                <div class="tab-pane fade" id="delivery">
+                    <div class="form-group">
+                        <label class="control-label col-xs-12 col-sm-2">{:__('Delivery_type')}:</label>
+                        <div class="col-xs-12 col-sm-8">
+
+                            <div class="radio">
+                            {foreach name="deliveryTypeList" item="vo"}
+                            <label for="row[delivery_type]-{$key|htmlentities}"><input id="row[delivery_type]-{$key|htmlentities}" data-rule="required" name="row[delivery_type]" type="radio" value="{$key|htmlentities}" {in name="key" value="$row.delivery_type"}checked{/in} /> {$vo|htmlentities}</label>
+                            {/foreach}
+                            </div>
+
+                        </div>
+                    </div>
+                    
+                    <!-- 快递配送设置 - 只有选择物流配送时显示 -->
+                    <div class="form-group" data-favisible="delivery_type=EXPRESS">
+                        <label class="control-label col-xs-12 col-sm-2">{:__('运费设置')}:</label>
+                        <div class="col-xs-12 col-sm-8">
+                            <div class="radio">
+                            {foreach name="expressTypeList" item="vo"}
+                            <label for="row[express_type]-{$key|htmlentities}"><input id="row[express_type]-{$key|htmlentities}" name="row[express_type]" type="radio" value="{$key|htmlentities}" {in name="key" value="$row.express_type"}checked{/in} /> {$vo|htmlentities}</label>
+                            {/foreach}
+                            </div>
+                        </div>
+                    </div>
+                    
+                    <!-- 统一运费 - 选择物流配送且选择统一运费时显示 -->
+                    <div class="form-group" data-favisible="delivery_type=EXPRESS&&express_type=2">
+                        <label class="control-label col-xs-12 col-sm-2">{:__('固定运费')}:</label>
+                        <div class="col-xs-12 col-sm-8">
+                            <div class="input-group">
+                                <input id="c-express_freight" class="form-control" name="row[express_freight]" type="number" value="{$row.express_freight|default='0.00'}" step="0.01" min="0">
+                                <span class="input-group-addon">元</span>
+                            </div>
+                        </div>
+                    </div>
+                    
+                    <!-- 运费模板 - 选择物流配送且选择运费模板时显示,且设为必填 -->
+                    <div class="form-group" data-favisible="delivery_type=EXPRESS&&express_type=3">
+                        <label class="control-label col-xs-12 col-sm-2">{:__('运费模板')}:</label>
+                        <div class="col-xs-12 col-sm-8">
+                            <input id="c-express_template_id" data-rule="required" data-source="shop/freight/index" class="form-control selectpage" name="row[express_template_id]" type="text" value="{$row.express_template_id}">
+                        </div>
+                    </div>
+                </div>
+                <div class="tab-pane fade" id="detail">
+                    <div class="row">
+                        <div class="col-md-6">
+                            <div class="form-group">
+                                <label class="control-label col-xs-12 col-sm-2">{:__('Content')}:</label>
+                                <div class="col-xs-12 col-sm-10">
+                                    <textarea id="c-content" class="form-control editor" rows="15" name="row[content]" cols="50">{$row.content|htmlentities}</textarea>
+                                </div>
+                            </div>
+                        </div>
+                        <div class="col-md-6">
+                            <div class="panel panel-default">
+                                <div class="panel-heading">
+                                    <h3 class="panel-title">{:__('商品详情预览')}</h3>
+                                </div>
+                                <div class="panel-body">
+                                    <div id="content-preview" style="overflow-y: auto; border: 1px solid #e5e5e5; min-height: 450px; padding: 10px;"></div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+                <div class="tab-pane fade" id="params">
+                   
+                </div>
+                <div class="tab-pane fade" id="sales">
+                    <div class="form-group">
+                        <label class="control-label col-xs-12 col-sm-2">{:__('Stock_show_type')}:</label>
+                        <div class="col-xs-12 col-sm-8">
+
+                            <div class="radio">
+                            {foreach name="stockShowTypeList" item="vo"}
+                            <label for="row[stock_show_type]-{$key|htmlentities}"><input id="row[stock_show_type]-{$key|htmlentities}" data-rule="required" name="row[stock_show_type]" type="radio" value="{$key|htmlentities}" {in name="key" value="$row.stock_show_type"}checked{/in} /> {$vo|htmlentities}</label>
+                            {/foreach}
+                            </div>
+
+                        </div>
+                    </div>
+                    <div class="form-group">
+                        <label class="control-label col-xs-12 col-sm-2">{:__('Sales_show_type')}:</label>
+                        <div class="col-xs-12 col-sm-8">
+                            <div class="radio">
+                            {foreach name="salesShowTypeList" item="vo"}
+                            <label for="row[sales_show_type]-{$key|htmlentities}"><input id="row[sales_show_type]-{$key|htmlentities}" data-rule="required" name="row[sales_show_type]" type="radio" value="{$key|htmlentities}" {in name="key" value="$row.sales_show_type"}checked{/in} /> {$vo|htmlentities}</label>
+                            {/foreach}
+                            </div>
+                        </div>
+                    </div>
+                    <div class="form-group">
+                        <label class="control-label col-xs-12 col-sm-2">{:__('Virtual_sales')}:</label>
+                        <div class="col-xs-12 col-sm-8">
+                            <input id="c-virtual_sales" class="form-control" name="row[virtual_sales]" type="number" step="1" min="0" value="{$row.virtual_sales|default='0'}">
+                        </div>
+                    </div>
+                    <div class="form-group">
+                        <label class="control-label col-xs-12 col-sm-2">{:__('Keywords')}:</label>
+                        <div class="col-xs-12 col-sm-8">
+                            <input id="c-keywords" class="form-control" name="row[keywords]" type="text" value="{$row.keywords|htmlentities}">
+                        </div>
+                    </div>
+                    <div class="form-group">
+                        <label class="control-label col-xs-12 col-sm-2">{:__('Description')}:</label>
+                        <div class="col-xs-12 col-sm-8">
+                            <textarea id="c-description" class="form-control" cols="10" rows="5" name="row[description]">{$row.description|htmlentities}</textarea>
+                        </div>
+                    </div>
+
+                </div>
+
+            </div>
+        </div>
+    </div>
+
+    <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>
+    </div>
+</form>
+
+{include file="shop/goods/attributetpl" /}
+{include file="shop/common/cardtpl" /} 

+ 44 - 30
application/api/controller/Goods.php

@@ -4,16 +4,15 @@ namespace app\api\controller;
 
 use app\common\model\Goods as GoodsModel;
 use app\common\model\Guarantee;
-use app\common\model\SkuSpec;
 use app\common\model\Collect;
 use app\common\model\Comment;
 use app\common\model\AttributeValue;
-use app\common\model\Coupon;
-use app\common\model\CouponCondition;
-use app\common\model\UserCoupon;
+// use app\common\model\Coupon;
+// use app\common\model\CouponCondition;
+// use app\common\model\UserCoupon;
 use fast\Http;
 use think\Log;
-
+use app\common\Service\SkuSpec as SkuSpecService;
 /**
  * 商品接口
  */
@@ -25,7 +24,6 @@ class Goods extends Base
     public function index()
     {
         $hots = GoodsModel::where('status', 'normal')
-            ->where("FIND_IN_SET('hot',`flag`)")
             ->order('weigh desc')
             ->limit(12)
             ->cache(false)
@@ -72,6 +70,7 @@ class Goods extends Base
         if (!$row) {
             $this->error('未找到该商品');
         }
+        // 浏览次数
         $row->setInc('views');
         //收藏
         if ($this->auth->isLogin()) {
@@ -79,7 +78,21 @@ class Goods extends Base
         } else {
             $row->is_collect = false;
         }
-        $row->sku_spec = SkuSpec::getGoodsSkuSpec($id);
+        $row->sku_spec = SkuSpecService::getGoodsSkuSpec($id);
+        //  要处理规格的图片
+        // 这个错误是因为 $row->sku_spec 是一个重载属性(通过魔术方法 __get() 获取),不能直接通过引用修改。我们需要先将其转换为普通数组,处理后再赋值回去。
+        if (!empty($row->sku_spec)) {
+            $skuSpecData = $row->sku_spec;  // 先转换为普通数组
+            foreach ($skuSpecData as $key => &$item) {
+                if (!empty($item['sku_value'])) {
+                    foreach ($item['sku_value'] as $k => &$v) {  
+                        $v['image'] = empty($v['image']) ? '' : cdnurl($v['image'], true);
+                    }
+                }
+            }
+            $row->sku_spec = $skuSpecData;  // 处理完后重新赋值
+        }
+
         //服务保障
         $row->guarantee = $row->guarantee_ids ? Guarantee::field('id,name,intro')->where('id', 'IN', $row->guarantee_ids)->where('status', 'normal')->select() : [];
         //属性
@@ -97,31 +110,32 @@ class Goods extends Base
         $row->setRelation('comment', $comment);
         unset($item);
         //优惠券
-        $conditions = CouponCondition::getGoodsCondition($id, $row->category_id, $row->brand_id);
-        $sql = "condition_ids IS NULL OR condition_ids=''";
-        foreach ($conditions as $key => $item) {
-            $sql .= " OR FIND_IN_SET('{$item['id']}',condition_ids)";
-        }
-        $couponList = Coupon::field('id,name,result,result_data,allow_num,begintime,endtime,use_times,received_num,give_num,mode,createtime')
-            ->where($sql)
-            ->where('is_open', 1)
-            ->where('is_private', 'no')
-            ->where('endtime', '>', time())
-            ->select();
-        //已经登录,渲染已领的优惠券
-        $coupon_ids = [];
-        if ($this->auth->isLogin()) {
-            $coupon_ids = UserCoupon::where('user_id', $this->auth->id)->column('coupon_id');
-        }
-        foreach ($couponList as $key => &$item) {
-            Coupon::render($item, $coupon_ids);
-            $item->hidden(['received_num', 'give_num', 'condition_ids']);
-        }
-        $row->coupon = $couponList;
+        // $conditions = CouponCondition::getGoodsCondition($id, $row->category_id, $row->brand_id);
+        // $sql = "condition_ids IS NULL OR condition_ids=''";
+        // foreach ($conditions as $key => $item) {
+        //     $sql .= " OR FIND_IN_SET('{$item['id']}',condition_ids)";
+        // }
+        // $couponList = Coupon::field('id,name,result,result_data,allow_num,begintime,endtime,use_times,received_num,give_num,mode,createtime')
+        //     ->where($sql)
+        //     ->where('is_open', 1)
+        //     ->where('is_private', 'no')
+        //     ->where('endtime', '>', time())
+        //     ->select();
+        // //已经登录,渲染已领的优惠券
+        // $coupon_ids = [];
+        // if ($this->auth->isLogin()) {
+        //     $coupon_ids = UserCoupon::where('user_id', $this->auth->id)->column('coupon_id');
+        // }
+        // foreach ($couponList as $key => &$item) {
+        //     Coupon::render($item, $coupon_ids);
+        //     $item->hidden(['received_num', 'give_num', 'condition_ids']);
+        // }
+        // $row->coupon = $couponList;
 
-        $row->visible(explode(',', 'id,title,subtitle,category_id,price,marketprice,sales,views,image,content,images,sku_spec,sku,comment,is_collect,guarantee,attributes,favor_rate,coupon'));
+        $row->visible(explode(',', 'id,title,subtitle,category_id,price,marketprice,sales,views,
+        image,content,images,sku_spec,sku,comment,is_collect,guarantee,attributes,favor_rate,coupon'));
         $row = $row->toArray();
-        $row['content'] = \addons\shop\library\Service::formatTplToUniapp($row['content']);
+        $row['content'] = \app\common\library\Service::formatTplToUniapp($row['content']);
         $this->success('获取成功', $row);
     }
 

+ 9 - 21
application/common/model/SkuSpec.php

@@ -20,25 +20,6 @@ class SkuSpec extends Model
     // 追加属性
     protected $append = [];
 
-    /**
-     * 获取指定商品的SKU信息
-     * @param int $goods_id 商品ID
-     * @return array
-     */
-    public static function getGoodsSkuSpec($goods_id)
-    {
-        $list = (new self)->field('MIN(`id`) AS `id`, MIN(`goods_id`) AS `goods_id`, `spec_id`')->where('goods_id', $goods_id)
-            ->with([
-                'Spec',
-                'SkuValue' => function ($query) use ($goods_id) {
-                    $query->where('goods_id', $goods_id)->field('id,goods_id,spec_id,spec_value_id')->with(['SpecValue']);
-                }
-            ])->group('spec_id')->select();
-
-        $list = collection($list)->toArray();
-        return $list;
-    }
-
     public function SkuValue()
     {
         return $this->hasMany('SkuSpec', 'spec_id', 'spec_id');
@@ -46,11 +27,18 @@ class SkuSpec extends Model
 
     public function Spec()
     {
-        return $this->hasOne('Spec', 'id', 'spec_id', [], 'LEFT')->bind(['title' => 'name']);
+        return $this->hasOne('Spec', 'id', 'spec_id', [], 'LEFT')
+        ->bind(['title' => 'name']);
     }
 
     public function SpecValue()
     {
-        return $this->hasOne('SpecValue', 'id', 'spec_value_id', [], 'LEFT')->bind(['title' => 'value']);
+        return $this->hasOne('SpecValue', 'id', 'spec_value_id', [], 'LEFT')
+         ->bind([
+            'title' => 'value',
+            'image' => 'image',
+            'desc' => 'desc'
+
+        ]);
     }
 }

+ 1 - 1
application/common/service/Activity.php

@@ -1,6 +1,6 @@
 <?php
 
-namespace app\common\service;
+namespace app\common\Service;
 
 use app\common\Enum\StatusEnum;
 use app\common\Enum\ActivityEnum;

+ 31 - 0
application/common/service/SkuSpec.php

@@ -0,0 +1,31 @@
+<?php
+
+namespace app\common\Service;
+
+use app\common\model\SkuSpec as SkuSpecModel;
+
+ class SkuSpec
+ {
+    /**
+     * 获取指定商品的SKU信息
+     * @param int $goods_id 商品ID
+     * @return array
+     */
+    public static function getGoodsSkuSpec($goods_id)
+    {
+        $list = (new SkuSpecModel())
+             ->field('MIN(`id`) AS `id`, MIN(`goods_id`) AS `goods_id`, `spec_id`')
+             ->where('goods_id', $goods_id)
+             ->with([
+                'Spec',
+                'SkuValue' => function ($query) use ($goods_id) {
+                    $query->where('goods_id', $goods_id)
+                    ->field('id,goods_id,spec_id,spec_value_id')
+                    ->with(['SpecValue']);
+                }
+            ])->group('spec_id')->select();
+
+        $list = collection($list)->toArray();
+        return $list;
+    }
+ }

+ 2 - 1
package.json

@@ -105,5 +105,6 @@
       "*.js",
       "build/**"
     ]
-  }
+  },
+  "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
 }

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


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