This commit is contained in:
Your Name
2026-05-27 17:29:27 +08:00
parent 19af50c344
commit 36be8fedad
5 changed files with 519 additions and 325 deletions
@@ -1,16 +1,24 @@
<template>
<view class="sugar-tree-graphic" :class="[`lv-${clampedLevel}`, `size-${size}`, { watering: watering }]">
<view
class="sugar-tree-graphic"
:class="[`lv-${clampedLevel}`, `tier-${visualTier}`, `size-${size}`, { watering: watering, 'is-max': clampedLevel >= MAX_LEVEL }]"
>
<view class="stg-glow" />
<view v-if="clampedLevel >= 7" class="stg-aura" />
<image v-if="!treeUseFallback" class="stg-image" :src="treeImageSrc" mode="aspectFit" @error="treeUseFallback = true" />
<text v-else class="stg-emoji">{{ treeEmoji }}</text>
<view v-if="clampedLevel >= 4" class="stg-sparkle stg-sparkle-a" />
<view v-if="clampedLevel >= 4" class="stg-sparkle stg-sparkle-b" />
<view v-if="clampedLevel >= 7" class="stg-sparkle stg-sparkle-a" />
<view v-if="clampedLevel >= 8" class="stg-sparkle stg-sparkle-b" />
<view v-if="clampedLevel >= MAX_LEVEL" class="stg-sparkle stg-sparkle-c" />
</view>
</template>
<script setup>
import { computed, ref } from 'vue'
import { svgToDataUrl } from '../utils/svgDataUrl.js'
import { TREE_MAX_LEVEL, TREE_LEVELS } from '../utils/treeLevels.js'
const MAX_LEVEL = TREE_MAX_LEVEL
// #ifdef MP-WEIXIN
const treeUseFallback = ref(true)
@@ -19,18 +27,24 @@ const treeUseFallback = ref(true)
const treeUseFallback = ref(false)
// #endif
const TREE_EMOJI = ['🌱', '🌿', '🌳', '🌸', '🌺']
const props = defineProps({
level: { type: Number, default: 0 },
size: { type: String, default: 'md' },
watering: { type: Boolean, default: false }
})
const clampedLevel = computed(() => Math.min(4, Math.max(0, Number(props.level) || 0)))
const treeEmoji = computed(() => TREE_EMOJI[clampedLevel.value] || TREE_EMOJI[0])
const clampedLevel = computed(() => Math.min(MAX_LEVEL, Math.max(0, Number(props.level) || 0)))
const visualTier = computed(() => {
const lv = clampedLevel.value
if (lv <= 0) return 0
if (lv <= 2) return 1
if (lv <= 4) return 2
if (lv <= 6) return 3
if (lv <= 8) return 4
return 5
})
const treeEmoji = computed(() => (TREE_LEVELS[clampedLevel.value] || TREE_LEVELS[0]).emoji)
/** 医疗青绿 + 柔和陶土盆,5 档成长态 */
function buildTreeSvg(level) {
const pot = `
<ellipse cx="24" cy="50" rx="15" ry="3" fill="#0ea5a4" opacity="0.12"/>
@@ -40,63 +54,63 @@ function buildTreeSvg(level) {
`
const soil = `<ellipse cx="24" cy="44.5" rx="8" ry="2.2" fill="#0d9488" opacity="0.18"/>`
if (level === 0) {
if (level <= 0) {
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 56" fill="none">
${pot}
${soil}
<circle cx="24" cy="41.5" r="2" fill="#6ee7b7" opacity="0.55"/>
<path d="M24 41.5v4" stroke="#14b8a6" stroke-width="1.2" stroke-linecap="round" opacity="0.7"/>
${pot}${soil}
<circle cx="24" cy="41.5" r="2.2" fill="#a8a29e" opacity="0.7"/>
<path d="M24 41.5v3.5" stroke="#78716c" stroke-width="1" stroke-linecap="round"/>
</svg>`
}
const trunk = `<rect x="22.2" y="30" width="3.6" height="14" rx="1.8" fill="#78716C"/>
<rect x="22.6" y="30" width="2.8" height="14" rx="1.4" fill="#A8A29E" opacity="0.35"/>`
const trunkH = level >= 6 ? 16 : level >= 3 ? 14 : 10
const trunkY = 46 - trunkH
const trunk = `<rect x="22.2" y="${trunkY}" width="3.6" height="${trunkH}" rx="1.8" fill="#78716C"/>
<rect x="22.6" y="${trunkY + 1}" width="2.8" height="${trunkH - 1}" rx="1.4" fill="#A8A29E" opacity="0.35"/>`
const leaf = (cx, cy, r, fill, opacity = 1) =>
`<circle cx="${cx}" cy="${cy}" r="${r}" fill="${fill}" opacity="${opacity}"/>
<circle cx="${cx - r * 0.25}" cy="${cy - r * 0.2}" r="${r * 0.35}" fill="#ecfdf5" opacity="0.55"/>`
const blooms =
level >= 4
? `
${leaf(17, 19, 3.2, '#fda4af', 0.95)}
${leaf(30, 17, 3, '#f9a8d4', 0.9)}
${leaf(24, 14, 3.4, '#fb7185', 0.95)}
<circle cx="24" cy="14" r="1.2" fill="#fef3c7"/>
<circle cx="17" cy="19" r="0.9" fill="#fef3c7"/>
<circle cx="30" cy="17" r="0.8" fill="#fef3c7"/>
`
const bloom =
level >= 7
? leaf(17, 18, 3, '#fda4af', 0.95) +
leaf(31, 17, 2.8, '#f9a8d4', 0.9) +
leaf(24, 13, 3.2, '#fb7185', 0.95) +
`<circle cx="24" cy="12" r="1.3" fill="#fef3c7"/>`
: ''
const crown =
level >= 9
? leaf(12, 22, 6, '#34d399', 0.9) +
leaf(36, 22, 6, '#34d399', 0.9) +
leaf(24, 10, 7, '#10b981', 0.95) +
bloom
: bloom
let canopy = ''
if (level === 1) {
canopy = leaf(24, 26, 7, '#34d399') + leaf(24, 25, 4.5, '#10b981', 0.85)
canopy = leaf(24, 38, 4, '#6ee7b7') + `<path d="M24 38v-5" stroke="#14b8a6" stroke-width="1.2" stroke-linecap="round"/>`
} else if (level === 2) {
canopy = leaf(24, 32, 5.5, '#34d399') + leaf(20, 34, 3.5, '#6ee7b7', 0.9)
} else if (level <= 4) {
canopy =
leaf(24, 24, 8, '#34d399') +
leaf(17, 27, 5.5, '#6ee7b7', 0.9) +
leaf(31, 27, 5.5, '#6ee7b7', 0.9)
} else if (level === 3) {
leaf(24, 28, 7, '#34d399') +
leaf(17, 30, 5, '#6ee7b7', 0.9) +
leaf(31, 30, 5, '#6ee7b7', 0.9)
} else if (level <= 6) {
canopy =
leaf(24, 21, 10, '#22c55e') +
leaf(15, 25, 7, '#4ade80', 0.92) +
leaf(33, 25, 7, '#4ade80', 0.92) +
leaf(24, 15, 6, '#10b981', 0.88)
leaf(24, 24, 9, '#22c55e') +
leaf(14, 26, 7, '#4ade80', 0.92) +
leaf(34, 26, 7, '#4ade80', 0.92) +
leaf(24, 16, 6, '#10b981', 0.88)
} else {
canopy =
leaf(24, 20, 11, '#059669') +
leaf(14, 24, 8, '#34d399', 0.95) +
leaf(34, 24, 8, '#34d399', 0.95) +
leaf(24, 13, 7.5, '#10b981', 0.9) +
blooms
leaf(24, 20, 10, '#059669') +
leaf(13, 24, 8, '#34d399', 0.95) +
leaf(35, 24, 8, '#34d399', 0.95) +
crown
}
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 56" fill="none">
${pot}
${soil}
${trunk}
${canopy}
</svg>`
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 56" fill="none">${pot}${soil}${trunk}${canopy}</svg>`
}
const treeImageSrc = computed(() => svgToDataUrl(buildTreeSvg(clampedLevel.value)))
@@ -110,34 +124,35 @@ const treeImageSrc = computed(() => svgToDataUrl(buildTreeSvg(clampedLevel.value
justify-content: center;
box-sizing: border-box;
}
.size-sm {
width: 64rpx;
height: 72rpx;
}
.size-md {
width: 80rpx;
height: 88rpx;
}
.size-lg {
width: 96rpx;
height: 108rpx;
}
.size-sm { width: 64rpx; height: 72rpx; }
.size-md { width: 80rpx; height: 88rpx; }
.size-lg { width: 112rpx; height: 124rpx; }
.stg-glow {
position: absolute;
inset: 8%;
inset: 6%;
border-radius: 50%;
background: radial-gradient(circle, rgba(14, 165, 164, 0.22) 0%, transparent 68%);
pointer-events: none;
}
.lv-0 .stg-glow {
background: radial-gradient(circle, rgba(148, 163, 184, 0.2) 0%, transparent 70%);
.lv-0 .stg-glow { background: radial-gradient(circle, rgba(148, 163, 184, 0.25) 0%, transparent 70%); }
.tier-4 .stg-glow,
.tier-5 .stg-glow,
.is-max .stg-glow {
background: radial-gradient(circle, rgba(251, 191, 36, 0.3) 0%, rgba(14, 165, 164, 0.15) 55%, transparent 72%);
}
.lv-4 .stg-glow {
background: radial-gradient(circle, rgba(251, 191, 36, 0.28) 0%, rgba(14, 165, 164, 0.12) 55%, transparent 72%);
.stg-aura {
position: absolute;
inset: -8%;
border-radius: 50%;
border: 2rpx solid rgba(253, 224, 71, 0.35);
animation: stg-aura-pulse 2.4s ease-in-out infinite;
pointer-events: none;
}
@keyframes stg-aura-pulse {
0%, 100% { transform: scale(0.92); opacity: 0.5; }
50% { transform: scale(1.05); opacity: 1; }
}
.stg-image {
position: relative;
z-index: 1;
@@ -155,29 +170,14 @@ const treeImageSrc = computed(() => svgToDataUrl(buildTreeSvg(clampedLevel.value
line-height: 1;
font-size: 72rpx;
}
.size-sm .stg-emoji {
font-size: 48rpx;
}
.size-md .stg-emoji {
font-size: 60rpx;
}
.size-lg .stg-emoji {
font-size: 72rpx;
}
.size-sm .stg-emoji { font-size: 48rpx; }
.size-md .stg-emoji { font-size: 60rpx; }
.size-lg .stg-emoji { font-size: 88rpx; }
@keyframes stg-water-bounce {
0%,
100% {
transform: scale(1);
}
35% {
transform: scale(1.08) translateY(-4rpx);
}
60% {
transform: scale(0.96) translateY(2rpx);
}
0%, 100% { transform: scale(1); }
35% { transform: scale(1.1) translateY(-6rpx); }
60% { transform: scale(0.96) translateY(2rpx); }
}
/* 开花态微光点缀 */
.stg-sparkle {
position: absolute;
z-index: 2;
@@ -187,16 +187,13 @@ const treeImageSrc = computed(() => svgToDataUrl(buildTreeSvg(clampedLevel.value
background: #fde68a;
box-shadow: 0 0 6rpx rgba(253, 224, 71, 0.8);
pointer-events: none;
animation: stg-sparkle-twinkle 1.8s ease-in-out infinite;
}
.stg-sparkle-a {
top: 6%;
right: 18%;
}
.stg-sparkle-b {
top: 14%;
left: 12%;
width: 6rpx;
height: 6rpx;
opacity: 0.85;
.stg-sparkle-a { top: 4%; right: 16%; }
.stg-sparkle-b { top: 12%; left: 10%; width: 6rpx; height: 6rpx; animation-delay: 0.4s; }
.stg-sparkle-c { top: 22%; right: 28%; width: 10rpx; height: 10rpx; animation-delay: 0.8s; }
@keyframes stg-sparkle-twinkle {
0%, 100% { opacity: 0.4; transform: scale(0.8); }
50% { opacity: 1; transform: scale(1.2); }
}
</style>