diff --git a/src/router.ts b/src/router.ts index ac7105a..0a38049 100644 --- a/src/router.ts +++ b/src/router.ts @@ -1,6 +1,5 @@ import { createRouter, createWebHistory } from 'vue-router' import AuthView from './views/AuthView.vue' -import OnboardingView from './views/OnboardingView.vue' import DashboardView from './views/DashboardView.vue' import AssessmentView from './views/AssessmentView.vue' import ProfileView from './views/ProfileView.vue' @@ -12,7 +11,7 @@ const router = createRouter({ routes: [ { path: '/', redirect: '/dashboard' }, { path: '/auth', component: AuthView, meta: { guest: true } }, - { path: '/onboarding', component: OnboardingView }, + { path: '/onboarding', redirect: '/assessment' }, { path: '/dashboard', component: DashboardView }, { path: '/assessment', component: AssessmentView }, { path: '/profile', component: ProfileView }, diff --git a/src/services/health.ts b/src/services/health.ts index b6d28e8..adf68a6 100644 --- a/src/services/health.ts +++ b/src/services/health.ts @@ -230,11 +230,6 @@ export const healthApi = { login: (payload: { phone: string; password: string }) => http.post('/api/health/auth/login', payload), dashboard: () => http.get('/api/health/dashboard'), - saveProfile: (payload: HealthProfile) => - http.put('/api/health/profile', payload), - saveModules: (moduleCodes: string[]) => - http.put('/api/health/modules', { moduleCodes }), - generatePlan: () => http.post('/api/health/plans/generate'), getAssessment: () => http.get('/api/health/assessment'), saveAssessment: (payload: SaveHealthAssessmentInput) => http.post('/api/health/assessment', payload), diff --git a/src/styles.css b/src/styles.css index bb17ef0..3264ffe 100644 --- a/src/styles.css +++ b/src/styles.css @@ -33,13 +33,12 @@ button { cursor: pointer; } .auth-page { min-height: 100vh; background: var(--cream); } .brand-panel { - min-height: 42vh; - padding: 54px 28px 42px; + padding: 46px 24px 46px; color: #fff; background: radial-gradient(circle at 82% 12%, rgba(236, 202, 137, .28), transparent 28%), linear-gradient(145deg, #0b5548, #123d37 74%); - border-radius: 0 0 42px 42px; + border-radius: 0 0 36px 36px; } .brand-badge { display: inline-flex; @@ -70,11 +69,11 @@ button { cursor: pointer; } border: 1px solid rgba(255,255,255,.42); border-radius: 50%; font-family: serif; font-size: 24px; } .brand-panel h1 { max-width: 560px; margin: 0; font-family: "Songti SC", serif; font-size: clamp(32px, 8vw, 54px); line-height: 1.2; font-weight: 500; } -.brand-copy { max-width: 480px; margin: 18px 0 24px; color: rgba(255,255,255,.72); line-height: 1.7; } -.feature-row, .feature-pill-row { display: flex; gap: 8px; flex-wrap: wrap; } +.brand-copy { max-width: 480px; margin: 14px 0 16px; color: rgba(255,255,255,.78); line-height: 1.6; } +.feature-row, .feature-pill-row { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 8px; } .feature-row span, .feature-pill-row span { padding: 6px 12px; background: rgba(255,255,255,.1); border: 1px solid rgba(255,255,255,.16); border-radius: 20px; font-size: 12px; } -.auth-card { position: relative; width: min(520px, calc(100% - 32px)); margin: -20px auto 0; padding: 24px 20px; background: #fff; border-radius: 24px; box-shadow: 0 18px 50px rgba(24,59,52,.12); } +.auth-card { position: relative; width: calc(100% - 32px); max-width: 440px; margin: -16px auto 32px; padding: 24px 20px; background: #fff; border-radius: 24px; box-shadow: 0 14px 40px rgba(24,59,52,.12); } .auth-tab-switch { display: flex; background: #f1efea; @@ -771,8 +770,8 @@ button { cursor: pointer; } .loading-card { color: #73817d; text-align: center; } @media (max-width: 520px) { - .brand-panel { min-height: 390px; } - .auth-card { margin-top: -54px; } + .brand-panel { padding: 42px 20px 42px; } + .auth-card { margin-top: -16px; } .plan-heading { display: block; } .plan-heading span { display: block; margin-top: 5px; text-align: left; } .device-grid { grid-template-columns: 1fr; } @@ -782,7 +781,20 @@ button { cursor: pointer; } } .dashboard-page { - padding-bottom: 84px !important; + padding: 16px 14px 84px !important; +} +.dashboard-header { + margin: 0 auto 12px !important; + padding: 4px 4px 0 !important; +} +.dashboard-header h1 { + margin: 2px 0 3px !important; + font-size: 26px !important; +} +.dashboard-header > div > p:last-child { + margin: 0 !important; + font-size: 13px !important; + line-height: 1.4 !important; } /* 评估未填提醒卡片 */ @@ -850,25 +862,28 @@ button { cursor: pointer; } /* 个性化舒睡启动参数卡片 */ .personalized-startup-card { background: #ffffff; - border: 1px solid rgba(12, 80, 69, 0.12); + border: 1px solid rgba(12, 80, 69, 0.1); border-radius: 20px; - padding: 16px; + padding: 18px 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; +.personalized-startup-card .section-title { + display: flex; + align-items: center; + margin-bottom: 4px; +} +.personalized-startup-card .section-title > span { + font-size: 17px; + font-weight: 700; + color: #143d36; + letter-spacing: 0.01em; } .startup-card-desc { - margin: 4px 0 12px; + margin: 0 0 14px; font-size: 12px; - color: #6b7d76; - line-height: 1.5; + color: #72847e; + line-height: 1.55; } .startup-params-tags { display: grid; @@ -892,6 +907,33 @@ button { cursor: pointer; } font-size: 12px; color: #173d36; } +.startup-scene-actions { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; + margin: 4px 0 12px; +} +.startup-scene-actions button { + min-height: 42px; + border-radius: 12px; + font-size: 13px; + font-weight: 700; + cursor: pointer; +} +.startup-scene-actions button:disabled { + cursor: not-allowed; + opacity: 0.45; +} +.startup-action-primary { + border: 1px solid #0c5045; + background: #0c5045; + color: #ffffff; +} +.startup-action-secondary { + border: 1px solid rgba(12, 80, 69, 0.2); + background: #f7f6f2; + color: #335b53; +} .detail-entry-bar { display: flex; align-items: center; diff --git a/src/views/AuthView.vue b/src/views/AuthView.vue index e740245..014b830 100644 --- a/src/views/AuthView.vue +++ b/src/views/AuthView.vue @@ -19,7 +19,7 @@ async function submit() { : await healthApi.register(form) saveTokens(output.accessToken, output.refreshToken) showToast({ type: 'success', message: mode.value === 'login' ? '登录成功' : '注册成功' }) - await router.replace(mode.value === 'login' ? '/dashboard' : '/onboarding') + await router.replace(mode.value === 'login' ? '/dashboard' : '/assessment') } catch (error) { showToast(error instanceof Error ? error.message : '操作失败') } finally { diff --git a/src/views/DashboardView.vue b/src/views/DashboardView.vue index 086aa2b..fc312ce 100644 --- a/src/views/DashboardView.vue +++ b/src/views/DashboardView.vue @@ -7,33 +7,14 @@ import { clearTokens } from '../services/auth' import { healthApi, type Dashboard, - type HomeAssistantDiscoveredDevice, type HomeAssistantStatus } from '../services/health' const router = useRouter() const loading = ref(true) -const generating = ref(false) const sceneRunning = ref<'sleep' | 'all-off'>() const dashboard = ref() const homeAssistant = ref() -const moduleNames: Record = { - sleep: '睡眠', - exercise: '运动', - diet: '饮食' -} - -const categoryNames: Record = { - EnvironmentControl: '场景控制', - EnvironmentSensor: '环境健康', - Sleep: '睡眠健康', - BodyComposition: '身体健康', - Fragrance: '香氛场景', - Lighting: '灯光场景', - Media: '音频场景', - Other: '智能外设' -} - const greeting = computed(() => { const hour = new Date().getHours() if (hour < 11) return '早上好' @@ -41,176 +22,8 @@ const greeting = computed(() => { return '晚上好' }) -const totalDeviceCount = computed(() => homeAssistant.value?.summary?.totalDevices ?? homeAssistant.value?.devices?.length ?? 0) -const onlineDeviceCount = computed(() => homeAssistant.value?.summary?.onlineDevices ?? homeAssistant.value?.devices?.filter(d => d.online).length ?? 0) -const totalEntityCount = computed(() => homeAssistant.value?.summary?.totalEntities ?? 0) -const availableEntityCount = computed(() => homeAssistant.value?.summary?.availableEntities ?? 0) -const sceneTotal = computed(() => homeAssistant.value?.sceneDevices?.total ?? 0) const sceneAvailable = computed(() => homeAssistant.value?.sceneDevices?.available ?? 0) -const sceneDevices = computed(() => { - if (!homeAssistant.value?.devices) return [] - const configuredCategories = new Set( - (homeAssistant.value.scenePresets || []).map(item => item.category) - ) - return homeAssistant.value.devices.filter(d => configuredCategories.has(d.category)) -}) - -const envDevice = computed(() => { - return homeAssistant.value?.devices?.find(d => d.category === 'EnvironmentSensor') -}) - -const sleepDevice = computed(() => { - return homeAssistant.value?.devices?.find(d => d.category === 'Sleep') -}) - -const bodyDevice = computed(() => { - return homeAssistant.value?.devices?.find(d => d.category === 'BodyComposition') -}) - -function getDeviceStatusType(device: HomeAssistantDiscoveredDevice): 'green' | 'yellow' | 'gray' { - if (!device.online) return 'gray' - if (device.category === 'Media') { - const hasMedia = device.entities.some(e => e.domain === 'media_player' && e.available) - return hasMedia ? 'green' : 'yellow' - } - const hasAvailable = device.entities.some(e => e.available) - return hasAvailable ? 'green' : 'yellow' -} - -function getDeviceStatusText(device: HomeAssistantDiscoveredDevice): string { - const type = getDeviceStatusType(device) - if (type === 'gray') return '设备离线' - if (type === 'yellow') { - if (device.category === 'Media') return '在线 · 播放功能暂不可用' - return '在线 · 部分功能受限' - } - if (device.category === 'EnvironmentControl') { - const humidity = device.metrics?.humidity - return humidity ? `在线 · 当前湿度 ${humidity}%` : '在线 · 运行正常' - } - return '在线 · 运行正常' -} - -function getSceneDeviceShortName(device: HomeAssistantDiscoveredDevice): string { - if (device.category === 'EnvironmentControl') return '除湿机 Max' - if (device.category === 'Fragrance') return '智能香氛机' - if (device.category === 'Lighting') return 'Lemesh 色温灯' - if (device.category === 'Media') return 'Sound Pro 音箱' - return device.name -} - -function getSceneDeviceStatus(device: HomeAssistantDiscoveredDevice): string { - const type = getDeviceStatusType(device) - if (type === 'green') { - if (device.category === 'EnvironmentControl' && device.metrics?.humidity) { - return `${device.metrics.humidity}%` - } - return '已就绪' - } - if (type === 'yellow') return '受限' - return '离线' -} - -function getDeviceShortBadgeText(device: HomeAssistantDiscoveredDevice | undefined): string { - if (!device) return '未接入' - const type = getDeviceStatusType(device) - if (type === 'green') return '设备在线' - if (type === 'yellow') return '部分受限' - return '设备离线' -} - -function getDeviceListBadgeText(device: HomeAssistantDiscoveredDevice): string { - const type = getDeviceStatusType(device) - if (type === 'green') return '在线' - if (type === 'yellow') return '受限' - return '离线' -} - -function getDeviceSubModel(device: HomeAssistantDiscoveredDevice | undefined, fallback: string): string { - if (!device) return fallback - if (device.category === 'EnvironmentSensor') return '罗威亚检测仪' - if (device.category === 'Sleep') return '8H 睡眠床垫' - if (device.category === 'BodyComposition') return '米家体脂秤' - return device.name -} - -function getTemperatureTag(temp: number | undefined | null): string { - if (temp == null) return '待采集' - if (temp < 18) return '偏凉' - if (temp <= 26) return '适宜' - return '偏暖' -} - -function getHumidityTag(hum: number | undefined | null): string { - if (hum == null) return '待采集' - if (hum < 40) return '干燥' - if (hum <= 60) return '舒适' - if (hum <= 70) return '稍潮' - return '潮湿' -} - -const switchingDeviceId = ref(null) -const syncingSensor = ref(false) -const showScenePresets = ref(true) - -function isControllable(device: HomeAssistantDiscoveredDevice): boolean { - return !!device.primaryControlEntityId -} - -function getPrimaryControlEntity(device: HomeAssistantDiscoveredDevice) { - if (!device.primaryControlEntityId) return undefined - return device.entities.find(e => e.entityId === device.primaryControlEntityId) -} - -function isPrimaryControlAvailable(device: HomeAssistantDiscoveredDevice): boolean { - return !!getPrimaryControlEntity(device)?.available -} - -function isDeviceTurnedOn(device: HomeAssistantDiscoveredDevice): boolean { - const target = getPrimaryControlEntity(device) - if (!target) return false - return target.state === 'on' || target.state === 'playing' -} - -async function toggleDeviceSwitch(device: HomeAssistantDiscoveredDevice) { - if (!isPrimaryControlAvailable(device)) { - showToast('设备主控功能当前不可用') - return - } - const currentOn = isDeviceTurnedOn(device) - const nextOn = !currentOn - switchingDeviceId.value = device.deviceId - try { - const res = await healthApi.switchDevice({ - deviceId: device.deviceId, - turnOn: nextOn, - entityId: device.primaryControlEntityId || undefined - }) - showToast({ - type: res.success ? 'success' : 'fail', - message: res.message || (nextOn ? '已开启设备' : '已关闭设备') - }) - homeAssistant.value = await healthApi.homeAssistantStatus() - } catch (error) { - showToast(error instanceof Error ? error.message : '控制失败') - } finally { - switchingDeviceId.value = null - } -} - -async function syncSensorData() { - syncingSensor.value = true - try { - homeAssistant.value = await healthApi.homeAssistantStatus() - showToast({ type: 'success', message: '状态已刷新' }) - } catch (error) { - showToast(error instanceof Error ? error.message : '刷新失败') - } finally { - syncingSensor.value = false - } -} - async function load() { loading.value = true try { @@ -242,19 +55,6 @@ async function executeScene(scene: 'sleep' | 'all-off') { } } -async function generate() { - generating.value = true - try { - await healthApi.generatePlan() - await load() - showToast({ type: 'success', message: '方案已更新' }) - } catch (error) { - showToast(error instanceof Error ? error.message : '生成失败') - } finally { - generating.value = false - } -} - function logout() { clearTokens() router.replace('/auth') @@ -279,6 +79,21 @@ onMounted(load)
正在读取今日健康方案…
diff --git a/src/views/OnboardingView.vue b/src/views/OnboardingView.vue deleted file mode 100644 index c8639e9..0000000 --- a/src/views/OnboardingView.vue +++ /dev/null @@ -1,99 +0,0 @@ - - -