Files
yshop-drink/yshop-drink-uniapp-vue3/pages/menu/menu.vue
T
2026-07-31 14:46:17 +08:00

1916 lines
50 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<layout>
<uv-navbar
:fixed="false"
:title="title"
left-arrow
@leftClick="$onClickLeft"
/>
<view class="container" v-if="!loading">
<!-- <view>
<image :src="shopAd" mode="aspectFill" class="w-100 " style="height: 250rpx;"></image>
</view> -->
<view class="notice-bar" v-if="store.notice">
<uv-notice-bar :text="store.notice"></uv-notice-bar>
</view>
<view class="main menu-page">
<view class="nav">
<view class="header">
<image v-if="store.image" :src="store.image" mode="aspectFill" class="store-header-img"></image>
<view class="left" v-if="orderType == 'takein'">
<view class="store-name" @click="selectShop()">
<text>{{ store.name }}</text>
<view class="iconfont iconarrow-right"></view>
</view>
<view class="store-location" v-if="store.dis">
<text>距离您 {{kmUnit(store.dis)}}</text>
</view>
</view>
<view class="left overflow-hidden" v-else>
<view class="store-name" @click="selectShop()">
<view>{{ store.name }}
<text class="small" v-if="store.distance > 0 && orderType == 'takeout'">(配送距离:
{{store.distance}}km)</text>
<text class="small" v-else-if="orderType == 'takeout'">(本店不支持外卖)</text>
</view>
<view class="iconfont iconarrow-right"></view>
</view>
</view>
<view class="right">
<view class="dinein" :class="{active: orderType == 'takein'}" @tap="takein">
<text class="iconfont-yshop icon-takein"></text><text>自取</text>
</view>
<view class="takeout" :class="{active: orderType == 'takeout'}" @tap="takout">
<text class="iconfont-yshop icon-takeout"></text><text>外卖</text>
</view>
</view>
</view>
</view>
<view class="menu-search">
<text class="iconsearch">⌕</text>
<input v-model="keyword" placeholder="搜索菜品、饮品" confirm-type="search" />
<button v-if="keyword" class="clear-search" aria-label="清空搜索" @tap="keyword = ''">×</button>
</view>
<!-- #ifdef H5 -->
<view class="content"
:style="{height: 'calc(100vh - 300rpx + '+(store.notice ? '0rpx':'60rpx')+')'}">
<!-- #endif -->
<!-- #ifndef H5 -->
<view class="content" :style="{height: 'calc(100vh - 350rpx + '+(store.notice ? '0rpx':'60rpx')+')'}">
<!-- #endif -->
<scroll-view class="menus" :scroll-into-view="menuScrollIntoView" scroll-with-animation scroll-y>
<view class="wrapper">
<view class="menu" :id="`menu-${item.id}`" :class="{'current': item.id === currentCateId}"
v-for="(item, index) in filteredGoods" :key="index" @tap="handleMenuTap(item.id)">
<text>{{ item.name }}</text>
<view class="dot" v-show="menuCartNum(item.id)">{{ menuCartNum(item.id) }}</view>
</view>
</view>
</scroll-view>
<!-- goods list begin -->
<scroll-view class="goods" scroll-with-animation scroll-y :scroll-top="cateScrollTop"
@scroll="handleGoodsScroll">
<view class="wrapper">
<view class="list">
<!-- category begin -->
<view class="category" v-for="(item, index) in filteredGoods" :key="index"
:id="`cate-${item.id}`">
<view class="title">
<text>{{ item.name }}</text>
<image mode="aspectFill" :src="item.icon" class="icon"></image>
</view>
<view class="items">
<!-- 商品 begin -->
<view class="good" v-for="(good, key) in item.goodsList" :key="key"
:class="{'background-grey': good.stock <= 0}">
<image mode="aspectFill" :src="good.image" class="image"
@tap="showGoodDetailModal(item, good)"></image>
<view class="right">
<text class="name">{{ good.storeName }}</text>
<text v-if="good.storeInfo" class="tips">{{ good.storeInfo }}</text>
<view class="price_and_action">
<text class="price">¥{{ good.price }}</text>
<view class="btn-group" v-if="good.stock > 0">
<template v-if="hasSpecialSpecs(good)">
<button type="primary" class="btn property_btn"
hover-class="none" size="mini"
@tap="showGoodDetailModal(item, good)">
<text class="iconfont iconadd-select"></text><text> 选规格</text>
</button>
<view class="dot" v-show="goodCartNum(good.id)">
{{ goodCartNum(good.id) }}</view>
</template>
<template v-else>
<button v-if="goodCartNum(good.id)" type="default" plain class="btn quantity-btn" size="mini" hover-class="none" @tap.stop="adjustSimpleGood(item, good, -1)"><text class="iconfont iconsami-select"></text></button>
<text v-if="goodCartNum(good.id)" class="number">{{ goodCartNum(good.id) }}</text>
<button type="primary" class="btn quantity-btn" size="mini" hover-class="none" @tap.stop="adjustSimpleGood(item, good, 1)"><text class="iconfont iconadd-select"></text></button>
</template>
</view>
<view v-if="good.stock == 0">已售罄</view>
</view>
</view>
</view>
<!-- 商品 end -->
</view>
</view>
<!-- category end -->
<uv-empty v-if="keyword.trim() && !filteredGoods.length" mode="search"></uv-empty>
<view class="bottom-spacer"></view>
</view>
</view>
</scroll-view>
<!-- goods list end -->
</view>
<!-- content end -->
<!-- 购物车栏 begin -->
<view
class="cart-box"
:class="{ 'cart-box--with-tip': !!deliveryTipText }"
v-if="showCartBox"
>
<view class="mark">
<image src="/static/images/menu/cart.png" class="cart-img" @tap="openCartPopup"></image>
<view class="tag" v-show="getCartGoodsNumber">{{ getCartGoodsNumber }}</view>
</view>
<view class="price" @tap="openCartPopup">
<view class="price__amount">¥{{ getCartGoodsPrice }}</view>
<view v-if="deliveryTipText" class="price__tip">{{ deliveryTipText }}</view>
</view>
<button
type="primary"
class="pay-btn"
@tap="onCartPayTap"
:disabled="cart.length > 0 && disabledPay"
>
{{ !cart.length ? '去点单' : (disabledPay ? `差${spread}元起送` : '去结算') }}
</button>
</view>
<!-- 购物车栏 end -->
</view>
<!-- 商品详情模态框 begin -->
<modal :show="goodDetailModalVisible" class="good-detail-modal" color="#5A5B5C" width="90%" custom
padding="0rpx" radius="12rpx">
<view class="cover">
<view class="btn-group">
<image src="/static/images/menu/close.png" @tap="closeGoodDetailModal"></image>
</view>
</view>
<scroll-view class="detail" scroll-y>
<view v-if="good.image" class="image">
<image :src="good.image"></image>
</view>
<view class="wrapper">
<view class="basic">
<view class="name">{{ good.storeName }}</view>
<view v-if="good.storeInfo" class="tips">{{ good.storeInfo }}</view>
</view>
<view class="properties">
<view class="property" v-for="(item, index) in good.productAttr" :key="index">
<view class="title">
<text class="name">{{ item.attrName }}</text>
</view>
<view class="values">
<view class="value" v-for="(value, key) in item.attrValueArr" :key="key"
:class="{'default': value == newValue[index]}"
@tap="changePropertyDefault(index, key,false)">
{{ value }}
</view>
</view>
</view>
</view>
</view>
</scroll-view>
<view class="action">
<view class="left">
<view class="price">¥{{ good.price }}</view>
<view class="props">
{{ good.valueStr }}
</view>
</view>
<view class="btn-group">
<text style="margin-right: 20rpx;">库存:{{good.stock}} </text>
<button type="default" plain class="btn" hover-class="none"
@tap="handlePropertyReduce">
<view class="iconfont iconsami-select"></view>
</button>
<view class="number">{{ good.number }}</view>
<button type="primary" class="btn" hover-class="none" @tap="handlePropertyAdd">
<view class="iconfont iconadd-select"></view>
</button>
</view>
</view>
<view class="add-to-cart-btn" @tap="handleAddToCartInModal">
<view>加入购物车</view>
</view>
</modal>
<!-- 商品详情模态框 end -->
<!-- 购物车详情popup -->
<uv-popup ref="popup" mode="bottom" class="cart-popup">
<template #default>
<view class="cart-popup">
<view class="top">
<text @tap="handleCartClear">清空</text>
</view>
<scroll-view class="cart-list" scroll-y>
<view class="wrapper">
<view class="item" v-for="(item, index) in cart" :key="index">
<view class="left">
<view class="name">{{ item.name }}</view>
<view class="props">{{ item.valueStr }}</view>
</view>
<view class="center">
<text>¥{{ item.price }}</text>
</view>
<view class="right">
<button type="default" plain class="btn" hover-class="none"
@tap="handleCartItemReduce(index)">
<view class="iconfont iconsami-select"></view>
</button>
<view class="number">{{ item.number }}</view>
<button type="primary" class="btn" hover-class="none"
@tap="handleCartItemAdd(index)">
<view class="iconfont iconadd-select"></view>
</button>
</view>
</view>
</view>
</scroll-view>
<!-- 结算栏 -->
<view class="cart-footer" v-if="cart.length">
<view class="summary">
<view class="row">
<text class="label">商品合计</text>
<text class="value">¥{{ cartGoodsTotal.toFixed(2) }}</text>
</view>
<view class="row" v-if="orderType == 'takeout'">
<text class="label">配送费</text>
<text class="value" v-if="cartDeliveryFee > 0">¥{{ cartDeliveryFee.toFixed(2) }}</text>
<text class="value free" v-else>免配送费</text>
</view>
<view class="row total">
<text class="label">应付总额</text>
<text class="value">¥{{ cartTotalWithDelivery }}</text>
</view>
<view class="tip" v-if="deliveryTipText">{{ deliveryTipText }}</view>
</view>
<button
type="primary"
class="checkout-btn"
:disabled="disabledPay"
@tap="onCartPayTap"
>
{{ cartPayBtnText }}
</button>
</view>
</view>
</template>
</uv-popup>
<!-- 购物车详情popup -->
<uv-toast ref="uToast"></uv-toast>
</view>
<!--轻提示-->
<view class="loading" v-else>
<uv-loading-icon color="#DA5650" size=40 mode="circle" ></uv-loading-icon>
<button type="primary" class="locate-store-btn" @click="init"
v-if="!store.id">定位最近的门店</button>
<!-- <uv-toast ref="uToast"></uv-toast> -->
</view>
</layout>
</template>
<script setup>
import {
ref,
computed,
nextTick,
watch
} from 'vue'
import { useMainStore } from '@/store/store'
import { storeToRefs } from 'pinia'
import { onLoad,onShow ,onPullDownRefresh,onHide} from '@dcloudio/uni-app'
import { formatDateTime,kmUnit } from '@/utils/util'
import {
shopNearby,
menuGoods,
getDeliveryFee
} from '@/api/goods'
import {
menuAds
} from '@/api/market'
import { addressAll } from '@/api/address'
const main = useMainStore()
const { orderType,address, store,location,isLogin } = storeToRefs(main)
const title = ref('米糖烘焙')
const text = ref('滚动通知')
const keyword = ref('')
const goods = ref([])
const ads = ref([])
const loading = ref(true)
const currentCateId = ref(0)
const cateScrollTop = ref(0)
const menuScrollIntoView = ref('')
const cart = ref([])
const goodDetailModalVisible = ref(false)
const good= ref({})
const category = ref({})
const cartPopupVisible = ref(false)
const sizeCalcState = ref(false)
const newValue = ref([])
const shopAd = ref('')
const isCartShow = ref(true)
const popup = ref()
// 点击左侧分类主动滚动时,暂时忽略右侧 scroll 回写,避免高亮回跳到上一项
const menuClickLock = ref(false)
let menuClickLockTimer = null
// 外卖配送费提示(默认地址 / 唯一地址预估)
const userAddresses = ref([])
const menuDeliveryFee = ref(0)
const deliveryTipReady = ref(false)
let deliveryFeeReqId = 0
const measureMenuLayout = () => {
}
const goodCartNum = computed(() => { //计算单个饮品添加到购物车的数量
return (id) => cart.value.reduce((acc, cur) => {
if (cur.id === id) {
return acc += cur.number
}
return acc
}, 0)
})
const menuCartNum = computed(() =>{
return (id) => cart.value.reduce((acc, cur) => {
if (cur.cate_id === id) {
return acc += cur.number
}
return acc
}, 0)
})
const getCartGoodsNumber = computed(() => { //计算购物车总数
return cart.value.reduce((acc, cur) => acc + cur.number, 0)
})
const getCartGoodsPrice = computed(() =>{ //计算购物车总价
let price = cart.value.reduce((acc, cur) => acc + cur.number * cur.price, 0);
return parseFloat(price).toFixed(2);
})
const disabledPay = computed(() => { //是否达到起送价
return orderType.value == 'takeout' && (getCartGoodsPrice < parseFloat(store.value.minPrice)) ? true :
false
})
const spread = computed(() => { //差多少元起送
if (orderType.value != 'takeout') return
return parseFloat((store.value.minPrice - getCartGoodsPrice).toFixed(2))
})
const freeDeliveryAmount = computed(() => parseFloat(store.value?.freeDeliveryAmount) || 0)
const lowestDeliveryFee = computed(() => {
let min = parseFloat(store.value?.deliveryPrice)
if (isNaN(min)) min = 0
let rules = store.value?.deliveryRules
if (typeof rules === 'string') {
try {
rules = JSON.parse(rules)
} catch (e) {
rules = []
}
}
if (Array.isArray(rules)) {
rules.forEach((rule) => {
const price = parseFloat(rule?.price)
if (!isNaN(price) && (min <= 0 || price < min)) {
min = price
}
})
}
return min
})
// 有默认地址用默认;否则唯一地址;都不满足则不预估
const estimateAddress = computed(() => {
const list = userAddresses.value || []
if (!list.length) return null
const preferred = list.find((item) => Number(item.isDefault) === 1)
if (preferred) return preferred
if (list.length === 1) return list[0]
return null
})
const canShowDeliveryTip = computed(() => {
if (orderType.value !== 'takeout' || !store.value?.id) return false
const list = userAddresses.value || []
if (!list.length) return false
return list.some((item) => Number(item.isDefault) === 1) || list.length === 1
})
const showCartBox = computed(() => {
if (!isCartShow.value) return false
if (cart.value.length > 0) return true
return canShowDeliveryTip.value
})
const formatMoney = (n) => {
const x = Number(n) || 0
return Math.round(x * 100) % 100 === 0 ? String(Math.round(x)) : x.toFixed(2)
}
const deliveryTipText = computed(() => {
const cartAmount = parseFloat(getCartGoodsPrice.value) || 0
const minPrice = parseFloat(store.value?.minPrice) || 0
const underMin = orderType.value === 'takeout' && minPrice > 0 && cartAmount < minPrice
const minOrderTip = underMin ? `满 ${formatMoney(minPrice)} 元起送` : ''
if (canShowDeliveryTip.value && deliveryTipReady.value) {
const fee = menuDeliveryFee.value
const freeAt = freeDeliveryAmount.value
let suffix = ''
if (underMin) {
suffix = minOrderTip
} else if (freeAt > 0 && fee > 0) {
if (cartAmount <= 0) {
suffix = `满 ${formatMoney(freeAt)} 元免配送费`
} else {
const need = Math.max(0, parseFloat((freeAt - cartAmount).toFixed(2)))
suffix = `还需 ${formatMoney(need)} 元免配送费`
}
}
if (fee <= 0) {
if (cartAmount > 0 && !underMin) return '已免配送费'
return suffix || ''
}
return suffix
? `另需配送费 ${formatMoney(fee)} 元(${suffix})`
: `另需配送费 ${formatMoney(fee)} 元`
}
// 未达起送价时,即使暂无配送费预估也展示起送提示
if (minOrderTip && (cart.value.length > 0 || canShowDeliveryTip.value)) {
return minOrderTip
}
return ''
})
// 购物车弹窗结算栏:商品合计、配送费、应付总额
const cartGoodsTotal = computed(() => parseFloat(getCartGoodsPrice.value) || 0)
const cartDeliveryFee = computed(() => {
if (orderType.value !== 'takeout') return 0
if (canShowDeliveryTip.value && deliveryTipReady.value) return menuDeliveryFee.value || 0
return 0
})
const cartTotalWithDelivery = computed(() =>
(cartGoodsTotal.value + cartDeliveryFee.value).toFixed(2)
)
const cartPayBtnText = computed(() => {
if (!cart.value.length) return '去点单'
return disabledPay.value ? `差${spread.value}元起送` : '去结算'
})
const filteredGoods = computed(() => {
const query = keyword.value.trim().toLowerCase()
if (!query) return goods.value
return goods.value.map(item => ({
...item,
goodsList: (item.goodsList || []).filter(good => [good.storeName, good.storeInfo, good.keyword]
.filter(Boolean).join(' ').toLowerCase().includes(query))
})).filter(item => item.goodsList.length)
})
watch(keyword, () => {
currentCateId.value = filteredGoods.value[0]?.id || 0
cateScrollTop.value = 0
sizeCalcState.value = false
})
const hasSpecialSpecs = (good) => (good.productAttr || []).some(attr => (attr.attrValueArr || []).length > 1)
const defaultSpec = (good) => (good.productAttr || []).map(attr => attr.attrValueArr?.[0]).filter(Boolean).join(',')
// 监听自定义事件
uni.$on('refreshMenu', () => {
// 在这里执行onLoad逻辑
console.log('refreshMenu1:',store.value.id)
init()
})
onPullDownRefresh(() => {
init()
})
onLoad(() => {
init();
refreshCart()
})
onHide(() => {
// 重新进入要重新计算页面高度,否则有问题
sizeCalcState.value = false;
})
onShow(() => {
//init();
isCartShow.value = true
refreshCart()
shopAd.value = uni.getStorageSync('shopAd')
loadAddressesForDelivery()
})
const in_array = (search, array) => {
for (var i in array) {
if (array[i] == search) {
return true;
}
}
return false;
}
const selectShop = () => {
uni.navigateTo({
url: '/pages/components/pages/shop/shop'
})
}
const uToast = ref()
const init = async() => { //页面初始化
loading.value = true;
//return
let error = {},
result = location.value
console.log('result:',result)
if (!location.value.hasOwnProperty('latitude')) {
console.log('result1:',location.value)
uni.getLocation(({
type: 'wgs84',
success: function (res) {
console.log('location1:',res)
result = {
latitude: res.latitude,
longitude: res.longitude
};
getShopList(result)
},
fail: function (res) {
uni.showToast({
title: '获取位置失败,请检查是否开启相关权限',
duration: 2000,
icon: 'error'
});
// 默认地为你为北京地址
result = {
latitude: 39.919990,
longitude: 116.456270
};
getShopList(result)
},
complete: function (res) {
}
}));
if(!result.hasOwnProperty('latitude')){
result = {
latitude: 39.919990,
longitude: 116.456270
};
getShopList(result)
}
return
}
getShopList(result)
}
const getShopList = async(res) => {
console.log('location9:',res)
try {
if (res) {
main.SET_LOCATION(res);
let shop_id = 0;
if (store.value.id) {
shop_id = store.value.id;
}
let shop = await shopNearby({
lat: res.latitude,
lng: res.longitude,
shop_id: shop_id,
kw: ''
});
if (shop) {
//广告图
getAds(shop.id);
shop.notice = shop.status == 1 ? shop.notice : '店铺营业时间为:' + formatDateTime(shop.startTime,'hh:mm')+' - '+formatDateTime(shop.endTime,'hh:mm') +
',不在营业时间内无法下单';
// 设置店铺信息
main.SET_STORE(shop);
let mygoods = await menuGoods({
shopId: shop.id
});
if (mygoods) {
goods.value = mygoods;
refreshCart();
}
console.log('goods:',mygoods)
nextTick(() => {
measureMenuLayout()
calcSize()
})
}
}
} finally {
loading.value = false;
uni.stopPullDownRefresh();
}
}
const refreshCart = () =>{
if (goods.value && goods.value.length > 0) {
let newGoods = goods.value;
cart.value = [];
let newCart = uni.getStorageSync('cart') || [];
let tmpCart = [];
if (newCart) {
for (let i in newCart) {
for (let ii in newGoods) {
for (let iii in newGoods[ii].goodsList) {
if (newCart[i].id == newGoods[ii].goodsList[iii].id) {
tmpCart.push(newCart[i]);
}
}
}
}
cart.value = tmpCart;
cartPopupVisible.value = false;
}
}
refreshMenuDeliveryFee()
}
const loadAddressesForDelivery = async () => {
if (!isLogin.value) {
userAddresses.value = []
refreshMenuDeliveryFee()
return
}
try {
const list = await addressAll()
userAddresses.value = list || []
} catch (e) {
userAddresses.value = []
}
refreshMenuDeliveryFee()
}
const applyLowestDeliveryFee = () => {
let fee = lowestDeliveryFee.value
const freeAt = freeDeliveryAmount.value
const cartAmount = parseFloat(getCartGoodsPrice.value) || 0
if (freeAt > 0 && cartAmount >= freeAt) {
fee = 0
}
menuDeliveryFee.value = fee
deliveryTipReady.value = true
}
const refreshMenuDeliveryFee = async () => {
const reqId = ++deliveryFeeReqId
if (!canShowDeliveryTip.value) {
menuDeliveryFee.value = 0
deliveryTipReady.value = false
return
}
const addr = estimateAddress.value
const hasDefault = (userAddresses.value || []).some((item) => Number(item.isDefault) === 1)
// 有默认地址:按默认地址预估;无默认地址:展示最低配送费
if (hasDefault && addr?.longitude && addr?.latitude && store.value?.id) {
try {
const res = await getDeliveryFee({
shopId: store.value.id,
lng: addr.longitude,
lat: addr.latitude,
amount: getCartGoodsPrice.value
})
if (reqId !== deliveryFeeReqId) return
if (res && !res.outOfRange) {
menuDeliveryFee.value = parseFloat(res.deliveryPrice) || 0
deliveryTipReady.value = true
return
}
} catch (e) {
// fall through
}
}
if (reqId !== deliveryFeeReqId) return
applyLowestDeliveryFee()
}
watch(
[orderType, () => store.value?.id, () => store.value?.deliveryPrice, () => store.value?.freeDeliveryAmount, getCartGoodsPrice, canShowDeliveryTip],
() => {
refreshMenuDeliveryFee()
}
)
const getAds = async(shop_id) =>{
let data = await menuAds({
shop_id: shop_id ? shop_id : 0
});
if (data) {
ads.value = data;
}
}
const takout = (force = false) => {
if (orderType.value == 'takeout' && force == false) return
main.SET_ORDER_TYPE('takeout');
if (!isLogin.value) {
uni.navigateTo({
url: '/pages/components/pages/login/login'
})
return
}
loadAddressesForDelivery()
}
const takein = (force = false) => {
if (orderType.value == 'takein' && force == false) return
main.SET_ORDER_TYPE('takein');
if (!isLogin.value) {
uni.navigateTo({
url: '/pages/components/pages/login/login'
})
return
}
}
const handleMenuTap = async (id) => { //点击菜单项事件
menuClickLock.value = true
if (menuClickLockTimer) clearTimeout(menuClickLockTimer)
currentCateId.value = id
menuScrollIntoView.value = `menu-${id}`
// 首次点击时分类高度尚未测完,必须等测量完成再滚动
if (!sizeCalcState.value) {
await calcSize()
}
const targetTop = filteredGoods.value.find(item => item.id == id)?.top || 0
await nextTick()
// 同一 scroll-top 重复赋值不会触发滚动,做微小偏移确保生效
cateScrollTop.value = cateScrollTop.value === targetTop ? targetTop + 0.01 : targetTop
// scroll-with-animation 期间屏蔽 scroll 回写,动画结束后再放开
menuClickLockTimer = setTimeout(() => {
menuClickLock.value = false
menuClickLockTimer = null
}, 400)
}
const handleGoodsScroll = ({ detail }) => { //商品列表滚动事件
if (menuClickLock.value) return
if (!sizeCalcState.value) {
calcSize()
}
const {
scrollTop
} = detail
// 加一点容差,避免刚好停在分类顶部时因浮点误差高亮到上一项
const tabs = filteredGoods.value.filter(item => item.top <= scrollTop + 2).reverse()
if (tabs.length > 0 && tabs[0].id !== currentCateId.value) {
currentCateId.value = tabs[0].id
}
}
const calcSize = () => {
return new Promise((resolve) => {
const list = filteredGoods.value
if (!list.length) {
sizeCalcState.value = true
resolve()
return
}
const query = uni.createSelectorQuery()
list.forEach((item) => {
query.select(`#cate-${item.id}`).boundingClientRect()
})
query.exec((rects) => {
let h = 0
list.forEach((item, index) => {
const rect = rects?.[index]
const height = rect?.height || 0
item.top = h
h += height
item.bottom = h
})
sizeCalcState.value = true
resolve()
})
})
}
const handleAddToCart = (cate, newGood, num) =>{ //添加到购物车
isCartShow.value = true
const valueStr = newGood.valueStr || ''
const index = cart.value.findIndex(item => item.id === newGood.id && item.valueStr === valueStr)
if (index > -1) {
cart.value[index].number += num
} else {
cart.value.push({
id: newGood.id,
cate_id: cate.id,
name: newGood.storeName,
price: newGood.price,
number: num,
image: newGood.image,
valueStr
})
}
uni.setStorageSync('cart', JSON.parse(JSON.stringify(cart.value)))
}
const adjustSimpleGood = (cate, newGood, num) => {
isCartShow.value = true
const valueStr = defaultSpec(newGood)
const index = cart.value.findIndex(item => item.id === newGood.id && item.valueStr === valueStr)
if (num > 0) {
handleAddToCart(cate, { ...newGood, valueStr }, num)
return
}
if (index < 0) return
cart.value[index].number += num
if (cart.value[index].number <= 0) cart.value.splice(index, 1)
uni.setStorageSync('cart', JSON.parse(JSON.stringify(cart.value)))
}
const showGoodDetailModal = (item, newGood) => {
if (!hasSpecialSpecs(newGood)) return adjustSimpleGood(item, newGood, 1)
isCartShow.value = true
good.value = JSON.parse(JSON.stringify({
...newGood,
number: 1
}))
category.value = JSON.parse(JSON.stringify(item))
goodDetailModalVisible.value = true;
console.log('goodDetailModalVisible:',goodDetailModalVisible.value)
changePropertyDefault(0, 0,true);
}
const closeGoodDetailModal = () => { //关闭饮品详情模态框
goodDetailModalVisible.value = false
category.value = {}
good.value = {}
}
const changePropertyDefault = (index, key, isDefault) => { //改变默认属性值
let valueStr = ''
console.log('good:',good.value)
if(isDefault){
newValue.value = []
for(let i = 0;i < good.value.productAttr.length;i++){
newValue.value[i] = good.value.productAttr[i].attrValueArr[0]
}
//valueStr = newValue.value.join(',')
}else{
newValue.value[index] = good.value.productAttr[index].attrValueArr[key]
//valueStr = newValue.value.join(',')
}
valueStr = newValue.value.join(',')
let productValue = good.value.productValue[valueStr]
if(!productValue) {
let skukey = JSON.parse(JSON.stringify(newValue.value))
skukey.sort((a, b) => a.localeCompare(b))
//console.log('skukey:',skukey)
valueStr = skukey.join(',')
productValue = good.value.productValue[valueStr]
}
//let productValue = good.value.productValue[valueStr]
good.value.number = 1;
good.value.price = parseFloat(productValue.price).toFixed(2);
good.value.stock = productValue.stock;
good.value.image = productValue.image ? productValue.image : good.value.image;
good.value.valueStr = valueStr
}
const handlePropertyAdd = () => {
good.value.number += 1
}
const handlePropertyReduce = () => {
if (good.value.number === 1) return
good.value.number -= 1
}
const handleAddToCartInModal = () => {
if (good.value.stock <= 0) {
uToast.value.show({message:'商品库存不足',type: 'error'});
return;
}
handleAddToCart(category.value, good.value, good.value.number)
closeGoodDetailModal()
}
const openCartPopup = () => { //打开/关闭购物车列表popup
isCartShow.value = true
if (!cart.value.length) return
popup.value.open()
}
const handleCartClear = () => { //清空购物车
uni.showModal({
title: '提示',
content: '确定清空购物车么',
success: ({
confirm
}) => {
if (confirm) {
popup.value.close()
cart.value = []
uni.setStorageSync('cart', JSON.parse(JSON.stringify(cart.value)))
}
}
})
}
const handleCartItemAdd = (index) => {
isCartShow.value = true
cart.value[index].number += 1
uni.setStorageSync('cart', JSON.parse(JSON.stringify(cart.value)))
}
const handleCartItemReduce = (index) => {
isCartShow.value = true
if (cart.value[index].number === 1) {
cart.value.splice(index, 1)
} else {
cart.value[index].number -= 1
}
if (!cart.value.length) {
cartPopupVisible.value = false
popup.value?.close?.()
}
uni.setStorageSync('cart', JSON.parse(JSON.stringify(cart.value)))
}
const onCartPayTap = () => {
if (!cart.value.length) return
toPay()
}
const toPay = () => {
if (!isLogin.value) {
uni.navigateTo({
url: '/pages/components/pages/login/login'
})
return
} else {
if (store.value.status == 0) {
uToast.value.show({message:'不在店铺营业时间内',type: 'error'});
return;
}
if(orderType.value == 'takeout' && store.value.distance <= 0){
uToast.value.show({message:'本店不支持外卖',type: 'error'});
return;
}
uni.showLoading({
title: '加载中'
})
uni.setStorageSync('cart', JSON.parse(JSON.stringify(cart.value)))
uni.navigateTo({
url: '/pages/components/pages/pay/pay'
})
}
uni.hideLoading()
}
</script>
<style lang="scss" scoped>
// 点餐页局部 token(与 uni.scss 全局变量配合)
$menu-nav-height: 140rpx;
$menu-sidebar-width: 200rpx;
$menu-radius-sm: 8rpx;
$menu-radius-pill: 38rpx;
$menu-radius-cart: 48rpx;
$menu-good-image-size: 160rpx;
$menu-control-size-sm: 44rpx;
$menu-control-size: 46rpx;
$menu-list-bottom-space: 200rpx;
$menu-cart-height: 96rpx;
$menu-cart-popup-max-height: 60vh;
$menu-cart-list-bottom: 156rpx;
$menu-cart-price-offset: 120rpx;
$menu-sold-out-bg: #e1e4e4;
$menu-secondary-color: #5a5b5c;
$menu-shadow-cart: 0 0 20rpx rgba(0, 0, 0, 0.2);
$menu-dot-size: 34rpx;
$menu-badge-size: 36rpx;
$menu-tag-size: 40rpx;
$menu-font-menu: 26rpx;
$menu-gap-sm: 10rpx;
$menu-gap-base: 20rpx;
$menu-divider-height: 2rpx;
@mixin text-ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
// 商品名:最多三行展示,超出省略
@mixin text-ellipsis-3 {
display: -webkit-box;
overflow: hidden;
text-overflow: ellipsis;
white-space: normal;
word-break: break-all;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
}
// $fix-btn: 是否把按钮锁成固定尺寸的小圆钮。
// 纯数量加减(详情弹窗、购物车弹窗)用 true;
// 商品列表这类混有“选规格”等宽按钮的地方用 false,靠 .quantity-btn 自己锁尺寸。
@mixin quantity-control($size, $fix-btn: true) {
.btn,
.number {
line-height: $size;
text-align: center;
}
@if $fix-btn {
.btn {
box-sizing: border-box;
flex: 0 0 $size;
width: $size;
min-width: $size;
height: $size;
padding: 0;
border-radius: $border-radius-circle;
}
}
.number {
min-width: 38rpx;
}
}
@mixin list-divider {
&::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: $menu-divider-height;
background-color: $border-color;
transform: scaleY(0.6);
}
}
/* #ifdef H5 */
page {
height: auto;
min-height: 100%;
}
/* #endif */
.container {
--menu-control-size-sm: #{$menu-control-size-sm};
--menu-control-size: #{$menu-control-size};
--menu-cart-popup-max-height: #{$menu-cart-popup-max-height};
overflow: hidden;
position: relative;
}
.loading {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
image {
width: 260rpx;
height: 260rpx;
position: relative;
margin-top: -200rpx;
/* #ifdef h5 */
margin-top: 0;
/* #endif */
}
}
.main {
width: 100%;
height: 100%;
position: relative;
}
.nav {
width: 100%;
height: $menu-nav-height;
display: flex;
flex-direction: column;
.header {
width: 100%;
height: $menu-nav-height;
display: flex;
align-items: center;
justify-content: space-between;
padding: $spacing-row-base;
background-color: $text-color-white;
.left {
flex: 1;
display: flex;
flex-direction: column;
.store-name {
display: flex;
align-items: center;
justify-content: flex-start;
margin-bottom: $menu-gap-sm;
font-size: $font-size-lg;
.small {
font-size: $font-size-sm;
color: $text-color-assist;
}
.iconfont {
margin-left: $menu-gap-sm;
line-height: 100%;
}
}
.store-location {
display: flex;
align-items: center;
justify-content: flex-start;
font-size: $font-size-sm;
color: $text-color-assist;
.iconfont {
display: table-cell;
vertical-align: middle;
line-height: 100%;
color: $color-primary;
}
}
}
.right {
display: flex;
align-items: center;
padding: 0 $menu-radius-pill;
font-size: $font-size-sm;
color: $text-color-assist;
background-color: $bg-color-grey;
border-radius: $menu-radius-pill;
.dinein,
.takeout {
position: relative;
display: flex;
align-items: center;
&.active {
padding: 14rpx $menu-radius-pill;
color: $text-color-white;
background-color: $color-primary;
border-radius: $menu-radius-pill;
}
}
.takeout {
flex: 1;
height: 100%;
margin-left: $spacing-row-base;
padding: 14rpx 0;
}
.dinein.active {
margin-left: -$menu-radius-pill;
}
.takeout.active {
margin-right: -$menu-radius-pill;
}
}
}
}
.content {
width: 100%;
height: calc(100vh - 212rpx);
/* #ifdef H5 */
height: calc(100vh - 212rpx - 188rpx);
/* #endif */
display: flex;
.menus {
width: $menu-sidebar-width;
height: 100%;
overflow: hidden;
background-color: $bg-color-grey;
.wrapper {
width: 100%;
height: 100%;
.menu {
position: relative;
display: flex;
align-items: center;
justify-content: flex-start;
padding: $spacing-row-lg $spacing-row-base;
font-size: $menu-font-menu;
color: $text-color-assist;
&.current {
color: $text-color-base;
background-color: $text-color-white;
}
.dot {
position: absolute;
top: 16rpx;
right: $menu-gap-sm;
width: $menu-dot-size;
height: $menu-dot-size;
font-size: 22rpx;
line-height: $menu-dot-size;
text-align: center;
color: $text-color-white;
background-color: $color-primary;
border-radius: $border-radius-circle;
}
&:last-child {
margin-bottom: $menu-list-bottom-space;
}
}
}
}
.goods {
flex: 1;
height: 100%;
overflow: hidden;
background-color: $text-color-white;
.wrapper {
width: 100%;
height: 100%;
padding: $spacing-row-base;
.ads {
height: calc(300 / 550 * 510rpx);
image {
width: 100%;
height: 100%;
border-radius: $menu-radius-sm;
}
}
.list {
width: 100%;
font-size: $font-size-base;
.category {
width: 100%;
.title {
display: flex;
align-items: center;
padding: $spacing-row-lg 0;
color: $text-color-base;
.icon {
width: 38rpx;
height: 38rpx;
margin-left: $menu-gap-sm;
}
}
&:last-child {
margin-bottom: $menu-list-bottom-space;
}
.items {
display: flex;
flex-direction: column;
.good {
display: flex;
align-items: center;
padding: 15rpx 0;
.image {
width: $menu-good-image-size;
height: $menu-good-image-size;
margin-right: $spacing-row-base;
border-radius: $menu-radius-sm;
}
.right {
flex: 1;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: space-between;
height: $menu-good-image-size;
padding-right: 14rpx;
overflow: hidden;
.name {
width: 100%;
margin-bottom: $menu-gap-sm;
font-size: 24rpx;
line-height: 32rpx;
font-weight: 600;
@include text-ellipsis-3;
}
.tips {
width: 100%;
height: 40rpx;
margin-bottom: $menu-gap-sm;
font-size: $font-size-sm;
line-height: 40rpx;
color: $text-color-assist;
@include text-ellipsis;
}
.price_and_action {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
.price {
font-size: $font-size-base;
font-weight: 600;
}
.btn-group {
position: relative;
display: flex;
align-items: center;
justify-content: flex-end;
.btn {
box-sizing: border-box;
height: var(--menu-control-size-sm);
padding: 0 $spacing-row-base;
font-size: $font-size-sm;
line-height: var(--menu-control-size-sm);
&.property_btn {
border-radius: 24rpx;
}
&.quantity-btn {
flex: 0 0 var(--menu-control-size-sm);
width: var(--menu-control-size-sm);
min-width: var(--menu-control-size-sm);
padding: 0;
border-radius: 50%;
}
&.add_btn,
&.reduce_btn {
width: var(--menu-control-size-sm);
padding: 0;
border-radius: var(--menu-control-size-sm);
}
}
.dot {
position: absolute;
top: -10rpx;
right: -12rpx;
width: $menu-badge-size;
height: $menu-badge-size;
font-size: $font-size-sm;
line-height: $menu-badge-size;
text-align: center;
color: $color-primary;
background-color: $text-color-white;
border: 1px solid $color-primary;
border-radius: $border-radius-circle;
}
@include quantity-control(var(--menu-control-size-sm), false);
.number {
min-width: 38rpx;
padding: 0 8rpx;
font-size: $font-size-sm;
}
}
}
}
}
}
}
}
}
}
}
.good-detail-modal {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
.cover {
display: flex;
align-items: center;
justify-content: center;
height: 20rpx;
.btn-group {
position: absolute;
top: 0;
right: $menu-gap-sm;
z-index: 210;
display: flex;
align-items: center;
justify-content: space-around;
image {
width: $img-size-lg;
height: $img-size-lg;
}
}
}
.detail {
position: relative;
width: 100%;
min-height: 1vh;
max-height: calc(90vh - 320rpx - 80rpx - 120rpx);
.image {
display: flex;
align-items: center;
justify-content: center;
image {
width: 260rpx;
height: 260rpx;
}
}
.wrapper {
width: 100%;
height: 100%;
overflow: hidden;
.basic {
display: flex;
flex-direction: column;
padding: 0 $spacing-row-base $spacing-row-lg;
.name {
margin-bottom: $menu-gap-sm;
font-size: $font-size-base;
color: $text-color-base;
}
.tips {
font-size: $font-size-sm;
color: $text-color-grey;
}
}
.properties {
display: flex;
flex-direction: column;
width: 100%;
padding: $menu-gap-sm $spacing-row-lg 0;
border-top: $menu-divider-height solid $bg-color-grey;
.property {
display: flex;
flex-direction: column;
width: 100%;
margin-bottom: $spacing-row-lg;
.title {
display: flex;
align-items: center;
justify-content: flex-start;
width: 100%;
margin-bottom: $spacing-row-base;
.name {
margin-right: $spacing-row-base;
font-size: $menu-font-menu;
color: $text-color-base;
}
.desc {
flex: 1;
font-size: $font-size-sm;
color: $color-primary;
@include text-ellipsis;
}
}
.values {
display: flex;
flex-wrap: wrap;
width: 100%;
.value {
margin-right: $spacing-col-base;
margin-bottom: $spacing-col-base;
padding: $spacing-col-base $spacing-row-lg;
font-size: $menu-font-menu;
color: $text-color-assist;
background-color: $bg-color-grey;
border-radius: $menu-radius-sm;
&.default {
color: $text-color-white;
background-color: $color-primary;
}
}
}
}
}
}
}
.action {
display: flex;
align-items: center;
justify-content: space-between;
height: 120rpx;
padding: 0 26rpx;
background-color: $bg-color-grey;
.left {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
margin-right: $spacing-row-base;
overflow: hidden;
.price {
font-size: $font-size-lg;
color: $text-color-base;
}
.props {
width: 100%;
font-size: $font-size-sm;
color: $text-color-assist;
@include text-ellipsis;
}
}
.btn-group {
display: flex;
align-items: center;
justify-content: space-around;
.btn {
font-size: $font-size-base;
}
@include quantity-control(var(--menu-control-size-sm));
}
}
.add-to-cart-btn {
display: flex;
align-items: center;
justify-content: center;
height: 80rpx;
font-size: $font-size-base;
color: $text-color-white;
background-color: $color-primary;
border-radius: 0 0 $border-radius-lg $border-radius-lg;
}
}
.cart-box {
position: fixed;
bottom: $spacing-row-lg;
/* #ifdef H5 */
bottom: var(--window-bottom);
/* #endif */
left: $spacing-row-lg;
right: $spacing-row-lg;
z-index: 9999;
display: flex;
align-items: center;
justify-content: space-between;
height: $menu-cart-height;
background-color: $text-color-white;
border-radius: $menu-radius-cart;
box-shadow: $menu-shadow-cart;
.cart-img {
position: relative;
width: $menu-cart-height;
height: $menu-cart-height;
margin-top: calc(-1 * #{$menu-cart-height} / 2);
}
.pay-btn {
display: flex;
align-items: center;
height: 100%;
padding: 0 $spacing-row-lg;
font-size: $font-size-base;
color: $text-color-white;
border-radius: 0 50rpx 50rpx 0;
}
.mark {
position: relative;
margin-right: $spacing-row-lg;
padding-left: 46rpx;
.tag {
position: absolute;
top: -50rpx;
right: -10rpx;
display: flex;
align-items: center;
justify-content: center;
width: $menu-tag-size;
height: $menu-tag-size;
padding: 4rpx;
font-size: $font-size-sm;
color: $text-color-white;
background-color: $color-primary;
border-radius: $border-radius-circle;
opacity: 0.9;
}
}
.price {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
justify-content: center;
padding-right: $spacing-row-lg;
padding-left: $spacing-row-base;
color: $text-color-base;
&__amount {
font-size: 34rpx;
font-weight: 700;
line-height: 1.2;
}
&__tip {
margin-top: 4rpx;
font-size: 20rpx;
line-height: 1.3;
color: #a8907a;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
.cart-box--with-tip {
height: auto;
min-height: $menu-cart-height;
padding: 12rpx 0;
}
.cart-popup {
.top {
padding: $menu-gap-sm $spacing-row-lg;
font-size: $font-size-sm;
text-align: right;
color: $menu-secondary-color;
background-color: $bg-color-primary;
}
.cart-list {
width: 100%;
min-height: 1vh;
max-height: var(--menu-cart-popup-max-height);
overflow: hidden;
background-color: $text-color-white;
.wrapper {
display: flex;
flex-direction: column;
height: 100%;
padding: 0 $spacing-row-lg;
margin-bottom: $menu-gap-base;
.item {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
padding: $spacing-row-lg 0;
@include list-divider;
.left {
flex: 1;
display: flex;
flex-direction: column;
margin-right: $spacing-row-lg;
overflow: hidden;
.name {
font-size: $font-size-sm;
color: $text-color-base;
}
.props {
font-size: $font-size-sm;
color: $text-color-assist;
@include text-ellipsis;
}
}
.center {
margin-right: $menu-cart-price-offset;
font-size: $font-size-base;
}
.right {
display: flex;
align-items: center;
justify-content: space-between;
@include quantity-control(var(--menu-control-size));
.btn {
font-size: $font-size-base;
}
}
}
}
}
.cart-footer {
display: flex;
align-items: center;
justify-content: space-between;
padding: $menu-gap-base $spacing-row-lg;
background-color: $text-color-white;
border-top: 1rpx solid $border-color;
.summary {
flex: 1;
min-width: 0;
margin-right: $spacing-row-lg;
.row {
display: flex;
align-items: center;
justify-content: space-between;
font-size: $font-size-sm;
line-height: 1.5;
color: $text-color-base;
.value.free {
color: $color-primary;
}
&.total {
margin-top: 4rpx;
font-weight: 700;
.value {
font-size: 32rpx;
color: $color-primary;
}
}
}
.tip {
margin-top: 6rpx;
font-size: 20rpx;
line-height: 1.3;
color: #a8907a;
}
}
.checkout-btn {
flex: 0 0 auto;
height: 72rpx;
padding: 0 48rpx;
font-size: $font-size-base;
line-height: 72rpx;
border-radius: 36rpx;
}
}
}
// 页面级工具类
.notice-bar {
height: 60rpx;
background-color: $text-color-white;
}
.store-header-img {
width: $img-size-lg;
height: $img-size-lg;
}
.bottom-spacer {
height: 110rpx;
}
.locate-store-btn {
position: absolute;
top: 650rpx;
z-index: 3001;
}
.background-grey {
padding: 15rpx !important;
background-color: $menu-sold-out-bg;
}
.menu-page {
padding: 0 24rpx;
box-sizing: border-box;
background: #fff;
.nav { height: auto; padding-top: 12rpx; }
.nav .header { height: 170rpx; padding: 24rpx; box-sizing: border-box; border-radius: 28rpx; background: linear-gradient(120deg, #fff3e5, #fffaf4); }
.nav .header .left { margin-left: 18rpx; }
.nav .header .left .store-name { font-size: 34rpx; font-weight: 700; }
.nav .header .right { padding: 4rpx; background: #fff; border: 2rpx solid #f5eee8; }
.nav .header .right .dinein, .nav .header .right .takeout { gap: 8rpx; margin: 0; padding: 16rpx 22rpx; }
.nav .header .right .active { margin: 0; background: linear-gradient(90deg, #ff9b20, #ff6f13); }
.menu-search { display: flex; align-items: center; height: 78rpx; margin: 22rpx 0; padding: 0 24rpx; border: 2rpx solid #eee9e5; border-radius: 42rpx; color: #9d9d9d; background: #fff; }
.menu-search input { flex: 1; margin-left: 14rpx; font-size: 28rpx; }
.menu-search .iconsearch { font-size: 40rpx; line-height: 1; }
.menu-search .clear-search { width: 48rpx; height: 48rpx; margin: 0; padding: 0; border-radius: 50%; color: #999; font-size: 34rpx; line-height: 44rpx; background: #f1efed; }
.menu-search .clear-search::after { border: 0; }
.content { height: calc(100vh - 440rpx) !important; }
.menus { width: 156rpx; border-radius: 24rpx 0 0 24rpx; background: #fffaf5; }
.menus .wrapper .menu { justify-content: center; padding: 30rpx 10rpx; color: #7c756e; }
.menus .wrapper .menu.current { color: #ff7119; font-weight: 700; background: #fff; }
.menus .wrapper .menu.current::before { content: ''; position: absolute; left: 0; width: 8rpx; height: 42rpx; border-radius: 0 8rpx 8rpx 0; background: #ff781b; }
.goods { border-radius: 0 24rpx 24rpx 0; }
.goods .wrapper { padding: 0 20rpx; }
.goods .wrapper .list .category .title { padding: 24rpx 0 14rpx; font-size: 34rpx; font-weight: 700; }
.goods .wrapper .list .category .title::before { content: ''; width: 8rpx; height: 30rpx; margin-right: 12rpx; border-radius: 8rpx; background: #ff9b20; }
.goods .wrapper .list .category .items .good { padding: 20rpx 0; border-bottom: 1rpx solid #f4f0ec; }
.goods .wrapper .list .category .items .good .image { width: 178rpx; height: 178rpx; border-radius: 18rpx; }
.goods .wrapper .list .category .items .good .right { height: 178rpx; }
.goods .wrapper .list .category .items .good .right .name { font-size: 31rpx; font-weight: 700; }
.goods .wrapper .list .category .items .good .right .price_and_action .price { font-size: 36rpx; color: #ff6817; }
.goods .wrapper .list .category .items .good .right .price_and_action .btn-group .btn.property_btn { height: 56rpx; padding: 0 18rpx; line-height: 56rpx; border-radius: 28rpx; background: #ff7417; }
.cart-box { bottom: 28rpx; min-height: 116rpx; height: auto; border-radius: 58rpx; box-shadow: 0 8rpx 28rpx rgba(91, 61, 32, .16); }
.cart-box .cart-img { width: 116rpx; height: 116rpx; }
.cart-box .pay-btn { padding: 0 46rpx; border-radius: 58rpx; background: linear-gradient(90deg, #ff9b20, #ff6817); }
.cart-box .price__tip { color: #b08968; }
}
.store-header-img { flex: 0 0 118rpx; width: 118rpx; height: 118rpx; border-radius: 16rpx; }
// Orange controls: keep filled badges, outlined decrements and warm spec selections consistent.
.menu-page .goods .btn-group {
.dot {
color: #fff;
background: #ff7417;
border-color: #ff7417;
box-shadow: 0 2rpx 8rpx rgba(255, 116, 23, .25);
}
.quantity-btn {
color: #ff7417;
border: 2rpx solid #ff7417;
background: #fff;
&[type='primary'] { color: #fff; background: #ff7417; }
}
.number { color: #2b201a; font-weight: 600; }
}
.cart-box .mark .tag {
color: #fff;
background: #ff6817;
border: 3rpx solid #fff;
opacity: 1;
box-shadow: 0 2rpx 10rpx rgba(255, 104, 23, .3);
}
.good-detail-modal {
background: #fff;
.detail { background: #fff; }
.detail .wrapper .properties { border-color: #fff0df; }
.detail .wrapper .properties .property .values .value { color: #8a7465; background: #fff5eb; }
.detail .wrapper .properties .property .values .value.default { color: #fff; background: #ff7417; }
.action { background: #fff7ef; }
.action .left .price { color: #ff6817; }
.action .btn-group .number { color: #2b201a; font-weight: 600; }
.action .btn-group .btn[type='default'] { color: #ff7417; border-color: #ff7417; background: #fff; }
.action .btn-group .btn[type='primary'] { background: #ff7417; }
.add-to-cart-btn { background: linear-gradient(90deg, #ff9b20, #ff6817); }
}
.cart-popup .right .btn[type='default'] { color: #ff7417; border-color: #ff7417; }
.cart-popup .right .btn[type='primary'] { background: #ff7417; }
</style>