feat: 添加场馆和分区选择器 + specTypeList 支持
- SeatSkuService: 返回 specTypeList 包含所有4维规格 - Goods.php: 注入 specTypeList - ticket_detail.html: - 添加 venueSelector 和 sectionSelector HTML 容器 - 添加 renderAllSelectors() 渲染场次/场馆/分区 - 添加 selectVenue/selectSection/filterSeats 函数 - CSS: 添加规格选择器样式pull/19/head
parent
fc07c2ece6
commit
de9134773f
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 = '';
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue