Files

1349 lines
30 KiB
Vue
Raw Permalink 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>
<view class="calendar-page theme-transition" :style="{ backgroundColor: themeConfig.bgColor }">
<canvas canvas-id="calendar-ai-canvas" id="calendar-ai-canvas" class="ai-canvas"></canvas>
<!-- 顶部导航 -->
<view class="calendar-header" :style="{
background: `linear-gradient(180deg, ${themeConfig.primaryColor} 0%, ${themeConfig.secondaryColor} 100%)`
}">
<view class="header-nav">
<view class="nav-btn" @click="prevMonth">
<FaIcon icon="chevron-left" :size="22" color="#fff" />
</view>
<view class="header-title">
<text class="year-text">{{ currentYear }}</text>
<text class="month-text">{{ currentMonth }}</text>
</view>
<view class="nav-btn" @click="nextMonth">
<FaIcon icon="chevron-right" :size="22" color="#fff" />
</view>
</view>
<view class="today-btn" @click="goToday">
<text>今天</text>
</view>
</view>
<!-- 日历卡片容器 -->
<view
class="calendar-card theme-transition"
:style="{ backgroundColor: themeConfig.cardBgColor }"
@touchstart="onTouchStart"
@touchmove="onTouchMove"
@touchend="onTouchEnd"
>
<!-- 星期标题 -->
<view class="week-header">
<view class="week-day" v-for="day in weekDays" :key="day">
<text>{{ day }}</text>
</view>
</view>
<!-- 日期网格 -->
<view class="calendar-grid">
<view
class="date-cell"
v-for="(day, index) in calendarDays"
:key="index"
:class="{
'other-month': !day.isCurrentMonth,
'today': day.isToday,
'selected': day.dateStr === selectedDate,
'has-mood': day.mood
}"
@click="selectDate(day)"
>
<text class="date-num">{{ day.day }}</text>
<text class="mood-emoji" v-if="day.mood">{{ day.mood }}</text>
<view class="mood-dot" v-if="day.mood" :style="{ backgroundColor: getMoodColor(day.mood) }"></view>
</view>
</view>
</view>
<!-- 选中心情详情无内容时显示 -->
<view class="mood-detail" v-if="selectedMood && !selectedMood.content">
<view class="detail-card theme-transition" :style="{ backgroundColor: themeConfig.cardBgColor }">
<view class="detail-header">
<text class="detail-date" :style="{ color: themeConfig.textPrimary }">{{ selectedDate }}</text>
<view class="detail-mood" :style="{
background: `linear-gradient(135deg, ${themeConfig.primaryColor} 0%, ${themeConfig.secondaryColor} 100%)`
}">
<text class="detail-emoji">{{ selectedMood.emoji }}</text>
<text class="detail-label">{{ selectedMood.label }}</text>
</view>
</view>
<view class="detail-empty">
<text class="empty-text" :style="{ color: themeConfig.textSecondary }">这天没有记录心情内容~</text>
</view>
</view>
</view>
<!-- 有内容时点击查看按钮 -->
<view class="mood-detail" v-if="selectedMood && selectedMood.content">
<view class="detail-card theme-transition" :style="{ backgroundColor: themeConfig.cardBgColor }">
<view class="detail-header">
<text class="detail-date" :style="{ color: themeConfig.textPrimary }">{{ selectedDate }}</text>
<view class="detail-mood" :style="{
background: `linear-gradient(135deg, ${themeConfig.primaryColor} 0%, ${themeConfig.secondaryColor} 100%)`
}">
<text class="detail-emoji">{{ selectedMood.emoji }}</text>
<text class="detail-label">{{ selectedMood.label }}</text>
</view>
</view>
<view class="content-preview">
<text class="preview-text" :style="{ color: themeConfig.textSecondary }">{{ selectedMood.content.substring(0, 50) }}{{ selectedMood.content.length > 50 ? '...' : '' }}</text>
<view class="view-more-btn" @click="showContentModal = true">
<view class="view-more-text" :style="{ color: themeConfig.primaryColor }">查看全文 <FaIcon icon="chevron-right" :size="14" :color="themeConfig.primaryColor" /></view>
</view>
</view>
</view>
</view>
<!-- 内容展示弹窗 -->
<view class="content-modal" v-if="showContentModal" @click="closeContentModal">
<view class="modal-content theme-transition" :style="{ backgroundColor: themeConfig.cardBgColor }" @click.stop>
<view class="modal-header">
<view class="modal-date-row">
<text class="modal-date" :style="{ color: themeConfig.textPrimary }">{{ selectedDate }}</text>
<view class="modal-mood-badge" :style="{
background: `linear-gradient(135deg, ${themeConfig.primaryColor} 0%, ${themeConfig.secondaryColor} 100%)`
}">
<text class="modal-emoji">{{ selectedMood?.emoji }}</text>
<text class="modal-label">{{ selectedMood?.label }}</text>
</view>
</view>
<view class="modal-close" @click="closeContentModal">×</view>
</view>
<scroll-view class="modal-scroll" scroll-y>
<view class="modal-text-wrapper">
<text class="modal-text" :style="{ color: themeConfig.textPrimary }">{{ selectedMood?.content || '' }}</text>
</view>
<view class="ai-section" v-if="selectedMood?.ai_text">
<view class="ai-section-header">
<FaIcon icon="leaf" :size="22" :color="themeConfig.primaryColor" />
<text class="ai-section-title">心语小笺</text>
</view>
<view class="ai-card">
<view class="ai-card-bg"></view>
<view class="ai-card-content">
<view class="ai-card-header">
<text class="ai-card-date">{{ selectedDate }}</text>
<text class="ai-card-mood">{{ selectedMood.emoji }}</text>
</view>
<view class="ai-card-divider"></view>
<view class="ai-card-text">
<text>{{ selectedMood.ai_text }}</text>
</view>
<view class="ai-card-footer">
<text class="ai-card-hint">AI 创作</text>
</view>
</view>
</view>
<view class="ai-section-actions">
<view class="ai-save-btn" @click="saveCalendarAiCard">
<FaIcon icon="camera" :size="18" :color="themeConfig.textPrimary" />
<text class="save-text">保存图片</text>
</view>
</view>
</view>
</scroll-view>
</view>
</view>
<!-- 当月统计 -->
<view class="month-stats">
<view class="stats-card theme-transition" :style="{ backgroundColor: themeConfig.cardBgColor }">
<text class="stats-title" :style="{ color: themeConfig.textPrimary }">本月心情统计</text>
<view class="stats-chart" v-if="moodStats.length > 0">
<view
class="chart-bar"
v-for="(item, index) in moodStats"
:key="index"
:style="{
height: item.percent + '%',
background: item.color
}"
>
<text class="bar-emoji">{{ item.emoji }}</text>
<text class="bar-count">{{ item.count }}</text>
</view>
</view>
<view v-else class="stats-empty">
<view class="empty-hint-circle">
<FaIcon icon="face-smile" :size="36" :color="themeConfig.primaryColor" />
</view>
<text class="empty-hint-text" :style="{ color: themeConfig.textSecondary }">本月还没有心情记录</text>
<text class="empty-hint-sub" :style="{ color: themeConfig.textDisabled }">点击日期开始记录吧</text>
</view>
<view v-if="moodStats.length > 0" class="stats-summary">
<text class="summary-text" :style="{ color: themeConfig.textSecondary }">
本月共记录 <text :style="{ color: themeConfig.primaryColor }">{{ totalMoods }}</text> 天心情
</text>
</view>
</view>
</view>
</view>
</template>
<script>
import { getThemeConfigFromStorage, THEME_PRESETS } from '@/utils/theme'
import { refreshNavBarTheme } from '@/utils/themeGlobal'
import { MOOD_LIST, LOGO_CONFIG } from '../../utils/constants'
import { getMoodByDate, getMoodList, getAiMoodResult } from '../../api/index'
import FaIcon from '@/components/FaIcon.vue'
export default {
components: { FaIcon },
data() {
return {
themeConfig: getThemeConfigFromStorage() || THEME_PRESETS['default'],
currentYear: new Date().getFullYear(),
currentMonth: new Date().getMonth() + 1,
selectedDate: '',
calendarDays: [],
selectedMood: null,
moodMap: {},
weekDays: ['一', '二', '三', '四', '五', '六', '日'],
showContentModal: false,
touchStartX: 0,
touchEndX: 0,
touchDeltaX: 0,
isScrolling: false,
aiResult: null,
isAiLoading: false
}
},
onLoad() {
this.initCalendar()
this.loadMoods()
refreshNavBarTheme()
// 动态设置导航栏右侧按钮
setTimeout(() => {
uni.setNavigationBarTitle({
title: '心情日历'
})
}, 100)
this.themeChangeHandler = (themeData) => {
let newTheme = null
if (themeData?.key && THEME_PRESETS[themeData.key]) {
newTheme = THEME_PRESETS[themeData.key]
} else if (themeData?.config) {
newTheme = themeData.config
}
if (newTheme) {
this.themeConfig = newTheme
refreshNavBarTheme()
}
}
uni.$on('themeChanged', this.themeChangeHandler)
},
onShow() {
const latestTheme = getThemeConfigFromStorage() || THEME_PRESETS['default']
if (JSON.stringify(latestTheme) !== JSON.stringify(this.themeConfig)) {
this.themeConfig = latestTheme
}
refreshNavBarTheme()
},
onUnload() {
uni.$off('themeChanged', this.themeChangeHandler)
},
onNavigationBarButtonTap() {
uni.switchTab({
url: '/pages/index/index'
})
},
computed: {
totalMoods() {
return Object.keys(this.moodMap).length
},
moodStats() {
const stats = MOOD_LIST.map(item => ({
emoji: item.emoji,
label: item.label,
count: 0,
percent: 0,
color: item.color || this.themeConfig.primaryColor
}))
const monthKey = `${this.currentYear}-${String(this.currentMonth).padStart(2, '0')}`
Object.keys(this.moodMap).forEach(date => {
if (date.startsWith(monthKey)) {
const mood = this.moodMap[date]
const stat = stats.find(s => s.emoji === mood.emoji)
if (stat) stat.count++
}
})
const filteredStats = stats.filter(s => s.count > 0)
if (filteredStats.length === 0) return []
const maxCount = Math.max(...filteredStats.map(s => s.count))
const minHeight = 15
const maxHeight = 100
filteredStats.forEach(s => {
if (maxCount === 0) {
s.percent = minHeight
} else {
const relativeHeight = (s.count / maxCount) * (maxHeight - minHeight)
s.percent = minHeight + relativeHeight
}
})
return filteredStats
}
},
methods: {
initCalendar() {
const today = new Date()
this.selectedDate = this.formatDate(today)
this.generateCalendar()
},
formatDate(date) {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
},
generateCalendar() {
const year = this.currentYear
const month = this.currentMonth - 1
const firstDay = new Date(year, month, 1)
const lastDay = new Date(year, month + 1, 0)
const days = []
const startDay = firstDay.getDay()
const prevMonthLastDay = new Date(year, month, 0).getDate()
const paddingDays = startDay === 0 ? 6 : startDay - 1
for (let i = paddingDays - 1; i >= 0; i--) {
const day = prevMonthLastDay - i
const date = new Date(year, month - 1, day)
days.push({
day,
dateStr: this.formatDate(date),
isCurrentMonth: false,
isToday: false,
mood: this.moodMap[this.formatDate(date)]?.emoji || null
})
}
for (let i = 1; i <= lastDay.getDate(); i++) {
const date = new Date(year, month, i)
const dateStr = this.formatDate(date)
const today = new Date()
days.push({
day: i,
dateStr,
isCurrentMonth: true,
isToday: this.formatDate(today) === dateStr,
mood: this.moodMap[dateStr]?.emoji || null
})
}
const remaining = 42 - days.length
for (let i = 1; i <= remaining; i++) {
const date = new Date(year, month + 1, i)
days.push({
day: i,
dateStr: this.formatDate(date),
isCurrentMonth: false,
isToday: false,
mood: this.moodMap[this.formatDate(date)]?.emoji || null
})
}
this.calendarDays = days
},
prevMonth() {
if (this.currentMonth === 1) {
this.currentYear--
this.currentMonth = 12
} else {
this.currentMonth--
}
this.selectedDate = ''
this.selectedMood = null
this.moodMap = {}
this.generateCalendar()
this.loadMoods()
},
nextMonth() {
if (this.currentMonth === 12) {
this.currentYear++
this.currentMonth = 1
} else {
this.currentMonth++
}
this.selectedDate = ''
this.selectedMood = null
this.moodMap = {}
this.generateCalendar()
this.loadMoods()
},
goToday() {
const today = new Date()
this.currentYear = today.getFullYear()
this.currentMonth = today.getMonth() + 1
this.selectedDate = this.formatDate(today)
this.moodMap = {}
this.generateCalendar()
this.loadMoods()
},
goBack() {
uni.switchTab({
url: '/pages/index/index'
})
},
selectDate(day) {
this.selectedDate = day.dateStr
this.loadSelectedMood()
},
async loadMoods() {
try {
const firstDay = new Date(this.currentYear, this.currentMonth - 1, 1)
const lastDay = new Date(this.currentYear, this.currentMonth, 0)
const startDate = this.formatDate(firstDay)
const endDate = this.formatDate(lastDay)
const res = await getMoodList({ start_date:startDate,end_date: endDate })
if (res) {
res.forEach(item => {
this.moodMap[item.date] = {
emoji: item.emoji,
content: item.content,
label: this.getMoodLabel(item.emoji),
mood_id: item.mood_id,
ai_text: item.ai_text,
ai_image_url: item.ai_image_url
}
})
this.generateCalendar()
this.loadSelectedMood()
}
} catch (e) {
console.error('加载心情记录失败:', e)
}
},
async loadSelectedMood() {
if (this.moodMap[this.selectedDate]) {
this.selectedMood = this.moodMap[this.selectedDate]
} else {
this.selectedMood = null
}
},
closeContentModal() {
this.showContentModal = false
},
getMoodLabel(emoji) {
const mood = MOOD_LIST.find(item => item.emoji === emoji)
return mood ? mood.label : ''
},
getMoodColor(emoji) {
const mood = MOOD_LIST.find(item => item.emoji === emoji)
return mood?.color || this.themeConfig.primaryColor
},
onTouchStart(e) {
this.touchStartX = e.touches[0].clientX
this.isScrolling = false
},
onTouchMove(e) {
this.touchEndX = e.touches[0].clientX
this.touchDeltaX = this.touchEndX - this.touchStartX
this.isScrolling = true
},
onTouchEnd() {
if (!this.isScrolling) return
const threshold = 50
if (this.touchDeltaX > threshold) {
this.prevMonth()
uni.showToast({
title: `${this.currentYear}${this.currentMonth}月`,
icon: 'none',
duration: 1000
})
} else if (this.touchDeltaX < -threshold) {
this.nextMonth()
uni.showToast({
title: `${this.currentYear}${this.currentMonth}月`,
icon: 'none',
duration: 1000
})
}
this.touchDeltaX = 0
this.isScrolling = false
},
saveCalendarAiCard() {
console.log('保存日历AI卡片')
if (!this.selectedMood?.ai_text) return
uni.showLoading({
title: '生成图片中...'
})
const cardWidth = 750
const cardHeight = 1000
const contentLines = this.wrapTextForImage(this.selectedMood.ai_text, 24, cardWidth - 80)
const contentHeight = contentLines.length * 44
const contentStartY = Math.max(160, (cardHeight - contentHeight - 200) / 2)
const canvasCtx = uni.createCanvasContext('calendar-ai-canvas')
canvasCtx.setFillStyle('#fefcf9')
canvasCtx.fillRect(0, 0, cardWidth, cardHeight)
const topGradient = canvasCtx.createLinearGradient(0, 0, 0, cardHeight * 0.3)
topGradient.addColorStop(0, '#fff8f0')
topGradient.addColorStop(1, 'rgba(255, 248, 240, 0)')
canvasCtx.setFillStyle(topGradient)
canvasCtx.fillRect(0, 0, cardWidth, cardHeight * 0.3)
canvasCtx.setFillStyle('#5c544e')
canvasCtx.setFontSize(32)
canvasCtx.setTextAlign('left')
canvasCtx.fillText('今日心情', 40, 70)
canvasCtx.setFillStyle('#999189')
canvasCtx.setFontSize(22)
canvasCtx.fillText(this.selectedDate, 40, 100)
canvasCtx.setFontSize(56)
canvasCtx.setTextAlign('right')
canvasCtx.fillText(this.selectedMood.emoji || '😊', cardWidth - 40, 85)
canvasCtx.setStrokeStyle('rgba(0, 0, 0, 0.05)')
canvasCtx.beginPath()
canvasCtx.moveTo(40, 125)
canvasCtx.lineTo(cardWidth - 40, 125)
canvasCtx.stroke()
canvasCtx.setFillStyle('#4a443e')
canvasCtx.setFontSize(28)
canvasCtx.setTextAlign('left')
let y = contentStartY
contentLines.forEach(line => {
canvasCtx.fillText(line, 40, y)
y += 44
})
canvasCtx.setStrokeStyle('rgba(0, 0, 0, 0.05)')
canvasCtx.beginPath()
canvasCtx.moveTo(40, cardHeight - 60)
canvasCtx.lineTo(cardWidth - 40, cardHeight - 60)
canvasCtx.stroke()
canvasCtx.setFillStyle('#b8b0a8')
canvasCtx.setFontSize(16)
canvasCtx.setTextAlign('center')
canvasCtx.fillText(LOGO_CONFIG.name || '简记 memo', cardWidth / 2, cardHeight - 30)
canvasCtx.draw(true, () => {
setTimeout(() => {
uni.canvasToTempFilePath({
canvasId: 'calendar-ai-canvas',
fileType: 'png',
success: (res) => {
console.log('canvasToTempFilePath success:', res)
uni.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success: () => {
console.log('saveImageToPhotosAlbum success')
uni.hideLoading()
uni.showToast({
title: '图片已保存到相册',
icon: 'success'
})
},
fail: (err) => {
console.error('保存图片失败:', err)
uni.hideLoading()
uni.showToast({
title: '保存失败,请重试',
icon: 'none'
})
}
})
},
fail: (err) => {
console.error('生成图片失败:', err)
uni.hideLoading()
uni.showToast({
title: '生成图片失败',
icon: 'none'
})
}
})
}, 1000)
})
},
wrapText(text, fontSize, maxWidth) {
const lines = []
let currentLine = ''
const charWidth = fontSize * 0.6
for (let i = 0; i < text.length; i++) {
const char = text[i]
const testLine = currentLine + char
const testWidth = testLine.length * charWidth
if (testWidth > maxWidth && currentLine) {
lines.push(currentLine)
currentLine = char
} else {
currentLine = testLine
}
}
if (currentLine) {
lines.push(currentLine)
}
return lines
},
wrapTextForImage(text, maxCharsPerLine, maxWidth) {
const lines = []
let currentLine = ''
for (let i = 0; i < text.length; i++) {
const char = text[i]
if (char === '\n') {
lines.push(currentLine)
currentLine = ''
continue
}
const testLine = currentLine + char
if (testLine.length > maxCharsPerLine && currentLine) {
lines.push(currentLine)
currentLine = char
} else {
currentLine = testLine
}
}
if (currentLine) {
lines.push(currentLine)
}
return lines
}
}
}
</script>
<style lang="scss" scoped>
.calendar-page {
min-height: 100vh;
padding: 20rpx 24rpx;
padding-bottom: 40px;
box-sizing: border-box;
position: relative;
z-index: 1;
}
.calendar-header {
padding: 60rpx 24rpx 48rpx;
border-radius: 0 0 32rpx 32rpx;
position: relative;
margin-bottom: 20rpx;
}
.header-nav {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24rpx;
}
.nav-btn {
width: 80rpx;
height: 80rpx;
border-radius: 20rpx;
background: rgba(255, 255, 255, 0.25);
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.12);
backdrop-filter: blur(10px);
}
.nav-btn:active {
transform: scale(0.88);
background: rgba(255, 255, 255, 0.4);
}
.nav-icon {
font-size: 44rpx;
color: #fff;
font-weight: 300;
}
.home-btn {
position: relative;
}
.home-icon {
font-size: 40rpx;
}
.home-btn::after {
content: '首页';
position: absolute;
bottom: -24rpx;
left: 50%;
transform: translateX(-50%);
font-size: 20rpx;
color: rgba(255, 255, 255, 0.9);
white-space: nowrap;
}
.header-title {
display: flex;
flex-direction: column;
align-items: center;
gap: 4rpx;
}
.year-text {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.85);
}
.month-text {
font-size: 44rpx;
font-weight: 700;
color: #fff;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.15);
}
.today-btn {
display: flex;
justify-content: center;
}
.today-btn text {
padding: 12rpx 32rpx;
background: rgba(255, 255, 255, 0.2);
border-radius: 12rpx;
font-size: 26rpx;
color: #fff;
}
.calendar-card {
border-radius: 12rpx;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.08);
overflow: hidden;
margin-bottom: 24rpx;
}
.week-header {
display: flex;
padding: 24rpx 0;
background: rgba(0, 0, 0, 0.03);
border-bottom: 1rpx solid rgba(0, 0, 0, 0.06);
}
.week-day {
flex: 1;
text-align: center;
font-size: 24rpx;
color: var(--textSecondary, #9AA4B2);
font-weight: 500;
}
.calendar-grid {
display: flex;
flex-wrap: wrap;
padding: 8rpx 0;
}
.date-cell {
width: calc(100% / 7);
display: flex;
flex-direction: column;
align-items: center;
padding: 6rpx 0;
position: relative;
transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
min-height: 64rpx;
justify-content: center;
gap: 2rpx;
}
.date-cell:active {
transform: scale(0.94);
}
.date-cell.other-month {
opacity: 0.3;
}
.date-cell.today .date-num {
background: linear-gradient(135deg, var(--primaryColor, #6B9DFF) 0%, var(--secondaryColor, #9B59B6) 100%);
color: #fff;
box-shadow: 0 4rpx 20rpx rgba(107, 157, 255, 0.4);
animation: today-pulse 2.5s ease-in-out infinite;
}
.date-cell.selected {
background: rgba(107, 157, 255, 0.08);
border-radius: 20rpx;
}
.date-cell.selected .date-num {
background: var(--primaryColor, #6B9DFF);
color: #fff;
box-shadow: 0 2rpx 12rpx rgba(107, 157, 255, 0.3);
}
.date-num {
width: 68rpx;
height: 68rpx;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
color: var(--textPrimary, #2A3A4D);
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
font-weight: 500;
background: rgba(0, 0, 0, 0.02);
}
.date-cell:not(.other-month):not(.selected) .date-num:active {
background: rgba(107, 157, 255, 0.15);
transform: scale(0.92);
}
.mood-emoji {
font-size: 28rpx;
margin-top: 4rpx;
transition: transform 0.2s ease;
}
.date-cell.has-mood .mood-emoji {
animation: bounce 1.5s ease-in-out infinite;
}
.mood-dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
margin-top: 2rpx;
box-shadow: 0 0 10rpx currentColor;
}
.date-cell.has-mood {
background: rgba(107, 157, 255, 0.04);
border-radius: 16rpx;
}
@keyframes today-pulse {
0%, 100% {
box-shadow: 0 0 0 0 rgba(107, 157, 255, 0.4);
}
50% {
box-shadow: 0 0 0 12rpx rgba(107, 157, 255, 0);
}
}
@keyframes bounce {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.15);
}
}
.mood-detail {
padding: 16rpx 0;
}
.detail-card {
border-radius: 12rpx;
padding: 28rpx;
box-shadow: 0 6rpx 24rpx rgba(0, 0, 0, 0.08);
background: linear-gradient(145deg, var(--cardBgColor, #ffffff) 0%, rgba(255, 255, 255, 0.95) 100%);
}
.detail-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20rpx;
}
.detail-date {
font-size: 28rpx;
font-weight: 600;
}
.detail-mood {
display: flex;
align-items: center;
gap: 8rpx;
padding: 8rpx 20rpx;
border-radius: 20rpx;
}
.detail-emoji {
font-size: 28rpx;
}
.detail-label {
font-size: 24rpx;
color: #fff;
font-weight: 500;
}
.detail-content {
padding-top: 16rpx;
border-top: 1rpx solid rgba(0, 0, 0, 0.06);
}
.content-text {
font-size: 28rpx;
line-height: 1.6;
}
.detail-empty {
padding-top: 16rpx;
border-top: 1rpx solid rgba(0, 0, 0, 0.06);
text-align: center;
}
.empty-text {
font-size: 26rpx;
}
.month-stats {
padding: 16rpx 0 60rpx;
}
.stats-card {
border-radius: 12rpx;
padding: 28rpx;
box-shadow: 0 6rpx 24rpx rgba(0, 0, 0, 0.08);
background: linear-gradient(145deg, var(--cardBgColor, #ffffff) 0%, rgba(255, 255, 255, 0.95) 100%);
}
.stats-title {
font-size: 28rpx;
font-weight: 600;
margin-bottom: 20rpx;
}
.stats-chart {
display: flex;
align-items: flex-end;
justify-content: space-around;
height: 240rpx;
padding: 40rpx 16rpx 24rpx;
border-bottom: 1rpx solid rgba(0, 0, 0, 0.06);
background: rgba(0, 0, 0, 0.02);
margin: 0 -28rpx;
margin-bottom: 0;
padding-left: 28rpx;
padding-right: 28rpx;
}
.chart-bar {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 60rpx;
min-height: 40rpx;
max-height: 180rpx;
border-radius: 16rpx 16rpx 10rpx 10rpx;
position: relative;
animation: bar-grow 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
transition: height 0.4s ease;
}
@keyframes bar-grow {
from {
height: 40rpx;
opacity: 0;
}
to {
opacity: 1;
}
}
.bar-emoji {
position: absolute;
top: -44rpx;
font-size: 36rpx;
animation: float 2s ease-in-out infinite;
}
@keyframes float {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-6rpx);
}
}
.bar-count {
font-size: 28rpx;
color: #ffffff;
font-weight: 600;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.3);
position: relative;
z-index: 1;
}
.stats-summary {
padding-top: 16rpx;
text-align: center;
}
.summary-text {
font-size: 24rpx;
}
.stats-empty {
padding: 48rpx 24rpx 24rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8rpx;
}
.empty-hint-circle {
width: 96rpx;
height: 96rpx;
border-radius: 50%;
background: rgba(155, 138, 251, 0.1);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 8rpx;
animation: hintPulse 2.4s ease-in-out infinite;
}
@keyframes hintPulse {
0%, 100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(155, 138, 251, 0.3); }
50% { transform: scale(1.05); box-shadow: 0 0 0 12rpx rgba(155, 138, 251, 0); }
}
.empty-hint-text {
font-size: 26rpx;
font-weight: 500;
letter-spacing: 0.5rpx;
}
.empty-hint-sub {
font-size: 22rpx;
opacity: 0.7;
letter-spacing: 0.5rpx;
}
/* 内容预览和弹窗样式 */
.content-preview {
padding-top: 16rpx;
border-top: 1rpx solid rgba(0, 0, 0, 0.06);
}
.preview-text {
font-size: 26rpx;
line-height: 1.6;
display: block;
margin-bottom: 12rpx;
word-wrap: break-word;
word-break: break-word;
overflow-wrap: break-word;
max-width: 100%;
box-sizing: border-box;
}
.view-more-btn {
display: flex;
justify-content: center;
padding: 8rpx 0;
}
.view-more-text {
font-size: 26rpx;
font-weight: 500;
}
/* 内容展示弹窗 */
.content-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 9999;
display: flex;
align-items: flex-end;
justify-content: center;
animation: fade-in 0.2s ease;
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.modal-content {
width: 100%;
max-height: 70vh;
border-radius: 12rpx 12rpx 0 0;
padding: 32rpx 24rpx;
box-sizing: border-box;
animation: slide-up 0.3s ease;
word-wrap: break-word;
word-break: break-word;
overflow-wrap: break-word;
}
@keyframes slide-up {
from {
transform: translateY(100%);
}
to {
transform: translateY(0);
}
}
.modal-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 24rpx;
flex-wrap: wrap;
}
.modal-date-row {
display: flex;
align-items: center;
gap: 16rpx;
flex: 1;
min-width: 0;
}
.modal-date {
font-size: 28rpx;
font-weight: 600;
word-wrap: break-word;
word-break: break-word;
}
.modal-mood-badge {
display: flex;
align-items: center;
gap: 6rpx;
padding: 6rpx 16rpx;
border-radius: 16rpx;
flex-shrink: 0;
}
.modal-emoji {
font-size: 24rpx;
}
.modal-label {
font-size: 22rpx;
color: #fff;
}
.modal-close {
width: 56rpx;
height: 56rpx;
border-radius: 14rpx;
background: rgba(0, 0, 0, 0.06);
display: flex;
align-items: center;
justify-content: center;
font-size: 36rpx;
color: var(--textSecondary, #9AA4B2);
flex-shrink: 0;
}
.modal-scroll {
max-height: 400rpx;
padding: 16rpx 0;
word-wrap: break-word;
word-break: break-word;
overflow-wrap: break-word;
}
.modal-text {
font-size: 28rpx;
line-height: 1.8;
display: block;
word-wrap: break-word;
word-break: break-word;
overflow-wrap: break-word;
max-width: 100%;
box-sizing: border-box;
white-space: pre-wrap;
}
.modal-text-wrapper {
margin-bottom: 24rpx;
}
.ai-section {
border-top: 1rpx solid rgba(0, 0, 0, 0.06);
padding-top: 24rpx;
}
.ai-section-header {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 20rpx;
}
.ai-section-icon {
display: flex;
align-items: center;
justify-content: center;
}
.ai-section-title {
font-size: 28rpx;
font-weight: 500;
color: #4a443e;
letter-spacing: 2rpx;
}
.ai-card {
position: relative;
border-radius: 12rpx;
overflow: hidden;
background: linear-gradient(135deg, #fff 0%, #fefcf9 100%);
border: 1rpx solid rgba(0, 0, 0, 0.03);
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.03);
}
.ai-card-bg {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 60%;
background: linear-gradient(180deg, rgba(255, 249, 240, 0.6) 0%, transparent 100%);
pointer-events: none;
}
.ai-card-content {
position: relative;
z-index: 1;
padding: 28rpx;
}
.ai-card-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 16rpx;
}
.ai-card-date {
font-size: 22rpx;
color: #b8b0a8;
}
.ai-card-mood {
font-size: 36rpx;
}
.ai-card-divider {
height: 1rpx;
background: linear-gradient(90deg, transparent, rgba(0, 0, 0, 0.04), transparent);
margin: 12rpx 0;
}
.ai-card-text {
min-height: 100rpx;
display: flex;
align-items: center;
justify-content: center;
}
.ai-card-text text {
font-size: 26rpx;
color: #4a443e;
line-height: 1.8;
letter-spacing: 1rpx;
text-align: left;
display: block;
white-space: pre-wrap;
word-break: break-word;
}
.ai-card-footer {
margin-top: 16rpx;
text-align: center;
}
.ai-card-hint {
font-size: 20rpx;
color: #c4b8ad;
}
.ai-section-actions {
margin-top: 20rpx;
display: flex;
justify-content: center;
}
.ai-save-btn {
display: flex;
align-items: center;
gap: 8rpx;
padding: 16rpx 32rpx;
background: linear-gradient(135deg, #f5f0e8 0%, #ede8df 100%);
border-radius: 14rpx;
border: 1rpx solid rgba(0, 0, 0, 0.04);
}
.ai-save-btn:active {
transform: scale(0.98);
}
.save-icon {
display: flex;
align-items: center;
justify-content: center;
}
.save-text {
font-size: 26rpx;
color: #5c544e;
font-weight: 500;
}
.ai-canvas {
position: fixed;
top: -9999rpx;
left: -9999rpx;
width: 750rpx;
height: 1000rpx;
}
</style>