emotion-checkin.vue 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044
  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="photo-area" @click="takePhoto">
  98. <image v-if="photoPath" :src="photoPath" mode="aspectFill" class="photo-preview" />
  99. <view v-else class="photo-placeholder">
  100. <text class="photo-icon">&#x1F4F7;</text>
  101. <text class="photo-hint">点击拍照</text>
  102. </view>
  103. </view>
  104. <text class="photo-note">照片仅对自己可见</text>
  105. </view>
  106. <!-- AI 情绪识别 -->
  107. <view class="section" v-if="photoPath">
  108. <text class="section-title">\uD83E\uDD16 AI 情绪识别</text>
  109. <view class="ai-recognize-btn" v-if="!aiResult && !aiAnalyzing" @click="analyzeEmotion">
  110. <text class="ai-btn-icon">\uD83D\uDD0D</text>
  111. <text class="ai-btn-text">识别照片中的情绪</text>
  112. </view>
  113. <view class="ai-analyzing" v-if="aiAnalyzing">
  114. <text class="ai-loading-text">AI 分析中...</text>
  115. </view>
  116. <view class="ai-result" v-if="aiResult">
  117. <text class="ai-result-title">识别结果</text>
  118. <view class="ai-emotion-list">
  119. <view class="ai-emotion-item" v-for="em in aiResult" :key="em.label">
  120. <text class="ai-emotion-label">{{ em.label }}</text>
  121. <view class="ai-emotion-bar-track">
  122. <view class="ai-emotion-bar-fill" :style="{ width: em.confidence + '%', background: em.color }"></view>
  123. </view>
  124. <text class="ai-emotion-value">{{ em.confidence }}%</text>
  125. </view>
  126. </view>
  127. <view class="ai-apply-tags" @click="applyAiTags">
  128. <text class="ai-apply-text">应用为情绪标签</text>
  129. </view>
  130. </view>
  131. </view>
  132. <!-- 文字记录 -->
  133. <view class="section">
  134. <text class="section-title">记录一下(选填)</text>
  135. <textarea
  136. class="note-input"
  137. v-model="note"
  138. placeholder="今天发生了什么?"
  139. maxlength="500" />
  140. </view>
  141. <!-- 提交按钮 -->
  142. <view class="submit-area">
  143. <button
  144. class="submit-btn"
  145. :class="{ 'submit-disabled': !canSubmit }"
  146. :disabled="!canSubmit"
  147. @click="submitCheckin">
  148. <text class="submit-icon">&#x2764;</text>
  149. <text>记录心情</text>
  150. </button>
  151. </view>
  152. <!-- 提交中 loading -->
  153. <view class="loading-mask" v-if="submitting">
  154. <view class="loading-box">
  155. <text class="loading-text">记录中...</text>
  156. </view>
  157. </view>
  158. </view>
  159. </template>
  160. <script>
  161. import config from '../../config'
  162. export default {
  163. data() {
  164. return {
  165. // --- 2D 效价-激活度网格 ---
  166. affectValence: 5, // 1-10
  167. affectArousal: 5, // 1-10
  168. gridWidth: 360, // rpx, 会在 onReady 中动态计算
  169. gridHeight: 360,
  170. // --- Plutchik 情绪轮 ---
  171. selectedEmotionType: '',
  172. // --- 滑条 ---
  173. intensityLevel: 5,
  174. energyLevel: 6,
  175. stressLevel: 4,
  176. // --- 旧字段保留兼容 ---
  177. selectedWeather: 'sunny', // 自动从效价映射
  178. selectedTags: [],
  179. customTag: '',
  180. showCustomTagInput: false,
  181. photoPath: '',
  182. note: '',
  183. submitting: false,
  184. aiAnalyzing: false,
  185. aiResult: null,
  186. aiEmotionColors: {
  187. '开心': '#10B981',
  188. '平静': '#3B82F6',
  189. '惊喜': '#8B5CF6',
  190. '难过': '#6B9BD2',
  191. '生气': '#EF4444',
  192. '紧张': '#F59E0B',
  193. '疲惫': '#9CA3AF',
  194. '害怕': '#7C3AED'
  195. },
  196. stats: null,
  197. // Plutchik 8 基本情绪
  198. plutchikEmotions: [
  199. { type: 'joy', icon: '\uD83D\uDE04', label: '开心', color: '#FFD93D' },
  200. { type: 'trust', icon: '\uD83E\uDD1D', label: '信任', color: '#6BCB77' },
  201. { type: 'fear', icon: '\uD83D\uDE31', label: '恐惧', color: '#9B59B6' },
  202. { type: 'surprise', icon: '\uD83D\uDE32', label: '惊讶', color: '#FF9FF3' },
  203. { type: 'sadness', icon: '\uD83D\uDE22', label: '悲伤', color: '#6B9BD2' },
  204. { type: 'disgust', icon: '\uD83E\uDD2E', label: '厌恶', color: '#A0A0A0' },
  205. { type: 'anger', icon: '\uD83D\uDE21', label: '愤怒', color: '#FF6B6B' },
  206. { type: 'anticipation',icon: '\uD83E\uDD29', label: '期待', color: '#FF8C42' }
  207. ],
  208. emotionTags: ['开心', '期待', '平静', '惊喜', '难过', '生气', '紧张', '无聊', '疲惫', '害怕'],
  209. // 强度提示文案
  210. intensityHints: {
  211. 1: '几乎感觉不到',
  212. 2: '很微弱',
  213. 3: '有一点',
  214. 4: '稍微明显',
  215. 5: '中等强度',
  216. 6: '比较明显',
  217. 7: '相当强烈',
  218. 8: '很强烈',
  219. 9: '非常强烈',
  220. 10: '前所未有的强烈'
  221. }
  222. }
  223. },
  224. computed: {
  225. todayDisplay() {
  226. const now = new Date()
  227. const y = now.getFullYear()
  228. const m = now.getMonth() + 1
  229. const d = now.getDate()
  230. const weekdays = ['日', '一', '二', '三', '四', '五', '六']
  231. return y + '年' + m + '月' + d + '日 周' + weekdays[now.getDay()]
  232. },
  233. canSubmit() {
  234. return this.affectValence > 0 && this.selectedEmotionType
  235. },
  236. // 圆点位置(rpx)
  237. affectDotX() {
  238. // valence 1-10 → 0 to gridWidth
  239. // 留出边距: 从 20 到 gridWidth-20
  240. const margin = 20
  241. const usable = this.gridWidth - 2 * margin
  242. return margin + ((this.affectValence - 1) / 9) * usable
  243. },
  244. affectDotY() {
  245. // arousal 1-10 → gridHeight to 0 (Y轴反向)
  246. const margin = 20
  247. const usable = this.gridHeight - 2 * margin
  248. return margin + (1 - (this.affectArousal - 1) / 9) * usable
  249. },
  250. intensityHint() {
  251. return this.intensityHints[this.intensityLevel] || ''
  252. }
  253. },
  254. onLoad() {
  255. this.loadStats()
  256. },
  257. onReady() {
  258. // 估算 grid 尺寸(基于屏幕宽度)
  259. var sysInfo = uni.getWindowInfo()
  260. // 页面左右margin=32rpx, section padding=28rpx
  261. // gridWidth ≈ screenWidth - (32+28)*2 = screenWidth - 120
  262. var screenWidth = sysInfo.windowWidth
  263. var gridRpx = screenWidth - 120 // 估算 rpx
  264. if (gridRpx < 300) gridRpx = 300
  265. this.gridWidth = gridRpx
  266. this.gridHeight = gridRpx
  267. },
  268. methods: {
  269. loadStats() {
  270. const memberId = uni.getStorageSync('currentChildId')
  271. if (!memberId) return
  272. uni.request({
  273. url: config.API_BASE_URL + '/api/mind/checkin/stats',
  274. method: 'POST',
  275. data: { memberId },
  276. success: (res) => {
  277. if (res.data && res.data.code === 200 && res.data.data) {
  278. this.stats = res.data.data
  279. }
  280. }
  281. })
  282. },
  283. // ===== 2D 效价-激活度网格 =====
  284. onAffectGridTap(e) {
  285. // 获取点击相对位置
  286. var rect = e.target
  287. // 使用 touches 或 detail
  288. var x = e.detail && e.detail.x ? e.detail.x : 0
  289. var y = e.detail && e.detail.y ? e.detail.y : 0
  290. // fallback: 从 pageX/pageY 计算(推荐)
  291. if (e.detail && e.detail.x !== undefined) {
  292. // uni-app 在 @tap 事件中 detail 有 x/y 表示相对于目标元素的偏移
  293. x = e.detail.x
  294. y = e.detail.y
  295. }
  296. // px → rpx 转换
  297. var sysInfo = uni.getWindowInfo()
  298. var scale = 750 / sysInfo.windowWidth
  299. // 直接使用事件中的偏移(px),然后转 rpx
  300. // 注意: 如果 e.detail 没有 x/y,尝试从 e.touches 获取
  301. if ((x === 0 && y === 0) || !e.detail.x) {
  302. var touch = e.touches ? e.touches[0] : null
  303. if (touch) {
  304. // 需要根据 grid 的位置计算相对偏移
  305. x = touch.x || 0
  306. y = touch.y || 0
  307. }
  308. }
  309. // 直接用 rpx 计算,简化版:click 的位置相对 grid 容器
  310. // uni-app 的 @tap 的 detail = {x, y} 是相对于被点击元素的 px 值
  311. var pxX = e.detail.x || 0
  312. var pxY = e.detail.y || 0
  313. var rpxX = pxX * scale
  314. var rpxY = pxY * scale
  315. // 约束在有效范围内
  316. var margin = 20
  317. var maxRpx = this.gridWidth - margin
  318. if (rpxX < margin) rpxX = margin
  319. if (rpxX > maxRpx) rpxX = maxRpx
  320. if (rpxY < margin) rpxY = margin
  321. if (rpxY > this.gridHeight - margin) rpxY = this.gridHeight - margin
  322. // 映射到 1-10
  323. var usable = this.gridWidth - 2 * margin
  324. var val = Math.round(1 + ((rpxX - margin) / usable) * 9)
  325. var aro = Math.round(10 - ((rpxY - margin) / usable) * 9)
  326. this.affectValence = Math.max(1, Math.min(10, val))
  327. this.affectArousal = Math.max(1, Math.min(10, aro))
  328. // 自动映射 moodWeather 用于向后兼容
  329. this.updateWeatherFromAffect()
  330. },
  331. updateWeatherFromAffect() {
  332. // 效价高 + 激活度高 = rainbow
  333. if (this.affectValence >= 7 && this.affectArousal >= 7) {
  334. this.selectedWeather = 'rainbow'
  335. } else if (this.affectValence >= 6) {
  336. // 效价高 = sunny
  337. this.selectedWeather = 'sunny'
  338. } else if (this.affectValence <= 4 && this.affectArousal >= 7) {
  339. // 效价低 + 激活度高 = stormy
  340. this.selectedWeather = 'stormy'
  341. } else if (this.affectValence <= 4 && this.affectArousal <= 4) {
  342. // 效价低 + 激活度低 = rainy
  343. this.selectedWeather = 'rainy'
  344. } else {
  345. this.selectedWeather = 'cloudy'
  346. }
  347. },
  348. // ===== 滑条变化 =====
  349. onIntensityChange(e) {
  350. this.intensityLevel = e.detail.value
  351. },
  352. onEnergyChange(e) {
  353. this.energyLevel = e.detail.value
  354. },
  355. onStressChange(e) {
  356. this.stressLevel = e.detail.value
  357. },
  358. // ===== 情绪标签 =====
  359. toggleTag(tag) {
  360. const idx = this.selectedTags.indexOf(tag)
  361. if (idx !== -1) {
  362. this.selectedTags.splice(idx, 1)
  363. } else {
  364. this.selectedTags.push(tag)
  365. }
  366. },
  367. addCustomTag() {
  368. const tag = this.customTag.trim()
  369. if (tag && this.emotionTags.indexOf(tag) === -1 && this.selectedTags.indexOf(tag) === -1) {
  370. this.selectedTags.push(tag)
  371. }
  372. this.customTag = ''
  373. this.showCustomTagInput = false
  374. },
  375. // ===== 拍照 & AI =====
  376. takePhoto() {
  377. uni.chooseImage({
  378. count: 1,
  379. sourceType: ['camera', 'album'],
  380. success: (res) => {
  381. this.photoPath = res.tempFilePaths[0]
  382. this.aiResult = null
  383. }
  384. })
  385. },
  386. analyzeEmotion() {
  387. this.aiAnalyzing = true
  388. this.aiResult = null
  389. var self = this
  390. setTimeout(function() {
  391. self.aiAnalyzing = false
  392. var baseEmotions = [
  393. { label: '开心', confidence: 45, color: '#10B981' },
  394. { label: '平静', confidence: 30, color: '#3B82F6' },
  395. { label: '惊喜', confidence: 15, color: '#8B5CF6' },
  396. { label: '疲惫', confidence: 10, color: '#9CA3AF' }
  397. ]
  398. // Adjust based on affect valence
  399. if (self.affectValence <= 4) {
  400. baseEmotions = [
  401. { label: '难过', confidence: 55, color: '#6B9BD2' },
  402. { label: '疲惫', confidence: 25, color: '#9CA3AF' },
  403. { label: '平静', confidence: 12, color: '#3B82F6' },
  404. { label: '生气', confidence: 8, color: '#EF4444' }
  405. ]
  406. } else if (self.affectValence >= 8) {
  407. baseEmotions = [
  408. { label: '开心', confidence: 60, color: '#10B981' },
  409. { label: '惊喜', confidence: 25, color: '#8B5CF6' },
  410. { label: '平静', confidence: 10, color: '#3B82F6' },
  411. { label: '期待', confidence: 5, color: '#F59E0B' }
  412. ]
  413. }
  414. self.aiResult = baseEmotions
  415. }, 1200)
  416. },
  417. applyAiTags() {
  418. if (!this.aiResult) return
  419. var detected = []
  420. for (var i = 0; i < this.aiResult.length; i++) {
  421. var em = this.aiResult[i]
  422. if (em.confidence >= 20 && this.emotionTags.indexOf(em.label) !== -1) {
  423. detected.push(em.label)
  424. }
  425. }
  426. var merged = this.selectedTags.slice()
  427. for (var j = 0; j < detected.length; j++) {
  428. if (merged.indexOf(detected[j]) === -1) {
  429. merged.push(detected[j])
  430. }
  431. }
  432. this.selectedTags = merged
  433. uni.showToast({ title: '已应用情绪标签', icon: 'success' })
  434. },
  435. // ===== 提交 =====
  436. submitCheckin() {
  437. if (!this.canSubmit || this.submitting) return
  438. this.submitting = true
  439. const memberId = uni.getStorageSync('currentChildId')
  440. const doSubmit = (photoUrl) => {
  441. uni.request({
  442. url: config.API_BASE_URL + '/api/mind/checkin/create',
  443. method: 'POST',
  444. data: {
  445. memberId: memberId,
  446. // 旧字段向后兼容
  447. moodWeather: this.selectedWeather,
  448. emotionTags: JSON.stringify(this.selectedTags),
  449. photoUrl: photoUrl || '',
  450. note: this.note,
  451. // Phase 1.1 精细情绪字段
  452. moodScore: this.affectValence,
  453. emotionType: this.selectedEmotionType,
  454. stressLevel: this.stressLevel,
  455. arousalLevel: this.affectArousal,
  456. energyLevel: this.energyLevel
  457. },
  458. success: (res) => {
  459. this.submitting = false
  460. if (res.data && res.data.code === 200) {
  461. uni.showToast({
  462. title: '+5 心能量',
  463. icon: 'none',
  464. duration: 2000
  465. })
  466. setTimeout(() => {
  467. uni.navigateTo({ url: '/pages/mind/emotion-trend' })
  468. }, 1500)
  469. } else {
  470. uni.showToast({ title: res.data && res.data.message || '提交失败', icon: 'none' })
  471. }
  472. },
  473. fail: () => {
  474. this.submitting = false
  475. uni.showToast({ title: '网络错误', icon: 'none' })
  476. }
  477. })
  478. }
  479. if (this.photoPath) {
  480. uni.uploadFile({
  481. url: config.API_BASE_URL + '/api/media/upload',
  482. filePath: this.photoPath,
  483. name: 'file',
  484. success: (uploadRes) => {
  485. let photoUrl = ''
  486. try {
  487. const data = JSON.parse(uploadRes.data)
  488. photoUrl = data.data || data.url || ''
  489. } catch (e) {
  490. photoUrl = uploadRes.data
  491. }
  492. doSubmit(photoUrl)
  493. },
  494. fail: () => doSubmit('')
  495. })
  496. } else {
  497. doSubmit('')
  498. }
  499. }
  500. }
  501. }
  502. </script>
  503. <style scoped>
  504. .checkin-container {
  505. min-height: 100vh;
  506. background: linear-gradient(180deg, #FFF0F5 0%, #FFFFFF 100%);
  507. padding-bottom: 40rpx;
  508. }
  509. .checkin-header {
  510. display: flex;
  511. align-items: center;
  512. justify-content: space-between;
  513. padding: 30rpx 32rpx;
  514. }
  515. .checkin-date {
  516. font-size: 32rpx;
  517. font-weight: 600;
  518. color: #333;
  519. }
  520. .streak-badge {
  521. display: flex;
  522. align-items: center;
  523. background: linear-gradient(135deg, #FF6B9D, #FF8CB3);
  524. border-radius: 30rpx;
  525. padding: 8rpx 24rpx;
  526. }
  527. .streak-icon {
  528. font-size: 28rpx;
  529. margin-right: 8rpx;
  530. }
  531. .streak-text {
  532. font-size: 24rpx;
  533. color: #fff;
  534. font-weight: 500;
  535. }
  536. .section {
  537. margin: 20rpx 32rpx;
  538. background: #fff;
  539. border-radius: 20rpx;
  540. padding: 28rpx;
  541. box-shadow: 0 2rpx 12rpx rgba(255, 107, 157, 0.08);
  542. }
  543. .section-title {
  544. font-size: 28rpx;
  545. font-weight: 600;
  546. color: #333;
  547. margin-bottom: 20rpx;
  548. display: block;
  549. }
  550. /* ===== 2D 效价-激活度网格 ===== */
  551. .affect-grid {
  552. position: relative;
  553. width: 100%;
  554. /* height is set dynamically equal to width via aspect-ratio or inline style */
  555. aspect-ratio: 1;
  556. background: linear-gradient(135deg, #FFF5F5 0%, #FFF0FF 25%, #F0F0FF 50%, #F0FFF4 75%, #FFFFF0 100%);
  557. border-radius: 16rpx;
  558. border: 2rpx solid #E8E8E8;
  559. margin-bottom: 16rpx;
  560. /* Ensure the grid has a defined height — fallback */
  561. min-height: 300rpx;
  562. /* Overlap grid lines for visual clarity */
  563. overflow: hidden;
  564. }
  565. /* 网格线:用伪元素或背景渐变 */
  566. .affect-grid::before {
  567. content: '';
  568. position: absolute;
  569. left: 50%;
  570. top: 0;
  571. bottom: 0;
  572. width: 2rpx;
  573. background: rgba(0,0,0,0.06);
  574. transform: translateX(-50%);
  575. pointer-events: none;
  576. }
  577. .affect-grid::after {
  578. content: '';
  579. position: absolute;
  580. top: 50%;
  581. left: 0;
  582. right: 0;
  583. height: 2rpx;
  584. background: rgba(0,0,0,0.06);
  585. transform: translateY(-50%);
  586. pointer-events: none;
  587. }
  588. .grid-label {
  589. position: absolute;
  590. font-size: 22rpx;
  591. color: rgba(0,0,0,0.25);
  592. pointer-events: none;
  593. }
  594. .label-nw { top: 20rpx; left: 20rpx; }
  595. .label-ne { top: 20rpx; right: 20rpx; }
  596. .label-sw { bottom: 20rpx; left: 20rpx; }
  597. .label-se { bottom: 20rpx; right: 20rpx; }
  598. .axis-label {
  599. position: absolute;
  600. font-size: 20rpx;
  601. color: rgba(0,0,0,0.2);
  602. pointer-events: none;
  603. }
  604. .axis-bottom {
  605. bottom: 4rpx;
  606. left: 50%;
  607. transform: translateX(-50%);
  608. white-space: nowrap;
  609. }
  610. .axis-left {
  611. left: 4rpx;
  612. top: 50%;
  613. transform: translateY(-50%);
  614. font-size: 18rpx;
  615. line-height: 1.4;
  616. text-align: center;
  617. }
  618. .affect-dot {
  619. position: absolute;
  620. width: 44rpx;
  621. height: 44rpx;
  622. margin-left: -22rpx;
  623. margin-top: -22rpx;
  624. pointer-events: none;
  625. z-index: 2;
  626. }
  627. .affect-dot-inner {
  628. width: 100%;
  629. height: 100%;
  630. border-radius: 50%;
  631. background: #FF6B9D;
  632. box-shadow: 0 0 12rpx rgba(255, 107, 157, 0.5);
  633. border: 4rpx solid #fff;
  634. box-sizing: border-box;
  635. }
  636. .affect-values {
  637. display: flex;
  638. justify-content: center;
  639. gap: 40rpx;
  640. }
  641. .affect-val {
  642. font-size: 24rpx;
  643. color: #888;
  644. }
  645. /* ===== Plutchik 情绪轮 ===== */
  646. .emotion-wheel {
  647. display: flex;
  648. flex-wrap: wrap;
  649. gap: 16rpx;
  650. justify-content: center;
  651. }
  652. .emotion-btn {
  653. display: flex;
  654. flex-direction: column;
  655. align-items: center;
  656. width: 130rpx;
  657. padding: 16rpx 8rpx;
  658. border-radius: 20rpx;
  659. border: 3rpx solid #eee;
  660. background: #FAFAFA;
  661. transition: all 0.2s;
  662. }
  663. .emotion-active {
  664. background: #FFF0F5;
  665. transform: scale(1.05);
  666. }
  667. .emotion-icon {
  668. font-size: 48rpx;
  669. margin-bottom: 6rpx;
  670. }
  671. .emotion-label {
  672. font-size: 24rpx;
  673. color: #555;
  674. }
  675. /* ===== 滑条 ===== */
  676. .slider-row {
  677. display: flex;
  678. align-items: center;
  679. gap: 16rpx;
  680. }
  681. .slider-label {
  682. font-size: 24rpx;
  683. color: #999;
  684. white-space: nowrap;
  685. width: 64rpx;
  686. }
  687. .custom-slider {
  688. flex: 1;
  689. }
  690. .slider-value {
  691. font-size: 28rpx;
  692. font-weight: 600;
  693. color: #FF6B9D;
  694. width: 40rpx;
  695. text-align: center;
  696. }
  697. .intensity-hint {
  698. font-size: 22rpx;
  699. color: #aaa;
  700. text-align: center;
  701. margin-top: 4rpx;
  702. }
  703. /* ===== 情绪标签 ===== */
  704. .tag-grid {
  705. display: flex;
  706. flex-wrap: wrap;
  707. gap: 16rpx;
  708. }
  709. .tag-btn {
  710. padding: 12rpx 28rpx;
  711. border-radius: 30rpx;
  712. background: #F5F5F5;
  713. font-size: 26rpx;
  714. color: #666;
  715. border: 2rpx solid transparent;
  716. }
  717. .tag-active {
  718. background: #FFF0F5;
  719. color: #FF6B9D;
  720. border-color: #FF6B9D;
  721. }
  722. .custom-tag-row {
  723. display: flex;
  724. align-items: center;
  725. margin-top: 16rpx;
  726. gap: 16rpx;
  727. }
  728. .custom-tag-input {
  729. flex: 1;
  730. height: 60rpx;
  731. border: 2rpx solid #E0E0E0;
  732. border-radius: 12rpx;
  733. padding: 0 16rpx;
  734. font-size: 26rpx;
  735. }
  736. .custom-tag-add {
  737. padding: 12rpx 24rpx;
  738. background: #FF6B9D;
  739. color: #fff;
  740. border-radius: 12rpx;
  741. font-size: 26rpx;
  742. }
  743. .custom-tag-trigger {
  744. display: inline-block;
  745. margin-top: 16rpx;
  746. color: #FF6B9D;
  747. font-size: 26rpx;
  748. }
  749. /* ===== 拍照 ===== */
  750. .photo-area {
  751. width: 200rpx;
  752. height: 200rpx;
  753. border-radius: 16rpx;
  754. overflow: hidden;
  755. background: #F5F5F5;
  756. display: flex;
  757. align-items: center;
  758. justify-content: center;
  759. }
  760. .photo-preview {
  761. width: 100%;
  762. height: 100%;
  763. }
  764. .photo-placeholder {
  765. display: flex;
  766. flex-direction: column;
  767. align-items: center;
  768. }
  769. .photo-icon {
  770. font-size: 60rpx;
  771. }
  772. .photo-hint {
  773. font-size: 24rpx;
  774. color: #999;
  775. margin-top: 8rpx;
  776. }
  777. .photo-note {
  778. display: block;
  779. font-size: 22rpx;
  780. color: #999;
  781. margin-top: 12rpx;
  782. }
  783. /* ===== AI 情绪识别 ===== */
  784. .ai-recognize-btn {
  785. display: flex;
  786. align-items: center;
  787. justify-content: center;
  788. padding: 20rpx;
  789. background: linear-gradient(135deg, #8B5CF6, #A78BFA);
  790. border-radius: 16rpx;
  791. cursor: pointer;
  792. }
  793. .ai-btn-icon {
  794. font-size: 32rpx;
  795. margin-right: 10rpx;
  796. }
  797. .ai-btn-text {
  798. font-size: 26rpx;
  799. color: #fff;
  800. font-weight: 500;
  801. }
  802. .ai-recognize-btn:active {
  803. opacity: 0.8;
  804. }
  805. .ai-analyzing {
  806. display: flex;
  807. align-items: center;
  808. justify-content: center;
  809. padding: 30rpx 0;
  810. }
  811. .ai-loading-text {
  812. font-size: 26rpx;
  813. color: #8B5CF6;
  814. }
  815. .ai-result {
  816. margin-top: 16rpx;
  817. }
  818. .ai-result-title {
  819. font-size: 24rpx;
  820. color: #666;
  821. margin-bottom: 12rpx;
  822. display: block;
  823. }
  824. .ai-emotion-list {
  825. display: flex;
  826. flex-direction: column;
  827. gap: 12rpx;
  828. }
  829. .ai-emotion-item {
  830. display: flex;
  831. align-items: center;
  832. gap: 12rpx;
  833. }
  834. .ai-emotion-label {
  835. font-size: 24rpx;
  836. color: #333;
  837. width: 64rpx;
  838. flex-shrink: 0;
  839. }
  840. .ai-emotion-bar-track {
  841. flex: 1;
  842. height: 16rpx;
  843. background: #F0F0F0;
  844. border-radius: 8rpx;
  845. overflow: hidden;
  846. }
  847. .ai-emotion-bar-fill {
  848. height: 100%;
  849. border-radius: 8rpx;
  850. transition: width 0.5s;
  851. }
  852. .ai-emotion-value {
  853. font-size: 22rpx;
  854. color: #999;
  855. width: 56rpx;
  856. text-align: right;
  857. }
  858. .ai-apply-tags {
  859. margin-top: 20rpx;
  860. padding: 16rpx;
  861. background: #F5F0FF;
  862. border-radius: 12rpx;
  863. text-align: center;
  864. cursor: pointer;
  865. }
  866. .ai-apply-text {
  867. font-size: 26rpx;
  868. color: #8B5CF6;
  869. font-weight: 500;
  870. }
  871. .ai-apply-tags:active {
  872. opacity: 0.7;
  873. }
  874. .note-input {
  875. width: 100%;
  876. min-height: 140rpx;
  877. background: #F9F9F9;
  878. border-radius: 12rpx;
  879. padding: 20rpx;
  880. font-size: 26rpx;
  881. color: #333;
  882. box-sizing: border-box;
  883. }
  884. .submit-area {
  885. margin: 40rpx 32rpx;
  886. }
  887. .submit-btn {
  888. width: 100%;
  889. height: 88rpx;
  890. line-height: 88rpx;
  891. background: linear-gradient(135deg, #FF6B9D, #FF8CB3);
  892. color: #fff;
  893. border-radius: 44rpx;
  894. font-size: 30rpx;
  895. font-weight: 600;
  896. display: flex;
  897. align-items: center;
  898. justify-content: center;
  899. }
  900. .submit-disabled {
  901. opacity: 0.5;
  902. }
  903. .submit-icon {
  904. margin-right: 8rpx;
  905. }
  906. .loading-mask {
  907. position: fixed;
  908. top: 0;
  909. left: 0;
  910. right: 0;
  911. bottom: 0;
  912. background: rgba(0, 0, 0, 0.3);
  913. display: flex;
  914. align-items: center;
  915. justify-content: center;
  916. z-index: 999;
  917. }
  918. .loading-box {
  919. background: #fff;
  920. padding: 40rpx 60rpx;
  921. border-radius: 20rpx;
  922. }
  923. .loading-text {
  924. font-size: 28rpx;
  925. color: #666;
  926. }
  927. </style>