Files
Yi.Framework/Yi.Ai.Vue3/src/components/ProductPackage/index.vue
2025-08-15 23:28:09 +08:00

971 lines
27 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.
<script setup lang="ts">
import { CircleCheck } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
import { createOrder, getOrderStatus } from '@/api';
import SupportModelList from '@/components/userPersonalCenter/components/SupportModelList.vue';
import ProductPage from '@/pages/products/index.vue';
const emit = defineEmits(['close']);
const packagesData = {
member: [
{ id: 1, name: '10个月', desc: '', price: 199.9, perMonth: 19.9, tag: '超高性价比', discount: '长期省心', key: 10 },
{ id: 2, name: '6个月', desc: '', price: 143.9, perMonth: 23.9, tag: '年度热销', key: 6 },
{ id: 3, name: '3个月', desc: '', price: 80.7, perMonth: 26.9, tag: '短期体验', discount: '', key: 3 },
{ id: 4, name: '1个月', desc: '', price: 29.9, originalPrice: 49.9, tag: '灵活选择', discount: '', key: 1 },
{ id: 5, name: '测试', desc: '', price: 0.01, originalPrice: 9.9, tag: '测试使用', discount: '', key: 0 },
],
token: [
{ id: 6, name: '10M 输入Token', desc: '', price: 49.9, tag: '轻量用户', discount: '' },
{ id: 7, name: '20M 输入Token', desc: '', price: 79.9, tag: '中等使用', discount: '' },
{ id: 8, name: '30M 输入Token', desc: '', price: 99.9, tag: '量大管饱', discount: '' },
{ id: 9, name: '联系站长', desc: '', price: 0, tag: '企业级需求', discount: '' },
],
};
const visible = ref(true);
const activeTab = ref('member');
const selectedId = ref<number | null>(packagesData.member[3].id);
const selectedPrice = ref(packagesData.member[3].price);
const selectPackageObject = ref<any>(packagesData.member[3]);
const showDetails = ref(false);
const isMobile = ref(false);
const isLoading = ref(false);
const paymentWindow = ref<Window | null>(null);
const pollInterval = ref<NodeJS.Timeout | null>(null);
function checkMobile() {
isMobile.value = window.innerWidth < 768;
}
onMounted(() => {
checkMobile();
window.addEventListener('resize', checkMobile);
});
onBeforeUnmount(() => {
window.removeEventListener('resize', checkMobile);
cleanupPayment();
});
function cleanupPayment() {
if (pollInterval.value) {
clearInterval(pollInterval.value);
pollInterval.value = null;
}
// if (paymentWindow.value && !paymentWindow.value.closed) {
// paymentWindow.value.close();
// }
// 清除轮询定时器
if (pollInterval.value) {
clearInterval(pollInterval.value);
pollInterval.value = null;
}
// 关闭支付窗口(如果还在)
// if (paymentWindow.value && !paymentWindow.value.closed) {
// paymentWindow.value.close();
// paymentWindow.value = null;
// }
retryCount = 0; // 重置重试计数器
}
const tabs = [
{ key: 'member', label: '会员套餐' },
// { key: 'token', label: 'Token 套餐' },
];
const benefitsData = {
member: [
{ name: '站点全模型解锁使用', value: '' },
{ name: '全站所有Ai日常“无限制”使用', value: '' },
{ name: 'Ai专线超级加速', value: '' },
{ name: '专属Api接口提供', value: '' },
{ name: '支持文件/图片/知识库功能', value: '' },
{ name: '支持各类第三方工具集成IDE/翻译/Utools等', value: '' },
{ name: '支持Mcp/FunctionCall开发', value: '' },
{ name: '支持安卓/ios/web/客户端使用', value: '' },
{ name: '支持售后群服务一起畅玩前沿Ai', value: '' },
],
token: [
{ name: 'Token 用途', value: '用于调用 API 或模型生成内容' },
{ name: '灵活计费', value: '按调用量扣费,更加自由' },
{ name: '支持多模型', value: '适配多种模型调用需求' },
],
};
const fullBenefitsData = {
member: [
{ category: 'YiXinAI AI Pro', free: '1项', vip: '5项', value: '价值68/月' },
{ category: '基础对话', free: '3条/天', vip: '不限条款', value: 'DeepSeek-R1 32b蒸馏版、AI-4o mini' },
{ category: '高级对话', free: '-', vip: '400条/月', value: 'DeepSeek-R1 671b满血版、4o、Cd3.5s、Code、文档对话' },
{ category: 'AI基础绘画', free: '-', vip: '500条/月', value: '' },
{ category: 'AI高级绘画', free: '-', vip: '50条/月', value: '' },
{ category: 'AI-4.0联网搜索', free: '-', vip: '支持', value: '' },
{ category: 'AI PPT', free: '1项', vip: '12项', value: '价值99/月' },
{ category: 'AI 思维导图', free: '1项', vip: '8项', value: '价值99/月' },
{ category: '文档对话', free: '0项', vip: '6项', value: '价值99/月' },
{ category: 'AI 绘图', free: '0项', vip: '5项', value: '价值99/月' },
{ category: 'AI 写作', free: '1项', vip: '6项', value: '价值99/月' },
],
};
const currentPackages = computed(() => packagesData[activeTab.value]);
const currentBenefits = computed(() => benefitsData[activeTab.value]);
function selectPackage(pkg: any) {
selectedId.value = pkg.id;
selectedPrice.value = pkg.price;
selectPackageObject.value = pkg;
}
async function pay() {
if (!selectedId.value) {
ElMessage.warning('请选择一个套餐');
return;
}
isLoading.value = true;
try {
const params = {
goodsType: selectPackageObject.value?.key,
};
const response = await createOrder(params);
console.log('订单创建成功:', response);
if (response.data.paymentPageHtml) {
handlePaymentPage(response.data.paymentPageHtml, response.data.orderId, response.data.outTradeNo);
}
else {
throw new Error('未获取到支付页面');
}
}
catch (error: any) {
console.error('支付失败:', error);
ElMessage.error(`支付失败: ${error.message || '未知错误'}`);
}
finally {
isLoading.value = false;
}
}
function handlePaymentPage(html: string, orderId: string, outTradeNo: string) {
// 关闭当前弹窗(如果有)
close();
// 创建支付窗口
paymentWindow.value = window.open('', '_blank');
if (!paymentWindow.value) {
ElMessage.error('无法打开支付窗口,请检查浏览器弹窗设置或允许弹窗');
return;
}
// 写入支付页面HTML并自动提交
paymentWindow.value.document.open();
paymentWindow.value.document.write(html);
// paymentWindow.value.document.close();
// 3秒后开始轮询支付状态给支付页面加载时间
setTimeout(() => {
startPolling(outTradeNo);
}, 3000);
}
function startPolling(outTradeNo: string) {
// 先清理之前的轮询任务
cleanupPayment();
// 立即检查一次状态(避免等待第一个间隔)
checkPaymentStatus(outTradeNo);
// 设置轮询任务每3秒检查一次
pollInterval.value = setInterval(() => {
checkPaymentStatus(outTradeNo);
}, 3000);
}
let retryCount = 0; // 错误重试计数器
async function checkPaymentStatus(outTradeNo: string) {
try {
const result = await getOrderStatus(outTradeNo);
console.log('订单状态检查结果:', result);
if (result.data.tradeStatus === 'TRADE_SUCCESS') {
// 支付成功处理
cleanupPayment();
ElMessage.success('支付成功!');
close(); // 关闭弹窗
// 可以在这里添加跳转到成功页面的逻辑
// window.location.href = '/pay/success?order=' + outTradeNo;
}
else if (result.data.tradeStatus === 'TRADE_CLOSED'
|| result.data.tradeStatus === 'TRADE_FAILED') {
// 支付失败处理
cleanupPayment();
ElMessage.warning(`支付失败: ${result.data.tradeStatusDesc || '未知原因'}`);
}
// 其他状态(如待支付)不做处理,继续轮询
}
catch (error) {
console.error('检查订单状态失败:', error);
// 网络错误等情况,可以重试几次后停止
if (retryCount > 3) {
cleanupPayment();
ElMessage.error('检查支付状态失败,请手动刷新页面确认');
}
retryCount++;
}
}
const router = useRouter();
function toggleDetails() {
showDetails.value = !showDetails.value;
}
function close() {
visible.value = false;
emit('close');
}
function onClose() {
emit('close');
}
</script>
<template>
<el-dialog
v-model="visible"
:width="isMobile ? '90%' : '980px'"
:fullscreen="isMobile && showDetails"
:show-close="false"
destroy-on-close
class="product-package-dialog"
@close="onClose"
>
<!-- 详情页 -->
<div v-if="showDetails" class="details-view">
<!-- 顶部标题和返回按钮 -->
<div class="flex items-center mb-6 sticky top-0 bg-white z-10 pt-2 pb-4">
<el-button text circle size="small" class="mr-2" @click="toggleDetails">
</el-button>
<div class="text-xl font-bold">
YiXinAI会员详细权益
</div>
</div>
<ProductPage />
<!-- 权益详情表格 -->
<div v-if="false" class="benefits-table">
<div class="table-header">
<div class="table-cell">
服务项
</div>
<div class="table-cell">
免费用户
</div>
<div class="table-cell">
AI大会员
</div>
</div>
<div v-for="(item, index) in fullBenefitsData.member" :key="index" class="table-row">
<div class="table-cell font-medium">
{{ item.category }}
</div>
<div class="table-cell">
{{ item.free }}
</div>
<div class="table-cell">
<div>{{ item.vip }}</div>
<div v-if="item.value" class="text-gray-500 text-xs">
{{ item.value }}
</div>
</div>
</div>
</div>
</div>
<!-- 主页面 -->
<div v-else>
<!-- 顶部标题和关闭按钮 -->
<div class="flex justify-between items-center mb-4">
<div class="text-xl font-bold">
购买套餐
</div>
<el-button circle size="small" @click="close">
</el-button>
</div>
<!-- Tab 切换 -->
<div class="flex border-b mb-6 overflow-x-auto">
<div
v-for="tab in tabs"
:key="tab.key"
class="cursor-pointer px-5 py-2 -mb-px border-b-2 transition whitespace-nowrap"
:class="activeTab === tab.key
? 'border-orange-500 text-orange-500 font-semibold'
: 'border-transparent text-gray-500 hover:text-orange-500'"
@click="activeTab = tab.key"
>
{{ tab.label }}
</div>
</div>
<!-- 移动端布局 -->
<div v-if="isMobile" class="mobile-layout">
<!-- 套餐卡片列表 -->
<div class="package-list">
<div
v-for="pkg in currentPackages"
:key="pkg.id"
class="package-card"
:class="{ selected: pkg.id === selectedId }"
@click="selectPackage(pkg)"
>
<!-- 标签 -->
<div v-if="pkg.discount" class="discount-tag">
{{ pkg.discount }}
</div>
<div v-if="pkg.tag" class="tag">
{{ pkg.tag }}
</div>
<!-- 套餐信息 -->
<div class="package-info">
<div class="package-name">
{{ pkg.name }}
</div>
<div class="package-desc">
{{ pkg.desc }}
</div>
</div>
<!-- 价格 -->
<div class="package-price">
<span class="price">¥{{ pkg.price }}</span>
<span v-if="pkg.perMonth" class="per-month">
¥{{ pkg.perMonth }}/
</span>
<div v-if="pkg.originalPrice" class="original-price">
原价¥{{ pkg.originalPrice }}
</div>
</div>
</div>
</div>
<!-- 权益预览 -->
<div class="benefits-preview max-h-200px overflow-y-auto">
<div class="section-title">
专属权益
</div>
<ul class="benefits-list">
<li
v-for="(b, index) in currentBenefits"
:key="index"
class="benefit-item"
>
<span class="dot">
<el-icon><CircleCheck /></el-icon>
</span>
<span>
<span class="benefit-name">{{ b.name }}</span>
<span v-if="b.value">{{ b.value }}</span>
</span>
</li>
</ul>
<SupportModelList />
</div>
<!-- 支付区域 -->
<div class="payment-area">
<div v-if="false" class="agreement-text">
登录和注册都代表同意YiXinAI的会员协议
</div>
<div class="payment-info">
<div class="actual-payment">
<span>实际支付</span>
<span class="price">¥{{ selectedPrice || 0 }}</span>
</div>
<el-button
text
type="primary"
class="view-details-btn"
@click="toggleDetails"
>
了解更多
</el-button>
<el-button
type="primary"
:disabled="!selectedId || isLoading"
:loading="isLoading"
class="pay-button"
@click="pay"
>
立即支付
</el-button>
</div>
<div class="note-text">
可叠加购买次数过期时间以最后订单为准<br>
最终解释权归YiXinAI所有
</div>
</div>
</div>
<!-- 桌面端布局 -->
<div v-else class="flex gap-6 desktop-layout">
<!-- 左栏 套餐卡片 + 支付 -->
<div class="w-[60%] flex flex-col justify-between">
<!-- 套餐卡片列表 -->
<div class="flex flex-wrap gap-4">
<div
v-for="pkg in currentPackages"
:key="pkg.id"
class="package-card"
:class="{ selected: pkg.id === selectedId }"
@click="selectPackage(pkg)"
>
<!-- 标签 -->
<div v-if="pkg.discount" class="discount-tag">
{{ pkg.discount }}
</div>
<div v-if="pkg.tag" class="tag">
{{ pkg.tag }}
</div>
<!-- 套餐信息 -->
<div class="package-info">
<div class="package-name">
{{ pkg.name }}
</div>
<div class="package-desc">
{{ pkg.desc }}
</div>
</div>
<!-- 价格 -->
<div class="package-price">
<span class="price">¥{{ pkg.price }}</span>
<span v-if="pkg.perMonth" class="per-month">
¥{{ pkg.perMonth }}/
</span>
<div v-if="pkg.originalPrice" class="original-price">
原价¥{{ pkg.originalPrice }}
</div>
</div>
</div>
</div>
<!-- 支付按钮 -->
<div class="payment-section">
<div v-if="false" class="agreement-text">
登录和注册都代表同意YiXinAI的会员协议
</div>
<div class="payment-action">
<div class="actual-payment">
<span>实际支付</span>
<span class="price">¥{{ selectedPrice || 0 }}</span>
</div>
<div>
<el-button class="pay-button" text type="primary" @click="toggleDetails">
了解更多
</el-button>
<el-button
type="primary"
size="large"
:disabled="!selectedId || isLoading"
:loading="isLoading"
class="pay-button"
@click="pay"
>
立即支付
</el-button>
</div>
</div>
<div class="note-text">
可叠加购买次数过期时间以最后订单为准<br>
最终解释权归YiXinAI所有
</div>
</div>
</div>
<!-- 右栏 套餐权益 + 详情 -->
<div class="w-[40%] flex flex-col justify-between right-panel max-h-400px overflow-y-auto">
<div>
<div class="section-title">
会员权益
</div>
<ul class="benefits-list ">
<li
v-for="(b, index) in currentBenefits"
:key="index"
class="benefit-item "
>
<span class="dot">
<el-icon><CircleCheck /></el-icon>
</span>
<span>
<span class="benefit-name">{{ b.name }}</span>
<span v-if="b.value">{{ b.value }}</span>
</span>
</li>
</ul>
<!-- 额外描述 -->
<div v-if="activeTab === 'member'" class="extra-description ">
<SupportModelList />
<!-- <div class="description-card"> -->
<!-- <div class="title"> -->
<!-- 前沿模型AI对话 -->
<!-- </div> -->
<!-- <div class="subtext"> -->
<!-- DP-RI深度思考精准解答 -->
<!-- </div> -->
<!-- <div class="subtext"> -->
<!-- AI写作文档对话AI思维导图等赋能职场 -->
<!-- </div> -->
<!-- </div> -->
<!-- <div class="description-card"> -->
<!-- <div class="title"> -->
<!-- AI绘图与设计能力 -->
<!-- </div> -->
<!-- <div class="subtext"> -->
<!-- 视觉吸睛赋能 -->
<!-- </div> -->
<!-- <div class="subtext"> -->
<!-- "AI+办公!"解锁300+工具箱会员权益 -->
<!-- </div> -->
<!-- </div> -->
</div>
</div>
<!-- 查看详情 -->
<div class="view-details">
<!-- <el-button text type="primary" @click="toggleDetails"> -->
<!-- 查看详情 -->
<!-- </el-button> -->
</div>
</div>
</div>
</div>
</el-dialog>
</template>
<style scoped lang="scss">
.product-package-dialog {
.el-dialog__header {
display: none;
}
.details-view {
height: 600px;
overflow-y: auto;
padding-right: 8px;
.benefits-table {
display: table;
width: 100%;
border-collapse: collapse;
.table-header, .table-row {
display: table-row;
}
.table-cell {
display: table-cell;
padding: 12px 16px;
border-bottom: 1px solid #eee;
vertical-align: middle;
}
.table-header {
font-weight: bold;
background-color: #f8f8f8;
.table-cell {
border-bottom: 2px solid #ddd;
}
}
}
}
/* 移动端样式 */
.mobile-layout {
display: flex;
flex-direction: column;
gap: 24px;
.package-list {
display: grid;
grid-template-columns: 1fr;
gap: 12px;
}
.package-card {
position: relative;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 16px;
transition: all 0.3s;
&.selected {
border: 3px solid #fde19d;
background-image: url('@/assets/images/product_background.svg');
background-repeat: no-repeat; /* 按需设置是否重复 */
background-size: cover; /* 按需设置背景图尺寸适配方式,比如 cover、contain 等 */
background-position: bottom; /* 按需设置背景图位置 */
box-shadow: 0 4px 6px -1px #fff4e3;
}
.discount-tag {
position: absolute;
top: -6px;
left: 8px;
background-color: #ef4444;
color: white;
font-size: 12px;
padding: 2px 8px;
border-radius: 4px;
}
.tag {
position: absolute;
top: -6px;
right: 8px;
background-color: #f97316;
color: white;
font-size: 12px;
padding: 2px 8px;
border-radius: 4px;
}
.package-info {
margin-bottom: 12px;
.package-name {
font-size: 16px;
font-weight: 600;
}
.package-desc {
font-size: 12px;
color: #6b7280;
}
}
.package-price {
.price {
font-size: 20px;
font-weight: 700;
color: #f97316;
}
.per-month {
font-size: 12px;
color: #6b7280;
margin-left: 4px;
}
.original-price {
font-size: 12px;
color: #9ca3af;
text-decoration: line-through;
}
}
}
.benefits-preview {
background-color: #f9fafb;
border-radius: 8px;
padding: 16px;
.section-title {
font-weight: 600;
margin-bottom: 12px;
}
.benefits-list {
list-style: none;
padding: 0;
margin: 0;
.benefit-item {
display: flex;
align-items: flex-start;
margin-bottom: 8px;
font-size: 14px;
color: #4b5563;
.dot {
color: #f97316;
margin-right: 8px;
}
.benefit-name {
font-weight: 500;
}
}
}
.view-details-btn {
width: 100%;
margin-top: 16px;
justify-content: flex-end;
}
}
.payment-area {
border-top: 1px solid #e5e7eb;
padding-top: 16px;
.agreement-text {
font-size: 12px;
color: #6b7280;
margin-bottom: 12px;
}
.payment-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
.actual-payment {
.price {
font-size: 18px;
font-weight: 700;
color: #f97316;
}
}
.pay-button {
width: 120px;
}
}
.note-text {
font-size: 12px;
color: #9ca3af;
}
}
}
/* 桌面端样式 */
.desktop-layout {
.package-card {
position: relative;
width: calc(50% - 0.5rem);
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 16px;
transition: all 0.3s;
display: flex;
flex-direction: column;
justify-content: space-between;
//background-color: rgba(255, 245, 238, 0.38);
background: linear-gradient(90deg, #f1f2ebb5, white);
&.selected {
border: 3px solid #fde19d;
background-image: url('@/assets/images/product_background.svg');
background-repeat: no-repeat; /* 按需设置是否重复 */
background-size: cover; /* 按需设置背景图尺寸适配方式,比如 cover、contain 等 */
background-position: bottom; /* 按需设置背景图位置 */
box-shadow: 0 4px 6px -1px #fff4e3;
}
.discount-tag {
position: absolute;
top: -6px;
left: 8px;
background-color: #ef4444;
color: white;
font-size: 12px;
padding: 2px 8px;
border-radius: 4px;
}
.tag {
position: absolute;
top: -6px;
right: 8px;
background-color: #f97316;
color: white;
font-size: 12px;
padding: 2px 8px;
border-radius: 4px;
}
.package-info {
margin-bottom: 12px;
.package-name {
font-size: 16px;
font-weight: 600;
}
.package-desc {
font-size: 12px;
color: #6b7280;
}
}
.package-price {
.price {
font-size: 20px;
font-weight: 700;
color: #f97316;
}
.per-month {
font-size: 12px;
color: #6b7280;
margin-left: 4px;
}
.original-price {
font-size: 12px;
color: #9ca3af;
text-decoration: line-through;
}
}
}
.payment-section {
border-top: 1px solid #e5e7eb;
padding-top: 16px;
margin-top: 16px;
.agreement-text {
font-size: 12px;
color: #6b7280;
margin-bottom: 12px;
}
.payment-action {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
.actual-payment {
.price {
font-size: 18px;
font-weight: 700;
color: #f97316;
}
}
}
.note-text {
font-size: 12px;
color: #9ca3af;
}
}
.right-panel {
.section-title {
font-weight: 600;
margin-bottom: 12px;
}
.benefits-list {
list-style: none;
padding: 0;
margin: 0;
.benefit-item {
display: flex;
align-items: flex-start;
margin-bottom: 8px;
font-size: 14px;
color: #4b5563;
.dot {
color: #f97316;
margin-right: 8px;
}
.benefit-name {
font-weight: 500;
}
}
}
.extra-description {
margin-top: 24px;
.description-card {
background-color: #f9fafb;
border-radius: 8px;
padding: 12px;
margin-bottom: 12px;
.title {
font-weight: 600;
margin-bottom: 4px;
}
.subtext {
font-size: 12px;
color: #6b7280;
line-height: 1.4;
}
}
}
.view-details {
border-top: 1px solid #e5e7eb;
padding-top: 16px;
margin-top: 16px;
}
}
}
/* 响应式调整 */
@media (max-width: 768px) {
.el-dialog {
margin-top: 20px !important;
margin-bottom: 20px !important;
}
.details-view {
height: auto;
max-height: 80vh;
.benefits-table {
display: block;
overflow-x: auto;
white-space: nowrap;
.table-header, .table-row {
display: table;
width: 100%;
table-layout: fixed;
}
}
}
}
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-thumb {
background-color: #ccc;
border-radius: 3px;
}
}
</style>