feat: 添加场馆和分区选择器 + specTypeList 支持

- SeatSkuService: 返回 specTypeList 包含所有4维规格
- Goods.php: 注入 specTypeList
- ticket_detail.html:
  - 添加 venueSelector 和 sectionSelector HTML 容器
  - 添加 renderAllSelectors() 渲染场次/场馆/分区
  - 添加 selectVenue/selectSection/filterSeats 函数
- CSS: 添加规格选择器样式
pull/19/head
Council 2026-04-21 13:02:38 +08:00
parent fc07c2ece6
commit de9134773f
4 changed files with 168 additions and 5 deletions

View File

@ -143,6 +143,7 @@ class Goods extends Common
'vr_seat_template' => $viewData['vr_seat_template'] ?? null, 'vr_seat_template' => $viewData['vr_seat_template'] ?? null,
'goods_spec_data' => $viewData['goods_spec_data'] ?? [], 'goods_spec_data' => $viewData['goods_spec_data'] ?? [],
'seatSpecMap' => $viewData['seatSpecMap'] ?? [], 'seatSpecMap' => $viewData['seatSpecMap'] ?? [],
'specTypeList' => $viewData['specTypeList'] ?? [],
]); ]);
// 使用绝对路径 + fetch() 方法,让 Think 驱动正确解析 include 路径 // 使用绝对路径 + fetch() 方法,让 Think 驱动正确解析 include 路径
$tplFile = ROOT . 'app' . DS . 'plugins' . DS . 'vr_ticket' . DS . 'view' . DS . 'goods' . DS . 'ticket_detail.html'; $tplFile = ROOT . 'app' . DS . 'plugins' . DS . 'vr_ticket' . DS . 'view' . DS . 'goods' . DS . 'ticket_detail.html';

View File

@ -422,8 +422,31 @@ class SeatSkuService extends BaseService
} }
} }
// ========== 新增:构建 seatSpecMap ========== // ========== 构建规格类型列表4维场馆、分区、座位号、场次==========
$seatSpecMap = self::buildSeatSpecMap($goodsId, $seatTemplate); // 从 GoodsSpecType 读取所有维度定义
$specTypeList = [];
$specTypes = \think\facade\Db::name('GoodsSpecType')
->where('goods_id', $goodsId)
->order('id', 'asc')
->select()
->toArray();
foreach ($specTypes as $type) {
$dimName = $type['name'] ?? '';
$values = json_decode($type['value'] ?? '[]', true);
$options = [];
foreach ($values as $v) {
if (isset($v['name'])) {
$options[] = $v['name'];
}
}
if (!empty($dimName) && !empty($options)) {
$specTypeList[$dimName] = [
'name' => $dimName,
'options' => $options,
];
}
}
// ========== 构建场次列表goods_spec_data========== // ========== 构建场次列表goods_spec_data==========
$sessions = $config['sessions'] ?? []; $sessions = $config['sessions'] ?? [];
@ -450,7 +473,7 @@ class SeatSkuService extends BaseService
} }
$goodsSpecData[] = [ $goodsSpecData[] = [
'spec_id' => 0, // 不再需要 spec_id前端用 seatSpecMap 'spec_id' => 0,
'spec_name' => $timeRange, 'spec_name' => $timeRange,
'price' => $sessionPrice ?? floatval($goods['price'] ?? 0), 'price' => $sessionPrice ?? floatval($goods['price'] ?? 0),
'start' => $start, 'start' => $start,
@ -484,6 +507,7 @@ class SeatSkuService extends BaseService
'vr_seat_template' => $seatTemplate ?: null, 'vr_seat_template' => $seatTemplate ?: null,
'goods_spec_data' => $goodsSpecData, 'goods_spec_data' => $goodsSpecData,
'seatSpecMap' => $seatSpecMap, 'seatSpecMap' => $seatSpecMap,
'specTypeList' => $specTypeList, // 4维规格类型列表
'goods_config' => $config, 'goods_config' => $config,
]; ];
} }

View File

@ -102,3 +102,16 @@
.vr-goods-info { background: #fff; border: 1px solid #e8e8e8; border-radius: 8px; padding: 15px; margin-bottom: 20px; } .vr-goods-info { background: #fff; border: 1px solid #e8e8e8; border-radius: 8px; padding: 15px; margin-bottom: 20px; }
.vr-goods-photos { display: grid; grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); gap: 8px; margin-bottom: 15px; } .vr-goods-photos { display: grid; grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); gap: 8px; margin-bottom: 15px; }
.vr-goods-photos img { width: 100%; aspect-ratio: 1; object-fit: cover; border-radius: 4px; } .vr-goods-photos img { width: 100%; aspect-ratio: 1; object-fit: cover; border-radius: 4px; }
/* 规格选择器样式 */
.vr-spec-selector { margin-bottom: 15px; }
.vr-spec-label { font-size: 14px; font-weight: bold; color: #333; margin-bottom: 10px; }
.vr-spec-options { display: flex; flex-wrap: wrap; gap: 8px; }
.vr-spec-option {
border: 1px solid #ddd; border-radius: 6px; padding: 8px 16px;
cursor: pointer; font-size: 13px; color: #333;
transition: all 0.15s;
}
.vr-spec-option:hover { border-color: #409eff; }
.vr-spec-option.selected { border-color: #409eff; background: #ecf5ff; color: #409eff; }

View File

@ -25,6 +25,18 @@
</div> </div>
</div> </div>
<!-- 场馆选择 -->
<div class="vr-seat-section" id="venueSection">
<div class="vr-section-title">选择场馆</div>
<div id="venueSelector"><!-- 由 JS 动态渲染 --></div>
</div>
<!-- 分区选择 -->
<div class="vr-seat-section" id="sectionSection">
<div class="vr-section-title">选择分区</div>
<div id="sectionSelector"><!-- 由 JS 动态渲染 --></div>
</div>
<!-- 座位图 --> <!-- 座位图 -->
<div class="vr-seat-section" id="seatSection" style="display:none"> <div class="vr-seat-section" id="seatSection" style="display:none">
<div class="vr-section-title">选择座位 <span <div class="vr-section-title">选择座位 <span
@ -80,6 +92,7 @@
// vr_seat_template already contains the decoded seat_map (venue, rooms, sections, seats) // vr_seat_template already contains the decoded seat_map (venue, rooms, sections, seats)
seatMap: <?php echo json_encode($vr_seat_template ?? []); ?>, seatMap: <?php echo json_encode($vr_seat_template ?? []); ?>,
seatSpecMap: <?php echo json_encode($seatSpecMap ?? []); ?>, seatSpecMap: <?php echo json_encode($seatSpecMap ?? []); ?>,
specTypeList: <?php echo json_encode($specTypeList ?? []); ?>, // 4维规格类型列表
selectedSeats: [], // [{seatKey, price, rowLabel, colNum, section}] selectedSeats: [], // [{seatKey, price, rowLabel, colNum, section}]
soldSeats: { }, // {seatKey: true} soldSeats: { }, // {seatKey: true}
currentSession: null, // 当前选中场次 value (e.g. "15:00-16:59") currentSession: null, // 当前选中场次 value (e.g. "15:00-16:59")
@ -87,11 +100,123 @@
currentSection: null, // 当前选中分区 char currentSection: null, // 当前选中分区 char
init: function() { init: function() {
this.renderSessions(); this.renderAllSelectors();
this.bindEvents(); this.bindEvents();
}, },
// 渲染场次列表 // 渲染所有选择器(场次、场馆、分区)
renderAllSelectors: function() {
var specTypeList = this.specTypeList;
var goodsSpecData = <?php echo json_encode($goods_spec_data ?? []); ?> || [];
// 1. 渲染场次选择器
var sessionHtml = '';
if (goodsSpecData.length > 0) {
goodsSpecData.forEach(function (spec) {
sessionHtml += '<div class="vr-session-item" data-session="' + spec.spec_name + '" onclick="vrTicketApp.selectSession(this)">' +
'<div class="date">' + spec.spec_name + '</div>' +
'<div class="price">¥' + spec.price + '</div></div>';
});
} else if (specTypeList['$vr-场次']) {
specTypeList['$vr-场次'].options.forEach(function(session) {
sessionHtml += '<div class="vr-session-item" data-session="' + session + '" onclick="vrTicketApp.selectSession(this)">' +
'<div class="date">' + session + '</div>' +
'<div class="price">¥0</div></div>';
});
}
document.getElementById('sessionGrid').innerHTML = sessionHtml;
// 2. 渲染场馆选择器
var venueHtml = '<div class="vr-spec-selector"><div class="vr-spec-label">选择场馆</div><div class="vr-spec-options">';
if (specTypeList['$vr-场馆']) {
specTypeList['$vr-场馆'].options.forEach(function(venue) {
venueHtml += '<div class="vr-spec-option" data-venue="' + venue + '" onclick="vrTicketApp.selectVenue(this)">' + venue + '</div>';
});
}
venueHtml += '</div></div>';
document.getElementById('venueSelector').innerHTML = venueHtml;
// 3. 渲染分区选择器
var sectionHtml = '<div class="vr-spec-selector"><div class="vr-spec-label">选择分区</div><div class="vr-spec-options">';
if (specTypeList['$vr-分区']) {
specTypeList['$vr-分区'].options.forEach(function(section) {
sectionHtml += '<div class="vr-spec-option" data-section="' + section + '" onclick="vrTicketApp.selectSection(this)">' + section + '</div>';
});
}
sectionHtml += '</div></div>';
document.getElementById('sectionSelector').innerHTML = sectionHtml;
},
// 选择场馆
selectVenue: function(el) {
document.querySelectorAll('#venueSelector .vr-spec-option').forEach(function (item) {
item.classList.remove('selected');
});
el.classList.add('selected');
this.currentVenue = el.dataset.venue;
this.filterSeats();
},
// 选择分区
selectSection: function(el) {
document.querySelectorAll('#sectionSelector .vr-spec-option').forEach(function (item) {
item.classList.remove('selected');
});
el.classList.add('selected');
this.currentSection = el.dataset.section;
this.filterSeats();
},
// 根据选择过滤座位
filterSeats: function() {
var self = this;
document.querySelectorAll('.vr-seat.available').forEach(function(el) {
var seatKey = el.dataset.seatKey;
var seatInfo = self.seatSpecMap[seatKey] || {};
var matchSession = true, matchVenue = true, matchSection = true;
if (self.currentSession) {
matchSession = false;
for (var i = 0; i < seatInfo.spec.length; i++) {
if (seatInfo.spec[i].type === '$vr-场次' && seatInfo.spec[i].value === self.currentSession) {
matchSession = true;
break;
}
}
}
if (self.currentVenue) {
matchVenue = false;
for (var i = 0; i < seatInfo.spec.length; i++) {
if (seatInfo.spec[i].type === '$vr-场馆' && seatInfo.spec[i].value === self.currentVenue) {
matchVenue = true;
break;
}
}
}
if (self.currentSection) {
matchSection = false;
for (var i = 0; i < seatInfo.spec.length; i++) {
if (seatInfo.spec[i].type === '$vr-分区' && seatInfo.spec[i].value === self.currentSection) {
matchSection = true;
break;
}
}
}
if (matchSession && matchVenue && matchSection && seatInfo.inventory > 0) {
el.style.opacity = '1';
el.style.pointerEvents = 'auto';
} else {
el.style.opacity = '0.3';
el.style.pointerEvents = 'none';
}
});
},
// 渲染场次列表(保留向后兼容)
renderSessions: function() { renderSessions: function() {
var specData = <?php echo json_encode($goods_spec_data ?? []); ?> || []; var specData = <?php echo json_encode($goods_spec_data ?? []); ?> || [];
var html = ''; var html = '';