|
|
@@ -98,25 +98,37 @@
|
|
|
<text class="custom-tag-trigger" v-if="!showCustomTagInput" @click="showCustomTagInput = true">+ 自定义标签</text>
|
|
|
</view>
|
|
|
|
|
|
- <!-- 拍照 -->
|
|
|
+ <!-- 拍照/录像 -->
|
|
|
<view class="section">
|
|
|
- <text class="section-title">拍一张照片记录心情</text>
|
|
|
- <view class="photo-area" @click="takePhoto">
|
|
|
- <image v-if="photoPath" :src="photoPath" mode="aspectFill" class="photo-preview" />
|
|
|
- <view v-else class="photo-placeholder">
|
|
|
- <text class="photo-icon">📷</text>
|
|
|
- <text class="photo-hint">点击拍照</text>
|
|
|
+ <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">照片仅对自己可见</text>
|
|
|
+ <text class="photo-note">{{ captureMode === 'photo' ? '照片' : '视频' }}仅对自己可见</text>
|
|
|
</view>
|
|
|
|
|
|
<!-- AI 情绪识别 -->
|
|
|
- <view class="section" v-if="photoPath">
|
|
|
+ <view class="section" v-if="photoPath || videoPath">
|
|
|
<text class="section-title">\uD83E\uDD16 AI 情绪识别</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">识别照片中的情绪</text>
|
|
|
+ <text class="ai-btn-text">{{ captureMode === 'video' ? '识别视频中的情绪' : '识别照片中的情绪' }}</text>
|
|
|
</view>
|
|
|
<view class="ai-analyzing" v-if="aiAnalyzing">
|
|
|
<text class="ai-loading-text">AI 分析中...</text>
|
|
|
@@ -201,7 +213,9 @@ export default {
|
|
|
selectedTags: [],
|
|
|
customTag: '',
|
|
|
showCustomTagInput: false,
|
|
|
+ captureMode: 'photo',
|
|
|
photoPath: '',
|
|
|
+ videoPath: '',
|
|
|
note: '',
|
|
|
submitting: false,
|
|
|
aiAnalyzing: false,
|
|
|
@@ -425,48 +439,212 @@ export default {
|
|
|
this.showCustomTagInput = false
|
|
|
},
|
|
|
|
|
|
- // ===== 拍照 & AI =====
|
|
|
- takePhoto() {
|
|
|
- uni.chooseImage({
|
|
|
- count: 1,
|
|
|
- sourceType: ['camera', 'album'],
|
|
|
- success: (res) => {
|
|
|
- this.photoPath = res.tempFilePaths[0]
|
|
|
- this.aiResult = null
|
|
|
- }
|
|
|
- })
|
|
|
+ // ===== 拍照/录像 & AI =====
|
|
|
+ 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
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
},
|
|
|
|
|
|
+ // ===== 增强型 AI 模拟识别 =====
|
|
|
+ // 基于效价/激活度/时段/强度等多维参数,生成更自然的模拟结果
|
|
|
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
|
|
|
- var baseEmotions = [
|
|
|
- { label: '开心', confidence: 45, color: '#10B981' },
|
|
|
- { label: '平静', confidence: 30, color: '#3B82F6' },
|
|
|
- { label: '惊喜', confidence: 15, color: '#8B5CF6' },
|
|
|
- { label: '疲惫', confidence: 10, color: '#9CA3AF' }
|
|
|
+ }, 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' }
|
|
|
]
|
|
|
- // Adjust based on affect valence
|
|
|
- if (self.affectValence <= 4) {
|
|
|
- baseEmotions = [
|
|
|
- { label: '难过', confidence: 55, color: '#6B9BD2' },
|
|
|
- { label: '疲惫', confidence: 25, color: '#9CA3AF' },
|
|
|
- { label: '平静', confidence: 12, color: '#3B82F6' },
|
|
|
- { label: '生气', confidence: 8, color: '#EF4444' }
|
|
|
- ]
|
|
|
- } else if (self.affectValence >= 8) {
|
|
|
- baseEmotions = [
|
|
|
- { label: '开心', confidence: 60, color: '#10B981' },
|
|
|
- { label: '惊喜', confidence: 25, color: '#8B5CF6' },
|
|
|
- { label: '平静', confidence: 10, color: '#3B82F6' },
|
|
|
- { label: '期待', confidence: 5, color: '#F59E0B' }
|
|
|
- ]
|
|
|
+ }
|
|
|
+
|
|
|
+ // 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
|
|
|
}
|
|
|
- self.aiResult = baseEmotions
|
|
|
- }, 1200)
|
|
|
+ }
|
|
|
},
|
|
|
|
|
|
applyAiTags() {
|
|
|
@@ -496,7 +674,7 @@ export default {
|
|
|
const app = getApp()
|
|
|
const childId = app.globalData.currentChildId
|
|
|
|
|
|
- const doSubmit = (photoUrl) => {
|
|
|
+ const doSubmit = (mediaUrl) => {
|
|
|
uni.request({
|
|
|
url: config.API_BASE_URL + '/api/mind/checkin/create',
|
|
|
method: 'POST',
|
|
|
@@ -505,7 +683,9 @@ export default {
|
|
|
// 旧字段向后兼容
|
|
|
moodWeather: this.selectedWeather,
|
|
|
emotionTags: JSON.stringify(this.selectedTags),
|
|
|
- photoUrl: photoUrl || '',
|
|
|
+ photoUrl: this.captureMode === 'photo' ? (mediaUrl || '') : '',
|
|
|
+ videoUrl: this.captureMode === 'video' ? (mediaUrl || '') : '',
|
|
|
+ mediaType: this.captureMode,
|
|
|
note: this.note,
|
|
|
// Phase 1.1 精细情绪字段
|
|
|
moodScore: this.affectValence,
|
|
|
@@ -536,20 +716,21 @@ export default {
|
|
|
})
|
|
|
}
|
|
|
|
|
|
- if (this.photoPath) {
|
|
|
+ var mediaPath = this.photoPath || this.videoPath
|
|
|
+ if (mediaPath) {
|
|
|
uni.uploadFile({
|
|
|
url: config.API_BASE_URL + '/api/upload',
|
|
|
- filePath: this.photoPath,
|
|
|
+ filePath: mediaPath,
|
|
|
name: 'file',
|
|
|
success: (uploadRes) => {
|
|
|
- let photoUrl = ''
|
|
|
+ let mediaUrl = ''
|
|
|
try {
|
|
|
const data = JSON.parse(uploadRes.data)
|
|
|
- photoUrl = data.data || data.url || ''
|
|
|
+ mediaUrl = data.data || data.url || ''
|
|
|
} catch (e) {
|
|
|
- photoUrl = uploadRes.data
|
|
|
+ mediaUrl = uploadRes.data
|
|
|
}
|
|
|
- doSubmit(photoUrl)
|
|
|
+ doSubmit(mediaUrl)
|
|
|
},
|
|
|
fail: () => doSubmit('')
|
|
|
})
|
|
|
@@ -884,6 +1065,32 @@ export default {
|
|
|
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 情绪识别 ===== */
|
|
|
.ai-recognize-btn {
|
|
|
display: flex;
|