203 lines
5.8 KiB
Vue
203 lines
5.8 KiB
Vue
<template>
|
|
<view class="sugar-tree-graphic" :class="[`lv-${clampedLevel}`, `size-${size}`, { watering: watering }]">
|
|
<view class="stg-glow" />
|
|
<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>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, ref } from 'vue'
|
|
import { svgToDataUrl } from '../utils/svgDataUrl.js'
|
|
|
|
// #ifdef MP-WEIXIN
|
|
const treeUseFallback = ref(true)
|
|
// #endif
|
|
// #ifndef MP-WEIXIN
|
|
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])
|
|
|
|
/** 医疗青绿 + 柔和陶土盆,5 档成长态 */
|
|
function buildTreeSvg(level) {
|
|
const pot = `
|
|
<ellipse cx="24" cy="50" rx="15" ry="3" fill="#0ea5a4" opacity="0.12"/>
|
|
<path d="M11 46h26c1.2 0 2 1 2 2.2v3.8c0 1-.8 1.8-1.8 1.8H10.8c-1 0-1.8-.8-1.8-1.8v-3.8c0-1.2.8-2.2 2-2.2z" fill="#E7E5E4"/>
|
|
<path d="M12.5 46h23c.8 0 1.5.7 1.5 1.5v2.2c0 .6-.5 1.1-1.1 1.1H12.1c-.6 0-1.1-.5-1.1-1.1v-2.2c0-.8.7-1.5 1.5-1.5z" fill="#D6D3D1"/>
|
|
<ellipse cx="24" cy="46.5" rx="9" ry="1.6" fill="#A8A29E" opacity="0.35"/>
|
|
`
|
|
const soil = `<ellipse cx="24" cy="44.5" rx="8" ry="2.2" fill="#0d9488" opacity="0.18"/>`
|
|
|
|
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"/>
|
|
</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 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"/>
|
|
`
|
|
: ''
|
|
|
|
let canopy = ''
|
|
if (level === 1) {
|
|
canopy = leaf(24, 26, 7, '#34d399') + leaf(24, 25, 4.5, '#10b981', 0.85)
|
|
} else if (level === 2) {
|
|
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) {
|
|
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)
|
|
} 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
|
|
}
|
|
|
|
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)))
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.sugar-tree-graphic {
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
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;
|
|
}
|
|
|
|
.stg-glow {
|
|
position: absolute;
|
|
inset: 8%;
|
|
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-4 .stg-glow {
|
|
background: radial-gradient(circle, rgba(251, 191, 36, 0.28) 0%, rgba(14, 165, 164, 0.12) 55%, transparent 72%);
|
|
}
|
|
|
|
.stg-image {
|
|
position: relative;
|
|
z-index: 1;
|
|
width: 100%;
|
|
height: 100%;
|
|
transition: transform 0.35s ease;
|
|
}
|
|
.sugar-tree-graphic.watering .stg-image,
|
|
.sugar-tree-graphic.watering .stg-emoji {
|
|
animation: stg-water-bounce 0.65s ease;
|
|
}
|
|
.stg-emoji {
|
|
position: relative;
|
|
z-index: 1;
|
|
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;
|
|
}
|
|
@keyframes stg-water-bounce {
|
|
0%,
|
|
100% {
|
|
transform: scale(1);
|
|
}
|
|
35% {
|
|
transform: scale(1.08) translateY(-4rpx);
|
|
}
|
|
60% {
|
|
transform: scale(0.96) translateY(2rpx);
|
|
}
|
|
}
|
|
|
|
/* 开花态微光点缀 */
|
|
.stg-sparkle {
|
|
position: absolute;
|
|
z-index: 2;
|
|
width: 8rpx;
|
|
height: 8rpx;
|
|
border-radius: 50%;
|
|
background: #fde68a;
|
|
box-shadow: 0 0 6rpx rgba(253, 224, 71, 0.8);
|
|
pointer-events: none;
|
|
}
|
|
.stg-sparkle-a {
|
|
top: 6%;
|
|
right: 18%;
|
|
}
|
|
.stg-sparkle-b {
|
|
top: 14%;
|
|
left: 12%;
|
|
width: 6rpx;
|
|
height: 6rpx;
|
|
opacity: 0.85;
|
|
}
|
|
</style>
|