1349 lines
30 KiB
Vue
1349 lines
30 KiB
Vue
<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>
|