1111
This commit is contained in:
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||||
<meta name="theme-color" content="#0c5045" />
|
<meta name="theme-color" content="#0c5045" />
|
||||||
<title>常青树健康管理</title>
|
<title>常青树健康管理</title>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const currentTab = computed(() => {
|
||||||
|
const path = route.path
|
||||||
|
if (path.startsWith('/assessment')) return 'assessment'
|
||||||
|
if (path.startsWith('/profile')) return 'profile'
|
||||||
|
return 'home'
|
||||||
|
})
|
||||||
|
|
||||||
|
function navigate(target: string) {
|
||||||
|
if (target === 'home' && route.path !== '/dashboard') {
|
||||||
|
router.push('/dashboard')
|
||||||
|
} else if (target === 'assessment' && route.path !== '/assessment') {
|
||||||
|
router.push('/assessment')
|
||||||
|
} else if (target === 'profile' && route.path !== '/profile') {
|
||||||
|
router.push('/profile')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<nav class="app-tab-bar">
|
||||||
|
<div
|
||||||
|
class="tab-item"
|
||||||
|
:class="{ active: currentTab === 'home' }"
|
||||||
|
@click="navigate('home')"
|
||||||
|
>
|
||||||
|
<div class="tab-icon">
|
||||||
|
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
|
||||||
|
<polyline points="9 22 9 12 15 12 15 22" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span class="tab-label">首页</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="tab-item"
|
||||||
|
:class="{ active: currentTab === 'assessment' }"
|
||||||
|
@click="navigate('assessment')"
|
||||||
|
>
|
||||||
|
<div class="tab-icon">
|
||||||
|
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
||||||
|
<polyline points="14 2 14 8 20 8" />
|
||||||
|
<path d="M9 13h6" />
|
||||||
|
<path d="M9 17h6" />
|
||||||
|
<polyline points="10 9 12 11 14 9" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span class="tab-label">健康评估</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="tab-item"
|
||||||
|
:class="{ active: currentTab === 'profile' }"
|
||||||
|
@click="navigate('profile')"
|
||||||
|
>
|
||||||
|
<div class="tab-icon">
|
||||||
|
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
|
||||||
|
<circle cx="12" cy="7" r="4" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span class="tab-label">我的</span>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.app-tab-bar {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 480px;
|
||||||
|
height: 60px;
|
||||||
|
background: rgba(255, 255, 255, 0.96);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
border-top: 1px solid rgba(18, 63, 55, 0.08);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 99;
|
||||||
|
padding-bottom: env(safe-area-inset-bottom, 0px);
|
||||||
|
box-shadow: 0 -4px 16px rgba(18, 63, 55, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex: 1;
|
||||||
|
height: 100%;
|
||||||
|
color: #7d8e87;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-icon {
|
||||||
|
margin-bottom: 2px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-label {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item.active {
|
||||||
|
color: #0c5045;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item.active .tab-icon {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item.active .tab-label {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+7
-1
@@ -2,6 +2,9 @@ import { createRouter, createWebHistory } from 'vue-router'
|
|||||||
import AuthView from './views/AuthView.vue'
|
import AuthView from './views/AuthView.vue'
|
||||||
import OnboardingView from './views/OnboardingView.vue'
|
import OnboardingView from './views/OnboardingView.vue'
|
||||||
import DashboardView from './views/DashboardView.vue'
|
import DashboardView from './views/DashboardView.vue'
|
||||||
|
import AssessmentView from './views/AssessmentView.vue'
|
||||||
|
import ProfileView from './views/ProfileView.vue'
|
||||||
|
import DeviceControlView from './views/DeviceControlView.vue'
|
||||||
import { hasToken } from './services/auth'
|
import { hasToken } from './services/auth'
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
@@ -10,7 +13,10 @@ const router = createRouter({
|
|||||||
{ path: '/', redirect: '/dashboard' },
|
{ path: '/', redirect: '/dashboard' },
|
||||||
{ path: '/auth', component: AuthView, meta: { guest: true } },
|
{ path: '/auth', component: AuthView, meta: { guest: true } },
|
||||||
{ path: '/onboarding', component: OnboardingView },
|
{ path: '/onboarding', component: OnboardingView },
|
||||||
{ path: '/dashboard', component: DashboardView }
|
{ path: '/dashboard', component: DashboardView },
|
||||||
|
{ path: '/assessment', component: AssessmentView },
|
||||||
|
{ path: '/profile', component: ProfileView },
|
||||||
|
{ path: '/device-control', component: DeviceControlView }
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
+80
-1
@@ -40,11 +40,63 @@ export type HealthPlan = {
|
|||||||
items: PlanItem[]
|
items: PlanItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PersonalizedSceneConfig = {
|
||||||
|
targetHumidity: number
|
||||||
|
dehumidifierMode: string
|
||||||
|
fanLevel: string
|
||||||
|
enableUv: boolean
|
||||||
|
lightBrightnessPercent: number
|
||||||
|
lightColorTemperatureKelvin: number
|
||||||
|
mattressTargetTemp: number
|
||||||
|
mattressLeftFirmness: number
|
||||||
|
mattressRightFirmness: number
|
||||||
|
mattressHeatingLevel: number
|
||||||
|
mattressMode: string
|
||||||
|
enableFragrance: boolean
|
||||||
|
fragranceLevel: number
|
||||||
|
enableSpeakerSleepMode: boolean
|
||||||
|
speakerAudioMode: string
|
||||||
|
speakerVolumePercent: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HealthAssessmentAnswers = {
|
||||||
|
sleepDuration: string
|
||||||
|
fallingAsleepSpeed: string
|
||||||
|
temperaturePreference: string
|
||||||
|
nightAwakening: string
|
||||||
|
mattressPreference: string
|
||||||
|
exerciseFrequency: string
|
||||||
|
muscleFatigueArea: string
|
||||||
|
activeTimeOfDay: string
|
||||||
|
mealRegularity: string
|
||||||
|
flavorPreference: string
|
||||||
|
lateNightEating: string
|
||||||
|
dailyWaterIntake: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SaveHealthAssessmentInput = {
|
||||||
|
basicInfo: HealthProfile
|
||||||
|
answers: HealthAssessmentAnswers
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HealthAssessmentOutput = {
|
||||||
|
hasCompletedAssessment: boolean
|
||||||
|
basicInfo: HealthProfile
|
||||||
|
answers: HealthAssessmentAnswers
|
||||||
|
sceneConfig: PersonalizedSceneConfig
|
||||||
|
bmi?: number | null
|
||||||
|
bmiStatus: string
|
||||||
|
}
|
||||||
|
|
||||||
export type Dashboard = {
|
export type Dashboard = {
|
||||||
user: HealthUser
|
user: HealthUser
|
||||||
profile?: HealthProfile
|
profile?: HealthProfile
|
||||||
selectedModules: string[]
|
selectedModules: string[]
|
||||||
currentPlan?: HealthPlan
|
currentPlan?: HealthPlan
|
||||||
|
hasCompletedAssessment: boolean
|
||||||
|
bmi?: number | null
|
||||||
|
bmiStatus?: string
|
||||||
|
sceneConfig?: PersonalizedSceneConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
export type HealthDeviceCategory =
|
export type HealthDeviceCategory =
|
||||||
@@ -131,6 +183,14 @@ export type HomeAssistantStatus = {
|
|||||||
sceneDevices: HomeAssistantSceneDevicesSummary
|
sceneDevices: HomeAssistantSceneDevicesSummary
|
||||||
environment: HomeAssistantEnvironmentData
|
environment: HomeAssistantEnvironmentData
|
||||||
scenePresets?: HomeAssistantScenePreset[]
|
scenePresets?: HomeAssistantScenePreset[]
|
||||||
|
controlParameters?: Record<string, HomeAssistantParameterState>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HomeAssistantParameterState = {
|
||||||
|
entityId: string
|
||||||
|
state: string
|
||||||
|
available: boolean
|
||||||
|
attributes?: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type HomeAssistantSceneAction = {
|
export type HomeAssistantSceneAction = {
|
||||||
@@ -150,6 +210,20 @@ export type HomeAssistantSceneResult = {
|
|||||||
actions: HomeAssistantSceneAction[]
|
actions: HomeAssistantSceneAction[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type HomeAssistantSetParameterInput = {
|
||||||
|
deviceId: string
|
||||||
|
entityId?: string
|
||||||
|
parameterType: string
|
||||||
|
value: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HomeAssistantSetParameterOutput = {
|
||||||
|
success: boolean
|
||||||
|
deviceId: string
|
||||||
|
parameterType: string
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
export const healthApi = {
|
export const healthApi = {
|
||||||
register: (payload: { phone: string; password: string; displayName: string }) =>
|
register: (payload: { phone: string; password: string; displayName: string }) =>
|
||||||
http.post<never, AuthOutput>('/api/health/auth/register', payload),
|
http.post<never, AuthOutput>('/api/health/auth/register', payload),
|
||||||
@@ -161,6 +235,9 @@ export const healthApi = {
|
|||||||
saveModules: (moduleCodes: string[]) =>
|
saveModules: (moduleCodes: string[]) =>
|
||||||
http.put<never, string[]>('/api/health/modules', { moduleCodes }),
|
http.put<never, string[]>('/api/health/modules', { moduleCodes }),
|
||||||
generatePlan: () => http.post<never, HealthPlan>('/api/health/plans/generate'),
|
generatePlan: () => http.post<never, HealthPlan>('/api/health/plans/generate'),
|
||||||
|
getAssessment: () => http.get<never, HealthAssessmentOutput>('/api/health/assessment'),
|
||||||
|
saveAssessment: (payload: SaveHealthAssessmentInput) =>
|
||||||
|
http.post<never, HealthAssessmentOutput>('/api/health/assessment', payload),
|
||||||
homeAssistantStatus: () =>
|
homeAssistantStatus: () =>
|
||||||
http.get<never, HomeAssistantStatus>('/api/health/home-assistant/status'),
|
http.get<never, HomeAssistantStatus>('/api/health/home-assistant/status'),
|
||||||
executeSleepScene: () =>
|
executeSleepScene: () =>
|
||||||
@@ -168,5 +245,7 @@ export const healthApi = {
|
|||||||
executeAllOffScene: () =>
|
executeAllOffScene: () =>
|
||||||
http.post<never, HomeAssistantSceneResult>('/api/health/home-assistant/scenes/all-off'),
|
http.post<never, HomeAssistantSceneResult>('/api/health/home-assistant/scenes/all-off'),
|
||||||
switchDevice: (payload: { deviceId: string; turnOn: boolean; entityId?: string }) =>
|
switchDevice: (payload: { deviceId: string; turnOn: boolean; entityId?: string }) =>
|
||||||
http.post<never, HomeAssistantDeviceSwitchOutput>('/api/health/home-assistant/devices/switch', payload)
|
http.post<never, HomeAssistantDeviceSwitchOutput>('/api/health/home-assistant/devices/switch', payload),
|
||||||
|
setParameter: (payload: HomeAssistantSetParameterInput) =>
|
||||||
|
http.post<never, HomeAssistantSetParameterOutput>('/api/health/home-assistant/devices/set-parameter', payload)
|
||||||
}
|
}
|
||||||
|
|||||||
+200
-1
@@ -11,7 +11,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
* { box-sizing: border-box; }
|
* { box-sizing: border-box; }
|
||||||
body { margin: 0; min-width: 320px; min-height: 100vh; background: #f2f0e9; }
|
body { margin: 0; min-width: 320px; min-height: 100vh; background: #e8e6df; }
|
||||||
|
#app {
|
||||||
|
max-width: 480px;
|
||||||
|
margin: 0 auto;
|
||||||
|
min-height: 100vh;
|
||||||
|
position: relative;
|
||||||
|
background: #f2f0e9;
|
||||||
|
box-shadow: 0 0 30px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
button, input, textarea { font: inherit; }
|
button, input, textarea { font: inherit; }
|
||||||
button { cursor: pointer; }
|
button { cursor: pointer; }
|
||||||
|
|
||||||
@@ -772,3 +780,194 @@ button { cursor: pointer; }
|
|||||||
.scene-actions { width: 100%; margin-top: 6px; }
|
.scene-actions { width: 100%; margin-top: 6px; }
|
||||||
.scene-actions button { flex: 1; }
|
.scene-actions button { flex: 1; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dashboard-page {
|
||||||
|
padding-bottom: 84px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 评估未填提醒卡片 */
|
||||||
|
.assessment-guide-banner {
|
||||||
|
background: linear-gradient(135deg, #b37318, #92580c);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 18px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
box-shadow: 0 6px 18px rgba(179, 115, 24, 0.22);
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.assessment-guide-banner::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
right: -20px;
|
||||||
|
bottom: -20px;
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
background: radial-gradient(circle, rgba(255,255,255,0.2) 0%, transparent 70%);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
.guide-badge {
|
||||||
|
display: inline-block;
|
||||||
|
background: rgba(255, 255, 255, 0.22);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.guide-body {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.guide-text h3 {
|
||||||
|
margin: 0 0 4px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.guide-text p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.88);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
.guide-btn {
|
||||||
|
white-space: nowrap;
|
||||||
|
padding: 8px 14px;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 0;
|
||||||
|
background: #ffffff;
|
||||||
|
color: #92580c;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 个性化舒睡启动参数卡片 */
|
||||||
|
.personalized-startup-card {
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid rgba(12, 80, 69, 0.12);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
box-shadow: 0 4px 16px rgba(18, 63, 55, 0.04);
|
||||||
|
}
|
||||||
|
.customized-tag {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #0c5045;
|
||||||
|
background: #eef6f3;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.startup-card-desc {
|
||||||
|
margin: 4px 0 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #6b7d76;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.startup-params-tags {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
.startup-param-badge {
|
||||||
|
background: #f7f6f2;
|
||||||
|
border: 1px solid rgba(18, 63, 55, 0.08);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
}
|
||||||
|
.startup-param-badge .lbl {
|
||||||
|
display: block;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #849690;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
.startup-param-badge strong {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #173d36;
|
||||||
|
}
|
||||||
|
.detail-entry-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
background: #f8f7f2;
|
||||||
|
border: 1px solid rgba(18, 63, 55, 0.08);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
margin-top: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.detail-entry-bar:active {
|
||||||
|
background: #f0eee7;
|
||||||
|
}
|
||||||
|
.entry-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.entry-left .icon {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
.entry-left strong {
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #173d36;
|
||||||
|
}
|
||||||
|
.entry-left small {
|
||||||
|
display: block;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #82928c;
|
||||||
|
}
|
||||||
|
.detail-entry-bar .arrow {
|
||||||
|
color: #a0ada7;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 7 台设备入口条 */
|
||||||
|
.all-control-banner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
background: linear-gradient(135deg, #edf6f3, #e5f0ec);
|
||||||
|
border: 1px solid rgba(12, 80, 69, 0.16);
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
margin-top: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.all-control-banner:active {
|
||||||
|
background: #dbeef7;
|
||||||
|
}
|
||||||
|
.ac-text {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.ac-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
.ac-text strong {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #0c5045;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.ac-text small {
|
||||||
|
display: block;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #55756d;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
.ac-arrow {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #0c5045;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@
|
|||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { Button as VanButton, showToast } from 'vant'
|
import { Button as VanButton, showToast } from 'vant'
|
||||||
|
import AppTabBar from '../components/AppTabBar.vue'
|
||||||
import { clearTokens } from '../services/auth'
|
import { clearTokens } from '../services/auth'
|
||||||
import {
|
import {
|
||||||
healthApi,
|
healthApi,
|
||||||
@@ -278,6 +279,61 @@ onMounted(load)
|
|||||||
<section v-if="loading" class="section-card loading-card">正在读取今日健康方案…</section>
|
<section v-if="loading" class="section-card loading-card">正在读取今日健康方案…</section>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
|
<!-- 未完成评估提示横幅 -->
|
||||||
|
<section v-if="!dashboard?.hasCompletedAssessment" class="assessment-guide-banner" @click="router.push('/assessment')">
|
||||||
|
<div class="guide-badge">重要提醒</div>
|
||||||
|
<div class="guide-body">
|
||||||
|
<div class="guide-text">
|
||||||
|
<h3>先填写个人基本信息与身体评估</h3>
|
||||||
|
<p>尚未进行身体及睡眠质量、运动情况、饮食情况三大维度评估。完成评估后将为您专属定制全套智能家居启动参数!</p>
|
||||||
|
</div>
|
||||||
|
<button class="guide-btn" type="button">去评估 ></button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 个性化智能家居启动参数概览 (已完成评估) -->
|
||||||
|
<section v-else class="section-card personalized-startup-card">
|
||||||
|
<div class="section-title">
|
||||||
|
<span>专属舒睡启动参数</span>
|
||||||
|
<span class="customized-tag">根据身体评估专属定制</span>
|
||||||
|
</div>
|
||||||
|
<p class="startup-card-desc">依据您的冷热偏好、入睡状态与骨骼肌肉承托需求,智能中枢已计算出全套设备预设:</p>
|
||||||
|
|
||||||
|
<div class="startup-params-tags">
|
||||||
|
<div class="startup-param-badge">
|
||||||
|
<span class="lbl">恒湿目标</span>
|
||||||
|
<strong>{{ dashboard?.sceneConfig?.targetHumidity || 55 }}% · {{ dashboard?.sceneConfig?.fanLevel || '1挡' }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="startup-param-badge">
|
||||||
|
<span class="lbl">助眠灯光</span>
|
||||||
|
<strong>{{ dashboard?.sceneConfig?.lightBrightnessPercent || 20 }}% · {{ dashboard?.sceneConfig?.lightColorTemperatureKelvin || 2700 }}K</strong>
|
||||||
|
</div>
|
||||||
|
<div class="startup-param-badge">
|
||||||
|
<span class="lbl">助眠音箱</span>
|
||||||
|
<strong>{{ dashboard?.sceneConfig?.enableSpeakerSleepMode ? '开启睡眠模式' : '关闭睡眠模式' }} · {{ dashboard?.sceneConfig?.speakerVolumePercent || 15 }}%</strong>
|
||||||
|
</div>
|
||||||
|
<div class="startup-param-badge">
|
||||||
|
<span class="lbl">8H 床垫</span>
|
||||||
|
<strong>左右加热 {{ dashboard?.sceneConfig?.mattressHeatingLevel ?? 1 }} 档 · {{ dashboard?.sceneConfig?.mattressMode || '智能适配' }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="startup-param-badge" v-if="dashboard?.sceneConfig?.enableUv">
|
||||||
|
<span class="lbl">除湿消杀</span>
|
||||||
|
<strong style="color: #7b42b5;">UV 深度消杀已开启</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-entry-bar" @click="router.push('/device-control')">
|
||||||
|
<div class="entry-left">
|
||||||
|
<span class="icon">⚙️</span>
|
||||||
|
<div>
|
||||||
|
<strong>进入设备详情与独立参数调节</strong>
|
||||||
|
<small>显示每个设备在线状态,可单独开关与精细滑块调节</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="arrow">></span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="hero-card">
|
<section class="hero-card">
|
||||||
<p class="eyebrow">今日健康建议</p>
|
<p class="eyebrow">今日健康建议</p>
|
||||||
<h2>{{ dashboard?.currentPlan?.title || '尚未生成健康方案' }}</h2>
|
<h2>{{ dashboard?.currentPlan?.title || '尚未生成健康方案' }}</h2>
|
||||||
@@ -633,10 +689,24 @@ onMounted(load)
|
|||||||
<div v-else style="padding: 16px; text-align: center; color: #82908c; font-size: 12px;">
|
<div v-else style="padding: 16px; text-align: center; color: #82908c; font-size: 12px;">
|
||||||
暂无已发现的设备
|
暂无已发现的设备
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="all-control-banner" @click="router.push('/device-control')">
|
||||||
|
<div class="ac-text">
|
||||||
|
<span class="ac-icon">🎛️</span>
|
||||||
|
<div>
|
||||||
|
<strong>查看全部 {{ totalDeviceCount }} 台设备状态与独立参数控制</strong>
|
||||||
|
<small>支持湿度、色温、左右加热档位与承托模式单独调节</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="ac-arrow">></span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<p class="medical-note">健康建议仅用于日常管理演示,不构成医疗诊断或治疗意见。</p>
|
<p class="medical-note">健康建议仅用于日常管理演示,不构成医疗诊断或治疗意见。</p>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- 固定底部导航栏 -->
|
||||||
|
<AppTabBar />
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,931 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, reactive, ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { Button as VanButton, showToast, Slider as VanSlider } from 'vant'
|
||||||
|
import {
|
||||||
|
healthApi,
|
||||||
|
type HomeAssistantDiscoveredDevice,
|
||||||
|
type HomeAssistantStatus
|
||||||
|
} from '../services/health'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const loading = ref(true)
|
||||||
|
const haStatus = ref<HomeAssistantStatus>()
|
||||||
|
const sceneRunning = ref<'sleep' | 'all-off'>()
|
||||||
|
const settingParam = ref<string | null>(null)
|
||||||
|
const switchingDeviceId = ref<string | null>(null)
|
||||||
|
|
||||||
|
// 本地参数临时状态映射
|
||||||
|
const localParams = reactive({
|
||||||
|
// 除湿机
|
||||||
|
dehumidifierHumidity: 55,
|
||||||
|
dehumidifierMode: '睡眠',
|
||||||
|
dehumidifierFan: '1挡',
|
||||||
|
dehumidifierUv: false,
|
||||||
|
|
||||||
|
// 氛围灯
|
||||||
|
lightBrightness: 20,
|
||||||
|
lightColorTemp: 2700,
|
||||||
|
|
||||||
|
// 音箱
|
||||||
|
speakerVolume: 15,
|
||||||
|
speakerSleepMode: true,
|
||||||
|
|
||||||
|
// 床垫
|
||||||
|
mattressLeftHeating: 1,
|
||||||
|
mattressRightHeating: 1,
|
||||||
|
mattressLeftMode: '智能适配',
|
||||||
|
mattressRightMode: '智能适配'
|
||||||
|
})
|
||||||
|
|
||||||
|
function numberValue(value: unknown): number | undefined {
|
||||||
|
if (typeof value === 'number' && Number.isFinite(value)) return value
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const parsed = Number.parseFloat(value)
|
||||||
|
return Number.isFinite(parsed) ? parsed : undefined
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadStatus() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
haStatus.value = await healthApi.homeAssistantStatus()
|
||||||
|
const controls = haStatus.value?.controlParameters ?? {}
|
||||||
|
const humidifier = controls.humidity
|
||||||
|
const humidity = numberValue(humidifier?.attributes?.humidity)
|
||||||
|
if (humidity !== undefined) localParams.dehumidifierHumidity = humidity
|
||||||
|
if (typeof humidifier?.attributes?.mode === 'string') localParams.dehumidifierMode = humidifier.attributes.mode
|
||||||
|
if (controls.fan_level?.state) localParams.dehumidifierFan = controls.fan_level.state.replace('档', '挡')
|
||||||
|
localParams.dehumidifierUv = controls.uv?.state === 'on'
|
||||||
|
|
||||||
|
const brightness = numberValue(controls.brightness?.attributes?.brightness)
|
||||||
|
if (brightness !== undefined) localParams.lightBrightness = Math.round(brightness / 255 * 100)
|
||||||
|
const kelvin = numberValue(controls.color_temp?.attributes?.color_temp_kelvin)
|
||||||
|
if (kelvin !== undefined) localParams.lightColorTemp = kelvin
|
||||||
|
|
||||||
|
const volume = numberValue(controls.volume?.attributes?.volume_level)
|
||||||
|
if (volume !== undefined) localParams.speakerVolume = Math.round(volume * 100)
|
||||||
|
localParams.speakerSleepMode = controls.speaker_sleep_mode?.state === 'on'
|
||||||
|
|
||||||
|
localParams.mattressLeftHeating = numberValue(controls.mattress_left_heating?.state) ?? 1
|
||||||
|
localParams.mattressRightHeating = numberValue(controls.mattress_right_heating?.state) ?? 1
|
||||||
|
if (controls.mattress_left_mode?.state) localParams.mattressLeftMode = controls.mattress_left_mode.state
|
||||||
|
if (controls.mattress_right_mode?.state) localParams.mattressRightMode = controls.mattress_right_mode.state
|
||||||
|
} catch (err) {
|
||||||
|
showToast(err instanceof Error ? err.message : '获取设备状态失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找具体设备
|
||||||
|
const dehumidifierDevice = computed(() =>
|
||||||
|
haStatus.value?.devices.find(d => d.category === 'EnvironmentControl')
|
||||||
|
)
|
||||||
|
const lightDevice = computed(() =>
|
||||||
|
haStatus.value?.devices.find(d => d.category === 'Lighting')
|
||||||
|
)
|
||||||
|
const speakerDevice = computed(() =>
|
||||||
|
haStatus.value?.devices.find(d => d.category === 'Media')
|
||||||
|
)
|
||||||
|
const fragranceDevice = computed(() =>
|
||||||
|
haStatus.value?.devices.find(d => d.category === 'Fragrance')
|
||||||
|
)
|
||||||
|
const mattressDevice = computed(() =>
|
||||||
|
haStatus.value?.devices.find(d => d.category === 'Sleep')
|
||||||
|
)
|
||||||
|
const sensorDevice = computed(() =>
|
||||||
|
haStatus.value?.devices.find(d => d.category === 'EnvironmentSensor')
|
||||||
|
)
|
||||||
|
const scaleDevice = computed(() =>
|
||||||
|
haStatus.value?.devices.find(d => d.category === 'BodyComposition')
|
||||||
|
)
|
||||||
|
|
||||||
|
// 判断设备是否开启
|
||||||
|
function isDeviceTurnedOn(device?: HomeAssistantDiscoveredDevice): boolean {
|
||||||
|
if (!device || !device.primaryControlEntityId) return false
|
||||||
|
const target = device.entities.find(e => e.entityId === device.primaryControlEntityId)
|
||||||
|
if (!target) return false
|
||||||
|
return target.state === 'on' || target.state === 'playing'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 单独开关设备
|
||||||
|
async function toggleDevice(device?: HomeAssistantDiscoveredDevice) {
|
||||||
|
if (!device || !device.primaryControlEntityId) {
|
||||||
|
showToast('该设备暂无可控主开关')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const nextOn = !isDeviceTurnedOn(device)
|
||||||
|
switchingDeviceId.value = device.deviceId
|
||||||
|
try {
|
||||||
|
const res = await healthApi.switchDevice({
|
||||||
|
deviceId: device.deviceId,
|
||||||
|
turnOn: nextOn,
|
||||||
|
entityId: device.primaryControlEntityId
|
||||||
|
})
|
||||||
|
showToast({
|
||||||
|
type: res.success ? 'success' : 'fail',
|
||||||
|
message: res.message || (nextOn ? '已开启' : '已关闭')
|
||||||
|
})
|
||||||
|
haStatus.value = await healthApi.homeAssistantStatus()
|
||||||
|
} catch (err) {
|
||||||
|
showToast(err instanceof Error ? err.message : '控制失败')
|
||||||
|
} finally {
|
||||||
|
switchingDeviceId.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调节参数 API
|
||||||
|
async function applyParam(deviceId: string, paramType: string, val: unknown, entityId?: string) {
|
||||||
|
const key = `${deviceId}_${paramType}`
|
||||||
|
settingParam.value = key
|
||||||
|
try {
|
||||||
|
const res = await healthApi.setParameter({
|
||||||
|
deviceId,
|
||||||
|
entityId,
|
||||||
|
parameterType: paramType,
|
||||||
|
value: val
|
||||||
|
})
|
||||||
|
showToast({
|
||||||
|
type: res.success ? 'success' : 'fail',
|
||||||
|
message: res.message || '参数设置成功'
|
||||||
|
})
|
||||||
|
await loadStatus()
|
||||||
|
} catch (err) {
|
||||||
|
showToast(err instanceof Error ? err.message : '参数设置失败')
|
||||||
|
} finally {
|
||||||
|
settingParam.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 一键场景
|
||||||
|
async function executeScene(scene: 'sleep' | 'all-off') {
|
||||||
|
sceneRunning.value = scene
|
||||||
|
try {
|
||||||
|
const res = scene === 'sleep'
|
||||||
|
? await healthApi.executeSleepScene()
|
||||||
|
: await healthApi.executeAllOffScene()
|
||||||
|
showToast({ type: res.success ? 'success' : 'fail', message: res.message })
|
||||||
|
haStatus.value = await healthApi.homeAssistantStatus()
|
||||||
|
} catch (err) {
|
||||||
|
showToast(err instanceof Error ? err.message : '场景执行失败')
|
||||||
|
} finally {
|
||||||
|
sceneRunning.value = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadStatus()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="control-page">
|
||||||
|
<!-- 顶部返回条 -->
|
||||||
|
<header class="top-nav">
|
||||||
|
<button class="back-btn" @click="router.back()">
|
||||||
|
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5">
|
||||||
|
<polyline points="15 18 9 12 15 6" />
|
||||||
|
</svg>
|
||||||
|
<span>返回首页</span>
|
||||||
|
</button>
|
||||||
|
<h2>智能设备详情与参数调节</h2>
|
||||||
|
<button class="refresh-btn" @click="loadStatus">刷新</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="control-content">
|
||||||
|
<!-- 汇总状态与一键总控 -->
|
||||||
|
<section class="summary-box">
|
||||||
|
<div class="summary-info">
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="num">{{ haStatus?.summary.totalDevices ?? 7 }}</span>
|
||||||
|
<span class="label">已接入设备</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="num online">{{ haStatus?.summary.onlineDevices ?? 0 }}</span>
|
||||||
|
<span class="label">当前在线</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="num">{{ haStatus?.summary.totalEntities ?? 0 }}</span>
|
||||||
|
<span class="label">实体能力</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="scene-actions-grid">
|
||||||
|
<button
|
||||||
|
class="scene-btn sleep-btn"
|
||||||
|
:disabled="sceneRunning !== undefined"
|
||||||
|
@click="executeScene('sleep')"
|
||||||
|
>
|
||||||
|
<div class="s-icon">🌙</div>
|
||||||
|
<div class="s-text">
|
||||||
|
<strong>一键启动全屋舒睡模式</strong>
|
||||||
|
<small>协同控制除湿、香氛、灯光、音箱与床垫</small>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="scene-btn off-btn"
|
||||||
|
:disabled="sceneRunning !== undefined"
|
||||||
|
@click="executeScene('all-off')"
|
||||||
|
>
|
||||||
|
<div class="s-icon">⚪</div>
|
||||||
|
<div class="s-text">
|
||||||
|
<strong>一键关闭设备</strong>
|
||||||
|
<small>安全待机休眠</small>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 设备 1:米家变频净化除湿机 Max -->
|
||||||
|
<section class="device-card" v-if="dehumidifierDevice">
|
||||||
|
<div class="device-head">
|
||||||
|
<div class="dev-info">
|
||||||
|
<span class="category-tag">恒湿净化</span>
|
||||||
|
<h3>{{ dehumidifierDevice.name }}</h3>
|
||||||
|
<span class="status-dot" :class="{ online: dehumidifierDevice.online }">
|
||||||
|
{{ dehumidifierDevice.online ? '设备在线' : '设备离线' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<VanButton
|
||||||
|
size="small"
|
||||||
|
round
|
||||||
|
:type="isDeviceTurnedOn(dehumidifierDevice) ? 'primary' : 'default'"
|
||||||
|
:color="isDeviceTurnedOn(dehumidifierDevice) ? '#0c5045' : ''"
|
||||||
|
:loading="switchingDeviceId === dehumidifierDevice.deviceId"
|
||||||
|
@click="toggleDevice(dehumidifierDevice)"
|
||||||
|
>
|
||||||
|
{{ isDeviceTurnedOn(dehumidifierDevice) ? '已开启' : '已关闭' }}
|
||||||
|
</VanButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="params-body">
|
||||||
|
<!-- 目标湿度滑块 -->
|
||||||
|
<div class="param-row">
|
||||||
|
<div class="param-header">
|
||||||
|
<span class="p-title">恒湿目标湿度</span>
|
||||||
|
<strong class="p-val">{{ localParams.dehumidifierHumidity }}%</strong>
|
||||||
|
</div>
|
||||||
|
<VanSlider
|
||||||
|
v-model="localParams.dehumidifierHumidity"
|
||||||
|
:min="40"
|
||||||
|
:max="70"
|
||||||
|
:step="1"
|
||||||
|
active-color="#0c5045"
|
||||||
|
@change="applyParam(dehumidifierDevice.deviceId, 'humidity', localParams.dehumidifierHumidity)"
|
||||||
|
/>
|
||||||
|
<div class="slider-scale">
|
||||||
|
<span>40% 干爽</span>
|
||||||
|
<span>55% 舒适</span>
|
||||||
|
<span>70% 润泽</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 运行模式切换 -->
|
||||||
|
<div class="param-row">
|
||||||
|
<span class="p-title">除湿运行模式</span>
|
||||||
|
<div class="btn-group">
|
||||||
|
<button
|
||||||
|
v-for="mode in ['睡眠', '智能']"
|
||||||
|
:key="mode"
|
||||||
|
:class="{ active: localParams.dehumidifierMode === mode }"
|
||||||
|
@click="localParams.dehumidifierMode = mode; applyParam(dehumidifierDevice.deviceId, 'dehumidifier_mode', mode)"
|
||||||
|
>{{ mode }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 风速切换 -->
|
||||||
|
<div class="param-row">
|
||||||
|
<span class="p-title">除湿机风速档位</span>
|
||||||
|
<div class="btn-group">
|
||||||
|
<button
|
||||||
|
v-for="fan in ['1挡', '2挡', '3挡']"
|
||||||
|
:key="fan"
|
||||||
|
:class="{ active: localParams.dehumidifierFan === fan }"
|
||||||
|
@click="localParams.dehumidifierFan = fan; applyParam(dehumidifierDevice.deviceId, 'fan_level', fan)"
|
||||||
|
>
|
||||||
|
{{ fan }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- UV 杀菌开关 -->
|
||||||
|
<div class="param-row inline-row">
|
||||||
|
<div>
|
||||||
|
<div class="p-title">UV 深度紫外消杀</div>
|
||||||
|
<small class="p-tip">夜间杀菌净化,除湿同时清新空气</small>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="mini-toggle"
|
||||||
|
:class="{ active: localParams.dehumidifierUv }"
|
||||||
|
@click="localParams.dehumidifierUv = !localParams.dehumidifierUv; applyParam(dehumidifierDevice.deviceId, 'uv', localParams.dehumidifierUv)"
|
||||||
|
>
|
||||||
|
{{ localParams.dehumidifierUv ? '已开启' : '已关闭' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 设备 2:Lemesh 色温灯 -->
|
||||||
|
<section class="device-card" v-if="lightDevice">
|
||||||
|
<div class="device-head">
|
||||||
|
<div class="dev-info">
|
||||||
|
<span class="category-tag">卧室灯光</span>
|
||||||
|
<h3>{{ lightDevice.name }}</h3>
|
||||||
|
<span class="status-dot" :class="{ online: lightDevice.online }">
|
||||||
|
{{ lightDevice.online ? '设备在线' : '设备离线' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<VanButton
|
||||||
|
size="small"
|
||||||
|
round
|
||||||
|
:type="isDeviceTurnedOn(lightDevice) ? 'primary' : 'default'"
|
||||||
|
:color="isDeviceTurnedOn(lightDevice) ? '#0c5045' : ''"
|
||||||
|
:loading="switchingDeviceId === lightDevice.deviceId"
|
||||||
|
@click="toggleDevice(lightDevice)"
|
||||||
|
>
|
||||||
|
{{ isDeviceTurnedOn(lightDevice) ? '已点亮' : '已熄灭' }}
|
||||||
|
</VanButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="params-body">
|
||||||
|
<!-- 亮度滑块 -->
|
||||||
|
<div class="param-row">
|
||||||
|
<div class="param-header">
|
||||||
|
<span class="p-title">助眠灯光亮度</span>
|
||||||
|
<strong class="p-val">{{ localParams.lightBrightness }}%</strong>
|
||||||
|
</div>
|
||||||
|
<VanSlider
|
||||||
|
v-model="localParams.lightBrightness"
|
||||||
|
:min="1"
|
||||||
|
:max="100"
|
||||||
|
active-color="#d6a657"
|
||||||
|
@change="applyParam(lightDevice.deviceId, 'brightness', localParams.lightBrightness)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 色温滑块 -->
|
||||||
|
<div class="param-row">
|
||||||
|
<div class="param-header">
|
||||||
|
<span class="p-title">灯光色温调节</span>
|
||||||
|
<strong class="p-val">{{ localParams.lightColorTemp }}K</strong>
|
||||||
|
</div>
|
||||||
|
<VanSlider
|
||||||
|
v-model="localParams.lightColorTemp"
|
||||||
|
:min="2200"
|
||||||
|
:max="5000"
|
||||||
|
:step="50"
|
||||||
|
active-color="#c87f2b"
|
||||||
|
@change="applyParam(lightDevice.deviceId, 'color_temp', localParams.lightColorTemp)"
|
||||||
|
/>
|
||||||
|
<div class="slider-scale">
|
||||||
|
<span>2200K 超暖橙光 (利于褪黑素)</span>
|
||||||
|
<span>5000K 明亮白光</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 设备 3:Xiaomi Sound Pro 智能音箱 -->
|
||||||
|
<section class="device-card" v-if="speakerDevice">
|
||||||
|
<div class="device-head">
|
||||||
|
<div class="dev-info">
|
||||||
|
<span class="category-tag">睡眠音频</span>
|
||||||
|
<h3>{{ speakerDevice.name }}</h3>
|
||||||
|
<span class="status-dot" :class="{ online: speakerDevice.online }">
|
||||||
|
{{ speakerDevice.online ? '设备在线' : '设备离线' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<VanButton
|
||||||
|
size="small"
|
||||||
|
round
|
||||||
|
:type="isDeviceTurnedOn(speakerDevice) ? 'primary' : 'default'"
|
||||||
|
:color="isDeviceTurnedOn(speakerDevice) ? '#0c5045' : ''"
|
||||||
|
:loading="switchingDeviceId === speakerDevice.deviceId"
|
||||||
|
@click="toggleDevice(speakerDevice)"
|
||||||
|
>
|
||||||
|
{{ isDeviceTurnedOn(speakerDevice) ? '正在播放' : '已暂停' }}
|
||||||
|
</VanButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="params-body">
|
||||||
|
<!-- 音量滑块 -->
|
||||||
|
<div class="param-row">
|
||||||
|
<div class="param-header">
|
||||||
|
<span class="p-title">助眠播放音量</span>
|
||||||
|
<strong class="p-val">{{ localParams.speakerVolume }}%</strong>
|
||||||
|
</div>
|
||||||
|
<VanSlider
|
||||||
|
v-model="localParams.speakerVolume"
|
||||||
|
:min="1"
|
||||||
|
:max="100"
|
||||||
|
active-color="#0c5045"
|
||||||
|
@change="applyParam(speakerDevice.deviceId, 'volume', localParams.speakerVolume)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="param-row inline-row">
|
||||||
|
<div>
|
||||||
|
<div class="p-title">音箱睡眠模式</div>
|
||||||
|
<small class="p-tip">使用设备当前音源,仅切换睡眠模式并调整音量</small>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="mini-toggle"
|
||||||
|
:class="{ active: localParams.speakerSleepMode }"
|
||||||
|
@click="localParams.speakerSleepMode = !localParams.speakerSleepMode; applyParam(speakerDevice.deviceId, 'speaker_sleep_mode', localParams.speakerSleepMode)"
|
||||||
|
>
|
||||||
|
{{ localParams.speakerSleepMode ? '已开启' : '已关闭' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 设备 4:米家智能香氛机 -->
|
||||||
|
<section class="device-card" v-if="fragranceDevice">
|
||||||
|
<div class="device-head">
|
||||||
|
<div class="dev-info">
|
||||||
|
<span class="category-tag">香氛助眠</span>
|
||||||
|
<h3>{{ fragranceDevice.name }}</h3>
|
||||||
|
<span class="status-dot" :class="{ online: fragranceDevice.online }">
|
||||||
|
{{ fragranceDevice.online ? '设备在线' : '设备离线' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<VanButton
|
||||||
|
size="small"
|
||||||
|
round
|
||||||
|
:type="isDeviceTurnedOn(fragranceDevice) ? 'primary' : 'default'"
|
||||||
|
:color="isDeviceTurnedOn(fragranceDevice) ? '#0c5045' : ''"
|
||||||
|
:loading="switchingDeviceId === fragranceDevice.deviceId"
|
||||||
|
@click="toggleDevice(fragranceDevice)"
|
||||||
|
>
|
||||||
|
{{ isDeviceTurnedOn(fragranceDevice) ? '释放中' : '已关闭' }}
|
||||||
|
</VanButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="params-body">
|
||||||
|
<div class="param-row">
|
||||||
|
<span class="p-title">当前可控能力</span>
|
||||||
|
<p class="p-tip">当前 Home Assistant 集成仅提供香氛机开关与氛围灯控制;浓度请在米家设备端预设。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 设备 5:8H AI智能好睡眠床垫X -->
|
||||||
|
<section class="device-card" v-if="mattressDevice">
|
||||||
|
<div class="device-head">
|
||||||
|
<div class="dev-info">
|
||||||
|
<span class="category-tag">智能承托与温控</span>
|
||||||
|
<h3>{{ mattressDevice.name }}</h3>
|
||||||
|
<span class="status-dot" :class="{ online: mattressDevice.online }">
|
||||||
|
{{ mattressDevice.online ? '设备在线' : '设备离线' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="params-body">
|
||||||
|
<!-- 左侧加热档位 -->
|
||||||
|
<div class="param-row">
|
||||||
|
<div class="param-header">
|
||||||
|
<span class="p-title">左侧加热档位</span>
|
||||||
|
<strong class="p-val">{{ localParams.mattressLeftHeating }} 档</strong>
|
||||||
|
</div>
|
||||||
|
<VanSlider
|
||||||
|
v-model="localParams.mattressLeftHeating"
|
||||||
|
:min="0"
|
||||||
|
:max="4"
|
||||||
|
:step="1"
|
||||||
|
active-color="#d66927"
|
||||||
|
@change="applyParam(mattressDevice.deviceId, 'mattress_left_heating', localParams.mattressLeftHeating)"
|
||||||
|
/>
|
||||||
|
<div class="slider-scale">
|
||||||
|
<span>0 关闭</span>
|
||||||
|
<span>2 舒适</span>
|
||||||
|
<span>4 强加热</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧加热档位 -->
|
||||||
|
<div class="param-row">
|
||||||
|
<div class="param-header">
|
||||||
|
<span class="p-title">右侧加热档位</span>
|
||||||
|
<strong class="p-val">{{ localParams.mattressRightHeating }} 档</strong>
|
||||||
|
</div>
|
||||||
|
<VanSlider
|
||||||
|
v-model="localParams.mattressRightHeating"
|
||||||
|
:min="0"
|
||||||
|
:max="4"
|
||||||
|
:step="1"
|
||||||
|
active-color="#d66927"
|
||||||
|
@change="applyParam(mattressDevice.deviceId, 'mattress_right_heating', localParams.mattressRightHeating)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="param-row">
|
||||||
|
<span class="p-title">左侧承托模式</span>
|
||||||
|
<div class="btn-group">
|
||||||
|
<button
|
||||||
|
v-for="mode in ['无', '瑜伽助眠', '智能适配', '智能腰托']"
|
||||||
|
:key="`left-${mode}`"
|
||||||
|
:class="{ active: localParams.mattressLeftMode === mode }"
|
||||||
|
@click="localParams.mattressLeftMode = mode; applyParam(mattressDevice.deviceId, 'mattress_left_mode', mode)"
|
||||||
|
>{{ mode }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="param-row">
|
||||||
|
<span class="p-title">右侧承托模式</span>
|
||||||
|
<div class="btn-group">
|
||||||
|
<button
|
||||||
|
v-for="mode in ['无', '瑜伽助眠', '智能适配', '智能腰托']"
|
||||||
|
:key="`right-${mode}`"
|
||||||
|
:class="{ active: localParams.mattressRightMode === mode }"
|
||||||
|
@click="localParams.mattressRightMode = mode; applyParam(mattressDevice.deviceId, 'mattress_right_mode', mode)"
|
||||||
|
>{{ mode }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 设备 6:米家空气检测仪 -->
|
||||||
|
<section class="device-card" v-if="sensorDevice">
|
||||||
|
<div class="device-head">
|
||||||
|
<div class="dev-info">
|
||||||
|
<span class="category-tag">环境质量监控</span>
|
||||||
|
<h3>{{ sensorDevice.name }}</h3>
|
||||||
|
<span class="status-dot" :class="{ online: sensorDevice.online }">
|
||||||
|
{{ sensorDevice.online ? '实时采集正常' : '设备离线' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button class="text-refresh" @click="loadStatus">手动刷新指标</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sensor-grid">
|
||||||
|
<div class="sensor-box">
|
||||||
|
<span class="s-label">温度</span>
|
||||||
|
<strong>{{ haStatus?.environment.temperature ?? '--' }} <small>°C</small></strong>
|
||||||
|
</div>
|
||||||
|
<div class="sensor-box">
|
||||||
|
<span class="s-label">湿度</span>
|
||||||
|
<strong>{{ haStatus?.environment.humidity ?? '--' }} <small>%</small></strong>
|
||||||
|
</div>
|
||||||
|
<div class="sensor-box">
|
||||||
|
<span class="s-label">PM2.5</span>
|
||||||
|
<strong>{{ haStatus?.environment.pm25 ?? '--' }} <small>μg/m³</small></strong>
|
||||||
|
</div>
|
||||||
|
<div class="sensor-box">
|
||||||
|
<span class="s-label">CO2 浓度</span>
|
||||||
|
<strong>{{ haStatus?.environment.co2 ?? '--' }} <small>ppm</small></strong>
|
||||||
|
</div>
|
||||||
|
<div class="sensor-box">
|
||||||
|
<span class="s-label">甲醛 HCHO</span>
|
||||||
|
<strong>{{ haStatus?.environment.hcho ?? '--' }} <small>mg/m³</small></strong>
|
||||||
|
</div>
|
||||||
|
<div class="sensor-box">
|
||||||
|
<span class="s-label">TVOC</span>
|
||||||
|
<strong>{{ haStatus?.environment.tvoc ?? '--' }} <small>mg/m³</small></strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 设备 7:体脂秤 -->
|
||||||
|
<section class="device-card" v-if="scaleDevice">
|
||||||
|
<div class="device-head">
|
||||||
|
<div class="dev-info">
|
||||||
|
<span class="category-tag">健康身体成分</span>
|
||||||
|
<h3>{{ scaleDevice.name }}</h3>
|
||||||
|
<span class="status-dot" :class="{ online: scaleDevice.online }">
|
||||||
|
{{ scaleDevice.online ? '已绑定就绪' : '设备离线' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="scale-note">
|
||||||
|
每次早晚晨起赤足上秤,数据将自动同步并用于校准您的 BMI 及个性化智能家具承托参数。
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.control-page {
|
||||||
|
background: #f2f0e9;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-nav {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 50;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 14px 16px;
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-bottom: 1px solid rgba(18, 63, 55, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: #0c5045;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-nav h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #153e37;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-btn {
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: #0c5045;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-content {
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 汇总卡片 */
|
||||||
|
.summary-box {
|
||||||
|
background: linear-gradient(135deg, #0c5045, #14443c);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
box-shadow: 0 6px 20px rgba(12, 80, 69, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-info {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
padding-bottom: 14px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item .num {
|
||||||
|
display: block;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-family: serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item .num.online {
|
||||||
|
color: #92eec4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item .label {
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(255, 255, 255, 0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-actions-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sleep-btn {
|
||||||
|
background: rgba(214, 166, 87, 0.2);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sleep-btn:active {
|
||||||
|
background: rgba(214, 166, 87, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.off-btn {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
.s-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.s-text strong {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.s-text small {
|
||||||
|
display: block;
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 设备卡片 */
|
||||||
|
.device-card {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
border: 1px solid rgba(18, 63, 55, 0.06);
|
||||||
|
box-shadow: 0 4px 16px rgba(18, 63, 55, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-head {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid rgba(18, 63, 55, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-tag {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #0c5045;
|
||||||
|
background: #eef6f3;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dev-info h3 {
|
||||||
|
margin: 4px 0 2px;
|
||||||
|
font-size: 15px;
|
||||||
|
color: #173d36;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #8c9e97;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot.online {
|
||||||
|
color: #0c5045;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-refresh {
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: #0c5045;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.params-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-row.inline-row {
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-title {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #556c64;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-val {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #0c5045;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-tip {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #8fa099;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-scale {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #8d9e97;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-group button {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px 4px;
|
||||||
|
background: #f7f6f2;
|
||||||
|
border: 1px solid rgba(18, 63, 55, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #4b5e57;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-group button.active {
|
||||||
|
background: #0c5045;
|
||||||
|
color: #fff;
|
||||||
|
border-color: #0c5045;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-toggle {
|
||||||
|
padding: 6px 14px;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid rgba(18, 63, 55, 0.15);
|
||||||
|
background: #f4f3ef;
|
||||||
|
color: #6a7c76;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-toggle.active {
|
||||||
|
background: #0c5045;
|
||||||
|
color: #fff;
|
||||||
|
border-color: #0c5045;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 传感器网格 */
|
||||||
|
.sensor-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sensor-box {
|
||||||
|
background: #f8f7f2;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 10px 8px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.s-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #798a83;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sensor-box strong {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #173d36;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sensor-box small {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #8c9e97;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scale-note {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #72847d;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,638 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { Button as VanButton, showConfirmDialog, showToast } from 'vant'
|
||||||
|
import AppTabBar from '../components/AppTabBar.vue'
|
||||||
|
import { clearTokens } from '../services/auth'
|
||||||
|
import { healthApi, type Dashboard, type HealthAssessmentOutput } from '../services/health'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const loading = ref(true)
|
||||||
|
const dashboard = ref<Dashboard>()
|
||||||
|
const assessment = ref<HealthAssessmentOutput>()
|
||||||
|
|
||||||
|
async function loadData() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const [dashRes, assessRes] = await Promise.all([
|
||||||
|
healthApi.dashboard(),
|
||||||
|
healthApi.getAssessment().catch(() => undefined)
|
||||||
|
])
|
||||||
|
dashboard.value = dashRes
|
||||||
|
assessment.value = assessRes
|
||||||
|
} catch (err) {
|
||||||
|
showToast(err instanceof Error ? err.message : '加载失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bmiDisplay = computed(() => {
|
||||||
|
if (dashboard.value?.bmi) {
|
||||||
|
return {
|
||||||
|
val: dashboard.value.bmi,
|
||||||
|
status: dashboard.value.bmiStatus || '正常'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (assessment.value?.bmi) {
|
||||||
|
return {
|
||||||
|
val: assessment.value.bmi,
|
||||||
|
status: assessment.value.bmiStatus || '正常'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
const ageDisplay = computed(() => {
|
||||||
|
const raw = dashboard.value?.profile?.birthday || assessment.value?.basicInfo.birthday
|
||||||
|
if (!raw) return undefined
|
||||||
|
const birthday = new Date(raw)
|
||||||
|
const today = new Date()
|
||||||
|
let age = today.getFullYear() - birthday.getFullYear()
|
||||||
|
if (today < new Date(today.getFullYear(), birthday.getMonth(), birthday.getDate())) age--
|
||||||
|
return age
|
||||||
|
})
|
||||||
|
|
||||||
|
// 根据评估答案提取体质标签
|
||||||
|
const profileTags = computed(() => {
|
||||||
|
const ans = assessment.value?.answers
|
||||||
|
if (!ans || !assessment.value?.hasCompletedAssessment) return []
|
||||||
|
|
||||||
|
const list: { label: string; desc: string; type: 'gold' | 'blue' | 'green' | 'purple' }[] = []
|
||||||
|
|
||||||
|
if (ans.temperaturePreference === 'heat_fear') {
|
||||||
|
list.push({ label: '体感燥热偏温', desc: '除湿机清爽48% · 床垫低档加热', type: 'blue' })
|
||||||
|
} else if (ans.temperaturePreference === 'cold_fear') {
|
||||||
|
list.push({ label: '体质畏寒怕冷', desc: '除湿机温润58% · 床垫加强加热', type: 'gold' })
|
||||||
|
} else {
|
||||||
|
list.push({ label: '冷热适中平衡', desc: '标准舒适温湿度', type: 'green' })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ans.fallingAsleepSpeed === 'hard') {
|
||||||
|
list.push({ label: '睡前敏感易失眠', desc: '2200K超暖暗光 · 音箱睡眠模式', type: 'purple' })
|
||||||
|
} else if (ans.fallingAsleepSpeed === 'fast') {
|
||||||
|
list.push({ label: '快速深度睡眠', desc: '自然转场柔光', type: 'green' })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ans.muscleFatigueArea === 'waist_back' || ans.muscleFatigueArea === 'back_neck') {
|
||||||
|
list.push({ label: '脊柱腰背劳损', desc: '床垫启用智能腰托模式', type: 'gold' })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ans.lateNightEating === 'often' || ans.lateNightEating === 'sometimes' || ans.flavorPreference === 'heavy') {
|
||||||
|
list.push({ label: '晚间进食空气净化', desc: '除湿机UV紫外消杀已启用', type: 'purple' })
|
||||||
|
}
|
||||||
|
|
||||||
|
return list
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleLogout() {
|
||||||
|
showConfirmDialog({
|
||||||
|
title: '确认退出',
|
||||||
|
message: '确定要退出当前账号登录吗?'
|
||||||
|
}).then(() => {
|
||||||
|
clearTokens()
|
||||||
|
showToast('已安全退出')
|
||||||
|
router.replace('/auth')
|
||||||
|
}).catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadData()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="profile-page">
|
||||||
|
<!-- 顶部用户卡片 -->
|
||||||
|
<header class="profile-header">
|
||||||
|
<div class="user-row">
|
||||||
|
<div class="avatar-box">
|
||||||
|
<span>{{ (dashboard?.user.displayName || '客').slice(0, 1) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="user-info">
|
||||||
|
<h2>{{ dashboard?.user.displayName || '尊贵会员' }}</h2>
|
||||||
|
<p class="phone-text">{{ dashboard?.user.phone || '健康管理账号' }}</p>
|
||||||
|
<span class="user-badge">常青树 · 智能健康睡眠会员</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="profile-content">
|
||||||
|
<!-- 身体指标基线档案 -->
|
||||||
|
<section class="profile-card">
|
||||||
|
<div class="card-head">
|
||||||
|
<h3>身体基线档案</h3>
|
||||||
|
<button class="edit-btn" @click="router.push('/assessment')">编辑指标</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="metrics-grid">
|
||||||
|
<div class="metric-item">
|
||||||
|
<span class="m-label">性别</span>
|
||||||
|
<strong class="m-val">{{ dashboard?.profile?.gender || assessment?.basicInfo.gender || '已保密' }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="metric-item">
|
||||||
|
<span class="m-label">身高</span>
|
||||||
|
<strong class="m-val">{{ dashboard?.profile?.heightCm || assessment?.basicInfo.heightCm || '--' }} <small>cm</small></strong>
|
||||||
|
</div>
|
||||||
|
<div class="metric-item">
|
||||||
|
<span class="m-label">年龄</span>
|
||||||
|
<strong class="m-val">{{ ageDisplay ?? '--' }} <small>岁</small></strong>
|
||||||
|
</div>
|
||||||
|
<div class="metric-item">
|
||||||
|
<span class="m-label">体重</span>
|
||||||
|
<strong class="m-val">{{ dashboard?.profile?.weightKg || assessment?.basicInfo.weightKg || '--' }} <small>kg</small></strong>
|
||||||
|
</div>
|
||||||
|
<div class="metric-item">
|
||||||
|
<span class="m-label">BMI 指数</span>
|
||||||
|
<strong class="m-val" :class="{ highlight: bmiDisplay }">
|
||||||
|
{{ bmiDisplay?.val || '--' }}
|
||||||
|
</strong>
|
||||||
|
<span class="m-status-tag" v-if="bmiDisplay">{{ bmiDisplay.status }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 专属评估画像标签 -->
|
||||||
|
<section class="profile-card">
|
||||||
|
<div class="card-head">
|
||||||
|
<h3>健康评估画像与专属策略</h3>
|
||||||
|
<span class="status-indicator" :class="{ done: assessment?.hasCompletedAssessment }">
|
||||||
|
{{ assessment?.hasCompletedAssessment ? '已完成评估' : '未评估' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="assessment?.hasCompletedAssessment && profileTags.length > 0" class="tags-container">
|
||||||
|
<div
|
||||||
|
v-for="(tag, idx) in profileTags"
|
||||||
|
:key="idx"
|
||||||
|
class="profile-tag-item"
|
||||||
|
:class="'theme-' + tag.type"
|
||||||
|
>
|
||||||
|
<div class="tag-title">{{ tag.label }}</div>
|
||||||
|
<div class="tag-desc">{{ tag.desc }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="empty-state">
|
||||||
|
<p>您尚未完成身体健康评估,全屋智能家具当前使用出厂默认参数运行。</p>
|
||||||
|
<VanButton
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
round
|
||||||
|
color="#0c5045"
|
||||||
|
@click="router.push('/assessment')"
|
||||||
|
>
|
||||||
|
立即进行健康评估
|
||||||
|
</VanButton>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 当前健康方案 -->
|
||||||
|
<section class="profile-card" v-if="dashboard?.currentPlan">
|
||||||
|
<div class="card-head">
|
||||||
|
<h3>当前执行方案</h3>
|
||||||
|
<span class="version-tag">版本 v{{ dashboard.currentPlan.version }}.0</span>
|
||||||
|
</div>
|
||||||
|
<p class="plan-summary-text">{{ dashboard.currentPlan.summary }}</p>
|
||||||
|
|
||||||
|
<div class="plan-items-list">
|
||||||
|
<div
|
||||||
|
v-for="(item, i) in dashboard.currentPlan.items"
|
||||||
|
:key="i"
|
||||||
|
class="plan-row"
|
||||||
|
>
|
||||||
|
<div class="plan-row-head">
|
||||||
|
<span class="dot"></span>
|
||||||
|
<strong>{{ item.title }}</strong>
|
||||||
|
</div>
|
||||||
|
<p class="plan-target">目标:{{ item.target }}</p>
|
||||||
|
<p class="plan-advice">指导:{{ item.advice }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 快捷功能入口与操作菜单 -->
|
||||||
|
<section class="action-card-menu">
|
||||||
|
<div class="menu-item" @click="router.push('/assessment')">
|
||||||
|
<div class="menu-left">
|
||||||
|
<div class="menu-icon icon-assess">
|
||||||
|
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
||||||
|
<polyline points="14 2 14 8 20 8" />
|
||||||
|
<line x1="16" y1="13" x2="8" y2="13" />
|
||||||
|
<line x1="16" y1="17" x2="8" y2="17" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="menu-text">
|
||||||
|
<strong>重新进行健康评估</strong>
|
||||||
|
<small>更新身体指标,重新智能推导家具启动参数</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="arrow">></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="menu-item" @click="router.push('/device-control')">
|
||||||
|
<div class="menu-left">
|
||||||
|
<div class="menu-icon icon-device">
|
||||||
|
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<rect x="4" y="4" width="16" height="16" rx="2" />
|
||||||
|
<rect x="9" y="9" width="6" height="6" />
|
||||||
|
<line x1="9" y1="1" x2="9" y2="4" />
|
||||||
|
<line x1="15" y1="1" x2="15" y2="4" />
|
||||||
|
<line x1="9" y1="20" x2="9" y2="23" />
|
||||||
|
<line x1="15" y1="20" x2="15" y2="23" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="menu-text">
|
||||||
|
<strong>设备详情与参数调节</strong>
|
||||||
|
<small>查看7台设备状态、独立开关与精细参数设置</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="arrow">></span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 退出登录按钮 -->
|
||||||
|
<div class="logout-wrap">
|
||||||
|
<button class="logout-btn" @click="handleLogout">
|
||||||
|
退出登录
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- 固定底部导航栏 -->
|
||||||
|
<AppTabBar />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.profile-page {
|
||||||
|
padding-bottom: 90px;
|
||||||
|
background: #f2f0e9;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header {
|
||||||
|
background: linear-gradient(135deg, #0c5045, #123d36);
|
||||||
|
color: #fff;
|
||||||
|
padding: 34px 20px 28px;
|
||||||
|
border-radius: 0 0 32px 32px;
|
||||||
|
box-shadow: 0 10px 30px rgba(12, 80, 69, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-box {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(135deg, #d6a657, #b88636);
|
||||||
|
color: #fff;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
font-size: 26px;
|
||||||
|
font-family: serif;
|
||||||
|
font-weight: 700;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info h2 {
|
||||||
|
margin: 0 0 4px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-text {
|
||||||
|
margin: 0 0 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-badge {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 11px;
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
color: #ffe6ba;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-content {
|
||||||
|
padding: 16px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-card {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 18px 16px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
box-shadow: 0 4px 18px rgba(18, 63, 55, 0.04);
|
||||||
|
border: 1px solid rgba(18, 63, 55, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-head {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-head h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 15px;
|
||||||
|
color: #123f37;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-btn {
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: #0c5045;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator {
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #f0f0ed;
|
||||||
|
color: #88948e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator.done {
|
||||||
|
background: #eef6f3;
|
||||||
|
color: #0c5045;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metrics-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-item {
|
||||||
|
background: #f8f7f3;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 10px 6px;
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-label {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #798a83;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-val {
|
||||||
|
font-size: 15px;
|
||||||
|
color: #173d36;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-val small {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #8fa099;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-val.highlight {
|
||||||
|
color: #0c5045;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-status-tag {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #0c5045;
|
||||||
|
background: rgba(12, 80, 69, 0.1);
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-top: 4px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 标签列表 */
|
||||||
|
.tags-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-tag-item {
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-tag-item.theme-gold {
|
||||||
|
background: #fcf9f2;
|
||||||
|
border-color: rgba(214, 166, 87, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-tag-item.theme-gold .tag-title {
|
||||||
|
color: #9c6c19;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-tag-item.theme-blue {
|
||||||
|
background: #f3f8fd;
|
||||||
|
border-color: rgba(66, 153, 225, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-tag-item.theme-blue .tag-title {
|
||||||
|
color: #2b6cb0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-tag-item.theme-green {
|
||||||
|
background: #f1f8f5;
|
||||||
|
border-color: rgba(12, 80, 69, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-tag-item.theme-green .tag-title {
|
||||||
|
color: #0c5045;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-tag-item.theme-purple {
|
||||||
|
background: #f8f4fd;
|
||||||
|
border-color: rgba(159, 122, 234, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-tag-item.theme-purple .tag-title {
|
||||||
|
color: #6b46c1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-desc {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #60736c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 16px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state p {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #84968f;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 方案卡片 */
|
||||||
|
.version-tag {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #0c5045;
|
||||||
|
background: #eef6f3;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan-summary-text {
|
||||||
|
margin: 0 0 14px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #556c64;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan-items-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan-row {
|
||||||
|
background: #f9f8f4;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan-row-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan-row-head .dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #0c5045;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan-row-head strong {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #173e37;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan-target, .plan-advice {
|
||||||
|
margin: 2px 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #697c75;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 菜单导航 */
|
||||||
|
.action-card-menu {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
border: 1px solid rgba(18, 63, 55, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-bottom: 1px solid rgba(18, 63, 55, 0.05);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item:active {
|
||||||
|
background: #f8f7f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 10px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-assess {
|
||||||
|
background: #eef7f3;
|
||||||
|
color: #0c5045;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-device {
|
||||||
|
background: #fbf5e8;
|
||||||
|
color: #b07e26;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-text strong {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #183e37;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-text small {
|
||||||
|
display: block;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #7b8e88;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow {
|
||||||
|
color: #b3bfb9;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-wrap {
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 13px 0;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid rgba(220, 70, 70, 0.25);
|
||||||
|
background: #ffffff;
|
||||||
|
color: #d64545;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-btn:active {
|
||||||
|
background: #fff5f5;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user