You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

755 lines
16 KiB

<template>
<Modal
v-model:open="visible"
title="开通会员"
:width="900"
:footer="null"
@cancel="handleCancel"
class="membership-modal"
>
<div class="membership-content">
<!-- 会员权益介绍 -->
<div class="membership-benefits">
<div class="benefits-header">
<CrownOutlined class="crown-icon" />
<h3>会员专享权益</h3>
</div>
<div class="benefits-list">
<div class="benefit-item">
<CheckCircleOutlined class="benefit-icon" />
<span>无限次合同审查</span>
</div>
<div class="benefit-item">
<CheckCircleOutlined class="benefit-icon" />
<span>优先处理队列</span>
</div>
</div>
</div>
<!-- 会员套餐选择 -->
<div class="membership-plans">
<h4>选择套餐</h4>
<div class="plans-grid">
<div
v-for="plan in membershipPlans"
:key="plan.id"
:class="['plan-card', { active: selectedPlan?.id === plan.id }]"
@click="selectPlan(plan)"
>
<div class="plan-header">
<div class="plan-duration">{{ plan.duration }}</div>
<div class="plan-price">
<span class="currency">¥</span>
<span class="amount">{{ plan.price }}</span>
</div>
</div>
<div class="plan-features">
<div class="feature-item">
<CheckOutlined class="feature-icon" />
<span>{{ plan.duration }}会员服务</span>
</div>
<div class="feature-item">
<CheckOutlined class="feature-icon" />
<span>所有会员权益</span>
</div>
<div v-if="plan.discount" class="feature-item discount">
<Tag color="red">{{ plan.discount }}</Tag>
</div>
</div>
<div class="plan-action">
<Button
:type="selectedPlan?.id === plan.id ? 'primary' : 'default'"
:ghost="selectedPlan?.id !== plan.id"
size="large"
block
>
{{ selectedPlan?.id === plan.id ? '已选择' : '选择套餐' }}
</Button>
</div>
</div>
</div>
</div>
<!-- 支付方式选择 -->
<div class="payment-methods">
<h4>支付方式</h4>
<Radio.Group v-model:value="selectedPayment" class="payment-options">
<Radio value="wechat" class="payment-option">
<div class="payment-item">
<WechatOutlined class="payment-icon wechat" />
<span>微信支付</span>
</div>
</Radio>
<Radio value="alipay" class="payment-option">
<div class="payment-item">
<AlipayCircleOutlined class="payment-icon alipay" />
<span>支付宝</span>
</div>
</Radio>
</Radio.Group>
</div>
<!-- 订单信息 -->
<div v-if="selectedPlan" class="order-summary">
<Divider />
<div class="summary-item">
<span>套餐</span>
<span>{{ selectedPlan.duration }}</span>
</div>
<div class="summary-item">
<span>价格</span>
<span class="price">¥{{ selectedPlan.price }}</span>
</div>
<div v-if="selectedPlan.originalPrice" class="summary-item discount">
<span>原价</span>
<span class="original-price">¥{{ selectedPlan.originalPrice }}</span>
</div>
<div v-if="currentOrderNo" class="summary-item">
<span>订单号</span>
<span class="order-no">{{ currentOrderNo }}</span>
</div>
<div v-if="paymentStatus" class="summary-item">
<span>支付状态</span>
<span :class="['status', getStatusClass(paymentStatus)]">{{ getStatusText(paymentStatus) }}</span>
</div>
</div>
<!-- 操作按钮 -->
<div class="modal-actions">
<Button @click="handleCancel" size="large">
取消
</Button>
<Button
type="primary"
size="large"
:loading="loading"
:disabled="!selectedPlan || !selectedPayment"
@click="handlePurchase"
>
立即购买 ¥{{ selectedPlan?.price || 0 }}
</Button>
</div>
</div>
</Modal>
<!-- 二维码支付弹窗 -->
<PaymentQRCodeModal
v-model:visible="qrCodeVisible"
:qr-code-url="qrCodeUrl"
:payment-method="selectedPayment"
:order-no="currentOrderNo"
:amount="selectedPlan?.price || 0"
@payment-success="handlePaymentSuccess"
@payment-failed="handlePaymentFailed"
@cancel="handleQrCodeCancel"
/>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import {
Modal,
Button,
Radio,
Tag,
Divider,
message
} from 'ant-design-vue';
import {
CrownOutlined,
CheckCircleOutlined,
CheckOutlined,
WechatOutlined,
AlipayCircleOutlined,
LoadingOutlined
} from '@ant-design/icons-vue';
import { createPayment, queryPaymentStatus } from '@/api/contractReview/Payment';
import { useUserStore } from '@/store/modules/user';
import PaymentQRCodeModal from './PaymentQRCodeModal.vue';
interface MembershipPlan {
id: string;
duration: string;
price: number;
originalPrice?: number;
discount?: string;
}
const props = defineProps<{
visible: boolean;
}>();
const emit = defineEmits<{
'update:visible': [value: boolean];
'purchase-success': [plan: MembershipPlan];
}>();
// 状态管理
const selectedPlan = ref<MembershipPlan | null>(null);
const selectedPayment = ref<string>('');
const loading = ref(false);
const currentOrderNo = ref<string>('');
const paymentStatus = ref<string>('');
const qrCodeVisible = ref(false);
const qrCodeUrl = ref<string>('');
// 用户信息
const userStore = useUserStore();
// 会员套餐数据
const membershipPlans: MembershipPlan[] = [
{
id: '1month',
duration: '1个月',
price: 0.01,
originalPrice: 129,
discount: '限时优惠'
},
{
id: '3months',
duration: '季度',
price: 269,
originalPrice: 387,
discount: '省¥118'
},
{
id: '6months',
duration: '半年',
price: 499,
originalPrice: 774,
discount: '省¥275'
},
{
id: '12months',
duration: '12个月',
price: 899,
originalPrice: 1548,
discount: '省¥649'
}
];
// 计算属性
const visible = computed({
get: () => props.visible,
set: (value) => emit('update:visible', value)
});
// 方法
function selectPlan(plan: MembershipPlan) {
selectedPlan.value = plan;
}
function handleCancel() {
visible.value = false;
selectedPlan.value = null;
selectedPayment.value = '';
currentOrderNo.value = '';
paymentStatus.value = '';
qrCodeVisible.value = false;
qrCodeUrl.value = '';
}
function handleQrCodeCancel() {
qrCodeVisible.value = false;
qrCodeUrl.value = '';
}
function handlePaymentSuccess() {
// 支付成功处理
if (selectedPlan.value) {
emit('purchase-success', selectedPlan.value);
}
qrCodeVisible.value = false;
handleCancel();
}
function handlePaymentFailed() {
// 支付失败处理
qrCodeVisible.value = false;
}
async function handlePurchase() {
if (!selectedPlan.value || !selectedPayment.value) {
message.warning('请选择套餐和支付方式');
return;
}
loading.value = true;
try {
// 构建支付请求
const paymentRequest = {
planType: selectedPlan.value.id,
planName: selectedPlan.value.duration,
duration: getDurationInMonths(selectedPlan.value.duration),
amount: selectedPlan.value.price,
paymentMethod: selectedPayment.value,
userId: userStore.getUserInfo?.userId || 1,
clientIp: '',
notifyUrl: '',
returnUrl: ''
};
// 调用后端支付API
const result = await createPayment(paymentRequest);
if (result.success) {
// 保存订单号用于状态查询
currentOrderNo.value = result.orderNo || '';
paymentStatus.value = result.payStatus || 'PENDING';
// 支付创建成功,显示二维码
if (result.qrCodeUrl) {
qrCodeUrl.value = result.qrCodeUrl;
qrCodeVisible.value = true;
}
message.success('支付订单创建成功,请扫码支付');
} else {
message.error(result.errorMsg || '支付创建失败');
}
} catch (error) {
console.error('支付失败:', error);
message.error('购买失败,请重试');
} finally {
loading.value = false;
}
}
// 获取套餐时长(月数)
function getDurationInMonths(duration: string): number {
switch (duration) {
case '1个月':
return 1;
case '季度':
return 3;
case '半年':
return 6;
case '12个月':
return 12;
default:
return 1;
}
}
// 轮询逻辑已迁移到 PaymentQRCodeModal 组件中
// 获取状态文本
function getStatusText(status: string): string {
const statusMap: Record<string, string> = {
'INIT': '订单生成',
'ING': '支付中',
'SUCCESS': '支付成功',
'FAIL': '支付失败',
'CANCEL': '已撤销',
'REFUND': '已退款',
'CLOSED': '已关闭',
'PENDING': '待支付' // 保留兼容性
};
return statusMap[status] || '未知状态';
}
// 获取状态样式类
function getStatusClass(status: string): string {
const statusClassMap: Record<string, string> = {
'INIT': 'status-pending', // 订单生成 - 待支付状态
'ING': 'status-pending', // 支付中 - 进行中状态
'SUCCESS': 'status-success', // 支付成功
'FAIL': 'status-failed', // 支付失败
'CANCEL': 'status-failed', // 已撤销 - 失败状态
'REFUND': 'status-closed', // 已退款 - 关闭状态
'CLOSED': 'status-closed', // 已关闭
'PENDING': 'status-pending' // 兼容性保留 - 待支付
};
return statusClassMap[status] || 'status-unknown';
}
</script>
<style lang="less" scoped>
.membership-modal {
:deep(.ant-modal-content) {
border-radius: 16px;
overflow: hidden;
}
:deep(.ant-modal-header) {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-bottom: none;
padding: 24px 24px 20px;
.ant-modal-title {
color: white;
font-size: 18px;
font-weight: 600;
}
}
:deep(.ant-modal-close) {
color: white;
&:hover {
color: rgba(255, 255, 255, 0.8);
}
}
:deep(.ant-modal-body) {
padding: 0;
}
}
.membership-content {
padding: 24px;
}
.membership-benefits {
margin-bottom: 32px;
padding: 20px;
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
border-radius: 12px;
.benefits-header {
display: flex;
align-items: center;
margin-bottom: 16px;
.crown-icon {
font-size: 24px;
color: #ffd700;
margin-right: 12px;
}
h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #333;
}
}
.benefits-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
.benefit-item {
display: flex;
align-items: center;
padding: 8px 0;
.benefit-icon {
color: #52c41a;
margin-right: 8px;
font-size: 16px;
}
span {
color: #666;
font-size: 14px;
}
}
}
}
.membership-plans {
margin-bottom: 32px;
h4 {
margin-bottom: 16px;
font-size: 16px;
font-weight: 600;
color: #333;
}
.plans-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
.plan-card {
border: 2px solid #e8e8e8;
border-radius: 12px;
padding: 12px;
cursor: pointer;
transition: all 0.3s ease;
background: white;
min-width: 0;
&:hover {
border-color: #667eea;
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(102, 126, 234, 0.15);
}
&.active {
border-color: #1890ff;
background: linear-gradient(135deg, #f0f8ff 0%, #e6f7ff 100%);
color: #1890ff;
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(24, 144, 255, 0.15);
.plan-price {
color: #1890ff;
.currency {
color: #1890ff;
}
.amount {
color: #1890ff;
}
}
.feature-item {
color: #333;
.feature-icon {
color: #52c41a;
}
}
}
.plan-header {
text-align: center;
margin-bottom: 16px;
.plan-duration {
font-size: 14px;
font-weight: 600;
margin-bottom: 6px;
}
.plan-price {
.currency {
font-size: 12px;
color: #666;
}
.amount {
font-size: 20px;
font-weight: 700;
color: #667eea;
}
}
}
.plan-features {
margin-bottom: 16px;
.feature-item {
display: flex;
align-items: center;
margin-bottom: 6px;
font-size: 11px;
color: #666;
.feature-icon {
margin-right: 4px;
color: #52c41a;
font-size: 10px;
}
&.discount {
justify-content: center;
margin-top: 6px;
}
}
}
.plan-action {
.ant-btn {
border-radius: 8px;
font-weight: 500;
}
}
}
}
}
.payment-methods {
margin-bottom: 24px;
h4 {
margin-bottom: 16px;
font-size: 16px;
font-weight: 600;
color: #333;
}
.payment-options {
display: flex;
gap: 16px;
.payment-option {
flex: 1;
.payment-item {
display: flex;
align-items: center;
padding: 12px 16px;
border: 2px solid #e8e8e8;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
border-color: #667eea;
}
.payment-icon {
font-size: 20px;
margin-right: 8px;
&.wechat {
color: #07c160;
}
&.alipay {
color: #1677ff;
}
}
span {
font-size: 14px;
color: #333;
}
}
}
:deep(.ant-radio-wrapper) {
width: 100%;
margin-right: 0;
.ant-radio {
display: none;
}
&.ant-radio-wrapper-checked .payment-item {
border-color: #667eea;
background: #f0f2ff;
}
}
}
}
.order-summary {
margin-bottom: 24px;
.summary-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
font-size: 14px;
.price {
font-weight: 600;
color: #667eea;
font-size: 16px;
}
.order-no {
font-family: monospace;
color: #666;
font-size: 12px;
}
.status {
font-weight: 500;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
&.status-pending {
background: #fff7e6;
color: #fa8c16;
}
&.status-success {
background: #f6ffed;
color: #52c41a;
}
&.status-failed {
background: #fff2f0;
color: #ff4d4f;
}
&.status-closed {
background: #f5f5f5;
color: #999;
}
&.status-unknown {
background: #f0f0f0;
color: #666;
}
}
&.discount {
.original-price {
text-decoration: line-through;
color: #999;
}
}
}
}
.modal-actions {
display: flex;
gap: 12px;
justify-content: flex-end;
.ant-btn {
min-width: 100px;
border-radius: 8px;
font-weight: 500;
}
}
// 响应式设计
@media (max-width: 1000px) {
.plans-grid {
grid-template-columns: repeat(2, 1fr) !important;
gap: 12px !important;
}
}
@media (max-width: 768px) {
.membership-content {
padding: 16px;
}
.plans-grid {
grid-template-columns: repeat(2, 1fr) !important;
gap: 12px !important;
}
.payment-options {
flex-direction: column;
gap: 12px;
}
.modal-actions {
flex-direction: column;
.ant-btn {
width: 100%;
}
}
}
@media (max-width: 480px) {
.plans-grid {
grid-template-columns: 1fr !important;
}
.benefits-list {
grid-template-columns: 1fr !important;
}
}
// 二维码弹窗样式已迁移到 PaymentQRCodeModal.vue 组件中
</style>