1129 lines
25 KiB
Vue
1129 lines
25 KiB
Vue
<template>
|
||
<view class="mood-page page-enter theme-transition" :style="{ background: themeConfig.bgColor }">
|
||
<PageHeader
|
||
title="记录心情"
|
||
subtitle="记录每一天的心情"
|
||
:show-back="false"
|
||
:show-right="true"
|
||
>
|
||
<template #right>
|
||
<view
|
||
class="header-calendar-btn"
|
||
@click="goToCalendar"
|
||
:style="{ background: themeConfig.cardBgColor, boxShadow: `0 4rpx 16rpx ${themeConfig.shadowLight}` }"
|
||
>
|
||
<FaIcon icon="calendar" :size="22" :color="themeConfig.primaryColor" />
|
||
</view>
|
||
</template>
|
||
</PageHeader>
|
||
|
||
<scroll-view
|
||
class="main-content"
|
||
scroll-y
|
||
:show-scrollbar="false"
|
||
>
|
||
<view class="content-wrap">
|
||
|
||
<!-- 心情录入卡片 -->
|
||
<AppCard class="input-card card-enter" :padding="24" :radius="20">
|
||
<view class="input-title" :style="{ color: themeConfig.textPrimary }">记录此刻心情</view>
|
||
<view class="input-subtitle" :style="{ color: themeConfig.textSecondary }">选择表情并写下此刻的感受</view>
|
||
|
||
<!-- 表情选择器 5 列网格 -->
|
||
<view class="mood-selector">
|
||
<view
|
||
v-for="item in MOOD_LIST"
|
||
:key="item.emoji"
|
||
class="mood-emoji"
|
||
:class="{ active: selectedMood === item.emoji }"
|
||
@click="selectMood(item)"
|
||
>
|
||
<view class="mood-circle" :class="{ 'mood-circle--active': selectedMood === item.emoji }">
|
||
<text class="emoji-text" :class="{ 'emoji-text--active': selectedMood === item.emoji }">{{ item.emoji }}</text>
|
||
</view>
|
||
<text class="mood-label" :style="{ color: themeConfig.textSecondary }">{{ item.label }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 快捷短语 -->
|
||
<view class="phrase-section" v-if="currentPhrases.length > 0">
|
||
<text class="phrase-label" :style="{ color: themeConfig.textTertiary }">快捷短语</text>
|
||
<view class="phrase-list">
|
||
<view
|
||
v-for="(phrase, idx) in currentPhrases"
|
||
:key="idx"
|
||
class="phrase-tag"
|
||
:style="{ background: themeConfig.primaryColor + '15' }"
|
||
@click="appendPhrase(phrase)"
|
||
>
|
||
<text class="phrase-text" :style="{ color: themeConfig.textSecondary }">{{ phrase }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 文本输入 -->
|
||
<view class="textarea-wrap" :style="{ background: themeConfig.bgColor }">
|
||
<textarea
|
||
v-model="moodContent"
|
||
class="mood-textarea"
|
||
placeholder="今天发生了什么?有什么想记录的..."
|
||
placeholder-class="input-placeholder"
|
||
:maxlength="200"
|
||
:style="{ color: themeConfig.textPrimary }"
|
||
/>
|
||
<text class="char-count" :style="{ color: themeConfig.textTertiary }">{{ moodContent.length }}/200</text>
|
||
</view>
|
||
|
||
<!-- 保存按钮 -->
|
||
<view
|
||
class="save-btn btn btn--block"
|
||
:class="{ 'save-btn--disabled': !selectedMood || saving }"
|
||
:style="saveBtnStyle"
|
||
@click="handleSaveMood"
|
||
>
|
||
<text class="save-btn__text">{{ saving ? '保存中...' : '保存心情' }}</text>
|
||
</view>
|
||
</AppCard>
|
||
|
||
<!-- 页面内平铺心语小笺(已有记录 + AI 内容时) -->
|
||
<view class="ai-section card-enter" v-if="todayMood && todayMood.ai_text">
|
||
<view class="ai-section-header">
|
||
<FaIcon icon="leaf" :size="22" :color="themeConfig.primaryColor" />
|
||
<text class="ai-section-title" :style="{ color: themeConfig.textPrimary }">心语小笺</text>
|
||
</view>
|
||
<AppCard :padding="28" :radius="20" :showShadow="true">
|
||
<view class="ai-card-content">
|
||
<view class="ai-card-header">
|
||
<text class="ai-card-date">{{ todayStr }}</text>
|
||
<text class="ai-card-mood">{{ todayMood.emoji }}</text>
|
||
</view>
|
||
<view class="ai-card-text">
|
||
<text :style="{ color: themeConfig.textPrimary }">{{ todayMood.ai_text }}</text>
|
||
</view>
|
||
<view class="ai-card-footer">
|
||
<text class="ai-card-hint">AI 创作</text>
|
||
</view>
|
||
</view>
|
||
</AppCard>
|
||
<view class="ai-section-actions">
|
||
<view
|
||
class="ai-inline-btn ai-inline-refresh"
|
||
@click="refreshInlineAi"
|
||
>
|
||
<text class="inline-btn-text">换一换</text>
|
||
</view>
|
||
<view
|
||
class="ai-inline-btn ai-inline-save"
|
||
@click="saveAiCard(todayMood.ai_text)"
|
||
>
|
||
<text class="inline-btn-text inline-btn-text-save">保存图片</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="bottom-spacer"></view>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<!-- AI 心语小笺弹窗 -->
|
||
<CenterModal
|
||
:visible="showAiModal"
|
||
title="心语小笺"
|
||
:show-cancel="false"
|
||
:show-confirm="false"
|
||
@close="closeAiModal"
|
||
>
|
||
<view class="ai-modal-body">
|
||
<!-- loading -->
|
||
<view class="ai-loading" v-if="aiLoading">
|
||
<view class="loading-spinner" :style="{ borderColor: themeConfig.primaryColor + '33', borderTopColor: themeConfig.primaryColor }"></view>
|
||
<text class="loading-text" :style="{ color: themeConfig.textSecondary }">AI 正在为你创作中...</text>
|
||
</view>
|
||
|
||
<!-- 内容 -->
|
||
<template v-else>
|
||
<view class="ai-modal-card">
|
||
<view class="ai-modal-bg"></view>
|
||
<view class="ai-modal-inner">
|
||
<view class="ai-modal-header">
|
||
<text class="ai-modal-date">{{ todayStr }}</text>
|
||
<text class="ai-modal-mood">{{ selectedMood || todayMood?.emoji || '😊' }}</text>
|
||
</view>
|
||
<view class="ai-modal-divider"></view>
|
||
<view class="ai-modal-text">
|
||
<text>{{ modalAiText }}</text>
|
||
</view>
|
||
<view class="ai-modal-footer">
|
||
<text class="ai-modal-hint">AI 创作</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
</view>
|
||
|
||
<!-- 弹窗底部按钮:移到 footer slot 保证可见 -->
|
||
<template #footer>
|
||
<view class="ai-modal-actions">
|
||
<view
|
||
class="ai-action-btn ai-action-secondary"
|
||
:style="{ background: themeConfig.inputBgColor || 'rgba(0,0,0,0.06)', color: themeConfig.textPrimary }"
|
||
@click="closeAiModal"
|
||
>
|
||
<text class="ai-action-btn__text" :style="{ color: themeConfig.textPrimary }">关闭</text>
|
||
</view>
|
||
<view
|
||
class="ai-action-btn ai-action-primary"
|
||
:style="aiRefreshBtnStyle"
|
||
v-if="!aiLoading"
|
||
@click="handleRefreshAi"
|
||
>
|
||
<text class="ai-action-btn__text">换一换</text>
|
||
</view>
|
||
<view
|
||
class="ai-action-btn ai-action-primary"
|
||
:style="aiSaveBtnStyle"
|
||
v-if="!aiLoading"
|
||
@click="saveAiCard(modalAiText)"
|
||
>
|
||
<text class="ai-action-btn__text">保存卡片</text>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
</CenterModal>
|
||
|
||
<GrowthModal ref="growthModal" />
|
||
|
||
<canvas
|
||
canvas-id="moodAiCanvas"
|
||
id="moodAiCanvas"
|
||
class="mood-canvas"
|
||
style="width: 750px; height: 1000px;"
|
||
></canvas>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import PageHeader from '@/components/PageHeader.vue'
|
||
import AppCard from '@/components/AppCard.vue'
|
||
import FaIcon from '@/components/FaIcon.vue'
|
||
import GrowthModal from '@/components/GrowthModal.vue'
|
||
import CenterModal from '@/components/CenterModal.vue'
|
||
|
||
import {
|
||
getMoodByDate,
|
||
saveMood,
|
||
getAiMoodResult,
|
||
regenerateAiMood
|
||
} from '@/api'
|
||
import themeMixin from '@/mixins/themeMixin.js'
|
||
import growthModalMixin from '@/mixins/growthModalMixin.js'
|
||
import { MOOD_LIST } from '@/utils/constants'
|
||
|
||
const POLL_INTERVAL = 2000
|
||
const MAX_POLL_COUNT = 30
|
||
|
||
export default {
|
||
name: 'MoodPage',
|
||
components: {
|
||
PageHeader,
|
||
AppCard,
|
||
FaIcon,
|
||
GrowthModal,
|
||
CenterModal
|
||
},
|
||
mixins: [themeMixin, growthModalMixin],
|
||
data() {
|
||
return {
|
||
MOOD_LIST,
|
||
todayMood: null,
|
||
selectedMood: '',
|
||
moodContent: '',
|
||
currentPhrases: [],
|
||
saving: false,
|
||
loading: false,
|
||
todayStr: '',
|
||
showAiModal: false,
|
||
aiLoading: false,
|
||
modalAiText: '',
|
||
currentMoodId: null,
|
||
pollTimer: null,
|
||
pollCount: 0
|
||
}
|
||
},
|
||
computed: {
|
||
/**
|
||
* 保存心情按钮样式:跟随主题渐变,禁用态半透明
|
||
*/
|
||
saveBtnStyle() {
|
||
const disabled = !this.selectedMood || this.saving
|
||
return {
|
||
background: disabled ? 'rgba(0, 0, 0, 0.06)' : this.themeConfig.gradientSoft,
|
||
color: disabled ? this.themeConfig.textTertiary : '#ffffff',
|
||
boxShadow: disabled ? 'none' : `0 6rpx 20rpx ${this.themeConfig.shadowColor || 'rgba(198,142,63,0.35)'}`,
|
||
pointerEvents: disabled ? 'none' : 'auto'
|
||
}
|
||
},
|
||
/**
|
||
* AI 弹窗中"换一换"按钮:主题渐变
|
||
*/
|
||
aiRefreshBtnStyle() {
|
||
return {
|
||
background: this.themeConfig.gradientSoft,
|
||
color: '#ffffff',
|
||
boxShadow: `0 4rpx 12rpx ${this.themeConfig.shadowColor || 'rgba(198,142,63,0.35)'}`
|
||
}
|
||
},
|
||
/**
|
||
* AI 弹窗中"保存卡片"按钮:纯主题色填充
|
||
*/
|
||
aiSaveBtnStyle() {
|
||
return {
|
||
background: this.themeConfig.primaryColor,
|
||
color: '#ffffff',
|
||
boxShadow: `0 4rpx 12rpx ${this.themeConfig.shadowColor || 'rgba(198,142,63,0.35)'}`
|
||
}
|
||
}
|
||
},
|
||
onLoad() {
|
||
this.initToday()
|
||
this.loadTodayMood()
|
||
},
|
||
onShow() {
|
||
this.loadTodayMood()
|
||
},
|
||
onUnload() {
|
||
this.clearPoll()
|
||
},
|
||
methods: {
|
||
initToday() {
|
||
const now = new Date()
|
||
this.todayStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
|
||
},
|
||
async loadTodayMood() {
|
||
this.loading = true
|
||
try {
|
||
const res = await getMoodByDate({ date: this.todayStr })
|
||
const data = res?.data || res
|
||
if (data && data.emoji) {
|
||
this.todayMood = data
|
||
this.selectedMood = data.emoji
|
||
this.moodContent = data.content || ''
|
||
// 用 ?? 而不是 ||,避免 mood_id=0 时被误判为 null
|
||
this.currentMoodId = data.mood_id ?? data.id ?? null
|
||
const moodItem = MOOD_LIST.find(m => m.emoji === data.emoji)
|
||
this.currentPhrases = moodItem?.phrases || []
|
||
} else {
|
||
this.todayMood = null
|
||
if (!this.selectedMood) {
|
||
this.currentPhrases = []
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.error('加载今日心情失败:', e)
|
||
this.todayMood = null
|
||
} finally {
|
||
this.loading = false
|
||
}
|
||
},
|
||
selectMood(item) {
|
||
this.selectedMood = item.emoji
|
||
this.currentPhrases = item.phrases || []
|
||
},
|
||
appendPhrase(phrase) {
|
||
if (!this.moodContent) {
|
||
this.moodContent = phrase
|
||
} else {
|
||
this.moodContent = this.moodContent + ',' + phrase
|
||
}
|
||
},
|
||
async handleSaveMood() {
|
||
if (!this.selectedMood) {
|
||
uni.showToast({ title: '请选择心情', icon: 'none' })
|
||
return
|
||
}
|
||
this.saving = true
|
||
try {
|
||
const res = await saveMood({
|
||
date: this.todayStr,
|
||
emoji: this.selectedMood,
|
||
content: this.moodContent.trim()
|
||
})
|
||
uni.showToast({ title: '保存成功', icon: 'success' })
|
||
|
||
const data = res?.data || res
|
||
// 用 ?? 而不是 ||,避免 mood_id=0 时被误判为 null
|
||
const moodId = data?.mood_id ?? data?.id ?? null
|
||
this.currentMoodId = moodId
|
||
|
||
await this.loadTodayMood()
|
||
|
||
// 打开 AI 弹窗
|
||
this.showAiModal = true
|
||
const aiText = data?.ai_text || this.todayMood?.ai_text
|
||
if (aiText) {
|
||
this.modalAiText = aiText
|
||
this.aiLoading = false
|
||
} else {
|
||
// 没有 ai_text 时,根据逻辑查询接口(轮询 AI 结果)而不是直接显示"暂无内容"
|
||
// 优先用保存返回的 moodId,否则用 todayMood 上的 ID,最后退回到主键 ID
|
||
const pollId = (moodId != null)
|
||
? moodId
|
||
: (this.todayMood?.mood_id ?? this.todayMood?.id ?? this.todayMood?.ID ?? null)
|
||
if (pollId != null) {
|
||
this.aiLoading = true
|
||
this.modalAiText = ''
|
||
this.startPollAi(pollId)
|
||
} else {
|
||
this.aiLoading = false
|
||
this.modalAiText = '暂无 AI 创作内容'
|
||
}
|
||
}
|
||
|
||
if (res?.growth) {
|
||
this.$nextTick(() => {
|
||
this.showGrowthModal(res.growth)
|
||
})
|
||
}
|
||
} catch (e) {
|
||
console.error('保存心情失败:', e)
|
||
uni.showToast({ title: '保存失败,请重试', icon: 'none' })
|
||
} finally {
|
||
this.saving = false
|
||
}
|
||
},
|
||
goToCalendar() {
|
||
uni.navigateTo({ url: '/pages/moodCalendar/moodCalendar' })
|
||
},
|
||
closeAiModal() {
|
||
this.showAiModal = false
|
||
this.clearPoll()
|
||
},
|
||
startPollAi(moodId) {
|
||
this.clearPoll()
|
||
this.pollCount = 0
|
||
this.aiLoading = true
|
||
this.modalAiText = ''
|
||
|
||
const doPoll = async () => {
|
||
if (!this.showAiModal) {
|
||
this.clearPoll()
|
||
return
|
||
}
|
||
this.pollCount++
|
||
if (this.pollCount > MAX_POLL_COUNT) {
|
||
this.aiLoading = false
|
||
this.modalAiText = 'AI 创作超时,请稍后重试'
|
||
this.clearPoll()
|
||
return
|
||
}
|
||
try {
|
||
const res = await getAiMoodResult(moodId)
|
||
const data = res?.data || res
|
||
if (data?.status === 1 && data?.ai_text) {
|
||
this.modalAiText = data.ai_text
|
||
this.aiLoading = false
|
||
this.clearPoll()
|
||
return
|
||
}
|
||
if (data?.status === -1) {
|
||
this.aiLoading = false
|
||
this.modalAiText = 'AI 创作失败,请重试'
|
||
this.clearPoll()
|
||
return
|
||
}
|
||
} catch (e) {
|
||
console.error('轮询 AI 结果失败:', e)
|
||
}
|
||
this.pollTimer = setTimeout(doPoll, POLL_INTERVAL)
|
||
}
|
||
|
||
doPoll()
|
||
},
|
||
clearPoll() {
|
||
if (this.pollTimer) {
|
||
clearTimeout(this.pollTimer)
|
||
this.pollTimer = null
|
||
}
|
||
},
|
||
async handleRefreshAi() {
|
||
if (this.currentMoodId == null) {
|
||
uni.showToast({ title: '暂无心情记录', icon: 'none' })
|
||
return
|
||
}
|
||
this.aiLoading = true
|
||
this.modalAiText = ''
|
||
try {
|
||
await regenerateAiMood(this.currentMoodId)
|
||
this.startPollAi(this.currentMoodId)
|
||
} catch (e) {
|
||
console.error('重新生成 AI 失败:', e)
|
||
uni.showToast({ title: '重新生成失败', icon: 'none' })
|
||
this.aiLoading = false
|
||
}
|
||
},
|
||
saveAiCard(aiText) {
|
||
const text = aiText || this.modalAiText
|
||
if (!text) {
|
||
uni.showToast({ title: '暂无卡片内容', icon: 'none' })
|
||
return
|
||
}
|
||
uni.showLoading({ title: '生成中...' })
|
||
|
||
const cardWidth = 750
|
||
const cardHeight = 900
|
||
const ctx = uni.createCanvasContext('moodAiCanvas')
|
||
const padding = 50
|
||
|
||
ctx.setFillStyle('#fefcf9')
|
||
ctx.fillRect(0, 0, cardWidth, cardHeight)
|
||
|
||
const topGrad = ctx.createLinearGradient(0, 0, 0, cardHeight * 0.35)
|
||
topGrad.addColorStop(0, 'rgba(255, 248, 240, 0.7)')
|
||
topGrad.addColorStop(1, 'rgba(255, 248, 240, 0)')
|
||
ctx.setFillStyle(topGrad)
|
||
ctx.fillRect(0, 0, cardWidth, cardHeight * 0.35)
|
||
|
||
ctx.setFillStyle('rgba(196, 184, 173, 0.12)')
|
||
ctx.fillRect(0, 0, 6, cardHeight)
|
||
|
||
ctx.setFillStyle('#b8b0a8')
|
||
ctx.setFontSize(22)
|
||
ctx.setTextAlign('left')
|
||
ctx.fillText(this.todayStr, padding, 80)
|
||
|
||
ctx.setFontSize(64)
|
||
ctx.setTextAlign('right')
|
||
ctx.fillText(this.selectedMood || this.todayMood?.emoji || '😊', cardWidth - padding, 90)
|
||
|
||
ctx.setStrokeStyle('rgba(0, 0, 0, 0.05)')
|
||
ctx.setLineWidth(1)
|
||
ctx.beginPath()
|
||
ctx.moveTo(padding, 110)
|
||
ctx.lineTo(cardWidth - padding, 110)
|
||
ctx.stroke()
|
||
|
||
const contentText = text
|
||
const maxWidth = cardWidth - padding * 2
|
||
const lineHeight = 48
|
||
ctx.setFillStyle('#4a443e')
|
||
ctx.setFontSize(28)
|
||
ctx.setTextAlign('left')
|
||
|
||
let y = 180
|
||
let currentLine = ''
|
||
for (let i = 0; i < text.length; i++) {
|
||
const testLine = currentLine + text[i]
|
||
const metrics = ctx.measureText(testLine)
|
||
if (metrics.width > maxWidth && currentLine.length > 0) {
|
||
ctx.fillText(currentLine, padding, y)
|
||
currentLine = text[i]
|
||
y += lineHeight
|
||
} else {
|
||
currentLine = testLine
|
||
}
|
||
}
|
||
if (currentLine) {
|
||
ctx.fillText(currentLine, padding, y)
|
||
}
|
||
|
||
const footerY = cardHeight - 100
|
||
ctx.setStrokeStyle('rgba(0, 0, 0, 0.04)')
|
||
ctx.setLineWidth(1)
|
||
ctx.beginPath()
|
||
ctx.moveTo(padding, footerY)
|
||
ctx.lineTo(cardWidth - padding, footerY)
|
||
ctx.stroke()
|
||
|
||
ctx.setFillStyle('#c4b8ad')
|
||
ctx.setFontSize(20)
|
||
ctx.setTextAlign('center')
|
||
ctx.fillText('AI 创作 · 简记 memo', cardWidth / 2, footerY + 40)
|
||
|
||
ctx.draw(true, () => {
|
||
setTimeout(() => {
|
||
uni.canvasToTempFilePath({
|
||
canvasId: 'moodAiCanvas',
|
||
fileType: 'png',
|
||
quality: 1,
|
||
success: (res) => {
|
||
uni.saveImageToPhotosAlbum({
|
||
filePath: res.tempFilePath,
|
||
success: () => {
|
||
uni.hideLoading()
|
||
uni.showToast({ title: '已保存到相册', icon: 'success' })
|
||
},
|
||
fail: (err) => {
|
||
console.error('保存图片失败:', err)
|
||
uni.hideLoading()
|
||
if (err.errMsg?.includes('auth deny')) {
|
||
uni.showModal({
|
||
title: '保存失败',
|
||
content: '请在设置中允许保存图片到相册',
|
||
showCancel: false
|
||
})
|
||
} else {
|
||
uni.showToast({ title: '保存失败', icon: 'none' })
|
||
}
|
||
}
|
||
})
|
||
},
|
||
fail: (err) => {
|
||
console.error('生成图片失败:', err)
|
||
uni.hideLoading()
|
||
uni.showToast({ title: '生成失败', icon: 'none' })
|
||
}
|
||
})
|
||
}, 300)
|
||
})
|
||
},
|
||
async refreshInlineAi() {
|
||
if (this.currentMoodId == null) {
|
||
uni.showToast({ title: '暂无心情记录', icon: 'none' })
|
||
return
|
||
}
|
||
try {
|
||
uni.showLoading({ title: '生成中...' })
|
||
await regenerateAiMood(this.currentMoodId)
|
||
// 轮询等待新结果
|
||
const result = await this.pollAiOnce(this.currentMoodId)
|
||
uni.hideLoading()
|
||
if (result) {
|
||
this.todayMood.ai_text = result
|
||
} else {
|
||
uni.showToast({ title: 'AI 生成失败', icon: 'none' })
|
||
}
|
||
} catch (e) {
|
||
uni.hideLoading()
|
||
console.error('重新生成 AI 失败:', e)
|
||
uni.showToast({ title: '重新生成失败', icon: 'none' })
|
||
}
|
||
},
|
||
pollAiOnce(moodId) {
|
||
return new Promise((resolve) => {
|
||
let count = 0
|
||
const maxCount = 30
|
||
const doPoll = async () => {
|
||
count++
|
||
if (count > maxCount) {
|
||
resolve(null)
|
||
return
|
||
}
|
||
try {
|
||
const res = await getAiMoodResult(moodId)
|
||
const data = res?.data || res
|
||
if (data?.status === 1 && data?.ai_text) {
|
||
resolve(data.ai_text)
|
||
return
|
||
}
|
||
if (data?.status === -1) {
|
||
resolve(null)
|
||
return
|
||
}
|
||
} catch (e) {
|
||
console.error('轮询 AI 失败:', e)
|
||
}
|
||
setTimeout(doPoll, POLL_INTERVAL)
|
||
}
|
||
doPoll()
|
||
})
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.mood-page {
|
||
min-height: 100vh;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.header-calendar-btn {
|
||
width: 60rpx;
|
||
height: 60rpx;
|
||
border-radius: 18rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.header-calendar-btn:active {
|
||
transform: scale(0.9);
|
||
}
|
||
|
||
.main-content {
|
||
height: calc(100vh - 180rpx);
|
||
position: relative;
|
||
z-index: 5;
|
||
}
|
||
|
||
.content-wrap {
|
||
padding: 0 32rpx;
|
||
padding-bottom: 40rpx;
|
||
}
|
||
|
||
/* 心情录入卡片 */
|
||
.input-card {
|
||
margin-top: 16rpx;
|
||
margin-bottom: 24rpx;
|
||
}
|
||
|
||
.input-title {
|
||
font-size: 32rpx;
|
||
font-weight: 600;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.input-subtitle {
|
||
font-size: 26rpx;
|
||
margin-bottom: 24rpx;
|
||
}
|
||
|
||
/* 表情选择器 5 列网格 */
|
||
.mood-selector {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 0;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.mood-emoji {
|
||
width: 20%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 8rpx;
|
||
padding: 10rpx 0 14rpx 0;
|
||
}
|
||
|
||
.mood-circle {
|
||
width: 96rpx;
|
||
height: 96rpx;
|
||
border-radius: 24rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: transparent;
|
||
transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||
}
|
||
|
||
.mood-circle--active {
|
||
background: #ffffff;
|
||
box-shadow: 0 0 0 4rpx rgba(0, 0, 0, 0.05), 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
|
||
}
|
||
|
||
.emoji-text {
|
||
font-size: 46rpx;
|
||
line-height: 1;
|
||
transition: font-size 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||
}
|
||
|
||
.emoji-text--active {
|
||
font-size: 56rpx;
|
||
}
|
||
|
||
.mood-emoji:active .mood-circle {
|
||
transform: scale(0.9);
|
||
}
|
||
|
||
.mood-emoji:active .emoji-text {
|
||
transform: scale(0.9);
|
||
}
|
||
|
||
.mood-label {
|
||
font-size: 22rpx;
|
||
}
|
||
|
||
/* 快捷短语 */
|
||
.phrase-section {
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.phrase-label {
|
||
font-size: 24rpx;
|
||
margin-bottom: 12rpx;
|
||
display: block;
|
||
}
|
||
|
||
.phrase-list {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 12rpx;
|
||
}
|
||
|
||
.phrase-tag {
|
||
padding: 12rpx 24rpx;
|
||
border-radius: 14rpx;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.phrase-tag:active {
|
||
transform: scale(0.95);
|
||
}
|
||
|
||
.phrase-text {
|
||
font-size: 26rpx;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* 文本输入 */
|
||
.textarea-wrap {
|
||
position: relative;
|
||
border-radius: 16rpx;
|
||
padding: 20rpx 20rpx 50rpx 20rpx;
|
||
margin-bottom: 24rpx;
|
||
}
|
||
|
||
.mood-textarea {
|
||
width: 100%;
|
||
height: 180rpx;
|
||
font-size: 28rpx;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.char-count {
|
||
position: absolute;
|
||
right: 20rpx;
|
||
bottom: 16rpx;
|
||
font-size: 22rpx;
|
||
}
|
||
|
||
/* 保存按钮:使用 <view> 时确保样式表现与 .btn 一致 */
|
||
.save-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 100%;
|
||
height: 88rpx;
|
||
border-radius: 14rpx;
|
||
font-size: 30rpx;
|
||
font-weight: 600;
|
||
letter-spacing: 0.3rpx;
|
||
margin: 16rpx 0 0;
|
||
transition: transform 0.2s ease, opacity 0.2s ease;
|
||
-webkit-tap-highlight-color: transparent;
|
||
}
|
||
|
||
.save-btn:active:not(.save-btn--disabled) {
|
||
transform: scale(0.98);
|
||
opacity: 0.92;
|
||
}
|
||
|
||
.save-btn--disabled {
|
||
opacity: 1; /* 透明度由 inline style 控制 */
|
||
}
|
||
|
||
.save-btn__text {
|
||
line-height: 1.2;
|
||
color: inherit;
|
||
}
|
||
|
||
/* AI 弹窗 */
|
||
.ai-modal-body {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
padding: 10rpx 0 20rpx;
|
||
}
|
||
|
||
.ai-loading {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 20rpx;
|
||
padding: 40rpx 0;
|
||
}
|
||
|
||
.loading-spinner {
|
||
width: 48rpx;
|
||
height: 48rpx;
|
||
border: 4rpx solid;
|
||
border-top: 4rpx solid;
|
||
border-radius: 50%;
|
||
animation: spin 1s linear infinite;
|
||
}
|
||
|
||
@keyframes spin {
|
||
0% { transform: rotate(0deg); }
|
||
100% { transform: rotate(360deg); }
|
||
}
|
||
|
||
.loading-text {
|
||
font-size: 28rpx;
|
||
}
|
||
|
||
.ai-modal-card {
|
||
position: relative;
|
||
width: 100%;
|
||
border-radius: 16rpx;
|
||
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.04);
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.ai-modal-bg {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
height: 50%;
|
||
background: linear-gradient(180deg, rgba(255, 249, 240, 0.5) 0%, transparent 100%);
|
||
pointer-events: none;
|
||
}
|
||
|
||
.ai-modal-inner {
|
||
position: relative;
|
||
z-index: 1;
|
||
padding: 28rpx;
|
||
}
|
||
|
||
.ai-modal-header {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.ai-modal-date {
|
||
font-size: 22rpx;
|
||
color: #b8b0a8;
|
||
}
|
||
|
||
.ai-modal-mood {
|
||
font-size: 44rpx;
|
||
}
|
||
|
||
.ai-modal-divider {
|
||
height: 1rpx;
|
||
background: linear-gradient(90deg, transparent, rgba(0, 0, 0, 0.04), transparent);
|
||
margin: 12rpx 0;
|
||
}
|
||
|
||
.ai-modal-text {
|
||
min-height: 100rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
}
|
||
|
||
.ai-modal-text text {
|
||
font-size: 28rpx;
|
||
color: #4a443e;
|
||
line-height: 1.8;
|
||
letter-spacing: 1rpx;
|
||
text-align: left;
|
||
display: block;
|
||
white-space: pre-wrap;
|
||
word-break: break-word;
|
||
}
|
||
|
||
.ai-modal-footer {
|
||
margin-top: 16rpx;
|
||
text-align: center;
|
||
}
|
||
|
||
.ai-modal-hint {
|
||
font-size: 20rpx;
|
||
color: #c4b8ad;
|
||
}
|
||
|
||
/* AI 弹窗内的底部三个按钮(在 footer slot 内) */
|
||
.ai-modal-actions {
|
||
display: flex;
|
||
gap: 16rpx;
|
||
width: 100%;
|
||
justify-content: center;
|
||
}
|
||
|
||
.ai-action-btn {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-height: 80rpx;
|
||
padding: 0 12rpx;
|
||
border-radius: 14rpx;
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
border: none;
|
||
outline: none;
|
||
transition: transform 0.2s ease, opacity 0.2s ease;
|
||
-webkit-tap-highlight-color: transparent;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.ai-action-btn:active {
|
||
transform: scale(0.97);
|
||
opacity: 0.92;
|
||
}
|
||
|
||
.ai-action-btn__text {
|
||
color: inherit;
|
||
line-height: 1.2;
|
||
}
|
||
|
||
.ai-action-secondary {
|
||
background: rgba(0, 0, 0, 0.06);
|
||
color: var(--textPrimary, #3D3630);
|
||
}
|
||
|
||
.refresh-icon {
|
||
font-size: 24rpx;
|
||
margin-right: 4rpx;
|
||
}
|
||
|
||
/* 页面内平铺心语小笺 */
|
||
.ai-section {
|
||
margin-bottom: 24rpx;
|
||
}
|
||
|
||
.ai-section-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12rpx;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.ai-section-icon {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.ai-section-title {
|
||
font-size: 28rpx;
|
||
font-weight: 500;
|
||
letter-spacing: 2rpx;
|
||
}
|
||
|
||
.ai-card-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.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: 44rpx;
|
||
}
|
||
|
||
.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: flex-start;
|
||
}
|
||
|
||
.ai-card-text text {
|
||
font-size: 28rpx;
|
||
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;
|
||
gap: 20rpx;
|
||
justify-content: center;
|
||
}
|
||
|
||
.ai-inline-btn {
|
||
flex: 1;
|
||
min-height: 80rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 0 16rpx;
|
||
border-radius: 14rpx;
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
border: none;
|
||
outline: none;
|
||
transition: transform 0.2s ease, opacity 0.2s ease;
|
||
-webkit-tap-highlight-color: transparent;
|
||
}
|
||
|
||
.ai-inline-btn:active {
|
||
transform: scale(0.96);
|
||
opacity: 0.9;
|
||
}
|
||
|
||
.ai-inline-refresh {
|
||
background: rgba(0, 0, 0, 0.06);
|
||
color: var(--textPrimary, #3D3630);
|
||
}
|
||
|
||
.ai-inline-save {
|
||
background: var(--gradientSoft, linear-gradient(135deg, #B85C3A 0%, #C68E3F 100%));
|
||
color: #fff;
|
||
border: none;
|
||
}
|
||
|
||
.inline-btn-text {
|
||
font-size: 26rpx;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.inline-btn-text-save {
|
||
font-size: 26rpx;
|
||
font-weight: 500;
|
||
color: #fff;
|
||
}
|
||
|
||
.refresh-icon {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.save-icon {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.bottom-spacer {
|
||
height: 100rpx;
|
||
}
|
||
|
||
.mood-canvas {
|
||
position: fixed;
|
||
left: -9999px;
|
||
top: -9999px;
|
||
pointer-events: none;
|
||
}
|
||
</style>
|