chuangte_bike_newxcx/page_user/verify/index.vue
2026-02-26 18:05:57 +08:00

481 lines
12 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">
<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"
/>
</view>
</template>
<script>
import AreaSelectPopup from './components/AreaSelectPopup.vue';
import CouponSelectPopup from './components/CouponSelectPopup.vue';
/**
* 核销页面
* - 顶部展示当前运营区,点击弹窗选择
* - 核销提示:抖音核销 / 美团核销(暂不开放)
* - 券码输入框
* - 核销按钮:调用准备接口 -> 多券则弹窗选择 -> 调用核销接口
*/
/** 核销方式配置icon(image url 或 uview icon 名), title, description, disabled */
const VERIFY_METHODS = [
{
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
}
]
export default {
name: 'VerifyIndex',
components: {
AreaSelectPopup,
CouponSelectPopup
},
data() {
return {
verifyMethods: VERIFY_METHODS,
bgc: { backgroundColor: '#fff' },
currentArea: null,
couponCode: '',
expandedType: 'douyin',
areaPopupVisible: false,
couponPopupVisible: false,
verifyLoading: false,
prepareItemList: [],
preparePayload: null, // 保存 prepare 的请求参数,供 verify 使用
appId: '1'
}
},
computed: {
canVerify() {
return this.currentArea && this.couponCode.trim().length > 0 && !this.verifyLoading
}
},
onLoad() {
this.appId = this.$store.state.appid || '1'
this.initDefaultArea()
},
methods: {
/** 初始化默认运营区:获取当前位置最近的运营区 */
async initDefaultArea() {
await this.fetchNearestArea()
},
/** 获取当前位置最近的运营区并设为默认 */
async fetchNearestArea() {
try {
const loc = await this.getLocation()
if (!loc) return
const url = `/app/area/nearbyVerifyList?appId=${this.appId}&center=${loc.longitude}&center=${loc.latitude}`
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.$u.toast('核销成功')
this.couponCode = ''
this.preparePayload = null
uni.navigateTo({ url: '/page_fenbao/huiyuan/myhuiyuan' })
} else {
throw new Error(res.msg || '核销失败')
}
} catch (e) {
uni.hideLoading()
throw e
}
}
}
}
</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>