feat: 完成激活码兑换功能
This commit is contained in:
5
Yi.Ai.Vue3/src/api/activationCode/index.ts
Normal file
5
Yi.Ai.Vue3/src/api/activationCode/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { post } from '@/utils/request';
|
||||
|
||||
export function redeemActivationCode(data: { code: string }) {
|
||||
return post<any>('/activationCode/Redeem', data);
|
||||
}
|
||||
@@ -142,6 +142,7 @@ function cleanupPayment() {
|
||||
const tabs = [
|
||||
{ key: 'member', label: '会员套餐' },
|
||||
{ key: 'token', label: '尊享Token包' },
|
||||
{ key: 'activation', label: '激活码' },
|
||||
];
|
||||
|
||||
const benefitsData = {
|
||||
@@ -210,8 +211,11 @@ function selectPackage(pkg: any) {
|
||||
}
|
||||
|
||||
watch(activeTab, () => {
|
||||
if (activeTab.value === 'activation') {
|
||||
return;
|
||||
}
|
||||
const packages = packagesData.value[activeTab.value as 'member' | 'token'];
|
||||
if (packages.length > 0) {
|
||||
if (packages && packages.length > 0) {
|
||||
const firstPackage = packages[0];
|
||||
selectedId.value = firstPackage.id;
|
||||
selectedPrice.value = firstPackage.price;
|
||||
@@ -315,6 +319,11 @@ function close() {
|
||||
function onClose() {
|
||||
emit('close');
|
||||
}
|
||||
|
||||
function goToActivation() {
|
||||
close();
|
||||
userStore.openUserCenter('activationCode');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -386,8 +395,30 @@ function onClose() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 激活码引导页 -->
|
||||
<div v-if="activeTab === 'activation'" class="activation-guide-container">
|
||||
<div class="activation-content">
|
||||
<div class="guide-icon">🎁</div>
|
||||
<h3 class="guide-title">激活码兑换</h3>
|
||||
<p class="guide-desc">如果您持有意心AI的会员激活码或Token兑换码,<br>请点击下方按钮前往控制台进行兑换。</p>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="large"
|
||||
class="redeem-jump-btn"
|
||||
round
|
||||
@click="goToActivation"
|
||||
>
|
||||
前往兑换中心
|
||||
</el-button>
|
||||
|
||||
<div class="guide-tips">
|
||||
<p>💡 兑换成功后权益将立即生效</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 移动端布局 -->
|
||||
<div v-if="isMobile" class="mobile-layout">
|
||||
<div v-else-if="isMobile" class="mobile-layout">
|
||||
<!-- 商品加载状态(无修改) -->
|
||||
<div v-if="isLoadingGoods" class="loading-container">
|
||||
<el-icon class="is-loading" :size="40">
|
||||
@@ -824,6 +855,73 @@ function onClose() {
|
||||
}
|
||||
}
|
||||
|
||||
/* 激活码引导页样式 */
|
||||
.activation-guide-container {
|
||||
padding: 40px 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 400px;
|
||||
background: linear-gradient(to bottom, #fff, #fdfdfd);
|
||||
border-radius: 8px;
|
||||
|
||||
.activation-content {
|
||||
text-align: center;
|
||||
max-width: 400px;
|
||||
|
||||
.guide-icon {
|
||||
font-size: 64px;
|
||||
margin-bottom: 24px;
|
||||
animation: float-icon 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.guide-title {
|
||||
font-size: 24px;
|
||||
font-weight: 800;
|
||||
color: #2c3e50;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.guide-desc {
|
||||
color: #606266;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 32px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.redeem-jump-btn {
|
||||
width: 200px;
|
||||
height: 48px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 1px;
|
||||
background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 100%);
|
||||
border: none;
|
||||
box-shadow: 0 4px 12px rgba(255, 117, 140, 0.3);
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(255, 117, 140, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
.guide-tips {
|
||||
margin-top: 24px;
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float-icon {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-10px); }
|
||||
}
|
||||
|
||||
/* 移动端样式(核心新增:主价格/弱化价格样式) */
|
||||
.mobile-layout {
|
||||
display: flex; flex-direction: column; gap: 24px;
|
||||
|
||||
@@ -0,0 +1,483 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, onUnmounted, nextTick } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { redeemActivationCode } from '@/api/activationCode';
|
||||
import { MagicStick } from '@element-plus/icons-vue';
|
||||
|
||||
const activationCode = ref('');
|
||||
const loading = ref(false);
|
||||
const canvasRef = ref<HTMLCanvasElement | null>(null);
|
||||
const containerRef = ref<HTMLElement | null>(null);
|
||||
let animationId: number;
|
||||
|
||||
// --- Advanced Physics & Visuals ---
|
||||
|
||||
class Particle {
|
||||
x: number;
|
||||
y: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
alpha: number;
|
||||
color: string;
|
||||
hue: number;
|
||||
size: number;
|
||||
decay: number;
|
||||
gravity: number;
|
||||
friction: number;
|
||||
brightness: number;
|
||||
flicker: boolean;
|
||||
|
||||
constructor(x: number, y: number, hue: number) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.hue = hue;
|
||||
|
||||
// Explosive physics
|
||||
const angle = Math.random() * Math.PI * 2;
|
||||
const speed = Math.random() * 15 + 2;
|
||||
this.vx = Math.cos(angle) * speed;
|
||||
this.vy = Math.sin(angle) * speed;
|
||||
|
||||
this.alpha = 1;
|
||||
this.decay = Math.random() * 0.015 + 0.005;
|
||||
this.gravity = 0.05;
|
||||
this.friction = 0.96;
|
||||
|
||||
this.size = Math.random() * 3 + 1;
|
||||
this.brightness = 50; // Standard brightness for white bg visibility (0-100% HSL L value)
|
||||
this.flicker = Math.random() > 0.5;
|
||||
}
|
||||
|
||||
update() {
|
||||
this.vx *= this.friction;
|
||||
this.vy *= this.friction;
|
||||
this.vy += this.gravity;
|
||||
|
||||
this.x += this.vx;
|
||||
this.y += this.vy;
|
||||
|
||||
this.alpha -= this.decay;
|
||||
this.hue += 0.5;
|
||||
}
|
||||
|
||||
draw(ctx: CanvasRenderingContext2D) {
|
||||
ctx.globalAlpha = this.alpha;
|
||||
// On white background:
|
||||
// We want high saturation (100%) and medium lightness (50%) to make colors pop against white.
|
||||
// If lightness is too high (like 80-100), it fades into white.
|
||||
const lightness = this.flicker ? Math.random() * 20 + 40 : this.brightness;
|
||||
ctx.fillStyle = `hsla(${this.hue}, 100%, ${lightness}%, ${this.alpha})`;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
}
|
||||
|
||||
class Shockwave {
|
||||
x: number;
|
||||
y: number;
|
||||
radius: number;
|
||||
alpha: number;
|
||||
lineWidth: number;
|
||||
hue: number;
|
||||
|
||||
constructor(x: number, y: number, hue: number) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.hue = hue;
|
||||
this.radius = 0;
|
||||
this.alpha = 1;
|
||||
this.lineWidth = 10;
|
||||
}
|
||||
|
||||
update() {
|
||||
this.radius += 15;
|
||||
this.alpha -= 0.05;
|
||||
this.lineWidth -= 0.2;
|
||||
}
|
||||
|
||||
draw(ctx: CanvasRenderingContext2D) {
|
||||
if (this.alpha <= 0) return;
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
|
||||
// Darker/More saturated shockwave for white background
|
||||
ctx.strokeStyle = `hsla(${this.hue}, 100%, 60%, ${this.alpha})`;
|
||||
ctx.lineWidth = Math.max(0, this.lineWidth);
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
let particles: Particle[] = [];
|
||||
let shockwaves: Shockwave[] = [];
|
||||
|
||||
function createExplosion(x: number, y: number, hue: number) {
|
||||
const particleCount = 120;
|
||||
for (let i = 0; i < particleCount; i++) {
|
||||
particles.push(new Particle(x, y, hue));
|
||||
}
|
||||
for (let i = 0; i < 60; i++) {
|
||||
particles.push(new Particle(x, y, (hue + 180) % 360));
|
||||
}
|
||||
shockwaves.push(new Shockwave(x, y, hue));
|
||||
}
|
||||
|
||||
function animate() {
|
||||
const canvas = canvasRef.value;
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
// Clear with transparent fade for trails on white
|
||||
// 'destination-out' erases content.
|
||||
// To leave a trail on a white background (canvas is transparent over white gradient):
|
||||
// We need to gently erase what's there.
|
||||
|
||||
ctx.globalCompositeOperation = 'destination-out';
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.1)'; // Alpha controls trail length
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Reset to default drawing
|
||||
ctx.globalCompositeOperation = 'source-over';
|
||||
|
||||
for (let i = particles.length - 1; i >= 0; i--) {
|
||||
particles[i].update();
|
||||
particles[i].draw(ctx);
|
||||
if (particles[i].alpha <= 0) particles.splice(i, 1);
|
||||
}
|
||||
|
||||
for (let i = shockwaves.length - 1; i >= 0; i--) {
|
||||
shockwaves[i].update();
|
||||
shockwaves[i].draw(ctx);
|
||||
if (shockwaves[i].alpha <= 0) shockwaves.splice(i, 1);
|
||||
}
|
||||
|
||||
if (particles.length > 0 || shockwaves.length > 0) {
|
||||
animationId = requestAnimationFrame(animate);
|
||||
} else {
|
||||
cancelAnimationFrame(animationId);
|
||||
}
|
||||
}
|
||||
|
||||
// Shake Effect
|
||||
const isShaking = ref(false);
|
||||
function triggerShake() {
|
||||
isShaking.value = true;
|
||||
setTimeout(() => isShaking.value = false, 500);
|
||||
}
|
||||
|
||||
function triggerCelebration() {
|
||||
const canvas = canvasRef.value;
|
||||
if (!canvas) return;
|
||||
const parent = canvas.parentElement;
|
||||
if (parent) {
|
||||
canvas.width = parent.clientWidth;
|
||||
canvas.height = parent.clientHeight;
|
||||
}
|
||||
|
||||
const cx = canvas.width / 2;
|
||||
const cy = canvas.height / 2;
|
||||
|
||||
// 1. Initial Mega Explosion
|
||||
triggerShake();
|
||||
createExplosion(cx, cy, Math.random() * 360);
|
||||
|
||||
// Start loop
|
||||
animate();
|
||||
|
||||
// 2. Machine Gun Fire sequence
|
||||
let count = 0;
|
||||
const timer = setInterval(() => {
|
||||
count++;
|
||||
const rx = cx + (Math.random() - 0.5) * canvas.width * 0.8;
|
||||
const ry = cy + (Math.random() - 0.5) * canvas.height * 0.8;
|
||||
|
||||
createExplosion(rx, ry, Math.random() * 360);
|
||||
|
||||
if (count % 3 === 0) triggerShake();
|
||||
|
||||
if (count > 25) {
|
||||
clearInterval(timer);
|
||||
}
|
||||
}, 120);
|
||||
}
|
||||
|
||||
async function handleRedeem() {
|
||||
if (!activationCode.value.trim()) {
|
||||
ElMessage.warning('请输入激活码');
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await redeemActivationCode({ code: activationCode.value });
|
||||
triggerCelebration();
|
||||
ElMessage.success({
|
||||
message: '兑换成功!奖励已到账',
|
||||
type: 'success',
|
||||
duration: 3000,
|
||||
showClose: true,
|
||||
});
|
||||
activationCode.value = '';
|
||||
} catch (error: any) {
|
||||
// console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
cancelAnimationFrame(animationId);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="containerRef" class="activation-container" :class="{ 'shake-anim': isShaking }">
|
||||
<!-- Removed Dark overlay -->
|
||||
<canvas ref="canvasRef" class="fireworks-canvas"></canvas>
|
||||
|
||||
<div class="content-wrapper">
|
||||
<div class="gift-icon-wrapper">
|
||||
<div class="gift-box">🎁</div>
|
||||
<div class="gift-glow"></div>
|
||||
</div>
|
||||
|
||||
<h2 class="title">激活码兑换</h2>
|
||||
<p class="subtitle">开启您的专属惊喜权益</p>
|
||||
|
||||
<div class="input-section">
|
||||
<el-input
|
||||
v-model="activationCode"
|
||||
placeholder="请输入您的激活码"
|
||||
class="activation-input"
|
||||
size="large"
|
||||
:prefix-icon="MagicStick"
|
||||
clearable
|
||||
@keyup.enter="handleRedeem"
|
||||
/>
|
||||
|
||||
<el-button
|
||||
class="redeem-btn"
|
||||
:loading="loading"
|
||||
@click="handleRedeem"
|
||||
>
|
||||
立即兑换
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="tips-section">
|
||||
<div class="tip-item">
|
||||
<span class="tip-dot">•</span>
|
||||
<span>若激活码内包含意心Ai会员物品,激活后需重新登录生效</span>
|
||||
</div>
|
||||
<div class="tip-item">
|
||||
<span class="tip-dot">•</span>
|
||||
<span>激活成功后,可在充值记录中查看物品是否到账</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.activation-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 500px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: radial-gradient(circle at center, #fffbf0 0%, #fff 100%);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
transition: transform 0.1s;
|
||||
}
|
||||
|
||||
.shake-anim {
|
||||
animation: shake 0.5s cubic-bezier(.36,.07,.19,.97) both;
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
10%, 90% { transform: translate3d(-2px, 0, 0); }
|
||||
20%, 80% { transform: translate3d(4px, 0, 0); }
|
||||
30%, 50%, 70% { transform: translate3d(-6px, 0, 0); }
|
||||
40%, 60% { transform: translate3d(6px, 0, 0); }
|
||||
}
|
||||
|
||||
.fireworks-canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
position: relative;
|
||||
z-index: 11;
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
padding: 40px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.gift-icon-wrapper {
|
||||
position: relative;
|
||||
margin-bottom: 24px;
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.gift-box {
|
||||
font-size: 72px;
|
||||
filter: drop-shadow(0 10px 15px rgba(255, 105, 180, 0.3));
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.gift-glow {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
background: radial-gradient(circle, rgba(255, 215, 0, 0.4) 0%, rgba(255, 255, 255, 0) 70%);
|
||||
border-radius: 50%;
|
||||
z-index: 1;
|
||||
animation: pulse-glow 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-glow {
|
||||
0% { transform: translate(-50%, -50%) scale(1); opacity: 0.5; }
|
||||
50% { transform: translate(-50%, -50%) scale(1.5); opacity: 1; }
|
||||
100% { transform: translate(-50%, -50%) scale(1); opacity: 0.5; }
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-12px); }
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 32px;
|
||||
font-weight: 800;
|
||||
background: linear-gradient(45deg, #ff9a9e, #fad0c4, #fad0c4);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: #2c3e50;
|
||||
margin: 0 0 8px 0;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 15px;
|
||||
color: #95a5a6;
|
||||
margin-bottom: 36px;
|
||||
}
|
||||
|
||||
.input-section {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
:deep(.activation-input .el-input__wrapper) {
|
||||
border-radius: 50px;
|
||||
padding: 10px 24px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
border: 2px solid transparent;
|
||||
background-image: linear-gradient(white, white), linear-gradient(to right, #e0e0e0, #e0e0e0);
|
||||
background-origin: border-box;
|
||||
background-clip: padding-box, border-box;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
:deep(.activation-input .el-input__wrapper:hover),
|
||||
:deep(.activation-input .el-input__wrapper.is-focus) {
|
||||
box-shadow: 0 8px 25px rgba(255, 105, 180, 0.15);
|
||||
background-image: linear-gradient(white, white), linear-gradient(135deg, #ff9a9e, #a18cd1);
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
:deep(.activation-input .el-input__inner) {
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
letter-spacing: 2px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.redeem-btn {
|
||||
width: 100%;
|
||||
height: 54px;
|
||||
border-radius: 50px;
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 4px;
|
||||
color: white;
|
||||
border: none;
|
||||
background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 50%, #a18cd1 100%);
|
||||
background-size: 200% 200%;
|
||||
animation: gradient-anim 5s ease infinite;
|
||||
box-shadow: 0 8px 20px rgba(255, 117, 140, 0.4);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes gradient-anim {
|
||||
0% { background-position: 0% 50%; }
|
||||
50% { background-position: 100% 50%; }
|
||||
100% { background-position: 0% 50%; }
|
||||
}
|
||||
|
||||
.redeem-btn:hover {
|
||||
transform: translateY(-3px) scale(1.02);
|
||||
box-shadow: 0 12px 30px rgba(161, 140, 209, 0.6);
|
||||
}
|
||||
|
||||
.redeem-btn:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.tips-section {
|
||||
width: 100%;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
border: 1px dashed #dcdfe6;
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
.tip-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 6px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.tip-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.tip-dot {
|
||||
color: #ff9a9e;
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
line-height: 14px;
|
||||
}
|
||||
</style>
|
||||
@@ -86,12 +86,14 @@ const navItems = [
|
||||
// { name: 'userInfo', label: '用户信息', icon: 'User' },
|
||||
{ name: 'apiKey', label: 'API密钥', icon: 'Key' },
|
||||
|
||||
|
||||
{ name: 'rechargeLog', label: '充值记录', icon: 'Document' },
|
||||
{ name: 'usageStatistics', label: '用量统计', icon: 'Histogram' },
|
||||
{ name: 'premiumService', label: '尊享服务', icon: 'ColdDrink' },
|
||||
{ name: 'dailyTask', label: '每日任务(限时)', icon: 'Trophy' },
|
||||
{ name: 'cardFlip', label: '每周邀请(限时)', icon: 'Present' },
|
||||
// { name: 'usageStatistics2', label: '用量统计2', icon: 'Histogram' },
|
||||
{ name: 'activationCode', label: '激活码兑换', icon: 'MagicStick' },
|
||||
];
|
||||
function openDialog() {
|
||||
dialogVisible.value = true;
|
||||
@@ -302,6 +304,24 @@ watch(() => guideTourStore.shouldStartUserCenterTour, (shouldStart) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ============ 监听 Store 状态,控制用户中心弹窗 (新增) ============
|
||||
watch(() => userStore.isUserCenterVisible, (val) => {
|
||||
dialogVisible.value = val;
|
||||
});
|
||||
|
||||
watch(() => userStore.userCenterActiveTab, (val) => {
|
||||
if (val) {
|
||||
activeNav.value = val;
|
||||
}
|
||||
});
|
||||
|
||||
// 监听本地 dialogVisible 变化,同步回 Store(可选,为了保持一致性)
|
||||
watch(dialogVisible, (val) => {
|
||||
if (!val) {
|
||||
userStore.closeUserCenter();
|
||||
}
|
||||
});
|
||||
|
||||
// ============ 暴露方法供外部调用 ============
|
||||
defineExpose({
|
||||
openDialog,
|
||||
@@ -440,6 +460,9 @@ defineExpose({
|
||||
<template #apiKey>
|
||||
<APIKeyManagement />
|
||||
</template>
|
||||
<template #activationCode>
|
||||
<activation-code />
|
||||
</template>
|
||||
<template #dailyTask>
|
||||
<daily-task />
|
||||
</template>
|
||||
|
||||
@@ -46,6 +46,21 @@ export const useUserStore = defineStore(
|
||||
isLoginDialogVisible.value = false;
|
||||
};
|
||||
|
||||
// 新增:用户中心弹框状态和激活标签
|
||||
const isUserCenterVisible = ref(false);
|
||||
const userCenterActiveTab = ref('user');
|
||||
|
||||
// 新增:打开用户中心方法
|
||||
const openUserCenter = (tab: string = 'user') => {
|
||||
userCenterActiveTab.value = tab;
|
||||
isUserCenterVisible.value = true;
|
||||
};
|
||||
|
||||
// 新增:关闭用户中心方法
|
||||
const closeUserCenter = () => {
|
||||
isUserCenterVisible.value = false;
|
||||
};
|
||||
|
||||
return {
|
||||
token,
|
||||
refreshToken,
|
||||
@@ -59,6 +74,11 @@ export const useUserStore = defineStore(
|
||||
isLoginDialogVisible,
|
||||
openLoginDialog,
|
||||
closeLoginDialog,
|
||||
// 新增:暴露用户中心状态和方法
|
||||
isUserCenterVisible,
|
||||
userCenterActiveTab,
|
||||
openUserCenter,
|
||||
closeUserCenter,
|
||||
};
|
||||
},
|
||||
{
|
||||
|
||||
@@ -127,7 +127,8 @@ function jwtPlugin(): {
|
||||
const data = await (error.response.json());
|
||||
// 弹窗提示
|
||||
ElMessage.error(data.error.message);
|
||||
return Promise.reject(data);
|
||||
// return Promise.reject(data);
|
||||
return data;
|
||||
}
|
||||
if (error.status === 401) {
|
||||
ElMessage.error('登录已过期,请重新登录!');
|
||||
|
||||
1
Yi.Ai.Vue3/types/components.d.ts
vendored
1
Yi.Ai.Vue3/types/components.d.ts
vendored
@@ -9,6 +9,7 @@ export {}
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
AccountPassword: typeof import('./../src/components/LoginDialog/components/FormLogin/AccountPassword.vue')['default']
|
||||
ActivationCode: typeof import('./../src/components/userPersonalCenter/components/ActivationCode.vue')['default']
|
||||
APIKeyManagement: typeof import('./../src/components/userPersonalCenter/components/APIKeyManagement.vue')['default']
|
||||
CardFlipActivity: typeof import('./../src/components/userPersonalCenter/components/CardFlipActivity.vue')['default']
|
||||
DailyTask: typeof import('./../src/components/userPersonalCenter/components/DailyTask.vue')['default']
|
||||
|
||||
1
Yi.Ai.Vue3/types/import_meta.d.ts
vendored
1
Yi.Ai.Vue3/types/import_meta.d.ts
vendored
@@ -7,7 +7,6 @@ interface ImportMetaEnv {
|
||||
readonly VITE_WEB_BASE_API: string;
|
||||
readonly VITE_API_URL: string;
|
||||
readonly VITE_FILE_UPLOAD_API: string;
|
||||
readonly VITE_BUILD_COMPRESS: string;
|
||||
readonly VITE_SSO_SEVER_URL: string;
|
||||
readonly VITE_APP_VERSION: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user