Prechádzať zdrojové kódy

fix: 小程序前端 GET 请求全部改为 POST + 成长打卡页美化与自动返回

- 后端: PurchaseController.java /challenge/progress @GetMapping→@PostMapping
- 前端: 7 处 GET→POST (api.js, service-tasks/chat/execution/rating/invite.vue)
- 成长打卡页美化 (diet/exercise/sleep/emotion-checkin.vue):
  * 统一卡片式布局、导航栏、表单/标签/媒体工具栏/提交按钮样式
  * 打卡成功后 800ms 自动 navigateBack 回成长页
Sisyphus 3 týždňov pred
rodič
commit
508427acae

+ 3 - 2
cfc-backend/src/main/java/com/etotem/cfc/controller/PurchaseController.java

@@ -56,8 +56,9 @@ public class PurchaseController {
         return Result.success(result);
     }
 
-    @GetMapping("/challenge/progress")
-    public Result<Map<String, Object>> getChallengeProgress(@RequestParam Long memberId, HttpServletRequest request) {
+    @PostMapping("/challenge/progress")
+    public Result<Map<String, Object>> getChallengeProgress(@RequestBody Map<String, Object> params, HttpServletRequest request) {
+        Long memberId = params.get("memberId") != null ? ((Number) params.get("memberId")).longValue() : null;
         Map<String, Object> result = challengeService.getChallengeProgress(memberId);
         return Result.success(result);
     }

+ 5 - 3
cfc-frontend/pages/assessment/execution.vue

@@ -139,10 +139,12 @@ export default {
       this.loading = true
       try {
         const res = await uni.request({
-          url: `${config.API_BASE_URL}/api/dan-execution/detail/${this.executionId}`,
-          method: 'GET',
+          url: `${config.API_BASE_URL}/api/dan-execution/detail`,
+          method: 'POST',
+          data: { executionId: this.executionId },
           header: {
-            'Authorization': `Bearer ${uni.getStorageSync('token')}`
+            'Authorization': `Bearer ${uni.getStorageSync('token')}`,
+            'Content-Type': 'application/json'
           }
         })
         if (res.data.code === 200) {

+ 5 - 3
cfc-frontend/pages/assessment/rating.vue

@@ -124,10 +124,12 @@ export default {
       this.loading = true
       try {
         const res = await uni.request({
-          url: `${config.API_BASE_URL}/api/dan-execution/detail/${this.executionId}`,
-          method: 'GET',
+          url: `${config.API_BASE_URL}/api/dan-execution/detail`,
+          method: 'POST',
+          data: { executionId: this.executionId },
           header: {
-            'Authorization': `Bearer ${uni.getStorageSync('token')}`
+            'Authorization': `Bearer ${uni.getStorageSync('token')}`,
+            'Content-Type': 'application/json'
           }
         })
         if (res.data.code === 200) {

+ 154 - 106
cfc-frontend/pages/growth/diet-checkin.vue

@@ -1,19 +1,38 @@
 <template>
-  <scroll-view class="content" scroll-y>
+  <view class="page">
+    <!-- 顶部导航栏 -->
+    <view class="nav-bar">
+      <view class="nav-left" @tap="goBack">
+        <text class="nav-icon">‹</text>
+        <text class="nav-title">饮食打卡</text>
+      </view>
+    </view>
+
+    <scroll-view class="content" scroll-y>
       <!-- 餐次选择 -->
       <view class="card">
-        <view class="card-accent"></view>
+        <view class="card-header">
+          <text class="card-icon">🍽️</text>
+          <text class="card-title">选择餐次</text>
+        </view>
         <view class="card-body">
-          <view class="form-group">
-            <text class="form-label">餐次 *</text>
-            <view class="tag-row">
-              <view class="tag-btn" v-for="m in mealTypes" :key="m.value"
-                :class="form.mealType === m.value ? 'tag-active' : ''"
-                @tap="form.mealType = m.value">
-                <text>{{ m.label }}</text>
-              </view>
+          <view class="tag-row">
+            <view class="tag-btn" v-for="m in mealTypes" :key="m.value"
+              :class="form.mealType === m.value ? 'tag-active' : ''"
+              @tap="form.mealType = m.value">
+              <text>{{ m.label }}</text>
             </view>
           </view>
+        </view>
+      </view>
+
+      <!-- 食物清单 & 备注 -->
+      <view class="card">
+        <view class="card-header">
+          <text class="card-icon">📝</text>
+          <text class="card-title">记录详情</text>
+        </view>
+        <view class="card-body">
           <view class="form-group">
             <text class="form-label">食物清单</text>
             <textarea class="form-textarea" v-model="form.foodItems" placeholder="吃了什么?如:米饭1碗、青菜、鸡腿" maxlength="200"></textarea>
@@ -25,46 +44,54 @@
         </view>
       </view>
 
-      <!-- 并排两个按钮 + 展开区域 -->
-      <view class="media-toolbar">
-        <view class="tool-btn" @tap="choosePhoto">
-          <text class="tool-icon">📷</text>
-          <text class="tool-label">上传照片</text>
-          <text class="tool-count" v-if="form.photos.length > 0">{{ form.photos.length }}/9</text>
-        </view>
-        <view class="tool-divider"></view>
-        <view class="tool-btn" @tap="toggleRecord">
-          <text class="tool-icon">🎙</text>
-          <text class="tool-label">{{ isRecording ? '录音中…' : (form.voicePath ? '已录制' : '录制音频') }}</text>
-          <text class="tool-count" v-if="isRecording">{{ recordSeconds }}s</text>
+      <!-- 媒体工具栏 -->
+      <view class="card">
+        <view class="card-header">
+          <text class="card-icon">📎</text>
+          <text class="card-title">添加媒体</text>
         </view>
-      </view>
-      <!-- 照片缩略图 -->
-      <view class="photo-strip" v-if="form.photos.length > 0">
-        <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-img"/>
-          <view class="photo-delete-overlay">
-            <text class="delete-icon">×</text>
+        <view class="card-body">
+          <view class="media-toolbar">
+            <view class="tool-btn" @tap="choosePhoto">
+              <text class="tool-icon">📷</text>
+              <text class="tool-label">照片</text>
+              <text class="tool-count" v-if="form.photos.length > 0">{{ form.photos.length }}/9</text>
+            </view>
+            <view class="tool-divider"></view>
+            <view class="tool-btn" @tap="toggleRecord">
+              <text class="tool-icon">🎙</text>
+              <text class="tool-label">{{ isRecording ? '录音中…' : (form.voicePath ? '已录制' : '录音') }}</text>
+              <text class="tool-count" v-if="isRecording">{{ recordSeconds }}s</text>
+            </view>
+          </view>
+          <!-- 照片缩略图 -->
+          <view class="photo-strip" v-if="form.photos.length > 0">
+            <view class="photo-item" v-for="(img, idx) in form.photos" :key="getItemKey(img, idx)" @tap="removePhoto(idx)">
+              <image :src="img" mode="aspectFill" class="photo-img"/>
+              <view class="photo-delete-overlay">
+                <text class="delete-icon">×</text>
+              </view>
+            </view>
           </view>
-        </view>
-      </view>
 
-      <!-- 录音状态 -->
-      <view class="voice-strip" v-if="isRecording">
-        <view class="waveform">
-          <view class="wave-bar" v-for="n in 7" :key="n" :style="{animationDelay: (n * 0.1) + 's'}"></view>
-        </view>
-        <view class="voice-controls">
-          <view class="mini-btn cancel-mini" @tap="cancelRecord"><text>取消</text></view>
-          <view class="mini-btn done-mini" @tap="finishRecord"><text>完成</text></view>
-        </view>
-      </view>
-      <view class="voice-strip" v-else-if="form.voicePath">
-        <view class="voice-player">
-          <text class="play-icon">▶</text>
-          <text class="voice-duration">{{ recordDurationText }}</text>
+          <!-- 录音状态 -->
+          <view class="voice-strip" v-if="isRecording">
+            <view class="waveform">
+              <view class="wave-bar" v-for="n in 7" :key="n" :style="{animationDelay: (n * 0.1) + 's'}"></view>
+            </view>
+            <view class="voice-controls">
+              <view class="mini-btn cancel-mini" @tap="cancelRecord"><text>取消</text></view>
+              <view class="mini-btn done-mini" @tap="finishRecord"><text>完成</text></view>
+            </view>
+          </view>
+          <view class="voice-strip" v-else-if="form.voicePath">
+            <view class="voice-player">
+              <text class="play-icon">▶</text>
+              <text class="voice-duration">{{ recordDurationText }}</text>
+            </view>
+            <view class="delete-voice" @tap="deleteVoice"><text>×</text></view>
+          </view>
         </view>
-        <view class="delete-voice" @tap="deleteVoice"><text>×</text></view>
       </view>
 
       <!-- 提交按钮 -->
@@ -76,9 +103,11 @@
 
       <!-- 历史记录 -->
       <view class="card" v-if="history.length > 0">
-        <view class="card-accent"></view>
-        <view class="card-body">
+        <view class="card-header">
+          <text class="card-icon">📜</text>
           <text class="card-title">今日记录</text>
+        </view>
+        <view class="card-body">
           <view class="history-list">
             <view class="history-item" v-for="item in history" :key="item.id">
               <view class="history-left">
@@ -92,7 +121,11 @@
           </view>
         </view>
       </view>
+
+      <!-- 底部留白 -->
+      <view class="bottom-spacer"></view>
     </scroll-view>
+  </view>
 </template>
 
 <script>
@@ -293,21 +326,17 @@ export default {
             voiceText: self.form.voiceText || ''
           },
           header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
-          success: function(res) {
-            self.submitting = false
-            if (res.data && res.data.code === 200) {
-              uni.showToast({ title: '打卡成功', icon: 'success' })
-              self.form.mealType = ''
-              self.form.foodItems = ''
-              self.form.remark = ''
-              self.form.photos = []
-              self.form.voicePath = ''
-              self.recordSeconds = 0
-              self.loadHistory()
-            } else {
-              uni.showToast({ title: (res.data && res.data.message) || '打卡失败', icon: 'none' })
-            }
-          },
+success: function(res) {
+              self.submitting = false
+              if (res.data && res.data.code === 200) {
+                uni.showToast({ title: '打卡成功', icon: 'success' })
+                setTimeout(function() {
+                  uni.navigateBack()
+                }, 800)
+              } else {
+                uni.showToast({ title: (res.data && res.data.message) || '打卡失败', icon: 'none' })
+              }
+            },
           fail: function() {
             self.submitting = false
             uni.showToast({ title: '网络错误', icon: 'none' })
@@ -323,58 +352,77 @@ export default {
 </script>
 
 <style scoped>
+/* ===== 页面基础 ===== */
 .page { min-height: 100vh; background: #FFF7ED; }
-.content { height: calc(100vh - 88rpx); }
-.card { margin: 16rpx 24rpx; background: #fff; border-radius: 20rpx; overflow: hidden; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); }
-.card-accent { height: 6rpx; background: linear-gradient(90deg, #FF8C42, #F97316); }
-.card-body { padding: 28rpx; }
-.card-title { font-size: 30rpx; font-weight: bold; color: #333; margin-bottom: 20rpx; display: block; }
-.form-group { margin-bottom: 24rpx; }
-.form-label { font-size: 28rpx; color: #333; font-weight: 600; margin-bottom: 12rpx; display: block; }
-.tag-row { display: flex; flex-wrap: wrap; gap: 12rpx; }
-.tag-btn { padding: 12rpx 28rpx; border-radius: 30rpx; border: 2rpx solid #e0e0e0; background: #fafafa; }
-.tag-active { border-color: #F97316; background: #FFF3E0; }
+.nav-bar { height: 88rpx; padding: 24rpx 24rpx 0; display: flex; align-items: flex-end; }
+.nav-left { display: flex; align-items: center; gap: 10rpx; }
+.nav-icon { font-size: 48rpx; color: #333; line-height: 1; }
+.nav-title { font-size: 34rpx; font-weight: 600; color: #333; }
+.content { padding-bottom: 40rpx; }
+
+/* ===== 卡片通用 ===== */
+.card { margin: 16rpx 20rpx; background: #fff; border-radius: 20rpx; overflow: hidden; box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.05); }
+.card-header { display: flex; align-items: center; gap: 10rpx; padding: 20rpx 24rpx 8rpx; border-bottom: 1rpx solid #f5f5f5; background: linear-gradient(90deg, #FFFAF0, #fff); }
+.card-icon { font-size: 32rpx; }
+.card-title { font-size: 30rpx; font-weight: 600; color: #333; }
+.card-body { padding: 20rpx 24rpx 24rpx; }
+
+/* ===== 表单 ===== */
+.form-group { margin-bottom: 20rpx; }
+.form-label { font-size: 26rpx; color: #333; font-weight: 500; margin-bottom: 10rpx; display: block; }
+.tag-row { display: flex; flex-wrap: wrap; gap: 10rpx; }
+.tag-btn { padding: 12rpx 24rpx; border-radius: 30rpx; border: 2rpx solid #e8e8e8; background: #fafafa; transition: all 0.2s ease; }
+.tag-active { border-color: #F97316; background: #FFF3E0; box-shadow: 0 4rpx 12rpx rgba(249,115,22,0.15); }
 .tag-btn text { font-size: 26rpx; color: #666; }
-.tag-active text { font-size: 26rpx; color: #F97316; font-weight: bold; }
-.form-textarea { width: 100%; height: 140rpx; border: 1rpx solid #e0e0e0; border-radius: 16rpx; padding: 20rpx; font-size: 28rpx; background: #FAFAFA; box-sizing: border-box; }
-.form-input { width: 100%; height: 80rpx; border: 1rpx solid #e0e0e0; border-radius: 16rpx; padding: 0 20rpx; font-size: 28rpx; background: #FAFAFA; box-sizing: border-box; }
-/* 录入框下方的并排工具栏 */
-.media-toolbar { display: flex; align-items: center; margin: 16rpx 24rpx; padding: 24rpx 0; border-top: 1rpx solid #f0f0f0; }
-.tool-btn { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8rpx; padding: 16rpx 0; }
-.tool-icon { font-size: 48rpx; }
+.tag-active text { color: #F97316; font-weight: 600; }
+.form-textarea { width: 100%; height: 120rpx; border: 1rpx solid #e8e8e8; border-radius: 14rpx; padding: 16rpx; font-size: 26rpx; background: #FAFAFA; box-sizing: border-box; font-family: inherit; }
+.form-input { width: 100%; height: 76rpx; border: 1rpx solid #e8e8e8; border-radius: 14rpx; padding: 0 16rpx; font-size: 26rpx; background: #FAFAFA; box-sizing: border-box; }
+
+/* ===== 媒体工具栏 ===== */
+.media-toolbar { display: flex; align-items: center; gap: 0; padding: 12rpx 0; }
+.tool-btn { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6rpx; padding: 16rpx 0; }
+.tool-icon { font-size: 42rpx; }
 .tool-label { font-size: 24rpx; color: #666; }
-.tool-count { font-size: 22rpx; color: #F97316; font-weight: bold; }
-.tool-divider { width: 1rpx; height: 60rpx; background: #e8e8e8; }
+.tool-count { font-size: 20rpx; color: #F97316; font-weight: 600; }
+.tool-divider { width: 1rpx; height: 56rpx; background: linear-gradient(180deg, transparent, #e8e8e8, transparent); }
 
-/* 照片缩略图条 */
-.photo-strip { display: flex; flex-wrap: wrap; gap: 12rpx; padding: 0 24rpx 4rpx; }
-.photo-item { position: relative; width: 140rpx; height: 140rpx; }
-.photo-img { width: 100%; height: 100%; border-radius: 12rpx; }
-.photo-delete-overlay { position: absolute; top: -6rpx; right: -6rpx; width: 36rpx; height: 36rpx; background: #EF4444; border-radius: 50%; display: flex; align-items: center; justify-content: center; box-shadow: 0 2rpx 6rpx rgba(239,68,68,0.3); z-index: 2; }
-.delete-icon { color: #fff; font-size: 24rpx; font-weight: bold; line-height: 36rpx; }
+/* ===== 照片缩略图 ===== */
+.photo-strip { display: flex; flex-wrap: wrap; gap: 10rpx; padding: 12rpx 0 4rpx; }
+.photo-item { position: relative; width: 132rpx; height: 132rpx; border-radius: 12rpx; overflow: hidden; }
+.photo-img { width: 100%; height: 100%; object-fit: cover; }
+.photo-delete-overlay { position: absolute; top: -6rpx; right: -6rpx; width: 32rpx; height: 32rpx; background: #EF4444; border-radius: 50%; display: flex; align-items: center; justify-content: center; box-shadow: 0 2rpx 8rpx rgba(239,68,68,0.3); z-index: 2; }
+.delete-icon { color: #fff; font-size: 22rpx; font-weight: bold; line-height: 32rpx; }
 
-/* 录音展开区 */
-.voice-strip { display: flex; align-items: center; justify-content: space-between; padding: 16rpx 24rpx 4rpx; gap: 16rpx; }
-.waveform { display: flex; align-items: center; gap: 6rpx; height: 48rpx; flex: 1; }
-.wave-bar { width: 6rpx; height: 10rpx; background: #F97316; border-radius: 3rpx; animation: wave-anim 0.8s ease-in-out infinite; }
-@keyframes wave-anim { 0%, 100% { height: 10rpx; } 50% { height: 36rpx; } }
-.voice-controls { display: flex; gap: 16rpx; }
-.mini-btn { padding: 12rpx 28rpx; border-radius: 24rpx; font-size: 26rpx; }
+/* ===== 录音展开区 ===== */
+.voice-strip { display: flex; align-items: center; justify-content: space-between; padding: 12rpx 0 4rpx; gap: 16rpx; }
+.waveform { display: flex; align-items: center; gap: 5rpx; height: 40rpx; flex: 1; }
+.wave-bar { width: 5rpx; height: 8rpx; background: #F97316; border-radius: 3rpx; animation: wave-anim 0.8s ease-in-out infinite; }
+@keyframes wave-anim { 0%, 100% { height: 8rpx; } 50% { height: 32rpx; } }
+.voice-controls { display: flex; gap: 12rpx; }
+.mini-btn { padding: 10rpx 24rpx; border-radius: 22rpx; font-size: 24rpx; }
 .cancel-mini { background: #f0f0f0; color: #666; }
-.done-mini { background: #F97316; color: #fff; font-weight: bold; }
+.done-mini { background: #F97316; color: #fff; font-weight: 600; }
+
+.voice-player { display: flex; align-items: center; gap: 10rpx; flex: 1; background: #FFF3E0; padding: 10rpx 20rpx; border-radius: 28rpx; }
+.play-icon { font-size: 26rpx; color: #F97316; }
+.voice-duration { font-size: 24rpx; color: #666; }
+.delete-voice { width: 40rpx; height: 40rpx; display: flex; align-items: center; justify-content: center; color: #999; font-size: 28rpx; }
 
-.voice-player { display: flex; align-items: center; gap: 12rpx; flex: 1; background: #FFF3E0; padding: 12rpx 24rpx; border-radius: 30rpx; }
-.play-icon { font-size: 28rpx; color: #F97316; }
-.voice-duration { font-size: 26rpx; color: #666; }
-.delete-voice { width: 44rpx; height: 44rpx; display: flex; align-items: center; justify-content: center; color: #999; font-size: 32rpx; }
-.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: 32rpx; font-weight: bold; }
+/* ===== 提交按钮 ===== */
+.submit-wrap { padding: 24rpx 20rpx 32rpx; }
+.submit-btn { background: linear-gradient(135deg, #FF8C42, #F97316); color: #fff; text-align: center; padding: 22rpx 0; border-radius: 14rpx; font-size: 30rpx; font-weight: 600; box-shadow: 0 6rpx 20rpx rgba(249,115,22,0.3); transition: all 0.2s ease; }
+.submit-btn:active { transform: scale(0.98); }
 .submitting { opacity: 0.6; }
-.history-list { display: flex; flex-direction: column; gap: 16rpx; }
-.history-item { display: flex; justify-content: space-between; align-items: center; padding: 12rpx 0; border-bottom: 1rpx solid #f5f5f5; }
+
+/* ===== 历史记录 ===== */
+.history-list { display: flex; flex-direction: column; gap: 12rpx; }
+.history-item { display: flex; justify-content: space-between; align-items: center; padding: 14rpx 0; border-bottom: 1rpx solid #f5f5f5; }
 .history-left { flex: 1; min-width: 0; }
-.history-type { font-size: 28rpx; font-weight: 500; color: #333; display: block; }
+.history-type { font-size: 26rpx; font-weight: 500; color: #333; display: block; }
 .history-content { font-size: 24rpx; color: #888; display: block; margin-top: 4rpx; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
 .history-right { text-align: right; margin-left: 16rpx; flex-shrink: 0; }
-.history-date { font-size: 24rpx; color: #999; }
+.history-date { font-size: 22rpx; color: #999; }
+
+/* ===== 底部留白 ===== */
+.bottom-spacer { height: 40rpx; }
 </style>

+ 150 - 101
cfc-frontend/pages/growth/exercise-checkin.vue

@@ -1,19 +1,38 @@
 <template>
-  <scroll-view class="content" scroll-y>
-      <!-- 运动表单 -->
+  <view class="page">
+    <!-- 顶部导航栏 -->
+    <view class="nav-bar">
+      <view class="nav-left" @tap="goBack">
+        <text class="nav-icon">‹</text>
+        <text class="nav-title">运动打卡</text>
+      </view>
+    </view>
+
+    <scroll-view class="content" scroll-y>
+      <!-- 运动类型 -->
       <view class="card">
-        <view class="card-accent"></view>
+        <view class="card-header">
+          <text class="card-icon">🏃</text>
+          <text class="card-title">运动类型</text>
+        </view>
         <view class="card-body">
-          <view class="form-group">
-            <text class="form-label">运动类型 *</text>
-            <view class="tag-row">
-              <view class="tag-btn" v-for="t in exerciseTypes" :key="t.value"
-                :class="form.exerciseType === t.value ? 'tag-active' : ''"
-                @tap="form.exerciseType = t.value">
-                <text>{{ t.label }}</text>
-              </view>
+          <view class="tag-row">
+            <view class="tag-btn" v-for="t in exerciseTypes" :key="t.value"
+              :class="form.exerciseType === t.value ? 'tag-active' : ''"
+              @tap="form.exerciseType = t.value">
+              <text>{{ t.label }}</text>
             </view>
           </view>
+        </view>
+      </view>
+
+      <!-- 时长 & 强度 & 卡路里 -->
+      <view class="card">
+        <view class="card-header">
+          <text class="card-icon">⏱️</text>
+          <text class="card-title">运动详情</text>
+        </view>
+        <view class="card-body">
           <view class="form-group">
             <text class="form-label">时长(分钟)*</text>
             <input class="form-input" type="number" v-model="form.durationMinutes" placeholder="运动了多久?" />
@@ -39,46 +58,54 @@
         </view>
       </view>
 
-      <!-- 并排两个按钮 + 展开区域 -->
-      <view class="media-toolbar">
-        <view class="tool-btn" @tap="choosePhoto">
-          <text class="tool-icon">📷</text>
-          <text class="tool-label">上传照片</text>
-          <text class="tool-count" v-if="form.photos.length > 0">{{ form.photos.length }}/9</text>
-        </view>
-        <view class="tool-divider"></view>
-        <view class="tool-btn" @tap="toggleRecord">
-          <text class="tool-icon">🎙</text>
-          <text class="tool-label">{{ isRecording ? '录音中…' : (form.voicePath ? '已录制' : '录制音频') }}</text>
-          <text class="tool-count" v-if="isRecording">{{ recordSeconds }}s</text>
+      <!-- 媒体工具栏 -->
+      <view class="card">
+        <view class="card-header">
+          <text class="card-icon">📎</text>
+          <text class="card-title">添加媒体</text>
         </view>
-      </view>
-      <!-- 照片缩略图 -->
-      <view class="photo-strip" v-if="form.photos.length > 0">
-        <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-img"/>
-          <view class="photo-delete-overlay">
-            <text class="delete-icon">×</text>
+        <view class="card-body">
+          <view class="media-toolbar">
+            <view class="tool-btn" @tap="choosePhoto">
+              <text class="tool-icon">📷</text>
+              <text class="tool-label">照片</text>
+              <text class="tool-count" v-if="form.photos.length > 0">{{ form.photos.length }}/9</text>
+            </view>
+            <view class="tool-divider"></view>
+            <view class="tool-btn" @tap="toggleRecord">
+              <text class="tool-icon">🎙</text>
+              <text class="tool-label">{{ isRecording ? '录音中…' : (form.voicePath ? '已录制' : '录音') }}</text>
+              <text class="tool-count" v-if="isRecording">{{ recordSeconds }}s</text>
+            </view>
+          </view>
+          <!-- 照片缩略图 -->
+          <view class="photo-strip" v-if="form.photos.length > 0">
+            <view class="photo-item" v-for="(img, idx) in form.photos" :key="getItemKey(img, idx)" @tap="removePhoto(idx)">
+              <image :src="img" mode="aspectFill" class="photo-img"/>
+              <view class="photo-delete-overlay">
+                <text class="delete-icon">×</text>
+              </view>
+            </view>
           </view>
-        </view>
-      </view>
 
-      <!-- 录音状态 -->
-      <view class="voice-strip" v-if="isRecording">
-        <view class="waveform">
-          <view class="wave-bar" v-for="n in 7" :key="n" :style="{animationDelay: (n * 0.1) + 's'}"></view>
-        </view>
-        <view class="voice-controls">
-          <view class="mini-btn cancel-mini" @tap="cancelRecord"><text>取消</text></view>
-          <view class="mini-btn done-mini" @tap="finishRecord"><text>完成</text></view>
-        </view>
-      </view>
-      <view class="voice-strip" v-else-if="form.voicePath">
-        <view class="voice-player">
-          <text class="play-icon">▶</text>
-          <text class="voice-duration">{{ recordDurationText }}</text>
+          <!-- 录音状态 -->
+          <view class="voice-strip" v-if="isRecording">
+            <view class="waveform">
+              <view class="wave-bar" v-for="n in 7" :key="n" :style="{animationDelay: (n * 0.1) + 's'}"></view>
+            </view>
+            <view class="voice-controls">
+              <view class="mini-btn cancel-mini" @tap="cancelRecord"><text>取消</text></view>
+              <view class="mini-btn done-mini" @tap="finishRecord"><text>完成</text></view>
+            </view>
+          </view>
+          <view class="voice-strip" v-else-if="form.voicePath">
+            <view class="voice-player">
+              <text class="play-icon">▶</text>
+              <text class="voice-duration">{{ recordDurationText }}</text>
+            </view>
+            <view class="delete-voice" @tap="deleteVoice"><text>×</text></view>
+          </view>
         </view>
-        <view class="delete-voice" @tap="deleteVoice"><text>×</text></view>
       </view>
 
       <!-- 提交按钮 -->
@@ -90,21 +117,29 @@
 
       <!-- 历史记录 -->
       <view class="card" v-if="history.length > 0">
-        <view class="card-accent"></view>
-        <view class="card-body">
+        <view class="card-header">
+          <text class="card-icon">📜</text>
           <text class="card-title">近期记录</text>
+        </view>
+        <view class="card-body">
           <view class="history-list">
             <view class="history-item" v-for="item in history" :key="item.id">
               <view class="history-left">
                 <text class="history-type">{{ exerciseTypeLabel(item.exerciseType) }}</text>
                 <text class="history-content">{{ item.durationMinutes }}分钟{{ item.caloriesBurned ? ' · ' + item.caloriesBurned + '千卡' : '' }}</text>
               </view>
-              <text class="history-date">{{ formatDate(item.createdAt) }}</text>
+              <view class="history-right">
+                <text class="history-date">{{ formatDate(item.createdAt) }}</text>
+              </view>
             </view>
           </view>
         </view>
       </view>
+
+      <!-- 底部留白 -->
+      <view class="bottom-spacer"></view>
     </scroll-view>
+  </view>
 </template>
 
 <script>
@@ -310,15 +345,9 @@ export default {
           success: function(res) {
             if (res.data && res.data.code === 200) {
               uni.showToast({ title: '打卡成功', icon: 'success' })
-              self.form.exerciseType = ''
-              self.form.durationMinutes = ''
-              self.form.intensity = 'medium'
-              self.form.caloriesBurned = ''
-              self.form.remark = ''
-              self.form.photos = []
-              self.form.voicePath = ''
-              self.recordSeconds = 0
-              self.loadHistory()
+              setTimeout(function() {
+                uni.navigateBack()
+              }, 800)
             } else {
               uni.showToast({ title: (res.data && res.data.message) || '打卡失败', icon: 'none' })
             }
@@ -338,56 +367,76 @@ export default {
 </script>
 
 <style scoped>
+/* ===== 页面基础 ===== */
 .page { min-height: 100vh; background: #FFF7ED; }
-.content { height: calc(100vh - 88rpx); }
-.card { margin: 16rpx 24rpx; background: #fff; border-radius: 20rpx; overflow: hidden; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); }
-.card-accent { height: 6rpx; background: linear-gradient(90deg, #FF8C42, #F97316); }
-.card-body { padding: 28rpx; }
-.card-title { font-size: 30rpx; font-weight: bold; color: #333; margin-bottom: 20rpx; display: block; }
-.form-group { margin-bottom: 24rpx; }
-.form-label { font-size: 28rpx; color: #333; font-weight: 600; margin-bottom: 12rpx; display: block; }
-.tag-row { display: flex; flex-wrap: wrap; gap: 12rpx; }
-.tag-btn { padding: 12rpx 28rpx; border-radius: 30rpx; border: 2rpx solid #e0e0e0; background: #fafafa; }
-.tag-active { border-color: #F97316; background: #FFF3E0; }
+.nav-bar { height: 88rpx; padding: 24rpx 24rpx 0; display: flex; align-items: flex-end; }
+.nav-left { display: flex; align-items: center; gap: 10rpx; }
+.nav-icon { font-size: 48rpx; color: #333; line-height: 1; }
+.nav-title { font-size: 34rpx; font-weight: 600; color: #333; }
+.content { padding-bottom: 40rpx; }
+
+/* ===== 卡片通用 ===== */
+.card { margin: 16rpx 20rpx; background: #fff; border-radius: 20rpx; overflow: hidden; box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.05); }
+.card-header { display: flex; align-items: center; gap: 10rpx; padding: 20rpx 24rpx 8rpx; border-bottom: 1rpx solid #f5f5f5; background: linear-gradient(90deg, #FFFAF0, #fff); }
+.card-icon { font-size: 32rpx; }
+.card-title { font-size: 30rpx; font-weight: 600; color: #333; }
+.card-body { padding: 20rpx 24rpx 24rpx; }
+
+/* ===== 表单 ===== */
+.form-group { margin-bottom: 20rpx; }
+.form-label { font-size: 26rpx; color: #333; font-weight: 500; margin-bottom: 10rpx; display: block; }
+.tag-row { display: flex; flex-wrap: wrap; gap: 10rpx; }
+.tag-btn { padding: 12rpx 24rpx; border-radius: 30rpx; border: 2rpx solid #e8e8e8; background: #fafafa; transition: all 0.2s ease; }
+.tag-active { border-color: #F97316; background: #FFF3E0; box-shadow: 0 4rpx 12rpx rgba(249,115,22,0.15); }
 .tag-btn text { font-size: 26rpx; color: #666; }
-.tag-active text { font-size: 26rpx; color: #F97316; font-weight: bold; }
-.form-input { width: 100%; height: 80rpx; border: 1rpx solid #e0e0e0; border-radius: 16rpx; padding: 0 20rpx; font-size: 28rpx; background: #FAFAFA; box-sizing: border-box; }
-/* 录入框下方的并排工具栏 */
-.media-toolbar { display: flex; align-items: center; margin: 16rpx 24rpx; padding: 24rpx 0; border-top: 1rpx solid #f0f0f0; }
-.tool-btn { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8rpx; padding: 16rpx 0; }
-.tool-icon { font-size: 48rpx; }
+.tag-active text { color: #F97316; font-weight: 600; }
+.form-input { width: 100%; height: 76rpx; border: 1rpx solid #e8e8e8; border-radius: 14rpx; padding: 0 16rpx; font-size: 26rpx; background: #FAFAFA; box-sizing: border-box; }
+
+/* ===== 媒体工具栏 ===== */
+.media-toolbar { display: flex; align-items: center; gap: 0; padding: 12rpx 0; }
+.tool-btn { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6rpx; padding: 16rpx 0; }
+.tool-icon { font-size: 42rpx; }
 .tool-label { font-size: 24rpx; color: #666; }
-.tool-count { font-size: 22rpx; color: #F97316; font-weight: bold; }
-.tool-divider { width: 1rpx; height: 60rpx; background: #e8e8e8; }
+.tool-count { font-size: 20rpx; color: #F97316; font-weight: 600; }
+.tool-divider { width: 1rpx; height: 56rpx; background: linear-gradient(180deg, transparent, #e8e8e8, transparent); }
 
-/* 照片缩略图条 */
-.photo-strip { display: flex; flex-wrap: wrap; gap: 12rpx; padding: 0 24rpx 4rpx; }
-.photo-item { position: relative; width: 140rpx; height: 140rpx; }
-.photo-img { width: 100%; height: 100%; border-radius: 12rpx; }
-.photo-delete-overlay { position: absolute; top: -6rpx; right: -6rpx; width: 36rpx; height: 36rpx; background: #EF4444; border-radius: 50%; display: flex; align-items: center; justify-content: center; box-shadow: 0 2rpx 6rpx rgba(239,68,68,0.3); z-index: 2; }
-.delete-icon { color: #fff; font-size: 24rpx; font-weight: bold; line-height: 36rpx; }
+/* ===== 照片缩略图 ===== */
+.photo-strip { display: flex; flex-wrap: wrap; gap: 10rpx; padding: 12rpx 0 4rpx; }
+.photo-item { position: relative; width: 132rpx; height: 132rpx; border-radius: 12rpx; overflow: hidden; }
+.photo-img { width: 100%; height: 100%; object-fit: cover; }
+.photo-delete-overlay { position: absolute; top: -6rpx; right: -6rpx; width: 32rpx; height: 32rpx; background: #EF4444; border-radius: 50%; display: flex; align-items: center; justify-content: center; box-shadow: 0 2rpx 8rpx rgba(239,68,68,0.3); z-index: 2; }
+.delete-icon { color: #fff; font-size: 22rpx; font-weight: bold; line-height: 32rpx; }
 
-/* 录音展开区 */
-.voice-strip { display: flex; align-items: center; justify-content: space-between; padding: 16rpx 24rpx 4rpx; gap: 16rpx; }
-.waveform { display: flex; align-items: center; gap: 6rpx; height: 48rpx; flex: 1; }
-.wave-bar { width: 6rpx; height: 10rpx; background: #F97316; border-radius: 3rpx; animation: wave-anim 0.8s ease-in-out infinite; }
-@keyframes wave-anim { 0%, 100% { height: 10rpx; } 50% { height: 36rpx; } }
-.voice-controls { display: flex; gap: 16rpx; }
-.mini-btn { padding: 12rpx 28rpx; border-radius: 24rpx; font-size: 26rpx; }
+/* ===== 录音展开区 ===== */
+.voice-strip { display: flex; align-items: center; justify-content: space-between; padding: 12rpx 0 4rpx; gap: 16rpx; }
+.waveform { display: flex; align-items: center; gap: 5rpx; height: 40rpx; flex: 1; }
+.wave-bar { width: 5rpx; height: 8rpx; background: #F97316; border-radius: 3rpx; animation: wave-anim 0.8s ease-in-out infinite; }
+@keyframes wave-anim { 0%, 100% { height: 8rpx; } 50% { height: 32rpx; } }
+.voice-controls { display: flex; gap: 12rpx; }
+.mini-btn { padding: 10rpx 24rpx; border-radius: 22rpx; font-size: 24rpx; }
 .cancel-mini { background: #f0f0f0; color: #666; }
-.done-mini { background: #F97316; color: #fff; font-weight: bold; }
+.done-mini { background: #F97316; color: #fff; font-weight: 600; }
+
+.voice-player { display: flex; align-items: center; gap: 10rpx; flex: 1; background: #FFF3E0; padding: 10rpx 20rpx; border-radius: 28rpx; }
+.play-icon { font-size: 26rpx; color: #F97316; }
+.voice-duration { font-size: 24rpx; color: #666; }
+.delete-voice { width: 40rpx; height: 40rpx; display: flex; align-items: center; justify-content: center; color: #999; font-size: 28rpx; }
 
-.voice-player { display: flex; align-items: center; gap: 12rpx; flex: 1; background: #FFF3E0; padding: 12rpx 24rpx; border-radius: 30rpx; }
-.play-icon { font-size: 28rpx; color: #F97316; }
-.voice-duration { font-size: 26rpx; color: #666; }
-.delete-voice { width: 44rpx; height: 44rpx; display: flex; align-items: center; justify-content: center; color: #999; font-size: 32rpx; }
-.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: 32rpx; font-weight: bold; }
+/* ===== 提交按钮 ===== */
+.submit-wrap { padding: 24rpx 20rpx 32rpx; }
+.submit-btn { background: linear-gradient(135deg, #FF8C42, #F97316); color: #fff; text-align: center; padding: 22rpx 0; border-radius: 14rpx; font-size: 30rpx; font-weight: 600; box-shadow: 0 6rpx 20rpx rgba(249,115,22,0.3); transition: all 0.2s ease; }
+.submit-btn:active { transform: scale(0.98); }
 .submitting { opacity: 0.6; }
-.history-list { display: flex; flex-direction: column; gap: 16rpx; }
-.history-item { display: flex; justify-content: space-between; align-items: center; padding: 12rpx 0; border-bottom: 1rpx solid #f5f5f5; }
+
+/* ===== 历史记录 ===== */
+.history-list { display: flex; flex-direction: column; gap: 12rpx; }
+.history-item { display: flex; justify-content: space-between; align-items: center; padding: 14rpx 0; border-bottom: 1rpx solid #f5f5f5; }
 .history-left { flex: 1; min-width: 0; }
-.history-type { font-size: 28rpx; font-weight: 500; color: #333; display: block; }
+.history-type { font-size: 26rpx; font-weight: 500; color: #333; display: block; }
 .history-content { font-size: 24rpx; color: #888; display: block; margin-top: 4rpx; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
-.history-date { font-size: 24rpx; color: #999; flex-shrink: 0; margin-left: 16rpx; }
+.history-right { text-align: right; margin-left: 16rpx; flex-shrink: 0; }
+.history-date { font-size: 22rpx; color: #999; }
+
+/* ===== 底部留白 ===== */
+.bottom-spacer { height: 40rpx; }
 </style>

+ 112 - 81
cfc-frontend/pages/growth/growth-emotion-checkin.vue

@@ -1,10 +1,21 @@
 <template>
-  <scroll-view class="content" scroll-y>
+  <view class="page">
+    <!-- 顶部导航栏 -->
+    <view class="nav-bar">
+      <view class="nav-left" @tap="goBack">
+        <text class="nav-icon">‹</text>
+        <text class="nav-title">心情打卡</text>
+      </view>
+    </view>
+
+    <scroll-view class="content" scroll-y>
       <!-- 情绪选择 -->
       <view class="card">
-        <view class="card-accent"></view>
-        <view class="card-body">
+        <view class="card-header">
+          <text class="card-icon">💖</text>
           <text class="card-title">现在的心情</text>
+        </view>
+        <view class="card-body">
           <view class="emotion-grid">
             <view class="emotion-item" v-for="em in emotions" :key="em.type"
               :class="{ 'emotion-active': selectedEmotion === em.type }"
@@ -18,9 +29,11 @@
 
       <!-- 心情强度 -->
       <view class="card">
-        <view class="card-accent"></view>
-        <view class="card-body">
+        <view class="card-header">
+          <text class="card-icon">📊</text>
           <text class="card-title">心情强度</text>
+        </view>
+        <view class="card-body">
           <view class="slider-row">
             <text class="slider-min">微弱</text>
             <slider min="1" max="10" :value="intensityLevel" @change="function(e){intensityLevel=e.detail.value}" block-size="20" activeColor="#FF6B9D" />
@@ -32,27 +45,29 @@
 
       <!-- 文字记录 + 媒体附件 -->
       <view class="card">
-        <view class="card-accent"></view>
-        <view class="card-body">
+        <view class="card-header">
+          <text class="card-icon">📝</text>
           <text class="card-title">文字记录</text>
+        </view>
+        <view class="card-body">
           <textarea class="note-input" v-model="note" placeholder="今天发生了什么?说说你的感受..." maxlength="500" />
-          <!-- 并排两个按钮 + 展开区域 -->
+          <!-- 媒体工具栏 -->
           <view class="media-toolbar">
             <view class="tool-btn" @tap="choosePhoto">
               <text class="tool-icon">📷</text>
-              <text class="tool-label">上传照片</text>
+              <text class="tool-label">照片</text>
               <text class="tool-count" v-if="photos.length > 0">{{ photos.length }}/9</text>
             </view>
             <view class="tool-divider"></view>
             <view class="tool-btn" @tap="toggleRecord">
               <text class="tool-icon">🎙</text>
-              <text class="tool-label">{{ isRecording ? '录音中…' : (voicePath ? '已录制' : '录') }}</text>
+              <text class="tool-label">{{ isRecording ? '录音中…' : (voicePath ? '已录制' : '录音') }}</text>
               <text class="tool-count" v-if="isRecording">{{ recordSeconds }}s</text>
             </view>
           </view>
           <!-- 照片缩略图 -->
           <view class="photo-strip" v-if="photos.length > 0">
-            <view class="photo-item" v-for="(img, idx) in photos" :key="getPhotoKey(img, idx)" @tap="removePhoto(idx)">
+            <view class="photo-item" v-for="(img, idx) in photos" :key="getItemKey(img, idx)" @tap="removePhoto(idx)">
               <image :src="img" mode="aspectFill" class="photo-img"/>
               <view class="photo-delete-overlay">
                 <text class="delete-icon">×</text>
@@ -88,21 +103,29 @@
 
       <!-- 历史记录 -->
       <view class="card" v-if="history.length > 0">
-        <view class="card-accent"></view>
-        <view class="card-body">
+        <view class="card-header">
+          <text class="card-icon">📜</text>
           <text class="card-title">近期心情</text>
+        </view>
+        <view class="card-body">
           <view class="history-list">
             <view class="history-item" v-for="item in history" :key="item.id">
               <view class="history-left">
                 <text class="history-type">{{ emotionIcon(item.emotionType) }} {{ emotionLabel(item.emotionType) }}</text>
                 <text class="history-content">{{ item.note || '-' }}</text>
               </view>
-              <text class="history-date">{{ formatDate(item.createdAt) }}</text>
+              <view class="history-right">
+                <text class="history-date">{{ formatDate(item.createdAt) }}</text>
+              </view>
             </view>
           </view>
         </view>
       </view>
+
+      <!-- 底部留白 -->
+      <view class="bottom-spacer"></view>
     </scroll-view>
+  </view>
 </template>
 
 <script>
@@ -284,21 +307,17 @@ export default {
             voiceUrl: voiceUrl
           },
           header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
-          success: function(res) {
-            self.submitting = false
-            if (res.data && res.data.code === 200) {
-              uni.showToast({ title: '+5 心能量', icon: 'none' })
-              self.selectedEmotion = ''
-              self.intensityLevel = 5
-              self.note = ''
-              self.photos = []
-              self.voicePath = ''
-              self.recordSeconds = 0
-              self.loadHistory()
-            } else {
-              uni.showToast({ title: (res.data && res.data.message) || '提交失败', icon: 'none' })
-            }
-          },
+success: function(res) {
+              self.submitting = false
+              if (res.data && res.data.code === 200) {
+                uni.showToast({ title: '+5 心能量', icon: 'none' })
+                setTimeout(function() {
+                  uni.navigateBack()
+                }, 800)
+              } else {
+                uni.showToast({ title: (res.data && res.data.message) || '提交失败', icon: 'none' })
+              }
+            },
           fail: function() {
             self.submitting = false
             uni.showToast({ title: '网络错误', icon: 'none' })
@@ -314,71 +333,83 @@ export default {
 </script>
 
 <style scoped>
+/* ===== 页面基础 ===== */
 .page { min-height: 100vh; background: #FFF7ED; }
-.content { height: calc(100vh - 88rpx); }
-.card { margin: 16rpx 24rpx; background: #fff; border-radius: 20rpx; overflow: hidden; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); }
-.card-accent { height: 6rpx; background: linear-gradient(90deg, #FF6B9D, #F97316); }
-.card-body { padding: 28rpx; }
-.card-title { font-size: 30rpx; font-weight: bold; color: #333; margin-bottom: 20rpx; display: block; }
-.form-group { margin-bottom: 24rpx; }
-.form-label { font-size: 28rpx; color: #333; font-weight: 600; margin-bottom: 12rpx; display: block; }
+.nav-bar { height: 88rpx; padding: 24rpx 24rpx 0; display: flex; align-items: flex-end; }
+.nav-left { display: flex; align-items: center; gap: 10rpx; }
+.nav-icon { font-size: 48rpx; color: #333; line-height: 1; }
+.nav-title { font-size: 34rpx; font-weight: 600; color: #333; }
+.content { padding-bottom: 40rpx; }
+
+/* ===== 卡片通用 ===== */
+.card { margin: 16rpx 20rpx; background: #fff; border-radius: 20rpx; overflow: hidden; box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.05); }
+.card-header { display: flex; align-items: center; gap: 10rpx; padding: 20rpx 24rpx 8rpx; border-bottom: 1rpx solid #f5f5f5; background: linear-gradient(90deg, #FFF5FA, #fff); }
+.card-icon { font-size: 32rpx; }
+.card-title { font-size: 30rpx; font-weight: 600; color: #333; }
+.card-body { padding: 20rpx 24rpx 24rpx; }
 
-/* 情绪网格 */
-.emotion-grid { display: flex; flex-wrap: wrap; gap: 16rpx; }
-.emotion-item { width: calc(25% - 12rpx); display: flex; flex-direction: column; align-items: center; padding: 20rpx 0; border-radius: 16rpx; background: #FAFAFA; border: 2rpx solid transparent; }
-.emotion-active { background: #FFF0F5; border-color: #FF6B9D; }
-.emotion-icon { font-size: 48rpx; margin-bottom: 8rpx; }
+/* ===== 情绪网格 ===== */
+.emotion-grid { display: flex; flex-wrap: wrap; gap: 12rpx; }
+.emotion-item { width: calc(25% - 9rpx); display: flex; flex-direction: column; align-items: center; padding: 18rpx 0; border-radius: 14rpx; background: #FAFAFA; border: 2rpx solid transparent; transition: all 0.2s ease; }
+.emotion-active { background: #FFF0F5; border-color: #FF6B9D; box-shadow: 0 4rpx 12rpx rgba(255,107,157,0.15); }
+.emotion-icon { font-size: 44rpx; margin-bottom: 6rpx; }
 .emotion-label { font-size: 24rpx; color: #666; }
-.emotion-active .emotion-label { color: #FF6B9D; font-weight: bold; }
+.emotion-active .emotion-label { color: #FF6B9D; font-weight: 600; }
 
-/* 滑块 */
-.slider-row { display: flex; align-items: center; gap: 12rpx; }
-.slider-min { font-size: 24rpx; color: #999; flex-shrink: 0; }
-.slider-val { font-size: 28rpx; color: #FF6B9D; font-weight: bold; width: 40rpx; text-align: center; }
-.slider-max { font-size: 24rpx; color: #999; flex-shrink: 0; }
+/* ===== 滑块 ===== */
+.slider-row { display: flex; align-items: center; gap: 10rpx; }
+.slider-min { font-size: 22rpx; color: #999; flex-shrink: 0; }
+.slider-val { font-size: 26rpx; color: #FF6B9D; font-weight: 600; width: 36rpx; 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: 16rpx; padding: 20rpx; font-size: 28rpx; background: #FAFAFA; box-sizing: border-box; margin-bottom: 20rpx; }
+/* ===== 文本输入 ===== */
+.note-input { width: 100%; height: 140rpx; border: 1rpx solid #e8e8e8; border-radius: 14rpx; padding: 16rpx; font-size: 26rpx; background: #FAFAFA; box-sizing: border-box; font-family: inherit; margin-bottom: 16rpx; }
 
-/* 录入工具栏 */
-.media-toolbar { display: flex; align-items: center; padding: 24rpx 0; border-top: 1rpx solid #f0f0f0; }
-.tool-btn { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8rpx; padding: 16rpx 0; }
-.tool-icon { font-size: 48rpx; }
+/* ===== 媒体工具栏 ===== */
+.media-toolbar { display: flex; align-items: center; gap: 0; padding: 12rpx 0; }
+.tool-btn { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6rpx; padding: 16rpx 0; }
+.tool-icon { font-size: 42rpx; }
 .tool-label { font-size: 24rpx; color: #666; }
-.tool-count { font-size: 22rpx; color: #FF6B9D; font-weight: bold; }
-.tool-divider { width: 1rpx; height: 60rpx; background: #e8e8e8; }
+.tool-count { font-size: 20rpx; color: #FF6B9D; font-weight: 600; }
+.tool-divider { width: 1rpx; height: 56rpx; background: linear-gradient(180deg, transparent, #e8e8e8, transparent); }
 
-/* 照片缩略图条 */
-.photo-strip { display: flex; flex-wrap: wrap; gap: 12rpx; padding: 16rpx 0 4rpx; }
-.photo-item { position: relative; width: 140rpx; height: 140rpx; }
-.photo-img { width: 100%; height: 100%; border-radius: 12rpx; }
-.photo-delete-overlay { position: absolute; top: -6rpx; right: -6rpx; width: 36rpx; height: 36rpx; background: #EF4444; border-radius: 50%; display: flex; align-items: center; justify-content: center; box-shadow: 0 2rpx 6rpx rgba(239,68,68,0.3); z-index: 2; }
-.delete-icon { color: #fff; font-size: 24rpx; font-weight: bold; line-height: 36rpx; }
+/* ===== 照片缩略图 ===== */
+.photo-strip { display: flex; flex-wrap: wrap; gap: 10rpx; padding: 12rpx 0 4rpx; }
+.photo-item { position: relative; width: 132rpx; height: 132rpx; border-radius: 12rpx; overflow: hidden; }
+.photo-img { width: 100%; height: 100%; object-fit: cover; }
+.photo-delete-overlay { position: absolute; top: -6rpx; right: -6rpx; width: 32rpx; height: 32rpx; background: #EF4444; border-radius: 50%; display: flex; align-items: center; justify-content: center; box-shadow: 0 2rpx 8rpx rgba(239,68,68,0.3); z-index: 2; }
+.delete-icon { color: #fff; font-size: 22rpx; font-weight: bold; line-height: 32rpx; }
 
-/* 录音展开区 */
-.voice-strip { display: flex; align-items: center; justify-content: space-between; padding: 16rpx 0 4rpx; gap: 16rpx; }
-.waveform { display: flex; align-items: center; gap: 6rpx; height: 48rpx; flex: 1; }
-.wave-bar { width: 6rpx; height: 10rpx; background: #FF6B9D; border-radius: 3rpx; animation: wave-anim 0.8s ease-in-out infinite; }
-@keyframes wave-anim { 0%, 100% { height: 10rpx; } 50% { height: 36rpx; } }
-.voice-controls { display: flex; gap: 16rpx; }
-.mini-btn { padding: 12rpx 28rpx; border-radius: 24rpx; font-size: 26rpx; }
+/* ===== 录音展开区 ===== */
+.voice-strip { display: flex; align-items: center; justify-content: space-between; padding: 12rpx 0 4rpx; gap: 16rpx; }
+.waveform { display: flex; align-items: center; gap: 5rpx; height: 40rpx; flex: 1; }
+.wave-bar { width: 5rpx; height: 8rpx; background: #FF6B9D; border-radius: 3rpx; animation: wave-anim 0.8s ease-in-out infinite; }
+@keyframes wave-anim { 0%, 100% { height: 8rpx; } 50% { height: 32rpx; } }
+.voice-controls { display: flex; gap: 12rpx; }
+.mini-btn { padding: 10rpx 24rpx; border-radius: 22rpx; font-size: 24rpx; }
 .cancel-mini { background: #f0f0f0; color: #666; }
-.done-mini { background: #FF6B9D; color: #fff; font-weight: bold; }
-.voice-player { display: flex; align-items: center; gap: 12rpx; flex: 1; background: #FFF0F5; padding: 12rpx 24rpx; border-radius: 30rpx; }
-.play-icon { font-size: 28rpx; color: #FF6B9D; }
-.voice-duration { font-size: 26rpx; color: #666; }
-.delete-voice { width: 44rpx; height: 44rpx; display: flex; align-items: center; justify-content: center; color: #999; font-size: 32rpx; }
+.done-mini { background: #FF6B9D; color: #fff; font-weight: 600; }
 
-/* 提交按钮 */
-.submit-wrap { padding: 24rpx 24rpx 40rpx; }
-.submit-btn { background: linear-gradient(135deg, #FF6B9D, #F97316); color: #fff; text-align: center; padding: 24rpx 0; border-radius: 16rpx; font-size: 32rpx; font-weight: bold; }
+.voice-player { display: flex; align-items: center; gap: 10rpx; flex: 1; background: #FFF0F5; padding: 10rpx 20rpx; border-radius: 28rpx; }
+.play-icon { font-size: 26rpx; color: #FF6B9D; }
+.voice-duration { font-size: 24rpx; color: #666; }
+.delete-voice { width: 40rpx; height: 40rpx; display: flex; align-items: center; justify-content: center; color: #999; font-size: 28rpx; }
+
+/* ===== 提交按钮 ===== */
+.submit-wrap { padding: 24rpx 20rpx 32rpx; }
+.submit-btn { background: linear-gradient(135deg, #FF6B9D, #F97316); color: #fff; text-align: center; padding: 22rpx 0; border-radius: 14rpx; font-size: 30rpx; font-weight: 600; box-shadow: 0 6rpx 20rpx rgba(255,107,157,0.3); transition: all 0.2s ease; }
+.submit-btn:active { transform: scale(0.98); }
 .submitting { opacity: 0.6; }
 
-/* 历史记录 */
-.history-list { display: flex; flex-direction: column; gap: 16rpx; }
-.history-item { display: flex; justify-content: space-between; align-items: center; padding: 12rpx 0; border-bottom: 1rpx solid #f5f5f5; }
+/* ===== 历史记录 ===== */
+.history-list { display: flex; flex-direction: column; gap: 12rpx; }
+.history-item { display: flex; justify-content: space-between; align-items: center; padding: 14rpx 0; border-bottom: 1rpx solid #f5f5f5; }
 .history-left { flex: 1; min-width: 0; }
-.history-type { font-size: 28rpx; font-weight: 500; color: #333; display: block; }
+.history-type { font-size: 26rpx; font-weight: 500; color: #333; display: block; }
 .history-content { font-size: 24rpx; color: #888; display: block; margin-top: 4rpx; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
-.history-date { font-size: 24rpx; color: #999; flex-shrink: 0; margin-left: 16rpx; }
+.history-right { text-align: right; margin-left: 16rpx; flex-shrink: 0; }
+.history-date { font-size: 22rpx; color: #999; }
+
+/* ===== 底部留白 ===== */
+.bottom-spacer { height: 40rpx; }
 </style>

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

@@ -1,8 +1,20 @@
 <template>
-  <scroll-view class="content" scroll-y>
-      <!-- 睡眠表单 -->
+  <view class="page">
+    <!-- 顶部导航栏 -->
+    <view class="nav-bar">
+      <view class="nav-left" @tap="goBack">
+        <text class="nav-icon">‹</text>
+        <text class="nav-title">睡眠打卡</text>
+      </view>
+    </view>
+
+    <scroll-view class="content" scroll-y>
+      <!-- 睡眠时间 -->
       <view class="card">
-        <view class="card-accent"></view>
+        <view class="card-header">
+          <text class="card-icon">🌙</text>
+          <text class="card-title">睡眠时间</text>
+        </view>
         <view class="card-body">
           <view class="form-group">
             <text class="form-label">入睡时间 *</text>
@@ -16,6 +28,16 @@
               <view class="picker-display">{{ form.wakeTime || '选择醒来时间' }}</view>
             </picker>
           </view>
+        </view>
+      </view>
+
+      <!-- 睡眠质量 & 备注 -->
+      <view class="card">
+        <view class="card-header">
+          <text class="card-icon">⭐</text>
+          <text class="card-title">睡眠质量</text>
+        </view>
+        <view class="card-body">
           <view class="form-group">
             <text class="form-label">睡眠质量(1-10)</text>
             <view class="slider-row">
@@ -41,21 +63,29 @@
 
       <!-- 历史记录 -->
       <view class="card" v-if="history.length > 0">
-        <view class="card-accent"></view>
-        <view class="card-body">
+        <view class="card-header">
+          <text class="card-icon">📜</text>
           <text class="card-title">近期睡眠</text>
+        </view>
+        <view class="card-body">
           <view class="history-list">
             <view class="history-item" v-for="item in history" :key="item.id">
               <view class="history-left">
                 <text class="history-type">{{ item.sleepTime }} - {{ item.wakeTime }}</text>
                 <text class="history-content">{{ item.durationMinutes }}分钟 · 质量{{ item.qualityScore }}分</text>
               </view>
-              <text class="history-date">{{ formatDate(item.createdAt) }}</text>
+              <view class="history-right">
+                <text class="history-date">{{ formatDate(item.createdAt) }}</text>
+              </view>
             </view>
           </view>
         </view>
       </view>
+
+      <!-- 底部留白 -->
+      <view class="bottom-spacer"></view>
     </scroll-view>
+  </view>
 </template>
 
 <script>
@@ -135,18 +165,16 @@ export default {
           },
           header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
           success: function(res) {
-            self.submitting = false
-            if (res.data && res.data.code === 200) {
-              uni.showToast({ title: '打卡成功', icon: 'success' })
-              self.form.sleepTime = '21:00'
-              self.form.wakeTime = '07:00'
-              self.form.qualityScore = 7
-              self.form.remark = ''
-              self.loadHistory()
-            } else {
-              uni.showToast({ title: (res.data && res.data.message) || '打卡失败', icon: 'none' })
-            }
-          },
+              self.submitting = false
+              if (res.data && res.data.code === 200) {
+                uni.showToast({ title: '打卡成功', icon: 'success' })
+                setTimeout(function() {
+                  uni.navigateBack()
+                }, 800)
+              } else {
+                uni.showToast({ title: (res.data && res.data.message) || '打卡失败', icon: 'none' })
+              }
+            },
           fail: function() {
             self.submitting = false
             uni.showToast({ title: '网络错误', icon: 'none' })
@@ -162,27 +190,46 @@ export default {
 </script>
 
 <style scoped>
+/* ===== 页面基础 ===== */
 .page { min-height: 100vh; background: #FFF7ED; }
-.content { height: calc(100vh - 88rpx); }
-.card { margin: 16rpx 24rpx; background: #fff; border-radius: 20rpx; overflow: hidden; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); }
-.card-accent { height: 6rpx; background: linear-gradient(90deg, #FF8C42, #F97316); }
-.card-body { padding: 28rpx; }
-.card-title { font-size: 30rpx; font-weight: bold; color: #333; margin-bottom: 20rpx; display: block; }
-.form-group { margin-bottom: 24rpx; }
-.form-label { font-size: 28rpx; color: #333; font-weight: 600; margin-bottom: 12rpx; display: block; }
-.picker-display { border: 1rpx solid #ddd; border-radius: 16rpx; padding: 20rpx; font-size: 28rpx; background: #FAFAFA; color: #333; min-height: 40rpx; }
-.slider-row { display: flex; align-items: center; gap: 12rpx; }
-.slider-min { font-size: 24rpx; color: #999; flex-shrink: 0; }
-.slider-val { font-size: 28rpx; color: #FF8C42; font-weight: bold; width: 40rpx; text-align: center; }
-.slider-max { font-size: 24rpx; color: #999; flex-shrink: 0; }
-.form-input { width: 100%; height: 80rpx; border: 1rpx solid #e0e0e0; border-radius: 16rpx; padding: 0 20rpx; font-size: 28rpx; background: #FAFAFA; box-sizing: border-box; }
-.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: 32rpx; font-weight: bold; }
+.nav-bar { height: 88rpx; padding: 24rpx 24rpx 0; display: flex; align-items: flex-end; }
+.nav-left { display: flex; align-items: center; gap: 10rpx; }
+.nav-icon { font-size: 48rpx; color: #333; line-height: 1; }
+.nav-title { font-size: 34rpx; font-weight: 600; color: #333; }
+.content { padding-bottom: 40rpx; }
+
+/* ===== 卡片通用 ===== */
+.card { margin: 16rpx 20rpx; background: #fff; border-radius: 20rpx; overflow: hidden; box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.05); }
+.card-header { display: flex; align-items: center; gap: 10rpx; padding: 20rpx 24rpx 8rpx; border-bottom: 1rpx solid #f5f5f5; background: linear-gradient(90deg, #FFFAF0, #fff); }
+.card-icon { font-size: 32rpx; }
+.card-title { font-size: 30rpx; font-weight: 600; color: #333; }
+.card-body { padding: 20rpx 24rpx 24rpx; }
+
+/* ===== 表单 ===== */
+.form-group { margin-bottom: 20rpx; }
+.form-label { font-size: 26rpx; color: #333; font-weight: 500; margin-bottom: 10rpx; display: block; }
+.picker-display { border: 1rpx solid #e8e8e8; border-radius: 14rpx; padding: 18rpx; font-size: 26rpx; background: #FAFAFA; color: #333; min-height: 40rpx; }
+.slider-row { display: flex; align-items: center; gap: 10rpx; }
+.slider-min { font-size: 22rpx; color: #999; flex-shrink: 0; }
+.slider-val { font-size: 26rpx; color: #FF8C42; font-weight: 600; width: 36rpx; text-align: center; }
+.slider-max { font-size: 22rpx; color: #999; flex-shrink: 0; }
+.form-input { width: 100%; height: 76rpx; border: 1rpx solid #e8e8e8; border-radius: 14rpx; padding: 0 16rpx; font-size: 26rpx; background: #FAFAFA; box-sizing: border-box; }
+
+/* ===== 提交按钮 ===== */
+.submit-wrap { padding: 24rpx 20rpx 32rpx; }
+.submit-btn { background: linear-gradient(135deg, #FF8C42, #F97316); color: #fff; text-align: center; padding: 22rpx 0; border-radius: 14rpx; font-size: 30rpx; font-weight: 600; box-shadow: 0 6rpx 20rpx rgba(249,115,22,0.3); transition: all 0.2s ease; }
+.submit-btn:active { transform: scale(0.98); }
 .submitting { opacity: 0.6; }
-.history-list { display: flex; flex-direction: column; gap: 16rpx; }
-.history-item { display: flex; justify-content: space-between; align-items: center; padding: 12rpx 0; border-bottom: 1rpx solid #f5f5f5; }
+
+/* ===== 历史记录 ===== */
+.history-list { display: flex; flex-direction: column; gap: 12rpx; }
+.history-item { display: flex; justify-content: space-between; align-items: center; padding: 14rpx 0; border-bottom: 1rpx solid #f5f5f5; }
 .history-left { flex: 1; min-width: 0; }
-.history-type { font-size: 28rpx; font-weight: 500; color: #333; display: block; }
+.history-type { font-size: 26rpx; font-weight: 500; color: #333; display: block; }
 .history-content { font-size: 24rpx; color: #888; display: block; margin-top: 4rpx; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
-.history-date { font-size: 24rpx; color: #999; flex-shrink: 0; margin-left: 16rpx; }
+.history-right { text-align: right; margin-left: 16rpx; flex-shrink: 0; }
+.history-date { font-size: 22rpx; color: #999; }
+
+/* ===== 底部留白 ===== */
+.bottom-spacer { height: 40rpx; }
 </style>

+ 4 - 0
cfc-frontend/pages/guide/bind/invite.vue

@@ -114,7 +114,9 @@ export default {
       // 从服务器获取成长规划师信息
       uni.request({
         url: `${config.API_BASE_URL}/api/guide/bind/validate`,
+        method: 'POST',
         data: { token: this.inviteToken },
+        header: { Authorization: `Bearer ${uni.getStorageSync('token')}` },
         success: (res) => {
           if (res.data.code === 200) {
             this.guideInfo = res.data.data
@@ -126,6 +128,8 @@ export default {
       // 获取家庭列表
       uni.request({
         url: `${config.API_BASE_URL}/api/family/user/family-members`,
+        method: 'POST',
+        data: {},
         header: { Authorization: `Bearer ${uni.getStorageSync('token')}` },
         success: (res) => {
           if (res.data.code === 200) {

+ 10 - 7
cfc-frontend/pages/teacher/chat.vue

@@ -53,13 +53,16 @@ export default {
   methods: {
     async loadMessages() {
       try {
-        const res = await uni.request({
-          url: `${config.API_BASE_URL}/api/guide/messages/${this.conversationId}/reply`,
-          method: 'GET',
-          header: { Authorization: `Bearer ${uni.getStorageSync('token')}` }
-        })
-        if (res.data && res.data.data) {
-          this.messages = res.data.data
+        const res = await getGuideMessages()
+        if (res && res.code === 200 && res.data && res.data.records) {
+          this.messages = res.data.records.map((m) => {
+            var t = m.repliedAt || m.createdAt || ''
+            return {
+              content: m.replyContent || m.content,
+              time: t.slice(0, 19).replace('T', ' '),
+              isMine: !!(m.replyContent && m.replyContent.trim())
+            }
+          })
         }
       } catch (e) {
         console.error(e)

+ 3 - 2
cfc-frontend/pages/teacher/service-tasks.vue

@@ -100,10 +100,11 @@ export default {
 
         const res = await uni.request({
           url: `${config.API_BASE_URL}/api/dan-execution/guide/my-tasks`,
-          method: 'GET',
+          method: 'POST',
           data: { plannerId: plannerId },
           header: {
-            'Authorization': `Bearer ${uni.getStorageSync('token')}`
+            'Authorization': `Bearer ${uni.getStorageSync('token')}`,
+            'Content-Type': 'application/json'
           }
         })
 

+ 1 - 16
cfc-frontend/utils/api.js

@@ -2712,22 +2712,7 @@ export function getRenewalInfo() {
 }
 
 export function getChallengeProgress(memberId) {
-  return new Promise((resolve, reject) => {
-    const token = uni.getStorageSync('token')
-    uni.request({
-      url: config.API_BASE_URL + '/api/purchase/challenge/progress?memberId=' + memberId,
-      method: 'GET',
-      header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
-      success: (res) => {
-        if (res.data && res.data.code === 200) {
-          resolve(res.data)
-        } else {
-          reject(res.data || { code: -1, message: '获取闯关进度失败' })
-        }
-      },
-      fail: (err) => reject(err)
-    })
-  })
+  return request('/api/purchase/challenge/progress', 'POST', { memberId: memberId })
 }
 
 // ── 管家自助选择 ──