Compare commits

...

6 Commits

34 changed files with 749 additions and 330 deletions

View File

@@ -1,6 +1,6 @@
# TakeoutSaaS C-Side Mini Program (Taro)
# TakeoutSaaS.C-Side-Mini-Program-V1
基于官方 `Taro CLI` 初始化的微信小程序前端,当前使用 `Taro 4 + Vue3 + Pinia + NutUI Taro + Vite`
基于官方 `Taro CLI` 初始化的面向消费者的微信小程序前端,当前使用 `Taro 4 + Vue3 + Pinia + NutUI Taro + Vite`
## 当前状态
- 已切换到 `Taro + Vue3 + NutUI` 方案。
@@ -36,6 +36,7 @@ pnpm lint
## 目录结构
- `src/pages`:按页面域拆分,每个页面保持 `index.vue + composables + styles`
- `src/components`:复用组件目录,统一使用 `src/components/<kebab-name>/index.vue`
- `mini/custom-tab-bar`:微信小程序原生 `custom-tab-bar` 源文件,会在构建时复制到 `dist/custom-tab-bar`
- `src/stores``app``cart`
- `src/services`:请求封装与 Mini API 领域接口
- `src/shared`类型、常量、mock 数据、格式化函数

View File

@@ -35,7 +35,12 @@ export default defineConfig<'vite'>(async (merge) => {
__TENANT_CODE__: JSON.stringify(tenantCode)
},
copy: {
patterns: [],
patterns: [
{
from: 'mini/custom-tab-bar',
to: 'dist/custom-tab-bar'
}
],
options: {}
},
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/",
"projectname": "TakeoutSaaS.C-Side-Mini-Program-Taro",
"description": "Takeout SaaS C-end WeChat mini program (Taro Vue3)",
"appid": "wx30f91e6afe79f405",
"setting": {
"urlCheck": true,
"es6": false,
"enhance": false,
"compileHotReLoad": false,
"postcss": false,
"minified": false,
"compileWorklet": false,
"uglifyFileName": false,
"uploadWithSourceMap": true,
"packNpmManually": false,
"packNpmRelationList": [],
"minifyWXSS": true,
"minifyWXML": true,
"localPlugins": false,
"disableUseStrict": false,
"useCompilerPlugins": false,
"condition": false,
"swc": false,
"disableSWC": true,
"babelSetting": {
"ignore": [],
"disablePlugins": [],
"outputPath": ""
}
},
"compileType": "miniprogram",
"simulatorPluginLibVersion": {},
"packOptions": {
"ignore": [],
"include": []
},
"editorSetting": {}
"miniprogramRoot": "dist/",
"projectname": "TakeoutSaaS.C-Side-Mini-Program-Taro",
"description": "Takeout SaaS C-end WeChat mini program (Taro Vue3)",
"appid": "wx30f91e6afe79f405",
"setting": {
"urlCheck": true,
"es6": true,
"enhance": true,
"compileHotReLoad": false,
"postcss": false,
"minified": false,
"compileWorklet": false,
"uglifyFileName": false,
"uploadWithSourceMap": true,
"packNpmManually": false,
"packNpmRelationList": [],
"minifyWXSS": true,
"minifyWXML": true,
"localPlugins": false,
"disableUseStrict": false,
"useCompilerPlugins": false,
"condition": false,
"swc": false,
"disableSWC": true,
"babelSetting": {
"ignore": [],
"disablePlugins": [],
"outputPath": ""
}
},
"compileType": "miniprogram",
"simulatorPluginLibVersion": {},
"packOptions": {
"ignore": [],
"include": []
},
"editorSetting": {}
}

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

@@ -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,3 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '首页'
navigationBarTitleText: '首页',
usingComponents: {}
})

View File

@@ -1,112 +1,145 @@
<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"
/>
<!-- 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 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>
</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>
<!-- 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,16 +147,18 @@
</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 { useTabBarSelection } from '@/utils/useTabBarSelection'
import { useHomePage } from './composables/useHomePage'
useTabBarSelection(0)
const {
cartCount,
categoryCards,
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,170 @@
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;
}

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

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

View File

@@ -126,8 +126,11 @@
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 { useTabBarSelection } from '@/utils/useTabBarSelection'
import { useMenuPage } from './composables/useMenuPage'
useTabBarSelection(1)
const {
activeDetail,
appStore,

View File

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

View File

@@ -61,14 +61,18 @@
<NutButton type="primary" block @click="goMenu">去点餐</NutButton>
</view>
</view>
</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 { useTabBarSelection } from '@/utils/useTabBarSelection'
import { useOrdersPage } from './composables/useOrdersPage'
useTabBarSelection(2)
const {
activeTab,
goMenu,

View File

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

View File

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

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)
}
}

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)
})
}