Browse Source

refactor(sleep-checkin): 重新设计并美化睡眠打卡页

- 修复睡眠质量滑条无法改分:删除原生 slider 内联函数绑定,改为 1-10 分点选评分格(@tap),时间选择器同样改正式方法
- 新增夜间主题头部:渐变星空+月亮动效、当日日期、实时睡眠时长、8小时建议刻度进度条、入睡/醒来时间轴
- 睡眠质量卡:表情+分数+等级徽章+文案;评分格按 差/一般/良好/很好 分档着色
- 新增睡眠情况快速标签(选填多选,写入备注)、备注字数统计、靛蓝渐变提交按钮
- 历史记录重构:图标+时间区间+时长+分档质量条,日期统一 yyyy-MM-dd,头部汇总平均时长/质量
- 移除冗余自定义导航栏(原生标题已有),改页面自然滚动;打卡成功后本地插入记录刷新历史,避免重复请求列表接口
E2E Test Bot 6 days ago
parent
commit
83b37759c
1 changed files with 671 additions and 138 deletions
  1. 671 138
      cfc-frontend/pages/growth/sleep-checkin.vue

+ 671 - 138
cfc-frontend/pages/growth/sleep-checkin.vue

@@ -1,82 +1,172 @@
 <template>
   <view class="page">
-    <!-- 顶部导航栏 -->
-    <view class="nav-bar">
-      <view class="nav-left" @tap="goBack">
-        <text class="nav-icon">‹</text>
-        <text class="nav-title">{{ todayText }}</text>
+    <view class="content">
+
+      <!-- ===== 夜间主题头部:日期 / 本次时长 / 入睡-醒来时间轴 ===== -->
+      <view class="hero">
+        <view class="hero-glow"></view>
+        <view class="hero-stars">
+          <view class="star" v-for="s in stars" :key="s.id"
+            :style="{ left: s.left, top: s.top, width: s.size, height: s.size, opacity: s.opacity, animationDelay: s.delay }"></view>
+        </view>
+        <view class="hero-moon">🌙</view>
+
+        <view class="hero-top">
+          <view class="hero-chip">
+            <text class="hero-chip-dot"></text>
+            <text class="hero-chip-text">今日睡眠打卡</text>
+          </view>
+          <text class="hero-date">{{ todayDate }} {{ todayWeek }}</text>
+        </view>
+
+        <view class="hero-duration">
+          <text class="hero-duration-num">{{ durationText }}</text>
+          <text class="hero-duration-label">本次睡眠时长</text>
+        </view>
+
+        <view class="hero-track">
+          <view class="hero-track-bar">
+            <view class="hero-track-fill" :style="{ width: durationPercent + '%' }"></view>
+            <view class="hero-track-mark"></view>
+          </view>
+          <view class="hero-ends">
+            <view class="hero-end">
+              <text class="hero-end-icon">🌙</text>
+              <text class="hero-end-time">{{ form.sleepTime || '--:--' }}</text>
+              <text class="hero-end-label">入睡</text>
+            </view>
+            <view class="hero-end hero-end-right">
+              <text class="hero-end-icon">☀️</text>
+              <text class="hero-end-time">{{ form.wakeTime || '--:--' }}</text>
+              <text class="hero-end-label">醒来</text>
+            </view>
+          </view>
+        </view>
+
+        <view class="hero-tip">💡 建议青少年每天睡足 8-10 小时</view>
       </view>
-    </view>
 
-    <scroll-view class="content" scroll-y>
-      <!-- 睡眠时间 -->
+      <!-- ===== 睡眠时间 ===== -->
       <view class="card">
-        <view class="card-header">
-          <text class="card-icon">🌙</text>
+        <view class="card-head">
+          <view class="card-head-icon"><text>🛏️</text></view>
           <text class="card-title">睡眠时间</text>
+          <text class="card-hint">点击卡片选择</text>
         </view>
         <view class="card-body">
-          <view class="form-group">
-            <text class="form-label">入睡时间 *</text>
-            <picker mode="time" :value="form.sleepTime" @change="function(e){form.sleepTime=e.detail.value}">
-              <view class="picker-display">{{ form.sleepTime || '选择入睡时间' }}</view>
+          <view class="time-row">
+            <picker mode="time" :value="form.sleepTime" @change="onSleepTimeChange">
+              <view class="time-tile">
+                <text class="time-tile-icon">🌙</text>
+                <text class="time-tile-label">入睡时间</text>
+                <text class="time-tile-value">{{ form.sleepTime || '请选择' }}</text>
+              </view>
             </picker>
-          </view>
-          <view class="form-group">
-            <text class="form-label">醒来时间 *</text>
-            <picker mode="time" :value="form.wakeTime" @change="function(e){form.wakeTime=e.detail.value}">
-              <view class="picker-display">{{ form.wakeTime || '选择醒来时间' }}</view>
+            <view class="time-arrow"><text>→</text></view>
+            <picker mode="time" :value="form.wakeTime" @change="onWakeTimeChange">
+              <view class="time-tile time-tile-sun">
+                <text class="time-tile-icon">☀️</text>
+                <text class="time-tile-label">醒来时间</text>
+                <text class="time-tile-value">{{ form.wakeTime || '请选择' }}</text>
+              </view>
             </picker>
           </view>
         </view>
       </view>
 
-      <!-- 睡眠质量 & 备注 -->
+      <!-- ===== 睡眠质量(点选打分,替代滑动条) ===== -->
       <view class="card">
-        <view class="card-header">
-          <text class="card-icon">⭐</text>
+        <view class="card-head">
+          <view class="card-head-icon"><text>⭐</text></view>
           <text class="card-title">睡眠质量</text>
+          <text class="card-hint">1-10 分</text>
         </view>
         <view class="card-body">
-          <view class="form-group">
-            <text class="form-label">睡眠质量(1-10)</text>
-            <view class="slider-row">
-              <text class="slider-min">差</text>
-              <slider min="1" max="10" :value="form.qualityScore" @change="function(e){form.qualityScore=e.detail.value}" block-size="20" activeColor="#FF8C42" />
-              <text class="slider-val">{{ form.qualityScore }}</text>
-              <text class="slider-max">好</text>
+          <view class="quality-head">
+            <view class="quality-face">{{ qualityFace }}</view>
+            <view class="quality-info">
+              <view class="quality-score-row">
+                <text class="quality-score">{{ form.qualityScore }}</text>
+                <text class="quality-unit">分</text>
+                <text class="quality-badge" :class="'quality-badge quality-badge-' + qualityTier">{{ qualityLabel }}</text>
+              </view>
+              <text class="quality-desc">{{ qualityDesc }}</text>
+            </view>
+          </view>
+
+          <view class="q-chips">
+            <view class="q-chip" v-for="o in qualityOptions" :key="o.v"
+              :class="'q-chip q-chip-' + o.tier + (form.qualityScore === o.v ? ' q-chip-active' : '')"
+              @tap="setQuality(o.v)">
+              <text>{{ o.v }}</text>
             </view>
           </view>
-          <view class="form-group">
-            <text class="form-label">备注(选填)</text>
-            <input class="form-input" v-model="form.remark" placeholder="睡眠感受..." />
+
+          <view class="q-scale">
+            <text class="q-scale-end">1 · 很差</text>
+            <text class="q-scale-mid">点击数字打分</text>
+            <text class="q-scale-end">10 · 很好</text>
+          </view>
+        </view>
+      </view>
+
+      <!-- ===== 快速标签(选填,将写入备注) ===== -->
+      <view class="card">
+        <view class="card-head">
+          <view class="card-head-icon"><text>🏷️</text></view>
+          <text class="card-title">睡眠情况</text>
+          <text class="card-hint">选填 · 可多选</text>
+        </view>
+        <view class="card-body">
+          <view class="tag-cloud">
+            <view class="s-tag" v-for="t in quickTags" :key="t"
+              :class="selectedTags.indexOf(t) > -1 ? 's-tag s-tag-active' : 's-tag'"
+              @tap="toggleTag(t)">
+              <text>{{ t }}</text>
+            </view>
           </view>
         </view>
       </view>
 
-      <!-- 提交按钮 -->
+      <!-- ===== 备注 ===== -->
+      <view class="card">
+        <view class="card-head">
+          <view class="card-head-icon"><text>📝</text></view>
+          <text class="card-title">备注</text>
+          <text class="card-hint">{{ form.remark.length }}/100</text>
+        </view>
+        <view class="card-body">
+          <textarea class="remark-input" v-model="form.remark"
+            placeholder="昨晚睡得怎么样?有什么想记录的…" placeholder-class="ph-gray" maxlength="100" />
+        </view>
+      </view>
+
+      <!-- ===== 提交按钮 ===== -->
       <view class="submit-wrap">
-        <view class="submit-btn" :class="{ 'submitting': submitting }" @tap="doCheckin">
-          <text>{{ submitting ? '提交中...' : '✓ 完成打卡' }}</text>
+        <view class="submit-btn" :class="submitting ? 'submit-btn submitting' : 'submit-btn'" @tap="doCheckin">
+          <text class="submit-text">{{ submitting ? '打卡中...' : '完成打卡' }}</text>
         </view>
       </view>
 
-      <!-- 历史记录 -->
+      <!-- ===== 历史记录 ===== -->
       <view class="card" v-if="history.length > 0">
-        <view class="card-header">
-          <text class="card-icon">📜</text>
+        <view class="card-head">
+          <view class="card-head-icon"><text>📜</text></view>
           <text class="card-title">近期睡眠</text>
+          <text class="card-hint" v-if="avgDurationText">平均 {{ avgDurationText }} · {{ avgQuality }} 分</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>
-              <view class="history-right">
-                <text class="history-date">{{ formatDate(item.createdAt) }}</text>
+              <view class="history-badge"><text class="history-badge-icon">🌙</text></view>
+              <view class="history-main">
+                <text class="history-line1">{{ item._range }}</text>
+                <text class="history-line2">{{ item._duration }} · 质量 {{ item.qualityScore || '-' }} 分</text>
+                <view class="qbar">
+                  <view class="qbar-fill" :style="{ width: item._percent + '%', background: item._barBg }"></view>
+                </view>
               </view>
+              <text class="history-date">{{ formatDate(item.createdAt) }}</text>
             </view>
           </view>
         </view>
@@ -84,13 +174,13 @@
 
       <!-- 底部留白 -->
       <view class="bottom-spacer"></view>
-    </scroll-view>
+    </view>
   </view>
 </template>
 
 <script>
 import config from '../../config'
-import { parseDate } from '../../utils/format.js'
+import { formatDate } from '../../utils/format.js'
 
 export default {
   data() {
@@ -99,7 +189,41 @@ export default {
       taskId: null,
       submitting: false,
       history: [],
-      todayText: '',
+      todayDate: '',
+      todayWeek: '',
+      stars: [
+        { id: 's1', left: '6%', top: '16%', size: '8rpx', opacity: 0.9, delay: '0s' },
+        { id: 's2', left: '16%', top: '42%', size: '5rpx', opacity: 0.6, delay: '0.8s' },
+        { id: 's3', left: '26%', top: '12%', size: '6rpx', opacity: 0.75, delay: '1.6s' },
+        { id: 's4', left: '38%', top: '46%', size: '4rpx', opacity: 0.5, delay: '0.4s' },
+        { id: 's5', left: '48%', top: '18%', size: '5rpx', opacity: 0.65, delay: '2.2s' }
+      ],
+      quickTags: ['入睡困难', '夜醒', '多梦', '打鼾', '早醒', '赖床', '熬夜', '午睡过长'],
+      selectedTags: [],
+      qualityOptions: [
+        { v: 1, tier: 'poor' },
+        { v: 2, tier: 'poor' },
+        { v: 3, tier: 'poor' },
+        { v: 4, tier: 'fair' },
+        { v: 5, tier: 'fair' },
+        { v: 6, tier: 'good' },
+        { v: 7, tier: 'good' },
+        { v: 8, tier: 'great' },
+        { v: 9, tier: 'great' },
+        { v: 10, tier: 'great' }
+      ],
+      qualityMap: {
+        1: { face: '😣', label: '很差', tier: 'poor', desc: '几乎整晚没睡好,白天完全不在状态' },
+        2: { face: '😣', label: '很差', tier: 'poor', desc: '睡得很浅,反复醒,醒来很累' },
+        3: { face: '😕', label: '较差', tier: 'poor', desc: '睡得不太踏实,精神差一些' },
+        4: { face: '😕', label: '偏差', tier: 'fair', desc: '将就睡了一晚,不算安稳' },
+        5: { face: '😐', label: '一般', tier: 'fair', desc: '不好不差,勉强够用' },
+        6: { face: '🙂', label: '尚可', tier: 'good', desc: '睡得还行,基本恢复精力' },
+        7: { face: '🙂', label: '不错', tier: 'good', desc: '睡得比较踏实,状态正常' },
+        8: { face: '😊', label: '良好', tier: 'great', desc: '睡得很沉,醒来神清气爽' },
+        9: { face: '😄', label: '很好', tier: 'great', desc: '一觉到天亮,几乎不想起床' },
+        10: { face: '🤩', label: '极好', tier: 'great', desc: '完美的一觉,元气满满' }
+      },
       form: {
         sleepTime: '21:00',
         wakeTime: '07:00',
@@ -108,9 +232,70 @@ export default {
       }
     }
   },
-  onLoad: function(options) {
+  computed: {
+    // 本次睡眠时长(分钟),跨午夜自动换算
+    durationMinutes: function () {
+      var s = this.form.sleepTime
+      var w = this.form.wakeTime
+      if (!s || !w) return 0
+      var sp = String(s).split(':')
+      var wp = String(w).split(':')
+      var sm = parseInt(sp[0]) * 60 + parseInt(sp[1])
+      var wm = parseInt(wp[0]) * 60 + parseInt(wp[1])
+      if (isNaN(sm) || isNaN(wm)) return 0
+      return wm > sm ? wm - sm : (24 * 60 - sm + wm)
+    },
+    durationText: function () {
+      return this.durationFromMinutes(this.durationMinutes)
+    },
+    // 相对 10 小时的睡眠进度百分比
+    durationPercent: function () {
+      if (!this.durationMinutes) return 0
+      return Math.min(100, Math.round(this.durationMinutes / 600 * 100))
+    },
+    qualityFace: function () {
+      var m = this.qualityMap[this.form.qualityScore]
+      return m ? m.face : '🙂'
+    },
+    qualityLabel: function () {
+      var m = this.qualityMap[this.form.qualityScore]
+      return m ? m.label : '一般'
+    },
+    qualityTier: function () {
+      var m = this.qualityMap[this.form.qualityScore]
+      return m ? m.tier : 'fair'
+    },
+    qualityDesc: function () {
+      var m = this.qualityMap[this.form.qualityScore]
+      return m ? m.desc : ''
+    },
+    avgDurationText: function () {
+      var total = 0
+      var count = 0
+      this.history.forEach(function (h) {
+        if (h.durationMinutes) { total += h.durationMinutes; count++ }
+      })
+      if (!count) return ''
+      return this.durationFromMinutes(Math.round(total / count))
+    },
+    avgQuality: function () {
+      var total = 0
+      var count = 0
+      this.history.forEach(function (h) {
+        if (h.qualityScore) { total += h.qualityScore; count++ }
+      })
+      if (!count) return 0
+      return (total / count).toFixed(1)
+    }
+  },
+  onLoad: function (options) {
     this.taskId = options.taskId || null
-    this.todayText = this.getToday()
+    var d = new Date()
+    var weekdays = ['日', '一', '二', '三', '四', '五', '六']
+    var m2 = ('0' + (d.getMonth() + 1)).slice(-2)
+    var day = ('0' + d.getDate()).slice(-2)
+    this.todayDate = d.getFullYear() + '-' + m2 + '-' + day
+    this.todayWeek = '周' + weekdays[d.getDay()]
     var role = uni.getStorageSync('currentRole') || uni.getStorageSync('role') || 'parent'
     if (role !== 'child') {
       this.memberId = uni.getStorageSync('currentChildId') || uni.getStorageSync('currentMemberId') || uni.getStorageSync('userId') || null
@@ -118,21 +303,31 @@ export default {
     this.loadHistory()
   },
   methods: {
-    getToday: function() {
-      var d = new Date()
-      var weekdays = ['日', '一', '二', '三', '四', '五', '六']
-      return d.getFullYear() + '年' + (d.getMonth() + 1) + '月' + d.getDate() + '日 周' + weekdays[d.getDay()]
-    },
-    goBack: function() { uni.navigateBack() },
-    formatDate: function(dateStr) {
-      if (!dateStr) return ''
-      var d = parseDate(dateStr)
-      if (!d) return dateStr
-      var m2 = (d.getMonth() + 1).toString().padStart(2, '0')
-      var day = d.getDate().toString().padStart(2, '0')
-      return m2 + '-' + day
-    },
-    loadHistory: function() {
+    onSleepTimeChange: function (e) {
+      this.form.sleepTime = e.detail.value
+    },
+    onWakeTimeChange: function (e) {
+      this.form.wakeTime = e.detail.value
+    },
+    setQuality: function (score) {
+      this.form.qualityScore = score
+    },
+    toggleTag: function (tag) {
+      var idx = this.selectedTags.indexOf(tag)
+      if (idx > -1) {
+        this.selectedTags.splice(idx, 1)
+      } else {
+        this.selectedTags.push(tag)
+      }
+    },
+    durationFromMinutes: function (mins) {
+      if (!mins || mins <= 0) return '--'
+      var h = Math.floor(mins / 60)
+      var m = mins % 60
+      if (h > 0) return h + ' 小时' + (m > 0 ? ' ' + m + ' 分' : '')
+      return m + ' 分钟'
+    },
+    loadHistory: function () {
       var self = this
       var token = uni.getStorageSync('token')
       if (!token || !self.memberId) return
@@ -141,60 +336,83 @@ export default {
         method: 'POST',
         data: { memberId: self.memberId },
         header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
-        success: function(res) {
+        success: function (res) {
           if (res.data && res.data.code === 200 && res.data.data) {
-            self.history = res.data.data.slice(0, 5)
+            var arr = Array.isArray(res.data.data) ? res.data.data : []
+            self.history = arr.slice(0, 5).map(function (item) {
+              self.decorateHistoryItem(item)
+              return item
+            })
           }
         }
       })
     },
-    async doCheckin() {
+    // 预计算列表渲染字段(避免模板中调用带参方法)
+    decorateHistoryItem: function (item) {
+      var score = item.qualityScore || 0
+      item._range = (item.sleepTime ? String(item.sleepTime).slice(0, 5) : '--:--') +
+        ' - ' + (item.wakeTime ? String(item.wakeTime).slice(0, 5) : '--:--')
+      item._duration = this.durationFromMinutes(item.durationMinutes)
+      item._percent = Math.min(100, Math.max(0, Math.round(score * 10)))
+      if (score >= 8) item._barBg = 'linear-gradient(90deg,#818CF8,#6366F1)'
+      else if (score >= 6) item._barBg = 'linear-gradient(90deg,#34D399,#10B981)'
+      else if (score >= 4) item._barBg = 'linear-gradient(90deg,#FBBF24,#F59E0B)'
+      else item._barBg = 'linear-gradient(90deg,#FB7185,#EF4444)'
+    },
+    buildRemark: function () {
+      var remark = this.form.remark ? this.form.remark.trim() : ''
+      var tagText = this.selectedTags.join(',')
+      if (tagText && remark) return tagText + ',' + remark
+      if (tagText) return tagText
+      return remark
+    },
+    doCheckin: function () {
       var self = this
+      if (self.submitting) return
       if (!self.form.sleepTime) { uni.showToast({ title: '请选择入睡时间', icon: 'none' }); return }
       if (!self.form.wakeTime) { uni.showToast({ title: '请选择醒来时间', icon: 'none' }); return }
       if (self.form.sleepTime === self.form.wakeTime) { uni.showToast({ title: '入睡和醒来时间不能相同', icon: 'none' }); return }
+      if (!self.form.qualityScore) { uni.showToast({ title: '请选择睡眠质量', icon: 'none' }); return }
       if (!self.memberId) { uni.showToast({ title: '成员ID无效', icon: 'none' }); return }
       self.submitting = true
-      try {
-        var sleepParts = self.form.sleepTime.split(':')
-        var wakeParts = self.form.wakeTime.split(':')
-        var sleepMin = parseInt(sleepParts[0]) * 60 + parseInt(sleepParts[1])
-        var wakeMin = parseInt(wakeParts[0]) * 60 + parseInt(wakeParts[1])
-        var duration = wakeMin > sleepMin ? wakeMin - sleepMin : (24 * 60 - sleepMin + wakeMin)
-        var token = uni.getStorageSync('token')
-        uni.request({
-          url: config.API_BASE_URL + '/api/health/sleep/create',
-          method: 'POST',
-          data: {
-            memberId: self.memberId,
-            taskId: self.taskId || undefined,
-            sleepTime: self.form.sleepTime,
-            wakeTime: self.form.wakeTime,
-            durationMinutes: duration,
-            qualityScore: self.form.qualityScore,
-            remark: self.form.remark || ''
-          },
-          header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
-          success: function(res) {
-              self.submitting = false
-              if (res.data && res.data.code === 200) {
-                uni.showToast({ title: self.taskId ? '打卡成功,任务已完成' : '打卡成功', 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' })
+      var token = uni.getStorageSync('token')
+      uni.request({
+        url: config.API_BASE_URL + '/api/health/sleep/create',
+        method: 'POST',
+        data: {
+          memberId: self.memberId,
+          taskId: self.taskId || undefined,
+          sleepTime: self.form.sleepTime,
+          wakeTime: self.form.wakeTime,
+          durationMinutes: self.durationMinutes,
+          qualityScore: self.form.qualityScore,
+          remark: self.buildRemark()
+        },
+        header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
+        success: function (res) {
+          self.submitting = false
+          if (res.data && res.data.code === 200) {
+            uni.showToast({ title: self.taskId ? '打卡成功,任务已完成' : '打卡成功', icon: 'success' })
+            // 本地插入新记录(复用已返回的数据,不重复请求列表接口)
+            var created = res.data.data
+            if (created) {
+              self.decorateHistoryItem(created)
+              var list = self.history.slice()
+              list.unshift(created)
+              self.history = list.slice(0, 5)
+            }
+            setTimeout(function () {
+              uni.navigateBack()
+            }, 800)
+          } else {
+            uni.showToast({ title: (res.data && res.data.message) || '打卡失败', icon: 'none' })
           }
-        })
-      } catch (e) {
-        self.submitting = false
-        uni.showToast({ title: '提交失败', icon: 'none' })
-      }
+        },
+        fail: function () {
+          self.submitting = false
+          uni.showToast({ title: '网络错误', icon: 'none' })
+        }
+      })
     }
   }
 }
@@ -202,45 +420,360 @@ export default {
 
 <style scoped>
 /* ===== 页面基础 ===== */
-.page { min-height: 100vh; background: #FFF7ED; }
-.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; }
+.page { min-height: 100vh; background: #EEF1F8; }
+.content { padding: 20rpx 0 0; }
+
+/* ===== 夜间主题头部 ===== */
+.hero {
+  position: relative;
+  margin: 8rpx 24rpx 8rpx;
+  padding: 36rpx 32rpx 32rpx;
+  border-radius: 32rpx;
+  overflow: hidden;
+  background: linear-gradient(155deg, #3B4A8C 0%, #2A3470 45%, #1B2350 100%);
+  box-shadow: 0 16rpx 40rpx rgba(27, 35, 80, 0.35);
+  color: #fff;
+}
+.hero-glow {
+  position: absolute;
+  top: -120rpx;
+  right: -80rpx;
+  width: 320rpx;
+  height: 320rpx;
+  border-radius: 50%;
+  background: radial-gradient(circle, rgba(255, 209, 102, 0.4) 0%, rgba(255, 209, 102, 0) 70%);
+}
+.hero-moon {
+  position: absolute;
+  top: 20rpx;
+  right: 32rpx;
+  font-size: 72rpx;
+  line-height: 1;
+  animation: moon-float 4s ease-in-out infinite;
+}
+@keyframes moon-float {
+  0%, 100% { transform: translateY(0); }
+  50% { transform: translateY(-10rpx); }
+}
+.star {
+  position: absolute;
+  border-radius: 50%;
+  background: #FFE9A8;
+  animation: star-twinkle 2.4s ease-in-out infinite;
+}
+@keyframes star-twinkle {
+  0%, 100% { opacity: 0.2; transform: scale(0.8); }
+  50% { opacity: 0.95; transform: scale(1.15); }
+}
+.hero-top {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  position: relative;
+  z-index: 1;
+}
+.hero-chip {
+  display: flex;
+  align-items: center;
+  gap: 10rpx;
+  padding: 8rpx 20rpx;
+  border-radius: 999rpx;
+  background: rgba(255, 255, 255, 0.14);
+}
+.hero-chip-dot {
+  width: 10rpx;
+  height: 10rpx;
+  border-radius: 50%;
+  background: #FFD166;
+  box-shadow: 0 0 12rpx rgba(255, 209, 102, 0.9);
+}
+.hero-chip-text { font-size: 22rpx; color: #fff; }
+.hero-date { font-size: 24rpx; color: rgba(255, 255, 255, 0.75); }
+.hero-duration {
+  position: relative;
+  z-index: 1;
+  margin-top: 24rpx;
+}
+.hero-duration-num {
+  font-size: 56rpx;
+  font-weight: 800;
+  color: #fff;
+  line-height: 1.1;
+}
+.hero-duration-label {
+  display: block;
+  font-size: 24rpx;
+  color: rgba(255, 255, 255, 0.7);
+  margin-top: 6rpx;
+}
+.hero-track {
+  position: relative;
+  z-index: 1;
+  margin-top: 28rpx;
+}
+.hero-track-bar {
+  position: relative;
+  height: 12rpx;
+  border-radius: 999rpx;
+  background: rgba(255, 255, 255, 0.16);
+}
+.hero-track-fill {
+  height: 100%;
+  border-radius: 999rpx;
+  background: linear-gradient(90deg, #FFD166, #FF9F68);
+  transition: width 0.3s ease;
+}
+.hero-track-mark {
+  position: absolute;
+  left: 80%;
+  top: -4rpx;
+  width: 4rpx;
+  height: 20rpx;
+  border-radius: 2rpx;
+  background: rgba(255, 255, 255, 0.85);
+}
+.hero-ends {
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
+  margin-top: 16rpx;
+}
+.hero-end {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start;
+  gap: 4rpx;
+}
+.hero-end-right { align-items: flex-end; }
+.hero-end-icon { font-size: 32rpx; }
+.hero-end-time { font-size: 34rpx; font-weight: 700; color: #fff; }
+.hero-end-label { font-size: 20rpx; color: rgba(255, 255, 255, 0.6); }
+.hero-tip {
+  position: relative;
+  z-index: 1;
+  margin-top: 24rpx;
+  padding-top: 20rpx;
+  border-top: 1rpx solid rgba(255, 255, 255, 0.12);
+  font-size: 22rpx;
+  color: rgba(255, 255, 255, 0.65);
+}
 
 /* ===== 卡片通用 ===== */
-.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; }
+.card {
+  margin: 24rpx 24rpx 0;
+  background: #fff;
+  border-radius: 28rpx;
+  box-shadow: 0 8rpx 28rpx rgba(15, 23, 42, 0.05);
+}
+.card-head {
+  display: flex;
+  align-items: center;
+  gap: 14rpx;
+  padding: 26rpx 28rpx 14rpx;
+}
+.card-head-icon {
+  width: 56rpx;
+  height: 56rpx;
+  border-radius: 18rpx;
+  background: linear-gradient(135deg, #EEF2FF, #E0E7FF);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 30rpx;
+  flex-shrink: 0;
+}
+.card-title {
+  flex: 1;
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #1E293B;
+}
+.card-hint { font-size: 22rpx; color: #94A3B8; }
+.card-body { padding: 4rpx 28rpx 30rpx; }
+
+/* ===== 睡眠时间 ===== */
+.time-row { display: flex; align-items: stretch; gap: 16rpx; }
+.time-tile {
+  flex: 1;
+  background: linear-gradient(160deg, #F8FAFF, #F1F5FF);
+  border: 2rpx solid #E6EBF5;
+  border-radius: 24rpx;
+  padding: 24rpx 16rpx;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 8rpx;
+  transition: all 0.2s ease;
+}
+.time-tile:active { transform: scale(0.97); border-color: #C7D2FE; }
+.time-tile-icon { font-size: 40rpx; line-height: 1; }
+.time-tile-label { font-size: 22rpx; color: #94A3B8; }
+.time-tile-value { font-size: 40rpx; font-weight: 700; color: #1E293B; }
+.time-arrow {
+  align-self: center;
+  font-size: 32rpx;
+  color: #94A3B8;
+  font-weight: 700;
+  flex-shrink: 0;
+}
+
+/* ===== 睡眠质量 ===== */
+.quality-head {
+  display: flex;
+  align-items: center;
+  gap: 24rpx;
+  background: linear-gradient(160deg, #F8FAFF, #F1F5FF);
+  border-radius: 24rpx;
+  padding: 26rpx 24rpx;
+  margin-bottom: 26rpx;
+}
+.quality-face {
+  width: 116rpx;
+  height: 116rpx;
+  border-radius: 50%;
+  background: #fff;
+  box-shadow: 0 6rpx 18rpx rgba(79, 70, 229, 0.12);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 62rpx;
+  flex-shrink: 0;
+}
+.quality-info { flex: 1; min-width: 0; }
+.quality-score-row { display: flex; align-items: baseline; }
+.quality-score {
+  font-size: 54rpx;
+  font-weight: 800;
+  color: #1E293B;
+  line-height: 1;
+}
+.quality-unit { font-size: 24rpx; color: #94A3B8; margin-left: 6rpx; }
+.quality-badge {
+  margin-left: 14rpx;
+  padding: 6rpx 18rpx;
+  border-radius: 999rpx;
+  font-size: 22rpx;
+  font-weight: 600;
+}
+.quality-badge-poor { background: #FEE2E2; color: #DC2626; }
+.quality-badge-fair { background: #FEF3C7; color: #B45309; }
+.quality-badge-good { background: #D1FAE5; color: #047857; }
+.quality-badge-great { background: #E0E7FF; color: #4338CA; }
+.quality-desc { display: block; font-size: 24rpx; color: #64748B; margin-top: 12rpx; }
+
+.q-chips { display: flex; flex-wrap: wrap; gap: 14rpx; }
+.q-chip {
+  flex: 1;
+  height: 78rpx;
+  border-radius: 22rpx;
+  background: #F8FAFC;
+  border: 2rpx solid #EDF1F7;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  transition: all 0.18s ease;
+}
+.q-chip text { font-size: 30rpx; font-weight: 600; color: #64748B; }
+.q-chip:active { transform: scale(0.94); }
+.q-chip-active { border-color: transparent; transform: translateY(-4rpx); }
+.q-chip-active text { color: #fff; }
+.q-chip-poor text { color: #F87171; }
+.q-chip-fair text { color: #D97706; }
+.q-chip-good text { color: #059669; }
+.q-chip-great text { color: #4F46E5; }
+.q-chip-poor.q-chip-active { background: linear-gradient(135deg, #FB7185, #EF4444); box-shadow: 0 8rpx 20rpx rgba(239, 68, 68, 0.3); }
+.q-chip-fair.q-chip-active { background: linear-gradient(135deg, #FBBF24, #F59E0B); box-shadow: 0 8rpx 20rpx rgba(245, 158, 11, 0.3); }
+.q-chip-good.q-chip-active { background: linear-gradient(135deg, #34D399, #10B981); box-shadow: 0 8rpx 20rpx rgba(16, 185, 129, 0.3); }
+.q-chip-great.q-chip-active { background: linear-gradient(135deg, #818CF8, #6366F1); box-shadow: 0 8rpx 20rpx rgba(99, 102, 241, 0.3); }
+
+.q-scale {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-top: 14rpx;
+}
+.q-scale-end { font-size: 22rpx; color: #94A3B8; }
+.q-scale-mid { font-size: 20rpx; color: #CBD5E1; }
+
+/* ===== 快速标签 ===== */
+.tag-cloud { display: flex; flex-wrap: wrap; gap: 16rpx; }
+.s-tag {
+  padding: 14rpx 26rpx;
+  border-radius: 999rpx;
+  background: #F8FAFC;
+  border: 2rpx solid #EDF1F7;
+  transition: all 0.18s ease;
+}
+.s-tag text { font-size: 26rpx; color: #475569; }
+.s-tag:active { transform: scale(0.96); }
+.s-tag-active { background: #EEF2FF; border-color: #6366F1; }
+.s-tag-active text { color: #4F46E5; font-weight: 600; }
+
+/* ===== 备注 ===== */
+.remark-input {
+  width: 100%;
+  min-height: 150rpx;
+  background: #F8FAFC;
+  border: 2rpx solid #EDF1F7;
+  border-radius: 20rpx;
+  padding: 20rpx;
+  font-size: 26rpx;
+  color: #1E293B;
+  box-sizing: border-box;
+}
+.ph-gray { color: #CBD5E1; font-size: 26rpx; }
 
 /* ===== 提交按钮 ===== */
-.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); }
+.submit-wrap { padding: 32rpx 24rpx 8rpx; }
+.submit-btn {
+  background: linear-gradient(135deg, #6366F1, #4F46E5 60%, #4338CA);
+  border-radius: 999rpx;
+  padding: 26rpx 0;
+  text-align: center;
+  box-shadow: 0 12rpx 28rpx rgba(79, 70, 229, 0.35);
+  transition: all 0.2s ease;
+}
+.submit-btn:active {
+  transform: scale(0.98);
+  box-shadow: 0 6rpx 14rpx rgba(79, 70, 229, 0.25);
+}
+.submit-text { color: #fff; font-size: 30rpx; font-weight: 700; letter-spacing: 2rpx; }
 .submitting { opacity: 0.6; }
 
 /* ===== 历史记录 ===== */
-.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: 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: 22rpx; color: #999; }
+.history-list { display: flex; flex-direction: column; }
+.history-item {
+  display: flex;
+  align-items: center;
+  gap: 20rpx;
+  padding: 20rpx 0;
+  border-bottom: 2rpx solid #F1F5F9;
+}
+.history-item:last-child { border-bottom: none; }
+.history-badge {
+  width: 76rpx;
+  height: 76rpx;
+  border-radius: 24rpx;
+  background: linear-gradient(135deg, #EEF2FF, #E0E7FF);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-shrink: 0;
+}
+.history-badge-icon { font-size: 36rpx; line-height: 1; }
+.history-main { flex: 1; min-width: 0; }
+.history-line1 { font-size: 28rpx; font-weight: 600; color: #1E293B; display: block; }
+.history-line2 { font-size: 22rpx; color: #64748B; display: block; margin-top: 6rpx; }
+.qbar {
+  height: 8rpx;
+  border-radius: 999rpx;
+  background: #F1F5F9;
+  margin-top: 12rpx;
+  overflow: hidden;
+}
+.qbar-fill { height: 100%; border-radius: 999rpx; transition: width 0.3s ease; }
+.history-date { font-size: 22rpx; color: #94A3B8; flex-shrink: 0; }
 
 /* ===== 底部留白 ===== */
-.bottom-spacer { height: 40rpx; }
+.bottom-spacer { height: 60rpx; }
 </style>