vr-shopxo-uniapp/docs/vr-ticket-uniapp-supplement.md

738 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# VR 票务 UniApp 补充文档
> 创建时间2026-05-14
> 背景:基于 vr-shopxo-plugin H5 实现ticket_detail.html+ 插件后端,为 vr-shopxo-uniapp 移植提供完整的数据结构、后端接口、交互规范
> 依赖vr-ticket-integration-plan.md原有 Phase 1-4 规划不变)
---
## 一、核心数据结构
### 1.1 vr_goods_configv3.0 协议)
商品表 `goods.vr_goods_config` 存储票务配置快照,发布时写入,读取时直接使用不做关联查询:
```json
{
"version": 3.0,
"template_id": 4,
"selected_rooms": ["room_001", "room_002"],
"selected_sections": {
"room_001": ["A", "B"],
"room_002": ["A"]
},
"sessions": [
{ "start": "15:00", "end": "16:59" },
{ "start": "18:00", "end": "20:59" }
],
"template_snapshot": {
"venue": {
"name": "测试场馆",
"address": "北京市朝阳区",
"location": { "lng": "116.4", "lat": "39.9" }
},
"rooms": [
{
"id": "room_001",
"name": "主要展厅",
"sections": [
{ "char": "A", "name": "VIP区", "color": "#e74c3c" },
{ "char": "B", "name": "看台", "color": "#3498db" },
{ "char": "C", "name": "普通", "color": "#2ecc71" }
],
"map": ["AAAAAA", "BBBBBB", "CCCCCC"],
"seats": {
"A": { "price": 899, "color": "#e74c3c", "label": "VIP" },
"B": { "price": 599, "color": "#3498db", "label": "看台" },
"C": { "price": 299, "color": "#2ecc71", "label": "普通" }
}
},
{
"id": "room_002",
"name": "新放映室 2",
"sections": [
{ "char": "A", "name": "VIP区", "color": "#e74c3c" },
{ "char": "B", "name": "普通", "color": "#2ecc71" }
],
"map": ["AAAAA", "BBBBB"],
"seats": {
"A": { "price": 699, "color": "#e74c3c", "label": "VIP" },
"B": { "price": 399, "color": "#2ecc71", "label": "普通" }
}
}
]
}
}
```
**关键说明**
- `selected_sections``room_id` 为 key因为不同 room 的相同 char 指向不同分区)
- `template_snapshot` 在 Admin 发布时从 `vr_seat_templates.seat_map` 读取并存储,不做实时关联查询
- `rooms[].id` 为 UUID 格式(如 `room_001`),用于前端座位 DOM 的 `data-seat-key` 属性
---
### 1.2 seatSpecMap座位规格映射
`seatSpecMap` 是前端选座的核心数据,按 `seat_key` 索引每个座位的完整规格信息:
```json
// seatSpecMap后端 GetGoodsViewData 动态构建,前端只读)
{
"room_001_A_1": {
"spec_base_id": 10001,
"price": 899.00,
"inventory": 1,
"spec": [
{ "type": "$vr-场次", "value": "15:00-16:59" },
{ "type": "$vr-场馆", "value": "测试场馆" },
{ "type": "$vr-演播室", "value": "主要展厅" },
{ "type": "$vr-分区", "value": "测试场馆-主要展厅-A" },
{ "type": "$vr-座位号", "value": "测试场馆-主要展厅-A-A1" }
],
"venueName": "测试场馆",
"roomId": "room_001",
"roomName": "主要展厅",
"section": { "char": "A", "name": "VIP区", "color": "#e74c3c" },
"rowLabel": "A",
"colNum": 1
}
}
```
**构建方式**(后端 `SeatSkuService::buildSeatSpecMap`
1. 查询 `GoodsSpecBase`(含 `extends.seat_key`+ `GoodsSpecValue`(含 `value`
2. 通过 `GoodsSpecValue.value` 匹配 `GoodsSpecType.value` JSON 中的 `name` 确定维度
3. 遍历 `seat_map.rooms[].map` 提取 `rowLabel``chr(65+rowIndex)`)和 `colNum`从1开始
4. 合并以上信息输出 `seatSpecMap`
**前端用途**
- `seatSpecMap[seatKey].price` → 座位价格
- `seatSpecMap[seatKey].inventory` → 是否可售≤0 = 已售)
- `seatSpecMap[seatKey].spec` → submit 时提交完整 5 维规格数组
---
### 1.3 SPEC_DIMS5 维规格维度常量)
```php
// PHP 后端SeatSkuService.php
const SPEC_DIMS = [
'$vr-场次', // 第1维
'$vr-场馆', // 第2维
'$vr-演播室', // 第3维
'$vr-分区', // 第4维
'$vr-座位号', // 第5维
];
```
前端用 `seatSpecMap[seatKey].spec` 数组代替直接访问 `SPEC_DIMS`
---
### 1.4 座位图字符矩阵
```json
// seat_map.rooms[].map — 字符串数组,每字符对应一列
map: ["AAAAAA", "BBBBBB", "CCCCCC"]
// A = VIP区座位B = 看台座位C = 普通座位
// _ 或 - = 过道/空位(不渲染座位)
```
**渲染规则**
- 字符 = `_` / `-`:渲染空白占位 div维持对齐
- 字符 in `sections[].char`:渲染可用座位(带分区颜色)
- 该座位 `inventory ≤ 0` 或在 `soldSeats` 中:渲染灰色已售座位
**座位 DOM `data-seat-key` 格式**`{roomId}_{rowLabel}_{colNum}`(例如 `room_001_A_1`
---
### 1.5 goods_spec_data场次列表
```json
// 后端 GetGoodsViewData 从 sessions[] + seatSpecMap 构建
[
{ "spec_id": 0, "spec_name": "15:00-16:59", "price": 299, "start": "15:00", "end": "16:59" },
{ "spec_id": 0, "spec_name": "18:00-20:59", "price": 399, "start": "18:00", "end": "20:59" }
]
```
前端用于渲染场次选择器横向滚动卡片。
---
## 二、后端 API 接口
### 2.1 商品详情 APIVR 扩展字段)
**请求**`POST /api/goods/detail`
```json
{ "id": 118 }
```
**响应**(关键字段):
```json
{
"code": 0,
"data": {
"id": 118,
"title": "VR演唱会",
"images": "[\"https://...jpg\"]",
"price": "299-899",
"is_vr_ticket": 1,
"vr_goods_config": {
"version": 3.0,
"template_id": 4,
"sessions": [...],
"template_snapshot": {
"venue": {...},
"rooms": [...]
}
}
}
}
```
**注意**`vr_goods_config` 直接嵌入商品详情响应,前端无需额外请求。
---
### 2.2 购物车提交 API多座位下单
**请求**`POST /api/cart/save`
```json
{
"goods_data": [
{
"goods_id": 118,
"spec_base_id": 10001,
"stock": 1,
"extension_data": "{\"attendee\":{\"real_name\":\"张三\",\"phone\":\"13800138000\",\"id_card\":\"...\"},\"seat\":{\"seat_key\":\"room_001_A_1\",\"row\":\"A\",\"col\":1,\"section\":\"VIP区\"}}"
},
{
"goods_id": 118,
"spec_base_id": 10002,
"stock": 1,
"extension_data": "{\"attendee\":{\"real_name\":\"李四\",\"phone\":\"13900139000\",\"id_card\":\"...\"},\"seat\":{\"seat_key\":\"room_001_A_2\",\"row\":\"A\",\"col\":2,\"section\":\"VIP区\"}}"
}
],
"buy_type": "goods",
"address_id": "0"
}
```
**说明**
- 每个座位单独一条 `goods_data` 记录
- `spec_base_id``seatSpecMap[seatKey].spec_base_id` 获取
- `extension_data` 为 JSON 序列化的观演人 + 座位信息
- 后端 ShopXO BuyService 按 `spec_base_id` 原子扣库存(`FOR UPDATE SKIP LOCKED`
---
### 2.3 票夹 API
**列表**`GET /api/plugins/index?pluginsname=vr_ticket&pluginscontrol=ticket&pluginsaction=list`
```
无参数(依赖 C 端 session
```
**响应**
```json
{
"code": 0,
"data": {
"tickets": [
{
"id": 482815,
"goods_id": 118,
"goods_title": "VR演唱会",
"seat_info": "主要展厅 A区 1排1座",
"session_time": "15:00-16:59",
"venue_name": "测试场馆",
"real_name": "张三",
"verify_status": 0,
"issued_at": "2026-05-01 12:00:00",
"short_code": "003a2hgmgety"
}
],
"count": 1
}
}
```
| verify_status | 含义 |
|---|---|
| 0 | 未核销 |
| 1 | 已核销 |
| 2 | 已退款 |
**票详情**`GET /api/plugins/index?...&pluginsaction=detail&id={ticketId}`
```json
{
"code": 0,
"data": {
"ticket": {
"short_code": "003a2hgmgety",
"qr_data": "eyJpZCI6NDgyODE1LCJnIjoxMTh9...",
"qr_expires_at": 1745291400,
"qr_expires_in": 1800,
"verify_status": 0,
"phone": "138****8000"
}
}
}
```
**QR payload签名前**
```json
{ "id": 482815, "g": 118, "iat": 1745286000, "exp": 1745287800 }
// iat = 签发时间戳exp = 过期时间戳签发后30分钟
// sig = HMAC-SHA256( payload_json, per-goods_secret )
```
---
## 三、交互规范(从 ticket_detail.html 移植)
### 3.1 选择器级联流程
```
用户选择场次
→ 重置:场馆/演播室/分区/座位图(全部清空+隐藏)
→ 更新 spec options 可用性(场次售罄检查)
用户选择场馆
→ 重置:演播室/分区/座位图(全部清空+隐藏)
→ 更新 spec options 可用性(场馆售罄检查)
用户选择演播室
→ 重置:分区/座位图(清空+隐藏分区)
→ 过滤分区选项(只显示属于该演播室的分区)
→ 动态加载该演播室的座位图(匹配 rooms[].name === currentRoom
→ 更新 spec options 可用性(演播室售罄检查)
用户选择分区
→ 显示座位图(之前已加载好)
→ filterSeats():只高亮符合当前 5 维选择的座位
→ 其他座位 opacity:0.3 不可点击
用户点击座位
→ toggleSeat加入/移出 selectedSeats[]
→ 更新已选座位 UI + 底部总价
→ 显示观演人表单(每座一个)
```
### 3.2 座位图渲染逻辑
```javascript
// renderSeatMap() — 渲染座位矩阵
mapData.forEach((rowStr, rowIndex) => {
const rowLabel = String.fromCharCode(65 + rowIndex); // 0→A, 1→B...
const chars = rowStr.split('');
chars.forEach((char, colIndex) => {
const colNum = colIndex + 1;
const seatKey = `${roomId}_${rowLabel}_${colNum}`;
const seatInfo = seatSpecMap[seatKey] || {};
const section = seatInfo.section || {};
const color = section.color || '#EA4C89';
const price = seatInfo.price || 0;
if (char === '_' || char === '-') {
// 过道空白
} else if (seatInfo.inventory > 0 && !soldSeats[seatKey]) {
// 可用座位(可点击)
} else {
// 已售座位(灰色)
}
});
});
```
### 3.3 filterSeats5 维过滤)
```javascript
filterSeats: function() {
// 当前 5 维全部匹配 + inventory > 0 → 高亮可用
// 否则 → opacity:0.3, pointerEvents:none
document.querySelectorAll('.vr-seat.available').forEach(function(el) {
const seatKey = el.dataset.seatKey;
const seatInfo = seatSpecMap[seatKey] || {};
let match = { session: true, venue: true, room: true, section: true };
if (self.currentSession) {
match.session = seatInfo.spec.some(s => s.type === '$vr-场次' && s.value === self.currentSession);
}
if (self.currentVenue) {
match.venue = seatInfo.spec.some(s => s.type === '$vr-场馆' && s.value === self.currentVenue);
}
if (self.currentRoom) {
match.room = seatInfo.spec.some(s => s.type === '$vr-演播室' && s.value === self.currentRoom);
}
if (self.currentSection) {
match.section = seatInfo.spec.some(s => s.type === '$vr-分区' && s.value === self.currentSection);
}
const available = match.session && match.venue && match.room && match.section && seatInfo.inventory > 0;
el.style.opacity = available ? '1' : '0.3';
el.style.pointerEvents = available ? 'auto' : 'none';
});
}
```
### 3.4 售罄级联灰化(从底向上)
```javascript
// updateSpecOptionsAvailability()
// 层级 1: 分区 — 有无可用座位 → 售罄变灰 + "(售罄)"标签
// 层级 2: 演播室 — 所有分区都售罄 → 演播室变灰
// 层级 3: 场馆 — 所有演播室都售罄 → 场馆变灰
// 层级 4: 场次 — 所有场馆都售罄 → 场次变灰
//
// 遍历 seatSpecMap 统计各层级可用座位数(只统计当前场次)
// 灰化时: opacity:0.4 + pointerEvents:none + 添加"(售罄)"span
// 恢复时: 移除灰化样式 + 移除"(售罄)"span
```
### 3.5 已选座位 UI
```javascript
// selectedSeats[] 数组,每选一座 push 一个对象
selectedSeats = [
{
seatKey: 'room_001_A_1',
price: 899,
rowLabel: 'A',
colNum: 1,
section: { char: 'A', name: 'VIP区', color: '#e74c3c' }
}
];
// updateSelectedUI()
// 合计总价 = selectedSeats.reduce((sum, s) => sum + s.price, 0)
// 每座显示一个观演人表单(姓名/手机/身份证)
// 底部购票按钮 disabled = selectedSeats.length === 0
```
### 3.6 观演人表单
```javascript
// renderAttendeeForms()
// 每个已选座位渲染一个表单块
`
<div class="vr-attendee-block" data-index="${i}">
<div class="vr-attendee-title">观演人 ${i+1} (${seatLabel})</div>
<div class="vr-form-row">
<label>姓名</label>
<input type="text" data-index="${i}" data-field="real_name" placeholder="请输入姓名" />
</div>
<div class="vr-form-row">
<label>手机</label>
<input type="tel" data-index="${i}" data-field="phone" placeholder="请输入手机号" />
</div>
<div class="vr-form-row">
<label>身份证</label>
<input type="text" data-index="${i}" data-field="id_card" placeholder="选填" />
</div>
</div>
`
```
---
## 四、UniApp 移植注意事项
### 4.1 页面路由
```
pages/goods-vr-ticket/goods-vr-ticket.vue — VR 票务详情页(已创建)
pages/goods-vr-ticket/components/
├── ticket-header/ — 顶部海报+收藏(已完成)
├── venue-card/ — 场馆卡片(已完成)
└── ticket-popup/ — 购票弹窗(已完成,待接入数据)
├── vr-session-select/ — 场次选择器(待实现)
├── vr-booking-block/ — 规格级联选择(场→馆→室→分区)(待实现)
├── vr-seat-selector/ — 座位选择器全屏遮罩(待实现)
└── vr-attendee-form/ — 观演人表单(待实现)
```
### 4.2 关键适配点
| H5 实现 | UniApp 适配 |
|---------|------------|
| `document.querySelectorAll()` | `uni.createSelectorQuery()` |
| `classList.add/remove` | Vue data driven: `:class="{ 'sold-out': isSoldOut }"` |
| `onclick="fn(this)"` | `@click="fn(item)"` — Vue 事件传参 |
| `style="display:none"` | `v-show``:style="{ display: isVisible ? 'block' : 'none' }"` |
| `scrollIntoView` | `uni.pageScrollTo({ selector })``scroll-view` |
| `CryptoJS.enc.Base64` | `btoa(unescape(encodeURIComponent(str)))` |
| `sessionStorage` | `uni.setStorageSync()` / `uni.getStorageSync()` |
| `window.location.href` | `uni.redirectTo()` / `uni.reLaunch()` |
| `$.ajax` | `uni.request()` |
| `_``-` 占位符 | UniApp 中 `_``-` 同样适用 |
### 4.3 API 请求封装
```javascript
// main.js 或 common/request.js 中封装
const request = (options) => {
const app = getApp();
return new Promise((resolve, reject) => {
uni.request({
url: app.globalData.get_request_url(options.action, options.controller || 'goods'),
method: options.method || 'POST',
data: options.data,
success: (res) => {
if (res.data.code == 0) {
resolve(res.data);
} else if (res.data.code == -400) {
// 未登录,跳转登录页
uni.redirectTo({ url: '/pages/login/login' });
} else {
uni.showToast({ title: res.data.msg || '请求失败', icon: 'none' });
reject(res.data);
}
},
fail: reject
});
});
};
```
### 4.4 vr_goods_config 解析流程
```javascript
// goods-vr-ticket.vue onLoad()
onLoad(params) {
const app = getApp();
const goodsId = params.id;
// 1. 尝试从全局缓存获取goods-detail 已缓存)
var goods = app.globalData.goods_data_cache_handle(goodsId);
if (goods != null) {
this.handleGoodsData(goods);
return;
}
// 2. 请求 API
request({ action: 'detail', controller: 'goods', data: { id: goodsId } })
.then(res => {
this.handleGoodsData(res.data.goods);
});
}
handleGoodsData(goods) {
this.goodsData = goods;
// 解析 vr_goods_config
var vrConfig = goods.vr_goods_config;
if (typeof vrConfig === 'string') {
vrConfig = JSON.parse(vrConfig);
}
this.vrConfig = vrConfig;
// 提取数据
this.venues = vrConfig.template_snapshot.venues;
this.sessions = vrConfig.sessions;
this.seatMap = vrConfig.template_snapshot;
// 构建 seatSpecMap由后端 GetGoodsViewData 返回,前端直接使用)
// TODO: 需要后端提供 seatSpecMap API 或嵌入 goods 响应中
this.buildSeatSpecMap();
}
```
### 4.5 待确认seatSpecMap 获取方式
**现状**`seatSpecMap` 在 H5 版由 PHP `SeatSkuService::GetGoodsViewData()` 注入模板变量,前端直接用。
**UniApp 方案 A推荐**:后端在商品详情 API 中直接返回 `seatSpecMap`(嵌入 `goods.vr_goods_config` 或单独字段)
**UniApp 方案 B**:前端按 `seatSpecMap` 相同逻辑用 JS 重建不推荐GoodsSpecValue 无法从前端获取)
**UniApp 方案 C**:新增 `GET /api/plugins/vr_ticket/seatmap?goods_id=xxx` 接口
---
## 五、购买链路完整流程
```
用户选择座位(多选)
填写观演人信息(每座一个表单)
点击"立即购票"
前端校验:座位数 === 观演人数
POST /api/cart/save
goods_data[] = selectedSeats.map(seat => ({
goods_id: goodsId,
spec_base_id: seatSpecMap[seat.seatKey].spec_base_id,
stock: 1,
extension_data: JSON.stringify({
attendee: { real_name, phone, id_card },
seat: { seat_key, row, col, section }
})
}))
buy_type: 'goods'
address_id: '0'
后端 CartSave → BuyService::BuyGoods
FOR UPDATE SKIP LOCKED 原子扣库存
库存不足 → 返回错误,提示用户重选
库存足够 → 写入订单
ShopXO 微信支付Native/H5/小程序)
支付成功 → Hook: plugins_service_order_pay_success_handle_end
→ TicketService::onOrderPaid()
→ 解析 extension_data 中的 5 维 spec
→ issueTicket() 生成短码 + QR 签名
→ 写入 vr_tickets 表
用户可在票夹页查看电子票QR码 + 短码)
```
---
## 六、票夹与 QR 票规范
### 6.1 QR 票数据结构
```json
// QR payloadBase64 编码)
{
"id": 482815,
"g": 118,
"iat": 1745286000,
"exp": 1745287800
}
// qr_data 存储格式vr_tickets.qr_data
"{short_code}|{base64(payload)}"
// 例: "003a2hgmgety|eyJpZCI6NDgyODE1LCJnIjoxMTh9..."
// 本地验证(前端)
const payload = JSON.parse(atob(qr_data.split('|')[1]));
if (payload.exp < Date.now() / 1000) {
// 已过期,显示倒计时刷新提示
}
```
### 6.2 短码格式
```
4位 goods_id(base36) + HMAC-XOR 混淆的 ticket_id
总计约 12 位,可读性好,无需搜索
```
### 6.3 票夹页面结构
```
pages/ticket-wallet/ticket-wallet.vue — 票夹列表(待实现)
├── ticket-card 组件(复用)
│ ├── 座位信息(场次/场馆/座位号)
│ ├── QR码大图可放大
│ ├── 短码(便于手动输入核销)
│ └── 状态标签(已核销/未核销/已退款)
└── Realtime 订阅 orders 表状态变更
```
---
## 七、B 端扫码核销UniApp Admin
### 7.1 核销页面
```
pages/admin-verify/admin-verify.vue — 扫码核销(待实现)
├── 摄像头扫码uni.scanCode
├── 短码/UUID 手动输入
└── 核销结果展示
```
### 7.2 核销 API
**UUID 核销**`POST /api/plugins/index?...&pluginsaction=verify`
```json
{ "ticket_code": "uuid格式-xxx-xxx" }
```
**短码自动路由**:后端 `verifyByShortCode()` 先尝试 UUID 格式,失败则解析短码。
**响应**
```json
{ "code": 0, "msg": "核销成功", "data": { "seat_info": "主要展厅 A区 1排1座", "real_name": "张三", "goods_name": "VR演唱会" }}
```
| code | 含义 |
|------|------|
| 0 | 核销成功 |
| -1 | 票不存在 |
| -2 | 已核销 |
| -3 | 已退款 |
| -999 | 系统异常 |
---
## 八、UniApp 待开发清单
### Phase 2 — 规格选择与选座(核心)
| 任务 | 状态 | 说明 |
|------|------|------|
| 接入 vr_goods_config 数据 | 待开发 | 解析场次/场馆/演播室/分区数据 |
| 场次选择器 | 待开发 | 横向滚动卡片,售罄灰化 |
| 场馆选择器 | 待开发 | 级联重置,售罄灰化 |
| 演播室选择器 | 待开发 | 动态过滤分区,显示座位图 |
| 分区选择器 | 待开发 | 演播室激活后显示,按 room 过滤 |
| 座位图渲染 | 待开发 | 字符矩阵渲染,可选/已售状态 |
| 多座位选择 | 待开发 | toggleSeatselectedSeats[] |
| 售罄级联灰化 | 待开发 | 从底向上 4 层灰化 + "(售罄)"标签 |
| 座位过滤 filterSeats | 待开发 | 按当前 5 维规格过滤 |
| 观演人表单 | 待开发 | 每座一个表单块(姓名/手机/身份证) |
### Phase 3 — 购买与支付
| 任务 | 状态 | 说明 |
|------|------|------|
| 确认购票提交 | 待开发 | CartSave API多座位 goods_data |
| 支付成功跳转 | 待开发 | 等待 ShopXO 支付回调 |
| 订单状态同步 | 待开发 | Realtime 订阅 orders 表 |
### Phase 4 — 票夹与核销
| 任务 | 状态 | 说明 |
|------|------|------|
| 票夹列表页 | 待开发 | Ticket.list API |
| 票详情页QR+短码) | 待开发 | Ticket.detail APIQR 缓存逻辑 |
| B 端核销页 | 待开发 | 扫码 + 手动输入verify API |
| Realtime 状态更新 | 待开发 | 核销后 QR 页面自动变灰 |
---
## 九、后端待配合事项
1. **seatSpecMap 返回**:建议在商品详情 API 响应中直接嵌入 `seatSpecMap`UniApp 无 GoodsSpecValue 读取权限
2. **CartSave API**:确认 `extension_data` 字段接受 JSON 序列化字符串
3. **ticket/list API**:确认 C 端 session 鉴权方式JWT 或 cookie
4. **QR 刷新机制**:前端 15 分钟阈值,前端先查 localStorage 缓存
---
## 十、参考文件索引
| 源文件 | 说明 |
|--------|------|
| `vr-shopxo-plugin/.../view/goods/ticket_detail.html` | H5 实现完整参考JS 逻辑 100% 可移植) |
| `vr-shopxo-plugin/.../service/SeatSkuService.php` | 后端 SKU 构建 + buildSeatSpecMap |
| `vr-shopxo-plugin/docs/VR_GOODS_CONFIG_SPEC.md` | v3.0 vr_goods_config 协议 |
| `vr-shopxo-plugin/docs/PLAN_5DIM_REFACTOR.md` | 5 维 SPEC_DIMS 详细说明 |
| `vr-shopxo-plugin/docs/PHASE_4_PLAN.md` | QR/短码/票夹/核销完整规范 |
| `vr-shopxo-plugin/docs/PHASE_4_API.md` | 后端 API 完整文档 |
| `vr-shopxo-plugin/docs/SPEC_SELECTOR_DESIGN.md` | 选择器交互规范 |
| `vr-shopxo-plugin/docs/SPEC_SELECTOR_DATA_DICTIONARY.md` | 模板变量数据字典 |
| `vr-shopxo-uniapp/docs/vr-ticket-integration-plan.md` | UniApp Phase 1-4 原规划 |