支付宝下单和其他细节优化
This commit is contained in:
+200
-46
@@ -108,14 +108,14 @@
|
||||
<!-- 顶部导航 -->
|
||||
<div class="member-dialog__order-header">
|
||||
<span class="member-dialog__order-back" @click="currentView = 'intro2'">‹ 返回会员介绍</span>
|
||||
<!-- 步骤条 -->
|
||||
<!-- 步骤条 — 二维码弹窗打开时显示第2步 -->
|
||||
<div class="member-dialog__steps">
|
||||
<div class="member-dialog__step member-dialog__step--active">
|
||||
<span class="member-dialog__step-num">1</span>
|
||||
<div class="member-dialog__step" :class="showQrCode ? 'member-dialog__step--done' : 'member-dialog__step--active'">
|
||||
<span class="member-dialog__step-num">{{ showQrCode ? '✓' : '1' }}</span>
|
||||
<span class="member-dialog__step-label">选择套餐</span>
|
||||
</div>
|
||||
<div class="member-dialog__step-line"></div>
|
||||
<div class="member-dialog__step">
|
||||
<div class="member-dialog__step-line" :class="{ 'member-dialog__step-line--done': showQrCode }"></div>
|
||||
<div class="member-dialog__step" :class="{ 'member-dialog__step--active': showQrCode }">
|
||||
<span class="member-dialog__step-num">2</span>
|
||||
<span class="member-dialog__step-label">支付方式</span>
|
||||
</div>
|
||||
@@ -144,12 +144,15 @@
|
||||
@click="selectedPlan = plan.key"
|
||||
>
|
||||
<div v-if="plan.recommend" class="member-dialog__order-plan-badge">★ 推荐</div>
|
||||
<!-- 选中圆圈 -->
|
||||
<div class="member-dialog__order-plan-radio">
|
||||
<div v-if="selectedPlan === plan.key" class="member-dialog__order-plan-radio-inner"></div>
|
||||
</div>
|
||||
|
||||
<div class="member-dialog__order-plan-info">
|
||||
<div class="member-dialog__order-plan-name">{{ plan.name }}</div>
|
||||
<div class="dflex">
|
||||
<div class="member-dialog__order-plan-name">{{ plan.name }}</div>
|
||||
<!-- 选中圆圈 -->
|
||||
<div class="member-dialog__order-plan-radio">
|
||||
<div v-if="selectedPlan === plan.key" class="member-dialog__order-plan-radio-inner"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="member-dialog__order-plan-price">
|
||||
<span class="member-dialog__order-plan-price-symbol">¥</span>
|
||||
<span class="member-dialog__order-plan-price-num">{{ plan.price }}</span>
|
||||
@@ -157,6 +160,7 @@
|
||||
</div>
|
||||
<div class="member-dialog__order-plan-desc">{{ plan.orderDesc }}</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -317,33 +321,36 @@
|
||||
<h2 class="member-dialog__order-success-title">支付成功,求职加速已开启</h2>
|
||||
<p class="member-dialog__order-success-desc">你已成功解锁 AI 求职加速权益,现在可以开始优化简历、匹配岗位并准备面试。</p>
|
||||
<button class="member-dialog__order-success-btn" @click="handleGoResume">开始优化我的简历</button>
|
||||
<button class="member-dialog__order-success-btn-secondary" @click="$emit('update:modelValue', false)">查看会员权益</button>
|
||||
<button class="member-dialog__order-success-btn-secondary" @click="handleViewMemberBenefits">查看会员权益</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ==================== 付款二维码遮罩层 ==================== -->
|
||||
<div v-if="showQrCode" class="member-dialog__qrcode-overlay" @click="showQrCode = false">
|
||||
<!-- ==================== 付款弹窗 ==================== -->
|
||||
<div v-if="showQrCode" class="member-dialog__qrcode-overlay" @click="handleCloseQrCode">
|
||||
<div class="member-dialog__qrcode-modal" @click.stop>
|
||||
<!-- 关闭按钮 -->
|
||||
<span class="member-dialog__qrcode-close" @click="showQrCode = false">✕</span>
|
||||
<span class="member-dialog__qrcode-close" @click="handleCloseQrCode">✕</span>
|
||||
<!-- 支付方式标题 -->
|
||||
<div class="member-dialog__qrcode-title">
|
||||
<span class="member-dialog__qrcode-title-icon">{{ selectedPayment === 'wechat' ? '💬' : '🔷' }}</span>
|
||||
<span>{{ selectedPayment === 'wechat' ? '微信支付' : '支付宝' }}</span>
|
||||
</div>
|
||||
<!-- 扫码提示 -->
|
||||
<h3 class="member-dialog__qrcode-subtitle">扫码完成支付</h3>
|
||||
<p class="member-dialog__qrcode-desc">请使用{{ selectedPayment === 'wechat' ? '微信' : '支付宝' }} App 扫描二维码完成支付,完成后此窗口会自动关闭。</p>
|
||||
<!-- 二维码占位 -->
|
||||
<h3 class="member-dialog__qrcode-subtitle">{{ selectedPayment === 'alipay' ? '扫码完成支付' : '扫码完成支付' }}</h3>
|
||||
<p class="member-dialog__qrcode-desc">{{ selectedPayment === 'alipay' ? '请在支付宝页面完成支付,完成后点击下方按钮确认。' : '请使用微信 App 扫描二维码完成支付,完成后此窗口会自动关闭。' }}</p>
|
||||
<!-- 支付宝:iframe 渲染支付表单 -->
|
||||
<div class="member-dialog__qrcode-image">
|
||||
<!-- TODO: 替换为真实二维码图片 -->
|
||||
<div class="member-dialog__qrcode-placeholder"></div>
|
||||
<iframe
|
||||
v-if="paymentFormHtml"
|
||||
:srcdoc="paymentFormHtml"
|
||||
frameborder="0"
|
||||
class="member-dialog__payment-iframe"
|
||||
scrolling="auto"
|
||||
></iframe>
|
||||
</div>
|
||||
<!-- 金额 -->
|
||||
<div class="member-dialog__qrcode-amount">¥{{ currentPlan.price }}</div>
|
||||
<!-- 我已完成支付按钮 -->
|
||||
<button class="member-dialog__qrcode-confirm-btn" @click="handlePaymentDone">我已完成支付</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -352,20 +359,23 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, reactive } from 'vue'
|
||||
import { ref, computed, watch, reactive, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useStore } from 'vuex'
|
||||
import { fetchMemberProductList, createMemberOrder, fetchOrderDetail, type MemberProduct } from '@/api/member'
|
||||
|
||||
/** 组件 Props — 控制弹窗显示/隐藏 */
|
||||
const props = defineProps<{ modelValue: boolean }>()
|
||||
|
||||
/** 组件 Emits — 通知父组件更新 modelValue */
|
||||
defineEmits<{ (e: 'update:modelValue', value: boolean): void }>()
|
||||
const emit = defineEmits<{ (e: 'update:modelValue', value: boolean): void }>()
|
||||
|
||||
const router = useRouter()
|
||||
const store = useStore()
|
||||
|
||||
// ==================== 类型定义 ====================
|
||||
|
||||
/** 套餐项类型 */
|
||||
/** 套餐项类型(包含后端完整数据 + 前端展示字段) */
|
||||
interface PlanItem {
|
||||
key: string
|
||||
name: string
|
||||
@@ -376,6 +386,8 @@ interface PlanItem {
|
||||
perMonth?: string
|
||||
btnText: string
|
||||
orderDesc: string
|
||||
/** 后端原始数据,下单时使用 */
|
||||
raw: MemberProduct
|
||||
}
|
||||
|
||||
/** 支付方式类型 */
|
||||
@@ -409,6 +421,15 @@ const agreeProtocol = ref(false)
|
||||
/** 是否显示二维码弹窗 */
|
||||
const showQrCode = ref(false)
|
||||
|
||||
/** 支付宝表单 HTML(用于 iframe 渲染) */
|
||||
const paymentFormHtml = ref('')
|
||||
|
||||
/** 当前订单ID(用于后续轮询支付状态) */
|
||||
const orderId = ref('')
|
||||
|
||||
/** 是否正在创建订单 */
|
||||
const creatingOrder = ref(false)
|
||||
|
||||
/** 监听弹窗开关 — 打开时锁定背景滚动,关闭时恢复并重置状态 */
|
||||
watch(() => props.modelValue, (val) => {
|
||||
document.body.style.overflow = val ? 'hidden' : ''
|
||||
@@ -416,6 +437,21 @@ watch(() => props.modelValue, (val) => {
|
||||
currentView.value = 'intro2'
|
||||
showQrCode.value = false
|
||||
agreeProtocol.value = false
|
||||
paymentFormHtml.value = ''
|
||||
orderId.value = ''
|
||||
// 清除轮询计时器
|
||||
if (pollTimer) {
|
||||
clearInterval(pollTimer)
|
||||
pollTimer = null
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
/** 监听二维码弹窗关闭 — 无论何种方式关闭都停止轮询 */
|
||||
watch(showQrCode, (val) => {
|
||||
if (!val && pollTimer) {
|
||||
clearInterval(pollTimer)
|
||||
pollTimer = null
|
||||
}
|
||||
})
|
||||
|
||||
@@ -423,17 +459,62 @@ watch(() => props.modelValue, (val) => {
|
||||
|
||||
/** 当前选中的套餐对象 */
|
||||
const currentPlan = computed(() => {
|
||||
return plans.find(p => p.key === selectedPlan.value) || plans[1]
|
||||
return plans.value.find(p => p.key === selectedPlan.value) || plans.value[0]
|
||||
})
|
||||
|
||||
// ==================== 常量数据 ====================
|
||||
|
||||
/** 套餐列表 */
|
||||
const plans: PlanItem[] = [
|
||||
{ key: 'weekly', name: '周会员', price: '17.99', unit: '周', tag: '临时体验 / 急用一次', btnText: '立即体验', orderDesc: '适合临时体验,急用一次的求职者' },
|
||||
{ key: 'quarterly', name: '季度会员', price: '49.99', unit: '3个月', recommend: true, perMonth: '16.66', btnText: '开启完整求职冲刺', orderDesc: '推荐正在集中投递、准备面试的求职者,性价比最高' },
|
||||
{ key: 'monthly', name: '月度会员', price: '19.99', unit: '月', tag: '标准求职', btnText: '开始求职加速', orderDesc: '标准求职周期,持续使用 AI 求职加速能力' },
|
||||
]
|
||||
/** 套餐列表(从接口获取) */
|
||||
const plans = ref<PlanItem[]>([])
|
||||
|
||||
/** 根据有效天数生成单位描述 */
|
||||
function getDurationUnit(days: number): string {
|
||||
if (days <= 7) return '周'
|
||||
if (days <= 31) return '月'
|
||||
if (days <= 93) return '3个月'
|
||||
if (days <= 186) return '半年'
|
||||
return '年'
|
||||
}
|
||||
|
||||
/** 将后端商品数据转换为前端套餐项 */
|
||||
function mapProductToPlan(product: MemberProduct): PlanItem {
|
||||
const priceYuan = (product.price / 100).toFixed(2)
|
||||
const monthlyPriceYuan = product.monthlyPrice ? (product.monthlyPrice / 100).toFixed(2) : undefined
|
||||
return {
|
||||
key: String(product.id),
|
||||
name: product.productName,
|
||||
price: priceYuan,
|
||||
unit: getDurationUnit(product.durationDays),
|
||||
tag: product.tag || undefined,
|
||||
recommend: product.isFeatured === 1,
|
||||
perMonth: monthlyPriceYuan,
|
||||
btnText: product.buyButtonText || '立即开通',
|
||||
orderDesc: product.tag || product.productName,
|
||||
raw: product,
|
||||
}
|
||||
}
|
||||
|
||||
/** 加载会员商品列表 */
|
||||
async function loadMemberProducts() {
|
||||
try {
|
||||
const res = await fetchMemberProductList()
|
||||
if (res.data && res.data.length) {
|
||||
// 按 sortOrder 排序
|
||||
const sorted = [...res.data].sort((a, b) => a.sortOrder - b.sortOrder)
|
||||
plans.value = sorted.map(mapProductToPlan)
|
||||
// 默认选中主推套餐,若无则选第一个
|
||||
const featured = plans.value.find(p => p.recommend)
|
||||
selectedPlan.value = featured ? featured.key : plans.value[0].key
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载会员商品列表失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
/** 组件挂载时加载商品列表 */
|
||||
onMounted(() => {
|
||||
loadMemberProducts()
|
||||
})
|
||||
|
||||
/** 求职加速能力列表 */
|
||||
const abilities = [
|
||||
@@ -485,29 +566,102 @@ function handleUpgrade(plan: PlanItem) {
|
||||
currentView.value = 'order1'
|
||||
}
|
||||
|
||||
/** 点击立即开启求职加速 — 显示二维码弹窗 */
|
||||
function handleShowQrCode() {
|
||||
if (!agreeProtocol.value) return
|
||||
showQrCode.value = true
|
||||
/** 点击立即开启求职加速 — 调用创建订单接口并显示支付弹窗 */
|
||||
async function handleShowQrCode() {
|
||||
if (!agreeProtocol.value || creatingOrder.value) return
|
||||
const plan = currentPlan.value
|
||||
if (!plan) return
|
||||
|
||||
creatingOrder.value = true
|
||||
try {
|
||||
// 支付渠道:1=微信 2=支付宝
|
||||
const payChannel = selectedPayment.value === 'alipay' ? 2 : 1
|
||||
const res = await createMemberOrder({
|
||||
productId: plan.key, // key 即为商品 id(字符串,避免大数精度丢失)
|
||||
payChannel,
|
||||
})
|
||||
if (res.data) {
|
||||
orderId.value = String(res.data.orderId)
|
||||
paymentFormHtml.value = res.data.payData
|
||||
showQrCode.value = true
|
||||
// 创建订单成功后立即开始轮询支付状态
|
||||
confirmPayment()
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('创建订单失败', e)
|
||||
} finally {
|
||||
creatingOrder.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 点击我已完成支付 — 进入确认支付结果步骤 */
|
||||
function handlePaymentDone() {
|
||||
showQrCode.value = false
|
||||
currentView.value = 'order2'
|
||||
// 模拟接口确认支付结果,成功后进入步骤三
|
||||
confirmPayment()
|
||||
}
|
||||
/** 轮询计时器 */
|
||||
let pollTimer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
/** 模拟确认支付结果接口 */
|
||||
/** 轮询查询订单支付状态 */
|
||||
async function confirmPayment() {
|
||||
// TODO: 替换为真实支付确认接口
|
||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||
currentView.value = 'order3'
|
||||
const startTime = Date.now()
|
||||
/** 超时时间:5 分钟 */
|
||||
const timeout = 5 * 60 * 1000
|
||||
/** 轮询间隔:500ms */
|
||||
const interval = 500
|
||||
|
||||
// 清除可能存在的旧计时器
|
||||
if (pollTimer) {
|
||||
clearInterval(pollTimer)
|
||||
pollTimer = null
|
||||
}
|
||||
|
||||
pollTimer = setInterval(async () => {
|
||||
try {
|
||||
const res = await fetchOrderDetail(orderId.value)
|
||||
const status = res.data?.status
|
||||
|
||||
if (status === 1) {
|
||||
// 支付成功 — 关闭二维码弹窗,进入完成步骤
|
||||
clearInterval(pollTimer!)
|
||||
pollTimer = null
|
||||
showQrCode.value = false
|
||||
currentView.value = 'order3'
|
||||
} else if (status === 2 || status === 3) {
|
||||
// 已退款或已关闭 — 提示并回到选择套餐步骤
|
||||
clearInterval(pollTimer!)
|
||||
pollTimer = null
|
||||
showQrCode.value = false
|
||||
ElMessage.error('支付未成功,请重新下单')
|
||||
currentView.value = 'order1'
|
||||
} else if (Date.now() - startTime >= timeout) {
|
||||
// 超时 — 提示并回到选择套餐步骤
|
||||
clearInterval(pollTimer!)
|
||||
pollTimer = null
|
||||
showQrCode.value = false
|
||||
ElMessage.error('支付未成功,请重新下单')
|
||||
currentView.value = 'order1'
|
||||
}
|
||||
// status === 0 继续等待
|
||||
} catch (e) {
|
||||
console.error('查询订单状态失败', e)
|
||||
}
|
||||
}, interval)
|
||||
}
|
||||
|
||||
/** 支付成功后跳转简历页 */
|
||||
function handleGoResume() {
|
||||
router.push('/resume')
|
||||
}
|
||||
|
||||
/** 关闭二维码弹窗 — 停止轮询 */
|
||||
function handleCloseQrCode() {
|
||||
showQrCode.value = false
|
||||
if (pollTimer) {
|
||||
clearInterval(pollTimer)
|
||||
pollTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
/** 查看会员权益 — 关闭会员弹窗,打开设置弹窗的会员 Tab */
|
||||
function handleViewMemberBenefits() {
|
||||
emit('update:modelValue', false)
|
||||
store.commit('SET_SETTINGS_TAB', 'member')
|
||||
store.commit('SET_SHOW_SETTINGS', true)
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user