refactor: move pages to subpackages to reduce main package size

This commit is contained in:
jacobxu666
2026-06-26 11:59:59 +08:00
parent 4b7c540343
commit 4acb2e6a9c
18 changed files with 113 additions and 108 deletions
+367
View File
@@ -0,0 +1,367 @@
<template>
<view class="page">
<!-- 购买类 -->
<view class="category">
<text class="category-title">购买类</text>
<view class="category-divider" />
<view
v-for="(item, idx) in purchaseList"
:key="'p' + idx"
class="faq-card"
>
<view class="faq-header" @tap="toggle('p', idx)">
<view class="faq-index">{{ idx + 1 }}</view>
<text class="faq-question">{{ item.q }}</text>
<view class="faq-arrow" :class="{ 'faq-arrow--up': isOpen('p', idx) }" />
</view>
<view v-show="isOpen('p', idx)" class="faq-body">
<view v-for="(p, pi) in item.a" :key="pi" class="faq-paragraph">
<text
v-for="(seg, si) in p"
:key="si"
:class="seg.highlight ? 'faq-highlight' : 'faq-text'"
@tap="seg.copy ? copyText(seg.text) : null"
>{{ seg.text }}</text>
</view>
</view>
</view>
</view>
<!-- 预约类 -->
<view class="category">
<text class="category-title">预约类</text>
<view class="category-divider" />
<view
v-for="(item, idx) in reserveList"
:key="'r' + idx"
class="faq-card"
>
<view class="faq-header" @tap="toggle('r', idx)">
<view class="faq-index">{{ idx + 1 }}</view>
<text class="faq-question">{{ item.q }}</text>
<view class="faq-arrow" :class="{ 'faq-arrow--up': isOpen('r', idx) }" />
</view>
<view v-show="isOpen('r', idx)" class="faq-body">
<view v-for="(p, pi) in item.a" :key="pi" class="faq-paragraph">
<text
v-for="(seg, si) in p"
:key="si"
:class="seg.highlight ? 'faq-highlight' : 'faq-text'"
@tap="seg.copy ? copyText(seg.text) : null"
>{{ seg.text }}</text>
</view>
</view>
</view>
</view>
<!-- 绑定类 -->
<view class="category">
<text class="category-title">绑定类</text>
<view class="category-divider" />
<view
v-for="(item, idx) in bindList"
:key="'b' + idx"
class="faq-card"
>
<view class="faq-header" @tap="toggle('b', idx)">
<view class="faq-index">{{ idx + 1 }}</view>
<text class="faq-question">{{ item.q }}</text>
<view class="faq-arrow" :class="{ 'faq-arrow--up': isOpen('b', idx) }" />
</view>
<view v-show="isOpen('b', idx)" class="faq-body">
<view v-for="(p, pi) in item.a" :key="pi" class="faq-paragraph">
<text
v-for="(seg, si) in p"
:key="si"
:class="seg.highlight ? 'faq-highlight' : 'faq-text'"
@tap="seg.copy ? copyText(seg.text) : null"
>{{ seg.text }}</text>
</view>
</view>
</view>
</view>
<!-- 底部安全区 -->
<view class="safe-bottom" />
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue'
// 展开状态:默认全部展开
const openMap = ref<Record<string, boolean>>({})
// 初始化所有条目为展开
const purchaseList = getPurchaseList()
const reserveList = getReserveList()
const bindList = getBindList()
purchaseList.forEach((_, i) => { openMap.value[`p${i}`] = true })
reserveList.forEach((_, i) => { openMap.value[`r${i}`] = true })
bindList.forEach((_, i) => { openMap.value[`b${i}`] = true })
function toggle(cat: string, idx: number) {
const key = `${cat}${idx}`
openMap.value[key] = !openMap.value[key]
}
function isOpen(cat: string, idx: number) {
return openMap.value[`${cat}${idx}`] ?? false
}
function copyText(text: string) {
uni.setClipboardData({
data: text,
success: () => uni.showToast({ title: '已复制', icon: 'success' })
})
}
type Seg = { text: string; highlight?: boolean; copy?: boolean }
type FaqItem = { q: string; a: Seg[][] }
function getPurchaseList(): FaqItem[] {
return [
{
q: '赣湘全域旅游年卡/超级玩家卡该如何进行购买?',
a: [
[{ text: '答:首先搜索"爱上萍乡"小程序,进入首页界面,点击购买即可。' }],
[{ text: '温馨提醒:', highlight: false }, { text: '1. 赣湘全域旅游年卡/超级玩家卡一经激活或销售七日以上,不适用于退款规则;2. 部分商家限制仅对女性或特定年龄段开放,详情请查阅各商家资料规则展示页面。' }]
]
},
{
q: '对于赣湘全域旅游年卡/超级玩家卡的使用疑问怎么进行人工咨询?',
a: [
[
{ text: '答:爱上萍乡公众号的客服电话是:' },
{ text: '15388025272', highlight: true, copy: true },
{ text: ',微信号为' },
{ text: 'cshaika2024', highlight: true, copy: true },
{ text: ',人工客服工作时间:9:00-17:00(周一至周五),周末及法定节假日不受理,请大家在工作日与我们联系哦~' }
]
]
},
{
q: '赣湘全域旅游年卡/超级玩家卡该如何进行退款',
a: [
[{ text: '答:赣湘全域旅游年卡/超级玩家卡绑定激活或销售七日以上不可退卡,不适用7天无理由退货,在"爱上萍乡"小程序上购买但未激活的用户请咨询客服中心处理退卡事宜,在其他平台上购买年卡的用户请于对应平台客服进行沟通。' }]
]
},
{
q: '未成年人可以购买吗?',
a: [
[{ text: '答:可以的,赣湘全域旅游年卡/超级玩家卡覆盖的部分景区、商家目前已经对未满18周岁的少年儿童开放,未成年人也可使用本人身份证信息实名制激活使用,大小同价,由于系统对接或行业法规等限制,部分景区、商家未向少年儿童开放,建议购买前一定要详细查阅平台公告及商品详细信息哦~' }]
]
},
{
q: '小孩可以去哪些景区?在哪里可以查看?',
a: [
[{ text: '答:自2023年7月31日起,赣湘全域旅游年卡/超级玩家卡包含的景区陆续向18周岁以下儿童放开使用限制,目前已有部分景区允许儿童凭借年卡免费入园。儿童可使用权益的景区商户名单可在"爱上萍乡"-购买 - 商品详情页或各景区、商户规则展示页面进行了解。' }]
]
},
{
q: '港澳台/外国人可以购买使用赣湘全域旅游年卡/超级玩家卡吗?',
a: [
[{ text: '答:赣湘全域旅游年卡/超级玩家卡需要实名上传中国大陆的身份证信息,港澳台/外国人目前是不能激活使用年卡的。' }]
]
}
]
}
function getReserveList(): FaqItem[] {
return [
{
q: '如何预约',
a: [
[{ text: '答:部分景区、商家至少需要提前一天进行线上预约或电话预约(最多可预约七天),详细规则请进入"爱上萍乡"公众号 - 点击使用指引 - 商家预约 - 预约即可,提交预约信息后将扣除赣湘全域旅游年卡/超级玩家卡使用次数。' }],
[{ text: '需至少提前一天取消预约,详细规则请进入"爱上萍乡"公众号 - 点击使用指引 - 我的预约 - 进入预约详情界面 - 点击取消预约即可。' }],
[{ text: '使用日当天不可取消预约,过期视为自动放弃使用权益。' }]
]
},
{
q: '如何出示赣湘全域旅游年卡/超级玩家卡二维码',
a: [
[{ text: '答:关注"爱上萍乡"公众号 - 点击使用指引 - 点击入园码即可出示年卡二维码;' }]
]
},
{
q: '如何找到景区和商家',
a: [
[{ text: '答:关注"爱上萍乡"公众号 - 点击年卡 - 年卡权益,下方的游吃住行娱乐购里的景区商家都可点击查看具体的赣湘全域旅游年卡/超级玩家卡使用规则。' }]
]
},
{
q: '预约了,如果不去会扣除次数吗?',
a: [
[{ text: '答:如预约之后因非客观因素无法前往且未提前取消预约则会扣除年卡使用次数,扣除使用次数后的权益不可恢复。' }]
]
},
{
q: '景区没有预约不能进去客服可以帮我预约吗?',
a: [
[{ text: '答:后台并没有权限给客户操作预约,请大家在"爱上萍乡"公众号自行预约哦。' }]
]
}
]
}
function getBindList(): FaqItem[] {
return [
{
q: '赣湘全域旅游年卡/超级玩家卡如何绑定?',
a: [
[{ text: '答:购买赣湘全域旅游年卡/超级玩家卡后,平台会通过短信发送一个8位数的核销码,请大家搜索"爱上萍乡"公众号,点击使用指引—激活年卡—核销码激活,最后上传个人身份信息即可。' }]
]
},
{
q: '赣湘全域旅游年卡/超级玩家卡可以解绑吗?',
a: [
[{ text: '答:注意啦!赣湘全域旅游年卡/超级玩家卡一旦绑定身份信息后,是无法进行解绑或换绑的哦。' }]
]
},
{
q: '一个账号能绑定多张卡吗?',
a: [
[{ text: '答:一个微信用户只能绑定4张赣湘全域旅游年卡/超级玩家卡,请不要多绑或者错绑哦~' }],
[{ text: '成人绑定年卡建议分开绑定,以免造成出行不便哦(只需把8位数的核销码发给需要绑定的人,选择核销码激活上传自己的身份信息即可)' }]
]
},
{
q: '赣湘全域旅游年卡/超级玩家卡已激活绑定能否给别人使用?',
a: [
[{ text: '答:赣湘全域旅游年卡/超级玩家卡实行一卡一人,已激活绑定的年卡仅限本人使用,不可转借他人。' }]
]
}
]
}
</script>
<style lang="scss" scoped>
.page {
min-height: 100vh;
background: $color-background;
padding: 48rpx 24rpx 0;
font-family: $font-family-body;
}
/* ===== 分类区块 ===== */
.category {
margin-bottom: 32rpx;
}
.category-title {
font-family: $font-family-body;
font-size: $font-size-base;
font-weight: $font-weight-bold;
color: $uv-main-color;
display: block;
margin-bottom: 16rpx;
}
.category-divider {
height: 1rpx;
background: $uv-border-color;
margin-bottom: 24rpx;
}
/* ===== 问答卡片 ===== */
.faq-card {
background: $color-card;
border-radius: 24rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
margin-bottom: 20rpx;
overflow: hidden;
}
.faq-header {
display: flex;
align-items: flex-start;
padding: 32rpx;
transition: background 0.15s ease;
&:active {
background: $uv-bg-color;
}
}
.faq-index {
width: 40rpx;
height: 40rpx;
border-radius: 50%;
background: rgba(60, 156, 255, 0.08);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
margin-right: 16rpx;
margin-top: 4rpx;
font-family: $font-family-body;
font-size: $font-size-sm;
font-weight: $font-weight-bold;
color: $uv-primary;
}
.faq-question {
flex: 1;
font-family: $font-family-body;
font-size: $font-size-base;
font-weight: $font-weight-medium;
color: $uv-main-color;
line-height: 1.5;
}
.faq-arrow {
width: 16rpx;
height: 16rpx;
border-right: 3rpx solid #cccccc;
border-bottom: 3rpx solid #cccccc;
transform: rotate(45deg);
flex-shrink: 0;
margin-left: 16rpx;
margin-top: 10rpx;
transition: transform 0.2s ease;
}
.faq-arrow--up {
transform: rotate(-135deg);
}
/* ===== 回答区域 ===== */
.faq-body {
padding: 0 32rpx 32rpx;
}
.faq-paragraph {
margin-bottom: 16rpx;
&:last-child {
margin-bottom: 0;
}
}
.faq-text {
font-family: $font-family-body;
font-size: $font-size-sm;
font-weight: 400;
color: $uv-content-color;
line-height: 1.7;
}
.faq-highlight {
font-family: $font-family-body;
font-size: $font-size-sm;
font-weight: 400;
color: $uv-primary;
line-height: 1.7;
}
/* ===== 底部安全区 ===== */
.safe-bottom {
height: 64rpx;
padding-bottom: env(safe-area-inset-bottom);
}
</style>
+201
View File
@@ -0,0 +1,201 @@
<template>
<view class="page">
<view class="list-container">
<view
v-for="(item, index) in notices"
:key="item.id"
class="notice-card fade-in"
:style="{ animationDelay: index * 60 + 'ms' }"
@click="openDetail(item)"
>
<!-- 第一行:标题 + 日期 + 箭头 -->
<view class="card-header">
<view class="header-text">
<text class="card-title">{{ item.title }}</text>
<text class="card-summary">{{ item.content }}</text>
</view>
<text class="card-date">{{ item.date }}</text>
<view class="card-arrow" />
</view>
</view>
</view>
<!-- 公告详情弹窗 -->
<AnnouncementModal
v-model="showModal"
:title="modalTitle"
:body="modalBody"
/>
<!-- 底部安全区 -->
<view class="safe-bottom" />
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import AnnouncementModal from '../../../components/AnnouncementModal.vue'
interface NoticeItem {
id: number
title: string
date: string
content: string
body: string
}
const notices = ref<NoticeItem[]>([
{ id: 1, title: '赣湘全域旅游年卡限时优惠', date: '2026-06-20', content: '赣湘全域旅游年卡限时优惠,工会会员专享¥300/套卡!', body: '赣湘全域旅游年卡限时优惠活动现已开启,工会会员凭有效证件可享专享价¥300/套卡(原价¥580)。套卡包含武功山金顶景区、杨岐山孽龙洞景区、仙凤三宝景区等42家合作景区全年免首道门票权益,覆盖萍乡、长沙、株洲、湘潭等地区。活动期间购买还可额外获赠君澜温泉单次体验券一张。活动截止至2026年7月31日,数量有限,售完即止。' },
{ id: 2, title: '武功山金顶景区开放通知', date: '2026-06-18', content: '武功山金顶景区已开放,持卡可免费入园1次。', body: '武功山金顶景区即日起正常开放,营业时间08:00-18:00。赣湘全域旅游年卡持卡用户可免费入园1次(含首道门票,价值¥70)。温馨提示:山顶温差较大,建议携带防风保暖衣物;雷雨天气请勿登顶;需提前1天通过小程序预约,每日限流5000人。' },
{ id: 3, title: '超级玩家卡全新上线', date: '2026-06-15', content: '超级玩家卡全新上线,40家景区免门票,仅¥198!', body: '超级玩家卡全新上线!仅需¥198即可享受40家合作景区免首道门票权益,单次最高可省¥228。覆盖景区包括武功山金顶(¥70)、君澜温泉(¥228)、杨岐山孽龙洞(¥68)、仙凤三宝(¥50)等热门景区。卡片有效期为购买之日起一年,法定节假日通用,不限购买次数。适合经常出游的家庭和户外爱好者。' },
{ id: 4, title: '端午节假期景区预约提醒', date: '2026-06-10', content: '端午假期热门景区需提前预约,请合理安排出行。', body: '端午节假期(6月14日至16日)期间,武功山金顶景区、君澜温泉等热门景区将实行限流措施,每日限流5000人。请持卡用户提前通过小程序预约,未预约将无法入园。建议错峰出行,关注景区实时客流信息。' },
{ id: 5, title: '新增合作景区:莲花荷博园', date: '2026-06-05', content: '莲花荷博园正式加入合作景区,持卡免门票6次。', body: '莲花荷博园于2026年6月起正式加入赣湘全域旅游年卡合作景区。持卡用户全年可免首道门票入园6次(门票原价¥35)。莲花荷博园位于萍乡市莲花县,夏季荷花盛开时景色最佳,是休闲观光的好去处。' },
])
/** 弹窗状态 */
const showModal = ref(false)
const modalTitle = ref('公告')
const modalBody = ref('')
function openDetail(item: NoticeItem) {
modalTitle.value = item.title
modalBody.value = item.body
showModal.value = true
}
</script>
<style lang="scss" scoped>
/* ================================================================
通知公告列表页 — Premium Utilitarian Minimalism
Typography:
Body/UI: PingFang SC (system)
Title: 28rpx / 600 weight / #1A1A1A / tracking -0.3rpx
Summary: 26rpx / 400 weight / #888888 / line-height 1.6
Date: 22rpx / 400 weight / #BBBBBB
Action: 26rpx / 500 weight / #3C9CFF
Color palette:
Canvas: #F7F8FA
Surface: #FFFFFF
Title: #1A1A1A (off-black, never pure #000)
Body: #888888
Muted: #BBBBBB
Primary: #3C9CFF
Icon bg: #ECF5FF ($uv-primary-light)
Spacing (rpx):
Card radius: 16
Card gap: 24
Side margin: 30
Card padding: 32 / 28
Icon: 56 × 56, radius 14
Icon→title: 16
Title→summary: 16
Summary→action: 24
================================================================ */
.page {
min-height: 100vh;
background: #F7F8FA;
font-family: 'SourceHanSerifCN', '思源宋体', -apple-system, 'PingFang SC', sans-serif;
}
.list-container {
padding: 24rpx 30rpx;
}
/* ====== 入场动画 ====== */
@keyframes cardFadeIn {
from {
opacity: 0;
transform: translateY(16rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.fade-in {
animation: cardFadeIn 500ms cubic-bezier(0.16, 1, 0.3, 1) both;
}
/* ====== 卡片 ====== */
.notice-card {
background: #FFFFFF;
border-radius: 24rpx;
padding: 32rpx 28rpx;
margin-bottom: 24rpx;
border: 1rpx solid rgba(0, 0, 0, 0.04);
&:active {
opacity: 0.88;
transition: opacity 100ms;
}
}
/* ====== 卡片内容行 ====== */
.card-header {
display: flex;
align-items: center;
}
/* 左侧文字区(标题 + 摘要) */
.header-text {
flex: 1;
min-width: 0;
margin-right: 16rpx;
}
/* 标题 */
.card-title {
display: block;
font-size: $font-size-base;
font-weight: $font-weight-bold;
color: $uv-main-color;
line-height: 1.3;
letter-spacing: -0.3rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-bottom: 8rpx;
}
/* 摘要 */
.card-summary {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
font-size: $font-size-sm;
font-weight: $font-weight-normal;
color: $uv-tips-color;
line-height: 1.6;
}
/* 日期 */
.card-date {
font-size: 22rpx;
font-weight: $font-weight-normal;
color: $uv-placeholder-color;
flex-shrink: 0;
letter-spacing: 0.5rpx;
}
/* 右箭头 */
.card-arrow {
width: 14rpx;
height: 14rpx;
border-right: 3rpx solid #dadbde;
border-bottom: 3rpx solid #dadbde;
transform: rotate(-45deg);
flex-shrink: 0;
margin-left: 16rpx;
}
/* ====== 底部安全区 ====== */
.safe-bottom {
height: calc(40rpx + env(safe-area-inset-bottom));
}
</style>
@@ -0,0 +1,618 @@
<template>
<view class="page">
<!-- 模块1:轮播大图 -->
<swiper
class="hero-swiper"
:indicator-dots="scenic.images.length > 1"
indicator-color="rgba(255,255,255,0.4)"
indicator-active-color="#ffffff"
:autoplay="scenic.images.length > 1"
circular
:interval="4000"
>
<swiper-item v-for="(img, idx) in scenic.images" :key="idx">
<image class="hero-img" :src="img" mode="aspectFill" />
</swiper-item>
</swiper>
<!-- 模块2:基础信息卡片 -->
<view class="info-card">
<!-- 标题行 -->
<view class="title-row">
<text class="scenic-name">{{ scenic.name }}</text>
</view>
<!-- 预约提醒横幅 -->
<view v-if="scenic.needReservation" class="notice-banner">
<view class="icon-wrap icon-alert" />
<text class="notice-text">需预约后出行,未预约无法入园</text>
</view>
<!-- 特色标签行 -->
<view class="tags-row" v-if="scenic.tags && scenic.tags.length">
<view class="feature-tag" v-for="tag in scenic.tags" :key="tag">
<text>{{ tag }}</text>
</view>
</view>
<!-- 预约时限 -->
<view v-if="scenic.needReservation && scenic.bookingDeadline" class="info-line">
<text class="text-assist">{{ scenic.bookingDeadline }}</text>
</view>
<!-- 开放时间 + 运营状态 -->
<view class="info-line" @click="onHoursClick">
<view class="icon-wrap icon-clock" />
<text class="text-body">{{ scenic.hours }}</text>
<view class="status-tag" :class="isOpen ? 'open' : 'closed'">
<view class="status-dot" />
<text>{{ isOpen ? '营业中' : '已闭园' }}</text>
</view>
</view>
<!-- 联系电话 -->
<view class="info-line clickable" @click="onPhoneClick">
<view class="icon-wrap icon-phone" />
<text class="text-body">{{ scenic.phone }}</text>
<view class="icon-wrap icon-chevron chevron-right" />
</view>
<!-- 地址 -->
<view class="info-line clickable" @click="onCopyAddress">
<view class="icon-wrap icon-map-pin" />
<text class="text-body address-text">{{ scenic.address }}</text>
<view class="map-btn" @click.stop="onCopyAddress">
<text>复制</text>
</view>
</view>
</view>
<!-- 模块3:卡种权益卡片(独立白卡,无绑定自动隐藏) -->
<view
class="benefit-card"
v-for="card in scenic.cards"
:key="card.type"
>
<view class="benefit-top">
<view class="card-type-tag" :class="card.type">
<text>{{ card.type === 'super' ? '超级玩家卡' : '赣湘全域旅游年卡' }}</text>
</view>
<text v-if="scenic.price > 0" class="ticket-price">¥{{ scenic.price }}</text>
</view>
<view class="benefit-mid">
<text class="ticket-label">首道门票</text>
<view class="member-row">
<view class="icon-wrap icon-crown" />
<text class="member-text">持卡用户免费</text>
</view>
</view>
<text class="remaining-text">当前卡剩余可使用 {{ card.remaining }} 次</text>
<view class="book-btn" @click="onBookClick(card.type)">
<text>立即预约</text>
</view>
</view>
<!-- 模块4:Tab切换栏 -->
<view class="tab-bar">
<view
class="tab-item"
:class="{ active: activeTab === 'notice' }"
@click="activeTab = 'notice'"
>
<text>预约须知</text>
<view v-if="activeTab === 'notice'" class="tab-indicator" />
</view>
<view
class="tab-item"
:class="{ active: activeTab === 'intro' }"
@click="activeTab = 'intro'"
>
<text>景区介绍</text>
<view v-if="activeTab === 'intro'" class="tab-indicator" />
</view>
</view>
<!-- 模块5:Tab内容区域 -->
<view class="tab-content">
<view v-if="activeTab === 'notice'" class="content-section">
<view class="content-block" v-for="(item, idx) in scenic.bookingNotice" :key="idx">
<text class="block-title">{{ item.title }}</text>
<view class="divider" />
<text class="block-text">{{ item.content }}</text>
</view>
</view>
<view v-if="activeTab === 'intro'" class="content-section">
<text class="intro-text">{{ scenic.introduction }}</text>
</view>
</view>
<!-- 底部安全区 -->
<view class="safe-bottom" />
</view>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
interface NoticeItem { title: string; content: string }
interface CardBenefit { type: 'super' | 'annual'; remaining: number }
interface ScenicDetail {
id: number; name: string; price: number; freeCount: number; level: string
hours: string; needReservation: boolean; bookingDeadline: string
phone: string; address: string; tags: string[]; images: string[]
introduction: string; bookingNotice: NoticeItem[]; cards: CardBenefit[]
}
const scenic = ref<ScenicDetail>({ tags: [], images: [], cards: [], bookingNotice: [] } as unknown as ScenicDetail)
const activeTab = ref<'notice' | 'intro'>('notice')
/** 根据营业时间判断运营状态 */
const isOpen = computed(() => {
const h = scenic.value.hours
if (!h || h.includes('暂停') || h.includes('闭')) return false
const match = h.match(/(\d{2}):(\d{2})\s*-\s*(\d{2}):(\d{2})/)
if (!match) return true
const now = new Date()
const current = now.getHours() * 60 + now.getMinutes()
const open = parseInt(match[1]) * 60 + parseInt(match[2])
const close = parseInt(match[3]) * 60 + parseInt(match[4])
return current >= open && current <= close
})
// 双卡种数据:超级玩家卡 remaining 与 freeCount 一致,年卡 remaining 稍多
function buildCards(freeCount: number): CardBenefit[] {
if (freeCount <= 0) return []
return [
{ type: 'super', remaining: freeCount },
{ type: 'annual', remaining: freeCount + 2 },
]
}
const scenicData: ScenicDetail[] = [
{ id: 1, name: '武功山金顶景区', price: 70, freeCount: 1, level: '5A', hours: '08:00-18:00', needReservation: true, bookingDeadline: '游玩当日16:00前预约', phone: '0799-7636666', address: '江西省萍乡市芦溪县东南边境', tags: ['5A景区', '高山草甸', '日出云海', '徒步圣地'], images: ['https://images.unsplash.com/photo-1513415564515-763d91423bdd?fm=jpg&q=80&w=750&h=500&fit=crop', 'https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?fm=jpg&q=80&w=750&h=500&fit=crop', 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?fm=jpg&q=80&w=750&h=500&fit=crop'], introduction: '武功山位于江西省萍乡市东南边缘,主峰白鹤峰海拔1918.3米,为江西省第一高峰。武功山以壮观的高山草甸、日出云海和千年古祭坛群著称,被誉为"云中草原、户外天堂"。景区内有金顶、羊狮幕、发云界等核心景观,四季景色各异,春赏杜鹃、夏避酷暑、秋观红叶、冬看雾凇,是徒步露营和摄影爱好者的理想目的地。', bookingNotice: [{ title: '预约方式', content: '需提前通过小程序在线预约,预约成功后凭二维码入园。每日限流5000人,建议提前1-3天预约。' }, { title: '入园须知', content: '请携带身份证原件,配合工作人员核验健康码。山顶温差较大,建议携带防风保暖衣物。雷雨天气请勿登顶。' }, { title: '退改规则', content: '预约成功后如需取消,请至少提前24小时操作。逾期未使用视为自动放弃,不予退款。' }], cards: buildCards(1) },
{ id: 2, name: '杨岐山孽龙洞景区', price: 68, freeCount: 6, level: '4A', hours: '08:30-17:30', needReservation: false, bookingDeadline: '', phone: '0799-3862888', address: '江西省萍乡市上栗县杨岐山', tags: ['4A景区', '溶洞奇观', '地质公园', '天然氧吧'], images: ['https://images.unsplash.com/photo-1506905925346-21bda4d32df4?fm=jpg&q=80&w=750&h=500&fit=crop', 'https://images.unsplash.com/photo-1519681393784-d120267933ba?fm=jpg&q=80&w=750&h=500&fit=crop'], introduction: '孽龙洞位于杨岐山风景名胜区内,是一个形成于1.8亿年前的天然溶洞。洞内全长4公里,已开发游览线路2.8公里,洞内常年恒温18℃,钟乳石形态各异,有"地下艺术长廊"之美誉。', bookingNotice: [{ title: '入园方式', content: '持惠民卡直接到景区售票处验证入园,无需预约。' }, { title: '游览提示', content: '洞内温度恒定18℃,建议携带薄外套。洞内道路湿滑,请穿防滑鞋,老人和儿童需有成人陪同。' }], cards: buildCards(6) },
{ id: 3, name: '仙凤三宝景区', price: 50, freeCount: 6, level: '4A', hours: '09:00-17:00', needReservation: false, bookingDeadline: '', phone: '0799-7558888', address: '江西省萍乡市芦溪县宣风镇', tags: ['4A景区', '亲子乐园', '萌宠互动', '田园体验'], images: ['https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?fm=jpg&q=80&w=750&h=500&fit=crop'], introduction: '仙凤三宝景区是一个集生态农业、亲子游乐、休闲度假于一体的综合性旅游景区。园内有萌宠乐园、花海观赏、果蔬采摘等特色项目,适合全家出行。', bookingNotice: [{ title: '入园方式', content: '持惠民卡直接到景区售票处验证入园,无需预约。' }, { title: '游玩提示', content: '适合亲子游玩,园内有小动物喂养体验,请勿投喂自带食物。' }], cards: buildCards(6) },
{ id: 4, name: '武功山君澜温泉', price: 228, freeCount: 6, level: '4A', hours: '10:00-22:00', needReservation: true, bookingDeadline: '游玩当日12:00前预约', phone: '0799-7639999', address: '江西省萍乡市芦溪县武功山景区内', tags: ['4A景区', '温泉养生', '度假休闲', '山间秘境'], images: ['https://images.unsplash.com/photo-1433086966358-54859d0ed716?fm=jpg&q=80&w=750&h=500&fit=crop', 'https://images.unsplash.com/photo-1545569341-9eb8b30979d9?fm=jpg&q=80&w=750&h=500&fit=crop'], introduction: '武功山君澜温泉坐落于武功山脚下,依山傍水,环境优美。温泉水取自地下1500米深层矿泉,富含多种对人体有益的微量元素。', bookingNotice: [{ title: '预约方式', content: '需提前通过小程序预约,建议提前1天。请自备泳衣泳帽。' }, { title: '退改规则', content: '提前24小时可免费取消。' }], cards: buildCards(6) },
{ id: 5, name: '凯光新天地生态旅游区', price: 90, freeCount: 6, level: '4A', hours: '08:00-18:00', needReservation: false, bookingDeadline: '', phone: '0799-6881888', address: '江西省萍乡市安源区高坑镇', tags: ['4A景区', '生态观光', '户外拓展', '花卉园林'], images: ['https://images.unsplash.com/photo-1441974231531-c6227db76b6e?fm=jpg&q=80&w=750&h=500&fit=crop'], introduction: '凯光新天地生态旅游区是集生态观光、休闲娱乐、户外运动于一体的大型生态旅游综合体。', bookingNotice: [{ title: '入园方式', content: '持惠民卡直接到景区售票处验证入园,无需预约。' }], cards: buildCards(6) },
{ id: 14, name: '安源路矿工人运动纪念馆', price: 0, freeCount: 0, level: '4A', hours: '09:00-16:30', needReservation: false, bookingDeadline: '', phone: '0799-6832001', address: '江西省萍乡市安源区安源镇', tags: ['4A景区', '红色教育', '免费参观', '爱国主义基地'], images: ['https://images.unsplash.com/photo-1565008576549-57569a49371d?fm=jpg&q=80&w=750&h=500&fit=crop'], introduction: '安源路矿工人运动纪念馆是全国重点文物保护单位,全面展示了安源路矿工人大罢工的光辉历史。', bookingNotice: [{ title: '参观须知', content: '免费参观,周一闭馆。请携带身份证原件入馆。' }], cards: buildCards(0) },
{ id: 30, name: '坊楼红色文化园', price: 0, freeCount: 0, level: '3A', hours: '09:00-17:00', needReservation: false, bookingDeadline: '', phone: '0799-7220000', address: '江西省萍乡市莲花县坊楼镇', tags: ['3A景区', '红色教育', '免费参观', '革命历史'], images: ['https://images.unsplash.com/photo-1565008576549-57569a49371d?fm=jpg&q=80&w=750&h=500&fit=crop'], introduction: '坊楼红色文化园展示了莲花县丰富的革命历史,是重要的红色教育基地。', bookingNotice: [{ title: '参观须知', content: '免费参观,请保持安静。' }], cards: buildCards(0) },
{ id: 38, name: '荷尧古渡口', price: 0, freeCount: 0, level: '2A', hours: '全天开放', needReservation: false, bookingDeadline: '', phone: '0799-3377200', address: '江西省萍乡市湘东区荷尧镇', tags: ['2A景区', '古渡口遗址', '免费开放', '历史文化'], images: ['https://images.unsplash.com/photo-1504280390367-361c6d9f38f4?fm=jpg&q=80&w=750&h=500&fit=crop'], introduction: '荷尧古渡口是一处保存完好的古渡口遗址,见证了千年的交通商贸历史。', bookingNotice: [{ title: '游览提示', content: '免费开放,全天可参观。临水区域请注意安全。' }], cards: buildCards(0) },
]
onLoad((options) => {
const id = parseInt(options?.id || '0')
const found = scenicData.find(s => s.id === id)
if (found) {
scenic.value = found
uni.setNavigationBarTitle({ title: found.name })
} else {
scenic.value = scenicData[0]
uni.setNavigationBarTitle({ title: scenicData[0].name })
}
})
function onHoursClick() {
uni.showToast({ title: '营业时间仅供参考', icon: 'none' })
}
function onPhoneClick() {
uni.makePhoneCall({ phoneNumber: scenic.value.phone })
}
function onCopyAddress() {
uni.setClipboardData({
data: scenic.value.address,
success: () => {
uni.showToast({ title: '地址已复制', icon: 'success' })
}
})
}
function onBookClick(cardType: string) {
uni.navigateTo({ url: '/packages/user/pages/reserve/index' })
}
</script>
<style lang="scss" scoped>
/* Lucide 图标 data URI — 统一 28rpx、stroke-width:2 */
$icon-size: 28rpx;
$icon-alert: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23ff7d00' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3'/%3E%3Cpath d='M12 9v4'/%3E%3Cpath d='M12 17h.01'/%3E%3C/svg%3E");
$icon-clock: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23666666' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'/%3E%3Cpolyline points='12 6 12 12 16 14'/%3E%3C/svg%3E");
$icon-phone: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23666666' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z'/%3E%3C/svg%3E");
$icon-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23999999' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m9 18 6-6-6-6'/%3E%3C/svg%3E");
$icon-map-pin: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23666666' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z'/%3E%3Ccircle cx='12' cy='10' r='3'/%3E%3C/svg%3E");
$icon-crown: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23f9ae3d' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m2 4 3 12h14l3-12-6 7-4-7-4 7-6-7zm3 16h14'/%3E%3C/svg%3E");
.page {
min-height: 100vh;
background: $color-background;
font-family: 'SourceHanSerifCN', '思源宋体', serif;
}
/* ====== 图标基础 ====== */
.icon-wrap {
width: $icon-size;
height: $icon-size;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
flex-shrink: 0;
}
.icon-alert { background-image: $icon-alert; }
.icon-clock { background-image: $icon-clock; }
.icon-phone { background-image: $icon-phone; }
.icon-chevron { background-image: $icon-chevron; }
.chevron-right { margin-left: auto; }
.icon-map-pin { background-image: $icon-map-pin; }
.icon-crown { background-image: $icon-crown; }
/* ====== 字体层级 ====== */
.text-title {
font-size: $font-size-title;
font-weight: $font-weight-bold;
color: $uv-main-color;
line-height: 48rpx;
}
.text-body {
font-size: $font-size-base;
color: $uv-content-color;
line-height: 40rpx;
}
.text-assist {
font-size: $font-size-sm;
color: $uv-tips-color;
line-height: 32rpx;
}
.text-hint {
font-size: 22rpx;
color: $uv-placeholder-color;
line-height: 32rpx;
}
/* ====== 模块1:轮播大图 ====== */
.hero-swiper {
width: 100%;
height: 480rpx;
:deep(.uni-swiper-dots) {
z-index: 10;
bottom: 64rpx !important;
}
:deep(.uni-swiper-dot) {
width: 16rpx;
height: 16rpx;
border-radius: 50%;
margin: 0 8rpx !important;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
}
:deep(.uni-swiper-dot-active) {
width: 32rpx;
border-radius: 8rpx;
}
}
.hero-img {
width: 100%;
height: 480rpx;
}
/* ====== 模块2:基础信息卡片 ====== */
.info-card {
background: $color-card;
margin: -40rpx $spacing-3 0;
padding: $spacing-3;
border-radius: $radius-lg;
box-shadow: $shadow-base;
position: relative;
z-index: 5;
}
.title-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: $spacing-2;
}
.scenic-name {
@extend .text-title;
flex: 1;
}
.notice-banner {
display: flex;
align-items: center;
background: $uv-warning-light;
border-radius: $radius-base;
padding: $spacing-2;
margin-bottom: $spacing-2;
gap: 12rpx;
}
.notice-text {
font-size: $font-size-sm;
color: #ff7d00;
font-weight: $font-weight-medium;
}
.tags-row {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
margin-bottom: $spacing-2;
}
.feature-tag {
padding: 4rpx 16rpx;
border: $border-width solid rgba($uv-primary, 0.3);
border-radius: $radius-sm;
text {
font-size: $font-size-sm;
color: $uv-primary;
}
}
.info-line {
display: flex;
align-items: center;
min-height: 64rpx;
padding: 16rpx 0;
gap: 12rpx;
border-bottom: $border-width solid rgba($uv-border-color, 0.3);
&:last-child {
border-bottom: none;
}
&.clickable {
min-height: 88rpx;
transition: background $transition-fast;
&:active {
background: $color-muted;
margin: 0 (-$spacing-3);
padding-left: $spacing-3;
padding-right: $spacing-3;
}
}
}
.address-text {
flex: 1;
line-height: 1.5;
}
/* 运营状态标签 */
.status-tag {
display: flex;
align-items: center;
gap: 8rpx;
margin-left: auto;
flex-shrink: 0;
text {
font-size: $font-size-sm;
font-weight: $font-weight-medium;
}
&.open text { color: $uv-success; }
&.closed text { color: $uv-error; }
}
.status-dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
.open & { background: $uv-success; }
.closed & { background: $uv-error; }
}
.map-btn {
padding: 8rpx 20rpx;
background: $uv-primary-light;
border-radius: $radius-sm;
flex-shrink: 0;
text {
font-size: $font-size-sm;
color: $uv-primary;
font-weight: $font-weight-medium;
}
&:active { opacity: 0.7; }
}
/* ====== 模块3:卡种权益卡片 ====== */
.benefit-card {
background: $color-card;
margin: $spacing-3 $spacing-3 $spacing-2;
padding: $spacing-3;
border-radius: $radius-lg;
box-shadow: $shadow-sm;
}
.benefit-top {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: $spacing-2;
}
.card-type-tag {
padding: 4rpx 16rpx;
border-radius: $radius-sm;
text {
font-size: 22rpx;
color: #fff;
font-weight: $font-weight-medium;
}
&.super {
background: #f9ae3d;
}
&.annual {
background: $uv-success;
}
}
.ticket-price {
font-size: $font-size-base;
color: $uv-placeholder-color;
text-decoration: line-through;
}
.benefit-mid {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12rpx;
}
.ticket-label {
font-size: $font-size-base;
color: $uv-content-color;
font-weight: $font-weight-medium;
}
.member-row {
display: flex;
align-items: center;
gap: 8rpx;
}
.member-text {
font-size: $font-size-sm;
color: #f9ae3d;
font-weight: $font-weight-medium;
}
.remaining-text {
font-size: 22rpx;
color: $uv-success;
display: block;
margin-bottom: $spacing-2;
}
.book-btn {
display: flex;
align-items: center;
justify-content: center;
height: 88rpx;
background: linear-gradient(135deg, $uv-primary, $uv-primary-dark);
border-radius: $radius-full;
box-shadow: 0 4rpx 16rpx rgba($uv-primary, 0.3);
transition: transform $transition-fast;
text {
font-size: $font-size-base;
color: #fff;
font-weight: $font-weight-medium;
}
&:active {
transform: scale(0.97);
opacity: 0.9;
}
}
/* ====== 模块4:Tab切换栏 ====== */
.tab-bar {
display: flex;
background: $color-card;
margin: 0 $spacing-3;
border-radius: $radius-lg $radius-lg 0 0;
box-shadow: $shadow-sm;
overflow: hidden;
}
.tab-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: $spacing-3 0 $spacing-2;
position: relative;
text {
font-size: $font-size-base;
color: $uv-placeholder-color;
font-weight: $font-weight-normal;
}
&.active text {
color: $uv-primary;
font-weight: $font-weight-medium;
}
&:active {
background: $color-muted;
}
}
.tab-indicator {
width: 48rpx;
height: 6rpx;
background: $uv-primary;
border-radius: $radius-full;
margin-top: 8rpx;
}
/* ====== 模块5:Tab内容 ====== */
.tab-content {
background: $color-card;
margin: 0 $spacing-3;
border-radius: 0 0 $radius-lg $radius-lg;
box-shadow: $shadow-sm;
padding: $spacing-3;
}
.content-section {
animation: fadeIn 200ms ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.content-block {
margin-bottom: $spacing-3;
&:last-child { margin-bottom: 0; }
}
.block-title {
font-size: $font-size-base;
font-weight: $font-weight-bold;
color: $uv-content-color;
display: block;
margin-bottom: 8rpx;
}
.divider {
height: $border-width;
background: rgba($uv-border-color, 0.4);
margin-bottom: $spacing-2;
}
.block-text {
font-size: $font-size-sm;
color: $uv-tips-color;
line-height: 1.8;
display: block;
}
.intro-text {
font-size: $font-size-sm;
color: $uv-tips-color;
line-height: 2;
display: block;
}
/* ====== 底部安全区 ====== */
.safe-bottom {
height: calc(40rpx + env(safe-area-inset-bottom));
}
</style>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,21 @@
<template>
<view class="page">
<image
class="guide-image"
src="https://hn12301.oss-cn-shenzhen.aliyuncs.com/hn12301/%E5%9B%BE%E7%89%87%E6%96%87%E5%AD%97%E6%9B%BF%E6%8D%A2.png"
mode="widthFix"
/>
</view>
</template>
<style lang="scss" scoped>
.page {
min-height: 100vh;
background: $color-card;
}
.guide-image {
width: 100%;
display: block;
}
</style>