| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679 |
- <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>
- <!-- 天气选择 -->
- <view class="section">
- <text class="section-title">今天心情怎么样?</text>
- <view class="weather-grid">
- <view
- v-for="w in weatherOptions"
- :key="w.value"
- :class="['weather-btn', selectedWeather === w.value ? 'weather-active' : '']"
- :style="{ borderColor: selectedWeather === w.value ? w.color : 'transparent' }"
- @click="selectedWeather = w.value">
- <text class="weather-icon">{{ w.icon }}</text>
- <text class="weather-label">{{ w.label }}</text>
- </view>
- </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="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>
- </view>
- </view>
- <text class="photo-note">照片仅对自己可见</text>
- </view>
- <!-- AI 情绪识别 -->
- <view class="section" v-if="photoPath">
- <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>
- </view>
- <view class="ai-analyzing" v-if="aiAnalyzing">
- <text class="ai-loading-text">AI 分析中...</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>
- <!-- 提交按钮 -->
- <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'
- export default {
- data() {
- return {
- selectedWeather: '',
- selectedTags: [],
- customTag: '',
- showCustomTagInput: false,
- photoPath: '',
- note: '',
- submitting: false,
- aiAnalyzing: false,
- aiResult: null,
- aiEmotionColors: {
- '开心': '#10B981',
- '平静': '#3B82F6',
- '惊喜': '#8B5CF6',
- '难过': '#6B9BD2',
- '生气': '#EF4444',
- '紧张': '#F59E0B',
- '疲惫': '#9CA3AF',
- '害怕': '#7C3AED'
- },
- stats: null,
- weatherOptions: [
- { value: 'sunny', icon: '\u2600\uFE0F', label: '心情很好', color: '#FFB347' },
- { value: 'cloudy', icon: '\u26C5', label: '一般般', color: '#A0A0A0' },
- { value: 'rainy', icon: '\uD83C\uDF27\uFE0F', label: '有点难过', color: '#6B9BD2' },
- { value: 'stormy', icon: '\u26A1', label: '很烦躁', color: '#8B5CF6' },
- { value: 'rainbow', icon: '\uD83C\uDF08', label: '特别开心', color: '#FF6B9D' }
- ],
- emotionTags: ['开心', '期待', '平静', '惊喜', '难过', '生气', '紧张', '无聊', '疲惫', '害怕']
- }
- },
- 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.selectedWeather
- }
- },
- onLoad() {
- this.loadStats()
- },
- methods: {
- loadStats() {
- const app = getApp()
- const childId = app.globalData.currentChildId
- if (!childId) return
- uni.request({
- url: config.API_BASE_URL + '/api/mind/checkin/stats',
- method: 'POST',
- data: { childId },
- success: (res) => {
- if (res.data && res.data.code === 200 && res.data.data) {
- this.stats = res.data.data
- }
- }
- })
- },
- 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
- },
- takePhoto() {
- uni.chooseImage({
- count: 1,
- sourceType: ['camera', 'album'],
- success: (res) => {
- this.photoPath = res.tempFilePaths[0]
- this.aiResult = null
- }
- })
- },
- analyzeEmotion() {
- this.aiAnalyzing = true
- this.aiResult = null
- // Simulate AI emotion analysis (in production, call a vision API)
- var self = this
- setTimeout(function() {
- self.aiAnalyzing = false
- // Simulated result based on the selected weather/mood as hint
- var baseEmotions = [
- { label: '开心', confidence: 45, color: '#10B981' },
- { label: '平静', confidence: 30, color: '#3B82F6' },
- { label: '惊喜', confidence: 15, color: '#8B5CF6' },
- { label: '疲惫', confidence: 10, color: '#9CA3AF' }
- ]
- // Adjust based on weather selection if available
- if (self.selectedWeather === 'rainy') {
- baseEmotions = [
- { label: '难过', confidence: 55, color: '#6B9BD2' },
- { label: '疲惫', confidence: 25, color: '#9CA3AF' },
- { label: '平静', confidence: 12, color: '#3B82F6' },
- { label: '生气', confidence: 8, color: '#EF4444' }
- ]
- } else if (self.selectedWeather === 'stormy') {
- baseEmotions = [
- { label: '生气', confidence: 48, color: '#EF4444' },
- { label: '紧张', confidence: 32, color: '#F59E0B' },
- { label: '害怕', confidence: 12, color: '#7C3AED' },
- { label: '开心', confidence: 8, color: '#10B981' }
- ]
- } else if (self.selectedWeather === 'rainbow') {
- baseEmotions = [
- { label: '开心', confidence: 60, color: '#10B981' },
- { label: '惊喜', confidence: 25, color: '#8B5CF6' },
- { label: '平静', confidence: 10, color: '#3B82F6' },
- { label: '期待', confidence: 5, color: '#F59E0B' }
- ]
- }
- self.aiResult = baseEmotions
- }, 1200)
- },
- applyAiTags() {
- if (!this.aiResult) return
- var detected = []
- for (var i = 0; i < this.aiResult.length; i++) {
- var em = this.aiResult[i]
- // Only apply high-confidence emotions as tags
- if (em.confidence >= 20 && this.emotionTags.indexOf(em.label) !== -1) {
- detected.push(em.label)
- }
- }
- // Merge with existing selections (no duplicates)
- 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
- const app = getApp()
- const childId = app.globalData.currentChildId
- // Upload photo first if exists, then submit
- const doSubmit = (photoUrl) => {
- uni.request({
- url: config.API_BASE_URL + '/api/mind/checkin/create',
- method: 'POST',
- data: {
- childId: childId,
- moodWeather: this.selectedWeather,
- emotionTags: JSON.stringify(this.selectedTags),
- photoUrl: photoUrl || '',
- note: this.note
- },
- success: (res) => {
- this.submitting = false
- if (res.data && res.data.code === 200) {
- uni.showToast({
- title: '+5 \u5FC3\u80FD\u91CF',
- icon: 'none',
- duration: 2000
- })
- setTimeout(() => {
- uni.navigateTo({ url: '/pages/mind/emotion-trend' })
- }, 1500)
- } else {
- uni.showToast({ title: res.data && res.data.message || '提交失败', icon: 'none' })
- }
- },
- fail: () => {
- this.submitting = false
- uni.showToast({ title: '网络错误', icon: 'none' })
- }
- })
- }
- if (this.photoPath) {
- uni.uploadFile({
- url: config.API_BASE_URL + '/api/upload',
- filePath: this.photoPath,
- name: 'file',
- success: (uploadRes) => {
- let photoUrl = ''
- try {
- const data = JSON.parse(uploadRes.data)
- photoUrl = data.data || data.url || ''
- } catch (e) {
- photoUrl = uploadRes.data
- }
- doSubmit(photoUrl)
- },
- 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;
- }
- .weather-grid {
- display: flex;
- flex-wrap: wrap;
- gap: 16rpx;
- justify-content: center;
- }
- .weather-btn {
- display: flex;
- flex-direction: column;
- align-items: center;
- width: 120rpx;
- padding: 16rpx 8rpx;
- border-radius: 16rpx;
- border: 3rpx solid transparent;
- background: #FAFAFA;
- transition: all 0.2s;
- }
- .weather-active {
- background: #FFF0F5;
- }
- .weather-icon {
- font-size: 44rpx;
- margin-bottom: 8rpx;
- }
- .weather-label {
- font-size: 22rpx;
- color: #666;
- }
- .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;
- }
- /* ===== AI 情绪识别 ===== */
- .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;
- }
- .ai-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;
- }
- .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>
|