535 lines
12 KiB
Vue
535 lines
12 KiB
Vue
<script lang="ts" setup>
|
||
import { Clock, Coin, TrophyBase, WarningFilled } from '@element-plus/icons-vue';
|
||
import { getPremiumTokenPackage } from '@/api/user';
|
||
import { showProductPackage } from '@/utils/product-package.ts';
|
||
|
||
// 尊享服务数据
|
||
const loading = ref(false);
|
||
const packageData = ref<any>({
|
||
totalQuota: 0, // 购买总额度
|
||
usedQuota: 0, // 已使用额度
|
||
remainingQuota: 0, // 剩余额度
|
||
usagePercentage: 0, // 使用百分比
|
||
packageName: 'Token包', // 套餐名称
|
||
expireDate: '', // 过期时间
|
||
});
|
||
|
||
// 计算属性
|
||
const usagePercent = computed(() => {
|
||
if (packageData.value.totalQuota === 0)
|
||
return 0;
|
||
return Number(((packageData.value.usedQuota / packageData.value.totalQuota) * 100).toFixed(2));
|
||
});
|
||
|
||
const remainingPercent = computed(() => {
|
||
if (packageData.value.totalQuota === 0)
|
||
return 0;
|
||
return Number(((packageData.value.remainingQuota / packageData.value.totalQuota) * 100).toFixed(2));
|
||
});
|
||
|
||
// 获取进度条颜色(基于剩余百分比)
|
||
const progressColor = computed(() => {
|
||
const percent = remainingPercent.value;
|
||
if (percent <= 10)
|
||
return '#f56c6c'; // 红色 - 剩余很少
|
||
if (percent <= 30)
|
||
return '#e6a23c'; // 橙色 - 剩余较少
|
||
return '#67c23a'; // 绿色 - 剩余充足
|
||
});
|
||
|
||
// 格式化数字 - 转换为万为单位
|
||
function formatNumber(num: number): string {
|
||
if (num === 0)
|
||
return '0';
|
||
const wan = num / 10000;
|
||
// 保留2位小数,去掉末尾的0
|
||
return wan.toFixed(2).replace(/\.?0+$/, '');
|
||
}
|
||
|
||
// 格式化原始数字(带千分位)
|
||
function formatRawNumber(num: number): string {
|
||
return num.toLocaleString();
|
||
}
|
||
/*
|
||
前端已准备好,后端需要提供以下API:
|
||
|
||
接口地址: GET /account/premium/token-package
|
||
|
||
返回数据格式:
|
||
{
|
||
"success": true,
|
||
"data": {
|
||
"totalQuota": 1000000, // 购买总额度
|
||
"usedQuota": 350000, // 已使用额度
|
||
"remainingQuota": 650000, // 剩余额度(可选,前端会自动计算)
|
||
"usagePercentage": 35, // 使用百分比(可选)
|
||
"packageName": "尊享VIP套餐", // 套餐名称(可选)
|
||
"expireDate": "2024-12-31" // 过期时间(可选)
|
||
}
|
||
} */
|
||
|
||
// 获取尊享服务Token包数据
|
||
async function fetchPremiumTokenPackage() {
|
||
loading.value = true;
|
||
try {
|
||
const res = await getPremiumTokenPackage();
|
||
if (res.data) {
|
||
// 适配新的接口字段名
|
||
const data = res.data;
|
||
packageData.value = {
|
||
totalQuota: data.premiumTotalTokens || 0, // 尊享包总token
|
||
usedQuota: data.premiumUsedTokens || 0, // 尊享包已使用token
|
||
remainingQuota: data.premiumRemainingTokens || 0, // 尊享包剩余token
|
||
usagePercentage: data.usagePercentage || 0,
|
||
packageName: data.packageName || '尊享Token包',
|
||
expireDate: data.expireDate || '',
|
||
};
|
||
|
||
// 计算剩余额度(如果接口没返回)
|
||
if (packageData.value.remainingQuota === 0 && packageData.value.totalQuota > 0) {
|
||
packageData.value.remainingQuota = packageData.value.totalQuota - packageData.value.usedQuota;
|
||
}
|
||
}
|
||
else {
|
||
ElMessage.warning('暂无尊享服务数据');
|
||
}
|
||
}
|
||
catch (error) {
|
||
console.error('获取尊享服务数据失败:', error);
|
||
ElMessage.error('获取尊享服务数据失败');
|
||
}
|
||
finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
|
||
// 刷新数据
|
||
function refreshData() {
|
||
fetchPremiumTokenPackage();
|
||
}
|
||
|
||
onMounted(() => {
|
||
fetchPremiumTokenPackage();
|
||
});
|
||
|
||
function onProductPackage() {
|
||
showProductPackage();
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div v-loading="loading" class="premium-service">
|
||
<div class="header">
|
||
<h2>
|
||
<el-icon><TrophyBase /></el-icon>
|
||
尊享服务
|
||
</h2>
|
||
<el-button
|
||
type="primary"
|
||
size="small"
|
||
@click="refreshData"
|
||
>
|
||
刷新数据
|
||
</el-button>
|
||
</div>
|
||
|
||
<!-- 套餐信息卡片 -->
|
||
<el-card class="package-card" shadow="hover">
|
||
<template #header>
|
||
<div class="card-header">
|
||
<el-icon class="header-icon">
|
||
<Coin />
|
||
</el-icon>
|
||
<span class="header-title">{{ packageData.packageName }}</span>
|
||
</div>
|
||
</template>
|
||
|
||
<div class="package-content">
|
||
<!-- 统计数据 -->
|
||
<div class="stats-grid">
|
||
<div class="stat-item total">
|
||
<div class="stat-label">
|
||
购买总额度
|
||
</div>
|
||
<div class="stat-value">
|
||
{{ formatNumber(packageData.totalQuota) }}
|
||
</div>
|
||
<div class="stat-unit">
|
||
万 Tokens
|
||
</div>
|
||
<div class="stat-raw" :title="`原始值: ${formatRawNumber(packageData.totalQuota)} Tokens`">
|
||
({{ formatRawNumber(packageData.totalQuota) }})
|
||
</div>
|
||
</div>
|
||
|
||
<div class="stat-item used">
|
||
<div class="stat-label">
|
||
已用额度
|
||
</div>
|
||
<div class="stat-value">
|
||
{{ formatNumber(packageData.usedQuota) }}
|
||
</div>
|
||
<div class="stat-unit">
|
||
万 Tokens
|
||
</div>
|
||
<div class="stat-raw" :title="`原始值: ${formatRawNumber(packageData.usedQuota)} Tokens`">
|
||
({{ formatRawNumber(packageData.usedQuota) }})
|
||
</div>
|
||
</div>
|
||
|
||
<div class="stat-item remaining">
|
||
<div class="stat-label">
|
||
剩余额度
|
||
</div>
|
||
<div class="stat-value">
|
||
{{ formatNumber(packageData.remainingQuota) }}
|
||
</div>
|
||
<div class="stat-unit">
|
||
万 Tokens
|
||
</div>
|
||
<div class="stat-raw" :title="`原始值: ${formatRawNumber(packageData.remainingQuota)} Tokens`">
|
||
({{ formatRawNumber(packageData.remainingQuota) }})
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 进度条 -->
|
||
<div class="progress-section">
|
||
<div class="progress-header">
|
||
<span class="progress-label">剩余进度</span>
|
||
<span class="progress-percent" :style="{ color: progressColor }">
|
||
{{ remainingPercent }}%
|
||
</span>
|
||
</div>
|
||
|
||
<el-progress
|
||
:percentage="remainingPercent"
|
||
:color="progressColor"
|
||
:stroke-width="20"
|
||
:show-text="false"
|
||
/>
|
||
|
||
<div class="progress-legend">
|
||
<div class="legend-item">
|
||
<span class="legend-dot " :style="{ background: progressColor }" />
|
||
<span class="legend-text">剩余: {{ remainingPercent }}%</span>
|
||
</div>
|
||
<div class="legend-item">
|
||
<span class="legend-dot used-dot" />
|
||
<span class="legend-text">已使用: {{ usagePercent }}%</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 购买提示卡片(额度不足时显示) -->
|
||
<el-card
|
||
v-if="remainingPercent < 20"
|
||
class="warning-card"
|
||
shadow="hover"
|
||
>
|
||
<div class="warning-content">
|
||
<el-icon class="warning-icon" color="#e6a23c">
|
||
<WarningFilled />
|
||
</el-icon>
|
||
<div class="warning-text">
|
||
<h3>额度即将用完</h3>
|
||
<p>您的Token额度已使用{{ usagePercent }}%,剩余额度较少,建议及时充值。</p>
|
||
</div>
|
||
<el-button type="warning" @click="onProductPackage()">
|
||
立即充值
|
||
</el-button>
|
||
</div>
|
||
</el-card>
|
||
<!-- 过期时间 -->
|
||
<div v-if="packageData.expireDate" class="expire-info">
|
||
<el-icon><Clock /></el-icon>
|
||
<span>有效期至: {{ packageData.expireDate }}</span>
|
||
</div>
|
||
|
||
<!-- 温馨提示 -->
|
||
<el-alert
|
||
class="tips-alert"
|
||
type="info"
|
||
:closable="false"
|
||
show-icon
|
||
>
|
||
<template #title>
|
||
<div class="tips-content">
|
||
<p>温馨提示:</p>
|
||
<ul>
|
||
<li>Token额度根据不同模型消耗速率不同</li>
|
||
<li>建议合理使用,避免额度过快消耗</li>
|
||
<li>额度不足时请及时充值,避免影响使用</li>
|
||
</ul>
|
||
</div>
|
||
</template>
|
||
</el-alert>
|
||
</div>
|
||
</el-card>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.premium-service {
|
||
padding: 20px;
|
||
position: relative;
|
||
}
|
||
|
||
.header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.header h2 {
|
||
display: flex;
|
||
align-items: center;
|
||
margin: 0;
|
||
font-size: 20px;
|
||
color: #333;
|
||
}
|
||
|
||
.header .el-icon {
|
||
margin-right: 8px;
|
||
color: #f59e0b;
|
||
}
|
||
|
||
/* 套餐卡片 */
|
||
.package-card {
|
||
margin-bottom: 20px;
|
||
border-radius: 12px;
|
||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.card-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
}
|
||
|
||
.header-icon {
|
||
font-size: 20px;
|
||
color: #f59e0b;
|
||
}
|
||
|
||
.package-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 24px;
|
||
}
|
||
|
||
/* 统计数据网格 */
|
||
.stats-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 16px;
|
||
}
|
||
|
||
.stat-item {
|
||
padding: 20px;
|
||
border-radius: 8px;
|
||
text-align: center;
|
||
transition: transform 0.2s;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
}
|
||
|
||
.stat-item:hover {
|
||
transform: translateY(-4px);
|
||
}
|
||
|
||
.stat-item.total {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
}
|
||
|
||
.stat-item.used {
|
||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||
}
|
||
|
||
.stat-item.remaining {
|
||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 14px;
|
||
opacity: 0.9;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 28px;
|
||
font-weight: 700;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.stat-unit {
|
||
font-size: 12px;
|
||
opacity: 0.8;
|
||
}
|
||
|
||
.stat-raw {
|
||
font-size: 11px;
|
||
opacity: 0.7;
|
||
margin-top: 4px;
|
||
cursor: help;
|
||
transition: opacity 0.2s;
|
||
}
|
||
|
||
.stat-raw:hover {
|
||
opacity: 1;
|
||
}
|
||
|
||
/* 进度条部分 */
|
||
.progress-section {
|
||
padding: 20px;
|
||
background: #f7f8fa;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.progress-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.progress-label {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
}
|
||
|
||
.progress-percent {
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.progress-legend {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 24px;
|
||
margin-top: 12px;
|
||
}
|
||
|
||
.legend-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
.legend-dot {
|
||
width: 12px;
|
||
height: 12px;
|
||
border-radius: 50%;
|
||
}
|
||
|
||
.used-dot {
|
||
background: #409eff;
|
||
}
|
||
|
||
.remaining-dot {
|
||
background: #e4e7ed;
|
||
}
|
||
|
||
.legend-text {
|
||
font-size: 14px;
|
||
color: #606266;
|
||
}
|
||
|
||
/* 过期信息 */
|
||
.expire-info {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 12px 16px;
|
||
background: #f0f9ff;
|
||
border-radius: 6px;
|
||
color: #0369a1;
|
||
font-size: 14px;
|
||
}
|
||
|
||
/* 温馨提示 */
|
||
.tips-alert {
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.tips-content p {
|
||
margin: 0 0 8px 0;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.tips-content ul {
|
||
margin: 0;
|
||
padding-left: 20px;
|
||
}
|
||
|
||
.tips-content li {
|
||
margin: 4px 0;
|
||
font-size: 13px;
|
||
}
|
||
|
||
/* 警告卡片 */
|
||
.warning-card {
|
||
border-radius: 12px;
|
||
border: 2px solid #e6a23c;
|
||
background: #fef3e9;
|
||
}
|
||
|
||
.warning-content {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
}
|
||
|
||
.warning-icon {
|
||
font-size: 40px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.warning-text {
|
||
flex: 1;
|
||
}
|
||
|
||
.warning-text h3 {
|
||
margin: 0 0 8px 0;
|
||
color: #e6a23c;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.warning-text p {
|
||
margin: 0;
|
||
color: #606266;
|
||
font-size: 14px;
|
||
}
|
||
|
||
/* 响应式布局 */
|
||
@media (max-width: 768px) {
|
||
.premium-service {
|
||
padding: 10px;
|
||
}
|
||
|
||
.stats-grid {
|
||
grid-template-columns: 1fr;
|
||
gap: 12px;
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 24px;
|
||
}
|
||
|
||
.progress-legend {
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.warning-content {
|
||
flex-direction: column;
|
||
text-align: center;
|
||
}
|
||
}
|
||
</style>
|