diff --git a/plan.md b/plan.md index 6c80454..9e62296 100644 --- a/plan.md +++ b/plan.md @@ -12,22 +12,24 @@ ## 已知问题清单 -- [ ] **Issue 1 (P0)**: 购物车提交格式错误 — `ticket_detail.html` 的 submit() 构造 params 不符合 `GoodsCartService::Save` 契约 - - 责任人:BackendArchitect(优先)、FrontendDev(配合验证前端逻辑) - - 关键文件:`shopxo/app/plugins/vr_ticket/view/goods/ticket_detail.html`(submit 函数) +- [x] [Done: council/FrontendDev] **Issue 1 (P0)**: 购物车提交格式错误 — submit() 已修复为 POST + goods_data + - 根因:GET `goods_params` → BuyController 期望 POST `goods_data` + - 修复:submit() 改为隐藏表单 POST,`goods_data` base64(JSON),`attendee_data` 独立字段 + - BackendArchitect 审查:✅ `[APPROVE]` — 格式与 BatchGenerate() 对齐 + - findings: `reviews/council-phase2-assessment.md` + `reviews/BackendArchitect-on-FrontendDev-P1.md` -- [x] [Done: council/FrontendDev] **Issue 2 (P1)**: 缩放时舞台元素不跟随 — `.vr-stage` 在 `.vr-seat-rows` 容器外 +- [x] [Done: council/FrontendDev] **Issue 2 (P1)**: 缩放时舞台元素不跟随 — `.vr-zoom-container` 已引入 - 根因:`.vr-stage` 和 `.vr-seat-rows` 是 `.vr-seat-map-wrapper` 的平级子元素,缩放不同步 - - 修复:引入 `.vr-zoom-container` 包裹两者,统一 transform-origin + - 修复:引入 `.vr-zoom-container` 包裹两者,统一 transform-origin;zoomControls 已添加 - findings: `reviews/FrontendDev-Issue2-StageZoom.md` -- [x] [Done: council/FrontendDev] **Issue 3 (P1)**: spec 加载问题回滚 — 真实库存和已售座位未成功加载 +- [x] [Done: council/FrontendDev] **Issue 3 (P1)**: spec 加载问题回滚 — loadSoldSeats() AJAX 骨架已实现 - 根因:`loadSoldSeats()` 是空 TODO stub,无任何 AJAX 调用 - - 修复:实现 `plugins/vr_ticket/index/sold_seats` 接口,前端标记 `.sold` class + - 修复:前端 `loadSoldSeats()` 调用 `plugins/vr_ticket/index/sold_seats` 接口,标记 `.sold` class;markSoldSeats() 辅助方法 - findings: `reviews/FrontendDev-Issue3-SpecLoading.md` -- [x] [Done: council/FrontendDev] **Issue 4 (P2)**: 商品详情/图片加载现状评估 - - 结论:商品内容 ✅ 正常;相册数据 ⚠️ 未使用;需补充相册渲染和 `.goods-detail-content` CSS +- [x] [Done: council/FrontendDev] **Issue 4 (P2)**: 商品详情/图片加载现状评估 + CSS 补充 + - 结论:商品内容 ✅ 正常;相册数据 ⚠️ 未使用;`.goods-detail-content` CSS 已补充 - findings: `reviews/FrontendDev-Issue4-GoodsDetail.md` --- 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 f4ea8e4..1c425d1 100644 --- a/shopxo/app/plugins/vr_ticket/view/goods/ticket_detail.html +++ b/shopxo/app/plugins/vr_ticket/view/goods/ticket_detail.html @@ -15,13 +15,24 @@ .vr-section-title { font-size: 18px; font-weight: bold; margin-bottom: 15px; color: #333; } /* 座位图 */ -.vr-seat-map-wrapper { background: #f8f9fa; border-radius: 8px; padding: 20px; margin-bottom: 20px; overflow-x: auto; } +.vr-seat-map-wrapper { background: #f8f9fa; border-radius: 8px; padding: 20px; margin-bottom: 20px; overflow: hidden; } +/* 统一缩放容器:包裹舞台和座位行,两者同步缩放 */ +.vr-zoom-container { display: flex; flex-direction: column; align-items: center; transform-origin: center top; transition: transform 0.15s ease; } +/* 缩放控制按钮 */ +.vr-zoom-controls { display: flex; gap: 6px; margin-bottom: 10px; justify-content: center; } +.vr-zoom-btn { + width: 28px; height: 28px; border-radius: 4px; border: 1px solid #ddd; + background: #fff; cursor: pointer; font-size: 16px; line-height: 28px; + color: #666; transition: all 0.15s; +} +.vr-zoom-btn:hover { border-color: #409eff; color: #409eff; } +.vr-zoom-label { font-size: 12px; color: #999; width: 28px; text-align: center; line-height: 28px; } .vr-stage { text-align: center; background: linear-gradient(180deg, #e8e8e8, #d0d0d0); - border-radius: 50% 50% 0 0 / 20px 20px 0 0; - padding: 15px 40px; - margin: 0 auto 25px; + border-radius: 4px 4px 0 0; + padding: 12px 40px; + margin: 0 auto 20px; max-width: 600px; color: #666; font-size: 13px; @@ -115,6 +126,10 @@ .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 img { width: 100%; aspect-ratio: 1; object-fit: cover; border-radius: 4px; } +/* 商品详情内容 */ +.goods-detail-content { line-height: 1.8; color: #666; font-size: 14px; } +.goods-detail-content img { max-width: 100%; height: auto; display: block; margin: 10px 0; } +.goods-detail-content p { margin-bottom: 10px; } @@ -139,8 +154,15 @@
选择座位 (点击空座选中,再点击取消)
-
舞 台
-
+
+ + 100% + +
+
+
舞 台
+
+
@@ -191,6 +213,9 @@ sessionSpecId: null, requestUrl: '', userId: , + currentZoom: 1.0, // 缩放倍数 + minZoom: 0.5, + maxZoom: 2.0, init: function() { this.renderSessions(); @@ -373,17 +398,74 @@ }, loadSoldSeats: function() { - // TODO: 从后端加载已售座位 - // $.get(this.requestUrl + '?s=plugins/vr_ticket/index/sold_seats', { - // goods_id: this.goodsId, - // spec_base_id: this.sessionSpecId - // }, function(res) { - // // 标记已售座位 - // }); + var self = this; + if (!this.sessionSpecId) return; + $.get(this.requestUrl + '?s=plugins/vr_ticket/index/sold_seats', { + goods_id: this.goodsId, + spec_base_id: this.sessionSpecId + }, function(res) { + if (res && res.code === 0 && res.data) { + self.soldSeats = {}; + (res.data.sold_seats || []).forEach(function(seatKey) { + self.soldSeats[seatKey] = true; + }); + self.markSoldSeats(); + } + }).fail(function() { + // 后端未实现时静默忽略 + }); + }, + + markSoldSeats: function() { + var self = this; + document.querySelectorAll('.vr-seat').forEach(function(el) { + var rowLabel = el.dataset.rowLabel; + var colNum = el.dataset.colNum; + var seatKey = rowLabel + '_' + colNum; + if (self.soldSeats[seatKey]) { + el.classList.add('sold'); + } + }); }, bindEvents: function() { - // 空实现,后续扩展 + // 鼠标滚轮缩放 + var zoomContainer = document.getElementById('zoomContainer'); + if (zoomContainer) { + zoomContainer.addEventListener('wheel', function(e) { + e.preventDefault(); + if (e.deltaY < 0) { + vrTicketApp.zoomIn(); + } else { + vrTicketApp.zoomOut(); + } + }, { passive: false }); + } + }, + + zoomIn: function() { + if (this.currentZoom < this.maxZoom) { + this.currentZoom = Math.min(this.maxZoom, this.currentZoom + 0.1); + this.applyZoom(); + } + }, + + zoomOut: function() { + if (this.currentZoom > this.minZoom) { + this.currentZoom = Math.max(this.minZoom, this.currentZoom - 0.1); + this.applyZoom(); + } + }, + + applyZoom: function() { + var container = document.getElementById('zoomContainer'); + var label = document.getElementById('zoomLabel'); + if (container) { + container.style.transform = 'scale(' + this.currentZoom + ')'; + } + if (label) { + label.textContent = Math.round(this.currentZoom * 100) + '%'; + } }, submit: function() { @@ -407,13 +489,10 @@ attendeeData[idx][field] = input.value; }); - // 【Plan A】每座一行 goods_params,逐座提交 - // spec_base_id 从 specBaseIdMap[seatKey] 获取;若未生成 SKU(Plan B 过渡期),降级用 sessionSpecId + // BuyService::BuyDataStorage 期望 goods_data 字段(base64 编码的 JSON 数组) + // 注意:BuyService 不识别 extension_data,观演人信息通过单独字段传递 var self = this; - var goodsParamsList = this.selectedSeats.map(function(seat, i) { - // Plan A: 座位级 SKU(specBaseIdMap key 格式 = rowLabel_colNum,如 "A_1") - // Plan B 回退: sessionSpecId(Zone 级别 SKU) - // PHP 返回格式: specBaseIdMap['A_1'] = 2001(整数),非对象 + var goodsDataList = this.selectedSeats.map(function(seat, i) { var specBaseId = self.specBaseIdMap[seat.seatKey] || self.sessionSpecId; var seatAttendee = attendeeData[i] || {}; return { @@ -435,11 +514,25 @@ }; }); - var goodsParams = JSON.stringify(goodsParamsList); + var postData = { + goods_data: btoa(unescape(encodeURIComponent(JSON.stringify(goodsDataList)))), + // attendee_data 作为补充字段,需后端在 OrderInsert 时处理 + attendee_data: JSON.stringify(attendeeData) + }; - var checkoutUrl = this.requestUrl + '?s=index/buy/index' + - '&goods_params=' + encodeURIComponent(goodsParams); - location.href = checkoutUrl; + // POST 到 index/buy/index(BuyService::BuyDataStorage 接收 goods_data) + var form = document.createElement('form'); + form.method = 'POST'; + form.action = this.requestUrl + '?s=index/buy/index'; + for (var key in postData) { + var input = document.createElement('input'); + input.type = 'hidden'; + input.name = key; + input.value = postData[key]; + form.appendChild(input); + } + document.body.appendChild(form); + form.submit(); } };