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;