fix: 个人中心优化

This commit is contained in:
Gsh
2025-10-29 00:17:36 +08:00
parent c6425ca206
commit dd3f6325bb
10 changed files with 366 additions and 152 deletions

View File

@@ -1,4 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { FullScreen } from '@element-plus/icons-vue';
import { ref, watch } from 'vue'; import { ref, watch } from 'vue';
interface NavItem { interface NavItem {
@@ -17,7 +18,7 @@ interface Props {
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
title: '弹窗标题', title: '弹窗标题',
width: '1000px', width: '75%',
defaultActive: '', defaultActive: '',
}); });
@@ -25,9 +26,13 @@ const emit = defineEmits(['update:modelValue', 'confirm', 'close', 'nav-change']
const visible = ref(false); const visible = ref(false);
const activeNav = ref(props.defaultActive || (props.navItems.length > 0 ? props.navItems[0].name : '')); const activeNav = ref(props.defaultActive || (props.navItems.length > 0 ? props.navItems[0].name : ''));
const isFullscreen = ref(false);
watch(() => props.modelValue, (val) => { watch(() => props.modelValue, (val) => {
visible.value = val; visible.value = val;
if (!val) {
isFullscreen.value = false; // 关闭时重置全屏状态
}
}); });
watch(() => props.defaultActive, (val) => { watch(() => props.defaultActive, (val) => {
@@ -51,17 +56,46 @@ function handleConfirm() {
emit('confirm', activeNav.value); emit('confirm', activeNav.value);
handleClose(); handleClose();
} }
function toggleFullscreen() {
isFullscreen.value = !isFullscreen.value;
}
</script> </script>
<template> <template>
<el-dialog <el-dialog
v-model="visible" v-model="visible"
:title="title" :title="title"
:width="width" :width="isFullscreen ? '100%' : width"
:before-close="handleClose" :before-close="handleClose"
:fullscreen="isFullscreen"
:top="isFullscreen ? '0' : '5vh'"
class="nav-dialog" class="nav-dialog"
> >
<template #header="{ titleId, titleClass }">
<div class="dialog-header">
<h4 :id="titleId" :class="titleClass">
{{ title }}
</h4>
<!-- 全屏按钮暂不做 -->
<div v-if="false" class="header-actions">
<slot name="extra-actions" />
<el-button
circle
plain
size="small"
class="fullscreen-btn"
:title="isFullscreen ? '退出全屏' : '全屏'"
@click="toggleFullscreen"
>
<el-icon>
<FullScreen />
</el-icon>
</el-button>
</div>
</div>
</template>
<div class="dialog-container"> <div class="dialog-container">
<!-- 左侧导航 --> <!-- 左侧导航 -->
<div class="nav-side"> <div class="nav-side">
@@ -104,24 +138,52 @@ function handleConfirm() {
</template> </template>
<style scoped> <style scoped>
.dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.header-actions {
display: flex;
gap: 8px;
align-items: center;
}
.fullscreen-btn {
transition: all 0.3s;
}
.fullscreen-btn:hover {
transform: scale(1.1);
}
.dialog-container { .dialog-container {
display: flex; display: flex;
height: 500px; height: 70vh;
min-height: 500px;
}
:deep(.el-dialog.is-fullscreen) .dialog-container {
height: calc(100vh - 120px);
} }
.nav-side { .nav-side {
width: 200px; width: 200px;
border-right: 1px solid #e6e6e6; border-right: 1px solid #e6e6e6;
flex-shrink: 0;
} }
.nav-menu { .nav-menu {
border-right: none; border-right: none;
height: 100%;
} }
.content-main { .content-main {
flex: 1; flex: 1;
padding: 0 20px; padding: 0 20px;
overflow: auto; overflow: auto;
min-width: 0;
} }
.empty-content { .empty-content {

View File

@@ -262,7 +262,7 @@ onMounted(async () => {
<!-- 自适应缩放 iframe --> <!-- 自适应缩放 iframe -->
<iframe <iframe
src="https://ccnetcore.com/article/3a1bc4d1-6a7d-751d-91cc-2817eb2ddcde" src="https://ccnetcore.com/article/3a1bc4d1-6a7d-751d-91cc-2817eb2ddcde"
class="min-w-full h-[700px] scale-100 duration-300" class="min-w-full iframe-responsive scale-100 duration-300"
loading="lazy" loading="lazy"
sandbox="allow-scripts allow-same-origin allow-popups" sandbox="allow-scripts allow-same-origin allow-popups"
@load="document.querySelector('.iframe-loading')?.remove()" @load="document.querySelector('.iframe-loading')?.remove()"
@@ -321,6 +321,13 @@ onMounted(async () => {
min-height: 200px; min-height: 200px;
} }
/* iframe 响应式高度 */
.iframe-responsive {
height: 60vh;
min-height: 400px;
max-height: 800px;
}
/* 未领取状态样式 */ /* 未领取状态样式 */
.unclaimed-state { .unclaimed-state {
text-align: center; text-align: center;

View File

@@ -1,8 +1,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref, onMounted, computed } from 'vue'; import type { CardFlipRecord, CardFlipStatusOutput, FlipCardOutput } from '@/api/cardFlip/types';
import { ElMessage, ElMessageBox } from 'element-plus'; import { ElMessage, ElMessageBox } from 'element-plus';
import { getWeeklyTaskStatus, flipCard, useInviteCode, generateMyInviteCode } from '@/api/cardFlip'; import { onMounted, ref } from 'vue';
import type { CardFlipStatusOutput, CardFlipRecord, FlipCardOutput } from '@/api/cardFlip/types'; import { flipCard, generateMyInviteCode, getWeeklyTaskStatus, useInviteCode } from '@/api/cardFlip';
const taskData = ref<CardFlipStatusOutput | null>(null); const taskData = ref<CardFlipStatusOutput | null>(null);
const loading = ref(false); const loading = ref(false);
@@ -26,9 +26,11 @@ async function fetchTaskStatus() {
try { try {
const res = await getWeeklyTaskStatus(); const res = await getWeeklyTaskStatus();
taskData.value = res.data; taskData.value = res.data;
} catch (error: any) { }
catch (error: any) {
ElMessage.error(error?.message || '获取任务状态失败'); ElMessage.error(error?.message || '获取任务状态失败');
} finally { }
finally {
loading.value = false; loading.value = false;
} }
} }
@@ -50,7 +52,7 @@ async function handleFlipCard(record: CardFlipRecord) {
confirmButtonText: '去使用', confirmButtonText: '去使用',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning', type: 'warning',
} },
) )
.then(() => { .then(() => {
inviteCodeDialog.value = true; inviteCodeDialog.value = true;
@@ -70,6 +72,12 @@ async function handleFlipCard(record: CardFlipRecord) {
// 等待翻牌动画完成 // 等待翻牌动画完成
await new Promise(resolve => setTimeout(resolve, 800)); await new Promise(resolve => setTimeout(resolve, 800));
// 刷新任务状态(先刷新数据,让卡片显示翻转后的结果)
await fetchTaskStatus();
// 稍微延迟后显示结果提示
await new Promise(resolve => setTimeout(resolve, 300));
// 显示结果 // 显示结果
if (res.data.isWin) { if (res.data.isWin) {
// 播放中奖动画 // 播放中奖动画
@@ -78,31 +86,31 @@ async function handleFlipCard(record: CardFlipRecord) {
ElMessage.success({ ElMessage.success({
message: res.data.rewardDesc || '🎉 恭喜中奖!', message: res.data.rewardDesc || '🎉 恭喜中奖!',
duration: 3000, duration: 3000,
customClass: 'win-message' customClass: 'win-message',
}); });
// 第9次中奖显示翻倍提示 // 第9次中奖显示翻倍提示
if (res.data.showDoubleRewardTip) { if (res.data.showDoubleRewardTip) {
setTimeout(() => { setTimeout(() => {
showDoubleRewardDialog.value = true; showDoubleRewardDialog.value = true;
}, 1500); }, 500);
} }
} else { }
else {
ElMessage.info({ ElMessage.info({
message: '😢 很遗憾,未中奖', message: '😢 很遗憾,未中奖',
duration: 2000 duration: 2000,
}); });
} }
}
// 刷新任务状态 catch (error: any) {
await fetchTaskStatus();
} catch (error: any) {
ElMessage.error(error?.message || '翻牌失败'); ElMessage.error(error?.message || '翻牌失败');
} finally { }
finally {
flipping.value = false; flipping.value = false;
setTimeout(() => { setTimeout(() => {
flippingCards.value.delete(record.flipNumber); flippingCards.value.delete(record.flipNumber);
}, 800); }, 300);
} }
} }
} }
@@ -112,17 +120,23 @@ function showWinAnimation() {
// 创建烟花效果 // 创建烟花效果
const container = document.querySelector('.card-flip-container'); const container = document.querySelector('.card-flip-container');
if (container) { if (container) {
for (let i = 0; i < 20; i++) { const colors = ['#f56c6c', '#409eff', '#67c23a', '#e6a23c', '#9c27b0', '#FFD700', '#FF6B9D'];
for (let i = 0; i < 50; i++) {
const confetti = document.createElement('div'); const confetti = document.createElement('div');
confetti.className = 'confetti'; confetti.className = 'confetti';
confetti.style.left = Math.random() * 100 + '%'; confetti.style.left = `${Math.random() * 100}%`;
confetti.style.animationDelay = Math.random() * 0.5 + 's'; confetti.style.animationDelay = `${Math.random() * 0.5}s`;
confetti.style.background = ['#f56c6c', '#409eff', '#67c23a', '#e6a23c', '#9c27b0'][Math.floor(Math.random() * 5)]; confetti.style.background = colors[Math.floor(Math.random() * colors.length)];
const size = 6 + Math.random() * 10;
confetti.style.width = `${size}px`;
confetti.style.height = `${size}px`;
confetti.style.borderRadius = Math.random() > 0.5 ? '50%' : '2px';
confetti.style.opacity = `${0.7 + Math.random() * 0.3}`;
container.appendChild(confetti); container.appendChild(confetti);
setTimeout(() => { setTimeout(() => {
confetti.remove(); confetti.remove();
}, 2000); }, 3000);
} }
} }
} }
@@ -141,9 +155,11 @@ async function handleUseInviteCode() {
inviteCodeDialog.value = false; inviteCodeDialog.value = false;
inputInviteCode.value = ''; inputInviteCode.value = '';
await fetchTaskStatus(); await fetchTaskStatus();
} catch (error: any) { }
catch (error: any) {
ElMessage.error(error?.message || '使用邀请码失败'); ElMessage.error(error?.message || '使用邀请码失败');
} finally { }
finally {
loading.value = false; loading.value = false;
} }
} }
@@ -155,9 +171,11 @@ async function handleGenerateInviteCode() {
await generateMyInviteCode(); await generateMyInviteCode();
ElMessage.success('✨ 邀请码生成成功!'); ElMessage.success('✨ 邀请码生成成功!');
await fetchTaskStatus(); await fetchTaskStatus();
} catch (error: any) { }
catch (error: any) {
ElMessage.error(error?.message || '生成邀请码失败'); ElMessage.error(error?.message || '生成邀请码失败');
} finally { }
finally {
loading.value = false; loading.value = false;
} }
} }
@@ -189,7 +207,8 @@ function getCardClass(record: CardFlipRecord): string[] {
classes.push('card-flipped'); classes.push('card-flipped');
if (record.isWin) { if (record.isWin) {
classes.push('card-win'); classes.push('card-win');
} else { }
else {
classes.push('card-lose'); classes.push('card-lose');
} }
} }
@@ -201,7 +220,8 @@ function getCardClass(record: CardFlipRecord): string[] {
// 可点击的卡片(任何未翻过的牌都可以点击) // 可点击的卡片(任何未翻过的牌都可以点击)
if (!record.isFlipped && (taskData.value?.canFlip || false)) { if (!record.isFlipped && (taskData.value?.canFlip || false)) {
classes.push('card-clickable'); classes.push('card-clickable');
} else if (!record.isFlipped) { }
else if (!record.isFlipped) {
classes.push('card-locked'); classes.push('card-locked');
} }
@@ -216,100 +236,13 @@ function toggleInviteSection() {
<template> <template>
<div v-loading="loading" class="card-flip-container"> <div v-loading="loading" class="card-flip-container">
<!-- 顶部统计栏 --> <!-- 邀请码操作区 -->
<div class="top-stats">
<div class="stat-item">
<div class="stat-icon-box progress">
<span class="stat-icon">🎯</span>
</div>
<div class="stat-content">
<div class="stat-value">{{ taskData?.totalFlips || 0 }}/10</div>
<div class="stat-label">已翻</div>
</div>
</div>
<div class="stat-item">
<div class="stat-icon-box gift">
<span class="stat-icon">🎁</span>
</div>
<div class="stat-content">
<div class="stat-value">{{ taskData ? 10 - taskData.totalFlips : 10 }}</div>
<div class="stat-label">剩余</div>
</div>
</div>
<div class="stat-item">
<div class="stat-icon-box invite">
<span class="stat-icon">👥</span>
</div>
<div class="stat-content">
<div class="stat-value">{{ taskData?.invitedCount || 0 }}</div>
<div class="stat-label">邀请</div>
</div>
</div>
</div>
<!-- 提示横幅 -->
<div v-if="taskData?.nextFlipTip" class="tip-banner">
<span class="tip-icon">💡</span>
<span class="tip-text">{{ taskData.nextFlipTip }}</span>
</div>
<!-- 翻牌区域 -->
<div class="cards-section">
<div class="cards-grid">
<div
v-for="record in taskData?.flipRecords"
:key="record.flipNumber"
:class="getCardClass(record)"
@click="handleFlipCard(record)"
>
<div class="card-inner">
<!-- 卡片正面 -->
<div class="card-front">
<div class="card-corner">
<span class="card-number">#{{ record.flipNumber }}</span>
</div>
<div class="card-content">
<div class="card-icon">🎴</div>
<div class="card-mystery">?</div>
</div>
<div class="card-type-badge">{{ record.flipTypeDesc }}</div>
<div class="card-shine"></div>
</div>
<!-- 卡片背面 -->
<div class="card-back">
<div v-if="record.isWin" class="card-result win">
<div class="win-glow"></div>
<div class="result-icon">🎉</div>
<div class="result-text">中奖啦!</div>
<div class="result-amount">{{ formatTokenDisplay(record.rewardAmount || 0) }}</div>
<div class="result-unit">Tokens</div>
</div>
<div v-else class="card-result lose">
<div class="result-icon">💫</div>
<div class="result-text">未中奖</div>
<div class="result-tip">继续加油!</div>
</div>
</div>
</div>
<!-- 锁定遮罩 -->
<div v-if="!record.isFlipped && record.flipNumber > (taskData?.totalFlips || 0) + 1" class="card-lock">
<span class="lock-icon">🔒</span>
</div>
</div>
</div>
</div>
<!-- 底部操作区 -->
<div class="bottom-actions"> <div class="bottom-actions">
<el-button <el-button
type="primary" type="primary"
:icon="showInviteSection ? 'ArrowUp' : 'Gift'" :icon="showInviteSection ? 'ArrowUp' : 'Gift'"
@click="toggleInviteSection"
class="invite-toggle-btn" class="invite-toggle-btn"
@click="toggleInviteSection"
> >
{{ showInviteSection ? '收起' : '邀请码' }} {{ showInviteSection ? '收起' : '邀请码' }}
</el-button> </el-button>
@@ -317,8 +250,8 @@ function toggleInviteSection() {
<el-button <el-button
type="warning" type="warning"
icon="Unlock" icon="Unlock"
@click="inviteCodeDialog = true"
class="use-code-btn" class="use-code-btn"
@click="inviteCodeDialog = true"
> >
使用邀请码 使用邀请码
</el-button> </el-button>
@@ -359,6 +292,125 @@ function toggleInviteSection() {
</div> </div>
</el-collapse-transition> </el-collapse-transition>
<!-- 顶部统计栏 -->
<div class="top-stats">
<div class="stat-item">
<div class="stat-icon-box progress">
<span class="stat-icon">🎯</span>
</div>
<div class="stat-content">
<div class="stat-value">
{{ taskData?.totalFlips || 0 }}/10
</div>
<div class="stat-label">
已翻
</div>
</div>
</div>
<div class="stat-item">
<div class="stat-icon-box gift">
<span class="stat-icon">🎁</span>
</div>
<div class="stat-content">
<div class="stat-value">
{{ taskData ? 10 - taskData.totalFlips : 10 }}
</div>
<div class="stat-label">
剩余
</div>
</div>
</div>
<div class="stat-item">
<div class="stat-icon-box invite">
<span class="stat-icon">👥</span>
</div>
<div class="stat-content">
<div class="stat-value">
{{ taskData?.invitedCount || 0 }}
</div>
<div class="stat-label">
邀请
</div>
</div>
</div>
</div>
<!-- 提示横幅 -->
<div v-if="taskData?.nextFlipTip" class="tip-banner">
<span class="tip-icon">💡</span>
<span class="tip-text">{{ taskData.nextFlipTip }}</span>
</div>
<!-- 翻牌区域 -->
<div class="cards-section">
<div class="cards-grid">
<div
v-for="record in taskData?.flipRecords"
:key="record.flipNumber"
:class="getCardClass(record)"
@click="handleFlipCard(record)"
>
<div class="card-inner">
<!-- 卡片正面 -->
<div class="card-front">
<div class="card-corner">
<span class="card-number">#{{ record.flipNumber }}</span>
</div>
<div class="card-content">
<div class="card-icon">
🎴
</div>
<div class="card-mystery">
?
</div>
</div>
<div class="card-type-badge">
{{ record.flipTypeDesc }}
</div>
<div class="card-shine" />
</div>
<!-- 卡片背面 -->
<div class="card-back">
<div v-if="record.isWin" class="card-result win">
<div class="win-glow" />
<div class="result-icon">
🎉
</div>
<div class="result-text">
中奖啦!
</div>
<div class="result-amount">
{{ formatTokenDisplay(record.rewardAmount || 0) }}
</div>
<div class="result-unit">
Tokens
</div>
</div>
<div v-else class="card-result lose">
<div class="result-icon">
💫
</div>
<div class="result-text">
未中奖
</div>
<div class="result-tip">
继续加油!
</div>
</div>
</div>
</div>
<!-- 锁定遮罩 -->
<div v-if="!record.isFlipped && record.flipNumber > (taskData?.totalFlips || 0) + 1" class="card-lock">
<span class="lock-icon">🔒</span>
</div>
</div>
</div>
</div>
<!-- 使用邀请码对话框 --> <!-- 使用邀请码对话框 -->
<el-dialog <el-dialog
v-model="inviteCodeDialog" v-model="inviteCodeDialog"
@@ -369,7 +421,9 @@ function toggleInviteSection() {
class="invite-dialog" class="invite-dialog"
> >
<div class="invite-dialog-content"> <div class="invite-dialog-content">
<p class="dialog-tip">请输入好友的邀请码解锁最后2次翻牌机会</p> <p class="dialog-tip">
请输入好友的邀请码解锁最后2次翻牌机会
</p>
<el-input <el-input
v-model="inputInviteCode" v-model="inputInviteCode"
placeholder="请输入8位邀请码" placeholder="请输入8位邀请码"
@@ -384,8 +438,10 @@ function toggleInviteSection() {
</el-input> </el-input>
</div> </div>
<template #footer> <template #footer>
<el-button @click="inviteCodeDialog = false">取消</el-button> <el-button @click="inviteCodeDialog = false">
<el-button type="primary" @click="handleUseInviteCode" :loading="loading"> 取消
</el-button>
<el-button type="primary" :loading="loading" @click="handleUseInviteCode">
确认使用 确认使用
</el-button> </el-button>
</template> </template>
@@ -401,16 +457,22 @@ function toggleInviteSection() {
> >
<div class="double-reward-content"> <div class="double-reward-content">
<div class="double-icon-container"> <div class="double-icon-container">
<div class="double-icon">🎁</div> <div class="double-icon">
<div class="double-sparkle"></div> 🎁
</div>
<div class="double-sparkle">
</div>
</div> </div>
<h3 class="double-title">恭喜获得额外翻倍包!</h3> <h3 class="double-title">
恭喜获得额外翻倍包!
</h3>
<p class="double-text"> <p class="double-text">
您已获得第9次奖励<br> 您已获得第9次奖励<br>
<span class="highlight">第10次翻牌将获得翻倍奖励</span><br> <span class="highlight">第10次翻牌将获得翻倍奖励</span><br>
赶快翻开最后一张牌吧 赶快翻开最后一张牌吧
</p> </p>
<el-button type="primary" size="large" @click="showDoubleRewardDialog = false" class="double-btn"> <el-button type="primary" size="large" class="double-btn" @click="showDoubleRewardDialog = false">
知道了去翻牌 🚀 知道了去翻牌 🚀
</el-button> </el-button>
</div> </div>
@@ -422,8 +484,9 @@ function toggleInviteSection() {
.card-flip-container { .card-flip-container {
position: relative; position: relative;
padding: 16px; padding: 16px;
min-height: 500px; min-height: 400px;
max-height: 70vh; max-height: none;
height: 100%;
overflow-y: auto; overflow-y: auto;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16px; border-radius: 16px;
@@ -464,10 +527,10 @@ function toggleInviteSection() {
background: rgba(255, 255, 255, 0.95); background: rgba(255, 255, 255, 0.95);
border-radius: 12px; border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease; transition: transform 0.3s ease, box-shadow 0.3s ease;
&:hover { &:hover {
transform: translateY(-2px); transform: translateY(-2px) scale(1.02);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15); box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15);
} }
} }
@@ -567,11 +630,18 @@ function toggleInviteSection() {
&:not(.card-flipping):not(.card-flipped) { &:not(.card-flipping):not(.card-flipped) {
.card-inner { .card-inner {
transition: transform 0.3s ease, box-shadow 0.3s ease; transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.3s ease, filter 0.3s ease;
} }
&:hover .card-inner { &:hover .card-inner {
transform: translateY(-4px); transform: translateY(-8px) scale(1.05) rotateZ(2deg);
box-shadow: 0 12px 28px rgba(102, 126, 234, 0.5), 0 4px 12px rgba(255, 215, 0, 0.3);
filter: brightness(1.1);
}
&:active .card-inner {
transform: translateY(-4px) scale(1.02);
box-shadow: 0 6px 16px rgba(102, 126, 234, 0.4);
} }
} }
} }
@@ -670,8 +740,9 @@ function toggleInviteSection() {
left: -100%; left: -100%;
width: 100%; width: 100%;
height: 100%; height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent); background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent);
animation: shine 3s infinite; animation: shine 3s infinite;
pointer-events: none;
} }
} }
@@ -682,8 +753,6 @@ function toggleInviteSection() {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
position: relative;
top: -170px;
} }
.card-result { .card-result {
@@ -698,17 +767,18 @@ function toggleInviteSection() {
&.win { &.win {
position: relative; position: relative;
overflow: hidden;
.win-glow { .win-glow {
position: absolute; position: absolute;
inset: 0; inset: 0;
background: radial-gradient(circle, rgba(255, 215, 0, 0.3) 0%, transparent 70%); background: radial-gradient(circle, rgba(255, 215, 0, 0.4) 0%, rgba(255, 107, 157, 0.2) 50%, transparent 70%);
animation: glow 1.5s infinite; animation: glow 1.5s infinite;
} }
.result-icon { .result-icon {
font-size: 32px; font-size: 32px;
animation: bounce 0.6s ease; animation: bounce 0.6s ease, rotate 2s ease-in-out infinite;
} }
.result-text { .result-text {
@@ -716,6 +786,7 @@ function toggleInviteSection() {
font-weight: bold; font-weight: bold;
color: #f56c6c; color: #f56c6c;
margin: 4px 0; margin: 4px 0;
text-shadow: 0 2px 4px rgba(245, 108, 108, 0.3);
} }
.result-amount { .result-amount {
@@ -725,6 +796,7 @@ function toggleInviteSection() {
-webkit-background-clip: text; -webkit-background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
margin: 2px 0; margin: 2px 0;
animation: scaleUp 0.5s ease;
} }
.result-unit { .result-unit {
@@ -777,16 +849,34 @@ function toggleInviteSection() {
.el-button { .el-button {
flex: 1; flex: 1;
font-weight: bold; font-weight: bold;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
&:active {
transform: translateY(0);
}
} }
.invite-toggle-btn { .invite-toggle-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none; border: none;
&:hover {
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
}
} }
.use-code-btn { .use-code-btn {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
border: none; border: none;
&:hover {
background: linear-gradient(135deg, #f5576c 0%, #f093fb 100%);
}
} }
} }
@@ -796,6 +886,7 @@ function toggleInviteSection() {
border-radius: 12px; border-radius: 12px;
padding: 16px; padding: 16px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
margin: 10px auto;
} }
.invite-header { .invite-header {
@@ -1028,4 +1119,24 @@ function toggleInviteSection() {
transform: translateY(0); transform: translateY(0);
} }
} }
@keyframes rotate {
0%, 100% {
transform: rotate(-5deg);
}
50% {
transform: rotate(5deg);
}
}
@keyframes scaleUp {
0% {
transform: scale(0.5);
opacity: 0;
}
100% {
transform: scale(1);
opacity: 1;
}
}
</style> </style>

View File

@@ -189,6 +189,8 @@ function getProgressColor(task: DailyTaskItem): string {
.daily-task-container { .daily-task-container {
padding: 20px; padding: 20px;
min-height: 400px; min-height: 400px;
height: 100%;
overflow-y: auto;
} }
.task-header { .task-header {

View File

@@ -273,6 +273,8 @@ function onProductPackage() {
.premium-service { .premium-service {
padding: 20px; padding: 20px;
position: relative; position: relative;
height: 100%;
overflow-y: auto;
} }
.header { .header {

View File

@@ -104,6 +104,11 @@ function contactCustomerService() {
innerVisibleContact.value = !innerVisibleContact.value; innerVisibleContact.value = !innerVisibleContact.value;
} }
// 暴露方法给父组件使用
defineExpose({
contactCustomerService,
});
// 过滤和排序后的数据 // 过滤和排序后的数据
const filteredData = computed(() => { const filteredData = computed(() => {
let data = [...logData.value]; let data = [...logData.value];
@@ -253,7 +258,6 @@ onMounted(() => {
</div> </div>
<el-table <el-table
v-loading="loading" v-loading="loading"
:data="filteredData" :data="filteredData"
style="width: 100%" style="width: 100%"
@@ -265,7 +269,7 @@ onMounted(() => {
<el-table-column <el-table-column
prop="content" prop="content"
label="套餐类型" label="套餐类型"
width="150" min-width="150"
sortable="custom" sortable="custom"
show-overflow-tooltip show-overflow-tooltip
/> />
@@ -273,7 +277,7 @@ onMounted(() => {
show-overflow-tooltip show-overflow-tooltip
prop="rechargeAmount" prop="rechargeAmount"
label="金额(元)" label="金额(元)"
width="110" min-width="110"
sortable="custom" sortable="custom"
> >
<template #default="{ row }"> <template #default="{ row }">
@@ -283,14 +287,14 @@ onMounted(() => {
<el-table-column <el-table-column
prop="creationTime" prop="creationTime"
label="充值时间" label="充值时间"
width="160" min-width="160"
sortable="custom" sortable="custom"
show-overflow-tooltip show-overflow-tooltip
/> />
<el-table-column <el-table-column
prop="expireDateTime" prop="expireDateTime"
label="到期时间" label="到期时间"
width="160" min-width="160"
sortable="custom" sortable="custom"
show-overflow-tooltip show-overflow-tooltip
/> />
@@ -302,7 +306,7 @@ onMounted(() => {
<span v-else>{{ row.contactInfo || '-' }}</span> <span v-else>{{ row.contactInfo || '-' }}</span>
</template> </template>
</el-table-column> --> </el-table-column> -->
<el-table-column show-overflow-tooltip prop="remark" label="备注" width="160"> <el-table-column show-overflow-tooltip prop="remark" label="备注" min-width="160">
<template #default="{ row }"> <template #default="{ row }">
<el-tooltip v-if="row.remark && row.remark.length > 10" :content="row.remark" placement="top"> <el-tooltip v-if="row.remark && row.remark.length > 10" :content="row.remark" placement="top">
<span class="ellipsis-text">{{ row.remark }}</span> <span class="ellipsis-text">{{ row.remark }}</span>
@@ -379,6 +383,8 @@ onMounted(() => {
border-radius: 16px; border-radius: 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease; transition: all 0.3s ease;
height: 100%;
overflow-y: auto;
} }
.recharge-log-container:hover { .recharge-log-container:hover {

View File

@@ -74,6 +74,8 @@ const gridTemplateColumns = computed(() => {
<style scoped> <style scoped>
.model-container { .model-container {
margin: 10px 0; margin: 10px 0;
height: 100%;
overflow-y: auto;
} }
.model-header { .model-header {

View File

@@ -494,7 +494,7 @@ onBeforeUnmount(() => {
</div> </div>
</template> </template>
<div class="chart-container"> <div class="chart-container">
<div ref="lineChart" class="chart" style="height: 400px;" /> <div ref="lineChart" class="chart" style="height: 350px;" />
</div> </div>
</el-card> </el-card>
@@ -505,7 +505,7 @@ onBeforeUnmount(() => {
</div> </div>
</template> </template>
<div class="chart-container"> <div class="chart-container">
<div ref="pieChart" class="chart" style="height: 450px;" /> <div ref="pieChart" class="chart" style="height: 400px;" />
</div> </div>
</el-card> </el-card>
@@ -516,7 +516,7 @@ onBeforeUnmount(() => {
</div> </div>
</template> </template>
<div class="chart-container"> <div class="chart-container">
<div ref="barChart" class="chart" style="height: 500px;" /> <div ref="barChart" class="chart" style="height: 450px;" />
</div> </div>
</el-card> </el-card>
</div> </div>
@@ -524,13 +524,14 @@ onBeforeUnmount(() => {
<style scoped> <style scoped>
.usage-statistics { .usage-statistics {
padding: 30px; padding: 20px;
position: relative; position: relative;
background: linear-gradient(135deg, #fff 0%, #f8f9fa 100%); background: linear-gradient(135deg, #fff 0%, #f8f9fa 100%);
border-radius: 16px; border-radius: 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease; transition: all 0.3s ease;
min-height: 100vh; height: 100%;
overflow-y: auto;
} }
.usage-statistics:hover { .usage-statistics:hover {

View File

@@ -293,6 +293,8 @@ function bindWechat() {
<style scoped> <style scoped>
.user-profile { .user-profile {
padding: 20px; padding: 20px;
height: 100%;
overflow-y: auto;
} }
.header { .header {

View File

@@ -2,6 +2,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'; import { computed } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { ChatLineRound } from '@element-plus/icons-vue';
import Popover from '@/components/Popover/index.vue'; import Popover from '@/components/Popover/index.vue';
import SvgIcon from '@/components/SvgIcon/index.vue'; import SvgIcon from '@/components/SvgIcon/index.vue';
import { useUserStore } from '@/stores'; import { useUserStore } from '@/stores';
@@ -58,6 +59,8 @@ const popoverList = ref([
]); ]);
const dialogVisible = ref(false); const dialogVisible = ref(false);
const rechargeLogRef = ref();
const activeNav = ref('user');
const navItems = [ const navItems = [
{ name: 'user', label: '用户信息', icon: 'User' }, { name: 'user', label: '用户信息', icon: 'User' },
@@ -82,6 +85,12 @@ function handleConfirm(activeNav: string) {
// 导航切换 // 导航切换
function handleNavChange(nav: string) { function handleNavChange(nav: string) {
activeNav.value = nav;
}
// 联系售后
function handleContactSupport() {
rechargeLogRef.value?.contactCustomerService();
} }
// 点击 // 点击
@@ -317,6 +326,16 @@ function onProductPackage() {
@confirm="handleConfirm" @confirm="handleConfirm"
@nav-change="handleNavChange" @nav-change="handleNavChange"
> >
<template #extra-actions>
<el-tooltip v-if="isUserVip() && activeNav === 'rechargeLog'" content="联系售后" placement="bottom">
<el-button circle plain size="small" @click="handleContactSupport">
<el-icon color="#07c160">
<ChatLineRound />
</el-icon>
</el-button>
</el-tooltip>
</template>
<!-- 用户管理内容 --> <!-- 用户管理内容 -->
<template #user> <template #user>
<user-management /> <user-management />
@@ -354,7 +373,7 @@ function onProductPackage() {
<card-flip-activity /> <card-flip-activity />
</template> </template>
<template #rechargeLog> <template #rechargeLog>
<recharge-log /> <recharge-log ref="rechargeLogRef" />
</template> </template>
</nav-dialog> </nav-dialog>
</div> </div>