| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833 |
- <template>
- <view class="page">
- <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="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="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>
- <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>
- </view>
- </scroll-view>
- <view class="tl-hint">点选「{{ timeMode === 'sleep' ? '入睡' : '醒来' }}」时间 · 每格 30 分钟,左右滑动找时间</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">1-10 分</text>
- </view>
- <view class="card-body">
- <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="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 ? '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-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-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>
- </view>
- <!-- 底部留白 -->
- <view class="bottom-spacer"></view>
- </view>
- </view>
- </template>
- <script>
- import config from '../../config'
- import { formatDate } from '../../utils/format.js'
- export default {
- data() {
- return {
- memberId: null,
- taskId: null,
- submitting: false,
- timeMode: 'sleep',
- tlScrollLeft: 0,
- tlTotalPx: 0,
- tlAdvancePx: 0,
- tlViewPx: 0,
- history: [],
- 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',
- qualityScore: 7,
- remark: ''
- }
- }
- },
- 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)
- },
- // 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]
- 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
- 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
- }
- 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: {
- 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)
- },
- 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
- },
- toggleTag: function (tag) {
- var idx = this.selectedTags.indexOf(tag)
- if (idx > -1) {
- this.selectedTags.splice(idx, 1)
- } else {
- this.selectedTags.push(tag)
- }
- },
- loadHistory: function () {
- var self = this
- var token = uni.getStorageSync('token')
- if (!token || !self.memberId) return
- uni.request({
- url: config.API_BASE_URL + '/api/health/sleep/list',
- method: 'POST',
- data: { memberId: self.memberId },
- header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
- success: function (res) {
- if (res.data && res.data.code === 200 && res.data.data) {
- var arr = Array.isArray(res.data.data) ? res.data.data : []
- self.history = arr.slice(0, 5).map(function (item) {
- self.decorateHistoryItem(item)
- return item
- })
- }
- }
- })
- },
- // 预计算列表渲染字段(避免模板中调用带参方法)
- 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
- 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' })
- }
- },
- fail: function () {
- self.submitting = false
- uni.showToast({ title: '网络错误', icon: 'none' })
- }
- })
- }
- }
- }
- </script>
- <style scoped>
- /* ===== 页面基础 ===== */
- .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;
- }
- .tl {
- position: relative;
- z-index: 1;
- margin-top: 28rpx;
- }
- .tl-pills {
- display: flex;
- gap: 16rpx;
- }
- .tl-pill {
- flex: 1;
- display: flex;
- 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;
- }
- .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;
- 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-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: 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; }
- /* ===== 睡眠质量 ===== */
- .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: transform 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: transform 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: 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: transform 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; }
- .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: 60rpx; }
- </style>
|