feat: 抖音核销接入小泥人API、公告详情页、景区预约人选人等多处优化
抖音核销(douyin-verify): - 对接小泥人 OAuth2 token 与 voucher-list 券码接口,按 code 分组展示订单券号及可激活次数 - 支持手动输入券码验证券码;核销流程改为先调后端 sendAndBind 发卡绑定,再验证券码 - 购卡须知改为富文本渲染,新增无可核销订单提示弹窗、客服电话拨打 - 增加 product_id 过滤与 per-page=100,验证券码命中后仅展示不弹提示 公告模块: - 首页/公告列表点击跳转到新增公告详情页(notice-detail),展示标题、发布时间、富文本内容及视频 - 公告列表卡片改为标题+发布时间+查看详情按钮,时间与按钮同排 - 富文本支持内嵌 video 解析播放;content 为空不显示分隔线;标题取公告 title 而非类型 首页与价格: - 首页大卡价格/划线价/立省改为商品详情接口动态获取 - 热门景区列表扩展至8条,支持优惠/折扣标签与默认占位图 预约/景区: - 预约页改为选择预约人(可选/已预约置灰),预约参数改用预约人信息 - 景区列表与详情支持优惠类型、折扣、特惠价展示,富文本图片防溢出 - 景区列表移除自定义导航改用原生导航 订单/绑卡: - 订单列表/详情增加抖音核销单号(thirdOrderNo)展示,核销订单隐藏金额与退款入口 - 绑卡页移除人脸可选开关,照片必传改为拍照 - 其他:图片域名统一为 hn12301,tabBar'旅游卡'改名,登录页支持 redirect 回跳
This commit is contained in:
+3
-3
@@ -4,11 +4,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// 首页 Banner
|
// 首页 Banner
|
||||||
export const IMG_HOME_BANNER = 'https://aliyunoss.hntgov.net/image/2026/04/14/d2bf03167e484ba59f7c2d62879ad07f.jpg'
|
export const IMG_HOME_BANNER = 'https://aliyunoss.hn12301.net/image/2026/04/14/d2bf03167e484ba59f7c2d62879ad07f.jpg'
|
||||||
|
|
||||||
// 年卡购买页图片
|
// 年卡购买页图片
|
||||||
export const IMG_CARD_BUY_1 = 'https://aliyunoss.hntgov.net/image/2026/06/23/70e9f4b3b5fd49adb1ff06edcb8fa032.jpg'
|
export const IMG_CARD_BUY_1 = 'https://aliyunoss.hn12301.net/image/2026/06/23/70e9f4b3b5fd49adb1ff06edcb8fa032.jpg'
|
||||||
export const IMG_CARD_BUY_2 = 'https://aliyunoss.hntgov.net/image/2026/06/25/f3e45bcb3b4f4a2997f107cc6335a1bc.jpg'
|
export const IMG_CARD_BUY_2 = 'https://aliyunoss.hn12301.net/image/2026/06/25/f3e45bcb3b4f4a2997f107cc6335a1bc.jpg'
|
||||||
|
|
||||||
// 入园码页面
|
// 入园码页面
|
||||||
export const IMG_QRCODE_LOGO = 'https://hn12301.oss-cn-shenzhen.aliyuncs.com/hn12301/%E5%88%9D%E5%BF%83%E6%BA%90%20%E6%B9%98%E9%84%82%E8%B5%A3%E6%96%87%E6%97%85%E4%B8%80%E5%8D%A1%E9%80%9A_1777447994254.png'
|
export const IMG_QRCODE_LOGO = 'https://hn12301.oss-cn-shenzhen.aliyuncs.com/hn12301/%E5%88%9D%E5%BF%83%E6%BA%90%20%E6%B9%98%E9%84%82%E8%B5%A3%E6%96%87%E6%97%85%E4%B8%80%E5%8D%A1%E9%80%9A_1777447994254.png'
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page">
|
||||||
|
<view class="detail-card">
|
||||||
|
<text class="detail-title">{{ title }}</text>
|
||||||
|
<text class="detail-date" v-if="date">{{ date }}</text>
|
||||||
|
<view class="divider" v-if="content" />
|
||||||
|
<template v-for="(part, i) in parts" :key="i">
|
||||||
|
<rich-text
|
||||||
|
v-if="part.html"
|
||||||
|
class="detail-content"
|
||||||
|
:nodes="fixRichHtml(part.html)"
|
||||||
|
/>
|
||||||
|
<video
|
||||||
|
v-else-if="part.videoSrc"
|
||||||
|
class="detail-video"
|
||||||
|
:src="part.videoSrc"
|
||||||
|
controls
|
||||||
|
object-fit="contain"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</view>
|
||||||
|
<video
|
||||||
|
v-if="videoUrl"
|
||||||
|
class="detail-video"
|
||||||
|
:src="videoUrl"
|
||||||
|
controls
|
||||||
|
object-fit="contain"
|
||||||
|
/>
|
||||||
|
<view class="safe-bottom" />
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
|
|
||||||
|
interface ContentPart {
|
||||||
|
html?: string
|
||||||
|
videoSrc?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function fixRichHtml(html: string): string {
|
||||||
|
if (!html) return ''
|
||||||
|
const style = 'max-width:100%;height:auto;display:block;'
|
||||||
|
return html
|
||||||
|
.replace(/<video[\s\S]*?<\/video>/gi, '')
|
||||||
|
.replace(/<img(?![^>]*max-width)[^>]*>/gi, (tag: string) => {
|
||||||
|
if (/style="[^"]*"/i.test(tag)) {
|
||||||
|
return tag.replace(/style="([^"]*)"/i, (_: string, s: string) => `style="${s};${style}"`)
|
||||||
|
}
|
||||||
|
return tag.replace(/<img/i, `<img style="${style}"`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseParts(html: string): ContentPart[] {
|
||||||
|
const parts: ContentPart[] = []
|
||||||
|
let lastIndex = 0
|
||||||
|
const videoRegex = /<video[\s\S]*?<\/video>/gi
|
||||||
|
let m: RegExpExecArray | null
|
||||||
|
while ((m = videoRegex.exec(html)) !== null) {
|
||||||
|
if (m.index > lastIndex) parts.push({ html: html.slice(lastIndex, m.index) })
|
||||||
|
const srcMatch =
|
||||||
|
m[0].match(/<source[^>]*src=["']([^"']+)["']/i) ||
|
||||||
|
m[0].match(/<video[^>]*src=["']([^"']+)["']/i)
|
||||||
|
parts.push({ videoSrc: srcMatch ? srcMatch[1] : '' })
|
||||||
|
lastIndex = m.index + m[0].length
|
||||||
|
}
|
||||||
|
if (lastIndex < html.length) parts.push({ html: html.slice(lastIndex) })
|
||||||
|
return parts
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = ref('公告')
|
||||||
|
const date = ref('')
|
||||||
|
const content = ref('')
|
||||||
|
const videoUrl = ref('')
|
||||||
|
const parts = computed(() => parseParts(content.value))
|
||||||
|
|
||||||
|
onLoad(() => {
|
||||||
|
const detail = uni.getStorageSync('notice_detail')
|
||||||
|
if (detail) {
|
||||||
|
title.value = detail.title || '公告'
|
||||||
|
date.value = detail.date || ''
|
||||||
|
content.value = detail.content || ''
|
||||||
|
videoUrl.value = detail.videoUrl || ''
|
||||||
|
uni.removeStorageSync('notice_detail')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #F7F8FA;
|
||||||
|
font-family: 'SourceHanSerifCN', '思源宋体', -apple-system, 'PingFang SC', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-card {
|
||||||
|
margin: 24rpx 30rpx;
|
||||||
|
background: #FFFFFF;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
padding: 40rpx 32rpx;
|
||||||
|
border: 1rpx solid rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-title {
|
||||||
|
display: block;
|
||||||
|
font-size: $font-size-xl;
|
||||||
|
font-weight: $font-weight-bold;
|
||||||
|
color: $uv-main-color;
|
||||||
|
line-height: 1.4;
|
||||||
|
letter-spacing: -0.3rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-date {
|
||||||
|
display: block;
|
||||||
|
margin-top: 16rpx;
|
||||||
|
font-size: $font-size-sm;
|
||||||
|
font-weight: $font-weight-normal;
|
||||||
|
color: $uv-placeholder-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
height: 1rpx;
|
||||||
|
background: rgba(0, 0, 0, 0.06);
|
||||||
|
margin: 32rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-content {
|
||||||
|
font-size: $font-size-base;
|
||||||
|
font-weight: $font-weight-normal;
|
||||||
|
color: $uv-main-color;
|
||||||
|
line-height: 1.8;
|
||||||
|
|
||||||
|
::v-deep img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-video {
|
||||||
|
width: 690rpx;
|
||||||
|
margin-left: 30rpx;
|
||||||
|
height: 400rpx;
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.safe-bottom {
|
||||||
|
height: calc(40rpx + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -8,24 +8,16 @@
|
|||||||
:style="{ animationDelay: index * 60 + 'ms' }"
|
:style="{ animationDelay: index * 60 + 'ms' }"
|
||||||
@click="openDetail(item)"
|
@click="openDetail(item)"
|
||||||
>
|
>
|
||||||
<!-- 第一行:标题 + 日期 + 箭头 -->
|
|
||||||
<view class="card-header">
|
|
||||||
<view class="header-text">
|
|
||||||
<text class="card-title">{{ item.title }}</text>
|
<text class="card-title">{{ item.title }}</text>
|
||||||
<text class="card-summary">{{ item.content }}</text>
|
<view class="card-footer">
|
||||||
</view>
|
<text class="card-date">发布时间:{{ item.date }}</text>
|
||||||
<text class="card-date">{{ item.date }}</text>
|
<view class="card-action-row">
|
||||||
|
<text class="card-action">查看详情</text>
|
||||||
<view class="card-arrow" />
|
<view class="card-arrow" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
</view>
|
||||||
<!-- 公告详情弹窗 -->
|
|
||||||
<AnnouncementModal
|
|
||||||
v-model="showModal"
|
|
||||||
:title="modalTitle"
|
|
||||||
:body="modalBody"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 底部安全区 -->
|
<!-- 底部安全区 -->
|
||||||
<view class="safe-bottom" />
|
<view class="safe-bottom" />
|
||||||
@@ -34,7 +26,6 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import AnnouncementModal from '../../../components/announcement-modal.vue'
|
|
||||||
import { post } from '../../../api/request'
|
import { post } from '../../../api/request'
|
||||||
import type { BannerItem } from '../../../api/types'
|
import type { BannerItem } from '../../../api/types'
|
||||||
|
|
||||||
@@ -43,6 +34,7 @@ interface NoticeItem {
|
|||||||
title: string
|
title: string
|
||||||
date: string
|
date: string
|
||||||
content: string
|
content: string
|
||||||
|
videoUrl: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const notices = ref<NoticeItem[]>([])
|
const notices = ref<NoticeItem[]>([])
|
||||||
@@ -55,21 +47,21 @@ onMounted(async () => {
|
|||||||
title: n.title,
|
title: n.title,
|
||||||
date: n.createdTime ? n.createdTime.slice(0, 10) : '',
|
date: n.createdTime ? n.createdTime.slice(0, 10) : '',
|
||||||
content: n.content || '',
|
content: n.content || '',
|
||||||
|
videoUrl: (n as any).videoUrl || '',
|
||||||
}))
|
}))
|
||||||
} catch {
|
} catch {
|
||||||
// 静默失败
|
// 静默失败
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
/** 弹窗状态 */
|
/** 打开公告详情页 */
|
||||||
const showModal = ref(false)
|
|
||||||
const modalTitle = ref('公告')
|
|
||||||
const modalBody = ref('')
|
|
||||||
|
|
||||||
function openDetail(item: NoticeItem) {
|
function openDetail(item: NoticeItem) {
|
||||||
modalTitle.value = item.title
|
uni.setStorageSync('notice_detail', {
|
||||||
modalBody.value = item.content
|
title: item.title,
|
||||||
showModal.value = true
|
content: item.content || '',
|
||||||
|
videoUrl: item.videoUrl || '',
|
||||||
|
})
|
||||||
|
uni.navigateTo({ url: '/packages/content/pages/notice-detail' })
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -144,63 +136,55 @@ function openDetail(item: NoticeItem) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ====== 卡片内容行 ====== */
|
|
||||||
.card-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 左侧文字区(标题 + 摘要) */
|
|
||||||
.header-text {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
margin-right: 16rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 标题 */
|
/* 标题 */
|
||||||
.card-title {
|
.card-title {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: $font-size-base;
|
font-size: $font-size-base;
|
||||||
font-weight: $font-weight-bold;
|
font-weight: $font-weight-bold;
|
||||||
color: $uv-main-color;
|
color: $uv-main-color;
|
||||||
line-height: 1.3;
|
line-height: 1.4;
|
||||||
letter-spacing: -0.3rpx;
|
letter-spacing: -0.3rpx;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
margin-bottom: 8rpx;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 摘要 */
|
/* 发布行:时间 + 查看详情按钮在一排 */
|
||||||
.card-summary {
|
.card-footer {
|
||||||
display: -webkit-box;
|
display: flex;
|
||||||
-webkit-line-clamp: 2;
|
align-items: center;
|
||||||
-webkit-box-orient: vertical;
|
justify-content: space-between;
|
||||||
overflow: hidden;
|
margin-top: 20rpx;
|
||||||
font-size: $font-size-sm;
|
|
||||||
font-weight: $font-weight-normal;
|
|
||||||
color: $uv-tips-color;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 日期 */
|
/* 发布时间 */
|
||||||
.card-date {
|
.card-date {
|
||||||
font-size: 22rpx;
|
font-size: 22rpx;
|
||||||
font-weight: $font-weight-normal;
|
font-weight: $font-weight-normal;
|
||||||
color: $uv-placeholder-color;
|
color: $uv-placeholder-color;
|
||||||
flex-shrink: 0;
|
|
||||||
letter-spacing: 0.5rpx;
|
letter-spacing: 0.5rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-action-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-action {
|
||||||
|
font-size: $font-size-sm;
|
||||||
|
font-weight: $font-weight-medium;
|
||||||
|
color: $uv-primary;
|
||||||
|
}
|
||||||
|
|
||||||
/* 右箭头 */
|
/* 右箭头 */
|
||||||
.card-arrow {
|
.card-arrow {
|
||||||
width: 14rpx;
|
width: 14rpx;
|
||||||
height: 14rpx;
|
height: 14rpx;
|
||||||
border-right: 3rpx solid #dadbde;
|
border-right: 3rpx solid $uv-primary;
|
||||||
border-bottom: 3rpx solid #dadbde;
|
border-bottom: 3rpx solid $uv-primary;
|
||||||
transform: rotate(-45deg);
|
transform: rotate(-45deg);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
margin-left: 16rpx;
|
margin-left: 12rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ====== 底部安全区 ====== */
|
/* ====== 底部安全区 ====== */
|
||||||
|
|||||||
@@ -95,7 +95,9 @@
|
|||||||
<view class="benefit-meta">
|
<view class="benefit-meta">
|
||||||
<view class="icon-wrap icon-crown" />
|
<view class="icon-wrap icon-crown" />
|
||||||
<text class="remaining-text">
|
<text class="remaining-text">
|
||||||
{{ card.remainingCount > 0 ? '全年剩余' + card.remainingCount + '次' : '全年可用' + card.freeCount + '次' }}
|
<template v-if="card.preferential === 'DISCOUNT'">可享{{ card.discountPercentage / 10 }}折优惠</template>
|
||||||
|
<template v-else-if="card.preferential === 'PREFERENTIAL'">可享特惠价{{ card.discountAmount / 100 }}元/次</template>
|
||||||
|
<template v-else>{{ card.remainingCount > 0 ? '全年剩余' + card.remainingCount + '次' : '全年可用' + card.freeCount + '次' }}</template>
|
||||||
</text>
|
</text>
|
||||||
<text v-if="card.bookingDeadline" class="deadline-text">{{ card.bookingDeadline }}</text>
|
<text v-if="card.bookingDeadline" class="deadline-text">{{ card.bookingDeadline }}</text>
|
||||||
</view>
|
</view>
|
||||||
@@ -129,10 +131,10 @@
|
|||||||
<!-- 模块5:Tab内容区域 -->
|
<!-- 模块5:Tab内容区域 -->
|
||||||
<view class="tab-content">
|
<view class="tab-content">
|
||||||
<view v-if="activeTab === 'notice'" class="content-section">
|
<view v-if="activeTab === 'notice'" class="content-section">
|
||||||
<rich-text class="notice-html" :nodes="scenic.bookingNoticeHtml" />
|
<rich-text class="notice-html" :nodes="bookingNoticeHtml" />
|
||||||
</view>
|
</view>
|
||||||
<view v-if="activeTab === 'intro'" class="content-section">
|
<view v-if="activeTab === 'intro'" class="content-section">
|
||||||
<rich-text class="notice-html" :nodes="scenic.introduction" />
|
<rich-text class="notice-html" :nodes="introductionHtml" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@@ -145,7 +147,7 @@
|
|||||||
<text class="notice-popup-title">{{ storeNotice.title }}</text>
|
<text class="notice-popup-title">{{ storeNotice.title }}</text>
|
||||||
</view>
|
</view>
|
||||||
<scroll-view class="notice-popup-body" scroll-y>
|
<scroll-view class="notice-popup-body" scroll-y>
|
||||||
<rich-text :nodes="storeNotice.content" />
|
<rich-text :nodes="storeNoticeHtml" />
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
<view class="notice-popup-footer">
|
<view class="notice-popup-footer">
|
||||||
<view class="notice-popup-btn" @click="noticeVisible = false">
|
<view class="notice-popup-btn" @click="noticeVisible = false">
|
||||||
@@ -166,7 +168,7 @@ import { onLoad, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
|
|||||||
import { post, getToken } from '../../../api/request'
|
import { post, getToken } from '../../../api/request'
|
||||||
|
|
||||||
interface NoticeItem { title: string; content: string }
|
interface NoticeItem { title: string; content: string }
|
||||||
interface CardBenefit { type: string; cardName: string; skuName: string; remainingCount: number; freeCount: number; bookingDeadline: string; cardModelId: string; resourceId: string; goodsId: string }
|
interface CardBenefit { type: string; cardName: string; skuName: string; remainingCount: number; freeCount: number; bookingDeadline: string; cardModelId: string; resourceId: string; goodsId: string; preferential: string; discountPercentage: number; discountAmount: number }
|
||||||
interface ScenicDetail {
|
interface ScenicDetail {
|
||||||
id: number; name: string; price: number; freeCount: number; level: string
|
id: number; name: string; price: number; freeCount: number; level: string
|
||||||
hours: string; needReservation: string; bookingDeadline: string
|
hours: string; needReservation: string; bookingDeadline: string
|
||||||
@@ -176,6 +178,23 @@ interface ScenicDetail {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const scenic = ref<ScenicDetail>({ tags: [], images: [], cards: [], bookingNotice: [] } as unknown as ScenicDetail)
|
const scenic = ref<ScenicDetail>({ tags: [], images: [], cards: [], bookingNotice: [] } as unknown as ScenicDetail)
|
||||||
|
|
||||||
|
/** 富文本 HTML 清洗:给所有 img 加 max-width:100%,防止超出容器边界 */
|
||||||
|
function fixRichHtml(html: string): string {
|
||||||
|
if (!html) return ''
|
||||||
|
const style = 'max-width:100%;height:auto;display:block;'
|
||||||
|
return html.replace(/<img(?![^>]*max-width)[^>]*>/gi, (tag: string) => {
|
||||||
|
if (/style="[^"]*"/i.test(tag)) {
|
||||||
|
return tag.replace(/style="([^"]*)"/i, (_: string, s: string) => `style="${s};${style}"`)
|
||||||
|
}
|
||||||
|
return tag.replace(/<img/i, `<img style="${style}"`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const introductionHtml = computed(() => fixRichHtml(scenic.value.introduction))
|
||||||
|
const bookingNoticeHtml = computed(() => fixRichHtml(scenic.value.bookingNoticeHtml))
|
||||||
|
const storeNoticeHtml = computed(() => fixRichHtml(storeNotice.value.content))
|
||||||
|
|
||||||
const activeTab = ref<'notice' | 'intro'>('notice')
|
const activeTab = ref<'notice' | 'intro'>('notice')
|
||||||
const currentSlide = ref(0)
|
const currentSlide = ref(0)
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
@@ -303,7 +322,7 @@ async function onBookClick(card: CardBenefit) {
|
|||||||
|
|
||||||
if (res?.hasValidCard) {
|
if (res?.hasValidCard) {
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: `/packages/user/pages/reserve/index?cardNumber=${res.cardNumber}&resourceId=${res.resourceId}&cardModelId=${card.cardModelId}`,
|
url: `/packages/user/pages/reserve/index?resourceId=${res.resourceId || card.resourceId}&cardModelId=${card.cardModelId}`,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
if (String(card.cardModelId) === '820093340659013') {
|
if (String(card.cardModelId) === '820093340659013') {
|
||||||
@@ -716,7 +735,7 @@ $icon-crown: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' w
|
|||||||
margin: 0 $spacing-3;
|
margin: 0 $spacing-3;
|
||||||
border-radius: 0 0 $radius-lg $radius-lg;
|
border-radius: 0 0 $radius-lg $radius-lg;
|
||||||
box-shadow: $shadow-sm;
|
box-shadow: $shadow-sm;
|
||||||
padding: $spacing-3;
|
padding: 24rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-section {
|
.content-section {
|
||||||
|
|||||||
@@ -1,20 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="page">
|
<view class="page">
|
||||||
<!-- 固定自定义导航栏 -->
|
<!-- 页面主体 -->
|
||||||
<view class="nav-bar" :style="{ paddingTop: statusBarHeight + 'px' }">
|
<view class="page-body">
|
||||||
<view class="nav-content" :style="{ height: navContentHeight + 'px' }">
|
|
||||||
<view class="nav-side">
|
|
||||||
<view class="nav-back" @click="goBack">
|
|
||||||
<view class="back-arrow" />
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<text class="nav-title">{{ navTitle }}</text>
|
|
||||||
<view class="nav-side" />
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 页面主体(避开导航栏) -->
|
|
||||||
<view class="page-body" :style="{ paddingTop: navTotalHeight + 'px' }">
|
|
||||||
<!-- 吸顶搜索筛选区(跟随 scroll-view 滚动) -->
|
<!-- 吸顶搜索筛选区(跟随 scroll-view 滚动) -->
|
||||||
<view class="sticky-header">
|
<view class="sticky-header">
|
||||||
<!-- 搜索框 -->
|
<!-- 搜索框 -->
|
||||||
@@ -96,7 +83,7 @@
|
|||||||
<!-- 名称 + 挂牌价 同行 -->
|
<!-- 名称 + 挂牌价 同行 -->
|
||||||
<view class="name-price-row">
|
<view class="name-price-row">
|
||||||
<text class="scenic-name">{{ scenic.name }}</text>
|
<text class="scenic-name">{{ scenic.name }}</text>
|
||||||
<view class="price-group">
|
<view v-if="scenic.price && Number(scenic.price) > 0" class="price-group">
|
||||||
<text class="price-symbol">¥</text>
|
<text class="price-symbol">¥</text>
|
||||||
<text class="price-value">{{ scenic.price }}</text>
|
<text class="price-value">{{ scenic.price }}</text>
|
||||||
</view>
|
</view>
|
||||||
@@ -118,12 +105,20 @@
|
|||||||
|
|
||||||
<!-- 剩余次数 + 预约标签 同行 -->
|
<!-- 剩余次数 + 预约标签 同行 -->
|
||||||
<view class="benefit-row">
|
<view class="benefit-row">
|
||||||
|
<view v-if="scenic.preferential === 'DISCOUNT'" class="remaining-tag">
|
||||||
|
<text>可享{{ scenic.discountPercentage / 10 }}折优惠</text>
|
||||||
|
</view>
|
||||||
|
<view v-else-if="scenic.preferential === 'PREFERENTIAL'" class="remaining-tag">
|
||||||
|
<text>可享特惠价{{ scenic.discountAmount / 100 }}元/次</text>
|
||||||
|
</view>
|
||||||
|
<template v-else>
|
||||||
<view v-if="scenic.remainingCount > 0" class="remaining-tag">
|
<view v-if="scenic.remainingCount > 0" class="remaining-tag">
|
||||||
<text>全年还剩{{ scenic.remainingCount }}次</text>
|
<text>全年还剩{{ scenic.remainingCount }}次</text>
|
||||||
</view>
|
</view>
|
||||||
<view v-else-if="scenic.freeCount > 0" class="remaining-tag">
|
<view v-else-if="scenic.freeCount > 0" class="remaining-tag">
|
||||||
<text>全年最多可用{{ scenic.freeCount }}次</text>
|
<text>全年最多可用{{ scenic.freeCount }}次</text>
|
||||||
</view>
|
</view>
|
||||||
|
</template>
|
||||||
<view class="benefit-tag" :class="scenic.needReservation ? 'warning' : 'success'">
|
<view class="benefit-tag" :class="scenic.needReservation ? 'warning' : 'success'">
|
||||||
<text>{{ scenic.needReservation ? '需预约' : '免预约' }}</text>
|
<text>{{ scenic.needReservation ? '需预约' : '免预约' }}</text>
|
||||||
</view>
|
</view>
|
||||||
@@ -213,6 +208,9 @@ interface ScenicItem {
|
|||||||
type: string
|
type: string
|
||||||
region: string
|
region: string
|
||||||
image: string
|
image: string
|
||||||
|
preferential: string
|
||||||
|
discountPercentage: number
|
||||||
|
discountAmount: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RegionNode {
|
interface RegionNode {
|
||||||
@@ -221,36 +219,11 @@ interface RegionNode {
|
|||||||
children: RegionNode[] | null
|
children: RegionNode[] | null
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 导航栏动态高度 ==========
|
// ========== 导航栏(使用 pages.json 原生导航栏) ==========
|
||||||
const statusBarHeight = ref(20)
|
|
||||||
const navContentHeight = ref(44)
|
|
||||||
const navTotalHeight = ref(64)
|
|
||||||
const stickyHeaderHeight = ref(100)
|
const stickyHeaderHeight = ref(100)
|
||||||
|
|
||||||
const windowHeight = ref(0)
|
const windowHeight = ref(0)
|
||||||
const scrollViewHeight = computed(() => Math.max(0, windowHeight.value - navTotalHeight.value - stickyHeaderHeight.value))
|
const scrollViewHeight = computed(() => Math.max(0, windowHeight.value - stickyHeaderHeight.value))
|
||||||
|
|
||||||
;(function initNavBar() {
|
|
||||||
try {
|
|
||||||
const windowInfo = uni.getWindowInfo()
|
|
||||||
statusBarHeight.value = windowInfo.statusBarHeight || 20
|
|
||||||
} catch {
|
|
||||||
statusBarHeight.value = 20
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const menuBtn = uni.getMenuButtonBoundingClientRect()
|
|
||||||
const capsuleHeight = menuBtn.height
|
|
||||||
const capsuleTop = menuBtn.top
|
|
||||||
const capsuleBottom = capsuleTop + capsuleHeight
|
|
||||||
const contentH = (capsuleBottom - statusBarHeight.value) + (capsuleTop - statusBarHeight.value)
|
|
||||||
navContentHeight.value = Math.max(contentH, 44)
|
|
||||||
navTotalHeight.value = statusBarHeight.value + navContentHeight.value
|
|
||||||
} catch {
|
|
||||||
navContentHeight.value = 44
|
|
||||||
navTotalHeight.value = statusBarHeight.value + 44
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
stickyHeaderHeight.value = 100
|
|
||||||
|
|
||||||
// ========== URL 参数 ==========
|
// ========== URL 参数 ==========
|
||||||
const cardNumber = ref('')
|
const cardNumber = ref('')
|
||||||
@@ -339,7 +312,10 @@ async function fetchScenicList(resetPage = true) {
|
|||||||
tips: s.tips || '',
|
tips: s.tips || '',
|
||||||
type: s.type || s.category || '',
|
type: s.type || s.category || '',
|
||||||
region: s.region?.name || s.region || '',
|
region: s.region?.name || s.region || '',
|
||||||
image: s.image || s.pic || s.coverImage || '',
|
image: s.image || s.pic || s.coverImage || 'https://aliyunoss.hn12301.net/pxmall/px_jqbj.png',
|
||||||
|
preferential: s.preferential || 'FREE',
|
||||||
|
discountPercentage: s.discountPercentage ?? 0,
|
||||||
|
discountAmount: s.discountAmount ?? 0,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
if (resetPage) {
|
if (resetPage) {
|
||||||
@@ -591,15 +567,6 @@ function clearSearch() {
|
|||||||
fetchScenicList()
|
fetchScenicList()
|
||||||
}
|
}
|
||||||
|
|
||||||
function goBack() {
|
|
||||||
const pages = getCurrentPages()
|
|
||||||
if (pages.length <= 1) {
|
|
||||||
uni.switchTab({ url: '/pages/index/index' })
|
|
||||||
} else {
|
|
||||||
uni.navigateBack()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onCardClick(scenic: ScenicItem) {
|
function onCardClick(scenic: ScenicItem) {
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: `/packages/content/pages/scenic-detail?shopId=${scenic.shopId}`,
|
url: `/packages/content/pages/scenic-detail?shopId=${scenic.shopId}`,
|
||||||
@@ -627,60 +594,6 @@ onShareTimeline(() => ({
|
|||||||
font-family: 'SourceHanSerifCN', '思源宋体', serif;
|
font-family: 'SourceHanSerifCN', '思源宋体', serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ====== 固定导航栏 ====== */
|
|
||||||
.nav-bar {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
z-index: 100;
|
|
||||||
background: $color-card;
|
|
||||||
box-shadow: $shadow-sm;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-content {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0 $spacing-3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-back {
|
|
||||||
width: 60rpx;
|
|
||||||
height: 60rpx;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: $radius-base;
|
|
||||||
flex-shrink: 0;
|
|
||||||
transition: background $transition-fast;
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
background: $color-muted;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.back-arrow {
|
|
||||||
width: 20rpx;
|
|
||||||
height: 20rpx;
|
|
||||||
border-left: 4rpx solid $color-foreground;
|
|
||||||
border-bottom: 4rpx solid $color-foreground;
|
|
||||||
transform: rotate(45deg);
|
|
||||||
margin-left: 6rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-title {
|
|
||||||
flex: 1;
|
|
||||||
text-align: center;
|
|
||||||
font-size: $font-size-lg;
|
|
||||||
font-weight: $font-weight-medium;
|
|
||||||
color: $color-foreground;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-side {
|
|
||||||
width: 80rpx;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ====== 页面主体 ====== */
|
/* ====== 页面主体 ====== */
|
||||||
.page-body {
|
.page-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
placeholder="请输入卡号"
|
placeholder="请输入卡号"
|
||||||
placeholder-class="placeholder"
|
placeholder-class="placeholder"
|
||||||
:disabled="faceBindMode"
|
:disabled="faceBindMode"
|
||||||
@blur="onCardNoBlur"
|
|
||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
<view class="divider" />
|
<view class="divider" />
|
||||||
@@ -75,7 +74,7 @@
|
|||||||
<view class="divider" />
|
<view class="divider" />
|
||||||
|
|
||||||
<!-- 持卡人照片 -->
|
<!-- 持卡人照片 -->
|
||||||
<view v-if="needFaceBind" class="form-row form-row--photo">
|
<view class="form-row form-row--photo">
|
||||||
<text class="form-label form-label--top">持卡人照片</text>
|
<text class="form-label form-label--top">持卡人照片</text>
|
||||||
<view class="upload-area" @tap="photoUrl ? previewPhoto() : chooseImage()">
|
<view class="upload-area" @tap="photoUrl ? previewPhoto() : chooseImage()">
|
||||||
<image v-if="photoUrl" class="upload-preview" :src="photoUrl" mode="aspectFit" />
|
<image v-if="photoUrl" class="upload-preview" :src="photoUrl" mode="aspectFit" />
|
||||||
@@ -152,7 +151,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, computed, watch } from 'vue'
|
import { ref, reactive, computed, watch } from 'vue'
|
||||||
import { onLoad } from '@dcloudio/uni-app'
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
import { checkFaceFlag, uploadImage, bindCard as apiBindCard, bindFaceImg } from '../api/card'
|
import { uploadImage, bindCard as apiBindCard, bindFaceImg } from '../api/card'
|
||||||
|
|
||||||
const faceBindMode = ref(false)
|
const faceBindMode = ref(false)
|
||||||
|
|
||||||
@@ -187,7 +186,6 @@ const photoUrl = ref('')
|
|||||||
const agreed = ref(false)
|
const agreed = ref(false)
|
||||||
const submitting = ref(false)
|
const submitting = ref(false)
|
||||||
const uploading = ref(false)
|
const uploading = ref(false)
|
||||||
const needFaceBind = ref(false)
|
|
||||||
const showIdTypePicker = ref(false)
|
const showIdTypePicker = ref(false)
|
||||||
const idTypePopup = ref<any>(null)
|
const idTypePopup = ref<any>(null)
|
||||||
|
|
||||||
@@ -200,15 +198,8 @@ onLoad(async (options) => {
|
|||||||
const idType = options.idType || ''
|
const idType = options.idType || ''
|
||||||
docType.value = idType || '1'
|
docType.value = idType || '1'
|
||||||
form.idCard = options.idNo || ''
|
form.idCard = options.idNo || ''
|
||||||
needFaceBind.value = true
|
|
||||||
} else if (options?.redeemCode) {
|
} else if (options?.redeemCode) {
|
||||||
form.cardNo = options.redeemCode
|
form.cardNo = options.redeemCode
|
||||||
try {
|
|
||||||
const res = await checkFaceFlag(options.redeemCode)
|
|
||||||
needFaceBind.value = !!res
|
|
||||||
} catch {
|
|
||||||
needFaceBind.value = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -227,22 +218,12 @@ function selectIdType(value: string) {
|
|||||||
showIdTypePicker.value = false
|
showIdTypePicker.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onCardNoBlur() {
|
|
||||||
if (!form.cardNo.trim()) return
|
|
||||||
try {
|
|
||||||
const res = await checkFaceFlag(form.cardNo.trim())
|
|
||||||
needFaceBind.value = !!res
|
|
||||||
} catch {
|
|
||||||
needFaceBind.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function chooseImage() {
|
function chooseImage() {
|
||||||
if (uploading.value) return
|
if (uploading.value) return
|
||||||
uni.chooseImage({
|
uni.chooseImage({
|
||||||
count: 1,
|
count: 1,
|
||||||
sizeType: ['original'],
|
sizeType: ['original'],
|
||||||
sourceType: ['album', 'camera'],
|
sourceType: ['camera'],
|
||||||
success: (res) => handleImage(res.tempFilePaths[0]),
|
success: (res) => handleImage(res.tempFilePaths[0]),
|
||||||
fail: (err) => {
|
fail: (err) => {
|
||||||
if (!err.errMsg.includes('cancel')) console.log('选择图片失败:', err)
|
if (!err.errMsg.includes('cancel')) console.log('选择图片失败:', err)
|
||||||
@@ -316,7 +297,7 @@ function validateForm() {
|
|||||||
if (!/^1[3-9]\d{9}$/.test(form.phone)) return '手机号格式不正确'
|
if (!/^1[3-9]\d{9}$/.test(form.phone)) return '手机号格式不正确'
|
||||||
if (!form.idCard.trim()) return `请输入${idTypeLabel.value}号码`
|
if (!form.idCard.trim()) return `请输入${idTypeLabel.value}号码`
|
||||||
if (docType.value === '1' && !/^\d{17}[\dXx]$/.test(form.idCard)) return '身份证号格式不正确'
|
if (docType.value === '1' && !/^\d{17}[\dXx]$/.test(form.idCard)) return '身份证号格式不正确'
|
||||||
if (needFaceBind.value && !photoUrl.value) return '请上传持卡人照片'
|
if (!photoUrl.value) return '请上传持卡人照片'
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,22 +15,43 @@
|
|||||||
<text class="price-label">价格:</text>
|
<text class="price-label">价格:</text>
|
||||||
<text class="price-symbol">¥</text>
|
<text class="price-symbol">¥</text>
|
||||||
<text class="price-value">{{ buyPrice }}</text>
|
<text class="price-value">{{ buyPrice }}</text>
|
||||||
<text class="price-original">¥3800+</text>
|
<text v-if="showPrice" class="price-original">¥{{ showPrice }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="btn-buy" :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>
|
<text class="btn-buy__text">{{ paying ? '支付中...' : '立即购买' }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- 购卡须知弹窗:内容为富文本 -->
|
||||||
|
<view v-if="showNotice" class="modal-mask" @tap="showNotice = false">
|
||||||
|
<view class="modal-panel" @tap.stop>
|
||||||
|
<text class="modal-title">购卡须知</text>
|
||||||
|
<scroll-view scroll-y class="modal-scroll">
|
||||||
|
<rich-text class="modal-html" :nodes="noticeHtml" />
|
||||||
|
</scroll-view>
|
||||||
|
<view class="modal-divider" />
|
||||||
|
<view class="modal-footer">
|
||||||
|
<view class="modal-foot-btn" @tap="showNotice = false">
|
||||||
|
<text class="modal-foot-text">取消</text>
|
||||||
|
</view>
|
||||||
|
<view class="modal-foot-split" />
|
||||||
|
<view class="modal-foot-btn" @tap="confirmBuy">
|
||||||
|
<text class="modal-foot-text modal-foot-text--red">确认购买</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { onLoad, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
|
import { onLoad, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
|
||||||
import { post, getToken } from '../../../api/request'
|
import { post, getToken } from '../../../api/request'
|
||||||
import { BASE_URL } from '../../../config'
|
import { BASE_URL } from '../../../config'
|
||||||
import { useUserStore } from '../../../store/user'
|
import { useUserStore } from '../../../store/user'
|
||||||
import { createOrder, prepay } from '../../../api/order'
|
import { createOrder, prepay } from '../../../api/order'
|
||||||
|
import type { BannerItem } from '../../../api/types'
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
||||||
@@ -39,10 +60,42 @@ const cardModleId = ref('')
|
|||||||
const mainPic = ref('')
|
const mainPic = ref('')
|
||||||
const introduction = ref('')
|
const introduction = ref('')
|
||||||
const buyPrice = ref('0')
|
const buyPrice = ref('0')
|
||||||
|
const showPrice = ref('')
|
||||||
const skuId = ref('')
|
const skuId = ref('')
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const paying = ref(false)
|
const paying = ref(false)
|
||||||
const shareTitle = ref('爱上萍乡旅游年卡')
|
const shareTitle = ref('爱上萍乡旅游年卡')
|
||||||
|
const showNotice = ref(false)
|
||||||
|
const noticeContent = ref('')
|
||||||
|
|
||||||
|
/** 富文本 HTML 清洗:给所有 img 加 max-width:100%,防止超出容器边界 */
|
||||||
|
function fixRichHtml(html: string): string {
|
||||||
|
if (!html) return ''
|
||||||
|
const style = 'max-width:100%;height:auto;display:block;'
|
||||||
|
return html.replace(/<img(?![^>]*max-width)[^>]*>/gi, (tag: string) => {
|
||||||
|
if (/style="[^"]*"/i.test(tag)) {
|
||||||
|
return tag.replace(/style="([^"]*)"/i, (_: string, s: string) => `style="${s};${style}"`)
|
||||||
|
}
|
||||||
|
return tag.replace(/<img/i, `<img style="${style}"`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const noticeHtml = computed(() => fixRichHtml(noticeContent.value))
|
||||||
|
|
||||||
|
/** 获取购卡须知富文本(type = CARD_PURCHASE_NOTICE) */
|
||||||
|
async function fetchPurchaseNotice() {
|
||||||
|
try {
|
||||||
|
const res = await post<BannerItem[]>('/mp-api/mpContent/queryList', {
|
||||||
|
channel: 'pingxiang',
|
||||||
|
type: 'CARD_PURCHASE_NOTICE',
|
||||||
|
})
|
||||||
|
const list = res || []
|
||||||
|
const item = list.find((i) => i.type === 'CARD_PURCHASE_NOTICE') || list[0]
|
||||||
|
noticeContent.value = item?.content || ''
|
||||||
|
} catch {
|
||||||
|
noticeContent.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onLoad((query) => {
|
onLoad((query) => {
|
||||||
goodsId.value = (query?.goodsId as string) || '821926819630853'
|
goodsId.value = (query?.goodsId as string) || '821926819630853'
|
||||||
@@ -59,7 +112,7 @@ interface CardModelDetail {
|
|||||||
name: string
|
name: string
|
||||||
goods: { id: string; mainPic: string }
|
goods: { id: string; mainPic: string }
|
||||||
goodsInfo: { introduction: string }
|
goodsInfo: { introduction: string }
|
||||||
sku: Array<{ id: string; buyPrice: number }>
|
sku: Array<{ id: string; buyPrice: number; showPrice: number }>
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
@@ -73,6 +126,8 @@ onMounted(async () => {
|
|||||||
skuId.value = sku?.id || ''
|
skuId.value = sku?.id || ''
|
||||||
const price = sku?.buyPrice || 0
|
const price = sku?.buyPrice || 0
|
||||||
buyPrice.value = (price / 100).toFixed(2)
|
buyPrice.value = (price / 100).toFixed(2)
|
||||||
|
const showPriceValue = sku?.showPrice || 0
|
||||||
|
showPrice.value = showPriceValue ? (showPriceValue / 100).toFixed(2) : ''
|
||||||
} catch {
|
} catch {
|
||||||
// 静默失败
|
// 静默失败
|
||||||
} finally {
|
} finally {
|
||||||
@@ -97,8 +152,15 @@ async function handleBuy() {
|
|||||||
uni.navigateTo({ url: '/pages/login/login' })
|
uni.navigateTo({ url: '/pages/login/login' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (paying.value) return
|
||||||
|
showNotice.value = true
|
||||||
|
fetchPurchaseNotice()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmBuy() {
|
||||||
if (paying.value) return
|
if (paying.value) return
|
||||||
paying.value = true
|
paying.value = true
|
||||||
|
showNotice.value = false
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const mobile = (uni.getStorageSync('phone') as string) || ''
|
const mobile = (uni.getStorageSync('phone') as string) || ''
|
||||||
@@ -276,4 +338,71 @@ $bar-height: 120rpx;
|
|||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== 购卡须知弹窗 ===== */
|
||||||
|
.modal-mask {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: $z-modal;
|
||||||
|
background: rgba(120, 120, 120, 0.4);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.modal-panel {
|
||||||
|
width: 662rpx;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
padding: 24rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.modal-title {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
font-family: $font-family-display;
|
||||||
|
font-size: $font-size-lg;
|
||||||
|
font-weight: $font-weight-bold;
|
||||||
|
color: $uv-main-color;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
.modal-scroll {
|
||||||
|
height: 700rpx;
|
||||||
|
}
|
||||||
|
.modal-html {
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
font-size: $font-size-sm;
|
||||||
|
color: $uv-content-color;
|
||||||
|
line-height: 1.7;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.modal-divider {
|
||||||
|
height: 1rpx;
|
||||||
|
background: $uv-border-color;
|
||||||
|
margin-top: 24rpx;
|
||||||
|
}
|
||||||
|
.modal-footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.modal-foot-btn {
|
||||||
|
flex: 1;
|
||||||
|
height: 88rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.modal-foot-split {
|
||||||
|
width: 1rpx;
|
||||||
|
height: 48rpx;
|
||||||
|
background: $uv-border-color;
|
||||||
|
}
|
||||||
|
.modal-foot-text {
|
||||||
|
font-size: $font-size-lg;
|
||||||
|
color: $uv-main-color;
|
||||||
|
&--red {
|
||||||
|
color: #ff383c;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="page">
|
<view class="page">
|
||||||
<!-- 头部:装饰渐变 + 标题 -->
|
<!-- 顶部背景图 -->
|
||||||
<view class="hero">
|
<image class="hero-bg" src="https://aliyunoss.hn12301.net/pxmall/px_douyin.png" mode="widthFix" />
|
||||||
<text class="hero-title">抖音购卡订单</text>
|
|
||||||
<view class="hero-pill">
|
|
||||||
<text class="hero-pill-text">订单核销</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<scroll-view scroll-y class="body">
|
<scroll-view scroll-y class="body">
|
||||||
<!-- 第 1 步:确认抖音下单信息 -->
|
<!-- 第 1 步:确认抖音下单信息 -->
|
||||||
@@ -29,35 +24,39 @@
|
|||||||
<view class="field">
|
<view class="field">
|
||||||
<text class="field-label">抖音的订单券号:</text>
|
<text class="field-label">抖音的订单券号:</text>
|
||||||
|
|
||||||
<!-- 有券号:单选列表 -->
|
<!-- 有券号:按 code 分组单选列表 -->
|
||||||
<template v-if="hasVoucher">
|
<template v-if="hasVoucher">
|
||||||
<text class="hint-red">当前微信手机号,可核销券号如下。请选择券号核销</text>
|
<text class="hint-red">当前微信手机号,可核销券号如下。请选择券号核销</text>
|
||||||
<view
|
<view
|
||||||
v-for="item in vouchers"
|
v-for="(group, index) in voucherGroups"
|
||||||
:key="item.id"
|
:key="group.code"
|
||||||
class="field-box field-box--row"
|
class="field-box field-box--row"
|
||||||
@tap="selectVoucher(item)"
|
@tap="selectVoucher(group)"
|
||||||
>
|
>
|
||||||
<text class="code-text">{{ item.code }}</text>
|
<text class="code-text">
|
||||||
<view class="radio" :class="{ 'radio--on': selectedId === item.id }">
|
抖音订单号{{ index + 1 }}:{{ group.code }}(可激活次数{{ group.count }}次)
|
||||||
<view v-if="selectedId === item.id" class="radio-dot" />
|
</text>
|
||||||
|
<view class="radio" :class="{ 'radio--on': selectedCode === group.code }">
|
||||||
|
<view v-if="selectedCode === group.code" class="radio-dot" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
</view>
|
||||||
|
|
||||||
<!-- 无券号:手输券号 -->
|
<!-- 手动输入券码 + 验证券码(仅当手机号未查到券码时才需填写) -->
|
||||||
<template v-else>
|
<view class="field" v-if="!hasVoucher">
|
||||||
<text class="hint-red">无可核销订单!请输入订单券号核销</text>
|
<text class="hint-red">当前微信手机号,无可核销订单!请输入订单券号核销</text>
|
||||||
<view class="field-box">
|
<view class="field-box">
|
||||||
<input
|
<input
|
||||||
class="field-input"
|
class="field-input"
|
||||||
v-model="manualCode"
|
v-model="codeInput"
|
||||||
type="number"
|
placeholder="请粘贴已复制的订单券号"
|
||||||
placeholder="请粘贴抖音订单券号"
|
|
||||||
placeholder-class="placeholder"
|
placeholder-class="placeholder"
|
||||||
/>
|
/>
|
||||||
|
<view class="verify-btn" @tap="verifyInputCode">
|
||||||
|
<text class="verify-btn-text">验证券码</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@@ -108,7 +107,7 @@
|
|||||||
<view class="face-box" @tap="faceImg ? previewFace() : chooseImage()">
|
<view class="face-box" @tap="faceImg ? previewFace() : chooseImage()">
|
||||||
<image v-if="faceImg" class="face-preview" :src="faceImg" mode="aspectFill" />
|
<image v-if="faceImg" class="face-preview" :src="faceImg" mode="aspectFill" />
|
||||||
<text v-else class="face-plus">+</text>
|
<text v-else class="face-plus">+</text>
|
||||||
<view v-if="faceImg" class="face-remove" @tap.stop="faceImg = ''">
|
<view v-if="faceImg" class="face-remove" @tap.stop="removePhoto">
|
||||||
<view class="face-remove__x" />
|
<view class="face-remove__x" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -142,21 +141,16 @@
|
|||||||
<!-- 底部固定按钮 -->
|
<!-- 底部固定按钮 -->
|
||||||
<view class="footer-bar">
|
<view class="footer-bar">
|
||||||
<view class="footer-btn" :class="{ 'footer-btn--disabled': submitting }" @tap="onConfirmVerify">
|
<view class="footer-btn" :class="{ 'footer-btn--disabled': submitting }" @tap="onConfirmVerify">
|
||||||
<text class="footer-btn-text">确认核销</text>
|
<text class="footer-btn-text">提交</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 购卡须知弹窗:内容为单张可滑动图片 -->
|
<!-- 购卡须知弹窗:内容为富文本 -->
|
||||||
<view v-if="showNotice" class="modal-mask" @tap="showNotice = false">
|
<view v-if="showNotice" class="modal-mask" @tap="showNotice = false">
|
||||||
<view class="modal-panel" @tap.stop>
|
<view class="modal-panel" @tap.stop>
|
||||||
<text class="modal-title">购卡须知</text>
|
<text class="modal-title">购卡须知</text>
|
||||||
<scroll-view scroll-y class="modal-scroll">
|
<scroll-view scroll-y class="modal-scroll">
|
||||||
<image
|
<rich-text class="modal-html" :nodes="noticeHtml" />
|
||||||
class="modal-img"
|
|
||||||
src="https://aliyunoss.hn12301.net/pxmall/aspx.png"
|
|
||||||
mode="widthFix"
|
|
||||||
@tap="previewNotice"
|
|
||||||
/>
|
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
<view class="modal-divider" />
|
<view class="modal-divider" />
|
||||||
<view class="modal-footer">
|
<view class="modal-footer">
|
||||||
@@ -171,6 +165,38 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- 无可核销订单弹窗 -->
|
||||||
|
<view v-if="showNoVoucher" class="modal-mask" @tap="showNoVoucher = false">
|
||||||
|
<view class="modal-panel" @tap.stop>
|
||||||
|
<text class="no-voucher-title">温馨提示</text>
|
||||||
|
<text class="no-voucher-sub">当前无可核销订单!</text>
|
||||||
|
<view class="no-voucher-body">
|
||||||
|
<view class="no-voucher-reason">
|
||||||
|
<text class="no-voucher-label">可能原因:</text>
|
||||||
|
</view>
|
||||||
|
<view class="no-voucher-reason">
|
||||||
|
<text class="no-voucher-num">1.</text>
|
||||||
|
<text>抖音购卡的手机号和当前登录手机号码不一致</text>
|
||||||
|
</view>
|
||||||
|
<view class="no-voucher-reason">
|
||||||
|
<text class="no-voucher-num">2.</text>
|
||||||
|
<text>抖音购卡的订单已经核销完成了</text>
|
||||||
|
</view>
|
||||||
|
<view class="no-voucher-reason">
|
||||||
|
<text class="no-voucher-num">3.</text>
|
||||||
|
<text class="no-voucher-text">如无法解决请拨打客服电话:</text>
|
||||||
|
<text class="no-voucher-phone" @tap="callService">0731-88912301</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="modal-divider" />
|
||||||
|
<view class="modal-footer">
|
||||||
|
<view class="modal-foot-btn" @tap="closeNoVoucher">
|
||||||
|
<text class="modal-foot-text modal-foot-text--primary">确认</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<!-- 证件类型选择(与绑卡页一致) -->
|
<!-- 证件类型选择(与绑卡页一致) -->
|
||||||
<uni-popup
|
<uni-popup
|
||||||
ref="idTypePopup"
|
ref="idTypePopup"
|
||||||
@@ -215,11 +241,10 @@ import { ref, reactive, computed, watch } from 'vue'
|
|||||||
import { onShow } from '@dcloudio/uni-app'
|
import { onShow } from '@dcloudio/uni-app'
|
||||||
import { useUserStore } from '../../../store/user'
|
import { useUserStore } from '../../../store/user'
|
||||||
import { uploadImage } from '../api/card'
|
import { uploadImage } from '../api/card'
|
||||||
import {
|
import { post } from '../../../api/request'
|
||||||
queryVoucherList,
|
import { BASE_URL } from '../../../config'
|
||||||
activateAndBind,
|
import type { VoucherItem } from '../api/douyin'
|
||||||
type VoucherItem,
|
import type { BannerItem } from '../../../api/types'
|
||||||
} from '../api/douyin'
|
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
||||||
@@ -229,15 +254,73 @@ const maskedPhone = computed(() =>
|
|||||||
)
|
)
|
||||||
|
|
||||||
const vouchers = ref<VoucherItem[]>([])
|
const vouchers = ref<VoucherItem[]>([])
|
||||||
const selectedId = ref('')
|
const vouchersLoaded = ref(false)
|
||||||
const manualCode = ref('')
|
const selectedCode = ref('')
|
||||||
|
const codeInput = ref('')
|
||||||
const hasVoucher = computed(() => vouchers.value.length > 0)
|
const hasVoucher = computed(() => vouchers.value.length > 0)
|
||||||
const selectedVoucher = computed(() => vouchers.value.find((v) => v.id === selectedId.value) || null)
|
|
||||||
|
interface VoucherGroup {
|
||||||
|
code: string
|
||||||
|
count: number
|
||||||
|
id: string
|
||||||
|
voucher: VoucherItem
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按 code 分组:同一订单多张券共享一个 code,合并显示并标注可激活次数 */
|
||||||
|
const voucherGroups = computed<VoucherGroup[]>(() => {
|
||||||
|
const map = new Map<string, VoucherItem[]>()
|
||||||
|
for (const v of vouchers.value) {
|
||||||
|
const list = map.get(v.code) || []
|
||||||
|
list.push(v)
|
||||||
|
map.set(v.code, list)
|
||||||
|
}
|
||||||
|
return Array.from(map.values()).map((list) => ({
|
||||||
|
code: list[0].code,
|
||||||
|
count: list.length,
|
||||||
|
id: list[0].id,
|
||||||
|
voucher: list[0],
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
const selectedVoucher = computed(() => voucherGroups.value.find((g) => g.code === selectedCode.value) || null)
|
||||||
|
|
||||||
const form = reactive({ userName: '', idNo: '' })
|
const form = reactive({ userName: '', idNo: '' })
|
||||||
const faceImg = ref('')
|
const faceImg = ref('')
|
||||||
|
const uploading = ref(false)
|
||||||
const submitting = ref(false)
|
const submitting = ref(false)
|
||||||
const showNotice = ref(false)
|
const showNotice = ref(false)
|
||||||
|
const showNoVoucher = ref(false)
|
||||||
|
const accessToken = ref('')
|
||||||
|
const tokenType = ref('')
|
||||||
|
const noticeContent = ref('')
|
||||||
|
|
||||||
|
/** 富文本 HTML 清洗:给所有 img 加 max-width:100%,防止超出容器边界 */
|
||||||
|
function fixRichHtml(html: string): string {
|
||||||
|
if (!html) return ''
|
||||||
|
const style = 'max-width:100%;height:auto;display:block;'
|
||||||
|
return html.replace(/<img(?![^>]*max-width)[^>]*>/gi, (tag: string) => {
|
||||||
|
if (/style="[^"]*"/i.test(tag)) {
|
||||||
|
return tag.replace(/style="([^"]*)"/i, (_: string, s: string) => `style="${s};${style}"`)
|
||||||
|
}
|
||||||
|
return tag.replace(/<img/i, `<img style="${style}"`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const noticeHtml = computed(() => fixRichHtml(noticeContent.value))
|
||||||
|
|
||||||
|
/** 获取购卡须知富文本(type = CARD_PURCHASE_NOTICE) */
|
||||||
|
async function fetchPurchaseNotice() {
|
||||||
|
try {
|
||||||
|
const res = await post<BannerItem[]>('/mp-api/mpContent/queryList', {
|
||||||
|
channel: 'pingxiang',
|
||||||
|
type: 'CARD_PURCHASE_NOTICE',
|
||||||
|
})
|
||||||
|
const list = res || []
|
||||||
|
const item = list.find((i) => i.type === 'CARD_PURCHASE_NOTICE') || list[0]
|
||||||
|
noticeContent.value = item?.content || ''
|
||||||
|
} catch {
|
||||||
|
noticeContent.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// xuchao: 证件类型与绑卡页 bind.vue 保持一致
|
// xuchao: 证件类型与绑卡页 bind.vue 保持一致
|
||||||
const idTypeOptions = [
|
const idTypeOptions = [
|
||||||
@@ -265,34 +348,117 @@ function selectIdType(value: string) {
|
|||||||
showIdTypePicker.value = false
|
showIdTypePicker.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
onShow(() => {
|
onShow(async () => {
|
||||||
|
await fetchOAuthToken()
|
||||||
phone.value = (uni.getStorageSync('phone') as string) || ''
|
phone.value = (uni.getStorageSync('phone') as string) || ''
|
||||||
if (!userStore.isLogin) {
|
if (!userStore.isLogin) {
|
||||||
uni.navigateTo({ url: '/pages/login/login' })
|
uni.navigateTo({ url: '/pages/login/login?redirect=/packages/user/pages/douyin-verify' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (!vouchersLoaded.value) {
|
||||||
fetchVouchers()
|
fetchVouchers()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
/** xuchao: 按当前手机号查询可核销券码(status=1 未使用) */
|
/** 获取第三方 OAuth2 token,后续接口放在 header 中 */
|
||||||
|
async function fetchOAuthToken() {
|
||||||
|
try {
|
||||||
|
console.log('[douyin] fetching OAuth token...')
|
||||||
|
const res = await uni.request({
|
||||||
|
url: 'https://www.xiaoniren.cn/openapi/oauth2-token/authorize?client_id=uwfdRQD0CjHrz0oY&client_key=186c07637ed40ef9ef0a2289b188ac',
|
||||||
|
method: 'GET',
|
||||||
|
})
|
||||||
|
console.log('[douyin] OAuth response:', res)
|
||||||
|
const body = (res as any).data
|
||||||
|
if (body?.data?.access_token) {
|
||||||
|
accessToken.value = body.data.access_token
|
||||||
|
tokenType.value = body.data.token_type || 'Bearer'
|
||||||
|
console.log('[douyin] OAuth token acquired')
|
||||||
|
} else {
|
||||||
|
console.warn('[douyin] OAuth response missing token:', body)
|
||||||
|
uni.showToast({ title: '获取凭证失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[douyin] fetchOAuthToken failed:', e)
|
||||||
|
uni.showToast({ title: '获取凭证失败: ' + ((e as any)?.errMsg || (e as any)?.message || '网络错误'), icon: 'none' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按当前手机号查询可核销券码 */
|
||||||
async function fetchVouchers() {
|
async function fetchVouchers() {
|
||||||
if (!phone.value) {
|
if (!phone.value || !accessToken.value) {
|
||||||
vouchers.value = []
|
vouchers.value = []
|
||||||
|
vouchersLoaded.value = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
vouchers.value = await queryVoucherList({ mobile: phone.value, status: 1 })
|
const res = await uni.request({
|
||||||
|
url: `https://www.xiaoniren.cn/openapi/ticket/voucher-list?status=1&mobile=${phone.value}&product_id=59221&page=1&per-page=100`,
|
||||||
|
method: 'GET',
|
||||||
|
header: {
|
||||||
|
Authorization: `${tokenType.value} ${accessToken.value}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const body = (res as any).data
|
||||||
|
const ret = body?.success ? (body?.data?.ret || []) : []
|
||||||
|
vouchers.value = (ret as VoucherItem[]).filter((v) => String(v.product_id) === '59221')
|
||||||
} catch {
|
} catch {
|
||||||
vouchers.value = []
|
vouchers.value = []
|
||||||
}
|
}
|
||||||
selectedId.value = ''
|
selectedCode.value = ''
|
||||||
manualCode.value = ''
|
vouchersLoaded.value = true
|
||||||
|
if (vouchers.value.length === 0) {
|
||||||
|
showNoVoucher.value = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** xuchao: 选中券码;若带实名信息则回填绑卡表单(可编辑) */
|
/** 验证券码:调用 voucher-list 接口按 code 查询,命中则自动选中 */
|
||||||
function selectVoucher(item: VoucherItem) {
|
async function verifyInputCode() {
|
||||||
selectedId.value = item.id
|
const code = codeInput.value.trim()
|
||||||
const info = item.voucherRealnameInfo?.[0]
|
if (!code) {
|
||||||
|
uni.showToast({ title: '请输入券码', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!accessToken.value) {
|
||||||
|
uni.showToast({ title: '获取凭证失败,请重试', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.showLoading({ title: '验证中...', mask: true })
|
||||||
|
try {
|
||||||
|
const res = await uni.request({
|
||||||
|
url: `https://www.xiaoniren.cn/openapi/ticket/voucher-list?status=1&code=${encodeURIComponent(code)}&product_id=59221&page=1&per-page=100`,
|
||||||
|
method: 'GET',
|
||||||
|
header: {
|
||||||
|
Authorization: `${tokenType.value} ${accessToken.value}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const body = (res as any).data
|
||||||
|
const list = body?.success ? (body?.data?.ret || []) : []
|
||||||
|
const matched = list.find((v: any) => String(v.code) === code)
|
||||||
|
if (matched) {
|
||||||
|
// 命中后按手机号查询一致的逻辑展示:回填列表并选中对应券码
|
||||||
|
vouchers.value = (list as VoucherItem[]).filter((v) => String(v.product_id) === '59221')
|
||||||
|
uni.hideLoading()
|
||||||
|
const group = voucherGroups.value.find((g) => g.code === matched.code)
|
||||||
|
if (group) {
|
||||||
|
selectVoucher(group)
|
||||||
|
} else {
|
||||||
|
selectVoucher({ code: matched.code, count: 1, id: matched.id, voucher: matched as VoucherItem })
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
uni.hideLoading()
|
||||||
|
uni.showToast({ title: '该券码未找到可核销的订单', icon: 'none' })
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
uni.hideLoading()
|
||||||
|
uni.showToast({ title: '验证失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** xuchao: 选中券码分组;若带实名信息则回填绑卡表单(可编辑) */
|
||||||
|
function selectVoucher(group: VoucherGroup) {
|
||||||
|
selectedCode.value = group.code
|
||||||
|
const info = group.voucher.voucherRealnameInfo?.[0]
|
||||||
if (info) {
|
if (info) {
|
||||||
if (info.realname) form.userName = info.realname
|
if (info.realname) form.userName = info.realname
|
||||||
if (info.card_id) form.idNo = info.card_id
|
if (info.card_id) form.idNo = info.card_id
|
||||||
@@ -301,20 +467,9 @@ function selectVoucher(item: VoucherItem) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 当前用于提交的券码:有券取选中项,无券取手输 */
|
|
||||||
function currentCode(): string {
|
|
||||||
return hasVoucher.value ? selectedVoucher.value?.code || '' : manualCode.value.trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
/** xuchao: 校验表单,返回错误文案,空串表示通过 */
|
/** xuchao: 校验表单,返回错误文案,空串表示通过 */
|
||||||
function validate(): string {
|
function validate(): string {
|
||||||
if (hasVoucher.value) {
|
if (!selectedCode.value) return '请选择要核销的券号'
|
||||||
if (!selectedId.value) return '请选择要核销的券号'
|
|
||||||
} else {
|
|
||||||
const code = manualCode.value.trim()
|
|
||||||
if (!code) return '请输入订单券号'
|
|
||||||
if (!/^\d+$/.test(code)) return '券号格式不正确,仅限数字'
|
|
||||||
}
|
|
||||||
if (!form.userName.trim()) return '请输入绑卡人姓名'
|
if (!form.userName.trim()) return '请输入绑卡人姓名'
|
||||||
if (!form.idNo.trim()) return '请输入证件号码'
|
if (!form.idNo.trim()) return '请输入证件号码'
|
||||||
if (docType.value === '1' && !/^\d{17}[\dXx]$/.test(form.idNo.trim())) return '身份证号格式不正确'
|
if (docType.value === '1' && !/^\d{17}[\dXx]$/.test(form.idNo.trim())) return '身份证号格式不正确'
|
||||||
@@ -331,32 +486,73 @@ function onConfirmVerify() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
showNotice.value = true
|
showNotice.value = true
|
||||||
|
fetchPurchaseNotice()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 弹窗内确认核销:调用激活且绑定接口 */
|
/** 弹窗内确认核销:先调 sendAndBind,成功后验证券码 */
|
||||||
async function doActivate() {
|
async function doActivate() {
|
||||||
if (submitting.value) return
|
if (submitting.value) return
|
||||||
submitting.value = true
|
submitting.value = true
|
||||||
uni.showLoading({ title: '核销中...', mask: true })
|
uni.showLoading({ title: '核销中...', mask: true })
|
||||||
try {
|
try {
|
||||||
await activateAndBind({
|
// 第一步:调后端发卡绑定
|
||||||
voucherId: selectedVoucher.value?.id || '',
|
const sendRes = await uni.request({
|
||||||
voucherCode: currentCode(),
|
url: `${BASE_URL}/open-api/card/sendAndBind`,
|
||||||
quantity: 1,
|
method: 'POST',
|
||||||
mobile: phone.value,
|
data: {
|
||||||
|
orderNo: selectedVoucher.value?.id || '',
|
||||||
|
typeId: '820093438328133',
|
||||||
userName: form.userName.trim(),
|
userName: form.userName.trim(),
|
||||||
idType: docType.value,
|
mobile: phone.value,
|
||||||
idNo: form.idNo.trim(),
|
idNo: form.idNo.trim(),
|
||||||
faceImg: faceImg.value,
|
idType: docType.value,
|
||||||
|
img: faceImg.value,
|
||||||
|
appId: 'AISHANGPX_DY_PRD_917406',
|
||||||
|
sign: '',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
const sendBody = (sendRes as any).data
|
||||||
|
const sendOk = sendBody?.code === 200 || sendBody?.errcode === 0
|
||||||
|
if (!sendOk) {
|
||||||
|
uni.hideLoading()
|
||||||
|
showNotice.value = false
|
||||||
|
uni.showToast({ title: sendBody?.msg || sendBody?.errmsg || '发卡绑定失败', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 第二步:验证券码
|
||||||
|
const validateRes = await uni.request({
|
||||||
|
url: 'https://www.xiaoniren.cn/openapi/ticket/validate',
|
||||||
|
method: 'POST',
|
||||||
|
header: {
|
||||||
|
Authorization: `${tokenType.value} ${accessToken.value}`,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
data: {
|
||||||
|
code: '',
|
||||||
|
id: selectedVoucher.value?.id || '',
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const validateBody = (validateRes as any).data
|
||||||
|
const validateOk = validateBody?.success && validateBody?.data?.error_code === 0
|
||||||
|
if (!validateOk) {
|
||||||
|
uni.hideLoading()
|
||||||
|
showNotice.value = false
|
||||||
|
uni.showToast({ title: validateBody?.data?.message || validateBody?.msg || '验证券码失败', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
uni.hideLoading()
|
uni.hideLoading()
|
||||||
showNotice.value = false
|
showNotice.value = false
|
||||||
uni.showToast({ title: '核销成功', icon: 'success' })
|
uni.showToast({ title: '核销成功', icon: 'success' })
|
||||||
setTimeout(() => uni.switchTab({ url: '/pages/qrcode/index' }), 1200)
|
vouchersLoaded.value = false
|
||||||
|
fetchVouchers()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// xuchao: 真实请求的错误提示由 request.ts 统一弹出,此处仅收尾,避免重复 toast
|
console.error('[douyin] doActivate failed:', e)
|
||||||
console.error('[douyin] activateAndBind failed:', e)
|
|
||||||
uni.hideLoading()
|
uni.hideLoading()
|
||||||
|
uni.showToast({ title: '核销失败', icon: 'none' })
|
||||||
} finally {
|
} finally {
|
||||||
submitting.value = false
|
submitting.value = false
|
||||||
}
|
}
|
||||||
@@ -364,10 +560,11 @@ async function doActivate() {
|
|||||||
|
|
||||||
/** 选择人脸照片 */
|
/** 选择人脸照片 */
|
||||||
function chooseImage() {
|
function chooseImage() {
|
||||||
|
if (uploading.value) return
|
||||||
uni.chooseImage({
|
uni.chooseImage({
|
||||||
count: 1,
|
count: 1,
|
||||||
sizeType: ['original'],
|
sizeType: ['original'],
|
||||||
sourceType: ['album', 'camera'],
|
sourceType: ['camera'],
|
||||||
success: (res) => handleImage(res.tempFilePaths[0]),
|
success: (res) => handleImage(res.tempFilePaths[0]),
|
||||||
fail: (err) => {
|
fail: (err) => {
|
||||||
if (!err.errMsg.includes('cancel')) console.log('选择图片失败:', err)
|
if (!err.errMsg.includes('cancel')) console.log('选择图片失败:', err)
|
||||||
@@ -376,6 +573,7 @@ function chooseImage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleImage(filePath: string) {
|
async function handleImage(filePath: string) {
|
||||||
|
uploading.value = true
|
||||||
uni.showLoading({ title: '处理中...' })
|
uni.showLoading({ title: '处理中...' })
|
||||||
try {
|
try {
|
||||||
const compressed = await compressImage(filePath)
|
const compressed = await compressImage(filePath)
|
||||||
@@ -385,6 +583,7 @@ async function handleImage(filePath: string) {
|
|||||||
} catch {
|
} catch {
|
||||||
uni.showToast({ title: '上传失败', icon: 'none' })
|
uni.showToast({ title: '上传失败', icon: 'none' })
|
||||||
} finally {
|
} finally {
|
||||||
|
uploading.value = false
|
||||||
uni.hideLoading()
|
uni.hideLoading()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -436,8 +635,16 @@ function previewFace() {
|
|||||||
if (faceImg.value) uni.previewImage({ urls: [faceImg.value] })
|
if (faceImg.value) uni.previewImage({ urls: [faceImg.value] })
|
||||||
}
|
}
|
||||||
|
|
||||||
function previewNotice() {
|
function removePhoto() {
|
||||||
uni.previewImage({ urls: ['https://aliyunoss.hn12301.net/pxmall/aspx.png'] })
|
faceImg.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function callService() {
|
||||||
|
uni.makePhoneCall({ phoneNumber: '0731-88912301' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeNoVoucher() {
|
||||||
|
showNoVoucher.value = false
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -448,37 +655,19 @@ function previewNotice() {
|
|||||||
font-family: $font-family-body;
|
font-family: $font-family-body;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== Hero ===== */
|
/* ===== Hero background ===== */
|
||||||
.hero {
|
.hero-bg {
|
||||||
padding: 48rpx 40rpx 56rpx;
|
|
||||||
background: linear-gradient(180deg, #fff4e6 0%, #fdeede 55%, #fff 100%);
|
|
||||||
}
|
|
||||||
.hero-title {
|
|
||||||
display: block;
|
display: block;
|
||||||
font-family: $font-family-display;
|
width: 100%;
|
||||||
font-size: 48rpx;
|
|
||||||
font-weight: $font-weight-bold;
|
|
||||||
color: $uv-main-color;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
.hero-pill {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-top: 20rpx;
|
|
||||||
padding: 12rpx 24rpx;
|
|
||||||
border-radius: 30rpx 0 0 10rpx;
|
|
||||||
background: linear-gradient(90deg, #fac28a 0%, #ff4c17 100%);
|
|
||||||
}
|
|
||||||
.hero-pill-text {
|
|
||||||
font-family: $font-family-display;
|
|
||||||
font-size: $font-size-sm;
|
|
||||||
font-weight: $font-weight-bold;
|
|
||||||
color: #fff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== Body / Section ===== */
|
/* ===== Body / Section ===== */
|
||||||
.body {
|
.body {
|
||||||
height: calc(100vh - 240rpx);
|
height: calc(100vh - 240rpx);
|
||||||
|
position: absolute;
|
||||||
|
top: 300rpx;
|
||||||
|
padding-bottom: 120rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
.section {
|
.section {
|
||||||
padding: 8rpx 32rpx 24rpx;
|
padding: 8rpx 32rpx 24rpx;
|
||||||
@@ -517,6 +706,7 @@ function previewNotice() {
|
|||||||
font-size: $font-size-sm;
|
font-size: $font-size-sm;
|
||||||
color: $uv-main-color;
|
color: $uv-main-color;
|
||||||
line-height: 48rpx;
|
line-height: 48rpx;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
.hint-red {
|
.hint-red {
|
||||||
display: block;
|
display: block;
|
||||||
@@ -561,6 +751,26 @@ function previewNotice() {
|
|||||||
color: $uv-tips-color;
|
color: $uv-tips-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== Verify button ===== */
|
||||||
|
.verify-btn {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: 16rpx;
|
||||||
|
padding: 10rpx 24rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
background: $uv-primary;
|
||||||
|
transition: opacity $transition-fast;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.verify-btn-text {
|
||||||
|
font-size: $font-size-sm;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: $font-weight-medium;
|
||||||
|
}
|
||||||
|
|
||||||
/* ===== Radio ===== */
|
/* ===== Radio ===== */
|
||||||
.radio {
|
.radio {
|
||||||
width: 34rpx;
|
width: 34rpx;
|
||||||
@@ -686,7 +896,7 @@ function previewNotice() {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
text {
|
text {
|
||||||
font-size: $font-size-sm;
|
font-size: $font-size-sm;
|
||||||
color: $uv-tips-color;
|
color: #8E8E93;
|
||||||
line-height: 40rpx;
|
line-height: 40rpx;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -751,9 +961,13 @@ function previewNotice() {
|
|||||||
.modal-scroll {
|
.modal-scroll {
|
||||||
height: 700rpx;
|
height: 700rpx;
|
||||||
}
|
}
|
||||||
.modal-img {
|
.modal-html {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: block;
|
display: block;
|
||||||
|
font-size: $font-size-sm;
|
||||||
|
color: $uv-content-color;
|
||||||
|
line-height: 1.7;
|
||||||
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
.modal-divider {
|
.modal-divider {
|
||||||
height: 1rpx;
|
height: 1rpx;
|
||||||
@@ -777,11 +991,62 @@ function previewNotice() {
|
|||||||
background: $uv-border-color;
|
background: $uv-border-color;
|
||||||
}
|
}
|
||||||
.modal-foot-text {
|
.modal-foot-text {
|
||||||
font-size: $font-size-sm;
|
font-size: $font-size-lg;
|
||||||
color: $uv-main-color;
|
color: $uv-main-color;
|
||||||
&--red {
|
&--red {
|
||||||
color: #ff383c;
|
color: #ff383c;
|
||||||
}
|
}
|
||||||
|
&--primary {
|
||||||
|
color: $uv-primary;
|
||||||
|
font-weight: $font-weight-bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== No voucher modal ===== */
|
||||||
|
.no-voucher-title {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
font-family: $font-family-display;
|
||||||
|
font-size: $font-size-lg;
|
||||||
|
font-weight: $font-weight-bold;
|
||||||
|
color: $uv-main-color;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
.no-voucher-sub {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
font-size: $font-size-base;
|
||||||
|
font-weight: $font-weight-bold;
|
||||||
|
color: #ff383c;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
.no-voucher-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12rpx;
|
||||||
|
padding: 24rpx;
|
||||||
|
}
|
||||||
|
.no-voucher-reason {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
font-size: $font-size-sm;
|
||||||
|
color: $uv-content-color;
|
||||||
|
line-height: 40rpx;
|
||||||
|
}
|
||||||
|
.no-voucher-label {
|
||||||
|
font-weight: $font-weight-bold;
|
||||||
|
color: $uv-main-color;
|
||||||
|
}
|
||||||
|
.no-voucher-num {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: 8rpx;
|
||||||
|
}
|
||||||
|
.no-voucher-text {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.no-voucher-phone {
|
||||||
|
color: $uv-primary;
|
||||||
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== ID type popup ===== */
|
/* ===== ID type popup ===== */
|
||||||
@@ -789,8 +1054,10 @@ function previewNotice() {
|
|||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 24rpx 24rpx 0 0;
|
border-radius: 24rpx 24rpx 0 0;
|
||||||
max-height: 70vh;
|
max-height: 70vh;
|
||||||
|
overflow-y: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
padding-bottom: 120rpx;
|
||||||
}
|
}
|
||||||
.popup-header {
|
.popup-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<!-- 模块2:商品信息卡片 -->
|
<!-- 模块2:商品信息卡片 -->
|
||||||
<view class="info-card">
|
<view class="info-card">
|
||||||
<text class="product-name">超级玩家卡</text>
|
<text class="product-name">超级玩家卡</text>
|
||||||
<view class="price-row">
|
<view v-if="!order.thirdOrderNo" class="price-row">
|
||||||
<view class="price-current">
|
<view class="price-current">
|
||||||
<text class="price-symbol">¥</text>
|
<text class="price-symbol">¥</text>
|
||||||
<text class="price-value">{{ (order.payAmount / 100).toFixed(2) }}</text>
|
<text class="price-value">{{ (order.payAmount / 100).toFixed(2) }}</text>
|
||||||
@@ -27,6 +27,10 @@
|
|||||||
<text class="detail-label">订单编号</text>
|
<text class="detail-label">订单编号</text>
|
||||||
<text class="detail-value">{{ order.id }}</text>
|
<text class="detail-value">{{ order.id }}</text>
|
||||||
</view>
|
</view>
|
||||||
|
<view v-if="order.thirdOrderNo" class="detail-row">
|
||||||
|
<text class="detail-label">核销单号</text>
|
||||||
|
<text class="detail-value">{{ order.thirdOrderNo }}</text>
|
||||||
|
</view>
|
||||||
<view v-if="order.redeemCode && order.payStatus === 2" class="detail-row">
|
<view v-if="order.redeemCode && order.payStatus === 2" class="detail-row">
|
||||||
<text class="detail-label">卡号</text>
|
<text class="detail-label">卡号</text>
|
||||||
<view class="detail-value-row">
|
<view class="detail-value-row">
|
||||||
@@ -40,15 +44,15 @@
|
|||||||
<text class="detail-label">下单时间</text>
|
<text class="detail-label">下单时间</text>
|
||||||
<text class="detail-value">{{ order.createTime }}</text>
|
<text class="detail-value">{{ order.createTime }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="detail-row">
|
<view v-if="!order.thirdOrderNo" class="detail-row">
|
||||||
<text class="detail-label">支付时间</text>
|
<text class="detail-label">支付时间</text>
|
||||||
<text class="detail-value">{{ order.payTime || '-' }}</text>
|
<text class="detail-value">{{ order.payTime || '-' }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="detail-row">
|
<view class="detail-row">
|
||||||
<text class="detail-label">支付渠道</text>
|
<text class="detail-label">支付渠道</text>
|
||||||
<text class="detail-value">{{ order.payChannel }}</text>
|
<text class="detail-value">{{ order.thirdOrderNo ? '抖音核销' : order.payChannel }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="detail-row">
|
<view v-if="!order.thirdOrderNo" class="detail-row">
|
||||||
<text class="detail-label">支付金额</text>
|
<text class="detail-label">支付金额</text>
|
||||||
<text class="detail-value detail-value--price">¥{{ (order.payAmount / 100).toFixed(2) }}</text>
|
<text class="detail-value detail-value--price">¥{{ (order.payAmount / 100).toFixed(2) }}</text>
|
||||||
</view>
|
</view>
|
||||||
@@ -88,7 +92,7 @@
|
|||||||
<text class="btn-action__text">立即支付</text>
|
<text class="btn-action__text">立即支付</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view v-else-if="Number(order.payStatus) === 2" class="bottom-bar__right">
|
<view v-else-if="Number(order.payStatus) === 2 && !order.thirdOrderNo" class="bottom-bar__right">
|
||||||
<view class="btn-action btn-action--outline" @tap="handleRefund">
|
<view class="btn-action btn-action--outline" @tap="handleRefund">
|
||||||
<text class="btn-action__text--outline">申请退款</text>
|
<text class="btn-action__text--outline">申请退款</text>
|
||||||
</view>
|
</view>
|
||||||
@@ -116,6 +120,7 @@ interface OrderDetail {
|
|||||||
payTime: string | null
|
payTime: string | null
|
||||||
payAmount: number
|
payAmount: number
|
||||||
payChannel: string
|
payChannel: string
|
||||||
|
thirdOrderNo: string
|
||||||
cardNo: string | null
|
cardNo: string | null
|
||||||
redeemCode: string | null
|
redeemCode: string | null
|
||||||
isBind: number | null
|
isBind: number | null
|
||||||
@@ -137,6 +142,7 @@ const order = ref<OrderDetail>({
|
|||||||
payTime: null,
|
payTime: null,
|
||||||
payAmount: 0,
|
payAmount: 0,
|
||||||
payChannel: '微信支付',
|
payChannel: '微信支付',
|
||||||
|
thirdOrderNo: '',
|
||||||
cardNo: null,
|
cardNo: null,
|
||||||
redeemCode: null,
|
redeemCode: null,
|
||||||
isBind: null,
|
isBind: null,
|
||||||
@@ -178,6 +184,7 @@ async function fetchOrderDetail() {
|
|||||||
payTime: res.payTime || null,
|
payTime: res.payTime || null,
|
||||||
payAmount: res.payAmount || 0,
|
payAmount: res.payAmount || 0,
|
||||||
payChannel: res.payChannel || '微信支付',
|
payChannel: res.payChannel || '微信支付',
|
||||||
|
thirdOrderNo: res.thirdOrderNo || '',
|
||||||
cardNo: res.cardNo || null,
|
cardNo: res.cardNo || null,
|
||||||
redeemCode: res.redeemCode || null,
|
redeemCode: res.redeemCode || null,
|
||||||
isBind: res.isBind ?? null,
|
isBind: res.isBind ?? null,
|
||||||
@@ -291,10 +298,8 @@ async function handleRefund() {
|
|||||||
$bar-height: 120rpx;
|
$bar-height: 120rpx;
|
||||||
|
|
||||||
.page {
|
.page {
|
||||||
min-height: 100vh;
|
|
||||||
background: $color-background;
|
background: $color-background;
|
||||||
padding: $spacing-3;
|
padding: $spacing-3;
|
||||||
padding-bottom: calc(#{$bar-height} + env(safe-area-inset-bottom) + #{$spacing-3});
|
|
||||||
font-family: $font-family-body;
|
font-family: $font-family-body;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,11 @@
|
|||||||
<view class="card-product">
|
<view class="card-product">
|
||||||
<view class="product-name-row">
|
<view class="product-name-row">
|
||||||
<text class="product-name">超级玩家卡</text>
|
<text class="product-name">超级玩家卡</text>
|
||||||
<view class="price-current">
|
<view v-if="order.thirdOrderNo" class="third-order">
|
||||||
|
<text class="third-order__label">核销单号:</text>
|
||||||
|
<text class="third-order__no">{{ order.thirdOrderNo }}</text>
|
||||||
|
</view>
|
||||||
|
<view v-else class="price-current">
|
||||||
<text class="price-symbol">¥</text>
|
<text class="price-symbol">¥</text>
|
||||||
<text class="price-value">{{ (order.payAmount / 100).toFixed(2) }}</text>
|
<text class="price-value">{{ (order.payAmount / 100).toFixed(2) }}</text>
|
||||||
</view>
|
</view>
|
||||||
@@ -67,7 +71,7 @@
|
|||||||
<!-- 底部信息行 -->
|
<!-- 底部信息行 -->
|
||||||
<view class="card-info">
|
<view class="card-info">
|
||||||
<text class="info-time">{{ order.createTime }}</text>
|
<text class="info-time">{{ order.createTime }}</text>
|
||||||
<text class="info-pay">{{ order.payChannel }}</text>
|
<text class="info-pay">{{ order.thirdOrderNo ? '抖音核销' : order.payChannel }}</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 操作按钮 -->
|
<!-- 操作按钮 -->
|
||||||
@@ -76,7 +80,7 @@
|
|||||||
<text class="btn-primary__text">立即支付</text>
|
<text class="btn-primary__text">立即支付</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="card-actions" v-else-if="order.status === 'activated'">
|
<view class="card-actions" v-else-if="order.status === 'activated' && !order.thirdOrderNo">
|
||||||
<view class="btn-outline" @tap.stop="showRefundModal(order)">
|
<view class="btn-outline" @tap.stop="showRefundModal(order)">
|
||||||
<text class="btn-outline__text">申请退款</text>
|
<text class="btn-outline__text">申请退款</text>
|
||||||
</view>
|
</view>
|
||||||
@@ -135,6 +139,7 @@ interface OrderItem {
|
|||||||
payAmount: number
|
payAmount: number
|
||||||
redeemCode: string | null
|
redeemCode: string | null
|
||||||
payChannel: string
|
payChannel: string
|
||||||
|
thirdOrderNo: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
@@ -195,6 +200,7 @@ async function fetchOrders(append = false) {
|
|||||||
payAmount: item.payAmount || 0,
|
payAmount: item.payAmount || 0,
|
||||||
redeemCode: item.redeemCode || null,
|
redeemCode: item.redeemCode || null,
|
||||||
payChannel: item.payChannel || '微信支付',
|
payChannel: item.payChannel || '微信支付',
|
||||||
|
thirdOrderNo: item.thirdOrderNo || '',
|
||||||
}))
|
}))
|
||||||
if (append) {
|
if (append) {
|
||||||
orders.value.push(...list)
|
orders.value.push(...list)
|
||||||
@@ -464,6 +470,30 @@ onShow(() => {
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 抖音核销单号 */
|
||||||
|
.third-order {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
flex-shrink: 0;
|
||||||
|
max-width: 60%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.third-order__label {
|
||||||
|
font-family: $font-family-body;
|
||||||
|
font-size: $font-size-sm;
|
||||||
|
color: $uv-tips-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.third-order__no {
|
||||||
|
font-family: $font-family-body;
|
||||||
|
font-size: $font-size-sm;
|
||||||
|
font-weight: $font-weight-medium;
|
||||||
|
color: $uv-main-color;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
/* Info Row */
|
/* Info Row */
|
||||||
.card-info {
|
.card-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -74,26 +74,37 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="user-card">
|
<view class="user-card">
|
||||||
<text class="section-title">预约人信息</text>
|
<text class="section-title">选择预约人</text>
|
||||||
<view class="user-row">
|
<view v-if="selectedCards.length > 0" class="person-list">
|
||||||
<text class="user-label">姓名</text>
|
<view
|
||||||
<text class="user-value">{{ userName }}</text>
|
v-for="c in selectedCards"
|
||||||
|
:key="c.cardNumber"
|
||||||
|
class="person-item"
|
||||||
|
:class="{
|
||||||
|
'person-item--active': selectedCard?.cardNumber === c.cardNumber && c.reserveStatus !== 'reserved',
|
||||||
|
'person-item--disabled': c.reserveStatus === 'reserved'
|
||||||
|
}"
|
||||||
|
@tap="c.reserveStatus !== 'reserved' && (selectedCard = c)"
|
||||||
|
>
|
||||||
|
<view class="person-info">
|
||||||
|
<text class="person-name">{{ c.name }}</text>
|
||||||
|
<text class="person-id">{{ maskId(c.identityNo) }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="user-row">
|
<text v-if="c.reserveStatus === 'reserved'" class="person-status">已预约</text>
|
||||||
<text class="user-label">身份证号</text>
|
<view v-else class="radio" :class="{ 'radio--on': selectedCard?.cardNumber === c.cardNumber }">
|
||||||
<text class="user-value">{{ userIdCard }}</text>
|
<view v-if="selectedCard?.cardNumber === c.cardNumber" class="radio-dot" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-else-if="selectedDate" class="no-person-tip">
|
||||||
|
<text class="no-person-tip__text">当日无可用预约人</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="user-warn">预约信息必须与持卡实名一致,不可更换他人</text>
|
<text class="user-warn">预约信息必须与持卡实名一致,不可更换他人</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="notice-card" v-if="bookingNotice">
|
<view class="notice-card" v-if="bookingNotice">
|
||||||
<text class="notice-title">预约须知</text>
|
<text class="notice-title">预约须知</text>
|
||||||
<view class="notice-list">
|
<rich-text class="notice-html" :nodes="bookingNoticeHtml" />
|
||||||
<view class="notice-item">
|
|
||||||
<view class="notice-dot" />
|
|
||||||
<text class="notice-text">{{ bookingNotice }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<view class="notice-link" @tap="goFaq">
|
<view class="notice-link" @tap="goFaq">
|
||||||
<text class="notice-link__text">查看完整FAQ</text>
|
<text class="notice-link__text">查看完整FAQ</text>
|
||||||
</view>
|
</view>
|
||||||
@@ -115,7 +126,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
import { onLoad } from '@dcloudio/uni-app'
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
import { post } from '../../../../api/request'
|
import { post } from '../../../../api/request'
|
||||||
import { BASE_URL } from '../../../../config'
|
import { BASE_URL } from '../../../../config'
|
||||||
@@ -126,12 +137,22 @@ interface ReserveOptionSession {
|
|||||||
stock: number
|
stock: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CardOption {
|
||||||
|
cardNumber: string
|
||||||
|
name: string
|
||||||
|
identityNo: string
|
||||||
|
expiresTime: string
|
||||||
|
reserveStatus: string
|
||||||
|
appointmentId: number | null
|
||||||
|
}
|
||||||
|
|
||||||
interface DateOption {
|
interface DateOption {
|
||||||
key: string
|
key: string
|
||||||
week: string
|
week: string
|
||||||
label: string
|
label: string
|
||||||
stock: number | null
|
stock: number | null
|
||||||
sessions: ReserveOptionSession[]
|
sessions: ReserveOptionSession[]
|
||||||
|
cards: CardOption[]
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
reason?: string
|
reason?: string
|
||||||
}
|
}
|
||||||
@@ -142,6 +163,26 @@ const userIdCard = ref('')
|
|||||||
const bookingNotice = ref('')
|
const bookingNotice = ref('')
|
||||||
const cardModelName = ref('')
|
const cardModelName = ref('')
|
||||||
const appointmentDays = ref<number | null>(null)
|
const appointmentDays = ref<number | null>(null)
|
||||||
|
const selectedCard = ref<CardOption | null>(null)
|
||||||
|
|
||||||
|
function maskId(id: string): string {
|
||||||
|
if (!id) return ''
|
||||||
|
return id.length >= 15 ? `${id.slice(0, 6)}****${id.slice(-4)}` : id
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 富文本 HTML 清洗:给所有 img 加 max-width:100%,防止超出容器边界 */
|
||||||
|
function fixRichHtml(html: string): string {
|
||||||
|
if (!html) return ''
|
||||||
|
const style = 'max-width:100%;height:auto;display:block;'
|
||||||
|
return html.replace(/<img(?![^>]*max-width)[^>]*>/gi, (tag: string) => {
|
||||||
|
if (/style="[^"]*"/i.test(tag)) {
|
||||||
|
return tag.replace(/style="([^"]*)"/i, (_: string, s: string) => `style="${s};${style}"`)
|
||||||
|
}
|
||||||
|
return tag.replace(/<img/i, `<img style="${style}"`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const bookingNoticeHtml = computed(() => fixRichHtml(bookingNotice.value))
|
||||||
|
|
||||||
const reserveParams = ref({
|
const reserveParams = ref({
|
||||||
scenicId: 0,
|
scenicId: 0,
|
||||||
@@ -194,6 +235,16 @@ const selectedDateStock = computed(() => {
|
|||||||
return d.stock == null ? 1 : d.stock
|
return d.stock == null ? 1 : d.stock
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const selectedCards = computed(() => {
|
||||||
|
if (!selectedDate.value) return []
|
||||||
|
return dateOptions.value.find(d => d.key === selectedDate.value)?.cards || []
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(selectedDate, () => {
|
||||||
|
const available = selectedCards.value.find(c => c.reserveStatus !== 'reserved')
|
||||||
|
selectedCard.value = available || null
|
||||||
|
})
|
||||||
|
|
||||||
onLoad(async (query: any) => {
|
onLoad(async (query: any) => {
|
||||||
const resourceId = query?.resourceId
|
const resourceId = query?.resourceId
|
||||||
if (!resourceId) return
|
if (!resourceId) return
|
||||||
@@ -216,15 +267,15 @@ onLoad(async (query: any) => {
|
|||||||
}
|
}
|
||||||
userName.value = res.userName || ''
|
userName.value = res.userName || ''
|
||||||
userIdCard.value = res.userIdCard || ''
|
userIdCard.value = res.userIdCard || ''
|
||||||
bookingNotice.value = Array.isArray(res.bookingNotice) ? res.bookingNotice.join('\n') : (res.bookingNotice || '')
|
bookingNotice.value = Array.isArray(res.bookingNotice) ? res.bookingNotice.map((n: any) => (typeof n === 'string' ? n : n?.content || n?.text || '')).join('<br/>') : (res.bookingNotice || '')
|
||||||
cardModelName.value = res.cardModelName || ''
|
cardModelName.value = res.cardModelName || ''
|
||||||
appointmentDays.value = (res.dates || []).length
|
appointmentDays.value = (res.dates || []).length
|
||||||
reserveParams.value = {
|
reserveParams.value = {
|
||||||
scenicId: parseInt(resourceId),
|
scenicId: parseInt(resourceId),
|
||||||
channel: 'Channel_SZH',
|
channel: 'Channel_SZH',
|
||||||
thirdId: cardModelId,
|
thirdId: String(res.thirdId || res.cardModelId || cardModelId),
|
||||||
cardNum: cardNumber,
|
cardNum: res.cardNum || '',
|
||||||
mobile: phone,
|
mobile: res.phone || phone,
|
||||||
cardModelName: res.cardModelName || ''
|
cardModelName: res.cardModelName || ''
|
||||||
}
|
}
|
||||||
dateOptions.value = (res.dates || []).map((d: any) => ({
|
dateOptions.value = (res.dates || []).map((d: any) => ({
|
||||||
@@ -233,6 +284,7 @@ onLoad(async (query: any) => {
|
|||||||
label: d.label,
|
label: d.label,
|
||||||
stock: d.stock,
|
stock: d.stock,
|
||||||
sessions: d.sessions || [],
|
sessions: d.sessions || [],
|
||||||
|
cards: d.cards || [],
|
||||||
disabled: d.disabled,
|
disabled: d.disabled,
|
||||||
reason: d.reason,
|
reason: d.reason,
|
||||||
}))
|
}))
|
||||||
@@ -252,6 +304,10 @@ onLoad(async (query: any) => {
|
|||||||
{ id: 's5', time: '12:00-18:00', stock: 100 },
|
{ id: 's5', time: '12:00-18:00', stock: 100 },
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
const mockCards: CardOption[] = [
|
||||||
|
{ cardNumber: 'CJ0000442472', name: '易佳欣', identityNo: '360312199906103124', expiresTime: '2027-08-05 00:00:00', reserveStatus: 'none', appointmentId: null },
|
||||||
|
{ cardNumber: 'CJ0000462004', name: '彭丽萍', identityNo: '360322200312042545', expiresTime: '2027-08-05 00:00:00', reserveStatus: 'none', appointmentId: null },
|
||||||
|
]
|
||||||
dateOptions.value = Array.from({ length: 3 }, (_, i) => {
|
dateOptions.value = Array.from({ length: 3 }, (_, i) => {
|
||||||
const d = new Date()
|
const d = new Date()
|
||||||
d.setDate(d.getDate() + i + 1)
|
d.setDate(d.getDate() + i + 1)
|
||||||
@@ -265,6 +321,7 @@ onLoad(async (query: any) => {
|
|||||||
label,
|
label,
|
||||||
stock: mockStocks[i],
|
stock: mockStocks[i],
|
||||||
sessions: mockSessions[`mock${i}`] || [],
|
sessions: mockSessions[`mock${i}`] || [],
|
||||||
|
cards: mockCards.map(c => i === 0 && c.cardNumber === 'CJ0000442472' ? { ...c, reserveStatus: 'reserved', appointmentId: 834707842323141 } : c),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
appointmentDays.value = 3
|
appointmentDays.value = 3
|
||||||
@@ -274,10 +331,18 @@ onLoad(async (query: any) => {
|
|||||||
hours: '08:00-18:00',
|
hours: '08:00-18:00',
|
||||||
image: 'https://images.unsplash.com/photo-1513415564515-763d91423bdd?fm=jpg&q=80&w=800&h=400&fit=crop',
|
image: 'https://images.unsplash.com/photo-1513415564515-763d91423bdd?fm=jpg&q=80&w=800&h=400&fit=crop',
|
||||||
}
|
}
|
||||||
bookingNotice.value = '请提前10分钟到达景区检票口;\n入园需出示预约码和身份证;\n如无法按时入园请提前取消预约。'
|
bookingNotice.value = '<p>请提前10分钟到达景区检票口;</p><p>入园需出示预约码和身份证;</p><p>如无法按时入园请提前取消预约。</p>'
|
||||||
userName.value = '张三'
|
userName.value = '易佳欣'
|
||||||
userIdCard.value = '360302199001011234'
|
userIdCard.value = '360312199906103124'
|
||||||
cardModelName.value = '赣湘全域旅游年卡'
|
cardModelName.value = '赣湘全域旅游年卡'
|
||||||
|
reserveParams.value = {
|
||||||
|
scenicId: parseInt(resourceId),
|
||||||
|
channel: 'Channel_SZH',
|
||||||
|
thirdId: cardModelId,
|
||||||
|
cardNum: 'CJ0000442472',
|
||||||
|
mobile: phone,
|
||||||
|
cardModelName: '赣湘全域旅游年卡'
|
||||||
|
}
|
||||||
const firstAvailable = dateOptions.value.find(d => d.stock == null || d.stock > 0)
|
const firstAvailable = dateOptions.value.find(d => d.stock == null || d.stock > 0)
|
||||||
if (firstAvailable) selectedDate.value = firstAvailable.key
|
if (firstAvailable) selectedDate.value = firstAvailable.key
|
||||||
}
|
}
|
||||||
@@ -285,6 +350,10 @@ onLoad(async (query: any) => {
|
|||||||
|
|
||||||
async function handleSubmit() {
|
async function handleSubmit() {
|
||||||
if (!selectedDate.value || submitting.value) return
|
if (!selectedDate.value || submitting.value) return
|
||||||
|
if (!selectedCard.value) {
|
||||||
|
uni.showToast({ title: '请选择预约人', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const dateObj = dateOptions.value.find(d => d.key === selectedDate.value)
|
const dateObj = dateOptions.value.find(d => d.key === selectedDate.value)
|
||||||
if (dateObj?.disabled) {
|
if (dateObj?.disabled) {
|
||||||
@@ -300,22 +369,23 @@ async function handleSubmit() {
|
|||||||
try {
|
try {
|
||||||
const session = selectedSessions.value.find(s => s.id === selectedSession.value)
|
const session = selectedSessions.value.find(s => s.id === selectedSession.value)
|
||||||
const p = reserveParams.value
|
const p = reserveParams.value
|
||||||
|
const card = selectedCard.value
|
||||||
const result = await post<any>(`${BASE_URL}/zhy-api/cardBag/appoint`, {
|
const result = await post<any>(`${BASE_URL}/zhy-api/cardBag/appoint`, {
|
||||||
scenicId: String(p.scenicId),
|
scenicId: String(p.scenicId),
|
||||||
channel: p.channel,
|
channel: p.channel,
|
||||||
thirdId: p.thirdId,
|
thirdId: p.thirdId,
|
||||||
cardNum: p.cardNum,
|
cardNum: card.cardNumber,
|
||||||
mobile: p.mobile,
|
mobile: p.mobile,
|
||||||
cardModelName: p.cardModelName,
|
cardModelName: p.cardModelName,
|
||||||
appointDate: selectedDate.value,
|
appointDate: selectedDate.value,
|
||||||
timeId: session ? String(session.id) : '',
|
timeId: session ? String(session.id) : '',
|
||||||
name: userName.value,
|
name: card.name,
|
||||||
idNo: userIdCard.value,
|
idNo: card.identityNo,
|
||||||
})
|
})
|
||||||
uni.showToast({ title: '预约成功', icon: 'success' })
|
uni.showToast({ title: '预约成功', icon: 'success' })
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: `/packages/user/pages/reserve/success?appointId=${result}&cardNumber=${p.cardNum}&cardModelId=${p.thirdId}`
|
url: `/packages/user/pages/reserve/success?appointId=${result}&cardNumber=${card.cardNumber}&cardModelId=${p.thirdId}`
|
||||||
})
|
})
|
||||||
}, 1500)
|
}, 1500)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@@ -648,6 +718,88 @@ $bar-height: 120rpx;
|
|||||||
margin-top: 12rpx;
|
margin-top: 12rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.person-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.person-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 24rpx;
|
||||||
|
border: 2rpx solid $uv-border-color;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
transition: all $transition-fast;
|
||||||
|
|
||||||
|
&--active {
|
||||||
|
border-color: $uv-primary;
|
||||||
|
background: rgba(60, 156, 255, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.person-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.person-name {
|
||||||
|
font-family: $font-family-body;
|
||||||
|
font-size: $font-size-base;
|
||||||
|
font-weight: $font-weight-medium;
|
||||||
|
color: $uv-main-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.person-id {
|
||||||
|
font-family: $font-family-body;
|
||||||
|
font-size: $font-size-sm;
|
||||||
|
color: $uv-tips-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.person-status {
|
||||||
|
font-family: $font-family-body;
|
||||||
|
font-size: $font-size-sm;
|
||||||
|
color: $uv-placeholder-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio {
|
||||||
|
width: 34rpx;
|
||||||
|
height: 34rpx;
|
||||||
|
border: 2rpx solid $uv-light-color;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
&--on {
|
||||||
|
border-color: $uv-primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-dot {
|
||||||
|
width: 18rpx;
|
||||||
|
height: 18rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: $uv-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-person-tip {
|
||||||
|
padding: 16rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-person-tip__text {
|
||||||
|
font-family: $font-family-body;
|
||||||
|
font-size: $font-size-sm;
|
||||||
|
color: $uv-placeholder-color;
|
||||||
|
}
|
||||||
|
|
||||||
.notice-card {
|
.notice-card {
|
||||||
margin: 24rpx 32rpx 0;
|
margin: 24rpx 32rpx 0;
|
||||||
background: #eef4ff;
|
background: #eef4ff;
|
||||||
@@ -668,27 +820,18 @@ $bar-height: 120rpx;
|
|||||||
margin-bottom: 16rpx;
|
margin-bottom: 16rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notice-item {
|
.notice-html {
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
margin-bottom: 12rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notice-dot {
|
|
||||||
width: 8rpx;
|
|
||||||
height: 8rpx;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: $uv-primary;
|
|
||||||
flex-shrink: 0;
|
|
||||||
margin-top: 14rpx;
|
|
||||||
margin-right: 12rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notice-text {
|
|
||||||
font-family: $font-family-body;
|
font-family: $font-family-body;
|
||||||
font-size: $font-size-sm;
|
font-size: $font-size-sm;
|
||||||
color: $uv-tips-color;
|
color: $uv-tips-color;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
word-break: break-all;
|
||||||
|
|
||||||
|
:deep(p) {
|
||||||
|
margin: 0 0 8rpx;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.notice-link {
|
.notice-link {
|
||||||
|
|||||||
@@ -63,14 +63,14 @@
|
|||||||
<!-- 操作 -->
|
<!-- 操作 -->
|
||||||
<view class="record-actions">
|
<view class="record-actions">
|
||||||
<view
|
<view
|
||||||
v-if="r.status === 'pending' && canCancel(r.date)"
|
v-if="r.canCancel"
|
||||||
class="btn-sm btn-sm--outline"
|
class="btn-sm btn-sm--outline"
|
||||||
@tap="showCancelModal(r)"
|
@tap="showCancelModal(r)"
|
||||||
>
|
>
|
||||||
<text class="btn-sm__text--outline">取消预约</text>
|
<text class="btn-sm__text--outline">取消预约</text>
|
||||||
</view>
|
</view>
|
||||||
<view
|
<view
|
||||||
v-if="r.status === 'pending'"
|
v-if="r.showEntryCode && r.status !== 'cancelled'"
|
||||||
class="btn-sm btn-sm--outline"
|
class="btn-sm btn-sm--outline"
|
||||||
@tap="showCode(r)"
|
@tap="showCode(r)"
|
||||||
>
|
>
|
||||||
@@ -215,6 +215,8 @@ interface ReserveRecord {
|
|||||||
verifyPhoto?: string
|
verifyPhoto?: string
|
||||||
cardInstruction?: string
|
cardInstruction?: string
|
||||||
cardModelId?: string
|
cardModelId?: string
|
||||||
|
canCancel?: boolean
|
||||||
|
showEntryCode?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
@@ -288,6 +290,8 @@ async function fetchRecords(append = false) {
|
|||||||
verifyPhoto: item.verifyPhoto || '',
|
verifyPhoto: item.verifyPhoto || '',
|
||||||
cardInstruction: item.cardInstruction || '',
|
cardInstruction: item.cardInstruction || '',
|
||||||
cardModelId: item.cardModelId || item.thirdId || '',
|
cardModelId: item.cardModelId || item.thirdId || '',
|
||||||
|
canCancel: !!item.canCancel,
|
||||||
|
showEntryCode: !!item.showEntryCode,
|
||||||
}))
|
}))
|
||||||
if (append) {
|
if (append) {
|
||||||
records.value.push(...list)
|
records.value.push(...list)
|
||||||
@@ -321,13 +325,6 @@ watch(activeTab, () => {
|
|||||||
fetchRecords()
|
fetchRecords()
|
||||||
})
|
})
|
||||||
|
|
||||||
function canCancel(date: string) {
|
|
||||||
const today = new Date()
|
|
||||||
today.setHours(0, 0, 0, 0)
|
|
||||||
const d = new Date(date)
|
|
||||||
return (d.getTime() - today.getTime()) / (1000 * 60 * 60 * 24) >= 1
|
|
||||||
}
|
|
||||||
|
|
||||||
function showCancelModal(r: ReserveRecord) {
|
function showCancelModal(r: ReserveRecord) {
|
||||||
cancelRecord.value = r
|
cancelRecord.value = r
|
||||||
cancelPopup.value?.open()
|
cancelPopup.value?.open()
|
||||||
@@ -556,11 +553,18 @@ function previewPhoto(url: string) {
|
|||||||
font-size: $font-size-base;
|
font-size: $font-size-base;
|
||||||
font-weight: $font-weight-medium;
|
font-weight: $font-weight-medium;
|
||||||
color: $uv-main-color;
|
color: $uv-main-color;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
margin-right: 16rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-tag {
|
.status-tag {
|
||||||
padding: 4rpx 14rpx;
|
padding: 4rpx 14rpx;
|
||||||
border-radius: 8rpx;
|
border-radius: 8rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
&--pending { background: #e8f3ff; }
|
&--pending { background: #e8f3ff; }
|
||||||
&--cancelled { background: $uv-bg-color; }
|
&--cancelled { background: $uv-bg-color; }
|
||||||
|
|||||||
@@ -38,13 +38,7 @@
|
|||||||
<text class="info-label">预约编号</text>
|
<text class="info-label">预约编号</text>
|
||||||
<text class="info-value">{{ reserveId }}</text>
|
<text class="info-value">{{ reserveId }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view v-if="status === 1" class="info-row info-row--last">
|
<view v-if="status !== 1" class="info-row info-row--last">
|
||||||
<text class="info-label">入园凭证</text>
|
|
||||||
<view class="code-btn" @tap="showCode">
|
|
||||||
<text class="code-btn__text">查看入园码</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<view v-else class="info-row info-row--last">
|
|
||||||
<text class="info-label">预约状态</text>
|
<text class="info-label">预约状态</text>
|
||||||
<text class="info-value">{{ statusLabel }}</text>
|
<text class="info-value">{{ statusLabel }}</text>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
+8
-3
@@ -15,7 +15,7 @@
|
|||||||
{
|
{
|
||||||
"path": "pages/qrcode/index",
|
"path": "pages/qrcode/index",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "入园码"
|
"navigationBarTitleText": "旅游卡"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -155,8 +155,7 @@
|
|||||||
{
|
{
|
||||||
"path": "pages/scenic-list",
|
"path": "pages/scenic-list",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationStyle": "custom",
|
"navigationBarTitleText": "全部合作景区"
|
||||||
"navigationBarTitleText": ""
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -171,6 +170,12 @@
|
|||||||
"navigationBarTitleText": "通知公告"
|
"navigationBarTitleText": "通知公告"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/notice-detail",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "公告详情"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/usage-guide",
|
"path": "pages/usage-guide",
|
||||||
"style": {
|
"style": {
|
||||||
|
|||||||
+47
-31
@@ -66,15 +66,15 @@
|
|||||||
<view class="promo-price-row">
|
<view class="promo-price-row">
|
||||||
<view class="promo-price-block">
|
<view class="promo-price-block">
|
||||||
<text class="promo-currency">¥</text>
|
<text class="promo-currency">¥</text>
|
||||||
<text class="promo-price">198</text>
|
<text class="promo-price">{{ promoPrice }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="promo-value-block">
|
<view class="promo-value-block">
|
||||||
<text class="promo-value">¥3800+</text>
|
<text class="promo-value">{{ promoShowPrice }}</text>
|
||||||
<text class="promo-save">立省1万+</text>
|
<text v-if="promoSave" class="promo-save">{{ promoSave }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<text class="promo-desc">一卡畅玩赣湘边核心景区</text>
|
<text class="promo-desc">一卡畅玩赣湘边核心景区</text>
|
||||||
<text class="promo-desc-sub">含武功山/君澜温泉/方特/橘子洲小火车/湘江游轮等</text>
|
<text class="promo-desc-sub">含武功山/方特/橘子洲小火车/湘江游轮等</text>
|
||||||
<text class="promo-highlight">单次最高可省299 | 单景区全年最高可免6次</text>
|
<text class="promo-highlight">单次最高可省299 | 单景区全年最高可免6次</text>
|
||||||
<view class="promo-cta">
|
<view class="promo-cta">
|
||||||
<text class="cta-label">立即抢购|专属特惠</text>
|
<text class="cta-label">立即抢购|专属特惠</text>
|
||||||
@@ -113,7 +113,7 @@
|
|||||||
<view class="spot-body">
|
<view class="spot-body">
|
||||||
<text class="spot-name">{{ spot.name }}</text>
|
<text class="spot-name">{{ spot.name }}</text>
|
||||||
<view class="spot-meta">
|
<view class="spot-meta">
|
||||||
<text class="spot-price">¥{{ spot.price }}</text>
|
<text v-if="spot.price && Number(spot.price) > 0" class="spot-price">¥{{ spot.price }}</text>
|
||||||
<view class="spot-benefit">
|
<view class="spot-benefit">
|
||||||
<text>{{ spot.benefit }}</text>
|
<text>{{ spot.benefit }}</text>
|
||||||
</view>
|
</view>
|
||||||
@@ -125,20 +125,14 @@
|
|||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
|
|
||||||
<!-- 公告弹窗 -->
|
|
||||||
<AnnouncementModal
|
|
||||||
v-model="showNoticeModal"
|
|
||||||
:title="noticeModalTitle"
|
|
||||||
:body="noticeModalBody"
|
|
||||||
/>
|
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
|
import { onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
|
||||||
import AnnouncementModal from '../../components/announcement-modal.vue'
|
|
||||||
import { post } from '../../api/request'
|
import { post } from '../../api/request'
|
||||||
|
import { BASE_URL } from '../../config'
|
||||||
import type { BannerItem } from '../../api/types'
|
import type { BannerItem } from '../../api/types'
|
||||||
import { useUserStore } from '../../store/user'
|
import { useUserStore } from '../../store/user'
|
||||||
|
|
||||||
@@ -161,12 +155,40 @@ const displayNotices = computed(() =>
|
|||||||
)
|
)
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
const CARD_GOODS_ID = '821926819630853'
|
||||||
|
const promoPrice = ref('198')
|
||||||
|
const promoShowPrice = ref('¥3800+')
|
||||||
|
const promoSave = ref('立省1万+')
|
||||||
|
|
||||||
|
function formatPrice(cents: number): string {
|
||||||
|
const n = cents / 100
|
||||||
|
return Number.isInteger(n) ? String(n) : n.toFixed(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchCardPrice() {
|
||||||
|
try {
|
||||||
|
const data = await post<any>(`${BASE_URL}/open-api/xgh/cardModelDetail?goodsId=${CARD_GOODS_ID}`, {})
|
||||||
|
const sku = data?.sku?.[0]
|
||||||
|
const buy = sku?.buyPrice || 0
|
||||||
|
const show = sku?.showPrice || 0
|
||||||
|
if (buy > 0) promoPrice.value = formatPrice(buy)
|
||||||
|
if (show > 0) {
|
||||||
|
promoShowPrice.value = `¥${formatPrice(show)}`
|
||||||
|
const save = show - buy
|
||||||
|
promoSave.value = save > 0 ? `立省${formatPrice(save)}` : ''
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 保留默认展示值
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
fetchCardPrice()
|
||||||
try {
|
try {
|
||||||
const [bannerData, noticeData, spotsData] = await Promise.all([
|
const [bannerData, noticeData, spotsData] = await Promise.all([
|
||||||
post<BannerItem[]>('/mp-api/mpContent/queryList', { channel: 'pingxiang', type: 'BANNER' }),
|
post<BannerItem[]>('/mp-api/mpContent/queryList', { channel: 'pingxiang', type: 'BANNER' }),
|
||||||
post<BannerItem[]>('/mp-api/mpContent/queryList', { channel: 'pingxiang', type: 'NOTICE' }),
|
post<BannerItem[]>('/mp-api/mpContent/queryList', { channel: 'pingxiang', type: 'NOTICE' }),
|
||||||
post<any>('/mp-api/mpScenic/queryScenicList', { channel: 'pingxiang', pageNo: 1, pageSize: 6, phone: uni.getStorageSync('phone') || '' }),
|
post<any>('/mp-api/mpScenic/queryScenicList', { channel: 'pingxiang', pageNo: 1, pageSize: 8, phone: uni.getStorageSync('phone') || '' }),
|
||||||
])
|
])
|
||||||
banners.value = bannerData || []
|
banners.value = bannerData || []
|
||||||
announcements.value = (noticeData || []).map(n => ({
|
announcements.value = (noticeData || []).map(n => ({
|
||||||
@@ -174,14 +196,14 @@ onMounted(async () => {
|
|||||||
typeLabel: '公告',
|
typeLabel: '公告',
|
||||||
}))
|
}))
|
||||||
const list = spotsData?.list || spotsData || []
|
const list = spotsData?.list || spotsData || []
|
||||||
hotSpots.value = (Array.isArray(list) ? list : []).slice(0, 6).map((s: any) => ({
|
hotSpots.value = (Array.isArray(list) ? list : []).slice(0, 8).map((s: any) => ({
|
||||||
id: s.id || s.scenicId,
|
id: s.id || s.scenicId,
|
||||||
shopId: s.shopId || s.id || s.scenicId,
|
shopId: s.shopId || s.id || s.scenicId,
|
||||||
name: s.name || s.scenicName,
|
name: s.name || s.scenicName,
|
||||||
price: ((s.price || s.ticketPrice || 0) / 100).toFixed(0),
|
price: ((s.price || s.ticketPrice || 0) / 100).toFixed(0),
|
||||||
benefit: s.benefit || `免门票`,
|
benefit: s.preferential === 'DISCOUNT' ? '享折扣' : s.preferential === 'PREFERENTIAL' ? '享优惠' : '免门票',
|
||||||
region: s.region?.name || s.region || s.city || '',
|
region: s.region?.name || s.region || s.city || '',
|
||||||
image: s.image || s.pic || s.coverImage || '',
|
image: s.image || s.pic || s.coverImage || 'https://aliyunoss.hn12301.net/pxmall/px_jqbj.png',
|
||||||
}))
|
}))
|
||||||
} catch {
|
} catch {
|
||||||
// 静默失败
|
// 静默失败
|
||||||
@@ -213,11 +235,11 @@ function onBannerClick(banner: BannerItem) {
|
|||||||
|
|
||||||
/** 金刚区操作项 */
|
/** 金刚区操作项 */
|
||||||
const quickActions = [
|
const quickActions = [
|
||||||
|
{ label: '抖音核销', icon: 'https://aliyunoss.hn12301.net/pxmall/douyin.png' },
|
||||||
{ label: '我要绑卡', icon: 'https://aliyunoss.hn12301.net/pxmall/card.png' },
|
{ label: '我要绑卡', icon: 'https://aliyunoss.hn12301.net/pxmall/card.png' },
|
||||||
{ label: '我要预约', icon: 'https://aliyunoss.hn12301.net/pxmall/reserve.png' },
|
{ label: '我要预约', icon: 'https://aliyunoss.hn12301.net/pxmall/reserve.png' },
|
||||||
{ label: '我要入园', icon: 'https://aliyunoss.hn12301.net/pxmall/check.png' },
|
{ label: '我要入园', icon: 'https://aliyunoss.hn12301.net/pxmall/check.png' },
|
||||||
{ label: '常见问题', icon: 'https://aliyunoss.hn12301.net/pxmall/faq.png' },
|
{ label: '常见问题', icon: 'https://aliyunoss.hn12301.net/pxmall/faq.png' },
|
||||||
{ label: '抖音核销', icon: 'https://aliyunoss.hn12301.net/pxmall/douyin.png' },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
/** 处理金刚区点击 */
|
/** 处理金刚区点击 */
|
||||||
@@ -243,10 +265,6 @@ function onQuickAction(item: { label: string }) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (item.label === '抖音核销') {
|
if (item.label === '抖音核销') {
|
||||||
if (!userStore.isLogin) {
|
|
||||||
uni.navigateTo({ url: '/pages/login/login' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
uni.navigateTo({ url: '/packages/user/pages/douyin-verify' })
|
uni.navigateTo({ url: '/packages/user/pages/douyin-verify' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -265,16 +283,14 @@ function onNoticeTouchStart() { swiperAutoplay.value = false }
|
|||||||
/** 触摸结束后恢复自动播放 */
|
/** 触摸结束后恢复自动播放 */
|
||||||
function onNoticeTouchEnd() { swiperAutoplay.value = true }
|
function onNoticeTouchEnd() { swiperAutoplay.value = true }
|
||||||
|
|
||||||
/** 公告弹窗 */
|
/** 打开公告详情页 */
|
||||||
const showNoticeModal = ref(false)
|
|
||||||
const noticeModalTitle = ref('公告')
|
|
||||||
const noticeModalBody = ref('')
|
|
||||||
|
|
||||||
/** 打开公告详情弹窗 */
|
|
||||||
function openNotice(item: BannerItem & { typeLabel: string }) {
|
function openNotice(item: BannerItem & { typeLabel: string }) {
|
||||||
noticeModalTitle.value = `${item.typeLabel}`
|
uni.setStorageSync('notice_detail', {
|
||||||
noticeModalBody.value = item.content || ''
|
title: item.title,
|
||||||
showNoticeModal.value = true
|
content: item.content || '',
|
||||||
|
videoUrl: (item as any).videoUrl || '',
|
||||||
|
})
|
||||||
|
uni.navigateTo({ url: '/packages/content/pages/notice-detail' })
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 跳转到公告列表 */
|
/** 跳转到公告列表 */
|
||||||
@@ -284,7 +300,7 @@ function goMoreNotice() {
|
|||||||
|
|
||||||
/** 跳转到年卡购买页 */
|
/** 跳转到年卡购买页 */
|
||||||
function goCardDetail() {
|
function goCardDetail() {
|
||||||
uni.navigateTo({ url: '/packages/user/pages/buy?goodsId=821926819630853' })
|
uni.navigateTo({ url: `/packages/user/pages/buy?goodsId=${CARD_GOODS_ID}` })
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 跳转到联名卡购买页 */
|
/** 跳转到联名卡购买页 */
|
||||||
|
|||||||
@@ -66,6 +66,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
import { useUserStore } from '../../store/user'
|
import { useUserStore } from '../../store/user'
|
||||||
import { showToast } from '../../utils'
|
import { showToast } from '../../utils'
|
||||||
|
|
||||||
@@ -74,6 +75,13 @@ const agreed = ref(false)
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const showContent = ref(false)
|
const showContent = ref(false)
|
||||||
const preLoginCode = ref('')
|
const preLoginCode = ref('')
|
||||||
|
const redirect = ref('')
|
||||||
|
|
||||||
|
onLoad((query) => {
|
||||||
|
if (query?.redirect) {
|
||||||
|
redirect.value = query.redirect as string
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -137,7 +145,11 @@ async function handleLogin(e: AnyObject) {
|
|||||||
|
|
||||||
showToast('登录成功', 'success')
|
showToast('登录成功', 'success')
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
if (redirect.value) {
|
||||||
|
uni.redirectTo({ url: redirect.value })
|
||||||
|
} else {
|
||||||
uni.navigateBack()
|
uni.navigateBack()
|
||||||
|
}
|
||||||
}, 1000)
|
}, 1000)
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
showToast(err instanceof Error ? err.message : '登录失败')
|
showToast(err instanceof Error ? err.message : '登录失败')
|
||||||
|
|||||||
@@ -366,6 +366,8 @@ function formatPhone(phone?: string) {
|
|||||||
|
|
||||||
/* Logout button */
|
/* Logout button */
|
||||||
.btn-area {
|
.btn-area {
|
||||||
|
position: relative;
|
||||||
|
z-index: 3;
|
||||||
padding: $spacing-5 $spacing-3;
|
padding: $spacing-5 $spacing-3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -84,7 +84,7 @@
|
|||||||
|
|
||||||
<!-- 入园码二维码 -->
|
<!-- 入园码二维码 -->
|
||||||
<view class="qrcode-section" v-if="!showFaceModal && !isLeaving">
|
<view class="qrcode-section" v-if="!showFaceModal && !isLeaving">
|
||||||
<text class="qrcode-title">入园码</text>
|
<text class="qrcode-title">入园提示</text>
|
||||||
<view>为确保信息安全,现入园核销只能使用人脸核销。</view>
|
<view>为确保信息安全,现入园核销只能使用人脸核销。</view>
|
||||||
<!-- <view class="qrcode-box"> -->
|
<!-- <view class="qrcode-box"> -->
|
||||||
<!-- 为确保信息安全,现入园核销只能使用人脸核销。 -->
|
<!-- 为确保信息安全,现入园核销只能使用人脸核销。 -->
|
||||||
@@ -344,10 +344,10 @@ function onFaceModalConfirm() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const featureActions = [
|
const featureActions = [
|
||||||
{ label: '我要绑定', icon: 'https://aliyunoss.hntgov.net/pxmall/card.png' },
|
{ label: '我要绑定', icon: 'https://aliyunoss.hn12301.net/pxmall/card.png' },
|
||||||
{ label: '我要预约', icon: 'https://aliyunoss.hntgov.net/pxmall/reserve.png' },
|
{ label: '我要预约', icon: 'https://aliyunoss.hn12301.net/pxmall/reserve.png' },
|
||||||
{ label: '预约/使用记录', icon: 'https://aliyunoss.hntgov.net/pxmall/record.png' },
|
{ label: '预约/使用记录', icon: 'https://aliyunoss.hn12301.net/pxmall/record.png' },
|
||||||
{ label: '使用说明', icon: 'https://aliyunoss.hntgov.net/pxmall/scenic.png' },
|
{ label: '使用说明', icon: 'https://aliyunoss.hn12301.net/pxmall/scenic.png' },
|
||||||
]
|
]
|
||||||
|
|
||||||
function onFeatureClick(item: { label: string }) {
|
function onFeatureClick(item: { label: string }) {
|
||||||
@@ -413,7 +413,7 @@ onUnmounted(() => { if (timer) clearInterval(timer) })
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
.page { min-height: 100vh; background: $color-background; font-family: $font-family-body; }
|
.page { min-height: 100vh; background: $color-background; font-family: $font-family-body; }
|
||||||
.scroll-content { height: 100vh; }
|
.scroll-content { }
|
||||||
|
|
||||||
/* === 加载中 === */
|
/* === 加载中 === */
|
||||||
.loading-state {
|
.loading-state {
|
||||||
|
|||||||
Reference in New Issue
Block a user