This commit is contained in:
2026-06-13 17:57:43 +08:00
parent 68678304ff
commit a0f441d8ae
28 changed files with 1933 additions and 2 deletions
+240
View File
@@ -0,0 +1,240 @@
<template>
<div class="leaf-view">
<div class="leaf-header">
<h2>🍃 每日心情叶</h2>
<p class="subtitle">{{ today }}</p>
</div>
<div v-if="leaf" class="leaf-main">
<!-- 叶子大图 -->
<div class="leaf-stage">
<div class="leaf-wrapper" :style="{ transform: `scale(${style.size}) rotate(${style.angle}deg)` }">
<svg viewBox="0 0 100 120" class="leaf-svg" :class="{ sway: !editingMood }">
<defs>
<linearGradient :id="`grad-${leaf.id}`" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" :stop-color="leafColor(style, 10)" />
<stop offset="100%" :stop-color="leafColor(style, -10)" />
</linearGradient>
</defs>
<path :d="leafPath(style.shapeVariant)" :fill="`url(#grad-${leaf.id})`" stroke="rgba(0,0,0,0.15)" stroke-width="0.5" />
<!-- 叶脉 -->
<g stroke="rgba(255,255,255,0.4)" stroke-width="0.8" fill="none" stroke-linecap="round">
<path v-for="(vp, i) in veinPaths(style)" :key="i" :d="vp" />
</g>
<!-- 斑点 -->
<circle v-for="(sp, i) in spotPositions(style)" :key="'s'+i" :cx="sp.x" :cy="sp.y" :r="sp.r" fill="rgba(255,255,255,0.25)" />
</svg>
</div>
</div>
<!-- 心情 -->
<div class="mood-section">
<div class="section-label">今日心情</div>
<div class="mood-grid">
<div v-for="m in moods" :key="m.key" class="mood-chip"
:class="{ active: leaf.mood === m.key }" @click="selectMood(m.key)">
<span class="mood-emoji">{{ m.emoji }}</span>
<span class="mood-name">{{ m.name }}</span>
</div>
</div>
</div>
<!-- 备注 -->
<div class="note-section">
<div class="section-label">写点什么可选</div>
<n-input v-model:value="note" type="textarea" :rows="2" placeholder="今天这片叶子想说什么..."
maxlength="200" show-count @blur="saveNote" />
</div>
<div v-if="leaf.mood" class="today-mood-display">
<span class="mood-emoji large">{{ currentMoodEmoji }}</span>
<span>{{ currentMoodName }}</span>
</div>
</div>
<div v-else class="loading">生长中...</div>
<!-- 收藏入口 -->
<div class="collection-entry">
<n-button quaternary type="primary" @click="showCollection = !showCollection">
📖 我的叶子图鉴 ({{ collection.length }})
</n-button>
</div>
<!-- 图鉴弹窗 -->
<div v-if="showCollection" class="modal-overlay" @click.self="showCollection = false">
<div class="collection-modal">
<div class="modal-header">
<h3>🍃 叶子图鉴</h3>
<span class="close-btn" @click="showCollection = false"></span>
</div>
<div class="collection-grid">
<div v-if="collection.length === 0" class="empty">还没有收藏的叶子</div>
<div v-for="l in collection" :key="l.id" class="collection-leaf" :title="l.leaf_date">
<svg viewBox="0 0 100 120" class="mini-leaf">
<path :d="leafPath(generateLeafStyle(l.leaf_seed).shapeVariant)"
:fill="leafColor(generateLeafStyle(l.leaf_seed))" />
</svg>
<span class="leaf-date">{{ l.leaf_date?.slice(5) }}</span>
<span v-if="l.mood" class="leaf-mood">{{ moodEmoji(l.mood) }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue'
import { useMessage } from 'naive-ui'
import { useLeavesStore } from '@/stores/leaves'
import {
generateLeafStyle, leafColor, leafPath, veinPaths, spotPositions,
} from '@/utils/leafGenerator'
const leavesStore = useLeavesStore()
const message = useMessage()
const note = ref('')
const showCollection = ref(false)
const editingMood = ref(false)
const moods = [
{ key: 'calm', emoji: '🌙', name: '平静' },
{ key: 'happy', emoji: '😊', name: '开心' },
{ key: 'energetic', emoji: '⚡', name: '活力' },
{ key: 'thoughtful', emoji: '🤔', name: '沉思' },
{ key: 'lazy', emoji: '😴', name: '慵懒' },
{ key: 'grateful', emoji: '🙏', name: '感恩' },
]
const leaf = computed(() => leavesStore.todayLeaf)
const style = computed(() => leaf.value ? generateLeafStyle(leaf.value.leaf_seed) : generateLeafStyle('0000000000000000'))
const collection = computed(() => leavesStore.collection)
const today = computed(() => new Date().toLocaleDateString('zh-CN', { month: 'long', day: 'numeric', weekday: 'long' }))
const currentMoodEmoji = computed(() => moods.find((m) => m.key === leaf.value?.mood)?.emoji || '')
const currentMoodName = computed(() => moods.find((m) => m.key === leaf.value?.mood)?.name || '')
onMounted(async () => {
await leavesStore.fetchToday()
note.value = leaf.value?.note || ''
})
watch(() => leavesStore.todayLeaf?.note, (n) => {
if (n !== undefined) note.value = n
})
async function selectMood(mood: string) {
editingMood.value = true
try {
await leavesStore.updateLeaf(leaf.value.id, { mood })
message.success('心情已记录')
} catch {
message.error('保存失败')
} finally {
setTimeout(() => (editingMood.value = false), 600)
}
}
let noteTimer: ReturnType<typeof setTimeout>
function saveNote() {
clearTimeout(noteTimer)
noteTimer = setTimeout(async () => {
if (!leaf.value) return
try {
await leavesStore.updateLeaf(leaf.value.id, { note: note.value })
} catch {}
}, 300)
}
function moodEmoji(key: string) {
return moods.find((m) => m.key === key)?.emoji || ''
}
// 加载收藏
watch(showCollection, async (val) => {
if (val) await leavesStore.fetchCollection()
})
</script>
<style scoped>
.leaf-view {
flex: 1; overflow-y: auto; padding: 24px;
background: linear-gradient(180deg, var(--color-bg) 0%, var(--color-primary-lightest) 100%);
}
.leaf-header { text-align: center; margin-bottom: 8px; }
.leaf-header h2 { margin: 0; font-size: 22px; color: var(--color-primary-darker); }
.subtitle { font-size: 13px; color: var(--color-text-hint); margin: 4px 0 0; }
.leaf-main { max-width: 480px; margin: 0 auto; }
/* 叶子舞台 */
.leaf-stage {
display: flex; justify-content: center; align-items: center;
height: 240px; margin-bottom: 20px;
filter: drop-shadow(0 8px 16px rgba(0,150,136,0.2));
}
.leaf-wrapper { transition: transform 0.4s; transform-origin: 50% 90%; }
.leaf-svg { width: 160px; height: 192px; }
.leaf-svg.sway { animation: leaf-sway 4s ease-in-out infinite; transform-origin: 50% 100%; }
@keyframes leaf-sway {
0%, 100% { transform: rotate(-2deg); }
50% { transform: rotate(2deg); }
}
/* 心情 */
.section-label { font-size: 13px; color: var(--color-text-secondary); margin-bottom: 10px; font-weight: 500; }
.mood-section { margin-bottom: 20px; }
.mood-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
.mood-chip {
display: flex; flex-direction: column; align-items: center; gap: 4px;
padding: 12px 8px; border-radius: 12px; background: var(--color-surface);
border: 2px solid transparent; cursor: pointer; transition: all 0.2s;
}
.mood-chip:hover { transform: translateY(-2px); border-color: var(--color-primary-lighter); }
.mood-chip.active { border-color: var(--color-primary); background: var(--color-primary-lightest); }
.mood-emoji { font-size: 24px; }
.mood-emoji.large { font-size: 28px; }
.mood-name { font-size: 12px; color: var(--color-text-secondary); }
.note-section { margin-bottom: 16px; }
.today-mood-display {
display: flex; align-items: center; justify-content: center; gap: 8px;
padding: 12px; background: var(--color-primary-lightest); border-radius: 12px;
font-size: 15px; color: var(--color-primary-darker); font-weight: 500;
}
.loading { text-align: center; padding: 60px; color: var(--color-text-hint); }
.collection-entry { text-align: center; margin-top: 20px; }
/* 图鉴弹窗 */
.modal-overlay {
position: fixed; inset: 0; background: rgba(0,0,0,0.4);
display: flex; align-items: center; justify-content: center; z-index: 1000;
}
.collection-modal {
width: 560px; max-height: 70vh; background: var(--color-surface); border-radius: 16px;
display: flex; flex-direction: column; box-shadow: 0 12px 40px rgba(0,0,0,0.15);
}
.modal-header {
display: flex; justify-content: space-between; align-items: center;
padding: 20px 24px 16px; border-bottom: 1px solid var(--color-border);
}
.modal-header h3 { margin: 0; font-size: 18px; }
.close-btn { cursor: pointer; font-size: 18px; color: var(--color-text-hint); }
.collection-grid {
flex: 1; overflow-y: auto; padding: 20px 24px;
display: grid; grid-template-columns: repeat(auto-fill, minmax(90px, 1fr)); gap: 16px;
}
.empty { grid-column: 1 / -1; text-align: center; color: var(--color-text-hint); padding: 30px; }
.collection-leaf {
display: flex; flex-direction: column; align-items: center; gap: 4px;
padding: 8px; border-radius: 10px; background: var(--color-bg); position: relative;
}
.mini-leaf { width: 56px; height: 67px; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.1)); }
.leaf-date { font-size: 11px; color: var(--color-text-hint); }
.leaf-mood { position: absolute; top: 4px; right: 4px; font-size: 14px; }
</style>