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>
@@ -27,13 +27,7 @@ const props = defineProps({
color: { type: String, default: '#0891B2' }
})
/** 微信小程序 image 对 data:svg 支持不稳定,默认用文字图标保证可见 */
// #ifdef MP-WEIXIN
const useFallback = ref(true)
// #endif
// #ifndef MP-WEIXIN
const useFallback = ref(false)
// #endif
/** Lucide 风格描边路径 */
const ICON_PATHS = {
@@ -51,7 +45,16 @@ const ICON_PATHS = {
droplet: '<path d="M12 22a7 7 0 0 0 7-7c0-2-1-3.5-2.5-5.5C15 7 12 2 12 2S9 7 7.5 9.5 5 13 5 15a7 7 0 0 0 7 7z"/>',
sparkles: '<path d="m12 3-1.9 5.8L4 12l5.8 1.9L12 21l1.9-5.8L20 12l-5.8-1.9L12 3Z"/><path d="M5 3v4M19 17v4M3 5h4M17 19h4"/>',
trophy: '<path d="M6 9H4.5a2.5 2.5 0 0 1 0-5H6M18 9h1.5a2.5 2.5 0 0 0 0-5H18M4 22h16M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20 7 22M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20 17 22M18 2H6v7a6 6 0 0 0 12 0V2Z"/>',
share: '<path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8M16 6l-4-4-4 4M12 2v13"/>'
share: '<path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8M16 6l-4-4-4 4M12 2v13"/>',
plus: '<path d="M5 12h14M12 5v14"/>',
refresh: '<path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8M3 3v5h5M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16M21 21v-5h-5"/>',
volume: '<path d="M11 5 6 9H2v6h4l5 4V5zM15.54 8.46a5 5 0 0 1 0 7.07M19.07 4.93a10 10 0 0 1 0 14.14"/>',
pause: '<rect width="4" height="16" x="14" y="4" rx="1"/><rect width="4" height="16" x="6" y="4" rx="1"/>',
play: '<polygon points="6 3 20 12 6 21 6 3"/>',
info: '<circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/>',
sun: '<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41-1.41M17.66 6.34l1.41-1.41M6.34 4.93l1.41 1.41"/>',
moon: '<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/>',
sunset: '<path d="M12 10V2M18.364 5.636l-2.12 2.12M5.636 18.364l2.12-2.12M22 18h-3M5 18H2M18.364 18.364l-2.12-2.12M5.636 5.636l2.12 2.12M16 12a4 4 0 1 1-8 0 4 4 0 0 1 8 0Z"/>'
}
/** 图片加载失败时的 emoji 回退 */
@@ -70,7 +73,16 @@ const ICON_FALLBACK = {
droplet: '💧',
sparkles: '✨',
trophy: '🏆',
share: '↗'
share: '↗',
plus: '',
refresh: '↻',
volume: '🔊',
pause: '⏸',
play: '▶',
info: '!',
sun: '☀',
moon: '🌙',
sunset: '☀'
}
const strokeColor = computed(() => {