emotion-checkin.vue 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372
  1. <template>
  2. <view class="checkin-container">
  3. <!-- 顶部:日期 + 连续打卡 -->
  4. <view class="checkin-header">
  5. <text class="checkin-date">{{ todayDisplay }}</text>
  6. <view class="streak-badge" v-if="stats && stats.currentStreak > 0">
  7. <text class="streak-icon">&#x1F525;</text>
  8. <text class="streak-text">连续{{ stats.currentStreak }}天</text>
  9. </view>
  10. </view>
  11. <!-- ===== Phase 1.1: 2D 效价-激活度网格 ===== -->
  12. <view class="section">
  13. <text class="section-title">在方格中点击你的情绪位置</text>
  14. <view class="affect-grid" @tap="onAffectGridTap">
  15. <!-- 象限标签 -->
  16. <text class="grid-label label-nw">烦躁/紧张</text>
  17. <text class="grid-label label-ne">兴奋/愉快</text>
  18. <text class="grid-label label-sw">低落/疲惫</text>
  19. <text class="grid-label label-se">平静/放松</text>
  20. <!-- 坐标轴 -->
  21. <text class="axis-label axis-bottom">消极 ← 情绪效价 → 积极</text>
  22. <text class="axis-label axis-left">激<br/>活<br/>度</text>
  23. <!-- 选中的圆点 -->
  24. <view class="affect-dot" :style="{ left: affectDotX + 'rpx', top: affectDotY + 'rpx' }">
  25. <view class="affect-dot-inner"></view>
  26. </view>
  27. </view>
  28. <view class="affect-values">
  29. <text class="affect-val">效价: {{ affectValence }}/10</text>
  30. <text class="affect-val">激活度: {{ affectArousal }}/10</text>
  31. </view>
  32. </view>
  33. <!-- ===== Plutchik 情绪轮 ===== -->
  34. <view class="section">
  35. <text class="section-title">选择你的核心情绪</text>
  36. <view class="emotion-wheel">
  37. <view
  38. v-for="em in plutchikEmotions"
  39. :key="em.type"
  40. :class="['emotion-btn', selectedEmotionType === em.type ? 'emotion-active' : '']"
  41. :style="{ borderColor: selectedEmotionType === em.type ? em.color : '#eee' }"
  42. @click="selectedEmotionType = em.type">
  43. <text class="emotion-icon">{{ em.icon }}</text>
  44. <text class="emotion-label">{{ em.label }}</text>
  45. </view>
  46. </view>
  47. </view>
  48. <!-- ===== 强度滑条 ===== -->
  49. <view class="section">
  50. <text class="section-title">情绪强度</text>
  51. <view class="slider-row">
  52. <text class="slider-label">弱</text>
  53. <slider class="custom-slider" min="1" max="10" :value="intensityLevel" @change="onIntensityChange" activeColor="#FF6B9D" block-size="20" />
  54. <text class="slider-value">{{ intensityLevel }}</text>
  55. </view>
  56. <view class="intensity-hint">{{ intensityHint }}</view>
  57. </view>
  58. <!-- ===== 精力水平 ===== -->
  59. <view class="section">
  60. <text class="section-title">精力水平</text>
  61. <view class="slider-row">
  62. <text class="slider-label">低</text>
  63. <slider class="custom-slider" min="1" max="10" :value="energyLevel" @change="onEnergyChange" activeColor="#10B981" block-size="20" />
  64. <text class="slider-value">{{ energyLevel }}</text>
  65. </view>
  66. </view>
  67. <!-- ===== 压力水平 ===== -->
  68. <view class="section">
  69. <text class="section-title">压力水平</text>
  70. <view class="slider-row">
  71. <text class="slider-label">无压力</text>
  72. <slider class="custom-slider" min="1" max="10" :value="stressLevel" @change="onStressChange" activeColor="#F59E0B" block-size="20" />
  73. <text class="slider-value">{{ stressLevel }}</text>
  74. </view>
  75. </view>
  76. <!-- ===== 情绪标签(精细标签) ===== -->
  77. <view class="section">
  78. <text class="section-title">情绪标签(可多选)</text>
  79. <view class="tag-grid">
  80. <view
  81. v-for="tag in emotionTags"
  82. :key="tag"
  83. :class="['tag-btn', selectedTags.indexOf(tag) !== -1 ? 'tag-active' : '']"
  84. @click="toggleTag(tag)">
  85. <text>{{ tag }}</text>
  86. </view>
  87. </view>
  88. <view class="custom-tag-row" v-if="showCustomTagInput">
  89. <input class="custom-tag-input" v-model="customTag" placeholder="输入自定义情绪" />
  90. <text class="custom-tag-add" @click="addCustomTag">添加</text>
  91. </view>
  92. <text class="custom-tag-trigger" v-if="!showCustomTagInput" @click="showCustomTagInput = true">+ 自定义标签</text>
  93. </view>
  94. <!-- 拍照/录像 -->
  95. <view class="section">
  96. <text class="section-title">拍一张照片或一段视频记录心情</text>
  97. <view class="capture-mode-bar">
  98. <text
  99. :class="['mode-btn', captureMode === 'photo' ? 'mode-active' : '']"
  100. @click="switchCaptureMode('photo')">📷 拍照</text>
  101. <text
  102. :class="['mode-btn', captureMode === 'video' ? 'mode-active' : '']"
  103. @click="switchCaptureMode('video')">🎬 录像</text>
  104. </view>
  105. <view class="photo-area" @click="takeMedia">
  106. <!-- 照片预览 -->
  107. <image v-if="captureMode === 'photo' && photoPath" :src="photoPath" mode="aspectFill" class="photo-preview" />
  108. <!-- 视频预览 -->
  109. <video v-if="captureMode === 'video' && videoPath" :src="videoPath" class="video-preview" objectFit="cover" autoplay muted loop />
  110. <!-- 占位 -->
  111. <view v-else-if="!photoPath && !videoPath" class="photo-placeholder">
  112. <text class="photo-icon">{{ captureMode === 'photo' ? '\u{1F4F7}' : '\u{1F3AC}' }}</text>
  113. <text class="photo-hint">{{ captureMode === 'photo' ? '点击拍照' : '点击录像' }}</text>
  114. </view>
  115. </view>
  116. <text class="photo-note">{{ captureMode === 'photo' ? '照片' : '视频' }}仅对自己可见</text>
  117. </view>
  118. <!-- 情绪识别 -->
  119. <view class="section" v-if="photoPath || videoPath">
  120. <text class="section-title">\uD83E\uDD16 情绪识别</text>
  121. <view class="ai-recognize-btn" v-if="!aiResult && !aiAnalyzing" @click="analyzeEmotion">
  122. <text class="ai-btn-icon">\uD83D\uDD0D</text>
  123. <text class="ai-btn-text">{{ captureMode === 'video' ? '识别视频中的情绪' : '识别照片中的情绪' }}</text>
  124. </view>
  125. <view class="ai-analyzing" v-if="aiAnalyzing">
  126. <text class="loading-text">分析中...</text>
  127. </view>
  128. <view class="ai-result" v-if="aiResult">
  129. <text class="ai-result-title">识别结果</text>
  130. <view class="ai-emotion-list">
  131. <view class="ai-emotion-item" v-for="em in aiResult" :key="em.label">
  132. <text class="ai-emotion-label">{{ em.label }}</text>
  133. <view class="ai-emotion-bar-track">
  134. <view class="ai-emotion-bar-fill" :style="{ width: em.confidence + '%', background: em.color }"></view>
  135. </view>
  136. <text class="ai-emotion-value">{{ em.confidence }}%</text>
  137. </view>
  138. </view>
  139. <view class="ai-apply-tags" @click="applyAiTags">
  140. <text class="ai-apply-text">应用为情绪标签</text>
  141. </view>
  142. </view>
  143. </view>
  144. <!-- 文字记录 -->
  145. <view class="section">
  146. <text class="section-title">记录一下(选填)</text>
  147. <textarea
  148. class="note-input"
  149. v-model="note"
  150. placeholder="今天发生了什么?"
  151. maxlength="500" />
  152. </view>
  153. <!-- Phase 1.2: 情绪风险告警 -->
  154. <PsychCrisisBanner v-if="currentChildId" :memberId="currentChildId" source="checkin" />
  155. <!-- 近期心情历史(合并自成长页心情记录) -->
  156. <view class="section" v-if="recentMoods.length > 0">
  157. <text class="section-title">近期心情</text>
  158. <view class="mood-list">
  159. <view class="mood-item" v-for="(mood, idx) in recentMoods" :key="idx">
  160. <text class="mood-emotion">{{ emotionIcon(mood.emotionType) }}</text>
  161. <view class="mood-info">
  162. <text class="mood-label">{{ emotionLabel(mood.emotionType) }}</text>
  163. <text class="mood-date">{{ formatMoodDate(mood) }}</text>
  164. </view>
  165. <text class="mood-note" v-if="mood.note">{{ mood.note }}</text>
  166. </view>
  167. </view>
  168. </view>
  169. <!-- 提交按钮 -->
  170. <view class="submit-area">
  171. <button
  172. class="submit-btn"
  173. :class="{ 'submit-disabled': !canSubmit }"
  174. :disabled="!canSubmit"
  175. @click="submitCheckin">
  176. <text class="submit-icon">&#x2764;</text>
  177. <text>记录心情</text>
  178. </button>
  179. </view>
  180. <!-- 提交中 loading -->
  181. <view class="loading-mask" v-if="submitting">
  182. <view class="loading-box">
  183. <text class="loading-text">记录中...</text>
  184. </view>
  185. </view>
  186. </view>
  187. </template>
  188. <script>
  189. import config from '../../config'
  190. import PsychCrisisBanner from '../../components/PsychCrisisBanner.vue'
  191. export default {
  192. components: { PsychCrisisBanner },
  193. data() {
  194. return {
  195. currentChildId: null,
  196. // --- 2D 效价-激活度网格 ---
  197. affectValence: 5, // 1-10
  198. affectArousal: 5, // 1-10
  199. gridWidth: 360, // rpx, 会在 onReady 中动态计算
  200. gridHeight: 360,
  201. // --- Plutchik 情绪轮 ---
  202. selectedEmotionType: '',
  203. // --- 滑条 ---
  204. intensityLevel: 5,
  205. energyLevel: 6,
  206. stressLevel: 4,
  207. // --- 旧字段保留兼容 ---
  208. selectedWeather: 'sunny', // 自动从效价映射
  209. selectedTags: [],
  210. customTag: '',
  211. showCustomTagInput: false,
  212. captureMode: 'photo',
  213. photoPath: '',
  214. videoPath: '',
  215. note: '',
  216. submitting: false,
  217. aiAnalyzing: false,
  218. aiResult: null,
  219. aiEmotionColors: {
  220. '开心': '#10B981',
  221. '平静': '#3B82F6',
  222. '惊喜': '#8B5CF6',
  223. '难过': '#6B9BD2',
  224. '生气': '#EF4444',
  225. '紧张': '#F59E0B',
  226. '疲惫': '#9CA3AF',
  227. '害怕': '#7C3AED'
  228. },
  229. stats: null,
  230. // 近期心情历史(合并自成长页心情记录)
  231. recentMoods: [],
  232. // Plutchik 8 基本情绪
  233. plutchikEmotions: [
  234. { type: 'joy', icon: '\uD83D\uDE04', label: '开心', color: '#FFD93D' },
  235. { type: 'trust', icon: '\uD83E\uDD1D', label: '信任', color: '#6BCB77' },
  236. { type: 'fear', icon: '\uD83D\uDE31', label: '恐惧', color: '#9B59B6' },
  237. { type: 'surprise', icon: '\uD83D\uDE32', label: '惊讶', color: '#FF9FF3' },
  238. { type: 'sadness', icon: '\uD83D\uDE22', label: '悲伤', color: '#6B9BD2' },
  239. { type: 'disgust', icon: '\uD83E\uDD2E', label: '厌恶', color: '#A0A0A0' },
  240. { type: 'anger', icon: '\uD83D\uDE21', label: '愤怒', color: '#FF6B6B' },
  241. { type: 'anticipation',icon: '\uD83E\uDD29', label: '期待', color: '#FF8C42' }
  242. ],
  243. emotionTags: ['开心', '期待', '平静', '惊喜', '难过', '生气', '紧张', '无聊', '疲惫', '害怕'],
  244. // 强度提示文案
  245. intensityHints: {
  246. 1: '几乎感觉不到',
  247. 2: '很微弱',
  248. 3: '有一点',
  249. 4: '稍微明显',
  250. 5: '中等强度',
  251. 6: '比较明显',
  252. 7: '相当强烈',
  253. 8: '很强烈',
  254. 9: '非常强烈',
  255. 10: '前所未有的强烈'
  256. }
  257. }
  258. },
  259. computed: {
  260. todayDisplay() {
  261. const now = new Date()
  262. const y = now.getFullYear()
  263. const m = now.getMonth() + 1
  264. const d = now.getDate()
  265. const weekdays = ['日', '一', '二', '三', '四', '五', '六']
  266. return y + '年' + m + '月' + d + '日 周' + weekdays[now.getDay()]
  267. },
  268. canSubmit() {
  269. return this.affectValence > 0 && this.selectedEmotionType
  270. },
  271. // 圆点位置(rpx)
  272. affectDotX() {
  273. // valence 1-10 → 0 to gridWidth
  274. // 留出边距: 从 20 到 gridWidth-20
  275. const margin = 20
  276. const usable = this.gridWidth - 2 * margin
  277. return margin + ((this.affectValence - 1) / 9) * usable
  278. },
  279. affectDotY() {
  280. // arousal 1-10 → gridHeight to 0 (Y轴反向)
  281. const margin = 20
  282. const usable = this.gridHeight - 2 * margin
  283. return margin + (1 - (this.affectArousal - 1) / 9) * usable
  284. },
  285. intensityHint() {
  286. return this.intensityHints[this.intensityLevel] || ''
  287. }
  288. },
  289. onLoad(options) {
  290. // memberId 优先取页面参数(成长页跳转传入),再兜底本地存储链
  291. var memberId = (options && options.memberId) || uni.getStorageSync('currentChildId') || uni.getStorageSync('currentMemberId') || uni.getStorageSync('userId') || null
  292. this.currentChildId = memberId
  293. this.loadStats()
  294. this.loadRecentMoods()
  295. },
  296. onReady() {
  297. // 估算 grid 尺寸(基于屏幕宽度)
  298. var sysInfo = uni.getWindowInfo()
  299. // 页面左右margin=32rpx, section padding=28rpx
  300. // gridWidth ≈ screenWidth - (32+28)*2 = screenWidth - 120
  301. var screenWidth = sysInfo.windowWidth
  302. var gridRpx = screenWidth - 120 // 估算 rpx
  303. if (gridRpx < 300) gridRpx = 300
  304. this.gridWidth = gridRpx
  305. this.gridHeight = gridRpx
  306. },
  307. methods: {
  308. loadStats() {
  309. var memberId = this.currentChildId
  310. if (!memberId) return
  311. uni.request({
  312. url: config.API_BASE_URL + '/api/mind/checkin/stats',
  313. method: 'POST',
  314. data: { memberId },
  315. success: (res) => {
  316. if (res.data && res.data.code === 200 && res.data.data) {
  317. this.stats = res.data.data
  318. }
  319. }
  320. })
  321. },
  322. // ===== 近期心情记录(合并自成长页心情记录) =====
  323. loadRecentMoods() {
  324. var self = this
  325. if (!this.currentChildId) return
  326. uni.request({
  327. url: config.API_BASE_URL + '/api/mind/checkin/list',
  328. method: 'POST',
  329. data: { memberId: this.currentChildId, page: 1, size: 5 },
  330. success: (res) => {
  331. if (res.data && res.data.code === 200) {
  332. var list = res.data.data
  333. // 兼容数组 / {records} / {list} 三种返回结构
  334. if (!(list instanceof Array)) {
  335. list = (list && (list.list || list.records)) || []
  336. }
  337. self.recentMoods = list.slice(0, 5)
  338. }
  339. }
  340. })
  341. },
  342. emotionLabel(type) {
  343. var map = {
  344. // Plutchik 词表(情绪打卡页)
  345. joy: '开心', trust: '信任', fear: '恐惧', surprise: '惊讶',
  346. sadness: '悲伤', disgust: '厌恶', anger: '愤怒', anticipation: '期待',
  347. // 成长页词表(旧数据兼容)
  348. calm: '平静', excited: '兴奋', sad: '难过', angry: '生气',
  349. anxious: '焦虑', tired: '疲惫', scared: '害怕'
  350. }
  351. return map[type] || type || '心情'
  352. },
  353. emotionIcon(type) {
  354. var map = {
  355. joy: '\u{1F604}', trust: '\u{1F91D}', fear: '\u{1F631}', surprise: '\u{1F632}',
  356. sadness: '\u{1F622}', disgust: '\u{1F922}', anger: '\u{1F621}', anticipation: '\u{1F600}',
  357. calm: '\u{1F60C}', excited: '\u{1F929}', sad: '\u{1F622}', angry: '\u{1F621}',
  358. anxious: '\u{1F630}', tired: '\u{1F634}', scared: '\u{1F631}'
  359. }
  360. return map[type] || '\u{1F604}'
  361. },
  362. formatMoodDate(mood) {
  363. var raw = (mood && (mood.checkinDate || mood.createdAt)) || ''
  364. return raw.substring(0, 10)
  365. },
  366. // ===== 2D 效价-激活度网格 =====
  367. onAffectGridTap(e) {
  368. // 从触点获取页面坐标(clientX/clientY,px)
  369. var touch = (e.touches && e.touches[0]) || (e.changedTouches && e.changedTouches[0]) || e.detail
  370. var clientX = touch ? (touch.clientX !== undefined ? touch.clientX : touch.x) : 0
  371. var clientY = touch ? (touch.clientY !== undefined ? touch.clientY : touch.y) : 0
  372. var self = this
  373. var query = uni.createSelectorQuery().in(this)
  374. query.select('.affect-grid').boundingClientRect(function(rect) {
  375. if (!rect) return
  376. // 相对网格的偏移(px)
  377. var pxX = clientX - rect.left
  378. var pxY = clientY - rect.top
  379. if (pxX < 0) pxX = 0
  380. if (pxY < 0) pxY = 0
  381. if (pxX > rect.width) pxX = rect.width
  382. if (pxY > rect.height) pxY = rect.height
  383. // px → rpx 转换(网格宽高单位为 rpx,通过比例换算)
  384. var scale = self.gridWidth / rect.width
  385. var rpxX = pxX * scale
  386. var rpxY = pxY * scale
  387. // 约束在有效范围内(跟 affectDotX/Y 的 margin 对齐)
  388. var margin = 20
  389. var maxRpx = self.gridWidth - margin
  390. if (rpxX < margin) rpxX = margin
  391. if (rpxX > maxRpx) rpxX = maxRpx
  392. if (rpxY < margin) rpxY = margin
  393. if (rpxY > self.gridHeight - margin) rpxY = self.gridHeight - margin
  394. // 映射到 1-10
  395. var usable = self.gridWidth - 2 * margin
  396. var val = Math.round(1 + ((rpxX - margin) / usable) * 9)
  397. var aro = Math.round(10 - ((rpxY - margin) / usable) * 9)
  398. self.affectValence = Math.max(1, Math.min(10, val))
  399. self.affectArousal = Math.max(1, Math.min(10, aro))
  400. // 自动映射 moodWeather 用于向后兼容
  401. self.updateWeatherFromAffect()
  402. }).exec()
  403. },
  404. updateWeatherFromAffect() {
  405. // 效价高 + 激活度高 = rainbow
  406. if (this.affectValence >= 7 && this.affectArousal >= 7) {
  407. this.selectedWeather = 'rainbow'
  408. } else if (this.affectValence >= 6) {
  409. // 效价高 = sunny
  410. this.selectedWeather = 'sunny'
  411. } else if (this.affectValence <= 4 && this.affectArousal >= 7) {
  412. // 效价低 + 激活度高 = stormy
  413. this.selectedWeather = 'stormy'
  414. } else if (this.affectValence <= 4 && this.affectArousal <= 4) {
  415. // 效价低 + 激活度低 = rainy
  416. this.selectedWeather = 'rainy'
  417. } else {
  418. this.selectedWeather = 'cloudy'
  419. }
  420. },
  421. // ===== 滑条变化 =====
  422. onIntensityChange(e) {
  423. this.intensityLevel = e.detail.value
  424. },
  425. onEnergyChange(e) {
  426. this.energyLevel = e.detail.value
  427. },
  428. onStressChange(e) {
  429. this.stressLevel = e.detail.value
  430. },
  431. // ===== 情绪标签 =====
  432. toggleTag(tag) {
  433. const idx = this.selectedTags.indexOf(tag)
  434. if (idx !== -1) {
  435. this.selectedTags.splice(idx, 1)
  436. } else {
  437. this.selectedTags.push(tag)
  438. }
  439. },
  440. addCustomTag() {
  441. const tag = this.customTag.trim()
  442. if (tag && this.emotionTags.indexOf(tag) === -1 && this.selectedTags.indexOf(tag) === -1) {
  443. this.selectedTags.push(tag)
  444. }
  445. this.customTag = ''
  446. this.showCustomTagInput = false
  447. },
  448. // ===== 拍照/录像 =====
  449. switchCaptureMode(mode) {
  450. this.captureMode = mode
  451. this.photoPath = ''
  452. this.videoPath = ''
  453. this.aiResult = null
  454. },
  455. takeMedia() {
  456. var self = this
  457. if (this.captureMode === 'photo') {
  458. uni.chooseImage({
  459. count: 1,
  460. sourceType: ['camera', 'album'],
  461. success: function(res) {
  462. self.photoPath = res.tempFilePaths[0]
  463. self.videoPath = ''
  464. self.aiResult = null
  465. }
  466. })
  467. } else {
  468. uni.chooseVideo({
  469. sourceType: ['camera', 'album'],
  470. maxDuration: 15,
  471. success: function(res) {
  472. self.videoPath = res.tempFilePath
  473. self.photoPath = ''
  474. self.aiResult = null
  475. }
  476. })
  477. }
  478. },
  479. // ===== 增强型模拟识别 =====
  480. // 基于效价/激活度/时段/强度等多维参数,生成更自然的模拟结果
  481. analyzeEmotion() {
  482. this.aiAnalyzing = true
  483. this.aiResult = null
  484. var self = this
  485. // 生成伪随机种子(不同照片/视频产生不同结果,模拟真实识别)
  486. var seed = this.genSeed()
  487. // 根据多维参数生成情绪结果
  488. var results = this.genEmotionResults(seed)
  489. // 处理时间随机变化(200-1600ms,让每次等待不一样)
  490. var delay = 600 + (seed % 1000)
  491. setTimeout(function() {
  492. self.aiResult = results
  493. self.aiAnalyzing = false
  494. }, delay)
  495. },
  496. // 基于文件路径 + 时间生成伪随机种子
  497. genSeed() {
  498. var path = this.photoPath || this.videoPath || ''
  499. var hash = 0
  500. for (var i = 0; i < path.length; i++) {
  501. hash = ((hash << 5) - hash) + path.charCodeAt(i)
  502. hash = hash & hash
  503. }
  504. hash = Math.abs(hash)
  505. return (hash + Date.now()) % 7919 // 质数取模
  506. },
  507. // 多维参数 → 情绪结果
  508. genEmotionResults(seed) {
  509. var valence = this.affectValence
  510. var arousal = this.affectArousal
  511. // 1. 从效价-激活度象限推导主要情绪分布
  512. var emotionPool = []
  513. if (valence >= 6 && arousal >= 6) {
  514. // Q1: 高激活 + 积极 → 开心/兴奋/惊喜
  515. emotionPool = [
  516. { label: '开心', base: 38, color: '#10B981' },
  517. { label: '兴奋', base: 22, color: '#8B5CF6' },
  518. { label: '惊喜', base: 18, color: '#FF9FF3' },
  519. { label: '满足', base: 12, color: '#3B82F6' },
  520. { label: '期待', base: 10, color: '#F59E0B' }
  521. ]
  522. } else if (valence >= 6 && arousal < 6) {
  523. // Q2: 低激活 + 积极 → 平静/满足/放松
  524. emotionPool = [
  525. { label: '平静', base: 32, color: '#3B82F6' },
  526. { label: '满足', base: 25, color: '#10B981' },
  527. { label: '放松', base: 20, color: '#6B9BD2' },
  528. { label: '开心', base: 15, color: '#10B981' },
  529. { label: '无聊', base: 8, color: '#9CA3AF' }
  530. ]
  531. } else if (valence < 6 && arousal >= 6) {
  532. // Q3: 高激活 + 消极 → 生气/紧张/恐惧
  533. emotionPool = [
  534. { label: '紧张', base: 30, color: '#F59E0B' },
  535. { label: '生气', base: 25, color: '#EF4444' },
  536. { label: '烦躁', base: 20, color: '#FF6B6B' },
  537. { label: '恐惧', base: 15, color: '#7C3AED' },
  538. { label: '厌恶', base: 10, color: '#A0A0A0' }
  539. ]
  540. } else {
  541. // Q4: 低激活 + 消极 → 难过/疲惫/低落
  542. emotionPool = [
  543. { label: '难过', base: 32, color: '#6B9BD2' },
  544. { label: '疲惫', base: 28, color: '#9CA3AF' },
  545. { label: '低落', base: 20, color: '#6B7280' },
  546. { label: '平静', base: 12, color: '#3B82F6' },
  547. { label: '无聊', base: 8, color: '#9CA3AF' }
  548. ]
  549. }
  550. // 2. 根据强度/压力/能量微调
  551. // 高强度 → 强化 top1
  552. if (this.intensityLevel >= 7) {
  553. emotionPool[0].base += 10
  554. } else if (this.intensityLevel <= 3) {
  555. // 低强度 → 向平静偏移
  556. for (var si = 0; si < emotionPool.length; si++) {
  557. if (emotionPool[si].label === '平静' || emotionPool[si].label === '无聊') {
  558. emotionPool[si].base += 8
  559. }
  560. }
  561. }
  562. // 高压力 → 添加紧张/烦躁倾向
  563. if (this.stressLevel >= 7) {
  564. var tensionIdx = -1
  565. var angerIdx = -1
  566. for (var ti = 0; ti < emotionPool.length; ti++) {
  567. if (emotionPool[ti].label === '紧张') tensionIdx = ti
  568. if (emotionPool[ti].label === '烦躁') angerIdx = ti
  569. }
  570. if (tensionIdx >= 0) emotionPool[tensionIdx].base += 12
  571. if (angerIdx >= 0) emotionPool[angerIdx].base += 8
  572. }
  573. // 3. 根据 Plutchik 已选情绪提升权重(用户选择了说明有倾向)
  574. if (this.selectedEmotionType) {
  575. var typeToLabel = {
  576. joy: '开心', trust: '信任', fear: '恐惧',
  577. surprise: '惊喜', sadness: '难过', disgust: '厌恶',
  578. anger: '生气', anticipation: '期待'
  579. }
  580. var matchedLabel = typeToLabel[this.selectedEmotionType]
  581. for (var mi = 0; mi < emotionPool.length; mi++) {
  582. if (emotionPool[mi].label === matchedLabel) {
  583. emotionPool[mi].base += 15
  584. }
  585. }
  586. }
  587. // 4. 根据时段微调
  588. var hour = new Date().getHours()
  589. if (hour < 6) {
  590. // 深夜:偏向疲惫/低落
  591. this.adjustPoolForTime(emotionPool, '疲惫', 10)
  592. this.adjustPoolForTime(emotionPool, '低落', 8)
  593. } else if (hour < 10) {
  594. // 早晨:偏向积极
  595. this.adjustPoolForTime(emotionPool, '开心', 8)
  596. this.adjustPoolForTime(emotionPool, '期待', 6)
  597. } else if (hour >= 22) {
  598. // 夜晚:偏向平静/疲惫
  599. this.adjustPoolForTime(emotionPool, '平静', 8)
  600. this.adjustPoolForTime(emotionPool, '疲惫', 6)
  601. }
  602. // 5. 引入伪随机噪声(让每次结果有细微差异)
  603. for (var ni = 0; ni < emotionPool.length; ni++) {
  604. var noise = (seed * (ni + 1) * 7) % 11 - 5 // -5 ~ +5
  605. emotionPool[ni].base += noise
  606. if (emotionPool[ni].base < 3) emotionPool[ni].base = 3
  607. }
  608. // 6. 降序排列,取 top 4,归一化到 100%
  609. emotionPool.sort(function(a, b) { return b.base - a.base })
  610. var top4 = emotionPool.slice(0, 4)
  611. var total = 0
  612. for (var sumi = 0; sumi < top4.length; sumi++) {
  613. total += top4[sumi].base
  614. }
  615. for (var normi = 0; normi < top4.length; normi++) {
  616. top4[normi].confidence = Math.round((top4[normi].base / total) * 100)
  617. }
  618. // 补余数确保 =100
  619. var sumConf = 0
  620. for (var ci = 0; ci < top4.length; ci++) {
  621. sumConf += top4[ci].confidence
  622. }
  623. var diff = 100 - sumConf
  624. if (diff !== 0 && top4.length > 0) {
  625. top4[0].confidence += diff
  626. }
  627. return top4
  628. },
  629. adjustPoolForTime(pool, label, delta) {
  630. for (var i = 0; i < pool.length; i++) {
  631. if (pool[i].label === label) {
  632. pool[i].base += delta
  633. return
  634. }
  635. }
  636. },
  637. applyAiTags() {
  638. if (!this.aiResult) return
  639. var detected = []
  640. for (var i = 0; i < this.aiResult.length; i++) {
  641. var em = this.aiResult[i]
  642. if (em.confidence >= 20 && this.emotionTags.indexOf(em.label) !== -1) {
  643. detected.push(em.label)
  644. }
  645. }
  646. var merged = this.selectedTags.slice()
  647. for (var j = 0; j < detected.length; j++) {
  648. if (merged.indexOf(detected[j]) === -1) {
  649. merged.push(detected[j])
  650. }
  651. }
  652. this.selectedTags = merged
  653. uni.showToast({ title: '已应用情绪标签', icon: 'success' })
  654. },
  655. // ===== 提交 =====
  656. submitCheckin() {
  657. if (!this.canSubmit || this.submitting) return
  658. this.submitting = true
  659. var memberId = this.currentChildId
  660. const doSubmit = (mediaUrl) => {
  661. uni.request({
  662. url: config.API_BASE_URL + '/api/mind/checkin/create',
  663. method: 'POST',
  664. data: {
  665. memberId: memberId,
  666. // 旧字段向后兼容
  667. moodWeather: this.selectedWeather,
  668. emotionTags: JSON.stringify(this.selectedTags),
  669. photoUrl: this.captureMode === 'photo' ? (mediaUrl || '') : '',
  670. videoUrl: this.captureMode === 'video' ? (mediaUrl || '') : '',
  671. mediaType: this.captureMode,
  672. note: this.note,
  673. // Phase 1.1 精细情绪字段
  674. moodScore: this.affectValence,
  675. emotionType: this.selectedEmotionType,
  676. stressLevel: this.stressLevel,
  677. arousalLevel: this.affectArousal,
  678. energyLevel: this.energyLevel
  679. },
  680. success: (res) => {
  681. this.submitting = false
  682. if (res.data && res.data.code === 200) {
  683. uni.showToast({
  684. title: '+5 心能量',
  685. icon: 'none',
  686. duration: 2000
  687. })
  688. setTimeout(() => {
  689. var trendUrl = '/pages/mind-detail/emotion-trend'
  690. // 成长页场景 memberId 可能来自参数/currentMemberId,透传给趋势页
  691. if (memberId) trendUrl = trendUrl + '?memberId=' + memberId
  692. uni.navigateTo({ url: trendUrl })
  693. }, 1500)
  694. } else {
  695. uni.showToast({ title: res.data && res.data.message || '提交失败', icon: 'none' })
  696. }
  697. },
  698. fail: () => {
  699. this.submitting = false
  700. uni.showToast({ title: '网络错误', icon: 'none' })
  701. }
  702. })
  703. }
  704. var mediaPath = this.photoPath || this.videoPath
  705. if (mediaPath) {
  706. uni.uploadFile({
  707. url: config.API_BASE_URL + '/api/media/upload',
  708. filePath: mediaPath,
  709. name: 'file',
  710. success: (uploadRes) => {
  711. let mediaUrl = ''
  712. try {
  713. const data = JSON.parse(uploadRes.data)
  714. mediaUrl = data.data || data.url || ''
  715. } catch (e) {
  716. mediaUrl = uploadRes.data
  717. }
  718. doSubmit(mediaUrl)
  719. },
  720. fail: () => doSubmit('')
  721. })
  722. } else {
  723. doSubmit('')
  724. }
  725. }
  726. }
  727. }
  728. </script>
  729. <style scoped>
  730. .checkin-container {
  731. min-height: 100vh;
  732. background: linear-gradient(180deg, #FFF0F5 0%, #FFFFFF 100%);
  733. padding-bottom: 40rpx;
  734. }
  735. .checkin-header {
  736. display: flex;
  737. align-items: center;
  738. justify-content: space-between;
  739. padding: 30rpx 32rpx;
  740. }
  741. .checkin-date {
  742. font-size: 32rpx;
  743. font-weight: 600;
  744. color: #333;
  745. }
  746. .streak-badge {
  747. display: flex;
  748. align-items: center;
  749. background: linear-gradient(135deg, #FF6B9D, #FF8CB3);
  750. border-radius: 30rpx;
  751. padding: 8rpx 24rpx;
  752. }
  753. .streak-icon {
  754. font-size: 28rpx;
  755. margin-right: 8rpx;
  756. }
  757. .streak-text {
  758. font-size: 24rpx;
  759. color: #fff;
  760. font-weight: 500;
  761. }
  762. .section {
  763. margin: 20rpx 32rpx;
  764. background: #fff;
  765. border-radius: 20rpx;
  766. padding: 28rpx;
  767. box-shadow: 0 2rpx 12rpx rgba(255, 107, 157, 0.08);
  768. }
  769. .section-title {
  770. font-size: 28rpx;
  771. font-weight: 600;
  772. color: #333;
  773. margin-bottom: 20rpx;
  774. display: block;
  775. }
  776. /* ===== 2D 效价-激活度网格 ===== */
  777. .affect-grid {
  778. position: relative;
  779. width: 100%;
  780. /* height is set dynamically equal to width via aspect-ratio or inline style */
  781. aspect-ratio: 1;
  782. background: linear-gradient(135deg, #FFF5F5 0%, #FFF0FF 25%, #F0F0FF 50%, #F0FFF4 75%, #FFFFF0 100%);
  783. border-radius: 16rpx;
  784. border: 2rpx solid #E8E8E8;
  785. margin-bottom: 16rpx;
  786. /* Ensure the grid has a defined height — fallback */
  787. min-height: 300rpx;
  788. /* Overlap grid lines for visual clarity */
  789. overflow: hidden;
  790. }
  791. /* 网格线:用伪元素或背景渐变 */
  792. .affect-grid::before {
  793. content: '';
  794. position: absolute;
  795. left: 50%;
  796. top: 0;
  797. bottom: 0;
  798. width: 2rpx;
  799. background: rgba(0,0,0,0.06);
  800. transform: translateX(-50%);
  801. pointer-events: none;
  802. }
  803. .affect-grid::after {
  804. content: '';
  805. position: absolute;
  806. top: 50%;
  807. left: 0;
  808. right: 0;
  809. height: 2rpx;
  810. background: rgba(0,0,0,0.06);
  811. transform: translateY(-50%);
  812. pointer-events: none;
  813. }
  814. .grid-label {
  815. position: absolute;
  816. font-size: 22rpx;
  817. color: rgba(0,0,0,0.25);
  818. pointer-events: none;
  819. }
  820. .label-nw { top: 20rpx; left: 20rpx; }
  821. .label-ne { top: 20rpx; right: 20rpx; }
  822. .label-sw { bottom: 20rpx; left: 20rpx; }
  823. .label-se { bottom: 20rpx; right: 20rpx; }
  824. .axis-label {
  825. position: absolute;
  826. font-size: 20rpx;
  827. color: rgba(0,0,0,0.2);
  828. pointer-events: none;
  829. }
  830. .axis-bottom {
  831. bottom: 4rpx;
  832. left: 50%;
  833. transform: translateX(-50%);
  834. white-space: nowrap;
  835. }
  836. .axis-left {
  837. left: 4rpx;
  838. top: 50%;
  839. transform: translateY(-50%);
  840. font-size: 18rpx;
  841. line-height: 1.4;
  842. text-align: center;
  843. }
  844. .affect-dot {
  845. position: absolute;
  846. width: 44rpx;
  847. height: 44rpx;
  848. margin-left: -22rpx;
  849. margin-top: -22rpx;
  850. pointer-events: none;
  851. z-index: 2;
  852. }
  853. .affect-dot-inner {
  854. width: 100%;
  855. height: 100%;
  856. border-radius: 50%;
  857. background: #FF6B9D;
  858. box-shadow: 0 0 12rpx rgba(255, 107, 157, 0.5);
  859. border: 4rpx solid #fff;
  860. box-sizing: border-box;
  861. }
  862. .affect-values {
  863. display: flex;
  864. justify-content: center;
  865. gap: 40rpx;
  866. }
  867. .affect-val {
  868. font-size: 24rpx;
  869. color: #888;
  870. }
  871. /* ===== Plutchik 情绪轮 ===== */
  872. .emotion-wheel {
  873. display: flex;
  874. flex-wrap: wrap;
  875. gap: 16rpx;
  876. justify-content: center;
  877. }
  878. .emotion-btn {
  879. display: flex;
  880. flex-direction: column;
  881. align-items: center;
  882. width: 130rpx;
  883. padding: 16rpx 8rpx;
  884. border-radius: 20rpx;
  885. border: 3rpx solid #eee;
  886. background: #FAFAFA;
  887. transition: all 0.2s;
  888. }
  889. .emotion-active {
  890. background: #FFF0F5;
  891. transform: scale(1.05);
  892. }
  893. .emotion-icon {
  894. font-size: 48rpx;
  895. margin-bottom: 6rpx;
  896. }
  897. .emotion-label {
  898. font-size: 24rpx;
  899. color: #555;
  900. }
  901. /* ===== 滑条 ===== */
  902. .slider-row {
  903. display: flex;
  904. align-items: center;
  905. gap: 16rpx;
  906. }
  907. .slider-label {
  908. font-size: 24rpx;
  909. color: #999;
  910. white-space: nowrap;
  911. width: 64rpx;
  912. }
  913. .custom-slider {
  914. flex: 1;
  915. }
  916. .slider-value {
  917. font-size: 28rpx;
  918. font-weight: 600;
  919. color: #FF6B9D;
  920. width: 40rpx;
  921. text-align: center;
  922. }
  923. .intensity-hint {
  924. font-size: 22rpx;
  925. color: #aaa;
  926. text-align: center;
  927. margin-top: 4rpx;
  928. }
  929. /* ===== 情绪标签 ===== */
  930. .tag-grid {
  931. display: flex;
  932. flex-wrap: wrap;
  933. gap: 16rpx;
  934. }
  935. .tag-btn {
  936. padding: 12rpx 28rpx;
  937. border-radius: 30rpx;
  938. background: #F5F5F5;
  939. font-size: 26rpx;
  940. color: #666;
  941. border: 2rpx solid transparent;
  942. }
  943. .tag-active {
  944. background: #FFF0F5;
  945. color: #FF6B9D;
  946. border-color: #FF6B9D;
  947. }
  948. .custom-tag-row {
  949. display: flex;
  950. align-items: center;
  951. margin-top: 16rpx;
  952. gap: 16rpx;
  953. }
  954. .custom-tag-input {
  955. flex: 1;
  956. height: 60rpx;
  957. border: 2rpx solid #E0E0E0;
  958. border-radius: 12rpx;
  959. padding: 0 16rpx;
  960. font-size: 26rpx;
  961. }
  962. .custom-tag-add {
  963. padding: 12rpx 24rpx;
  964. background: #FF6B9D;
  965. color: #fff;
  966. border-radius: 12rpx;
  967. font-size: 26rpx;
  968. }
  969. .custom-tag-trigger {
  970. display: inline-block;
  971. margin-top: 16rpx;
  972. color: #FF6B9D;
  973. font-size: 26rpx;
  974. }
  975. /* ===== 拍照 ===== */
  976. .photo-area {
  977. width: 200rpx;
  978. height: 200rpx;
  979. border-radius: 16rpx;
  980. overflow: hidden;
  981. background: #F5F5F5;
  982. display: flex;
  983. align-items: center;
  984. justify-content: center;
  985. }
  986. .photo-preview {
  987. width: 100%;
  988. height: 100%;
  989. }
  990. .photo-placeholder {
  991. display: flex;
  992. flex-direction: column;
  993. align-items: center;
  994. }
  995. .photo-icon {
  996. font-size: 60rpx;
  997. }
  998. .photo-hint {
  999. font-size: 24rpx;
  1000. color: #999;
  1001. margin-top: 8rpx;
  1002. }
  1003. .photo-note {
  1004. display: block;
  1005. font-size: 22rpx;
  1006. color: #999;
  1007. margin-top: 12rpx;
  1008. }
  1009. /* ===== 拍照/录像模式切换 ===== */
  1010. .capture-mode-bar {
  1011. display: flex;
  1012. gap: 20rpx;
  1013. margin-bottom: 20rpx;
  1014. }
  1015. .mode-btn {
  1016. padding: 10rpx 28rpx;
  1017. border-radius: 30rpx;
  1018. font-size: 26rpx;
  1019. color: #999;
  1020. background: #f5f5f5;
  1021. border: 2rpx solid #eee;
  1022. }
  1023. .mode-btn.mode-active {
  1024. color: #FF6B9D;
  1025. background: #FFF0F5;
  1026. border-color: #FF6B9D;
  1027. font-weight: 500;
  1028. }
  1029. .video-preview {
  1030. width: 100%;
  1031. height: 100%;
  1032. border-radius: 16rpx;
  1033. }
  1034. /* ===== 情绪识别 ===== */
  1035. .ai-recognize-btn {
  1036. display: flex;
  1037. align-items: center;
  1038. justify-content: center;
  1039. padding: 20rpx;
  1040. background: linear-gradient(135deg, #8B5CF6, #A78BFA);
  1041. border-radius: 16rpx;
  1042. cursor: pointer;
  1043. }
  1044. .ai-btn-icon {
  1045. font-size: 32rpx;
  1046. margin-right: 10rpx;
  1047. }
  1048. .ai-btn-text {
  1049. font-size: 26rpx;
  1050. color: #fff;
  1051. font-weight: 500;
  1052. }
  1053. .ai-recognize-btn:active {
  1054. opacity: 0.8;
  1055. }
  1056. .ai-analyzing {
  1057. display: flex;
  1058. align-items: center;
  1059. justify-content: center;
  1060. padding: 30rpx 0;
  1061. }
  1062. .loading-text {
  1063. font-size: 26rpx;
  1064. color: #8B5CF6;
  1065. }
  1066. .ai-result {
  1067. margin-top: 16rpx;
  1068. }
  1069. .ai-result-title {
  1070. font-size: 24rpx;
  1071. color: #666;
  1072. margin-bottom: 12rpx;
  1073. display: block;
  1074. }
  1075. .ai-emotion-list {
  1076. display: flex;
  1077. flex-direction: column;
  1078. gap: 12rpx;
  1079. }
  1080. .ai-emotion-item {
  1081. display: flex;
  1082. align-items: center;
  1083. gap: 12rpx;
  1084. }
  1085. .ai-emotion-label {
  1086. font-size: 24rpx;
  1087. color: #333;
  1088. width: 64rpx;
  1089. flex-shrink: 0;
  1090. }
  1091. .ai-emotion-bar-track {
  1092. flex: 1;
  1093. height: 16rpx;
  1094. background: #F0F0F0;
  1095. border-radius: 8rpx;
  1096. overflow: hidden;
  1097. }
  1098. .ai-emotion-bar-fill {
  1099. height: 100%;
  1100. border-radius: 8rpx;
  1101. transition: width 0.5s;
  1102. }
  1103. .ai-emotion-value {
  1104. font-size: 22rpx;
  1105. color: #999;
  1106. width: 56rpx;
  1107. text-align: right;
  1108. }
  1109. .ai-apply-tags {
  1110. margin-top: 20rpx;
  1111. padding: 16rpx;
  1112. background: #F5F0FF;
  1113. border-radius: 12rpx;
  1114. text-align: center;
  1115. cursor: pointer;
  1116. }
  1117. .ai-apply-text {
  1118. font-size: 26rpx;
  1119. color: #8B5CF6;
  1120. font-weight: 500;
  1121. }
  1122. .ai-apply-tags:active {
  1123. opacity: 0.7;
  1124. }
  1125. .note-input {
  1126. width: 100%;
  1127. min-height: 140rpx;
  1128. background: #F9F9F9;
  1129. border-radius: 12rpx;
  1130. padding: 20rpx;
  1131. font-size: 26rpx;
  1132. color: #333;
  1133. box-sizing: border-box;
  1134. }
  1135. /* ===== 近期心情历史(合并自成长页心情记录) ===== */
  1136. .mood-list {
  1137. display: flex;
  1138. flex-direction: column;
  1139. }
  1140. .mood-item {
  1141. display: flex;
  1142. align-items: center;
  1143. padding: 20rpx 0;
  1144. border-bottom: 1rpx solid #F5F5F5;
  1145. }
  1146. .mood-item:last-child {
  1147. border-bottom: none;
  1148. }
  1149. .mood-emotion {
  1150. font-size: 40rpx;
  1151. margin-right: 20rpx;
  1152. flex-shrink: 0;
  1153. }
  1154. .mood-info {
  1155. flex: 1;
  1156. display: flex;
  1157. flex-direction: column;
  1158. }
  1159. .mood-label {
  1160. font-size: 28rpx;
  1161. color: #333;
  1162. font-weight: 500;
  1163. }
  1164. .mood-date {
  1165. font-size: 22rpx;
  1166. color: #999;
  1167. margin-top: 6rpx;
  1168. }
  1169. .mood-note {
  1170. font-size: 26rpx;
  1171. color: #666;
  1172. max-width: 300rpx;
  1173. overflow: hidden;
  1174. text-overflow: ellipsis;
  1175. white-space: nowrap;
  1176. margin-left: 20rpx;
  1177. flex-shrink: 0;
  1178. }
  1179. .submit-area {
  1180. margin: 40rpx 32rpx;
  1181. }
  1182. .submit-btn {
  1183. width: 100%;
  1184. height: 88rpx;
  1185. line-height: 88rpx;
  1186. background: linear-gradient(135deg, #FF6B9D, #FF8CB3);
  1187. color: #fff;
  1188. border-radius: 44rpx;
  1189. font-size: 30rpx;
  1190. font-weight: 600;
  1191. display: flex;
  1192. align-items: center;
  1193. justify-content: center;
  1194. }
  1195. .submit-disabled {
  1196. opacity: 0.5;
  1197. }
  1198. .submit-icon {
  1199. margin-right: 8rpx;
  1200. }
  1201. .loading-mask {
  1202. position: fixed;
  1203. top: 0;
  1204. left: 0;
  1205. right: 0;
  1206. bottom: 0;
  1207. background: rgba(0, 0, 0, 0.3);
  1208. display: flex;
  1209. align-items: center;
  1210. justify-content: center;
  1211. z-index: 999;
  1212. }
  1213. .loading-box {
  1214. background: #fff;
  1215. padding: 40rpx 60rpx;
  1216. border-radius: 20rpx;
  1217. }
  1218. .loading-text {
  1219. font-size: 28rpx;
  1220. color: #666;
  1221. }
  1222. </style>