feat: redesign onboarding and home UI

This commit is contained in:
nepiedg
2026-04-25 00:32:12 +08:00
parent 4bd79ded0c
commit ba0a712306
8 changed files with 2020 additions and 485 deletions
+89
View File
@@ -0,0 +1,89 @@
设计原则
整合四位专家的核心诉求:
冷静但不冷漠、有趣但不幼稚、净化而非刺激
最终界面布局(完整结构)
📐 整体高度:约手机一屏半,单页垂直流
【顶部区】个人状态栏(高度 30%
*.txt
Plaintext
┌─────────────────────────────────────────────────────┐
│ [头像] 意志骑士 Lv.3 💰 ×24 │ ← 毛玻璃背景
│ │ backdrop-blur: 12px
│ ╭─────────────────────────────────────────────╮ │
│ │ ████████████████░░░░░░░░░░░░░░░░░░░░░░░░░ │ │ ← HP生命槽
│ │ HP: 72% 浊→清 视觉语言 │ │ 渐变填充 #6EE7B7#67E8F9
│ ╰─────────────────────────────────────────────╯ │ 24px高,12px圆角
│ │
│ ✦ 深度含氧(已坚持 2 小时 17 分) │ ← Buff标签
│ [薰衣草紫边框] │ 胶囊形
└─────────────────────────────────────────────────────┘
设计决策说明:
保留游戏化元素(等级/称号),但降低视觉优先级——置于次要位置
采用色彩心理学专家的"浊清动效"替代碎裂动效,保护用户情绪
毛玻璃背景融合未来主义美学,但保持经典主义的秩序感
【中部区】核心控制台(高度 35%
*.txt
Plaintext
【中下区】健康仪表盘(高度 20%
*.txt
Plaintext
┌─────────────────────────────────────────────────────┐
│ │
│ ╭─────────╮ ╭────────────────────────╮ │
│ │ ◐ │ │ 呼吸系统 +12% │ │
│ │ 84% │ │ 心脏功能 +8% │ │
│ │ 健康 │ │ 血液纯净 +15% │ │
│ ╰─────────╯ │ ──────────────────── │ │
│ 健康指数 │ +3.2 小时 │ │ ← 生命回收
│ (圆环图) │ 生命已回收 │ │ 强调色24px
│ ╰────────────────────────╯ │
│ │
└─────────────────────────────────────────────────────┘
设计决策说明:
健康指数圆环图代替多个独立卡片,符合极简主义减法原则
生命回收概念来自未来主义,成为核心数据展示
【底部区】成长曲线(高度 15%
*.txt
Plaintext
┌─────────────────────────────────────────────────────┐
│ │
│ 7 天趋势 │
│ │ │
│ 5 ├ ● │ ← 琥珀金转折点
│ │ ●────────────╱ │
│ 3 ├──●────────────────╱ │
│ │╱ │
│ 0 └──┬────┬────┬────┬────┬────┬────┬────→ │
│ 周一 周二 周三 周四 周五 周六 周日 │
│ │
│ 背景:极淡青绿渐变 (#ECFDF5#F0F9FF) │
│ │
└─────────────────────────────────────────────────────┘
最终色板
用途 色值 来源 说明
HP高填充 #67E8F9 心理学专家调整 降低饱和度,更持久
HP低填充 #D97706 暗琥珀 心理学专家 警示非恐惧
主渐变起 #6EE7B7 心理学专家调整 生命力、成长
SOS/里程碑 #FBBF24 全场共识 温暖、成就
Buff标签 #A78BFA 薰衣草紫 心理学专家原创 成长、过渡
背景 #F8FAFC 微蓝白 经典主义 干净、呼吸感
文字主色 #1E293B 深蓝灰 经典主义 冷静、专业
动效规范
动效 实现 周期 意义
HP呼吸闪烁 微弱明度变化 ±5% 4-6秒 与身体节奏共振
浊清过渡 颜色从暗琥珀→清澈渐变 2秒 非破坏性反馈
呼吸线脉动 顶部细线不透明度 10-20% 6秒 身体自愈暗示
设计决策摘要
问题 最终决策 采纳来源
HP碎裂动效 ❌ 取消 → 改用"浊清"视觉语言 色彩心理学专家
等级/称号 ✅ 保留,但降低视觉优先级 未来主义
金币奖励 ✅ 保留图标形式,删除独立卡片 极简主义融合
游戏化整体 ⚠️ 保留数值化反馈,删除幼稚外壳 经典主义+未来主义融合
布局风格 瑞士网格+毛玻璃 经典主义+未来主义融合
配色饱和度 降低15-20% 经典主义+色彩心理学
一句话总结
"一个冷静的生物数据仪表盘,用颜色的语言告诉用户:你的每一次选择,都在让生命更加清澈。"
@@ -11,83 +11,85 @@
</view> </view>
<view class="dialog-body"> <view class="dialog-body">
<view v-if="quickModeActive" class="quick-banner"> <template v-if="quickModeActive">
<view class="quick-banner-chip"> <view class="quick-banner">
<text class="quick-banner-chip-label">默认时间</text> <view class="quick-banner-chip">
<text class="quick-banner-chip-value">{{ formData.smoke_time_only }}</text> <text class="quick-banner-chip-label">默认时间</text>
<text class="quick-banner-chip-value">{{ formData.smoke_time_only }}</text>
</view>
<view class="quick-banner-chip" v-if="type === 'smoke'">
<text class="quick-banner-chip-label">默认数量</text>
<text class="quick-banner-chip-value">{{ formData.num }} </text>
</view>
<text class="quick-banner-tip">先选择系统场景如果选其他再补充自定义原因</text>
</view> </view>
<view class="quick-banner-chip" v-if="type === 'smoke'">
<text class="quick-banner-chip-label">默认数量</text>
<text class="quick-banner-chip-value">{{ formData.num }} </text>
</view>
<text class="quick-banner-tip">选择原因和烟瘾等级即可快速保存需要时再展开高级项</text>
</view>
<view v-if="quickModeActive && type === 'smoke'" class="section-card level-section-quick"> <view v-if="type === 'smoke'" class="section-card level-section-quick">
<view class="level-header"> <view class="level-header">
<text class="section-title">烟瘾等级</text> <text class="section-title">烟瘾等级</text>
<view class="level-badge">Level {{ formData.level }}</view> <view class="level-badge">Level {{ formData.level }}</view>
</view> </view>
<slider <slider
class="level-slider" class="level-slider"
:value="formData.level" :value="formData.level"
:min="1" :min="1"
:max="5" :max="5"
:step="1" :step="1"
activeColor="#22C55E" activeColor="#22C55E"
backgroundColor="#E5E7EB" backgroundColor="#E5E7EB"
block-color="#22C55E" block-color="#22C55E"
:block-size="20" :block-size="20"
@change="onLevelChange" @change="onLevelChange"
/> />
<view class="level-scale"> <view class="level-scale">
<text class="level-scale-text">无感</text> <text class="level-scale-text">无感</text>
<text class="level-scale-text">强烈</text> <text class="level-scale-text">强烈</text>
</view>
</view>
<view class="section-card">
<view class="section-heading">
<text class="section-title">{{ reasonSectionTitle }}</text>
<text class="section-caption">可多选</text>
</view>
<view class="reason-chip-grid">
<view
v-for="item in quickReasonOptions"
:key="item.key"
class="reason-chip"
:class="{ 'reason-chip-active': isReasonSelected(item.key) }"
@tap="toggleReason(item.key)"
>
<text class="reason-chip-text">{{ item.label }}</text>
</view> </view>
</view> </view>
</view>
<view class="remark-section"> <view class="section-card scene-section">
<view class="section-heading"> <view class="section-heading">
<text class="section-title">{{ remarkTitle }}</text> <text class="section-title">{{ sceneSectionTitle }}</text>
<text class="section-caption">{{ remarkCaption }}</text> <text class="section-caption">系统预设可多选</text>
</view>
<view class="reason-chip-grid">
<view
v-for="item in quickReasonOptions"
:key="item.key"
class="reason-chip"
:class="{ 'reason-chip-active': isReasonSelected(item.key) }"
@tap="toggleReason(item.key)"
>
<text class="reason-chip-text">{{ item.label }}</text>
</view>
</view>
</view> </view>
<view class="remark-card">
<textarea
class="form-textarea"
v-model="formData.remark"
:placeholder="remarkPlaceholder"
maxlength="200"
/>
</view>
</view>
<view v-if="quickModeActive" class="advanced-toggle" @tap="showAdvanced = !showAdvanced"> <view v-if="showCustomReasonInput" class="remark-section">
<view> <view class="section-heading">
<text class="advanced-toggle-title">{{ showAdvanced ? '收起高级设置' : '展开高级设置' }}</text> <text class="section-title">其他场景</text>
<text class="advanced-toggle-desc">修改时间和数量</text> <text class="section-caption">自填</text>
</view>
<view class="remark-card">
<textarea
class="form-textarea form-textarea-compact"
v-model="formData.custom_reason"
:placeholder="customReasonPlaceholder"
maxlength="120"
/>
</view>
</view> </view>
<text class="advanced-toggle-arrow">{{ showAdvanced ? '⌃' : '⌄' }}</text>
</view>
<view v-if="showAdvanced || !quickModeActive" class="advanced-fields"> <view class="advanced-toggle" @tap="showAdvanced = !showAdvanced">
<view>
<text class="advanced-toggle-title">{{ showAdvanced ? '收起高级设置' : '展开高级设置' }}</text>
<text class="advanced-toggle-desc">修改时间和数量</text>
</view>
<text class="advanced-toggle-arrow">{{ showAdvanced ? '⌃' : '⌄' }}</text>
</view>
</template>
<view v-if="showAdvanced || !quickModeActive" class="advanced-fields detail-top-fields">
<view class="form-row"> <view class="form-row">
<picker class="picker-card" mode="date" :value="formData.smoke_time" @change="onDateChange"> <picker class="picker-card" mode="date" :value="formData.smoke_time" @change="onDateChange">
<view class="input-card"> <view class="input-card">
@@ -121,7 +123,7 @@
</view> </view>
</view> </view>
<view v-if="!quickModeActive" class="section-card"> <view class="section-card">
<view class="level-header"> <view class="level-header">
<text class="section-title">{{ type === 'smoke' ? '烟瘾程度' : '忍住强度' }}</text> <text class="section-title">{{ type === 'smoke' ? '烟瘾程度' : '忍住强度' }}</text>
<view class="level-badge">Level {{ formData.level }}</view> <view class="level-badge">Level {{ formData.level }}</view>
@@ -144,8 +146,57 @@
</view> </view>
</view> </view>
</view> </view>
</view>
<template v-if="!quickModeActive">
<view class="section-card scene-section">
<view class="section-heading">
<text class="section-title">{{ sceneSectionTitle }}</text>
<text class="section-caption">系统预设可多选</text>
</view>
<view class="reason-chip-grid">
<view
v-for="item in quickReasonOptions"
:key="item.key"
class="reason-chip"
:class="{ 'reason-chip-active': isReasonSelected(item.key) }"
@tap="toggleReason(item.key)"
>
<text class="reason-chip-text">{{ item.label }}</text>
</view>
</view>
</view>
<view v-if="showCustomReasonInput" class="remark-section">
<view class="section-heading">
<text class="section-title">其他场景</text>
<text class="section-caption">自填</text>
</view>
<view class="remark-card">
<textarea
class="form-textarea form-textarea-compact"
v-model="formData.custom_reason"
:placeholder="customReasonPlaceholder"
maxlength="120"
/>
</view>
</view>
<view class="remark-section remark-section-bottom">
<view class="section-heading">
<text class="section-title">原因</text>
<text class="section-caption">可选放在最后补充</text>
</view>
<view class="remark-card">
<textarea
class="form-textarea"
v-model="formData.remark"
:placeholder="remarkPlaceholder"
maxlength="200"
/>
</view>
</view>
</template>
</view>
<view class="dialog-footer"> <view class="dialog-footer">
<view class="dialog-btn-primary" @tap="submit"> <view class="dialog-btn-primary" @tap="submit">
<view class="btn-icon"></view> <view class="btn-icon"></view>
@@ -188,6 +239,7 @@ export default {
smoke_time_only: '', smoke_time_only: '',
smoke_at: '', smoke_at: '',
remark: '', remark: '',
custom_reason: '',
reason_tags: [], reason_tags: [],
level: 2, level: 2,
num: 1 num: 1
@@ -212,9 +264,20 @@ export default {
quickReasonOptions() { quickReasonOptions() {
return getReasonOptions(this.type) return getReasonOptions(this.type)
}, },
sceneSectionTitle() {
return this.type === 'smoke' ? '选择抽烟场景' : '选择忍住场景'
},
reasonSectionTitle() { reasonSectionTitle() {
return this.type === 'smoke' ? '这次为什么会抽?' : '这次是怎么扛住的?' return this.type === 'smoke' ? '这次为什么会抽?' : '这次是怎么扛住的?'
}, },
showCustomReasonInput() {
return this.formData.reason_tags.includes('other')
},
customReasonPlaceholder() {
return this.type === 'smoke'
? '写下系统场景里没有覆盖的触发点...'
: '写下这次具体是怎么撑过去的...'
},
remarkTitle() { remarkTitle() {
return this.formData.reason_tags.includes('other') ? '补充说明' : '补充备注' return this.formData.reason_tags.includes('other') ? '补充说明' : '补充备注'
}, },
@@ -253,6 +316,7 @@ export default {
smoke_time_only: this.initialData.smoke_time_only || '', smoke_time_only: this.initialData.smoke_time_only || '',
smoke_at: this.initialData.smoke_at || '', smoke_at: this.initialData.smoke_at || '',
remark: this.initialData.remark || '', remark: this.initialData.remark || '',
custom_reason: '',
reason_tags: normalizeReasonTags(this.initialData.reason_tags), reason_tags: normalizeReasonTags(this.initialData.reason_tags),
level: this.initialData.level ?? 2, level: this.initialData.level ?? 2,
num: this.resolveInitialNum(this.initialData) num: this.resolveInitialNum(this.initialData)
@@ -273,6 +337,7 @@ export default {
smoke_time_only: timeStr, smoke_time_only: timeStr,
smoke_at: datetimeStr, smoke_at: datetimeStr,
remark: '', remark: '',
custom_reason: '',
reason_tags: [], reason_tags: [],
level: 2, level: 2,
num: this.type === 'smoke' ? 1 : 0 num: this.type === 'smoke' ? 1 : 0
@@ -350,14 +415,16 @@ export default {
}, },
buildRemark() { buildRemark() {
const customRemark = (this.formData.remark || '').trim() const customRemark = (this.formData.remark || '').trim()
const customReason = (this.formData.custom_reason || '').trim()
const reasonLabels = getReasonLabels(this.formData.reason_tags, this.type).filter(label => label !== '其他') const reasonLabels = getReasonLabels(this.formData.reason_tags, this.type).filter(label => label !== '其他')
if (!reasonLabels.length) { const parts = [...reasonLabels]
return customRemark if (customReason) {
parts.push(customReason)
} }
if (!customRemark) { if (customRemark) {
return reasonLabels.join('、') parts.push(customRemark)
} }
return `${reasonLabels.join('')}${customRemark}` return parts.join('')
}, },
submit() { submit() {
if (!this.isTimeValid()) { if (!this.isTimeValid()) {
@@ -546,6 +613,11 @@ export default {
box-shadow: 0 10rpx 18rpx rgba(26, 163, 122, 0.12); box-shadow: 0 10rpx 18rpx rgba(26, 163, 122, 0.12);
} }
.scene-section {
background:
linear-gradient(135deg, rgba(255, 255, 255, 0.86), rgba(240, 249, 255, 0.72));
}
.reason-chip-text { .reason-chip-text {
font-size: 24rpx; font-size: 24rpx;
font-weight: 600; font-weight: 600;
@@ -576,6 +648,14 @@ export default {
box-sizing: border-box; box-sizing: border-box;
} }
.form-textarea-compact {
min-height: 112rpx;
}
.remark-section-bottom {
margin-top: 4rpx;
}
.advanced-toggle { .advanced-toggle {
display: flex; display: flex;
align-items: center; align-items: center;
+1 -1
View File
@@ -1,6 +1,6 @@
const ENV = { const ENV = {
development: { development: {
BASE_URL: 'http://localhost:8080/api/v1', BASE_URL: 'http://192.168.31.73:9003/api/v1',
MINI_PROGRAM_ID: 2 MINI_PROGRAM_ID: 2
}, },
production: { production: {
+2 -2
View File
@@ -7,13 +7,13 @@
}, },
"pages": [ "pages": [
{ {
"path": "pages/mode-select/index", "path": "pages/index/index",
"style": { "style": {
"navigationStyle": "custom" "navigationStyle": "custom"
} }
}, },
{ {
"path": "pages/index/index", "path": "pages/mode-select/index",
"style": { "style": {
"navigationStyle": "custom" "navigationStyle": "custom"
} }
+1265 -267
View File
File diff suppressed because it is too large Load Diff
+157 -33
View File
@@ -115,8 +115,13 @@
<!-- 成就风格 --> <!-- 成就风格 -->
<view v-if="achievementThemes.length > 0" class="form-section"> <view v-if="achievementThemes.length > 0" class="form-section">
<text class="section-label">成就称号风格</text> <view class="theme-section-head">
<text class="section-hint">打卡越久称号越高</text> <view>
<text class="section-label">成就称号风格</text>
<text class="section-hint">选择一套更能激励你的成长称号</text>
</view>
<text class="theme-section-badge">可更换</text>
</view>
<view class="theme-list"> <view class="theme-list">
<view <view
v-for="theme in achievementThemes" v-for="theme in achievementThemes"
@@ -125,16 +130,25 @@
:class="{ 'theme-card-active': formData.achievement_theme_id === theme.id }" :class="{ 'theme-card-active': formData.achievement_theme_id === theme.id }"
@tap="formData.achievement_theme_id = theme.id" @tap="formData.achievement_theme_id = theme.id"
> >
<view class="theme-glow"></view>
<view class="theme-header"> <view class="theme-header">
<text class="theme-icon">{{ theme.icon }}</text> <view class="theme-icon-wrap">
<text class="theme-name">{{ theme.name }}</text> <text class="theme-icon">{{ theme.icon }}</text>
</view>
<view class="theme-title-wrap">
<text class="theme-name">{{ theme.name }}</text>
<text class="theme-desc">打卡进度会逐步解锁称号</text>
</view>
<view class="theme-check">
<text v-if="formData.achievement_theme_id === theme.id"></text>
</view>
</view> </view>
<view class="theme-levels"> <view class="theme-levels">
<text <text
v-for="(level, idx) in theme.levels" v-for="(level, idx) in theme.levels"
:key="level.id" :key="level.id"
class="theme-level" class="theme-level"
>{{ level.name }}<text v-if="idx < theme.levels.length - 1" class="theme-arrow"> </text></text> >{{ level.name }}</text>
</view> </view>
</view> </view>
</view> </view>
@@ -263,10 +277,6 @@ async function handleSubmit() {
uni.showLoading({ title: '保存中...' }) uni.showLoading({ title: '保存中...' })
await profileStore.saveProfile(formData.value) await profileStore.saveProfile(formData.value)
uni.hideLoading() uni.hideLoading()
if (!formData.value.mode) {
uni.redirectTo({ url: '/pages/mode-select/index' })
return
}
uni.switchTab({ url: '/pages/index/index' }) uni.switchTab({ url: '/pages/index/index' })
} catch (e) { } catch (e) {
uni.hideLoading() uni.hideLoading()
@@ -539,66 +549,180 @@ onShareAppMessage(() => {
margin-bottom: 16rpx; margin-bottom: 16rpx;
} }
.theme-section-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 20rpx;
margin-bottom: 18rpx;
}
.theme-section-head .section-hint {
margin-bottom: 0;
}
.theme-section-badge {
flex-shrink: 0;
padding: 8rpx 16rpx;
border-radius: 999rpx;
background: rgba(110, 231, 183, 0.16);
border: 1rpx solid rgba(16, 185, 129, 0.16);
font-size: 20rpx;
font-weight: 700;
color: #0F766E;
}
.theme-list { .theme-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12rpx; gap: 18rpx;
} }
.theme-card { .theme-card {
padding: 20rpx; position: relative;
border-radius: 20rpx; overflow: hidden;
background: #F9FBFA; padding: 24rpx;
border: 2rpx solid #F0F0F0; border-radius: 28rpx;
background:
linear-gradient(135deg, rgba(255, 255, 255, 0.96), rgba(248, 250, 252, 0.92));
border: 2rpx solid rgba(226, 232, 240, 0.9);
box-sizing: border-box; box-sizing: border-box;
box-shadow: 0 10rpx 28rpx rgba(15, 23, 42, 0.04);
} }
.theme-card-active { .theme-card-active {
background: #E8F5F0; background:
border-color: #10B981; radial-gradient(circle at top right, rgba(103, 232, 249, 0.28), transparent 34%),
linear-gradient(135deg, rgba(236, 253, 245, 0.98), rgba(240, 249, 255, 0.94));
border-color: rgba(16, 185, 129, 0.72);
box-shadow: 0 18rpx 42rpx rgba(16, 185, 129, 0.13);
}
.theme-glow {
position: absolute;
top: -70rpx;
right: -70rpx;
width: 190rpx;
height: 190rpx;
border-radius: 50%;
background: rgba(110, 231, 183, 0.12);
}
.theme-card-active .theme-glow {
background: rgba(103, 232, 249, 0.28);
} }
.theme-header { .theme-header {
position: relative;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10rpx; gap: 18rpx;
margin-bottom: 10rpx; margin-bottom: 20rpx;
}
.theme-icon-wrap {
width: 72rpx;
height: 72rpx;
border-radius: 24rpx;
display: flex;
align-items: center;
justify-content: center;
background: rgba(241, 245, 249, 0.92);
border: 1rpx solid rgba(226, 232, 240, 0.9);
box-shadow: inset 0 1rpx 0 rgba(255, 255, 255, 0.7);
flex-shrink: 0;
}
.theme-card-active .theme-icon-wrap {
background: linear-gradient(135deg, #6EE7B7, #67E8F9);
border-color: rgba(255, 255, 255, 0.9);
box-shadow: 0 12rpx 26rpx rgba(20, 184, 166, 0.18);
} }
.theme-icon { .theme-icon {
font-size: 32rpx; font-size: 36rpx;
line-height: 1;
}
.theme-title-wrap {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 6rpx;
} }
.theme-name { .theme-name {
font-size: 28rpx; font-size: 29rpx;
font-weight: 700; line-height: 1.25;
color: #1A1A1A; font-weight: 800;
color: #1E293B;
}
.theme-desc {
font-size: 21rpx;
line-height: 1.4;
color: #64748B;
} }
.theme-card-active .theme-name { .theme-card-active .theme-name {
color: #10B981; color: #0F766E;
}
.theme-check {
width: 42rpx;
height: 42rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
border: 2rpx solid rgba(148, 163, 184, 0.34);
background: rgba(255, 255, 255, 0.78);
color: #FFFFFF;
font-size: 24rpx;
font-weight: 900;
flex-shrink: 0;
}
.theme-card-active .theme-check {
border-color: transparent;
background: linear-gradient(135deg, #10B981, #06B6D4);
box-shadow: 0 8rpx 18rpx rgba(16, 185, 129, 0.2);
} }
.theme-levels { .theme-levels {
font-size: 22rpx; position: relative;
color: #999999; display: flex;
line-height: 1.6; flex-wrap: wrap;
gap: 10rpx;
} }
.theme-level { .theme-level {
font-size: 22rpx; max-width: 100%;
padding: 8rpx 14rpx;
border-radius: 999rpx;
background: rgba(241, 245, 249, 0.95);
border: 1rpx solid rgba(226, 232, 240, 0.9);
font-size: 21rpx;
line-height: 1.35;
font-weight: 700;
color: #64748B;
} }
.theme-card-active .theme-level { .theme-card-active .theme-level {
color: #10B981; background: rgba(255, 255, 255, 0.72);
border-color: rgba(16, 185, 129, 0.15);
color: #0F766E;
}
.theme-card-active .theme-level:first-child {
background: rgba(251, 191, 36, 0.18);
border-color: rgba(251, 191, 36, 0.22);
color: #B45309;
} }
.theme-arrow { .theme-arrow {
color: #CCCCCC; display: none;
}
.theme-card-active .theme-arrow {
color: #6EE7B7;
} }
.bottom-space { .bottom-space {
+349 -105
View File
@@ -1,13 +1,38 @@
<template> <template>
<view class="page"> <view class="page">
<view class="bg-orb bg-orb-main"></view>
<view class="bg-orb bg-orb-soft"></view>
<view class="header"> <view class="header">
<text class="title">监督人机制</text> <view class="header-copy">
<text class="subtitle">邀请朋友监督你的戒烟旅程或者你来监督别人</text> <text class="eyebrow">ACCOUNTABILITY</text>
<text class="title">监督人机制</text>
<text class="subtitle">邀请朋友监督你的戒烟旅程也可以成为别人的坚持搭子</text>
</view>
<view class="hero-panel">
<view class="hero-stat">
<text class="hero-stat-value">{{ supervisorItems.length }}/3</text>
<text class="hero-stat-label">监督我的人</text>
</view>
<view class="hero-divider"></view>
<view class="hero-stat">
<text class="hero-stat-value">{{ overviewItems.length }}</text>
<text class="hero-stat-label">我监督的人</text>
</view>
<view class="hero-divider"></view>
<view class="hero-stat">
<text class="hero-stat-value">{{ reminderEnabled ? 'ON' : 'OFF' }}</text>
<text class="hero-stat-label">提醒状态</text>
</view>
</view>
</view> </view>
<view class="card"> <view class="card">
<view class="card-head"> <view class="card-head">
<text class="card-title">邀请监督人</text> <view class="card-title-wrap">
<view class="card-icon">🤝</view>
<text class="card-title">邀请监督人</text>
</view>
<text class="card-meta">已绑定 {{ supervisorItems.length }}/3</text> <text class="card-meta">已绑定 {{ supervisorItems.length }}/3</text>
</view> </view>
@@ -36,11 +61,15 @@
<view class="card"> <view class="card">
<view class="card-head"> <view class="card-head">
<text class="card-title">我监督的人</text> <view class="card-title-wrap">
<view class="card-icon card-icon-blue">👀</view>
<text class="card-title">我监督的人</text>
</view>
<text class="card-meta">{{ overviewItems.length }} </text> <text class="card-meta">{{ overviewItems.length }} </text>
</view> </view>
<view v-if="overviewItems.length === 0" class="empty"> <view v-if="overviewItems.length === 0" class="empty">
<text class="empty-icon">🫶</text>
<text class="empty-text">还没有绑定监督关系</text> <text class="empty-text">还没有绑定监督关系</text>
<text class="empty-hint">收到口令后可去绑定监督页面完成绑定</text> <text class="empty-hint">收到口令后可去绑定监督页面完成绑定</text>
<button class="btn btn-ghost" @tap="gotoBindPage">去绑定监督</button> <button class="btn btn-ghost" @tap="gotoBindPage">去绑定监督</button>
@@ -69,10 +98,14 @@
<view class="card"> <view class="card">
<view class="card-head"> <view class="card-head">
<text class="card-title">监督我的人</text> <view class="card-title-wrap">
<view class="card-icon card-icon-orange">🛡</view>
<text class="card-title">监督我的人</text>
</view>
<text class="card-meta">{{ supervisorItems.length }} </text> <text class="card-meta">{{ supervisorItems.length }} </text>
</view> </view>
<view v-if="supervisorItems.length === 0" class="empty"> <view v-if="supervisorItems.length === 0" class="empty">
<text class="empty-icon">🌱</text>
<text class="empty-text">还没有人监督你</text> <text class="empty-text">还没有人监督你</text>
<text class="empty-hint">你可以先生成邀请口令发送给朋友</text> <text class="empty-hint">你可以先生成邀请口令发送给朋友</text>
</view> </view>
@@ -89,7 +122,10 @@
<view class="card"> <view class="card">
<view class="card-head"> <view class="card-head">
<text class="card-title">提醒设置</text> <view class="card-title-wrap">
<view class="card-icon card-icon-purple">🔔</view>
<text class="card-title">提醒设置</text>
</view>
<text class="card-meta">默认关闭</text> <text class="card-meta">默认关闭</text>
</view> </view>
@@ -136,7 +172,10 @@
<view class="card"> <view class="card">
<view class="card-head"> <view class="card-head">
<text class="card-title">提醒测试监督人</text> <view class="card-title-wrap">
<view class="card-icon card-icon-gray"></view>
<text class="card-title">提醒测试监督人</text>
</view>
<text class="card-meta">仅写日志</text> <text class="card-meta">仅写日志</text>
</view> </view>
<view class="settings"> <view class="settings">
@@ -417,122 +456,285 @@ onShow(async () => {
<style scoped> <style scoped>
.page { .page {
position: relative;
min-height: 100vh; min-height: 100vh;
padding: 28rpx 28rpx 40rpx; padding: 30rpx 28rpx 48rpx;
box-sizing: border-box; box-sizing: border-box;
background: linear-gradient(180deg, #eef7f3 0%, #f7faf8 40%, #fbfdff 100%); overflow: hidden;
background:
radial-gradient(circle at 16% 0%, rgba(31, 191, 143, 0.18), transparent 34%),
radial-gradient(circle at 92% 10%, rgba(96, 165, 250, 0.15), transparent 30%),
linear-gradient(180deg, #eef7f3 0%, #f7faf8 42%, #fbfdff 100%);
}
.bg-orb {
position: absolute;
border-radius: 999rpx;
pointer-events: none;
filter: blur(2rpx);
}
.bg-orb-main {
top: 126rpx;
right: -86rpx;
width: 230rpx;
height: 230rpx;
background: rgba(26, 163, 122, 0.12);
}
.bg-orb-soft {
top: 520rpx;
left: -100rpx;
width: 260rpx;
height: 260rpx;
background: rgba(96, 165, 250, 0.1);
}
.header,
.card,
.footer {
position: relative;
z-index: 1;
} }
.header { .header {
padding: 8rpx 6rpx 18rpx; padding: 10rpx 4rpx 20rpx;
}
.header-copy {
padding: 4rpx 2rpx 24rpx;
}
.eyebrow {
display: inline-flex;
align-items: center;
align-self: flex-start;
padding: 8rpx 18rpx;
border-radius: 999rpx;
background: rgba(255, 255, 255, 0.74);
border: 1rpx solid rgba(26, 163, 122, 0.14);
color: #1aa37a;
font-size: 20rpx;
font-weight: 800;
letter-spacing: 1.8rpx;
} }
.title { .title {
display: block; display: block;
font-size: 40rpx; margin-top: 18rpx;
font-weight: 800; font-size: 48rpx;
line-height: 1.15;
font-weight: 900;
color: #0f172a; color: #0f172a;
letter-spacing: 0.5rpx; letter-spacing: -0.8rpx;
} }
.subtitle { .subtitle {
display: block; display: block;
margin-top: 10rpx; margin-top: 12rpx;
font-size: 24rpx; max-width: 620rpx;
line-height: 1.6; font-size: 25rpx;
line-height: 1.7;
color: #64748b; color: #64748b;
} }
.hero-panel {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 20rpx;
border-radius: 30rpx;
background: linear-gradient(135deg, rgba(15, 118, 110, 0.94), rgba(26, 163, 122, 0.86));
box-shadow: 0 18rpx 44rpx rgba(15, 118, 110, 0.2);
overflow: hidden;
}
.hero-stat {
flex: 1;
min-width: 0;
text-align: center;
}
.hero-stat-value {
display: block;
font-size: 34rpx;
line-height: 1.2;
font-weight: 900;
color: #ffffff;
letter-spacing: -0.4rpx;
}
.hero-stat-label {
display: block;
margin-top: 8rpx;
font-size: 20rpx;
line-height: 1.35;
color: rgba(255, 255, 255, 0.76);
}
.hero-divider {
width: 1rpx;
height: 54rpx;
background: rgba(255, 255, 255, 0.24);
}
.card { .card {
margin-top: 18rpx; margin-top: 20rpx;
background: rgba(255, 255, 255, 0.92); padding: 26rpx;
border-radius: 26rpx; border-radius: 32rpx;
border: 1rpx solid rgba(15, 23, 42, 0.06); background: rgba(255, 255, 255, 0.9);
padding: 22rpx 22rpx; border: 1rpx solid rgba(255, 255, 255, 0.86);
box-shadow: 0 10rpx 26rpx rgba(15, 23, 42, 0.05); box-shadow: 0 18rpx 44rpx rgba(15, 23, 42, 0.07);
backdrop-filter: blur(18rpx);
} }
.card-head { .card-head {
display: flex; display: flex;
align-items: baseline; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 16rpx; gap: 16rpx;
} }
.card-title-wrap {
display: flex;
align-items: center;
gap: 12rpx;
min-width: 0;
}
.card-icon {
width: 50rpx;
height: 50rpx;
line-height: 50rpx;
border-radius: 18rpx;
text-align: center;
font-size: 25rpx;
background: rgba(26, 163, 122, 0.12);
}
.card-icon-blue {
background: rgba(96, 165, 250, 0.14);
}
.card-icon-orange {
background: rgba(251, 146, 60, 0.15);
}
.card-icon-purple {
background: rgba(168, 85, 247, 0.13);
}
.card-icon-gray {
background: rgba(100, 116, 139, 0.12);
}
.card-title { .card-title {
font-size: 28rpx; font-size: 29rpx;
font-weight: 800; font-weight: 900;
color: #0f172a; color: #0f172a;
} }
.card-meta { .card-meta {
font-size: 22rpx; flex-shrink: 0;
color: #94a3b8; padding: 6rpx 14rpx;
border-radius: 999rpx;
background: rgba(241, 245, 249, 0.78);
font-size: 21rpx;
font-weight: 700;
color: #64748b;
} }
.invite-box { .invite-box {
margin-top: 18rpx; position: relative;
margin-top: 22rpx;
padding: 26rpx;
border-radius: 28rpx;
background:
linear-gradient(135deg, rgba(236, 253, 245, 0.94), rgba(255, 255, 255, 0.92));
border: 1rpx solid rgba(26, 163, 122, 0.14);
overflow: hidden;
} }
.invite-label { .invite-label {
display: block; display: block;
font-size: 22rpx; font-size: 22rpx;
font-weight: 800;
color: #64748b; color: #64748b;
} }
.invite-token { .invite-token {
display: block; display: block;
margin-top: 10rpx; margin-top: 12rpx;
font-size: 36rpx; font-size: 52rpx;
line-height: 1.15;
font-weight: 900; font-weight: 900;
letter-spacing: 2rpx; letter-spacing: 4rpx;
color: #0f766e; color: #0f766e;
font-family: 'DIN Alternate', -apple-system, sans-serif; font-family: 'DIN Alternate', -apple-system, sans-serif;
} }
.invite-hint { .invite-hint {
display: block; display: block;
margin-top: 10rpx; margin-top: 14rpx;
font-size: 22rpx; font-size: 23rpx;
line-height: 1.6; line-height: 1.65;
color: #475569; color: #475569;
} }
.invite-actions { .invite-actions {
display: flex; display: flex;
gap: 14rpx; gap: 14rpx;
margin-top: 16rpx; margin-top: 22rpx;
}
.invite-actions .btn {
flex: 1;
} }
.invite-expire { .invite-expire {
display: block; display: block;
margin-top: 14rpx; margin-top: 16rpx;
font-size: 22rpx; font-size: 21rpx;
color: #94a3b8; color: #94a3b8;
} }
.invite-empty { .invite-empty {
margin-top: 18rpx; margin-top: 22rpx;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16rpx; gap: 18rpx;
padding: 22rpx;
border-radius: 26rpx;
background: rgba(248, 250, 252, 0.74);
border: 1rpx dashed rgba(26, 163, 122, 0.22);
} }
.invite-empty-text { .invite-empty-text {
font-size: 24rpx; font-size: 24rpx;
line-height: 1.6; line-height: 1.65;
color: #475569; color: #475569;
} }
.empty { .empty {
margin-top: 18rpx; margin-top: 22rpx;
padding: 16rpx 6rpx 6rpx; padding: 34rpx 24rpx;
border-radius: 28rpx;
text-align: center;
background: linear-gradient(180deg, rgba(248, 250, 252, 0.78), rgba(255, 255, 255, 0.76));
border: 1rpx dashed rgba(100, 116, 139, 0.18);
}
.empty-icon {
display: block;
font-size: 48rpx;
line-height: 1;
margin-bottom: 16rpx;
} }
.empty-text { .empty-text {
display: block; display: block;
font-size: 24rpx; font-size: 26rpx;
font-weight: 700; font-weight: 900;
color: #0f172a; color: #0f172a;
} }
@@ -544,28 +746,34 @@ onShow(async () => {
color: #64748b; color: #64748b;
} }
.empty .btn {
margin-top: 20rpx;
}
.list { .list {
margin-top: 18rpx; margin-top: 22rpx;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 14rpx; gap: 16rpx;
} }
.row { .row {
display: flex; display: flex;
gap: 16rpx; gap: 18rpx;
align-items: flex-start; align-items: flex-start;
padding: 16rpx; padding: 20rpx;
border-radius: 20rpx; border-radius: 26rpx;
background: rgba(241, 245, 249, 0.6); background: linear-gradient(180deg, rgba(248, 250, 252, 0.9), rgba(255, 255, 255, 0.9));
border: 1rpx solid rgba(15, 23, 42, 0.04); border: 1rpx solid rgba(15, 23, 42, 0.05);
} }
.avatar { .avatar {
width: 74rpx; width: 82rpx;
height: 74rpx; height: 82rpx;
border-radius: 50%; border-radius: 50%;
background: #e2e8f0; background: #e2e8f0;
border: 4rpx solid #ffffff;
box-shadow: 0 10rpx 22rpx rgba(15, 23, 42, 0.09);
flex-shrink: 0; flex-shrink: 0;
} }
@@ -577,24 +785,27 @@ onShow(async () => {
.name { .name {
display: block; display: block;
font-size: 28rpx; font-size: 28rpx;
font-weight: 800; line-height: 1.35;
font-weight: 900;
color: #0f172a; color: #0f172a;
} }
.meta { .meta {
margin-top: 10rpx; margin-top: 12rpx;
display: flex; display: flex;
gap: 10rpx; gap: 10rpx;
flex-wrap: wrap; flex-wrap: wrap;
} }
.pill { .pill {
padding: 8rpx 12rpx; padding: 8rpx 13rpx;
border-radius: 999rpx; border-radius: 999rpx;
background: #ffffff; background: #ffffff;
border: 1rpx solid rgba(15, 23, 42, 0.06); border: 1rpx solid rgba(15, 23, 42, 0.06);
font-size: 20rpx; font-size: 20rpx;
font-weight: 800;
color: #334155; color: #334155;
box-shadow: 0 6rpx 14rpx rgba(15, 23, 42, 0.04);
} }
.pill-muted { .pill-muted {
@@ -603,74 +814,94 @@ onShow(async () => {
.pill-up { .pill-up {
color: #0f766e; color: #0f766e;
background: rgba(204, 251, 241, 0.6); background: rgba(204, 251, 241, 0.72);
border-color: rgba(15, 118, 110, 0.12);
} }
.pill-down { .pill-down {
color: #b91c1c; color: #b91c1c;
background: rgba(254, 226, 226, 0.8); background: rgba(254, 226, 226, 0.86);
border-color: rgba(185, 28, 28, 0.1);
} }
.status { .status {
display: block; display: block;
margin-top: 10rpx; margin-top: 12rpx;
font-size: 22rpx; font-size: 22rpx;
line-height: 1.55;
color: #64748b; color: #64748b;
} }
.row-actions { .row-actions {
margin-top: 12rpx; margin-top: 14rpx;
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
} }
.mini-btn {
height: 56rpx;
line-height: 56rpx;
padding: 0 18rpx;
border-radius: 14rpx;
background: #ffffff;
border: 1rpx solid rgba(185, 28, 28, 0.28);
color: #b91c1c;
font-size: 22rpx;
font-weight: 700;
}
.footer { .footer {
margin-top: 20rpx; margin-top: 22rpx;
padding-bottom: 20rpx; padding-bottom: 20rpx;
display: flex; display: flex;
justify-content: center; justify-content: center;
} }
.btn { .btn,
height: 76rpx; .mini-btn {
line-height: 76rpx; box-sizing: border-box;
padding: 0 22rpx; border: 0;
border-radius: 18rpx; margin: 0;
background: linear-gradient(180deg, #1aa37a 0%, #0f766e 100%);
color: #ffffff;
font-size: 26rpx;
font-weight: 700;
} }
.btn[disabled] { .btn::after,
opacity: 0.6; .mini-btn::after {
border: 0;
}
.btn {
height: 78rpx;
line-height: 78rpx;
padding: 0 28rpx;
border-radius: 22rpx;
background: linear-gradient(135deg, #1aa37a 0%, #0f766e 100%);
color: #ffffff;
font-size: 26rpx;
font-weight: 900;
box-shadow: 0 12rpx 24rpx rgba(15, 118, 110, 0.18);
}
.btn[disabled],
.mini-btn[disabled] {
opacity: 0.55;
box-shadow: none;
} }
.btn-ghost { .btn-ghost {
background: #ffffff; background: rgba(255, 255, 255, 0.82);
color: #0f766e; color: #0f766e;
border: 1rpx solid rgba(15, 118, 110, 0.25); border: 1rpx solid rgba(15, 118, 110, 0.18);
box-shadow: none;
}
.mini-btn {
height: 58rpx;
line-height: 58rpx;
padding: 0 20rpx;
border-radius: 18rpx;
background: rgba(255, 255, 255, 0.9);
border: 1rpx solid rgba(185, 28, 28, 0.2);
color: #b91c1c;
font-size: 22rpx;
font-weight: 800;
} }
.mini-btn-neutral { .mini-btn-neutral {
border-color: rgba(15, 23, 42, 0.12); min-width: 144rpx;
border-color: rgba(15, 23, 42, 0.1);
color: #334155; color: #334155;
} }
.settings { .settings {
margin-top: 18rpx; margin-top: 22rpx;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16rpx; gap: 16rpx;
@@ -678,20 +909,20 @@ onShow(async () => {
.setting-row { .setting-row {
display: flex; display: flex;
align-items: flex-start; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 18rpx; gap: 20rpx;
padding: 14rpx 12rpx; padding: 20rpx;
border-radius: 18rpx; border-radius: 24rpx;
background: rgba(241, 245, 249, 0.45); background: rgba(248, 250, 252, 0.8);
border: 1rpx solid rgba(15, 23, 42, 0.04); border: 1rpx solid rgba(15, 23, 42, 0.05);
} }
.setting-label { .setting-label {
font-size: 24rpx; flex-shrink: 0;
font-weight: 800; font-size: 25rpx;
font-weight: 900;
color: #0f172a; color: #0f172a;
padding-top: 6rpx;
} }
.setting-control { .setting-control {
@@ -704,13 +935,15 @@ onShow(async () => {
} }
.num-input { .num-input {
width: 200rpx; width: 190rpx;
height: 64rpx; height: 66rpx;
padding: 0 14rpx; padding: 0 18rpx;
border-radius: 14rpx; border-radius: 18rpx;
background: rgba(255, 255, 255, 0.92); background: rgba(255, 255, 255, 0.96);
border: 1rpx solid rgba(15, 23, 42, 0.08); border: 1rpx solid rgba(15, 23, 42, 0.08);
font-size: 26rpx; font-size: 27rpx;
font-weight: 800;
color: #0f172a;
text-align: right; text-align: right;
} }
@@ -726,15 +959,26 @@ onShow(async () => {
gap: 14rpx; gap: 14rpx;
} }
.setting-actions .btn {
flex: 1;
}
.settings-note { .settings-note {
padding: 18rpx 20rpx;
border-radius: 22rpx;
background: rgba(236, 253, 245, 0.7);
font-size: 22rpx; font-size: 22rpx;
line-height: 1.6; line-height: 1.65;
color: #64748b; color: #64748b;
} }
.run-result { .run-result {
padding: 18rpx 20rpx;
border-radius: 22rpx;
background: rgba(204, 251, 241, 0.64);
font-size: 22rpx; font-size: 22rpx;
line-height: 1.6;
color: #0f766e; color: #0f766e;
font-weight: 700; font-weight: 900;
} }
</style> </style>
+2 -2
View File
@@ -3,8 +3,8 @@
* 所有页面自动注入,无需手动 import * 所有页面自动注入,无需手动 import
*/ */
@import './styles/variables'; @import '@/styles/_variables.scss';
@import './styles/mixins'; @import '@/styles/_mixins.scss';
/* 覆盖 uni-app 默认颜色变量 */ /* 覆盖 uni-app 默认颜色变量 */
$uni-color-primary: $color-primary-dark; $uni-color-primary: $color-primary-dark;