Browse Source

fix(frontend): 同步远端重构合并 + 保持 :key 修复与 trial 环境地址

liaoxg 2 months ago
parent
commit
83e33c0f60

+ 1 - 1
cfc-frontend/config.js

@@ -22,7 +22,7 @@ try {
         API_BASE_URL = 'https://cfc.etotem.com.cn'
         break
       case 'trial':
-        API_BASE_URL = 'https://cfc.iwintrue.com'
+        API_BASE_URL = 'https://cfc.etotem.com.cn'
         break
       case 'release':
         API_BASE_URL = 'https://cfc.etotem.com.cn'

+ 56 - 105
cfc-frontend/pages/growth-main/index.vue

@@ -162,34 +162,6 @@
           </view>
         </view>
       </view>
-      <!-- 健康报告状态卡片 -->
-      <view class="section" v-if="reportStatus.hasReport">
-        <view class="report-card" @click="goToReport">
-          <view class="report-left">
-            <text class="report-icon">📋</text>
-            <view class="report-info">
-              <text class="report-title">已有健康报告</text>
-              <text class="report-desc" v-if="reportStatus.reportType === 'gut_flora'">肠道菌群检测 · {{ reportStatus.reportDate }}</text>
-              <text class="report-desc" v-else-if="reportStatus.reportType === 'physical_exam'">体检报告 · {{ reportStatus.reportDate }}</text>
-              <text class="report-desc" v-else>健康报告 · {{ reportStatus.reportDate }}</text>
-              <text class="report-score" v-if="reportStatus.overallScore">综合评分:{{ reportStatus.overallScore }}分</text>
-            </view>
-          </view>
-          <text class="report-arrow">›</text>
-        </view>
-      </view>
-      <view class="section" v-else>
-        <view class="report-upload-card" @click="goToUploadReport">
-          <view class="report-upload-left">
-            <text class="report-upload-icon">📷</text>
-            <view class="report-upload-info">
-              <text class="report-upload-title">上传健康报告</text>
-              <text class="report-upload-desc">上传体检或菌群检测报告,获取个性化营养方案</text>
-            </view>
-          </view>
-          <text class="report-upload-arrow">›</text>
-        </view>
-      </view>
       <!-- 今日成长 -->
       <view class="section">
         <view class="progress-card">
@@ -198,7 +170,7 @@
             <text class="progress-date">{{ todayDate }}</text>
           </view>
           <view class="progress-grid" v-if="progressItems.length > 0">
-            <view class="progress-grid-item" v-for="(item, idx) in progressItems" :key="getProgressKey(item)" @click="handleProgressClick(item)">
+            <view class="progress-grid-item" v-for="(item, idx) in progressItems" :key="getProgressKey(item)" @click="handleProgressClick(idx)">
               <text class="progress-grid-icon">{{ item.icon }}</text>
               <text class="progress-grid-label">{{ item.label }}</text>
               <text class="progress-grid-status" :class="item.done ? 'status-done' : 'status-pending'">{{ item.done ? '✔' : (item.actionText || '去记录') }}</text>
@@ -226,36 +198,33 @@
           <text class="action-icon">🍽️</text>
           <view class="action-info">
             <text class="action-title">饮食推荐</text>
-            <text class="action-desc">基于菌群报告的个性化食材建议</text>
+            <text class="action-desc">选同住人,获取今日个性化饮食建议</text>
           </view>
           <text class="action-arrow">›</text>
         </view>
       </view>
-      <!-- 任务中心 -->
-      <view class="section">
-        <view class="action-card" @click="goToTasks">
-          <text class="action-icon">📋</text>
-          <view class="action-info">
-            <text class="action-title">任务中心</text>
-            <text class="action-desc">查看今日待办任务,完成挑战获取奖励</text>
+      <!-- 任务中心(内联) -->
+      <view class="section" v-if="recentTasks.length > 0">
+        <view class="task-preview-card">
+          <view class="task-preview-header">
+            <text class="task-preview-title">📋 待办任务</text>
+            <text class="task-preview-more" @click="goToTasks">更多 ›</text>
           </view>
-          <text class="action-arrow">›</text>
-        </view>
-      </view>
-      <DimensionActivities :isLoggedIn="true" @activityClick="goActivityDetail" @moreActivities="goMoreActivities" style="margin-bottom:60rpx" />
-      <!-- 今日任务 -->
-      <DailyTaskCard v-if="isLoggedIn" />
-      <!-- 报告上传 -->
-      <view class="section">
-        <view class="action-card" @click="goToUploadReport">
-          <text class="action-icon">📷</text>
-          <view class="action-info">
-            <text class="action-title">上传报告</text>
-            <text class="action-desc">体检报告 · 菌群检测 · 认知测评 · 心理测评 · 情绪打卡</text>
+          <view class="task-preview-list">
+            <view class="task-preview-item" v-for="(task, idx) in recentTasks" :key="getTaskKey(task, idx)" @click="goToTasks">
+              <view class="task-preview-left">
+                <text class="task-preview-name">{{ task.title }}</text>
+                <view class="task-preview-meta">
+                  <text class="task-preview-points">+{{ task.points }}分</text>
+                  <text class="task-preview-deadline" v-if="task.deadline">⏱ {{ formatTaskTime(task.deadline) }}</text>
+                </view>
+              </view>
+              <text class="task-preview-arrow">›</text>
+            </view>
           </view>
-          <text class="action-arrow">›</text>
         </view>
       </view>
+      <DimensionActivities :isLoggedIn="true" @activityClick="goActivityDetail" @moreActivities="goMoreActivities" style="margin-bottom:60rpx" />
       <!-- 推荐阅读 -->
       <view class="section" v-if="recommendArticles.length > 0">
         <view class="section-header">
@@ -281,16 +250,14 @@ import LoginGuideCard from '../../components/LoginGuideCard.vue'
 import DimensionActivities from '../../components/DimensionActivities.vue'
 import DimensionProducts from '../../components/DimensionProducts.vue'
 import ArticleBookshelf from '../../components/ArticleBookshelf.vue'
-import DailyTaskCard from '../../components/daily-task-card.vue'
-import { healthCheckinList, getHealthMealList, getFoodCautionList, getGrowthRecommendations, getExerciseRecords, getSleepRecords, getEmotionCheckinList, getCheckinList, getLatestHealthReport } from '../../utils/api.js'
+import { healthCheckinList, getHealthMealList, getFoodCautionList, getGrowthRecommendations, getExerciseRecords, getSleepRecords, getEmotionCheckinList, getCheckinList, getTodayTasks } from '../../utils/api.js'
 import { parseDate } from '../../utils/format.js'
 export default {
-  components: { LoginGuideCard, DimensionActivities, DimensionProducts, ArticleBookshelf, DailyTaskCard },
-  data() { return { memberId: '', todayDate: '', hasIncomplete: false, incompleteCount: 0, progressItems: [], streakDays: 0, activeChallenges: 0, completeness: 0, cautionCount: 0,
+  components: { LoginGuideCard, DimensionActivities, DimensionProducts, ArticleBookshelf },
+  data() { return { memberId: '', todayDate: '', hasIncomplete: false, incompleteCount: 0, progressItems: [], recentTasks: [], streakDays: 0, activeChallenges: 0, completeness: 0, cautionCount: 0,
       healthArticles: [],
       growthArticles: [],
-      isLoggedIn: false,
-      reportStatus: { hasReport: false, reportId: null, reportType: '', reportDate: '', overallScore: null } } },
+      isLoggedIn: false } },
   computed: {
     doneCount: function() { var c = 0; for (var i = 0; i < this.progressItems.length; i++) if (this.progressItems[i] && this.progressItems[i].done) c++; return c },
     totalCount: function() { return this.progressItems.length },
@@ -314,25 +281,35 @@ export default {
     pad: function(n) { return n < 10 ? '0' + n : '' + n },
     getProgressKey: function(item) { return item && item.label ? item.label : 'p-' + Math.random() },
     dismissReminder: function() { this.hasIncomplete = false },
-    handleProgressClick: function(item) {
+    handleProgressClick: function(idx) {
+      var item = this.progressItems[idx]
       if (!item || !item.key) return
       var m = { diet: '/pages/growth/diet-checkin', exercise: '/pages/growth/exercise-checkin', sleep: '/pages/growth/sleep-checkin', emotion: '/pages/growth/growth-emotion-checkin', checkin: '/pages/wealth-sub/checkin' }
       var url = m[item.key]
       if (url) uni.navigateTo({ url: url })
     },
-    goToReport: function() {
-      if (this.reportStatus.reportId) uni.navigateTo({ url: '/pages/health/report-detail?id=' + this.reportStatus.reportId })
+    getTaskKey: function(task, idx) { return (task && task.id) ? ('t' + task.id) : ('tk' + idx) },
+    formatTaskTime: function(dateVal) {
+      if (!dateVal) return ''
+      var d = parseDate(dateVal)
+      if (!d) return ''
+      return (d.getMonth() + 1) + '/' + d.getDate() + ' ' + this.pad(d.getHours()) + ':' + this.pad(d.getMinutes())
     },
-    goToUploadReport: function() {
-      var id = this.memberId || ''
-      uni.navigateTo({ url: '/pages/health/report-upload?memberId=' + id + '&from=growth' })
+    async loadRecentTasks() {
+      var self = this
+      try {
+        var res = await getTodayTasks(self.memberId ? { memberId: self.memberId } : {})
+        if (res && res.code === 200 && res.data) {
+          self.recentTasks = (res.data || []).slice(0, 3)
+        }
+      } catch(e) {}
     },
     goActivityDetail: function(act) { uni.navigateTo({ url: '/pages/activity/activity-detail/activity-detail?id=' + act.id }) },
     goMoreActivities: function() { uni.navigateTo({ url: '/pages/activity/index' }) },
     goToTasks: function() { uni.navigateTo({ url: '/pages/child/tasks' }) },
     handleGuestFeatureTap: function() { uni.showToast({ title: '登录后即可体验该功能', icon: 'none' }) },
     goToCaution: function() { uni.navigateTo({ url: '/pages/growth/caution-detail' }) },
-    goToFoods: function() { uni.navigateTo({ url: '/pages/growth/foods-detail' }) },
+    goToFoods: function() { uni.navigateTo({ url: '/pages/diet/index' }) },
     goToArticle: function(article) { var id = article && article.id ? article.id : article; if (!id) return; uni.navigateTo({ url: '/pages/article-center/article-detail?id=' + id }) },
     goToProduct: function(prod) { var id = prod && prod.id ? prod.id : prod; if (!id) return; uni.navigateTo({ url: '/pages/shop/detail/detail?id=' + id }) },
     goMoreProducts: function() { uni.navigateTo({ url: '/pages/shop/index/index' }) },
@@ -356,33 +333,12 @@ export default {
     },
     async loadAllData() {
       var self = this
-      try { await self.loadReportStatus() } catch(e) {}
       try { await self.loadProgressItems() } catch(e) {}
+      try { await self.loadRecentTasks() } catch(e) {}
       try { await self.loadCautionData() } catch(e) {}
       try { await self.loadGrowthRecommendations() } catch(e) {}
       try { await self.loadMilestones() } catch(e) {}
     },
-    async loadReportStatus() {
-      var self = this
-      if (!self.memberId) return
-      try {
-        var res = await getLatestHealthReport(self.memberId)
-        if (res.code === 200 && res.data) {
-          var r = res.data
-          self.reportStatus = {
-            hasReport: true,
-            reportId: r.id,
-            reportType: r.reportType || '',
-            reportDate: r.reportDate ? r.reportDate.substring(0, 10) : '',
-            overallScore: r.overallScore || null
-          }
-        } else {
-          self.reportStatus = { hasReport: false, reportId: null, reportType: '', reportDate: '', overallScore: null }
-        }
-      } catch(e) {
-        self.reportStatus = { hasReport: false, reportId: null, reportType: '', reportDate: '', overallScore: null }
-      }
-    },
     async loadProgressItems() {
       var self = this
       var items = []
@@ -550,25 +506,20 @@ export default {
 .progress-footer{display:flex;justify-content:space-between;align-items:center;margin-top:16rpx;padding-top:16rpx;border-top:1rpx solid #f5f5f5}
 .footer-text{font-size:24rpx;color:#666;font-weight:bold}
 .footer-streak{font-size:24rpx;color:#F97316;font-weight:bold}
-/* 报告状态卡片 */
-.report-card{background:#fff;border-radius:16rpx;padding:20rpx 24rpx;box-shadow:0 2rpx 12rpx rgba(0,0,0,0.06);display:flex;align-items:center;cursor:pointer}
-.report-card:active{opacity:0.85}
-.report-left{display:flex;align-items:center;flex:1;min-width:0}
-.report-icon{font-size:40rpx;margin-right:16rpx;flex-shrink:0}
-.report-info{flex:1;min-width:0}
-.report-title{font-size:28rpx;font-weight:600;color:#333;display:block}
-.report-desc{font-size:22rpx;color:#888;display:block;margin-top:4rpx;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
-.report-score{font-size:22rpx;color:#10B981;font-weight:bold;display:block;margin-top:2rpx}
-.report-arrow{font-size:32rpx;color:#ccc;flex-shrink:0}
-/* 报告上传提示卡 */
-.report-upload-card{background:linear-gradient(135deg,#FF8C42,#F97316);border-radius:16rpx;padding:20rpx 24rpx;box-shadow:0 4rpx 16rpx rgba(249,115,22,0.25);display:flex;align-items:center}
-.report-upload-card:active{opacity:0.9}
-.report-upload-left{display:flex;align-items:center;flex:1;min-width:0}
-.report-upload-icon{font-size:44rpx;margin-right:16rpx;flex-shrink:0}
-.report-upload-info{flex:1;min-width:0}
-.report-upload-title{font-size:28rpx;font-weight:600;color:#fff;display:block}
-.report-upload-desc{font-size:22rpx;color:rgba(255,255,255,0.85);display:block;margin-top:4rpx}
-.report-upload-arrow{font-size:32rpx;color:rgba(255,255,255,0.7);flex-shrink:0}
+/* 任务中心预览 */
+.task-preview-card{background:#fff;border-radius:20rpx;padding:24rpx;box-shadow:0 2rpx 12rpx rgba(0,0,0,0.06)}
+.task-preview-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:16rpx}
+.task-preview-title{font-size:28rpx;font-weight:bold;color:#333}
+.task-preview-more{font-size:24rpx;color:#F97316}
+.task-preview-list{display:flex;flex-direction:column}
+.task-preview-item{display:flex;align-items:center;padding:16rpx 0;border-bottom:1rpx solid #f5f5f5}
+.task-preview-item:last-child{border-bottom:none}
+.task-preview-left{flex:1;min-width:0}
+.task-preview-name{font-size:26rpx;color:#333;display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+.task-preview-meta{display:flex;align-items:center;gap:12rpx;margin-top:6rpx}
+.task-preview-points{font-size:22rpx;color:#F97316;font-weight:bold}
+.task-preview-deadline{font-size:20rpx;color:#999}
+.task-preview-arrow{font-size:28rpx;color:#ccc;flex-shrink:0;margin-left:12rpx}
 /* ===== 五维闭环体系样式 ===== */
 .wuxing-section { margin: 0 24rpx 16rpx; background: #fff; border-radius: 20rpx; padding: 24rpx; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); }
 .wuxing-title { font-size: 30rpx; font-weight: bold; color: #333; text-align: center; margin-bottom: 6rpx; }

+ 64 - 37
cfc-frontend/pages/growth/diet-checkin.vue

@@ -38,15 +38,19 @@
       <view class="card">
         <view class="card-accent"></view>
         <view class="card-body">
-          <text class="card-title">📷 拍照记录</text>
-          <view class="photo-row">
-            <view class="photo-item" v-for="(img, idx) in form.photos" :key="getPhotoKey(img, idx)" @tap="removePhoto(idx)">
-              <image :src="img" mode="aspectFill" class="photo-preview" />
-              <text class="photo-del">×</text>
+          <view class="section-header">
+            <text class="card-title">📷 拍照记录</text>
+            <text class="photo-count">{{ form.photos.length }}/9</text>
+          </view>
+          <view class="photo-grid">
+            <view class="photo-thumb" v-for="(img, idx) in form.photos" :key="getPhotoKey(img, idx)" @tap="removePhoto(idx)">
+              <image :src="img" mode="aspectFill" class="photo-img" />
+              <view class="photo-delete">
+                <text class="photo-delete-icon">×</text>
+              </view>
             </view>
-            <view class="photo-add" @tap="choosePhoto">
-              <text class="photo-add-icon">+</text>
-              <text class="photo-add-text">拍照</text>
+            <view class="photo-add-btn" v-if="form.photos.length < 9" @tap="choosePhoto">
+              <text class="camera-icon">📷</text>
             </view>
           </view>
         </view>
@@ -57,20 +61,29 @@
         <view class="card-accent"></view>
         <view class="card-body">
           <text class="card-title">🎙️ 语音打卡</text>
-          <view class="voice-row">
-            <view class="voice-wave" v-if="isRecording">
-              <text class="wave-bar active">◉</text>
-              <text class="wave-bar">○</text>
-              <text class="wave-bar">○</text>
-              <text class="wave-text">录音中 {{ recordSeconds }}s</text>
+          <view class="voice-recorder">
+            <view class="voice-active" v-if="isRecording">
+              <view class="waveform">
+                <view class="wave-bar wave-1"></view>
+                <view class="wave-bar wave-2"></view>
+                <view class="wave-bar wave-3"></view>
+                <view class="wave-bar wave-4"></view>
+                <view class="wave-bar wave-5"></view>
+              </view>
+              <text class="recording-time">{{ recordSeconds }}s</text>
             </view>
-            <text class="voice-duration" v-else-if="form.voicePath">{{ recordDurationText }}</text>
-            <view class="voice-btn" :class="{ 'voice-btn-recording': isRecording }" @tap="toggleRecord">
-              <text class="voice-btn-icon">{{ isRecording ? '⏹' : '🎙' }}</text>
-              <text class="voice-btn-text">{{ isRecording ? '结束' : '按住录音' }}</text>
+            <view class="voice-done" v-else-if="form.voicePath">
+              <text class="play-icon">▶</text>
+              <text class="done-text">{{ recordDurationText }}</text>
+            </view>
+            <view class="record-btn-wrap">
+              <view class="pulse-ring" v-if="isRecording"></view>
+              <view class="record-btn" :class="{ 'record-btn-active': isRecording }" @tap="toggleRecord">
+                <view class="record-btn-dot"></view>
+              </view>
             </view>
           </view>
-          <text class="voice-hint">长按录音,松开结束,可补充文字记录</text>
+          <text class="voice-hint">点击开始录音</text>
         </view>
       </view>
 
@@ -323,25 +336,39 @@ export default {
 .tag-active text { font-size: 24rpx; color: #F97316; font-weight: bold; }
 .form-textarea { width: 100%; height: 120rpx; border: 1rpx solid #e0e0e0; border-radius: 12rpx; padding: 16rpx; font-size: 26rpx; background: #FAFAFA; box-sizing: border-box; }
 .form-input { width: 100%; border: 1rpx solid #e0e0e0; border-radius: 12rpx; padding: 16rpx; font-size: 26rpx; background: #FAFAFA; box-sizing: border-box; }
-.photo-row { display: flex; flex-wrap: wrap; gap: 16rpx; }
-.photo-item { position: relative; width: 160rpx; height: 160rpx; }
-.photo-preview { width: 100%; height: 100%; border-radius: 12rpx; object-fit: cover; }
-.photo-del { position: absolute; top: -8rpx; right: -8rpx; width: 36rpx; height: 36rpx; background: rgba(0,0,0,0.5); color: #fff; border-radius: 50%; font-size: 24rpx; text-align: center; line-height: 36rpx; }
-.photo-add { width: 160rpx; height: 160rpx; border: 2rpx dashed #ccc; border-radius: 12rpx; display: flex; flex-direction: column; align-items: center; justify-content: center; }
-.photo-add-icon { font-size: 48rpx; color: #ccc; }
-.photo-add-text { font-size: 22rpx; color: #999; margin-top: 8rpx; }
-.voice-row { display: flex; flex-direction: column; align-items: center; gap: 12rpx; padding: 16rpx 0; }
-.voice-wave { display: flex; align-items: center; gap: 8rpx; font-size: 24rpx; color: #F97316; }
-.wave-bar { font-size: 28rpx; color: #ccc; }
-.wave-bar.active { animation: wave 0.6s infinite; }
-@keyframes wave { 0%, 100% { opacity: 0.3; } 50% { opacity: 1; } }
-.voice-duration { font-size: 24rpx; color: #666; }
-.voice-btn { display: flex; flex-direction: column; align-items: center; gap: 8rpx; padding: 20rpx 48rpx; border-radius: 40rpx; background: #FFF3E0; border: 2rpx solid #FFB74D; }
-.voice-btn-recording { background: #FEE2E2; border-color: #EF4444; }
-.voice-btn-icon { font-size: 40rpx; }
-.voice-btn-text { font-size: 24rpx; color: #F97316; }
-.voice-btn-recording .voice-btn-text { color: #EF4444; }
+.section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20rpx; }
+.section-header .card-title { margin-bottom: 0; }
+.photo-count { font-size: 24rpx; color: #999; }
+.photo-grid { display: flex; flex-wrap: wrap; gap: 16rpx; }
+.photo-thumb { position: relative; width: 160rpx; height: 160rpx; }
+.photo-img { width: 100%; height: 100%; border-radius: 16rpx; }
+.photo-delete { position: absolute; top: 0; right: 0; width: 36rpx; height: 36rpx; background: #EF4444; border-radius: 50%; display: flex; align-items: center; justify-content: center; box-shadow: 0 2rpx 8rpx rgba(239,68,68,0.3); }
+.photo-delete-icon { color: #fff; font-size: 24rpx; font-weight: bold; }
+.photo-add-btn { width: 160rpx; height: 160rpx; border-radius: 16rpx; background: linear-gradient(135deg, #FF8C42, #F97316); display: flex; align-items: center; justify-content: center; box-shadow: 0 4rpx 16rpx rgba(249,115,22,0.25); }
+.camera-icon { font-size: 52rpx; }
+.voice-recorder { display: flex; flex-direction: column; align-items: center; padding: 24rpx 0 16rpx; }
+.voice-active { display: flex; flex-direction: column; align-items: center; margin-bottom: 20rpx; }
+.waveform { display: flex; align-items: center; justify-content: center; gap: 8rpx; height: 48rpx; }
+.wave-bar { width: 6rpx; height: 8rpx; background: #EF4444; border-radius: 3rpx; }
+.wave-1 { animation: waveform-anim 0.8s ease-in-out infinite 0s; }
+.wave-2 { animation: waveform-anim 0.8s ease-in-out infinite 0.1s; }
+.wave-3 { animation: waveform-anim 0.8s ease-in-out infinite 0.2s; }
+.wave-4 { animation: waveform-anim 0.8s ease-in-out infinite 0.3s; }
+.wave-5 { animation: waveform-anim 0.8s ease-in-out infinite 0.4s; }
+.recording-time { font-size: 28rpx; color: #EF4444; font-weight: bold; margin-top: 8rpx; }
+.voice-done { display: flex; align-items: center; gap: 12rpx; margin-bottom: 20rpx; padding: 12rpx 24rpx; background: #FFF7ED; border-radius: 30rpx; }
+.play-icon { font-size: 24rpx; color: #F97316; }
+.done-text { font-size: 26rpx; color: #666; }
+.record-btn-wrap { position: relative; width: 80rpx; height: 80rpx; }
+.pulse-ring { position: absolute; top: 0; left: 0; width: 80rpx; height: 80rpx; border-radius: 50%; border: 4rpx solid #EF4444; animation: pulse-expand 1.5s ease-out infinite; }
+.record-btn { position: relative; width: 80rpx; height: 80rpx; border-radius: 50%; background: linear-gradient(135deg, #FF8C42, #F97316); display: flex; align-items: center; justify-content: center; box-shadow: 0 4rpx 16rpx rgba(249,115,22,0.3); z-index: 1; }
+.record-btn-active { background: linear-gradient(135deg, #EF4444, #DC2626); box-shadow: 0 4rpx 16rpx rgba(239,68,68,0.4); animation: btn-pulse 1s ease-in-out infinite; }
+.record-btn-dot { width: 24rpx; height: 24rpx; border-radius: 50%; background: #fff; }
+.record-btn-active .record-btn-dot { width: 20rpx; height: 20rpx; border-radius: 4rpx; background: #fff; }
 .voice-hint { font-size: 22rpx; color: #999; text-align: center; }
+@keyframes waveform-anim { 0%, 100% { height: 8rpx; } 50% { height: 40rpx; } }
+@keyframes pulse-expand { 0% { transform: scale(1); opacity: 0.6; } 100% { transform: scale(1.8); opacity: 0; } }
+@keyframes btn-pulse { 0%, 100% { box-shadow: 0 4rpx 16rpx rgba(239,68,68,0.4); } 50% { box-shadow: 0 4rpx 24rpx rgba(239,68,68,0.6); } }
 .submit-wrap { padding: 24rpx 24rpx 40rpx; }
 .submit-btn { background: linear-gradient(135deg, #FF8C42, #F97316); color: #fff; text-align: center; padding: 24rpx 0; border-radius: 16rpx; font-size: 30rpx; font-weight: bold; }
 .submitting { opacity: 0.6; }

+ 64 - 37
cfc-frontend/pages/growth/exercise-checkin.vue

@@ -52,15 +52,19 @@
       <view class="card">
         <view class="card-accent"></view>
         <view class="card-body">
-          <text class="card-title">📷 拍照记录</text>
-          <view class="photo-row">
-            <view class="photo-item" v-for="(img, idx) in form.photos" :key="getPhotoKey(img, idx)" @tap="removePhoto(idx)">
-              <image :src="img" mode="aspectFill" class="photo-preview" />
-              <text class="photo-del">×</text>
+          <view class="section-header">
+            <text class="card-title">📷 拍照记录</text>
+            <text class="photo-count">{{ form.photos.length }}/9</text>
+          </view>
+          <view class="photo-grid">
+            <view class="photo-thumb" v-for="(img, idx) in form.photos" :key="getPhotoKey(img, idx)" @tap="removePhoto(idx)">
+              <image :src="img" mode="aspectFill" class="photo-img" />
+              <view class="photo-delete">
+                <text class="photo-delete-icon">×</text>
+              </view>
             </view>
-            <view class="photo-add" @tap="choosePhoto">
-              <text class="photo-add-icon">+</text>
-              <text class="photo-add-text">拍照</text>
+            <view class="photo-add-btn" v-if="form.photos.length < 9" @tap="choosePhoto">
+              <text class="camera-icon">📷</text>
             </view>
           </view>
         </view>
@@ -71,20 +75,29 @@
         <view class="card-accent"></view>
         <view class="card-body">
           <text class="card-title">🎙️ 语音打卡</text>
-          <view class="voice-row">
-            <view class="voice-wave" v-if="isRecording">
-              <text class="wave-bar active">◉</text>
-              <text class="wave-bar">○</text>
-              <text class="wave-bar">○</text>
-              <text class="wave-text">录音中 {{ recordSeconds }}s</text>
+          <view class="voice-recorder">
+            <view class="voice-active" v-if="isRecording">
+              <view class="waveform">
+                <view class="wave-bar wave-1"></view>
+                <view class="wave-bar wave-2"></view>
+                <view class="wave-bar wave-3"></view>
+                <view class="wave-bar wave-4"></view>
+                <view class="wave-bar wave-5"></view>
+              </view>
+              <text class="recording-time">{{ recordSeconds }}s</text>
             </view>
-            <text class="voice-duration" v-else-if="form.voicePath">{{ recordDurationText }}</text>
-            <view class="voice-btn" :class="{ 'voice-btn-recording': isRecording }" @tap="toggleRecord">
-              <text class="voice-btn-icon">{{ isRecording ? '⏹' : '🎙' }}</text>
-              <text class="voice-btn-text">{{ isRecording ? '结束' : '按住录音' }}</text>
+            <view class="voice-done" v-else-if="form.voicePath">
+              <text class="play-icon">▶</text>
+              <text class="done-text">{{ recordDurationText }}</text>
+            </view>
+            <view class="record-btn-wrap">
+              <view class="pulse-ring" v-if="isRecording"></view>
+              <view class="record-btn" :class="{ 'record-btn-active': isRecording }" @tap="toggleRecord">
+                <view class="record-btn-dot"></view>
+              </view>
             </view>
           </view>
-          <text class="voice-hint">长按录音,松开结束</text>
+          <text class="voice-hint">点击开始录音</text>
         </view>
       </view>
 
@@ -342,25 +355,39 @@ export default {
 .tag-btn text { font-size: 24rpx; color: #666; }
 .tag-active text { font-size: 24rpx; color: #F97316; font-weight: bold; }
 .form-input { width: 100%; border: 1rpx solid #e0e0e0; border-radius: 12rpx; padding: 16rpx; font-size: 26rpx; background: #FAFAFA; box-sizing: border-box; }
-.photo-row { display: flex; flex-wrap: wrap; gap: 16rpx; }
-.photo-item { position: relative; width: 160rpx; height: 160rpx; }
-.photo-preview { width: 100%; height: 100%; border-radius: 12rpx; object-fit: cover; }
-.photo-del { position: absolute; top: -8rpx; right: -8rpx; width: 36rpx; height: 36rpx; background: rgba(0,0,0,0.5); color: #fff; border-radius: 50%; font-size: 24rpx; text-align: center; line-height: 36rpx; }
-.photo-add { width: 160rpx; height: 160rpx; border: 2rpx dashed #ccc; border-radius: 12rpx; display: flex; flex-direction: column; align-items: center; justify-content: center; }
-.photo-add-icon { font-size: 48rpx; color: #ccc; }
-.photo-add-text { font-size: 22rpx; color: #999; margin-top: 8rpx; }
-.voice-row { display: flex; flex-direction: column; align-items: center; gap: 12rpx; padding: 16rpx 0; }
-.voice-wave { display: flex; align-items: center; gap: 8rpx; font-size: 24rpx; color: #F97316; }
-.wave-bar { font-size: 28rpx; color: #ccc; }
-.wave-bar.active { animation: wave 0.6s infinite; }
-@keyframes wave { 0%, 100% { opacity: 0.3; } 50% { opacity: 1; } }
-.voice-duration { font-size: 24rpx; color: #666; }
-.voice-btn { display: flex; flex-direction: column; align-items: center; gap: 8rpx; padding: 20rpx 48rpx; border-radius: 40rpx; background: #FFF3E0; border: 2rpx solid #FFB74D; }
-.voice-btn-recording { background: #FEE2E2; border-color: #EF4444; }
-.voice-btn-icon { font-size: 40rpx; }
-.voice-btn-text { font-size: 24rpx; color: #F97316; }
-.voice-btn-recording .voice-btn-text { color: #EF4444; }
+.section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20rpx; }
+.section-header .card-title { margin-bottom: 0; }
+.photo-count { font-size: 24rpx; color: #999; }
+.photo-grid { display: flex; flex-wrap: wrap; gap: 16rpx; }
+.photo-thumb { position: relative; width: 160rpx; height: 160rpx; }
+.photo-img { width: 100%; height: 100%; border-radius: 16rpx; }
+.photo-delete { position: absolute; top: 0; right: 0; width: 36rpx; height: 36rpx; background: #EF4444; border-radius: 50%; display: flex; align-items: center; justify-content: center; box-shadow: 0 2rpx 8rpx rgba(239,68,68,0.3); }
+.photo-delete-icon { color: #fff; font-size: 24rpx; font-weight: bold; }
+.photo-add-btn { width: 160rpx; height: 160rpx; border-radius: 16rpx; background: linear-gradient(135deg, #FF8C42, #F97316); display: flex; align-items: center; justify-content: center; box-shadow: 0 4rpx 16rpx rgba(249,115,22,0.25); }
+.camera-icon { font-size: 52rpx; }
+.voice-recorder { display: flex; flex-direction: column; align-items: center; padding: 24rpx 0 16rpx; }
+.voice-active { display: flex; flex-direction: column; align-items: center; margin-bottom: 20rpx; }
+.waveform { display: flex; align-items: center; justify-content: center; gap: 8rpx; height: 48rpx; }
+.wave-bar { width: 6rpx; height: 8rpx; background: #EF4444; border-radius: 3rpx; }
+.wave-1 { animation: waveform-anim 0.8s ease-in-out infinite 0s; }
+.wave-2 { animation: waveform-anim 0.8s ease-in-out infinite 0.1s; }
+.wave-3 { animation: waveform-anim 0.8s ease-in-out infinite 0.2s; }
+.wave-4 { animation: waveform-anim 0.8s ease-in-out infinite 0.3s; }
+.wave-5 { animation: waveform-anim 0.8s ease-in-out infinite 0.4s; }
+.recording-time { font-size: 28rpx; color: #EF4444; font-weight: bold; margin-top: 8rpx; }
+.voice-done { display: flex; align-items: center; gap: 12rpx; margin-bottom: 20rpx; padding: 12rpx 24rpx; background: #FFF7ED; border-radius: 30rpx; }
+.play-icon { font-size: 24rpx; color: #F97316; }
+.done-text { font-size: 26rpx; color: #666; }
+.record-btn-wrap { position: relative; width: 80rpx; height: 80rpx; }
+.pulse-ring { position: absolute; top: 0; left: 0; width: 80rpx; height: 80rpx; border-radius: 50%; border: 4rpx solid #EF4444; animation: pulse-expand 1.5s ease-out infinite; }
+.record-btn { position: relative; width: 80rpx; height: 80rpx; border-radius: 50%; background: linear-gradient(135deg, #FF8C42, #F97316); display: flex; align-items: center; justify-content: center; box-shadow: 0 4rpx 16rpx rgba(249,115,22,0.3); z-index: 1; }
+.record-btn-active { background: linear-gradient(135deg, #EF4444, #DC2626); box-shadow: 0 4rpx 16rpx rgba(239,68,68,0.4); animation: btn-pulse 1s ease-in-out infinite; }
+.record-btn-dot { width: 24rpx; height: 24rpx; border-radius: 50%; background: #fff; }
+.record-btn-active .record-btn-dot { width: 20rpx; height: 20rpx; border-radius: 4rpx; background: #fff; }
 .voice-hint { font-size: 22rpx; color: #999; text-align: center; }
+@keyframes waveform-anim { 0%, 100% { height: 8rpx; } 50% { height: 40rpx; } }
+@keyframes pulse-expand { 0% { transform: scale(1); opacity: 0.6; } 100% { transform: scale(1.8); opacity: 0; } }
+@keyframes btn-pulse { 0%, 100% { box-shadow: 0 4rpx 16rpx rgba(239,68,68,0.4); } 50% { box-shadow: 0 4rpx 24rpx rgba(239,68,68,0.6); } }
 .submit-wrap { padding: 24rpx 24rpx 40rpx; }
 .submit-btn { background: linear-gradient(135deg, #FF8C42, #F97316); color: #fff; text-align: center; padding: 24rpx 0; border-radius: 16rpx; font-size: 30rpx; font-weight: bold; }
 .submitting { opacity: 0.6; }

+ 64 - 37
cfc-frontend/pages/growth/growth-emotion-checkin.vue

@@ -51,15 +51,19 @@
       <view class="card">
         <view class="card-accent"></view>
         <view class="card-body">
-          <text class="card-title">📷 拍照记录</text>
-          <view class="photo-row">
-            <view class="photo-item" v-for="(img, idx) in photos" :key="getPhotoKey(img, idx)" @tap="removePhoto(idx)">
-              <image :src="img" mode="aspectFill" class="photo-preview" />
-              <text class="photo-del">×</text>
+          <view class="section-header">
+            <text class="card-title">📷 拍照记录</text>
+            <text class="photo-count">{{ photos.length }}/9</text>
+          </view>
+          <view class="photo-grid">
+            <view class="photo-thumb" v-for="(img, idx) in photos" :key="getPhotoKey(img, idx)" @tap="removePhoto(idx)">
+              <image :src="img" mode="aspectFill" class="photo-img" />
+              <view class="photo-delete">
+                <text class="photo-delete-icon">×</text>
+              </view>
             </view>
-            <view class="photo-add" @tap="choosePhoto">
-              <text class="photo-add-icon">+</text>
-              <text class="photo-add-text">拍照</text>
+            <view class="photo-add-btn" v-if="photos.length < 9" @tap="choosePhoto">
+              <text class="camera-icon">📷</text>
             </view>
           </view>
         </view>
@@ -70,20 +74,29 @@
         <view class="card-accent"></view>
         <view class="card-body">
           <text class="card-title">🎙️ 语音打卡</text>
-          <view class="voice-row">
-            <view class="voice-wave" v-if="isRecording">
-              <text class="wave-bar active">◉</text>
-              <text class="wave-bar">○</text>
-              <text class="wave-bar">○</text>
-              <text class="wave-text">录音中 {{ recordSeconds }}s</text>
+          <view class="voice-recorder">
+            <view class="voice-active" v-if="isRecording">
+              <view class="waveform">
+                <view class="wave-bar wave-1"></view>
+                <view class="wave-bar wave-2"></view>
+                <view class="wave-bar wave-3"></view>
+                <view class="wave-bar wave-4"></view>
+                <view class="wave-bar wave-5"></view>
+              </view>
+              <text class="recording-time">{{ recordSeconds }}s</text>
             </view>
-            <text class="voice-duration" v-else-if="voicePath">{{ recordDurationText }}</text>
-            <view class="voice-btn" :class="{ 'voice-btn-recording': isRecording }" @tap="toggleRecord">
-              <text class="voice-btn-icon">{{ isRecording ? '⏹' : '🎙' }}</text>
-              <text class="voice-btn-text">{{ isRecording ? '结束' : '按住录音' }}</text>
+            <view class="voice-done" v-else-if="voicePath">
+              <text class="play-icon">▶</text>
+              <text class="done-text">{{ recordDurationText }}</text>
+            </view>
+            <view class="record-btn-wrap">
+              <view class="pulse-ring" v-if="isRecording"></view>
+              <view class="record-btn" :class="{ 'record-btn-active': isRecording }" @tap="toggleRecord">
+                <view class="record-btn-dot"></view>
+              </view>
             </view>
           </view>
-          <text class="voice-hint">长按录音,松开结束</text>
+          <text class="voice-hint">点击开始录音</text>
         </view>
       </view>
 
@@ -334,25 +347,39 @@ export default {
 .slider-val { font-size: 24rpx; color: #FF6B9D; font-weight: bold; width: 40rpx; text-align: center; }
 .slider-max { font-size: 22rpx; color: #999; flex-shrink: 0; }
 .note-input { width: 100%; height: 160rpx; border: 1rpx solid #e0e0e0; border-radius: 12rpx; padding: 16rpx; font-size: 26rpx; background: #FAFAFA; box-sizing: border-box; }
-.photo-row { display: flex; flex-wrap: wrap; gap: 16rpx; }
-.photo-item { position: relative; width: 160rpx; height: 160rpx; }
-.photo-preview { width: 100%; height: 100%; border-radius: 12rpx; object-fit: cover; }
-.photo-del { position: absolute; top: -8rpx; right: -8rpx; width: 36rpx; height: 36rpx; background: rgba(0,0,0,0.5); color: #fff; border-radius: 50%; font-size: 24rpx; text-align: center; line-height: 36rpx; }
-.photo-add { width: 160rpx; height: 160rpx; border: 2rpx dashed #ccc; border-radius: 12rpx; display: flex; flex-direction: column; align-items: center; justify-content: center; }
-.photo-add-icon { font-size: 48rpx; color: #ccc; }
-.photo-add-text { font-size: 22rpx; color: #999; margin-top: 8rpx; }
-.voice-row { display: flex; flex-direction: column; align-items: center; gap: 12rpx; padding: 16rpx 0; }
-.voice-wave { display: flex; align-items: center; gap: 8rpx; font-size: 24rpx; color: #FF6B9D; }
-.wave-bar { font-size: 28rpx; color: #ccc; }
-.wave-bar.active { animation: wave 0.6s infinite; }
-@keyframes wave { 0%, 100% { opacity: 0.3; } 50% { opacity: 1; } }
-.voice-duration { font-size: 24rpx; color: #666; }
-.voice-btn { display: flex; flex-direction: column; align-items: center; gap: 8rpx; padding: 20rpx 48rpx; border-radius: 40rpx; background: #FFF0F5; border: 2rpx solid #FF6B9D; }
-.voice-btn-recording { background: #FEE2E2; border-color: #EF4444; }
-.voice-btn-icon { font-size: 40rpx; }
-.voice-btn-text { font-size: 24rpx; color: #FF6B9D; }
-.voice-btn-recording .voice-btn-text { color: #EF4444; }
+.section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20rpx; }
+.section-header .card-title { margin-bottom: 0; }
+.photo-count { font-size: 24rpx; color: #999; }
+.photo-grid { display: flex; flex-wrap: wrap; gap: 16rpx; }
+.photo-thumb { position: relative; width: 160rpx; height: 160rpx; }
+.photo-img { width: 100%; height: 100%; border-radius: 16rpx; }
+.photo-delete { position: absolute; top: 0; right: 0; width: 36rpx; height: 36rpx; background: #EF4444; border-radius: 50%; display: flex; align-items: center; justify-content: center; box-shadow: 0 2rpx 8rpx rgba(239,68,68,0.3); }
+.photo-delete-icon { color: #fff; font-size: 24rpx; font-weight: bold; }
+.photo-add-btn { width: 160rpx; height: 160rpx; border-radius: 16rpx; background: linear-gradient(135deg, #FF8C42, #F97316); display: flex; align-items: center; justify-content: center; box-shadow: 0 4rpx 16rpx rgba(249,115,22,0.25); }
+.camera-icon { font-size: 52rpx; }
+.voice-recorder { display: flex; flex-direction: column; align-items: center; padding: 24rpx 0 16rpx; }
+.voice-active { display: flex; flex-direction: column; align-items: center; margin-bottom: 20rpx; }
+.waveform { display: flex; align-items: center; justify-content: center; gap: 8rpx; height: 48rpx; }
+.wave-bar { width: 6rpx; height: 8rpx; background: #EF4444; border-radius: 3rpx; }
+.wave-1 { animation: waveform-anim 0.8s ease-in-out infinite 0s; }
+.wave-2 { animation: waveform-anim 0.8s ease-in-out infinite 0.1s; }
+.wave-3 { animation: waveform-anim 0.8s ease-in-out infinite 0.2s; }
+.wave-4 { animation: waveform-anim 0.8s ease-in-out infinite 0.3s; }
+.wave-5 { animation: waveform-anim 0.8s ease-in-out infinite 0.4s; }
+.recording-time { font-size: 28rpx; color: #EF4444; font-weight: bold; margin-top: 8rpx; }
+.voice-done { display: flex; align-items: center; gap: 12rpx; margin-bottom: 20rpx; padding: 12rpx 24rpx; background: #FFF7ED; border-radius: 30rpx; }
+.play-icon { font-size: 24rpx; color: #F97316; }
+.done-text { font-size: 26rpx; color: #666; }
+.record-btn-wrap { position: relative; width: 80rpx; height: 80rpx; }
+.pulse-ring { position: absolute; top: 0; left: 0; width: 80rpx; height: 80rpx; border-radius: 50%; border: 4rpx solid #EF4444; animation: pulse-expand 1.5s ease-out infinite; }
+.record-btn { position: relative; width: 80rpx; height: 80rpx; border-radius: 50%; background: linear-gradient(135deg, #FF8C42, #F97316); display: flex; align-items: center; justify-content: center; box-shadow: 0 4rpx 16rpx rgba(249,115,22,0.3); z-index: 1; }
+.record-btn-active { background: linear-gradient(135deg, #EF4444, #DC2626); box-shadow: 0 4rpx 16rpx rgba(239,68,68,0.4); animation: btn-pulse 1s ease-in-out infinite; }
+.record-btn-dot { width: 24rpx; height: 24rpx; border-radius: 50%; background: #fff; }
+.record-btn-active .record-btn-dot { width: 20rpx; height: 20rpx; border-radius: 4rpx; background: #fff; }
 .voice-hint { font-size: 22rpx; color: #999; text-align: center; }
+@keyframes waveform-anim { 0%, 100% { height: 8rpx; } 50% { height: 40rpx; } }
+@keyframes pulse-expand { 0% { transform: scale(1); opacity: 0.6; } 100% { transform: scale(1.8); opacity: 0; } }
+@keyframes btn-pulse { 0%, 100% { box-shadow: 0 4rpx 16rpx rgba(239,68,68,0.4); } 50% { box-shadow: 0 4rpx 24rpx rgba(239,68,68,0.6); } }
 .submit-wrap { padding: 24rpx 24rpx 40rpx; }
 .submit-btn { background: linear-gradient(135deg, #FF6B9D, #FF8CB3); color: #fff; text-align: center; padding: 24rpx 0; border-radius: 16rpx; font-size: 30rpx; font-weight: bold; }
 .submitting { opacity: 0.6; }

+ 64 - 37
cfc-frontend/pages/growth/sleep-checkin.vue

@@ -45,15 +45,19 @@
       <view class="card">
         <view class="card-accent"></view>
         <view class="card-body">
-          <text class="card-title">📷 拍照记录</text>
-          <view class="photo-row">
-            <view class="photo-item" v-for="(img, idx) in form.photos" :key="getPhotoKey(img, idx)" @tap="removePhoto(idx)">
-              <image :src="img" mode="aspectFill" class="photo-preview" />
-              <text class="photo-del">×</text>
+          <view class="section-header">
+            <text class="card-title">📷 拍照记录</text>
+            <text class="photo-count">{{ form.photos.length }}/9</text>
+          </view>
+          <view class="photo-grid">
+            <view class="photo-thumb" v-for="(img, idx) in form.photos" :key="getPhotoKey(img, idx)" @tap="removePhoto(idx)">
+              <image :src="img" mode="aspectFill" class="photo-img" />
+              <view class="photo-delete">
+                <text class="photo-delete-icon">×</text>
+              </view>
             </view>
-            <view class="photo-add" @tap="choosePhoto">
-              <text class="photo-add-icon">+</text>
-              <text class="photo-add-text">拍照</text>
+            <view class="photo-add-btn" v-if="form.photos.length < 9" @tap="choosePhoto">
+              <text class="camera-icon">📷</text>
             </view>
           </view>
         </view>
@@ -64,20 +68,29 @@
         <view class="card-accent"></view>
         <view class="card-body">
           <text class="card-title">🎙️ 语音打卡</text>
-          <view class="voice-row">
-            <view class="voice-wave" v-if="isRecording">
-              <text class="wave-bar active">◉</text>
-              <text class="wave-bar">○</text>
-              <text class="wave-bar">○</text>
-              <text class="wave-text">录音中 {{ recordSeconds }}s</text>
+          <view class="voice-recorder">
+            <view class="voice-active" v-if="isRecording">
+              <view class="waveform">
+                <view class="wave-bar wave-1"></view>
+                <view class="wave-bar wave-2"></view>
+                <view class="wave-bar wave-3"></view>
+                <view class="wave-bar wave-4"></view>
+                <view class="wave-bar wave-5"></view>
+              </view>
+              <text class="recording-time">{{ recordSeconds }}s</text>
             </view>
-            <text class="voice-duration" v-else-if="form.voicePath">{{ recordDurationText }}</text>
-            <view class="voice-btn" :class="{ 'voice-btn-recording': isRecording }" @tap="toggleRecord">
-              <text class="voice-btn-icon">{{ isRecording ? '⏹' : '🎙' }}</text>
-              <text class="voice-btn-text">{{ isRecording ? '结束' : '按住录音' }}</text>
+            <view class="voice-done" v-else-if="form.voicePath">
+              <text class="play-icon">▶</text>
+              <text class="done-text">{{ recordDurationText }}</text>
+            </view>
+            <view class="record-btn-wrap">
+              <view class="pulse-ring" v-if="isRecording"></view>
+              <view class="record-btn" :class="{ 'record-btn-active': isRecording }" @tap="toggleRecord">
+                <view class="record-btn-dot"></view>
+              </view>
             </view>
           </view>
-          <text class="voice-hint">长按录音,松开结束</text>
+          <text class="voice-hint">点击开始录音</text>
         </view>
       </view>
 
@@ -320,25 +333,39 @@ export default {
 .slider-val { font-size: 24rpx; color: #FF8C42; font-weight: bold; width: 40rpx; text-align: center; }
 .slider-max { font-size: 22rpx; color: #999; flex-shrink: 0; }
 .form-input { width: 100%; border: 1rpx solid #e0e0e0; border-radius: 12rpx; padding: 16rpx; font-size: 26rpx; background: #FAFAFA; box-sizing: border-box; }
-.photo-row { display: flex; flex-wrap: wrap; gap: 16rpx; }
-.photo-item { position: relative; width: 160rpx; height: 160rpx; }
-.photo-preview { width: 100%; height: 100%; border-radius: 12rpx; object-fit: cover; }
-.photo-del { position: absolute; top: -8rpx; right: -8rpx; width: 36rpx; height: 36rpx; background: rgba(0,0,0,0.5); color: #fff; border-radius: 50%; font-size: 24rpx; text-align: center; line-height: 36rpx; }
-.photo-add { width: 160rpx; height: 160rpx; border: 2rpx dashed #ccc; border-radius: 12rpx; display: flex; flex-direction: column; align-items: center; justify-content: center; }
-.photo-add-icon { font-size: 48rpx; color: #ccc; }
-.photo-add-text { font-size: 22rpx; color: #999; margin-top: 8rpx; }
-.voice-row { display: flex; flex-direction: column; align-items: center; gap: 12rpx; padding: 16rpx 0; }
-.voice-wave { display: flex; align-items: center; gap: 8rpx; font-size: 24rpx; color: #F97316; }
-.wave-bar { font-size: 28rpx; color: #ccc; }
-.wave-bar.active { animation: wave 0.6s infinite; }
-@keyframes wave { 0%, 100% { opacity: 0.3; } 50% { opacity: 1; } }
-.voice-duration { font-size: 24rpx; color: #666; }
-.voice-btn { display: flex; flex-direction: column; align-items: center; gap: 8rpx; padding: 20rpx 48rpx; border-radius: 40rpx; background: #FFF3E0; border: 2rpx solid #FFB74D; }
-.voice-btn-recording { background: #FEE2E2; border-color: #EF4444; }
-.voice-btn-icon { font-size: 40rpx; }
-.voice-btn-text { font-size: 24rpx; color: #F97316; }
-.voice-btn-recording .voice-btn-text { color: #EF4444; }
+.section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20rpx; }
+.section-header .card-title { margin-bottom: 0; }
+.photo-count { font-size: 24rpx; color: #999; }
+.photo-grid { display: flex; flex-wrap: wrap; gap: 16rpx; }
+.photo-thumb { position: relative; width: 160rpx; height: 160rpx; }
+.photo-img { width: 100%; height: 100%; border-radius: 16rpx; }
+.photo-delete { position: absolute; top: 0; right: 0; width: 36rpx; height: 36rpx; background: #EF4444; border-radius: 50%; display: flex; align-items: center; justify-content: center; box-shadow: 0 2rpx 8rpx rgba(239,68,68,0.3); }
+.photo-delete-icon { color: #fff; font-size: 24rpx; font-weight: bold; }
+.photo-add-btn { width: 160rpx; height: 160rpx; border-radius: 16rpx; background: linear-gradient(135deg, #FF8C42, #F97316); display: flex; align-items: center; justify-content: center; box-shadow: 0 4rpx 16rpx rgba(249,115,22,0.25); }
+.camera-icon { font-size: 52rpx; }
+.voice-recorder { display: flex; flex-direction: column; align-items: center; padding: 24rpx 0 16rpx; }
+.voice-active { display: flex; flex-direction: column; align-items: center; margin-bottom: 20rpx; }
+.waveform { display: flex; align-items: center; justify-content: center; gap: 8rpx; height: 48rpx; }
+.wave-bar { width: 6rpx; height: 8rpx; background: #EF4444; border-radius: 3rpx; }
+.wave-1 { animation: waveform-anim 0.8s ease-in-out infinite 0s; }
+.wave-2 { animation: waveform-anim 0.8s ease-in-out infinite 0.1s; }
+.wave-3 { animation: waveform-anim 0.8s ease-in-out infinite 0.2s; }
+.wave-4 { animation: waveform-anim 0.8s ease-in-out infinite 0.3s; }
+.wave-5 { animation: waveform-anim 0.8s ease-in-out infinite 0.4s; }
+.recording-time { font-size: 28rpx; color: #EF4444; font-weight: bold; margin-top: 8rpx; }
+.voice-done { display: flex; align-items: center; gap: 12rpx; margin-bottom: 20rpx; padding: 12rpx 24rpx; background: #FFF7ED; border-radius: 30rpx; }
+.play-icon { font-size: 24rpx; color: #F97316; }
+.done-text { font-size: 26rpx; color: #666; }
+.record-btn-wrap { position: relative; width: 80rpx; height: 80rpx; }
+.pulse-ring { position: absolute; top: 0; left: 0; width: 80rpx; height: 80rpx; border-radius: 50%; border: 4rpx solid #EF4444; animation: pulse-expand 1.5s ease-out infinite; }
+.record-btn { position: relative; width: 80rpx; height: 80rpx; border-radius: 50%; background: linear-gradient(135deg, #FF8C42, #F97316); display: flex; align-items: center; justify-content: center; box-shadow: 0 4rpx 16rpx rgba(249,115,22,0.3); z-index: 1; }
+.record-btn-active { background: linear-gradient(135deg, #EF4444, #DC2626); box-shadow: 0 4rpx 16rpx rgba(239,68,68,0.4); animation: btn-pulse 1s ease-in-out infinite; }
+.record-btn-dot { width: 24rpx; height: 24rpx; border-radius: 50%; background: #fff; }
+.record-btn-active .record-btn-dot { width: 20rpx; height: 20rpx; border-radius: 4rpx; background: #fff; }
 .voice-hint { font-size: 22rpx; color: #999; text-align: center; }
+@keyframes waveform-anim { 0%, 100% { height: 8rpx; } 50% { height: 40rpx; } }
+@keyframes pulse-expand { 0% { transform: scale(1); opacity: 0.6; } 100% { transform: scale(1.8); opacity: 0; } }
+@keyframes btn-pulse { 0%, 100% { box-shadow: 0 4rpx 16rpx rgba(239,68,68,0.4); } 50% { box-shadow: 0 4rpx 24rpx rgba(239,68,68,0.6); } }
 .submit-wrap { padding: 24rpx 24rpx 40rpx; }
 .submit-btn { background: linear-gradient(135deg, #FF8C42, #F97316); color: #fff; text-align: center; padding: 24rpx 0; border-radius: 16rpx; font-size: 30rpx; font-weight: bold; }
 .submitting { opacity: 0.6; }