# Phase 3 前端模板开发计划

> 日期：2026-04-20 | 状态：进行中
> 背景：Council 调研结论 + CSS 样式机制确认 → Demo 快速落地

---

## 一、调研结论摘要（Council 651e0bf2）

### Q2 — 单订单多SKU（多座位选择的前提）

**结论：✅ 可行，走购物车路线**

ShopXO `BuyService.php:86` 循环处理 `goods_data` 数组，每行独立 `spec_base_id`。现有 `ticket_detail.html` Plan A 代码已写好，但 `submit()` 函数有 bug：只把第一个座位编码进 URL，后续座位丢失。

**最小改动（Demo 1天可上线）：**
- 修复 `submit()`：将 `goodsParamsList` 整体编码，POST 到购物车 `CartSave`，再跳转合并支付
- 绕过 `OrderSplitService` 拆单风险（购物车结算路径不触发按仓库拆单）

### Q1 — ShopXO 自定义模板最佳实践

**结论：原生 PHP + 内联 JS，渐进增强**

- ShopXO view/goods/ 模板使用原生 PHP + 原生 JS，session/buy 控制器直接 render
- 不走 DIY 设计器（只支持静态 HTML 区块，无法参数化）
- H5 直接浏览器预览，无需构建

### Q3 — 第三方无代码构建服务

**结论：辅助有限，座位图等核心交互必须手写**

- 无代码服务适合静态展示区块（票务介绍、艺人信息图）
- 座位图等高交互组件无法用无代码工具精确生成
- 生成代码后需后处理：路径替换 + 变量注入

### Q4 — uni-app 兼容性技术栈选型

**结论：fork shopxo-uniapp，票务页面自研**

- fork `shopxo-uniapp` → `vr-shopxo-uniapp`
- 票务页面（ticket-seat / ticket-wallet / ticket-verify）自研 Vue 3 组件
- 商城标准页面复用 shopxo-uniapp 原生实现
- CSS 一致（H5/小程序都基于 WebView）

---

## 二、CSS 样式注入机制（ShopXO 官方能力）

### 三层注入体系

| 层级 | 机制 | 甲方操作入口 |
|------|------|------------|
| **CSS 变量** | `header_style_root.html` 定义 `:root` 变量，后台主题配置可改 | ShopXO 后台「主题配色」 |
| **插件 CSS Hook** | `plugins_css_data` 钩子注入独立 CSS 文件 | 替换 `static/plugins/vr_ticket/css/ticket.css` |
| **内联 `<style>`** | 当前 `.vr-ticket-page` 样式块，完全隔离 | 直接修改 `ticket_detail.html` |

### CSS 变量体系（ShopXO 官方）

`header_style_root.html` 定义了完整的 CSS 变量系统：

```css
/* 主色 */
--color-main: #E22C08;           /* 可在后台改为甲方品牌色 */
--color-main-light: #ffe3de;
--color-main-hover: #EA6B52;

/* 圆角 */
--border-radius-sm: 0.2rem;
--border-radius: 0.4rem;
--border-radius-lg: 0.8rem;

/* 阴影 */
--box-shadow: 0 5px 20px rgba(50,55,58,0.1);
--box-shadow-sm: 0 2px 8px rgba(50,55,58,0.1);
--box-shadow-lg: 0 8px 34px rgba(50,55,58,0.1);
```

vr_ticket 模板内的 `.vr-ticket-page` 可以直接引用这些变量，实现主题色统一。例如：
```css
.vr-purchase-btn {
    background: var(--color-main);          /* 继承 ShopXO 主题色 */
    border-radius: var(--border-radius-lg);
    box-shadow: var(--box-shadow-sm);
}
```

### 插件 CSS Hook（推荐方案）

在插件 service 中注册 `plugins_css_data` 钩子，加载独立 CSS 文件：

```php
// plugins/vr_ticket/hook/ViewGoodsSpiderCss.php
public function handle()
{
    return 'plugins/vr_ticket/css/ticket.css';
}
```

甲方样式微调时，只需替换 `static/plugins/vr_ticket/css/ticket.css`，不需要改 PHP 模板。

### 当前 ticket_detail.html 样式结构

```
ticket_detail.html
├── <style> 完全独立的内联样式块（.vr-ticket-page 等）
├── HTML 结构（.vr-ticket-page #vrTicketApp）
├── 内联 JS（vrTicketApp 对象）
└── ModuleInclude('public/footer')
```

样式完全隔离，不受 ShopXO 升级影响。甲方设计师可以专注修改 CSS，不需要理解 PHP 模板逻辑。

---

## 三、Demo 交付计划（最小可行方案）

### 目标：1天内上线可演示的多座位下单 Demo

### 当前代码状态

- `ticket_detail.html` 已有 Plan A 代码（submit 函数存在 URL 编码 bug）
- 座位图渲染正常（A/B/C 三排 + 舞台 + 颜色分区 + 选座 UI + 观演人表单）
- `loadSoldSeats()` ✅ 已实现（seatSpecMap.inventory，无需后端 API）

### Demo 交付清单

#### P0 — 必须完成（Demo 当天）

| 任务 | 文件 | 说明 | 优先级 |
|------|------|------|--------|
| **修复 submit() bug** | `ticket_detail.html` | 当前只传第一个座位，需整体编码 goodsParamsList | 🔴 P0 |
| **购物车路由接通** | `ticket_detail.html` | 改用 `CartSave` API 提交多座位，跳转合并支付 | 🔴 P0 |
| **场次切换重置已选座位** | `ticket_detail.html` | `selectSession()` 调用座位重置逻辑（已有代码未调用） | 🔴 P0 |
| **座位类型图例** | `ticket_detail.html` | 已完成 ✅，确认正常显示 | ✅ 已完成 |
| **购买栏按钮状态联动** | `ticket_detail.html` | 已实现 ✅，`disabled` 状态根据选座数量变化 | ✅ 已完成 |

#### P1 — Demo 当天完成后继续

| 任务 | 文件 | 说明 | 优先级 |
|------|------|------|--------|
| ~~**loadSoldSeats() 实现**~~ ✅ | `ticket_detail.html` + 前端 seatSpecMap | seatSpecMap.inventory 反推已售座位，无需后端 | 已完成 |
| **座位图缩放/拖拽交互** | `ticket_detail.html` | 原生 JS < 200 行实现 | 🟡 P1 |
| **CSS 样式文件分离** | `static/vr_ticket/css/ticket.css` | 从内联 `<style>` 抽离，通过 `plugins_css_data` 钩子注册 | 🟡 P1 |
| **甲方主题色变量接入** | `ticket_detail.html` <style> | 将硬编码色值改为 `var(--color-main)` 等变量 | 🟡 P1 |

#### P2 — 后续迭代

| 任务 | 说明 | 优先级 |
|------|------|--------|
| shopxo-uniapp fork | 建立 `vr-shopxo-uniapp` 项目骨架 | 🟢 P2 |
| ticket-seat.vue | uni-app 选座核心组件 | 🟢 P2 |
| B 端核销页 | 小程序扫码核销页面 | 🟢 P2 |

---

## 四、关键技术细节

### 4.1 submit() 修复方案

**当前 bug：** `location.href = checkoutUrl + '&goods_params=' + encodeURIComponent(goodsParams)`
URL 方式只能传字符串，多座位数据会被截断或丢失。

**修复方案：** 走购物车 API + 合并支付

```javascript
submit: function() {
    // 1. 收集所有座位数据（现有代码正常）
    var goodsParamsList = this.selectedSeats.map(function(seat, i) {
        var specBaseId = self.specBaseIdMap[seat.seatKey] || self.sessionSpecId;
        var seatAttendee = attendeeData[i] || {};
        return {
            goods_id: self.goodsId,
            spec_base_id: parseInt(specBaseId) || 0,
            stock: 1,
            extension_data: JSON.stringify({
                attendee: seatAttendee,
                seat: { seatKey: seat.seatKey, label: seat.label, price: seat.price }
            })
        };
    });

    // 2. 逐座提交到购物车
    var deferreds = goodsParamsList.map(function(params) {
        return $.post(__goods_cart_save_url__, {
            goods_id: params.goods_id,
            spec_id: params.spec_base_id,
            stock: params.stock,
            // extension_data 作为自定义字段存储
        });
    });

    // 3. 全部成功后跳转合并支付
    $.when.apply($, deferreds).done(function() {
        location.href = __root__ + '?s=index/cart/index';
    }).fail(function() {
        alert('座位已被占用，请重新选择');
    });
}
```

**为什么走购物车路线：**
- `BuyCart` → `BuyTypeGoodsList` → 直接调用 `BuyGoods`，完美支持多 `goods_data` 行
- 购物车结算路径不触发 `OrderSplitService` 按仓库拆单（只按商品拆）
- `plugins_service_order_pay_success_handle_end` 钩子正常触发，QR 票生成不受影响

### 4.2 CSS 变量主题化方案

当前 `ticket_detail.html` 内联样式中的硬编码色值，全部改为 CSS 变量：

```css
/* 改造前 */
.vr-purchase-btn { background: linear-gradient(135deg, #409eff, #3b8ef8); }

/* 改造后 */
.vr-purchase-btn {
    background: linear-gradient(135deg, var(--color-main), var(--color-main-hover));
    border-radius: var(--border-radius-lg);
    box-shadow: var(--box-shadow-sm);
}
```

甲方想要调整主题色时，有三条路：
1. **后台改**：ShopXO 管理后台 → 主题配色 → 自动同步到所有 `var(--color-*)` 变量
2. **文件改**：替换 `static/plugins/vr_ticket/css/ticket.css`
3. **代码改**：直接修改 `ticket_detail.html` 的 `<style>` 块

### 4.3 specBaseIdMap 降级策略

```javascript
var specBaseId = self.specBaseIdMap[seat.seatKey] || self.sessionSpecId;
```

- 如果 `seatKey`（如 "A_1"）在 `specBaseIdMap` 中有对应记录 → 座位级 SKU（精确到每个座位）
- 如果 `specBaseIdMap` 中没有（如后台未批量创建座位规格）→ 降级到 `sessionSpecId`（Zone 级别，同一 zone 全部座位共享一个 SKU）

**后台需要提前创建座位规格**，vr_ticket 后台管理界面需增加「批量生成座位规格」功能。

---

## 五、文件清单

| 文件 | 当前状态 | 下一步 |
|------|---------|--------|
| `shopxo/app/plugins/vr_ticket/view/goods/ticket_detail.html` | Plan A 代码有 bug | 修复 submit()，接购物车 |
| `shopxo/app/plugins/vr_ticket/static/css/ticket.css` | 不存在 | 从 ticket_detail.html 抽离样式 |
| `docs/council-research-output.md` | ✅ 已完成 | 无需修改 |
| `shopxo/app/service/BuyService.php` | 参考 | 无需修改（走购物车路线） |
| `shopxo/app/service/SeatSkuService.php` | 有缺陷（单模板模式） | 等待 Issue #15/#16 修复 |

---

## 六、技术风险

| 风险 | 严重程度 | 缓解 |
|------|---------|------|
| `submit()` 只传第一座位（已发现） | 🔴 高 | 修复 submit()，改走购物车 API |
| OrderSplitService 拆单（多座位变多笔支付） | 🔴 高 | 购物车路线绕过 |
| 座位级 SKU 后台未创建（specBaseIdMap 空） | 🟡 中 | 降级到 Zone 级别 + 后台增加批量生成功能 |
| 甲方主题色调整后样式不一致 | 🟡 中 | CSS 变量化，所有色值引用 `var(--color-*)` |
| shopxo-uniapp fork 官方更新同步成本 | 🟢 低 | 票务页面与商城页面目录隔离 |