Files
Yi.Framework/Yi.Ai.Vue3/src/components/userPersonalCenter/components/APIKeyManagement.vue

578 lines
12 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 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>