Files
yuezi-saas/client/src/views/home/index.vue
T

323 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>