- 设计令牌层:mm-* CSS 变量与工具类、uView 换肤、优设标题黑字体 - 九个页面全部重写为新风格(列表/详情/下单/出行人/订单/支付成功/登录) - 补齐 H5 运行时缺失的基础样式(uni-view/scroll-view/toast/swiper 等,App.vue 全局) - 修复:详情头图轮播、预订须知弹层滚动、出行人数量按购买数限制、订单详情去箭头 - 新增 gmy-back-touch 返回悬浮钮,除首页外全页接入,点击返回上一页 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
594 lines
14 KiB
Vue
594 lines
14 KiB
Vue
<template>
|
||
<view v-if="obj" class="page mm-page">
|
||
<gmy-back-touch></gmy-back-touch>
|
||
<!-- xuchao: 头图轮播(保留原有数字指示器) -->
|
||
<u-swiper :list="list" height="750" mode="number" indicator-pos="bottomRight"
|
||
marginBottom="30rpx"></u-swiper>
|
||
|
||
<!-- xuchao: 信息面板 —— 上移盖住头图底部的圆角 sheet,承载景区核心信息 -->
|
||
<view class="sheet mm-fade-up">
|
||
<view class="t_head">
|
||
<view class="t_name mm-display">{{obj.tickets.name}}</view>
|
||
<view class="t_level mm-grad" v-if="obj.shop.level">{{obj.shop.level}}</view>
|
||
</view>
|
||
<view class="t_info" v-if="obj.tickets.remark1">{{obj.tickets.remark1}}</view>
|
||
|
||
<!-- xuchao: 开园时间 —— 蓝色浅底提示条 -->
|
||
<view class="t_open" v-if="obj.tickets.openDate">
|
||
<u-icon name="clock-fill" color="#0052FF" size="28"></u-icon>
|
||
<text class="t_open__label">开园时间</text>
|
||
<text class="t_open__val">{{obj.tickets.openDate}}</text>
|
||
</view>
|
||
|
||
<!-- xuchao: 地址 + 导航/电话快捷圆钮 -->
|
||
<view class="t_addr">
|
||
<view class="t_addr__text">{{obj.shop.address}}</view>
|
||
<view class="t_addr__acts">
|
||
<view class="t_addr__btn mm-grad" @click="handleOpen()">
|
||
<u-icon name="map-fill" color="#ffffff" size="32"></u-icon>
|
||
</view>
|
||
<view class="t_addr__btn mm-grad" @click="playPhone()">
|
||
<u-icon name="phone-fill" color="#ffffff" size="32"></u-icon>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- xuchao: 门票规格列表 -->
|
||
<view class="sec mm-fade-up">
|
||
<view class="sec__title mm-sec-title">门票规格</view>
|
||
<view class="sku" v-for="(item,index) in obj.skuList" :key="index">
|
||
<!-- xuchao: 名称提为独立整行,填充卡片宽度,不再被右侧价格列挤压换行 -->
|
||
<view class="sku__name">【{{getSkuType(item.typeId)}}】{{item.skuName}}</view>
|
||
<view class="sku__body">
|
||
<view class="sku__left">
|
||
<view class="sku__rule">{{item.bookTimeLimit}}之前{{item.bookType?'可订当天':'可订次日'}}</view>
|
||
<view class="sku__know" @click="seeSkuDetail(item.id)">
|
||
<text>预订须知</text>
|
||
<u-icon name="arrow-right" color="#0052FF" size="20"></u-icon>
|
||
</view>
|
||
</view>
|
||
<view class="sku__right">
|
||
<view class="sku__origin">原售价 <text
|
||
class="sku__origin-num">¥{{item.calendar.listPrice?(item.calendar.listPrice/100).toFixed(2):0}}</text></view>
|
||
<view class="sku__price">
|
||
<text class="sku__price-label">¥</text>
|
||
<text
|
||
class="sku__price-num mm-display mm-grad-text">{{item.calendar.salePrice?(item.calendar.salePrice/100).toFixed(2):0}}</text>
|
||
</view>
|
||
<view class="sku__buy mm-grad" @tap="toOrderSure(item.id)">购买</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- xuchao: 产品介绍(后端富文本) -->
|
||
<view class="sec mm-fade-up">
|
||
<view class="sec__title mm-sec-title">产品介绍</view>
|
||
<view class="intro">
|
||
<u-parse :html="obj.tickets.introduction"></u-parse>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- xuchao: 预订须知弹层 -->
|
||
<u-popup v-model="show" border-radius="20" mode="bottom" height="1000">
|
||
<view class="pp" v-if="skuObj">
|
||
<view class="pp__head">{{skuObj.sku.skuName}}</view>
|
||
|
||
<view class="pp__sec">
|
||
<view class="mm-sec-title">预订规则</view>
|
||
<view class="pp__row">
|
||
<view class="pp__label">预订时间</view>
|
||
<view class="pp__val">{{skuObj.sku.bookTimeLimit}}之前{{skuObj.sku.bookType?'可订当天':'可订次日'}}</view>
|
||
</view>
|
||
<view class="pp__row">
|
||
<view class="pp__label">生效时间</view>
|
||
<view class="pp__val">出票后立即使用</view>
|
||
</view>
|
||
<view class="pp__row">
|
||
<view class="pp__label">有效期</view>
|
||
<view class="pp__val">指定出行日期1天内有效</view>
|
||
</view>
|
||
<view class="pp__row">
|
||
<view class="pp__label">适用人群</view>
|
||
<view class="pp__val">{{skuObj.sku.typeInfo}}</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="pp__sec">
|
||
<view class="mm-sec-title">费用说明</view>
|
||
<view class="pp__row">
|
||
<view class="pp__label">费用包含</view>
|
||
<view class="pp__val">{{skuObj.sku.contentWith}}</view>
|
||
</view>
|
||
<view class="pp__row">
|
||
<view class="pp__label">费用不含</view>
|
||
<view class="pp__val">{{skuObj.sku.contentWithout}}</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="pp__sec">
|
||
<view class="mm-sec-title">使用说明</view>
|
||
<view class="pp__row">
|
||
<view class="pp__label">入园方式</view>
|
||
<view class="pp__val">{{skuObj.sku.enterChangeFlag?'凭('+getTyepStr(skuObj.sku.enterType)+')换票入园':'无需换票'}}</view>
|
||
</view>
|
||
<view class="pp__row" v-if="skuObj.sku.enterChangeFlag">
|
||
<view class="pp__label">换票时间</view>
|
||
<view class="pp__val">{{skuObj.sku.changeStart}}~{{skuObj.sku.changeEnd}}</view>
|
||
</view>
|
||
<view class="pp__row" v-if="skuObj.sku.enterChangeFlag">
|
||
<view class="pp__label">换票地点</view>
|
||
<view class="pp__val">{{skuObj.sku.changePostion}}</view>
|
||
</view>
|
||
<view class="pp__row">
|
||
<view class="pp__label">入园时间</view>
|
||
<view class="pp__val">{{skuObj.sku.enterStart}}~{{skuObj.sku.enterEnd}}</view>
|
||
</view>
|
||
<view class="pp__row">
|
||
<view class="pp__label">入园地址</view>
|
||
<view class="pp__val">{{skuObj.sku.enterPostion}}</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="pp__sec">
|
||
<view class="mm-sec-title">取消政策</view>
|
||
<u-parse class="pp__parse" :html="skuObj.sku.refundPolicy"></u-parse>
|
||
</view>
|
||
|
||
<view class="pp__sec">
|
||
<view class="mm-sec-title">其他说明</view>
|
||
<view class="pp__row">
|
||
<view class="pp__label">补充说明</view>
|
||
<view class="pp__val">{{skuObj.sku.remark}}</view>
|
||
</view>
|
||
<view class="pp__row">
|
||
<view class="pp__label">发票说明</view>
|
||
<view class="pp__val">{{skuObj.sku.receiptPolicy}}</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</u-popup>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
var that = ''
|
||
import {
|
||
getSkuType
|
||
} from '../../unit/utils.js'
|
||
import gmyBackTouch from "@/components/gmy-back-touch/gmy-back-touch.vue"
|
||
export default {
|
||
components: {
|
||
'gmy-back-touch': gmyBackTouch
|
||
},
|
||
data() {
|
||
return {
|
||
show: false,
|
||
list: [],
|
||
obj: '',
|
||
skuObj: '',
|
||
typeStr: '',
|
||
getSkuType: '',
|
||
enterTypeList: [{
|
||
value: "二维码",
|
||
label: 1
|
||
},
|
||
{
|
||
value: "实体票",
|
||
label: 2
|
||
},
|
||
{
|
||
value: "第三方凭证码",
|
||
label: 3
|
||
},
|
||
{
|
||
value: "身份证",
|
||
label: 4
|
||
},
|
||
{
|
||
value: "护照",
|
||
label: 5
|
||
},
|
||
{
|
||
value: "港澳居民来往内地通行证",
|
||
label: 6
|
||
},
|
||
{
|
||
value: "台湾居民来往大陆通行证",
|
||
label: 7
|
||
}
|
||
]
|
||
};
|
||
},
|
||
onLoad(e) {
|
||
that = this
|
||
this.getSkuType = getSkuType
|
||
this.getTicketsDetailMethod(e.id)
|
||
},
|
||
methods: {
|
||
playPhone() {
|
||
uni.makePhoneCall({
|
||
phoneNumber: that.obj.shop.mobile
|
||
})
|
||
|
||
},
|
||
// xuchao: 到景区去 —— 微信/浏览器环境用 uni.openLocation 打开地图,失败兜底跳高德网页
|
||
handleOpen() {
|
||
const lat = parseFloat(this.obj.shop.lat)
|
||
const lng = parseFloat(this.obj.shop.lng)
|
||
if (!lat || !lng) {
|
||
uni.showToast({
|
||
icon: 'none',
|
||
title: '暂无该景区位置信息'
|
||
})
|
||
return
|
||
}
|
||
uni.openLocation({
|
||
latitude: lat,
|
||
longitude: lng,
|
||
name: this.obj.shop.name,
|
||
address: this.obj.shop.address || '',
|
||
fail: () => {
|
||
// 备用方案:跳转高德地图网页
|
||
window.location.href =
|
||
`https://uri.amap.com/marker?position=${lng},${lat}&name=${this.obj.shop.name}`
|
||
}
|
||
})
|
||
},
|
||
getTyepStr(e) {
|
||
let typeStr = ''
|
||
e.split(',').forEach((item) => {
|
||
this.enterTypeList.forEach((item1) => {
|
||
if (item == item1.label) {
|
||
typeStr = typeStr + item1.value + '、'
|
||
}
|
||
})
|
||
})
|
||
return typeStr.slice(0, -1)
|
||
},
|
||
seeSkuDetail(e) {
|
||
this.$u.api.getSkuDetail({
|
||
id: e
|
||
}).then(res => {
|
||
if (res) {
|
||
this.skuObj = res.data
|
||
this.show = true
|
||
}
|
||
})
|
||
},
|
||
getTicketsDetailMethod(e) {
|
||
this.list = []
|
||
this.$u.api.getTicketsDetail({
|
||
id: e
|
||
}).then(res => {
|
||
if (res) {
|
||
// xuchao: 头图首张放单票主图(与列表卡片一致),再拼宣传图,跳过与主图重复项
|
||
const mainPic = res.data.tickets.mainPic
|
||
if (mainPic) {
|
||
this.list.push({
|
||
image: mainPic
|
||
})
|
||
}
|
||
if (res.data.tickets.propagatePic) {
|
||
res.data.tickets.propagatePic.split(',').forEach((item, index) => {
|
||
if (item && item !== mainPic) {
|
||
this.list.push({
|
||
image: item
|
||
})
|
||
}
|
||
})
|
||
}
|
||
|
||
this.obj = res.data
|
||
}
|
||
})
|
||
},
|
||
toOrderSure(e) {
|
||
this.$u.api.getSkuDetail({
|
||
id: e
|
||
}).then(res => {
|
||
if (res) {
|
||
this.skuObj = res.data
|
||
// this.$store.commit('setScenicDetail', res.data)
|
||
uni.setStorageSync('scenicDetail',res.data)
|
||
uni.navigateTo({
|
||
url: 'ordersure'
|
||
})
|
||
}
|
||
})
|
||
|
||
}
|
||
},
|
||
};
|
||
</script>
|
||
|
||
<style lang="less" scoped>
|
||
.page {
|
||
padding-bottom: 80rpx;
|
||
}
|
||
|
||
/* xuchao: 头图固定 298px 高(u-swiper 的 height 属性只认 rpx,故在此覆写其内联高度),
|
||
图片按比例缩放居中裁切(object-fit: cover) */
|
||
.u-swiper-wrap /deep/ uni-swiper {
|
||
height: 298px !important;
|
||
}
|
||
|
||
.u-swiper-wrap /deep/ img {
|
||
object-fit: cover;
|
||
}
|
||
|
||
/* ---------- 信息 sheet(盖住头图底部) ---------- */
|
||
.sheet {
|
||
position: relative;
|
||
z-index: 10;
|
||
margin-top: -56rpx;
|
||
background: var(--mm-card);
|
||
border-radius: 40rpx 40rpx 0 0;
|
||
padding: 40rpx 32rpx 28rpx;
|
||
box-shadow: 0 -12rpx 32rpx rgba(15, 23, 42, 0.06);
|
||
}
|
||
|
||
.t_head {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.t_name {
|
||
flex: 1;
|
||
min-width: 0;
|
||
font-size: 44rpx;
|
||
line-height: 1.2;
|
||
color: var(--mm-fg);
|
||
}
|
||
|
||
/* xuchao: 景区等级徽标 —— 渐变胶囊,与景区名右对齐 */
|
||
.t_level {
|
||
flex: none;
|
||
margin: 8rpx 0 0 16rpx;
|
||
padding: 6rpx 20rpx;
|
||
border-radius: 999rpx;
|
||
color: #fff;
|
||
font-size: 22rpx;
|
||
box-shadow: var(--mm-shadow-accent);
|
||
}
|
||
|
||
.t_info {
|
||
margin-top: 12rpx;
|
||
font-size: 26rpx;
|
||
line-height: 1.6;
|
||
color: var(--mm-muted-fg);
|
||
}
|
||
|
||
.t_open {
|
||
margin-top: 24rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
background: rgba(0, 82, 255, 0.05);
|
||
border-radius: 16rpx;
|
||
padding: 18rpx 24rpx;
|
||
}
|
||
|
||
.t_open__label {
|
||
flex: none;
|
||
margin: 0 16rpx 0 12rpx;
|
||
font-size: 24rpx;
|
||
font-weight: 500;
|
||
color: var(--mm-accent);
|
||
}
|
||
|
||
.t_open__val {
|
||
font-size: 24rpx;
|
||
color: var(--mm-fg);
|
||
}
|
||
|
||
.t_addr {
|
||
margin-top: 24rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
background: var(--mm-muted);
|
||
border-radius: 20rpx;
|
||
padding: 24rpx;
|
||
}
|
||
|
||
.t_addr__text {
|
||
flex: 1;
|
||
min-width: 0;
|
||
font-size: 26rpx;
|
||
line-height: 1.5;
|
||
color: var(--mm-fg);
|
||
}
|
||
|
||
.t_addr__acts {
|
||
flex: none;
|
||
display: flex;
|
||
gap: 16rpx;
|
||
margin-left: 20rpx;
|
||
}
|
||
|
||
.t_addr__btn {
|
||
width: 76rpx;
|
||
height: 76rpx;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
box-shadow: var(--mm-shadow-accent);
|
||
transition: transform 0.15s ease;
|
||
}
|
||
|
||
.t_addr__btn:active {
|
||
transform: scale(0.9);
|
||
}
|
||
|
||
/* ---------- 内容分区(门票规格 / 产品介绍) ---------- */
|
||
.sec {
|
||
margin: 24rpx 32rpx 0;
|
||
background: var(--mm-muted);
|
||
border-radius: var(--mm-radius);
|
||
padding: 28rpx 24rpx;
|
||
}
|
||
|
||
.sec__title {
|
||
margin-bottom: 4rpx;
|
||
}
|
||
|
||
/* ---------- 规格卡片 ---------- */
|
||
.sku {
|
||
margin-top: 20rpx;
|
||
background: var(--mm-card);
|
||
border: 2rpx solid var(--mm-border);
|
||
border-radius: 20rpx;
|
||
padding: 24rpx;
|
||
box-shadow: var(--mm-shadow-sm);
|
||
}
|
||
|
||
/* xuchao: 名称下方的两栏区(左规则/预订须知,右价格/购买) */
|
||
.sku__body {
|
||
margin-top: 20rpx;
|
||
display: flex;
|
||
}
|
||
|
||
.sku__left {
|
||
flex: 1;
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.sku__name {
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
line-height: 1.4;
|
||
color: var(--mm-fg);
|
||
}
|
||
|
||
.sku__rule {
|
||
font-size: 22rpx;
|
||
color: var(--mm-muted-fg);
|
||
}
|
||
|
||
.sku__know {
|
||
margin-top: 14rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4rpx;
|
||
font-size: 24rpx;
|
||
color: var(--mm-accent);
|
||
}
|
||
|
||
.sku__right {
|
||
flex: none;
|
||
width: 268rpx;
|
||
margin-left: 20rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-end;
|
||
}
|
||
|
||
.sku__origin {
|
||
font-size: 20rpx;
|
||
color: var(--mm-muted-fg);
|
||
}
|
||
|
||
.sku__origin-num {
|
||
text-decoration: line-through;
|
||
}
|
||
|
||
.sku__price {
|
||
display: flex;
|
||
align-items: baseline;
|
||
margin-top: 10rpx;
|
||
}
|
||
|
||
.sku__price-label {
|
||
font-size: 20rpx;
|
||
color: var(--mm-muted-fg);
|
||
}
|
||
|
||
.sku__price-num {
|
||
font-size: 44rpx;
|
||
line-height: 1;
|
||
}
|
||
|
||
.sku__buy {
|
||
margin-top: 16rpx;
|
||
width: 160rpx;
|
||
height: 64rpx;
|
||
border-radius: 999rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: #fff;
|
||
font-size: 26rpx;
|
||
font-weight: 500;
|
||
box-shadow: 0 6rpx 16rpx rgba(0, 82, 255, 0.25);
|
||
transition: transform 0.15s ease;
|
||
}
|
||
|
||
.sku__buy:active {
|
||
transform: scale(0.94);
|
||
}
|
||
|
||
/* ---------- 产品介绍富文本 ---------- */
|
||
.intro {
|
||
margin-top: 16rpx;
|
||
font-size: 26rpx;
|
||
line-height: 1.7;
|
||
color: var(--mm-fg);
|
||
}
|
||
|
||
.intro /deep/ img {
|
||
max-width: 100% !important;
|
||
display: block;
|
||
border-radius: 12rpx;
|
||
}
|
||
|
||
/* ---------- 预订须知弹层 ---------- */
|
||
.pp {
|
||
padding: 0 48rpx 80rpx;
|
||
}
|
||
|
||
.pp__head {
|
||
height: 120rpx;
|
||
line-height: 120rpx;
|
||
text-align: center;
|
||
font-size: 32rpx;
|
||
font-weight: 600;
|
||
color: var(--mm-fg);
|
||
border-bottom: 2rpx solid var(--mm-border);
|
||
}
|
||
|
||
.pp__sec {
|
||
padding-top: 40rpx;
|
||
}
|
||
|
||
.pp__row {
|
||
display: flex;
|
||
margin-top: 24rpx;
|
||
}
|
||
|
||
.pp__label {
|
||
flex: none;
|
||
width: 120rpx;
|
||
font-size: 26rpx;
|
||
color: var(--mm-muted-fg);
|
||
}
|
||
|
||
.pp__val {
|
||
flex: 1;
|
||
margin-left: 48rpx;
|
||
font-size: 26rpx;
|
||
line-height: 1.6;
|
||
color: var(--mm-fg);
|
||
}
|
||
|
||
.pp__parse {
|
||
margin-top: 24rpx;
|
||
font-size: 26rpx;
|
||
line-height: 1.7;
|
||
color: var(--mm-fg);
|
||
}
|
||
</style>
|