Compare commits

...

3 Commits

34 changed files with 747 additions and 328 deletions

View File

@@ -36,6 +36,7 @@ pnpm lint
## 目录结构 ## 目录结构
- `src/pages`:按页面域拆分,每个页面保持 `index.vue + composables + styles` - `src/pages`:按页面域拆分,每个页面保持 `index.vue + composables + styles`
- `src/components`:复用组件目录,统一使用 `src/components/<kebab-name>/index.vue` - `src/components`:复用组件目录,统一使用 `src/components/<kebab-name>/index.vue`
- `mini/custom-tab-bar`:微信小程序原生 `custom-tab-bar` 源文件,会在构建时复制到 `dist/custom-tab-bar`
- `src/stores``app``cart` - `src/stores``app``cart`
- `src/services`:请求封装与 Mini API 领域接口 - `src/services`:请求封装与 Mini API 领域接口
- `src/shared`类型、常量、mock 数据、格式化函数 - `src/shared`类型、常量、mock 数据、格式化函数

View File

@@ -35,7 +35,12 @@ export default defineConfig<'vite'>(async (merge) => {
__TENANT_CODE__: JSON.stringify(tenantCode) __TENANT_CODE__: JSON.stringify(tenantCode)
}, },
copy: { copy: {
patterns: [], patterns: [
{
from: 'mini/custom-tab-bar',
to: 'dist/custom-tab-bar'
}
],
options: {} options: {}
}, },
framework: 'vue3', framework: 'vue3',

View File

@@ -0,0 +1,49 @@
Component({
data: {
selected: 0,
list: [
{
pagePath: 'pages/home/index',
text: '首页',
iconPath: '../assets/tabbar/home.png',
selectedIconPath: '../assets/tabbar/home-active.png'
},
{
pagePath: 'pages/menu/index',
text: '点餐',
iconPath: '../assets/tabbar/menu.png',
selectedIconPath: '../assets/tabbar/menu-active.png'
},
{
pagePath: 'pages/orders/index',
text: '订单',
iconPath: '../assets/tabbar/orders.png',
selectedIconPath: '../assets/tabbar/orders-active.png'
},
{
pagePath: 'pages/profile/index',
text: '我的',
iconPath: '../assets/tabbar/profile.png',
selectedIconPath: '../assets/tabbar/profile-active.png'
}
]
},
methods: {
setCurrent(index) {
this.setData({ selected: index })
},
switchTab(event) {
const index = Number(event.currentTarget.dataset.index)
const currentItem = this.data.list[this.data.selected]
const targetItem = this.data.list[index]
const currentPath = currentItem ? currentItem.pagePath : ''
if (!targetItem || currentPath === targetItem.pagePath) {
return
}
this.setData({ selected: index })
wx.switchTab({ url: `/${targetItem.pagePath}` })
}
}
})

View File

@@ -0,0 +1,3 @@
{
"component": true
}

View File

@@ -0,0 +1,20 @@
<view class="custom-tab-bar">
<view
wx:for="{{list}}"
wx:key="pagePath"
class="custom-tab-bar__item"
data-index="{{index}}"
bindtap="switchTab"
>
<image
class="custom-tab-bar__icon"
src="{{selected === index ? item.selectedIconPath : item.iconPath}}"
mode="aspectFit"
/>
<text
class="custom-tab-bar__label {{selected === index ? 'custom-tab-bar__label--active' : ''}}"
>
{{item.text}}
</text>
</view>
</view>

View File

@@ -0,0 +1,42 @@
.custom-tab-bar {
display: flex;
align-items: flex-start;
padding-top: 14px;
padding-bottom: calc(6px + constant(safe-area-inset-bottom));
padding-bottom: calc(6px + env(safe-area-inset-bottom));
min-height: 72px;
background: #ffffff;
border-top: 1rpx solid #e5e7eb;
box-shadow: 0 -4px 18px rgba(15, 23, 42, 0.05);
}
.custom-tab-bar__item {
flex: 1;
min-height: 44px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
}
.custom-tab-bar__icon {
width: 26px;
height: 26px;
display: block;
flex-shrink: 0;
margin-bottom: -3px;
}
.custom-tab-bar__label {
display: block;
margin-top: 0;
font-size: 12px;
line-height: 1;
color: #64748b;
font-weight: 500;
}
.custom-tab-bar__label--active {
color: #16a34a;
font-weight: 600;
}

View File

@@ -1,39 +1,39 @@
{ {
"miniprogramRoot": "dist/", "miniprogramRoot": "dist/",
"projectname": "TakeoutSaaS.C-Side-Mini-Program-Taro", "projectname": "TakeoutSaaS.C-Side-Mini-Program-Taro",
"description": "Takeout SaaS C-end WeChat mini program (Taro Vue3)", "description": "Takeout SaaS C-end WeChat mini program (Taro Vue3)",
"appid": "wx30f91e6afe79f405", "appid": "wx30f91e6afe79f405",
"setting": { "setting": {
"urlCheck": true, "urlCheck": true,
"es6": false, "es6": true,
"enhance": false, "enhance": true,
"compileHotReLoad": false, "compileHotReLoad": false,
"postcss": false, "postcss": false,
"minified": false, "minified": false,
"compileWorklet": false, "compileWorklet": false,
"uglifyFileName": false, "uglifyFileName": false,
"uploadWithSourceMap": true, "uploadWithSourceMap": true,
"packNpmManually": false, "packNpmManually": false,
"packNpmRelationList": [], "packNpmRelationList": [],
"minifyWXSS": true, "minifyWXSS": true,
"minifyWXML": true, "minifyWXML": true,
"localPlugins": false, "localPlugins": false,
"disableUseStrict": false, "disableUseStrict": false,
"useCompilerPlugins": false, "useCompilerPlugins": false,
"condition": false, "condition": false,
"swc": false, "swc": false,
"disableSWC": true, "disableSWC": true,
"babelSetting": { "babelSetting": {
"ignore": [], "ignore": [],
"disablePlugins": [], "disablePlugins": [],
"outputPath": "" "outputPath": ""
} }
}, },
"compileType": "miniprogram", "compileType": "miniprogram",
"simulatorPluginLibVersion": {}, "simulatorPluginLibVersion": {},
"packOptions": { "packOptions": {
"ignore": [], "ignore": [],
"include": [] "include": []
}, },
"editorSetting": {} "editorSetting": {}
} }

View File

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

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

View File

@@ -1,3 +1,4 @@
export default definePageConfig({ export default definePageConfig({
navigationBarTitleText: '首页' navigationBarTitleText: '首页',
usingComponents: {}
}) })

View File

@@ -1,112 +1,145 @@
<template> <template>
<view class="home-page"> <view class="hp">
<!-- Store Card --> <!-- Store Card -->
<view class="home-page__store-card"> <view class="hp__store" @click="goStoreSelect">
<view class="home-page__store-icon-wrap"> <view class="hp__store-info">
<text class="home-page__store-icon-emoji">📍</text> <view class="hp__store-row">
</view> <text class="hp__store-name">{{ currentStore.name }}</text>
<view class="home-page__store-text"> <view class="hp__store-status">
<view class="home-page__store-name"> <view class="hp__store-dot" />
<text class="home-page__store-name-text">{{ currentStore.name }}</text>
<view class="home-page__store-status">
<view class="home-page__store-status-dot" />
<text>营业中</text> <text>营业中</text>
</view> </view>
</view> </view>
<text class="home-page__store-addr">{{ currentStore.address }}</text> <text class="hp__store-addr">{{ currentStore.address }}</text>
</view> </view>
<view class="home-page__store-switch" @click="goStoreSelect"> <view class="hp__store-switch">
<text>切换</text> <text>切换门店</text>
<text class="home-page__chevron"></text> <text class="hp__store-arrow"></text>
</view> </view>
</view> </view>
<!-- Dine-in Scan Button --> <!-- Dine-in Scan Button -->
<view v-if="isDineIn" class="home-page__scan-tab" @click="goMenu"> <view class="hp__scan" @click="goMenu">
<text>🔲</text> <view class="hp__scan-ico">
<text>堂食扫码点餐</text> <IconScan size="18" color="#fff" />
</view>
<text class="hp__scan-text">堂食扫码点餐</text>
</view> </view>
<!-- Search Bar --> <!-- Search Bar -->
<view class="home-page__search" @click="goMenu"> <view class="hp__search" @click="goMenu">
<text class="home-page__search-icon">🔍</text> <view class="hp__search-ico">
<text class="home-page__search-placeholder">搜索菜品套餐饮品</text> <IconSearch size="14" color="#9CA3AF" />
</view>
<text class="hp__search-ph">搜索菜品套餐饮品</text>
</view> </view>
<!-- Banner --> <!-- Banner -->
<view class="home-page__banner" @click="goMenu"> <view class="hp__banner" @click="goMenu">
<view class="home-page__banner-content"> <view class="hp__banner-body">
<view class="home-page__banner-tag"> <view class="hp__banner-label">
<text> 新客专享</text> <text>新客专享</text>
</view> </view>
<text class="home-page__banner-title">首单立减</text> <text class="hp__banner-title">首单立减</text>
<view class="home-page__banner-amount"> <view class="hp__banner-price">
<text class="home-page__banner-unit">¥</text> <text class="hp__banner-sym">¥</text>
<text>12</text> <text class="hp__banner-val">12</text>
</view> </view>
<text class="home-page__banner-desc">全场满38再减8 · 限时三天</text> <text class="hp__banner-note">全场满 38 再减 8 · 限时三天</text>
<view class="home-page__banner-cta"> <view class="hp__banner-btn">
<text>立即领取 </text> <text>立即领取</text>
</view> </view>
</view> </view>
<view class="home-page__banner-ring-outer" />
<view class="home-page__banner-ring" />
<image <image
class="home-page__banner-food-img" class="hp__banner-img"
src="https://images.unsplash.com/photo-1504674900247-0877df9cc836?w=240&h=240&fit=crop&auto=format&q=80" src="https://images.unsplash.com/photo-1504674900247-0877df9cc836?w=280&h=280&fit=crop&q=80"
mode="aspectFill" mode="aspectFill"
/> />
<view class="hp__banner-ring" />
</view> </view>
<!-- Categories --> <!-- Categories -->
<view class="home-page__categories"> <view class="hp__cats">
<view <view
v-for="category in categoryCards" v-for="c in categoryCards"
:key="category.key" :key="c.key"
class="home-page__cat-item" class="hp__cat"
@click="goMenu" @click="goMenu"
> >
<view class="home-page__cat-icon" :class="category.toneClass"> <view class="hp__cat-ico" :class="`hp__cat-ico--${c.tone}`">
<text>{{ category.icon }}</text> <text>{{ c.char }}</text>
<text v-if="category.badge" class="home-page__cat-badge">{{ category.badge }}</text> <text v-if="c.badge" class="hp__cat-badge">{{ c.badge }}</text>
</view> </view>
<text class="home-page__cat-label">{{ category.label }}</text> <text class="hp__cat-name">{{ c.label }}</text>
</view> </view>
</view> </view>
<!-- Section Header --> <!-- Promo Strip -->
<view class="home-page__section-head"> <view class="hp__promo" @click="goMenu">
<text class="home-page__section-title">热门推荐</text> <view class="hp__promo-tag">
<view class="home-page__section-more" @click="goMenu"> <text>限时</text>
<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>
</view> </view>
<!-- Product List --> <!-- Product Grid -->
<view class="home-page__product-list"> <view class="hp__grid">
<ProductCard <view
v-for="product in recommendedProducts" v-for="p in recommendedProducts"
:key="product.id" :key="p.id"
:product="product" class="hp__card"
@select="goMenu" @click="goMenu"
@action="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> </view>
<!-- Trust Section --> <!-- Trust Strip -->
<view class="home-page__trust"> <view class="hp__trust">
<view class="home-page__trust-grid"> <view v-for="t in trustItems" :key="t.key" class="hp__trust-pill">
<view v-for="item in trustItems" :key="item.key" class="home-page__trust-item"> <view class="hp__trust-dot" />
<view class="home-page__trust-icon"><text>{{ item.icon }}</text></view> <text>{{ t.label }}</text>
<text class="home-page__trust-label">{{ item.label }}</text>
</view>
</view> </view>
</view> </view>
<!-- Floating Cart FAB --> <!-- Floating Cart FAB -->
<view v-if="cartCount > 0" class="home-page__fab" @click="goMenu"> <view v-if="cartCount > 0" class="hp__fab" @click="goMenu">
<text class="home-page__fab-icon">🛒</text> <view class="hp__fab-ico">
<view class="home-page__fab-badge"> <IconCart size="20" color="#fff" />
</view>
<view class="hp__fab-badge">
<text>{{ cartCount }}</text> <text>{{ cartCount }}</text>
</view> </view>
</view> </view>
@@ -114,16 +147,18 @@
</template> </template>
<script setup lang="ts"> <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 { useTabBarSelection } from '@/utils/useTabBarSelection'
import { useHomePage } from './composables/useHomePage' import { useHomePage } from './composables/useHomePage'
useTabBarSelection(0)
const { const {
cartCount, cartCount,
categoryCards, categoryCards,
currentStore, currentStore,
goMenu, goMenu,
goStoreSelect, goStoreSelect,
isDineIn,
recommendedProducts, recommendedProducts,
trustItems trustItems
} = useHomePage() } = useHomePage()

View File

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

View File

@@ -1,73 +1,57 @@
@use '../../../styles/variables' as *; @use '../../../styles/variables' as *;
.home-page { // ─── Page Shell ───
.hp {
min-height: 100vh; min-height: 100vh;
padding: 14px 16px 24px; padding: 12px 16px 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 14px; gap: 16px;
background: $bg; background: $bg;
} }
.home-page__store-card { // ─── Store Card ───
.hp__store {
background: $card; background: $card;
border-radius: $r-lg; border-radius: $r-lg;
padding: 14px 16px; padding: 16px;
box-shadow: $shadow-sm; box-shadow: $shadow-sm;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 12px;
} }
.home-page__store-icon-wrap { .hp__store-info {
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 {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
} }
.home-page__store-name { .hp__store-row {
font-size: 15px;
font-weight: 600;
color: $text-1;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; gap: 8px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
.home-page__store-name-text { .hp__store-name {
font-size: 16px;
font-weight: 600;
color: $text-1;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
.home-page__store-status { .hp__store-status {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 3px; gap: 4px;
font-size: 11px; font-size: 11px;
color: $primary; color: $primary;
font-weight: 500; font-weight: 500;
flex-shrink: 0; flex-shrink: 0;
} }
.home-page__store-status-dot { .hp__store-dot {
width: 6px; width: 6px;
height: 6px; height: 6px;
border-radius: 50%; border-radius: 50%;
@@ -75,87 +59,170 @@
animation: pulse-dot 2s ease-in-out infinite; animation: pulse-dot 2s ease-in-out infinite;
} }
.home-page__store-addr { .hp__store-addr {
font-size: 12px; font-size: 12px;
color: $text-3; color: $text-3;
margin-top: 3px; margin-top: 4px;
white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap;
display: block;
} }
.home-page__store-switch { .hp__store-switch {
font-size: 13px;
color: $primary;
font-weight: 500;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 2px; gap: 2px;
font-size: 13px;
color: $primary;
font-weight: 500;
padding: 8px 0; padding: 8px 0;
white-space: nowrap; white-space: nowrap;
flex-shrink: 0; flex-shrink: 0;
} }
.home-page__chevron { .hp__store-arrow {
font-size: 16px; 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; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 5px; border-radius: $r-full;
height: 44px;
border-radius: $r-sm;
background: $primary;
color: #fff;
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 500;
box-shadow: 0 2px 10px rgba(22, 163, 74, 0.35); 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; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
background: #F1F5F9; background: #F1F5F9;
border-radius: $r-md; border-radius: $r-sm;
padding: 0 14px; padding: 0 14px;
height: 40px; height: 40px;
} }
.home-page__search-icon { .hp__search-ico {
font-size: 14px; display: flex;
align-items: center;
flex-shrink: 0;
} }
.home-page__search-placeholder { .hp__search-ph {
font-size: 14px; font-size: 14px;
color: $text-4; color: $text-4;
} }
.home-page__section-head { // ─── Section Header ───
.hp__hd {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 2px 0; padding: 4px 0;
} }
.home-page__section-title { .hp__hd-title {
font-size: 18px; font-size: 18px;
font-weight: 700; font-weight: 700;
color: $text-1; color: $text-1;
letter-spacing: 0.3px;
} }
.home-page__section-more { .hp__hd-more {
font-size: 13px;
color: $text-4;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 2px; gap: 2px;
font-size: 13px;
color: $text-4;
padding: 8px 0;
} }
.home-page__product-list { .hp__hd-arrow {
display: flex; font-size: 16px;
flex-direction: column; line-height: 1;
gap: 12px;
} }
// ─── 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;
}

View File

@@ -1,54 +1,54 @@
@use '../../../styles/variables' as *; @use '../../../styles/variables' as *;
.home-page__categories { .hp__cats {
display: grid; display: grid;
grid-template-columns: repeat(6, 1fr); grid-template-columns: repeat(6, 1fr);
gap: 2px; gap: 2px;
padding: 4px 0; padding: 4px 0;
} }
.home-page__cat-item { .hp__cat {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: 6px; gap: 6px;
padding: 6px 2px 8px; padding: 4px 0 6px;
border-radius: $r-sm;
} }
.home-page__cat-icon { .hp__cat-ico {
width: 48px; width: 48px;
height: 48px; height: 48px;
border-radius: 16px; border-radius: 14px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
position: relative; position: relative;
font-size: 18px; font-size: 20px;
font-weight: 700;
&--orange { background: #FFF4ED; } &--warm { background: #FFF4ED; color: #EA580C; }
&--green-soft { background: #F0FDF4; } &--green { background: #F0FDF4; color: #16A34A; }
&--yellow { background: #FFFBEB; } &--amber { background: #FFFBEB; color: #D97706; }
&--green-light { background: #ECFDF5; } &--teal { background: #F0FDFA; color: #0D9488; }
&--mint { background: #F0FDF4; } &--blue { background: #EFF6FF; color: #2563EB; }
&--amber { background: #FFF7ED; } &--rose { background: #FFF1F2; color: #E11D48; }
} }
.home-page__cat-label { .hp__cat-badge {
font-size: 12px;
font-weight: 500;
color: $text-2;
}
.home-page__cat-badge {
position: absolute; position: absolute;
top: -3px; top: -4px;
right: -6px; right: -8px;
font-size: 9px; font-size: 9px;
font-weight: 600;
background: $red; background: $red;
color: #fff; color: #fff;
padding: 1px 5px; padding: 1px 5px;
border-radius: 999px; border-radius: 999px;
font-weight: 600;
line-height: 1.4; 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 *; @use '../../../styles/variables' as *;
.home-page__fab { .hp__fab {
position: fixed; position: fixed;
bottom: 92px; bottom: 92px;
right: 16px; right: 16px;
@@ -8,30 +8,33 @@
width: 52px; width: 52px;
height: 52px; height: 52px;
border-radius: 50%; 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); box-shadow: 0 4px 16px rgba(22, 163, 74, 0.35);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.home-page__fab-icon { .hp__fab-ico {
font-size: 20px; display: flex;
align-items: center;
justify-content: center;
} }
.home-page__fab-badge { .hp__fab-badge {
position: absolute; position: absolute;
top: -2px; top: -2px;
right: -2px; right: -2px;
width: 20px; min-width: 18px;
height: 20px; height: 18px;
border-radius: 50%; border-radius: 9px;
background: $red; background: $red;
color: #fff; color: #fff;
font-size: 11px; font-size: 10px;
font-weight: 700; font-weight: 700;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 0 4px;
border: 2px solid #fff; border: 2px solid #fff;
} }

View File

@@ -1,5 +1,6 @@
@forward './base.scss'; @forward './base.scss';
@forward './banner.scss'; @forward './banner.scss';
@forward './category.scss'; @forward './category.scss';
@forward './product.scss';
@forward './trust.scss'; @forward './trust.scss';
@forward './fab.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 *; @use '../../../styles/variables' as *;
.home-page__trust { .hp__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 {
display: flex; display: flex;
flex-direction: column; flex-wrap: wrap;
gap: 8px;
padding: 4px 0;
}
.hp__trust-pill {
display: inline-flex;
align-items: center; align-items: center;
gap: 6px; gap: 6px;
padding: 6px 4px; padding: 6px 12px;
}
.home-page__trust-icon {
width: 38px;
height: 38px;
border-radius: 11px;
background: $primary-lighter; background: $primary-lighter;
display: flex; border-radius: 999px;
align-items: center; font-size: 11px;
justify-content: center; color: $primary-dark;
font-size: 16px; font-weight: 500;
} }
.home-page__trust-label { .hp__trust-dot {
font-size: 11px; width: 5px;
color: $text-2; height: 5px;
font-weight: 500; border-radius: 50%;
text-align: center; background: $primary;
line-height: 1.4; flex-shrink: 0;
} }

View File

@@ -1,3 +1,4 @@
export default definePageConfig({ export default definePageConfig({
navigationBarTitleText: '点餐' navigationBarTitleText: '点餐',
usingComponents: {}
}) })

View File

@@ -126,8 +126,11 @@
import ProductCard from '@/components/product-card/index.vue' import ProductCard from '@/components/product-card/index.vue'
import CartDrawer from '@/components/cart-drawer/index.vue' import CartDrawer from '@/components/cart-drawer/index.vue'
import SpecPopup from '@/components/spec-popup/index.vue' import SpecPopup from '@/components/spec-popup/index.vue'
import { useTabBarSelection } from '@/utils/useTabBarSelection'
import { useMenuPage } from './composables/useMenuPage' import { useMenuPage } from './composables/useMenuPage'
useTabBarSelection(1)
const { const {
activeDetail, activeDetail,
appStore, appStore,

View File

@@ -1,3 +1,4 @@
export default definePageConfig({ export default definePageConfig({
navigationBarTitleText: '订单' navigationBarTitleText: '订单',
usingComponents: {}
}) })

View File

@@ -61,14 +61,18 @@
<NutButton type="primary" block @click="goMenu">去点餐</NutButton> <NutButton type="primary" block @click="goMenu">去点餐</NutButton>
</view> </view>
</view> </view>
</view> </view>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Button as NutButton, Empty, Price, Tag } from '@nutui/nutui-taro' import { Button as NutButton, Empty, Price, Tag } from '@nutui/nutui-taro'
import PageHero from '@/components/page-hero/index.vue' import PageHero from '@/components/page-hero/index.vue'
import { useTabBarSelection } from '@/utils/useTabBarSelection'
import { useOrdersPage } from './composables/useOrdersPage' import { useOrdersPage } from './composables/useOrdersPage'
useTabBarSelection(2)
const { const {
activeTab, activeTab,
goMenu, goMenu,

View File

@@ -1,3 +1,4 @@
export default definePageConfig({ export default definePageConfig({
navigationBarTitleText: '我的' navigationBarTitleText: '我的',
usingComponents: {}
}) })

View File

@@ -44,14 +44,18 @@
<NutButton block plain @click="goDineIn">编辑桌号</NutButton> <NutButton block plain @click="goDineIn">编辑桌号</NutButton>
</view> </view>
</view> </view>
</view> </view>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Button as NutButton } from '@nutui/nutui-taro' import { Button as NutButton } from '@nutui/nutui-taro'
import PageHero from '@/components/page-hero/index.vue' import PageHero from '@/components/page-hero/index.vue'
import { useTabBarSelection } from '@/utils/useTabBarSelection'
import { useProfilePage } from './composables/useProfilePage' import { useProfilePage } from './composables/useProfilePage'
useTabBarSelection(3)
const { const {
customerStore, customerStore,
draftName, draftName,

View File

@@ -8,13 +8,13 @@ const productMap: Record<Id, MiniProductCard> = {
id: '2000000000000000001', id: '2000000000000000001',
name: '招牌鸡腿饭', name: '招牌鸡腿饭',
description: '招牌酱汁鸡腿搭配时蔬与米饭', 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, price: 22,
priceText: formatPrice(22), priceText: formatPrice(22),
originalPrice: 26, originalPrice: 26,
originalPriceText: formatPrice(26), originalPriceText: formatPrice(26),
salesText: '月售 268', salesText: '月售 268',
tagTexts: ['招牌', '热销'], tagTexts: ['招牌'],
soldOut: false, soldOut: false,
hasOptions: false hasOptions: false
}, },
@@ -22,13 +22,13 @@ const productMap: Record<Id, MiniProductCard> = {
id: '2000000000000000002', id: '2000000000000000002',
name: '黑椒牛柳饭', name: '黑椒牛柳饭',
description: '黑椒风味牛柳,适合工作日快餐', 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, price: 29,
priceText: formatPrice(29), priceText: formatPrice(29),
originalPrice: 33, originalPrice: 33,
originalPriceText: formatPrice(33), originalPriceText: formatPrice(33),
salesText: '月售 186', salesText: '月售 186',
tagTexts: ['牛肉', '推荐'], tagTexts: ['推荐'],
soldOut: false, soldOut: false,
hasOptions: false hasOptions: false
}, },
@@ -36,7 +36,7 @@ const productMap: Record<Id, MiniProductCard> = {
id: '2000000000000000003', id: '2000000000000000003',
name: '藤椒鸡丝沙拉', name: '藤椒鸡丝沙拉',
description: '轻食组合,适合晚餐与健身场景', 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, price: 18,
priceText: formatPrice(18), priceText: formatPrice(18),
salesText: '月售 92', salesText: '月售 92',
@@ -48,7 +48,7 @@ const productMap: Record<Id, MiniProductCard> = {
id: '2000000000000000004', id: '2000000000000000004',
name: '芝士薯球', name: '芝士薯球',
description: '外酥里糯的轻食小吃', 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, price: 12,
priceText: formatPrice(12), priceText: formatPrice(12),
salesText: '月售 143', salesText: '月售 143',
@@ -60,7 +60,7 @@ const productMap: Record<Id, MiniProductCard> = {
id: '2000000000000000005', id: '2000000000000000005',
name: '茉莉轻乳茶', name: '茉莉轻乳茶',
description: '清爽不腻,适合套餐搭配', 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, price: 10,
priceText: formatPrice(10), priceText: formatPrice(10),
salesText: '月售 310', salesText: '月售 310',
@@ -72,11 +72,11 @@ const productMap: Record<Id, MiniProductCard> = {
id: '2000000000000000006', id: '2000000000000000006',
name: '冰美式', name: '冰美式',
description: '办公场景高频搭配饮品', 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, price: 14,
priceText: formatPrice(14), priceText: formatPrice(14),
salesText: '月售 201', salesText: '月售 201',
tagTexts: ['办公必备'], tagTexts: ['热卖'],
soldOut: false, soldOut: false,
hasOptions: false hasOptions: false
} }
@@ -150,6 +150,3 @@ export function buildMockPriceEstimate (payload: PriceEstimatePayload): PriceEst
payableAmountText: formatPrice(payableAmount) payableAmountText: formatPrice(payableAmount)
} }
} }

View File

@@ -0,0 +1,35 @@
import Taro, { useDidShow } from '@tarojs/taro'
import { onMounted } from 'vue'
type CustomTabBarInstance = {
setCurrent: (index: number) => void
}
function syncTabBarSelection(index: number, retries = 6) {
const page = Taro.getCurrentInstance().page
if (!page) {
return
}
const tabBar = Taro.getTabBar<CustomTabBarInstance>(page)
if (tabBar?.setCurrent) {
tabBar.setCurrent(index)
return
}
if (retries > 0) {
setTimeout(() => syncTabBarSelection(index, retries - 1), 16)
}
}
export function useTabBarSelection(index: number) {
onMounted(() => {
syncTabBarSelection(index)
})
useDidShow(() => {
syncTabBarSelection(index)
})
}