chuangte_bike_newxcx/page_user/verify/index.vue
2026-02-27 11:29:31 +08:00

520 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="page">
<view class="container">
<!-- 运营区选择 -->
<view class="section area-section animate-fade-in" @click="openAreaPopup" v-if="!areaId">
<view class="section-label">当前运营区</view>
<view class="area-select">
<text class="area-name">{{ currentArea ? currentArea.name : '请选择运营区' }}</text>
<u-icon name="arrow-right" size="32" color="#999"></u-icon>
</view>
</view>
<!-- 核销方式手风琴 -->
<view class="verify-section animate-fade-in" style="animation-delay: 0.08s">
<view class="section-label">核销方式</view>
<view class="accordion">
<view
v-for="item in verifyMethods"
:key="item.type"
class="accordion-item"
:class="{
expanded: expandedType === item.type,
disabled: item.disabled
}"
>
<view
class="accordion-header tip-card"
:class="{ 'tip-card--disabled': item.disabled }"
@click="toggleAccordion(item)"
>
<view class="tip-icon-wrap" :class="{ 'tip-icon-wrap--disabled': item.disabled }">
<image class="tip-icon" :src="item.icon" mode="aspectFit" />
</view>
<view class="tip-content">
<text class="tip-title">{{ item.title }}</text>
<text class="tip-desc">{{ item.description }}</text>
</view>
<view class="arrow-wrap" :class="{ 'arrow-wrap--expanded': expandedType === item.type }">
<u-icon name="arrow-down" size="28" :color="item.disabled ? '#c8c9cc' : '#969799'"></u-icon>
</view>
</view>
<view
class="accordion-body"
:class="{
'accordion-body--expanded': expandedType === item.type,
'accordion-body--disabled': item.disabled
}"
>
<view class="accordion-inner">
<view v-if="!item.disabled" class="input-wrap">
<u-input
v-model="couponCode"
type="text"
placeholder="请输入券码"
:border="false"
:custom-style="{ fontSize: '30rpx', padding: '0 24rpx' }"
/>
</view>
<view v-else class="coming-soon">{{ item.description }}</view>
</view>
</view>
</view>
</view>
</view>
<!-- 核销按钮 -->
<view class="submit-section animate-fade-in" style="animation-delay: 0.16s">
<u-button
type="primary"
:loading="verifyLoading"
:disabled="!canVerify"
@click="onVerify"
>
核销
</u-button>
</view>
</view>
<!-- 运营区选择弹窗 -->
<area-select-popup
v-model="areaPopupVisible"
:selected-area="currentArea"
:app-id="appId"
@select="onAreaSelect"
/>
<!-- 券码选择弹窗 -->
<coupon-select-popup
v-model="couponPopupVisible"
:item-list="prepareItemList"
@confirm="onCouponConfirm"
/>
<!-- 核销成功弹窗 -->
<verify-success-popup
v-model="successPopupVisible"
:coupon-name="successCouponName"
:redirect-url="redirectUrl"
:redirect-remark="redirectRemark"
@viewCard="onViewCard"
@continueVerify="onContinueVerify"
@redirect="onRedirect"
/>
</view>
</template>
<script>
import AreaSelectPopup from './components/AreaSelectPopup.vue';
import CouponSelectPopup from './components/CouponSelectPopup.vue';
import VerifySuccessPopup from './components/VerifySuccessPopup.vue';
/**
* 核销页面
* - 顶部展示当前运营区,点击弹窗选择
* - 核销提示:抖音核销 / 美团核销(暂不开放)
* - 券码输入框
* - 核销按钮:调用准备接口 -> 多券则弹窗选择 -> 调用核销接口
*/
export default {
name: 'VerifyIndex',
components: {
AreaSelectPopup,
CouponSelectPopup,
VerifySuccessPopup
},
data() {
return {
verifyMethods: [
{
type: 'douyin',
icon: 'https://api.ccttiot.com/%E6%8A%96%E9%9F%B3-1772093491243.png',
title: '抖音核销',
description: '请输入抖音券码进行核销',
disabled: false
},
{
type: 'meituan',
icon: 'https://api.ccttiot.com/%E7%BE%8E%E5%9B%A2-copy-1772093915776.png',
title: '美团核销',
description: '即将开放',
disabled: true
}
],
bgc: { backgroundColor: '#fff' },
currentArea: null,
couponCode: '',
expandedType: 'douyin',
areaPopupVisible: false,
couponPopupVisible: false,
verifyLoading: false,
prepareItemList: [],
preparePayload: null, // 保存 prepare 的请求参数,供 verify 使用
appId: '1',
areaId: null,
successPopupVisible: false,
successCouponName: '',
redirectUrl: '',
redirectRemark: ''
}
},
computed: {
canVerify() {
return this.currentArea && this.couponCode.trim().length > 0 && !this.verifyLoading
}
},
onLoad(options) {
this.appId = this.$store.state.appid || '1'
this.areaId = options.areaId
if (options.redirectUrl) {
this.redirectUrl = decodeURIComponent(options.redirectUrl)
}
this.redirectRemark = options.redirectRemark
this.initDefaultArea()
},
methods: {
/** 获取当前位置最近的运营区并设为默认 */
async initDefaultArea() {
try {
const loc = await this.getLocation()
if (!loc) return
const url = `/app/area/nearbyVerifyList?appId=${this.appId}&center=${loc.longitude}&center=${loc.latitude}&areaId=${this.areaId}`
const res = await this.$u.get(url)
if (res.code === 200 && Array.isArray(res.data) && res.data.length > 0) {
const nearest = res.data[0]
this.currentArea = { id: nearest.id, name: nearest.name }
}
} catch (e) {
console.warn('获取附近运营区失败', e)
}
},
/** 获取定位 */
getLocation() {
return new Promise((resolve) => {
uni.getLocation({
type: 'gcj02',
success: (res) => resolve(res),
fail: () => resolve(null)
})
})
},
openAreaPopup() {
this.areaPopupVisible = true
},
toggleAccordion(item) {
if (item.disabled) return
this.expandedType = this.expandedType === item.type ? '' : item.type
},
onAreaSelect(area) {
this.currentArea = area
},
/** 执行核销流程 */
async onVerify() {
if (!this.canVerify) return
this.verifyLoading = true
try {
const code = this.couponCode.trim()
const areaId = this.currentArea.id
const prepareRes = await this.callDouyinPrepare(code, areaId)
if (!prepareRes || !prepareRes.itemList || prepareRes.itemList.length === 0) {
this.$u.toast('未解析到可核销的券')
return
}
this.preparePayload = { code, areaId }
if (prepareRes.itemList.length > 1) {
this.prepareItemList = prepareRes.itemList
this.couponPopupVisible = true
} else {
await this.doVerify(prepareRes.itemList[0])
}
} catch (e) {
this.$u.toast(e.message || '核销准备失败')
} finally {
this.verifyLoading = false
}
},
/** 用户在弹窗中选择了券,执行核销 */
async onCouponConfirm(item) {
this.verifyLoading = true
try {
await this.doVerify(item)
} catch (e) {
this.$u.toast(e.message || '核销失败')
} finally {
this.verifyLoading = false
}
},
/** 调用核销准备接口 */
callDouyinPrepare(code, areaId) {
return this.$u
.post('/app/vipUser/douyinPrepare', { code, areaId })
.then((res) => {
if (res.code === 200) return res.data
throw new Error(res.msg || '准备失败')
})
},
/** 调用核销验券接口 */
async doVerify(item) {
if (!this.preparePayload) {
throw new Error('参数异常,请重试')
}
uni.showLoading({ title: '核销中...', mask: true })
try {
const res = await this.$u.post('/app/vipUser/douyinVerify', {
code: this.preparePayload.code,
areaId: this.preparePayload.areaId,
certificateId: item.certificateId
})
uni.hideLoading()
if (res.code === 200) {
this.successCouponName = item?.vip?.name
this.successPopupVisible = true
this.couponCode = ''
this.preparePayload = null
} else {
throw new Error(res.msg || '核销失败')
}
} catch (e) {
uni.hideLoading()
throw e
}
},
/** 查看卡券 */
onViewCard() {
this.successPopupVisible = false
uni.navigateTo({ url: '/page_fenbao/huiyuan/myhuiyuan' })
},
/** 继续核销 */
onContinueVerify() {
this.successPopupVisible = false
this.couponCode = ''
this.preparePayload = null
this.prepareItemList = []
},
/** 重定向redirectRemark 按钮点击) */
onRedirect() {
this.successPopupVisible = false
if (this.redirectUrl) {
if (this.redirectUrl.startsWith('http')) {
uni.redirectTo({ url: '/page_fenbao/webview?url=' + encodeURIComponent(this.redirectUrl) })
} else {
uni.redirectTo({ url: this.redirectUrl.startsWith('/') ? this.redirectUrl : '/' + this.redirectUrl })
}
}
}
}
}
</script>
<style lang="scss" scoped>
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(24rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fade-in {
animation: fadeInUp 0.4s ease-out both;
}
.page {
min-height: 100vh;
background: #f7f8fa;
}
.container {
padding: 30rpx;
}
.section {
margin-bottom: 32rpx;
padding: 30rpx;
background: #fff;
border-radius: 16rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04);
transition: transform 0.2s ease, box-shadow 0.2s ease;
&:active {
transform: scale(0.99);
}
}
.section-label {
font-size: 26rpx;
color: #999;
margin-bottom: 16rpx;
}
.area-section {
.area-select {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16rpx 0;
.area-name {
font-size: 32rpx;
color: #333;
font-weight: 500;
}
}
}
.verify-section {
margin-bottom: 32rpx;
.section-label {
margin-bottom: 16rpx;
}
.accordion {
.accordion-item {
margin-bottom: 20rpx;
&:last-child {
margin-bottom: 0;
}
&.expanded {
.accordion-header {
border-radius: 16rpx 16rpx 0 0;
}
}
}
.accordion-header {
cursor: pointer;
transition: background 0.25s ease, border-radius 0.25s ease;
&:active:not(.tip-card--disabled) {
opacity: 0.95;
}
}
.arrow-wrap {
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&--expanded {
transform: rotate(180deg);
}
}
.accordion-body {
overflow: hidden;
max-height: 0;
background: #fff;
border: 1rpx solid transparent;
border-top: none;
border-radius: 0 0 16rpx 16rpx;
transition: max-height 0.35s cubic-bezier(0.4, 0, 0.2, 1),
border-color 0.25s ease;
&--expanded {
max-height: 300rpx;
border-color: rgba(57, 150, 253, 0.12);
}
&--disabled.accordion-body--expanded {
border-color: rgba(0, 0, 0, 0.06);
}
}
.accordion-inner {
padding: 20rpx 24rpx 24rpx;
}
.coming-soon {
font-size: 28rpx;
color: #969799;
text-align: center;
padding: 24rpx 0;
}
}
.tip-card {
display: flex;
align-items: center;
padding: 24rpx 20rpx;
background: linear-gradient(135deg, #f8fbff 0%, #f0f7ff 100%);
border-radius: 16rpx;
border: 1rpx solid rgba(57, 150, 253, 0.12);
transition: background 0.25s ease, border-color 0.25s ease, opacity 0.2s ease;
&--disabled {
background: linear-gradient(135deg, #f8f9fa 0%, #f0f1f3 100%);
border-color: rgba(0, 0, 0, 0.06);
opacity: 0.85;
}
}
.tip-icon-wrap {
width: 72rpx;
height: 72rpx;
flex-shrink: 0;
margin-right: 24rpx;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.2s ease;
}
.tip-icon {
width: 52rpx;
height: 52rpx;
}
.tip-content {
flex: 1;
min-width: 0;
}
.tip-title {
display: block;
font-size: 30rpx;
color: #1a1a1a;
font-weight: 600;
margin-bottom: 6rpx;
}
.tip-desc {
display: block;
font-size: 26rpx;
color: #646566;
line-height: 1.4;
}
.input-wrap {
border: 1rpx solid #e4e7ed;
border-radius: 12rpx;
overflow: hidden;
background: #fff;
}
}
.submit-section {
margin-top: 60rpx;
.u-btn {
width: 100%;
height: 88rpx;
font-size: 32rpx;
transition: opacity 0.25s ease, transform 0.2s ease;
&[disabled] {
opacity: 0.6;
}
&:active:not([disabled]) {
transform: scale(0.98);
}
}
}
</style>