feat: H5微客宝UI全面升级 - 对齐原型图设计

This commit is contained in:
li
2026-03-14 20:38:01 +08:00
parent 6bf79d6e65
commit 66ea2f558d
5 changed files with 624 additions and 276 deletions
+238 -100
View File
@@ -13,9 +13,7 @@ onMounted(async () => {
try {
const res = await homeApi.index()
homeData.value = res.data?.data
} catch { } finally {
loading.value = false
}
} catch { } finally { loading.value = false }
})
const customer = computed(() => homeData.value?.customer || store.info)
@@ -26,125 +24,265 @@ const nannyOrder = computed(() => homeData.value?.nanny_order)
const mealTypeLabel = { 1: '早餐', 2: '上午茶', 3: '午餐', 4: '下午茶', 5: '晚餐', 6: '宵夜' }
const features = [
{ icon: 'records', label: '护理记录', color: '#ff8fa3', path: '/care/records' },
{ icon: 'chart-trending-o', label: '健康指标', color: '#7ec8e3', path: '/care/metrics' },
{ icon: 'balance-list-o', label: '我的服务', color: '#a8d8a8', path: '/service/orders' },
{ icon: 'user-o', label: '我的月嫂', color: '#c9b8f0', path: '/nanny' },
{ icon: 'gold-coin-o', label: '我的账户', color: '#ffd580', path: '/account' },
{ icon: 'chat-o', label: '投诉反馈', color: '#ffb085', path: '/complaints' },
{ label: '月子餐', bg: '#fff0f4', color: '#f43f5e', icon: 'food-o', path: '/meal' },
{ label: '服务包', bg: '#eff6ff', color: '#3b82f6', icon: 'balance-list-o', path: '/service/orders' },
{ label: '护理记录', bg: '#fffbeb', color: '#f59e0b', icon: 'orders-o', path: '/care/records' },
{ label: '我的月嫂', bg: '#f5f3ff', color: '#8b5cf6', icon: 'user-circle-o', path: '/nanny' },
]
const today = new Date().toLocaleDateString('zh-CN', { month: 'long', day: 'numeric' })
</script>
<template>
<div class="page">
<!-- 顶部横幅 -->
<div class="top-banner">
<div class="banner-content">
<div>
<p class="greeting">您好,{{ customer?.name || '亲爱的客户' }} 👋</p>
<p class="sub-text" v-if="customer?.expected_date">
预产期:{{ customer.expected_date }}
</p>
<p class="sub-text" v-else-if="customer?.actual_date">
宝宝出生:{{ customer.actual_date }}
</p>
</div>
<van-icon name="home-o" size="40" color="rgba(255,255,255,0.6)" />
<!-- 顶部问候区 -->
<div class="header">
<div>
<p class="greet-sub">欢迎回来,</p>
<h2 class="greet-name">
{{ customer?.name || '亲爱的' }}妈妈
<van-icon name="shield-o" color="#f43f5e" size="17" style="vertical-align:middle;margin-left:3px" />
</h2>
</div>
<div class="avatar">
<van-icon name="user-circle-o" size="34" color="#f43f5e" />
</div>
</div>
<div class="content">
<!-- 今日用膳 -->
<section class="card" v-if="todayMeals.length">
<div class="card-header">
<span class="card-title">🍱 今日用膳</span>
<span class="more" @click="router.push('/meal')">查看全部 ›</span>
</div>
<div class="meal-row" v-for="m in todayMeals" :key="m.id">
<span class="meal-type">{{ mealTypeLabel[m.meal_type] || '用餐' }}</span>
<span class="meal-dishes" v-if="m.dishes && m.dishes.length">
{{ m.dishes.map(d => d.name).join(' · ') }}
</span>
<span class="meal-dishes" v-else>待排餐</span>
</div>
</section>
<!-- 滚动主体 -->
<div class="body">
<!-- 最新护理 -->
<section class="card" v-if="latestRecords.length">
<div class="card-header">
<span class="card-title">💆 护理记录</span>
<span class="more" @click="router.push('/care/records')">更多 ›</span>
<!-- 安心守护横幅(原型:宝宝监控卡) -->
<div class="hero-card">
<span class="live-badge">● 在住中</span>
<div class="hero-center">
<div class="hero-icon-wrap">
<van-icon name="home-o" size="30" color="white" />
</div>
<p class="hero-name">{{ customer?.name ? customer.name + ' 妈妈' : '宫中有喜' }}</p>
<p class="hero-desc">贴心守护,安心坐月子 🌸</p>
</div>
<div class="record-item" v-for="r in latestRecords" :key="r.id">
<span class="record-nurse">{{ r.nurse?.name || '护理师' }}</span>
<span class="record-time">{{ r.recorded_at?.slice(0, 10) }}</span>
</div>
</section>
<span class="hero-badge-br">月子会所 · 专属守护</span>
</div>
<!-- 月嫂信息 -->
<section class="card" v-if="nannyOrder">
<div class="card-header">
<span class="card-title">👩‍⚕️ 我的月嫂</span>
<span class="more" @click="router.push('/nanny')">详情 ›</span>
<!-- 4格金刚位 -->
<div class="features-row">
<div
v-for="f in features"
:key="f.path"
class="feat-item"
@click="router.push(f.path)"
>
<div class="feat-icon" :style="{ background: f.bg }">
<van-icon :name="f.icon" :color="f.color" size="26" />
</div>
<span class="feat-label">{{ f.label }}</span>
</div>
<div class="nanny-row">
<van-icon name="user-circle-o" size="36" color="#e8718d" />
<div class="nanny-info">
<p class="nanny-name">{{ nannyOrder.nanny?.name }}</p>
<p class="nanny-sub">{{ nannyOrder.nanny?.level || '专业月嫂' }}</p>
</div>
<!-- 今日服务排期(teal 渐变卡) -->
<div class="schedule-card">
<div class="sch-bg-icon">🍽</div>
<div class="sch-head">
<div>
<h5 class="sch-title">今日用膳</h5>
<p class="sch-date">{{ today }} · 共 {{ todayMeals.length }} 餐</p>
</div>
<span class="sch-more-btn" @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">{{ mealTypeLabel[m.meal_type] || '用餐' }}</span>
<div class="sch-sep" />
<span class="sch-content">
{{ m.dishes?.length ? m.dishes.map(d => d.name).join('、') : '待排餐' }}
</span>
</div>
</div>
</section>
<p class="sch-empty" v-else>今日暂无用餐安排</p>
</div>
<!-- 功能入口九宫格 -->
<section class="card">
<div class="card-title" style="margin-bottom:12px">快捷功能</div>
<div class="features-grid">
<div
v-for="f in features"
:key="f.path"
class="feature-item"
@click="router.push(f.path)"
>
<div class="feature-icon" :style="{ background: f.color + '22' }">
<van-icon :name="f.icon" :color="f.color" size="22" />
<!-- 最近监测数据(2列) -->
<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>
<span class="feature-label">{{ f.label }}</span>
</div>
</div>
</section>
</div>
</div>
</div>
</template>
<style scoped>
.page { background: #f8f0f5; min-height: 100vh; padding-bottom: 60px; }
.top-banner {
background: linear-gradient(135deg, #f5a0b0 0%, #e8718d 100%);
padding: 50px 20px 30px;
border-radius: 0 0 24px 24px;
.page {
background: #fff8fc;
min-height: 100vh;
padding-bottom: 90px;
}
.banner-content { display: flex; justify-content: space-between; align-items: flex-start; color: white; }
.greeting { font-size: 20px; font-weight: 700; margin: 0 0 4px; }
.sub-text { font-size: 13px; opacity: 0.85; margin: 0; }
.content { padding: 16px; }
.card { background: white; border-radius: 16px; padding: 16px; margin-bottom: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.04); }
.card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
.card-title { font-size: 15px; font-weight: 600; color: #1d1d1f; }
.more { font-size: 13px; color: #e8718d; }
.meal-row { display: flex; gap: 8px; padding: 6px 0; border-bottom: 1px solid #f5f5f5; }
.meal-row:last-child { border: none; }
.meal-type { font-size: 12px; color: white; background: #e8718d; padding: 2px 8px; border-radius: 10px; white-space: nowrap; }
.meal-dishes { font-size: 13px; color: #666; }
.record-item { display: flex; justify-content: space-between; padding: 6px 0; border-bottom: 1px solid #f5f5f5; font-size: 13px; }
.record-item:last-child { border: none; }
.record-nurse { color: #333; }
.record-time { color: #999; }
.nanny-row { display: flex; align-items: center; gap: 12px; }
.nanny-name { font-size: 15px; font-weight: 600; margin: 0 0 2px; }
.nanny-sub { font-size: 12px; color: #999; margin: 0; }
.features-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
.feature-item { display: flex; flex-direction: column; align-items: center; gap: 6px; cursor: pointer; }
.feature-icon { width: 50px; height: 50px; border-radius: 14px; display: flex; align-items: center; justify-content: center; }
.feature-label { font-size: 12px; color: #666; text-align: center; }
/* ── Header ─────────────────────────────── */
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 52px 22px 18px;
background: white;
}
.greet-sub { font-size: 12px; color: #9ca3af; margin: 0 0 3px; }
.greet-name { font-size: 20px; font-weight: 700; color: #111827; margin: 0; }
.avatar {
width: 50px; height: 50px;
background: #fff0f4;
border-radius: 50%;
border: 2px solid #fecdd3;
display: flex; align-items: center; justify-content: center;
}
/* ── Body ───────────────────────────────── */
.body { padding: 16px; }
/* ── Hero Card ──────────────────────────── */
.hero-card {
background: linear-gradient(135deg, #0d9488, #14b8a6);
border-radius: 28px;
height: 178px;
margin-bottom: 22px;
position: relative;
overflow: hidden;
box-shadow: 0 8px 28px rgba(20, 184, 166, 0.38);
display: flex;
align-items: center;
justify-content: center;
}
.live-badge {
position: absolute;
top: 14px; left: 16px;
background: rgba(255,255,255,0.22);
color: white; font-size: 11px;
padding: 3px 10px; border-radius: 20px;
animation: pulse 2s infinite;
}
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.5; } }
.hero-badge-br {
position: absolute;
bottom: 12px; right: 14px;
background: rgba(0,0,0,0.16);
color: white; font-size: 10px;
padding: 3px 10px; border-radius: 20px;
}
.hero-center { display: flex; flex-direction: column; align-items: center; gap: 6px; }
.hero-icon-wrap {
width: 58px; height: 58px;
background: rgba(255,255,255,0.2);
backdrop-filter: blur(8px);
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
margin-bottom: 4px;
}
.hero-name { color: white; font-size: 16px; font-weight: 700; margin: 0; }
.hero-desc { color: rgba(255,255,255,0.82); font-size: 12px; margin: 0; }
/* ── Features 4-grid ───────────────────── */
.features-row {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
margin-bottom: 22px;
}
.feat-item { display: flex; flex-direction: column; align-items: center; gap: 7px; cursor: pointer; }
.feat-icon {
width: 52px; height: 52px;
border-radius: 18px;
display: flex; align-items: center; justify-content: center;
box-shadow: 0 2px 8px rgba(0,0,0,0.07);
}
.feat-label { font-size: 11px; font-weight: 600; color: #374151; text-align: center; }
/* ── Schedule Card (Teal) ──────────────── */
.schedule-card {
background: linear-gradient(135deg, #0d9488, #0891b2);
border-radius: 28px;
padding: 20px 18px;
margin-bottom: 20px;
color: white;
position: relative;
overflow: hidden;
box-shadow: 0 6px 22px rgba(13, 148, 136, 0.35);
}
.sch-bg-icon {
position: absolute; right: -8px; top: -8px;
font-size: 80px; opacity: 0.12;
}
.sch-head { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 14px; }
.sch-title { font-size: 16px; font-weight: 700; margin: 0 0 3px; }
.sch-date { font-size: 11px; color: rgba(255,255,255,0.72); margin: 0; }
.sch-more-btn {
background: rgba(255,255,255,0.22);
padding: 4px 12px; border-radius: 20px;
font-size: 11px; cursor: pointer; white-space: nowrap;
}
.sch-list { display: flex; flex-direction: column; gap: 8px; }
.sch-item {
background: rgba(255,255,255,0.14);
border-radius: 14px; padding: 9px 14px;
display: flex; align-items: center; gap: 10px;
}
.sch-time { font-size: 12px; font-weight: 600; white-space: nowrap; }
.sch-sep { width: 1px; height: 18px; background: rgba(255,255,255,0.25); }
.sch-content { font-size: 12px; font-weight: 500; }
.sch-empty { text-align: center; font-size: 13px; color: rgba(255,255,255,0.65); margin: 0; }
/* ── Data Grid ─────────────────────────── */
.data-section { margin-bottom: 20px; }
.sec-title { font-size: 15px; 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: 22px; padding: 16px;
box-shadow: 0 2px 14px 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>