feat: 增加支付宝在线支付、套餐订购弹窗、会员权益、支持模型展示等

This commit is contained in:
Gsh
2025-08-14 00:26:39 +08:00
parent 48d8c528f6
commit ee6b4827fa
11 changed files with 1564 additions and 38 deletions

View File

@@ -1,5 +1,6 @@
export * from './auth';
export * from './chat';
export * from './model';
export * from './pay';
export * from './session';
export * from './user';

View File

@@ -0,0 +1,11 @@
import { get, post } from '@/utils/request.ts';
// 创建订单并发起支付
export function createOrder(params: any) {
return post<any>(`/pay/Order`, params).json();
}
// 查询订单状态
export function getOrderStatus(OutTradeNo: any) {
return get<any>(`/pay/OrderStatus?OutTradeNo=${OutTradeNo}`).json();
}

View File

@@ -56,6 +56,7 @@ async function showPopover() {
// 点击
// 处理模型点击
function handleModelClick(item: GetSessionListVO) {
console.log('modelStore.modelList', modelStore.modelList);
if (!isModelAvailable(item)) {
ElMessageBox.confirm(
`

View File

@@ -0,0 +1,947 @@
<script setup lang="ts">
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 visible = ref(true);
const activeTab = ref('member');
const selectedId = ref<number | null>(null);
const selectedPrice = ref(0);
const selectPackageObject = ref<any>(null);
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 packagesData = {
member: [
{ id: 1, name: '10个月', desc: '', price: 199, 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 benefitsData = {
member: [
{ name: '基础+高级模型访问', value: '' },
{ name: 'AI专线超级加速', value: '' },
{ name: '售后微信群支持', value: '' },
{ name: '专属Api服务', 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"></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"></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-color: #f97316;
background-color: #fff7ed;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.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;
&.selected {
border-color: #f97316;
background-color: #fff7ed;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.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>

View File

@@ -0,0 +1,202 @@
<script lang="ts" setup>
import { useModelStore } from '@/stores/modules/model';
// interface Model {
// id: string;
// category: string;
// modelId: string;
// modelName: string;
// modelDescribe: string;
// modelPrice: number;
// modelType: string;
// modelShow: string;
// systemPrompt: string | null;
// apiHost: string | null;
// apiKey: string | null;
// remark: string;
// }
// 从store获取模型列表
/*
const modelList = ref<Model[]>([
{
id: '077be103-1456-a4bb-409c-3a15f04e1ad9',
category: 'chat',
modelId: 'DeepSeek-R1-0528',
modelName: 'DeepSeek-R1',
modelDescribe: '国产开源,深度思索模式,不过幻读问题比较大,同时具备思考响应链,在开源模型中永远的神!',
modelPrice: 0,
modelType: '1',
modelShow: '0',
systemPrompt: null,
apiHost: null,
apiKey: null,
remark: '国产开源,深度思索模式,不过幻读问题比较大,同时具备思考响应链,在开源模型中永远的神!',
},
{
id: '077be103-1456-a4bb-409c-3a15f04e1ab7',
category: 'chat',
modelId: 'DeepSeek-V3-0324',
modelName: 'DeepSeek-V3',
modelDescribe: '国产开源,简单聊天模式,对于中文文章语义体验较好,但响应速度一般',
modelPrice: 0,
modelType: '1',
modelShow: '0',
systemPrompt: null,
apiHost: null,
apiKey: null,
remark: '国产开源,简单聊天模式,对于中文文章语义体验较好,但响应速度一般',
},
]);
*/
// 实际使用时您可以从store中获取模型列表
// import { useModelStore } from '@/stores/model';
const modelStore = useModelStore();
const modelList = computed(() => modelStore.modelList);
console.log('modelList---', modelList);
</script>
<template>
<div class="model-container">
<div class="model-header">
支持的模型
</div>
<div class="model-grid">
<div v-for="model in modelList" :key="model.id" class="model-card">
<div class="model-card-header">
<h3 class="model-name">
{{ model.modelName }}
</h3>
<div class="model-price">
<template v-if="model.modelPrice === 0">
<span class="free-tag">{{ model.modelId === 'DeepSeek-R1-0528' ? '免费' : 'Vip专享' }}</span>
</template>
<template v-else>
<span class="price">{{ model.modelPrice }}/</span>
<span class="per-token">{{ model.modelPrice * 100 }}/百万Token</span>
</template>
</div>
</div>
<div class="model-description">
{{ model.modelDescribe }}
</div>
<div class="model-footer">
<span class="model-id">{{ model.modelId }}</span>
<!-- <el-tag v-if="model.category === 'chat'" size="small" type="success"> -->
<!-- 对话 -->
<!-- </el-tag> -->
<!-- <el-tag v-else size="small" type="info"> -->
<!-- 其他 -->
<!-- </el-tag> -->
</div>
</div>
</div>
</div>
</template>
<style scoped>
.model-container {
//padding: 10px;
max-width: 300px;
margin: 10px 0 ;
}
.model-header {
font-size: 14px;
margin-bottom: 24px;
color: #333;
font-weight: 600;
}
.model-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 20px;
}
.model-card {
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
padding: 10px;
transition: all 0.3s ease;
border: 1px solid #ebeef5;
}
.model-card:hover {
transform: translateY(-5px);
box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.1);
}
.model-card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
.model-name {
font-size: 14px;
font-weight: 600;
margin: 0;
color: #333;
}
.model-price {
text-align: right;
}
.free-tag {
background: #f0f9eb;
color: #67c23a;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.price {
display: block;
font-size: 13px;
font-weight: 600;
color: #f56c6c;
}
.per-token {
font-size: 12px;
color: #909399;
}
.model-description {
color: #606266;
line-height: 1.6;
margin-bottom: 16px;
min-height: 60px;
}
.model-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 12px;
border-top: 1px solid #f0f0f0;
}
.model-id {
font-size: 12px;
color: #909399;
}
@media (max-width: 768px) {
.model-grid {
grid-template-columns: 1fr;
}
.model-card {
padding: 16px;
}
}
</style>

View File

@@ -5,6 +5,7 @@ import Popover from '@/components/Popover/index.vue';
import SvgIcon from '@/components/SvgIcon/index.vue';
import { useUserStore } from '@/stores';
import { useSessionStore } from '@/stores/modules/session';
import { showProductPackage } from '@/utils/product-package';
import { getUserProfilePicture, isUserVip } from '@/utils/user';
const router = useRouter();
@@ -63,6 +64,7 @@ const navItems = [
{ name: 'apiKey', label: 'API密钥', icon: 'Key' },
{ name: 'rechargeLog', label: '充值记录', icon: 'Document' },
{ name: 'usageStatistics', label: '用量统计', icon: 'Histogram' },
// { name: 'usageStatistics2', label: '用量统计2', icon: 'Histogram' },
];
function openDialog() {
dialogVisible.value = true;
@@ -163,12 +165,30 @@ function openVipGuide() {
}
/* 弹出面板 结束 */
function onProductPackage() {
showProductPackage();
}
// 直接调用
</script>
<template>
<div class="flex items-center gap-2">
<el-button
class="buy-btn flex items-center gap-2 px-5 py-2 font-semibold shadow-lg"
@click="onProductPackage"
>
<!-- <svg -->
<!-- xmlns="http://www.w3.org/2000/svg" -->
<!-- class="icon-rocket w-5 h-5 animate-bounce" -->
<!-- viewBox="0 0 24 24" -->
<!-- fill="currentColor" -->
<!-- > -->
<!-- <path d="M12 2C10 5 8 8 8 11l-5 5c0 3 3 3 5 5s5-3 8-5l5-5c-3-2-6-4-8-9z" /> -->
<!-- </svg> -->
<span>立即购买</span>
</el-button>
<!-- 用户信息区域 -->
<div class=" cursor-pointer flex flex-col text-right mr-2 leading-tight" @click="openVipGuide">
<div class=" cursor-pointer flex flex-col text-right mr-2 leading-tight" @click="onProductPackage">
<div class="text-sm font-semibold text-gray-800">
{{ userStore.userInfo?.user.nick ?? '未登录用户' }}
</div>
@@ -264,6 +284,10 @@ function openVipGuide() {
<template #usageStatistics>
<usage-statistics />
</template>
<!-- 用量统计 -->
<!-- <template #usageStatistics2> -->
<!-- <usage-statistics2 /> -->
<!-- </template> -->
<!-- 角色管理内容 -->
<template #role>
@@ -297,4 +321,31 @@ function openVipGuide() {
border-radius: 8px;
box-shadow: 0 4px 16px rgb(0 0 0 / 8%);
}
.buy-btn {
background: linear-gradient(90deg, #FFD700, #FFC107);
color: #fff;
border: none;
border-radius: 9999px;
transition: transform 0.2s, box-shadow 0.2s;
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(255, 215, 0, 0.5);
background: linear-gradient(90deg, #FFC107, #FFD700);
}
.icon-rocket {
color: #fff;
}
.animate-bounce {
animation: bounce 1.2s infinite;
}
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-4px); }
}
</style>

View File

@@ -0,0 +1,201 @@
<script setup lang="ts">
import { ElButton, ElDivider } from 'element-plus';
import { onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
import { getUserInfo } from '@/api';
import {
SSO_CLIENT_LOGIN_AGAIN,
SSO_SEVER_URL,
} from '@/config/sso.ts';
import { useUserStore } from '@/stores';
import { useSessionStore } from '@/stores/modules/session.ts';
const userStore = useUserStore();
const router = useRouter();
const sessionStore = useSessionStore();
interface PayResult {
out_trade_no: string;
trade_no: string;
total_amount: string;
[key: string]: string;
}
const payResult = ref<PayResult>({
out_trade_no: '',
trade_no: '',
total_amount: '',
});
function parseUrlParams() {
const params = new URLSearchParams(window.location.search);
const result: PayResult = {
out_trade_no: params.get('out_trade_no') || '',
trade_no: params.get('trade_no') || '',
total_amount: params.get('total_amount') || '',
};
params.forEach((value, key) => {
if (!(key in result))
result[key] = value;
});
payResult.value = result;
}
onMounted(() => {
parseUrlParams();
});
function handleThirdPartyLogin(type: any) {
const redirectUri = encodeURIComponent(`${window.location.origin}/chat`);
console.log('cccc', type);
const popup = window.open(
`${SSO_SEVER_URL}/login?client_id=${type}&redirect_uri=${redirectUri}`,
'SSOLogin',
'width=1000,height=800',
);
// 使用标志位防止重复执行
let isHandled = false;
const messageHandler = async (event: any) => {
if (event.origin === new URL(SSO_SEVER_URL).origin
&& event.data.type === 'SSO_LOGIN_SUCCESS'
&& !isHandled) {
isHandled = true;
try {
// 清理监听
window.removeEventListener('message', messageHandler);
const { token, refreshToken } = event.data;
userStore.setToken(token, refreshToken);
const resUserInfo = await getUserInfo();
userStore.setUserInfo(resUserInfo.data);
// 关闭弹窗
if (popup && !popup.closed) {
popup.close();
}
// 后续逻辑
ElMessage.success('登录成功');
userStore.closeLoginDialog();
await sessionStore.requestSessionList(1, true);
await router.replace('/');
}
catch (error) {
console.error('登录处理失败:', error);
ElMessage.error('登录失败');
}
}
};
// 先移除旧监听,再添加新监听
window.removeEventListener('message', messageHandler);
window.addEventListener('message', messageHandler);
// 超时自动清理
// setTimeout(() => {
// if (!isHandled) {
// window.removeEventListener('message', messageHandler);
// if (popup && !popup.closed)
// popup.close();
// ElMessage.warning('登录超时');
// }
// }, 60 * 1000); // 60分钟超时关闭
}
function toHome() {
router.replace('/');
}
</script>
<template>
<div class="pay-result-container flex flex-col items-center justify-center p-6 min-h-screen bg-gray-50">
<div class="card bg-white rounded-xl shadow-lg p-8 max-w-md w-full text-center">
<!-- 成功提示 -->
<h1 class="text-4xl font-extrabold mb-4 text-orange-500">
🎉 恭喜
</h1>
<p class="text-xl font-semibold mb-6">
您已成为尊贵的 <span class="text-orange-500">YixinAI VIP</span>
</p>
<!-- 订单信息卡片 -->
<div class="order-info bg-gray-100 p-4 rounded-lg mb-4 text-left">
<p class="mb-2">
<strong>商户订单号</strong>{{ payResult.out_trade_no }}
</p>
<p class="mb-2">
<strong>支付交易号</strong>{{ payResult.trade_no }}
</p>
<p class="mb-0">
<strong>支付金额</strong><span class="text-red-500 font-bold">¥{{ payResult.total_amount }}</span>
</p>
</div>
<!-- 更多信息提示 -->
<div class="mb-6 text-gray-600 text-sm">
更多订单信息和会员详情<br>请前往 <strong>用户中心 充值记录</strong> 查看<br>
用户中心在首页右上角个人头像点击下拉菜单
</div>
<!-- 重新登录提示 -->
<div class="mb-4 text-gray-600">
开通 VIP 需要重新登录意社区生效
</div>
<ElDivider content-position="center">
<span class="text-gray-400 text-sm">操作</span>
</ElDivider>
<!-- 按钮区域 -->
<div class="flex flex-col gap-3 mt-4">
<ElButton
class="w-full py-3 text-lg font-medium"
type="primary"
size="large"
@click="handleThirdPartyLogin(SSO_CLIENT_LOGIN_AGAIN)"
>
意社区重新登录
</ElButton>
<ElButton
class="w-full py-3 text-lg font-medium"
type="default"
size="large"
@click="toHome()"
>
返回首页
</ElButton>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.pay-result-container {
.card {
transition: transform 0.2s;
&:hover {
transform: translateY(-5px);
}
}
.order-info p {
font-size: 1rem;
}
.el-button {
border-radius: 8px;
}
@media (max-width: 768px) {
.card {
padding: 6vw;
}
.order-info p {
font-size: 0.95rem;
}
.el-button {
font-size: 1rem;
}
}
}
</style>

View File

@@ -1,5 +1,4 @@
<script lang="ts" setup>
import { CircleCheck } from '@element-plus/icons-vue';
import { ElMessageBox } from 'element-plus';
import SupportModelProducts from '@/components/SupportModelProducts/indexl.vue';
@@ -193,46 +192,124 @@ function openContact() {
</p>
<div class="grid md:grid-cols-3 gap-6 flex justify-center">
<el-card
style="width: 400px;"
v-for="(plan, index) in pricing"
:key="index"
class="rounded-2xl shadow hover:shadow-lg transition-all" :class="[plan.isPopular ? 'border-2 border-blue-500' : '']"
>
<div class="flex flex-col items-center text-center">
<h2 class="text-2xl font-semibold mb-2">
{{ plan.name }}
</h2>
<p class="text-3xl font-bold text-blue-600 mb-2" style="color: #E8CB96">
<span>优惠后</span>{{ plan.newPrice }} <del style="color: #889F9F;font-size: 16px">{{ plan.price }}</del>
</p>
<p class="text-sm text-gray-500 mb-4">
{{ plan.period }}
</p>
<el-divider />
<ul class="text-left space-y-2 w-full min-h-200px">
<li v-for="(feature, i) in plan.features" :key="i" class="flex items-start gap-2">
<el-icon><CircleCheck /></el-icon>
<span>{{ feature }}</span>
</li>
</ul>
<el-button
class="mt-6 w-full"
type="primary"
size="large"
style="background: #D7BD8D;color: white;border: #191919"
@click="openContact"
<div class="text-center relative">
<h3 class="text-lg font-bold mb-3">
请扫码加入微信交流群<br>
获取专属客服支持
</h3>
<div class="mb-4 flex items-center justify-center space-x-2">
<!-- <span class="font-semibold">站长微信账号</span> -->
<!-- <span id="wechat-id" class="text-blue-600 font-mono select-text">chengzilaoge520</span> -->
<span
v-if="false"
class="cursor-pointer" onclick="navigator.clipboard.writeText('chengzilaoge520').then(() => { window.parent.ElMessage({
message: '微信号已复制到剪贴板',
type: 'success',
duration: 2000,
});})"
title="点击复制"
>
立即订阅
</el-button>
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 opacity-70 hover:opacity-100" viewBox="0 0 24 24" fill="currentColor">
<path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v16h14c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 18H8V7h11v16z" />
</svg>
</span>
</div>
</el-card>
</div>
<el-divider class="my-16">
<div class="flex justify-center mb-4">
<div v-if="false">
<h4>
站长微信
</h4>
<img
:src="wxSrc"
class="w-50 h-70 border border-gray-200 rounded-lg shadow-md cursor-pointer hover:shadow-lg transition-transform hover:scale-105"
onclick="document.getElementById('wechat-qrcode-fullscreen').style.display = 'flex'"
alt="微信二维码"
>
</div><div>
<h4>
微信交流群
</h4>
<img
:src="wxGroupQD"
class="w-50 h-70 border border-gray-200 rounded-lg shadow-md cursor-pointer hover:shadow-lg transition-transform hover:scale-105"
onclick="document.getElementById('wx-group-qrcode-fullscreen').style.display = 'flex'"
alt="微信二维码"
>
</div>
</div>
<div v-if="false" class="text-sm text-gray-600">
<p class="mb-1">
请备注 <span class="inline-block bg-yellow-100 text-yellow-800 px-2 py-0.5 rounded text-xs">ai</span> 快速通过验证
</p>
</div>
<!-- 全屏放大二维码 -->
<div
id="wechat-qrcode-fullscreen"
style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.8); z-index:9999; justify-content:center; align-items:center;"
onclick="this.style.display='none'"
>
<img
:src="wxSrc"
style="max-width:90%; max-height:90%; border:8px solid white; border-radius:16px; box-shadow:0 0 40px rgba(255,255,255,0.2);"
>
</div>
<div
id="wx-group-qrcode-fullscreen"
style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.8); z-index:9999; justify-content:center; align-items:center;"
onclick="this.style.display='none'"
>
<img
:src="wxGroupQD"
style="max-width:90%; max-height:90%; border:8px solid white; border-radius:16px; box-shadow:0 0 40px rgba(255,255,255,0.2);"
>
</div>
</div>
<!-- <el-card -->
<!-- style="width: 400px;" -->
<!-- v-for="(plan, index) in pricing" -->
<!-- :key="index" -->
<!-- class="rounded-2xl shadow hover:shadow-lg transition-all" :class="[plan.isPopular ? 'border-2 border-blue-500' : '']" -->
<!-- > -->
<!-- <div class="flex flex-col items-center text-center"> -->
<!-- <h2 class="text-2xl font-semibold mb-2"> -->
<!-- {{ plan.name }} -->
<!-- </h2> -->
<!-- <p class="text-3xl font-bold text-blue-600 mb-2" style="color: #E8CB96"> -->
<!-- <span>优惠后</span>{{ plan.newPrice }} <del style="color: #889F9F;font-size: 16px">{{ plan.price }}</del> -->
<!-- </p> -->
<!-- <p class="text-sm text-gray-500 mb-4"> -->
<!-- {{ plan.period }} -->
<!-- </p> -->
<!-- <el-divider /> -->
<!-- <ul class="text-left space-y-2 w-full min-h-200px"> -->
<!-- <li v-for="(feature, i) in plan.features" :key="i" class="flex items-start gap-2"> -->
<!-- <el-icon><CircleCheck /></el-icon> -->
<!-- <span>{{ feature }}</span> -->
<!-- </li> -->
<!-- </ul> -->
<!-- <el-button -->
<!-- class="mt-6 w-full" -->
<!-- type="primary" -->
<!-- size="large" -->
<!-- style="background: #D7BD8D;color: white;border: #191919" -->
<!-- @click="openContact" -->
<!-- > -->
<!-- 立即订阅 -->
<!-- </el-button> -->
<!-- </div> -->
<!-- </el-card> -->
</div>
<!-- <SupportModelList /> -->
<el-divider v-if="false" class="my-16">
充值流程说明
</el-divider>
<el-card class="max-w-2xl mx-auto shadow-md rounded-2xl">
<el-card v-if="false" class="max-w-2xl mx-auto shadow-md rounded-2xl">
<h3 class="text-xl font-semibold mb-4">
如何充值 VIP
</h3>
@@ -270,7 +347,7 @@ function openContact() {
<el-divider class="my-16">
加入群聊
</el-divider>
<el-collapse class="max-w-3xl mx-auto flex justify-center" accordion >
<el-collapse class="max-w-3xl mx-auto flex justify-center" accordion>
<el-image
style="width: 500px; height: auto;"
:src="wxGroupQD"

View File

@@ -41,6 +41,19 @@ export const layoutRouter: RouteRecordRaw[] = [
isDefaultChat: false, // 根据实际情况设置
layout: 'blankPage', // 如果需要自定义布局
},
},
{
path: '/pay-result',
name: 'payResult',
component: () => import('@/pages/payResult/index.vue'),
meta: {
title: '支付结果',
keepAlive: true, // 如果需要缓存
isDefaultChat: false, // 根据实际情况设置
layout: 'blankPage', // 如果需要自定义布局
},
},
],
},

View File

@@ -0,0 +1,20 @@
import { createApp, h } from 'vue';
import ProductPackage from '@/components/ProductPackage/index.vue';
export function showProductPackage() {
const div = document.createElement('div');
document.body.appendChild(div);
const app = createApp({
render() {
return h(ProductPackage, {
onClose: () => {
app.unmount();
div.remove();
},
});
},
});
app.mount(div);
}

View File

@@ -44,11 +44,13 @@ declare module 'vue' {
ModelSelect: typeof import('./../src/components/ModelSelect/index.vue')['default']
NavDialog: typeof import('./../src/components/userPersonalCenter/NavDialog.vue')['default']
Popover: typeof import('./../src/components/Popover/index.vue')['default']
ProductPackage: typeof import('./../src/components/ProductPackage/index.vue')['default']
QrCodeLogin: typeof import('./../src/components/LoginDialog/components/QrCodeLogin/index.vue')['default']
RechargeLog: typeof import('./../src/components/userPersonalCenter/components/RechargeLog.vue')['default']
RegistrationForm: typeof import('./../src/components/LoginDialog/components/FormLogin/RegistrationForm.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SupportModelList: typeof import('./../src/components/userPersonalCenter/components/SupportModelList.vue')['default']
SvgIcon: typeof import('./../src/components/SvgIcon/index.vue')['default']
UsageStatistics: typeof import('./../src/components/userPersonalCenter/components/UsageStatistics.vue')['default']
UserManagement: typeof import('./../src/components/userPersonalCenter/components/UserManagement.vue')['default']