支付宝下单和其他细节优化

This commit is contained in:
2026-05-21 15:10:02 +08:00
parent 39bf8548df
commit 1637ee5bbc
33 changed files with 1511 additions and 232 deletions
+200 -46
View File
@@ -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>