更新
This commit is contained in:
@@ -1,16 +1,24 @@
|
|||||||
<template>
|
<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 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" />
|
<image v-if="!treeUseFallback" class="stg-image" :src="treeImageSrc" mode="aspectFit" @error="treeUseFallback = true" />
|
||||||
<text v-else class="stg-emoji">{{ treeEmoji }}</text>
|
<text v-else class="stg-emoji">{{ treeEmoji }}</text>
|
||||||
<view v-if="clampedLevel >= 4" class="stg-sparkle stg-sparkle-a" />
|
<view v-if="clampedLevel >= 7" class="stg-sparkle stg-sparkle-a" />
|
||||||
<view v-if="clampedLevel >= 4" class="stg-sparkle stg-sparkle-b" />
|
<view v-if="clampedLevel >= 8" class="stg-sparkle stg-sparkle-b" />
|
||||||
|
<view v-if="clampedLevel >= MAX_LEVEL" class="stg-sparkle stg-sparkle-c" />
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { svgToDataUrl } from '../utils/svgDataUrl.js'
|
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
|
// #ifdef MP-WEIXIN
|
||||||
const treeUseFallback = ref(true)
|
const treeUseFallback = ref(true)
|
||||||
@@ -19,18 +27,24 @@ const treeUseFallback = ref(true)
|
|||||||
const treeUseFallback = ref(false)
|
const treeUseFallback = ref(false)
|
||||||
// #endif
|
// #endif
|
||||||
|
|
||||||
const TREE_EMOJI = ['🌱', '🌿', '🌳', '🌸', '🌺']
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
level: { type: Number, default: 0 },
|
level: { type: Number, default: 0 },
|
||||||
size: { type: String, default: 'md' },
|
size: { type: String, default: 'md' },
|
||||||
watering: { type: Boolean, default: false }
|
watering: { type: Boolean, default: false }
|
||||||
})
|
})
|
||||||
|
|
||||||
const clampedLevel = computed(() => Math.min(4, Math.max(0, Number(props.level) || 0)))
|
const clampedLevel = computed(() => Math.min(MAX_LEVEL, Math.max(0, Number(props.level) || 0)))
|
||||||
const treeEmoji = computed(() => TREE_EMOJI[clampedLevel.value] || TREE_EMOJI[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) {
|
function buildTreeSvg(level) {
|
||||||
const pot = `
|
const pot = `
|
||||||
<ellipse cx="24" cy="50" rx="15" ry="3" fill="#0ea5a4" opacity="0.12"/>
|
<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"/>`
|
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">
|
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 56" fill="none">
|
||||||
${pot}
|
${pot}${soil}
|
||||||
${soil}
|
<circle cx="24" cy="41.5" r="2.2" fill="#a8a29e" opacity="0.7"/>
|
||||||
<circle cx="24" cy="41.5" r="2" fill="#6ee7b7" opacity="0.55"/>
|
<path d="M24 41.5v3.5" stroke="#78716c" stroke-width="1" stroke-linecap="round"/>
|
||||||
<path d="M24 41.5v4" stroke="#14b8a6" stroke-width="1.2" stroke-linecap="round" opacity="0.7"/>
|
|
||||||
</svg>`
|
</svg>`
|
||||||
}
|
}
|
||||||
|
|
||||||
const trunk = `<rect x="22.2" y="30" width="3.6" height="14" rx="1.8" fill="#78716C"/>
|
const trunkH = level >= 6 ? 16 : level >= 3 ? 14 : 10
|
||||||
<rect x="22.6" y="30" width="2.8" height="14" rx="1.4" fill="#A8A29E" opacity="0.35"/>`
|
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) =>
|
const leaf = (cx, cy, r, fill, opacity = 1) =>
|
||||||
`<circle cx="${cx}" cy="${cy}" r="${r}" fill="${fill}" opacity="${opacity}"/>
|
`<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"/>`
|
<circle cx="${cx - r * 0.25}" cy="${cy - r * 0.2}" r="${r * 0.35}" fill="#ecfdf5" opacity="0.55"/>`
|
||||||
|
|
||||||
const blooms =
|
const bloom =
|
||||||
level >= 4
|
level >= 7
|
||||||
? `
|
? leaf(17, 18, 3, '#fda4af', 0.95) +
|
||||||
${leaf(17, 19, 3.2, '#fda4af', 0.95)}
|
leaf(31, 17, 2.8, '#f9a8d4', 0.9) +
|
||||||
${leaf(30, 17, 3, '#f9a8d4', 0.9)}
|
leaf(24, 13, 3.2, '#fb7185', 0.95) +
|
||||||
${leaf(24, 14, 3.4, '#fb7185', 0.95)}
|
`<circle cx="24" cy="12" r="1.3" fill="#fef3c7"/>`
|
||||||
<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 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 = ''
|
let canopy = ''
|
||||||
if (level === 1) {
|
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) {
|
} else if (level === 2) {
|
||||||
|
canopy = leaf(24, 32, 5.5, '#34d399') + leaf(20, 34, 3.5, '#6ee7b7', 0.9)
|
||||||
|
} else if (level <= 4) {
|
||||||
canopy =
|
canopy =
|
||||||
leaf(24, 24, 8, '#34d399') +
|
leaf(24, 28, 7, '#34d399') +
|
||||||
leaf(17, 27, 5.5, '#6ee7b7', 0.9) +
|
leaf(17, 30, 5, '#6ee7b7', 0.9) +
|
||||||
leaf(31, 27, 5.5, '#6ee7b7', 0.9)
|
leaf(31, 30, 5, '#6ee7b7', 0.9)
|
||||||
} else if (level === 3) {
|
} else if (level <= 6) {
|
||||||
canopy =
|
canopy =
|
||||||
leaf(24, 21, 10, '#22c55e') +
|
leaf(24, 24, 9, '#22c55e') +
|
||||||
leaf(15, 25, 7, '#4ade80', 0.92) +
|
leaf(14, 26, 7, '#4ade80', 0.92) +
|
||||||
leaf(33, 25, 7, '#4ade80', 0.92) +
|
leaf(34, 26, 7, '#4ade80', 0.92) +
|
||||||
leaf(24, 15, 6, '#10b981', 0.88)
|
leaf(24, 16, 6, '#10b981', 0.88)
|
||||||
} else {
|
} else {
|
||||||
canopy =
|
canopy =
|
||||||
leaf(24, 20, 11, '#059669') +
|
leaf(24, 20, 10, '#059669') +
|
||||||
leaf(14, 24, 8, '#34d399', 0.95) +
|
leaf(13, 24, 8, '#34d399', 0.95) +
|
||||||
leaf(34, 24, 8, '#34d399', 0.95) +
|
leaf(35, 24, 8, '#34d399', 0.95) +
|
||||||
leaf(24, 13, 7.5, '#10b981', 0.9) +
|
crown
|
||||||
blooms
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 56" fill="none">
|
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 56" fill="none">${pot}${soil}${trunk}${canopy}</svg>`
|
||||||
${pot}
|
|
||||||
${soil}
|
|
||||||
${trunk}
|
|
||||||
${canopy}
|
|
||||||
</svg>`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const treeImageSrc = computed(() => svgToDataUrl(buildTreeSvg(clampedLevel.value)))
|
const treeImageSrc = computed(() => svgToDataUrl(buildTreeSvg(clampedLevel.value)))
|
||||||
@@ -110,34 +124,35 @@ const treeImageSrc = computed(() => svgToDataUrl(buildTreeSvg(clampedLevel.value
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
.size-sm { width: 64rpx; height: 72rpx; }
|
||||||
.size-sm {
|
.size-md { width: 80rpx; height: 88rpx; }
|
||||||
width: 64rpx;
|
.size-lg { width: 112rpx; height: 124rpx; }
|
||||||
height: 72rpx;
|
|
||||||
}
|
|
||||||
.size-md {
|
|
||||||
width: 80rpx;
|
|
||||||
height: 88rpx;
|
|
||||||
}
|
|
||||||
.size-lg {
|
|
||||||
width: 96rpx;
|
|
||||||
height: 108rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stg-glow {
|
.stg-glow {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 8%;
|
inset: 6%;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: radial-gradient(circle, rgba(14, 165, 164, 0.22) 0%, transparent 68%);
|
background: radial-gradient(circle, rgba(14, 165, 164, 0.22) 0%, transparent 68%);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
.lv-0 .stg-glow {
|
.lv-0 .stg-glow { background: radial-gradient(circle, rgba(148, 163, 184, 0.25) 0%, transparent 70%); }
|
||||||
background: radial-gradient(circle, rgba(148, 163, 184, 0.2) 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 {
|
.stg-aura {
|
||||||
background: radial-gradient(circle, rgba(251, 191, 36, 0.28) 0%, rgba(14, 165, 164, 0.12) 55%, transparent 72%);
|
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 {
|
.stg-image {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
@@ -155,29 +170,14 @@ const treeImageSrc = computed(() => svgToDataUrl(buildTreeSvg(clampedLevel.value
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
font-size: 72rpx;
|
font-size: 72rpx;
|
||||||
}
|
}
|
||||||
.size-sm .stg-emoji {
|
.size-sm .stg-emoji { font-size: 48rpx; }
|
||||||
font-size: 48rpx;
|
.size-md .stg-emoji { font-size: 60rpx; }
|
||||||
}
|
.size-lg .stg-emoji { font-size: 88rpx; }
|
||||||
.size-md .stg-emoji {
|
|
||||||
font-size: 60rpx;
|
|
||||||
}
|
|
||||||
.size-lg .stg-emoji {
|
|
||||||
font-size: 72rpx;
|
|
||||||
}
|
|
||||||
@keyframes stg-water-bounce {
|
@keyframes stg-water-bounce {
|
||||||
0%,
|
0%, 100% { transform: scale(1); }
|
||||||
100% {
|
35% { transform: scale(1.1) translateY(-6rpx); }
|
||||||
transform: scale(1);
|
60% { transform: scale(0.96) translateY(2rpx); }
|
||||||
}
|
|
||||||
35% {
|
|
||||||
transform: scale(1.08) translateY(-4rpx);
|
|
||||||
}
|
|
||||||
60% {
|
|
||||||
transform: scale(0.96) translateY(2rpx);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 开花态微光点缀 */
|
|
||||||
.stg-sparkle {
|
.stg-sparkle {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
@@ -187,16 +187,13 @@ const treeImageSrc = computed(() => svgToDataUrl(buildTreeSvg(clampedLevel.value
|
|||||||
background: #fde68a;
|
background: #fde68a;
|
||||||
box-shadow: 0 0 6rpx rgba(253, 224, 71, 0.8);
|
box-shadow: 0 0 6rpx rgba(253, 224, 71, 0.8);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
animation: stg-sparkle-twinkle 1.8s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
.stg-sparkle-a {
|
.stg-sparkle-a { top: 4%; right: 16%; }
|
||||||
top: 6%;
|
.stg-sparkle-b { top: 12%; left: 10%; width: 6rpx; height: 6rpx; animation-delay: 0.4s; }
|
||||||
right: 18%;
|
.stg-sparkle-c { top: 22%; right: 28%; width: 10rpx; height: 10rpx; animation-delay: 0.8s; }
|
||||||
}
|
@keyframes stg-sparkle-twinkle {
|
||||||
.stg-sparkle-b {
|
0%, 100% { opacity: 0.4; transform: scale(0.8); }
|
||||||
top: 14%;
|
50% { opacity: 1; transform: scale(1.2); }
|
||||||
left: 12%;
|
|
||||||
width: 6rpx;
|
|
||||||
height: 6rpx;
|
|
||||||
opacity: 0.85;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -27,13 +27,7 @@ const props = defineProps({
|
|||||||
color: { type: String, default: '#0891B2' }
|
color: { type: String, default: '#0891B2' }
|
||||||
})
|
})
|
||||||
|
|
||||||
/** 微信小程序 image 对 data:svg 支持不稳定,默认用文字图标保证可见 */
|
|
||||||
// #ifdef MP-WEIXIN
|
|
||||||
const useFallback = ref(true)
|
|
||||||
// #endif
|
|
||||||
// #ifndef MP-WEIXIN
|
|
||||||
const useFallback = ref(false)
|
const useFallback = ref(false)
|
||||||
// #endif
|
|
||||||
|
|
||||||
/** Lucide 风格描边路径 */
|
/** Lucide 风格描边路径 */
|
||||||
const ICON_PATHS = {
|
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"/>',
|
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"/>',
|
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"/>',
|
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 回退 */
|
/** 图片加载失败时的 emoji 回退 */
|
||||||
@@ -70,7 +73,16 @@ const ICON_FALLBACK = {
|
|||||||
droplet: '💧',
|
droplet: '💧',
|
||||||
sparkles: '✨',
|
sparkles: '✨',
|
||||||
trophy: '🏆',
|
trophy: '🏆',
|
||||||
share: '↗'
|
share: '↗',
|
||||||
|
plus: '+',
|
||||||
|
refresh: '↻',
|
||||||
|
volume: '🔊',
|
||||||
|
pause: '⏸',
|
||||||
|
play: '▶',
|
||||||
|
info: '!',
|
||||||
|
sun: '☀',
|
||||||
|
moon: '🌙',
|
||||||
|
sunset: '☀'
|
||||||
}
|
}
|
||||||
|
|
||||||
const strokeColor = computed(() => {
|
const strokeColor = computed(() => {
|
||||||
|
|||||||
@@ -224,8 +224,10 @@
|
|||||||
<view class="hero-row">
|
<view class="hero-row">
|
||||||
<view class="hero-main">
|
<view class="hero-main">
|
||||||
<view class="hero-greet-row">
|
<view class="hero-greet-row">
|
||||||
<text class="hero-greet-emoji">{{ timeGreetingEmoji }}</text>
|
<view class="hero-greet-icon-wrap">
|
||||||
<text class="hero-greet-text">{{ timeGreeting }}</text>
|
<TongjiIcon :name="timeGreetingMeta.icon" size="md" color="#FFFFFF" />
|
||||||
|
</view>
|
||||||
|
<text class="hero-greet-text">{{ timeGreetingMeta.text }}</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="hero-title">{{ patientName || '健康档案' }}</text>
|
<text class="hero-title">{{ patientName || '健康档案' }}</text>
|
||||||
<view class="hero-subtitle-row">
|
<view class="hero-subtitle-row">
|
||||||
@@ -236,14 +238,18 @@
|
|||||||
class="hero-status-chip"
|
class="hero-status-chip"
|
||||||
:class="todayChecked ? 'done' : 'pending'"
|
:class="todayChecked ? 'done' : 'pending'"
|
||||||
>
|
>
|
||||||
<text class="hero-status-icon">{{ todayChecked ? '✓' : '!' }}</text>
|
<view class="hero-status-icon">
|
||||||
|
<TongjiIcon :name="todayChecked ? 'check-circle' : 'info'" size="sm" color="#ffffff" />
|
||||||
|
</view>
|
||||||
<text class="hero-status-text">{{ todayChecked ? '今日已记录' : '今日待记录' }}</text>
|
<text class="hero-status-text">{{ todayChecked ? '今日已记录' : '今日待记录' }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="hero-pill-group">
|
<view class="hero-pill-group">
|
||||||
<view class="hero-pill input" @click="openInputForm()">
|
<view class="hero-pill input" @click="openInputForm()">
|
||||||
<text class="hero-pill-icon">+</text>
|
<view class="hero-pill-icon-wrap">
|
||||||
|
<TongjiIcon name="plus" size="sm" color="#0d9488" />
|
||||||
|
</view>
|
||||||
<text class="hero-pill-text">录入今日</text>
|
<text class="hero-pill-text">录入今日</text>
|
||||||
</view>
|
</view>
|
||||||
<view
|
<view
|
||||||
@@ -251,11 +257,15 @@
|
|||||||
:class="{ speaking: ttsSpeaking, disabled: !ttsEnabled }"
|
:class="{ speaking: ttsSpeaking, disabled: !ttsEnabled }"
|
||||||
@click="ttsSpeaking ? ttsStop() : speakLatestSummary()"
|
@click="ttsSpeaking ? ttsStop() : speakLatestSummary()"
|
||||||
>
|
>
|
||||||
<text class="hero-pill-icon">{{ ttsSpeaking ? '⏸' : '🔊' }}</text>
|
<view class="hero-pill-icon-wrap">
|
||||||
|
<TongjiIcon :name="ttsSpeaking ? 'pause' : 'volume'" size="sm" :color="ttsSpeaking ? '#0f172a' : '#ffffff'" />
|
||||||
|
</view>
|
||||||
<text class="hero-pill-text">{{ ttsSpeaking ? '停止' : '朗读' }}</text>
|
<text class="hero-pill-text">{{ ttsSpeaking ? '停止' : '朗读' }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="hero-pill" @click="onRefresh">
|
<view class="hero-pill" @click="onRefresh">
|
||||||
<text class="hero-pill-icon">↻</text>
|
<view class="hero-pill-icon-wrap">
|
||||||
|
<TongjiIcon name="refresh" size="sm" color="#ffffff" />
|
||||||
|
</view>
|
||||||
<text class="hero-pill-text">刷新</text>
|
<text class="hero-pill-text">刷新</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -508,24 +518,42 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 控糖树成长区 -->
|
<!-- 控糖树成长区(10 级成长 + 路线图)-->
|
||||||
<view class="tree-growth-zone">
|
<view class="tree-growth-zone">
|
||||||
<view class="tree-graphic-wrap tree-tappable" @click="onTreeWhisper">
|
<view class="tree-graphic-wrap tree-tappable" @click="onTreeWhisper">
|
||||||
|
<view class="tree-lv-badge">Lv.{{ treeLevel }}</view>
|
||||||
|
<view class="tree-mood-pill">
|
||||||
|
<text class="tree-mood-emoji">{{ treeEmoji }}</text>
|
||||||
|
<text class="tree-mood-text">{{ treeMood }}</text>
|
||||||
|
</view>
|
||||||
<SugarTreeGraphic :level="treeLevel" size="lg" :watering="treeWatering" />
|
<SugarTreeGraphic :level="treeLevel" size="lg" :watering="treeWatering" />
|
||||||
<view v-if="floatReward" :key="floatReward.key" class="float-reward">
|
<view v-if="floatReward" :key="floatReward.key" class="float-reward">
|
||||||
<text>+{{ floatReward.text }}</text>
|
<text>+{{ floatReward.text }}</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="tree-tap-hint">点我聊聊天</text>
|
<text class="tree-tap-hint">点树听鼓励 ›</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="tree-progress-wrap">
|
<view class="tree-progress-wrap">
|
||||||
<view class="tree-level-info">
|
<view class="tree-level-info">
|
||||||
<text class="tree-level-tag">Lv.{{ treeLevel }} {{ treeLevelName }}</text>
|
<text class="tree-level-tag">{{ treeLevelName }}</text>
|
||||||
<text class="tree-progress-text" v-if="treeLevel < 4">{{ treeProgress }}/100 XP</text>
|
<text class="tree-progress-text">{{ treeProgressLabel }}</text>
|
||||||
<text class="tree-progress-text" v-else>已满级</text>
|
|
||||||
</view>
|
</view>
|
||||||
<view class="tree-progress-track">
|
<view class="tree-progress-track">
|
||||||
<view class="tree-progress-bar" :style="{ width: treeProgress + '%' }" />
|
<view class="tree-progress-bar" :style="{ width: treeProgress + '%' }" />
|
||||||
</view>
|
</view>
|
||||||
|
<text v-if="treeNextHint" class="tree-next-hint">{{ treeNextHint }}</text>
|
||||||
|
<scroll-view scroll-x class="tree-roadmap-scroll" :show-scrollbar="false">
|
||||||
|
<view class="tree-roadmap-row">
|
||||||
|
<view
|
||||||
|
v-for="node in TREE_LEVEL_ROADMAP"
|
||||||
|
:key="node.level"
|
||||||
|
class="tree-roadmap-node"
|
||||||
|
:class="{ done: node.level < treeLevel, current: node.level === treeLevel, locked: node.level > treeLevel }"
|
||||||
|
>
|
||||||
|
<text class="tree-roadmap-emoji">{{ node.emoji }}</text>
|
||||||
|
<text class="tree-roadmap-lv">Lv{{ node.level }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
<view class="tree-water-action">
|
<view class="tree-water-action">
|
||||||
<button class="water-btn" :class="{ disabled: treeWatering, pulse: claimablePoints > 0 }" @click="waterTree">
|
<button class="water-btn" :class="{ disabled: treeWatering, pulse: claimablePoints > 0 }" @click="waterTree">
|
||||||
<TongjiIcon name="droplet" size="sm" color="#FFFFFF" />
|
<TongjiIcon name="droplet" size="sm" color="#FFFFFF" />
|
||||||
@@ -1313,6 +1341,7 @@ import { onLoad, onShow, onHide, onUnload, onPullDownRefresh, onShareAppMessage
|
|||||||
import SugarTreeGraphic from '../components/SugarTreeGraphic.vue'
|
import SugarTreeGraphic from '../components/SugarTreeGraphic.vue'
|
||||||
import TongjiIcon from '../components/TongjiIcon.vue'
|
import TongjiIcon from '../components/TongjiIcon.vue'
|
||||||
import CelebrateBurst from '../components/CelebrateBurst.vue'
|
import CelebrateBurst from '../components/CelebrateBurst.vue'
|
||||||
|
import { TREE_LEVELS, TREE_MAX_LEVEL, calcTreeFromPoints, pickTreeWhisper } from '../utils/treeLevels.js'
|
||||||
|
|
||||||
const { proxy } = getCurrentInstance()
|
const { proxy } = getCurrentInstance()
|
||||||
|
|
||||||
@@ -1776,9 +1805,6 @@ async function applyCard(card) {
|
|||||||
summary_line: '分享战报给家人,邀请他们为您点赞鼓劲',
|
summary_line: '分享战报给家人,邀请他们为您点赞鼓劲',
|
||||||
recent: []
|
recent: []
|
||||||
}
|
}
|
||||||
gamifyTreeLevel.value = null
|
|
||||||
gamifyTreeProgress.value = null
|
|
||||||
gamifyTreeLevelName.value = ''
|
|
||||||
gamifyStatusTip.value = ''
|
gamifyStatusTip.value = ''
|
||||||
await fetchGamifyState()
|
await fetchGamifyState()
|
||||||
}
|
}
|
||||||
@@ -1875,25 +1901,18 @@ function onRefresh() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============ 时段问候 / 打卡 / 健康日历 ============
|
// ============ 时段问候 / 打卡 / 健康日历 ============
|
||||||
const timeGreeting = computed(() => {
|
/** 时段问候:文案 + 统一 TongjiIcon(与全页图标体系一致)*/
|
||||||
const h = new Date().getHours()
|
function buildTimeGreetingMeta(hour = new Date().getHours()) {
|
||||||
if (h < 5) return '夜深了'
|
const h = Number(hour) || 0
|
||||||
if (h < 11) return '早上好'
|
if (h < 5) return { text: '夜深了', icon: 'moon' }
|
||||||
if (h < 13) return '中午好'
|
if (h < 11) return { text: '早上好', icon: 'sun' }
|
||||||
if (h < 18) return '下午好'
|
if (h < 13) return { text: '中午好', icon: 'sun' }
|
||||||
if (h < 22) return '晚上好'
|
if (h < 18) return { text: '下午好', icon: 'sun' }
|
||||||
return '夜深了'
|
if (h < 22) return { text: '晚上好', icon: 'sunset' }
|
||||||
})
|
return { text: '夜深了', icon: 'moon' }
|
||||||
|
}
|
||||||
|
|
||||||
const timeGreetingEmoji = computed(() => {
|
const timeGreetingMeta = computed(() => buildTimeGreetingMeta())
|
||||||
const h = new Date().getHours()
|
|
||||||
if (h < 5) return '🌙'
|
|
||||||
if (h < 11) return '☀️'
|
|
||||||
if (h < 13) return '🌤'
|
|
||||||
if (h < 18) return '🌅'
|
|
||||||
if (h < 22) return '🌆'
|
|
||||||
return '🌙'
|
|
||||||
})
|
|
||||||
|
|
||||||
// 复用已有的 formatDate(date) -> 'YYYY-MM-DD'
|
// 复用已有的 formatDate(date) -> 'YYYY-MM-DD'
|
||||||
|
|
||||||
@@ -3163,10 +3182,8 @@ const gamifyBadges = ref([])
|
|||||||
const gamifyTaskAwards = ref({})
|
const gamifyTaskAwards = ref({})
|
||||||
const gamifyTodayTasks = ref([])
|
const gamifyTodayTasks = ref([])
|
||||||
const gamifyClaimablePoints = ref(0)
|
const gamifyClaimablePoints = ref(0)
|
||||||
const gamifyTreeLevel = ref(null)
|
|
||||||
const gamifyTreeProgress = ref(null)
|
|
||||||
const gamifyTreeLevelName = ref('')
|
|
||||||
const gamifyStatusTip = ref('')
|
const gamifyStatusTip = ref('')
|
||||||
|
const TREE_LEVEL_ROADMAP = TREE_LEVELS
|
||||||
const treeWatering = ref(false)
|
const treeWatering = ref(false)
|
||||||
|
|
||||||
function isApiSuccess(res) {
|
function isApiSuccess(res) {
|
||||||
@@ -3192,15 +3209,6 @@ function applyGamifyPayload(data) {
|
|||||||
}))
|
}))
|
||||||
: []
|
: []
|
||||||
gamifyClaimablePoints.value = Number(data.claimable_points) || 0
|
gamifyClaimablePoints.value = Number(data.claimable_points) || 0
|
||||||
if (data.tree_level != null && data.tree_level !== '') {
|
|
||||||
gamifyTreeLevel.value = Math.min(4, Math.max(0, Number(data.tree_level) || 0))
|
|
||||||
}
|
|
||||||
if (data.tree_progress != null && data.tree_progress !== '') {
|
|
||||||
gamifyTreeProgress.value = Math.min(100, Math.max(0, Number(data.tree_progress) || 0))
|
|
||||||
}
|
|
||||||
if (data.tree_level_name) {
|
|
||||||
gamifyTreeLevelName.value = String(data.tree_level_name)
|
|
||||||
}
|
|
||||||
if (data.message) {
|
if (data.message) {
|
||||||
gamifyStatusTip.value = String(data.message)
|
gamifyStatusTip.value = String(data.message)
|
||||||
}
|
}
|
||||||
@@ -3300,8 +3308,20 @@ const unlockedBadges = computed(() => {
|
|||||||
if (gamifyPoints.value >= 100) {
|
if (gamifyPoints.value >= 100) {
|
||||||
list.push({ id: 'p100', label: '积分破百', icon: 'trophy', color: '#7C3AED' })
|
list.push({ id: 'p100', label: '积分破百', icon: 'trophy', color: '#7C3AED' })
|
||||||
}
|
}
|
||||||
|
if (gamifyPoints.value >= 200) {
|
||||||
|
list.push({ id: 'p200', label: '积分200+', icon: 'sparkles', color: '#7C3AED' })
|
||||||
|
}
|
||||||
if (treeLevel.value >= 3) {
|
if (treeLevel.value >= 3) {
|
||||||
list.push({ id: 'tree', label: treeLevelName.value, icon: 'glucose', color: '#0D9488' })
|
list.push({ id: 'tree3', label: '小树苗', icon: 'glucose', color: '#0D9488' })
|
||||||
|
}
|
||||||
|
if (treeLevel.value >= 5) {
|
||||||
|
list.push({ id: 'tree5', label: '拔节高', icon: 'activity', color: '#059669' })
|
||||||
|
}
|
||||||
|
if (treeLevel.value >= 7) {
|
||||||
|
list.push({ id: 'tree7', label: '初绽香', icon: 'heart', color: '#EA580C' })
|
||||||
|
}
|
||||||
|
if (treeState.value.isMax) {
|
||||||
|
list.push({ id: 'tree9', label: '圆满树', icon: 'trophy', color: '#CA8A04' })
|
||||||
}
|
}
|
||||||
return list
|
return list
|
||||||
})
|
})
|
||||||
@@ -3315,14 +3335,6 @@ const taskJustDoneId = ref('')
|
|||||||
let celebrateTimer = null
|
let celebrateTimer = null
|
||||||
let taskPopTimer = null
|
let taskPopTimer = null
|
||||||
|
|
||||||
const TREE_WHISPERS = [
|
|
||||||
'再浇一点水,我就长高啦。',
|
|
||||||
'您今天真棒,小树都开心得晃叶子。',
|
|
||||||
'坚持记录,我会开出稳糖花。',
|
|
||||||
'空腹餐后都记全,我会长得更快。',
|
|
||||||
'家人点赞的时候,我也会发光。'
|
|
||||||
]
|
|
||||||
|
|
||||||
const INVITE_TREE_WHISPERS = [
|
const INVITE_TREE_WHISPERS = [
|
||||||
'Ta 正在努力控糖,您的鼓励很重要。',
|
'Ta 正在努力控糖,您的鼓励很重要。',
|
||||||
'好习惯比完美数值更珍贵,一起加油。',
|
'好习惯比完美数值更珍贵,一起加油。',
|
||||||
@@ -3365,7 +3377,7 @@ function popTaskDone(taskId) {
|
|||||||
|
|
||||||
function onTreeWhisper() {
|
function onTreeWhisper() {
|
||||||
if (treeWatering.value) return
|
if (treeWatering.value) return
|
||||||
const line = TREE_WHISPERS[Math.floor(Math.random() * TREE_WHISPERS.length)]
|
const line = pickTreeWhisper(treeLevel.value)
|
||||||
showUserToast(line, { duration: 2800 })
|
showUserToast(line, { duration: 2800 })
|
||||||
hapticLight()
|
hapticLight()
|
||||||
}
|
}
|
||||||
@@ -3411,11 +3423,11 @@ async function waterTree() {
|
|||||||
triggerCelebrate('浇水成功', tip || `+${addedPoints} 稳糖积分,小树咕咚喝饱啦`)
|
triggerCelebrate('浇水成功', tip || `+${addedPoints} 稳糖积分,小树咕咚喝饱啦`)
|
||||||
const claimedIds = Array.isArray(data.claimed_tasks) ? data.claimed_tasks : []
|
const claimedIds = Array.isArray(data.claimed_tasks) ? data.claimed_tasks : []
|
||||||
claimedIds.forEach((id) => popTaskDone(id))
|
claimedIds.forEach((id) => popTaskDone(id))
|
||||||
const newLevel = treeLevel.value
|
const newLevel = treeLevel.value
|
||||||
if (newLevel > oldLevel) {
|
if (newLevel > oldLevel) {
|
||||||
const names = ['嫩芽期', '成长雏形', '枝繁叶茂', '含苞待放', '花开稳糖']
|
const meta = TREE_LEVELS[newLevel] || TREE_LEVELS[0]
|
||||||
setTimeout(() => triggerCelebrate('小树升级啦', `恭喜进入 ${names[newLevel] || treeLevelName.value}`), 520)
|
setTimeout(() => triggerCelebrate('小树升级啦', `Lv.${newLevel} ${meta.name} · ${meta.desc}`), 520)
|
||||||
}
|
}
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (todayTasks.value.every((t) => t.completed && t.claimed)) {
|
if (todayTasks.value.every((t) => t.completed && t.claimed)) {
|
||||||
triggerCelebrate('今日任务大满贯', '明天继续来浇浇水吧')
|
triggerCelebrate('今日任务大满贯', '明天继续来浇浇水吧')
|
||||||
@@ -3460,25 +3472,20 @@ async function waterTree() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const treeLevel = computed(() => {
|
const treeState = computed(() => calcTreeFromPoints(gamifyPoints.value))
|
||||||
if (gamifyTreeLevel.value != null) {
|
const treeLevel = computed(() => treeState.value.level)
|
||||||
return Math.min(4, Math.max(0, gamifyTreeLevel.value))
|
const treeProgress = computed(() => treeState.value.progress)
|
||||||
}
|
const treeLevelName = computed(() => treeState.value.name)
|
||||||
return Math.min(4, Math.floor(gamifyPoints.value / 100))
|
const treeEmoji = computed(() => treeState.value.emoji)
|
||||||
|
const treeMood = computed(() => treeState.value.mood)
|
||||||
|
const treeProgressLabel = computed(() => {
|
||||||
|
if (treeState.value.isMax) return '已满级 · 圆满树'
|
||||||
|
return `${treeState.value.xpInLevel}/${treeState.value.xpNeed} XP`
|
||||||
})
|
})
|
||||||
|
const treeNextHint = computed(() => {
|
||||||
const treeProgress = computed(() => {
|
if (treeState.value.isMax) return '您已养成满级稳糖树,习惯就是最好的奖励'
|
||||||
if (treeLevel.value >= 4) return 100
|
if (claimablePoints.value > 0) return `浇水后可再积 ${claimablePoints.value} 分,离升级更近一步`
|
||||||
if (gamifyTreeProgress.value != null) {
|
return `再积 ${treeState.value.pointsToNext} 分,升级「${treeState.value.nextName}」`
|
||||||
return Math.min(100, Math.max(0, gamifyTreeProgress.value))
|
|
||||||
}
|
|
||||||
return gamifyPoints.value % 100
|
|
||||||
})
|
|
||||||
|
|
||||||
const treeLevelName = computed(() => {
|
|
||||||
if (gamifyTreeLevelName.value) return gamifyTreeLevelName.value
|
|
||||||
const names = ['嫩芽期', '成长雏形', '枝繁叶茂', '含苞待放', '花开稳糖']
|
|
||||||
return names[treeLevel.value] || '嫩芽期'
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// ============ 分享 / 家人点赞 / 邀请观看 ============
|
// ============ 分享 / 家人点赞 / 邀请观看 ============
|
||||||
@@ -3969,7 +3976,7 @@ async function onFamilyLike() {
|
|||||||
border-color: #fde68a;
|
border-color: #fde68a;
|
||||||
color: var(--slate-900);
|
color: var(--slate-900);
|
||||||
animation: voice-pulse 1.4s ease-in-out infinite;
|
animation: voice-pulse 1.4s ease-in-out infinite;
|
||||||
.hero-pill-text, .hero-pill-icon {
|
.hero-pill-text {
|
||||||
color: var(--slate-900);
|
color: var(--slate-900);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3981,11 +3988,6 @@ async function onFamilyLike() {
|
|||||||
border: 2rpx solid #ffffff;
|
border: 2rpx solid #ffffff;
|
||||||
padding: 16rpx 32rpx;
|
padding: 16rpx 32rpx;
|
||||||
box-shadow: 0 8rpx 20rpx rgba(15, 23, 42, 0.1);
|
box-shadow: 0 8rpx 20rpx rgba(15, 23, 42, 0.1);
|
||||||
.hero-pill-icon {
|
|
||||||
color: var(--primary);
|
|
||||||
font-weight: 800;
|
|
||||||
font-size: 38rpx;
|
|
||||||
}
|
|
||||||
.hero-pill-text {
|
.hero-pill-text {
|
||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -3999,10 +4001,13 @@ async function onFamilyLike() {
|
|||||||
0%, 100% { box-shadow: 0 0 0 0 rgba(251, 191, 36, 0.55); }
|
0%, 100% { box-shadow: 0 0 0 0 rgba(251, 191, 36, 0.55); }
|
||||||
50% { box-shadow: 0 0 0 12rpx rgba(251, 191, 36, 0); }
|
50% { box-shadow: 0 0 0 12rpx rgba(251, 191, 36, 0); }
|
||||||
}
|
}
|
||||||
.hero-pill-icon {
|
.hero-pill-icon-wrap {
|
||||||
font-size: 34rpx;
|
width: 36rpx;
|
||||||
color: #ffffff;
|
height: 36rpx;
|
||||||
line-height: 1;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.hero-pill-text {
|
.hero-pill-text {
|
||||||
font-size: 32rpx;
|
font-size: 32rpx;
|
||||||
@@ -4012,88 +4017,81 @@ async function onFamilyLike() {
|
|||||||
|
|
||||||
/* === Bento 风格指标卡 === */
|
/* === Bento 风格指标卡 === */
|
||||||
.stat-grid {
|
.stat-grid {
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-wrap: wrap;
|
grid-template-columns: repeat(2, 1fr);
|
||||||
gap: 16rpx;
|
gap: 20rpx;
|
||||||
margin: -60rpx 24rpx 0;
|
margin: -60rpx 24rpx 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
}
|
}
|
||||||
.stat-card {
|
.stat-card {
|
||||||
flex: 1 1 calc(50% - 8rpx);
|
|
||||||
min-width: 0;
|
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border-radius: var(--radius-card);
|
border-radius: var(--radius-card);
|
||||||
padding: 30rpx 24rpx 26rpx;
|
padding: 30rpx 26rpx;
|
||||||
box-shadow: var(--shadow-premium);
|
box-shadow: var(--shadow-premium);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12rpx;
|
justify-content: space-between;
|
||||||
position: relative;
|
min-height: 250rpx;
|
||||||
overflow: hidden;
|
|
||||||
border-top: 8rpx solid transparent;
|
|
||||||
border: 1rpx solid rgba(15, 23, 42, 0.04);
|
border: 1rpx solid rgba(15, 23, 42, 0.04);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
transition: var(--transition-smooth);
|
transition: var(--transition-smooth);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
&:active {
|
&:active {
|
||||||
transform: translateY(-2rpx);
|
transform: translateY(-2rpx);
|
||||||
box-shadow: 0 16rpx 36rpx rgba(15, 23, 42, 0.08);
|
box-shadow: 0 16rpx 36rpx rgba(15, 23, 42, 0.08);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.stat-card::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: -30rpx;
|
|
||||||
width: 120rpx;
|
|
||||||
height: 120rpx;
|
|
||||||
border-radius: 999rpx;
|
|
||||||
opacity: 0.06;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
.stat-fasting {
|
.stat-fasting {
|
||||||
border-top-color: var(--primary);
|
background: radial-gradient(circle at 100% 0%, #e6fdf9 0%, #ffffff 65%);
|
||||||
&::before { background: var(--primary); }
|
|
||||||
}
|
}
|
||||||
.stat-postprandial {
|
.stat-postprandial {
|
||||||
border-top-color: var(--warning);
|
background: radial-gradient(circle at 100% 0%, #fff7ed 0%, #ffffff 65%);
|
||||||
&::before { background: var(--warning); }
|
|
||||||
}
|
}
|
||||||
.stat-other {
|
.stat-other {
|
||||||
flex: 1 1 100% !important;
|
grid-column: span 2;
|
||||||
flex-direction: row !important;
|
background: radial-gradient(circle at 100% 0%, #f5f3ff 0%, #ffffff 65%);
|
||||||
|
display: grid !important;
|
||||||
|
grid-template-areas:
|
||||||
|
"head value"
|
||||||
|
"foot value" !important;
|
||||||
|
grid-template-columns: 1fr auto !important;
|
||||||
|
grid-template-rows: auto auto !important;
|
||||||
align-items: center !important;
|
align-items: center !important;
|
||||||
justify-content: space-between !important;
|
min-height: 170rpx !important;
|
||||||
padding: 24rpx 32rpx !important;
|
padding: 26rpx 32rpx !important;
|
||||||
border-top: none !important;
|
|
||||||
border-left: 8rpx solid var(--violet) !important;
|
|
||||||
gap: 16rpx !important;
|
|
||||||
&::before { display: none !important; }
|
|
||||||
.stat-card-head {
|
.stat-card-head {
|
||||||
display: flex;
|
grid-area: head !important;
|
||||||
flex-direction: row;
|
display: flex !important;
|
||||||
align-items: center;
|
align-items: center !important;
|
||||||
gap: 12rpx;
|
justify-content: flex-start !important;
|
||||||
flex: 1;
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
gap: 12rpx !important;
|
||||||
}
|
}
|
||||||
.stat-card-value-row {
|
.stat-card-value-row {
|
||||||
margin-left: auto;
|
grid-area: value !important;
|
||||||
display: flex;
|
display: flex !important;
|
||||||
align-items: baseline;
|
align-items: baseline !important;
|
||||||
gap: 6rpx;
|
margin: 0 !important;
|
||||||
margin-top: 0;
|
padding: 0 !important;
|
||||||
|
gap: 6rpx !important;
|
||||||
}
|
}
|
||||||
.stat-card-foot {
|
.stat-card-foot {
|
||||||
display: flex;
|
grid-area: foot !important;
|
||||||
flex-direction: row;
|
display: flex !important;
|
||||||
align-items: center;
|
align-items: center !important;
|
||||||
gap: 12rpx;
|
justify-content: flex-start !important;
|
||||||
margin-top: 0;
|
margin-top: 12rpx !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
gap: 16rpx !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.stat-card.is-high {
|
.stat-card.is-high {
|
||||||
box-shadow: var(--shadow-glow-danger);
|
box-shadow: var(--shadow-glow-danger);
|
||||||
border-color: rgba(239, 68, 68, 0.1);
|
border-color: rgba(239, 68, 68, 0.08);
|
||||||
}
|
}
|
||||||
.stat-card-head {
|
.stat-card-head {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -4102,52 +4100,61 @@ async function onFamilyLike() {
|
|||||||
gap: 8rpx;
|
gap: 8rpx;
|
||||||
}
|
}
|
||||||
.stat-card-tag {
|
.stat-card-tag {
|
||||||
display: flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6rpx;
|
padding: 8rpx 18rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.stat-fasting .stat-card-tag {
|
||||||
|
background: rgba(13, 148, 136, 0.09);
|
||||||
|
color: #0d9488;
|
||||||
|
}
|
||||||
|
.stat-postprandial .stat-card-tag {
|
||||||
|
background: rgba(245, 158, 11, 0.09);
|
||||||
|
color: #d97706;
|
||||||
|
}
|
||||||
|
.stat-other .stat-card-tag {
|
||||||
|
background: rgba(99, 102, 241, 0.09);
|
||||||
|
color: #4f46e5;
|
||||||
}
|
}
|
||||||
.stat-card-tag-dot {
|
.stat-card-tag-dot {
|
||||||
width: 16rpx;
|
display: none;
|
||||||
height: 16rpx;
|
|
||||||
border-radius: var(--radius-pill);
|
|
||||||
}
|
}
|
||||||
.stat-fasting .stat-card-tag-dot { background: var(--primary); }
|
|
||||||
.stat-postprandial .stat-card-tag-dot { background: var(--warning); }
|
|
||||||
.stat-other .stat-card-tag-dot { background: var(--violet); }
|
|
||||||
|
|
||||||
.stat-card-tag-text {
|
.stat-card-tag-text {
|
||||||
font-size: 26rpx;
|
font-size: 26rpx;
|
||||||
color: var(--slate-900);
|
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
.stat-card-flag {
|
.stat-card-flag {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4rpx;
|
gap: 6rpx;
|
||||||
padding: 4rpx 12rpx 4rpx 8rpx;
|
padding: 6rpx 14rpx;
|
||||||
border-radius: var(--radius-pill);
|
border-radius: var(--radius-pill);
|
||||||
background: var(--danger-grad);
|
background: rgba(239, 68, 68, 0.08);
|
||||||
box-shadow: 0 4rpx 10rpx rgba(239, 68, 68, 0.2);
|
border: 1rpx solid rgba(239, 68, 68, 0.15);
|
||||||
}
|
}
|
||||||
.stat-card-flag-icon {
|
.stat-card-flag-icon {
|
||||||
font-size: 20rpx;
|
font-size: 20rpx;
|
||||||
color: #ffffff;
|
color: var(--danger);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
.stat-card-flag-text {
|
.stat-card-flag-text {
|
||||||
font-size: 22rpx;
|
font-size: 22rpx;
|
||||||
color: #ffffff;
|
color: var(--danger);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
.stat-card-value-row {
|
.stat-card-value-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
gap: 8rpx;
|
gap: 6rpx;
|
||||||
flex-wrap: wrap;
|
margin-top: 16rpx;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
}
|
}
|
||||||
.stat-card-value {
|
.stat-card-value {
|
||||||
font-size: 60rpx; /* 更大数字 */
|
font-size: 64rpx;
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
background: linear-gradient(180deg, var(--slate-900) 0%, #334155 100%);
|
background: linear-gradient(180deg, var(--slate-900) 0%, #334155 100%);
|
||||||
-webkit-background-clip: text;
|
-webkit-background-clip: text;
|
||||||
@@ -4183,25 +4190,25 @@ async function onFamilyLike() {
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4rpx;
|
gap: 4rpx;
|
||||||
padding: 6rpx 12rpx 6rpx 9rpx;
|
padding: 6rpx 14rpx;
|
||||||
border-radius: var(--radius-pill);
|
border-radius: var(--radius-pill);
|
||||||
background: var(--slate-100);
|
|
||||||
transition: var(--transition-smooth);
|
transition: var(--transition-smooth);
|
||||||
&.trend-up {
|
&.trend-up {
|
||||||
background: #fef3c7;
|
background: rgba(239, 68, 68, 0.08);
|
||||||
.stat-card-trend-arrow,
|
.stat-card-trend-arrow,
|
||||||
.stat-card-trend-delta {
|
.stat-card-trend-delta {
|
||||||
color: #b45309;
|
color: var(--danger);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&.trend-down {
|
&.trend-down {
|
||||||
background: #dcfce7;
|
background: rgba(16, 185, 129, 0.08);
|
||||||
.stat-card-trend-arrow,
|
.stat-card-trend-arrow,
|
||||||
.stat-card-trend-delta {
|
.stat-card-trend-delta {
|
||||||
color: #15803d;
|
color: var(--success);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&.trend-flat {
|
&.trend-flat {
|
||||||
|
background: var(--slate-100);
|
||||||
.stat-card-trend-arrow,
|
.stat-card-trend-arrow,
|
||||||
.stat-card-trend-delta {
|
.stat-card-trend-delta {
|
||||||
color: var(--slate-600);
|
color: var(--slate-600);
|
||||||
@@ -5043,8 +5050,16 @@ async function onFamilyLike() {
|
|||||||
gap: 12rpx;
|
gap: 12rpx;
|
||||||
margin-bottom: 6rpx;
|
margin-bottom: 6rpx;
|
||||||
}
|
}
|
||||||
.hero-greet-emoji {
|
.hero-greet-icon-wrap {
|
||||||
font-size: 36rpx;
|
width: 52rpx;
|
||||||
|
height: 52rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.18);
|
||||||
|
border: 1rpx solid rgba(255, 255, 255, 0.28);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.hero-greet-text {
|
.hero-greet-text {
|
||||||
font-size: 30rpx;
|
font-size: 30rpx;
|
||||||
@@ -5080,16 +5095,12 @@ async function onFamilyLike() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.hero-status-icon {
|
.hero-status-icon {
|
||||||
width: 36rpx;
|
width: 32rpx;
|
||||||
height: 36rpx;
|
height: 32rpx;
|
||||||
border-radius: var(--radius-pill);
|
|
||||||
background: rgba(255, 255, 255, 0.4);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 24rpx;
|
flex-shrink: 0;
|
||||||
line-height: 1;
|
|
||||||
font-weight: 800;
|
|
||||||
}
|
}
|
||||||
.hero-status-text {
|
.hero-status-text {
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
@@ -5879,23 +5890,117 @@ async function onFamilyLike() {
|
|||||||
}
|
}
|
||||||
.tree-growth-zone {
|
.tree-growth-zone {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
gap: 24rpx;
|
gap: 20rpx;
|
||||||
background: var(--slate-50);
|
background: linear-gradient(145deg, #f0fdfa 0%, #f8fafc 55%, #fffbeb 100%);
|
||||||
border-radius: 24rpx;
|
border-radius: 24rpx;
|
||||||
padding: 20rpx 24rpx;
|
padding: 24rpx 20rpx;
|
||||||
border: 1rpx solid var(--slate-100);
|
border: 2rpx solid #99f6e4;
|
||||||
}
|
}
|
||||||
.tree-graphic-wrap {
|
.tree-graphic-wrap {
|
||||||
position: relative;
|
position: relative;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: 140rpx;
|
width: 152rpx;
|
||||||
height: 154rpx;
|
min-height: 200rpx;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
.tree-lv-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 2;
|
||||||
|
padding: 4rpx 14rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: linear-gradient(135deg, #0ea5a4, #14b8a6);
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 20rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba(14, 165, 164, 0.35);
|
||||||
|
}
|
||||||
|
.tree-mood-pill {
|
||||||
|
position: absolute;
|
||||||
|
top: 36rpx;
|
||||||
|
right: -8rpx;
|
||||||
|
z-index: 2;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4rpx;
|
||||||
|
padding: 4rpx 10rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.92);
|
||||||
|
border: 1rpx solid #e2e8f0;
|
||||||
|
box-shadow: 0 2rpx 8rpx rgba(15, 23, 42, 0.08);
|
||||||
|
}
|
||||||
|
.tree-mood-emoji {
|
||||||
|
font-size: 22rpx;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.tree-mood-text {
|
||||||
|
font-size: 20rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
.tree-next-hint {
|
||||||
|
display: block;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #0f766e;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
.tree-roadmap-scroll {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 14rpx;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.tree-roadmap-row {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 10rpx;
|
||||||
|
padding-bottom: 4rpx;
|
||||||
|
}
|
||||||
|
.tree-roadmap-node {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4rpx;
|
||||||
|
min-width: 56rpx;
|
||||||
|
padding: 8rpx 6rpx;
|
||||||
|
border-radius: 14rpx;
|
||||||
|
background: #f1f5f9;
|
||||||
|
border: 1rpx solid #e2e8f0;
|
||||||
|
opacity: 0.55;
|
||||||
|
&.done {
|
||||||
|
opacity: 0.85;
|
||||||
|
background: #ecfdf5;
|
||||||
|
border-color: #a7f3d0;
|
||||||
|
}
|
||||||
|
&.current {
|
||||||
|
opacity: 1;
|
||||||
|
background: linear-gradient(180deg, #ccfbf1, #ffffff);
|
||||||
|
border-color: #14b8a6;
|
||||||
|
box-shadow: 0 4rpx 14rpx rgba(14, 165, 164, 0.25);
|
||||||
|
transform: scale(1.06);
|
||||||
|
}
|
||||||
|
&.locked {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tree-roadmap-emoji {
|
||||||
|
font-size: 26rpx;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.tree-roadmap-lv {
|
||||||
|
font-size: 18rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #64748b;
|
||||||
|
.tree-roadmap-node.current & {
|
||||||
|
color: #0f766e;
|
||||||
|
}
|
||||||
|
}
|
||||||
.tree-tappable:active {
|
.tree-tappable:active {
|
||||||
opacity: 0.92;
|
opacity: 0.92;
|
||||||
}
|
}
|
||||||
@@ -5956,14 +6061,14 @@ async function onFamilyLike() {
|
|||||||
color: #b45309;
|
color: #b45309;
|
||||||
}
|
}
|
||||||
.badge-wall {
|
.badge-wall {
|
||||||
margin-top: 4rpx;
|
margin-top: 12rpx;
|
||||||
}
|
}
|
||||||
.badge-wall-title {
|
.badge-wall-title {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 26rpx;
|
font-size: 32rpx;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
color: #475569;
|
color: #475569;
|
||||||
margin-bottom: 12rpx;
|
margin-bottom: 16rpx;
|
||||||
padding-left: 6rpx;
|
padding-left: 6rpx;
|
||||||
}
|
}
|
||||||
.badge-wall-scroll {
|
.badge-wall-scroll {
|
||||||
@@ -5972,24 +6077,24 @@ async function onFamilyLike() {
|
|||||||
}
|
}
|
||||||
.badge-wall-row {
|
.badge-wall-row {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
gap: 12rpx;
|
gap: 14rpx;
|
||||||
padding-bottom: 4rpx;
|
padding-bottom: 8rpx;
|
||||||
}
|
}
|
||||||
.badge-chip {
|
.badge-chip {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8rpx;
|
gap: 10rpx;
|
||||||
min-width: 120rpx;
|
min-width: 140rpx;
|
||||||
padding: 14rpx 16rpx;
|
padding: 18rpx 20rpx;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border: 1rpx solid var(--slate-200);
|
border: 1rpx solid var(--slate-200);
|
||||||
border-radius: 16rpx;
|
border-radius: 18rpx;
|
||||||
box-shadow: var(--shadow-sm);
|
box-shadow: var(--shadow-sm);
|
||||||
}
|
}
|
||||||
.badge-chip-icon {
|
.badge-chip-icon {
|
||||||
width: 52rpx;
|
width: 58rpx;
|
||||||
height: 52rpx;
|
height: 58rpx;
|
||||||
border-radius: var(--radius-pill);
|
border-radius: var(--radius-pill);
|
||||||
background: var(--slate-50);
|
background: var(--slate-50);
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -5997,7 +6102,7 @@ async function onFamilyLike() {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
.badge-chip-label {
|
.badge-chip-label {
|
||||||
font-size: 22rpx;
|
font-size: 28rpx;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #334155;
|
color: #334155;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -6016,15 +6121,15 @@ async function onFamilyLike() {
|
|||||||
gap: 12rpx;
|
gap: 12rpx;
|
||||||
}
|
}
|
||||||
.tree-level-tag {
|
.tree-level-tag {
|
||||||
font-size: 26rpx;
|
font-size: 30rpx;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--slate-900);
|
color: var(--slate-900);
|
||||||
background: var(--slate-200);
|
background: var(--slate-200);
|
||||||
padding: 4rpx 14rpx;
|
padding: 6rpx 18rpx;
|
||||||
border-radius: var(--radius-pill);
|
border-radius: var(--radius-pill);
|
||||||
}
|
}
|
||||||
.tree-progress-text {
|
.tree-progress-text {
|
||||||
font-size: 24rpx;
|
font-size: 28rpx;
|
||||||
color: var(--slate-600);
|
color: var(--slate-600);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
@@ -6246,14 +6351,14 @@ async function onFamilyLike() {
|
|||||||
}
|
}
|
||||||
.pg-share-title {
|
.pg-share-title {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 28rpx;
|
font-size: 34rpx;
|
||||||
font-weight: 700;
|
font-weight: 800;
|
||||||
color: #0f766e;
|
color: #0f766e;
|
||||||
}
|
}
|
||||||
.pg-share-sub {
|
.pg-share-sub {
|
||||||
display: block;
|
display: block;
|
||||||
margin-top: 4rpx;
|
margin-top: 6rpx;
|
||||||
font-size: 22rpx;
|
font-size: 28rpx;
|
||||||
color: var(--slate-600);
|
color: var(--slate-600);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6261,12 +6366,12 @@ async function onFamilyLike() {
|
|||||||
.family-like-strip {
|
.family-like-strip {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 16rpx;
|
gap: 18rpx;
|
||||||
margin: 20rpx 24rpx 0;
|
margin: 20rpx 24rpx 0;
|
||||||
padding: 22rpx 24rpx;
|
padding: 24rpx 28rpx;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border-radius: 24rpx;
|
border-radius: 24rpx;
|
||||||
border: 1rpx solid #fecdd3;
|
border: 1.5rpx solid #fecdd3;
|
||||||
box-shadow: 0 4rpx 16rpx rgba(225, 29, 72, 0.06);
|
box-shadow: 0 4rpx 16rpx rgba(225, 29, 72, 0.06);
|
||||||
transition: var(--transition-smooth);
|
transition: var(--transition-smooth);
|
||||||
&.has-likes {
|
&.has-likes {
|
||||||
@@ -6277,7 +6382,7 @@ async function onFamilyLike() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.family-like-icon {
|
.family-like-icon {
|
||||||
font-size: 36rpx;
|
font-size: 40rpx;
|
||||||
}
|
}
|
||||||
.family-like-body {
|
.family-like-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -6285,40 +6390,42 @@ async function onFamilyLike() {
|
|||||||
}
|
}
|
||||||
.family-like-title {
|
.family-like-title {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 28rpx;
|
font-size: 34rpx;
|
||||||
font-weight: 700;
|
font-weight: 800;
|
||||||
color: #9f1239;
|
color: #9f1239;
|
||||||
}
|
}
|
||||||
.family-like-sub {
|
.family-like-sub {
|
||||||
display: block;
|
display: block;
|
||||||
margin-top: 6rpx;
|
margin-top: 8rpx;
|
||||||
font-size: 24rpx;
|
font-size: 30rpx;
|
||||||
color: var(--slate-600);
|
color: var(--slate-600);
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
.family-like-recent {
|
.family-like-recent {
|
||||||
margin-top: 8rpx;
|
margin-top: 10rpx;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 8rpx;
|
gap: 10rpx;
|
||||||
}
|
}
|
||||||
.family-like-recent-item {
|
.family-like-recent-item {
|
||||||
font-size: 22rpx;
|
font-size: 26rpx;
|
||||||
color: #be123c;
|
color: #be123c;
|
||||||
background: #ffe4e6;
|
background: #ffe4e6;
|
||||||
padding: 4rpx 12rpx;
|
padding: 6rpx 16rpx;
|
||||||
border-radius: var(--radius-pill);
|
border-radius: var(--radius-pill);
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
.family-like-arrow {
|
.family-like-arrow {
|
||||||
font-size: 32rpx;
|
font-size: 36rpx;
|
||||||
color: var(--slate-400);
|
color: var(--slate-400);
|
||||||
}
|
}
|
||||||
|
|
||||||
.encourage-strip {
|
.encourage-strip {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 14rpx;
|
gap: 16rpx;
|
||||||
margin: 16rpx 24rpx 0;
|
margin: 16rpx 24rpx 0;
|
||||||
padding: 20rpx 22rpx;
|
padding: 24rpx 24rpx;
|
||||||
background: #ecfdf5;
|
background: #ecfdf5;
|
||||||
border-radius: 20rpx;
|
border-radius: 20rpx;
|
||||||
border: 1rpx solid #a7f3d0;
|
border: 1rpx solid #a7f3d0;
|
||||||
@@ -6328,20 +6435,21 @@ async function onFamilyLike() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.encourage-icon {
|
.encourage-icon {
|
||||||
font-size: 32rpx;
|
font-size: 36rpx;
|
||||||
}
|
}
|
||||||
.encourage-text {
|
.encourage-text {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 26rpx;
|
font-size: 32rpx;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
color: #065f46;
|
color: #065f46;
|
||||||
line-height: 1.45;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
.encourage-tip {
|
.encourage-tip {
|
||||||
display: block;
|
display: block;
|
||||||
margin-top: 6rpx;
|
margin-top: 8rpx;
|
||||||
font-size: 22rpx;
|
font-size: 28rpx;
|
||||||
color: var(--slate-600);
|
color: var(--slate-600);
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* === 共享/邀请查看样式 === */
|
/* === 共享/邀请查看样式 === */
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
/** 控糖树等级配置(前后端算法需保持一致:每级 50 积分,最高 Lv.9) */
|
||||||
|
export const TREE_MAX_LEVEL = 9
|
||||||
|
export const TREE_XP_PER_LEVEL = 50
|
||||||
|
|
||||||
|
export const TREE_LEVELS = [
|
||||||
|
{ level: 0, name: '种子眠', emoji: '🫘', mood: '困困', desc: '稳糖种子在土里打盹' },
|
||||||
|
{ level: 1, name: '破土芽', emoji: '🌱', mood: '探头', desc: '探出第一抹新绿' },
|
||||||
|
{ level: 2, name: '展两叶', emoji: '🌿', mood: '好奇', desc: '两片嫩叶迎风展' },
|
||||||
|
{ level: 3, name: '小树苗', emoji: '🪴', mood: '精神', desc: '身子骨硬朗起来' },
|
||||||
|
{ level: 4, name: '青枝繁', emoji: '🌳', mood: '茁壮', desc: '枝叶渐密,元气足' },
|
||||||
|
{ level: 5, name: '拔节高', emoji: '🌲', mood: '挺拔', desc: '一节一节往上蹿' },
|
||||||
|
{ level: 6, name: '稳糖冠', emoji: '💚', mood: '沉稳', desc: '树冠成形,习惯成自然' },
|
||||||
|
{ level: 7, name: '初绽香', emoji: '🌸', mood: '开心', desc: '枝头冒出第一朵花' },
|
||||||
|
{ level: 8, name: '漫开花', emoji: '🌺', mood: '灿烂', desc: '花开满枝,越记越稳' },
|
||||||
|
{ level: 9, name: '圆满树', emoji: '🏆', mood: '荣耀', desc: '满级大树,习惯大师' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const WHISPERS_BY_LEVEL = {
|
||||||
|
0: ['种子在睡觉,记一笔就醒啦。', '今天浇第一滴水,芽就要冒出来。'],
|
||||||
|
1: ['破土啦!再坚持几天就长高。', '小芽最喜欢规律的记录了。'],
|
||||||
|
2: ['两片叶子为你鼓掌。', '空腹餐后都记全,我会长得更快。'],
|
||||||
|
3: ['我已经是一棵小树啦。', '家人点赞的时候,我也会发光。'],
|
||||||
|
4: ['枝叶越来越密,您真棒。', '连续记录,我会开出更多叶子。'],
|
||||||
|
5: ['拔节中!习惯比完美更重要。', '再浇一点水,我就更高啦。'],
|
||||||
|
6: ['习惯成自然,树冠成形啦。', '您今天的坚持,小树都记得。'],
|
||||||
|
7: ['开花啦!闻到春天的味道了吗?', '稳糖花只开给坚持的人。'],
|
||||||
|
8: ['满树花香,您已是控糖达人。', '明天继续来,花儿会更艳。'],
|
||||||
|
9: ['满级大树陪您一路稳糖。', '圆满不是终点,习惯才是。']
|
||||||
|
}
|
||||||
|
|
||||||
|
export function calcTreeFromPoints(points) {
|
||||||
|
const pts = Math.max(0, Number(points) || 0)
|
||||||
|
const level = Math.min(TREE_MAX_LEVEL, Math.floor(pts / TREE_XP_PER_LEVEL))
|
||||||
|
const xpInLevel = level >= TREE_MAX_LEVEL ? TREE_XP_PER_LEVEL : pts % TREE_XP_PER_LEVEL
|
||||||
|
const progress = level >= TREE_MAX_LEVEL ? 100 : Math.round((xpInLevel / TREE_XP_PER_LEVEL) * 100)
|
||||||
|
const meta = TREE_LEVELS[level] || TREE_LEVELS[0]
|
||||||
|
const nextMeta = level < TREE_MAX_LEVEL ? TREE_LEVELS[level + 1] : null
|
||||||
|
const pointsToNext = level >= TREE_MAX_LEVEL ? 0 : TREE_XP_PER_LEVEL - xpInLevel
|
||||||
|
return {
|
||||||
|
level,
|
||||||
|
progress,
|
||||||
|
name: meta.name,
|
||||||
|
emoji: meta.emoji,
|
||||||
|
mood: meta.mood,
|
||||||
|
desc: meta.desc,
|
||||||
|
xpInLevel,
|
||||||
|
xpNeed: TREE_XP_PER_LEVEL,
|
||||||
|
pointsToNext,
|
||||||
|
nextName: nextMeta?.name || '',
|
||||||
|
isMax: level >= TREE_MAX_LEVEL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pickTreeWhisper(level) {
|
||||||
|
const lv = Math.min(TREE_MAX_LEVEL, Math.max(0, Number(level) || 0))
|
||||||
|
const list = WHISPERS_BY_LEVEL[lv] || WHISPERS_BY_LEVEL[0]
|
||||||
|
return list[Math.floor(Math.random() * list.length)]
|
||||||
|
}
|
||||||
@@ -148,14 +148,33 @@ class DailyGamifyLogic
|
|||||||
return $list;
|
return $list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 与前端 tongji/utils/treeLevels.js 保持一致 */
|
||||||
|
protected const TREE_MAX_LEVEL = 9;
|
||||||
|
protected const TREE_XP_PER_LEVEL = 50;
|
||||||
|
|
||||||
protected static function treeMeta(int $points): array
|
protected static function treeMeta(int $points): array
|
||||||
{
|
{
|
||||||
$level = min(4, (int) floor($points / 100));
|
$points = max(0, (int) $points);
|
||||||
$names = ['嫩芽期', '成长雏形', '枝繁叶茂', '含苞待放', '花开稳糖'];
|
$level = min(self::TREE_MAX_LEVEL, (int) floor($points / self::TREE_XP_PER_LEVEL));
|
||||||
|
$names = ['种子眠', '破土芽', '展两叶', '小树苗', '青枝繁', '拔节高', '稳糖冠', '初绽香', '漫开花', '圆满树'];
|
||||||
|
$xpIn = $level >= self::TREE_MAX_LEVEL ? self::TREE_XP_PER_LEVEL : ($points % self::TREE_XP_PER_LEVEL);
|
||||||
|
$progress = $level >= self::TREE_MAX_LEVEL
|
||||||
|
? 100
|
||||||
|
: (int) round(($xpIn / self::TREE_XP_PER_LEVEL) * 100);
|
||||||
|
$nextName = $level < self::TREE_MAX_LEVEL ? ($names[$level + 1] ?? '') : '';
|
||||||
|
$pointsToNext = $level >= self::TREE_MAX_LEVEL
|
||||||
|
? 0
|
||||||
|
: (self::TREE_XP_PER_LEVEL - $xpIn);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'tree_level' => $level,
|
'tree_level' => $level,
|
||||||
'tree_progress' => $level >= 4 ? 100 : ($points % 100),
|
'tree_progress' => $progress,
|
||||||
'tree_level_name' => $names[$level] ?? '嫩芽期',
|
'tree_level_name' => $names[$level] ?? '种子眠',
|
||||||
|
'tree_xp_in_level' => $xpIn,
|
||||||
|
'tree_xp_need' => self::TREE_XP_PER_LEVEL,
|
||||||
|
'tree_points_next' => $pointsToNext,
|
||||||
|
'tree_next_name' => $nextName,
|
||||||
|
'tree_is_max' => $level >= self::TREE_MAX_LEVEL,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user