feat: 新增绑卡激活、旅游卡购买、工会购买、使用说明、常见问题、订单管理页面
- 新增 pages/card-bind 绑卡激活页(证件类型下拉、照片上传、表单校验) - 新增 pages/card-buy 超级玩家卡购买页(微信支付) - 新增 pages/union-buy 工会购买页 - 新增 pages/usage-guide 使用说明页 - 新增 pages/faq 常见问题页(购买类/预约类/绑定类折叠问答) - 新增 pages/order 订单列表页(Tab筛选/下拉刷新/状态标签) - 新增 pages/order 订单详情页(状态差异化展示/卡号复制) - 登录页背景改为白色、按钮统一主色、协议未同意提示 - 首页金刚区跳转对接(绑卡/预约/入园/FAQ) - 旅游卡页使用说明跳转、景区权益改为使用说明 - 我的页面菜单跳转对接(订单/使用说明/FAQ) - 景区列表新增卡种筛选、吸顶区动态高度适配 - 全局统一思源宋体、主色#3c9cff、rpx单位 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,18 @@
|
|||||||
"navigationBarTitleText": "登录"
|
"navigationBarTitleText": "登录"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/login/agreement",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "服务条款"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/login/privacy",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "隐私条款"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/profile/index",
|
"path": "pages/profile/index",
|
||||||
"style": {
|
"style": {
|
||||||
@@ -48,6 +60,48 @@
|
|||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "通知公告"
|
"navigationBarTitleText": "通知公告"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/card-bind/card-bind",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "绑卡激活"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/card-buy/card-buy",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "超级玩家卡"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/usage-guide/usage-guide",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "使用说明"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/faq/faq",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "常见问题"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/union-buy/union-buy",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "工会购买"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/order/order-list",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "我的订单"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/order/order-detail",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "订单详情"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"subPackages": [
|
"subPackages": [
|
||||||
|
|||||||
@@ -0,0 +1,754 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page">
|
||||||
|
<!-- 1. 头部标题区 -->
|
||||||
|
<view class="header">
|
||||||
|
<view class="header-title">
|
||||||
|
<text class="header-title__bind">绑定</text>
|
||||||
|
<text class="header-title__card">赣湘全域旅游年卡/超级玩家卡</text>
|
||||||
|
</view>
|
||||||
|
<text class="header-desc">请务必填写您的真实信息,以便于景区实名制预约</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 2. 白色圆角表单卡片 -->
|
||||||
|
<view class="form-card">
|
||||||
|
<!-- 卡号 -->
|
||||||
|
<view class="form-row">
|
||||||
|
<text class="form-label">卡号</text>
|
||||||
|
<input
|
||||||
|
class="form-input"
|
||||||
|
v-model="form.cardNo"
|
||||||
|
placeholder="请输入卡号"
|
||||||
|
placeholder-class="placeholder"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<view class="divider" />
|
||||||
|
|
||||||
|
<!-- 持卡人姓名 -->
|
||||||
|
<view class="form-row">
|
||||||
|
<text class="form-label">持卡人姓名</text>
|
||||||
|
<input
|
||||||
|
class="form-input"
|
||||||
|
v-model="form.name"
|
||||||
|
placeholder="请输入持卡人姓名"
|
||||||
|
placeholder-class="placeholder"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<view class="divider" />
|
||||||
|
|
||||||
|
<!-- 持卡人手机 -->
|
||||||
|
<view class="form-row">
|
||||||
|
<text class="form-label">持卡人手机</text>
|
||||||
|
<input
|
||||||
|
class="form-input"
|
||||||
|
v-model="form.phone"
|
||||||
|
type="number"
|
||||||
|
maxlength="11"
|
||||||
|
placeholder="请输入持卡人手机号"
|
||||||
|
placeholder-class="placeholder"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<view class="divider" />
|
||||||
|
|
||||||
|
<!-- 证件类型 -->
|
||||||
|
<view class="form-row" @tap="showIdTypePicker = true">
|
||||||
|
<text class="form-label">证件类型</text>
|
||||||
|
<text class="form-value">{{ docType }}</text>
|
||||||
|
<view class="chevron-right" />
|
||||||
|
</view>
|
||||||
|
<view class="divider" />
|
||||||
|
|
||||||
|
<!-- 证件号码 -->
|
||||||
|
<view class="form-row">
|
||||||
|
<text class="form-label">证件号码</text>
|
||||||
|
<input
|
||||||
|
class="form-input"
|
||||||
|
v-model="form.idCard"
|
||||||
|
:placeholder="idPlaceholder"
|
||||||
|
placeholder-class="placeholder"
|
||||||
|
:maxlength="docType === '身份证' ? 18 : 30"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<view class="divider" />
|
||||||
|
|
||||||
|
<!-- 持卡人照片 -->
|
||||||
|
<view class="form-row form-row--photo">
|
||||||
|
<text class="form-label form-label--top">持卡人照片</text>
|
||||||
|
<view class="upload-area" @tap="photoUrl ? previewPhoto() : choosePhoto()">
|
||||||
|
<image v-if="photoUrl" class="upload-preview" :src="photoUrl" mode="aspectFill" />
|
||||||
|
<view v-else class="upload-placeholder">
|
||||||
|
<view class="upload-icon" />
|
||||||
|
<text class="upload-text">点击上传</text>
|
||||||
|
</view>
|
||||||
|
<view v-if="photoUrl" class="upload-remove" @tap.stop="removePhoto">
|
||||||
|
<view class="upload-remove__x" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 3. 协议勾选区域 -->
|
||||||
|
<view class="agreement" @tap="agreed = !agreed">
|
||||||
|
<view class="checkbox" :class="{ 'checkbox--checked': agreed }">
|
||||||
|
<view v-if="agreed" class="checkbox__check" />
|
||||||
|
</view>
|
||||||
|
<view class="agreement__text">
|
||||||
|
<text class="agreement__label">我已阅读并知晓</text>
|
||||||
|
<text class="agreement__link" @tap.stop="showNotice = true">《用卡须知》</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 4. 底部操作按钮组 -->
|
||||||
|
<view class="btn-group">
|
||||||
|
<button
|
||||||
|
class="btn-confirm"
|
||||||
|
:class="{ 'btn-confirm--disabled': submitting }"
|
||||||
|
:disabled="submitting"
|
||||||
|
@tap="handleSubmit"
|
||||||
|
>
|
||||||
|
<text class="btn-confirm__text">{{ submitting ? '提交中...' : '确认绑卡' }}</text>
|
||||||
|
</button>
|
||||||
|
<button class="btn-cancel" @tap="handleCancel">
|
||||||
|
<text class="btn-cancel__text">取消</text>
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 弹窗:证件类型选择 -->
|
||||||
|
<uni-popup ref="idTypePopup" type="bottom" :mask-click="true" @change="(e: any) => showIdTypePicker = e.show">
|
||||||
|
<view class="popup-sheet">
|
||||||
|
<view class="popup-header">
|
||||||
|
<text class="popup-title">选择证件类型</text>
|
||||||
|
<view class="popup-close" @tap="showIdTypePicker = false">
|
||||||
|
<view class="popup-close__icon" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="id-type-list">
|
||||||
|
<view
|
||||||
|
v-for="item in idTypeOptions"
|
||||||
|
:key="item"
|
||||||
|
class="id-type-item"
|
||||||
|
:class="{ 'id-type-item--active': docType === item }"
|
||||||
|
@tap="selectIdType(item)"
|
||||||
|
>
|
||||||
|
<text class="id-type-item__text">{{ item }}</text>
|
||||||
|
<view v-if="docType === item" class="id-type-item__check" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</uni-popup>
|
||||||
|
|
||||||
|
<!-- 弹窗:用卡须知 -->
|
||||||
|
<uni-popup ref="noticePopup" type="bottom" :mask-click="true" @change="(e: any) => showNotice = e.show">
|
||||||
|
<view class="popup-sheet">
|
||||||
|
<view class="popup-header">
|
||||||
|
<text class="popup-title">用卡须知</text>
|
||||||
|
<view class="popup-close" @tap="showNotice = false">
|
||||||
|
<view class="popup-close__icon" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<scroll-view scroll-y class="popup-body">
|
||||||
|
<text class="popup-content">1. 年卡仅限本人使用,不得转借他人。\n2. 入园时需出示有效身份证件及年卡二维码。\n3. 年卡有效期内可不限次数游览合作景区。\n4. 部分景区节假日需提前预约,请以景区公告为准。\n5. 年卡一经激活,不予退换。</text>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</uni-popup>
|
||||||
|
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, watch, computed } from 'vue'
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
cardNo: '',
|
||||||
|
name: '',
|
||||||
|
phone: '18684839400',
|
||||||
|
idCard: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const docType = ref('身份证')
|
||||||
|
const idTypeOptions = ['身份证', '护照', '港澳通行证', '台胞证']
|
||||||
|
|
||||||
|
const idPlaceholder = computed(() => {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
'身份证': '请输入身份证号码',
|
||||||
|
'护照': '请输入护照号码',
|
||||||
|
'港澳通行证': '请输入通行证号码',
|
||||||
|
'台胞证': '请输入台胞证号码'
|
||||||
|
}
|
||||||
|
return map[docType.value] || '请输入证件号码'
|
||||||
|
})
|
||||||
|
|
||||||
|
const photoUrl = ref('')
|
||||||
|
const agreed = ref(false)
|
||||||
|
const submitting = ref(false)
|
||||||
|
const showNotice = ref(false)
|
||||||
|
const showIdTypePicker = ref(false)
|
||||||
|
const noticePopup = ref<any>(null)
|
||||||
|
const idTypePopup = ref<any>(null)
|
||||||
|
|
||||||
|
watch(showNotice, (v) => {
|
||||||
|
if (v) noticePopup.value?.open()
|
||||||
|
else noticePopup.value?.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(showIdTypePicker, (v) => {
|
||||||
|
if (v) idTypePopup.value?.open()
|
||||||
|
else idTypePopup.value?.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
function selectIdType(item: string) {
|
||||||
|
docType.value = item
|
||||||
|
showIdTypePicker.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function choosePhoto() {
|
||||||
|
uni.chooseMedia({
|
||||||
|
count: 1,
|
||||||
|
mediaType: ['image'],
|
||||||
|
sourceType: ['camera'],
|
||||||
|
success: (res) => {
|
||||||
|
photoUrl.value = res.tempFiles[0].tempFilePath
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function previewPhoto() {
|
||||||
|
uni.previewImage({ urls: [photoUrl.value] })
|
||||||
|
}
|
||||||
|
|
||||||
|
function removePhoto() {
|
||||||
|
photoUrl.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateForm() {
|
||||||
|
if (!form.cardNo.trim()) return '请输入卡号'
|
||||||
|
if (!form.name.trim()) return '请输入姓名'
|
||||||
|
if (!form.phone.trim()) return '请输入联系电话'
|
||||||
|
if (!/^1[3-9]\d{9}$/.test(form.phone)) return '手机号格式不正确'
|
||||||
|
if (!form.idCard.trim()) return `请输入${docType.value}号码`
|
||||||
|
if (docType.value === '身份证' && !/^\d{17}[\dXx]$/.test(form.idCard)) return '身份证号格式不正确'
|
||||||
|
if (!photoUrl.value) return '请上传持卡人照片'
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
if (!agreed.value) {
|
||||||
|
uni.showToast({ title: '请先阅读并知晓用卡须知', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const err = validateForm()
|
||||||
|
if (err) {
|
||||||
|
uni.showToast({ title: err, icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
submitting.value = true
|
||||||
|
uni.showLoading({ title: '提交中...', mask: true })
|
||||||
|
|
||||||
|
try {
|
||||||
|
await new Promise(r => setTimeout(r, 1500))
|
||||||
|
uni.hideLoading()
|
||||||
|
uni.showToast({ title: '绑卡成功', icon: 'success' })
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.switchTab({ url: '/pages/index/index' })
|
||||||
|
}, 1500)
|
||||||
|
} catch {
|
||||||
|
uni.hideLoading()
|
||||||
|
uni.showToast({ title: '绑卡失败,请重试', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCancel() {
|
||||||
|
uni.navigateBack({ fail: () => uni.switchTab({ url: '/pages/index/index' }) })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
/* ===== Design Tokens ===== */
|
||||||
|
$font: 'SourceHanSerifCN', '思源宋体', -apple-system, 'PingFang SC', 'Helvetica Neue', sans-serif;
|
||||||
|
$primary: #f9ae3d;
|
||||||
|
$primary-dark: #e89a2a;
|
||||||
|
$accent: #3c9cff;
|
||||||
|
$text-main: #111111;
|
||||||
|
$text-label: #222222;
|
||||||
|
$text-body: #333333;
|
||||||
|
$text-hint: #666666;
|
||||||
|
$text-weak: #999999;
|
||||||
|
$divider: #eeeeee;
|
||||||
|
|
||||||
|
/* ===== Page ===== */
|
||||||
|
.page {
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #ffffff;
|
||||||
|
font-family: $font;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 1. Header ===== */
|
||||||
|
.header {
|
||||||
|
padding: 40rpx 32rpx 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: baseline;
|
||||||
|
line-height: 48rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title__bind {
|
||||||
|
font-family: $font;
|
||||||
|
font-size: 38rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: $text-main;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title__card {
|
||||||
|
font-family: $font;
|
||||||
|
font-size: 38rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: $accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-desc {
|
||||||
|
display: block;
|
||||||
|
font-family: $font;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 400;
|
||||||
|
color: $text-hint;
|
||||||
|
line-height: 36rpx;
|
||||||
|
margin-top: 12rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 2. Form Card ===== */
|
||||||
|
.form-card {
|
||||||
|
margin: 24rpx 24rpx 0;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 88rpx;
|
||||||
|
padding: 0 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row--photo {
|
||||||
|
align-items: flex-start;
|
||||||
|
height: auto;
|
||||||
|
padding: 24rpx 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
font-family: $font;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: $text-label;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 40rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 160rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label--top {
|
||||||
|
padding-top: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
flex: 1;
|
||||||
|
height: 88rpx;
|
||||||
|
font-family: $font;
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 400;
|
||||||
|
color: $text-body;
|
||||||
|
line-height: 40rpx;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder {
|
||||||
|
font-family: $font;
|
||||||
|
color: $text-weak;
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
height: 1rpx;
|
||||||
|
background: $divider;
|
||||||
|
margin: 0 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Form Value + Chevron ===== */
|
||||||
|
.form-value {
|
||||||
|
flex: 1;
|
||||||
|
font-family: $font;
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 400;
|
||||||
|
color: $text-body;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chevron-right {
|
||||||
|
width: 12rpx;
|
||||||
|
height: 12rpx;
|
||||||
|
border-top: 2rpx solid $text-weak;
|
||||||
|
border-right: 2rpx solid $text-weak;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Photo Upload ===== */
|
||||||
|
.upload-area {
|
||||||
|
width: 180rpx;
|
||||||
|
height: 180rpx;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
background: #f7f7f7;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: auto;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
transition: transform 0.15s ease;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.97);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-placeholder {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-icon {
|
||||||
|
width: 60rpx;
|
||||||
|
height: 60rpx;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='60' height='60' viewBox='0 0 24 24' fill='none' stroke='%23999' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2'/%3E%3Ccircle cx='12' cy='7' r='4'/%3E%3C/svg%3E");
|
||||||
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-text {
|
||||||
|
font-family: $font;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: $accent;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-preview {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-remove {
|
||||||
|
position: absolute;
|
||||||
|
top: 8rpx;
|
||||||
|
right: 8rpx;
|
||||||
|
width: 40rpx;
|
||||||
|
height: 40rpx;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-remove__x {
|
||||||
|
width: 16rpx;
|
||||||
|
height: 16rpx;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::before,
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 16rpx;
|
||||||
|
height: 2rpx;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 1rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
transform: translate(-50%, -50%) rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
transform: translate(-50%, -50%) rotate(-45deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 3. Agreement ===== */
|
||||||
|
.agreement {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin: 24rpx 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 20rpx;
|
||||||
|
height: 20rpx;
|
||||||
|
border: 2rpx solid #dadbde;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 12rpx;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox--checked {
|
||||||
|
background: $accent;
|
||||||
|
border-color: $accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox__check {
|
||||||
|
width: 12rpx;
|
||||||
|
height: 12rpx;
|
||||||
|
background-image: 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='white' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E");
|
||||||
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agreement__text {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
line-height: 36rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agreement__label {
|
||||||
|
font-family: $font;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 400;
|
||||||
|
color: $text-hint;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agreement__link {
|
||||||
|
font-family: $font;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 400;
|
||||||
|
color: $accent;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 4. Buttons ===== */
|
||||||
|
.btn-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 24rpx;
|
||||||
|
padding: 0 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-confirm {
|
||||||
|
flex: 1;
|
||||||
|
height: 96rpx;
|
||||||
|
border-radius: 48rpx;
|
||||||
|
background: #398ade;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
line-height: normal;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 6rpx 20rpx rgba(57, 138, 222, 0.28);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:not(.btn-confirm--disabled) {
|
||||||
|
transform: scale(0.98);
|
||||||
|
box-shadow: 0 2rpx 8rpx rgba(57, 138, 222, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-confirm--disabled {
|
||||||
|
background: #e5e7eb;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-confirm__text {
|
||||||
|
font-family: $font;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #ffffff;
|
||||||
|
letter-spacing: 2rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-confirm--disabled .btn-confirm__text {
|
||||||
|
color: $text-weak;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel {
|
||||||
|
flex: 1;
|
||||||
|
height: 96rpx;
|
||||||
|
border-radius: 48rpx;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 2rpx solid #dadbde;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel__text {
|
||||||
|
font-family: $font;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 400;
|
||||||
|
color: $text-hint;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Popup Sheet ===== */
|
||||||
|
.popup-sheet {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 24rpx 24rpx 0 0;
|
||||||
|
max-height: 70vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-family: $font;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 32rpx;
|
||||||
|
border-bottom: 1rpx solid $divider;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-title {
|
||||||
|
font-family: $font;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: $text-main;
|
||||||
|
line-height: 48rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-close {
|
||||||
|
width: 48rpx;
|
||||||
|
height: 48rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-close__icon {
|
||||||
|
width: 20rpx;
|
||||||
|
height: 20rpx;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::before,
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 20rpx;
|
||||||
|
height: 3rpx;
|
||||||
|
background: $text-hint;
|
||||||
|
border-radius: 2rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
transform: translate(-50%, -50%) rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
transform: translate(-50%, -50%) rotate(-45deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-body {
|
||||||
|
padding: 32rpx;
|
||||||
|
max-height: 55vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-content {
|
||||||
|
font-family: $font;
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 400;
|
||||||
|
color: $text-body;
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== ID Type Picker List ===== */
|
||||||
|
.id-type-list {
|
||||||
|
padding: 16rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.id-type-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 96rpx;
|
||||||
|
padding: 0 32rpx;
|
||||||
|
transition: background 0.15s ease;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--active {
|
||||||
|
background: rgba(60, 156, 255, 0.04);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.id-type-item__text {
|
||||||
|
font-family: $font;
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 400;
|
||||||
|
color: $text-body;
|
||||||
|
line-height: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.id-type-item--active .id-type-item__text {
|
||||||
|
color: $accent;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.id-type-item__check {
|
||||||
|
width: 24rpx;
|
||||||
|
height: 24rpx;
|
||||||
|
background-image: 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='%233c9cff' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E");
|
||||||
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page">
|
||||||
|
<!-- 图片区域 -->
|
||||||
|
<view class="image-section">
|
||||||
|
<image
|
||||||
|
class="card-image"
|
||||||
|
src="https://aliyunoss.hntgov.net/image/2026/06/23/70e9f4b3b5fd49adb1ff06edcb8fa032.jpg"
|
||||||
|
mode="widthFix"
|
||||||
|
/>
|
||||||
|
<image
|
||||||
|
class="card-image"
|
||||||
|
src="https://aliyunoss.hntgov.net/image/2026/06/25/f3e45bcb3b4f4a2997f107cc6335a1bc.jpg"
|
||||||
|
mode="widthFix"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 底部固定购买区域 -->
|
||||||
|
<view class="bottom-bar">
|
||||||
|
<view class="price-row">
|
||||||
|
<text class="price-label">价格:</text>
|
||||||
|
<text class="price-symbol">¥</text>
|
||||||
|
<text class="price-value">198</text>
|
||||||
|
<text class="price-original">¥3800+</text>
|
||||||
|
</view>
|
||||||
|
<button class="btn-buy" :disabled="paying" @tap="handleBuy">
|
||||||
|
<text class="btn-buy__text">{{ paying ? '支付中...' : '立即购买' }}</text>
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const paying = ref(false)
|
||||||
|
|
||||||
|
async function handleBuy() {
|
||||||
|
if (paying.value) return
|
||||||
|
paying.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 调用后端创建订单接口(mock)
|
||||||
|
const orderRes: any = await new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve({
|
||||||
|
orderId: 'ORD' + Date.now(),
|
||||||
|
payParams: {
|
||||||
|
timeStamp: String(Math.floor(Date.now() / 1000)),
|
||||||
|
nonceStr: 'mock_nonce_' + Date.now(),
|
||||||
|
package: 'prepay_id=mock_prepay_id',
|
||||||
|
signType: 'MD5',
|
||||||
|
paySign: 'mock_pay_sign'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, 800)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 调用微信支付
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
uni.requestPayment({
|
||||||
|
provider: 'wxpay',
|
||||||
|
...orderRes.payParams,
|
||||||
|
success: () => resolve(),
|
||||||
|
fail: (err: any) => reject(err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
uni.showToast({ title: '支付成功', icon: 'success' })
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.switchTab({ url: '/pages/index/index' })
|
||||||
|
}, 1500)
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err?.errMsg?.includes('cancel')) {
|
||||||
|
uni.showToast({ title: '已取消支付', icon: 'none' })
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: '支付失败,请重试', icon: 'none' })
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
paying.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
$font: 'SourceHanSerifCN', '思源宋体', -apple-system, 'PingFang SC', 'Helvetica Neue', sans-serif;
|
||||||
|
$primary: #398ade;
|
||||||
|
$text-main: #111111;
|
||||||
|
$text-hint: #666666;
|
||||||
|
$text-weak: #999999;
|
||||||
|
$bar-height: 120rpx;
|
||||||
|
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #ffffff;
|
||||||
|
padding-bottom: calc(#{$bar-height} + env(safe-area-inset-bottom));
|
||||||
|
font-family: $font;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 图片区域 ===== */
|
||||||
|
.image-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-image {
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 底部固定购买区域 ===== */
|
||||||
|
.bottom-bar {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: $bar-height;
|
||||||
|
background: #ffffff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 0rpx 0 178rpx;
|
||||||
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
|
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.06);
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-label {
|
||||||
|
font-family: $font;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: $text-hint;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-symbol {
|
||||||
|
font-family: $font;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #e53935;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-value {
|
||||||
|
font-family: $font;
|
||||||
|
font-size: 48rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #e53935;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-original {
|
||||||
|
font-family: $font;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: $text-weak;
|
||||||
|
text-decoration: line-through;
|
||||||
|
margin-left: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-buy {
|
||||||
|
width: auto;
|
||||||
|
min-width: auto;
|
||||||
|
height: 80rpx;
|
||||||
|
padding: 0 56rpx;
|
||||||
|
border-radius: 40rpx;
|
||||||
|
background: $primary;
|
||||||
|
border: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 4rpx 16rpx rgba(57, 138, 222, 0.3);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:not([disabled]) {
|
||||||
|
transform: scale(0.97);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[disabled] {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-buy__text {
|
||||||
|
font-family: $font;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ffffff;
|
||||||
|
letter-spacing: 2rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,374 @@
|
|||||||
|
<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>
|
||||||
|
$font: 'SourceHanSerifCN', '思源宋体', -apple-system, 'PingFang SC', 'Helvetica Neue', sans-serif;
|
||||||
|
$primary: #3c9cff;
|
||||||
|
$text-main: #111111;
|
||||||
|
$text-label: #222222;
|
||||||
|
$text-body: #333333;
|
||||||
|
$text-hint: #666666;
|
||||||
|
$divider: #eeeeee;
|
||||||
|
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f7f8fa;
|
||||||
|
padding: 48rpx 24rpx 0;
|
||||||
|
font-family: $font;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 分类区块 ===== */
|
||||||
|
.category {
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-title {
|
||||||
|
font-family: $font;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: $text-main;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-divider {
|
||||||
|
height: 1rpx;
|
||||||
|
background: $divider;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 问答卡片 ===== */
|
||||||
|
.faq-card {
|
||||||
|
background: #ffffff;
|
||||||
|
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: #f9f9f9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: $primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-question {
|
||||||
|
flex: 1;
|
||||||
|
font-family: $font;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
color: $text-label;
|
||||||
|
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;
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 400;
|
||||||
|
color: $text-body;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-highlight {
|
||||||
|
font-family: $font;
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 400;
|
||||||
|
color: $primary;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 底部安全区 ===== */
|
||||||
|
.safe-bottom {
|
||||||
|
height: 64rpx;
|
||||||
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="page">
|
<view class="page">
|
||||||
<!-- Hero: 运营Banner轮播 -->
|
<!-- Hero: 运营Banner轮播 -->
|
||||||
<view class="hero animate-fade-in-up" @click="goCardDetail()">
|
<view class="hero animate-fade-in-up" @click="goUnionBuy()">
|
||||||
<swiper class="hero-swiper" autoplay circular :interval="5000">
|
<swiper class="hero-swiper" autoplay circular :interval="5000">
|
||||||
<swiper-item>
|
<swiper-item>
|
||||||
<image
|
<image
|
||||||
@@ -153,6 +153,22 @@ const quickActions = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
function onQuickAction(item: { label: string }) {
|
function onQuickAction(item: { label: string }) {
|
||||||
|
if (item.label === '我要绑卡') {
|
||||||
|
uni.navigateTo({ url: '/pages/card-bind/card-bind' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (item.label === '我要预约') {
|
||||||
|
uni.navigateTo({ url: '/pages/scenic-list/scenic-list' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (item.label === '我要入园') {
|
||||||
|
uni.switchTab({ url: '/pages/qrcode/index' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (item.label === '常见问题') {
|
||||||
|
uni.navigateTo({ url: '/pages/faq/faq' })
|
||||||
|
return
|
||||||
|
}
|
||||||
uni.showToast({ title: `${item.label}功能开发中`, icon: 'none' })
|
uni.showToast({ title: `${item.label}功能开发中`, icon: 'none' })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,7 +214,11 @@ const hotSpots = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
function goCardDetail() {
|
function goCardDetail() {
|
||||||
uni.navigateTo({ url: '/packages/user/pages/card/list' })
|
uni.navigateTo({ url: '/pages/card-buy/card-buy' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function goUnionBuy() {
|
||||||
|
uni.navigateTo({ url: '/pages/union-buy/union-buy' })
|
||||||
}
|
}
|
||||||
|
|
||||||
function goSpot(spot: ScenicSpotItem) {
|
function goSpot(spot: ScenicSpotItem) {
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page">
|
||||||
|
<view class="content">
|
||||||
|
<text class="title">服务条款</text>
|
||||||
|
<text class="update-time">更新日期:2025年6月1日</text>
|
||||||
|
|
||||||
|
<view class="section">
|
||||||
|
<text class="section-title">一、总则</text>
|
||||||
|
<text class="section-body">欢迎您使用爱上萍乡小程序(以下简称"本平台")。本服务条款是您与本平台之间关于使用本平台服务所订立的协议。请您在使用本平台服务前,仔细阅读并充分理解本条款内容。</text>
|
||||||
|
<text class="section-body">如您不同意本条款的任何内容,请勿使用本平台服务。您一旦使用本平台服务,即视为您已充分理解并同意本条款全部内容。</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="section">
|
||||||
|
<text class="section-title">二、服务内容</text>
|
||||||
|
<text class="section-body">本平台为用户提供文旅年卡浏览、购买、激活、绑定及入园码展示等相关服务。具体服务内容以平台实际提供为准。</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="section">
|
||||||
|
<text class="section-title">三、用户注册与账户安全</text>
|
||||||
|
<text class="section-body">您通过微信授权登录即完成注册。您应妥善保管账户信息及登录凭证,因您个人原因导致的信息泄露或损失,由您自行承担。</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="section">
|
||||||
|
<text class="section-title">四、用户行为规范</text>
|
||||||
|
<text class="section-body">您在使用本平台服务时,应遵守中华人民共和国相关法律法规,不得利用本平台从事任何违法违规活动,不得干扰本平台的正常运行。</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="section">
|
||||||
|
<text class="section-title">五、知识产权</text>
|
||||||
|
<text class="section-body">本平台的所有内容,包括但不限于文字、图片、界面设计、商标等,均受知识产权法律保护,未经许可不得擅自使用。</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="section">
|
||||||
|
<text class="section-title">六、免责声明</text>
|
||||||
|
<text class="section-body">因不可抗力或非本平台原因导致的服务中断或数据丢失,本平台不承担责任。本平台有权根据需要修改服务条款,修改后的条款将在平台公布后生效。</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="section">
|
||||||
|
<text class="section-title">七、条款修改</text>
|
||||||
|
<text class="section-body">本平台有权根据需要不时地制定、修改本服务条款,修改后的条款将被公布在本平台上。如您继续使用本平台服务,即视为您已接受经修订的条款。</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #FFFFFF;
|
||||||
|
padding: $spacing-6 $spacing-6 $spacing-12;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
display: block;
|
||||||
|
font-family: $font-family-display;
|
||||||
|
font-size: $font-size-2xl;
|
||||||
|
font-weight: $font-weight-bold;
|
||||||
|
color: $uv-main-color;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: $spacing-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-time {
|
||||||
|
display: block;
|
||||||
|
font-size: $font-size-sm;
|
||||||
|
color: $uv-tips-color;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: $spacing-8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
margin-bottom: $spacing-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
display: block;
|
||||||
|
font-size: $font-size-lg;
|
||||||
|
font-weight: $font-weight-semibold;
|
||||||
|
color: $uv-main-color;
|
||||||
|
margin-bottom: $spacing-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-body {
|
||||||
|
display: block;
|
||||||
|
font-size: $font-size-base;
|
||||||
|
color: $uv-content-color;
|
||||||
|
line-height: $line-height-relaxed;
|
||||||
|
margin-bottom: $spacing-2;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+297
-85
@@ -1,42 +1,99 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="container">
|
<view class="login-page">
|
||||||
<view class="header">
|
<!-- Brand area -->
|
||||||
<image class="logo" src="/static/logo.png" mode="aspectFit" />
|
<view class="brand" :class="{ 'brand--visible': showContent }">
|
||||||
<text class="app-name">湖南12301文旅专区</text>
|
<image class="brand-logo" src="/static/logo.png" mode="aspectFit" />
|
||||||
<text class="app-desc">年卡购买 · 景区预约 · 权益查看</text>
|
<text class="brand-name">爱上萍乡</text>
|
||||||
|
<text class="brand-desc">赣湘之间,发现旅途美好</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="login-area">
|
<!-- Actions: 2 buttons -->
|
||||||
<button class="btn-wx-login"
|
<view class="actions" :class="{ 'actions--visible': showContent }">
|
||||||
open-type="getPhoneNumber"
|
<!-- 未同意条款:普通按钮,点击提示 -->
|
||||||
@getphonenumber="handleGetPhoneNumber"
|
<button
|
||||||
:disabled="loading">
|
v-if="!agreed"
|
||||||
{{ loading ? '登录中...' : '微信一键登录' }}
|
class="btn-primary"
|
||||||
|
:class="{ 'btn-primary--disabled': loading }"
|
||||||
|
:disabled="loading"
|
||||||
|
@tap="showToast('请先阅读并同意服务条款和隐私政策')"
|
||||||
|
>
|
||||||
|
<view class="btn-primary__inner">
|
||||||
|
<view class="wx-icon" />
|
||||||
|
<text class="btn-primary__text">微信一键登录</text>
|
||||||
|
</view>
|
||||||
|
</button>
|
||||||
|
<!-- 已同意条款:微信授权按钮 -->
|
||||||
|
<button
|
||||||
|
v-else
|
||||||
|
class="btn-primary"
|
||||||
|
:class="{ 'btn-primary--disabled': loading }"
|
||||||
|
open-type="getPhoneNumber"
|
||||||
|
@getphonenumber="handleLogin"
|
||||||
|
:disabled="loading"
|
||||||
|
>
|
||||||
|
<view v-if="!loading" class="btn-primary__inner">
|
||||||
|
<view class="wx-icon" />
|
||||||
|
<text class="btn-primary__text">微信一键登录</text>
|
||||||
|
</view>
|
||||||
|
<view v-else class="btn-primary__inner">
|
||||||
|
<view class="loading-spinner" />
|
||||||
|
<text class="btn-primary__text">登录中...</text>
|
||||||
|
</view>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="btn-cancel" @tap="handleCancel">
|
||||||
|
<text class="btn-cancel__text">取消</text>
|
||||||
</button>
|
</button>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="agreement">
|
<!-- Agreement: checkbox + links -->
|
||||||
<text class="agreement-text">登录即表示同意</text>
|
<view class="agreement" :class="{ 'agreement--visible': showContent }">
|
||||||
<text class="agreement-link">《用户协议》</text>
|
<view
|
||||||
<text class="agreement-text">和</text>
|
class="checkbox"
|
||||||
<text class="agreement-link">《隐私政策》</text>
|
:class="{ 'checkbox--checked': agreed }"
|
||||||
|
@tap="agreed = !agreed"
|
||||||
|
>
|
||||||
|
<view v-if="agreed" class="checkbox__check" />
|
||||||
|
</view>
|
||||||
|
<view class="agreement__text">
|
||||||
|
<text class="agreement__label">我已阅读并同意</text>
|
||||||
|
<text class="agreement__link" @tap="openAgreement">《服务条款》</text>
|
||||||
|
<text class="agreement__link" @tap="openPrivacy">《隐私政策》</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { useUserStore } from '../../store/user'
|
import { useUserStore } from '../../store/user'
|
||||||
import { showToast } from '../../utils'
|
import { showToast } from '../../utils'
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
const agreed = ref(false)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
const showContent = ref(false)
|
||||||
|
|
||||||
/** 微信手机号授权回调 → 登录 */
|
onMounted(() => {
|
||||||
async function handleGetPhoneNumber(e: any) {
|
setTimeout(() => {
|
||||||
// 用户拒绝授权手机号
|
showContent.value = true
|
||||||
|
}, 80)
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleCancel() {
|
||||||
|
uni.navigateBack({ fail: () => uni.switchTab({ url: '/pages/index/index' }) })
|
||||||
|
}
|
||||||
|
|
||||||
|
function openAgreement() {
|
||||||
|
uni.navigateTo({ url: '/pages/login/agreement' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function openPrivacy() {
|
||||||
|
uni.navigateTo({ url: '/pages/login/privacy' })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleLogin(e: any) {
|
||||||
if (e.detail.errMsg !== 'getPhoneNumber:ok') {
|
if (e.detail.errMsg !== 'getPhoneNumber:ok') {
|
||||||
showToast('需要手机号授权才能登录')
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,20 +105,18 @@ async function handleGetPhoneNumber(e: any) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取微信登录 code(与 phoneCode 是完全不同的凭证)
|
|
||||||
const loginRes: any = await new Promise((resolve, reject) => {
|
const loginRes: any = await new Promise((resolve, reject) => {
|
||||||
uni.login({ provider: 'weixin', success: resolve, fail: reject })
|
uni.login({ provider: 'weixin', success: resolve, fail: reject })
|
||||||
})
|
})
|
||||||
|
|
||||||
// 防御性检查:loginCode 不能为空
|
|
||||||
if (!loginRes || !loginRes.code) {
|
if (!loginRes || !loginRes.code) {
|
||||||
showToast('微信登录失败,请重试')
|
showToast('微信登录失败,请重试')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await userStore.wxLogin({
|
await userStore.wxLogin({
|
||||||
loginCode: loginRes.code, // wx.login() 返回的 code
|
loginCode: loginRes.code,
|
||||||
phoneCode: phoneCode // getPhoneNumber 返回的 code
|
phoneCode: phoneCode
|
||||||
})
|
})
|
||||||
|
|
||||||
showToast('登录成功', 'success')
|
showToast('登录成功', 'success')
|
||||||
@@ -76,75 +131,232 @@ async function handleGetPhoneNumber(e: any) {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style lang="scss" scoped>
|
||||||
.container {
|
/* Lucide icons — data URI pattern (matches scenic-detail) */
|
||||||
display: flex;
|
$icon-size: 40rpx;
|
||||||
flex-direction: column;
|
$icon-wechat: 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='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M7.9 20A9 9 0 1 0 4 16.1L2 22Z'/%3E%3C/svg%3E");
|
||||||
min-height: 100vh;
|
$icon-check: 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='white' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E");
|
||||||
padding: 0 60rpx;
|
|
||||||
box-sizing: border-box;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
.login-page {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding-top: 160rpx;
|
|
||||||
margin-bottom: 80rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
width: 140rpx;
|
|
||||||
height: 140rpx;
|
|
||||||
margin-bottom: 30rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-name {
|
|
||||||
font-size: 44rpx;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #333;
|
|
||||||
margin-bottom: 16rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-desc {
|
|
||||||
font-size: 26rpx;
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-area {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-wx-login {
|
|
||||||
width: 100%;
|
|
||||||
height: 96rpx;
|
|
||||||
line-height: 96rpx;
|
|
||||||
background: #07C160;
|
|
||||||
color: #fff;
|
|
||||||
border-radius: 48rpx;
|
|
||||||
font-size: 32rpx;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-wx-login[disabled] {
|
|
||||||
background: #9ED6B5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.agreement {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
height: 100vh;
|
||||||
|
background: #FFFFFF;
|
||||||
|
padding: 0 48rpx;
|
||||||
|
gap: 48rpx;
|
||||||
|
overflow-x: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== Brand ========== */
|
||||||
|
.brand {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20rpx);
|
||||||
|
transition: opacity 0.6s ease, transform 0.6s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand--visible {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-logo {
|
||||||
|
width: 160rpx;
|
||||||
|
height: 160rpx;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-name {
|
||||||
|
font-family: $font-family-display;
|
||||||
|
font-size: 56rpx;
|
||||||
|
font-weight: $font-weight-bold;
|
||||||
|
color: $uv-main-color;
|
||||||
|
line-height: $line-height-tight;
|
||||||
|
letter-spacing: 4rpx;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-desc {
|
||||||
|
font-size: $font-size-base;
|
||||||
|
color: $uv-tips-color;
|
||||||
|
font-weight: $font-weight-normal;
|
||||||
|
letter-spacing: 2rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== Actions ========== */
|
||||||
|
.actions {
|
||||||
|
width: 100%;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20rpx);
|
||||||
|
transition: opacity 0.6s ease 0.1s, transform 0.6s ease 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions--visible {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
width: 100%;
|
||||||
|
height: 92rpx;
|
||||||
|
border-radius: $radius-xl;
|
||||||
|
background: $uv-primary;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0 0 24rpx 0;
|
||||||
|
line-height: normal;
|
||||||
|
box-shadow: 0 6rpx 20rpx rgba(60, 156, 255, 0.28);
|
||||||
|
transition: all $transition-base;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary--disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:active:not(.btn-primary--disabled) {
|
||||||
|
transform: scale(0.98);
|
||||||
|
box-shadow: 0 2rpx 8rpx rgba(60, 156, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary__inner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 92rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary__text {
|
||||||
|
font-size: $font-size-lg;
|
||||||
|
font-weight: $font-weight-semibold;
|
||||||
|
color: #FFFFFF;
|
||||||
|
margin-left: $spacing-2;
|
||||||
|
letter-spacing: 2rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wx-icon {
|
||||||
|
width: $icon-size;
|
||||||
|
height: $icon-size;
|
||||||
|
background-image: $icon-wechat;
|
||||||
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
width: 36rpx;
|
||||||
|
height: 36rpx;
|
||||||
|
border: 4rpx solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-top-color: #FFFFFF;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel {
|
||||||
|
width: 100%;
|
||||||
|
height: 92rpx;
|
||||||
|
border-radius: $radius-xl;
|
||||||
|
background: #FFFFFF;
|
||||||
|
border: 2rpx solid $uv-border-color;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 92rpx;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
|
||||||
|
transition: all $transition-base;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
background: $uv-bg-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel__text {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
font-size: $font-size-lg;
|
||||||
|
font-weight: $font-weight-medium;
|
||||||
|
color: $uv-content-color;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== Agreement ========== */
|
||||||
|
.agreement {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20rpx);
|
||||||
|
transition: opacity 0.6s ease 0.2s, transform 0.6s ease 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agreement--visible {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 40rpx;
|
||||||
|
height: 40rpx;
|
||||||
|
border: 3rpx solid $uv-border-color;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
margin-right: $spacing-2;
|
||||||
|
margin-top: 2rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all $transition-base;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox--checked {
|
||||||
|
background: $uv-primary;
|
||||||
|
border-color: $uv-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox__check {
|
||||||
|
width: 24rpx;
|
||||||
|
height: 24rpx;
|
||||||
|
background-image: $icon-check;
|
||||||
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agreement__text {
|
||||||
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
padding: 40rpx 0 60rpx;
|
align-items: center;
|
||||||
|
line-height: $line-height-normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.agreement-text {
|
.agreement__label {
|
||||||
font-size: 22rpx;
|
font-size: $font-size-base;
|
||||||
color: #999;
|
color: $uv-content-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.agreement-link {
|
.agreement__link {
|
||||||
font-size: 22rpx;
|
font-size: $font-size-base;
|
||||||
color: #4A90D9;
|
color: $uv-primary;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page">
|
||||||
|
<view class="content">
|
||||||
|
<text class="title">隐私条款</text>
|
||||||
|
<text class="update-time">更新日期:2025年6月1日</text>
|
||||||
|
|
||||||
|
<view class="section">
|
||||||
|
<text class="section-title">一、信息收集</text>
|
||||||
|
<text class="section-body">为向您提供服务,我们可能会收集以下信息:微信授权登录信息(OpenID)、手机号码、年卡购买及激活信息。我们仅收集为提供服务所必需的最少信息。</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="section">
|
||||||
|
<text class="section-title">二、信息使用</text>
|
||||||
|
<text class="section-body">我们收集的信息将用于:验证您的身份、处理年卡的购买与激活、提供入园码服务、发送服务通知。我们不会将您的个人信息用于与上述目的无关的其他用途。</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="section">
|
||||||
|
<text class="section-title">三、信息存储</text>
|
||||||
|
<text class="section-body">您的个人信息将存储在中华人民共和国境内的安全服务器上。我们采用业界标准的加密技术保护数据传输过程,并采取严格的数据访问控制措施。</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="section">
|
||||||
|
<text class="section-title">四、信息共享</text>
|
||||||
|
<text class="section-body">未经您的同意,我们不会向第三方共享、转让或公开披露您的个人信息,法律法规另有规定的除外。</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="section">
|
||||||
|
<text class="section-title">五、信息安全</text>
|
||||||
|
<text class="section-body">我们采取符合业界标准的安全防护措施来保护您的个人信息,防止数据未经授权的访问、公开披露、使用、修改、损坏或丢失。</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="section">
|
||||||
|
<text class="section-title">六、您的权利</text>
|
||||||
|
<text class="section-body">您有权访问、更正、删除您的个人信息。如需行使上述权利,请通过平台客服渠道联系我们,我们将在合理期限内予以处理。</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="section">
|
||||||
|
<text class="section-title">七、未成年人保护</text>
|
||||||
|
<text class="section-body">如您是未满18周岁的未成年人,请在您的监护人陪同下阅读本隐私条款,并在取得监护人对您使用本平台服务的同意后,方可使用本平台服务。</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="section">
|
||||||
|
<text class="section-title">八、条款更新</text>
|
||||||
|
<text class="section-body">我们可能不时更新本隐私条款。更新后的条款将在平台上公布,建议您定期查阅。如您在隐私条款更新后继续使用本平台服务,即视为您同意更新后的隐私条款。</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #FFFFFF;
|
||||||
|
padding: $spacing-6 $spacing-6 $spacing-12;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
display: block;
|
||||||
|
font-family: $font-family-display;
|
||||||
|
font-size: $font-size-2xl;
|
||||||
|
font-weight: $font-weight-bold;
|
||||||
|
color: $uv-main-color;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: $spacing-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-time {
|
||||||
|
display: block;
|
||||||
|
font-size: $font-size-sm;
|
||||||
|
color: $uv-tips-color;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: $spacing-8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
margin-bottom: $spacing-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
display: block;
|
||||||
|
font-size: $font-size-lg;
|
||||||
|
font-weight: $font-weight-semibold;
|
||||||
|
color: $uv-main-color;
|
||||||
|
margin-bottom: $spacing-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-body {
|
||||||
|
display: block;
|
||||||
|
font-size: $font-size-base;
|
||||||
|
color: $uv-content-color;
|
||||||
|
line-height: $line-height-relaxed;
|
||||||
|
margin-bottom: $spacing-2;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,600 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page">
|
||||||
|
<!-- 模块1:订单状态卡片 -->
|
||||||
|
<view class="status-card" :class="`status-card--${order.status}`">
|
||||||
|
<view class="status-icon-wrap">
|
||||||
|
<view class="status-icon" :class="`status-icon--${order.status}`" />
|
||||||
|
</view>
|
||||||
|
<text class="status-title">{{ statusLabel }}</text>
|
||||||
|
<text class="status-desc">{{ statusDesc }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 模块2:商品信息卡片 -->
|
||||||
|
<view class="info-card">
|
||||||
|
<text class="product-name">超级玩家卡</text>
|
||||||
|
<view class="price-row">
|
||||||
|
<view class="price-current">
|
||||||
|
<text class="price-symbol">¥</text>
|
||||||
|
<text class="price-value">198</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<text class="product-desc">一卡全年畅玩赣湘合作景区商户,实名绑定使用</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 模块3:订单信息明细 -->
|
||||||
|
<view class="info-card">
|
||||||
|
<view class="detail-row">
|
||||||
|
<text class="detail-label">订单编号</text>
|
||||||
|
<text class="detail-value">{{ order.id }}</text>
|
||||||
|
</view>
|
||||||
|
<view v-if="order.status === 'pending' || order.status === 'activated'" class="detail-row">
|
||||||
|
<text class="detail-label">卡号</text>
|
||||||
|
<view class="detail-value-row">
|
||||||
|
<text class="detail-value detail-value--code">{{ order.redeemCode }}</text>
|
||||||
|
<view v-if="order.status === 'pending'" class="copy-btn" @tap="copyCode">
|
||||||
|
<text class="copy-btn__text">复制</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="detail-row">
|
||||||
|
<text class="detail-label">下单时间</text>
|
||||||
|
<text class="detail-value">{{ order.createTime }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="detail-row">
|
||||||
|
<text class="detail-label">支付时间</text>
|
||||||
|
<text class="detail-value">{{ order.payTime }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="detail-row">
|
||||||
|
<text class="detail-label">支付渠道</text>
|
||||||
|
<text class="detail-value">{{ order.payChannel }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="detail-row detail-row--last">
|
||||||
|
<text class="detail-label">支付金额</text>
|
||||||
|
<text class="detail-value detail-value--price">¥{{ order.payAmount }}.00</text>
|
||||||
|
</view>
|
||||||
|
<view v-if="order.status === 'activated' && order.activatedTime" class="detail-row detail-row--last">
|
||||||
|
<text class="detail-label">绑定时间</text>
|
||||||
|
<text class="detail-value">{{ order.activatedTime }}</text>
|
||||||
|
</view>
|
||||||
|
<view v-if="order.status === 'refunded' && order.refundedTime" class="detail-row detail-row--last">
|
||||||
|
<text class="detail-label">退款时间</text>
|
||||||
|
<text class="detail-value">{{ order.refundedTime }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 模块4:使用提示卡片 -->
|
||||||
|
<view class="tips-card">
|
||||||
|
<text class="tips-title">使用须知</text>
|
||||||
|
<view class="tips-list">
|
||||||
|
<view class="tips-item">
|
||||||
|
<view class="tips-dot" />
|
||||||
|
<text class="tips-text">卡一经绑定或售出满7天不支持退款</text>
|
||||||
|
</view>
|
||||||
|
<view class="tips-item">
|
||||||
|
<view class="tips-dot" />
|
||||||
|
<text class="tips-text">实名绑定大陆身份证,港澳台/外籍无法绑定</text>
|
||||||
|
</view>
|
||||||
|
<view class="tips-item">
|
||||||
|
<view class="tips-dot" />
|
||||||
|
<text class="tips-text">预约需提前1天,未提前取消将扣除使用次数</text>
|
||||||
|
</view>
|
||||||
|
<view class="tips-item">
|
||||||
|
<view class="tips-dot" />
|
||||||
|
<text class="tips-text">一卡一人不可转借</text>
|
||||||
|
</view>
|
||||||
|
<view class="tips-item">
|
||||||
|
<view class="tips-dot" />
|
||||||
|
<text class="tips-text">第三方平台下单退款需联系对应平台客服</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="tips-link" @tap="goFaq">
|
||||||
|
<text class="tips-link__text">查看完整FAQ</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 底部安全区占位 -->
|
||||||
|
<view class="safe-bottom" />
|
||||||
|
|
||||||
|
<!-- 模块5:底部固定操作栏 -->
|
||||||
|
<view class="bottom-bar">
|
||||||
|
<view v-if="order.status === 'pending'" class="bottom-bar__right">
|
||||||
|
<view class="btn-action btn-action--outline" @tap="handleRefund">
|
||||||
|
<text class="btn-action__text--outline">申请退款</text>
|
||||||
|
</view>
|
||||||
|
<view class="btn-action btn-action--primary" @tap="goActivate">
|
||||||
|
<text class="btn-action__text">立即绑定</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-else class="bottom-bar__right">
|
||||||
|
<view class="btn-action btn-action--outline" @tap="goHome">
|
||||||
|
<text class="btn-action__text--outline">返回首页</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
|
|
||||||
|
type OrderStatus = 'pending' | 'activated' | 'refunded' | 'expired'
|
||||||
|
|
||||||
|
interface OrderDetail {
|
||||||
|
id: string
|
||||||
|
status: OrderStatus
|
||||||
|
createTime: string
|
||||||
|
payTime: string
|
||||||
|
payAmount: number
|
||||||
|
redeemCode: string
|
||||||
|
payChannel: string
|
||||||
|
activatedTime?: string
|
||||||
|
refundedTime?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const order = ref<OrderDetail>({
|
||||||
|
id: 'ORD20260625001',
|
||||||
|
status: 'pending',
|
||||||
|
createTime: '2026-06-25 10:32',
|
||||||
|
payTime: '2026-06-25 10:33',
|
||||||
|
payAmount: 198,
|
||||||
|
redeemCode: 'CJ0000012398',
|
||||||
|
payChannel: '微信支付',
|
||||||
|
activatedTime: undefined,
|
||||||
|
refundedTime: undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock 订单数据集
|
||||||
|
const mockOrders: OrderDetail[] = [
|
||||||
|
{ id: 'ORD20260625001', status: 'pending', createTime: '2026-06-25 10:32', payTime: '2026-06-25 10:33', payAmount: 198, redeemCode: 'CJ0000012398', payChannel: '微信支付' },
|
||||||
|
{ id: 'ORD20260624002', status: 'activated', createTime: '2026-06-24 14:18', payTime: '2026-06-24 14:19', payAmount: 198, redeemCode: 'CJ0000012399', payChannel: '微信支付', activatedTime: '2026-06-24 15:00' },
|
||||||
|
{ id: 'ORD20260623003', status: 'pending', createTime: '2026-06-23 09:45', payTime: '2026-06-23 09:46', payAmount: 198, redeemCode: 'CJ0000012400', payChannel: '微信支付' },
|
||||||
|
{ id: 'ORD20260620004', status: 'refunded', createTime: '2026-06-20 16:22', payTime: '2026-06-20 16:23', payAmount: 198, redeemCode: 'CJ0000012401', payChannel: '微信支付', refundedTime: '2026-06-21 10:15' },
|
||||||
|
{ id: 'ORD20260618005', status: 'activated', createTime: '2026-06-18 11:08', payTime: '2026-06-18 11:09', payAmount: 198, redeemCode: 'CJ0000012402', payChannel: '微信支付', activatedTime: '2026-06-18 12:30' },
|
||||||
|
{ id: 'ORD20260510007', status: 'activated', createTime: '2026-05-10 13:40', payTime: '2026-05-10 13:41', payAmount: 198, redeemCode: 'CJ0000012403', payChannel: '微信支付', activatedTime: '2026-05-10 14:20' },
|
||||||
|
]
|
||||||
|
|
||||||
|
onLoad((query: any) => {
|
||||||
|
if (query?.orderId) {
|
||||||
|
const found = mockOrders.find(o => o.id === query.orderId)
|
||||||
|
if (found) {
|
||||||
|
order.value = { ...found }
|
||||||
|
} else {
|
||||||
|
order.value.id = query.orderId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const statusLabel = computed(() => {
|
||||||
|
const map: Record<OrderStatus, string> = {
|
||||||
|
pending: '待绑定',
|
||||||
|
activated: '已绑定',
|
||||||
|
refunded: '已退款',
|
||||||
|
expired: '已过期'
|
||||||
|
}
|
||||||
|
return map[order.value.status]
|
||||||
|
})
|
||||||
|
|
||||||
|
const statusDesc = computed(() => {
|
||||||
|
const map: Record<OrderStatus, string> = {
|
||||||
|
pending: '已微信支付,未实名绑定,可绑定或申请退款',
|
||||||
|
activated: '卡已实名绑定,不可退款,仅限本人使用',
|
||||||
|
refunded: '退款审核通过,金额原路返还微信',
|
||||||
|
expired: '下单超过绑定有效期,卡号失效无法绑卡'
|
||||||
|
}
|
||||||
|
return map[order.value.status]
|
||||||
|
})
|
||||||
|
|
||||||
|
function copyCode() {
|
||||||
|
uni.setClipboardData({
|
||||||
|
data: order.value.redeemCode,
|
||||||
|
success: () => uni.showToast({ title: '已复制', icon: 'success' })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function goActivate() {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/card-bind/card-bind?redeemCode=${order.value.redeemCode}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRefund() {
|
||||||
|
uni.showModal({
|
||||||
|
title: '申请退款',
|
||||||
|
content: '赣湘全域旅游年卡/超级玩家卡绑定或销售七日以上不可退卡。确认申请退款?',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
order.value.status = 'refunded'
|
||||||
|
order.value.refundedTime = new Date().toISOString().slice(0, 16).replace('T', ' ')
|
||||||
|
uni.showToast({ title: '退款申请已提交', icon: 'success' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function goFaq() {
|
||||||
|
uni.navigateTo({ url: '/pages/faq/faq' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function goHome() {
|
||||||
|
uni.switchTab({ url: '/pages/index/index' })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
$font: 'SourceHanSerifCN', '思源宋体', -apple-system, 'PingFang SC', 'Helvetica Neue', sans-serif;
|
||||||
|
$primary: #3c9cff;
|
||||||
|
$text-main: #111111;
|
||||||
|
$text-label: #222222;
|
||||||
|
$text-body: #333333;
|
||||||
|
$text-hint: #666666;
|
||||||
|
$text-weak: #999999;
|
||||||
|
$divider: #eeeeee;
|
||||||
|
$card-radius: 24rpx;
|
||||||
|
$bar-height: 120rpx;
|
||||||
|
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f7f8fa;
|
||||||
|
padding: 24rpx;
|
||||||
|
padding-bottom: calc(#{$bar-height} + env(safe-area-inset-bottom) + 24rpx);
|
||||||
|
font-family: $font;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 模块1:状态卡片 ===== */
|
||||||
|
.status-card {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: $card-radius;
|
||||||
|
padding: 48rpx 32rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
|
||||||
|
|
||||||
|
&--pending { border-top: 6rpx solid #ff9500; }
|
||||||
|
&--activated { border-top: 6rpx solid #00b42a; }
|
||||||
|
&--refunded { border-top: 6rpx solid #86909c; }
|
||||||
|
&--expired { border-top: 6rpx solid #f53f3f; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-icon-wrap {
|
||||||
|
width: 80rpx;
|
||||||
|
height: 80rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card--pending .status-icon-wrap { background: rgba(255, 149, 0, 0.1); }
|
||||||
|
.status-card--activated .status-icon-wrap { background: rgba(0, 180, 42, 0.1); }
|
||||||
|
.status-card--refunded .status-icon-wrap { background: rgba(134, 144, 156, 0.1); }
|
||||||
|
.status-card--expired .status-icon-wrap { background: rgba(245, 63, 63, 0.1); }
|
||||||
|
|
||||||
|
.status-icon {
|
||||||
|
width: 36rpx;
|
||||||
|
height: 36rpx;
|
||||||
|
|
||||||
|
&--pending {
|
||||||
|
border: 4rpx solid #ff9500;
|
||||||
|
border-radius: 50%;
|
||||||
|
position: relative;
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 4rpx;
|
||||||
|
height: 16rpx;
|
||||||
|
background: #ff9500;
|
||||||
|
transform: translate(-50%, -70%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--activated {
|
||||||
|
border: 4rpx solid #00b42a;
|
||||||
|
border-radius: 50%;
|
||||||
|
position: relative;
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 40%;
|
||||||
|
left: 50%;
|
||||||
|
width: 16rpx;
|
||||||
|
height: 8rpx;
|
||||||
|
border-left: 4rpx solid #00b42a;
|
||||||
|
border-bottom: 4rpx solid #00b42a;
|
||||||
|
transform: translate(-50%, -50%) rotate(-45deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--refunded {
|
||||||
|
position: relative;
|
||||||
|
&::before, &::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 28rpx;
|
||||||
|
height: 4rpx;
|
||||||
|
background: #86909c;
|
||||||
|
border-radius: 2rpx;
|
||||||
|
}
|
||||||
|
&::before { transform: translate(-50%, -50%) rotate(45deg); }
|
||||||
|
&::after { transform: translate(-50%, -50%) rotate(-45deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
&--expired {
|
||||||
|
border: 4rpx solid #f53f3f;
|
||||||
|
border-radius: 50%;
|
||||||
|
position: relative;
|
||||||
|
&::before, &::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 20rpx;
|
||||||
|
height: 4rpx;
|
||||||
|
background: #f53f3f;
|
||||||
|
border-radius: 2rpx;
|
||||||
|
}
|
||||||
|
&::before { transform: translate(-50%, -50%) rotate(45deg); }
|
||||||
|
&::after { transform: translate(-50%, -50%) rotate(-45deg); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-title {
|
||||||
|
font-family: $font;
|
||||||
|
font-size: 38rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: $text-main;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-desc {
|
||||||
|
font-family: $font;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: $text-hint;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 模块2:商品信息卡片 ===== */
|
||||||
|
.info-card {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: $card-radius;
|
||||||
|
padding: 32rpx;
|
||||||
|
margin-top: 24rpx;
|
||||||
|
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-name {
|
||||||
|
display: block;
|
||||||
|
font-family: $font;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: $text-main;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-current {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
margin-right: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-symbol {
|
||||||
|
font-family: $font;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #e53935;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-value {
|
||||||
|
font-family: $font;
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #e53935;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-desc {
|
||||||
|
font-family: $font;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: $text-hint;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 模块3:订单明细 ===== */
|
||||||
|
.detail-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20rpx 0;
|
||||||
|
border-bottom: 1rpx solid $divider;
|
||||||
|
|
||||||
|
&--last {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-label {
|
||||||
|
font-family: $font;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: $text-label;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-value {
|
||||||
|
font-family: $font;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: $text-body;
|
||||||
|
text-align: right;
|
||||||
|
|
||||||
|
&--code {
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--price {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #e53935;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-value-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn {
|
||||||
|
padding: 4rpx 16rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
background: rgba(60, 156, 255, 0.08);
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn__text {
|
||||||
|
font-family: $font;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: $primary;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 模块4:使用提示 ===== */
|
||||||
|
.tips-card {
|
||||||
|
background: rgba(60, 156, 255, 0.04);
|
||||||
|
border-radius: $card-radius;
|
||||||
|
padding: 32rpx;
|
||||||
|
margin-top: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips-title {
|
||||||
|
display: block;
|
||||||
|
font-family: $font;
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: $text-main;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips-list {
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips-dot {
|
||||||
|
width: 8rpx;
|
||||||
|
height: 8rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: $primary;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: 14rpx;
|
||||||
|
margin-right: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips-text {
|
||||||
|
font-family: $font;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: $text-hint;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips-link {
|
||||||
|
&:active {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips-link__text {
|
||||||
|
font-family: $font;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: $primary;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 底部安全区 ===== */
|
||||||
|
.safe-bottom {
|
||||||
|
height: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 模块5:底部操作栏 ===== */
|
||||||
|
.bottom-bar {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: $bar-height;
|
||||||
|
background: #ffffff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 0 48rpx;
|
||||||
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
|
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.06);
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-bar__right {
|
||||||
|
display: flex;
|
||||||
|
gap: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-action {
|
||||||
|
height: 80rpx;
|
||||||
|
padding: 0 56rpx;
|
||||||
|
border-radius: 40rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
|
||||||
|
&--primary {
|
||||||
|
background: $primary;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.97);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--outline {
|
||||||
|
border: 2rpx solid $divider;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.97);
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-action__text {
|
||||||
|
font-family: $font;
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-action__text--outline {
|
||||||
|
font-family: $font;
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
color: $text-hint;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,588 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page">
|
||||||
|
<!-- Tab 状态切换 -->
|
||||||
|
<scroll-view scroll-x class="tab-bar">
|
||||||
|
<view class="tab-inner">
|
||||||
|
<view
|
||||||
|
v-for="tab in tabs"
|
||||||
|
:key="tab.value"
|
||||||
|
class="tab-item"
|
||||||
|
:class="{ 'tab-item--active': activeTab === tab.value }"
|
||||||
|
@tap="switchTab(tab.value)"
|
||||||
|
>
|
||||||
|
<text class="tab-text">{{ tab.label }}</text>
|
||||||
|
<view v-if="activeTab === tab.value" class="tab-underline" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<!-- 订单列表 -->
|
||||||
|
<scroll-view
|
||||||
|
scroll-y
|
||||||
|
class="order-list"
|
||||||
|
refresher-enabled
|
||||||
|
:refresher-triggered="refreshing"
|
||||||
|
@refresherrefresh="onRefresh"
|
||||||
|
@scrolltolower="onLoadMore"
|
||||||
|
>
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<view v-if="!loading && filteredOrders.length === 0" class="empty-state">
|
||||||
|
<view class="empty-icon">
|
||||||
|
<view class="empty-bag" />
|
||||||
|
<view class="empty-bag__handle" />
|
||||||
|
</view>
|
||||||
|
<text class="empty-text">暂无订单,快去选购赣湘全域旅游年卡/超级玩家卡</text>
|
||||||
|
<view class="empty-btn" @tap="goBuy">
|
||||||
|
<text class="empty-btn__text">去购买</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 订单卡片 -->
|
||||||
|
<view v-else class="order-cards">
|
||||||
|
<view
|
||||||
|
v-for="order in filteredOrders"
|
||||||
|
:key="order.id"
|
||||||
|
class="order-card"
|
||||||
|
@tap="goDetail(order.id)"
|
||||||
|
>
|
||||||
|
<!-- 卡片头部 -->
|
||||||
|
<view class="card-header">
|
||||||
|
<text class="order-no">#{{ order.id }}</text>
|
||||||
|
<view class="status-tag" :class="`status-tag--${order.status}`">
|
||||||
|
<text class="status-tag__text">{{ statusLabel(order.status) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 商品信息 -->
|
||||||
|
<view class="card-product">
|
||||||
|
<view class="product-name-row">
|
||||||
|
<text class="product-name">超级玩家卡</text>
|
||||||
|
<view class="price-current">
|
||||||
|
<text class="price-symbol">¥</text>
|
||||||
|
<text class="price-value">{{ order.payAmount }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 底部信息行 -->
|
||||||
|
<view class="card-info">
|
||||||
|
<text class="info-time">{{ order.createTime }}</text>
|
||||||
|
<text class="info-pay">{{ order.payChannel }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<view class="card-actions" v-if="order.status === 'pending'">
|
||||||
|
<view class="btn-outline" @tap.stop="showRefundModal(order)">
|
||||||
|
<text class="btn-outline__text">申请退款</text>
|
||||||
|
</view>
|
||||||
|
<view class="btn-primary" @tap.stop="goActivate(order)">
|
||||||
|
<text class="btn-primary__text">立即绑定</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="card-actions" v-else>
|
||||||
|
<view class="btn-outline" @tap.stop="goDetail(order.id)">
|
||||||
|
<text class="btn-outline__text">查看详情</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 加载更多 -->
|
||||||
|
<view v-if="filteredOrders.length > 0" class="load-more">
|
||||||
|
<text v-if="loading" class="load-more__text">加载中...</text>
|
||||||
|
<text v-else-if="noMore" class="load-more__text">没有更多订单了</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<!-- 退款弹窗 -->
|
||||||
|
<uni-popup ref="refundPopup" type="center" :mask-click="true">
|
||||||
|
<view class="refund-modal">
|
||||||
|
<text class="refund-title">申请退款</text>
|
||||||
|
<view class="refund-body">
|
||||||
|
<text class="refund-text">赣湘全域旅游年卡/超级玩家卡绑定或销售七日以上不可退卡,不适用7天无理由退货。</text>
|
||||||
|
<text class="refund-text">仅小程序下单、未绑定、购买未满7天可申请退款;第三方平台下单需联系对应平台客服。</text>
|
||||||
|
</view>
|
||||||
|
<view class="refund-actions">
|
||||||
|
<view class="btn-outline" @tap="closeRefund">
|
||||||
|
<text class="btn-outline__text">取消</text>
|
||||||
|
</view>
|
||||||
|
<view class="btn-primary" @tap="submitRefund">
|
||||||
|
<text class="btn-primary__text">确认退款</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</uni-popup>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
|
type OrderStatus = 'pending' | 'activated' | 'refunded' | 'expired'
|
||||||
|
|
||||||
|
interface OrderItem {
|
||||||
|
id: string
|
||||||
|
status: OrderStatus
|
||||||
|
createTime: string
|
||||||
|
payAmount: number
|
||||||
|
originalPrice: number
|
||||||
|
cardType: string
|
||||||
|
redeemCode: string
|
||||||
|
payChannel: string
|
||||||
|
activatedTime?: string
|
||||||
|
refundedTime?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ label: '全部', value: '' },
|
||||||
|
{ label: '待绑定', value: 'pending' },
|
||||||
|
{ label: '已绑定', value: 'activated' },
|
||||||
|
{ label: '已退款', value: 'refunded' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const activeTab = ref('')
|
||||||
|
const loading = ref(false)
|
||||||
|
const refreshing = ref(false)
|
||||||
|
const noMore = ref(false)
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const refundPopup = ref<any>(null)
|
||||||
|
const refundOrderId = ref('')
|
||||||
|
|
||||||
|
const orders = ref<OrderItem[]>([
|
||||||
|
{ id: 'ORD20260625001', status: 'pending', createTime: '2026-06-25 10:32', payAmount: 198, originalPrice: 3800, cardType: '超级玩家卡', redeemCode: 'CJ0000012398', payChannel: '微信支付' },
|
||||||
|
{ id: 'ORD20260624002', status: 'activated', createTime: '2026-06-24 14:18', payAmount: 198, originalPrice: 3800, cardType: '超级玩家卡', redeemCode: 'CJ0000012399', payChannel: '微信支付', activatedTime: '2026-06-24 15:00' },
|
||||||
|
{ id: 'ORD20260623003', status: 'pending', createTime: '2026-06-23 09:45', payAmount: 198, originalPrice: 3800, cardType: '超级玩家卡', redeemCode: 'CJ0000012400', payChannel: '微信支付' },
|
||||||
|
{ id: 'ORD20260620004', status: 'refunded', createTime: '2026-06-20 16:22', payAmount: 198, originalPrice: 3800, cardType: '超级玩家卡', redeemCode: 'CJ0000012401', payChannel: '微信支付', refundedTime: '2026-06-21 10:15' },
|
||||||
|
{ id: 'ORD20260618005', status: 'activated', createTime: '2026-06-18 11:08', payAmount: 198, originalPrice: 3800, cardType: '超级玩家卡', redeemCode: 'CJ0000012402', payChannel: '微信支付', activatedTime: '2026-06-18 12:30' },
|
||||||
|
{ id: 'ORD20260510007', status: 'activated', createTime: '2026-05-10 13:40', payAmount: 198, originalPrice: 3800, cardType: '超级玩家卡', redeemCode: 'CJ0000012403', payChannel: '微信支付', activatedTime: '2026-05-10 14:20' },
|
||||||
|
])
|
||||||
|
|
||||||
|
const filteredOrders = computed(() => {
|
||||||
|
if (!activeTab.value) return orders.value
|
||||||
|
return orders.value.filter(o => o.status === activeTab.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
function statusLabel(status: OrderStatus) {
|
||||||
|
const map: Record<OrderStatus, string> = {
|
||||||
|
pending: '待绑定',
|
||||||
|
activated: '已绑定',
|
||||||
|
refunded: '已退款',
|
||||||
|
expired: '已过期'
|
||||||
|
}
|
||||||
|
return map[status]
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchTab(value: string) {
|
||||||
|
activeTab.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRefresh() {
|
||||||
|
refreshing.value = true
|
||||||
|
currentPage.value = 1
|
||||||
|
noMore.value = false
|
||||||
|
setTimeout(() => {
|
||||||
|
refreshing.value = false
|
||||||
|
}, 800)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLoadMore() {
|
||||||
|
if (loading.value || noMore.value) return
|
||||||
|
loading.value = true
|
||||||
|
setTimeout(() => {
|
||||||
|
loading.value = false
|
||||||
|
noMore.value = true
|
||||||
|
}, 800)
|
||||||
|
}
|
||||||
|
|
||||||
|
function goDetail(orderId: string) {
|
||||||
|
uni.navigateTo({ url: `/pages/order/order-detail?orderId=${orderId}` })
|
||||||
|
}
|
||||||
|
|
||||||
|
function goActivate(order: OrderItem) {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/card-bind/card-bind?redeemCode=${order.redeemCode}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function showRefundModal(order: OrderItem) {
|
||||||
|
refundOrderId.value = order.id
|
||||||
|
refundPopup.value?.open()
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeRefund() {
|
||||||
|
refundPopup.value?.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitRefund() {
|
||||||
|
refundPopup.value?.close()
|
||||||
|
const order = orders.value.find(o => o.id === refundOrderId.value)
|
||||||
|
if (order) {
|
||||||
|
order.status = 'refunded'
|
||||||
|
order.refundedTime = new Date().toISOString().slice(0, 16).replace('T', ' ')
|
||||||
|
}
|
||||||
|
uni.showToast({ title: '退款申请已提交', icon: 'success' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function goBuy() {
|
||||||
|
uni.navigateTo({ url: '/pages/card-buy/card-buy' })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
$font: 'SourceHanSerifCN', '思源宋体', -apple-system, 'PingFang SC', 'Helvetica Neue', sans-serif;
|
||||||
|
$primary: #3c9cff;
|
||||||
|
$text-main: #111111;
|
||||||
|
$text-label: #222222;
|
||||||
|
$text-body: #333333;
|
||||||
|
$text-hint: #666666;
|
||||||
|
$text-weak: #999999;
|
||||||
|
$divider: #eeeeee;
|
||||||
|
$card-radius: 24rpx;
|
||||||
|
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f7f8fa;
|
||||||
|
font-family: $font;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Tab Bar ===== */
|
||||||
|
.tab-bar {
|
||||||
|
white-space: nowrap;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-inner {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 24rpx 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-text {
|
||||||
|
font-family: $font;
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
color: $text-hint;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item--active .tab-text {
|
||||||
|
color: $primary;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-underline {
|
||||||
|
width: 40rpx;
|
||||||
|
height: 4rpx;
|
||||||
|
border-radius: 2rpx;
|
||||||
|
background: $primary;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Order List ===== */
|
||||||
|
.order-list {
|
||||||
|
height: calc(100vh - 96rpx);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-cards {
|
||||||
|
padding: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Order Card ===== */
|
||||||
|
.order-card {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: $card-radius;
|
||||||
|
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
|
||||||
|
padding: 32rpx;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.99);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-no {
|
||||||
|
font-family: $font;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: $text-weak;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status Tags */
|
||||||
|
.status-tag {
|
||||||
|
padding: 4rpx 16rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
|
||||||
|
&--pending {
|
||||||
|
background: rgba(255, 149, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--activated {
|
||||||
|
background: rgba(0, 180, 42, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--refunded {
|
||||||
|
background: rgba(134, 144, 156, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--expired {
|
||||||
|
background: rgba(245, 63, 63, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tag__text {
|
||||||
|
font-family: $font;
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tag--pending .status-tag__text { color: #ff9500; }
|
||||||
|
.status-tag--activated .status-tag__text { color: #00b42a; }
|
||||||
|
.status-tag--refunded .status-tag__text { color: #86909c; }
|
||||||
|
.status-tag--expired .status-tag__text { color: #f53f3f; }
|
||||||
|
|
||||||
|
/* Product Info */
|
||||||
|
.card-product {
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-name-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-name {
|
||||||
|
font-family: $font;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
color: $text-main;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-current {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-symbol {
|
||||||
|
font-family: $font;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #e53935;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-value {
|
||||||
|
font-family: $font;
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #e53935;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Info Row */
|
||||||
|
.card-info {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-bottom: 20rpx;
|
||||||
|
border-bottom: 1rpx solid $divider;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-time {
|
||||||
|
font-family: $font;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: $text-hint;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-pay {
|
||||||
|
font-family: $font;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: $text-hint;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Action Buttons */
|
||||||
|
.card-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline {
|
||||||
|
height: 64rpx;
|
||||||
|
padding: 0 36rpx;
|
||||||
|
border-radius: 32rpx;
|
||||||
|
border: 2rpx solid $divider;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.97);
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline__text {
|
||||||
|
font-family: $font;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
color: $text-hint;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
height: 64rpx;
|
||||||
|
padding: 0 36rpx;
|
||||||
|
border-radius: 32rpx;
|
||||||
|
background: $primary;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.97);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary__text {
|
||||||
|
font-family: $font;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Empty State ===== */
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 160rpx 48rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
width: 120rpx;
|
||||||
|
height: 120rpx;
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-bag {
|
||||||
|
width: 80rpx;
|
||||||
|
height: 64rpx;
|
||||||
|
border: 4rpx solid #cccccc;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-bag__handle {
|
||||||
|
width: 40rpx;
|
||||||
|
height: 24rpx;
|
||||||
|
border: 4rpx solid #cccccc;
|
||||||
|
border-bottom: none;
|
||||||
|
border-radius: 20rpx 20rpx 0 0;
|
||||||
|
position: absolute;
|
||||||
|
top: 16rpx;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
font-family: $font;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: $text-weak;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-btn {
|
||||||
|
height: 72rpx;
|
||||||
|
padding: 0 56rpx;
|
||||||
|
border-radius: 36rpx;
|
||||||
|
background: $primary;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.97);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-btn__text {
|
||||||
|
font-family: $font;
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Load More ===== */
|
||||||
|
.load-more {
|
||||||
|
text-align: center;
|
||||||
|
padding: 16rpx 0 120rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-more__text {
|
||||||
|
font-family: $font;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: $text-weak;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Refund Modal ===== */
|
||||||
|
.refund-modal {
|
||||||
|
width: 600rpx;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: $card-radius;
|
||||||
|
padding: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refund-title {
|
||||||
|
display: block;
|
||||||
|
font-family: $font;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: $text-main;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refund-body {
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refund-text {
|
||||||
|
display: block;
|
||||||
|
font-family: $font;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: $text-body;
|
||||||
|
line-height: 1.7;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refund-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 24rpx;
|
||||||
|
|
||||||
|
.btn-outline,
|
||||||
|
.btn-primary {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
<view class="hero-overlay" />
|
<view class="hero-overlay" />
|
||||||
<view class="hero-content">
|
<view class="hero-content">
|
||||||
<view class="avatar-wrap">
|
<view class="avatar-wrap">
|
||||||
<image class="avatar" :src="userStore.userInfo?.avatarUrl || defaultAvatar" mode="aspectFill" />
|
<image class="avatar" src="/static/logo.png" mode="aspectFill" />
|
||||||
</view>
|
</view>
|
||||||
<view class="user-info">
|
<view class="user-info">
|
||||||
<text class="nickname">{{ userStore.userInfo?.nickname || '未登录' }}</text>
|
<text class="nickname">{{ userStore.userInfo?.nickname || '未登录' }}</text>
|
||||||
@@ -23,21 +23,21 @@
|
|||||||
<!-- 功能菜单 -->
|
<!-- 功能菜单 -->
|
||||||
<view class="menu-section">
|
<view class="menu-section">
|
||||||
<view class="menu-card">
|
<view class="menu-card">
|
||||||
<view class="menu-item" @click="showComingSoon">
|
<view class="menu-item" @click="goOrders">
|
||||||
<view class="menu-icon-wrap menu-icon-wrap--warning">
|
<view class="menu-icon-wrap menu-icon-wrap--warning">
|
||||||
<view class="icon-order" />
|
<view class="icon-order" />
|
||||||
</view>
|
</view>
|
||||||
<text class="menu-text">订单管理</text>
|
<text class="menu-text">订单管理</text>
|
||||||
<view class="menu-chevron" />
|
<view class="menu-chevron" />
|
||||||
</view>
|
</view>
|
||||||
<view class="menu-item" @click="showComingSoon">
|
<view class="menu-item" @click="goUsageGuide">
|
||||||
<view class="menu-icon-wrap menu-icon-wrap--primary">
|
<view class="menu-icon-wrap menu-icon-wrap--primary">
|
||||||
<view class="icon-rule" />
|
<view class="icon-rule" />
|
||||||
</view>
|
</view>
|
||||||
<text class="menu-text">使用规则</text>
|
<text class="menu-text">使用说明</text>
|
||||||
<view class="menu-chevron" />
|
<view class="menu-chevron" />
|
||||||
</view>
|
</view>
|
||||||
<view class="menu-item" @click="showComingSoon">
|
<view class="menu-item" @click="goFaq">
|
||||||
<view class="menu-icon-wrap menu-icon-wrap--success">
|
<view class="menu-icon-wrap menu-icon-wrap--success">
|
||||||
<view class="icon-faq" />
|
<view class="icon-faq" />
|
||||||
</view>
|
</view>
|
||||||
@@ -67,7 +67,6 @@
|
|||||||
import { useUserStore } from '../../store/user'
|
import { useUserStore } from '../../store/user'
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const defaultAvatar = 'https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0'
|
|
||||||
|
|
||||||
function goLogin() { uni.navigateTo({ url: '/pages/login/login' }) }
|
function goLogin() { uni.navigateTo({ url: '/pages/login/login' }) }
|
||||||
|
|
||||||
@@ -91,6 +90,12 @@ function handleLogout() {
|
|||||||
|
|
||||||
function showComingSoon() { uni.showToast({ title: '即将上线,敬请期待', icon: 'none' }) }
|
function showComingSoon() { uni.showToast({ title: '即将上线,敬请期待', icon: 'none' }) }
|
||||||
|
|
||||||
|
function goFaq() { uni.navigateTo({ url: '/pages/faq/faq' }) }
|
||||||
|
|
||||||
|
function goUsageGuide() { uni.navigateTo({ url: '/pages/usage-guide/usage-guide' }) }
|
||||||
|
|
||||||
|
function goOrders() { uni.navigateTo({ url: '/pages/order/order-list' }) }
|
||||||
|
|
||||||
function callService() { uni.makePhoneCall({ phoneNumber: '400-123-0101' }) }
|
function callService() { uni.makePhoneCall({ phoneNumber: '400-123-0101' }) }
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -211,17 +211,29 @@ function copyCardNo() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function goBindCard() {
|
function goBindCard() {
|
||||||
uni.navigateTo({ url: '/packages/user/pages/card/bind' })
|
uni.navigateTo({ url: '/pages/card-bind/card-bind' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const featureActions = [
|
const featureActions = [
|
||||||
{ label: '我要绑定', icon: '/static/icon/card.png' },
|
{ label: '我要绑定', icon: '/static/icon/card.png' },
|
||||||
{ label: '我要预约', icon: '/static/icon/reserve.png' },
|
{ label: '我要预约', icon: '/static/icon/reserve.png' },
|
||||||
{ label: '使用记录', icon: '/static/icon/record.png' },
|
{ label: '使用记录', icon: '/static/icon/record.png' },
|
||||||
{ label: '景区权益', icon: '/static/icon/scenic.png' },
|
{ label: '使用说明', icon: '/static/icon/scenic.png' },
|
||||||
]
|
]
|
||||||
|
|
||||||
function onFeatureClick(item: { label: string }) {
|
function onFeatureClick(item: { label: string }) {
|
||||||
|
if (item.label === '我要绑定') {
|
||||||
|
uni.navigateTo({ url: '/pages/card-bind/card-bind' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (item.label === '我要预约') {
|
||||||
|
uni.navigateTo({ url: '/pages/scenic-list/scenic-list' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (item.label === '使用说明') {
|
||||||
|
uni.navigateTo({ url: '/pages/usage-guide/usage-guide' })
|
||||||
|
return
|
||||||
|
}
|
||||||
uni.showToast({ title: `${item.label}功能开发中`, icon: 'none' })
|
uni.showToast({ title: `${item.label}功能开发中`, icon: 'none' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,10 @@
|
|||||||
<text class="dropdown-label" :class="{ active: selectedLevel }">{{ selectedLevel || '景区等级' }}</text>
|
<text class="dropdown-label" :class="{ active: selectedLevel }">{{ selectedLevel || '景区等级' }}</text>
|
||||||
<view class="dropdown-arrow" />
|
<view class="dropdown-arrow" />
|
||||||
</view>
|
</view>
|
||||||
|
<view class="dropdown-trigger" @click="openPicker('cardType')">
|
||||||
|
<text class="dropdown-label" :class="{ active: selectedCardType }">{{ selectedCardType ? (selectedCardType === '年卡' ? '赣湘全域旅游年卡' : '超级玩家卡') : '全部卡种' }}</text>
|
||||||
|
<view class="dropdown-arrow" />
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@@ -171,7 +175,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed, onMounted, getCurrentInstance } from 'vue'
|
||||||
|
|
||||||
interface ScenicItem {
|
interface ScenicItem {
|
||||||
id: number
|
id: number
|
||||||
@@ -186,6 +190,7 @@ interface ScenicItem {
|
|||||||
type: string
|
type: string
|
||||||
region: string
|
region: string
|
||||||
image: string
|
image: string
|
||||||
|
cardTypes: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 导航栏动态高度 ==========
|
// ========== 导航栏动态高度 ==========
|
||||||
@@ -225,8 +230,18 @@ const stickyHeaderHeight = ref(96) // 预估,onMounted 后精确测量
|
|||||||
// #endif
|
// #endif
|
||||||
})()
|
})()
|
||||||
|
|
||||||
// 吸顶筛选区高度(搜索框72rpx + 下拉行约72rpx + padding 32rpx ≈ 176rpx ≈ 88px)
|
// 吸顶筛选区高度:onMounted 后精确测量,预估值兜底
|
||||||
stickyHeaderHeight.value = 88
|
stickyHeaderHeight.value = 100
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const query = uni.createSelectorQuery().in(getCurrentInstance())
|
||||||
|
query.select('.sticky-header').boundingClientRect((rect: any) => {
|
||||||
|
if (rect && rect.height) {
|
||||||
|
// 实际高度 + 4px 安全间距,防止不同机型小数精度导致遮挡
|
||||||
|
stickyHeaderHeight.value = Math.ceil(rect.height) + 4
|
||||||
|
}
|
||||||
|
}).exec()
|
||||||
|
})
|
||||||
|
|
||||||
// ========== 数据 ==========
|
// ========== 数据 ==========
|
||||||
const scenicList: ScenicItem[] = [
|
const scenicList: ScenicItem[] = [
|
||||||
@@ -271,19 +286,31 @@ const scenicList: ScenicItem[] = [
|
|||||||
{ id: 39, name: '新泉红色研学基地', price: 31, freeCount: 6, level: '3A', status: '正常营业', hours: '09:00-17:00', needReservation: true, tips: '研学活动需提前预约', type: '红色教育', region: '芦溪县', image: 'https://images.unsplash.com/photo-1565008576549-57569a49371d?fm=jpg&q=80&w=800&h=400&fit=crop' },
|
{ id: 39, name: '新泉红色研学基地', price: 31, freeCount: 6, level: '3A', status: '正常营业', hours: '09:00-17:00', needReservation: true, tips: '研学活动需提前预约', type: '红色教育', region: '芦溪县', image: 'https://images.unsplash.com/photo-1565008576549-57569a49371d?fm=jpg&q=80&w=800&h=400&fit=crop' },
|
||||||
{ id: 40, name: '张佳坊山地乐园', price: 55, freeCount: 6, level: '3A', status: '正常营业', hours: '09:00-17:30', needReservation: false, tips: '山地运动项目丰富,注意安全', type: '休闲娱乐', region: '芦溪县', image: 'https://images.unsplash.com/photo-1551632811-561732d1e306?fm=jpg&q=80&w=800&h=400&fit=crop' },
|
{ id: 40, name: '张佳坊山地乐园', price: 55, freeCount: 6, level: '3A', status: '正常营业', hours: '09:00-17:30', needReservation: false, tips: '山地运动项目丰富,注意安全', type: '休闲娱乐', region: '芦溪县', image: 'https://images.unsplash.com/photo-1551632811-561732d1e306?fm=jpg&q=80&w=800&h=400&fit=crop' },
|
||||||
{ id: 41, name: '南坑户外拓展基地', price: 44, freeCount: 6, level: '3A', status: '正常营业', hours: '08:30-18:00', needReservation: true, tips: '团队拓展需提前预约', type: '休闲娱乐', region: '莲花县', image: 'https://images.unsplash.com/photo-1501854140801-50d01698950b?fm=jpg&q=80&w=800&h=400&fit=crop' },
|
{ id: 41, name: '南坑户外拓展基地', price: 44, freeCount: 6, level: '3A', status: '正常营业', hours: '08:30-18:00', needReservation: true, tips: '团队拓展需提前预约', type: '休闲娱乐', region: '莲花县', image: 'https://images.unsplash.com/photo-1501854140801-50d01698950b?fm=jpg&q=80&w=800&h=400&fit=crop' },
|
||||||
{ id: 42, name: '老关荷花景区', price: 34, freeCount: 6, level: '2A', status: '正常营业', hours: '08:00-18:00', needReservation: false, tips: '夏季荷花盛开,景色宜人', type: '自然风光', region: '湘东区', image: 'https://images.unsplash.com/photo-1490750967868-88aa4486c946?fm=jpg&q=80&w=800&h=400&fit=crop' },
|
{ id: 42, name: '老关荷花景区', price: 34, freeCount: 6, level: '2A', status: '正常营业', hours: '08:00-18:00', needReservation: false, tips: '夏季荷花盛开,景色宜人', type: '自然风光', region: '湘东区', image: 'https://images.unsplash.com/photo-1490750967868-88aa4486c946?fm=jpg&q=80&w=800&h=400&fit=crop', cardTypes: ['年卡', '超级卡'] },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// 卡种分配:部分景区仅支持年卡,部分仅支持超级卡,其余两者均可
|
||||||
|
;(function assignCardTypes() {
|
||||||
|
const annualOnly = new Set([4, 14, 18, 30, 35])
|
||||||
|
const superOnly = new Set([1, 13, 17, 34, 38])
|
||||||
|
scenicList.forEach(s => {
|
||||||
|
if (annualOnly.has(s.id)) s.cardTypes = ['年卡']
|
||||||
|
else if (superOnly.has(s.id)) s.cardTypes = ['超级卡']
|
||||||
|
else if (!s.cardTypes) s.cardTypes = ['年卡', '超级卡']
|
||||||
|
})
|
||||||
|
})()
|
||||||
|
|
||||||
// ========== 搜索筛选 ==========
|
// ========== 搜索筛选 ==========
|
||||||
const searchText = ref('')
|
const searchText = ref('')
|
||||||
const selectedRegion = ref('')
|
const selectedRegion = ref('')
|
||||||
const selectedLevel = ref('')
|
const selectedLevel = ref('')
|
||||||
|
const selectedCardType = ref('')
|
||||||
|
|
||||||
// uni-popup 单实例引用
|
// uni-popup 单实例引用
|
||||||
const filterPopup = ref()
|
const filterPopup = ref()
|
||||||
|
|
||||||
// 当前弹窗类型 & 临时选中值
|
// 当前弹窗类型 & 临时选中值
|
||||||
const activePickerType = ref<'region' | 'level'>('region')
|
const activePickerType = ref<'region' | 'level' | 'cardType'>('region')
|
||||||
const tempValue = ref('')
|
const tempValue = ref('')
|
||||||
|
|
||||||
const regionOptions = [
|
const regionOptions = [
|
||||||
@@ -303,18 +330,27 @@ const levelOptions = [
|
|||||||
{ label: '2A', value: '2A' },
|
{ label: '2A', value: '2A' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const cardTypeOptions = [
|
||||||
|
{ label: '全部卡种', value: '' },
|
||||||
|
{ label: '赣湘全域旅游年卡', value: '年卡' },
|
||||||
|
{ label: '超级玩家卡', value: '超级卡' },
|
||||||
|
]
|
||||||
|
|
||||||
// 动态标题 & 选项列表
|
// 动态标题 & 选项列表
|
||||||
const popupTitle = computed(() =>
|
const popupTitle = computed(() => {
|
||||||
activePickerType.value === 'region' ? '选择地区' : '选择等级'
|
const map = { region: '选择地区', level: '选择等级', cardType: '选择卡种' }
|
||||||
)
|
return map[activePickerType.value]
|
||||||
|
})
|
||||||
|
|
||||||
const currentOptions = computed(() =>
|
const currentOptions = computed(() => {
|
||||||
activePickerType.value === 'region' ? regionOptions : levelOptions
|
const map = { region: regionOptions, level: levelOptions, cardType: cardTypeOptions }
|
||||||
)
|
return map[activePickerType.value]
|
||||||
|
})
|
||||||
|
|
||||||
function openPicker(type: 'region' | 'level') {
|
function openPicker(type: 'region' | 'level' | 'cardType') {
|
||||||
activePickerType.value = type
|
activePickerType.value = type
|
||||||
tempValue.value = type === 'region' ? selectedRegion.value : selectedLevel.value
|
const valMap = { region: selectedRegion.value, level: selectedLevel.value, cardType: selectedCardType.value }
|
||||||
|
tempValue.value = valMap[type]
|
||||||
filterPopup.value?.open()
|
filterPopup.value?.open()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,11 +359,8 @@ function onOptionTap(value: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function confirmFilter() {
|
function confirmFilter() {
|
||||||
if (activePickerType.value === 'region') {
|
const map = { region: selectedRegion, level: selectedLevel, cardType: selectedCardType }
|
||||||
selectedRegion.value = tempValue.value
|
map[activePickerType.value].value = tempValue.value
|
||||||
} else {
|
|
||||||
selectedLevel.value = tempValue.value
|
|
||||||
}
|
|
||||||
filterPopup.value?.close()
|
filterPopup.value?.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,6 +388,9 @@ const filteredScenics = computed(() => {
|
|||||||
if (selectedLevel.value && scenic.level !== selectedLevel.value) {
|
if (selectedLevel.value && scenic.level !== selectedLevel.value) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if (selectedCardType.value && !scenic.cardTypes.includes(selectedCardType.value)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page">
|
||||||
|
<image
|
||||||
|
class="guide-image"
|
||||||
|
src="https://hn12301.oss-cn-shenzhen.aliyuncs.com/hn12301/px-062501.png"
|
||||||
|
mode="widthFix"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-image {
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -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: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-image {
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 485 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 83 KiB |
Reference in New Issue
Block a user