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
+132 -3
View File
@@ -15,22 +15,43 @@
<text class="price-label">价格:</text>
<text class="price-symbol">¥</text>
<text class="price-value">{{ buyPrice }}</text>
<text class="price-original">¥3800+</text>
<text v-if="showPrice" class="price-original">¥{{ showPrice }}</text>
</view>
<view class="btn-buy" :class="{ 'btn-buy--disabled': paying }" @tap="handleBuy">
<text class="btn-buy__text">{{ paying ? '支付中...' : '立即购买' }}</text>
</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>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { onLoad, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
import { post, getToken } from '../../../api/request'
import { BASE_URL } from '../../../config'
import { useUserStore } from '../../../store/user'
import { createOrder, prepay } from '../../../api/order'
import type { BannerItem } from '../../../api/types'
const userStore = useUserStore()
@@ -39,10 +60,42 @@ const cardModleId = ref('')
const mainPic = ref('')
const introduction = ref('')
const buyPrice = ref('0')
const showPrice = ref('')
const skuId = ref('')
const loading = ref(true)
const paying = ref(false)
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) => {
goodsId.value = (query?.goodsId as string) || '821926819630853'
@@ -59,7 +112,7 @@ interface CardModelDetail {
name: string
goods: { id: string; mainPic: string }
goodsInfo: { introduction: string }
sku: Array<{ id: string; buyPrice: number }>
sku: Array<{ id: string; buyPrice: number; showPrice: number }>
}
onMounted(async () => {
@@ -73,6 +126,8 @@ onMounted(async () => {
skuId.value = sku?.id || ''
const price = sku?.buyPrice || 0
buyPrice.value = (price / 100).toFixed(2)
const showPriceValue = sku?.showPrice || 0
showPrice.value = showPriceValue ? (showPriceValue / 100).toFixed(2) : ''
} catch {
// 静默失败
} finally {
@@ -97,8 +152,15 @@ async function handleBuy() {
uni.navigateTo({ url: '/pages/login/login' })
return
}
if (paying.value) return
showNotice.value = true
fetchPurchaseNotice()
}
async function confirmBuy() {
if (paying.value) return
paying.value = true
showNotice.value = false
try {
const mobile = (uni.getStorageSync('phone') as string) || ''
@@ -276,4 +338,71 @@ $bar-height: 120rpx;
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>