|
|
@@ -24,54 +24,32 @@
|
|
|
<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 class="tl">
|
|
|
+ <view class="tl-pills">
|
|
|
+ <view class="tl-pill tl-pill-moon" :class="timeMode === 'sleep' ? 'tl-pill-on' : ''" @tap="setTimeMode('sleep')">
|
|
|
+ <text class="tl-pill-ic">🌙</text>
|
|
|
+ <text class="tl-pill-t">入睡</text>
|
|
|
+ <text class="tl-pill-time">{{ form.sleepTime || '--:--' }}</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 class="tl-pill tl-pill-sun" :class="timeMode === 'wake' ? 'tl-pill-on' : ''" @tap="setTimeMode('wake')">
|
|
|
+ <text class="tl-pill-ic">☀️</text>
|
|
|
+ <text class="tl-pill-t">醒来</text>
|
|
|
+ <text class="tl-pill-time">{{ form.wakeTime || '--:--' }}</text>
|
|
|
</view>
|
|
|
</view>
|
|
|
- </view>
|
|
|
-
|
|
|
- <view class="hero-tip">💡 建议青少年每天睡足 8-10 小时</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="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>
|
|
|
+ <scroll-view class="tl-scroll" scroll-x :scroll-left="tlScrollLeft" scroll-with-animation :show-scrollbar="false">
|
|
|
+ <view class="tl-bar">
|
|
|
+ <view class="tl-seg" v-for="seg in segments" :key="seg.k" :class="seg.cls" @tap="onTimelineTap(seg.k)">
|
|
|
+ <text class="tl-seg-time" v-if="seg.showLabel">{{ seg.label }}</text>
|
|
|
</view>
|
|
|
- </picker>
|
|
|
- <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>
|
|
|
+ </scroll-view>
|
|
|
+
|
|
|
+ <view class="tl-hint">点选「{{ timeMode === 'sleep' ? '入睡' : '醒来' }}」时间 · 每格 30 分钟,左右滑动找时间</view>
|
|
|
</view>
|
|
|
+
|
|
|
+ <view class="hero-tip">💡 建议青少年每天睡足 8-10 小时</view>
|
|
|
</view>
|
|
|
|
|
|
<!-- ===== 睡眠质量(点选打分,替代滑动条) ===== -->
|
|
|
@@ -188,6 +166,11 @@ export default {
|
|
|
memberId: null,
|
|
|
taskId: null,
|
|
|
submitting: false,
|
|
|
+ timeMode: 'sleep',
|
|
|
+ tlScrollLeft: 0,
|
|
|
+ tlTotalPx: 0,
|
|
|
+ tlAdvancePx: 0,
|
|
|
+ tlViewPx: 0,
|
|
|
history: [],
|
|
|
todayDate: '',
|
|
|
todayWeek: '',
|
|
|
@@ -248,10 +231,25 @@ export default {
|
|
|
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))
|
|
|
+ // 24 小时 × 2 格/小时 = 48 个半小时间隔段(用于时间轴高亮与标签)
|
|
|
+ segments: function () {
|
|
|
+ var self = this
|
|
|
+ var s = this.slotOf(this.form.sleepTime)
|
|
|
+ var w = this.slotOf(this.form.wakeTime)
|
|
|
+ var arr = []
|
|
|
+ for (var i = 0; i < 48; i++) {
|
|
|
+ var on = false
|
|
|
+ if (s >= 0 && w >= 0 && s !== w) {
|
|
|
+ if (w > s) on = i >= s && i <= w
|
|
|
+ else on = i >= s || i <= w
|
|
|
+ }
|
|
|
+ var cls = on ? 'tl-seg tl-seg-on' : 'tl-seg'
|
|
|
+ if (i === s) cls += ' tl-seg-sleep'
|
|
|
+ if (i === w) cls += ' tl-seg-wake'
|
|
|
+ var showLabel = (i % 2 === 0) || i === s || i === w
|
|
|
+ arr.push({ k: i, cls: cls, showLabel: showLabel, label: self.timeFromSlot(i) })
|
|
|
+ }
|
|
|
+ return arr
|
|
|
},
|
|
|
qualityFace: function () {
|
|
|
var m = this.qualityMap[this.form.qualityScore]
|
|
|
@@ -302,12 +300,60 @@ export default {
|
|
|
}
|
|
|
this.loadHistory()
|
|
|
},
|
|
|
+ onReady: function () {
|
|
|
+ var self = this
|
|
|
+ // 量取时间轴实际像素宽度,用于把选中时间滚动到可视区中间
|
|
|
+ uni.createSelectorQuery().in(this).select('.tl-bar').boundingClientRect(function (rect) {
|
|
|
+ if (rect && rect.width) {
|
|
|
+ self.tlTotalPx = rect.width
|
|
|
+ self.tlAdvancePx = rect.width / 48
|
|
|
+ var winW = uni.getWindowInfo().windowWidth
|
|
|
+ self.tlViewPx = winW - 112 * (winW / 750)
|
|
|
+ self.scrollToSlot(self.slotOf(self.form.sleepTime))
|
|
|
+ }
|
|
|
+ }).exec()
|
|
|
+ },
|
|
|
methods: {
|
|
|
- onSleepTimeChange: function (e) {
|
|
|
- this.form.sleepTime = e.detail.value
|
|
|
+ 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 + ' 分钟'
|
|
|
+ },
|
|
|
+ // 时间轴:半小时间隔 (0=00:00 .. 47=23:30)
|
|
|
+ slotOf: function (t) {
|
|
|
+ if (!t) return -1
|
|
|
+ var p = String(t).split(':')
|
|
|
+ var h = parseInt(p[0]); var m = parseInt(p[1])
|
|
|
+ if (isNaN(h) || isNaN(m)) return -1
|
|
|
+ return Math.round((h * 60 + m) / 30) % 48
|
|
|
+ },
|
|
|
+ timeFromSlot: function (i) {
|
|
|
+ var h = Math.floor(i / 2)
|
|
|
+ var m = (i % 2) * 30
|
|
|
+ return ('0' + h).slice(-2) + ':' + ('0' + m).slice(-2)
|
|
|
+ },
|
|
|
+ setTimeMode: function (mode) {
|
|
|
+ this.timeMode = mode
|
|
|
+ this.scrollToSlot(this.slotOf(this.form[mode === 'sleep' ? 'sleepTime' : 'wakeTime']))
|
|
|
+ },
|
|
|
+ onTimelineTap: function (i) {
|
|
|
+ var self = this
|
|
|
+ var t = this.timeFromSlot(i)
|
|
|
+ if (self.timeMode === 'sleep') {
|
|
|
+ self.form.sleepTime = t
|
|
|
+ self.timeMode = 'wake'
|
|
|
+ } else {
|
|
|
+ self.form.wakeTime = t
|
|
|
+ }
|
|
|
+ self.scrollToSlot(i)
|
|
|
},
|
|
|
- onWakeTimeChange: function (e) {
|
|
|
- this.form.wakeTime = e.detail.value
|
|
|
+ scrollToSlot: function (slot) {
|
|
|
+ if (slot < 0 || !this.tlAdvancePx) return
|
|
|
+ var target = slot * this.tlAdvancePx - this.tlViewPx / 2 + this.tlAdvancePx / 2
|
|
|
+ var max = Math.max(0, this.tlTotalPx - this.tlViewPx)
|
|
|
+ this.tlScrollLeft = Math.max(0, Math.min(target, max))
|
|
|
},
|
|
|
setQuality: function (score) {
|
|
|
this.form.qualityScore = score
|
|
|
@@ -320,13 +366,6 @@ export default {
|
|
|
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')
|
|
|
@@ -506,48 +545,89 @@ export default {
|
|
|
color: rgba(255, 255, 255, 0.7);
|
|
|
margin-top: 6rpx;
|
|
|
}
|
|
|
-.hero-track {
|
|
|
+.tl {
|
|
|
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;
|
|
|
+.tl-pills {
|
|
|
+ display: flex;
|
|
|
+ gap: 16rpx;
|
|
|
}
|
|
|
-.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 {
|
|
|
+.tl-pill {
|
|
|
+ flex: 1;
|
|
|
display: flex;
|
|
|
- justify-content: space-between;
|
|
|
- align-items: flex-start;
|
|
|
- margin-top: 16rpx;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10rpx;
|
|
|
+ padding: 16rpx 20rpx;
|
|
|
+ border-radius: 20rpx;
|
|
|
+ background: rgba(255, 255, 255, 0.1);
|
|
|
+ border: 2rpx solid rgba(255, 255, 255, 0.14);
|
|
|
+ transition: transform 0.2s ease;
|
|
|
}
|
|
|
-.hero-end {
|
|
|
+.tl-pill:active { transform: scale(0.97); }
|
|
|
+.tl-pill-on {
|
|
|
+ background: rgba(255, 255, 255, 0.22);
|
|
|
+ border-color: rgba(255, 209, 102, 0.75);
|
|
|
+ box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.22);
|
|
|
+}
|
|
|
+.tl-pill-ic { font-size: 30rpx; line-height: 1; }
|
|
|
+.tl-pill-t { font-size: 24rpx; color: rgba(255, 255, 255, 0.72); }
|
|
|
+.tl-pill-time {
|
|
|
+ flex: 1;
|
|
|
+ text-align: right;
|
|
|
+ font-size: 34rpx;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #fff;
|
|
|
+}
|
|
|
+.tl-pill-on .tl-pill-time { color: #FFD166; }
|
|
|
+.tl-scroll {
|
|
|
+ margin-top: 20rpx;
|
|
|
+ width: 100%;
|
|
|
+ height: 92rpx;
|
|
|
+}
|
|
|
+.tl-bar {
|
|
|
display: flex;
|
|
|
- flex-direction: column;
|
|
|
- align-items: flex-start;
|
|
|
gap: 4rpx;
|
|
|
+ padding: 0 2rpx;
|
|
|
+ align-items: stretch;
|
|
|
+}
|
|
|
+.tl-seg {
|
|
|
+ width: 96rpx;
|
|
|
+ height: 76rpx;
|
|
|
+ flex-shrink: 0;
|
|
|
+ border-radius: 10rpx;
|
|
|
+ background: rgba(255, 255, 255, 0.13);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ transition: background 0.15s ease;
|
|
|
+}
|
|
|
+.tl-seg:active { background: rgba(255, 255, 255, 0.4); }
|
|
|
+.tl-seg-time {
|
|
|
+ font-size: 18rpx;
|
|
|
+ color: rgba(255, 255, 255, 0.8);
|
|
|
+ line-height: 1;
|
|
|
+}
|
|
|
+.tl-seg-on { background: rgba(255, 209, 102, 0.55); }
|
|
|
+.tl-seg-on .tl-seg-time { color: rgba(255, 255, 255, 0.95); }
|
|
|
+.tl-seg-sleep {
|
|
|
+ background: linear-gradient(180deg, #FFD166, #FFB84D);
|
|
|
+ box-shadow: 0 0 12rpx rgba(255, 209, 102, 0.8);
|
|
|
+}
|
|
|
+.tl-seg-wake {
|
|
|
+ background: linear-gradient(180deg, #FF9F68, #FF7A45);
|
|
|
+ box-shadow: 0 0 12rpx rgba(255, 159, 104, 0.8);
|
|
|
+}
|
|
|
+.tl-seg-sleep .tl-seg-time, .tl-seg-wake .tl-seg-time {
|
|
|
+ color: #4A2A00;
|
|
|
+ font-weight: 700;
|
|
|
+}
|
|
|
+.tl-hint {
|
|
|
+ font-size: 20rpx;
|
|
|
+ color: rgba(255, 255, 255, 0.55);
|
|
|
+ margin-top: 14rpx;
|
|
|
}
|
|
|
-.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;
|
|
|
@@ -591,32 +671,6 @@ export default {
|
|
|
.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;
|
|
|
@@ -671,7 +725,7 @@ export default {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
- transition: all 0.18s ease;
|
|
|
+ transition: transform 0.18s ease;
|
|
|
}
|
|
|
.q-chip text { font-size: 30rpx; font-weight: 600; color: #64748B; }
|
|
|
.q-chip:active { transform: scale(0.94); }
|
|
|
@@ -702,7 +756,7 @@ export default {
|
|
|
border-radius: 999rpx;
|
|
|
background: #F8FAFC;
|
|
|
border: 2rpx solid #EDF1F7;
|
|
|
- transition: all 0.18s ease;
|
|
|
+ transition: transform 0.18s ease;
|
|
|
}
|
|
|
.s-tag text { font-size: 26rpx; color: #475569; }
|
|
|
.s-tag:active { transform: scale(0.96); }
|