578 lines
12 KiB
Vue
578 lines
12 KiB
Vue
<script lang="ts" setup>
|
||
import { CircleCheck } from '@element-plus/icons-vue';
|
||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||
import { onMounted, ref } from 'vue';
|
||
import { useRouter } from 'vue-router';
|
||
import { applyApiKey, getApiKey } from '@/api/model/index.ts';
|
||
import { isUserVip } from '@/utils/user';
|
||
|
||
const apiKey = ref('');
|
||
const showKey = ref(false);
|
||
const loading = ref(true);
|
||
const displayedKey = ref('');
|
||
const showSuccessDialog = ref(false);
|
||
const svg = `
|
||
<path class="path" d="
|
||
M 30 15
|
||
L 28 17
|
||
M 25.61 25.61
|
||
A 15 15, 0, 0, 1, 15 30
|
||
A 15 15, 0, 1, 1, 27.99 7.5
|
||
L 15 15
|
||
" style="stroke-width: 4px; fill: rgba(0, 0, 0, 0)"/>
|
||
`;
|
||
const isOpening = ref(false);
|
||
const confettis = ref<any[]>([]);
|
||
const router = useRouter();
|
||
|
||
// 获取API密钥
|
||
async function fetchApiKey() {
|
||
try {
|
||
const res = await getApiKey();
|
||
if (res.data?.apiKey) {
|
||
apiKey.value = res.data.apiKey;
|
||
displayedKey.value = res.data.apiKey;
|
||
}
|
||
}
|
||
catch (error) {
|
||
console.error('获取API密钥失败:', error);
|
||
}
|
||
}
|
||
|
||
// 领取密钥
|
||
async function handleClaim() {
|
||
if (!isUserVip()) {
|
||
ElMessageBox.confirm(
|
||
`
|
||
<div class="text-center leading-relaxed">
|
||
<h3 class="text-lg font-bold mb-3">成为 YiXinAI-VIP</h3>
|
||
<p class="mb-2">
|
||
解锁所有 AI 模型,无限加速,专属客服,尽享尊贵体验。
|
||
</p>
|
||
<p class="text-sm text-gray-500">点击下方按钮,立即升级为 VIP 会员!</p>
|
||
</div>
|
||
`,
|
||
'会员尊享',
|
||
{
|
||
confirmButtonText: '前往产品页面',
|
||
cancelButtonText: '关闭',
|
||
dangerouslyUseHTMLString: true,
|
||
type: 'info',
|
||
center: true,
|
||
roundButton: true,
|
||
},
|
||
)
|
||
.then(() => {
|
||
router.push({
|
||
name: 'products', // 使用命名路由
|
||
query: { from: 'user' }, // 可选:添加来源标识
|
||
});
|
||
})
|
||
.catch(() => {
|
||
// 点击右上角关闭或“关闭”按钮,不执行任何操作
|
||
});
|
||
return;
|
||
}
|
||
if (isOpening.value)
|
||
return;
|
||
|
||
try {
|
||
isOpening.value = true;
|
||
generateConfetti();
|
||
|
||
loading.value = true;
|
||
await applyApiKey();
|
||
await fetchApiKey();
|
||
|
||
showSuccessDialog.value = true;
|
||
}
|
||
catch (error) {
|
||
console.error('领取失败:', error);
|
||
ElMessage.error('领取失败,请稍后重试');
|
||
}
|
||
finally {
|
||
loading.value = false;
|
||
setTimeout(() => isOpening.value = false, 1000);
|
||
}
|
||
}
|
||
|
||
// 重置密钥
|
||
async function handleReset() {
|
||
try {
|
||
await ElMessageBox.confirm('确定要重置API密钥吗?原密钥将立即失效', '提示', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning',
|
||
});
|
||
loading.value = true;
|
||
await applyApiKey(); // 使用相同的申请接口重置
|
||
await fetchApiKey();
|
||
ElMessage.success('API密钥已重置');
|
||
}
|
||
catch (error) {
|
||
if (error !== 'cancel') {
|
||
ElMessage.error('重置失败');
|
||
}
|
||
}
|
||
finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
|
||
// 切换密钥显示状态
|
||
function toggleKeyVisibility() {
|
||
showKey.value = !showKey.value;
|
||
}
|
||
|
||
// 复制API密钥
|
||
function copyApiKey() {
|
||
navigator.clipboard.writeText(apiKey.value || '')
|
||
.then(() => ElMessage.success('已复制到剪贴板'))
|
||
.catch(() => ElMessage.error('复制失败'));
|
||
}
|
||
|
||
// 生成随机彩带
|
||
function generateConfetti() {
|
||
const colors = ['#ff9a9e', '#fad0c4', '#fbc2eb', '#a6c1ee', '#a18cd1', '#fdcbf1'];
|
||
const newConfettis = [];
|
||
|
||
for (let i = 0; i < 30; i++) {
|
||
newConfettis.push({
|
||
style: {
|
||
left: `${50 + (Math.random() - 0.5) * 30}%`,
|
||
backgroundColor: colors[Math.floor(Math.random() * colors.length)],
|
||
width: `${6 + Math.random() * 6}px`,
|
||
height: `${6 + Math.random() * 6}px`,
|
||
transform: `rotate(${Math.random() * 360}deg)`,
|
||
animationDuration: `${1 + Math.random() * 2}s`,
|
||
animationDelay: `${Math.random() * 0.5}s`,
|
||
},
|
||
});
|
||
}
|
||
|
||
confettis.value = newConfettis;
|
||
setTimeout(() => confettis.value = [], 2000);
|
||
}
|
||
|
||
onMounted(async () => {
|
||
await fetchApiKey();
|
||
loading.value = false;
|
||
});
|
||
</script>
|
||
|
||
<template>
|
||
<div
|
||
v-loading="loading"
|
||
class="api-key-management"
|
||
element-loading-text="Loading..."
|
||
:element-loading-spinner="svg"
|
||
element-loading-svg-view-box="-10, -10, 50, 50"
|
||
element-loading-background="rgba(122, 122, 122, 0.8)"
|
||
>
|
||
<!-- 未领取状态 -->
|
||
<div v-if="!apiKey" class="unclaimed-state">
|
||
<div class="gift-container" @click="handleClaim">
|
||
<div class="gift-box" :class="{ opening: isOpening }">
|
||
<div class="gift-lid" />
|
||
<div class="gift-body">
|
||
<div class="ribbon-horizontal" />
|
||
<div class="ribbon-vertical" />
|
||
<div class="ribbon-bow">
|
||
<div class="bow-left" />
|
||
<div class="bow-right" />
|
||
<div class="bow-center" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div
|
||
v-for="(confetti, index) in confettis" :key="index" class="confetti"
|
||
:style="confetti.style"
|
||
/>
|
||
</div>
|
||
<div class="claim-text">
|
||
<h3>🎁 尊享会员专属福利 🎁</h3>
|
||
<h3>
|
||
YiXinAi重磅推出专属Api接入服务
|
||
</h3>
|
||
<h3>YiXinAI-Vip 限时免费领取</h3>
|
||
<p>点击礼盒领取您的专属API密钥</p>
|
||
</div>
|
||
</div>
|
||
<!-- 已领取状态 -->
|
||
<div v-else-if="apiKey" class="claimed-state">
|
||
<h3 class="key-title">
|
||
🎉 恭喜您已获得专属API密钥 🎉
|
||
</h3>
|
||
|
||
<div class="key-display">
|
||
<el-input
|
||
v-model="displayedKey"
|
||
:type="showKey ? 'text' : 'password'"
|
||
readonly
|
||
class="key-input"
|
||
>
|
||
<template #append>
|
||
<el-button-group>
|
||
<el-button
|
||
:icon="showKey ? 'Hide' : 'View'"
|
||
@click="toggleKeyVisibility"
|
||
/>
|
||
<el-button
|
||
icon="DocumentCopy"
|
||
@click="copyApiKey"
|
||
/>
|
||
</el-button-group>
|
||
</template>
|
||
</el-input>
|
||
</div>
|
||
|
||
<div class="key-actions">
|
||
<el-button type="warning" @click="handleReset">
|
||
重置密钥
|
||
</el-button>
|
||
<p class="key-hint">
|
||
重置后原密钥将立即失效
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- 使用说明 -->
|
||
<div v-if="apiKey" class="usage-guide">
|
||
<el-divider />
|
||
<h3>使用说明</h3>
|
||
<div class="guide-content">
|
||
<p><strong>API地址:</strong>https://ai.ccnetcore.com</p>
|
||
<p><strong>密钥:</strong>上面申请的token</p>
|
||
<p><strong>模型:</strong>聊天界面显示的模型名称</p>
|
||
</div>
|
||
|
||
<div class="guide-images">
|
||
<el-image
|
||
style="max-width: 100%; margin: 10px 0;"
|
||
src="/images/api_usage_instructions.png"
|
||
fit="contain"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 领取成功弹窗 -->
|
||
<el-dialog
|
||
v-model="showSuccessDialog"
|
||
title="领取成功"
|
||
width="400px"
|
||
:show-close="false"
|
||
:close-on-click-modal="false"
|
||
:close-on-press-escape="false"
|
||
>
|
||
<div class="success-dialog">
|
||
<el-icon color="#67C23A" :size="60">
|
||
<CircleCheck />
|
||
</el-icon>
|
||
<p class="success-message">
|
||
恭喜您成功领取API密钥!
|
||
</p>
|
||
<p class="success-tip">
|
||
请妥善保管您的密钥
|
||
</p>
|
||
</div>
|
||
<template #footer>
|
||
<el-button type="primary" @click="showSuccessDialog = false">
|
||
确定
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.api-key-management {
|
||
padding: 20px;
|
||
max-width: 600px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
/* 未领取状态样式 */
|
||
.unclaimed-state {
|
||
text-align: center;
|
||
margin: 40px 0;
|
||
}
|
||
|
||
.gift-box {
|
||
position: relative;
|
||
width: 180px;
|
||
height: 180px;
|
||
margin: 0 auto 30px;
|
||
cursor: pointer;
|
||
transition: transform 0.3s;
|
||
}
|
||
|
||
.gift-box:hover {
|
||
transform: scale(1.05);
|
||
}
|
||
|
||
.gift-lid {
|
||
position: absolute;
|
||
width: 180px;
|
||
height: 40px;
|
||
background: #E74C3C;
|
||
top: -20px;
|
||
left: 0;
|
||
border-radius: 4px 4px 0 0;
|
||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.gift-body {
|
||
position: absolute;
|
||
width: 180px;
|
||
height: 140px;
|
||
background: #C0392B;
|
||
top: 20px;
|
||
left: 0;
|
||
border-radius: 0 0 4px 4px;
|
||
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.ribbon {
|
||
position: absolute;
|
||
width: 40px;
|
||
height: 100%;
|
||
background: #F1C40F;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
}
|
||
|
||
.gift-label {
|
||
position: absolute;
|
||
bottom: -30px;
|
||
width: 100%;
|
||
text-align: center;
|
||
font-weight: bold;
|
||
color: #E74C3C;
|
||
}
|
||
|
||
.claim-hint {
|
||
margin-top: 20px;
|
||
font-size: 16px;
|
||
color: #E74C3C;
|
||
font-weight: bold;
|
||
}
|
||
|
||
/* 已领取状态样式 */
|
||
.claimed-state {
|
||
margin: 30px 0;
|
||
}
|
||
|
||
.key-title {
|
||
text-align: center;
|
||
color: #333;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.key-display {
|
||
margin: 20px 0;
|
||
}
|
||
|
||
.key-input {
|
||
font-family: monospace;
|
||
}
|
||
|
||
/* 添加按钮间距 */
|
||
.action-btn {
|
||
margin-left: 4px;
|
||
}
|
||
|
||
/* 或者直接设置最后一个按钮的左边距 */
|
||
.key-input :deep(.el-input-group__append .el-button + .el-button) {
|
||
margin-left: 4px;
|
||
}
|
||
.key-actions {
|
||
text-align: center;
|
||
margin-top: 30px;
|
||
}
|
||
|
||
.key-hint {
|
||
color: #999;
|
||
font-size: 14px;
|
||
margin-top: 10px;
|
||
}
|
||
|
||
/* 使用说明样式 */
|
||
.usage-guide {
|
||
margin-top: 40px;
|
||
}
|
||
|
||
.guide-content {
|
||
background: #f8f9fa;
|
||
padding: 15px;
|
||
border-radius: 4px;
|
||
line-height: 1.8;
|
||
}
|
||
|
||
/* 成功弹窗样式 */
|
||
.success-dialog {
|
||
text-align: center;
|
||
padding: 20px 0;
|
||
}
|
||
|
||
.success-message {
|
||
font-size: 18px;
|
||
color: #333;
|
||
margin: 15px 0 5px;
|
||
}
|
||
|
||
.success-tip {
|
||
color: #999;
|
||
font-size: 14px;
|
||
}
|
||
|
||
/* 未领取状态样式 */
|
||
.unclaimed-state {
|
||
text-align: center;
|
||
margin: 30px 0;
|
||
perspective: 600px;
|
||
}
|
||
|
||
.gift-container {
|
||
position: relative;
|
||
width: 150px;
|
||
height: 150px;
|
||
margin: 0 auto;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.gift-box {
|
||
position: relative;
|
||
width: 100%;
|
||
height: 100%;
|
||
transition: all 0.3s;
|
||
transform-style: preserve-3d;
|
||
}
|
||
|
||
.gift-box:hover:not(.opening) {
|
||
transform: translateY(-5px);
|
||
}
|
||
|
||
.gift-box.opening .gift-lid {
|
||
transform: rotateX(180deg) translateY(-30px);
|
||
opacity: 0;
|
||
}
|
||
|
||
.gift-lid {
|
||
position: absolute;
|
||
width: 150px;
|
||
height: 30px;
|
||
background: linear-gradient(135deg, #ff7676, #e74c3c);
|
||
top: -15px;
|
||
left: 0;
|
||
border-radius: 8px 8px 0 0;
|
||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||
transition: all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||
transform-origin: bottom;
|
||
z-index: 2;
|
||
}
|
||
|
||
.gift-body {
|
||
position: absolute;
|
||
width: 150px;
|
||
height: 120px;
|
||
background: linear-gradient(135deg, #e74c3c, #c0392b);
|
||
top: 15px;
|
||
left: 0;
|
||
border-radius: 0 0 8px 8px;
|
||
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
|
||
z-index: 1;
|
||
}
|
||
|
||
.ribbon-horizontal {
|
||
position: absolute;
|
||
width: 100%;
|
||
height: 12px;
|
||
background: rgba(255,255,255,0.3);
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
}
|
||
|
||
.ribbon-vertical {
|
||
position: absolute;
|
||
width: 12px;
|
||
height: 100%;
|
||
background: rgba(255,255,255,0.3);
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
}
|
||
|
||
.ribbon-bow {
|
||
position: absolute;
|
||
top: -10px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
z-index: 3;
|
||
}
|
||
|
||
.bow-left, .bow-right {
|
||
position: absolute;
|
||
width: 20px;
|
||
height: 20px;
|
||
background: linear-gradient(135deg, #f1c40f, #f39c12);
|
||
border-radius: 50%;
|
||
}
|
||
|
||
.bow-left {
|
||
left: -25px;
|
||
clip-path: polygon(0 0, 100% 0, 100% 50%, 0 50%);
|
||
}
|
||
|
||
.bow-right {
|
||
left: 5px;
|
||
clip-path: polygon(0 0, 100% 0, 100% 50%, 0 50%);
|
||
}
|
||
|
||
.bow-center {
|
||
position: absolute;
|
||
width: 12px;
|
||
height: 30px;
|
||
background: linear-gradient(135deg, #f1c40f, #f39c12);
|
||
left: -6px;
|
||
top: 10px;
|
||
border-radius: 6px;
|
||
}
|
||
|
||
.confetti {
|
||
position: absolute;
|
||
width: 8px;
|
||
height: 8px;
|
||
background: #f1c40f;
|
||
top: 50%;
|
||
left: 50%;
|
||
opacity: 0;
|
||
animation: confetti-fall 2s ease-out forwards;
|
||
z-index: 10;
|
||
}
|
||
|
||
@keyframes confetti-fall {
|
||
0% {
|
||
transform: translate(0, 0) rotate(0deg);
|
||
opacity: 1;
|
||
}
|
||
100% {
|
||
transform: translate(var(--tx), 200px) rotate(360deg);
|
||
opacity: 0;
|
||
--tx: calc((var(--random) - 0.5) * 200px);
|
||
}
|
||
}
|
||
|
||
.claim-text {
|
||
margin-top: 30px;
|
||
}
|
||
|
||
.claim-text h3 {
|
||
color: #e74c3c;
|
||
margin-bottom: 10px;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.claim-text p {
|
||
color: #666;
|
||
font-size: 14px;
|
||
}
|
||
</style>
|