修改样式
@@ -4,7 +4,7 @@ NODE_ENV=development
|
|||||||
VITE_DELETE_CONSOLE=false
|
VITE_DELETE_CONSOLE=false
|
||||||
|
|
||||||
#本地环境
|
#本地环境
|
||||||
VITE_SERVER_BASEURL=https://howhowfresh.com/prod-api
|
#VITE_SERVER_BASEURL=https://howhowfresh.com/prod-api
|
||||||
#VITE_SERVER_BASEURL=http://192.168.0.158:8889
|
VITE_SERVER_BASEURL=http://192.168.0.197:8889
|
||||||
#VITE_SERVER_BASEURL=http://192.168.0.158:8888
|
#VITE_SERVER_BASEURL=http://192.168.0.158:8888
|
||||||
#VITE_SERVER_BASEURL=http://liuyao.nat100.top/meiguowaimai
|
#VITE_SERVER_BASEURL=http://liuyao.nat100.top/meiguowaimai
|
||||||
@@ -10,15 +10,12 @@ onLaunch(initConfig);
|
|||||||
|
|
||||||
onShow(() => {
|
onShow(() => {
|
||||||
console.log('%c外卖用户端--用户端', 'background: #00A76D; color: white; padding: 3px; border-radius: 2px;');
|
console.log('%c外卖用户端--用户端', 'background: #00A76D; color: white; padding: 3px; border-radius: 2px;');
|
||||||
console.log("App Show");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onHide(() => {
|
onHide(() => {
|
||||||
console.log("App Hide");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onError((error: string) => {
|
onError((error: string) => {
|
||||||
console.log('App Error', error)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -29,12 +26,9 @@ function initApp() {
|
|||||||
const color = plus.android.newObject("android.graphics.Color");
|
const color = plus.android.newObject("android.graphics.Color");
|
||||||
const ac = plus.android.runtimeMainActivity();
|
const ac = plus.android.runtimeMainActivity();
|
||||||
const c2int = plus.android.invoke(color, "parseColor", "#FFFFFF");
|
const c2int = plus.android.invoke(color, "parseColor", "#FFFFFF");
|
||||||
console.log("c2int===" + JSON.stringify(c2int))
|
|
||||||
const win = plus.android.invoke(ac, "getWindow");
|
const win = plus.android.invoke(ac, "getWindow");
|
||||||
console.log("win===" + JSON.stringify(win))
|
|
||||||
plus.android.invoke(win, "setNavigationBarColor", c2int);
|
plus.android.invoke(win, "setNavigationBarColor", c2int);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('error', e)
|
|
||||||
}
|
}
|
||||||
// #endif
|
// #endif
|
||||||
}
|
}
|
||||||
@@ -87,12 +81,10 @@ function initConfig() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(!configStore.isShowedLanguageSelectPage) {
|
if(!configStore.isShowedLanguageSelectPage) {
|
||||||
console.log('未展示过语言选择页面,导航到语言选择页面')
|
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: '/pages-login/pages/choose-language/index',
|
url: '/pages-login/pages/choose-language/index',
|
||||||
success() {
|
success() {
|
||||||
configStore.isShowedLanguageSelectPage = true
|
configStore.isShowedLanguageSelectPage = true
|
||||||
console.log('导航到语言选择页面成功')
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -236,8 +236,6 @@ let commentPlaceholder = ref("说点什么..."); // 输入框占位符
|
|||||||
|
|
||||||
// 发送评论
|
// 发送评论
|
||||||
function sendClick({ item1, index1, item2, index2 } = replyTemp) {
|
function sendClick({ item1, index1, item2, index2 } = replyTemp) {
|
||||||
console.log('replyTemp', replyTemp.item1)
|
|
||||||
console.log('replyTemp', replyTemp.item2)
|
|
||||||
const data = replyTemp.item2 || replyTemp.item1
|
const data = replyTemp.item2 || replyTemp.item1
|
||||||
appCommentPublishCommentPost({
|
appCommentPublishCommentPost({
|
||||||
body: {
|
body: {
|
||||||
@@ -248,7 +246,6 @@ function sendClick({ item1, index1, item2, index2 } = replyTemp) {
|
|||||||
content: commentValue.value,
|
content: commentValue.value,
|
||||||
}
|
}
|
||||||
}).then(res=> {
|
}).then(res=> {
|
||||||
console.log(res)
|
|
||||||
emit("update");
|
emit("update");
|
||||||
cPopupShow.value = false;
|
cPopupShow.value = false;
|
||||||
commentValue.value = ""; // 清空输入框值
|
commentValue.value = ""; // 清空输入框值
|
||||||
@@ -263,7 +260,6 @@ function sendClick({ item1, index1, item2, index2 } = replyTemp) {
|
|||||||
|
|
||||||
function expandReply(item1) {
|
function expandReply(item1) {
|
||||||
// item1.childrenShow = item1.children
|
// item1.childrenShow = item1.children
|
||||||
console.log(item1)
|
|
||||||
appCommentReplyListPost({
|
appCommentReplyListPost({
|
||||||
params: {
|
params: {
|
||||||
pageNum: 1,
|
pageNum: 1,
|
||||||
@@ -273,7 +269,6 @@ function expandReply(item1) {
|
|||||||
parentId: item1.id,
|
parentId: item1.id,
|
||||||
}
|
}
|
||||||
}).then(res=> {
|
}).then(res=> {
|
||||||
console.log('回复列表', res)
|
|
||||||
item1.children = res.rows.map(item => {
|
item1.children = res.rows.map(item => {
|
||||||
let userInfo = {}
|
let userInfo = {}
|
||||||
// 普通用户
|
// 普通用户
|
||||||
@@ -314,7 +309,6 @@ function shrinkReply(item1) {
|
|||||||
const delPopupRef = ref(null);
|
const delPopupRef = ref(null);
|
||||||
let delTemp = reactive({}); // 临时数据
|
let delTemp = reactive({}); // 临时数据
|
||||||
function deleteClick({ item1, index1, item2, index2 }) {
|
function deleteClick({ item1, index1, item2, index2 }) {
|
||||||
console.log('删123除', item1, index1, item2, index2)
|
|
||||||
message
|
message
|
||||||
.confirm({
|
.confirm({
|
||||||
title: t("common.prompt.system-prompt"),
|
title: t("common.prompt.system-prompt"),
|
||||||
@@ -336,7 +330,6 @@ function deleteClick({ item1, index1, item2, index2 }) {
|
|||||||
id: item2.id ? item2.id : item1.id,
|
id: item2.id ? item2.id : item1.id,
|
||||||
}
|
}
|
||||||
}).then(res=> {
|
}).then(res=> {
|
||||||
console.log('删除成功', res)
|
|
||||||
emit("deleteFun");
|
emit("deleteFun");
|
||||||
shrinkReply(item1)
|
shrinkReply(item1)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -231,7 +231,25 @@
|
|||||||
"deliveryFee": "Shipping Fee",
|
"deliveryFee": "Shipping Fee",
|
||||||
"featured-on": "Featured on CHEFLINK",
|
"featured-on": "Featured on CHEFLINK",
|
||||||
"featured-dishes": "Featured Dishes",
|
"featured-dishes": "Featured Dishes",
|
||||||
"nearby-merchants": "Nearby Merchants"
|
"nearby-merchants": "Nearby Merchants",
|
||||||
|
"quickTabs": {
|
||||||
|
"memberZone": "Member Zone",
|
||||||
|
"liveSeafoodAir": "Limited Live Seafood",
|
||||||
|
"mustEatList": "CHEFLINK Must-Eat",
|
||||||
|
"newCalendar": "New Arrival Calendar",
|
||||||
|
"newCalendarNav": "Today's New",
|
||||||
|
"freshSeafoodToday": "Fresh Seafood Today"
|
||||||
|
},
|
||||||
|
"mustEatListTabs": {
|
||||||
|
"merchant": "Merchant Rank",
|
||||||
|
"dish": "Product Rank",
|
||||||
|
"all": "All"
|
||||||
|
},
|
||||||
|
"noticeBar": {
|
||||||
|
"thursday": "Every Thursday: Fresh seafood + New York store delivery",
|
||||||
|
"sunday": "Every Sunday: Boston local store delivery",
|
||||||
|
"freshCatch": "Freshly caught seafood is listed based on daily catch. Limited quantity, while supplies last."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"ai": {
|
"ai": {
|
||||||
"recommend": {
|
"recommend": {
|
||||||
@@ -477,6 +495,7 @@
|
|||||||
"appointmentDelivery": "Appointment delivery",
|
"appointmentDelivery": "Appointment delivery",
|
||||||
"appointmentPickup": "Appointment pickup",
|
"appointmentPickup": "Appointment pickup",
|
||||||
"chooseTime": "Choose the time",
|
"chooseTime": "Choose the time",
|
||||||
|
"chooseTimeForStore": "Please choose a delivery time for \"{name}\"",
|
||||||
"chooseTips": "Choose tips",
|
"chooseTips": "Choose tips",
|
||||||
"contactPhone": "Contact phone",
|
"contactPhone": "Contact phone",
|
||||||
"deliveryPreference": "Delivery preference",
|
"deliveryPreference": "Delivery preference",
|
||||||
@@ -634,6 +653,7 @@
|
|||||||
"tips3": "membership to enjoy a ",
|
"tips3": "membership to enjoy a ",
|
||||||
"tips4": "Enjoy delivery service over",
|
"tips4": "Enjoy delivery service over",
|
||||||
"tips5": "Delivery fee",
|
"tips5": "Delivery fee",
|
||||||
|
"deliveryScheduleDays": "Delivery days: {days}",
|
||||||
"start": " and up",
|
"start": " and up",
|
||||||
"title": "The minimum order amount for this store is",
|
"title": "The minimum order amount for this store is",
|
||||||
"toast": {
|
"toast": {
|
||||||
@@ -706,7 +726,10 @@
|
|||||||
"removeProductTitle": "Remove the product?",
|
"removeProductTitle": "Remove the product?",
|
||||||
"removeProductDesc": "Are you sure you want to remove {name} from your shopping cart?",
|
"removeProductDesc": "Are you sure you want to remove {name} from your shopping cart?",
|
||||||
"emptyTitle": "Your cart is empty",
|
"emptyTitle": "Your cart is empty",
|
||||||
"emptyAction": "Explore Restaurants"
|
"emptyAction": "Explore Restaurants",
|
||||||
|
"orderNoticeTitle": "Order Notice",
|
||||||
|
"multiStoreCheckoutNoticeTitle": "Check delivery dates before checkout",
|
||||||
|
"multiStoreCheckoutNoticeDesc": "Your cart contains multiple stores, and items may have different delivery dates. Please confirm each item's delivery date before placing the order."
|
||||||
},
|
},
|
||||||
"choosePaymethod": {
|
"choosePaymethod": {
|
||||||
"creditCard": "Credit card payment",
|
"creditCard": "Credit card payment",
|
||||||
|
|||||||
@@ -231,7 +231,25 @@
|
|||||||
"deliveryFee": "配送费",
|
"deliveryFee": "配送费",
|
||||||
"featured-on": "精选商家",
|
"featured-on": "精选商家",
|
||||||
"featured-dishes": "精选菜品",
|
"featured-dishes": "精选菜品",
|
||||||
"nearby-merchants": "附近商家"
|
"nearby-merchants": "附近商家",
|
||||||
|
"quickTabs": {
|
||||||
|
"memberZone": "会员专区",
|
||||||
|
"liveSeafoodAir": "限量空运活海鲜",
|
||||||
|
"mustEatList": "CHEFLINK必吃榜",
|
||||||
|
"newCalendar": "上新日历",
|
||||||
|
"newCalendarNav": "今日上新",
|
||||||
|
"freshSeafoodToday": "今日现打海鲜"
|
||||||
|
},
|
||||||
|
"mustEatListTabs": {
|
||||||
|
"merchant": "商家榜单",
|
||||||
|
"dish": "产品榜单",
|
||||||
|
"all": "综合"
|
||||||
|
},
|
||||||
|
"noticeBar": {
|
||||||
|
"thursday": "每周四:好好鲜生生鲜 + 纽约店铺配送",
|
||||||
|
"sunday": "每周日:波士顿本地店铺配送",
|
||||||
|
"freshCatch": "渔船现打海鲜将根据当天捕捞情况临时上新,数量有限,售完即止。"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"ai": {
|
"ai": {
|
||||||
"recommend": {
|
"recommend": {
|
||||||
@@ -477,6 +495,7 @@
|
|||||||
"appointmentDelivery": "预约配送",
|
"appointmentDelivery": "预约配送",
|
||||||
"appointmentPickup": "预约自取",
|
"appointmentPickup": "预约自取",
|
||||||
"chooseTime": "选择时间",
|
"chooseTime": "选择时间",
|
||||||
|
"chooseTimeForStore": "请为「{name}」选择配送时间",
|
||||||
"chooseTips": "选择小费",
|
"chooseTips": "选择小费",
|
||||||
"contactPhone": "联系电话",
|
"contactPhone": "联系电话",
|
||||||
"deliveryPreference": "配送偏好",
|
"deliveryPreference": "配送偏好",
|
||||||
@@ -634,6 +653,7 @@
|
|||||||
"tips3": "会员可享受",
|
"tips3": "会员可享受",
|
||||||
"tips4": "最低起送金额",
|
"tips4": "最低起送金额",
|
||||||
"tips5": "配送费",
|
"tips5": "配送费",
|
||||||
|
"deliveryScheduleDays": "配送日:{days}",
|
||||||
"start": "起",
|
"start": "起",
|
||||||
"title": "这家店的起送金额是",
|
"title": "这家店的起送金额是",
|
||||||
"toast": {
|
"toast": {
|
||||||
@@ -706,7 +726,10 @@
|
|||||||
"removeProductTitle": "移除商品?",
|
"removeProductTitle": "移除商品?",
|
||||||
"removeProductDesc": "确定要将 {name} 从购物车中移除吗?",
|
"removeProductDesc": "确定要将 {name} 从购物车中移除吗?",
|
||||||
"emptyTitle": "购物车为空",
|
"emptyTitle": "购物车为空",
|
||||||
"emptyAction": "去逛逛餐厅"
|
"emptyAction": "去逛逛餐厅",
|
||||||
|
"orderNoticeTitle": "下单须知",
|
||||||
|
"multiStoreCheckoutNoticeTitle": "下单前请确认配送日期",
|
||||||
|
"multiStoreCheckoutNoticeDesc": "当前购物车有多个店铺,商品可能不是同一天配送。请先确认每个商品的配送日期,再提交订单。"
|
||||||
},
|
},
|
||||||
"choosePaymethod": {
|
"choosePaymethod": {
|
||||||
"creditCard": "信用卡支付",
|
"creditCard": "信用卡支付",
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
"name" : "CHEFLINK delivery",
|
"name" : "CHEFLINK delivery",
|
||||||
"appid" : "__UNI__06509BE",
|
"appid" : "__UNI__06509BE",
|
||||||
"description" : "",
|
"description" : "",
|
||||||
"versionName" : "3.1.8",
|
"versionName" : "3.1.9",
|
||||||
"versionCode" : 318,
|
"versionCode" : 319,
|
||||||
"transformPx" : false,
|
"transformPx" : false,
|
||||||
/* 5+App特有相关 */
|
/* 5+App特有相关 */
|
||||||
"app-plus" : {
|
"app-plus" : {
|
||||||
|
|||||||
@@ -0,0 +1,330 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { debounce } from 'throttle-debounce'
|
||||||
|
import { getFeaturedDishList } from '@/pages-store/service'
|
||||||
|
import { appCollectCollectPost } from '@/service'
|
||||||
|
import { CollectionType } from '@/constant/enums'
|
||||||
|
import { useConfigStore } from '@/store'
|
||||||
|
import { formatSalesCount } from '@/utils/utils'
|
||||||
|
import {
|
||||||
|
getFeaturedDishImage,
|
||||||
|
getFeaturedDishPromoLabel,
|
||||||
|
isFeaturedDishSoldOut,
|
||||||
|
parseFeaturedDishListRes,
|
||||||
|
} from '@/utils/featuredDish'
|
||||||
|
import type { FeaturedDishQueryBody, RouteQueryMap } from '../utils/featured-dish-query'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
buildQuery: (routeQuery?: RouteQueryMap) => FeaturedDishQueryBody
|
||||||
|
routeQuery?: RouteQueryMap
|
||||||
|
pageTitle?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const configStore = useConfigStore()
|
||||||
|
|
||||||
|
const paging = ref<ZPagingInstance | null>(null)
|
||||||
|
const dataList = ref<Record<string, unknown>[]>([])
|
||||||
|
|
||||||
|
async function onQuery(pageNum: number, pageSize: number) {
|
||||||
|
try {
|
||||||
|
const filterBody = props.buildQuery(props.routeQuery)
|
||||||
|
const res = await getFeaturedDishList({
|
||||||
|
pageNum,
|
||||||
|
pageSize,
|
||||||
|
...filterBody,
|
||||||
|
})
|
||||||
|
const { rows, total } = parseFeaturedDishListRes(res)
|
||||||
|
await paging.value?.completeByTotal(rows, total)
|
||||||
|
} catch {
|
||||||
|
await paging.value?.complete(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const featuredDishColumns = computed(() => {
|
||||||
|
const list = dataList.value
|
||||||
|
return [
|
||||||
|
list.filter((_, i) => i % 2 === 0),
|
||||||
|
list.filter((_, i) => i % 2 === 1),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
function navigateToDishes(item: Record<string, unknown>) {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages-store/pages/store/dishes?id=${item.id}&storeId=${item.merchantId}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const collectDish = debounce(
|
||||||
|
1300,
|
||||||
|
(item: Record<string, unknown>) => {
|
||||||
|
appCollectCollectPost({
|
||||||
|
body: {
|
||||||
|
targetId: String(item.id),
|
||||||
|
targetType: CollectionType.DISH,
|
||||||
|
},
|
||||||
|
}).then(() => {
|
||||||
|
item.isCollect = !item.isCollect
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{ atBegin: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
refresh: () => paging.value?.refresh(),
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<z-paging
|
||||||
|
ref="paging"
|
||||||
|
v-model="dataList"
|
||||||
|
bg-color="#f2f2f2"
|
||||||
|
:auto="true"
|
||||||
|
@query="onQuery"
|
||||||
|
>
|
||||||
|
<template #top>
|
||||||
|
<navbar :title="pageTitle || ''" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<view class="featured-dishes-section px-24rpx pb-40rpx pt-16rpx">
|
||||||
|
<view class="waterfall-row flex gap-16rpx items-start">
|
||||||
|
<view
|
||||||
|
v-for="(col, colIndex) in featuredDishColumns"
|
||||||
|
:key="colIndex"
|
||||||
|
class="waterfall-col flex-1 min-w-0 flex flex-col gap-16rpx"
|
||||||
|
>
|
||||||
|
<view
|
||||||
|
v-for="item in col"
|
||||||
|
:key="String(item.id ?? item.merchantId) + '-' + String(item.dishName)"
|
||||||
|
class="featured-dish-card w-full"
|
||||||
|
@click="navigateToDishes(item)"
|
||||||
|
>
|
||||||
|
<view class="featured-dish-image">
|
||||||
|
<view v-if="item.isNew == 1" class="dish-new-ribbon">
|
||||||
|
<text class="dish-new-ribbon__text">NEW</text>
|
||||||
|
</view>
|
||||||
|
<image
|
||||||
|
:src="getFeaturedDishImage(item)"
|
||||||
|
mode="aspectFill"
|
||||||
|
class="featured-dish-img"
|
||||||
|
/>
|
||||||
|
<view
|
||||||
|
v-if="isFeaturedDishSoldOut(item.stock)"
|
||||||
|
class="featured-dish-sold-dim"
|
||||||
|
/>
|
||||||
|
<image
|
||||||
|
v-if="isFeaturedDishSoldOut(item.stock)"
|
||||||
|
src="/static/app/images/SoldOut.png"
|
||||||
|
mode="aspectFill"
|
||||||
|
class="featured-dish-sold-overlay"
|
||||||
|
/>
|
||||||
|
<view
|
||||||
|
class="featured-dish-collect w-56rpx h-56rpx absolute z-4 top-12rpx right-12rpx center"
|
||||||
|
@click.stop="collectDish(item)"
|
||||||
|
>
|
||||||
|
<image
|
||||||
|
v-if="!item.isCollect"
|
||||||
|
src="@img-store/1334.png"
|
||||||
|
mode="aspectFill"
|
||||||
|
class="w-44rpx h-44rpx featured-dish-collect-icon"
|
||||||
|
/>
|
||||||
|
<image
|
||||||
|
v-else
|
||||||
|
src="@img-store/1337.png"
|
||||||
|
mode="aspectFill"
|
||||||
|
class="w-44rpx h-44rpx featured-dish-collect-icon"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="featured-dish-body">
|
||||||
|
<view class="featured-dish-meta flex items-start justify-between gap-12rpx mb-14rpx">
|
||||||
|
<view class="min-w-0 flex-1">
|
||||||
|
<text class="featured-dish-price">US${{ item.originalPrice }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="featured-dish-sales shrink-0">
|
||||||
|
{{ t('pages-store.store.sales') }}: {{ formatSalesCount(item.salesCount) }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
<view class="featured-dish-title line-clamp-2 mb-16rpx">
|
||||||
|
{{ item.dishName }}
|
||||||
|
</view>
|
||||||
|
<view class="flex items-center justify-between gap-12rpx">
|
||||||
|
<view
|
||||||
|
v-if="Number(item.memberPrice) > 0"
|
||||||
|
class="featured-dish-member shrink min-w-0"
|
||||||
|
>
|
||||||
|
<text class="featured-dish-member-inner">
|
||||||
|
{{ t('pages-store.store.members') }}: US${{ item.memberPrice }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
<view v-else class="flex-1 min-w-0"></view>
|
||||||
|
<view class="featured-dish-add center shrink-0">
|
||||||
|
<image src="@img/chef/1285.png" class="w-28rpx h-28rpx" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
v-if="getFeaturedDishPromoLabel(item)"
|
||||||
|
class="featured-dish-promo mt-16rpx"
|
||||||
|
>
|
||||||
|
<text class="featured-dish-promo-text">{{ getFeaturedDishPromoLabel(item) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<template #bottom>
|
||||||
|
<view class="h-24rpx"></view>
|
||||||
|
<view :style="[configStore.iosSafeBottomPlaceholder]"></view>
|
||||||
|
</template>
|
||||||
|
</z-paging>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.featured-dish-collect-icon {
|
||||||
|
filter: drop-shadow(0 2rpx 6rpx rgba(0, 0, 0, 0.18));
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-dish-image {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 0;
|
||||||
|
padding-bottom: 100%;
|
||||||
|
background: #f0f0f0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-dish-img {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-dish-sold-dim {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 2;
|
||||||
|
background: rgba(201, 197, 197, 0.6);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-dish-sold-overlay {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 140rpx;
|
||||||
|
height: 140rpx;
|
||||||
|
z-index: 3;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-dish-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-dish-body {
|
||||||
|
padding: 20rpx 20rpx 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-dish-price {
|
||||||
|
color: #e02e24;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-dish-sales {
|
||||||
|
color: #999;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.35;
|
||||||
|
max-width: 48%;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-dish-title {
|
||||||
|
color: #1a1a1a;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-dish-member {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
max-width: calc(100% - 88rpx);
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-dish-member-inner {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 6rpx 18rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: linear-gradient(180deg, #fff5eb 0%, #ffe8d6 100%);
|
||||||
|
color: #c45c1a;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.35;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-dish-add {
|
||||||
|
width: 56rpx;
|
||||||
|
height: 56rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #14181b;
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-dish-promo {
|
||||||
|
padding: 12rpx 16rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
background: #fff0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-dish-promo-text {
|
||||||
|
color: #e02e24;
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dish-new-ribbon {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 184rpx;
|
||||||
|
height: 184rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
z-index: 5;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
position: absolute;
|
||||||
|
top: 26rpx;
|
||||||
|
left: -66rpx;
|
||||||
|
width: 240rpx;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
|
letter-spacing: 2rpx;
|
||||||
|
line-height: 44rpx;
|
||||||
|
background: #e23636;
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,297 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { debounce } from 'throttle-debounce'
|
||||||
|
import usePage from '@/hooks/usePage'
|
||||||
|
import { getFeaturedDishList } from '@/pages-store/service'
|
||||||
|
import { appCollectCollectPost } from '@/service'
|
||||||
|
import { CollectionType } from '@/constant/enums'
|
||||||
|
import { formatSalesCount } from '@/utils/utils'
|
||||||
|
import type { FeaturedDishQueryBody, RouteQueryMap } from '../utils/featured-dish-query'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
buildQuery: (routeQuery?: RouteQueryMap) => FeaturedDishQueryBody
|
||||||
|
routeQuery?: RouteQueryMap
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
function isSoldOutStock(stockLike: unknown) {
|
||||||
|
const n = Number(stockLike)
|
||||||
|
return !Number.isNaN(n) && n <= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDishPromoLabel(item: Record<string, unknown>): string {
|
||||||
|
const raw = item.marketingLabel ?? item.hotSaleTag ?? item.rankTag ?? item.promotionLabel
|
||||||
|
return typeof raw === 'string' && raw.trim() ? raw.trim() : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function dishImageSrc(item: Record<string, unknown>) {
|
||||||
|
const url = item.dishImageUrl ?? item.dishImage
|
||||||
|
if (typeof url === 'string' && url) return url.split(',')[0]
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const { paging, dataList, queryList } = usePage((pageNum, pageSize) => {
|
||||||
|
const body = props.buildQuery(props.routeQuery)
|
||||||
|
return getFeaturedDishList({
|
||||||
|
pageNum,
|
||||||
|
pageSize,
|
||||||
|
...body,
|
||||||
|
}).then((res: { rows?: unknown[]; total?: number }) => ({
|
||||||
|
rows: res?.rows ?? [],
|
||||||
|
total: res?.total ?? 0,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
const featuredDishColumns = computed(() => {
|
||||||
|
const list = dataList.value as Record<string, unknown>[]
|
||||||
|
return [
|
||||||
|
list.filter((_, i) => i % 2 === 0),
|
||||||
|
list.filter((_, i) => i % 2 === 1),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
function navigateToDishes(item: Record<string, unknown>) {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages-store/pages/store/dishes?id=${item.id}&storeId=${item.merchantId}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const collectDish = debounce(
|
||||||
|
1300,
|
||||||
|
(item: Record<string, unknown>) => {
|
||||||
|
appCollectCollectPost({
|
||||||
|
body: {
|
||||||
|
targetId: String(item.id),
|
||||||
|
targetType: CollectionType.DISH,
|
||||||
|
},
|
||||||
|
}).then(() => {
|
||||||
|
item.isCollect = !item.isCollect
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{ atBegin: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
refresh: () => paging.value?.refresh(),
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<z-paging
|
||||||
|
ref="paging"
|
||||||
|
v-model="dataList"
|
||||||
|
:fixed="false"
|
||||||
|
:auto="true"
|
||||||
|
bg-color="#f2f2f2"
|
||||||
|
@query="queryList"
|
||||||
|
>
|
||||||
|
<view class="featured-dishes-section px-24rpx pb-40rpx pt-16rpx">
|
||||||
|
<view class="waterfall-row flex gap-16rpx items-start">
|
||||||
|
<view
|
||||||
|
v-for="(col, colIndex) in featuredDishColumns"
|
||||||
|
:key="colIndex"
|
||||||
|
class="waterfall-col flex-1 min-w-0 flex flex-col gap-16rpx"
|
||||||
|
>
|
||||||
|
<view
|
||||||
|
v-for="item in col"
|
||||||
|
:key="String(item.id ?? item.merchantId) + '-' + String(item.dishName)"
|
||||||
|
class="featured-dish-card w-full"
|
||||||
|
@click="navigateToDishes(item)"
|
||||||
|
>
|
||||||
|
<view class="featured-dish-image">
|
||||||
|
<view v-if="item.isNew == 1" class="dish-new-ribbon">
|
||||||
|
<text class="dish-new-ribbon__text">NEW</text>
|
||||||
|
</view>
|
||||||
|
<image
|
||||||
|
:src="dishImageSrc(item)"
|
||||||
|
mode="aspectFill"
|
||||||
|
class="featured-dish-img"
|
||||||
|
/>
|
||||||
|
<view
|
||||||
|
v-if="isSoldOutStock(item.stock)"
|
||||||
|
class="featured-dish-sold-dim"
|
||||||
|
/>
|
||||||
|
<image
|
||||||
|
v-if="isSoldOutStock(item.stock)"
|
||||||
|
src="/static/app/images/SoldOut.png"
|
||||||
|
mode="aspectFill"
|
||||||
|
class="featured-dish-sold-overlay"
|
||||||
|
/>
|
||||||
|
<view
|
||||||
|
class="featured-dish-collect w-56rpx h-56rpx absolute z-4 top-12rpx right-12rpx center"
|
||||||
|
@click.stop="collectDish(item)"
|
||||||
|
>
|
||||||
|
<image
|
||||||
|
v-if="!item.isCollect"
|
||||||
|
src="@img-store/1334.png"
|
||||||
|
mode="aspectFill"
|
||||||
|
class="w-44rpx h-44rpx featured-dish-collect-icon"
|
||||||
|
/>
|
||||||
|
<image
|
||||||
|
v-else
|
||||||
|
src="@img-store/1337.png"
|
||||||
|
mode="aspectFill"
|
||||||
|
class="w-44rpx h-44rpx featured-dish-collect-icon"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="featured-dish-body">
|
||||||
|
<view class="featured-dish-meta flex items-start justify-between gap-12rpx mb-14rpx">
|
||||||
|
<view class="min-w-0 flex-1">
|
||||||
|
<text class="featured-dish-price">US${{ item.originalPrice }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="featured-dish-sales shrink-0">
|
||||||
|
{{ t('pages-store.store.sales') }}: {{ formatSalesCount(item.salesCount) }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
<view class="featured-dish-title line-clamp-2 mb-16rpx">
|
||||||
|
{{ item.dishName }}
|
||||||
|
</view>
|
||||||
|
<view class="flex items-center justify-between gap-12rpx">
|
||||||
|
<view
|
||||||
|
v-if="Number(item.memberPrice) > 0"
|
||||||
|
class="featured-dish-member shrink min-w-0"
|
||||||
|
>
|
||||||
|
<text class="featured-dish-member-inner">
|
||||||
|
{{ t('pages-store.store.members') }}: US${{ item.memberPrice }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
<view v-else class="flex-1 min-w-0"></view>
|
||||||
|
<view class="featured-dish-add center shrink-0">
|
||||||
|
<image src="@img/chef/1285.png" class="w-28rpx h-28rpx" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
v-if="getDishPromoLabel(item)"
|
||||||
|
class="featured-dish-promo mt-16rpx"
|
||||||
|
>
|
||||||
|
<text class="featured-dish-promo-text">{{ getDishPromoLabel(item) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</z-paging>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.featured-dish-collect-icon {
|
||||||
|
filter: drop-shadow(0 2rpx 6rpx rgba(0, 0, 0, 0.18));
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-dish-image {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 0;
|
||||||
|
padding-bottom: 100%;
|
||||||
|
background: #f0f0f0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-dish-img {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-dish-sold-dim {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 2;
|
||||||
|
background: rgba(201, 197, 197, 0.6);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-dish-sold-overlay {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 140rpx;
|
||||||
|
height: 140rpx;
|
||||||
|
z-index: 3;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-dish-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-dish-body {
|
||||||
|
padding: 20rpx 20rpx 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-dish-price {
|
||||||
|
color: #e02e24;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-dish-sales {
|
||||||
|
color: #999;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.35;
|
||||||
|
max-width: 48%;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-dish-title {
|
||||||
|
color: #1a1a1a;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-dish-member-inner {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #ce7138;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-dish-member {
|
||||||
|
padding: 4rpx 12rpx;
|
||||||
|
background: rgba(206, 113, 56, 0.1);
|
||||||
|
border-radius: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-dish-add {
|
||||||
|
width: 52rpx;
|
||||||
|
height: 52rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #14181b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-dish-promo-text {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dish-new-ribbon {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 4;
|
||||||
|
padding: 6rpx 16rpx;
|
||||||
|
background: linear-gradient(135deg, #ff6b35, #e02e24);
|
||||||
|
border-radius: 0 0 16rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dish-new-ribbon__text {
|
||||||
|
font-size: 20rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import FeaturedDishTopicPage from './featured-dish-topic-page.vue'
|
||||||
|
import { buildFreshSeafoodTodayQuery, type RouteQueryMap } from '../utils/featured-dish-query'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
routeQuery?: RouteQueryMap
|
||||||
|
pageTitle?: string
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<featured-dish-topic-page
|
||||||
|
:build-query="buildFreshSeafoodTodayQuery"
|
||||||
|
:page-title="pageTitle"
|
||||||
|
:route-query="routeQuery"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import FeaturedDishTopicPage from './featured-dish-topic-page.vue'
|
||||||
|
import { buildLiveSeafoodAirQuery, type RouteQueryMap } from '../utils/featured-dish-query'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
routeQuery?: RouteQueryMap
|
||||||
|
pageTitle?: string
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<featured-dish-topic-page
|
||||||
|
:build-query="buildLiveSeafoodAirQuery"
|
||||||
|
:page-title="pageTitle"
|
||||||
|
:route-query="routeQuery"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import FeaturedDishTopicPage from './featured-dish-topic-page.vue'
|
||||||
|
import { buildMemberZoneQuery, type RouteQueryMap } from '../utils/featured-dish-query'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
routeQuery?: RouteQueryMap
|
||||||
|
pageTitle?: string
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<featured-dish-topic-page
|
||||||
|
:build-query="buildMemberZoneQuery"
|
||||||
|
:page-title="pageTitle"
|
||||||
|
:route-query="routeQuery"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,505 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { appMerchantFeaturedListPost, appRecipeCategoryListGet } from '@/service'
|
||||||
|
import { getFeaturedDishList } from '@/pages-store/service'
|
||||||
|
import { useUserStore, useConfigStore } from '@/store'
|
||||||
|
import SearchDishResultItem from '@/pages/search/components/search-dish-result-item.vue'
|
||||||
|
import { buildMustEatListQuery, type RouteQueryMap } from '../utils/featured-dish-query'
|
||||||
|
import { parseFeaturedDishListRes } from '@/utils/featuredDish'
|
||||||
|
|
||||||
|
type MustEatTab = 'merchant' | 'dish'
|
||||||
|
type RecipeCategoryItem = { id?: string | number; categoryName?: string }
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
routeQuery?: RouteQueryMap
|
||||||
|
pageTitle?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const configStore = useConfigStore()
|
||||||
|
|
||||||
|
const activeTab = ref<MustEatTab>('merchant')
|
||||||
|
const tabIndex = ref(0)
|
||||||
|
|
||||||
|
const tabs = computed(() => [
|
||||||
|
{ label: t('pages.home.mustEatListTabs.merchant'), value: 'merchant' as MustEatTab },
|
||||||
|
{ label: t('pages.home.mustEatListTabs.dish'), value: 'dish' as MustEatTab },
|
||||||
|
])
|
||||||
|
|
||||||
|
const paging = ref<ZPagingInstance | null>(null)
|
||||||
|
const dataList = ref<Record<string, unknown>[]>([])
|
||||||
|
const merchantSourceList = ref<Record<string, unknown>[]>([])
|
||||||
|
const recipeCategoryList = ref<RecipeCategoryItem[]>([])
|
||||||
|
const activeCategoryId = ref<string | number>('')
|
||||||
|
|
||||||
|
const categoryTabs = computed(() => [
|
||||||
|
{ id: '' as const, name: t('pages.home.mustEatListTabs.all') },
|
||||||
|
...recipeCategoryList.value.map((c) => ({
|
||||||
|
id: c.id ?? '',
|
||||||
|
name: String(c.categoryName ?? ''),
|
||||||
|
})),
|
||||||
|
])
|
||||||
|
|
||||||
|
const activeCategoryIndex = computed(() => {
|
||||||
|
const idx = categoryTabs.value.findIndex(
|
||||||
|
(c) => String(c.id) === String(activeCategoryId.value),
|
||||||
|
)
|
||||||
|
return idx >= 0 ? idx : 0
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
appRecipeCategoryListGet({}).then((res) => {
|
||||||
|
recipeCategoryList.value = (res.data as RecipeCategoryItem[]) || []
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function onTabChange(index: number) {
|
||||||
|
tabIndex.value = index
|
||||||
|
activeTab.value = tabs.value[index]?.value ?? 'merchant'
|
||||||
|
dataList.value = []
|
||||||
|
paging.value?.reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCategoryChange(id: string | number) {
|
||||||
|
if (String(activeCategoryId.value) === String(id)) return
|
||||||
|
activeCategoryId.value = id
|
||||||
|
dataList.value = []
|
||||||
|
paging.value?.reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
function categoryNameById(id: string | number): string {
|
||||||
|
if (id === '' || id == null) return ''
|
||||||
|
const hit = recipeCategoryList.value.find((c) => String(c.id) === String(id))
|
||||||
|
return String(hit?.categoryName ?? '')
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterMerchantsByCategory(list: Record<string, unknown>[]) {
|
||||||
|
const name = categoryNameById(activeCategoryId.value)
|
||||||
|
if (!name) return list
|
||||||
|
return list.filter((item) => {
|
||||||
|
const zh = item.merchantCategoryNamesZh as string[] | undefined
|
||||||
|
const en = item.merchantCategoryNamesEn as string[] | undefined
|
||||||
|
const names = [...(zh || []), ...(en || [])]
|
||||||
|
return names.some((n) => n.includes(name) || name.includes(n))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onQuery(pageNum: number, pageSize: number) {
|
||||||
|
try {
|
||||||
|
if (activeTab.value === 'merchant') {
|
||||||
|
if (pageNum > 1) {
|
||||||
|
await paging.value?.completeByNoMore([], true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const res = await appMerchantFeaturedListPost({
|
||||||
|
body: {
|
||||||
|
lat: userStore.userLocation.latitude,
|
||||||
|
lng: userStore.userLocation.longitude,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const list = Array.isArray((res as { data?: unknown[] })?.data)
|
||||||
|
? ((res as { data: unknown[] }).data as Record<string, unknown>[])
|
||||||
|
: []
|
||||||
|
merchantSourceList.value = list
|
||||||
|
const filtered = filterMerchantsByCategory(list)
|
||||||
|
await paging.value?.completeByTotal(filtered, filtered.length)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterBody = buildMustEatListQuery(props.routeQuery)
|
||||||
|
if (activeCategoryId.value !== '' && activeCategoryId.value != null) {
|
||||||
|
filterBody.recipeCategoryId = activeCategoryId.value
|
||||||
|
}
|
||||||
|
const res = await getFeaturedDishList({
|
||||||
|
pageNum,
|
||||||
|
pageSize,
|
||||||
|
...filterBody,
|
||||||
|
})
|
||||||
|
const { rows, total } = parseFeaturedDishListRes(res)
|
||||||
|
await paging.value?.completeByTotal(rows, total)
|
||||||
|
} catch {
|
||||||
|
await paging.value?.complete(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function merchantImagePair(item: Record<string, unknown>): [string, string] {
|
||||||
|
const shop = String(item.shopImages ?? '')
|
||||||
|
.split(',')
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
const logo = String(item.logo ?? '').trim()
|
||||||
|
let a = shop[0] || logo || ''
|
||||||
|
let b = shop[1] || shop[0] || logo || ''
|
||||||
|
if (!a && !b) return ['', '']
|
||||||
|
if (!b) b = a
|
||||||
|
if (!a) a = b
|
||||||
|
return [a, b]
|
||||||
|
}
|
||||||
|
|
||||||
|
function merchantCategoryLine(item: Record<string, unknown>): string {
|
||||||
|
const zh = item.merchantCategoryNamesZh as string[] | undefined
|
||||||
|
const en = item.merchantCategoryNamesEn as string[] | undefined
|
||||||
|
if (uni.getLocale() === 'zh-Hans' && zh?.length) return zh[0] || ''
|
||||||
|
if (en?.length) return en[0] || ''
|
||||||
|
if (zh?.length) return zh[0] || ''
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function ratingStars(rating?: unknown) {
|
||||||
|
return Math.min(5, Math.max(0, Math.round(Number(rating) || 0)))
|
||||||
|
}
|
||||||
|
|
||||||
|
function ratingText(rating?: unknown) {
|
||||||
|
const n = Number(rating)
|
||||||
|
return Number.isFinite(n) ? n.toFixed(1) : '0.0'
|
||||||
|
}
|
||||||
|
|
||||||
|
function openMerchant(item: Record<string, unknown>) {
|
||||||
|
if (item?.id == null) return
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages-store/pages/store/index?id=${item.id}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<z-paging
|
||||||
|
ref="paging"
|
||||||
|
v-model="dataList"
|
||||||
|
bg-color="#f2f2f2"
|
||||||
|
:auto="true"
|
||||||
|
@query="onQuery"
|
||||||
|
>
|
||||||
|
<template #top>
|
||||||
|
<navbar :title="pageTitle || ''" />
|
||||||
|
<view class="must-eat-tabbar">
|
||||||
|
<view class="must-eat-segment">
|
||||||
|
<view
|
||||||
|
class="must-eat-segment__thumb"
|
||||||
|
:style="{ transform: `translateX(${tabIndex * 100}%)` }"
|
||||||
|
/>
|
||||||
|
<view
|
||||||
|
v-for="(tab, index) in tabs"
|
||||||
|
:key="tab.value"
|
||||||
|
class="must-eat-segment__item"
|
||||||
|
:class="{ 'must-eat-segment__item--on': tabIndex === index }"
|
||||||
|
@click="onTabChange(index)"
|
||||||
|
>
|
||||||
|
<text class="must-eat-segment__text">{{ tab.label }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<scroll-view
|
||||||
|
class="must-eat-categories"
|
||||||
|
scroll-x
|
||||||
|
:show-scrollbar="false"
|
||||||
|
enable-flex
|
||||||
|
>
|
||||||
|
<view class="must-eat-categories__track">
|
||||||
|
<view
|
||||||
|
v-for="(cat, index) in categoryTabs"
|
||||||
|
:key="String(cat.id === '' ? 'all' : cat.id)"
|
||||||
|
class="must-eat-categories__item"
|
||||||
|
:class="{
|
||||||
|
'must-eat-categories__item--on': activeCategoryIndex === index,
|
||||||
|
}"
|
||||||
|
@click="onCategoryChange(cat.id)"
|
||||||
|
>
|
||||||
|
<text class="must-eat-categories__text">{{ cat.name }}</text>
|
||||||
|
<view
|
||||||
|
v-if="activeCategoryIndex === index"
|
||||||
|
class="must-eat-categories__line"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<view class="must-eat-categories__tail" />
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 商家:精选商家,一行一条(同 home-store 卡片) -->
|
||||||
|
<view v-if="activeTab === 'merchant'" class="must-eat-list px-24rpx pb-40rpx pt-8rpx">
|
||||||
|
<view
|
||||||
|
v-for="(item, index) in dataList"
|
||||||
|
:key="String(item.id ?? index)"
|
||||||
|
class="merchant-card"
|
||||||
|
@click="openMerchant(item)"
|
||||||
|
>
|
||||||
|
<view class="merchant-card__imgs">
|
||||||
|
<view
|
||||||
|
v-for="(src, ii) in merchantImagePair(item)"
|
||||||
|
:key="ii"
|
||||||
|
class="merchant-card__img-slot"
|
||||||
|
:class="
|
||||||
|
ii === 0
|
||||||
|
? 'merchant-card__img-slot--first'
|
||||||
|
: 'merchant-card__img-slot--last'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<image
|
||||||
|
v-if="src"
|
||||||
|
class="merchant-card__img"
|
||||||
|
:src="src"
|
||||||
|
mode="aspectFill"
|
||||||
|
/>
|
||||||
|
<view v-else class="merchant-card__img merchant-card__img--empty" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="merchant-card__body">
|
||||||
|
<text class="merchant-card__name">{{ item.merchantName }}</text>
|
||||||
|
<text
|
||||||
|
v-if="merchantCategoryLine(item)"
|
||||||
|
class="merchant-card__cate"
|
||||||
|
>{{ merchantCategoryLine(item) }}</text>
|
||||||
|
<view class="merchant-card__rating">
|
||||||
|
<text
|
||||||
|
v-for="si in [1, 2, 3, 4, 5]"
|
||||||
|
:key="si"
|
||||||
|
class="merchant-card__star"
|
||||||
|
:class="{
|
||||||
|
'merchant-card__star--on': si <= ratingStars(item.rating),
|
||||||
|
}"
|
||||||
|
>★</text>
|
||||||
|
<text class="merchant-card__score">{{ ratingText(item.rating) }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="merchant-card__tag">{{ t('pages.home.brandTag') }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="merchant-card__go" @click.stop="openMerchant(item)">
|
||||||
|
<view class="i-carbon:chevron-right merchant-card__go-icon"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 商品:销量排序,一行一条 -->
|
||||||
|
<view v-else class="must-eat-dish-list pb-40rpx pt-8rpx">
|
||||||
|
<search-dish-result-item
|
||||||
|
v-for="item in dataList"
|
||||||
|
:key="String(item.id ?? item.merchantId)"
|
||||||
|
:item="item as any"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<template #bottom>
|
||||||
|
<view class="h-24rpx"></view>
|
||||||
|
<view :style="[configStore.iosSafeBottomPlaceholder]"></view>
|
||||||
|
</template>
|
||||||
|
</z-paging>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.must-eat-tabbar {
|
||||||
|
background: #fff;
|
||||||
|
padding: 16rpx 24rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.must-eat-segment {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
height: 80rpx;
|
||||||
|
padding: 8rpx;
|
||||||
|
background: #ececec;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.must-eat-segment__thumb {
|
||||||
|
position: absolute;
|
||||||
|
left: 8rpx;
|
||||||
|
top: 8rpx;
|
||||||
|
width: calc((100% - 16rpx) / 2);
|
||||||
|
height: calc(100% - 16rpx);
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.08);
|
||||||
|
transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.must-eat-segment__item {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.must-eat-segment__text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.2;
|
||||||
|
transition: color 0.2s, font-weight 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.must-eat-segment__item--on .must-eat-segment__text {
|
||||||
|
color: #14181b;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.must-eat-categories {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.must-eat-categories__track {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
min-width: 100%;
|
||||||
|
padding: 8rpx 0 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.must-eat-categories__item {
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16rpx 28rpx 14rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.must-eat-categories__item--on {
|
||||||
|
padding-bottom: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.must-eat-categories__text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #999;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.3;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.must-eat-categories__item--on .must-eat-categories__text {
|
||||||
|
color: #14181b;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.must-eat-categories__line {
|
||||||
|
margin-top: 10rpx;
|
||||||
|
width: 40rpx;
|
||||||
|
height: 6rpx;
|
||||||
|
background: #14181b;
|
||||||
|
border-radius: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.must-eat-categories__tail {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 24rpx;
|
||||||
|
height: 1rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.merchant-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
padding: 24rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.merchant-card__imgs {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12rpx;
|
||||||
|
width: 152rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.merchant-card__img-slot {
|
||||||
|
width: 152rpx;
|
||||||
|
height: 96rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #e8eaed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.merchant-card__img-slot--first {
|
||||||
|
border-radius: 12rpx 12rpx 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.merchant-card__img-slot--last {
|
||||||
|
border-radius: 0 0 12rpx 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.merchant-card__img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.merchant-card__body {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 0 20rpx 0 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.merchant-card__name {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #14181b;
|
||||||
|
line-height: 1.35;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.merchant-card__cate {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #333;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.merchant-card__rating {
|
||||||
|
margin-top: 12rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.merchant-card__star {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.merchant-card__star--on {
|
||||||
|
color: #ffb400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.merchant-card__score {
|
||||||
|
margin-left: 8rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.merchant-card__tag {
|
||||||
|
margin-top: 10rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #ce7138;
|
||||||
|
}
|
||||||
|
|
||||||
|
.merchant-card__go {
|
||||||
|
width: 48rpx;
|
||||||
|
height: 48rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.merchant-card__go-icon {
|
||||||
|
font-size: 36rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.must-eat-dish-list {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import FeaturedDishTopicPage from './featured-dish-topic-page.vue'
|
||||||
|
import { buildNewCalendarQuery, type RouteQueryMap } from '../utils/featured-dish-query'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
routeQuery?: RouteQueryMap
|
||||||
|
pageTitle?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
/** 导航栏固定「今日上新」,不使用路由 categoryName */
|
||||||
|
const navTitle = computed(() => t('pages.home.quickTabs.newCalendarNav'))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<featured-dish-topic-page
|
||||||
|
:build-query="buildNewCalendarQuery"
|
||||||
|
:page-title="navTitle"
|
||||||
|
:route-query="routeQuery"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onLoad, onReady } from '@dcloudio/uni-app'
|
||||||
|
import { computed, ref, type Component } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import TopicMemberZone from './components/topic-member-zone.vue'
|
||||||
|
import TopicLiveSeafoodAir from './components/topic-live-seafood-air.vue'
|
||||||
|
import TopicMustEatList from './components/topic-must-eat-list.vue'
|
||||||
|
import TopicNewCalendar from './components/topic-new-calendar.vue'
|
||||||
|
import TopicFreshSeafoodToday from './components/topic-fresh-seafood-today.vue'
|
||||||
|
import type { RouteQueryMap } from './utils/featured-dish-query'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const topic = ref('')
|
||||||
|
const routeQuery = ref<RouteQueryMap>({})
|
||||||
|
|
||||||
|
const VALID_TOPICS = [
|
||||||
|
'member-zone',
|
||||||
|
'live-seafood-air',
|
||||||
|
'must-eat-list',
|
||||||
|
'new-calendar',
|
||||||
|
'fresh-seafood-today',
|
||||||
|
] as const
|
||||||
|
|
||||||
|
type QuickTopic = (typeof VALID_TOPICS)[number]
|
||||||
|
|
||||||
|
const topicComponentMap: Record<QuickTopic, Component> = {
|
||||||
|
'member-zone': TopicMemberZone,
|
||||||
|
'live-seafood-air': TopicLiveSeafoodAir,
|
||||||
|
'must-eat-list': TopicMustEatList,
|
||||||
|
'new-calendar': TopicNewCalendar,
|
||||||
|
'fresh-seafood-today': TopicFreshSeafoodToday,
|
||||||
|
}
|
||||||
|
|
||||||
|
const topicTitleKeyMap: Record<QuickTopic, string> = {
|
||||||
|
'member-zone': 'pages.home.quickTabs.memberZone',
|
||||||
|
'live-seafood-air': 'pages.home.quickTabs.liveSeafoodAir',
|
||||||
|
'must-eat-list': 'pages.home.quickTabs.mustEatList',
|
||||||
|
'new-calendar': 'pages.home.quickTabs.newCalendar',
|
||||||
|
'fresh-seafood-today': 'pages.home.quickTabs.freshSeafoodToday',
|
||||||
|
}
|
||||||
|
|
||||||
|
const categoryNameFromRoute = computed(() => {
|
||||||
|
const name = routeQuery.value.categoryName
|
||||||
|
return name ? decodeURIComponent(name) : ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const pageTitle = computed(() => {
|
||||||
|
if (categoryNameFromRoute.value) return categoryNameFromRoute.value
|
||||||
|
const key = topicTitleKeyMap[topic.value as QuickTopic]
|
||||||
|
return key ? t(key) : ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const isValidTopic = computed(() =>
|
||||||
|
VALID_TOPICS.includes(topic.value as QuickTopic),
|
||||||
|
)
|
||||||
|
|
||||||
|
const activeTopicComponent = computed(() => {
|
||||||
|
if (!isValidTopic.value) return null
|
||||||
|
return topicComponentMap[topic.value as QuickTopic] ?? null
|
||||||
|
})
|
||||||
|
|
||||||
|
onLoad((query?: Record<string, string | undefined>) => {
|
||||||
|
topic.value = String(query?.topic ?? '')
|
||||||
|
const { topic: _t, ...rest } = query ?? {}
|
||||||
|
routeQuery.value = rest
|
||||||
|
})
|
||||||
|
|
||||||
|
onReady(() => {
|
||||||
|
if (!isValidTopic.value) {
|
||||||
|
uni.showToast({ title: '参数无效', icon: 'none' })
|
||||||
|
setTimeout(() => uni.navigateBack(), 1500)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<view class="quick-topic-root">
|
||||||
|
<component
|
||||||
|
v-if="activeTopicComponent"
|
||||||
|
:is="activeTopicComponent"
|
||||||
|
:page-title="pageTitle"
|
||||||
|
:route-query="routeQuery"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.quick-topic-root {
|
||||||
|
min-height: 100vh;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
/** 精选菜品列表 POST body,见 docs/app-featured-dish-list.md */
|
||||||
|
export type FeaturedDishQueryBody = {
|
||||||
|
hasMemberPrice?: any
|
||||||
|
stockMin?: any
|
||||||
|
stockMax?: any
|
||||||
|
recipeCategoryId?: any
|
||||||
|
createBeginTime?: any
|
||||||
|
createEndTime?: any
|
||||||
|
salesSort?: 'asc' | 'desc'
|
||||||
|
isNew?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RouteQueryMap = Record<string, string | undefined>
|
||||||
|
/** 将 URL 上可选筛选参数合并进 body(分页仍走 query) */
|
||||||
|
export function mergeRouteQueryIntoBody(
|
||||||
|
base: FeaturedDishQueryBody,
|
||||||
|
routeQuery: RouteQueryMap = {},
|
||||||
|
): FeaturedDishQueryBody {
|
||||||
|
const body: FeaturedDishQueryBody = { ...base }
|
||||||
|
const recipeCategoryId = routeQuery.recipeCategoryId
|
||||||
|
const stockMin = routeQuery.stockMin
|
||||||
|
const stockMax = routeQuery.stockMax
|
||||||
|
const createBeginTime = routeQuery.createBeginTime
|
||||||
|
const createEndTime = routeQuery.createEndTime
|
||||||
|
const hasMemberPrice = routeQuery.hasMemberPrice
|
||||||
|
const salesSort = routeQuery.salesSort
|
||||||
|
|
||||||
|
if (recipeCategoryId != null) body.recipeCategoryId = recipeCategoryId
|
||||||
|
if (stockMin != null) body.stockMin = stockMin
|
||||||
|
if (stockMax != null) body.stockMax = stockMax
|
||||||
|
if (createBeginTime != null) body.createBeginTime = createBeginTime
|
||||||
|
if (createEndTime != null) body.createEndTime = createEndTime
|
||||||
|
if (hasMemberPrice != null) body.hasMemberPrice = hasMemberPrice
|
||||||
|
if (salesSort === 'asc' || salesSort === 'desc') body.salesSort = salesSort
|
||||||
|
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 会员专区:仅查有会员价菜品 */
|
||||||
|
export function buildMemberZoneQuery(routeQuery?: RouteQueryMap): FeaturedDishQueryBody {
|
||||||
|
return mergeRouteQueryIntoBody({ hasMemberPrice: true }, routeQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 海鲜菜谱分类 ID(保持字符串,勿转 number) */
|
||||||
|
const SEAFOOD_RECIPE_CATEGORY_ID = '2037471880338415618'
|
||||||
|
|
||||||
|
/** 限量空运活海鲜:海鲜分类 + 库存范围 */
|
||||||
|
export function buildLiveSeafoodAirQuery(routeQuery?: RouteQueryMap): FeaturedDishQueryBody {
|
||||||
|
return mergeRouteQueryIntoBody(
|
||||||
|
{
|
||||||
|
recipeCategoryId: SEAFOOD_RECIPE_CATEGORY_ID,
|
||||||
|
stockMin: 1,
|
||||||
|
stockMax: 200,
|
||||||
|
},
|
||||||
|
routeQuery,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 必吃榜:按销量降序 */
|
||||||
|
export function buildMustEatListQuery(routeQuery?: RouteQueryMap): FeaturedDishQueryBody {
|
||||||
|
return mergeRouteQueryIntoBody({ salesSort: 'desc' }, routeQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 当天 00:00:00 ~ 23:59:59(本地时区,10 位秒级时间戳) */
|
||||||
|
export function getTodayCreateTimeRange(): { createBeginTime: number; createEndTime: number } {
|
||||||
|
const now = new Date()
|
||||||
|
const createBeginTime = Math.floor(
|
||||||
|
new Date(
|
||||||
|
now.getFullYear(),
|
||||||
|
now.getMonth(),
|
||||||
|
now.getDate(),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
).getTime() / 1000,
|
||||||
|
)
|
||||||
|
const createEndTime = Math.floor(
|
||||||
|
new Date(
|
||||||
|
now.getFullYear(),
|
||||||
|
now.getMonth(),
|
||||||
|
now.getDate(),
|
||||||
|
23,
|
||||||
|
59,
|
||||||
|
59,
|
||||||
|
0,
|
||||||
|
).getTime() / 1000,
|
||||||
|
)
|
||||||
|
return { createBeginTime, createEndTime }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 上新日历:当天创建 + 新品(isNew = 1) */
|
||||||
|
export function buildNewCalendarQuery(routeQuery?: RouteQueryMap): FeaturedDishQueryBody {
|
||||||
|
return mergeRouteQueryIntoBody(
|
||||||
|
{
|
||||||
|
...getTodayCreateTimeRange(),
|
||||||
|
isNew: 1,
|
||||||
|
},
|
||||||
|
routeQuery,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 今日现打海鲜:海鲜分类 + 当天创建时间(秒级时间戳) */
|
||||||
|
export function buildFreshSeafoodTodayQuery(routeQuery?: RouteQueryMap): FeaturedDishQueryBody {
|
||||||
|
return mergeRouteQueryIntoBody(
|
||||||
|
{
|
||||||
|
recipeCategoryId: SEAFOOD_RECIPE_CATEGORY_ID,
|
||||||
|
...getTodayCreateTimeRange(),
|
||||||
|
},
|
||||||
|
routeQuery,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TOPIC_QUERY_BUILDERS: Record<
|
||||||
|
string,
|
||||||
|
(routeQuery?: RouteQueryMap) => FeaturedDishQueryBody
|
||||||
|
> = {
|
||||||
|
'member-zone': buildMemberZoneQuery,
|
||||||
|
'live-seafood-air': buildLiveSeafoodAirQuery,
|
||||||
|
'must-eat-list': buildMustEatListQuery,
|
||||||
|
'new-calendar': buildNewCalendarQuery,
|
||||||
|
'fresh-seafood-today': buildFreshSeafoodTodayQuery,
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
/** 首页 tabs-type 对应精选菜品专题 slug(与 quick-topic 子组件一致) */
|
||||||
|
export const QUICK_TOPIC_SLUGS = [
|
||||||
|
'member-zone',
|
||||||
|
'live-seafood-air',
|
||||||
|
'must-eat-list',
|
||||||
|
'new-calendar',
|
||||||
|
'fresh-seafood-today',
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export type QuickTopicSlug = (typeof QUICK_TOPIC_SLUGS)[number]
|
||||||
|
|
||||||
|
export function isQuickTopicSlug(value: string): value is QuickTopicSlug {
|
||||||
|
return (QUICK_TOPIC_SLUGS as readonly string[]).includes(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 /app/merchantCategory/list 单项映射为 quick-topic 的 topic。
|
||||||
|
* 优先读后端字段 topicKey / code / topic;否则按列表顺序与五个专题一一对应。
|
||||||
|
*/
|
||||||
|
export function resolveQuickTopicFromMerchantCategory(
|
||||||
|
item: Record<string, unknown>,
|
||||||
|
index: number,
|
||||||
|
): QuickTopicSlug {
|
||||||
|
const raw = item.topicKey ?? item.code ?? item.topic
|
||||||
|
if (typeof raw === 'string' && isQuickTopicSlug(raw)) {
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
const safeIndex = index >= 0 && index < QUICK_TOPIC_SLUGS.length ? index : 0
|
||||||
|
return QUICK_TOPIC_SLUGS[safeIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildQuickTopicUrl(
|
||||||
|
topic: QuickTopicSlug,
|
||||||
|
extra: { merchantCategoryId?: string | number; categoryName?: string } = {},
|
||||||
|
) {
|
||||||
|
const parts = [`topic=${encodeURIComponent(topic)}`]
|
||||||
|
if (extra.merchantCategoryId != null && extra.merchantCategoryId !== '') {
|
||||||
|
parts.push(`merchantCategoryId=${encodeURIComponent(String(extra.merchantCategoryId))}`)
|
||||||
|
}
|
||||||
|
if (extra.categoryName) {
|
||||||
|
parts.push(`categoryName=${encodeURIComponent(extra.categoryName)}`)
|
||||||
|
}
|
||||||
|
return `/pages-store/pages/dishes/quick-topic?${parts.join('&')}`
|
||||||
|
}
|
||||||
@@ -25,21 +25,18 @@ import {
|
|||||||
} from "@/service";
|
} from "@/service";
|
||||||
import useEventEmit from "@/hooks/useEventEmit";
|
import useEventEmit from "@/hooks/useEventEmit";
|
||||||
import {EventEnum} from "@/constant/enums";
|
import {EventEnum} from "@/constant/enums";
|
||||||
import { getDistanceInMiles} from "@/utils/utils";
|
import { getDistanceInMiles, parseMerchantCartPayload } from "@/utils/utils";
|
||||||
|
import {
|
||||||
|
buildReservationTimeUrl,
|
||||||
|
buildDayAppointmentSlot,
|
||||||
|
findNearestDeliveryScheduleDate,
|
||||||
|
loadCheckoutMerchantAppointments,
|
||||||
|
type MerchantAppointmentSlot,
|
||||||
|
} from "@/utils/deliverySchedule";
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const configStore = useConfigStore();
|
const configStore = useConfigStore();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
|
||||||
function fillI18nParams(template: string, params: Record<string, string | number>) {
|
|
||||||
let text = template;
|
|
||||||
Object.keys(params).forEach((key) => {
|
|
||||||
const value = String(params[key] ?? "");
|
|
||||||
text = text.replace(new RegExp(`\\{${key}\\}`, "g"), value);
|
|
||||||
text = text.replace(new RegExp(`\\$\\{${key}\\}`, "g"), value);
|
|
||||||
});
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
const appDisplayName = computed(() => {
|
const appDisplayName = computed(() => {
|
||||||
const name = String((Config as any)?.appName ?? "").trim();
|
const name = String((Config as any)?.appName ?? "").trim();
|
||||||
return name || "CHEFLINK";
|
return name || "CHEFLINK";
|
||||||
@@ -111,43 +108,134 @@ const showDeliveryTime = computed(() => {
|
|||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
});
|
});
|
||||||
// 切换配送时间点击事件(目前仅支持预约配送)
|
const merchantAppointmentMap = ref<Record<string, MerchantAppointmentSlot>>({});
|
||||||
function toggleDeliveryTimeType(type: number) {
|
const merchantAppointmentDisplay = ref<Record<string, string>>({});
|
||||||
// 统一设置为预约配送
|
const selectingMerchantId = ref<string | null>(null);
|
||||||
deliveryTimeType.value = 2;
|
|
||||||
|
|
||||||
// 跳转到时间选择页面
|
function ensureDefaultMerchantAppointment(merchant: {
|
||||||
const businessHours = (storeDetail.value as any)?.businessHours;
|
id?: string | number;
|
||||||
const url = businessHours
|
deliveryScheduleTimes?: string;
|
||||||
? `/pages/address/reservation-time?storeBusinessHours=${encodeURIComponent(
|
}) {
|
||||||
businessHours
|
const key = String(merchant.id ?? "");
|
||||||
)}`
|
if (!key || merchantAppointmentMap.value[key]?.startTime) return;
|
||||||
: "/pages/address/reservation-time";
|
|
||||||
|
|
||||||
|
const nearest = findNearestDeliveryScheduleDate(
|
||||||
|
merchant.deliveryScheduleTimes
|
||||||
|
);
|
||||||
|
if (!nearest) return;
|
||||||
|
|
||||||
|
const slot = buildDayAppointmentSlot(nearest);
|
||||||
|
merchantAppointmentMap.value[key] = slot;
|
||||||
|
syncAppointmentDisplay(key, { date: nearest, timeSlot: "" });
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncAppointmentDisplay(merchantId: string, data: any) {
|
||||||
|
const label =
|
||||||
|
dayjs(data.date).format("MM-DD") +
|
||||||
|
(data.timeSlot ? ` ${data.timeSlot}` : "");
|
||||||
|
merchantAppointmentDisplay.value[merchantId] = label;
|
||||||
|
if (deliveryMethod.value === 0) {
|
||||||
|
userSelectedDeliveryTimeDate.value = label;
|
||||||
|
} else {
|
||||||
|
userSelectedSelfPickupTimeDate.value = label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openReservationForMerchant(merchant: {
|
||||||
|
id?: string | number;
|
||||||
|
businessHours?: string;
|
||||||
|
deliveryScheduleTimes?: string;
|
||||||
|
}) {
|
||||||
|
if (!merchant?.id) return;
|
||||||
|
selectingMerchantId.value = String(merchant.id);
|
||||||
|
let schedule = merchant.deliveryScheduleTimes;
|
||||||
|
let hours = merchant.businessHours;
|
||||||
|
if (!schedule || !hours) {
|
||||||
|
try {
|
||||||
|
const res: any = await appMerchantDetailMerchantIdGet({
|
||||||
|
params: { merchantId: merchant.id as any },
|
||||||
|
});
|
||||||
|
schedule = res.data?.deliveryScheduleTimes ?? schedule;
|
||||||
|
hours = res.data?.businessHours ?? hours;
|
||||||
|
const mid = String(merchant.id);
|
||||||
|
const found = cartDataList.value.find(
|
||||||
|
(m: any) => String(m.id) === mid
|
||||||
|
) as any;
|
||||||
|
if (found) {
|
||||||
|
found.deliveryScheduleTimes = schedule;
|
||||||
|
found.businessHours = hours;
|
||||||
|
}
|
||||||
|
if (String(storeId.value) === mid) {
|
||||||
|
storeDetail.value = {
|
||||||
|
...storeDetail.value,
|
||||||
|
deliveryScheduleTimes: schedule,
|
||||||
|
businessHours: hours,
|
||||||
|
} as MerchantVo;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url,
|
url: buildReservationTimeUrl({
|
||||||
|
merchantId: merchant.id,
|
||||||
|
deliveryScheduleTimes: schedule,
|
||||||
|
businessHours: hours,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const diyTime = ref({})
|
// 切换配送时间点击事件(目前仅支持预约配送)
|
||||||
useEventEmit(EventEnum.CHOOSE_APPOINTMENT_TIME, (data) => {
|
function toggleDeliveryTimeType(
|
||||||
console.log('CHOOSE_APPOINTMENT_TIME', data)
|
_type: number,
|
||||||
if(data) {
|
merchant?: { id?: string | number; businessHours?: string; deliveryScheduleTimes?: string }
|
||||||
diyTime.value = data;
|
) {
|
||||||
deliveryTimeType.value = 2
|
deliveryTimeType.value = 2;
|
||||||
|
if (orderType.value === "batch" && merchant) {
|
||||||
|
void openReservationForMerchant(merchant);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void openReservationForMerchant({
|
||||||
|
id: storeId.value,
|
||||||
|
businessHours: (storeDetail.value as any)?.businessHours,
|
||||||
|
deliveryScheduleTimes: storeDetail.value?.deliveryScheduleTimes,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 配送还是自取
|
function getMerchantPillText(merchantId: string | number) {
|
||||||
if(deliveryMethod.value === 0) {
|
const key = String(merchantId);
|
||||||
userSelectedDeliveryTimeDate.value =
|
if (merchantAppointmentDisplay.value[key]) {
|
||||||
dayjs(data.date).format('MM-DD') +
|
return `${merchantAppointmentDisplay.value[key]} >`;
|
||||||
(data.timeSlot ? ' ' + data.timeSlot : '');
|
}
|
||||||
} else {
|
const ap = merchantAppointmentMap.value[key];
|
||||||
userSelectedSelfPickupTimeDate.value =
|
if (ap?.startTime) {
|
||||||
dayjs(data.date).format('MM-DD') +
|
const d = dayjs(Number(ap.startTime));
|
||||||
(data.timeSlot ? ' ' + data.timeSlot : '');
|
if (d.isValid()) {
|
||||||
|
return `${d.format("dddd")}, ${d.format("MM/DD")} >`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
return t("pages-store.checkout.chooseTime");
|
||||||
|
}
|
||||||
|
|
||||||
|
const diyTime = ref<MerchantAppointmentSlot | Record<string, never>>({});
|
||||||
|
useEventEmit(EventEnum.CHOOSE_APPOINTMENT_TIME, (data: any) => {
|
||||||
|
if (!data?.startTime || !data?.endTime) return;
|
||||||
|
const merchantId = String(
|
||||||
|
data.merchantId || selectingMerchantId.value || storeId.value || ""
|
||||||
|
);
|
||||||
|
if (!merchantId) return;
|
||||||
|
|
||||||
|
merchantAppointmentMap.value[merchantId] = {
|
||||||
|
date: data.date,
|
||||||
|
timeSlot: data.timeSlot,
|
||||||
|
startTime: data.startTime,
|
||||||
|
endTime: data.endTime,
|
||||||
|
};
|
||||||
|
diyTime.value = merchantAppointmentMap.value[merchantId];
|
||||||
|
deliveryTimeType.value = 2;
|
||||||
|
syncAppointmentDisplay(merchantId, data);
|
||||||
|
selectingMerchantId.value = null;
|
||||||
|
});
|
||||||
|
|
||||||
// 设置配送时间(分钟)
|
// 设置配送时间(分钟)
|
||||||
const setDeliveryMinutes = (minutes: number) => {
|
const setDeliveryMinutes = (minutes: number) => {
|
||||||
@@ -408,6 +496,19 @@ onLoad(async (options: any)=> {
|
|||||||
// 严格按顺序执行
|
// 严格按顺序执行
|
||||||
await safeAwait('getAddressList', getAddressList)
|
await safeAwait('getAddressList', getAddressList)
|
||||||
await safeAwait('getBatchCartInfo', getBatchCartInfo)
|
await safeAwait('getBatchCartInfo', getBatchCartInfo)
|
||||||
|
merchantAppointmentMap.value = loadCheckoutMerchantAppointments()
|
||||||
|
cartDataList.value.forEach((m: any) => {
|
||||||
|
const key = String(m.id);
|
||||||
|
const ap = merchantAppointmentMap.value[key];
|
||||||
|
if (ap?.startTime) {
|
||||||
|
syncAppointmentDisplay(key, {
|
||||||
|
date: ap.date ? new Date(ap.date as any) : new Date(Number(ap.startTime)),
|
||||||
|
timeSlot: ap.timeSlot,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ensureDefaultMerchantAppointment(m);
|
||||||
|
}
|
||||||
|
});
|
||||||
await safeAwait('appUserCardSelectDefault', appUserCardSelectDefault)
|
await safeAwait('appUserCardSelectDefault', appUserCardSelectDefault)
|
||||||
} else if(options.storeId) {
|
} else if(options.storeId) {
|
||||||
// 普通下单模式
|
// 普通下单模式
|
||||||
@@ -469,7 +570,7 @@ async function getCartInfo() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
console.log('购物车列表', res)
|
console.log('购物车列表', res)
|
||||||
cartDataList.value = res.data
|
cartDataList.value = parseMerchantCartPayload(res?.data).items as MerchantCartVo[]
|
||||||
|
|
||||||
// 购物车有菜品,查询菜品会员折扣价 + 计算价格(严格串行)
|
// 购物车有菜品,查询菜品会员折扣价 + 计算价格(严格串行)
|
||||||
if(cartDataList.value.length > 0) {
|
if(cartDataList.value.length > 0) {
|
||||||
@@ -504,8 +605,8 @@ async function getBatchCartInfo() {
|
|||||||
});
|
});
|
||||||
console.log(`批量模式-店铺 ${merchant.merchantName} 的商品列表`, cartRes);
|
console.log(`批量模式-店铺 ${merchant.merchantName} 的商品列表`, cartRes);
|
||||||
|
|
||||||
// 只保留选中的商品(根据 cartIds 过滤)
|
const payload = parseMerchantCartPayload(cartRes?.data);
|
||||||
const filteredItems = (cartRes.data || []).filter((item: any) =>
|
const filteredItems = payload.items.filter((item: any) =>
|
||||||
cartIds.includes(String(item.id))
|
cartIds.includes(String(item.id))
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -513,6 +614,8 @@ async function getBatchCartInfo() {
|
|||||||
if (filteredItems.length > 0) {
|
if (filteredItems.length > 0) {
|
||||||
return {
|
return {
|
||||||
...merchant,
|
...merchant,
|
||||||
|
deliveryScheduleTimes:
|
||||||
|
payload.deliveryScheduleTimes ?? merchant.deliveryScheduleTimes,
|
||||||
merchantCartVoList: filteredItems
|
merchantCartVoList: filteredItems
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -532,6 +635,7 @@ async function getBatchCartInfo() {
|
|||||||
if (merchantTipIndexMap.value[id] === undefined) {
|
if (merchantTipIndexMap.value[id] === undefined) {
|
||||||
merchantTipIndexMap.value[id] = 2;
|
merchantTipIndexMap.value[id] = 2;
|
||||||
}
|
}
|
||||||
|
ensureDefaultMerchantAppointment(m);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 批量模式:查询菜品会员折扣价 + 计算价格(严格串行)
|
// 批量模式:查询菜品会员折扣价 + 计算价格(严格串行)
|
||||||
@@ -616,6 +720,11 @@ async function getStoreDetail() {
|
|||||||
deliveryMethod.value = 0
|
deliveryMethod.value = 0
|
||||||
showDeliverySwitch.value = true
|
showDeliverySwitch.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ensureDefaultMerchantAppointment({
|
||||||
|
id: storeId.value,
|
||||||
|
deliveryScheduleTimes: storeDetail.value?.deliveryScheduleTimes,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const priceData = ref({})
|
const priceData = ref({})
|
||||||
@@ -659,13 +768,25 @@ async function appMerchantOrderCalculatePriceCart() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const startMap: Record<string, number> = {}
|
||||||
|
const endMap: Record<string, number> = {}
|
||||||
|
cartDataList.value.forEach((m: any) => {
|
||||||
|
const ap = merchantAppointmentMap.value[String(m.id)]
|
||||||
|
if (ap?.startTime && ap?.endTime) {
|
||||||
|
startMap[String(m.id)] = ap.startTime
|
||||||
|
endMap[String(m.id)] = ap.endTime
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const res: any = await appMerchantOrderCalculatePriceCartBatchPost({
|
const res: any = await appMerchantOrderCalculatePriceCartBatchPost({
|
||||||
body: {
|
body: {
|
||||||
addressId: targetAddressId,
|
addressId: targetAddressId,
|
||||||
cartIds: cartIds,
|
cartIds: cartIds,
|
||||||
receiveMethod: deliveryMethod.value === 0 ? 1 : 2,
|
receiveMethod: deliveryMethod.value === 0 ? 1 : 2,
|
||||||
merchantCouponMap: couponMap,
|
merchantCouponMap: couponMap,
|
||||||
merchantTipMap: tipMap, // 小费金额(美元)
|
merchantTipMap: tipMap,
|
||||||
|
merchantStartScheduledTimeMap: startMap,
|
||||||
|
merchantEndScheduledTimeMap: endMap,
|
||||||
phone: formData.value.phone,
|
phone: formData.value.phone,
|
||||||
areaCode: contact.value.areaCode,
|
areaCode: contact.value.areaCode,
|
||||||
}
|
}
|
||||||
@@ -792,13 +913,30 @@ function handleGoSettle() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 仅支持预约配送:必须先选择预约时间
|
if (deliveryTimeType.value === 2) {
|
||||||
if (deliveryTimeType.value === 2 && (!diyTime.value.startTime || !diyTime.value.endTime)) {
|
if (orderType.value === "batch") {
|
||||||
|
for (const m of cartDataList.value as any[]) {
|
||||||
|
const ap = merchantAppointmentMap.value[String(m.id)];
|
||||||
|
if (!ap?.startTime || !ap?.endTime) {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: t('pages-store.checkout.chooseTime'),
|
title: t("pages-store.checkout.chooseTimeForStore", {
|
||||||
icon: 'none'
|
name: m.merchantName || "",
|
||||||
})
|
}),
|
||||||
return
|
icon: "none",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const ap = merchantAppointmentMap.value[String(storeId.value)];
|
||||||
|
if (!ap?.startTime || !ap?.endTime) {
|
||||||
|
uni.showToast({
|
||||||
|
title: t("pages-store.checkout.chooseTime"),
|
||||||
|
icon: "none",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 批量下单
|
// 批量下单
|
||||||
@@ -851,14 +989,22 @@ function handleGoSettle() {
|
|||||||
needTableware: needTableware.value ? 1 : 2, // 餐具 1是 2否
|
needTableware: needTableware.value ? 1 : 2, // 餐具 1是 2否
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果是预约派送
|
if (deliveryTimeType.value === 1) {
|
||||||
if(deliveryTimeType.value === 1) {
|
|
||||||
const result = getTimeStamps(showDeliveryTime.value);
|
const result = getTimeStamps(showDeliveryTime.value);
|
||||||
data.startScheduledTime = result.startTimestamp
|
data.startScheduledTime = result.startTimestamp
|
||||||
data.endScheduledTime = result.endTimestamp
|
data.endScheduledTime = result.endTimestamp
|
||||||
} else {
|
} else {
|
||||||
data.startScheduledTime = diyTime.value.startTime
|
const startMap: Record<string, number> = {}
|
||||||
data.endScheduledTime = diyTime.value.endTime
|
const endMap: Record<string, number> = {}
|
||||||
|
cartDataList.value.forEach((m: any) => {
|
||||||
|
const ap = merchantAppointmentMap.value[String(m.id)]
|
||||||
|
if (ap?.startTime && ap?.endTime) {
|
||||||
|
startMap[String(m.id)] = ap.startTime
|
||||||
|
endMap[String(m.id)] = ap.endTime
|
||||||
|
}
|
||||||
|
})
|
||||||
|
data.merchantStartScheduledTimeMap = startMap
|
||||||
|
data.merchantEndScheduledTimeMap = endMap
|
||||||
}
|
}
|
||||||
console.log('批量下单参数', data)
|
console.log('批量下单参数', data)
|
||||||
appMerchantOrderCreateOrderCartBatchPost({
|
appMerchantOrderCreateOrderCartBatchPost({
|
||||||
@@ -924,8 +1070,9 @@ function handleGoSettle() {
|
|||||||
data.startScheduledTime = result.startTimestamp
|
data.startScheduledTime = result.startTimestamp
|
||||||
data.endScheduledTime = result.endTimestamp
|
data.endScheduledTime = result.endTimestamp
|
||||||
} else {
|
} else {
|
||||||
data.startScheduledTime = diyTime.value.startTime
|
const ap = merchantAppointmentMap.value[String(storeId.value)]
|
||||||
data.endScheduledTime = diyTime.value.endTime
|
data.startScheduledTime = ap?.startTime ?? (diyTime.value as any).startTime
|
||||||
|
data.endScheduledTime = ap?.endTime ?? (diyTime.value as any).endTime
|
||||||
}
|
}
|
||||||
console.log('下单参数', data)
|
console.log('下单参数', data)
|
||||||
appMerchantOrderCreateOrderCartPost({
|
appMerchantOrderCreateOrderCartPost({
|
||||||
@@ -1201,22 +1348,10 @@ const serviceFeeAmount = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const deliveryPillText = computed(() => {
|
const deliveryPillText = computed(() => {
|
||||||
const raw =
|
if (orderType.value === "batch") {
|
||||||
deliveryMethod.value === 0
|
return t("pages-store.checkout.chooseTime");
|
||||||
? userSelectedDeliveryTimeDate.value
|
|
||||||
: userSelectedSelfPickupTimeDate.value;
|
|
||||||
if (!raw) return t('pages-store.checkout.chooseTime');
|
|
||||||
const str = String(raw);
|
|
||||||
const m = str.match(/^(\d{2})-(\d{2})/);
|
|
||||||
if (m) {
|
|
||||||
const y = dayjs().year();
|
|
||||||
const d = dayjs(`${y}-${m[1]}-${m[2]}`);
|
|
||||||
if (d.isValid()) {
|
|
||||||
const w = d.format('dddd');
|
|
||||||
return `${w}, ${m[1]}/${m[2]} >`;
|
|
||||||
}
|
}
|
||||||
}
|
return getMerchantPillText(storeId.value);
|
||||||
return `${str} >`;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function safeToNumber(val: unknown, fallback = 0) {
|
function safeToNumber(val: unknown, fallback = 0) {
|
||||||
@@ -1282,24 +1417,24 @@ const scheduledServiceEndLabel = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const localDeliverySubtitleText = computed(() =>
|
const localDeliverySubtitleText = computed(() =>
|
||||||
fillI18nParams(t('pages-store.checkout.localDeliverySubtitle'), { name: appDisplayName.value })
|
t('pages-store.checkout.localDeliverySubtitle', { name: appDisplayName.value })
|
||||||
);
|
);
|
||||||
|
|
||||||
const itemsGoodsTotalWithPriceText = computed(() =>
|
const itemsGoodsTotalWithPriceText = computed(() =>
|
||||||
fillI18nParams(t('pages-store.checkout.itemsGoodsTotalWithPrice'), {
|
t('pages-store.checkout.itemsGoodsTotalWithPrice', {
|
||||||
count: cartTotalPieceCount.value,
|
count: cartTotalPieceCount.value,
|
||||||
amount: displayGoodsAmountStr.value,
|
amount: displayGoodsAmountStr.value,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const scheduledDeliveryWindowText = computed(() =>
|
const scheduledDeliveryWindowText = computed(() =>
|
||||||
fillI18nParams(t('pages-store.checkout.scheduledDeliveryWindow'), {
|
t('pages-store.checkout.scheduledDeliveryWindow', {
|
||||||
time: scheduledServiceEndLabel.value,
|
time: scheduledServiceEndLabel.value,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const deliverBeforeText = computed(() =>
|
const deliverBeforeText = computed(() =>
|
||||||
fillI18nParams(t('pages-store.checkout.deliverBefore'), {
|
t('pages-store.checkout.deliverBefore', {
|
||||||
time: deliveryMethod.value === 0
|
time: deliveryMethod.value === 0
|
||||||
? String(userSelectedDeliveryTimeDate.value ?? '')
|
? String(userSelectedDeliveryTimeDate.value ?? '')
|
||||||
: String(userSelectedSelfPickupTimeDate.value ?? ''),
|
: String(userSelectedSelfPickupTimeDate.value ?? ''),
|
||||||
@@ -1307,17 +1442,17 @@ const deliverBeforeText = computed(() =>
|
|||||||
);
|
);
|
||||||
|
|
||||||
const memberThanksText = computed(() =>
|
const memberThanksText = computed(() =>
|
||||||
fillI18nParams(t('pages-store.checkout.memberThanks'), { name: appDisplayName.value })
|
t('pages-store.checkout.memberThanks', { name: appDisplayName.value })
|
||||||
);
|
);
|
||||||
|
|
||||||
const subtotalWithPiecesText = computed(() =>
|
const subtotalWithPiecesText = computed(() =>
|
||||||
fillI18nParams(t('pages-store.checkout.subtotalWithPieces'), {
|
t('pages-store.checkout.subtotalWithPieces', {
|
||||||
count: cartTotalPieceCount.value,
|
count: cartTotalPieceCount.value,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const subtotalOneLineText = computed(() =>
|
const subtotalOneLineText = computed(() =>
|
||||||
fillI18nParams(t('pages-store.checkout.subtotalOneLine'), {
|
t('pages-store.checkout.subtotalOneLine', {
|
||||||
amount: displayPaidStr.value,
|
amount: displayPaidStr.value,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -1588,25 +1723,6 @@ function handleClose(merchantId?: string) {
|
|||||||
|
|
||||||
<!-- 批量模式:多店铺列表 -->
|
<!-- 批量模式:多店铺列表 -->
|
||||||
<view class="checkout-block" v-if="orderType === 'batch' && cartDataList.length > 0">
|
<view class="checkout-block" v-if="orderType === 'batch' && cartDataList.length > 0">
|
||||||
<view class="checkout-section-label">{{ t('pages-store.checkout.confirmOrder') }}</view>
|
|
||||||
<view v-if="showAppointmentEntry" class="checkout-card checkout-gutter checkout-batch-pill-card">
|
|
||||||
<view class="checkout-confirm-band checkout-confirm-band--compact">
|
|
||||||
<view @click.stop="toggleDeliveryTimeType(2)" class="checkout-date-pill">
|
|
||||||
{{ deliveryPillText }}
|
|
||||||
</view>
|
|
||||||
<view class="checkout-confirm-band-right">
|
|
||||||
<text class="checkout-confirm-band-line1">
|
|
||||||
{{ cartTotalPieceCount }} {{ t('pages-user.cart.items') }} · ${{ displayGoodsAmountStr }}
|
|
||||||
</text>
|
|
||||||
<view v-if="deliveryMethod === 0" class="checkout-confirm-band-line2">
|
|
||||||
<text>{{ t('pages-store.checkout.shippingFee') }}</text>
|
|
||||||
<text v-if="showListDeliveryStrike" class="checkout-price-strike"> ${{ listDeliveryFeeStr }}</text>
|
|
||||||
<text v-if="showListDeliveryStrike"> </text>
|
|
||||||
<text class="checkout-batch-shipping-now">${{ displayDeliveryFeeStr }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<view class="checkout-gutter">
|
<view class="checkout-gutter">
|
||||||
<view
|
<view
|
||||||
v-for="merchant in cartDataList"
|
v-for="merchant in cartDataList"
|
||||||
@@ -1620,7 +1736,7 @@ function handleClose(merchantId?: string) {
|
|||||||
:src="merchant.logo"
|
:src="merchant.logo"
|
||||||
mode="aspectFill"
|
mode="aspectFill"
|
||||||
/>
|
/>
|
||||||
<view>
|
<view class="flex-1 min-w-0">
|
||||||
<view class="text-28rpx lh-28rpx text-#333 font-500"
|
<view class="text-28rpx lh-28rpx text-#333 font-500"
|
||||||
>{{ merchant.merchantName }}</view
|
>{{ merchant.merchantName }}</view
|
||||||
>
|
>
|
||||||
@@ -1630,6 +1746,17 @@ function handleClose(merchantId?: string) {
|
|||||||
>
|
>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
<view
|
||||||
|
v-if="showAppointmentEntry"
|
||||||
|
class="checkout-merchant-schedule-row"
|
||||||
|
>
|
||||||
|
<view
|
||||||
|
class="checkout-date-pill"
|
||||||
|
@click.stop="toggleDeliveryTimeType(2, merchant)"
|
||||||
|
>
|
||||||
|
{{ getMerchantPillText(merchant.id) }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<!-- 商品列表 -->
|
<!-- 商品列表 -->
|
||||||
<view class="checkout-merchant-body">
|
<view class="checkout-merchant-body">
|
||||||
@@ -2758,6 +2885,16 @@ $checkout-gutter: 32rpx;
|
|||||||
border-bottom: 1rpx solid $checkout-border;
|
border-bottom: 1rpx solid $checkout-border;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.checkout-merchant-schedule-row {
|
||||||
|
padding: 20rpx 24rpx;
|
||||||
|
border-bottom: 1rpx solid $checkout-border;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkout-confirm-band-right--full {
|
||||||
|
width: 100%;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
.checkout-merchant-body {
|
.checkout-merchant-body {
|
||||||
padding: 8rpx 24rpx 16rpx;
|
padding: 8rpx 24rpx 16rpx;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,16 +20,6 @@ import dayjs from 'dayjs'
|
|||||||
import useEventEmit from "@/hooks/useEventEmit";
|
import useEventEmit from "@/hooks/useEventEmit";
|
||||||
const configStore = useConfigStore();
|
const configStore = useConfigStore();
|
||||||
|
|
||||||
function fillI18nParams(template: string, params: Record<string, string | number>) {
|
|
||||||
let text = template
|
|
||||||
Object.keys(params).forEach((key) => {
|
|
||||||
const value = String(params[key] ?? '')
|
|
||||||
text = text.replace(new RegExp(`\\{${key}\\}`, 'g'), value)
|
|
||||||
text = text.replace(new RegExp(`\\$\\{${key}\\}`, 'g'), value)
|
|
||||||
})
|
|
||||||
return text
|
|
||||||
}
|
|
||||||
|
|
||||||
// 价格明细
|
// 价格明细
|
||||||
const priceDetailRef = ref<InstanceType<typeof PriceDetail>>();
|
const priceDetailRef = ref<InstanceType<typeof PriceDetail>>();
|
||||||
// 打开价格明细
|
// 打开价格明细
|
||||||
@@ -139,7 +129,7 @@ const orderTotalItemCount = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const orderTotalItemCountText = computed(() => {
|
const orderTotalItemCountText = computed(() => {
|
||||||
return fillI18nParams(t('pages.order.totalItemCount'), {
|
return t('pages.order.totalItemCount', {
|
||||||
count: orderTotalItemCount.value,
|
count: orderTotalItemCount.value,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import Config from "@/config";
|
|||||||
import {useConfigStore, useUserStore} from "@/store";
|
import {useConfigStore, useUserStore} from "@/store";
|
||||||
import DishesSkeleton from "./components/dishes-skeleton.vue";
|
import DishesSkeleton from "./components/dishes-skeleton.vue";
|
||||||
import {CollectionType} from "@/constant/enums";
|
import {CollectionType} from "@/constant/enums";
|
||||||
|
import { formatDeliveryScheduleDays } from "@/utils/deliverySchedule";
|
||||||
|
import { parseMerchantCartPayload } from "@/utils/utils";
|
||||||
const configStore = useConfigStore();
|
const configStore = useConfigStore();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
|
||||||
@@ -317,18 +319,8 @@ const appDisplayName = computed(() => {
|
|||||||
return name || "CHEFLINK";
|
return name || "CHEFLINK";
|
||||||
});
|
});
|
||||||
|
|
||||||
function fillI18nParams(template: string, params: Record<string, string | number>) {
|
|
||||||
let text = template;
|
|
||||||
Object.keys(params).forEach((key) => {
|
|
||||||
const value = String(params[key] ?? "");
|
|
||||||
text = text.replace(new RegExp(`\\{${key}\\}`, "g"), value);
|
|
||||||
text = text.replace(new RegExp(`\\$\\{${key}\\}`, "g"), value);
|
|
||||||
});
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
const memberPriceLabelText = computed(() =>
|
const memberPriceLabelText = computed(() =>
|
||||||
fillI18nParams(t("pages-store.store.dishDetail.memberPriceLabel"), {
|
t("pages-store.store.dishDetail.memberPriceLabel", {
|
||||||
name: appDisplayName.value,
|
name: appDisplayName.value,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -426,7 +418,7 @@ function getCartInfo() {
|
|||||||
}
|
}
|
||||||
}).then((res: any)=> {
|
}).then((res: any)=> {
|
||||||
console.log('购物车列表', res)
|
console.log('购物车列表', res)
|
||||||
cartDataList.value = res.data ?? []
|
cartDataList.value = parseMerchantCartPayload(res?.data).items as MerchantCartVo[]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -658,6 +650,13 @@ function navigateToCart() {
|
|||||||
|
|
||||||
// 获取商家详情信息
|
// 获取商家详情信息
|
||||||
const storeDetail = ref<MerchantVo>({})
|
const storeDetail = ref<MerchantVo>({})
|
||||||
|
const deliveryScheduleDaysText = computed(() => {
|
||||||
|
const days = formatDeliveryScheduleDays(
|
||||||
|
storeDetail.value?.deliveryScheduleTimes,
|
||||||
|
(key) => t(key)
|
||||||
|
);
|
||||||
|
return days ? t("pages-store.store.deliveryScheduleDays", { days }) : "";
|
||||||
|
});
|
||||||
function getStoreDetail() {
|
function getStoreDetail() {
|
||||||
appMerchantDetailMerchantIdGet({
|
appMerchantDetailMerchantIdGet({
|
||||||
params: {
|
params: {
|
||||||
@@ -836,6 +835,10 @@ function getStoreDetail() {
|
|||||||
<text class="store-pill__name">{{ storeDetail.merchantName }}</text>
|
<text class="store-pill__name">{{ storeDetail.merchantName }}</text>
|
||||||
<text class="store-pill__arr">›</text>
|
<text class="store-pill__arr">›</text>
|
||||||
</view>
|
</view>
|
||||||
|
<view
|
||||||
|
v-if="deliveryScheduleDaysText"
|
||||||
|
class="delivery-schedule-hint"
|
||||||
|
>{{ deliveryScheduleDaysText }}</view>
|
||||||
|
|
||||||
<!-- 产品详情 -->
|
<!-- 产品详情 -->
|
||||||
<view class="section-title">{{
|
<view class="section-title">{{
|
||||||
@@ -1198,6 +1201,7 @@ function getStoreDetail() {
|
|||||||
|
|
||||||
.dish-info {
|
.dish-info {
|
||||||
padding: 28rpx 30rpx 24rpx;
|
padding: 28rpx 30rpx 24rpx;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.price-block {
|
.price-block {
|
||||||
@@ -1279,6 +1283,9 @@ function getStoreDetail() {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 12rpx;
|
gap: 12rpx;
|
||||||
margin-bottom: 16rpx;
|
margin-bottom: 16rpx;
|
||||||
|
position: absolute;
|
||||||
|
top: 28rpx;
|
||||||
|
right: 30rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.meta-tag {
|
.meta-tag {
|
||||||
@@ -1315,6 +1322,13 @@ function getStoreDetail() {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.delivery-schedule-hint {
|
||||||
|
margin-top: 16rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 32rpx;
|
||||||
|
color: #00a76d;
|
||||||
|
}
|
||||||
|
|
||||||
.store-pill__arr {
|
.store-pill__arr {
|
||||||
font-size: 32rpx;
|
font-size: 32rpx;
|
||||||
color: #5a8fd8;
|
color: #5a8fd8;
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ import {
|
|||||||
} from "@/service";
|
} from "@/service";
|
||||||
import {CollectionType} from "@/constant/enums";
|
import {CollectionType} from "@/constant/enums";
|
||||||
import {useUserStore} from "@/store";
|
import {useUserStore} from "@/store";
|
||||||
import { parseBusinessHoursUtils, getDistanceInMiles } from "@/utils/utils";
|
import { parseBusinessHoursUtils, getDistanceInMiles, parseMerchantCartPayload } from "@/utils/utils";
|
||||||
|
import { formatDeliveryScheduleDays } from "@/utils/deliverySchedule";
|
||||||
import CouponPopup from './components/coupon-popup.vue'
|
import CouponPopup from './components/coupon-popup.vue'
|
||||||
import {getMerchantCouponReceiveListApi} from "@/pages-user/service";
|
import {getMerchantCouponReceiveListApi} from "@/pages-user/service";
|
||||||
// import type { MerchantVo } from '@/service/types'
|
// import type { MerchantVo } from '@/service/types'
|
||||||
@@ -77,6 +78,13 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
// 获取商家详情信息
|
// 获取商家详情信息
|
||||||
const storeDetail = ref<MerchantVo>({})
|
const storeDetail = ref<MerchantVo>({})
|
||||||
|
const deliveryScheduleDaysText = computed(() => {
|
||||||
|
const days = formatDeliveryScheduleDays(
|
||||||
|
storeDetail.value?.deliveryScheduleTimes,
|
||||||
|
(key) => t(key)
|
||||||
|
);
|
||||||
|
return days ? t("pages-store.store.deliveryScheduleDays", { days }) : "";
|
||||||
|
});
|
||||||
// 闭店提示信息
|
// 闭店提示信息
|
||||||
const closingInfo = ref({ show: false, minutes: 0 })
|
const closingInfo = ref({ show: false, minutes: 0 })
|
||||||
|
|
||||||
@@ -232,7 +240,7 @@ function getCartInfo() {
|
|||||||
}
|
}
|
||||||
}).then((res: any)=> {
|
}).then((res: any)=> {
|
||||||
console.log('购物车列表', res)
|
console.log('购物车列表', res)
|
||||||
cartDataList.value = res.data
|
cartDataList.value = parseMerchantCartPayload(res?.data).items as MerchantCartVo[]
|
||||||
|
|
||||||
// 购物车有菜品,查询菜品会员折扣价
|
// 购物车有菜品,查询菜品会员折扣价
|
||||||
if(cartDataList.value.length > 0) {
|
if(cartDataList.value.length > 0) {
|
||||||
@@ -571,6 +579,10 @@ function handleShare() {
|
|||||||
{{ storeDetail?.deliveryTime }}{{ daySuffix(storeDetail?.deliveryTime) }}
|
{{ storeDetail?.deliveryTime }}{{ daySuffix(storeDetail?.deliveryTime) }}
|
||||||
</view>
|
</view>
|
||||||
<view class="text-24rpx lh-24rpx text-#7D7D7D mt-8rpx">{{ t('pages-store.store.earTime') }}</view>
|
<view class="text-24rpx lh-24rpx text-#7D7D7D mt-8rpx">{{ t('pages-store.store.earTime') }}</view>
|
||||||
|
<view
|
||||||
|
v-if="deliveryScheduleDaysText"
|
||||||
|
class="text-24rpx lh-32rpx text-#00A76D mt-8rpx"
|
||||||
|
>{{ deliveryScheduleDaysText }}</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="+storeDetail?.selfPickup === 1 && deliveryMethod === 1">
|
<template v-if="+storeDetail?.selfPickup === 1 && deliveryMethod === 1">
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const { t } = useI18n();
|
||||||
|
const emit = defineEmits(["confirm", "close", "update:modelValue"]);
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: boolean;
|
||||||
|
zIndex?: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
function handleClose() {
|
||||||
|
emit("update:modelValue", false);
|
||||||
|
emit("close");
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleConfirm() {
|
||||||
|
emit("confirm");
|
||||||
|
handleClose();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<wd-popup
|
||||||
|
:model-value="modelValue"
|
||||||
|
@update:model-value="(val) => emit('update:modelValue', val)"
|
||||||
|
position="bottom"
|
||||||
|
:z-index="zIndex || 2000"
|
||||||
|
custom-style="background: transparent;"
|
||||||
|
@close="handleClose"
|
||||||
|
>
|
||||||
|
<view class="multi-store-notice">
|
||||||
|
<view class="multi-store-notice__title">
|
||||||
|
{{ t("pages-user.cart.orderNoticeTitle") }}
|
||||||
|
</view>
|
||||||
|
<view class="multi-store-notice__desc">
|
||||||
|
{{ t("pages-user.cart.multiStoreCheckoutNoticeDesc") }}
|
||||||
|
</view>
|
||||||
|
<view class="multi-store-notice__btn-fixed">
|
||||||
|
<wd-button
|
||||||
|
block
|
||||||
|
custom-class="multi-store-notice__btn"
|
||||||
|
custom-style="height:88rpx;line-height:88rpx;"
|
||||||
|
@click="handleConfirm"
|
||||||
|
@tap="handleConfirm"
|
||||||
|
>
|
||||||
|
{{ t("common.gotIt") }}
|
||||||
|
</wd-button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</wd-popup>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.multi-store-notice {
|
||||||
|
margin: 0 24rpx calc(24rpx + constant(safe-area-inset-bottom));
|
||||||
|
margin: 0 24rpx calc(24rpx + env(safe-area-inset-bottom));
|
||||||
|
border-radius: 32rpx;
|
||||||
|
padding: 44rpx 40rpx 180rpx;
|
||||||
|
background: #fff;
|
||||||
|
min-height: 420rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-store-notice__title {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 42rpx;
|
||||||
|
line-height: 52rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-store-notice__desc {
|
||||||
|
margin-top: 28rpx;
|
||||||
|
font-size: 30rpx;
|
||||||
|
line-height: 44rpx;
|
||||||
|
color: #333;
|
||||||
|
// text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-store-notice__btn-fixed {
|
||||||
|
position: fixed;
|
||||||
|
left: 64rpx;
|
||||||
|
right: 64rpx;
|
||||||
|
bottom: calc(48rpx + constant(safe-area-inset-bottom));
|
||||||
|
bottom: calc(48rpx + env(safe-area-inset-bottom));
|
||||||
|
z-index: 2200;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -5,18 +5,8 @@ const emit = defineEmits(['confirm','close'])
|
|||||||
const show = ref(false);
|
const show = ref(false);
|
||||||
const goodsName = ref('');
|
const goodsName = ref('');
|
||||||
|
|
||||||
function fillI18nParams(template: string, params: Record<string, string | number>) {
|
|
||||||
let text = template;
|
|
||||||
Object.keys(params).forEach((key) => {
|
|
||||||
const value = String(params[key] ?? "");
|
|
||||||
text = text.replace(new RegExp(`\\{${key}\\}`, "g"), value);
|
|
||||||
text = text.replace(new RegExp(`\\$\\{${key}\\}`, "g"), value);
|
|
||||||
});
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeProductDescText = computed(() =>
|
const removeProductDescText = computed(() =>
|
||||||
fillI18nParams(t("pages-user.cart.removeProductDesc"), { name: goodsName.value })
|
t("pages-user.cart.removeProductDesc", { name: goodsName.value })
|
||||||
);
|
);
|
||||||
function onOpen(title: string) {
|
function onOpen(title: string) {
|
||||||
if (title) {
|
if (title) {
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watch, nextTick } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
import {
|
import {
|
||||||
appMerchantCartDeleteByMerchantIdPost,
|
appMerchantCartDeleteByMerchantIdPost,
|
||||||
appMerchantCartListMerchantPost,
|
appMerchantCartListMerchantPost,
|
||||||
@@ -9,27 +11,35 @@ import {
|
|||||||
appMerchantCartAddCartPost,
|
appMerchantCartAddCartPost,
|
||||||
appMerchantDishDishIdGet,
|
appMerchantDishDishIdGet,
|
||||||
appMerchantDetailMerchantIdGet,
|
appMerchantDetailMerchantIdGet,
|
||||||
appAppointmentTimeUpdateAppointmentTimePost,
|
} from '@/service';
|
||||||
} from "@/service";
|
import {
|
||||||
|
buildReservationTimeUrl,
|
||||||
|
buildDayAppointmentSlot,
|
||||||
|
findNearestDeliveryScheduleDate,
|
||||||
|
saveCheckoutMerchantAppointments,
|
||||||
|
type MerchantAppointmentSlot,
|
||||||
|
} from '@/utils/deliverySchedule';
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import "dayjs/locale/zh-cn";
|
import "dayjs/locale/zh-cn";
|
||||||
import Config from "@/config";
|
import Config from '@/config';
|
||||||
import { useUserStore } from "@/store";
|
import { parseMerchantCartPayload } from '@/utils/utils';
|
||||||
import useEventEmit from "@/hooks/useEventEmit";
|
import { useUserStore } from '@/store';
|
||||||
import { EventEnum } from "@/constant/enums";
|
import useEventEmit from '@/hooks/useEventEmit';
|
||||||
import { onShow } from "@dcloudio/uni-app";
|
import { EventEnum } from '@/constant/enums';
|
||||||
|
import { onShow } from '@dcloudio/uni-app';
|
||||||
|
|
||||||
dayjs.locale("zh-cn");
|
dayjs.locale("zh-cn");
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
import RemoveCart from "./components/remove-cart.vue";
|
import RemoveCart from './components/remove-cart.vue';
|
||||||
import RemoveStore from "./components/remove-store.vue";
|
import RemoveStore from './components/remove-store.vue';
|
||||||
import CartSkeleton from "./components/cart-skeleton.vue";
|
import MultiStoreNotice from "./components/multi-store-notice.vue";
|
||||||
|
import CartSkeleton from './components/cart-skeleton.vue';
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
|
||||||
const removeCartRef = ref<InstanceType<typeof RemoveCart>>();
|
const removeCartRef = ref<InstanceType<typeof RemoveCart>>();
|
||||||
const removeItemRef = ref<InstanceType<typeof RemoveStore>>();
|
const removeItemRef = ref<InstanceType<typeof RemoveStore>>();
|
||||||
|
const isMultiStoreNoticeOpen = ref(false);
|
||||||
interface CartItem {
|
interface CartItem {
|
||||||
id: string;
|
id: string;
|
||||||
dishId: string;
|
dishId: string;
|
||||||
@@ -51,6 +61,8 @@ interface CartMerchant {
|
|||||||
cartTotalPrice: number;
|
cartTotalPrice: number;
|
||||||
merchantCartVoList: CartItem[];
|
merchantCartVoList: CartItem[];
|
||||||
allChecked: boolean;
|
allChecked: boolean;
|
||||||
|
businessHours?: string;
|
||||||
|
deliveryScheduleTimes?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dataList = ref<CartMerchant[]>([]);
|
const dataList = ref<CartMerchant[]>([]);
|
||||||
@@ -74,16 +86,6 @@ const appDisplayName = computed(() => {
|
|||||||
return name || "CHEFLINK";
|
return name || "CHEFLINK";
|
||||||
});
|
});
|
||||||
|
|
||||||
function fillI18nParams(template: string, params: Record<string, string | number>) {
|
|
||||||
let text = template;
|
|
||||||
Object.keys(params).forEach((key) => {
|
|
||||||
const value = String(params[key] ?? "");
|
|
||||||
text = text.replace(new RegExp(`\\{${key}\\}`, "g"), value);
|
|
||||||
text = text.replace(new RegExp(`\\$\\{${key}\\}`, "g"), value);
|
|
||||||
});
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizePriceString(val: unknown) {
|
function normalizePriceString(val: unknown) {
|
||||||
const s = String(val ?? "").trim();
|
const s = String(val ?? "").trim();
|
||||||
if (!/^\d+(\.\d+)?$/.test(s)) return "0";
|
if (!/^\d+(\.\d+)?$/.test(s)) return "0";
|
||||||
@@ -107,8 +109,6 @@ function formatPriceForView(val: unknown) {
|
|||||||
function getCartLineDisplayPrice(item: CartItem) {
|
function getCartLineDisplayPrice(item: CartItem) {
|
||||||
// const memberCent = priceToCentString(item?.memberPrice);
|
// const memberCent = priceToCentString(item?.memberPrice);
|
||||||
// console.log('memberCent', memberCent);
|
// console.log('memberCent', memberCent);
|
||||||
console.log('item?.memberPrice', item?.memberPrice);
|
|
||||||
console.log(userStore.userInfo.userMembershipVo, 'userStore.isMember');
|
|
||||||
if (userStore.userInfo.userMembershipVo!==null) {
|
if (userStore.userInfo.userMembershipVo!==null) {
|
||||||
return item?.memberPrice;
|
return item?.memberPrice;
|
||||||
}
|
}
|
||||||
@@ -157,44 +157,63 @@ const selectedGoodsSubtotal = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const deliveryUnifiedText = computed(() =>
|
const deliveryUnifiedText = computed(() =>
|
||||||
fillI18nParams(t("pages-user.cart.deliveryUnified"), { name: appDisplayName.value })
|
t("pages-user.cart.deliveryUnified", { name: appDisplayName.value })
|
||||||
);
|
);
|
||||||
|
|
||||||
const itemsTotalWithPriceText = computed(() =>
|
const itemsTotalWithPriceText = computed(() =>
|
||||||
fillI18nParams(t("pages-user.cart.itemsTotalWithPrice"), {
|
t("pages-user.cart.itemsTotalWithPrice", {
|
||||||
count: priceData.value.totalQuantity ?? selectedItemQty.value,
|
count: priceData.value.totalQuantity ?? selectedItemQty.value,
|
||||||
amount: selectedGoodsSubtotal.value,
|
amount: selectedGoodsSubtotal.value,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
/** 与地址页、结算页一致:展示用户已保存预约日,无则展示今天(选时间会跳转 reservation-time 并写回接口) */
|
const merchantAppointmentMap = ref<Record<string, MerchantAppointmentSlot>>({});
|
||||||
const deliveryScheduleLabel = computed(() => {
|
const merchantAppointmentDisplay = ref<Record<string, string>>({});
|
||||||
const ap = userStore.appointmentTime as any;
|
const selectingMerchantId = ref<string | null>(null);
|
||||||
|
|
||||||
|
function ensureDefaultMerchantAppointment(merchant: CartMerchant) {
|
||||||
|
const key = String(merchant.id);
|
||||||
|
if (merchantAppointmentMap.value[key]?.startTime) return;
|
||||||
|
|
||||||
|
const nearest = findNearestDeliveryScheduleDate(
|
||||||
|
merchant.deliveryScheduleTimes
|
||||||
|
);
|
||||||
|
if (!nearest) return;
|
||||||
|
|
||||||
|
const slot = buildDayAppointmentSlot(nearest);
|
||||||
|
merchantAppointmentMap.value[key] = slot;
|
||||||
|
merchantAppointmentDisplay.value[key] = dayjs(nearest).format("ddd, MM/DD");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMerchantScheduleLabel(merchantId: string | number) {
|
||||||
|
const key = String(merchantId);
|
||||||
|
if (merchantAppointmentDisplay.value[key]) {
|
||||||
|
return merchantAppointmentDisplay.value[key];
|
||||||
|
}
|
||||||
|
const ap = merchantAppointmentMap.value[key];
|
||||||
if (ap?.startTime) {
|
if (ap?.startTime) {
|
||||||
const start = dayjs(+ap.startTime);
|
const d = dayjs(Number(ap.startTime));
|
||||||
if (start.isValid() && start.isAfter(dayjs().subtract(1, "minute"))) {
|
if (d.isValid()) return d.format("ddd, MM/DD");
|
||||||
return start.format("ddd, MM/DD");
|
|
||||||
}
|
}
|
||||||
}
|
return t("pages-store.checkout.chooseTime");
|
||||||
if (ap?.endTime) {
|
}
|
||||||
const end = dayjs(+ap.endTime);
|
|
||||||
if (end.isValid() && end.isAfter(dayjs().subtract(1, "minute"))) {
|
|
||||||
return end.format("ddd, MM/DD");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dayjs().format("ddd, MM/DD");
|
|
||||||
});
|
|
||||||
|
|
||||||
useEventEmit(EventEnum.CHOOSE_APPOINTMENT_TIME, (data: any) => {
|
useEventEmit(EventEnum.CHOOSE_APPOINTMENT_TIME, (data: any) => {
|
||||||
if (!data?.startTime || !data?.endTime) return;
|
if (!data?.startTime || !data?.endTime) return;
|
||||||
appAppointmentTimeUpdateAppointmentTimePost({
|
const merchantId = String(
|
||||||
body: {
|
data.merchantId || selectingMerchantId.value || ""
|
||||||
|
);
|
||||||
|
if (!merchantId) return;
|
||||||
|
merchantAppointmentMap.value[merchantId] = {
|
||||||
|
date: data.date,
|
||||||
|
timeSlot: data.timeSlot,
|
||||||
startTime: data.startTime,
|
startTime: data.startTime,
|
||||||
endTime: data.endTime,
|
endTime: data.endTime,
|
||||||
} as any,
|
};
|
||||||
}).then(() => {
|
merchantAppointmentDisplay.value[merchantId] = dayjs(data.date).format(
|
||||||
userStore.getAppointmentTime();
|
"ddd, MM/DD"
|
||||||
});
|
);
|
||||||
|
selectingMerchantId.value = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@@ -282,7 +301,7 @@ function confirmRemove() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const batchCheckout = () => {
|
const continueCheckoutWithValidation = () => {
|
||||||
if (selectedCount.value === 0) {
|
if (selectedCount.value === 0) {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
icon: "none",
|
icon: "none",
|
||||||
@@ -299,12 +318,44 @@ const batchCheckout = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
goCheckoutNow();
|
||||||
|
};
|
||||||
|
|
||||||
|
const batchCheckout = () => {
|
||||||
|
continueCheckoutWithValidation();
|
||||||
|
};
|
||||||
|
|
||||||
|
const goCheckoutNow = () => {
|
||||||
|
const selectedMerchants = dataList.value.filter((m) =>
|
||||||
|
m.merchantCartVoList.some((item) => item.checked)
|
||||||
|
);
|
||||||
|
for (const m of selectedMerchants) {
|
||||||
|
const ap = merchantAppointmentMap.value[String(m.id)];
|
||||||
|
if (!ap?.startTime || !ap?.endTime) {
|
||||||
|
uni.showToast({
|
||||||
|
title: t("pages-store.checkout.chooseTimeForStore", {
|
||||||
|
name: m.merchantName || "",
|
||||||
|
}),
|
||||||
|
icon: "none",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
saveCheckoutMerchantAppointments(merchantAppointmentMap.value);
|
||||||
const cartIds = selectedCartItems.value.map((item) => item.id);
|
const cartIds = selectedCartItems.value.map((item) => item.id);
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: `/pages-store/pages/order/checkout?cartIds=${cartIds.join(",")}&type=batch`,
|
url: `/pages-store/pages/order/checkout?cartIds=${cartIds.join(",")}&type=batch`,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const confirmMultiStoreNotice = () => {
|
||||||
|
continueCheckoutWithValidation();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMultiStoreNoticeClose = () => {
|
||||||
|
isMultiStoreNoticeOpen.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
const goToHome = () => {
|
const goToHome = () => {
|
||||||
uni.switchTab({
|
uni.switchTab({
|
||||||
url: "/pages/home/index",
|
url: "/pages/home/index",
|
||||||
@@ -436,34 +487,35 @@ function handleRemoveItemPopupClose() {
|
|||||||
delItemData.value = null;
|
delItemData.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onScheduleTap() {
|
async function onMerchantScheduleTap(merchant: CartMerchant) {
|
||||||
if (!userStore.isLogin) {
|
if (!userStore.isLogin) {
|
||||||
uni.showToast({ title: t("common.pleaseLogin"), icon: "none" });
|
uni.showToast({ title: t("common.pleaseLogin"), icon: "none" });
|
||||||
setTimeout(() => uni.navigateTo({ url: Config.loginPath }), 400);
|
setTimeout(() => uni.navigateTo({ url: Config.loginPath }), 400);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!dataList.value.length) return;
|
selectingMerchantId.value = String(merchant.id);
|
||||||
|
let businessHours = merchant.businessHours;
|
||||||
const first = dataList.value[0];
|
let schedule = merchant.deliveryScheduleTimes;
|
||||||
let businessHours = (first as any).businessHours as string | undefined;
|
if (!businessHours || !schedule) {
|
||||||
if (!businessHours) {
|
|
||||||
try {
|
try {
|
||||||
const detail: any = await appMerchantDetailMerchantIdGet({
|
const detail: any = await appMerchantDetailMerchantIdGet({
|
||||||
params: { merchantId: first.id as any },
|
params: { merchantId: merchant.id as any },
|
||||||
});
|
});
|
||||||
businessHours = detail.data?.businessHours;
|
businessHours = detail.data?.businessHours ?? businessHours;
|
||||||
|
schedule = detail.data?.deliveryScheduleTimes ?? schedule;
|
||||||
|
merchant.businessHours = businessHours;
|
||||||
|
merchant.deliveryScheduleTimes = schedule;
|
||||||
} catch {
|
} catch {
|
||||||
businessHours = "";
|
/* ignore */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
uni.navigateTo({
|
||||||
const url = businessHours
|
url: buildReservationTimeUrl({
|
||||||
? `/pages/address/reservation-time?storeBusinessHours=${encodeURIComponent(
|
merchantId: merchant.id,
|
||||||
businessHours
|
deliveryScheduleTimes: schedule,
|
||||||
)}`
|
businessHours,
|
||||||
: "/pages/address/reservation-time";
|
}),
|
||||||
|
});
|
||||||
uni.navigateTo({ url });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const addingRecommendId = ref<string | number | null>(null);
|
const addingRecommendId = ref<string | number | null>(null);
|
||||||
@@ -566,14 +618,14 @@ onShow(() => {
|
|||||||
userStore.getAppointmentTime();
|
userStore.getAppointmentTime();
|
||||||
// 从结算页返回时强制刷新购物车,避免已结算商品残留
|
// 从结算页返回时强制刷新购物车,避免已结算商品残留
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
getCartList();
|
getCartList({ showMultiStoreNotice: true });
|
||||||
} else {
|
} else {
|
||||||
dataList.value = [];
|
dataList.value = [];
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function getCartList() {
|
async function getCartList(options?: { showMultiStoreNotice?: boolean }) {
|
||||||
try {
|
try {
|
||||||
const res: any = await appMerchantCartListMerchantPost({});
|
const res: any = await appMerchantCartListMerchantPost({});
|
||||||
const merchants = res.data;
|
const merchants = res.data;
|
||||||
@@ -589,10 +641,13 @@ async function getCartList() {
|
|||||||
merchantId: merchant.id,
|
merchantId: merchant.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const payload = parseMerchantCartPayload(cartRes?.data);
|
||||||
return {
|
return {
|
||||||
...merchant,
|
...merchant,
|
||||||
|
deliveryScheduleTimes:
|
||||||
|
payload.deliveryScheduleTimes ?? merchant.deliveryScheduleTimes,
|
||||||
allChecked: false,
|
allChecked: false,
|
||||||
merchantCartVoList: (cartRes.data || []).map((item: any) => ({
|
merchantCartVoList: payload.items.map((item: any) => ({
|
||||||
...item,
|
...item,
|
||||||
checked: false,
|
checked: false,
|
||||||
})),
|
})),
|
||||||
@@ -608,7 +663,13 @@ async function getCartList() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
dataList.value = await Promise.all(merchantPromises);
|
dataList.value = await Promise.all(merchantPromises);
|
||||||
|
dataList.value.forEach((m) => ensureDefaultMerchantAppointment(m));
|
||||||
syncItemCountCaches();
|
syncItemCountCaches();
|
||||||
|
if (options?.showMultiStoreNotice && dataList.value.length > 1) {
|
||||||
|
nextTick(() => {
|
||||||
|
isMultiStoreNoticeOpen.value = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("获取购物车列表失败", error);
|
console.error("获取购物车列表失败", error);
|
||||||
dataList.value = [];
|
dataList.value = [];
|
||||||
@@ -667,11 +728,7 @@ async function getCartList() {
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="cart-delivery-row mt-28rpx">
|
<view class="cart-delivery-row mt-28rpx">
|
||||||
<view class="cart-date-pill" @click="onScheduleTap">
|
<view class="cart-price-summary cart-price-summary--full">
|
||||||
<text>{{ deliveryScheduleLabel }}</text>
|
|
||||||
<text class="cart-date-pill-arr">›</text>
|
|
||||||
</view>
|
|
||||||
<view class="cart-price-summary">
|
|
||||||
<text class="cart-summary-line1">{{
|
<text class="cart-summary-line1">{{
|
||||||
itemsTotalWithPriceText
|
itemsTotalWithPriceText
|
||||||
}}</text>
|
}}</text>
|
||||||
@@ -754,6 +811,16 @@ async function getCartList() {
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<view class="cart-merchant-schedule-row">
|
||||||
|
<view
|
||||||
|
class="cart-date-pill"
|
||||||
|
@click="onMerchantScheduleTap(merchant)"
|
||||||
|
>
|
||||||
|
<text>{{ getMerchantScheduleLabel(merchant.id) }}</text>
|
||||||
|
<text class="cart-date-pill-arr">›</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<!-- 商品行 -->
|
<!-- 商品行 -->
|
||||||
<view
|
<view
|
||||||
v-for="(item, itemIndex) in merchant.merchantCartVoList"
|
v-for="(item, itemIndex) in merchant.merchantCartVoList"
|
||||||
@@ -881,7 +948,7 @@ async function getCartList() {
|
|||||||
</z-paging>
|
</z-paging>
|
||||||
|
|
||||||
<view
|
<view
|
||||||
v-if="dataList.length > 0"
|
v-if="dataList.length > 0 && !isMultiStoreNoticeOpen"
|
||||||
class="cart-footer-wrap"
|
class="cart-footer-wrap"
|
||||||
>
|
>
|
||||||
<view class="cart-footer-float">
|
<view class="cart-footer-float">
|
||||||
@@ -928,6 +995,11 @@ async function getCartList() {
|
|||||||
@confirm="handleRemoveItem"
|
@confirm="handleRemoveItem"
|
||||||
@close="handleRemoveItemPopupClose"
|
@close="handleRemoveItemPopupClose"
|
||||||
/>
|
/>
|
||||||
|
<multi-store-notice
|
||||||
|
v-model="isMultiStoreNoticeOpen"
|
||||||
|
@confirm="confirmMultiStoreNotice"
|
||||||
|
@close="handleMultiStoreNoticeClose"
|
||||||
|
></multi-store-notice>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -997,6 +1069,10 @@ async function getCartList() {
|
|||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cart-merchant-schedule-row {
|
||||||
|
padding: 0 24rpx 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
.cart-price-summary {
|
.cart-price-summary {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -1004,6 +1080,10 @@ async function getCartList() {
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cart-price-summary--full {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.cart-summary-line1 {
|
.cart-summary-line1 {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
@@ -1205,7 +1285,7 @@ async function getCartList() {
|
|||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
z-index: 999;
|
z-index: 998;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1279,6 +1359,7 @@ async function getCartList() {
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
<style>
|
<style>
|
||||||
page {
|
page {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import RemoveStore from "./components/remove-store.vue";
|
|||||||
import StoreCartSkeleton from "./components/store-cart-skeleton.vue";
|
import StoreCartSkeleton from "./components/store-cart-skeleton.vue";
|
||||||
import { onBeforeUnmount, ref } from "vue";
|
import { onBeforeUnmount, ref } from "vue";
|
||||||
import {useConfigStore} from "@/store";
|
import {useConfigStore} from "@/store";
|
||||||
|
import { parseMerchantCartPayload } from "@/utils/utils";
|
||||||
const configStore = useConfigStore();
|
const configStore = useConfigStore();
|
||||||
// 骨架屏加载状态
|
// 骨架屏加载状态
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
@@ -219,8 +220,9 @@ function getCartInfo() {
|
|||||||
}
|
}
|
||||||
}).then((res: any)=> {
|
}).then((res: any)=> {
|
||||||
console.log('购物车列表', res)
|
console.log('购物车列表', res)
|
||||||
cartDataList.value = res.data
|
const items = parseMerchantCartPayload(res?.data).items as MerchantCartVo[]
|
||||||
syncItemCountCache(res.data || [])
|
cartDataList.value = items
|
||||||
|
syncItemCountCache(items)
|
||||||
|
|
||||||
// 购物车有菜品,查询菜品会员折扣价
|
// 购物车有菜品,查询菜品会员折扣价
|
||||||
if(cartDataList.value.length > 0) {
|
if(cartDataList.value.length > 0) {
|
||||||
|
|||||||
@@ -42,20 +42,10 @@ function formatCouponDetail(item: any) {
|
|||||||
return t('pages-store.store.discount')
|
return t('pages-store.store.discount')
|
||||||
}
|
}
|
||||||
|
|
||||||
function fillI18nParams(template: string, params: Record<string, string | number>) {
|
|
||||||
let text = template
|
|
||||||
Object.keys(params).forEach((key) => {
|
|
||||||
const value = String(params[key] ?? '')
|
|
||||||
text = text.replace(new RegExp(`\\{${key}\\}`, 'g'), value)
|
|
||||||
text = text.replace(new RegExp(`\\$\\{${key}\\}`, 'g'), value)
|
|
||||||
})
|
|
||||||
return text
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatCouponMerchantText(item: any) {
|
function formatCouponMerchantText(item: any) {
|
||||||
const name = String(item?.merchantVo?.merchantName || '').trim()
|
const name = String(item?.merchantVo?.merchantName || '').trim()
|
||||||
if (name) {
|
if (name) {
|
||||||
return fillI18nParams(t('pages-user.coupon.merchant-only'), { name })
|
return t('pages-user.coupon.merchant-only', { name })
|
||||||
}
|
}
|
||||||
return item?.snapshotMerchantId
|
return item?.snapshotMerchantId
|
||||||
? t('pages-user.coupon.merchant-specific')
|
? t('pages-user.coupon.merchant-specific')
|
||||||
|
|||||||
@@ -276,6 +276,12 @@
|
|||||||
{
|
{
|
||||||
"path": "pages/dishes/index"
|
"path": "pages/dishes/index"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/dishes/quick-topic",
|
||||||
|
"style": {
|
||||||
|
"onReachBottomDistance": 80
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/home-store/index"
|
"path": "pages/home-store/index"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,75 +1,54 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {EventEnum} from "@/constant/enums";
|
import { EventEnum } from "@/constant/enums";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
// 店铺的营业时间(示例:周一到周五营业,周六周日不营业)
|
|
||||||
// 测试用的营业时间字符串
|
|
||||||
// MONDAY/TUESDAY/WEDNESDAY 09:00-18:00;THURSDAY/FRIDAY 08:00-09:00; // 周一到周五9点到18点,周四到周五8点到9点
|
|
||||||
// MONDAY/TUESDAY/WEDNESDAY 09:00-18:00;THURSDAY/FRIDAY 08:00-12:00;SATURDAY/SUNDAY 10:00-20:00 // 周六周日10点到20点
|
|
||||||
// MONDAY/TUESDAY/WEDNESDAY 09:00-16:00
|
|
||||||
// MONDAY 09:00-18:00;TUESDAY 09:00-10:00;WEDNESDAY 09:00-18:00;THURSDAY 09:00-18:00;FRIDAY 08:00-09:00
|
|
||||||
const storeBusinessHours = ref('');
|
const storeBusinessHours = ref('');
|
||||||
// 是否仅选择日期(当前需求:只选哪一天配送,不选具体时间段)
|
|
||||||
const onlySelectDay = ref(true);
|
const onlySelectDay = ref(true);
|
||||||
|
|
||||||
// 业务规则:只能预约周一 / 周四 / 周五
|
import { parseDeliveryScheduleTimes, isDeliveryScheduleDay } from '@/utils/deliverySchedule';
|
||||||
// JS 中:0-周日 1-周一 ... 4-周四 5-周五
|
|
||||||
const allowedWeekdays = [1, 4, 5];
|
const allowedWeekdays = ref<number[]>([]);
|
||||||
const isAllowedDay = (date: Date): boolean => {
|
const isAllowedDay = (date: Date): boolean => {
|
||||||
const dayIndex = date.getDay();
|
return isDeliveryScheduleDay(date, allowedWeekdays.value);
|
||||||
return allowedWeekdays.includes(dayIndex);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 解析商家营业时间的接口
|
|
||||||
interface BusinessHours {
|
interface BusinessHours {
|
||||||
days: string[]; // 营业的星期几
|
days: string[];
|
||||||
startTime: string; // 开始时间 HH:mm
|
startTime: string;
|
||||||
endTime: string; // 结束时间 HH:mm
|
endTime: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析商家营业时间字符串
|
|
||||||
* @param businessHoursStr 营业时间字符串,支持多种格式:
|
|
||||||
* - MONDAY/TUESDAY/WEDNESDAY 09:00-18:00;THURSDAY/FRIDAY/SATURDAY/SUNDAY 10:00-20:00
|
|
||||||
* - MONDAY/TUESDAY/WEDNESDAY 09:00-18:00
|
|
||||||
* - MONDAY 09:00-18:00;TUESDAY 09:00-10:00;WEDNESDAY 09:00-18:00
|
|
||||||
* @returns 解析后的营业时间数组
|
|
||||||
*/
|
|
||||||
const parseBusinessHours = (businessHoursStr: string): BusinessHours[] => {
|
const parseBusinessHours = (businessHoursStr: string): BusinessHours[] => {
|
||||||
if (!businessHoursStr) return [];
|
if (!businessHoursStr) return [];
|
||||||
|
|
||||||
const businessHours: BusinessHours[] = [];
|
const businessHours: BusinessHours[] = [];
|
||||||
// 使用分号分割不同的营业时间段
|
|
||||||
const segments = businessHoursStr.split(";");
|
const segments = businessHoursStr.split(";");
|
||||||
|
|
||||||
segments.forEach((segment) => {
|
segments.forEach((segment) => {
|
||||||
const trimmedSegment = segment.trim();
|
const trimmedSegment = segment.trim();
|
||||||
if (!trimmedSegment) return;
|
if (!trimmedSegment) return;
|
||||||
|
|
||||||
// 使用空格分割星期几和时间
|
|
||||||
const parts = trimmedSegment.split(" ");
|
const parts = trimmedSegment.split(" ");
|
||||||
if (parts.length !== 2) return;
|
if (parts.length !== 2) return;
|
||||||
|
|
||||||
const dayStr = parts[0].trim().toUpperCase();
|
const dayStr = parts[0].trim().toUpperCase();
|
||||||
const timeStr = parts[1];
|
const timeStr = parts[1];
|
||||||
|
|
||||||
// 解析时间范围
|
|
||||||
const timeRange = timeStr.split("-");
|
const timeRange = timeStr.split("-");
|
||||||
if (timeRange.length !== 2) return;
|
if (timeRange.length !== 2) return;
|
||||||
|
|
||||||
const startTime = timeRange[0].trim();
|
const startTime = timeRange[0].trim();
|
||||||
const endTime = timeRange[1].trim();
|
const endTime = timeRange[1].trim();
|
||||||
|
|
||||||
// 按斜杠分割星期几,支持 MONDAY/TUESDAY/WEDNESDAY 格式
|
|
||||||
const days = dayStr
|
const days = dayStr
|
||||||
.split("/")
|
.split("/")
|
||||||
.map((day) => day.trim())
|
.map((day) => day.trim())
|
||||||
.filter((day) => day);
|
.filter((day) => day);
|
||||||
|
|
||||||
businessHours.push({
|
businessHours.push({
|
||||||
days, // 支持多个星期几共享同一营业时间
|
days,
|
||||||
startTime,
|
startTime,
|
||||||
endTime,
|
endTime,
|
||||||
});
|
});
|
||||||
@@ -78,17 +57,11 @@ const parseBusinessHours = (businessHoursStr: string): BusinessHours[] => {
|
|||||||
return businessHours;
|
return businessHours;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取指定日期的营业时间
|
|
||||||
* @param date 日期
|
|
||||||
* @returns 营业时间对象,如果不营业返回null
|
|
||||||
*/
|
|
||||||
const getBusinessHoursForDate = (date: Date): BusinessHours | null => {
|
const getBusinessHoursForDate = (date: Date): BusinessHours | null => {
|
||||||
if (!storeBusinessHours.value) return null;
|
if (!storeBusinessHours.value) return null;
|
||||||
|
|
||||||
const businessHours = parseBusinessHours(storeBusinessHours.value);
|
const businessHours = parseBusinessHours(storeBusinessHours.value);
|
||||||
|
|
||||||
// 获取星期几的英文名称(确保是大写)(不要国际化导致判断失效)
|
|
||||||
const dayNames = [
|
const dayNames = [
|
||||||
"SUNDAY",
|
"SUNDAY",
|
||||||
"MONDAY",
|
"MONDAY",
|
||||||
@@ -100,44 +73,29 @@ const getBusinessHoursForDate = (date: Date): BusinessHours | null => {
|
|||||||
];
|
];
|
||||||
const dayName = dayNames[date.getDay()];
|
const dayName = dayNames[date.getDay()];
|
||||||
|
|
||||||
// 查找包含当前星期几的营业时间
|
|
||||||
return businessHours.find((hours) => hours.days.includes(dayName)) || null;
|
return businessHours.find((hours) => hours.days.includes(dayName)) || null;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查日期是否营业
|
|
||||||
* @param date 日期
|
|
||||||
* @returns 是否营业
|
|
||||||
*/
|
|
||||||
const isDateOpen = (date: Date): boolean => {
|
const isDateOpen = (date: Date): boolean => {
|
||||||
if (!storeBusinessHours.value) return true; // 如果没有营业时间限制,默认营业
|
if (!storeBusinessHours.value) return true;
|
||||||
return getBusinessHoursForDate(date) !== null;
|
return getBusinessHoursForDate(date) !== null;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查日期是否应该显示为可选择状态(营业且有可用时间段)
|
|
||||||
* @param date 日期
|
|
||||||
* @returns 是否可选择
|
|
||||||
*/
|
|
||||||
const isDateSelectable = (date: Date): boolean => {
|
const isDateSelectable = (date: Date): boolean => {
|
||||||
// 新增:限制只能预约周一 / 周四 / 周五
|
|
||||||
if (!isAllowedDay(date)) {
|
if (!isAllowedDay(date)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果只选日期模式,营业即可选择
|
|
||||||
if (onlySelectDay.value) {
|
if (onlySelectDay.value) {
|
||||||
if (!storeBusinessHours.value) return true;
|
if (!storeBusinessHours.value) return true;
|
||||||
return isDateOpen(date);
|
return isDateOpen(date);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 原逻辑:需要营业且有可用时间段
|
if (!storeBusinessHours.value) return true;
|
||||||
if (!storeBusinessHours.value) return true; // 如果没有营业时间限制,默认可选择
|
|
||||||
if (!isDateOpen(date)) return false;
|
if (!isDateOpen(date)) return false;
|
||||||
return hasAvailableTimeSlots(date);
|
return hasAvailableTimeSlots(date);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 生成未来 7 天:设计稿为「本周」5 个圆 +「下周」2 个圆
|
|
||||||
const dateOptions = computed(() => {
|
const dateOptions = computed(() => {
|
||||||
const dates: Date[] = [];
|
const dates: Date[] = [];
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
@@ -154,10 +112,8 @@ const dateOptions = computed(() => {
|
|||||||
const thisWeekDates = computed(() => dateOptions.value.slice(0, 5));
|
const thisWeekDates = computed(() => dateOptions.value.slice(0, 5));
|
||||||
const nextWeekDates = computed(() => dateOptions.value.slice(5, 7));
|
const nextWeekDates = computed(() => dateOptions.value.slice(5, 7));
|
||||||
|
|
||||||
// 状态管理 - 初始化为第一个营业日期
|
|
||||||
const selectedDate = ref<Date>();
|
const selectedDate = ref<Date>();
|
||||||
|
|
||||||
// 检查指定时间是否在营业时间内
|
|
||||||
const isTimeInBusinessHours = (
|
const isTimeInBusinessHours = (
|
||||||
hour: number,
|
hour: number,
|
||||||
minute: number,
|
minute: number,
|
||||||
@@ -169,35 +125,28 @@ const isTimeInBusinessHours = (
|
|||||||
return timeStr >= businessHours.startTime && timeStr <= businessHours.endTime;
|
return timeStr >= businessHours.startTime && timeStr <= businessHours.endTime;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 检查指定日期是否有可用时间段
|
|
||||||
const hasAvailableTimeSlots = (date: Date): boolean => {
|
const hasAvailableTimeSlots = (date: Date): boolean => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const currentHour = now.getHours();
|
const currentHour = now.getHours();
|
||||||
const currentMinute = now.getMinutes();
|
const currentMinute = now.getMinutes();
|
||||||
|
|
||||||
// 获取指定日期的营业时间
|
|
||||||
const businessHours = getBusinessHoursForDate(date);
|
const businessHours = getBusinessHoursForDate(date);
|
||||||
|
|
||||||
// 如果没有营业时间,返回false
|
|
||||||
if (!businessHours) {
|
if (!businessHours) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析营业时间的开始和结束时间
|
|
||||||
const [startHour, startMinute] = businessHours.startTime
|
const [startHour, startMinute] = businessHours.startTime
|
||||||
.split(":")
|
.split(":")
|
||||||
.map(Number);
|
.map(Number);
|
||||||
const [endHour, endMinute] = businessHours.endTime.split(":").map(Number);
|
const [endHour, endMinute] = businessHours.endTime.split(":").map(Number);
|
||||||
|
|
||||||
// 检查是否有可用时间段
|
|
||||||
for (let hour = startHour; hour <= endHour; hour++) {
|
for (let hour = startHour; hour <= endHour; hour++) {
|
||||||
for (let minute = 0; minute < 60; minute += 30) {
|
for (let minute = 0; minute < 60; minute += 30) {
|
||||||
// 检查时间段开始时间是否在营业时间内
|
|
||||||
if (!isTimeInBusinessHours(hour, minute, businessHours)) {
|
if (!isTimeInBusinessHours(hour, minute, businessHours)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算时间段结束时间
|
|
||||||
let nextHour = hour;
|
let nextHour = hour;
|
||||||
let nextMinute = minute + 30;
|
let nextMinute = minute + 30;
|
||||||
if (nextMinute >= 60) {
|
if (nextMinute >= 60) {
|
||||||
@@ -205,7 +154,6 @@ const hasAvailableTimeSlots = (date: Date): boolean => {
|
|||||||
nextMinute = 0;
|
nextMinute = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查时间段结束时间是否超出营业时间
|
|
||||||
if (!isTimeInBusinessHours(nextHour, nextMinute, businessHours)) {
|
if (!isTimeInBusinessHours(nextHour, nextMinute, businessHours)) {
|
||||||
if (
|
if (
|
||||||
nextHour > endHour ||
|
nextHour > endHour ||
|
||||||
@@ -216,12 +164,10 @@ const hasAvailableTimeSlots = (date: Date): boolean => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 避免生成开始时间和结束时间相同的无效时间段
|
|
||||||
if (hour === nextHour && minute === nextMinute) {
|
if (hour === nextHour && minute === nextMinute) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果是今天,过滤掉已经过去的时间
|
|
||||||
if (date.toDateString() === now.toDateString()) {
|
if (date.toDateString() === now.toDateString()) {
|
||||||
if (
|
if (
|
||||||
hour < currentHour ||
|
hour < currentHour ||
|
||||||
@@ -231,7 +177,6 @@ const hasAvailableTimeSlots = (date: Date): boolean => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果能到这里,说明有可用时间段
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -239,69 +184,55 @@ const hasAvailableTimeSlots = (date: Date): boolean => {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 初始化选中日期为第一个有可用时间段的营业日期
|
const userPickedDate = ref(false);
|
||||||
const initializeSelectedDate = () => {
|
|
||||||
if (onlySelectDay.value) {
|
|
||||||
// 仅选日期模式:选择第一个“允许预约且营业”的日期(或第一个允许的日期)
|
|
||||||
const firstOpen = dateOptions.value.find(
|
|
||||||
(d) => isAllowedDay(d) && isDateOpen(d)
|
|
||||||
);
|
|
||||||
const firstAllowed = firstOpen || dateOptions.value.find((d) => isAllowedDay(d));
|
|
||||||
selectedDate.value = firstAllowed || dateOptions.value[0];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 非仅选日期模式:保留原逻辑
|
|
||||||
for (const date of dateOptions.value) {
|
|
||||||
if (isDateSelectable(date)) {
|
|
||||||
selectedDate.value = date;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/** 从今天起查找最近可配送日(配送日 + 营业时间) */
|
||||||
|
const findNearestDeliverableDate = (maxDays = 30): Date | null => {
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const nextBusinessDate = findNextBusinessDate(today);
|
today.setHours(0, 0, 0, 0);
|
||||||
if (nextBusinessDate) {
|
for (let i = 0; i <= maxDays; i++) {
|
||||||
selectedDate.value = nextBusinessDate;
|
const d = new Date(today);
|
||||||
nextTick(() => {
|
d.setDate(today.getDate() + i);
|
||||||
uni.showToast({
|
if (isDateSelectable(d)) {
|
||||||
title: t('pages.address.reservationTime.currentTimeExpired'),
|
return d;
|
||||||
icon: "none",
|
|
||||||
duration: 2000,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
selectedDate.value = dateOptions.value[0];
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const initializeSelectedDate = (resetUserPick = false) => {
|
||||||
|
if (resetUserPick) {
|
||||||
|
userPickedDate.value = false;
|
||||||
|
}
|
||||||
|
if (userPickedDate.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nearest = findNearestDeliverableDate(30);
|
||||||
|
if (nearest) {
|
||||||
|
selectedDate.value = nearest;
|
||||||
|
selectedTimeSlot.value = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedDate.value = dateOptions.value[0] ?? new Date();
|
||||||
|
selectedTimeSlot.value = "";
|
||||||
};
|
};
|
||||||
|
|
||||||
// 监听dateOptions变化,初始化选中日期
|
|
||||||
watch(
|
watch(
|
||||||
dateOptions,
|
() => [...allowedWeekdays.value],
|
||||||
() => {
|
() => {
|
||||||
if (!selectedDate.value) {
|
|
||||||
initializeSelectedDate();
|
initializeSelectedDate();
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// 监听营业时间字符串变化,重新初始化选中日期
|
watch(storeBusinessHours, () => {
|
||||||
watch(
|
|
||||||
storeBusinessHours,
|
|
||||||
() => {
|
|
||||||
if (storeBusinessHours.value) {
|
|
||||||
initializeSelectedDate();
|
initializeSelectedDate();
|
||||||
}
|
});
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
);
|
|
||||||
const selectedTimeSlot = ref<string>("");
|
const selectedTimeSlot = ref<string>("");
|
||||||
|
|
||||||
/** 圆圈内上行:星期(与全局 dayjs 语言一致) */
|
|
||||||
const formatWeekdayCircle = (date: Date) => dayjs(date).format("dddd");
|
const formatWeekdayCircle = (date: Date) => dayjs(date).format("dddd");
|
||||||
|
|
||||||
/** 圆圈内下行:MM/DD;不可选时显示文案 */
|
|
||||||
const formatCircleSubLine = (date: Date) => {
|
const formatCircleSubLine = (date: Date) => {
|
||||||
if (!isDateSelectable(date)) {
|
if (!isDateSelectable(date)) {
|
||||||
return t("pages.address.reservationTime.notAvailable");
|
return t("pages.address.reservationTime.notAvailable");
|
||||||
@@ -309,34 +240,21 @@ const formatCircleSubLine = (date: Date) => {
|
|||||||
return dayjs(date).format("MM/DD");
|
return dayjs(date).format("MM/DD");
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查时间是否在营业时间内
|
|
||||||
* @param hour 小时
|
|
||||||
* @param minute 分钟
|
|
||||||
* @param businessHours 营业时间对象
|
|
||||||
* @returns 是否在营业时间内
|
|
||||||
*/
|
|
||||||
|
|
||||||
// 生成时间段选项(根据商家营业时间过滤)
|
|
||||||
const timeSlots = computed(() => {
|
const timeSlots = computed(() => {
|
||||||
const slots: string[] = [];
|
const slots: string[] = [];
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const currentHour = now.getHours();
|
const currentHour = now.getHours();
|
||||||
const currentMinute = now.getMinutes();
|
const currentMinute = now.getMinutes();
|
||||||
|
|
||||||
// 如果还没有选中日期,返回空数组
|
|
||||||
if (!selectedDate.value) {
|
if (!selectedDate.value) {
|
||||||
return slots;
|
return slots;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取选中日期的营业时间
|
|
||||||
const businessHours = getBusinessHoursForDate(selectedDate.value);
|
const businessHours = getBusinessHoursForDate(selectedDate.value);
|
||||||
|
|
||||||
// 如果没有营业时间限制,使用原有逻辑(0-24小时)
|
|
||||||
if (!businessHours) {
|
if (!businessHours) {
|
||||||
for (let hour = 0; hour < 24; hour++) {
|
for (let hour = 0; hour < 24; hour++) {
|
||||||
for (let minute = 0; minute < 60; minute += 30) {
|
for (let minute = 0; minute < 60; minute += 30) {
|
||||||
// 如果是今天,过滤掉已经过去的时间
|
|
||||||
if (selectedDate.value.toDateString() === now.toDateString()) {
|
if (selectedDate.value.toDateString() === now.toDateString()) {
|
||||||
if (
|
if (
|
||||||
hour < currentHour ||
|
hour < currentHour ||
|
||||||
@@ -361,21 +279,17 @@ const timeSlots = computed(() => {
|
|||||||
return slots;
|
return slots;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析营业时间的开始和结束时间
|
|
||||||
const [startHour, startMinute] = businessHours.startTime
|
const [startHour, startMinute] = businessHours.startTime
|
||||||
.split(":")
|
.split(":")
|
||||||
.map(Number);
|
.map(Number);
|
||||||
const [endHour, endMinute] = businessHours.endTime.split(":").map(Number);
|
const [endHour, endMinute] = businessHours.endTime.split(":").map(Number);
|
||||||
|
|
||||||
// 生成营业时间内的时间段
|
|
||||||
for (let hour = startHour; hour <= endHour; hour++) {
|
for (let hour = startHour; hour <= endHour; hour++) {
|
||||||
for (let minute = 0; minute < 60; minute += 30) {
|
for (let minute = 0; minute < 60; minute += 30) {
|
||||||
// 检查时间段开始时间是否在营业时间内
|
|
||||||
if (!isTimeInBusinessHours(hour, minute, businessHours)) {
|
if (!isTimeInBusinessHours(hour, minute, businessHours)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算时间段结束时间
|
|
||||||
let nextHour = hour;
|
let nextHour = hour;
|
||||||
let nextMinute = minute + 30;
|
let nextMinute = minute + 30;
|
||||||
if (nextMinute >= 60) {
|
if (nextMinute >= 60) {
|
||||||
@@ -383,9 +297,7 @@ const timeSlots = computed(() => {
|
|||||||
nextMinute = 0;
|
nextMinute = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查时间段结束时间是否超出营业时间
|
|
||||||
if (!isTimeInBusinessHours(nextHour, nextMinute, businessHours)) {
|
if (!isTimeInBusinessHours(nextHour, nextMinute, businessHours)) {
|
||||||
// 如果结束时间超出营业时间,但开始时间在营业时间内,则调整结束时间为营业结束时间
|
|
||||||
if (
|
if (
|
||||||
nextHour > endHour ||
|
nextHour > endHour ||
|
||||||
(nextHour === endHour && nextMinute > endMinute)
|
(nextHour === endHour && nextMinute > endMinute)
|
||||||
@@ -395,12 +307,10 @@ const timeSlots = computed(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 避免生成开始时间和结束时间相同的无效时间段(如 18:00 - 18:00)
|
|
||||||
if (hour === nextHour && minute === nextMinute) {
|
if (hour === nextHour && minute === nextMinute) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果是今天,过滤掉已经过去的时间
|
|
||||||
if (selectedDate.value.toDateString() === now.toDateString()) {
|
if (selectedDate.value.toDateString() === now.toDateString()) {
|
||||||
if (
|
if (
|
||||||
hour < currentHour ||
|
hour < currentHour ||
|
||||||
@@ -423,11 +333,9 @@ const timeSlots = computed(() => {
|
|||||||
return slots;
|
return slots;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 监听时间段变化,如果当前选中日期没有可用时间段,自动选择下一个营业日期
|
|
||||||
watch(timeSlots, (newSlots) => {
|
watch(timeSlots, (newSlots) => {
|
||||||
if (onlySelectDay.value) return; // 仅选日期模式不需要处理时间段
|
if (onlySelectDay.value) return;
|
||||||
if (selectedDate.value && newSlots.length === 0) {
|
if (selectedDate.value && newSlots.length === 0) {
|
||||||
// 当前选中日期没有可用时间段,寻找下一个营业日期
|
|
||||||
const nextBusinessDate = findNextBusinessDate(selectedDate.value);
|
const nextBusinessDate = findNextBusinessDate(selectedDate.value);
|
||||||
if (nextBusinessDate) {
|
if (nextBusinessDate) {
|
||||||
selectedDate.value = nextBusinessDate;
|
selectedDate.value = nextBusinessDate;
|
||||||
@@ -441,14 +349,12 @@ watch(timeSlots, (newSlots) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 寻找下一个有可用时间段的营业日期
|
|
||||||
const findNextBusinessDate = (currentDate: Date): Date | null => {
|
const findNextBusinessDate = (currentDate: Date): Date | null => {
|
||||||
const maxDays = 30; // 最多向前查找30天
|
const maxDays = 30;
|
||||||
for (let i = 1; i <= maxDays; i++) {
|
for (let i = 1; i <= maxDays; i++) {
|
||||||
const nextDate = new Date(currentDate);
|
const nextDate = new Date(currentDate);
|
||||||
nextDate.setDate(currentDate.getDate() + i);
|
nextDate.setDate(currentDate.getDate() + i);
|
||||||
|
|
||||||
// 检查是否在dateOptions范围内
|
|
||||||
const isInRange = dateOptions.value.some((date) =>
|
const isInRange = dateOptions.value.some((date) =>
|
||||||
dayjs(date).isSame(dayjs(nextDate), "day")
|
dayjs(date).isSame(dayjs(nextDate), "day")
|
||||||
);
|
);
|
||||||
@@ -465,9 +371,7 @@ const findNextBusinessDate = (currentDate: Date): Date | null => {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 选择日期
|
|
||||||
const selectDate = (date: Date) => {
|
const selectDate = (date: Date) => {
|
||||||
// 检查日期是否可选择,如果不可选择则不允许选择
|
|
||||||
if (!isDateSelectable(date)) {
|
if (!isDateSelectable(date)) {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: t('pages.address.reservationTime.dateNotSelectable'),
|
title: t('pages.address.reservationTime.dateNotSelectable'),
|
||||||
@@ -476,12 +380,11 @@ const selectDate = (date: Date) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
userPickedDate.value = true;
|
||||||
selectedDate.value = date;
|
selectedDate.value = date;
|
||||||
// 选择新日期后,清空已选择的时间段
|
|
||||||
selectedTimeSlot.value = "";
|
selectedTimeSlot.value = "";
|
||||||
};
|
};
|
||||||
|
|
||||||
// 选择时间段
|
|
||||||
const selectTimeSlot = (timeSlot: string) => {
|
const selectTimeSlot = (timeSlot: string) => {
|
||||||
selectedTimeSlot.value = timeSlot;
|
selectedTimeSlot.value = timeSlot;
|
||||||
};
|
};
|
||||||
@@ -489,14 +392,12 @@ const selectTimeSlot = (timeSlot: string) => {
|
|||||||
const isDateSelected = (date: Date) =>
|
const isDateSelected = (date: Date) =>
|
||||||
!!selectedDate.value && dayjs(selectedDate.value).isSame(dayjs(date), "day");
|
!!selectedDate.value && dayjs(selectedDate.value).isSame(dayjs(date), "day");
|
||||||
|
|
||||||
// 提交预约
|
|
||||||
const submitReservation = () => {
|
const submitReservation = () => {
|
||||||
const dateVal = selectedDate.value;
|
const dateVal = selectedDate.value;
|
||||||
if (!dateVal) {
|
if (!dateVal) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 非仅选日期模式,需要选择时间段
|
|
||||||
if (!onlySelectDay.value) {
|
if (!onlySelectDay.value) {
|
||||||
if (!selectedTimeSlot.value) {
|
if (!selectedTimeSlot.value) {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
@@ -507,13 +408,11 @@ const submitReservation = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算开始/结束时间
|
|
||||||
const selectedDateDayjs = dayjs(dateVal);
|
const selectedDateDayjs = dayjs(dateVal);
|
||||||
let startTime: dayjs.Dayjs;
|
let startTime: dayjs.Dayjs;
|
||||||
let endTime: dayjs.Dayjs;
|
let endTime: dayjs.Dayjs;
|
||||||
|
|
||||||
if (onlySelectDay.value) {
|
if (onlySelectDay.value) {
|
||||||
// 仅选日期:优先使用营业时间范围;若无营业时间限制,则使用当天起止
|
|
||||||
const bh = getBusinessHoursForDate(dateVal);
|
const bh = getBusinessHoursForDate(dateVal);
|
||||||
if (bh) {
|
if (bh) {
|
||||||
const [startHour, startMinute] = bh.startTime.split(':').map(Number);
|
const [startHour, startMinute] = bh.startTime.split(':').map(Number);
|
||||||
@@ -533,7 +432,6 @@ const submitReservation = () => {
|
|||||||
endTime = selectedDateDayjs.endOf('day');
|
endTime = selectedDateDayjs.endOf('day');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 选择了时间段:解析并生成起止时间
|
|
||||||
const [startTimeStr, endTimeStr] = selectedTimeSlot.value.split(' - ');
|
const [startTimeStr, endTimeStr] = selectedTimeSlot.value.split(' - ');
|
||||||
const [startHour, startMinute] = startTimeStr.split(':').map(Number);
|
const [startHour, startMinute] = startTimeStr.split(':').map(Number);
|
||||||
const [endHour, endMinute] = endTimeStr.split(':').map(Number);
|
const [endHour, endMinute] = endTimeStr.split(':').map(Number);
|
||||||
@@ -557,6 +455,7 @@ const submitReservation = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
uni.$emit(EventEnum.CHOOSE_APPOINTMENT_TIME, {
|
uni.$emit(EventEnum.CHOOSE_APPOINTMENT_TIME, {
|
||||||
|
merchantId: storeId.value,
|
||||||
date: dateVal,
|
date: dateVal,
|
||||||
timeSlot: onlySelectDay.value ? '' : selectedTimeSlot.value,
|
timeSlot: onlySelectDay.value ? '' : selectedTimeSlot.value,
|
||||||
startTime: startTime.valueOf(),
|
startTime: startTime.valueOf(),
|
||||||
@@ -573,19 +472,23 @@ const submitReservation = () => {
|
|||||||
|
|
||||||
const storeId = ref(null);
|
const storeId = ref(null);
|
||||||
|
|
||||||
// 页面加载时处理参数
|
|
||||||
onLoad((options: any) => {
|
onLoad((options: any) => {
|
||||||
if (options.storeId) {
|
if (options.merchantId) {
|
||||||
|
storeId.value = options.merchantId;
|
||||||
|
} else if (options.storeId) {
|
||||||
storeId.value = options.storeId;
|
storeId.value = options.storeId;
|
||||||
}
|
}
|
||||||
|
if (options.deliveryScheduleTimes) {
|
||||||
|
allowedWeekdays.value = parseDeliveryScheduleTimes(
|
||||||
|
decodeURIComponent(options.deliveryScheduleTimes)
|
||||||
|
);
|
||||||
|
}
|
||||||
if (options.storeBusinessHours) {
|
if (options.storeBusinessHours) {
|
||||||
storeBusinessHours.value = options.storeBusinessHours;
|
storeBusinessHours.value = decodeURIComponent(options.storeBusinessHours);
|
||||||
// 如果传递了该参数,进入仅选日期模式
|
|
||||||
onlySelectDay.value = true;
|
onlySelectDay.value = true;
|
||||||
}
|
}
|
||||||
// 无论是否传参,统一初始化选中日期,避免首屏未选导致不显示时间段
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
initializeSelectedDate();
|
initializeSelectedDate(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -635,7 +538,6 @@ onLoad((options: any) => {
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<!-- 时间段选择区域:在仅选日期模式下隐藏 -->
|
|
||||||
<view v-if="!onlySelectDay" class="pb-138rpx mx-40rpx bg-white rounded-24rpx overflow-hidden mt-24rpx">
|
<view v-if="!onlySelectDay" class="pb-138rpx mx-40rpx bg-white rounded-24rpx overflow-hidden mt-24rpx">
|
||||||
<view
|
<view
|
||||||
v-for="(timeSlot, index) in timeSlots"
|
v-for="(timeSlot, index) in timeSlots"
|
||||||
@@ -648,7 +550,6 @@ onLoad((options: any) => {
|
|||||||
@click="selectTimeSlot(timeSlot)"
|
@click="selectTimeSlot(timeSlot)"
|
||||||
>
|
>
|
||||||
<text class="text-32rpx font-regular">{{ timeSlot }}</text>
|
<text class="text-32rpx font-regular">{{ timeSlot }}</text>
|
||||||
<!-- 单选按钮 -->
|
|
||||||
<image
|
<image
|
||||||
:src="
|
:src="
|
||||||
selectedTimeSlot === timeSlot
|
selectedTimeSlot === timeSlot
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const keyword = ref('');
|
const keyword = ref('');
|
||||||
function handleSearch(){
|
function handleSearch(){
|
||||||
console.log("1111111111");
|
|
||||||
|
|
||||||
}
|
}
|
||||||
function handleClickItem(){
|
function handleClickItem(){
|
||||||
|
|||||||
@@ -1,79 +1,30 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="class-bullet-container">
|
<view class="class-bullet-container">
|
||||||
<!-- 第一行:跑马灯 + 手动拖动 -->
|
<view class="category-grid">
|
||||||
<view
|
<view
|
||||||
class="scroll-row"
|
v-for="item in topCategories"
|
||||||
@touchstart="onTouchStart1"
|
:key="item.id"
|
||||||
@touchmove.stop.prevent="onTouchMove1"
|
|
||||||
@touchend="onTouchEnd1"
|
|
||||||
@touchcancel="onTouchEnd1"
|
|
||||||
>
|
|
||||||
<view class="marquee-viewport">
|
|
||||||
<view class="marquee-track" :style="trackStyle1">
|
|
||||||
<view class="track-group row1-group-a">
|
|
||||||
<view
|
|
||||||
v-for="(item, idx) in categories"
|
|
||||||
:key="item.id + '-row1-a-' + idx"
|
|
||||||
class="category-item"
|
class="category-item"
|
||||||
@click="handleItemClick(item)"
|
@click="handleItemClick(item)"
|
||||||
>
|
>
|
||||||
<image v-if="item.categoryImage || item.logoUrl" :src="item.categoryImage || item.logoUrl" class="category-icon" mode="aspectFit" />
|
<image
|
||||||
|
v-if="item.categoryImage || item.logoUrl"
|
||||||
|
:src="item.categoryImage || item.logoUrl"
|
||||||
|
class="category-icon"
|
||||||
|
mode="aspectFit"
|
||||||
|
/>
|
||||||
<text class="category-text">{{ item.categoryName || item.name }}</text>
|
<text class="category-text">{{ item.categoryName || item.name }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
<view class="category-item" @click="handleMoreClick">
|
||||||
<view class="track-group">
|
<image src="@/static/app/images/more.png" class="category-icon" mode="aspectFit" />
|
||||||
<view
|
<text class="category-text">更多</text>
|
||||||
v-for="(item, idx) in categories"
|
|
||||||
:key="item.id + '-row1-b-' + idx"
|
|
||||||
class="category-item"
|
|
||||||
@click="handleItemClick(item)"
|
|
||||||
>
|
|
||||||
<image v-if="item.categoryImage || item.logoUrl" :src="item.categoryImage || item.logoUrl" class="category-icon" mode="aspectFit" />
|
|
||||||
<text class="category-text">{{ item.categoryName || item.name }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 第二行:同向跑马灯 + 手动拖动 -->
|
|
||||||
<view
|
|
||||||
class="scroll-row"
|
|
||||||
@touchstart="onTouchStart2"
|
|
||||||
@touchmove.stop.prevent="onTouchMove2"
|
|
||||||
@touchend="onTouchEnd2"
|
|
||||||
@touchcancel="onTouchEnd2"
|
|
||||||
>
|
|
||||||
<view class="marquee-viewport">
|
|
||||||
<view class="marquee-track" :style="trackStyle2">
|
|
||||||
<view class="track-group row2-group-a">
|
|
||||||
<view
|
|
||||||
v-for="(item, idx) in categories"
|
|
||||||
:key="item.id + '-row2-a-' + idx"
|
|
||||||
class="category-item"
|
|
||||||
@click="handleItemClick(item)"
|
|
||||||
>
|
|
||||||
<image v-if="item.categoryImage || item.logoUrl" :src="item.categoryImage || item.logoUrl" class="category-icon" mode="aspectFit" />
|
|
||||||
<text class="category-text">{{ item.categoryName || item.name }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<view
|
|
||||||
v-for="(item, idx) in categories"
|
|
||||||
:key="item.id + '-row2-b-' + idx"
|
|
||||||
class="category-item"
|
|
||||||
@click="handleItemClick(item)"
|
|
||||||
>
|
|
||||||
<image v-if="item.categoryImage || item.logoUrl" :src="item.categoryImage || item.logoUrl" class="category-icon" mode="aspectFit" />
|
|
||||||
<text class="category-text">{{ item.categoryName || item.name }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, onMounted, onUnmounted, nextTick, getCurrentInstance, watch } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useCategoryNavStore } from '@/store'
|
import { useCategoryNavStore } from '@/store'
|
||||||
|
|
||||||
// 定义分类项接口(与模板字段一致)
|
// 定义分类项接口(与模板字段一致)
|
||||||
@@ -118,114 +69,7 @@ const mockCategories: CategoryItem[] = [
|
|||||||
const categories = computed(() => {
|
const categories = computed(() => {
|
||||||
return props.categories.length > 0 ? props.categories : mockCategories
|
return props.categories.length > 0 ? props.categories : mockCategories
|
||||||
})
|
})
|
||||||
|
const topCategories = computed(() => categories.value.slice(0, 4))
|
||||||
const offset1 = ref(0)
|
|
||||||
const offset2 = ref(0)
|
|
||||||
const loopWidth1 = ref(0)
|
|
||||||
const loopWidth2 = ref(0)
|
|
||||||
const isTouching1 = ref(false)
|
|
||||||
const isTouching2 = ref(false)
|
|
||||||
const touchStartX1 = ref(0)
|
|
||||||
const touchStartX2 = ref(0)
|
|
||||||
const touchStartOffset1 = ref(0)
|
|
||||||
const touchStartOffset2 = ref(0)
|
|
||||||
|
|
||||||
let resumeTimer1: ReturnType<typeof setTimeout> | null = null
|
|
||||||
let resumeTimer2: ReturnType<typeof setTimeout> | null = null
|
|
||||||
let raf1 = 0
|
|
||||||
let raf2 = 0
|
|
||||||
let lastTs1 = 0
|
|
||||||
let lastTs2 = 0
|
|
||||||
const RESUME_DELAY = 800
|
|
||||||
const PX_PER_SEC_1 = 22
|
|
||||||
const PX_PER_SEC_2 = 18
|
|
||||||
|
|
||||||
const trackStyle1 = computed(() => ({
|
|
||||||
transform: `translate3d(${-offset1.value}px, 0, 0)`,
|
|
||||||
}))
|
|
||||||
|
|
||||||
const trackStyle2 = computed(() => ({
|
|
||||||
transform: `translate3d(${-offset2.value}px, 0, 0)`,
|
|
||||||
}))
|
|
||||||
|
|
||||||
function normalizeOffset(value: number, loopWidth: number) {
|
|
||||||
if (loopWidth <= 0) return value
|
|
||||||
let next = value % loopWidth
|
|
||||||
if (next < 0) next += loopWidth
|
|
||||||
return next
|
|
||||||
}
|
|
||||||
|
|
||||||
function onTouchStart1(e: any) {
|
|
||||||
const touch = e?.touches?.[0]
|
|
||||||
isTouching1.value = true
|
|
||||||
touchStartX1.value = Number(touch?.clientX || 0)
|
|
||||||
touchStartOffset1.value = offset1.value
|
|
||||||
if (resumeTimer1) {
|
|
||||||
clearTimeout(resumeTimer1)
|
|
||||||
resumeTimer1 = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onTouchMove1(e: any) {
|
|
||||||
if (!isTouching1.value) return
|
|
||||||
const touch = e?.touches?.[0]
|
|
||||||
const x = Number(touch?.clientX || 0)
|
|
||||||
const delta = x - touchStartX1.value
|
|
||||||
offset1.value = normalizeOffset(touchStartOffset1.value - delta, loopWidth1.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
function onTouchEnd1() {
|
|
||||||
resumeTimer1 = setTimeout(() => {
|
|
||||||
isTouching1.value = false
|
|
||||||
resumeTimer1 = null
|
|
||||||
}, RESUME_DELAY)
|
|
||||||
}
|
|
||||||
|
|
||||||
function onTouchStart2(e: any) {
|
|
||||||
const touch = e?.touches?.[0]
|
|
||||||
isTouching2.value = true
|
|
||||||
touchStartX2.value = Number(touch?.clientX || 0)
|
|
||||||
touchStartOffset2.value = offset2.value
|
|
||||||
if (resumeTimer2) {
|
|
||||||
clearTimeout(resumeTimer2)
|
|
||||||
resumeTimer2 = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onTouchMove2(e: any) {
|
|
||||||
if (!isTouching2.value) return
|
|
||||||
const touch = e?.touches?.[0]
|
|
||||||
const x = Number(touch?.clientX || 0)
|
|
||||||
const delta = x - touchStartX2.value
|
|
||||||
offset2.value = normalizeOffset(touchStartOffset2.value - delta, loopWidth2.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
function onTouchEnd2() {
|
|
||||||
resumeTimer2 = setTimeout(() => {
|
|
||||||
isTouching2.value = false
|
|
||||||
resumeTimer2 = null
|
|
||||||
}, RESUME_DELAY)
|
|
||||||
}
|
|
||||||
|
|
||||||
function tickRow1(ts: number) {
|
|
||||||
if (!lastTs1) lastTs1 = ts
|
|
||||||
const dt = ts - lastTs1
|
|
||||||
lastTs1 = ts
|
|
||||||
if (!isTouching1.value) {
|
|
||||||
offset1.value = normalizeOffset(offset1.value + (PX_PER_SEC_1 * dt) / 1000, loopWidth1.value)
|
|
||||||
}
|
|
||||||
raf1 = requestAnimationFrame(tickRow1)
|
|
||||||
}
|
|
||||||
|
|
||||||
function tickRow2(ts: number) {
|
|
||||||
if (!lastTs2) lastTs2 = ts
|
|
||||||
const dt = ts - lastTs2
|
|
||||||
lastTs2 = ts
|
|
||||||
if (!isTouching2.value) {
|
|
||||||
offset2.value = normalizeOffset(offset2.value + (PX_PER_SEC_2 * dt) / 1000, loopWidth2.value)
|
|
||||||
}
|
|
||||||
raf2 = requestAnimationFrame(tickRow2)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 点击处理(列表页菜谱 id 来自 store,不放在 URL query)
|
// 点击处理(列表页菜谱 id 来自 store,不放在 URL query)
|
||||||
const handleItemClick = (item: CategoryItem) => {
|
const handleItemClick = (item: CategoryItem) => {
|
||||||
@@ -238,123 +82,46 @@ function navigateTo(url: string) {
|
|||||||
uni.navigateTo({ url })
|
uni.navigateTo({ url })
|
||||||
}
|
}
|
||||||
|
|
||||||
function measureLoopWidths() {
|
function handleMoreClick() {
|
||||||
const instance = getCurrentInstance()
|
navigateTo('/pages-store/pages/list/index')
|
||||||
if (!instance) return
|
|
||||||
const query = uni.createSelectorQuery().in(instance.proxy)
|
|
||||||
query.select('.row1-group-a').boundingClientRect()
|
|
||||||
query.select('.row2-group-a').boundingClientRect()
|
|
||||||
query.exec((res: Array<{ width?: number } | null>) => {
|
|
||||||
const w1 = Number(res?.[0]?.width || 0)
|
|
||||||
const w2 = Number(res?.[1]?.width || 0)
|
|
||||||
loopWidth1.value = w1 > 0 ? w1 : 0
|
|
||||||
loopWidth2.value = w2 > 0 ? w2 : 0
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureMeasuredWidths(retry = 0) {
|
|
||||||
measureLoopWidths()
|
|
||||||
if (retry >= 8) return
|
|
||||||
setTimeout(() => {
|
|
||||||
if (loopWidth1.value > 0 && loopWidth2.value > 0) return
|
|
||||||
ensureMeasuredWidths(retry + 1)
|
|
||||||
}, 220)
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
nextTick(() => {
|
|
||||||
ensureMeasuredWidths()
|
|
||||||
})
|
|
||||||
raf1 = requestAnimationFrame(tickRow1)
|
|
||||||
raf2 = requestAnimationFrame(tickRow2)
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => categories.value.length,
|
|
||||||
() => {
|
|
||||||
nextTick(() => {
|
|
||||||
ensureMeasuredWidths()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (raf1) cancelAnimationFrame(raf1)
|
|
||||||
if (raf2) cancelAnimationFrame(raf2)
|
|
||||||
if (resumeTimer1) {
|
|
||||||
clearTimeout(resumeTimer1)
|
|
||||||
resumeTimer1 = null
|
|
||||||
}
|
|
||||||
if (resumeTimer2) {
|
|
||||||
clearTimeout(resumeTimer2)
|
|
||||||
resumeTimer2 = null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.class-bullet-container {
|
.class-bullet-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: hidden;
|
}
|
||||||
|
|
||||||
.scroll-row {
|
.category-grid {
|
||||||
margin-bottom: 12rpx;
|
display: grid;
|
||||||
overflow: hidden;
|
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||||
|
gap: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
&:last-child {
|
.category-item {
|
||||||
margin-bottom: 0;
|
min-width: 0;
|
||||||
}
|
min-height: 108rpx;
|
||||||
}
|
padding: 8rpx 4rpx;
|
||||||
|
|
||||||
.marquee-viewport {
|
|
||||||
width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.marquee-track {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
width: max-content;
|
|
||||||
will-change: transform;
|
|
||||||
}
|
|
||||||
|
|
||||||
.track-group {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-item {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
min-width: 120rpx;
|
gap: 8rpx;
|
||||||
height: 60rpx;
|
}
|
||||||
margin-right: 20rpx;
|
|
||||||
padding: 0 20rpx;
|
.category-icon {
|
||||||
background: #fff;
|
width: 54rpx;
|
||||||
border: none;
|
height: 54rpx;
|
||||||
border-radius: 30rpx;
|
|
||||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
&:active {
|
.category-text {
|
||||||
transform: translateY(0) scale(0.96);
|
font-size: 22rpx;
|
||||||
}
|
|
||||||
|
|
||||||
.category-icon {
|
|
||||||
width: 32rpx;
|
|
||||||
height: 32rpx;
|
|
||||||
margin-right: 8rpx;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-text {
|
|
||||||
font-size: 28rpx;
|
|
||||||
color: #333;
|
color: #333;
|
||||||
|
line-height: 1.2;
|
||||||
|
max-width: 100%;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
overflow: hidden;
|
||||||
}
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -79,14 +79,14 @@ function subtitleLine(item: any): string {
|
|||||||
<image
|
<image
|
||||||
:src="getCardImages(item)[0]"
|
:src="getCardImages(item)[0]"
|
||||||
class="featured-card__media-half featured-card__media-half--top"
|
class="featured-card__media-half featured-card__media-half--top"
|
||||||
mode="aspectFill"
|
mode="scaleToFill"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<image
|
<image
|
||||||
v-else-if="getCardImages(item).length >= 1"
|
v-else-if="getCardImages(item).length >= 1"
|
||||||
:src="getCardImages(item)[0]"
|
:src="getCardImages(item)[0]"
|
||||||
class="featured-card__media-single"
|
class="featured-card__media-single"
|
||||||
mode="aspectFill"
|
mode="scaleToFill"
|
||||||
/>
|
/>
|
||||||
<view v-else class="featured-card__media-single featured-card__media-placeholder" />
|
<view v-else class="featured-card__media-single featured-card__media-placeholder" />
|
||||||
</view>
|
</view>
|
||||||
@@ -148,9 +148,9 @@ function subtitleLine(item: any): string {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
width: 620rpx;
|
width: 660rpx;
|
||||||
height: 256rpx;
|
height: 306rpx;
|
||||||
min-height: 256rpx;
|
min-height: 306rpx;
|
||||||
margin-left: 16rpx;
|
margin-left: 16rpx;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 24rpx;
|
border-radius: 24rpx;
|
||||||
@@ -162,7 +162,7 @@ function subtitleLine(item: any): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.featured-card__media {
|
.featured-card__media {
|
||||||
width: 232rpx;
|
width: 323rpx;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
background: #f0f0f0;
|
background: #f0f0f0;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -204,23 +204,17 @@ function subtitleLine(item: any): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.featured-card__name {
|
.featured-card__name {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
line-height: 34rpx;
|
line-height: 34rpx;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #1a1a1a;
|
color: #1a1a1a;
|
||||||
}
|
white-space: normal;
|
||||||
.featured-card__name--primary {
|
word-break: break-word;
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
.featured-card__name--secondary {
|
.featured-card__name--secondary {
|
||||||
font-size: 28rpx;
|
font-weight: 500;
|
||||||
line-height: 34rpx;
|
|
||||||
color: #1a1a1a;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.featured-card__media-placeholder {
|
.featured-card__media-placeholder {
|
||||||
|
|||||||
@@ -2,18 +2,18 @@
|
|||||||
<view class="home-skeleton">
|
<view class="home-skeleton">
|
||||||
<!-- 顶部头部(与 home-top-header 对齐) -->
|
<!-- 顶部头部(与 home-top-header 对齐) -->
|
||||||
<view class="header-section">
|
<view class="header-section">
|
||||||
<view class="header-left">
|
<view class="header-toolbar">
|
||||||
<view class="brand-row">
|
<view class="brand-row">
|
||||||
<view class="logo-skeleton skeleton-item"></view>
|
<view class="logo-skeleton skeleton-item"></view>
|
||||||
<view class="app-title-skeleton skeleton-item"></view>
|
<view class="app-title-skeleton skeleton-item"></view>
|
||||||
</view>
|
</view>
|
||||||
<view class="delivery-info-skeleton skeleton-item"></view>
|
|
||||||
</view>
|
|
||||||
<view class="header-right">
|
<view class="header-right">
|
||||||
<view class="location-pill-skeleton skeleton-item"></view>
|
<view class="location-pill-skeleton skeleton-item"></view>
|
||||||
<view class="cart-btn-skeleton skeleton-item"></view>
|
<view class="cart-btn-skeleton skeleton-item"></view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
<view class="notice-row-skeleton skeleton-item"></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<!-- 搜索栏 + 消息按钮 -->
|
<!-- 搜索栏 + 消息按钮 -->
|
||||||
<view class="search-section">
|
<view class="search-section">
|
||||||
@@ -121,19 +121,22 @@
|
|||||||
.header-section {
|
.header-section {
|
||||||
padding: 18rpx 24rpx 0;
|
padding: 18rpx 24rpx 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
flex-direction: column;
|
||||||
|
gap: 10rpx;
|
||||||
|
|
||||||
|
.header-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 12rpx;
|
gap: 12rpx;
|
||||||
|
|
||||||
.header-left {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-row {
|
.brand-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 14rpx;
|
gap: 14rpx;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo-skeleton {
|
.logo-skeleton {
|
||||||
@@ -149,10 +152,9 @@
|
|||||||
border-radius: 8rpx;
|
border-radius: 8rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.delivery-info-skeleton {
|
.notice-row-skeleton {
|
||||||
margin-top: 12rpx;
|
width: 100%;
|
||||||
width: 300rpx;
|
height: 32rpx;
|
||||||
height: 30rpx;
|
|
||||||
border-radius: 8rpx;
|
border-radius: 8rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,6 +162,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10rpx;
|
gap: 10rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.location-pill-skeleton {
|
.location-pill-skeleton {
|
||||||
|
|||||||
@@ -1,50 +1,81 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
const props = defineProps<{
|
defineProps<{
|
||||||
appName: string
|
appName: string
|
||||||
locationText: string
|
locationText: string
|
||||||
appointmentTimeShow?: string
|
|
||||||
cartBadgeTotal?: number
|
cartBadgeTotal?: number
|
||||||
isLogin?: boolean
|
isLogin?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'clickAddress'): void
|
|
||||||
(e: 'clickLocation'): void
|
(e: 'clickLocation'): void
|
||||||
(e: 'clickCart'): void
|
(e: 'clickCart'): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const noticeTexts = computed(() => [
|
||||||
|
t('pages.home.noticeBar.thursday'),
|
||||||
|
t('pages.home.noticeBar.sunday'),
|
||||||
|
t('pages.home.noticeBar.freshCatch'),
|
||||||
|
])
|
||||||
|
|
||||||
|
/** 横向无缝滚动:多条拼接为一条,重复一遍避免循环时跳变 */
|
||||||
|
const noticeScrollText = computed(() => {
|
||||||
|
const items = noticeTexts.value.filter(Boolean)
|
||||||
|
if (!items.length) return ''
|
||||||
|
const once = items.join(' ')
|
||||||
|
return `${once} ${once}`
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<view class="home-top-header px-24rpx pt-12rpx pb-8rpx">
|
<view class="home-top-header px-24rpx pt-12rpx pb-8rpx">
|
||||||
<view class="flex items-center justify-between gap-12rpx">
|
<view class="home-top-header__toolbar flex items-center justify-between gap-12rpx">
|
||||||
<view class="min-w-0 flex-1">
|
<view class="flex items-center gap-14rpx min-w-0 flex-1">
|
||||||
<view class="flex items-center gap-14rpx min-w-0">
|
<image
|
||||||
<image src="@img/logo.png" class="w-52rpx h-52rpx shrink-0" style="border-radius: 50%;"></image>
|
src="@img/logo.png"
|
||||||
<text class="text-32rpx lh-36rpx text-#1a1a1a font-bold tracking-tight shrink-0">{{ appName }}</text>
|
class="w-52rpx h-52rpx shrink-0"
|
||||||
</view>
|
style="border-radius: 50%"
|
||||||
<view @click="emit('clickAddress')" class="home-delivery-row mt-10rpx flex items-center min-w-0 text-26rpx lh-32rpx text-#00A76D">
|
/>
|
||||||
<text v-if="appointmentTimeShow">{{ t('pages.address.appTime') }}: {{ appointmentTimeShow }}</text>
|
<text class="text-32rpx lh-36rpx text-#1a1a1a font-bold tracking-tight line-clamp-1">
|
||||||
<text v-else>{{ t('pages.address.reservation') }}</text>
|
{{ appName }}
|
||||||
<image src="@img/chef/119.png" class="w-22rpx h-22rpx ml-8rpx shrink-0"></image>
|
</text>
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
<view class="flex items-center gap-10rpx shrink-0">
|
<view class="flex items-center gap-10rpx shrink-0">
|
||||||
<view class="home-loc-pill" @click="emit('clickLocation')">
|
<view class="home-loc-pill" @click="emit('clickLocation')">
|
||||||
<text class="home-loc-pill__text line-clamp-1">{{ locationText || t('pages.home.default-location') }}</text>
|
<text class="home-loc-pill__text line-clamp-1">
|
||||||
<image src="@img/chef/119.png" class="home-loc-pill__arrow w-20rpx h-20rpx shrink-0 op-50 mt-2rpx"></image>
|
{{ locationText || t('pages.home.default-location') }}
|
||||||
|
</text>
|
||||||
|
<image
|
||||||
|
src="@img/chef/119.png"
|
||||||
|
class="home-loc-pill__arrow w-20rpx h-20rpx shrink-0 op-50 mt-2rpx"
|
||||||
|
/>
|
||||||
</view>
|
</view>
|
||||||
<view class="home-cart-btn" @click="emit('clickCart')">
|
<view class="home-cart-btn" @click="emit('clickCart')">
|
||||||
<view class="i-carbon:shopping-cart text-36rpx text-#14181b"></view>
|
<view class="i-carbon:shopping-cart text-36rpx text-#14181b" />
|
||||||
<view v-if="isLogin && (cartBadgeTotal || 0) > 0" class="home-cart-badge">
|
<view
|
||||||
|
v-if="isLogin && (cartBadgeTotal || 0) > 0"
|
||||||
|
class="home-cart-badge"
|
||||||
|
>
|
||||||
{{ (cartBadgeTotal || 0) > 99 ? '99+' : cartBadgeTotal }}
|
{{ (cartBadgeTotal || 0) > 99 ? '99+' : cartBadgeTotal }}
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<view class="home-notice-wrap mt-10rpx">
|
||||||
|
<wd-notice-bar
|
||||||
|
:text="noticeScrollText"
|
||||||
|
:delay="1"
|
||||||
|
:speed="60"
|
||||||
|
color="#00A76D"
|
||||||
|
background-color="transparent"
|
||||||
|
custom-class="home-notice-bar"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -53,6 +84,30 @@ const emit = defineEmits<{
|
|||||||
background: #f2f2f2;
|
background: #f2f2f2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.home-notice-wrap {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
:deep(.home-notice-bar) {
|
||||||
|
padding: 0;
|
||||||
|
font-size: 26rpx;
|
||||||
|
line-height: 32rpx;
|
||||||
|
border-radius: 0;
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.wd-notice-bar__wrap) {
|
||||||
|
height: 32rpx;
|
||||||
|
line-height: 32rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.wd-notice-bar__content) {
|
||||||
|
font-size: 26rpx;
|
||||||
|
line-height: 32rpx;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.home-loc-pill {
|
.home-loc-pill {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -102,4 +157,3 @@ const emit = defineEmits<{
|
|||||||
border-radius: 999rpx;
|
border-radius: 999rpx;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { useUserStore } from '@/store';
|
||||||
import { useUserStore } from "@/store";
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const expanded = ref(true)
|
|
||||||
const touchStartX = ref(0)
|
|
||||||
|
|
||||||
function navigateTo(url: string) {
|
function navigateTo(url: string) {
|
||||||
if(userStore.checkLogin()) {
|
if(userStore.checkLogin()) {
|
||||||
@@ -13,46 +10,19 @@ function navigateTo(url: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function togglePanel() {
|
|
||||||
expanded.value = !expanded.value
|
|
||||||
}
|
|
||||||
|
|
||||||
function onFabClick() {
|
function onFabClick() {
|
||||||
if (!expanded.value) {
|
|
||||||
expanded.value = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
navigateTo('/pages/ai/recommend/index')
|
navigateTo('/pages/ai/recommend/index')
|
||||||
}
|
}
|
||||||
|
|
||||||
function onHandleTouchStart(e: any) {
|
|
||||||
touchStartX.value = Number(e?.touches?.[0]?.clientX || 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
function onHandleTouchEnd(e: any) {
|
|
||||||
const endX = Number(e?.changedTouches?.[0]?.clientX || 0)
|
|
||||||
const delta = endX - touchStartX.value
|
|
||||||
if (delta > 20) {
|
|
||||||
expanded.value = true
|
|
||||||
} else if (delta < -20) {
|
|
||||||
expanded.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- 仅 AI 跳转做右侧球体悬浮 -->
|
<!-- 仅 AI 跳转做右侧球体悬浮 -->
|
||||||
<view
|
<view
|
||||||
class="ai-floating"
|
class="ai-floating"
|
||||||
:class="{ 'is-collapsed': !expanded }"
|
|
||||||
@touchstart.stop="onHandleTouchStart"
|
|
||||||
@touchend.stop="onHandleTouchEnd"
|
|
||||||
@touchcancel.stop="onHandleTouchEnd"
|
|
||||||
>
|
>
|
||||||
<view v-if="expanded" class="ai-close" @click.stop="expanded = false">×</view>
|
|
||||||
|
|
||||||
<view class="ai-fab" @click="onFabClick">
|
<view class="ai-fab" @click="onFabClick">
|
||||||
<image src="@img/chef/115.png" class="ai-icon"></image>
|
<image src="@/static/app/images/aidiancan.gif" class="ai-icon"></image>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
@@ -72,38 +42,9 @@ function onHandleTouchEnd(e: any) {
|
|||||||
transform: translateY(-50%) translateX(55rpx);
|
transform: translateY(-50%) translateX(55rpx);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ai-close {
|
|
||||||
position: absolute;
|
|
||||||
left: -18rpx;
|
|
||||||
top: -15rpx;
|
|
||||||
width: 34rpx;
|
|
||||||
height: 34rpx;
|
|
||||||
z-index: 2;
|
|
||||||
border-radius: 999rpx;
|
|
||||||
background: #ffffff;
|
|
||||||
border: 1rpx solid #e5e7eb;
|
|
||||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: #7b8794;
|
|
||||||
font-size: 24rpx;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-fab {
|
|
||||||
width: 92rpx;
|
|
||||||
height: 92rpx;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: #e2e7eb;
|
|
||||||
box-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.18);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-icon {
|
.ai-icon {
|
||||||
width: 44rpx;
|
width: 260rpx;
|
||||||
height: 44rpx;
|
height: 260rpx;
|
||||||
|
margin-right:-70rpx !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
/** 外部列表(菜谱页等);首页不传则使用固定五项 */
|
||||||
list: {
|
list: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
@@ -21,77 +25,105 @@ const props = defineProps({
|
|||||||
default: 'logoUrl',
|
default: 'logoUrl',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
const emit = defineEmits(['changeType']);
|
|
||||||
watchEffect(() => {
|
|
||||||
if (props.currentId) {
|
|
||||||
selectedIndex.value = props.currentId;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const selectedIndex = ref();
|
const emit = defineEmits(['changeType'])
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
function selectTab(item: any) {
|
const fixedTabs = [
|
||||||
selectedIndex.value = item[props.valueKey];
|
{
|
||||||
// 触发父组件事件
|
id: 'member-zone',
|
||||||
console.log('selectTab', item[props.valueKey]);
|
nameKey: 'pages.home.quickTabs.memberZone',
|
||||||
emit('changeType', item[props.valueKey]);
|
logoUrl: '/static/app/images/home/huiyuanzhuanqu.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'live-seafood-air',
|
||||||
|
nameKey: 'pages.home.quickTabs.liveSeafoodAir',
|
||||||
|
logoUrl: '/static/app/images/home/kongyunhaixian.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'must-eat-list',
|
||||||
|
nameKey: 'pages.home.quickTabs.mustEatList',
|
||||||
|
logoUrl: '/static/app/images/home/bichibang.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'new-calendar',
|
||||||
|
nameKey: 'pages.home.quickTabs.newCalendar',
|
||||||
|
logoUrl: '/static/app/images/home/shangxinrili.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'fresh-seafood-today',
|
||||||
|
nameKey: 'pages.home.quickTabs.freshSeafoodToday',
|
||||||
|
logoUrl: '/static/app/images/home/xiandahaixian.png',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const useFixedTabs = computed(() => !props.list || props.list.length === 0)
|
||||||
|
|
||||||
|
function selectTab(item: Record<string, unknown>) {
|
||||||
|
emit('changeType', item[props.valueKey])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function imageWidthByIndex(index: number) {
|
||||||
|
return (index + 1) % 2 === 1 ? '104rpx' : '210rpx'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTabName(item: Record<string, unknown>) {
|
||||||
|
if (useFixedTabs.value && item.nameKey) {
|
||||||
|
return t(String(item.nameKey))
|
||||||
|
}
|
||||||
|
return item[props.labelKey]
|
||||||
|
}
|
||||||
|
|
||||||
|
function getImage(item: Record<string, unknown>) {
|
||||||
|
return String(item[props.imgKey] ?? '')
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayList = computed(() =>
|
||||||
|
useFixedTabs.value ? fixedTabs : props.list,
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<scroll-view :scroll-x="true">
|
<scroll-view :scroll-x="true">
|
||||||
<view class="flex items-center">
|
<view class="flex items-center">
|
||||||
<view class="shrink-0 w-30rpx"></view>
|
<view class="shrink-0 w-30rpx"></view>
|
||||||
<template v-for="(item, index) in list" :key="index">
|
|
||||||
<view
|
<view
|
||||||
|
v-for="(item, index) in displayList"
|
||||||
|
:key="String((item as Record<string, unknown>)[valueKey] ?? (item as Record<string, unknown>).id ?? index)"
|
||||||
:class="[index === 0 ? '' : 'ml-40rpx']"
|
:class="[index === 0 ? '' : 'ml-40rpx']"
|
||||||
class="w-112rpx flex flex-col items-center"
|
class="tab-item shrink-0 flex flex-col items-center"
|
||||||
@click="selectTab(item)"
|
@click="selectTab(item as Record<string, unknown>)"
|
||||||
>
|
|
||||||
<view
|
|
||||||
:class="['img-wrap', selectedIndex == item[props.valueKey] ? 'img-selected' : '']"
|
|
||||||
>
|
>
|
||||||
<image
|
<image
|
||||||
class="tab-img rounded-50% overflow-hidden bg-common"
|
class="tab-img"
|
||||||
:src="item[props.imgKey]"
|
:src="getImage(item as Record<string, unknown>)"
|
||||||
mode="aspectFill"
|
mode="scaleToFill"
|
||||||
></image>
|
:style="{ width: imageWidthByIndex(index) }"
|
||||||
|
/>
|
||||||
|
<text class="tab-label line-clamp-1">{{ getTabName(item as Record<string, unknown>) }}</text>
|
||||||
</view>
|
</view>
|
||||||
<text
|
|
||||||
:class="selectedIndex == item[props.valueKey] ? 'text-#CE7138' : 'text-#333'"
|
|
||||||
class="line-clamp-1 text-20rpx lh-20rpx mt-12rpx font-500"
|
|
||||||
>{{ item[props.labelKey] }}</text
|
|
||||||
>
|
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
<view class="shrink-0 w-30rpx op-0">1</view>
|
<view class="shrink-0 w-30rpx op-0">1</view>
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.img-wrap {
|
.tab-item {
|
||||||
display: flex;
|
min-width: 94rpx;
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.2s;
|
|
||||||
width: 102rpx;
|
|
||||||
height: 102rpx;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: #fff;
|
|
||||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.07);
|
|
||||||
}
|
}
|
||||||
.img-selected {
|
|
||||||
border: 4rpx solid #ce7138;
|
.tab-label {
|
||||||
border-radius: 50%;
|
margin-top: 10rpx;
|
||||||
overflow: hidden;
|
font-size: 22rpx;
|
||||||
box-sizing: border-box;
|
line-height: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-img {
|
.tab-img {
|
||||||
width: 98rpx;
|
height: 144rpx;
|
||||||
height: 98rpx;
|
width: auto;
|
||||||
border-radius: 50%;
|
display: block;
|
||||||
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
margin-bottom: 10rpx;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
import useEventEmit from "@/hooks/useEventEmit";
|
import useEventEmit from "@/hooks/useEventEmit";
|
||||||
import {CollectionType, EventEnum} from "@/constant/enums";
|
import {CollectionType, EventEnum} from "@/constant/enums";
|
||||||
import { useConfigStore, useUserStore } from "@/store";
|
import { useConfigStore, useUserStore } from "@/store";
|
||||||
@@ -17,7 +19,6 @@ import HomeSkeleton from "./components/home-skeleton.vue";
|
|||||||
import {
|
import {
|
||||||
appMarketActivityListPost,
|
appMarketActivityListPost,
|
||||||
appMerchantCartListMerchantPost,
|
appMerchantCartListMerchantPost,
|
||||||
appMerchantCategoryListGet,
|
|
||||||
appMerchantFeaturedListPost,
|
appMerchantFeaturedListPost,
|
||||||
appMerchantLabelListGet,
|
appMerchantLabelListGet,
|
||||||
appMerchantNearbyListPost, appMerchantRecommendListPost,
|
appMerchantNearbyListPost, appMerchantRecommendListPost,
|
||||||
@@ -26,6 +27,10 @@ import {
|
|||||||
} from "@/service";
|
} from "@/service";
|
||||||
import usePage from "@/hooks/usePage";
|
import usePage from "@/hooks/usePage";
|
||||||
import {getFeaturedDishList} from "@/pages-store/service";
|
import {getFeaturedDishList} from "@/pages-store/service";
|
||||||
|
import {
|
||||||
|
buildQuickTopicUrl,
|
||||||
|
isQuickTopicSlug,
|
||||||
|
} from '@/pages-store/pages/dishes/utils/quick-topic-route'
|
||||||
import { formatSalesCount } from "@/utils/utils";
|
import { formatSalesCount } from "@/utils/utils";
|
||||||
const configStore = useConfigStore();
|
const configStore = useConfigStore();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
@@ -34,7 +39,35 @@ const props = defineProps<{
|
|||||||
price?: Array<string> | string;
|
price?: Array<string> | string;
|
||||||
}>();
|
}>();
|
||||||
const emit = defineEmits(["toggleScore", "togglePrice", "toggleNotOpen"]);
|
const emit = defineEmits(["toggleScore", "togglePrice", "toggleNotOpen"]);
|
||||||
const { t } = useI18n();
|
const { t, locale } = useI18n();
|
||||||
|
|
||||||
|
/** 首页运营图:按语言切换(中文 / 英文) */
|
||||||
|
const HOME_PROMO_BANNERS = {
|
||||||
|
memberUpgrade: {
|
||||||
|
zh: 'https://www.howhowfresh.com/minio/ruoyi/2026/06/03/0b9a7f865aba442e84e51034a3ec900c.png',
|
||||||
|
en: 'https://www.howhowfresh.com/minio/ruoyi/2026/06/03/c5b9df3a922d4d17ae1b6eb8c1d7524a.png',
|
||||||
|
},
|
||||||
|
deliveryTime: {
|
||||||
|
zh: 'https://www.howhowfresh.com/minio/ruoyi/2026/06/03/c5673a8874594755bdde7ed7fcbd1982.jpg',
|
||||||
|
en: 'https://www.howhowfresh.com/minio/ruoyi/2026/06/03/1da02f1e0af34cea91a4f643247176be.png',
|
||||||
|
},
|
||||||
|
} as const
|
||||||
|
|
||||||
|
const isEnglishLocale = computed(() =>
|
||||||
|
String(locale.value || uni.getLocale() || '').toLowerCase().startsWith('en'),
|
||||||
|
)
|
||||||
|
|
||||||
|
const memberUpgradeBannerSrc = computed(() =>
|
||||||
|
isEnglishLocale.value
|
||||||
|
? HOME_PROMO_BANNERS.memberUpgrade.en
|
||||||
|
: HOME_PROMO_BANNERS.memberUpgrade.zh,
|
||||||
|
)
|
||||||
|
|
||||||
|
const deliveryTimeBannerSrc = computed(() =>
|
||||||
|
isEnglishLocale.value
|
||||||
|
? HOME_PROMO_BANNERS.deliveryTime.en
|
||||||
|
: HOME_PROMO_BANNERS.deliveryTime.zh,
|
||||||
|
)
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
@@ -49,12 +82,6 @@ function getDishPromoLabel(item: Record<string, unknown>): string {
|
|||||||
return typeof raw === 'string' && raw.trim() ? raw.trim() : ''
|
return typeof raw === 'string' && raw.trim() ? raw.trim() : ''
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFeaturedDishDisplayPrice(item: Record<string, any>) {
|
|
||||||
const firstSpecPrice = item?.merchantSideDishVoList?.[0]?.merchantSideDishItemVoList?.[0]?.actualSalePrice
|
|
||||||
if (firstSpecPrice != null && String(firstSpecPrice) !== '') return firstSpecPrice
|
|
||||||
if (item?.actualSalePrice != null && String(item.actualSalePrice) !== '') return item.actualSalePrice
|
|
||||||
return item?.discountPrice ?? 0
|
|
||||||
}
|
|
||||||
|
|
||||||
function navigateTo(url: string) {
|
function navigateTo(url: string) {
|
||||||
if(userStore.checkLogin()) {
|
if(userStore.checkLogin()) {
|
||||||
@@ -64,17 +91,15 @@ function navigateTo(url: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const swiperList = ref([]);
|
const swiperList = ref<any[]>([])
|
||||||
const currentSwiper = ref(0);
|
|
||||||
|
|
||||||
async function initData() {
|
async function initData() {
|
||||||
// 只在首次加载时显示骨架屏,避免切换时的白屏
|
// 只在首次加载时显示骨架屏,避免切换时的白屏
|
||||||
if(featuredList.value.length === 0) {
|
if(featuredList.value.length === 0) {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
}
|
}
|
||||||
appMarketActivityList()
|
getAppMarketActivityList()
|
||||||
getAppMerchantLabelList()
|
getAppMerchantLabelList()
|
||||||
getAppMerchantCategoryList()
|
|
||||||
getAppFeaturedList()
|
getAppFeaturedList()
|
||||||
getAppNearbyListPost()
|
getAppNearbyListPost()
|
||||||
// 获取当前用户购物车信息
|
// 获取当前用户购物车信息
|
||||||
@@ -104,11 +129,20 @@ defineExpose({
|
|||||||
init: getIndexList,
|
init: getIndexList,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 获取轮播图列表
|
/** 首页轮播:POST /app/marketActivity/list */
|
||||||
function appMarketActivityList() {
|
function getAppMarketActivityList() {
|
||||||
appMarketActivityListPost({}).then(res=> {
|
appMarketActivityListPost({})
|
||||||
console.log('互动列表', res)
|
.then((res) => {
|
||||||
swiperList.value = res.data
|
const list = Array.isArray(res?.data) ? res.data : []
|
||||||
|
swiperList.value = list
|
||||||
|
.filter((item) => item?.activityImageUrl || item?.activityImage)
|
||||||
|
.map((item) => ({
|
||||||
|
...item,
|
||||||
|
activityImage: item.activityImageUrl || item.activityImage,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
swiperList.value = []
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,15 +158,7 @@ function getAppMerchantLabelList() {
|
|||||||
// appMerchantLabelList.value = res.data || []
|
// appMerchantLabelList.value = res.data || []
|
||||||
// })
|
// })
|
||||||
}
|
}
|
||||||
// 查询所有商家分类数据
|
|
||||||
const appMerchantCategoryList = ref([])
|
|
||||||
const currentCategory = ref('')
|
const currentCategory = ref('')
|
||||||
function getAppMerchantCategoryList() {
|
|
||||||
appMerchantCategoryListGet({}).then((res: any) => {
|
|
||||||
console.log('查询所有商家分类数据', res)
|
|
||||||
appMerchantCategoryList.value = res.data || []
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查询精选商家列表(首页)
|
// 查询精选商家列表(首页)
|
||||||
const featuredList = ref([])
|
const featuredList = ref([])
|
||||||
@@ -208,11 +234,24 @@ function handleItemClick(e) {
|
|||||||
merchantLabelId.value = e.id
|
merchantLabelId.value = e.id
|
||||||
// paging.value.refresh()
|
// paging.value.refresh()
|
||||||
}
|
}
|
||||||
function tabsTypeChange(id: string) {
|
function tabsTypeChange(id: string | number) {
|
||||||
currentCategory.value = id
|
const topic = String(id)
|
||||||
navigateTo('/pages-store/pages/home-store/index?merchantCategoryIds=' + id)
|
if (!isQuickTopicSlug(topic)) {
|
||||||
// console.log('分类切换', id)
|
return
|
||||||
// paging.value.refresh()
|
}
|
||||||
|
currentCategory.value = topic
|
||||||
|
const titleKeys: Record<string, string> = {
|
||||||
|
'member-zone': 'pages.home.quickTabs.memberZone',
|
||||||
|
'live-seafood-air': 'pages.home.quickTabs.liveSeafoodAir',
|
||||||
|
'must-eat-list': 'pages.home.quickTabs.mustEatList',
|
||||||
|
'new-calendar': 'pages.home.quickTabs.newCalendar',
|
||||||
|
'fresh-seafood-today': 'pages.home.quickTabs.freshSeafoodToday',
|
||||||
|
}
|
||||||
|
navigateTo(
|
||||||
|
buildQuickTopicUrl(topic, {
|
||||||
|
categoryName: t(titleKeys[topic]),
|
||||||
|
}),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 是否展示精选商家和附近商家 true 显示 false隐藏
|
// 是否展示精选商家和附近商家 true 显示 false隐藏
|
||||||
@@ -253,6 +292,9 @@ function onRefresh() {
|
|||||||
console.log('手动触发下拉刷新了')
|
console.log('手动触发下拉刷新了')
|
||||||
merchantLabelId.value = ''
|
merchantLabelId.value = ''
|
||||||
currentCategory.value = ''
|
currentCategory.value = ''
|
||||||
|
getAppMarketActivityList()
|
||||||
|
getAppFeaturedList()
|
||||||
|
getAppNearbyListPost()
|
||||||
paging.value.refresh()
|
paging.value.refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,9 +310,14 @@ function handleClickSwiper(item: any) {
|
|||||||
case 3: // 会员
|
case 3: // 会员
|
||||||
navigateTo('/pages-user/pages/member/index')
|
navigateTo('/pages-user/pages/member/index')
|
||||||
break
|
break
|
||||||
|
// case 4:
|
||||||
|
// navigateTo('/pages/ai/chat/index')
|
||||||
|
// break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const walletUrl = "pages-user/pages/balance/index";
|
||||||
|
|
||||||
function navigateToDishes(item: any) {
|
function navigateToDishes(item: any) {
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: '/pages-store/pages/store/dishes?id=' + item.id + '&storeId=' + item.merchantId,
|
url: '/pages-store/pages/store/dishes?id=' + item.id + '&storeId=' + item.merchantId,
|
||||||
@@ -313,10 +360,8 @@ const debouncedEmit = debounce(1300, (isCollected: boolean, id: string, type: Co
|
|||||||
<home-top-header
|
<home-top-header
|
||||||
:app-name="Config.appName"
|
:app-name="Config.appName"
|
||||||
:location-text="userStore.userLocation.location"
|
:location-text="userStore.userLocation.location"
|
||||||
:appointment-time-show="userStore.appointmentTimeShow"
|
|
||||||
:cart-badge-total="cartBadgeTotal"
|
:cart-badge-total="cartBadgeTotal"
|
||||||
:is-login="userStore.isLogin"
|
:is-login="userStore.isLogin"
|
||||||
@click-address="navigateTo('/pages/address/index')"
|
|
||||||
@click-location="navigateTo('/pages-user/pages/search-address/index')"
|
@click-location="navigateTo('/pages-user/pages/search-address/index')"
|
||||||
@click-cart="goCart"
|
@click-cart="goCart"
|
||||||
/>
|
/>
|
||||||
@@ -330,17 +375,17 @@ const debouncedEmit = debounce(1300, (isCollected: boolean, id: string, type: Co
|
|||||||
<view class="px-24rpx pt-12rpx pb-8rpx">
|
<view class="px-24rpx pt-12rpx pb-8rpx">
|
||||||
<search />
|
<search />
|
||||||
<msg-box />
|
<msg-box />
|
||||||
<!-- 分类标签(双行横滑) -->
|
<!-- 分类标签 -->
|
||||||
<view class="mt-28rpx" v-if="appMerchantLabelList.length > 0">
|
<view class="mt-28rpx" v-if="appMerchantLabelList.length > 0">
|
||||||
<class-bullet :categories="appMerchantLabelList" @itemClick="handleItemClick" />
|
<class-bullet :categories="appMerchantLabelList" @itemClick="handleItemClick" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
<!-- 轮播图:/app/marketActivity/list -->
|
||||||
<swiper
|
<swiper
|
||||||
class="home-promo-swiper card-swiper"
|
v-if="swiperList.length > 0"
|
||||||
:circular="true"
|
class="card-swiper"
|
||||||
:autoplay="true"
|
:circular="swiperList.length > 1"
|
||||||
previous-margin="48rpx"
|
:autoplay="swiperList.length > 1"
|
||||||
next-margin="48rpx"
|
|
||||||
>
|
>
|
||||||
<swiper-item
|
<swiper-item
|
||||||
v-for="(item, sIdx) in swiperList"
|
v-for="(item, sIdx) in swiperList"
|
||||||
@@ -349,21 +394,37 @@ const debouncedEmit = debounce(1300, (isCollected: boolean, id: string, type: Co
|
|||||||
>
|
>
|
||||||
<image
|
<image
|
||||||
:src="item.activityImage"
|
:src="item.activityImage"
|
||||||
class="swiper-item-content w-full h-100% rounded-32rpx bg-common"
|
class="swiper-item-content w-full h-100%"
|
||||||
|
mode="scaleToFill"
|
||||||
></image>
|
></image>
|
||||||
</swiper-item>
|
</swiper-item>
|
||||||
</swiper>
|
</swiper>
|
||||||
|
|
||||||
<!-- 快捷入口(圆形分类) -->
|
<!-- 快捷入口(固定五项,跳转精选菜品专题页) -->
|
||||||
<tabs-type @changeType="tabsTypeChange" v-if="appMerchantCategoryList.length > 0" :list="appMerchantCategoryList" :currentId="currentCategory" class="mt-28rpx home-tabs-quick" />
|
<tabs-type
|
||||||
|
:current-id="currentCategory"
|
||||||
|
class="mt-28rpx mb-24rpx home-tabs-quick"
|
||||||
|
@change-type="tabsTypeChange"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- 筛选工具 -->
|
|
||||||
<!-- <filtrate-tool class="mt-32rpx" @togglePickup="togglePickup" @toggleDiscount="toggleDiscount" @toggleScore="toggleScore" @togglePrice="togglePrice" />-->
|
|
||||||
|
|
||||||
|
<image
|
||||||
|
:src="memberUpgradeBannerSrc"
|
||||||
|
class="w-100% h-[340rpx]"
|
||||||
|
mode="widthFix"
|
||||||
|
@click="navigateTo('/pages-user/pages/member/index')"
|
||||||
|
/>
|
||||||
|
<image
|
||||||
|
:src="deliveryTimeBannerSrc"
|
||||||
|
class="w-100% h-[200rpx] rounded-24rpx mt-4rpx"
|
||||||
|
mode="widthFix"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 精选商家和附近商家 -->
|
||||||
<view class="animate-in fade-in animate-ease-in animate-duration-300" v-if="isShowMerchant">
|
<view class="animate-in fade-in animate-ease-in animate-duration-300" v-if="isShowMerchant">
|
||||||
<!-- Featured on ChefLink 精选商家(浅底 + 横向卡片,对齐设计稿) -->
|
<!-- Featured on ChefLink 精选商家(浅底 + 横向卡片,对齐设计稿) -->
|
||||||
<view v-if="featuredList.length > 0" class="featured-merchants-section mt-36rpx px-24rpx pt-8rpx pb-16rpx">
|
<view v-if="featuredList.length > 0" class="featured-merchants-section mt-36rpx px-24rpx pt-8rpx pb-16rpx">
|
||||||
<view class="mb-24rpx px-6rpx text-32rpx lh-44rpx text-#1a1a1a">
|
<view class="mb-24rpx px-6rpx text-28rpx lh-36rpx text-#1a1a1a">
|
||||||
{{ t('pages.home.featured-on') }}
|
{{ t('pages.home.featured-on') }}
|
||||||
</view>
|
</view>
|
||||||
<featured-on :list="featuredList" />
|
<featured-on :list="featuredList" />
|
||||||
@@ -371,7 +432,7 @@ const debouncedEmit = debounce(1300, (isCollected: boolean, id: string, type: Co
|
|||||||
|
|
||||||
<!-- Nearby Merchants 附近商家 -->
|
<!-- Nearby Merchants 附近商家 -->
|
||||||
<view v-if="nearbyList.length > 0" class="nearby-merchants-block mt-36rpx px-24rpx pb-16rpx">
|
<view v-if="nearbyList.length > 0" class="nearby-merchants-block mt-36rpx px-24rpx pb-16rpx">
|
||||||
<view class="mb-24rpx px-6rpx text-32rpx lh-44rpx text-#1a1a1a ">
|
<view class="mb-24rpx px-6rpx text-28rpx lh-36rpx text-#1a1a1a ">
|
||||||
{{ t('pages.home.nearby-merchants') }}
|
{{ t('pages.home.nearby-merchants') }}
|
||||||
</view>
|
</view>
|
||||||
<nearby-merchants :list="nearbyList" />
|
<nearby-merchants :list="nearbyList" />
|
||||||
@@ -380,7 +441,7 @@ const debouncedEmit = debounce(1300, (isCollected: boolean, id: string, type: Co
|
|||||||
|
|
||||||
<!-- List 精选菜品瀑布流(浅底 + 白卡片 + 阴影,结构对齐设计稿) -->
|
<!-- List 精选菜品瀑布流(浅底 + 白卡片 + 阴影,结构对齐设计稿) -->
|
||||||
<view class="featured-dishes-section mt-36rpx px-24rpx pb-40rpx">
|
<view class="featured-dishes-section mt-36rpx px-24rpx pb-40rpx">
|
||||||
<view class="mb-24rpx px-6rpx text-32rpx lh-44rpx text-#1a1a1a "
|
<view class="mb-24rpx px-6rpx text-28rpx lh-36rpx text-#1a1a1a "
|
||||||
>{{ t('pages.home.featured-dishes') }}</view>
|
>{{ t('pages.home.featured-dishes') }}</view>
|
||||||
<view class="waterfall-row flex gap-16rpx items-start">
|
<view class="waterfall-row flex gap-16rpx items-start">
|
||||||
<view
|
<view
|
||||||
@@ -492,10 +553,6 @@ const debouncedEmit = debounce(1300, (isCollected: boolean, id: string, type: Co
|
|||||||
padding-left: 2rpx;
|
padding-left: 2rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-promo-swiper {
|
|
||||||
margin-top: 8rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-tabs-quick {
|
.home-tabs-quick {
|
||||||
padding-left: 8rpx;
|
padding-left: 8rpx;
|
||||||
padding-right: 8rpx;
|
padding-right: 8rpx;
|
||||||
@@ -511,19 +568,14 @@ const debouncedEmit = debounce(1300, (isCollected: boolean, id: string, type: Co
|
|||||||
}
|
}
|
||||||
|
|
||||||
.card-swiper {
|
.card-swiper {
|
||||||
height: 400rpx;
|
height: 420rpx;
|
||||||
}
|
}
|
||||||
.swiper-item-content {
|
.swiper-item-content {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
transform: scale(0.94);
|
|
||||||
border-radius: 32rpx;
|
|
||||||
box-shadow: 0 8rpx 28rpx rgba(0, 0, 0, 0.08);
|
|
||||||
transition: transform 0.3s ease;
|
transition: transform 0.3s ease;
|
||||||
}
|
}
|
||||||
.swiper-item-active .swiper-item-content {
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 精选商家 / 精选菜品 区块(与页面背景统一) */
|
/* 精选商家 / 精选菜品 区块(与页面背景统一) */
|
||||||
.featured-merchants-section,
|
.featured-merchants-section,
|
||||||
|
|||||||
@@ -42,16 +42,6 @@ const {paging, dataList, loading, queryList, firstLoaded} = usePage<MerchantOrde
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function fillI18nParams(template: string, params: Record<string, string | number>) {
|
|
||||||
let text = template
|
|
||||||
Object.keys(params).forEach((key) => {
|
|
||||||
const value = String(params[key] ?? '')
|
|
||||||
text = text.replace(new RegExp(`\\{${key}\\}`, 'g'), value)
|
|
||||||
text = text.replace(new RegExp(`\\$\\{${key}\\}`, 'g'), value)
|
|
||||||
})
|
|
||||||
return text
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeTimestamp(input: unknown): number | null {
|
function normalizeTimestamp(input: unknown): number | null {
|
||||||
if (input == null || input === '') return null
|
if (input == null || input === '') return null
|
||||||
|
|
||||||
@@ -112,7 +102,7 @@ function getTotalDishCount(item: MerchantOrderVo) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getTotalDishCountText(item: MerchantOrderVo) {
|
function getTotalDishCountText(item: MerchantOrderVo) {
|
||||||
return fillI18nParams(t('pages.order.totalItemCount'), {
|
return t('pages.order.totalItemCount', {
|
||||||
count: getTotalDishCount(item),
|
count: getTotalDishCount(item),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,6 +123,14 @@ export interface CalculatePriceCartBatchBo {
|
|||||||
* 商户小费映射 { merchantId: tipAmount } (小费金额,单位:美元)
|
* 商户小费映射 { merchantId: tipAmount } (小费金额,单位:美元)
|
||||||
*/
|
*/
|
||||||
merchantTipMap?: Record<string, number>;
|
merchantTipMap?: Record<string, number>;
|
||||||
|
/**
|
||||||
|
* 商户预约开始时间映射 { merchantId: timestamp }
|
||||||
|
*/
|
||||||
|
merchantStartScheduledTimeMap?: Record<string, number | string>;
|
||||||
|
/**
|
||||||
|
* 商户预约结束时间映射 { merchantId: timestamp }
|
||||||
|
*/
|
||||||
|
merchantEndScheduledTimeMap?: Record<string, number | string>;
|
||||||
[property: string]: any;
|
[property: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,6 +294,14 @@ export interface CreateOrderCartBatchBo {
|
|||||||
* 商户小费映射 { merchantId: tipAmount } (小费金额,单位:美元)
|
* 商户小费映射 { merchantId: tipAmount } (小费金额,单位:美元)
|
||||||
*/
|
*/
|
||||||
merchantTipMap?: Record<string, number>;
|
merchantTipMap?: Record<string, number>;
|
||||||
|
/**
|
||||||
|
* 商户预约开始时间映射 { merchantId: timestamp }
|
||||||
|
*/
|
||||||
|
merchantStartScheduledTimeMap?: Record<string, number | string>;
|
||||||
|
/**
|
||||||
|
* 商户预约结束时间映射 { merchantId: timestamp }
|
||||||
|
*/
|
||||||
|
merchantEndScheduledTimeMap?: Record<string, number | string>;
|
||||||
[property: string]: any;
|
[property: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3754,6 +3754,8 @@
|
|||||||
'deliveryService'?: number;
|
'deliveryService'?: number;
|
||||||
/** 配送时长(如"30分钟") */
|
/** 配送时长(如"30分钟") */
|
||||||
'deliveryTime'?: string;
|
'deliveryTime'?: string;
|
||||||
|
/** 可配送星期(1-7逗号分隔,1=周一 … 6=周六,7=周日) */
|
||||||
|
'deliveryScheduleTimes'?: string;
|
||||||
/** 配送费 */
|
/** 配送费 */
|
||||||
'deliveryFee'?: number;
|
'deliveryFee'?: number;
|
||||||
/** 店铺图片URL(多张逗号分隔) */
|
/** 店铺图片URL(多张逗号分隔) */
|
||||||
|
|||||||
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 502 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 839 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 249 KiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 802 B |
@@ -35,7 +35,7 @@ export const useUserStore = defineStore(
|
|||||||
const address = ref(null)
|
const address = ref(null)
|
||||||
|
|
||||||
// 预约时间
|
// 预约时间
|
||||||
const appointmentTime = ref('')
|
const appointmentTime = ref<any>(null)
|
||||||
|
|
||||||
// 首页展示的预约时间
|
// 首页展示的预约时间
|
||||||
const appointmentTimeShow = computed(()=> {
|
const appointmentTimeShow = computed(()=> {
|
||||||
@@ -77,7 +77,6 @@ export const useUserStore = defineStore(
|
|||||||
function getAppointmentTime() {
|
function getAppointmentTime() {
|
||||||
if (!isLogin.value) return
|
if (!isLogin.value) return
|
||||||
appAppointmentTimeQueryAppointmentTimePost({}).then(res => {
|
appAppointmentTimeQueryAppointmentTimePost({}).then(res => {
|
||||||
console.log('查询用户的预约时间', res)
|
|
||||||
appointmentTime.value = res.data || ''
|
appointmentTime.value = res.data || ''
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,179 @@
|
|||||||
|
/**
|
||||||
|
* 店铺配送日 deliveryScheduleTimes(接口约定)
|
||||||
|
* 1=周一 … 6=周六,7=周日
|
||||||
|
* 示例:"1,4" 周一、周四;"1,4,7" 含周日
|
||||||
|
*/
|
||||||
|
const API_WEEKDAY_I18N_KEYS: Record<number, string> = {
|
||||||
|
1: "monday",
|
||||||
|
2: "tuesday",
|
||||||
|
3: "wednesday",
|
||||||
|
4: "thursday",
|
||||||
|
5: "friday",
|
||||||
|
6: "saturday",
|
||||||
|
7: "sunday",
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 解析接口原始值为 API 星期数字 1–7 */
|
||||||
|
export function parseApiWeekdayNumbers(raw?: string | null): number[] {
|
||||||
|
if (!raw || typeof raw !== "string") return [];
|
||||||
|
const seen = new Set<number>();
|
||||||
|
const result: number[] = [];
|
||||||
|
raw.split(",").forEach((part) => {
|
||||||
|
const n = Number(part.trim());
|
||||||
|
if (Number.isInteger(n) && n >= 1 && n <= 7 && !seen.has(n)) {
|
||||||
|
seen.add(n);
|
||||||
|
result.push(n);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result.sort((a, b) => a - b);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** API 星期 → Date.getDay()(0=周日 … 6=周六) */
|
||||||
|
export function apiWeekdayToJsDay(apiDay: number): number | null {
|
||||||
|
if (apiDay >= 1 && apiDay <= 6) return apiDay;
|
||||||
|
if (apiDay === 7) return 0;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 解析为内部使用的 JS 星期(供日历可选判断) */
|
||||||
|
export function parseDeliveryScheduleTimes(
|
||||||
|
raw?: string | null
|
||||||
|
): number[] {
|
||||||
|
return parseApiWeekdayNumbers(raw)
|
||||||
|
.map(apiWeekdayToJsDay)
|
||||||
|
.filter((n): n is number => n !== null)
|
||||||
|
.sort((a, b) => a - b);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isDeliveryScheduleDay(
|
||||||
|
date: Date,
|
||||||
|
schedule?: string | null | number[]
|
||||||
|
): boolean {
|
||||||
|
const allowed = Array.isArray(schedule)
|
||||||
|
? schedule
|
||||||
|
: parseDeliveryScheduleTimes(schedule);
|
||||||
|
if (!allowed.length) return true;
|
||||||
|
return allowed.includes(date.getDay());
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MerchantAppointmentSlot = {
|
||||||
|
date?: Date | string;
|
||||||
|
timeSlot?: string;
|
||||||
|
startTime: number;
|
||||||
|
endTime: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 从指定日期起,找最近一天符合配送日规则的可配送日期 */
|
||||||
|
export function findNearestDeliveryScheduleDate(
|
||||||
|
deliveryScheduleTimes?: string | null,
|
||||||
|
fromDate: Date = new Date(),
|
||||||
|
maxDays = 30
|
||||||
|
): Date | null {
|
||||||
|
const base = new Date(fromDate);
|
||||||
|
base.setHours(0, 0, 0, 0);
|
||||||
|
for (let i = 0; i <= maxDays; i++) {
|
||||||
|
const d = new Date(base);
|
||||||
|
d.setDate(base.getDate() + i);
|
||||||
|
if (isDeliveryScheduleDay(d, deliveryScheduleTimes)) {
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按自然日生成默认预约时段(全天) */
|
||||||
|
export function buildDayAppointmentSlot(date: Date): MerchantAppointmentSlot {
|
||||||
|
const start = new Date(date);
|
||||||
|
start.setHours(0, 0, 0, 0);
|
||||||
|
const end = new Date(date);
|
||||||
|
end.setHours(23, 59, 59, 999);
|
||||||
|
return {
|
||||||
|
date,
|
||||||
|
timeSlot: "",
|
||||||
|
startTime: start.getTime(),
|
||||||
|
endTime: end.getTime(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 格式化配送日列表,用于店铺/商品详情展示 */
|
||||||
|
export function formatDeliveryScheduleDays(
|
||||||
|
raw: string | undefined | null,
|
||||||
|
weekdayLabel: (key: string) => string
|
||||||
|
): string {
|
||||||
|
const apiDays = parseApiWeekdayNumbers(raw);
|
||||||
|
if (!apiDays.length) return "";
|
||||||
|
return apiDays
|
||||||
|
.map((d) => {
|
||||||
|
const key = API_WEEKDAY_I18N_KEYS[d];
|
||||||
|
return key
|
||||||
|
? weekdayLabel(`pages-store.store.weekdays.${key}`)
|
||||||
|
: "";
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("、");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildReservationTimeUrl(options: {
|
||||||
|
merchantId?: string | number;
|
||||||
|
deliveryScheduleTimes?: string;
|
||||||
|
businessHours?: string;
|
||||||
|
}): string {
|
||||||
|
const params: string[] = [];
|
||||||
|
if (options.merchantId != null && options.merchantId !== "") {
|
||||||
|
params.push(`merchantId=${encodeURIComponent(String(options.merchantId))}`);
|
||||||
|
}
|
||||||
|
if (options.deliveryScheduleTimes) {
|
||||||
|
params.push(
|
||||||
|
`deliveryScheduleTimes=${encodeURIComponent(options.deliveryScheduleTimes)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (options.businessHours) {
|
||||||
|
params.push(
|
||||||
|
`storeBusinessHours=${encodeURIComponent(options.businessHours)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const qs = params.length ? `?${params.join("&")}` : "";
|
||||||
|
return `/pages/address/reservation-time${qs}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CHECKOUT_MERCHANT_APPOINTMENTS_KEY =
|
||||||
|
"checkout_merchant_appointments";
|
||||||
|
|
||||||
|
export function saveCheckoutMerchantAppointments(
|
||||||
|
map: Record<string, MerchantAppointmentSlot>
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
uni.setStorageSync(
|
||||||
|
CHECKOUT_MERCHANT_APPOINTMENTS_KEY,
|
||||||
|
JSON.stringify(map)
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadCheckoutMerchantAppointments(): Record<
|
||||||
|
string,
|
||||||
|
MerchantAppointmentSlot
|
||||||
|
> {
|
||||||
|
try {
|
||||||
|
const raw = uni.getStorageSync(CHECKOUT_MERCHANT_APPOINTMENTS_KEY);
|
||||||
|
if (!raw) return {};
|
||||||
|
const parsed = JSON.parse(String(raw));
|
||||||
|
return parsed && typeof parsed === "object" ? parsed : {};
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatAppointmentPillLabel(
|
||||||
|
slot: MerchantAppointmentSlot | undefined,
|
||||||
|
fallback: string
|
||||||
|
): string {
|
||||||
|
if (!slot?.startTime) return fallback;
|
||||||
|
const start = new Date(Number(slot.startTime));
|
||||||
|
if (Number.isNaN(start.getTime())) return fallback;
|
||||||
|
const m = `${String(start.getMonth() + 1).padStart(2, "0")}-${String(start.getDate()).padStart(2, "0")}`;
|
||||||
|
const weekdays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
||||||
|
return `${weekdays[start.getDay()]}, ${m.replace("-", "/")} >`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
/** 精选菜品列表项展示工具(与首页瀑布流一致) */
|
||||||
|
|
||||||
|
export function getFeaturedDishImage(item: Record<string, unknown>): string {
|
||||||
|
const raw = item.dishImageUrl ?? item.dishImage
|
||||||
|
if (typeof raw !== 'string' || !raw.trim()) return ''
|
||||||
|
return raw.split(',')[0].trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isFeaturedDishSoldOut(stockLike: unknown): boolean {
|
||||||
|
const n = Number(stockLike)
|
||||||
|
return !Number.isNaN(n) && n <= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFeaturedDishPromoLabel(item: Record<string, unknown>): string {
|
||||||
|
const raw = item.marketingLabel ?? item.hotSaleTag ?? item.rankTag ?? item.promotionLabel
|
||||||
|
return typeof raw === 'string' && raw.trim() ? raw.trim() : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 解析 featuredDishList 分页响应(兼容 rows 在根或 data 下) */
|
||||||
|
export function parseFeaturedDishListRes(res: unknown): {
|
||||||
|
rows: Record<string, unknown>[]
|
||||||
|
total: number
|
||||||
|
} {
|
||||||
|
if (!res || typeof res !== 'object') {
|
||||||
|
return { rows: [], total: 0 }
|
||||||
|
}
|
||||||
|
const r = res as Record<string, unknown>
|
||||||
|
const data = r.data
|
||||||
|
let rows: unknown[] = []
|
||||||
|
if (Array.isArray(r.rows)) {
|
||||||
|
rows = r.rows
|
||||||
|
} else if (data && typeof data === 'object' && Array.isArray((data as Record<string, unknown>).rows)) {
|
||||||
|
rows = (data as Record<string, unknown>).rows as unknown[]
|
||||||
|
}
|
||||||
|
const totalRaw = r.total ?? (data && typeof data === 'object' ? (data as Record<string, unknown>).total : undefined)
|
||||||
|
const total = Number(totalRaw)
|
||||||
|
const rowList = rows as Record<string, unknown>[]
|
||||||
|
let resolvedTotal = Number.isFinite(total) ? total : rowList.length
|
||||||
|
// 部分接口有 rows 但 total 为 0,避免 z-paging 把 total 当成 success 后判失败
|
||||||
|
if (resolvedTotal <= 0 && rowList.length > 0) {
|
||||||
|
resolvedTotal = rowList.length
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
rows: rowList,
|
||||||
|
total: resolvedTotal,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,6 @@ type IRes<T> = T extends ArrayBuffer ? ArrayBuffer : IResData<T>
|
|||||||
|
|
||||||
|
|
||||||
export const http = <T>(options: CustomRequestOptions) => {
|
export const http = <T>(options: CustomRequestOptions) => {
|
||||||
console.log(options, 'options')
|
|
||||||
// 1. 返回 Promise 对象
|
// 1. 返回 Promise 对象
|
||||||
return new Promise<IRes<T>>((resolve, reject) => {
|
return new Promise<IRes<T>>((resolve, reject) => {
|
||||||
uni.request({
|
uni.request({
|
||||||
@@ -15,7 +14,6 @@ export const http = <T>(options: CustomRequestOptions) => {
|
|||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
// 响应成功
|
// 响应成功
|
||||||
success(res) {
|
success(res) {
|
||||||
console.log(res, '111111')
|
|
||||||
|
|
||||||
const ErrorMessage = {
|
const ErrorMessage = {
|
||||||
401: i18n.global.t('common.prompt.authentication-failed-please-log-in'),
|
401: i18n.global.t('common.prompt.authentication-failed-please-log-in'),
|
||||||
|
|||||||
@@ -456,3 +456,49 @@ export function tWithParams(
|
|||||||
|
|
||||||
return str
|
return str
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type MerchantCartPayload = {
|
||||||
|
items: any[]
|
||||||
|
deliveryScheduleTimes?: string
|
||||||
|
merchantId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 listByMerchantId 等接口的 data:
|
||||||
|
* - 数组:直接为购物车项列表
|
||||||
|
* - 对象:{ merchantId, deliveryScheduleTimes, merchantCartVoList }
|
||||||
|
*/
|
||||||
|
export function parseMerchantCartPayload(data: unknown): MerchantCartPayload {
|
||||||
|
if (data == null) return { items: [] }
|
||||||
|
if (Array.isArray(data)) return { items: data }
|
||||||
|
if (typeof data !== 'object') return { items: [] }
|
||||||
|
|
||||||
|
const o = data as Record<string, unknown>
|
||||||
|
let items: any[] = []
|
||||||
|
|
||||||
|
if (Array.isArray(o.merchantCartVoList)) {
|
||||||
|
items = o.merchantCartVoList
|
||||||
|
} else if (Array.isArray(o.rows)) {
|
||||||
|
items = o.rows
|
||||||
|
} else if (Array.isArray(o.list)) {
|
||||||
|
items = o.list
|
||||||
|
} else if (Array.isArray(o.data)) {
|
||||||
|
items = o.data
|
||||||
|
} else if (o.id != null && (o.dishId != null || o.merchantDishVo != null)) {
|
||||||
|
items = [o]
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
items,
|
||||||
|
deliveryScheduleTimes:
|
||||||
|
typeof o.deliveryScheduleTimes === 'string'
|
||||||
|
? o.deliveryScheduleTimes
|
||||||
|
: undefined,
|
||||||
|
merchantId: o.merchantId != null ? String(o.merchantId) : undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @deprecated 请优先使用 parseMerchantCartPayload */
|
||||||
|
export function normalizeMerchantCartList(data: unknown): any[] {
|
||||||
|
return parseMerchantCartPayload(data).items
|
||||||
|
}
|
||||||
@@ -9,7 +9,6 @@ import UniLayouts from '@uni-helper/vite-plugin-uni-layouts'
|
|||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default ({command, mode}) => {
|
export default ({command, mode}) => {
|
||||||
const {UNI_PLATFORM} = process.env
|
const {UNI_PLATFORM} = process.env
|
||||||
console.log('UNI_PLATFORM -> ', UNI_PLATFORM) // 得到 mp-weixin, h5, app 等
|
|
||||||
|
|
||||||
const env = loadEnv(mode, path.resolve(process.cwd(), 'env'))
|
const env = loadEnv(mode, path.resolve(process.cwd(), 'env'))
|
||||||
const {
|
const {
|
||||||
@@ -20,9 +19,6 @@ export default ({command, mode}) => {
|
|||||||
VITE_APP_PROXY,
|
VITE_APP_PROXY,
|
||||||
VITE_APP_PROXY_PREFIX
|
VITE_APP_PROXY_PREFIX
|
||||||
} = env
|
} = env
|
||||||
console.log('环境变量 env -> ', env)
|
|
||||||
|
|
||||||
|
|
||||||
return defineConfig({
|
return defineConfig({
|
||||||
envDir: './env', // 自定义env目录
|
envDir: './env', // 自定义env目录
|
||||||
css: {
|
css: {
|
||||||
|
|||||||