323 lines
12 KiB
Vue
323 lines
12 KiB
Vue
<script setup>
|
||
import { ref, onMounted, computed } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import { homeApi } from '@/api/index'
|
||
import { useCustomerStore } from '@/stores/customer'
|
||
|
||
const router = useRouter()
|
||
const store = useCustomerStore()
|
||
const homeData = ref(null)
|
||
const loading = ref(true)
|
||
|
||
onMounted(async () => {
|
||
try {
|
||
const res = await homeApi.index()
|
||
homeData.value = res.data?.data
|
||
} catch { } finally { loading.value = false }
|
||
})
|
||
|
||
const customer = computed(() => homeData.value?.customer || store.info)
|
||
const todayMeals = computed(() => homeData.value?.today_meals || [])
|
||
const latestRecords = computed(() => homeData.value?.care_records || [])
|
||
const nannyOrder = computed(() => homeData.value?.nanny_order)
|
||
|
||
// 膳食时间映射
|
||
const mealTime = { 1: '07:30', 2: '12:00', 3: '15:00', 4: '18:00', 5: '21:00' }
|
||
const mealLabel = { 1: '早餐', 2: '午餐', 3: '下午茶', 4: '晚餐', 5: '宵夜' }
|
||
|
||
// 入住天数
|
||
const checkinDays = computed(() => {
|
||
const d = customer.value?.actual_date
|
||
if (!d) return null
|
||
const diff = Math.floor((Date.now() - new Date(d).getTime()) / 86400000)
|
||
return diff > 0 ? diff : null
|
||
})
|
||
|
||
const features = [
|
||
{ label: '月子餐', bg: '#fff0f4', color: '#f43f5e', icon: 'fire-o', path: '/meal' },
|
||
{ label: '护理记录', bg: '#fffbeb', color: '#f59e0b', icon: 'notes-o', path: '/care/records' },
|
||
{ label: '健康档案', bg: '#f0fdf4', color: '#10b981', icon: 'records-o', path: '/grow' },
|
||
{ label: '服务包', bg: '#eff6ff', color: '#3b82f6', icon: 'gift-o', path: '/service/orders' },
|
||
{ label: '我的月嫂', bg: '#f5f3ff', color: '#8b5cf6', icon: 'manager-o', path: '/nanny' },
|
||
{ label: '账户余额', bg: '#fff7ed', color: '#f97316', icon: 'balance-o', path: '/account' },
|
||
]
|
||
|
||
const today = new Date().toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric' })
|
||
</script>
|
||
|
||
<template>
|
||
<div class="page">
|
||
<!-- ── 顶部问候 ── -->
|
||
<div class="header">
|
||
<div>
|
||
<p class="greet-sub">欢迎回来,</p>
|
||
<h2 class="greet-name">
|
||
{{ customer?.name || '亲爱的' }}妈妈
|
||
<van-icon name="shield-o" color="#f43f5e" size="16" style="vertical-align:middle;margin-left:2px" />
|
||
</h2>
|
||
</div>
|
||
<div class="avatar-wrap">
|
||
<van-icon name="contact" size="32" color="#f43f5e" />
|
||
</div>
|
||
</div>
|
||
|
||
<div class="body">
|
||
|
||
<!-- ── 宝宝监控卡(暗色摄像风格) ── -->
|
||
<div class="cam-card">
|
||
<!-- 暗色网格背景 -->
|
||
<div class="cam-grid" />
|
||
<!-- LIVE badge -->
|
||
<div class="cam-topbar">
|
||
<span class="live-dot">● LIVE</span>
|
||
<span class="cam-label">Cam 02 (室内)</span>
|
||
</div>
|
||
<!-- 播放按钮 -->
|
||
<div class="play-wrap">
|
||
<div class="play-btn">
|
||
<svg width="22" height="24" viewBox="0 0 22 24" fill="none">
|
||
<path d="M2 2L20 12L2 22V2Z" fill="white" />
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
<!-- 底部文字 -->
|
||
<div class="cam-bottom">
|
||
<span class="cam-title">宝宝安防监控 · 实时画面</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── 6 格功能入口(圆形图标 3×2) ── -->
|
||
<div class="features-row">
|
||
<div
|
||
v-for="f in features"
|
||
:key="f.path"
|
||
class="feat-item"
|
||
@click="router.push(f.path)"
|
||
>
|
||
<div class="feat-circle" :style="{ background: f.bg }">
|
||
<van-icon :name="f.icon" :color="f.color" size="26" />
|
||
</div>
|
||
<span class="feat-label">{{ f.label }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── 今日服务排期(teal 渐变卡) ── -->
|
||
<div class="schedule-card">
|
||
<div class="sch-head">
|
||
<div>
|
||
<h5 class="sch-title">今日服务排期</h5>
|
||
<p class="sch-date">{{ today }}(共 {{ todayMeals.length }} 项)</p>
|
||
</div>
|
||
<span class="sch-day-badge" v-if="checkinDays">入住第{{ checkinDays }}天</span>
|
||
<span class="sch-more-btn" v-else @click="router.push('/meal')">查看全部</span>
|
||
</div>
|
||
<div class="sch-list" v-if="todayMeals.length">
|
||
<div v-for="m in todayMeals.slice(0, 3)" :key="m.id" class="sch-item">
|
||
<span class="sch-time">{{ mealTime[m.meal_type] || '--:--' }}</span>
|
||
<div class="sch-sep" />
|
||
<span class="sch-content">
|
||
{{ mealLabel[m.meal_type] || '用餐' }}
|
||
<span v-if="m.dishes?.length">:{{ m.dishes.map(d => d.name || d).join('、') }}</span>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<p class="sch-empty" v-else>今日暂无服务安排</p>
|
||
</div>
|
||
|
||
<!-- ── 最近监测数据 ── -->
|
||
<div class="data-section">
|
||
<h5 class="sec-title">最近监测数据</h5>
|
||
<div class="data-grid">
|
||
<div class="data-card" @click="router.push('/care/records')">
|
||
<p class="data-label">最近护理</p>
|
||
<p class="data-val">{{ latestRecords.length ? (latestRecords[0]?.nurse?.name || '已护理') : '暂无记录' }}</p>
|
||
<p class="data-ext">{{ latestRecords.length ? latestRecords[0]?.recorded_at?.slice(5, 10) : '--' }}</p>
|
||
</div>
|
||
<div class="data-card" @click="router.push('/nanny')">
|
||
<p class="data-label">我的月嫂</p>
|
||
<p class="data-val">{{ nannyOrder?.nanny?.name || '待分配' }}</p>
|
||
<p class="data-ext blue">{{ nannyOrder?.nanny?.level ? '高级月嫂' : '---' }}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── 护理动态 ── -->
|
||
<div v-if="latestRecords.length" class="records-section">
|
||
<div class="rec-head">
|
||
<h5 class="sec-title">护理动态</h5>
|
||
<span class="rec-more" @click="router.push('/care/records')">全部 ›</span>
|
||
</div>
|
||
<div class="rec-list">
|
||
<div v-for="r in latestRecords" :key="r.id" class="rec-item">
|
||
<div class="rec-emoji">💆</div>
|
||
<div class="rec-info">
|
||
<p class="rec-nurse">{{ r.nurse?.name || '护理师' }}</p>
|
||
<p class="rec-time">{{ r.recorded_at?.slice(0, 16) || '' }}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.page { background: #f5f5f7; min-height: 100vh; padding-bottom: 90px; }
|
||
|
||
/* ── Header ── */
|
||
.header {
|
||
display: flex; justify-content: space-between; align-items: center;
|
||
padding: 52px 22px 18px;
|
||
background: white;
|
||
}
|
||
.greet-sub { font-size: 13px; color: #9ca3af; margin: 0 0 2px; }
|
||
.greet-name { font-size: 22px; font-weight: 800; color: #111827; margin: 0; letter-spacing: -0.3px; }
|
||
.avatar-wrap {
|
||
width: 52px; height: 52px;
|
||
background: #fff0f4;
|
||
border-radius: 50%;
|
||
border: 2px solid #fecdd3;
|
||
display: flex; align-items: center; justify-content: center;
|
||
}
|
||
|
||
/* ── Body ── */
|
||
.body { padding: 14px 16px; }
|
||
|
||
/* ── Camera Card ── */
|
||
.cam-card {
|
||
border-radius: 24px;
|
||
height: 200px;
|
||
margin-bottom: 22px;
|
||
position: relative;
|
||
overflow: hidden;
|
||
background: #0d1117;
|
||
box-shadow: 0 10px 32px rgba(0,0,0,0.35);
|
||
}
|
||
.cam-grid {
|
||
position: absolute; inset: 0;
|
||
background-image:
|
||
linear-gradient(rgba(255,255,255,0.04) 1px, transparent 1px),
|
||
linear-gradient(90deg, rgba(255,255,255,0.04) 1px, transparent 1px);
|
||
background-size: 40px 40px;
|
||
}
|
||
/* 暗色影调 */
|
||
.cam-card::after {
|
||
content: '';
|
||
position: absolute; inset: 0;
|
||
background: radial-gradient(ellipse at 50% 50%, rgba(20,184,166,0.12) 0%, rgba(0,0,0,0.55) 100%);
|
||
}
|
||
.cam-topbar {
|
||
position: absolute; top: 14px; left: 14px;
|
||
display: flex; align-items: center; gap: 8px;
|
||
z-index: 2;
|
||
}
|
||
.live-dot {
|
||
background: #ef4444;
|
||
color: white; font-size: 11px; font-weight: 700;
|
||
padding: 3px 10px; border-radius: 20px;
|
||
letter-spacing: 0.5px;
|
||
animation: blink 1.8s ease-in-out infinite;
|
||
}
|
||
@keyframes blink { 0%,100%{ opacity:1 } 50%{ opacity:0.55 } }
|
||
.cam-label { color: rgba(255,255,255,0.75); font-size: 12px; }
|
||
.play-wrap {
|
||
position: absolute; inset: 0;
|
||
display: flex; align-items: center; justify-content: center;
|
||
z-index: 2;
|
||
}
|
||
.play-btn {
|
||
width: 64px; height: 64px;
|
||
border-radius: 50%;
|
||
background: rgba(255,255,255,0.18);
|
||
backdrop-filter: blur(10px);
|
||
border: 2px solid rgba(255,255,255,0.35);
|
||
display: flex; align-items: center; justify-content: center;
|
||
padding-left: 4px;
|
||
}
|
||
.cam-bottom {
|
||
position: absolute; bottom: 16px; left: 0; right: 0;
|
||
text-align: center; z-index: 2;
|
||
}
|
||
.cam-title { color: rgba(255,255,255,0.88); font-size: 14px; font-weight: 600; letter-spacing: 0.5px; }
|
||
|
||
/* ── Features (圆形 3×2) ── */
|
||
.features-row {
|
||
display: grid; grid-template-columns: repeat(3, 1fr);
|
||
gap: 12px 8px;
|
||
margin-bottom: 22px;
|
||
background: white;
|
||
border-radius: 22px;
|
||
padding: 18px 8px 16px;
|
||
box-shadow: 0 2px 12px rgba(0,0,0,0.05);
|
||
}
|
||
.feat-item { display: flex; flex-direction: column; align-items: center; gap: 8px; cursor: pointer; }
|
||
.feat-circle {
|
||
width: 56px; height: 56px;
|
||
border-radius: 50%;
|
||
display: flex; align-items: center; justify-content: center;
|
||
box-shadow: 0 3px 10px rgba(0,0,0,0.08);
|
||
}
|
||
.feat-label { font-size: 12px; font-weight: 600; color: #374151; text-align: center; }
|
||
|
||
/* ── Schedule Card ── */
|
||
.schedule-card {
|
||
background: linear-gradient(135deg, #0d9488, #0891b2);
|
||
border-radius: 24px;
|
||
padding: 20px 18px;
|
||
margin-bottom: 20px;
|
||
color: white;
|
||
box-shadow: 0 6px 24px rgba(13,148,136,0.38);
|
||
}
|
||
.sch-head { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 16px; }
|
||
.sch-title { font-size: 18px; font-weight: 800; margin: 0 0 4px; letter-spacing: -0.2px; }
|
||
.sch-date { font-size: 12px; color: rgba(255,255,255,0.72); margin: 0; }
|
||
.sch-day-badge {
|
||
background: rgba(255,255,255,0.22);
|
||
padding: 5px 14px; border-radius: 20px;
|
||
font-size: 12px; font-weight: 600; white-space: nowrap; flex-shrink: 0;
|
||
}
|
||
.sch-more-btn {
|
||
background: rgba(255,255,255,0.22);
|
||
padding: 5px 12px; border-radius: 20px;
|
||
font-size: 11px; cursor: pointer; white-space: nowrap; flex-shrink: 0;
|
||
}
|
||
.sch-list { display: flex; flex-direction: column; gap: 10px; }
|
||
.sch-item {
|
||
background: rgba(255,255,255,0.15);
|
||
border-radius: 14px; padding: 11px 14px;
|
||
display: flex; align-items: center; gap: 12px;
|
||
}
|
||
.sch-time { font-size: 13px; font-weight: 700; white-space: nowrap; min-width: 42px; }
|
||
.sch-sep { width: 1px; height: 20px; background: rgba(255,255,255,0.3); flex-shrink: 0; }
|
||
.sch-content { font-size: 13px; font-weight: 500; }
|
||
.sch-empty { text-align: center; font-size: 13px; color: rgba(255,255,255,0.6); margin: 4px 0 0; }
|
||
|
||
/* ── Data Grid ── */
|
||
.data-section { margin-bottom: 20px; }
|
||
.sec-title { font-size: 16px; font-weight: 700; color: #1f2937; margin: 0 0 12px; }
|
||
.data-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
||
.data-card {
|
||
background: white; border-radius: 20px; padding: 16px;
|
||
box-shadow: 0 2px 12px rgba(0,0,0,0.05); cursor: pointer;
|
||
}
|
||
.data-label { font-size: 11px; color: #9ca3af; margin: 0 0 6px; }
|
||
.data-val { font-size: 17px; font-weight: 700; color: #1f2937; margin: 0 0 3px; }
|
||
.data-ext { font-size: 12px; color: #10b981; margin: 0; }
|
||
.data-ext.blue { color: #6366f1; }
|
||
|
||
/* ── Records ── */
|
||
.records-section { }
|
||
.rec-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
|
||
.rec-more { font-size: 13px; color: #f43f5e; cursor: pointer; }
|
||
.rec-list { display: flex; flex-direction: column; gap: 8px; }
|
||
.rec-item {
|
||
background: white; border-radius: 18px; padding: 12px 16px;
|
||
display: flex; align-items: center; gap: 12px;
|
||
box-shadow: 0 2px 10px rgba(0,0,0,0.04);
|
||
}
|
||
.rec-emoji { font-size: 22px; }
|
||
.rec-nurse { font-size: 14px; font-weight: 600; color: #1f2937; margin: 0 0 2px; }
|
||
.rec-time { font-size: 12px; color: #9ca3af; margin: 0; }
|
||
</style>
|