This commit is contained in:
lcj
2026-07-07 09:00:53 +08:00
parent 203d129ffb
commit 6d89d1049f
19 changed files with 476 additions and 255 deletions
+9 -37
View File
@@ -97,7 +97,7 @@
</view>
<view class="agreement__text">
<text class="agreement__label">我已阅读并知晓</text>
<text class="agreement__link" @tap.stop="showNotice = true">《用卡须知》</text>
<text class="agreement__link" @tap.stop="openNotice">《用卡须知》</text>
</view>
</view>
@@ -117,7 +117,7 @@
</view>
<!-- 弹窗:证件类型选择 -->
<uni-popup ref="idTypePopup" type="bottom" :mask-click="true" @change="(e: any) => showIdTypePicker = e.show">
<uni-popup ref="idTypePopup" type="bottom" :mask-click="true" mask-background="transparent" @change="(e: any) => showIdTypePicker = e.show">
<view class="popup-sheet">
<view class="popup-header">
<text class="popup-title">选择证件类型</text>
@@ -140,21 +140,6 @@
</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>
<!-- 隐藏 canvas,用于图片压缩 -->
<canvas
canvas-id="myCanvas"
@@ -165,7 +150,7 @@
</template>
<script setup lang="ts">
import { ref, reactive, watch, computed } from 'vue'
import { ref, reactive, computed, watch } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { checkFaceFlag, uploadImage, bindCard as apiBindCard, bindFaceImg } from '../api/card'
@@ -203,9 +188,7 @@ const agreed = ref(false)
const submitting = ref(false)
const uploading = ref(false)
const needFaceBind = ref(false)
const showNotice = ref(false)
const showIdTypePicker = ref(false)
const noticePopup = ref<any>(null)
const idTypePopup = ref<any>(null)
onLoad(async (options) => {
@@ -229,10 +212,10 @@ onLoad(async (options) => {
}
})
watch(showNotice, (v) => {
if (v) noticePopup.value?.open()
else noticePopup.value?.close()
})
/** 打开用卡须知(跳转新页面) */
function openNotice() {
uni.navigateTo({ url: '/packages/user/pages/card-notice' })
}
watch(showIdTypePicker, (v) => {
if (v) idTypePopup.value?.open()
@@ -756,6 +739,8 @@ function handleCancel() {
max-height: 70vh;
display: flex;
flex-direction: column;
box-shadow: none;
padding-bottom: 0;
font-family: $font-family-body;
}
@@ -813,19 +798,6 @@ function handleCancel() {
}
}
.popup-body {
padding: 32rpx;
max-height: 55vh;
}
.popup-content {
font-family: $font-family-body;
font-size: $font-size-sm;
font-weight: 400;
color: $uv-content-color;
line-height: 1.8;
}
/* ===== ID Type Picker List ===== */
.id-type-list {
padding: 16rpx 0;
+18 -23
View File
@@ -4,7 +4,6 @@
<text class="loading-text">加载中...</text>
</view>
<template v-else>
<image class="card-image" :src="mainPic" mode="widthFix" />
<view class="intro-section">
<rich-text :nodes="introduction" />
</view>
@@ -18,9 +17,9 @@
<text class="price-value">{{ buyPrice }}</text>
<text class="price-original">¥3800+</text>
</view>
<button class="btn-buy" :disabled="paying" @tap="handleBuy">
<view class="btn-buy" :class="{ 'btn-buy--disabled': paying }" @tap="handleBuy">
<text class="btn-buy__text">{{ paying ? '支付中...' : '立即购买' }}</text>
</button>
</view>
</view>
</view>
</template>
@@ -95,6 +94,8 @@ async function handleBuy() {
mobile,
couponId: '',
})
const orderId = orderRes.orderId
const outTradeNo = orderRes.outTradeNo
const openId = getToken()
if (!openId) {
@@ -130,12 +131,12 @@ async function handleBuy() {
uni.showToast({ title: '支付成功', icon: 'success' })
setTimeout(() => {
uni.switchTab({ url: '/pages/index/index' })
uni.redirectTo({ url: `/packages/user/pages/order/detail?orderId=${outTradeNo}` })
}, 1500)
} catch (err: any) {
uni.showToast({ title: err?.errMsg?.includes('cancel') ? '已取消支付' : '支付失败,请重试', icon: 'none' })
setTimeout(() => {
uni.redirectTo({ url: '/packages/user/pages/order/list' })
uni.redirectTo({ url: `/packages/user/pages/order/list?outTradeNo=${outTradeNo}` })
}, 1500)
} finally {
paying.value = false
@@ -149,6 +150,7 @@ $bar-height: 120rpx;
.page {
min-height: 100vh;
background: $color-card;
padding-top: 20rpx;
padding-bottom: calc(#{$bar-height} + env(safe-area-inset-bottom));
font-family: $font-family-body;
}
@@ -165,14 +167,6 @@ $bar-height: 120rpx;
color: $uv-tips-color;
}
/* ===== 图片 ===== */
.card-image {
width: 100%;
display: block;
margin: 0;
padding: 0;
}
/* ===== 富文本介绍 ===== */
.intro-section {
width: 100%;
@@ -185,15 +179,13 @@ $bar-height: 120rpx;
left: 0;
right: 0;
bottom: 0;
width: 100%;
box-sizing: border-box;
min-height: $bar-height;
background: $color-card;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 0rpx 0 178rpx;
padding-bottom: env(safe-area-inset-bottom);
padding: 20rpx 32rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.06);
z-index: 100;
}
@@ -207,6 +199,8 @@ $bar-height: 120rpx;
.price-row {
display: flex;
align-items: baseline;
flex: 1;
min-width: 0;
}
.price-symbol {
@@ -246,15 +240,12 @@ $bar-height: 120rpx;
box-shadow: 0 4rpx 16rpx rgba(60, 156, 255, 0.3);
transition: all 0.2s ease;
&::after {
display: none;
}
&:active:not([disabled]) {
&:active {
transform: scale(0.97);
background: $uv-primary;
}
&[disabled] {
&--disabled {
opacity: 0.6;
}
}
@@ -265,5 +256,9 @@ $bar-height: 120rpx;
font-weight: 700;
color: #ffffff;
letter-spacing: 2rpx;
&:active {
color: #ffffff;
}
}
</style>
+96
View File
@@ -0,0 +1,96 @@
<template>
<view class="page">
<view class="bg-ornament" />
<view class="scroll-wrap">
<view class="content">
<view v-if="loading" class="loading">
<view class="loading-spinner" />
<text class="loading-text">加载中...</text>
</view>
<rich-text v-else class="rich-body" :nodes="content" />
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { post } from '../../../api/request'
import { showToast } from '../../../utils'
const content = ref('')
const loading = ref(true)
onMounted(async () => {
try {
const list = await post<any[]>('/mp-api/mpContent/queryList', { channel: 'pingxiang', type: 'CARD_NOTICE' })
content.value = list?.[0]?.content || ''
} catch {
showToast('加载失败')
} finally {
loading.value = false
}
})
</script>
<style scoped>
.page {
min-height: 100vh;
background: linear-gradient(180deg, #f5f7fa 0%, #ffffff 140rpx);
position: relative;
}
.bg-ornament {
position: fixed;
top: -120rpx;
right: -80rpx;
width: 360rpx;
height: 360rpx;
border-radius: 50%;
background: linear-gradient(135deg, rgba(60, 156, 255, 0.08), rgba(60, 156, 255, 0.02));
pointer-events: none;
z-index: 0;
}
.scroll-wrap {
position: relative;
z-index: 1;
min-height: 100vh;
}
.content {
padding: 48rpx;
}
.rich-body {
font-size: 28rpx;
color: $uv-content-color;
line-height: 1.9;
}
.loading {
display: flex;
flex-direction: column;
align-items: center;
padding: 120rpx 0;
gap: 24rpx;
}
.loading-spinner {
width: 48rpx;
height: 48rpx;
border: 4rpx solid #e8ecf1;
border-top-color: $uv-primary;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.loading-text {
font-size: $font-size-base;
color: $uv-tips-color;
}
</style>
+1 -1
View File
@@ -718,7 +718,7 @@ $bar-height: 120rpx;
display: flex;
align-items: center;
justify-content: center;
padding: 0 48rpx;
padding: 20rpx 48rpx;
padding-bottom: env(safe-area-inset-bottom);
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.06);
z-index: 100;
+66 -5
View File
@@ -94,13 +94,14 @@
</scroll-view>
<!-- 入园码弹窗 -->
<uni-popup ref="codePopup" type="center" :mask-click="true">
<uni-popup ref="codePopup" type="center" :mask-click="true" @close="closeCode">
<view class="code-modal">
<text class="code-modal__title">入园码</text>
<view class="qr-box">
<image v-if="codeSrc" class="qr-img" :src="codeSrc" mode="aspectFit" />
</view>
<text class="code-modal__desc">入园时请出示此二维码+本人身份证</text>
<text class="code-modal__countdown">{{ countdown }}秒后自动刷新</text>
</view>
</uni-popup>
@@ -191,7 +192,7 @@
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { ref, computed, watch, onUnmounted } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { post } from '../../../../api/request'
import { BASE_URL } from '../../../../config'
@@ -235,6 +236,16 @@ const detailPopup = ref<any>(null)
const codeSrc = ref('')
const detailRecord = ref<ReserveRecord | null>(null)
const cancelRecord = ref<ReserveRecord | null>(null)
const countdown = ref(20)
let refreshTimer: ReturnType<typeof setInterval> | null = null
let countdownTimer: ReturnType<typeof setInterval> | null = null
onUnmounted(() => {
clearTimer()
// #ifdef MP-WEIXIN
wx.offUserCaptureScreen()
// #endif
})
const records = ref<ReserveRecord[]>([])
@@ -293,6 +304,15 @@ async function fetchRecords(append = false) {
}
onLoad(() => {
// #ifdef MP-WEIXIN
wx.setVisualEffectOnCapture({ visualEffect: 'hidden' })
wx.onUserCaptureScreen(() => {
uni.showToast({ title: '该页面截图无效,请不要截图', icon: 'none' })
if (currentRecord.value) {
fetchCode(currentRecord.value)
}
})
// #endif
fetchRecords()
})
@@ -335,14 +355,49 @@ async function confirmCancel() {
async function showCode(r: ReserveRecord) {
codeSrc.value = ''
currentRecord.value = r
await fetchCode(r)
codePopup.value?.open()
startTimer()
}
function closeCode() {
clearTimer()
}
function clearTimer() {
if (refreshTimer) {
clearInterval(refreshTimer)
refreshTimer = null
}
if (countdownTimer) {
clearInterval(countdownTimer)
countdownTimer = null
}
}
function startTimer() {
clearTimer()
countdown.value = 20
const id = currentRecord.value?.id
if (!id) return
refreshTimer = setInterval(() => fetchCode(currentRecord.value!), 20000)
countdownTimer = setInterval(() => {
countdown.value--
if (countdown.value <= 0) countdown.value = 20
}, 1000)
}
const currentRecord = ref<ReserveRecord | null>(null)
async function fetchCode(r: ReserveRecord) {
countdown.value = 20
try {
const res = await post<any>(`${BASE_URL}/zhy-api/cardBag/getQrByAppointmentId?appointmentId=${r.id}`)
if (res.qrType == 1) {
codeSrc.value = 'data:image/png;base64,' + res.qrCode
codePopup.value?.open()
} else if (res.qrType == 2) {
codeSrc.value = res.qrUrl
codePopup.value?.open()
} else {
await fetchQrByCard(r)
}
@@ -363,7 +418,6 @@ async function fetchQrByCard(r: ReserveRecord) {
})
if (res.qrCode) {
codeSrc.value = res.qrCode.startsWith('data:') ? res.qrCode : 'data:image/png;base64,' + res.qrCode
codePopup.value?.open()
} else {
uni.showToast({ icon: 'none', title: '暂无核销码' })
}
@@ -628,6 +682,13 @@ function previewPhoto(url: string) {
.code-modal__desc { font-family: $font-family-body; font-size: $font-size-sm; color: $uv-tips-color; }
.code-modal__countdown {
font-family: $font-family-body;
font-size: 24rpx;
color: $uv-placeholder-color;
margin-top: 12rpx;
}
/* ===== Cancel Modal ===== */
.cancel-modal {
width: 600rpx; background: $color-card; border-radius: $radius-lg; padding: 40rpx;
+61 -4
View File
@@ -72,20 +72,21 @@
</view>
<!-- 入园码弹窗 -->
<uni-popup ref="codePopup" type="center" :mask-click="true">
<uni-popup ref="codePopup" type="center" :mask-click="true" @close="closeCode">
<view class="code-modal">
<text class="code-modal__title">入园码</text>
<view class="qr-box">
<image v-if="codeSrc" class="qr-img" :src="codeSrc" mode="aspectFit" />
</view>
<text class="code-modal__desc">入园时请出示此二维码+本人身份证</text>
<text class="code-modal__countdown">{{ countdown }}秒后自动刷新</text>
</view>
</uni-popup>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ref, onUnmounted } from 'vue'
import { onLoad, onBackPress } from '@dcloudio/uni-app'
import { post } from '../../../../api/request'
import { BASE_URL } from '../../../../config'
@@ -105,8 +106,27 @@ const codeSrc = ref('')
const codePopup = ref<any>(null)
const cardNumber = ref('')
const cardModelId = ref('')
const countdown = ref(20)
let refreshTimer: ReturnType<typeof setInterval> | null = null
let countdownTimer: ReturnType<typeof setInterval> | null = null
onUnmounted(() => {
clearTimer()
// #ifdef MP-WEIXIN
wx.offUserCaptureScreen()
// #endif
})
onLoad(async (query: any) => {
// #ifdef MP-WEIXIN
wx.setVisualEffectOnCapture({ visualEffect: 'hidden' })
wx.onUserCaptureScreen(() => {
uni.showToast({ title: '该页面截图无效,请不要截图', icon: 'none' })
if (codePopup.value) {
fetchCode()
}
})
// #endif
const appointId = query?.appointId
if (!appointId) return
const phone = (uni.getStorageSync('phone') as string) || ''
@@ -146,14 +166,44 @@ onBackPress(() => {
})
async function showCode() {
await fetchCode()
codePopup.value?.open()
startTimer()
}
function closeCode() {
clearTimer()
}
function clearTimer() {
if (refreshTimer) {
clearInterval(refreshTimer)
refreshTimer = null
}
if (countdownTimer) {
clearInterval(countdownTimer)
countdownTimer = null
}
}
function startTimer() {
clearTimer()
countdown.value = 20
refreshTimer = setInterval(fetchCode, 20000)
countdownTimer = setInterval(() => {
countdown.value--
if (countdown.value <= 0) countdown.value = 20
}, 1000)
}
async function fetchCode() {
countdown.value = 20
try {
const res = await post<any>(`${BASE_URL}/zhy-api/cardBag/getQrByAppointmentId?appointmentId=${reserveId.value}`)
if (res.qrType == 1) {
codeSrc.value = 'data:image/png;base64,' + res.qrCode
codePopup.value?.open()
} else if (res.qrType == 2) {
codeSrc.value = res.qrUrl
codePopup.value?.open()
} else {
await fetchQrByCard()
}
@@ -471,6 +521,13 @@ function goHome() {
margin-top: 8rpx;
}
.code-modal__countdown {
font-family: $font-family-body;
font-size: 24rpx;
color: $uv-placeholder-color;
margin-top: 12rpx;
}
.qr-box {
width: 360rpx;
height: 360rpx;