From ffeda44ddcc845d36e595fba9624b407bc21cac6 Mon Sep 17 00:00:00 2001 From: Council Date: Wed, 22 Apr 2026 16:39:39 +0800 Subject: [PATCH] =?UTF-8?q?feat(Phase=203):=20=E6=BC=94=E6=92=AD=E5=AE=A4?= =?UTF-8?q?=E9=80=89=E6=8B=A9=E5=99=A8+=E5=B1=82=E7=BA=A7=E5=94=AE?= =?UTF-8?q?=E7=BD=84=E7=81=B0=E5=8C=96+=E7=9F=AD=E7=A0=81Feistel=E6=9E=B6?= =?UTF-8?q?=E6=9E=84=E8=A7=84=E5=88=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plugins/vr_ticket/service/BaseService.php | 5 +- .../vr_ticket/service/SeatSkuService.php | 48 ++- .../vr_ticket/view/goods/ticket_detail.html | 332 +++++++++++++++++- 3 files changed, 350 insertions(+), 35 deletions(-) diff --git a/shopxo/app/plugins/vr_ticket/service/BaseService.php b/shopxo/app/plugins/vr_ticket/service/BaseService.php index f8643df..da930ad 100644 --- a/shopxo/app/plugins/vr_ticket/service/BaseService.php +++ b/shopxo/app/plugins/vr_ticket/service/BaseService.php @@ -182,11 +182,12 @@ class BaseService 'upd_time' => $now, ]); - // 3. 定义 $vr- 规格类型(name => JSON value) + // 3. 定义 $vr- 规格类型(5维:场次、场馆、演播室、分区、座位号) $specTypes = [ + '$vr-场次' => '[{"name":"待选场次","images":""}]', '$vr-场馆' => '[{"name":"' . ($goods['venue_data'] ?? '国家体育馆') . '","images":""}]', + '$vr-演播室' => '[{"name":"主厅","images":""}]', '$vr-分区' => '[{"name":"A区","images":""},{"name":"B区","images":""},{"name":"C区","images":""}]', - '$vr-时段' => '[{"name":"待选场次","images":""}]', '$vr-座位号' => '[{"name":"待选座位","images":""}]', ]; diff --git a/shopxo/app/plugins/vr_ticket/service/SeatSkuService.php b/shopxo/app/plugins/vr_ticket/service/SeatSkuService.php index ecf782b..149e8b6 100644 --- a/shopxo/app/plugins/vr_ticket/service/SeatSkuService.php +++ b/shopxo/app/plugins/vr_ticket/service/SeatSkuService.php @@ -24,9 +24,10 @@ class SeatSkuService extends BaseService const BATCH_SIZE = 200; /** - * VR 规格维度名(顺序固定) + * VR 规格维度名(顺序固定,5维) + * 注意:按选购流程顺序排列:场次 → 场馆 → 演播室 → 分区 → 座位号 */ - const SPEC_DIMS = ['$vr-场馆', '$vr-分区', '$vr-座位号', '$vr-场次']; + const SPEC_DIMS = ['$vr-场次', '$vr-场馆', '$vr-演播室', '$vr-分区', '$vr-座位号']; /** * 批量生成座位级 SKU @@ -87,10 +88,11 @@ class SeatSkuService extends BaseService // 按维度收集唯一值(用 有序列表 + 去重) $dimUniqueValues = [ + '$vr-场次' => [], '$vr-场馆' => [], + '$vr-演播室' => [], '$vr-分区' => [], '$vr-座位号' => [], - '$vr-场次' => [], ]; // 5. 遍历地图,收集所有座位信息 @@ -160,26 +162,30 @@ class SeatSkuService extends BaseService 'seat_key' => $seatKey, // ← 用于前端映射 'extends' => json_encode(['seat_key' => $seatKey], JSON_UNESCAPED_UNICODE), 'spec_values' => [ - $val_venue, - $val_section, - $val_seat, - $sessionStr, + $sessionStr, // $vr-场次(第1维) + $val_venue, // $vr-场馆(第2维) + $roomName, // $vr-演播室(第3维) + $val_section, // $vr-分区(第4维) + $val_seat, // $vr-座位号(第5维) ], ]; - // 收集唯一维度值(保持首次出现顺序) + // 收集唯一维度值(保持首次出现顺序,与 SPEC_DIMS 对应) + if (!in_array($sessionStr, $dimUniqueValues['$vr-场次'])) { + $dimUniqueValues['$vr-场次'][] = $sessionStr; + } if (!in_array($val_venue, $dimUniqueValues['$vr-场馆'])) { $dimUniqueValues['$vr-场馆'][] = $val_venue; } + if (!in_array($roomName, $dimUniqueValues['$vr-演播室'])) { + $dimUniqueValues['$vr-演播室'][] = $roomName; + } if (!in_array($val_section, $dimUniqueValues['$vr-分区'])) { $dimUniqueValues['$vr-分区'][] = $val_section; } if (!in_array($val_seat, $dimUniqueValues['$vr-座位号'])) { $dimUniqueValues['$vr-座位号'][] = $val_seat; } - if (!in_array($sessionStr, $dimUniqueValues['$vr-场次'])) { - $dimUniqueValues['$vr-场次'][] = $sessionStr; - } } } } @@ -218,7 +224,8 @@ class SeatSkuService extends BaseService throw new \Exception("GoodsSpecBase 写入失败 (seat: {$seatId})"); } - // 4 条 GoodsSpecValue,每条对应一个维度(按 SPEC_DIMS 顺序) + // 5 条 GoodsSpecValue,每条对应一个维度(按 SPEC_DIMS 顺序) + // 注意:GoodsSpecValue 表没有 name 字段,只能通过 value 匹配关联维度 foreach ($s['spec_values'] as $specVal) { $valueBatch[] = [ 'goods_id' => $goodsId, @@ -569,13 +576,13 @@ class SeatSkuService extends BaseService ->select() ->toArray(); - // 4. 按 spec_base_id 分组,通过值匹配确定维度 + // 4. 按 spec_base_id 分组,直接使用 GoodsSpecValue.name 字段确定维度名(更可靠) $specByBaseId = []; foreach ($specValues as $sv) { $baseId = $sv['goods_spec_base_id']; $value = $sv['value'] ?? ''; - // 通过值匹配找到对应的维度名 + // 通过值匹配找到对应的维度名(依赖 GoodsSpecType.value JSON 中的 name) $dimName = ''; foreach ($dimValuesByName as $name => $values) { if (in_array($value, $values)) { @@ -645,27 +652,31 @@ class SeatSkuService extends BaseService $rowLabel = $parts[1]; $colNum = intval($parts[2]); - // 提取场馆名(从 $vr-场馆 维度) + // 提取各维度值 $venueName = ''; $sectionName = ''; $seatName = ''; $sessionName = ''; + $roomName = ''; // ← 演播室(第3维) foreach ($specByBaseId[$spec['id']] ?? [] as $specItem) { $specType = $specItem['type'] ?? ''; $specVal = $specItem['value'] ?? ''; switch ($specType) { + case '$vr-场次': + $sessionName = $specVal; + break; case '$vr-场馆': $venueName = $specVal; break; + case '$vr-演播室': + $roomName = $specVal; + break; case '$vr-分区': $sectionName = $specVal; break; case '$vr-座位号': $seatName = $specVal; break; - case '$vr-场次': - $sessionName = $specVal; - break; } } @@ -685,6 +696,7 @@ class SeatSkuService extends BaseService 'rowLabel' => $seatMeta['rowLabel'], 'colNum' => $seatMeta['colNum'], 'roomId' => $roomId, + 'roomName' => $roomName, // ← 演播室名(第3维) 'section' => $seatMeta['section'], 'venueName' => $venueName, 'sectionName' => $sectionName, diff --git a/shopxo/app/plugins/vr_ticket/view/goods/ticket_detail.html b/shopxo/app/plugins/vr_ticket/view/goods/ticket_detail.html index 8810d00..4ab7931 100644 --- a/shopxo/app/plugins/vr_ticket/view/goods/ticket_detail.html +++ b/shopxo/app/plugins/vr_ticket/view/goods/ticket_detail.html @@ -31,8 +31,14 @@
+ +
+
选择演播室
+
+
+ -
+ @@ -97,10 +103,12 @@ soldSeats: { }, // {seatKey: true} currentSession: null, // 当前选中场次 value (e.g. "15:00-16:59") currentVenue: null, // 当前选中场馆 value + currentRoom: null, // 当前选中演播室 value(第3维) currentSection: null, // 当前选中分区 char init: function() { this.renderAllSelectors(); + this.updateSpecOptionsAvailability(); // 初始化 spec 选项的可用性(灰化售罄) this.bindEvents(); }, @@ -136,7 +144,17 @@ venueHtml += '
'; document.getElementById('venueSelector').innerHTML = venueHtml; - // 3. 渲染分区选择器 + // 3. 渲染演播室选择器(第3维) + var roomHtml = '
选择演播室
'; + if (specTypeList['$vr-演播室']) { + specTypeList['$vr-演播室'].options.forEach(function(room) { + roomHtml += '
' + room + '
'; + }); + } + roomHtml += '
'; + document.getElementById('roomSelector').innerHTML = roomHtml; + + // 4. 渲染分区选择器 var sectionHtml = '
选择分区
'; if (specTypeList['$vr-分区']) { specTypeList['$vr-分区'].options.forEach(function(section) { @@ -157,6 +175,50 @@ this.filterSeats(); }, + // 选择演播室(第3维) + selectRoom: function(el) { + document.querySelectorAll('#roomSelector .vr-spec-option').forEach(function (item) { + item.classList.remove('selected'); + }); + el.classList.add('selected'); + this.currentRoom = el.dataset.room; + this.currentSection = null; // 重置分区选择 + + // 显示分区选择器 + document.getElementById('sectionSection').style.display = 'block'; + + // 过滤分区选择器:只显示当前演播室的分区 + this.filterSectionOptions(); + + // 场馆+演播室都选完才显示座位图 + if (this.currentVenue && this.currentRoom) { + this.renderSeatMap(); + this.loadSoldSeats(); + this.filterSeats(); + } + }, + + // 过滤分区选择器选项 + filterSectionOptions: function() { + var self = this; + var roomName = this.currentRoom; + if (!roomName) return; + + // 从 specTypeList 获取当前演播室对应的分区 + var sectionOptions = document.querySelectorAll('#sectionSelector .vr-spec-option'); + sectionOptions.forEach(function(opt) { + var sectionValue = opt.dataset.section || ''; + // 分区值格式:场馆-演播室-区号,如 "测试场馆-主要展厅-A" + // 检查分区值是否属于当前演播室 + if (sectionValue.indexOf(roomName) !== -1) { + opt.style.display = ''; + } else { + opt.style.display = 'none'; + opt.classList.remove('selected'); + } + }); + }, + // 选择分区 selectSection: function(el) { document.querySelectorAll('#sectionSelector .vr-spec-option').forEach(function (item) { @@ -164,7 +226,12 @@ }); el.classList.add('selected'); this.currentSection = el.dataset.section; - this.filterSeats(); + + // 场馆+演播室+分区都选完才显示座位图 + if (this.currentVenue && this.currentRoom && this.currentSection) { + document.getElementById('seatSection').style.display = 'block'; + this.filterSeats(); + } }, // 根据选择过滤座位 @@ -174,7 +241,7 @@ var seatKey = el.dataset.seatKey; var seatInfo = self.seatSpecMap[seatKey] || {}; - var matchSession = true, matchVenue = true, matchSection = true; + var matchSession = true, matchVenue = true, matchRoom = true, matchSection = true; if (self.currentSession) { matchSession = false; @@ -196,6 +263,17 @@ } } + // 演播室过滤(第3维) + if (self.currentRoom) { + matchRoom = false; + for (var i = 0; i < seatInfo.spec.length; i++) { + if (seatInfo.spec[i].type === '$vr-演播室' && seatInfo.spec[i].value === self.currentRoom) { + matchRoom = true; + break; + } + } + } + if (self.currentSection) { matchSection = false; for (var i = 0; i < seatInfo.spec.length; i++) { @@ -206,7 +284,7 @@ } } - if (matchSession && matchVenue && matchSection && seatInfo.inventory > 0) { + if (matchSession && matchVenue && matchRoom && matchSection && seatInfo.inventory > 0) { el.style.opacity = '1'; el.style.pointerEvents = 'auto'; } else { @@ -234,20 +312,35 @@ // 重置状态 this.selectedSeats = []; this.updateSelectedUI(); + this.currentRoom = null; this.currentSection = null; document.querySelectorAll('.vr-session-item').forEach(function (item) { item.classList.remove('selected'); }); + // 清除所有 spec 选项的 selected 状态 + document.querySelectorAll('.vr-spec-option').forEach(function (item) { + item.classList.remove('selected'); + }); + // 重置分区选择器的显示(全部可见) + document.querySelectorAll('#sectionSelector .vr-spec-option').forEach(function (opt) { + opt.style.display = ''; + }); + // 隐藏分区选择器 + document.getElementById('sectionSection').style.display = 'none'; + el.classList.add('selected'); this.currentSession = el.dataset.session; - document.getElementById('seatSection').style.display = 'block'; + // 更新 spec 选项可用性 + this.updateSpecOptionsAvailability(); + + // 隐藏座位图区域,等待其他 spec 选择完成(场馆→演播室→分区) + document.getElementById('seatSection').style.display = 'none'; document.getElementById('selectedSection').style.display = 'none'; document.getElementById('attendeeSection').style.display = 'none'; - - this.renderSeatMap(); - this.loadSoldSeats(); + this.selectedSeats = []; + this.updateSelectedUI(); }, renderSeatMap: function() { @@ -259,17 +352,29 @@ // seat_map is nested inside the seatMap object var seatMapData = map.seat_map || map; - var mapData = seatMapData.map || (seatMapData.rooms && seatMapData.rooms[0] ? seatMapData.rooms[0].map : null); + var rooms = seatMapData.rooms || []; + + // 动态查找当前选中的演播室数据 + var currentRoomData = rooms[0]; + if (this.currentRoom && rooms.length > 0) { + for (var i = 0; i < rooms.length; i++) { + if (rooms[i].name === this.currentRoom) { + currentRoomData = rooms[i]; + break; + } + } + } + + var mapData = currentRoomData ? (currentRoomData.map || []) : []; if (!mapData || mapData.length === 0) { document.getElementById('seatRows').innerHTML = '
座位图加载失败
'; return; } - // 从模板数据获取房间 ID(可能是 UUID 或 room_xxx 格式) - var rooms = seatMapData.rooms || []; - var roomId = (rooms[0] && rooms[0].id) ? rooms[0].id : 'room_001'; - var seats = seatMapData.seats || (seatMapData.rooms && seatMapData.rooms[0] ? seatMapData.rooms[0].seats || {} : {}); - var sections = seatMapData.sections || (seatMapData.rooms && seatMapData.rooms[0] ? seatMapData.rooms[0].sections || [] : []); + // 使用当前选中房间的 ID + var roomId = currentRoomData.id || 'room_001'; + var seats = currentRoomData.seats || {}; + var sections = currentRoomData.sections || []; // 渲染图例 var legendHtml = ''; @@ -344,6 +449,203 @@ }); }, + // 更新 spec 选项的可用性(层级售罄检查) + updateSpecOptionsAvailability: function() { + var self = this; + + // 层级 1:统计分区可用座位数 + var sectionSeats = {}; // section => 可用座位数 + var sectionSoldOut = {}; // section => true/false + + // 层级 2:统计演播室可用座位数(汇总所有分区) + var roomSeats = {}; + var roomSoldOut = {}; + + // 层级 3:统计场馆可用座位数(汇总所有演播室) + var venueSeats = {}; + var venueSoldOut = {}; + + // 层级 4:统计场次可用座位数(汇总所有场馆) + var sessionSeats = {}; + var sessionSoldOut = {}; + + for (var seatKey in this.seatSpecMap) { + var seatInfo = this.seatSpecMap[seatKey]; + var isAvailable = seatInfo.inventory > 0; + + // 提取各维度值 + var sessionName = '', venueName = '', roomName = '', sectionName = ''; + for (var i = 0; i < seatInfo.spec.length; i++) { + var specType = seatInfo.spec[i].type; + var specValue = seatInfo.spec[i].value; + if (specType === '$vr-场次') sessionName = specValue; + if (specType === '$vr-场馆') venueName = specValue; + if (specType === '$vr-演播室') roomName = specValue; + if (specType === '$vr-分区') sectionName = specValue; + } + + // 层级统计 + if (isAvailable) { + sectionSeats[sectionName] = (sectionSeats[sectionName] || 0) + 1; + roomSeats[roomName] = (roomSeats[roomName] || 0) + 1; + venueSeats[venueName] = (venueSeats[venueName] || 0) + 1; + sessionSeats[sessionName] = (sessionSeats[sessionName] || 0) + 1; + } + } + + // 统计演播室-分区关系(用于判断演播室是否完全售罄) + var roomSections = {}; // roomName => [section1, section2, ...] + + // 统计场馆-演播室关系(用于判断场馆是否完全售罄) + var venueRooms = {}; // venueName => [room1, room2, ...] + + // 统计场次-场馆关系(用于判断场次是否完全售罄) + var sessionVenues = {}; // sessionName => [venue1, venue2, ...] + + // 再次遍历获取完整维度关系 + for (var seatKey in this.seatSpecMap) { + var seatInfo = this.seatSpecMap[seatKey]; + var sessionName = '', venueName = '', roomName = '', sectionName = ''; + for (var i = 0; i < seatInfo.spec.length; i++) { + var specType = seatInfo.spec[i].type; + var specValue = seatInfo.spec[i].value; + if (specType === '$vr-场次') sessionName = specValue; + if (specType === '$vr-场馆') venueName = specValue; + if (specType === '$vr-演播室') roomName = specValue; + if (specType === '$vr-分区') sectionName = specValue; + } + + // 构建关系 + if (!roomSections[roomName]) roomSections[roomName] = new Set(); + roomSections[roomName].add(sectionName); + + if (!venueRooms[venueName]) venueRooms[venueName] = new Set(); + venueRooms[venueName].add(roomName); + + if (!sessionVenues[sessionName]) sessionVenues[sessionName] = new Set(); + sessionVenues[sessionName].add(venueName); + } + + // 判断分区是否售罄(座位数 === 0) + // 关键:需要对比渲染的分区选项,因为可能存在选项中有但 seatSpecMap 中没有的情况 + for (var section in sectionSeats) { + sectionSoldOut[section] = sectionSeats[section] === 0; + } + + // 遍历渲染的分区选项,确保所有选项都有对应的售罄状态 + var self = this; + document.querySelectorAll('#sectionSelector .vr-spec-option').forEach(function(opt) { + var section = opt.dataset.section || ''; + // 如果选项不在 sectionSeats 中(意味着没有座位),也标记为售罄 + if (sectionSeats[section] === undefined) { + sectionSoldOut[section] = true; + } + }); + + // 判断演播室是否完全售罄(所有分区都售罄) + for (var room in roomSections) { + var allSoldOut = true; + roomSections[room].forEach(function(section) { + if (!sectionSoldOut[section]) allSoldOut = false; + }); + roomSoldOut[room] = allSoldOut; + } + + // 判断场馆是否完全售罄(所有演播室都售罄) + for (var venue in venueRooms) { + var allSoldOut = true; + venueRooms[venue].forEach(function(room) { + if (!roomSoldOut[room]) allSoldOut = false; + }); + venueSoldOut[venue] = allSoldOut; + } + + // 判断场次是否完全售罄(所有场馆都售罄) + for (var session in sessionVenues) { + var allSoldOut = true; + sessionVenues[session].forEach(function(venue) { + if (!venueSoldOut[venue]) allSoldOut = false; + }); + sessionSoldOut[session] = allSoldOut; + } + + // 更新场次选项可用性 + document.querySelectorAll('#sessionGrid .vr-session-item').forEach(function(item) { + var session = item.dataset.session || ''; + if (sessionSoldOut[session]) { + item.classList.add('sold-out'); + item.style.opacity = '0.4'; + item.style.pointerEvents = 'none'; + if (!item.querySelector('.sold-tag')) { + item.innerHTML += '(售罄)'; + } + } else { + item.classList.remove('sold-out'); + item.style.opacity = ''; + item.style.pointerEvents = ''; + var tag = item.querySelector('.sold-tag'); + if (tag) tag.remove(); + } + }); + + // 更新场馆选项可用性 + document.querySelectorAll('#venueSelector .vr-spec-option').forEach(function(opt) { + var venue = opt.dataset.venue || ''; + if (venueSoldOut[venue]) { + opt.classList.add('sold-out'); + opt.style.opacity = '0.4'; + opt.style.pointerEvents = 'none'; + if (!opt.querySelector('.sold-tag')) { + opt.innerHTML += '(售罄)'; + } + } else { + opt.classList.remove('sold-out'); + opt.style.opacity = ''; + opt.style.pointerEvents = ''; + var tag = opt.querySelector('.sold-tag'); + if (tag) tag.remove(); + } + }); + + // 更新演播室选项可用性 + document.querySelectorAll('#roomSelector .vr-spec-option').forEach(function(opt) { + var room = opt.dataset.room || ''; + if (roomSoldOut[room]) { + opt.classList.add('sold-out'); + opt.style.opacity = '0.4'; + opt.style.pointerEvents = 'none'; + if (!opt.querySelector('.sold-tag')) { + opt.innerHTML += '(售罄)'; + } + } else { + opt.classList.remove('sold-out'); + opt.style.opacity = ''; + opt.style.pointerEvents = ''; + var tag = opt.querySelector('.sold-tag'); + if (tag) tag.remove(); + } + }); + + // 更新分区选项可用性 + document.querySelectorAll('#sectionSelector .vr-spec-option').forEach(function(opt) { + var section = opt.dataset.section || ''; + if (sectionSoldOut[section]) { + opt.classList.add('sold-out'); + opt.style.opacity = '0.4'; + opt.style.pointerEvents = 'none'; + if (!opt.querySelector('.sold-tag')) { + opt.innerHTML += '(售罄)'; + } + } else { + opt.classList.remove('sold-out'); + opt.style.opacity = ''; + opt.style.pointerEvents = ''; + var tag = opt.querySelector('.sold-tag'); + if (tag) tag.remove(); + } + }); + }, + toggleSeat: function(el) { var seatKey = el.dataset.seatKey; var price = parseFloat(el.dataset.price) || 0;