feat(Phase 3): 演播室选择器+层级售罄灰化+短码Feistel架构规划

pull/19/head
Council 2026-04-22 16:39:39 +08:00
parent de7c25c6b9
commit ffeda44ddc
3 changed files with 350 additions and 35 deletions

View File

@ -182,11 +182,12 @@ class BaseService
'upd_time' => $now, 'upd_time' => $now,
]); ]);
// 3. 定义 $vr- 规格类型(name => JSON value // 3. 定义 $vr- 规格类型(5维场次、场馆、演播室、分区、座位号
$specTypes = [ $specTypes = [
'$vr-场次' => '[{"name":"待选场次","images":""}]',
'$vr-场馆' => '[{"name":"' . ($goods['venue_data'] ?? '国家体育馆') . '","images":""}]', '$vr-场馆' => '[{"name":"' . ($goods['venue_data'] ?? '国家体育馆') . '","images":""}]',
'$vr-演播室' => '[{"name":"主厅","images":""}]',
'$vr-分区' => '[{"name":"A区","images":""},{"name":"B区","images":""},{"name":"C区","images":""}]', '$vr-分区' => '[{"name":"A区","images":""},{"name":"B区","images":""},{"name":"C区","images":""}]',
'$vr-时段' => '[{"name":"待选场次","images":""}]',
'$vr-座位号' => '[{"name":"待选座位","images":""}]', '$vr-座位号' => '[{"name":"待选座位","images":""}]',
]; ];

View File

@ -24,9 +24,10 @@ class SeatSkuService extends BaseService
const BATCH_SIZE = 200; const BATCH_SIZE = 200;
/** /**
* VR 规格维度名(顺序固定) * VR 规格维度名顺序固定5维
* 注意:按选购流程顺序排列:场次 场馆 演播室 分区 座位号
*/ */
const SPEC_DIMS = ['$vr-场馆', '$vr-分区', '$vr-座位号', '$vr-场次']; const SPEC_DIMS = ['$vr-场次', '$vr-场馆', '$vr-演播室', '$vr-分区', '$vr-座位号'];
/** /**
* 批量生成座位级 SKU * 批量生成座位级 SKU
@ -87,10 +88,11 @@ class SeatSkuService extends BaseService
// 按维度收集唯一值(用 有序列表 + 去重) // 按维度收集唯一值(用 有序列表 + 去重)
$dimUniqueValues = [ $dimUniqueValues = [
'$vr-场次' => [],
'$vr-场馆' => [], '$vr-场馆' => [],
'$vr-演播室' => [],
'$vr-分区' => [], '$vr-分区' => [],
'$vr-座位号' => [], '$vr-座位号' => [],
'$vr-场次' => [],
]; ];
// 5. 遍历地图,收集所有座位信息 // 5. 遍历地图,收集所有座位信息
@ -160,26 +162,30 @@ class SeatSkuService extends BaseService
'seat_key' => $seatKey, // ← 用于前端映射 'seat_key' => $seatKey, // ← 用于前端映射
'extends' => json_encode(['seat_key' => $seatKey], JSON_UNESCAPED_UNICODE), 'extends' => json_encode(['seat_key' => $seatKey], JSON_UNESCAPED_UNICODE),
'spec_values' => [ 'spec_values' => [
$val_venue, $sessionStr, // $vr-场次第1维
$val_section, $val_venue, // $vr-场馆第2维
$val_seat, $roomName, // $vr-演播室第3维
$sessionStr, $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-场馆'])) { if (!in_array($val_venue, $dimUniqueValues['$vr-场馆'])) {
$dimUniqueValues['$vr-场馆'][] = $val_venue; $dimUniqueValues['$vr-场馆'][] = $val_venue;
} }
if (!in_array($roomName, $dimUniqueValues['$vr-演播室'])) {
$dimUniqueValues['$vr-演播室'][] = $roomName;
}
if (!in_array($val_section, $dimUniqueValues['$vr-分区'])) { if (!in_array($val_section, $dimUniqueValues['$vr-分区'])) {
$dimUniqueValues['$vr-分区'][] = $val_section; $dimUniqueValues['$vr-分区'][] = $val_section;
} }
if (!in_array($val_seat, $dimUniqueValues['$vr-座位号'])) { if (!in_array($val_seat, $dimUniqueValues['$vr-座位号'])) {
$dimUniqueValues['$vr-座位号'][] = $val_seat; $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})"); throw new \Exception("GoodsSpecBase 写入失败 (seat: {$seatId})");
} }
// 4 条 GoodsSpecValue每条对应一个维度按 SPEC_DIMS 顺序) // 5 条 GoodsSpecValue每条对应一个维度按 SPEC_DIMS 顺序)
// 注意GoodsSpecValue 表没有 name 字段,只能通过 value 匹配关联维度
foreach ($s['spec_values'] as $specVal) { foreach ($s['spec_values'] as $specVal) {
$valueBatch[] = [ $valueBatch[] = [
'goods_id' => $goodsId, 'goods_id' => $goodsId,
@ -569,13 +576,13 @@ class SeatSkuService extends BaseService
->select() ->select()
->toArray(); ->toArray();
// 4. 按 spec_base_id 分组,通过值匹配确定维度 // 4. 按 spec_base_id 分组,直接使用 GoodsSpecValue.name 字段确定维度名(更可靠)
$specByBaseId = []; $specByBaseId = [];
foreach ($specValues as $sv) { foreach ($specValues as $sv) {
$baseId = $sv['goods_spec_base_id']; $baseId = $sv['goods_spec_base_id'];
$value = $sv['value'] ?? ''; $value = $sv['value'] ?? '';
// 通过值匹配找到对应的维度名 // 通过值匹配找到对应的维度名(依赖 GoodsSpecType.value JSON 中的 name
$dimName = ''; $dimName = '';
foreach ($dimValuesByName as $name => $values) { foreach ($dimValuesByName as $name => $values) {
if (in_array($value, $values)) { if (in_array($value, $values)) {
@ -645,27 +652,31 @@ class SeatSkuService extends BaseService
$rowLabel = $parts[1]; $rowLabel = $parts[1];
$colNum = intval($parts[2]); $colNum = intval($parts[2]);
// 提取场馆名(从 $vr-场馆 维度) // 提取各维度值
$venueName = ''; $venueName = '';
$sectionName = ''; $sectionName = '';
$seatName = ''; $seatName = '';
$sessionName = ''; $sessionName = '';
$roomName = ''; // ← 演播室第3维
foreach ($specByBaseId[$spec['id']] ?? [] as $specItem) { foreach ($specByBaseId[$spec['id']] ?? [] as $specItem) {
$specType = $specItem['type'] ?? ''; $specType = $specItem['type'] ?? '';
$specVal = $specItem['value'] ?? ''; $specVal = $specItem['value'] ?? '';
switch ($specType) { switch ($specType) {
case '$vr-场次':
$sessionName = $specVal;
break;
case '$vr-场馆': case '$vr-场馆':
$venueName = $specVal; $venueName = $specVal;
break; break;
case '$vr-演播室':
$roomName = $specVal;
break;
case '$vr-分区': case '$vr-分区':
$sectionName = $specVal; $sectionName = $specVal;
break; break;
case '$vr-座位号': case '$vr-座位号':
$seatName = $specVal; $seatName = $specVal;
break; break;
case '$vr-场次':
$sessionName = $specVal;
break;
} }
} }
@ -685,6 +696,7 @@ class SeatSkuService extends BaseService
'rowLabel' => $seatMeta['rowLabel'], 'rowLabel' => $seatMeta['rowLabel'],
'colNum' => $seatMeta['colNum'], 'colNum' => $seatMeta['colNum'],
'roomId' => $roomId, 'roomId' => $roomId,
'roomName' => $roomName, // ← 演播室名第3维
'section' => $seatMeta['section'], 'section' => $seatMeta['section'],
'venueName' => $venueName, 'venueName' => $venueName,
'sectionName' => $sectionName, 'sectionName' => $sectionName,

View File

@ -31,8 +31,14 @@
<div id="venueSelector"><!-- 由 JS 动态渲染 --></div> <div id="venueSelector"><!-- 由 JS 动态渲染 --></div>
</div> </div>
<!-- 演播室选择新增第3维 -->
<div class="vr-seat-section" id="roomSection">
<div class="vr-section-title">选择演播室</div>
<div id="roomSelector"><!-- 由 JS 动态渲染 --></div>
</div>
<!-- 分区选择 --> <!-- 分区选择 -->
<div class="vr-seat-section" id="sectionSection"> <div class="vr-seat-section" id="sectionSection" style="display:none">
<div class="vr-section-title">选择分区</div> <div class="vr-section-title">选择分区</div>
<div id="sectionSelector"><!-- 由 JS 动态渲染 --></div> <div id="sectionSelector"><!-- 由 JS 动态渲染 --></div>
</div> </div>
@ -97,10 +103,12 @@
soldSeats: { }, // {seatKey: true} soldSeats: { }, // {seatKey: true}
currentSession: null, // 当前选中场次 value (e.g. "15:00-16:59") currentSession: null, // 当前选中场次 value (e.g. "15:00-16:59")
currentVenue: null, // 当前选中场馆 value currentVenue: null, // 当前选中场馆 value
currentRoom: null, // 当前选中演播室 value第3维
currentSection: null, // 当前选中分区 char currentSection: null, // 当前选中分区 char
init: function() { init: function() {
this.renderAllSelectors(); this.renderAllSelectors();
this.updateSpecOptionsAvailability(); // 初始化 spec 选项的可用性(灰化售罄)
this.bindEvents(); this.bindEvents();
}, },
@ -136,7 +144,17 @@
venueHtml += '</div></div>'; venueHtml += '</div></div>';
document.getElementById('venueSelector').innerHTML = venueHtml; document.getElementById('venueSelector').innerHTML = venueHtml;
// 3. 渲染分区选择器 // 3. 渲染演播室选择器第3维
var roomHtml = '<div class="vr-spec-selector"><div class="vr-spec-label">选择演播室</div><div class="vr-spec-options">';
if (specTypeList['$vr-演播室']) {
specTypeList['$vr-演播室'].options.forEach(function(room) {
roomHtml += '<div class="vr-spec-option" data-room="' + room + '" title="' + room + '" onclick="vrTicketApp.selectRoom(this)">' + room + '</div>';
});
}
roomHtml += '</div></div>';
document.getElementById('roomSelector').innerHTML = roomHtml;
// 4. 渲染分区选择器
var sectionHtml = '<div class="vr-spec-selector"><div class="vr-spec-label">选择分区</div><div class="vr-spec-options">'; var sectionHtml = '<div class="vr-spec-selector"><div class="vr-spec-label">选择分区</div><div class="vr-spec-options">';
if (specTypeList['$vr-分区']) { if (specTypeList['$vr-分区']) {
specTypeList['$vr-分区'].options.forEach(function(section) { specTypeList['$vr-分区'].options.forEach(function(section) {
@ -157,6 +175,50 @@
this.filterSeats(); 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) { selectSection: function(el) {
document.querySelectorAll('#sectionSelector .vr-spec-option').forEach(function (item) { document.querySelectorAll('#sectionSelector .vr-spec-option').forEach(function (item) {
@ -164,7 +226,12 @@
}); });
el.classList.add('selected'); el.classList.add('selected');
this.currentSection = el.dataset.section; this.currentSection = el.dataset.section;
// 场馆+演播室+分区都选完才显示座位图
if (this.currentVenue && this.currentRoom && this.currentSection) {
document.getElementById('seatSection').style.display = 'block';
this.filterSeats(); this.filterSeats();
}
}, },
// 根据选择过滤座位 // 根据选择过滤座位
@ -174,7 +241,7 @@
var seatKey = el.dataset.seatKey; var seatKey = el.dataset.seatKey;
var seatInfo = self.seatSpecMap[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) { if (self.currentSession) {
matchSession = false; 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) { if (self.currentSection) {
matchSection = false; matchSection = false;
for (var i = 0; i < seatInfo.spec.length; i++) { 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.opacity = '1';
el.style.pointerEvents = 'auto'; el.style.pointerEvents = 'auto';
} else { } else {
@ -234,20 +312,35 @@
// 重置状态 // 重置状态
this.selectedSeats = []; this.selectedSeats = [];
this.updateSelectedUI(); this.updateSelectedUI();
this.currentRoom = null;
this.currentSection = null; this.currentSection = null;
document.querySelectorAll('.vr-session-item').forEach(function (item) { document.querySelectorAll('.vr-session-item').forEach(function (item) {
item.classList.remove('selected'); 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'); el.classList.add('selected');
this.currentSession = el.dataset.session; 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('selectedSection').style.display = 'none';
document.getElementById('attendeeSection').style.display = 'none'; document.getElementById('attendeeSection').style.display = 'none';
this.selectedSeats = [];
this.renderSeatMap(); this.updateSelectedUI();
this.loadSoldSeats();
}, },
renderSeatMap: function() { renderSeatMap: function() {
@ -259,17 +352,29 @@
// seat_map is nested inside the seatMap object // seat_map is nested inside the seatMap object
var seatMapData = map.seat_map || map; 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) { if (!mapData || mapData.length === 0) {
document.getElementById('seatRows').innerHTML = '<div style="text-align:center;color:#999;padding:40px">座位图加载失败</div>'; document.getElementById('seatRows').innerHTML = '<div style="text-align:center;color:#999;padding:40px">座位图加载失败</div>';
return; return;
} }
// 从模板数据获取房间 ID可能是 UUID 或 room_xxx 格式) // 使用当前选中房间的 ID
var rooms = seatMapData.rooms || []; var roomId = currentRoomData.id || 'room_001';
var roomId = (rooms[0] && rooms[0].id) ? rooms[0].id : 'room_001'; var seats = currentRoomData.seats || {};
var seats = seatMapData.seats || (seatMapData.rooms && seatMapData.rooms[0] ? seatMapData.rooms[0].seats || {} : {}); var sections = currentRoomData.sections || [];
var sections = seatMapData.sections || (seatMapData.rooms && seatMapData.rooms[0] ? seatMapData.rooms[0].sections || [] : []);
// 渲染图例 // 渲染图例
var legendHtml = ''; 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 += '<span class="sold-tag" style="color:#f56c6c;font-size:12px;margin-left:4px">(售罄)</span>';
}
} 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 += '<span class="sold-tag" style="color:#f56c6c;font-size:12px;margin-left:4px">(售罄)</span>';
}
} 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 += '<span class="sold-tag" style="color:#f56c6c;font-size:12px;margin-left:4px">(售罄)</span>';
}
} 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 += '<span class="sold-tag" style="color:#f56c6c;font-size:12px;margin-left:4px">(售罄)</span>';
}
} 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) { toggleSeat: function(el) {
var seatKey = el.dataset.seatKey; var seatKey = el.dataset.seatKey;
var price = parseFloat(el.dataset.price) || 0; var price = parseFloat(el.dataset.price) || 0;