feat: 新增翻牌幸运值悬浮球及相关逻辑

- .claude/settings.local.json:新增 Read 权限路径(Read(//e/code/github/Yi/Yi.Ai.Vue3/**))
- Yi.Ai.Vue3/src/components/userPersonalCenter/components/CardFlipActivity.vue:
  - 新增 luckyValue 响应式状态与 updateLuckyValue() 方法,并在获取任务状态后更新幸运值
  - 新增悬浮球 UI(SVG 进度环、图标、百分比文本)及样式和动画
  - 调整了 v-loading 为 false,并注释了部分错误提示(可能为调试遗留)
- 说明:样式使用嵌套写法(scss/sass 风格),请确认构建流程支持;建议确认 v-loading 与错误提示变更是否为预期并视情况修正。
This commit is contained in:
chenchun
2025-10-30 21:16:19 +08:00
parent fb25e75a3a
commit c4b631c815
2 changed files with 173 additions and 3 deletions

View File

@@ -1,7 +1,8 @@
{
"permissions": {
"allow": [
"Bash(dotnet build \"E:\\code\\github\\Yi\\Yi.Abp.Net8\\module\\ai-hub\\Yi.Framework.AiHub.Application\\Yi.Framework.AiHub.Application.csproj\" --no-restore)"
"Bash(dotnet build \"E:\\code\\github\\Yi\\Yi.Abp.Net8\\module\\ai-hub\\Yi.Framework.AiHub.Application\\Yi.Framework.AiHub.Application.csproj\" --no-restore)",
"Read(//e/code/github/Yi/Yi.Ai.Vue3/**)"
],
"deny": [],
"ask": []

View File

@@ -17,6 +17,9 @@ const showInviteSection = ref(false);
// 翻牌动画状态
const flippingCards = ref<Set<number>>(new Set());
// 幸运值状态
const luckyValue = ref(0); // 当前幸运值 0-100
onMounted(() => {
fetchTaskStatus();
});
@@ -35,6 +38,9 @@ async function fetchTaskStatus() {
try {
const res = await getWeeklyTaskStatus();
taskData.value = res.data;
// 根据已翻牌次数更新幸运值
updateLuckyValue();
}
catch (error: any) {
ElMessage.error(error?.message || '获取任务状态失败');
@@ -49,6 +55,15 @@ async function fetchTaskStatus() {
}
}
// 更新幸运值
function updateLuckyValue() {
if (taskData.value) {
const flips = taskData.value.totalFlips;
// 每次翻牌增加10%幸运值
luckyValue.value = Math.min(flips * 12, 100);
}
}
// 翻牌
async function handleFlipCard(record: CardFlipRecord) {
// 检查是否可以翻牌(任何未翻过的牌都可以)
@@ -183,7 +198,7 @@ async function handleUseInviteCode() {
await fetchTaskStatus();
}
catch (error: any) {
ElMessage.error(error?.message || '使用邀请码失败');
// ElMessage.error(error?.message || '使用邀请码失败');
}
finally {
loading.value = false;
@@ -261,7 +276,39 @@ function toggleInviteSection() {
</script>
<template>
<div v-loading="loading" class="card-flip-container">
<div v-loading="false" class="card-flip-container">
<!-- 幸运值悬浮球 -->
<div class="lucky-float-ball" :class="{ 'lucky-full': luckyValue >= 100 }">
<div class="lucky-circle">
<svg class="lucky-progress" viewBox="0 0 100 100">
<defs>
<linearGradient id="luckyGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#667eea;stop-opacity:1" />
<stop offset="100%" style="stop-color:#764ba2;stop-opacity:1" />
</linearGradient>
</defs>
<circle
class="lucky-bg"
cx="50"
cy="50"
r="40"
/>
<circle
class="lucky-bar"
cx="50"
cy="50"
r="40"
:stroke-dasharray="`${luckyValue * 2.51} 251`"
/>
</svg>
<div class="lucky-content">
<div class="lucky-icon">{{ luckyValue >= 100 ? '🍀' : '✨' }}</div>
<div class="lucky-text">{{ luckyValue }}%</div>
</div>
</div>
<div class="lucky-label">翻牌幸运值</div>
</div>
<!-- 初始加载骨架屏 -->
<div v-if="initialLoading" class="loading-skeleton">
<div class="skeleton-stats">
@@ -545,6 +592,128 @@ function toggleInviteSection() {
}
}
/* 幸运值悬浮球 */
.lucky-float-ball {
position: fixed;
left: 50%;
/* left: 20px; */
/* top: 20px; */
z-index: 999;
bottom: 20px;
transition: all 0.3s
cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
&:hover {
transform: scale(1.1);
}
&.lucky-full {
animation: lucky-celebration 2s ease-in-out infinite;
.lucky-circle {
box-shadow: 0 0 20px rgba(255, 215, 0, 0.8), 0 0 40px rgba(255, 215, 0, 0.6);
}
.lucky-content {
animation: lucky-pulse 1.5s ease-in-out infinite;
}
}
}
.lucky-circle {
position: relative;
width: 80px;
height: 80px;
background: rgba(255, 255, 255, 0.95);
border-radius: 50%;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2), 0 8px 32px rgba(102, 126, 234, 0.3);
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.lucky-progress {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
transform: rotate(-90deg);
.lucky-bg {
fill: none;
stroke: rgba(102, 126, 234, 0.1);
stroke-width: 6;
}
.lucky-bar {
fill: none;
stroke: url(#luckyGradient);
stroke-width: 6;
stroke-linecap: round;
transition: stroke-dasharray 0.5s ease;
}
}
.lucky-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
.lucky-icon {
font-size: 24px;
line-height: 1;
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2));
}
.lucky-text {
font-size: 14px;
font-weight: bold;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
line-height: 1;
}
}
.lucky-label {
text-align: center;
margin-top: 6px;
font-size: 12px;
font-weight: bold;
color: rgba(255, 255, 255, 0.95);
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
letter-spacing: 1px;
}
@keyframes lucky-celebration {
0%, 100% {
transform: scale(1) rotate(0deg);
}
25% {
transform: scale(1.1) rotate(-5deg);
}
50% {
transform: scale(1.15) rotate(0deg);
}
75% {
transform: scale(1.1) rotate(5deg);
}
}
@keyframes lucky-pulse {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
}
/* 顶部统计栏 */
.top-stats {
display: flex;