500 lines
13 KiB
Vue
500 lines
13 KiB
Vue
<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"
|
||
@viewCard="onViewCard"
|
||
@continueVerify="onContinueVerify"
|
||
/>
|
||
</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: ''
|
||
}
|
||
},
|
||
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
|
||
this.initDefaultArea()
|
||
},
|
||
methods: {
|
||
/** 获取当前位置最近的运营区并设为默认 */
|
||
async initDefaultArea() {
|
||
try {
|
||
const loc = await this.getLocation()
|
||
if (!loc) return
|
||
const url = `/app/area/nearbyVerifyList?appId=${this.appId}¢er=${loc.longitude}¢er=${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.skuTitle || (item.vip && 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 = []
|
||
}
|
||
}
|
||
}
|
||
</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>
|