Files
simple-memo-wx/pages/mood/mood.vue
T

1129 lines
25 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>
<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>