| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372 |
- <template>
- <view class="checkin-container">
- <!-- 顶部:日期 + 连续打卡 -->
- <view class="checkin-header">
- <text class="checkin-date">{{ todayDisplay }}</text>
- <view class="streak-badge" v-if="stats && stats.currentStreak > 0">
- <text class="streak-icon">🔥</text>
- <text class="streak-text">连续{{ stats.currentStreak }}天</text>
- </view>
- </view>
- <!-- ===== Phase 1.1: 2D 效价-激活度网格 ===== -->
- <view class="section">
- <text class="section-title">在方格中点击你的情绪位置</text>
- <view class="affect-grid" @tap="onAffectGridTap">
- <!-- 象限标签 -->
- <text class="grid-label label-nw">烦躁/紧张</text>
- <text class="grid-label label-ne">兴奋/愉快</text>
- <text class="grid-label label-sw">低落/疲惫</text>
- <text class="grid-label label-se">平静/放松</text>
- <!-- 坐标轴 -->
- <text class="axis-label axis-bottom">消极 ← 情绪效价 → 积极</text>
- <text class="axis-label axis-left">激<br/>活<br/>度</text>
- <!-- 选中的圆点 -->
- <view class="affect-dot" :style="{ left: affectDotX + 'rpx', top: affectDotY + 'rpx' }">
- <view class="affect-dot-inner"></view>
- </view>
- </view>
- <view class="affect-values">
- <text class="affect-val">效价: {{ affectValence }}/10</text>
- <text class="affect-val">激活度: {{ affectArousal }}/10</text>
- </view>
- </view>
- <!-- ===== Plutchik 情绪轮 ===== -->
- <view class="section">
- <text class="section-title">选择你的核心情绪</text>
- <view class="emotion-wheel">
- <view
- v-for="em in plutchikEmotions"
- :key="em.type"
- :class="['emotion-btn', selectedEmotionType === em.type ? 'emotion-active' : '']"
- :style="{ borderColor: selectedEmotionType === em.type ? em.color : '#eee' }"
- @click="selectedEmotionType = em.type">
- <text class="emotion-icon">{{ em.icon }}</text>
- <text class="emotion-label">{{ em.label }}</text>
- </view>
- </view>
- </view>
- <!-- ===== 强度滑条 ===== -->
- <view class="section">
- <text class="section-title">情绪强度</text>
- <view class="slider-row">
- <text class="slider-label">弱</text>
- <slider class="custom-slider" min="1" max="10" :value="intensityLevel" @change="onIntensityChange" activeColor="#FF6B9D" block-size="20" />
- <text class="slider-value">{{ intensityLevel }}</text>
- </view>
- <view class="intensity-hint">{{ intensityHint }}</view>
- </view>
- <!-- ===== 精力水平 ===== -->
- <view class="section">
- <text class="section-title">精力水平</text>
- <view class="slider-row">
- <text class="slider-label">低</text>
- <slider class="custom-slider" min="1" max="10" :value="energyLevel" @change="onEnergyChange" activeColor="#10B981" block-size="20" />
- <text class="slider-value">{{ energyLevel }}</text>
- </view>
- </view>
- <!-- ===== 压力水平 ===== -->
- <view class="section">
- <text class="section-title">压力水平</text>
- <view class="slider-row">
- <text class="slider-label">无压力</text>
- <slider class="custom-slider" min="1" max="10" :value="stressLevel" @change="onStressChange" activeColor="#F59E0B" block-size="20" />
- <text class="slider-value">{{ stressLevel }}</text>
- </view>
- </view>
- <!-- ===== 情绪标签(精细标签) ===== -->
- <view class="section">
- <text class="section-title">情绪标签(可多选)</text>
- <view class="tag-grid">
- <view
- v-for="tag in emotionTags"
- :key="tag"
- :class="['tag-btn', selectedTags.indexOf(tag) !== -1 ? 'tag-active' : '']"
- @click="toggleTag(tag)">
- <text>{{ tag }}</text>
- </view>
- </view>
- <view class="custom-tag-row" v-if="showCustomTagInput">
- <input class="custom-tag-input" v-model="customTag" placeholder="输入自定义情绪" />
- <text class="custom-tag-add" @click="addCustomTag">添加</text>
- </view>
- <text class="custom-tag-trigger" v-if="!showCustomTagInput" @click="showCustomTagInput = true">+ 自定义标签</text>
- </view>
- <!-- 拍照/录像 -->
- <view class="section">
- <text class="section-title">拍一张照片或一段视频记录心情</text>
- <view class="capture-mode-bar">
- <text
- :class="['mode-btn', captureMode === 'photo' ? 'mode-active' : '']"
- @click="switchCaptureMode('photo')">📷 拍照</text>
- <text
- :class="['mode-btn', captureMode === 'video' ? 'mode-active' : '']"
- @click="switchCaptureMode('video')">🎬 录像</text>
- </view>
- <view class="photo-area" @click="takeMedia">
- <!-- 照片预览 -->
- <image v-if="captureMode === 'photo' && photoPath" :src="photoPath" mode="aspectFill" class="photo-preview" />
- <!-- 视频预览 -->
- <video v-if="captureMode === 'video' && videoPath" :src="videoPath" class="video-preview" objectFit="cover" autoplay muted loop />
- <!-- 占位 -->
- <view v-else-if="!photoPath && !videoPath" class="photo-placeholder">
- <text class="photo-icon">{{ captureMode === 'photo' ? '\u{1F4F7}' : '\u{1F3AC}' }}</text>
- <text class="photo-hint">{{ captureMode === 'photo' ? '点击拍照' : '点击录像' }}</text>
- </view>
- </view>
- <text class="photo-note">{{ captureMode === 'photo' ? '照片' : '视频' }}仅对自己可见</text>
- </view>
- <!-- 情绪识别 -->
- <view class="section" v-if="photoPath || videoPath">
- <text class="section-title">\uD83E\uDD16 情绪识别</text>
- <view class="ai-recognize-btn" v-if="!aiResult && !aiAnalyzing" @click="analyzeEmotion">
- <text class="ai-btn-icon">\uD83D\uDD0D</text>
- <text class="ai-btn-text">{{ captureMode === 'video' ? '识别视频中的情绪' : '识别照片中的情绪' }}</text>
- </view>
- <view class="ai-analyzing" v-if="aiAnalyzing">
- <text class="loading-text">分析中...</text>
- </view>
- <view class="ai-result" v-if="aiResult">
- <text class="ai-result-title">识别结果</text>
- <view class="ai-emotion-list">
- <view class="ai-emotion-item" v-for="em in aiResult" :key="em.label">
- <text class="ai-emotion-label">{{ em.label }}</text>
- <view class="ai-emotion-bar-track">
- <view class="ai-emotion-bar-fill" :style="{ width: em.confidence + '%', background: em.color }"></view>
- </view>
- <text class="ai-emotion-value">{{ em.confidence }}%</text>
- </view>
- </view>
- <view class="ai-apply-tags" @click="applyAiTags">
- <text class="ai-apply-text">应用为情绪标签</text>
- </view>
- </view>
- </view>
- <!-- 文字记录 -->
- <view class="section">
- <text class="section-title">记录一下(选填)</text>
- <textarea
- class="note-input"
- v-model="note"
- placeholder="今天发生了什么?"
- maxlength="500" />
- </view>
- <!-- Phase 1.2: 情绪风险告警 -->
- <PsychCrisisBanner v-if="currentChildId" :memberId="currentChildId" source="checkin" />
- <!-- 近期心情历史(合并自成长页心情记录) -->
- <view class="section" v-if="recentMoods.length > 0">
- <text class="section-title">近期心情</text>
- <view class="mood-list">
- <view class="mood-item" v-for="(mood, idx) in recentMoods" :key="idx">
- <text class="mood-emotion">{{ emotionIcon(mood.emotionType) }}</text>
- <view class="mood-info">
- <text class="mood-label">{{ emotionLabel(mood.emotionType) }}</text>
- <text class="mood-date">{{ formatMoodDate(mood) }}</text>
- </view>
- <text class="mood-note" v-if="mood.note">{{ mood.note }}</text>
- </view>
- </view>
- </view>
- <!-- 提交按钮 -->
- <view class="submit-area">
- <button
- class="submit-btn"
- :class="{ 'submit-disabled': !canSubmit }"
- :disabled="!canSubmit"
- @click="submitCheckin">
- <text class="submit-icon">❤</text>
- <text>记录心情</text>
- </button>
- </view>
- <!-- 提交中 loading -->
- <view class="loading-mask" v-if="submitting">
- <view class="loading-box">
- <text class="loading-text">记录中...</text>
- </view>
- </view>
- </view>
- </template>
- <script>
- import config from '../../config'
- import PsychCrisisBanner from '../../components/PsychCrisisBanner.vue'
- export default {
- components: { PsychCrisisBanner },
- data() {
- return {
- currentChildId: null,
- // --- 2D 效价-激活度网格 ---
- affectValence: 5, // 1-10
- affectArousal: 5, // 1-10
- gridWidth: 360, // rpx, 会在 onReady 中动态计算
- gridHeight: 360,
- // --- Plutchik 情绪轮 ---
- selectedEmotionType: '',
- // --- 滑条 ---
- intensityLevel: 5,
- energyLevel: 6,
- stressLevel: 4,
- // --- 旧字段保留兼容 ---
- selectedWeather: 'sunny', // 自动从效价映射
- selectedTags: [],
- customTag: '',
- showCustomTagInput: false,
- captureMode: 'photo',
- photoPath: '',
- videoPath: '',
- note: '',
- submitting: false,
- aiAnalyzing: false,
- aiResult: null,
- aiEmotionColors: {
- '开心': '#10B981',
- '平静': '#3B82F6',
- '惊喜': '#8B5CF6',
- '难过': '#6B9BD2',
- '生气': '#EF4444',
- '紧张': '#F59E0B',
- '疲惫': '#9CA3AF',
- '害怕': '#7C3AED'
- },
- stats: null,
- // 近期心情历史(合并自成长页心情记录)
- recentMoods: [],
- // Plutchik 8 基本情绪
- plutchikEmotions: [
- { type: 'joy', icon: '\uD83D\uDE04', label: '开心', color: '#FFD93D' },
- { type: 'trust', icon: '\uD83E\uDD1D', label: '信任', color: '#6BCB77' },
- { type: 'fear', icon: '\uD83D\uDE31', label: '恐惧', color: '#9B59B6' },
- { type: 'surprise', icon: '\uD83D\uDE32', label: '惊讶', color: '#FF9FF3' },
- { type: 'sadness', icon: '\uD83D\uDE22', label: '悲伤', color: '#6B9BD2' },
- { type: 'disgust', icon: '\uD83E\uDD2E', label: '厌恶', color: '#A0A0A0' },
- { type: 'anger', icon: '\uD83D\uDE21', label: '愤怒', color: '#FF6B6B' },
- { type: 'anticipation',icon: '\uD83E\uDD29', label: '期待', color: '#FF8C42' }
- ],
- emotionTags: ['开心', '期待', '平静', '惊喜', '难过', '生气', '紧张', '无聊', '疲惫', '害怕'],
- // 强度提示文案
- intensityHints: {
- 1: '几乎感觉不到',
- 2: '很微弱',
- 3: '有一点',
- 4: '稍微明显',
- 5: '中等强度',
- 6: '比较明显',
- 7: '相当强烈',
- 8: '很强烈',
- 9: '非常强烈',
- 10: '前所未有的强烈'
- }
- }
- },
- computed: {
- todayDisplay() {
- const now = new Date()
- const y = now.getFullYear()
- const m = now.getMonth() + 1
- const d = now.getDate()
- const weekdays = ['日', '一', '二', '三', '四', '五', '六']
- return y + '年' + m + '月' + d + '日 周' + weekdays[now.getDay()]
- },
- canSubmit() {
- return this.affectValence > 0 && this.selectedEmotionType
- },
- // 圆点位置(rpx)
- affectDotX() {
- // valence 1-10 → 0 to gridWidth
- // 留出边距: 从 20 到 gridWidth-20
- const margin = 20
- const usable = this.gridWidth - 2 * margin
- return margin + ((this.affectValence - 1) / 9) * usable
- },
- affectDotY() {
- // arousal 1-10 → gridHeight to 0 (Y轴反向)
- const margin = 20
- const usable = this.gridHeight - 2 * margin
- return margin + (1 - (this.affectArousal - 1) / 9) * usable
- },
- intensityHint() {
- return this.intensityHints[this.intensityLevel] || ''
- }
- },
- onLoad(options) {
- // memberId 优先取页面参数(成长页跳转传入),再兜底本地存储链
- var memberId = (options && options.memberId) || uni.getStorageSync('currentChildId') || uni.getStorageSync('currentMemberId') || uni.getStorageSync('userId') || null
- this.currentChildId = memberId
- this.loadStats()
- this.loadRecentMoods()
- },
- onReady() {
- // 估算 grid 尺寸(基于屏幕宽度)
- var sysInfo = uni.getWindowInfo()
- // 页面左右margin=32rpx, section padding=28rpx
- // gridWidth ≈ screenWidth - (32+28)*2 = screenWidth - 120
- var screenWidth = sysInfo.windowWidth
- var gridRpx = screenWidth - 120 // 估算 rpx
- if (gridRpx < 300) gridRpx = 300
- this.gridWidth = gridRpx
- this.gridHeight = gridRpx
- },
- methods: {
- loadStats() {
- var memberId = this.currentChildId
- if (!memberId) return
- uni.request({
- url: config.API_BASE_URL + '/api/mind/checkin/stats',
- method: 'POST',
- data: { memberId },
- success: (res) => {
- if (res.data && res.data.code === 200 && res.data.data) {
- this.stats = res.data.data
- }
- }
- })
- },
- // ===== 近期心情记录(合并自成长页心情记录) =====
- loadRecentMoods() {
- var self = this
- if (!this.currentChildId) return
- uni.request({
- url: config.API_BASE_URL + '/api/mind/checkin/list',
- method: 'POST',
- data: { memberId: this.currentChildId, page: 1, size: 5 },
- success: (res) => {
- if (res.data && res.data.code === 200) {
- var list = res.data.data
- // 兼容数组 / {records} / {list} 三种返回结构
- if (!(list instanceof Array)) {
- list = (list && (list.list || list.records)) || []
- }
- self.recentMoods = list.slice(0, 5)
- }
- }
- })
- },
- emotionLabel(type) {
- var map = {
- // Plutchik 词表(情绪打卡页)
- joy: '开心', trust: '信任', fear: '恐惧', surprise: '惊讶',
- sadness: '悲伤', disgust: '厌恶', anger: '愤怒', anticipation: '期待',
- // 成长页词表(旧数据兼容)
- calm: '平静', excited: '兴奋', sad: '难过', angry: '生气',
- anxious: '焦虑', tired: '疲惫', scared: '害怕'
- }
- return map[type] || type || '心情'
- },
- emotionIcon(type) {
- var map = {
- joy: '\u{1F604}', trust: '\u{1F91D}', fear: '\u{1F631}', surprise: '\u{1F632}',
- sadness: '\u{1F622}', disgust: '\u{1F922}', anger: '\u{1F621}', anticipation: '\u{1F600}',
- calm: '\u{1F60C}', excited: '\u{1F929}', sad: '\u{1F622}', angry: '\u{1F621}',
- anxious: '\u{1F630}', tired: '\u{1F634}', scared: '\u{1F631}'
- }
- return map[type] || '\u{1F604}'
- },
- formatMoodDate(mood) {
- var raw = (mood && (mood.checkinDate || mood.createdAt)) || ''
- return raw.substring(0, 10)
- },
- // ===== 2D 效价-激活度网格 =====
- onAffectGridTap(e) {
- // 从触点获取页面坐标(clientX/clientY,px)
- var touch = (e.touches && e.touches[0]) || (e.changedTouches && e.changedTouches[0]) || e.detail
- var clientX = touch ? (touch.clientX !== undefined ? touch.clientX : touch.x) : 0
- var clientY = touch ? (touch.clientY !== undefined ? touch.clientY : touch.y) : 0
- var self = this
- var query = uni.createSelectorQuery().in(this)
- query.select('.affect-grid').boundingClientRect(function(rect) {
- if (!rect) return
- // 相对网格的偏移(px)
- var pxX = clientX - rect.left
- var pxY = clientY - rect.top
- if (pxX < 0) pxX = 0
- if (pxY < 0) pxY = 0
- if (pxX > rect.width) pxX = rect.width
- if (pxY > rect.height) pxY = rect.height
- // px → rpx 转换(网格宽高单位为 rpx,通过比例换算)
- var scale = self.gridWidth / rect.width
- var rpxX = pxX * scale
- var rpxY = pxY * scale
- // 约束在有效范围内(跟 affectDotX/Y 的 margin 对齐)
- var margin = 20
- var maxRpx = self.gridWidth - margin
- if (rpxX < margin) rpxX = margin
- if (rpxX > maxRpx) rpxX = maxRpx
- if (rpxY < margin) rpxY = margin
- if (rpxY > self.gridHeight - margin) rpxY = self.gridHeight - margin
- // 映射到 1-10
- var usable = self.gridWidth - 2 * margin
- var val = Math.round(1 + ((rpxX - margin) / usable) * 9)
- var aro = Math.round(10 - ((rpxY - margin) / usable) * 9)
- self.affectValence = Math.max(1, Math.min(10, val))
- self.affectArousal = Math.max(1, Math.min(10, aro))
- // 自动映射 moodWeather 用于向后兼容
- self.updateWeatherFromAffect()
- }).exec()
- },
- updateWeatherFromAffect() {
- // 效价高 + 激活度高 = rainbow
- if (this.affectValence >= 7 && this.affectArousal >= 7) {
- this.selectedWeather = 'rainbow'
- } else if (this.affectValence >= 6) {
- // 效价高 = sunny
- this.selectedWeather = 'sunny'
- } else if (this.affectValence <= 4 && this.affectArousal >= 7) {
- // 效价低 + 激活度高 = stormy
- this.selectedWeather = 'stormy'
- } else if (this.affectValence <= 4 && this.affectArousal <= 4) {
- // 效价低 + 激活度低 = rainy
- this.selectedWeather = 'rainy'
- } else {
- this.selectedWeather = 'cloudy'
- }
- },
- // ===== 滑条变化 =====
- onIntensityChange(e) {
- this.intensityLevel = e.detail.value
- },
- onEnergyChange(e) {
- this.energyLevel = e.detail.value
- },
- onStressChange(e) {
- this.stressLevel = e.detail.value
- },
- // ===== 情绪标签 =====
- toggleTag(tag) {
- const idx = this.selectedTags.indexOf(tag)
- if (idx !== -1) {
- this.selectedTags.splice(idx, 1)
- } else {
- this.selectedTags.push(tag)
- }
- },
- addCustomTag() {
- const tag = this.customTag.trim()
- if (tag && this.emotionTags.indexOf(tag) === -1 && this.selectedTags.indexOf(tag) === -1) {
- this.selectedTags.push(tag)
- }
- this.customTag = ''
- this.showCustomTagInput = false
- },
- // ===== 拍照/录像 =====
- switchCaptureMode(mode) {
- this.captureMode = mode
- this.photoPath = ''
- this.videoPath = ''
- this.aiResult = null
- },
- takeMedia() {
- var self = this
- if (this.captureMode === 'photo') {
- uni.chooseImage({
- count: 1,
- sourceType: ['camera', 'album'],
- success: function(res) {
- self.photoPath = res.tempFilePaths[0]
- self.videoPath = ''
- self.aiResult = null
- }
- })
- } else {
- uni.chooseVideo({
- sourceType: ['camera', 'album'],
- maxDuration: 15,
- success: function(res) {
- self.videoPath = res.tempFilePath
- self.photoPath = ''
- self.aiResult = null
- }
- })
- }
- },
- // ===== 增强型模拟识别 =====
- // 基于效价/激活度/时段/强度等多维参数,生成更自然的模拟结果
- analyzeEmotion() {
- this.aiAnalyzing = true
- this.aiResult = null
- var self = this
- // 生成伪随机种子(不同照片/视频产生不同结果,模拟真实识别)
- var seed = this.genSeed()
- // 根据多维参数生成情绪结果
- var results = this.genEmotionResults(seed)
- // 处理时间随机变化(200-1600ms,让每次等待不一样)
- var delay = 600 + (seed % 1000)
- setTimeout(function() {
- self.aiResult = results
- self.aiAnalyzing = false
- }, delay)
- },
- // 基于文件路径 + 时间生成伪随机种子
- genSeed() {
- var path = this.photoPath || this.videoPath || ''
- var hash = 0
- for (var i = 0; i < path.length; i++) {
- hash = ((hash << 5) - hash) + path.charCodeAt(i)
- hash = hash & hash
- }
- hash = Math.abs(hash)
- return (hash + Date.now()) % 7919 // 质数取模
- },
- // 多维参数 → 情绪结果
- genEmotionResults(seed) {
- var valence = this.affectValence
- var arousal = this.affectArousal
- // 1. 从效价-激活度象限推导主要情绪分布
- var emotionPool = []
- if (valence >= 6 && arousal >= 6) {
- // Q1: 高激活 + 积极 → 开心/兴奋/惊喜
- emotionPool = [
- { label: '开心', base: 38, color: '#10B981' },
- { label: '兴奋', base: 22, color: '#8B5CF6' },
- { label: '惊喜', base: 18, color: '#FF9FF3' },
- { label: '满足', base: 12, color: '#3B82F6' },
- { label: '期待', base: 10, color: '#F59E0B' }
- ]
- } else if (valence >= 6 && arousal < 6) {
- // Q2: 低激活 + 积极 → 平静/满足/放松
- emotionPool = [
- { label: '平静', base: 32, color: '#3B82F6' },
- { label: '满足', base: 25, color: '#10B981' },
- { label: '放松', base: 20, color: '#6B9BD2' },
- { label: '开心', base: 15, color: '#10B981' },
- { label: '无聊', base: 8, color: '#9CA3AF' }
- ]
- } else if (valence < 6 && arousal >= 6) {
- // Q3: 高激活 + 消极 → 生气/紧张/恐惧
- emotionPool = [
- { label: '紧张', base: 30, color: '#F59E0B' },
- { label: '生气', base: 25, color: '#EF4444' },
- { label: '烦躁', base: 20, color: '#FF6B6B' },
- { label: '恐惧', base: 15, color: '#7C3AED' },
- { label: '厌恶', base: 10, color: '#A0A0A0' }
- ]
- } else {
- // Q4: 低激活 + 消极 → 难过/疲惫/低落
- emotionPool = [
- { label: '难过', base: 32, color: '#6B9BD2' },
- { label: '疲惫', base: 28, color: '#9CA3AF' },
- { label: '低落', base: 20, color: '#6B7280' },
- { label: '平静', base: 12, color: '#3B82F6' },
- { label: '无聊', base: 8, color: '#9CA3AF' }
- ]
- }
- // 2. 根据强度/压力/能量微调
- // 高强度 → 强化 top1
- if (this.intensityLevel >= 7) {
- emotionPool[0].base += 10
- } else if (this.intensityLevel <= 3) {
- // 低强度 → 向平静偏移
- for (var si = 0; si < emotionPool.length; si++) {
- if (emotionPool[si].label === '平静' || emotionPool[si].label === '无聊') {
- emotionPool[si].base += 8
- }
- }
- }
- // 高压力 → 添加紧张/烦躁倾向
- if (this.stressLevel >= 7) {
- var tensionIdx = -1
- var angerIdx = -1
- for (var ti = 0; ti < emotionPool.length; ti++) {
- if (emotionPool[ti].label === '紧张') tensionIdx = ti
- if (emotionPool[ti].label === '烦躁') angerIdx = ti
- }
- if (tensionIdx >= 0) emotionPool[tensionIdx].base += 12
- if (angerIdx >= 0) emotionPool[angerIdx].base += 8
- }
- // 3. 根据 Plutchik 已选情绪提升权重(用户选择了说明有倾向)
- if (this.selectedEmotionType) {
- var typeToLabel = {
- joy: '开心', trust: '信任', fear: '恐惧',
- surprise: '惊喜', sadness: '难过', disgust: '厌恶',
- anger: '生气', anticipation: '期待'
- }
- var matchedLabel = typeToLabel[this.selectedEmotionType]
- for (var mi = 0; mi < emotionPool.length; mi++) {
- if (emotionPool[mi].label === matchedLabel) {
- emotionPool[mi].base += 15
- }
- }
- }
- // 4. 根据时段微调
- var hour = new Date().getHours()
- if (hour < 6) {
- // 深夜:偏向疲惫/低落
- this.adjustPoolForTime(emotionPool, '疲惫', 10)
- this.adjustPoolForTime(emotionPool, '低落', 8)
- } else if (hour < 10) {
- // 早晨:偏向积极
- this.adjustPoolForTime(emotionPool, '开心', 8)
- this.adjustPoolForTime(emotionPool, '期待', 6)
- } else if (hour >= 22) {
- // 夜晚:偏向平静/疲惫
- this.adjustPoolForTime(emotionPool, '平静', 8)
- this.adjustPoolForTime(emotionPool, '疲惫', 6)
- }
- // 5. 引入伪随机噪声(让每次结果有细微差异)
- for (var ni = 0; ni < emotionPool.length; ni++) {
- var noise = (seed * (ni + 1) * 7) % 11 - 5 // -5 ~ +5
- emotionPool[ni].base += noise
- if (emotionPool[ni].base < 3) emotionPool[ni].base = 3
- }
- // 6. 降序排列,取 top 4,归一化到 100%
- emotionPool.sort(function(a, b) { return b.base - a.base })
- var top4 = emotionPool.slice(0, 4)
- var total = 0
- for (var sumi = 0; sumi < top4.length; sumi++) {
- total += top4[sumi].base
- }
- for (var normi = 0; normi < top4.length; normi++) {
- top4[normi].confidence = Math.round((top4[normi].base / total) * 100)
- }
- // 补余数确保 =100
- var sumConf = 0
- for (var ci = 0; ci < top4.length; ci++) {
- sumConf += top4[ci].confidence
- }
- var diff = 100 - sumConf
- if (diff !== 0 && top4.length > 0) {
- top4[0].confidence += diff
- }
- return top4
- },
- adjustPoolForTime(pool, label, delta) {
- for (var i = 0; i < pool.length; i++) {
- if (pool[i].label === label) {
- pool[i].base += delta
- return
- }
- }
- },
- applyAiTags() {
- if (!this.aiResult) return
- var detected = []
- for (var i = 0; i < this.aiResult.length; i++) {
- var em = this.aiResult[i]
- if (em.confidence >= 20 && this.emotionTags.indexOf(em.label) !== -1) {
- detected.push(em.label)
- }
- }
- var merged = this.selectedTags.slice()
- for (var j = 0; j < detected.length; j++) {
- if (merged.indexOf(detected[j]) === -1) {
- merged.push(detected[j])
- }
- }
- this.selectedTags = merged
- uni.showToast({ title: '已应用情绪标签', icon: 'success' })
- },
- // ===== 提交 =====
- submitCheckin() {
- if (!this.canSubmit || this.submitting) return
- this.submitting = true
- var memberId = this.currentChildId
- const doSubmit = (mediaUrl) => {
- uni.request({
- url: config.API_BASE_URL + '/api/mind/checkin/create',
- method: 'POST',
- data: {
- memberId: memberId,
- // 旧字段向后兼容
- moodWeather: this.selectedWeather,
- emotionTags: JSON.stringify(this.selectedTags),
- photoUrl: this.captureMode === 'photo' ? (mediaUrl || '') : '',
- videoUrl: this.captureMode === 'video' ? (mediaUrl || '') : '',
- mediaType: this.captureMode,
- note: this.note,
- // Phase 1.1 精细情绪字段
- moodScore: this.affectValence,
- emotionType: this.selectedEmotionType,
- stressLevel: this.stressLevel,
- arousalLevel: this.affectArousal,
- energyLevel: this.energyLevel
- },
- success: (res) => {
- this.submitting = false
- if (res.data && res.data.code === 200) {
- uni.showToast({
- title: '+5 心能量',
- icon: 'none',
- duration: 2000
- })
- setTimeout(() => {
- var trendUrl = '/pages/mind-detail/emotion-trend'
- // 成长页场景 memberId 可能来自参数/currentMemberId,透传给趋势页
- if (memberId) trendUrl = trendUrl + '?memberId=' + memberId
- uni.navigateTo({ url: trendUrl })
- }, 1500)
- } else {
- uni.showToast({ title: res.data && res.data.message || '提交失败', icon: 'none' })
- }
- },
- fail: () => {
- this.submitting = false
- uni.showToast({ title: '网络错误', icon: 'none' })
- }
- })
- }
- var mediaPath = this.photoPath || this.videoPath
- if (mediaPath) {
- uni.uploadFile({
- url: config.API_BASE_URL + '/api/media/upload',
- filePath: mediaPath,
- name: 'file',
- success: (uploadRes) => {
- let mediaUrl = ''
- try {
- const data = JSON.parse(uploadRes.data)
- mediaUrl = data.data || data.url || ''
- } catch (e) {
- mediaUrl = uploadRes.data
- }
- doSubmit(mediaUrl)
- },
- fail: () => doSubmit('')
- })
- } else {
- doSubmit('')
- }
- }
- }
- }
- </script>
- <style scoped>
- .checkin-container {
- min-height: 100vh;
- background: linear-gradient(180deg, #FFF0F5 0%, #FFFFFF 100%);
- padding-bottom: 40rpx;
- }
- .checkin-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 30rpx 32rpx;
- }
- .checkin-date {
- font-size: 32rpx;
- font-weight: 600;
- color: #333;
- }
- .streak-badge {
- display: flex;
- align-items: center;
- background: linear-gradient(135deg, #FF6B9D, #FF8CB3);
- border-radius: 30rpx;
- padding: 8rpx 24rpx;
- }
- .streak-icon {
- font-size: 28rpx;
- margin-right: 8rpx;
- }
- .streak-text {
- font-size: 24rpx;
- color: #fff;
- font-weight: 500;
- }
- .section {
- margin: 20rpx 32rpx;
- background: #fff;
- border-radius: 20rpx;
- padding: 28rpx;
- box-shadow: 0 2rpx 12rpx rgba(255, 107, 157, 0.08);
- }
- .section-title {
- font-size: 28rpx;
- font-weight: 600;
- color: #333;
- margin-bottom: 20rpx;
- display: block;
- }
- /* ===== 2D 效价-激活度网格 ===== */
- .affect-grid {
- position: relative;
- width: 100%;
- /* height is set dynamically equal to width via aspect-ratio or inline style */
- aspect-ratio: 1;
- background: linear-gradient(135deg, #FFF5F5 0%, #FFF0FF 25%, #F0F0FF 50%, #F0FFF4 75%, #FFFFF0 100%);
- border-radius: 16rpx;
- border: 2rpx solid #E8E8E8;
- margin-bottom: 16rpx;
- /* Ensure the grid has a defined height — fallback */
- min-height: 300rpx;
- /* Overlap grid lines for visual clarity */
- overflow: hidden;
- }
- /* 网格线:用伪元素或背景渐变 */
- .affect-grid::before {
- content: '';
- position: absolute;
- left: 50%;
- top: 0;
- bottom: 0;
- width: 2rpx;
- background: rgba(0,0,0,0.06);
- transform: translateX(-50%);
- pointer-events: none;
- }
- .affect-grid::after {
- content: '';
- position: absolute;
- top: 50%;
- left: 0;
- right: 0;
- height: 2rpx;
- background: rgba(0,0,0,0.06);
- transform: translateY(-50%);
- pointer-events: none;
- }
- .grid-label {
- position: absolute;
- font-size: 22rpx;
- color: rgba(0,0,0,0.25);
- pointer-events: none;
- }
- .label-nw { top: 20rpx; left: 20rpx; }
- .label-ne { top: 20rpx; right: 20rpx; }
- .label-sw { bottom: 20rpx; left: 20rpx; }
- .label-se { bottom: 20rpx; right: 20rpx; }
- .axis-label {
- position: absolute;
- font-size: 20rpx;
- color: rgba(0,0,0,0.2);
- pointer-events: none;
- }
- .axis-bottom {
- bottom: 4rpx;
- left: 50%;
- transform: translateX(-50%);
- white-space: nowrap;
- }
- .axis-left {
- left: 4rpx;
- top: 50%;
- transform: translateY(-50%);
- font-size: 18rpx;
- line-height: 1.4;
- text-align: center;
- }
- .affect-dot {
- position: absolute;
- width: 44rpx;
- height: 44rpx;
- margin-left: -22rpx;
- margin-top: -22rpx;
- pointer-events: none;
- z-index: 2;
- }
- .affect-dot-inner {
- width: 100%;
- height: 100%;
- border-radius: 50%;
- background: #FF6B9D;
- box-shadow: 0 0 12rpx rgba(255, 107, 157, 0.5);
- border: 4rpx solid #fff;
- box-sizing: border-box;
- }
- .affect-values {
- display: flex;
- justify-content: center;
- gap: 40rpx;
- }
- .affect-val {
- font-size: 24rpx;
- color: #888;
- }
- /* ===== Plutchik 情绪轮 ===== */
- .emotion-wheel {
- display: flex;
- flex-wrap: wrap;
- gap: 16rpx;
- justify-content: center;
- }
- .emotion-btn {
- display: flex;
- flex-direction: column;
- align-items: center;
- width: 130rpx;
- padding: 16rpx 8rpx;
- border-radius: 20rpx;
- border: 3rpx solid #eee;
- background: #FAFAFA;
- transition: all 0.2s;
- }
- .emotion-active {
- background: #FFF0F5;
- transform: scale(1.05);
- }
- .emotion-icon {
- font-size: 48rpx;
- margin-bottom: 6rpx;
- }
- .emotion-label {
- font-size: 24rpx;
- color: #555;
- }
- /* ===== 滑条 ===== */
- .slider-row {
- display: flex;
- align-items: center;
- gap: 16rpx;
- }
- .slider-label {
- font-size: 24rpx;
- color: #999;
- white-space: nowrap;
- width: 64rpx;
- }
- .custom-slider {
- flex: 1;
- }
- .slider-value {
- font-size: 28rpx;
- font-weight: 600;
- color: #FF6B9D;
- width: 40rpx;
- text-align: center;
- }
- .intensity-hint {
- font-size: 22rpx;
- color: #aaa;
- text-align: center;
- margin-top: 4rpx;
- }
- /* ===== 情绪标签 ===== */
- .tag-grid {
- display: flex;
- flex-wrap: wrap;
- gap: 16rpx;
- }
- .tag-btn {
- padding: 12rpx 28rpx;
- border-radius: 30rpx;
- background: #F5F5F5;
- font-size: 26rpx;
- color: #666;
- border: 2rpx solid transparent;
- }
- .tag-active {
- background: #FFF0F5;
- color: #FF6B9D;
- border-color: #FF6B9D;
- }
- .custom-tag-row {
- display: flex;
- align-items: center;
- margin-top: 16rpx;
- gap: 16rpx;
- }
- .custom-tag-input {
- flex: 1;
- height: 60rpx;
- border: 2rpx solid #E0E0E0;
- border-radius: 12rpx;
- padding: 0 16rpx;
- font-size: 26rpx;
- }
- .custom-tag-add {
- padding: 12rpx 24rpx;
- background: #FF6B9D;
- color: #fff;
- border-radius: 12rpx;
- font-size: 26rpx;
- }
- .custom-tag-trigger {
- display: inline-block;
- margin-top: 16rpx;
- color: #FF6B9D;
- font-size: 26rpx;
- }
- /* ===== 拍照 ===== */
- .photo-area {
- width: 200rpx;
- height: 200rpx;
- border-radius: 16rpx;
- overflow: hidden;
- background: #F5F5F5;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .photo-preview {
- width: 100%;
- height: 100%;
- }
- .photo-placeholder {
- display: flex;
- flex-direction: column;
- align-items: center;
- }
- .photo-icon {
- font-size: 60rpx;
- }
- .photo-hint {
- font-size: 24rpx;
- color: #999;
- margin-top: 8rpx;
- }
- .photo-note {
- display: block;
- font-size: 22rpx;
- color: #999;
- margin-top: 12rpx;
- }
- /* ===== 拍照/录像模式切换 ===== */
- .capture-mode-bar {
- display: flex;
- gap: 20rpx;
- margin-bottom: 20rpx;
- }
- .mode-btn {
- padding: 10rpx 28rpx;
- border-radius: 30rpx;
- font-size: 26rpx;
- color: #999;
- background: #f5f5f5;
- border: 2rpx solid #eee;
- }
- .mode-btn.mode-active {
- color: #FF6B9D;
- background: #FFF0F5;
- border-color: #FF6B9D;
- font-weight: 500;
- }
- .video-preview {
- width: 100%;
- height: 100%;
- border-radius: 16rpx;
- }
- /* ===== 情绪识别 ===== */
- .ai-recognize-btn {
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 20rpx;
- background: linear-gradient(135deg, #8B5CF6, #A78BFA);
- border-radius: 16rpx;
- cursor: pointer;
- }
- .ai-btn-icon {
- font-size: 32rpx;
- margin-right: 10rpx;
- }
- .ai-btn-text {
- font-size: 26rpx;
- color: #fff;
- font-weight: 500;
- }
- .ai-recognize-btn:active {
- opacity: 0.8;
- }
- .ai-analyzing {
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 30rpx 0;
- }
- .loading-text {
- font-size: 26rpx;
- color: #8B5CF6;
- }
- .ai-result {
- margin-top: 16rpx;
- }
- .ai-result-title {
- font-size: 24rpx;
- color: #666;
- margin-bottom: 12rpx;
- display: block;
- }
- .ai-emotion-list {
- display: flex;
- flex-direction: column;
- gap: 12rpx;
- }
- .ai-emotion-item {
- display: flex;
- align-items: center;
- gap: 12rpx;
- }
- .ai-emotion-label {
- font-size: 24rpx;
- color: #333;
- width: 64rpx;
- flex-shrink: 0;
- }
- .ai-emotion-bar-track {
- flex: 1;
- height: 16rpx;
- background: #F0F0F0;
- border-radius: 8rpx;
- overflow: hidden;
- }
- .ai-emotion-bar-fill {
- height: 100%;
- border-radius: 8rpx;
- transition: width 0.5s;
- }
- .ai-emotion-value {
- font-size: 22rpx;
- color: #999;
- width: 56rpx;
- text-align: right;
- }
- .ai-apply-tags {
- margin-top: 20rpx;
- padding: 16rpx;
- background: #F5F0FF;
- border-radius: 12rpx;
- text-align: center;
- cursor: pointer;
- }
- .ai-apply-text {
- font-size: 26rpx;
- color: #8B5CF6;
- font-weight: 500;
- }
- .ai-apply-tags:active {
- opacity: 0.7;
- }
- .note-input {
- width: 100%;
- min-height: 140rpx;
- background: #F9F9F9;
- border-radius: 12rpx;
- padding: 20rpx;
- font-size: 26rpx;
- color: #333;
- box-sizing: border-box;
- }
- /* ===== 近期心情历史(合并自成长页心情记录) ===== */
- .mood-list {
- display: flex;
- flex-direction: column;
- }
- .mood-item {
- display: flex;
- align-items: center;
- padding: 20rpx 0;
- border-bottom: 1rpx solid #F5F5F5;
- }
- .mood-item:last-child {
- border-bottom: none;
- }
- .mood-emotion {
- font-size: 40rpx;
- margin-right: 20rpx;
- flex-shrink: 0;
- }
- .mood-info {
- flex: 1;
- display: flex;
- flex-direction: column;
- }
- .mood-label {
- font-size: 28rpx;
- color: #333;
- font-weight: 500;
- }
- .mood-date {
- font-size: 22rpx;
- color: #999;
- margin-top: 6rpx;
- }
- .mood-note {
- font-size: 26rpx;
- color: #666;
- max-width: 300rpx;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- margin-left: 20rpx;
- flex-shrink: 0;
- }
- .submit-area {
- margin: 40rpx 32rpx;
- }
- .submit-btn {
- width: 100%;
- height: 88rpx;
- line-height: 88rpx;
- background: linear-gradient(135deg, #FF6B9D, #FF8CB3);
- color: #fff;
- border-radius: 44rpx;
- font-size: 30rpx;
- font-weight: 600;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .submit-disabled {
- opacity: 0.5;
- }
- .submit-icon {
- margin-right: 8rpx;
- }
- .loading-mask {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: rgba(0, 0, 0, 0.3);
- display: flex;
- align-items: center;
- justify-content: center;
- z-index: 999;
- }
- .loading-box {
- background: #fff;
- padding: 40rpx 60rpx;
- border-radius: 20rpx;
- }
- .loading-text {
- font-size: 28rpx;
- color: #666;
- }
- </style>
|