1.6
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user