feat: 自定义 tabbar 组件替代原生 tabbar,支持 CSS 控制图标大小

Made-with: Cursor
This commit is contained in:
2026-03-11 17:02:57 +08:00
parent 79a78da82c
commit 007d380757
23 changed files with 658 additions and 287 deletions

View File

@@ -18,6 +18,7 @@ export default defineAppConfig({
backgroundColor: '#F8FAFC'
},
tabBar: {
custom: true,
color: '#64748b',
selectedColor: '#16a34a',
backgroundColor: '#ffffff',
@@ -25,19 +26,27 @@ export default defineAppConfig({
list: [
{
pagePath: 'pages/home/index',
text: '首页'
text: '首页',
iconPath: 'assets/tabbar/home.png',
selectedIconPath: 'assets/tabbar/home-active.png'
},
{
pagePath: 'pages/menu/index',
text: '点餐'
text: '点餐',
iconPath: 'assets/tabbar/menu.png',
selectedIconPath: 'assets/tabbar/menu-active.png'
},
{
pagePath: 'pages/orders/index',
text: '订单'
text: '订单',
iconPath: 'assets/tabbar/orders.png',
selectedIconPath: 'assets/tabbar/orders-active.png'
},
{
pagePath: 'pages/profile/index',
text: '我的'
text: '我的',
iconPath: 'assets/tabbar/profile.png',
selectedIconPath: 'assets/tabbar/profile-active.png'
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
src/assets/tabbar/home.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 875 B

BIN
src/assets/tabbar/menu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 987 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 776 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,101 @@
<template>
<view class="tab-bar">
<view
v-for="(item, idx) in tabs"
:key="item.pagePath"
class="tab-bar__item"
@tap="onSwitch(idx)"
>
<view class="tab-bar__icon">
<image
class="tab-bar__icon-img"
:src="current === idx ? item.activeIcon : item.icon"
mode="aspectFit"
/>
</view>
<text
class="tab-bar__label"
:class="{ 'tab-bar__label--active': current === idx }"
>
{{ item.text }}
</text>
</view>
</view>
</template>
<script setup lang="ts">
import Taro from '@tarojs/taro'
import homeIcon from '@/assets/tabbar/home.png'
import homeActiveIcon from '@/assets/tabbar/home-active.png'
import menuIcon from '@/assets/tabbar/menu.png'
import menuActiveIcon from '@/assets/tabbar/menu-active.png'
import ordersIcon from '@/assets/tabbar/orders.png'
import ordersActiveIcon from '@/assets/tabbar/orders-active.png'
import profileIcon from '@/assets/tabbar/profile.png'
import profileActiveIcon from '@/assets/tabbar/profile-active.png'
defineProps<{ current: number }>()
const tabs = [
{ pagePath: '/pages/home/index', text: '首页', icon: homeIcon, activeIcon: homeActiveIcon },
{ pagePath: '/pages/menu/index', text: '点餐', icon: menuIcon, activeIcon: menuActiveIcon },
{ pagePath: '/pages/orders/index', text: '订单', icon: ordersIcon, activeIcon: ordersActiveIcon },
{ pagePath: '/pages/profile/index', text: '我的', icon: profileIcon, activeIcon: profileActiveIcon }
]
function onSwitch(idx: number) {
Taro.switchTab({ url: tabs[idx].pagePath })
}
</script>
<style lang="scss">
.tab-bar {
display: flex;
align-items: center;
justify-content: space-around;
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 100px;
padding-bottom: env(safe-area-inset-bottom);
background-color: #fff;
border-top: 1rpx solid #e5e7eb;
z-index: 999;
&__item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex: 1;
padding: 8px 0;
}
&__icon {
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
&__icon-img {
width: 44px;
height: 44px;
}
&__label {
font-size: 20px;
color: #64748b;
margin-top: 2px;
line-height: 1.2;
&--active {
color: #16a34a;
}
}
}
</style>

View File

@@ -4,34 +4,55 @@ import { pinia, useAppStore, useCartStore } from '@/stores'
import {
FulfillmentScenes,
demoHotProducts,
type FulfillmentScene,
type MiniProductCard
} from '@/shared'
import { openRoute } from '@/utils/router'
const categoryCards = [
{ key: 'recommend', icon: '⭐', label: '推荐', toneClass: 'home-page__cat-icon--orange' },
{ key: 'meal', icon: '🍚', label: '主食', toneClass: 'home-page__cat-icon--green-soft' },
{ key: 'snack', icon: '🍜', label: '小吃', toneClass: 'home-page__cat-icon--yellow', badge: 'HOT' },
{ key: 'drink', icon: '🧋', label: '饮品', toneClass: 'home-page__cat-icon--green-light' },
{ key: 'set', icon: '📦', label: '套餐', toneClass: 'home-page__cat-icon--mint' },
{ key: 'dessert', icon: '🍰', label: '甜品', toneClass: 'home-page__cat-icon--amber' }
] as const
export interface HomeCategoryItem {
key: string
char: string
label: string
tone: string
badge?: string
}
const trustItems = [
{ key: 'fresh', icon: '⚡', label: '现炒现做' },
{ key: 'fast', icon: '🕐', label: '30分钟送达' },
{ key: 'pickup', icon: '🛍', label: '自提更快' },
{ key: 'quality', icon: '🛡', label: '品质保证' }
] as const
export interface HomeTrustItem {
key: string
label: string
}
const categoryCards: HomeCategoryItem[] = [
{ key: 'recommend', char: '荐', label: '推荐', tone: 'warm' },
{ key: 'staple', char: '饭', label: '主食', tone: 'green' },
{ key: 'snack', char: '食', label: '小吃', tone: 'amber', badge: '热卖' },
{ key: 'drink', char: '饮', label: '饮品', tone: 'teal' },
{ key: 'combo', char: '套', label: '套餐', tone: 'blue' },
{ key: 'dessert', char: '甜', label: '甜品', tone: 'rose' }
]
const trustItems: HomeTrustItem[] = [
{ key: 'fresh', label: '现炒现做' },
{ key: 'fast', label: '30分钟送达' },
{ key: 'pickup', label: '自提免等' },
{ key: 'quality', label: '品质保障' }
]
export function useHomePage () {
const appStore = useAppStore(pinia)
const cartStore = useCartStore(pinia)
const currentStore = computed(() => appStore.currentStore)
const cartCount = computed(() => cartStore.itemCount)
const currentScene = computed(() => appStore.scene)
const sceneOptions = computed(() => appStore.sceneOptions)
const isDineIn = computed(() => appStore.scene === FulfillmentScenes.DineIn)
const recommendedProducts = ref<MiniProductCard[]>(demoHotProducts)
function switchScene (scene: string) {
appStore.setScene(scene as FulfillmentScene)
}
async function refreshPage () {
await appStore.initBootstrap()
await appStore.initStores()
@@ -52,11 +73,14 @@ export function useHomePage () {
return {
cartCount,
categoryCards,
currentScene,
currentStore,
goMenu,
goStoreSelect,
isDineIn,
recommendedProducts,
sceneOptions,
switchScene,
trustItems
}
}

View File

@@ -1,112 +1,151 @@
<template>
<view class="home-page">
<view class="hp">
<!-- Store Card -->
<view class="home-page__store-card">
<view class="home-page__store-icon-wrap">
<text class="home-page__store-icon-emoji">📍</text>
</view>
<view class="home-page__store-text">
<view class="home-page__store-name">
<text class="home-page__store-name-text">{{ currentStore.name }}</text>
<view class="home-page__store-status">
<view class="home-page__store-status-dot" />
<view class="hp__store" @click="goStoreSelect">
<view class="hp__store-info">
<view class="hp__store-row">
<text class="hp__store-name">{{ currentStore.name }}</text>
<view class="hp__store-status">
<view class="hp__store-dot" />
<text>营业中</text>
</view>
</view>
<text class="home-page__store-addr">{{ currentStore.address }}</text>
<text class="hp__store-addr">{{ currentStore.address }}</text>
</view>
<view class="home-page__store-switch" @click="goStoreSelect">
<text>切换</text>
<text class="home-page__chevron"></text>
<view class="hp__store-switch">
<text>切换门店</text>
<text class="hp__store-arrow"></text>
</view>
</view>
<!-- Dine-in Scan Button -->
<view v-if="isDineIn" class="home-page__scan-tab" @click="goMenu">
<text>🔲</text>
<text>堂食扫码点餐</text>
<view class="hp__scan" @click="goMenu">
<view class="hp__scan-ico">
<IconScan size="18" color="#fff" />
</view>
<text class="hp__scan-text">堂食扫码点餐</text>
</view>
<!-- Search Bar -->
<view class="home-page__search" @click="goMenu">
<text class="home-page__search-icon">🔍</text>
<text class="home-page__search-placeholder">搜索菜品套餐饮品</text>
<view class="hp__search" @click="goMenu">
<view class="hp__search-ico">
<IconSearch size="14" color="#9CA3AF" />
</view>
<text class="hp__search-ph">搜索菜品套餐饮品</text>
</view>
<!-- Banner -->
<view class="home-page__banner" @click="goMenu">
<view class="home-page__banner-content">
<view class="home-page__banner-tag">
<text> 新客专享</text>
<view class="hp__banner" @click="goMenu">
<view class="hp__banner-body">
<view class="hp__banner-label">
<text>新客专享</text>
</view>
<text class="home-page__banner-title">首单立减</text>
<view class="home-page__banner-amount">
<text class="home-page__banner-unit">¥</text>
<text>12</text>
<text class="hp__banner-title">首单立减</text>
<view class="hp__banner-price">
<text class="hp__banner-sym">¥</text>
<text class="hp__banner-val">12</text>
</view>
<text class="home-page__banner-desc">全场满38再减8 · 限时三天</text>
<view class="home-page__banner-cta">
<text>立即领取 </text>
<text class="hp__banner-note">全场满 38 再减 8 · 限时三天</text>
<view class="hp__banner-btn">
<text>立即领取</text>
</view>
</view>
<view class="home-page__banner-ring-outer" />
<view class="home-page__banner-ring" />
<image
class="home-page__banner-food-img"
src="https://images.unsplash.com/photo-1504674900247-0877df9cc836?w=240&h=240&fit=crop&auto=format&q=80"
class="hp__banner-img"
src="https://images.unsplash.com/photo-1504674900247-0877df9cc836?w=280&h=280&fit=crop&q=80"
mode="aspectFill"
/>
<view class="hp__banner-ring" />
</view>
<!-- Categories -->
<view class="home-page__categories">
<view class="hp__cats">
<view
v-for="category in categoryCards"
:key="category.key"
class="home-page__cat-item"
v-for="c in categoryCards"
:key="c.key"
class="hp__cat"
@click="goMenu"
>
<view class="home-page__cat-icon" :class="category.toneClass">
<text>{{ category.icon }}</text>
<text v-if="category.badge" class="home-page__cat-badge">{{ category.badge }}</text>
<view class="hp__cat-ico" :class="`hp__cat-ico--${c.tone}`">
<text>{{ c.char }}</text>
<text v-if="c.badge" class="hp__cat-badge">{{ c.badge }}</text>
</view>
<text class="home-page__cat-label">{{ category.label }}</text>
<text class="hp__cat-name">{{ c.label }}</text>
</view>
</view>
<!-- Section Header -->
<view class="home-page__section-head">
<text class="home-page__section-title">热门推荐</text>
<view class="home-page__section-more" @click="goMenu">
<text>查看全部 </text>
<!-- Promo Strip -->
<view class="hp__promo" @click="goMenu">
<view class="hp__promo-tag">
<text>限时</text>
</view>
<text class="hp__promo-text">人气双人餐 超值特惠 ¥49 </text>
<text class="hp__promo-arrow"></text>
</view>
<!-- Section Header: Hot Products -->
<view class="hp__hd">
<text class="hp__hd-title">热门推荐</text>
<view class="hp__hd-more" @click="goMenu">
<text>查看全部</text>
<text class="hp__hd-arrow"></text>
</view>
</view>
<!-- Product List -->
<view class="home-page__product-list">
<ProductCard
v-for="product in recommendedProducts"
:key="product.id"
:product="product"
@select="goMenu"
@action="goMenu"
/>
</view>
<!-- Trust Section -->
<view class="home-page__trust">
<view class="home-page__trust-grid">
<view v-for="item in trustItems" :key="item.key" class="home-page__trust-item">
<view class="home-page__trust-icon"><text>{{ item.icon }}</text></view>
<text class="home-page__trust-label">{{ item.label }}</text>
<!-- Product Grid -->
<view class="hp__grid">
<view
v-for="p in recommendedProducts"
:key="p.id"
class="hp__card"
@click="goMenu"
>
<view class="hp__card-cover">
<image class="hp__card-img" :src="p.coverImageUrl" mode="aspectFill" />
<text v-if="p.tagTexts?.length" class="hp__card-tag">{{ p.tagTexts[0] }}</text>
</view>
<view class="hp__card-body">
<text class="hp__card-name">{{ p.name }}</text>
<text class="hp__card-desc">{{ p.description }}</text>
<text class="hp__card-sales">{{ p.salesText }}</text>
<view class="hp__card-foot">
<view class="hp__card-prices">
<text class="hp__card-sym">¥</text>
<text class="hp__card-num">{{ p.price }}</text>
<text v-if="p.originalPriceText" class="hp__card-old">¥{{ p.originalPriceText }}</text>
</view>
<view
class="hp__card-add"
:class="{ 'hp__card-add--pill': p.hasOptions }"
@click.stop="goMenu"
>
<text>{{ p.hasOptions ? '选规格' : '+' }}</text>
</view>
</view>
</view>
</view>
</view>
<!-- Trust Strip -->
<view class="hp__trust">
<view v-for="t in trustItems" :key="t.key" class="hp__trust-pill">
<view class="hp__trust-dot" />
<text>{{ t.label }}</text>
</view>
</view>
<!-- Bottom Spacer (TabBar safe area) -->
<view class="hp__spacer" />
<!-- Custom TabBar -->
<TabBar :current="0" />
<!-- Floating Cart FAB -->
<view v-if="cartCount > 0" class="home-page__fab" @click="goMenu">
<text class="home-page__fab-icon">🛒</text>
<view class="home-page__fab-badge">
<view v-if="cartCount > 0" class="hp__fab" @click="goMenu">
<view class="hp__fab-ico">
<IconCart size="20" color="#fff" />
</view>
<view class="hp__fab-badge">
<text>{{ cartCount }}</text>
</view>
</view>
@@ -114,7 +153,8 @@
</template>
<script setup lang="ts">
import ProductCard from '@/components/product-card/index.vue'
import { Search as IconSearch, Cart2 as IconCart, Scan2 as IconScan } from '@nutui/icons-vue-taro'
import TabBar from '@/components/tab-bar/index.vue'
import { useHomePage } from './composables/useHomePage'
const {
@@ -123,7 +163,6 @@ const {
currentStore,
goMenu,
goStoreSelect,
isDineIn,
recommendedProducts,
trustItems
} = useHomePage()

View File

@@ -1,31 +1,30 @@
.home-page__banner {
border-radius: 24px;
@use '../../../styles/variables' as *;
.hp__banner {
border-radius: $r-xl;
overflow: hidden;
position: relative;
min-height: 160px;
background: linear-gradient(135deg, #14532D 0%, #166534 35%, #15803D 70%, #1A7A42 100%);
min-height: 156px;
background: linear-gradient(135deg, #14532D 0%, #166534 40%, #15803D 100%);
padding: 22px 20px;
display: flex;
flex-direction: column;
justify-content: center;
box-shadow: 0 4px 16px rgba(22, 101, 52, 0.25), 0 8px 32px rgba(22, 101, 52, 0.12);
}
.home-page__banner-content {
.hp__banner-body {
position: relative;
z-index: 1;
max-width: 60%;
z-index: 2;
max-width: 58%;
}
.home-page__banner-tag {
.hp__banner-label {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px 10px;
background: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 999px;
color: rgba(255, 255, 255, 0.9);
color: rgba(255, 255, 255, 0.88);
font-size: 11px;
font-weight: 500;
width: fit-content;
@@ -33,89 +32,79 @@
letter-spacing: 0.3px;
}
.home-page__banner-title {
.hp__banner-title {
font-size: 15px;
font-weight: 500;
color: rgba(255, 255, 255, 0.85);
color: rgba(255, 255, 255, 0.82);
display: block;
line-height: 1.4;
}
.home-page__banner-amount {
.hp__banner-price {
display: flex;
align-items: baseline;
gap: 2px;
margin: 2px 0;
}
.hp__banner-sym {
font-size: 18px;
font-weight: 700;
color: #fff;
}
.hp__banner-val {
font-size: 36px;
font-weight: 800;
color: #fff;
line-height: 1.1;
margin: 4px 0 2px;
display: flex;
align-items: baseline;
gap: 2px;
}
.home-page__banner-unit {
font-size: 18px;
font-weight: 700;
}
.home-page__banner-desc {
.hp__banner-note {
font-size: 12px;
color: rgba(255, 255, 255, 0.6);
color: rgba(255, 255, 255, 0.55);
margin-top: 6px;
display: block;
line-height: 1.5;
letter-spacing: 0.2px;
}
.home-page__banner-cta {
.hp__banner-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 4px;
height: 32px;
padding: 0 18px;
background: rgba(255, 255, 255, 0.95);
height: 30px;
padding: 0 16px;
background: rgba(255, 255, 255, 0.93);
color: #14532D;
font-size: 12px;
font-weight: 700;
border-radius: 999px;
width: fit-content;
margin-top: 14px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
letter-spacing: 0.3px;
}
.home-page__banner-food-img {
.hp__banner-img {
position: absolute;
right: 10px;
right: 12px;
top: 50%;
transform: translateY(-50%);
width: 120px;
height: 120px;
width: 116px;
height: 116px;
border-radius: 50%;
border: 3px solid rgba(255, 255, 255, 0.15);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
border: 3px solid rgba(255, 255, 255, 0.12);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.18);
z-index: 1;
background: linear-gradient(135deg, rgba(34, 197, 94, 0.2), rgba(22, 163, 74, 0.2));
}
.home-page__banner-ring {
.hp__banner-ring {
position: absolute;
right: -2px;
right: 2px;
top: 50%;
transform: translateY(-50%);
width: 144px;
height: 144px;
width: 140px;
height: 140px;
border-radius: 50%;
border: 1.5px solid rgba(255, 255, 255, 0.08);
z-index: 0;
}
.home-page__banner-ring-outer {
position: absolute;
right: -14px;
top: 50%;
transform: translateY(-50%);
width: 168px;
height: 168px;
border-radius: 50%;
border: 1px solid rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.06);
z-index: 0;
}

View File

@@ -1,73 +1,57 @@
@use '../../../styles/variables' as *;
.home-page {
// ─── Page Shell ───
.hp {
min-height: 100vh;
padding: 14px 16px 24px;
padding: 12px 16px 0;
display: flex;
flex-direction: column;
gap: 14px;
gap: 16px;
background: $bg;
}
.home-page__store-card {
// ─── Store Card ───
.hp__store {
background: $card;
border-radius: $r-lg;
padding: 14px 16px;
padding: 16px;
box-shadow: $shadow-sm;
display: flex;
align-items: center;
gap: 12px;
}
.home-page__store-icon-wrap {
width: 42px;
height: 42px;
border-radius: $r-sm;
background: $primary-light;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.home-page__store-icon-emoji {
font-size: 18px;
}
.home-page__store-text {
.hp__store-info {
flex: 1;
min-width: 0;
}
.home-page__store-name {
font-size: 15px;
font-weight: 600;
color: $text-1;
.hp__store-row {
display: flex;
align-items: center;
gap: 6px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
gap: 8px;
}
.home-page__store-name-text {
.hp__store-name {
font-size: 16px;
font-weight: 600;
color: $text-1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.home-page__store-status {
.hp__store-status {
display: inline-flex;
align-items: center;
gap: 3px;
gap: 4px;
font-size: 11px;
color: $primary;
font-weight: 500;
flex-shrink: 0;
}
.home-page__store-status-dot {
.hp__store-dot {
width: 6px;
height: 6px;
border-radius: 50%;
@@ -75,87 +59,175 @@
animation: pulse-dot 2s ease-in-out infinite;
}
.home-page__store-addr {
.hp__store-addr {
font-size: 12px;
color: $text-3;
margin-top: 3px;
white-space: nowrap;
margin-top: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: block;
}
.home-page__store-switch {
font-size: 13px;
color: $primary;
font-weight: 500;
.hp__store-switch {
display: flex;
align-items: center;
gap: 2px;
font-size: 13px;
color: $primary;
font-weight: 500;
padding: 8px 0;
white-space: nowrap;
flex-shrink: 0;
}
.home-page__chevron {
font-size: 16px;
.hp__store-arrow {
font-size: 18px;
line-height: 1;
color: $primary;
}
.home-page__scan-tab {
// ─── Scene Tabs ───
.hp__tabs {
display: flex;
gap: 0;
background: #EEF2F6;
border-radius: $r-full;
padding: 3px;
}
.hp__tab {
flex: 1;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
height: 44px;
border-radius: $r-sm;
background: $primary;
color: #fff;
border-radius: $r-full;
font-size: 14px;
font-weight: 600;
box-shadow: 0 2px 10px rgba(22, 163, 74, 0.35);
font-weight: 500;
color: $text-3;
transition: all 0.25s ease;
}
.home-page__search {
.hp__tab--on {
background: $primary;
color: #fff;
font-weight: 600;
box-shadow: 0 2px 8px rgba(22, 163, 74, 0.3);
}
// ─── Dine-in Scan Button ───
.hp__scan {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
height: 52px;
border-radius: $r-md;
background: $primary;
box-shadow: 0 2px 12px rgba(22, 163, 74, 0.3);
}
.hp__scan-ico {
display: flex;
align-items: center;
}
.hp__scan-text {
font-size: 16px;
font-weight: 600;
color: #fff;
letter-spacing: 0.5px;
}
// ─── Search Bar ───
.hp__search {
display: flex;
align-items: center;
gap: 8px;
background: #F1F5F9;
border-radius: $r-md;
border-radius: $r-sm;
padding: 0 14px;
height: 40px;
}
.home-page__search-icon {
font-size: 14px;
.hp__search-ico {
display: flex;
align-items: center;
flex-shrink: 0;
}
.home-page__search-placeholder {
.hp__search-ph {
font-size: 14px;
color: $text-4;
}
.home-page__section-head {
// ─── Section Header ───
.hp__hd {
display: flex;
align-items: center;
justify-content: space-between;
padding: 2px 0;
padding: 4px 0;
}
.home-page__section-title {
.hp__hd-title {
font-size: 18px;
font-weight: 700;
color: $text-1;
letter-spacing: 0.3px;
}
.home-page__section-more {
font-size: 13px;
color: $text-4;
.hp__hd-more {
display: flex;
align-items: center;
gap: 2px;
font-size: 13px;
color: $text-4;
padding: 8px 0;
}
.home-page__product-list {
display: flex;
flex-direction: column;
gap: 12px;
.hp__hd-arrow {
font-size: 16px;
line-height: 1;
}
// ─── Promo Strip ───
.hp__promo {
display: flex;
align-items: center;
gap: 8px;
background: #FFFBEB;
border-radius: $r-sm;
padding: 10px 14px;
border: 1px solid #FEF3C7;
}
.hp__promo-tag {
background: $accent;
color: #fff;
font-size: 10px;
font-weight: 600;
padding: 2px 7px;
border-radius: 4px;
flex-shrink: 0;
}
.hp__promo-text {
flex: 1;
font-size: 13px;
color: #92400E;
font-weight: 500;
}
.hp__promo-arrow {
font-size: 18px;
color: $accent;
line-height: 1;
flex-shrink: 0;
}
// ─── Bottom Spacer ───
.hp__spacer {
height: 80px;
flex-shrink: 0;
}

View File

@@ -1,54 +1,54 @@
@use '../../../styles/variables' as *;
.home-page__categories {
.hp__cats {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 2px;
padding: 4px 0;
}
.home-page__cat-item {
.hp__cat {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 6px 2px 8px;
border-radius: $r-sm;
padding: 4px 0 6px;
}
.home-page__cat-icon {
.hp__cat-ico {
width: 48px;
height: 48px;
border-radius: 16px;
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
font-size: 18px;
font-size: 20px;
font-weight: 700;
&--orange { background: #FFF4ED; }
&--green-soft { background: #F0FDF4; }
&--yellow { background: #FFFBEB; }
&--green-light { background: #ECFDF5; }
&--mint { background: #F0FDF4; }
&--amber { background: #FFF7ED; }
&--warm { background: #FFF4ED; color: #EA580C; }
&--green { background: #F0FDF4; color: #16A34A; }
&--amber { background: #FFFBEB; color: #D97706; }
&--teal { background: #F0FDFA; color: #0D9488; }
&--blue { background: #EFF6FF; color: #2563EB; }
&--rose { background: #FFF1F2; color: #E11D48; }
}
.home-page__cat-label {
font-size: 12px;
font-weight: 500;
color: $text-2;
}
.home-page__cat-badge {
.hp__cat-badge {
position: absolute;
top: -3px;
right: -6px;
top: -4px;
right: -8px;
font-size: 9px;
font-weight: 600;
background: $red;
color: #fff;
padding: 1px 5px;
border-radius: 999px;
font-weight: 600;
line-height: 1.4;
}
.hp__cat-name {
font-size: 12px;
font-weight: 500;
color: $text-2;
}

View File

@@ -1,6 +1,6 @@
@use '../../../styles/variables' as *;
.home-page__fab {
.hp__fab {
position: fixed;
bottom: 92px;
right: 16px;
@@ -8,30 +8,33 @@
width: 52px;
height: 52px;
border-radius: 50%;
background: linear-gradient(135deg, $primary 0%, $primary-dark 100%);
background: $primary;
box-shadow: 0 4px 16px rgba(22, 163, 74, 0.35);
display: flex;
align-items: center;
justify-content: center;
}
.home-page__fab-icon {
font-size: 20px;
.hp__fab-ico {
display: flex;
align-items: center;
justify-content: center;
}
.home-page__fab-badge {
.hp__fab-badge {
position: absolute;
top: -2px;
right: -2px;
width: 20px;
height: 20px;
border-radius: 50%;
min-width: 18px;
height: 18px;
border-radius: 9px;
background: $red;
color: #fff;
font-size: 11px;
font-size: 10px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
padding: 0 4px;
border: 2px solid #fff;
}

View File

@@ -1,5 +1,6 @@
@forward './base.scss';
@forward './banner.scss';
@forward './category.scss';
@forward './product.scss';
@forward './trust.scss';
@forward './fab.scss';

View File

@@ -0,0 +1,137 @@
@use '../../../styles/variables' as *;
.hp__grid {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.hp__card {
width: calc(50% - 6px);
background: $card;
border-radius: $r-lg;
box-shadow: $shadow-sm;
overflow: hidden;
display: flex;
flex-direction: column;
}
.hp__card-cover {
width: 100%;
height: 136px;
position: relative;
overflow: hidden;
}
.hp__card-img {
width: 100%;
height: 100%;
background: $border;
}
.hp__card-tag {
position: absolute;
top: 8px;
left: 8px;
font-size: 10px;
font-weight: 600;
padding: 2px 8px;
border-radius: 6px;
color: #fff;
background: rgba(22, 163, 74, 0.88);
line-height: 1.5;
}
.hp__card-body {
padding: 10px 12px 12px;
display: flex;
flex-direction: column;
flex: 1;
}
.hp__card-name {
font-size: 14px;
font-weight: 600;
color: $text-1;
line-height: 1.4;
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.hp__card-desc {
font-size: 11px;
color: $text-3;
margin-top: 3px;
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
}
.hp__card-sales {
font-size: 10px;
color: $text-4;
margin-top: 4px;
display: block;
}
.hp__card-foot {
display: flex;
align-items: flex-end;
justify-content: space-between;
margin-top: auto;
padding-top: 8px;
}
.hp__card-prices {
display: flex;
align-items: baseline;
gap: 1px;
flex-wrap: wrap;
}
.hp__card-sym {
font-size: 11px;
font-weight: 600;
color: $primary-dark;
}
.hp__card-num {
font-size: 18px;
font-weight: 700;
color: $primary-dark;
line-height: 1;
}
.hp__card-old {
font-size: 10px;
color: $text-4;
text-decoration: line-through;
margin-left: 3px;
}
.hp__card-add {
width: 28px;
height: 28px;
border-radius: 50%;
background: $primary;
color: #fff;
font-size: 18px;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
&--pill {
width: auto;
height: 28px;
padding: 0 10px;
border-radius: 999px;
font-size: 11px;
font-weight: 600;
}
}

View File

@@ -1,41 +1,28 @@
@use '../../../styles/variables' as *;
.home-page__trust {
background: $card;
border-radius: $r-lg;
padding: 16px 12px;
box-shadow: $shadow-xs;
}
.home-page__trust-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 6px;
}
.home-page__trust-item {
.hp__trust {
display: flex;
flex-direction: column;
flex-wrap: wrap;
gap: 8px;
padding: 4px 0;
}
.hp__trust-pill {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 4px;
}
.home-page__trust-icon {
width: 38px;
height: 38px;
border-radius: 11px;
padding: 6px 12px;
background: $primary-lighter;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
border-radius: 999px;
font-size: 11px;
color: $primary-dark;
font-weight: 500;
}
.home-page__trust-label {
font-size: 11px;
color: $text-2;
font-weight: 500;
text-align: center;
line-height: 1.4;
.hp__trust-dot {
width: 5px;
height: 5px;
border-radius: 50%;
background: $primary;
flex-shrink: 0;
}

View File

@@ -102,6 +102,9 @@
@change-qty="(key, delta) => cartStore.changeQuantity(key, delta)"
/>
<!-- Custom TabBar -->
<TabBar :current="1" />
<!-- Spec Popup -->
<SpecPopup
v-if="detailVisible && activeDetail"
@@ -126,6 +129,7 @@
import ProductCard from '@/components/product-card/index.vue'
import CartDrawer from '@/components/cart-drawer/index.vue'
import SpecPopup from '@/components/spec-popup/index.vue'
import TabBar from '@/components/tab-bar/index.vue'
import { useMenuPage } from './composables/useMenuPage'
const {

View File

@@ -61,12 +61,16 @@
<NutButton type="primary" block @click="goMenu">去点餐</NutButton>
</view>
</view>
<!-- Custom TabBar -->
<TabBar :current="2" />
</view>
</template>
<script setup lang="ts">
import { Button as NutButton, Empty, Price, Tag } from '@nutui/nutui-taro'
import PageHero from '@/components/page-hero/index.vue'
import TabBar from '@/components/tab-bar/index.vue'
import { useOrdersPage } from './composables/useOrdersPage'
const {

View File

@@ -44,12 +44,16 @@
<NutButton block plain @click="goDineIn">编辑桌号</NutButton>
</view>
</view>
<!-- Custom TabBar -->
<TabBar :current="3" />
</view>
</template>
<script setup lang="ts">
import { Button as NutButton } from '@nutui/nutui-taro'
import PageHero from '@/components/page-hero/index.vue'
import TabBar from '@/components/tab-bar/index.vue'
import { useProfilePage } from './composables/useProfilePage'
const {

View File

@@ -8,13 +8,13 @@ const productMap: Record<Id, MiniProductCard> = {
id: '2000000000000000001',
name: '招牌鸡腿饭',
description: '招牌酱汁鸡腿搭配时蔬与米饭',
coverImageUrl: 'https://dummyimage.com/160x160/f3f4f6/111827&text=Chicken',
coverImageUrl: 'https://images.unsplash.com/photo-1546069901-ba9599a7e63c?w=400&h=400&fit=crop&q=80',
price: 22,
priceText: formatPrice(22),
originalPrice: 26,
originalPriceText: formatPrice(26),
salesText: '月售 268',
tagTexts: ['招牌', '热销'],
tagTexts: ['招牌'],
soldOut: false,
hasOptions: false
},
@@ -22,13 +22,13 @@ const productMap: Record<Id, MiniProductCard> = {
id: '2000000000000000002',
name: '黑椒牛柳饭',
description: '黑椒风味牛柳,适合工作日快餐',
coverImageUrl: 'https://dummyimage.com/160x160/e5e7eb/111827&text=Beef',
coverImageUrl: 'https://images.unsplash.com/photo-1544025162-d76694265947?w=400&h=400&fit=crop&q=80',
price: 29,
priceText: formatPrice(29),
originalPrice: 33,
originalPriceText: formatPrice(33),
salesText: '月售 186',
tagTexts: ['牛肉', '推荐'],
tagTexts: ['推荐'],
soldOut: false,
hasOptions: false
},
@@ -36,7 +36,7 @@ const productMap: Record<Id, MiniProductCard> = {
id: '2000000000000000003',
name: '藤椒鸡丝沙拉',
description: '轻食组合,适合晚餐与健身场景',
coverImageUrl: 'https://dummyimage.com/160x160/dbeafe/111827&text=Salad',
coverImageUrl: 'https://images.unsplash.com/photo-1512621776951-a57141f2eefd?w=400&h=400&fit=crop&q=80',
price: 18,
priceText: formatPrice(18),
salesText: '月售 92',
@@ -48,7 +48,7 @@ const productMap: Record<Id, MiniProductCard> = {
id: '2000000000000000004',
name: '芝士薯球',
description: '外酥里糯的轻食小吃',
coverImageUrl: 'https://dummyimage.com/160x160/fef3c7/111827&text=Snack',
coverImageUrl: 'https://images.unsplash.com/photo-1555939594-58d7cb561ad1?w=400&h=400&fit=crop&q=80',
price: 12,
priceText: formatPrice(12),
salesText: '月售 143',
@@ -60,7 +60,7 @@ const productMap: Record<Id, MiniProductCard> = {
id: '2000000000000000005',
name: '茉莉轻乳茶',
description: '清爽不腻,适合套餐搭配',
coverImageUrl: 'https://dummyimage.com/160x160/ecfccb/111827&text=Tea',
coverImageUrl: 'https://images.unsplash.com/photo-1558857563-b371033873b8?w=400&h=400&fit=crop&q=80',
price: 10,
priceText: formatPrice(10),
salesText: '月售 310',
@@ -72,11 +72,11 @@ const productMap: Record<Id, MiniProductCard> = {
id: '2000000000000000006',
name: '冰美式',
description: '办公场景高频搭配饮品',
coverImageUrl: 'https://dummyimage.com/160x160/e0f2fe/111827&text=Coffee',
coverImageUrl: 'https://images.unsplash.com/photo-1509042239860-f550ce710b93?w=400&h=400&fit=crop&q=80',
price: 14,
priceText: formatPrice(14),
salesText: '月售 201',
tagTexts: ['办公必备'],
tagTexts: ['热卖'],
soldOut: false,
hasOptions: false
}
@@ -150,6 +150,3 @@ export function buildMockPriceEstimate (payload: PriceEstimatePayload): PriceEst
payableAmountText: formatPrice(payableAmount)
}
}