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:
lcj
2026-08-07 10:45:53 +08:00
parent 40cebaa3a5
commit f0911c67a0
18 changed files with 1076 additions and 421 deletions
+3 -3
View File
@@ -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>
+37 -53
View File
@@ -8,25 +8,17 @@
:style="{ animationDelay: index * 60 + 'ms' }" :style="{ animationDelay: index * 60 + 'ms' }"
@click="openDetail(item)" @click="openDetail(item)"
> >
<!-- 第一行:标题 + 日期 + 箭头 --> <text class="card-title">{{ item.title }}</text>
<view class="card-header"> <view class="card-footer">
<view class="header-text"> <text class="card-date">发布时间:{{ item.date }}</text>
<text class="card-title">{{ item.title }}</text> <view class="card-action-row">
<text class="card-summary">{{ item.content }}</text> <text class="card-action">查看详情</text>
<view class="card-arrow" />
</view> </view>
<text class="card-date">{{ item.date }}</text>
<view class="card-arrow" />
</view> </view>
</view> </view>
</view> </view>
<!-- 公告详情弹窗 -->
<AnnouncementModal
v-model="showModal"
:title="modalTitle"
:body="modalBody"
/>
<!-- 底部安全区 --> <!-- 底部安全区 -->
<view class="safe-bottom" /> <view class="safe-bottom" />
</view> </view>
@@ -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;
} }
/* ====== 底部安全区 ====== */ /* ====== 底部安全区 ====== */
+26 -7
View File
@@ -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 {
+24 -111
View File
@@ -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.remainingCount > 0" class="remaining-tag"> <view v-if="scenic.preferential === 'DISCOUNT'" class="remaining-tag">
<text>全年还剩{{ scenic.remainingCount }}次</text> <text>可享{{ scenic.discountPercentage / 10 }}折优惠</text>
</view> </view>
<view v-else-if="scenic.freeCount > 0" class="remaining-tag"> <view v-else-if="scenic.preferential === 'PREFERENTIAL'" class="remaining-tag">
<text>全年最多可用{{ scenic.freeCount }}次</text> <text>可享特惠价{{ scenic.discountAmount / 100 }}元/次</text>
</view> </view>
<template v-else>
<view v-if="scenic.remainingCount > 0" class="remaining-tag">
<text>全年还剩{{ scenic.remainingCount }}次</text>
</view>
<view v-else-if="scenic.freeCount > 0" class="remaining-tag">
<text>全年最多可用{{ scenic.freeCount }}次</text>
</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;
+4 -23
View File
@@ -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 ''
} }
+132 -3
View File
@@ -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>
+378 -111
View File
@@ -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>
</template> </view>
</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
} }
fetchVouchers() if (!vouchersLoaded.value) {
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: {
userName: form.userName.trim(), orderNo: selectedVoucher.value?.id || '',
idType: docType.value, typeId: '820093438328133',
idNo: form.idNo.trim(), userName: form.userName.trim(),
faceImg: faceImg.value, mobile: phone.value,
idNo: form.idNo.trim(),
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
View File
@@ -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;
} }
+33 -3
View File
@@ -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;
+185 -42
View File
@@ -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>
<text v-if="c.reserveStatus === 'reserved'" class="person-status">已预约</text>
<view v-else class="radio" :class="{ 'radio--on': selectedCard?.cardNumber === c.cardNumber }">
<view v-if="selectedCard?.cardNumber === c.cardNumber" class="radio-dot" />
</view>
</view>
</view> </view>
<view class="user-row"> <view v-else-if="selectedDate" class="no-person-tip">
<text class="user-label">身份证号</text> <text class="no-person-tip__text">当日无可用预约人</text>
<text class="user-value">{{ userIdCard }}</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 {
+13 -9
View File
@@ -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; }
+1 -7
View File
@@ -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
View File
@@ -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": {
+48 -32
View File
@@ -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>
<!-- 公告弹窗 --> </view>
<AnnouncementModal
v-model="showNoticeModal"
:title="noticeModalTitle"
:body="noticeModalBody"
/>
</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}` })
} }
/** 跳转到联名卡购买页 */ /** 跳转到联名卡购买页 */
+13 -1
View File
@@ -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(() => {
uni.navigateBack() if (redirect.value) {
uni.redirectTo({ url: redirect.value })
} else {
uni.navigateBack()
}
}, 1000) }, 1000)
} catch (err: unknown) { } catch (err: unknown) {
showToast(err instanceof Error ? err.message : '登录失败') showToast(err instanceof Error ? err.message : '登录失败')
+2
View File
@@ -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;
} }
+6 -6
View File
@@ -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 {