feat: VR ticket page integration (goods-vr-ticket) + improved .gitignore
- Add goods-vr-ticket page with ticket-header/venue-card/ticket-popup components - Integrate VR redirect logic in goods-detail (detects vr_goods_config → redirect) - Update pages.json to register goods-vr-ticket route - Add iconfont improvements - Update .gitignore: add node_modules, .agent, .claude, .vite, .vscode, .gitnexus, references, AGENTS.md, CLAUDE.mdmaster
parent
4d17eaa577
commit
2c13951249
|
|
@ -1,7 +1,27 @@
|
|||
# OS / IDE
|
||||
.DS_Store
|
||||
.hbuilderx
|
||||
.hbuilderx/
|
||||
.vscode/
|
||||
*.log
|
||||
unpackage
|
||||
|
||||
# Build output
|
||||
unpackage/
|
||||
sitemap.json
|
||||
|
||||
# AI Agent / Claude
|
||||
.agent/
|
||||
.claude/
|
||||
.vite/
|
||||
.gitnexus/
|
||||
references/
|
||||
AGENTS.md
|
||||
CLAUDE.md
|
||||
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Prettier (project-level override, not shared)
|
||||
.prettierrc.cjs
|
||||
pages/diy/init_data.js
|
||||
|
||||
# Local init data (generated per environment)
|
||||
pages/diy/init_data.js
|
||||
|
|
@ -44,6 +44,11 @@
|
|||
display: flex;
|
||||
font-size: inherit;
|
||||
overflow: hidden;
|
||||
/* 限制最大尺寸防止缩放异常 */
|
||||
max-width: 100px;
|
||||
max-height: 100px;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
/* 因icon大小被设置为和字体大小一致,而span等标签的下边缘会和字体的基线对齐,故需设置一个往下的偏移比例,来纠正视觉上的未对齐效果 */
|
||||
vertical-align: -0.15em;
|
||||
outline: none;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,441 @@
|
|||
# VR票务详情页面 UniApp 移植规划文档
|
||||
|
||||
> 创建时间:2026-05-13
|
||||
> 状态:**Phase 3 导航优化完成(防闪烁/缓存预检查)**
|
||||
|
||||
---
|
||||
|
||||
## 📌 背景与目标
|
||||
|
||||
### 背景
|
||||
- 后端 VR Ticket 插件 (`vr_ticket`) 已实现票务商品详情页模板 `ticket_detail.html`
|
||||
- 前端参考项目 [show-start](file:///Users/bigemon/WorkSpace/vr-shopxo-uniapp/references/show-start/src/App.vue) 提供了高质量的 UI 设计
|
||||
- 需要将参考项目的 UI 移植到 UniApp 环境中,并整合票务商品的4维选择逻辑
|
||||
|
||||
### 用户需求
|
||||
1. **保持 show-start 的 popup 交互模式** 作为购票弹窗基础
|
||||
2. **点击"立即购票"按钮** 后弹出购票弹窗
|
||||
3. 弹窗内容包含:场次选择、场馆/演播室/分区选择、"选座"按钮
|
||||
4. **选座功能**:独立的全屏遮罩座位选择器,选完后返回已选座位 tags
|
||||
|
||||
---
|
||||
|
||||
## 📋 数据结构规范(Phase 2 核心)
|
||||
|
||||
### VR 票务扩展字段
|
||||
从后端 API 返回的商品扩展数据中,票务商品会包含以下关键字段:
|
||||
|
||||
```typescript
|
||||
interface VRTicketGoods {
|
||||
id: number;
|
||||
title: string;
|
||||
images: string[];
|
||||
is_vr_ticket: 1; // 票务商品标识
|
||||
|
||||
// VR 票务扩展数据(来自 vr_config 或 extension_data)
|
||||
vr_config?: {
|
||||
// 场次规格列表(从 goods_spec_data 解析)
|
||||
spec_list?: Array<{
|
||||
spec_name: string; // 场次名称,如 "2026-06-01 14:00"
|
||||
price: number; // 价格
|
||||
inventory: number; // 库存
|
||||
}>;
|
||||
|
||||
// 座位规格映射(从 seat_spec_map 解析)
|
||||
seat_spec_map?: Record<string, {
|
||||
spec: Array<{ type: string; value: string }>; // 4维规格
|
||||
price: number;
|
||||
inventory: number;
|
||||
section: { name: string; color: string };
|
||||
}>;
|
||||
|
||||
// 座位模板(座位图数据)
|
||||
seat_template?: {
|
||||
venue: { name: string; address: string; location: { lng: string; lat: string } };
|
||||
rooms: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
map: string[]; // 座位矩阵,如 ["AAAAA", "AAB__AA"]
|
||||
sections: Array<{ char: string; name: string; price: number; color: string }>;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 4维规格体系
|
||||
票务商品的选座流程采用 **4维规格级联选择**:
|
||||
|
||||
| 维度 | 字段名 | 说明 | 示例 |
|
||||
|------|--------|------|------|
|
||||
| 1 | `$vr-场次` | 演出场次/时间 | "2026-06-01 14:00" |
|
||||
| 2 | `$vr-场馆` | 演出场馆 | "北京凯迪拉克中心" |
|
||||
| 3 | `$vr-演播室` | 室内演出厅 | "主要展厅" |
|
||||
| 4 | `$vr-分区` | 座位分区 | "VIP区"、"普通区" |
|
||||
|
||||
**选择级联逻辑**:选择场次 → 筛选可用场馆 → 选择场馆 → 筛选可用演播室 → 选择演播室 → 筛选可用分区 → 选择分区 → 加载座位图
|
||||
|
||||
---
|
||||
|
||||
## 📐 UI/UX 设计概览
|
||||
|
||||
### 页面结构
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Header Banner (海报 + 标题 + 收藏) │
|
||||
├─────────────────────────────────────┤
|
||||
│ Location Card (最近场馆) │
|
||||
├─────────────────────────────────────┤
|
||||
│ Service Tags (服务说明) │
|
||||
├─────────────────────────────────────┤
|
||||
│ 演出详情 Section │
|
||||
├─────────────────────────────────────┤
|
||||
│ 购票须知 Section │
|
||||
├─────────────────────────────────────┤
|
||||
│ 观演须知 Section │
|
||||
├─────────────────────────────────────┤
|
||||
│ Bottom Bar (客服 + 立即购票) │ ← Fixed 底部
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 购票弹窗 Popup 交互(核心功能区)
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ [场次选择 - 横向卡片滚动] │ ← vr-session-grid
|
||||
├─────────────────────────────────────┤
|
||||
│ [选择场馆] │ ← vr-booking-block
|
||||
├─────────────────────────────────────┤
|
||||
│ [选择演播室] → [选择分区] │ ← 级联选择
|
||||
├─────────────────────────────────────┤
|
||||
│ [选座] 按钮 ← 打开座位选择器 │
|
||||
│ │
|
||||
│ 未选座时:尚未选择 [立刻选座] │
|
||||
│ 已选座时: │
|
||||
│ ┌─────┐ ┌─────┐ ┌─────┐ │
|
||||
│ │ A1 │ │ A2 │ │ A3 │ │ ← 已选座位 tags
|
||||
│ └─────┘ └─────┘ └─────┘ │
|
||||
│ 【重新选座】 │
|
||||
├─────────────────────────────────────┤
|
||||
│ 合计: ¥888 [确认购票] │ ← 底部购买栏
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 座位选择器(全屏遮罩)
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ ← 返回 选择座位 [已完成 3 座] │
|
||||
├─────────────────────────────────────┤
|
||||
│ 🎵 舞 台 │ ← 舞台指示
|
||||
├─────────────────────────────────────┤
|
||||
│ A ■ ■ ■ ■ ■ │ ← 座位行(A排)
|
||||
│ B ■ ■ ■ ■ ■ ■ ■ │ ← 可用座位:■ 点击选中
|
||||
│ C ■ _ _ ■ ■ ■ │ ← 过道:_
|
||||
│ D ■ ■ ■ ■ ■ │ ← 已售:灰色
|
||||
├─────────────────────────────────────┤
|
||||
│ 图例: [VIP] [普通] [已售] │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 座位状态定义
|
||||
- **可用座位**:可点击选择,带分区颜色
|
||||
- **已选座位**:高亮边框,数字标识
|
||||
- **已售座位**:灰色,不可点击
|
||||
- **过道/空位**:占位符,不渲染
|
||||
|
||||
---
|
||||
|
||||
## 🔄 阶段进展记录
|
||||
|
||||
### Phase 1: 基础 UI 移植 ✅ 已完成
|
||||
- [x] 在 `pages.json` 中注册新页面路由
|
||||
- [x] 创建 `pages/goods-vr-ticket/` 目录
|
||||
- [x] 搬运 `App.vue` 内容到 `goods-vr-ticket.vue`
|
||||
- [x] 适配 UniApp 语法(scrollable、navigation 等)
|
||||
- [x] 替换内联 SVG → iconfont 组件(兼容<E585BC><E5AEB9>信小程序)
|
||||
- [x] 添加 popup 容器左右留白(padding: 16px)
|
||||
- [x] 修复 popup-content 溢出问题(overflow-x, word-break)
|
||||
|
||||
### Phase 2: 购票弹窗与4维选择(开发中)
|
||||
- [x] 购票弹窗基础结构(当前 popupType='buy')
|
||||
- [ ] 接入商品 API 数据(vr_config 解析)
|
||||
- [ ] 场次选择器实现(横向滚动卡片)
|
||||
- [ ] 场馆选择器实现
|
||||
- [ ] 演播室/分区级联选择
|
||||
- [ ] 座位选择器组件开发
|
||||
- [ ] 已选座位 UI 展示
|
||||
- [ ] 底部总价计算
|
||||
|
||||
### Phase 3: 完整购票流程(待开发)
|
||||
- [ ] 观演人信息表单(每座必填)
|
||||
- [ ] 订单提交 API 集成
|
||||
- [ ] 跳转支付流程
|
||||
|
||||
### Phase 4: 优化与测试
|
||||
- [ ] 滚动穿透处理(catchtouchmove)
|
||||
- [ ] 真机/小程序测试
|
||||
- [ ] 性能优化
|
||||
|
||||
---
|
||||
|
||||
## 📝 待办事项
|
||||
|
||||
### ✅ 已完成
|
||||
- [x] 在 `pages.json` 中注册新页面路由
|
||||
- [x] 创建 `pages/goods-vr-ticket/` 目录
|
||||
- [x] 搬运 `App.vue` 内容到 `goods-vr-ticket.vue`
|
||||
- [x] 适配 UniApp 语法(scrollable、navigation 等)
|
||||
- [x] 替换内联 SVG → iconfont 组件(兼容微信小程序)
|
||||
- [x] 添加 popup 容器左右留白(padding: 16px)
|
||||
- [x] 修复 popup-content 溢出问题(overflow-x, word-break)
|
||||
- [x] 购票弹窗基础框架(popupType='buy' 判断)
|
||||
|
||||
### ⏳ 后续待办
|
||||
- [x] 在 `goods-detail.vue` 中添加跳转判断逻辑
|
||||
- [ ] 解析商品数据中的 vr_config 等扩展字段
|
||||
- [ ] 实现场次选择器(横向滚动卡片)
|
||||
- [ ] 实现场馆选择器(场次筛选后可用)
|
||||
- [ ] 实现演播室→分区级联选择
|
||||
- [ ] 实现选座按钮与座位选择器(全屏遮罩)
|
||||
- [ ] 实现已选座位 tags 展示
|
||||
- [ ] 实现重新选座功能
|
||||
- [ ] 连接后端 BuyService API 获取动态票务数据
|
||||
- [ ] 实现底部购买栏联动(已选座数、总价)
|
||||
- [ ] 添加 popup 滚动事件处理(:catchtouchmove)
|
||||
- [ ] 观演人信息表单(姓名/手机/身份证)
|
||||
- [ ] 订单提交入口
|
||||
- [ ] 测试页面在真机/小程序环境运行
|
||||
|
||||
---
|
||||
|
||||
## 🔌 API 接口规范
|
||||
|
||||
### 商品详情接口(扩展字段)
|
||||
请求商品详情时,后端返回 `vr_config` 扩展数据:
|
||||
|
||||
```javascript
|
||||
// GET /api/goods/detail?id=xxx
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"id": 100,
|
||||
"title": "王一浩「To U」巡演北京收官站",
|
||||
"images": "[\"xxx.jpg\"]",
|
||||
"is_vr_ticket": 1,
|
||||
// VR 票务扩展
|
||||
"vr_config": {
|
||||
"goods_spec_data": [
|
||||
{ "spec_name": "2026-06-01 14:00", "price": 580, "inventory": 100 },
|
||||
{ "spec_name": "2026-06-01 20:00", "price": 680, "inventory": 50 }
|
||||
],
|
||||
"seat_spec_map": {
|
||||
"room_001_A_1": {
|
||||
"spec": [
|
||||
{ "type": "$vr-场次", "value": "2026-06-01 14:00" },
|
||||
{ "type": "$vr-场馆", "value": "北京凯迪拉克中心" },
|
||||
{ "type": "$vr-演播室", "value": "主要展厅" },
|
||||
{ "type": "$vr-分区", "value": "VIP区" }
|
||||
],
|
||||
"price": 580,
|
||||
"inventory": 1,
|
||||
"section": { "name": "VIP区", "color": "#ff4d4f" }
|
||||
}
|
||||
},
|
||||
"vr_seat_template": {
|
||||
"venue": { "name": "北京凯迪拉克中心", "address": "...", "location": { "lng": "116.4", "lat": "39.9" } },
|
||||
"rooms": [...]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 订单提交接口
|
||||
```javascript
|
||||
// POST /api/buy/add
|
||||
{
|
||||
"goods_data": [
|
||||
{
|
||||
"goods_id": 100,
|
||||
"spec": [...], // 4维规格数组
|
||||
"stock": 1,
|
||||
"order_base": {
|
||||
"extension_data": {
|
||||
"attendee": {
|
||||
"real_name": "张三",
|
||||
"phone": "13800138000",
|
||||
"id_card": "110101199001011234"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"buy_type": "goods",
|
||||
"address_id": "0",
|
||||
"site_model": "2"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 技术实现要点
|
||||
|
||||
### 1. 4维规格筛选逻辑
|
||||
参考 `ticket_detail.html` 中的 `filterSeats` 方法:
|
||||
|
||||
```javascript
|
||||
// 根据当前选择的4维过滤可用座位
|
||||
filterSeats: function() {
|
||||
var matchSession = this.currentSession != null;
|
||||
var matchVenue = this.currentVenue != null;
|
||||
var matchRoom = this.currentRoom != null;
|
||||
var matchSection = this.currentSection != null;
|
||||
|
||||
// 遍历所有座位,检查是否满足当前选择条件
|
||||
for (var i = 0; i < seatInfo.spec.length; i++) {
|
||||
if (seatInfo.spec[i].type === '$vr-场次' && seatInfo.spec[i].value === this.currentSession) {
|
||||
matchSession = true;
|
||||
}
|
||||
// ... 同理其他维度
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 座位状态管理
|
||||
- `selectedSeats[]`:已选座位数组
|
||||
- `soldSeats{}`:已售座位映射
|
||||
- 切换场次时清空已选座位
|
||||
|
||||
### 3. 级联选择
|
||||
- 选择「场次」后 → 重置「场馆/演播室/分区」,重新计算可选场馆
|
||||
- 选择「场馆」后 → 筛选出<E98089><E587BA>场馆下的演播室
|
||||
- 选择「演播室」后 → 筛选出该演播室下的分区,显示分区选择器
|
||||
|
||||
### 4. 座位渲染
|
||||
座位图使用字符矩阵渲染:
|
||||
- `A`/`B`/...:座位字符
|
||||
- `_`:过道/空位
|
||||
|
||||
---
|
||||
|
||||
## 📦 文件结构
|
||||
|
||||
```
|
||||
pages/goods-vr-ticket/
|
||||
├── goods-vr-ticket.vue # 主页面(已移植 Phase 1)
|
||||
├── goods-vr-ticket.css # 独立样式(可选)
|
||||
└── components/
|
||||
├── vr-session-select/ # 场次选择器组件
|
||||
├── vr-venue-select/ # 场馆选择器组件
|
||||
├── vr-seat-selector/ # 座位选择器(全屏遮罩)
|
||||
└── vr-attendee-form/ # 观演人表单
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔗 相关参考
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| [references/show-start/src/App.vue](file:///Users/bigemon/WorkSpace/vr-shopxo-uniapp/references/show-start/src/App.vue) | 源 UI 参考(Vue 2) |
|
||||
| [pages/goods-detail/goods-detail.vue](file:///Users/bigemon/WorkSpace/vr-shopxo-uniapp/pages/goods-detail/goods-detail.vue) | 商品详情页(跳转入口) |
|
||||
| [vr_ticket/ticket_detail.html](file:///Users/bigemon/WorkSpace/vr-shopxo-plugin/shopxo/app/plugins/vr_ticket/view/goods/ticket_detail.html) | 后端票务模板参考(4维选择+座位图) |
|
||||
| [pages.json](file:///Users/bigemon/WorkSpace/vr-shopxo-uniapp/pages.json) | 页面路由配置 |
|
||||
|
||||
---
|
||||
|
||||
## 🎯 关键文件
|
||||
|
||||
| 文件 | 状态 | 说明 |
|
||||
|------|------|------|
|
||||
| pages/goods-vr-ticket/goods-vr-ticket.vue | ✅ 已移植 | 主页面(Phase 1 完成) |
|
||||
| pages/goods-detail/goods-detail.vue | ⏳ 待修改 | 添加 is_vr_ticket 跳转判断 |
|
||||
| components/iconfont/iconfont.vue | ✅ 已加固 | iconfont 组件 |
|
||||
|
||||
---
|
||||
|
||||
> ⚠️ **注意**:当前 Phase 2 正在开发中,核心为 4维规格选择器 + 座位选择器。Phase 3 将实现观演人表单和订单提交。座时:尚未选择 [立刻选座] │
|
||||
│ 已选座时: │
|
||||
│ [A区-2排-3号] [A区-2排-4号] │
|
||||
│ 【重新选座】 │
|
||||
├─────────────────────────────────────┤
|
||||
│ [确认购票] │
|
||||
└─────────────────────────────────────┘
|
||||
|
||||
点击[选座] → 打开座位选择器(全屏遮罩)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🗂️ 文件结构(Phase 1 完成后)
|
||||
|
||||
```
|
||||
pages/goods-vr-ticket/
|
||||
├── goods-vr-ticket.vue # 主页面(移植自 App.vue)
|
||||
└── goods-vr-ticket.css # 独立样式文件(可选)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 待办事项
|
||||
|
||||
### ✅ 已完成
|
||||
- [x] 在 `pages.json` 中注册新页面路由
|
||||
- [x] 创建 `pages/goods-vr-ticket/` 目录
|
||||
- [x] 搬运 `App.vue` 内容到 `goods-vr-ticket.vue`
|
||||
- [x] 适配 UniApp 语法(scrollable、navigation 等)
|
||||
- [x] 替换内联 SVG → iconfont 组件(兼容微信小程序)
|
||||
- [x] 添加 popup 容器左右留白(padding: 16px)
|
||||
- [x] 修复 popup-content 溢出问题(overflow-x, word-break)
|
||||
|
||||
### ⏳ 后续待办
|
||||
- [ ] 在 `goods-detail.vue` 中添加跳转判断逻辑
|
||||
- [ ] 对 商品数据里的vr_config 等数据进行解析,
|
||||
- [ ] 实现购票弹窗 Popup 交互,移植老的4 维规格选择到新的购票弹窗 Popup 中。
|
||||
- [ ] 实现购票弹窗内的选座逻辑
|
||||
- [ ] 连接后端 BuyService API(动态票务数据)
|
||||
- [ ] 优化 popup 滚动事件处理(:catchtouchmove)
|
||||
- [ ] 测试页面在真机/小程序环境运行
|
||||
|
||||
---
|
||||
|
||||
## 🔄 阶段进展记录
|
||||
|
||||
### 2026-05-13 样式迁移完成
|
||||
- ✅ 移除 inline SVG,改用 iconfont 组件
|
||||
- ✅ map-icon 容器添加边框
|
||||
- ✅ venue-popup-info 添加地址图标(icon-location)
|
||||
- ✅ popup-container 添加 padding: 0 16px
|
||||
- ✅ popup-content 修复溢出(word-break, box-sizing)
|
||||
|
||||
### 2026-05-14 导航优化完成
|
||||
- ✅ goods-detail.vue 添加白色遮罩(初始覆盖,200ms后渐变消失)
|
||||
- ✅ 添加缓存预检查(快速判断VR票务,无需等待API)
|
||||
- ✅ goods-vr-ticket.vue 加载遮罩 + pageShow 显示控制
|
||||
- ✅ 修复返回导航:智能判断页面栈,无历史则跳转首页
|
||||
- ✅ goods-detail 使用 redirectTo 替代 navigateTo,避免循环
|
||||
|
||||
### 关键文件
|
||||
| 文件 | 状态 |
|
||||
|------|------|
|
||||
| pages/goods-vr-ticket/goods-vr-ticket.vue | ✅ 已移植 |
|
||||
| pages/goods-vr-ticket/goods-vr-ticket.css | ✅ 已移植 |
|
||||
| pages/goods-vr-ticket/components/ticket-header/index.vue | ✅ 已移植 |
|
||||
| pages/goods-vr-ticket/components/venue-card/index.vue | ✅ 已移植 |
|
||||
| pages/goods-vr-ticket/components/ticket-popup/index.vue | ✅ 已移植 |
|
||||
| components/iconfont/iconfont.vue | ✅ 已加固尺寸 |
|
||||
| pages/goods-detail/goods-detail.vue | ✅ 已加固导航 |
|
||||
|
||||
---
|
||||
|
||||
## 🔗 相关参考
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| [references/show-start/src/App.vue](file:///Users/bigemon/WorkSpace/vr-shopxo-uniapp/references/show-start/src/App.vue) | 源 UI 参考(Vue 2) |
|
||||
| [pages/goods-detail/goods-detail.vue](file:///Users/bigemon/WorkSpace/vr-shopxo-uniapp/pages/goods-detail/goods-detail.vue) | 商品详情页(跳转入口) |
|
||||
| [vr_ticket/ticket_detail.html](file:///Users/bigemon/WorkSpace/vr-shopxo-plugin/shopxo/app/plugins/vr_ticket/view/goods/ticket_detail.html) | 后端票务模板参考 |
|
||||
| [pages.json](file:///Users/bigemon/WorkSpace/vr-shopxo-uniapp/pages.json) | 页面路由配置 |
|
||||
|
||||
---
|
||||
|
||||
> ⚠️ **注意**:当前为 Phase 1 规划,仅实现初步移植和测试环境搭建。后续 Phase 2-4 将逐步实现座位选择器、4维规格选择、完整购票流程等高级功能。
|
||||
13
pages.json
13
pages.json
|
|
@ -211,6 +211,19 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "pages/goods-vr-ticket",
|
||||
"pages": [
|
||||
{
|
||||
"path": "goods-vr-ticket",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"enablePullDownRefresh": true,
|
||||
"navigationBarTitleText": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "pages/login",
|
||||
"pages": [
|
||||
|
|
|
|||
|
|
@ -9,10 +9,12 @@
|
|||
top: 28px;
|
||||
z-index: 11;
|
||||
}
|
||||
|
||||
.top-nav-left-icon {
|
||||
left: auto;
|
||||
margin-left: 20rpx;
|
||||
}
|
||||
|
||||
.top-nav-left-icon .icon {
|
||||
background: rgb(255 255 255 / 70%);
|
||||
width: 25px;
|
||||
|
|
@ -23,6 +25,7 @@
|
|||
text-align: center;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
.top-nav {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
|
|
@ -32,20 +35,24 @@
|
|||
padding-bottom: 5px;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.top-nav-content {
|
||||
margin: 0 auto;
|
||||
width: 310rpx;
|
||||
margin-left: calc(50% - 170rpx);
|
||||
}
|
||||
|
||||
.top-nav-content text {
|
||||
font-size: 30rpx;
|
||||
/* #ifdef H5 || APP */
|
||||
font-size: 14px;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.top-nav-content text:not(:last-child) {
|
||||
margin-right: 30rpx;
|
||||
}
|
||||
|
||||
.top-nav-content .nav-active {
|
||||
font-weight: 500;
|
||||
padding-bottom: 2rpx;
|
||||
|
|
@ -53,6 +60,25 @@
|
|||
border-width: 0 0 2px 0;
|
||||
}
|
||||
|
||||
/**
|
||||
/* VR 票务跳转遮罩 */
|
||||
.vr-redirect-cover {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: #fff;
|
||||
z-index: 9999;
|
||||
opacity: 1;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.vr-redirect-cover-fade {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* 顶部更多导航
|
||||
*/
|
||||
|
|
@ -62,6 +88,7 @@
|
|||
top: 62px;
|
||||
left: 20rpx;
|
||||
}
|
||||
|
||||
.nav-more-view .triangle {
|
||||
width: 0;
|
||||
height: 0;
|
||||
|
|
@ -71,12 +98,15 @@
|
|||
top: -5px;
|
||||
left: calc(20rpx + 42px);
|
||||
}
|
||||
|
||||
.nav-more-view .content {
|
||||
background: rgb(0 0 0 / 0.7);
|
||||
}
|
||||
|
||||
.nav-more-view .content .item {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.nav-more-view .content .item:not(:last-child) {
|
||||
border-bottom: 1px solid #585858;
|
||||
}
|
||||
|
|
@ -100,48 +130,61 @@
|
|||
margin-right: 15rpx;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.goods-buy-nav .image {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
margin: 10rpx 0 5rpx 0;
|
||||
}
|
||||
|
||||
.goods-buy-nav .dis-block {
|
||||
margin-top: -10rpx;
|
||||
}
|
||||
|
||||
.goods-buy-nav .badge-icon {
|
||||
position: absolute;
|
||||
top: 2rpx;
|
||||
left: calc(50% + 25rpx);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.goods-buy-nav-btn-number-0 .btn,
|
||||
.goods-buy-nav-btn-number-1 .btn {
|
||||
width: calc(100% - 15rpx) !important;
|
||||
}
|
||||
|
||||
.goods-buy-nav-btn-number-2 .btn {
|
||||
width: calc(50% - 15rpx) !important;
|
||||
}
|
||||
|
||||
.goods-buy-nav-btn-number-3 .btn {
|
||||
width: calc(33.33% - 15rpx) !important;
|
||||
}
|
||||
|
||||
.goods-buy-nav-btn-number-4 .btn {
|
||||
width: calc(25% - 15rpx) !important;
|
||||
}
|
||||
|
||||
.goods-buy-nav-btn-number-5 .btn {
|
||||
width: calc(20% - 15rpx) !important;
|
||||
}
|
||||
|
||||
.goods-buy-nav-btn-number-6 .btn {
|
||||
width: calc(16.66% - 15rpx) !important;
|
||||
}
|
||||
|
||||
.goods-buy-nav-btn-number-7 .btn {
|
||||
width: calc(14.28% - 15rpx) !important;
|
||||
}
|
||||
|
||||
.goods-buy-nav-btn-number-8 .btn {
|
||||
width: calc(12.5% - 15rpx) !important;
|
||||
}
|
||||
|
||||
.goods-buy-nav-btn-number-9 .btn {
|
||||
width: calc(11.11% - 15rpx) !important;
|
||||
}
|
||||
|
||||
.goods-buy-nav-btn-number-10 .btn {
|
||||
width: calc(10% - 15rpx) !important;
|
||||
}
|
||||
|
|
@ -153,16 +196,19 @@
|
|||
position: relative;
|
||||
min-height: 86rpx;
|
||||
}
|
||||
|
||||
.goods-base-content .goods-title-content .goods-title {
|
||||
font-size: 30rpx;
|
||||
line-height: 40rpx;
|
||||
font-weight: bold;
|
||||
width: calc(100% - 200rpx);
|
||||
}
|
||||
|
||||
.goods-base-content image {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
}
|
||||
|
||||
.goods-base-content .collect {
|
||||
width: 76rpx;
|
||||
}
|
||||
|
|
@ -176,16 +222,19 @@
|
|||
left: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.goods-video video {
|
||||
width: 100%;
|
||||
height: 55vh !important;
|
||||
}
|
||||
|
||||
.goods-video-submit {
|
||||
left: 50rpx;
|
||||
top: auto;
|
||||
bottom: 60rpx;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.goods-video-submit image {
|
||||
width: 80rpx;
|
||||
height: 80rpx !important;
|
||||
|
|
@ -199,25 +248,31 @@
|
|||
.goods-base-right-opt {
|
||||
min-height: 145rpx;
|
||||
}
|
||||
|
||||
.price-content {
|
||||
width: calc(100% - 200rpx);
|
||||
}
|
||||
|
||||
.goods-base-right-opt {
|
||||
width: 200rpx;
|
||||
}
|
||||
|
||||
.price-content,
|
||||
.goods-base-right-opt {
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.goods-base-right-opt image {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
}
|
||||
|
||||
.goods-base-right-opt .collect {
|
||||
width: 76rpx;
|
||||
}
|
||||
|
||||
.price-content .price-icon {
|
||||
padding: 0px 8rpx;
|
||||
color: #fff;
|
||||
|
|
@ -225,9 +280,11 @@
|
|||
background: #FF5722;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.price-content .sales-price {
|
||||
font-size: 42rpx;
|
||||
}
|
||||
|
||||
.price-content .item:not(:last-child) {
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
|
@ -248,13 +305,16 @@
|
|||
.countdown-content {
|
||||
width: 38%;
|
||||
}
|
||||
|
||||
.countdown-content .time-title {
|
||||
font-size: 38rpx;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.goods-base-price-countdown {
|
||||
background: #e84742 !important;
|
||||
}
|
||||
|
||||
.goods-base-price-countdown .price-content {
|
||||
background: #f9534e;
|
||||
background-repeat: no-repeat;
|
||||
|
|
@ -262,21 +322,25 @@
|
|||
width: 60%;
|
||||
min-height: 164rpx;
|
||||
}
|
||||
|
||||
.goods-base-price-countdown .price-content .price-icon {
|
||||
border-color: #fff;
|
||||
color: #fff;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.goods-base-price-countdown .price-content .sales-price,
|
||||
.goods-base-price-countdown .price-content .sales-price-unit,
|
||||
.goods-base-price-countdown .price-content .points-price-value,
|
||||
.goods-base-price-countdown .price-content .points-price-unit {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.goods-base-price-countdown .price-content .original-price,
|
||||
.goods-base-price-countdown .price-content .unit{
|
||||
.goods-base-price-countdown .price-content .unit {
|
||||
color: #ffcece !important;
|
||||
}
|
||||
|
||||
.price-content .price-icon.seckill {
|
||||
position: absolute;
|
||||
right: 20rpx;
|
||||
|
|
@ -291,9 +355,11 @@
|
|||
padding: 8rpx 10rpx;
|
||||
width: calc(50% - 20rpx);
|
||||
}
|
||||
|
||||
.goods-parameters .item:nth-child(2n-1) {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.goods-parameters .item:nth-child(2n) {
|
||||
float: right;
|
||||
}
|
||||
|
|
@ -306,9 +372,11 @@
|
|||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.popup-params-container .item .name {
|
||||
width: 220rpx;
|
||||
}
|
||||
|
||||
.popup-params-container .item .value {
|
||||
width: calc(100% - 240rpx);
|
||||
}
|
||||
|
|
@ -331,9 +399,11 @@
|
|||
background: #fffbfc;
|
||||
color: #d2354c;
|
||||
}
|
||||
|
||||
.goods-panel-container view {
|
||||
padding: 15rpx 0;
|
||||
}
|
||||
|
||||
.goods-panel-container view:not(:first-child) {
|
||||
border-top: 1px dashed #fff1f2;
|
||||
}
|
||||
|
|
@ -360,6 +430,7 @@
|
|||
-moz-box-shadow: 0 8px 10px rgba(0, 0, 0, 0.2);
|
||||
box-shadow: 0 8px 10px rgb(0 0 0 / 20%);
|
||||
}
|
||||
|
||||
.plugins-salerecords-tips image,
|
||||
.plugins-salerecords-tips-top-left image,
|
||||
.plugins-salerecords-tips-top-right image,
|
||||
|
|
@ -371,15 +442,18 @@
|
|||
height: 50rpx;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.plugins-salerecords-tips-top-left {
|
||||
left: 5%;
|
||||
top: 13%;
|
||||
}
|
||||
|
||||
.plugins-salerecords-tips-top-right {
|
||||
right: 5%;
|
||||
top: 13%;
|
||||
left: auto;
|
||||
}
|
||||
|
||||
.plugins-salerecords-tips-top-center {
|
||||
top: 13%;
|
||||
}
|
||||
|
|
@ -388,9 +462,11 @@
|
|||
.plugins-salerecords-tips-top-left {
|
||||
top: 5%;
|
||||
}
|
||||
|
||||
.plugins-salerecords-tips-top-right {
|
||||
top: 5%;
|
||||
}
|
||||
|
||||
.plugins-salerecords-tips-top-center {
|
||||
top: 5%;
|
||||
}
|
||||
|
|
@ -401,12 +477,14 @@
|
|||
bottom: 10%;
|
||||
top: auto;
|
||||
}
|
||||
|
||||
.plugins-salerecords-tips-bottom-right {
|
||||
right: 5%;
|
||||
bottom: 10%;
|
||||
left: auto;
|
||||
top: auto;
|
||||
}
|
||||
|
||||
.plugins-salerecords-tips-bottom-center {
|
||||
bottom: 10%;
|
||||
top: auto;
|
||||
|
|
@ -418,6 +496,7 @@
|
|||
.coupon-padding {
|
||||
padding: 2rpx;
|
||||
}
|
||||
|
||||
.mini-coupon {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
|
|
@ -426,6 +505,7 @@
|
|||
border-radius: 4rpx;
|
||||
padding: 0 16rpx;
|
||||
}
|
||||
|
||||
.mini-coupon::before,
|
||||
.mini-coupon::after {
|
||||
content: '';
|
||||
|
|
@ -437,64 +517,80 @@
|
|||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.mini-coupon::before {
|
||||
left: -8rpx;
|
||||
}
|
||||
|
||||
.mini-coupon::after {
|
||||
right: -8rpx;
|
||||
}
|
||||
|
||||
.mini-coupon-br::before,
|
||||
.mini-coupon-br::after {
|
||||
border: 1px solid #FE5500;
|
||||
}
|
||||
|
||||
.coupon-srcoll {
|
||||
white-space: nowrap;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.coupon-srcoll::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.received-coupon {
|
||||
background: #FFEBE0;
|
||||
border: 2rpx solid #FE5500;
|
||||
color: #FE5500;
|
||||
}
|
||||
|
||||
.not-received-coupon {
|
||||
background: #FE5500;
|
||||
border: 2rpx solid #FE5500;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.received-coupon .divider-l::before {
|
||||
background: red;
|
||||
}
|
||||
|
||||
.not-received-coupon .divider-l::before {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.plugins-coupon-container-view .item-title {
|
||||
min-width: 70rpx;
|
||||
}
|
||||
|
||||
.plugins-coupon-container-view .item:not(:last-child) {
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
|
||||
.plugins-coupon-container {
|
||||
max-height: 50vh;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.plugins-coupon-container .v-left {
|
||||
padding: 20rpx 10rpx 20rpx 20rpx;
|
||||
}
|
||||
|
||||
.plugins-coupon-container .item:not(:last-child) {
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.plugins-coupon-container .item,
|
||||
.plugins-coupon-container .v-right,
|
||||
.plugins-coupon-container .v-right .circle {
|
||||
height: 175rpx;
|
||||
}
|
||||
|
||||
.plugins-coupon-container .v-left .base .price {
|
||||
font-size: 46rpx;
|
||||
}
|
||||
|
|
@ -505,17 +601,21 @@
|
|||
.plugins-label navigator:not(:last-child) {
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.plugins-label navigator {
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.plugins-label-top-left,
|
||||
.plugins-label-top-center,
|
||||
.plugins-label-top-right {
|
||||
top: calc(var(--status-bar-height) + 130rpx);
|
||||
}
|
||||
|
||||
.plugins-label-text {
|
||||
padding: 20rpx 20rpx 0 20rpx;
|
||||
}
|
||||
|
||||
.plugins-label-bottom-left,
|
||||
.plugins-label-bottom-center,
|
||||
.plugins-label-bottom-right {
|
||||
|
|
@ -531,14 +631,17 @@
|
|||
border-color: #ffe2e0;
|
||||
color: #dd514c;
|
||||
}
|
||||
|
||||
.plugins-intellectstools-content-top-container {
|
||||
background: #daeeff;
|
||||
color: #2b6e8f;
|
||||
border: 1px solid #c9e9ff;
|
||||
}
|
||||
|
||||
.plugins-intellectstools-content-top-container .panel-content .item:first-child {
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
.plugins-intellectstools-content-top-container .panel-title,
|
||||
.plugins-intellectstools-content-top-container .panel-content .item:not(:last-child) {
|
||||
border-bottom: 1px solid #c8e4fb !important;
|
||||
|
|
@ -561,13 +664,16 @@
|
|||
width: 32rpx;
|
||||
height: 32rpx !important;
|
||||
}
|
||||
|
||||
.plugins-goodsservice-container .item image {
|
||||
width: 70rpx;
|
||||
height: 70rpx !important;
|
||||
}
|
||||
|
||||
.plugins-goodsservice-container .item .left {
|
||||
width: 80rpx;
|
||||
}
|
||||
|
||||
.plugins-goodsservice-container .item .right {
|
||||
width: calc(100% - 90rpx);
|
||||
}
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
<template>
|
||||
<view :class="theme_view">
|
||||
<!-- VR 票务跳转遮罩 -->
|
||||
<view :class="['vr-redirect-cover', vr_cover_fade ? 'vr-redirect-cover-fade' : '']"></view>
|
||||
|
||||
<!-- 顶部导航 -->
|
||||
<view v-if="goods != null" :class="'page ' + (is_single_page == 1 ? ' single-page-top' : '')">
|
||||
<!-- 单页模式不展示导航栏 -->
|
||||
|
|
@ -710,6 +713,9 @@
|
|||
goods_bottom_nav_status: true,
|
||||
// 底部导航展示状态、如果已开启多门店默认展示、则先展示加载
|
||||
goods_bottom_opt_nav_status: app.globalData.get_config('plugins_base.realstore', null) == null,
|
||||
// VR 票务跳转遮罩
|
||||
vr_redirect_cover: true,
|
||||
vr_cover_fade: false,
|
||||
// 更多导航
|
||||
nav_more_status: false,
|
||||
nav_more_timer: null,
|
||||
|
|
@ -843,6 +849,20 @@
|
|||
// 是否自定义购物车状态
|
||||
is_opt_cart: params.is_opt_cart === undefined ? app.globalData.data.is_goods_bottom_opt_cart || 0 : parseInt(params.is_opt_cart || 0),
|
||||
});
|
||||
|
||||
// VR票务遮罩延迟渐变取消
|
||||
setTimeout(() => {
|
||||
this.setData({ vr_cover_fade: true });
|
||||
}, 200);
|
||||
|
||||
// 从缓存预检查VR票务,快速跳转
|
||||
var cachedGoods = app.globalData.goods_data_cache_handle(params.id);
|
||||
if (cachedGoods && cachedGoods.vr_goods_config) {
|
||||
uni.redirectTo({
|
||||
url: '/pages/goods-vr-ticket/goods-vr-ticket?id=' + params.id
|
||||
});
|
||||
return;
|
||||
}
|
||||
},
|
||||
|
||||
onShow() {
|
||||
|
|
@ -960,6 +980,16 @@
|
|||
// 商品数据
|
||||
this.init_result_data_handle(goods);
|
||||
|
||||
// VR 票务商品跳转判断
|
||||
var isVRTicket = (goods.vr_goods_config || null) != null;
|
||||
|
||||
if (isVRTicket) {
|
||||
uni.redirectTo({
|
||||
url: '/pages/goods-vr-ticket/goods-vr-ticket?id=' + goods.id
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 基础数据
|
||||
var plugins_seckill_data = data.plugins_seckill_data || null;
|
||||
var upd_data = {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,114 @@
|
|||
<template>
|
||||
<view class="ticket-header">
|
||||
<view class="poster">
|
||||
<image class="poster-img" :src="posterUrl" :mode="posterMode" />
|
||||
</view>
|
||||
<view class="header-info">
|
||||
<text class="title">{{ title }}</text>
|
||||
<text class="tag">{{ tag }}</text>
|
||||
<view class="collect-btn" @click="handleCollect">
|
||||
<text class="heart-icon">{{ collected ? '♥' : '♡' }}</text>
|
||||
<text>{{ collected ? '已收藏' : '收藏演出' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'TicketHeader',
|
||||
props: {
|
||||
posterUrl: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
tag: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
collected: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
posterMode: {
|
||||
type: String,
|
||||
default: 'aspectFill'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleCollect() {
|
||||
this.$emit('collect');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.ticket-header {
|
||||
background:
|
||||
radial-gradient(ellipse at 20% 80%, rgba(120, 80, 255, 0.15) 0%, transparent 50%),
|
||||
radial-gradient(ellipse at 80% 20%, rgba(255, 0, 127, 0.1) 0%, transparent 40%),
|
||||
linear-gradient(160deg, #1a1625 0%, #2d2840 50%, #1f1b2e 100%);
|
||||
padding: 54px 20px 64px 20px;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
color: #fff;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.poster {
|
||||
flex-shrink: 0;
|
||||
width: 105px;
|
||||
height: 140px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 10px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.poster-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.header-info {
|
||||
margin-left: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
font-size: 12px;
|
||||
color: #d0cdd8;
|
||||
margin-top: 10px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.collect-btn {
|
||||
margin-top: auto;
|
||||
align-self: flex-start;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
padding: 6px 14px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.heart-icon {
|
||||
font-size: 14px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,337 @@
|
|||
<template>
|
||||
<view class="ticket-popup" v-if="visible" @click="handleMaskClick">
|
||||
<view class="popup-inner" @click.stop>
|
||||
<!-- Popup Header: 服务说明 -->
|
||||
<view class="popup-header" v-if="type === 'service'">
|
||||
<text class="popup-title-text">服务说明</text>
|
||||
<text class="popup-close" @click="close">✕</text>
|
||||
</view>
|
||||
|
||||
<!-- Popup Header: 所有场馆 -->
|
||||
<view class="popup-header" v-if="type === 'venue'">
|
||||
<text class="popup-title-text">所有场馆</text>
|
||||
<text class="popup-close" @click="close">✕</text>
|
||||
</view>
|
||||
|
||||
<!-- Popup Header: 须知 (带tabs) -->
|
||||
<view class="popup-header has-tabs" v-if="type === 'notice'">
|
||||
<view class="popup-tabs">
|
||||
<text
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === 'buy' }"
|
||||
@click="activeTab = 'buy'"
|
||||
>购票须知</text>
|
||||
<text
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === 'watch' }"
|
||||
@click="activeTab = 'watch'"
|
||||
>观演须知</text>
|
||||
</view>
|
||||
<text class="popup-close" @click="close">✕</text>
|
||||
</view>
|
||||
|
||||
<!-- Popup Content -->
|
||||
<scroll-view class="popup-content" scroll-y :scroll-top="0">
|
||||
<!-- 服务说明 -->
|
||||
<template v-if="type === 'service'">
|
||||
<view class="service-item" v-for="(item, index) in services" :key="index">
|
||||
<view class="service-item-title">
|
||||
<text :class="item.status === 'success' ? 'icon-success' : 'icon-warning'">
|
||||
{{ item.status === 'success' ? '✔' : '!' }}
|
||||
</text>
|
||||
{{ item.title }}
|
||||
</view>
|
||||
<view class="service-item-content">{{ item.desc }}</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- 须知 -->
|
||||
<template v-if="type === 'notice'">
|
||||
<view class="service-item" v-for="(item, index) in currentList" :key="index">
|
||||
<view class="service-item-title notice-pure-title">{{ item.title }}</view>
|
||||
<view class="service-item-content">{{ item.desc }}</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- 场馆列表 -->
|
||||
<template v-if="type === 'venue'">
|
||||
<view class="venue-item" v-for="(item, index) in venues" :key="index" @click="handleVenueClick(item)">
|
||||
<view class="venue-info">
|
||||
<view class="venue-row">
|
||||
<iconfont name="icon-location" color="#ff404f" size="20px" propStyle="margin-right: 6px;"></iconfont>
|
||||
<text class="venue-name">{{ item.name }}</text>
|
||||
</view>
|
||||
<text class="venue-address">{{ item.address }}</text>
|
||||
</view>
|
||||
<view class="venue-nav">
|
||||
<view class="nav-circle">
|
||||
<iconfont name="icon-map-navigator" color="#fff" size="32rpx"></iconfont>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'TicketPopup',
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'service', // service | venue | notice
|
||||
validator: val => ['service', 'venue', 'notice'].includes(val)
|
||||
},
|
||||
// 服务列表数据
|
||||
services: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
// 场馆列表数据
|
||||
venues: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
// 购票须知数据
|
||||
buyNotices: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
// 观演须知数据
|
||||
watchNotices: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
// 须知默认选中 tab
|
||||
defaultTab: {
|
||||
type: String,
|
||||
default: 'buy'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeTab: this.defaultTab
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
currentList() {
|
||||
return this.activeTab === 'buy' ? this.buyNotices : this.watchNotices;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
defaultTab(val) {
|
||||
this.activeTab = val;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleMaskClick() {
|
||||
this.$emit('close');
|
||||
},
|
||||
close() {
|
||||
this.$emit('close');
|
||||
},
|
||||
handleVenueClick(venue) {
|
||||
this.$emit('nav', venue);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Popup 容器样式 - 复用主 CSS 文件中的定义 */
|
||||
.ticket-popup {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
z-index: 999;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.popup-inner {
|
||||
width: 100%;
|
||||
height: 70vh;
|
||||
background-color: #fff;
|
||||
border-radius: 16px 16px 0 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 70vh;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.popup-header {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.popup-header.has-tabs {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.popup-title-text {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
.popup-tabs {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
position: relative;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.tab-item.active {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
.tab-item.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -12px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 50%;
|
||||
height: 3px;
|
||||
background-color: #ff404f;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.popup-close {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
font-size: 20px;
|
||||
color: #333;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.popup-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 24px 20px 40px 20px;
|
||||
box-sizing: border-box;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* 服务/须知列表项 */
|
||||
.service-item {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.service-item-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #111;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.notice-pure-title {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.service-item-content {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
padding-left: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.icon-success, .icon-warning {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
color: #fff;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.icon-success {
|
||||
background-color: #2ac55a;
|
||||
}
|
||||
|
||||
.icon-warning {
|
||||
background-color: transparent;
|
||||
color: #ff404f;
|
||||
border: 1px solid #ff404f;
|
||||
}
|
||||
|
||||
/* 场馆列表项 */
|
||||
.venue-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.venue-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.venue-info {
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.venue-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.venue-name {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #111;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.venue-address {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.venue-nav {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nav-circle {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background-color: #d2cfcf;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
<template>
|
||||
<view class="venue-card" @click="handleClick">
|
||||
<view class="venue-badge" v-if="isNearest">离我最近</view>
|
||||
<view class="venue-content">
|
||||
<view class="venue-info">
|
||||
<text class="venue-name">{{ venue.name || '获取中...' }}</text>
|
||||
<text class="venue-address" @click.stop="handleAddressClick">{{ venue.address || '获取中...' }}</text>
|
||||
</view>
|
||||
<view class="venue-action">
|
||||
<view class="map-icon">
|
||||
<iconfont name="icon-location" color="#e61818" size="16px"></iconfont>
|
||||
</view>
|
||||
<text class="map-more">所有场馆 ></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'VenueCard',
|
||||
props: {
|
||||
venue: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
isNearest: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClick() {
|
||||
this.$emit('click', this.venue);
|
||||
},
|
||||
handleAddressClick() {
|
||||
this.$emit('nav', this.venue);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.venue-card {
|
||||
margin-top: 10px;
|
||||
background-color: #fcfcfc;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-image: radial-gradient(circle at right center, rgba(240,240,240,0.8) 0%, transparent 50%);
|
||||
}
|
||||
|
||||
.venue-badge {
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
background-color: #ff404f;
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 4px;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.venue-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: stretch;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.venue-info {
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.venue-name {
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
.venue-address {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.venue-action {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.map-icon {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
background-color: #ffffff;
|
||||
border: 1.5px solid #f76464;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.map-more {
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
margin-top: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,569 @@
|
|||
/* =========== 基础页面样式 =========== */
|
||||
.ticket-page {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
background-color: #f5f6f8;
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.header-banner {
|
||||
background:
|
||||
radial-gradient(ellipse at 20% 80%, rgba(120, 80, 255, 0.15) 0%, transparent 50%),
|
||||
radial-gradient(ellipse at 80% 20%, rgba(255, 0, 127, 0.1) 0%, transparent 40%),
|
||||
linear-gradient(160deg, #1a1625 0%, #2d2840 50%, #1f1b2e 100%);
|
||||
padding: 54px 20px 64px 20px;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
color: #fff;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.poster {
|
||||
flex-shrink: 0;
|
||||
width: 105px;
|
||||
height: 140px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.poster-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.header-info {
|
||||
margin-left: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.header-info .title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.header-info .tag {
|
||||
font-size: 12px;
|
||||
color: #d0cdd8;
|
||||
margin-top: 10px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.collect-btn {
|
||||
margin-top: auto;
|
||||
align-self: flex-start;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
padding: 6px 14px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.heart-icon {
|
||||
font-size: 14px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
/* =========== Main Content =========== */
|
||||
.main-content {
|
||||
background-color: #fff;
|
||||
border-radius: 16px 16px 0 0;
|
||||
margin-top: -24px;
|
||||
padding: 20px;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* Location Card */
|
||||
.location-card {
|
||||
margin-top: 10px;
|
||||
background-color: #fcfcfc;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-image: radial-gradient(circle at right center, rgba(240, 240, 240, 0.8) 0%, transparent 50%);
|
||||
}
|
||||
|
||||
.location-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: stretch;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.location-info .venue {
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
color: #111;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.location-info .address {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.location-info {
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.venue-badge {
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
background-color: #ff404f;
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.map-more {
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
margin-top: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.map-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.map-icon {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
background-color: #ffffff;
|
||||
border: 1.5px solid #f76464;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pin-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
fill: #e61818;
|
||||
}
|
||||
|
||||
/* Service Header */
|
||||
.service-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.service-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.service-tags-wrapper {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.service-tags-wrapper::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 30px;
|
||||
background: linear-gradient(to right, transparent, #fff);
|
||||
}
|
||||
|
||||
.service-more {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.tag-item {
|
||||
font-size: 11px;
|
||||
color: #333;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.icon-success,
|
||||
.icon-warning {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
color: #fff;
|
||||
margin-right: 6px;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.icon-success {
|
||||
background-color: #2ac55a;
|
||||
}
|
||||
|
||||
.icon-warning {
|
||||
background-color: transparent;
|
||||
color: #ff404f;
|
||||
border: 1px solid #ff404f;
|
||||
}
|
||||
|
||||
/* =========== Section Card =========== */
|
||||
.mt-10 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.section-card {
|
||||
background-color: #fff;
|
||||
padding: 24px 20px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #111;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.section-more {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.notice-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px 16px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.notice-item {
|
||||
width: calc(50% - 8px);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.notice-title {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.notice-title .dot {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
background-color: #d8d8d8;
|
||||
border-radius: 50%;
|
||||
margin-right: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.notice-desc {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 6px;
|
||||
padding-left: 12px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* =========== Bottom Bar =========== */
|
||||
.bottom-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 20px;
|
||||
padding-bottom: calc(10px + constant(safe-area-inset-bottom));
|
||||
padding-bottom: calc(10px + env(safe-area-inset-bottom));
|
||||
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.03);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.customer-service {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-right: 24px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.service-icon {
|
||||
font-size: 20px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.customer-service text {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.buy-button {
|
||||
flex: 1;
|
||||
background-color: #ff404f;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 25px;
|
||||
padding: 14px 0;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* =========== Popup 样式 =========== */
|
||||
.popup-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
z-index: 999;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.popup-container {
|
||||
width: 100%;
|
||||
height: 70vh;
|
||||
background-color: #fff;
|
||||
border-radius: 16px 16px 0 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 70vh;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.popup-header {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.popup-header.has-tabs {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.popup-title-text {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
.popup-tabs {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
position: relative;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.tab-item.active {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
.tab-item.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -12px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 50%;
|
||||
height: 3px;
|
||||
background-color: #ff404f;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.popup-close {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
font-size: 20px;
|
||||
color: #333;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.popup-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 24px 20px 40px 20px;
|
||||
box-sizing: border-box;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* 弹窗内的说明列表样式 */
|
||||
.service-popup-global-desc {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.service-popup-item {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.service-popup-item-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #111;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.notice-pure-title {
|
||||
/* 须知弹窗里没有icon,可以微调样式 */
|
||||
}
|
||||
|
||||
.service-popup-item-content {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
padding-left: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* 带有icon时,文字需要和icon对齐,缩进处理 */
|
||||
.service-popup-item-title .icon-success,
|
||||
.service-popup-item-title .icon-warning {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* 场馆列表样式 */
|
||||
.location-card {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.venue-popup-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.venue-popup-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.venue-popup-info {
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.venue-info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.venue-popup-name {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #111;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.venue-popup-address {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.venue-popup-nav {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nav-icon-circle {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background-color: #d2cfcf;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* =========== 顶部导航按钮 =========== */
|
||||
.pf {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.top-nav-left-icon {
|
||||
position: fixed;
|
||||
left: 12px;
|
||||
top: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.back-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
color: #333;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.round {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.cp {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
|
@ -0,0 +1,344 @@
|
|||
<template>
|
||||
<view class="ticket-page">
|
||||
<!-- 顶部返回按钮 -->
|
||||
<view class="top-nav-left-icon pf" style="top: 44px;">
|
||||
<view class="icon back-icon round cp" @click="goBack">‹</view>
|
||||
</view>
|
||||
|
||||
<!-- Header Banner - 使用头部组件 -->
|
||||
<ticket-header
|
||||
:poster-url="goodsData.images"
|
||||
:title="goodsData.title"
|
||||
:tag="goodsData.simple_desc"
|
||||
:collected="collected"
|
||||
@collect="toggleCollect"
|
||||
/>
|
||||
|
||||
<!-- Main Content -->
|
||||
<view class="main-content">
|
||||
<!-- Location Card - 使用场馆卡片组件 -->
|
||||
<venue-card
|
||||
:venue="currentVenue"
|
||||
:is-nearest="true"
|
||||
@nav="openAmap"
|
||||
@click="openPopup('venue')"
|
||||
/>
|
||||
|
||||
<!-- Service Header -->
|
||||
<view class="service-header" @click="openPopup('service')">
|
||||
<view class="service-tags-wrapper">
|
||||
<view class="service-tags">
|
||||
<view class="tag-item" v-for="(item, index) in services" :key="index">
|
||||
<text :class="item.status === 'success' ? 'icon-success' : 'icon-warning'">
|
||||
{{ item.status === 'success' ? '✔' : '!' }}
|
||||
</text>
|
||||
{{ item.title }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="service-more">></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Section: 演出详情 -->
|
||||
<view class="section-card mt-10">
|
||||
<view class="section-header">
|
||||
<text class="section-title">演出详情</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Section: 购票须知 -->
|
||||
<view class="section-card mt-10">
|
||||
<view class="section-header">
|
||||
<text class="section-title">购票须知</text>
|
||||
<view class="section-more" @click="openPopup('notice', 'buy')">全部 ></view>
|
||||
</view>
|
||||
<view class="notice-grid">
|
||||
<view class="notice-item" v-for="(item, index) in buyNotices.slice(0, 4)" :key="index">
|
||||
<view class="notice-title"><view class="dot"></view>{{ item.title }}</view>
|
||||
<view class="notice-desc">{{ item.desc }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Section: 观演须知 -->
|
||||
<view class="section-card mt-10" :style="{ marginBottom: '90px' }">
|
||||
<view class="section-header">
|
||||
<text class="section-title">观演须知</text>
|
||||
<view class="section-more" @click="openPopup('notice', 'watch')">全部 ></view>
|
||||
</view>
|
||||
<view class="notice-grid">
|
||||
<view class="notice-item" v-for="(item, index) in watchNotices.slice(0, 4)" :key="index">
|
||||
<view class="notice-title"><view class="dot"></view>{{ item.title }}</view>
|
||||
<view class="notice-desc">{{ item.desc }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Bottom Bar -->
|
||||
<view class="bottom-bar">
|
||||
<view class="customer-service">
|
||||
<view class="service-icon">🎧</view>
|
||||
<text>客服</text>
|
||||
</view>
|
||||
<button class="buy-button" @click="openTicketPopup">立即购票</button>
|
||||
</view>
|
||||
|
||||
<!-- Popup Container - 使用弹窗组件 -->
|
||||
<ticket-popup
|
||||
:visible="popupVisible"
|
||||
:type="popupType"
|
||||
:services="services"
|
||||
:venues="venues"
|
||||
:buy-notices="buyNotices"
|
||||
:watch-notices="watchNotices"
|
||||
:default-tab="noticeActiveTab"
|
||||
@close="closePopup"
|
||||
@nav="openAmap"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// 引入子组件
|
||||
import TicketHeader from './components/ticket-header/index.vue';
|
||||
import VenueCard from './components/venue-card/index.vue';
|
||||
import TicketPopup from './components/ticket-popup/index.vue';
|
||||
|
||||
export default {
|
||||
name: 'GoodsVrTicket',
|
||||
// 注册子组件
|
||||
components: {
|
||||
TicketHeader,
|
||||
VenueCard,
|
||||
TicketPopup
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// App 实例
|
||||
app: null,
|
||||
// 商品ID
|
||||
goodsId: null,
|
||||
// 商品数据
|
||||
goodsData: {},
|
||||
// VR 配置数据
|
||||
vrConfig: [],
|
||||
// 场馆列表
|
||||
venues: [],
|
||||
// 当前选中的场馆
|
||||
currentVenue: {},
|
||||
// 收藏状态
|
||||
collected: false,
|
||||
// 弹窗状态
|
||||
popupVisible: false,
|
||||
popupType: 'service',
|
||||
noticeActiveTab: 'buy',
|
||||
// 服务说明数据 (可从 API 获取)
|
||||
services: [
|
||||
{ title: '电子票', desc: '现场验票时,请观演人出示APP票夹中的电子票二维码验票入场,不支持截屏。', status: 'success' },
|
||||
{ title: '不支持转赠', desc: '本场演出实行实名制购票观演政策,下单时需填写观演人信息。购票完成后,观演人信息不可更改,需携带购票时填写的证件验证入场观演。', status: 'warning' },
|
||||
{ title: '不支持退换票', desc: '票品为有价票券,非普通商品,其背后承载的文化服务具有时效性,稀缺性等特征,不支持退换。', status: 'warning' }
|
||||
],
|
||||
// 购票须知
|
||||
buyNotices: [
|
||||
{ title: '实名购票', desc: '本场演出实行实名制购票观演政策,一证一票。' },
|
||||
{ title: '演出时长以现场为准', desc: '受现场不可控因素影响,实际演出时长以现场为准。' },
|
||||
{ title: '定时购说明', desc: '使用定时购服务后,将在开票时为您自动购票,不保证购票成功,也不影响开票后的正常购票,使用需扣减票价等额90%的秀豆积分。' },
|
||||
{ title: '候补说明', desc: '使用候补服务后,将按照订单出票时间先后顺序候补,不保证一定成功,候补服务费一般为订单费用的5.5%。' }
|
||||
],
|
||||
// 观演须知
|
||||
watchNotices: [
|
||||
{ title: '站席观演', desc: '场地内不设座位,均为站席观演。' },
|
||||
{ title: '禁止携带物品', desc: '由于安保和版权的原因,大多数演出、展览及比赛场所禁止携带食品、饮料、专业摄录设备、打火机等物品。' },
|
||||
{ title: '退换政策', desc: '票品为有价票券,非普通商品,其背后承载的文化服务具有时效性,稀缺性等特征,不支持退换。' }
|
||||
]
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
// 根据当前选中的 tab 动态返回对应的须知列表
|
||||
currentNoticeList() {
|
||||
return this.noticeActiveTab === 'buy' ? this.buyNotices : this.watchNotices;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 返回上一页
|
||||
goBack() {
|
||||
const pages = getCurrentPages();
|
||||
if (pages.length > 1) {
|
||||
// 有历史页面,返回上一页
|
||||
uni.navigateBack();
|
||||
} else {
|
||||
// 无历史页面,返回首页
|
||||
uni.reLaunch({
|
||||
url: '/pages/index/index'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onLoad(params) {
|
||||
// 获取 app 实例
|
||||
const app = getApp();
|
||||
this.app = app;
|
||||
|
||||
// 获取页面参数
|
||||
if (params && params.id) {
|
||||
this.goodsId = params.id;
|
||||
}
|
||||
|
||||
// 尝试从缓存获取商品数据
|
||||
var goods = app.globalData.goods_data_cache_handle(this.goodsId);
|
||||
if (goods != null) {
|
||||
console.log('[VR Ticket] goods from cache:', goods);
|
||||
this.handleGoodsData(goods);
|
||||
} else {
|
||||
// 请求 API 获取商品详情
|
||||
this.loadGoodsDetail();
|
||||
}
|
||||
},
|
||||
|
||||
// 加载商品详情
|
||||
loadGoodsDetail() {
|
||||
var self = this;
|
||||
uni.request({
|
||||
url: this.app.globalData.get_request_url('detail', 'goods'),
|
||||
method: 'POST',
|
||||
data: { id: this.goodsId },
|
||||
dataType: 'json',
|
||||
success: function(res) {
|
||||
if (res.data.code == 0) {
|
||||
var goods = res.data.data.goods;
|
||||
console.log('[VR Ticket] goods from API:', goods);
|
||||
self.handleGoodsData(goods);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 处理商品数据
|
||||
handleGoodsData(goods) {
|
||||
// 存储商品数据
|
||||
this.goodsData = goods;
|
||||
|
||||
// 解析 vr_goods_config
|
||||
var vrConfig = goods.vr_goods_config;
|
||||
if (typeof vrConfig === 'string') {
|
||||
try {
|
||||
vrConfig = JSON.parse(vrConfig);
|
||||
} catch (e) {
|
||||
vrConfig = [];
|
||||
}
|
||||
}
|
||||
this.vrConfig = vrConfig;
|
||||
|
||||
console.log('[VR Ticket] vrConfig parsed:', vrConfig);
|
||||
|
||||
// 提取场馆数据
|
||||
this.initVenueData();
|
||||
},
|
||||
|
||||
// 初始化场馆数据
|
||||
initVenueData() {
|
||||
// 使用从商品数据解析的 vrConfig
|
||||
const rawApiData = this.vrConfig || [];
|
||||
this.venues = rawApiData.map(item => item.template_snapshot.venue);
|
||||
this.findClosestVenue();
|
||||
},
|
||||
|
||||
// 找出距离最近的场馆
|
||||
findClosestVenue() {
|
||||
// 模拟当前用户坐标 (例如: 北京市朝阳区)
|
||||
// 在实际项目中,可以使用 uni.getLocation 获取真实经纬度
|
||||
const currentLoc = { lat: 39.9042, lng: 116.4074 };
|
||||
|
||||
let closest = this.venues[0];
|
||||
let minDistance = Infinity;
|
||||
|
||||
this.venues.forEach(venue => {
|
||||
if (venue.location && venue.location.lat && venue.location.lng) {
|
||||
const lat = parseFloat(venue.location.lat);
|
||||
const lng = parseFloat(venue.location.lng);
|
||||
|
||||
// 使用 Haversine 公式计算距离
|
||||
const distance = this.calculateDistance(currentLoc.lat, currentLoc.lng, lat, lng);
|
||||
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
closest = venue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.currentVenue = closest || this.venues[0];
|
||||
},
|
||||
|
||||
// Haversine 距离计算 (单位:米)
|
||||
calculateDistance(lat1, lng1, lat2, lng2) {
|
||||
const radLat1 = lat1 * Math.PI / 180.0;
|
||||
const radLat2 = lat2 * Math.PI / 180.0;
|
||||
const a = radLat1 - radLat2;
|
||||
const b = (lng1 - lng2) * Math.PI / 180.0;
|
||||
let s = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a / 2), 2) + Math.cos(radLat1) * Math.cos(radLat2) * Math.pow(Math.sin(b / 2), 2)));
|
||||
return s * 6378137.0;
|
||||
},
|
||||
|
||||
// 导航到高德地图
|
||||
openAmap(venue) {
|
||||
const { location, name } = venue;
|
||||
if (!location || !location.lat || !location.lng) {
|
||||
uni.showToast({
|
||||
title: '该场馆暂无位置信息',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 使用 uni.openLocation 打开地图导航
|
||||
uni.openLocation({
|
||||
latitude: parseFloat(location.lat),
|
||||
longitude: parseFloat(location.lng),
|
||||
name: name,
|
||||
address: venue.address,
|
||||
fail: () => {
|
||||
// 如果失败,使用高德 URL Scheme
|
||||
const url = `https://uri.amap.com/marker?position=${location.lng},${location.lat}&name=${encodeURIComponent(name)}`;
|
||||
// #ifdef H5
|
||||
window.open(url, '_blank');
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
plus.runtime.openWeb(url);
|
||||
// #endif
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 切换收藏状态
|
||||
toggleCollect() {
|
||||
this.collected = !this.collected;
|
||||
// TODO: 调用收藏 API
|
||||
},
|
||||
|
||||
// 打开购票弹窗
|
||||
openTicketPopup() {
|
||||
this.popupType = 'buy';
|
||||
this.popupVisible = true;
|
||||
},
|
||||
|
||||
// 打开弹窗:type 区分是服务还是须知,tab 区分须知的默认选中项
|
||||
openPopup(type, tab = 'buy') {
|
||||
this.popupType = type;
|
||||
if (type === 'notice') {
|
||||
this.noticeActiveTab = tab;
|
||||
}
|
||||
this.popupVisible = true;
|
||||
},
|
||||
|
||||
// 关闭弹窗
|
||||
closePopup() {
|
||||
this.popupVisible = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import "./goods-vr-ticket.css";
|
||||
</style>
|
||||
Loading…
Reference in New Issue