feat: 全站 UI 按 Minimalist Modern 重设计,新增返回悬浮钮

- 设计令牌层: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>
This commit is contained in:
jacobxu666
2026-08-05 12:48:45 +08:00
co-authored by Claude Opus 4.8
parent dd25c0ae63
commit 77e5cb9510
19 changed files with 2690 additions and 1335 deletions
+423 -229
View File
@@ -1,159 +1,151 @@
<template>
<view v-if="obj">
<view class="wrap">
<u-swiper :list="list" height='750' mode='number' indicator-pos='bottomRight'
marginBottom='30rpx'></u-swiper>
<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>
<view style="position: absolute;top: 700rpx;">
<view class="title">
<view class="t_name">{{obj.tickets.name}} <text style="color: #FF8D28;">{{obj.shop.level}}</text></view>
<view class="t_info">{{obj.tickets.remark1}}</view>
<view class="t_opentime">
<view style="display: flex;gap: 8rpx;color: #FF8D28;">
<image src="https://aliyunoss.hn12301.net/xgh/ticket_opentime.png"
style="width: 36rpx;height: 36rpx;"></image>开园时间
</view>
<text style="color: #8E8E93;font-size: 24rpx;">{{obj.tickets.openDate}}</text>
</view>
<view class="t_address">
<view style="width: 548rpx;">{{obj.shop.address}}</view>
<image src="https://aliyunoss.hn12301.net/xgh/ticket_dh.png" @click="handleOpen()"></image>
<image style="margin-left: 24rpx;" src="https://aliyunoss.hn12301.net/xgh/ticket_dhua.png"
@click="playPhone()"></image>
</view>
<!-- <view style="color: #FF0000;font-size: 32rpx;margin-top: 24rpx;"><text
style="font-size: 20rpx;">¥</text>{{(obj.minPrice/100).toFixed(0)}} ~ {{(obj.maxPrice/100).toFixed(0)}}</view>
<view style="display: flex;margin-top: 24rpx;">
<text class="tag" v-for="item in obj.tickets.tags.split(',')" :key="item">{{item}}</text>
</view> -->
</view>
<view class="ticket">
<!-- <text style="color: #000;font-size: 36rpx;line-height: 88rpx;font-weight: 700;">成人票</text> -->
<view class="ticket_item" style="margin-top: 0rpx;" v-for="(item,index) in obj.skuList" :key="index">
<view style="width: 338rpx;display: flex;flex-direction: column;">
<text
style="color: #000;font-size: 28rpx;font-weight: 700;">【{{getSkuType(item.typeId)}}】{{item.skuName}}</text>
<view
style="color: #7C7C7E;display: flex;align-items: center;margin-top: 16rpx;font-size: 24rpx;">
{{item.bookTimeLimit}}之前{{item.bookType?'可订当天':'可订明日'}}<text
style="color: #DFE0E2;font-size: 48rpx;padding: 0rpx 12rpx;">·</text>
<!-- 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 style="color: #0066F6;margin-top: 16rpx;" @click="seeSkuDetail(item.id)">预订须知 ></view>
</view>
<view
style="width: 292rpx;display: flex;flex-direction: column;margin-left: 24rpx;align-items: flex-end;">
<view
style="color: #FF0000;font-size: 28rpx;margin-top: 0rpx;display: flex;justify-content: flex-end;">
原售价<text
style="font-size: 28rpx;font-weight: normal;text-decoration: line-through;">¥{{item.calendar.listPrice?(item.calendar.listPrice/100).toFixed(2):0}}</text>
</view>
<view style="color: #FF0000;font-size: 36rpx;font-weight: bold;line-height: 88rpx;">
<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
style="font-size: 20rpx;font-weight: normal;">工会会员补贴价¥</text>{{item.calendar.salePrice?(item.calendar.salePrice/100).toFixed(2):0}}
class="sku__price-num mm-display mm-grad-text">{{item.calendar.salePrice?(item.calendar.salePrice/100).toFixed(2):0}}</text>
</view>
<view class="btn" @tap="toOrderSure(item.id)">购买</view>
<view class="sku__buy mm-grad" @tap="toOrderSure(item.id)">购买</view>
</view>
</view>
</view>
<view class="ticket">
<text style="color: #000;font-size: 36rpx;line-height: 88rpx;font-weight: 700;">产品介绍</text>
</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>
<u-popup v-model="show" border-radius='20' mode='bottom' height='1000'>
<view style="padding:0rpx 48rpx;" v-if="skuObj">
<view
style="width: 654rpx;height: 142rpx;line-height: 142rpx;text-align: center;color: #000;font-size: 32rpx;font-weight: bold;border-bottom: 2rpx #E1E1E1 dashed;">
{{skuObj.sku.skuName}}
</view>
<view style="width: 654rpx;padding: 48rpx 0rpx">
<text style="color: #000;font-size: 32rpx;font-weight: bold;">预订规则</text>
<view style="display: flex;margin-top: 24rpx;">
<view style="color: #808080;width: 120rpx;">预订时间</view>
<view style="width: 494rpx;margin-left: 48rpx;color: #000;">
{{skuObj.sku.bookTimeLimit}}之前{{skuObj.sku.bookType?'可订当天':'可订明日'}}
</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 style="display: flex;margin-top: 24rpx;">
<view style="color: #808080;width: 120rpx;">生效时间</view>
<view style="width: 494rpx;margin-left: 48rpx;color: #000;">出票后立即使用</view>
<view class="pp__row">
<view class="pp__label">生效时间</view>
<view class="pp__val">出票后立即使用</view>
</view>
<view style="display: flex;margin-top: 24rpx;">
<view style="color: #808080;width: 120rpx;">有效期</view>
<view style="width: 494rpx;margin-left: 48rpx;color: #000;">指定出行日期1天内有效</view>
<view class="pp__row">
<view class="pp__label">有效期</view>
<view class="pp__val">指定出行日期1天内有效</view>
</view>
<view style="display: flex;margin-top: 24rpx;">
<view style="color: #808080;width: 120rpx;">适用人群</view>
<view style="width: 494rpx;margin-left: 48rpx;color: #000;">{{skuObj.sku.typeInfo}}</view>
<view class="pp__row">
<view class="pp__label">适用人群</view>
<view class="pp__val">{{skuObj.sku.typeInfo}}</view>
</view>
</view>
<view style="width: 654rpx;padding: 48rpx 0rpx">
<text style="color: #000;font-size: 32rpx;font-weight: bold;">费用说明</text>
<view style="display: flex;margin-top: 24rpx;">
<view style="color: #808080;width: 120rpx;">费用包含</view>
<view style="width: 494rpx;margin-left: 48rpx;color: #000;">{{skuObj.sku.contentWith}}</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 style="display: flex;margin-top: 24rpx;">
<view style="color: #808080;width: 120rpx;">费用不含</view>
<view style="width: 494rpx;margin-left: 48rpx;color: #000;">{{skuObj.sku.contentWithout}}</view>
</view>
</view>
<view style="width: 654rpx;padding: 48rpx 0rpx">
<text style="color: #000;font-size: 32rpx;font-weight: bold;">使用说明</text>
<view style="display: flex;margin-top: 24rpx;">
<view style="color: #808080;width: 120rpx;">入园方式</view>
<view style="width: 494rpx;margin-left: 48rpx;color: #000;">
{{skuObj.sku.enterChangeFlag?'凭('+getTyepStr(skuObj.sku.enterType)+')换票入园':'无需换票'}}
</view>
</view>
<view style="display: flex;margin-top: 24rpx;" v-if="skuObj.sku.enterChangeFlag">
<view style="color: #808080;width: 120rpx;">换票时间</view>
<view style="width: 494rpx;margin-left: 48rpx;color: #000;">
{{skuObj.sku.changeStart}}~{{skuObj.sku.changeEnd}}
</view>
</view>
<view style="display: flex;margin-top: 24rpx;" v-if="skuObj.sku.enterChangeFlag">
<view style="color: #808080;width: 120rpx;">换票地点</view>
<view style="width: 494rpx;margin-left: 48rpx;color: #000;">{{skuObj.sku.changePostion}}</view>
</view>
<view style="display: flex;margin-top: 24rpx;">
<view style="color: #808080;width: 120rpx;">入园时间</view>
<view style="width: 494rpx;margin-left: 48rpx;color: #000;">
{{skuObj.sku.enterStart}}~{{skuObj.sku.enterEnd}}
</view>
</view>
<view style="display: flex;margin-top: 24rpx;">
<view style="color: #808080;width: 120rpx;">入园地址</view>
<view style="width: 494rpx;margin-left: 48rpx;color: #000;">{{skuObj.sku.enterPostion}}</view>
<view class="pp__row">
<view class="pp__label">费用不含</view>
<view class="pp__val">{{skuObj.sku.contentWithout}}</view>
</view>
</view>
<view style="width: 654rpx;padding: 48rpx 0rpx">
<text style="color: #000;font-size: 32rpx;font-weight: bold;">取消政策</text>
<u-parse style="margin-top: 24rpx;" :html="skuObj.sku.refundPolicy"></u-parse>
<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 style="width: 654rpx;padding: 48rpx 0rpx">
<text style="color: #000;font-size: 32rpx;font-weight: bold;">其他说明</text>
<view style="display: flex;margin-top: 24rpx;">
<view style="color: #808080;width: 120rpx;">补充说明</view>
<view style="width: 494rpx;margin-left: 48rpx;color: #000;">{{skuObj.sku.remark}}</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 style="display: flex;margin-top: 24rpx;">
<view style="color: #808080;width: 120rpx;">发票说明</view>
<view style="width: 494rpx;margin-left: 48rpx;color: #000;">{{skuObj.sku.receiptPolicy}}</view>
<view class="pp__row">
<view class="pp__label">发票说明</view>
<view class="pp__val">{{skuObj.sku.receiptPolicy}}</view>
</view>
</view>
</view>
</u-popup>
@@ -165,7 +157,11 @@
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,
@@ -267,11 +263,20 @@
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) => {
this.list.push({
image: item
})
if (item && item !== mainPic) {
this.list.push({
image: item
})
}
})
}
@@ -297,103 +302,292 @@
},
};
</script>
<style>
page {
background: #F8F9FC;
<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>
<style lang="less" scoped>
.wrap {
width: 750rpx;
height: 420rpx;
}
.ticket {
width: 750rpx;
background: #fff;
padding: 24rpx;
margin-top: 24rpx;
.ticket_item {
margin-top: 24rpx;
background: #F8F9FC;
border-radius: 8rpx;
padding: 24rpx;
display: flex;
}
.btn {
width: 152rpx;
height: 56rpx;
border-radius: 200rpx;
background: linear-gradient(180deg, #FF9E49 0%, #ED3B1A 100%);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
margin-top: 20rpx;
}
}
.title {
width: 750rpx;
background-color: #fff;
margin-top: 50rpx;
padding: 24rpx;
z-index: 10000;
.t_info {
color: #FF383C;
line-height: 48rpx;
margin-top: 12rpx;
font-weight: bold;
}
.t_opentime {
background-color: #F8F9FD;
padding: 8rpx;
display: flex;
flex-direction: column;
gap: 12rpx;
margin-top: 24rpx;
}
.t_address {
width: 702rpx;
height: 100rpx;
margin-top: 24rpx;
background: url('https://aliyunoss.hn12301.net/xgh/t_address_bj.png');
background-size: 100% 100%;
color: #000;
display: flex;
align-items: center;
padding-left: 24rpx;
image {
width: 40rpx;
height: 70rpx;
}
}
.t_name {
width: 702rpx;
color: #000;
font-size: 36rpx;
font-weight: 700;
line-height: 48rpx;
}
.tag {
color: #7C7C7E;
font-size: 20rpx;
padding: 5rpx 10rpx;
background: #F8F9FC;
border-radius: 4rpx;
display: block;
margin-right: 12rpx;
}
}
</style>