Files
chat/frontend/src/views/garden/LeafView.vue
T
2026-06-13 17:57:43 +08:00

241 lines
9.1 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>