Merge pull request !2 from ISFP_T/new_ui
This commit is contained in:
2026-04-15 01:27:52 +00:00
committed by Gitee
71 changed files with 10761 additions and 3139 deletions
+1 -1
View File
@@ -5,6 +5,6 @@ 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.5.200:8080 #VITE_SERVER_BASEURL=http://192.168.5.4:8080
#VITE_SERVER_BASEURL=http://192.168.0.148:8888 #VITE_SERVER_BASEURL=http://192.168.0.148:8888
#VITE_SERVER_BASEURL=http://liuyao.nat100.top/meiguowaimai #VITE_SERVER_BASEURL=http://liuyao.nat100.top/meiguowaimai
+35 -7
View File
@@ -1,6 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
// import {upload} from '@/utils/upload/alioss' import { upload } from '@/utils/upload/alioss'
import { uploadToS3 } from '@/utils/upload/ymx'
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@@ -59,8 +58,7 @@ function chooseImage(type: 'album' | 'camera') {
// }) // })
// //
// for (let i = 0; i < files.length; i++) { // for (let i = 0; i < files.length; i++) {
// // asyncList.push(upload(files[i])) // asyncList.push(upload(files[i]))
// asyncList.push(uploadToS3(files[i]))
// console.log(asyncList) // console.log(asyncList)
// } // }
// //
@@ -101,9 +99,7 @@ function chooseImage(type: 'album' | 'camera') {
mask: true, mask: true,
}) })
for (let i = 0; i < files.length; i++) { for (let i = 0; i < files.length; i++) {
// asyncList.push(upload(files[i])) asyncList.push(upload(files[i]))
asyncList.push(uploadToS3(files[i]))
console.log(asyncList)
} }
Promise.all(asyncList) Promise.all(asyncList)
.then((results) => { .then((results) => {
@@ -116,6 +112,38 @@ function chooseImage(type: 'album' | 'camera') {
} }
}) })
// #endif // #endif
// #ifndef APP-PLUS
uni.chooseImage({
count: props.count,
sizeType: ['compressed'],
sourceType: type === 'album' ? ['album'] : ['camera'],
success: async (res) => {
let files = [...res.tempFilePaths]
if (props.count > 0) {
files = files.slice(0, props.count)
}
if (!props.isUpload) {
return emits('change', files)
}
await uni.showLoading({
title: t('common.loading') + '...',
mask: true,
})
const asyncList: Promise<unknown>[] = []
for (let i = 0; i < files.length; i++) {
asyncList.push(upload(files[i]))
}
Promise.all(asyncList)
.then((results) => {
emits('change', results)
})
.finally(() => {
uni.hideLoading()
})
},
})
// #endif
} }
+132 -2
View File
@@ -1,8 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted } from 'vue'
import Config from '@/config' import Config from '@/config'
import {throttle} from 'throttle-debounce' import {throttle} from 'throttle-debounce'
import {useUserStore} from '@/store'
const {t} = useI18n() const {t} = useI18n()
const userStore = useUserStore()
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@@ -10,11 +13,17 @@ const props = withDefaults(
disabled?: boolean disabled?: boolean
focus?: boolean focus?: boolean
placeholder?: string placeholder?: string
/** 搜索落地页:右侧圆形购物车,与返回键对称 */
trailingCart?: boolean
/** trailingCart 为 true 时:gray=搜索首页浅底;white=结果页白底(与设计稿一致) */
trailingSurface?: 'gray' | 'white'
}>(), }>(),
{ {
modelValue: '', modelValue: '',
disabled: false, disabled: false,
focus: false, focus: false,
trailingCart: false,
trailingSurface: 'gray',
}, },
) )
@@ -39,6 +48,35 @@ function handleClickLeft() {
uni.navigateBack() uni.navigateBack()
} }
function goCart() {
if (userStore.checkLogin()) {
uni.navigateTo({ url: '/pages-user/pages/cart/index' })
}
}
const cartBadgeTotal = computed(() => {
const list = userStore.userCartAllData
if (!Array.isArray(list) || list.length === 0) return 0
let n = 0
for (const m of list) {
n += (m as { merchantCartVoList?: unknown[] })?.merchantCartVoList?.length || 0
}
return n
})
onMounted(() => {
if (props.trailingCart) {
userStore.getUserCartAllData()
}
})
const headerRootClass = computed(() => {
if (!props.trailingCart) return 'header-search header-search--default'
return props.trailingSurface === 'white'
? 'header-search header-search--trailing-white'
: 'header-search header-search--landing'
})
defineOptions({ defineOptions({
name: 'HeaderSearch', name: 'HeaderSearch',
@@ -46,10 +84,52 @@ defineOptions({
</script> </script>
<template> <template>
<view <view
class="relative z-1 bg-#fff" :class="headerRootClass"
> >
<status-bar/> <status-bar/>
<view class="flex-center-sb px-30rpx h-88rpx"> <view
v-if="trailingCart"
class="header-search__row header-search__row--triple px-24rpx pt-8rpx pb-12rpx"
>
<view
class="header-search__round-btn center shrink-0"
:class="trailingSurface === 'white' ? 'header-search__round-btn--muted' : ''"
@click="handleClickLeft"
>
<view class="i-carbon:chevron-left text-34rpx text-#14181b"></view>
</view>
<view class="header-search__pill flex-1 min-w-0 h-80rpx flex items-center px-28rpx">
<image src="@img/chef/100222.png" class="w-28rpx h-28rpx mr-14rpx shrink-0"></image>
<wd-input
no-border
clearable
:focus-when-clear="false"
:disabled="disabled"
:focus="focus"
confirm-type="search"
custom-class="flex items-center !text-28rpx !bg-transparent flex-1 min-w-0"
placeholderStyle="font-size: 28rpx;color: #999999; font-weight: 500;"
:modelValue="modelValue"
:placeholder="placeholder || t('common.prompt.please-enter-keyword-search')"
@update:modelValue="handleInputUpdateModelValue"
@confirm="handleSearch"
>
</wd-input>
</view>
<view
class="header-search__round-btn header-search__round-btn--cart center shrink-0"
:class="trailingSurface === 'white' ? 'header-search__round-btn--muted' : ''"
@click="goCart"
>
<view class="i-carbon:shopping-cart text-34rpx text-#14181b"></view>
<view
v-if="userStore.isLogin && cartBadgeTotal > 0"
class="header-search__cart-badge"
>{{ cartBadgeTotal > 99 ? '99+' : cartBadgeTotal }}</view
>
</view>
</view>
<view v-else class="flex-center-sb px-30rpx h-88rpx">
<view class="shrink-0" @click="handleClickLeft"> <view class="shrink-0" @click="handleClickLeft">
<view class="i-fluent:ios-arrow-ltr-24-filled text-36rpx text-#333"></view> <view class="i-fluent:ios-arrow-ltr-24-filled text-36rpx text-#333"></view>
</view> </view>
@@ -79,6 +159,56 @@ defineOptions({
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.header-search--landing {
background: #f7f8fa;
}
.header-search--trailing-white {
background: #fff;
}
.header-search--default {
position: relative;
z-index: 1;
background: #fff;
}
.header-search__row--triple {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
}
.header-search__round-btn {
width: 72rpx;
height: 72rpx;
border-radius: 50%;
background: #fff;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
}
.header-search__round-btn--muted {
background: #f0f0f0;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
}
.header-search__round-btn--cart {
position: relative;
}
.header-search__cart-badge {
position: absolute;
top: 6rpx;
right: 6rpx;
min-width: 28rpx;
height: 28rpx;
padding: 0 6rpx;
font-size: 18rpx;
line-height: 28rpx;
font-weight: 600;
color: #fff;
text-align: center;
background: #e23636;
border-radius: 999rpx;
}
.header-search__pill {
background: #f2f2f2;
border-radius: 999rpx;
}
:deep(.wd-input__clear) { :deep(.wd-input__clear) {
background: transparent !important; background: transparent !important;
} }
+23 -2
View File
@@ -4,9 +4,12 @@ const props = withDefaults(defineProps<{
fixed?: boolean fixed?: boolean
showLeft?: boolean showLeft?: boolean
customClass?: string customClass?: string
/** 左侧返回为白底圆形+阴影(用于预约日期等浅色页) */
circleBack?: boolean
}>(), { }>(), {
fixed: true, fixed: true,
showLeft: true showLeft: true,
circleBack: false
}); });
function handleClickLeft() { function handleClickLeft() {
@@ -31,7 +34,17 @@ function handleClickLeft() {
@click-left="handleClickLeft"> @click-left="handleClickLeft">
<template #left> <template #left>
<view class="shrink-0" v-if="showLeft"> <view class="shrink-0" v-if="showLeft">
<view class="i-carbon:chevron-left text-50rpx text-primary ml-[-10rpx]"></view> <view
class="flex items-center justify-center"
:class="circleBack ? 'navbar-circle-back' : ''"
>
<view
:class="[
'i-carbon:chevron-left text-50rpx',
circleBack ? 'ml-0 text-#111' : 'ml-[-10rpx] text-primary',
]"
></view>
</view>
</view> </view>
</template> </template>
<template #right> <template #right>
@@ -41,6 +54,14 @@ function handleClickLeft() {
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.navbar-circle-back {
width: 72rpx;
height: 72rpx;
border-radius: 50%;
background-color: #fff;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08);
}
:deep(.wd-navbar) { :deep(.wd-navbar) {
z-index: 2 !important; z-index: 2 !important;
.wd-navbar__title { .wd-navbar__title {
+2
View File
@@ -23,6 +23,8 @@ const Config = {
shareLink: "https://www.howhowfresh.com/h5/", shareLink: "https://www.howhowfresh.com/h5/",
shareImage: "https://hanguomcn.oss-ap-northeast-2.aliyuncs.com/images/20250901105756_1756695476183_56ac07ac.png", shareImage: "https://hanguomcn.oss-ap-northeast-2.aliyuncs.com/images/20250901105756_1756695476183_56ac07ac.png",
shareDesc: "分享专属二维码邀请新用户注册即邀请成功。一起来注册体验点餐功能。", shareDesc: "分享专属二维码邀请新用户注册即邀请成功。一起来注册体验点餐功能。",
// Zelle 支付二维码图片地址
zellePayPath: "https://www.howhowfresh.com/minio/ruoyi/2026/04/14/ef120c97e50c419da2263a59d6679229.png",
// 登录端口 // 登录端口
userPort: 1, userPort: 1,
// 手机区号数组 // 手机区号数组
+90 -8
View File
@@ -171,7 +171,7 @@
"officeDescription": "Workplaces with entry restrictions", "officeDescription": "Workplaces with entry restrictions",
"other": "Other", "other": "Other",
"otherDescription": "Hospitals, parks, outdoors, etc", "otherDescription": "Hospitals, parks, outdoors, etc",
"title": "Select building type" "title": "Building type selection"
}, },
"deliveryInstructions": "Delivery Instructions", "deliveryInstructions": "Delivery Instructions",
"deliveryPointInfo": "Delivery Point Information", "deliveryPointInfo": "Delivery Point Information",
@@ -206,14 +206,19 @@
"dateNotSelectable": "This date is not selectable", "dateNotSelectable": "This date is not selectable",
"noAvailableTime": "No available time for current date, automatically selected next business day", "noAvailableTime": "No available time for current date, automatically selected next business day",
"notAvailable": "Not available", "notAvailable": "Not available",
"nextWeek": "Next week",
"pageTitle": "Delivery date",
"reservationSuccess": "Reservation successful", "reservationSuccess": "Reservation successful",
"selectTimeSlot": "Please select a time slot" "selectTimeSlot": "Please select a time slot",
"thisWeek": "This week"
}, },
"savedAddresses": "Saved addresses", "savedAddresses": "Saved addresses",
"title": "Address", "title": "Address",
"titleDetail": "Address Details" "titleDetail": "Address Details"
}, },
"browse": { "browse": {
"brandTag": "CHEFLINK Selected",
"moreRecipes": "More Recipes",
"titleCuisine": "Nearby Cuisine", "titleCuisine": "Nearby Cuisine",
"titleRecipes": "Selected Recipes" "titleRecipes": "Selected Recipes"
}, },
@@ -228,7 +233,10 @@
"mine": { "mine": {
"activity-description": "Activity description", "activity-description": "Activity description",
"activity-description-tip": "Share the QR code and invite new users to register successfully. Successful invitation", "activity-description-tip": "Share the QR code and invite new users to register successfully. Successful invitation",
"collection": "Collection", "collection": "My Collection",
"collectionBatchDeleteConfirm": "Remove {count} selected favorite(s)?",
"collectionBatchModeHint": "Select items to remove, then tap the trash again to delete",
"collectionSelectFirst": "Please select favorites to remove",
"complaintsAndSuggestions": "Complaints and suggestions", "complaintsAndSuggestions": "Complaints and suggestions",
"customer-service-phone": "Customer service phone", "customer-service-phone": "Customer service phone",
"dial": "Dial", "dial": "Dial",
@@ -261,17 +269,22 @@
"PU": "PU", "PU": "PU",
"accept": "ACPT", "accept": "ACPT",
"all": "ALL", "all": "ALL",
"cancelOrder": "Cancel",
"completed": "DCOMP", "completed": "DCOMP",
"confirmReady": "CONF/READY", "confirmReady": "CONF/READY",
"onTheWay": "OTW", "onTheWay": "OTW",
"title": "History" "title": "History",
"totalItemCount": "{count} items"
}, },
"store": { "store": {
"all": "All" "all": "All"
}, },
"search": { "search": {
"hot-title": "Popular Categories", "hot-title": "Hot searches",
"recently": "Recently", "clear-history": "Clear",
"recently": "Search history",
"weekly-sales": "Weekly sales",
"member-price-line": "Member",
"result": { "result": {
"food": "Food", "food": "Food",
"recipe": "Recipe", "recipe": "Recipe",
@@ -364,6 +377,36 @@
"checkout": { "checkout": {
"addAddress": "Add address", "addAddress": "Add address",
"addCoupon": "Add coupon", "addCoupon": "Add coupon",
"confirmOrder": "Confirm order",
"couponInputPlaceholder": "Enter coupon code",
"deliverBefore": "Arriving before {time}",
"deliveryInfo": "Delivery",
"driverTip": "Driver tip",
"driverTipNote": "100% of your tip goes to the driver.",
"fillAddressHint": "Add delivery address and contact",
"freeTag": "Free",
"localDelivery": "Local delivery",
"localDeliverySubtitle": "Delivered by {name}.",
"memberThanks": "Thanks for being a {name} member.",
"orderTotalLine": "Order total",
"orderBreakdown": "Order details",
"pay": "Pay",
"payMethodSection": "Payment method",
"pickupSubtitle": "Pick up at the store at your scheduled time.",
"pickupTitle": "Pickup",
"scheduledDeliveryHint": "Delivery fee refunded if we miss the promised window.",
"scheduledDeliveryTitle": "Scheduled delivery",
"scheduledDeliveryWindow": "By {time}",
"scheduledDeliveryAddon": "+ $ 2.99",
"scheduledDeliveryStandardTitle": "Standard delivery",
"scheduledDeliveryStandardDesc": "Delivered in your selected window. No rush fee.",
"serviceFee": "Service fee",
"shippingFee": "Delivery fee",
"subtotalBar": "Subtotal",
"subtotalOneLine": "Subtotal: $ {amount}",
"subtotalWithPieces": "Items total ({count})",
"itemsGoodsTotalWithPrice": "{count} items total ${amount}",
"useDiscount": "Discounts",
"appointmentDelivery": "Appointment delivery", "appointmentDelivery": "Appointment delivery",
"appointmentPickup": "Appointment pickup", "appointmentPickup": "Appointment pickup",
"chooseTime": "Choose the time", "chooseTime": "Choose the time",
@@ -406,6 +449,7 @@
"deliveryAddress": "Delivery address", "deliveryAddress": "Delivery address",
"deliveryPhotos": "Delivery of photos", "deliveryPhotos": "Delivery of photos",
"deliveryTime": "Delivery time", "deliveryTime": "Delivery time",
"beforeDeadline": " before",
"estimatedDeliveryTime": "Estimated delivery time", "estimatedDeliveryTime": "Estimated delivery time",
"orderInfo": "Ordering Information", "orderInfo": "Ordering Information",
"orderNumber": "Order number", "orderNumber": "Order number",
@@ -435,7 +479,11 @@
"total": "Total", "total": "Total",
"upTime": "Estimated self-pickup time", "upTime": "Estimated self-pickup time",
"useCode": "Use code", "useCode": "Use code",
"writeOff": "Meal Code" "writeOff": "Meal Code",
"uploadPaidVoucher": "I've paid — upload proof",
"voucherSubmitSuccess": "Proof submitted",
"voucherSubmitFailed": "Submit failed, please try again",
"voucherUploadFailed": "Upload failed, please try again"
}, },
"store": { "store": {
"addToCart": "Add to cart", "addToCart": "Add to cart",
@@ -490,6 +538,23 @@
"recommend": "Recommended for you", "recommend": "Recommended for you",
"required": "Required", "required": "Required",
"sales": "Sales", "sales": "Sales",
"dishDetail": {
"productDetails": "Product details",
"productIntro": "Introduction",
"origin": "Origin",
"unitQty": "Unit qty.",
"category": "Category",
"brand": "Brand",
"allergen": "Allergen",
"weeklySales": "Weekly sales",
"forYou": "Recommended for you",
"memberPriceLabel": "{name} member price",
"dash": "—",
"lbUnit": "lb",
"hotSalesBadge": "Fresh picks",
"stockLabel": "Stock",
"loadMoreRecommend": "Load more"
},
"securityCode": { "securityCode": {
"cantSee": "Can't see clearly? Change one", "cantSee": "Can't see clearly? Change one",
"confirm": "Confirm" "confirm": "Confirm"
@@ -557,7 +622,23 @@
"title": "Cart", "title": "Cart",
"totalPrice": "Total price", "totalPrice": "Total price",
"viewCart": "View Shopping Cart", "viewCart": "View Shopping Cart",
"viewStore": "View Store" "viewStore": "View Store",
"localDelivery": "Local delivery",
"deliveryUnified": "Orders are delivered by {name}.",
"itemsTotalWithPrice": "{count} items total ${amount}",
"deliveryFeeLine": "Delivery",
"serviceFeeLine": "Service fee",
"subtotalLine": "Subtotal",
"selectAll": "Select all",
"forYou": "Recommended for you",
"calculating": "Calculating...",
"selectItemsHint": "Select items to checkout",
"removeCartTitle": "Delete shopping cart?",
"removeCartDesc": "Are you sure you want to delete this cart?",
"removeProductTitle": "Remove the product?",
"removeProductDesc": "Are you sure you want to remove {name} from your shopping cart?",
"emptyTitle": "Your cart is empty",
"emptyAction": "Explore Restaurants"
}, },
"choosePaymethod": { "choosePaymethod": {
"creditCard": "Credit card payment", "creditCard": "Credit card payment",
@@ -585,6 +666,7 @@
"coupon": { "coupon": {
"all-merchants": "Applicable to all merchants", "all-merchants": "Applicable to all merchants",
"expiry-date": "Expiry date: ", "expiry-date": "Expiry date: ",
"merchant-only": "Only available at {name}",
"merchant-specific": "For specific merchant use", "merchant-specific": "For specific merchant use",
"no-coupons": "You currently do not have any coupons", "no-coupons": "You currently do not have any coupons",
"redeem-now": "Redeem now", "redeem-now": "Redeem now",
+90 -8
View File
@@ -171,7 +171,7 @@
"officeDescription": "有进入限制的工作场所", "officeDescription": "有进入限制的工作场所",
"other": "其他", "other": "其他",
"otherDescription": "医院、公园、户外等", "otherDescription": "医院、公园、户外等",
"title": "选择建筑类型" "title": "建筑类型选择"
}, },
"deliveryInstructions": "配送说明", "deliveryInstructions": "配送说明",
"deliveryPointInfo": "交货点信息", "deliveryPointInfo": "交货点信息",
@@ -206,14 +206,19 @@
"dateNotSelectable": "该日期不可选择", "dateNotSelectable": "该日期不可选择",
"noAvailableTime": "当前日期无可用时间,已自动选择下一个营业日", "noAvailableTime": "当前日期无可用时间,已自动选择下一个营业日",
"notAvailable": "不营业", "notAvailable": "不营业",
"nextWeek": "下周",
"pageTitle": "送货日",
"reservationSuccess": "预约成功", "reservationSuccess": "预约成功",
"selectTimeSlot": "请选择时间段" "selectTimeSlot": "请选择时间段",
"thisWeek": "本周"
}, },
"savedAddresses": "保存的地址", "savedAddresses": "保存的地址",
"title": "地址管理", "title": "地址管理",
"titleDetail": "地址详情" "titleDetail": "地址详情"
}, },
"browse": { "browse": {
"brandTag": "CHEFLINK 严选",
"moreRecipes": "更多食谱",
"titleCuisine": "附近的美食", "titleCuisine": "附近的美食",
"titleRecipes": "选择食谱" "titleRecipes": "选择食谱"
}, },
@@ -228,7 +233,10 @@
"mine": { "mine": {
"activity-description": "活动说明:", "activity-description": "活动说明:",
"activity-description-tip": "分享专属二维码邀请新用户注册即邀请成功。一起来注册体验点餐功能。", "activity-description-tip": "分享专属二维码邀请新用户注册即邀请成功。一起来注册体验点餐功能。",
"collection": "收藏", "collection": "我的收藏",
"collectionBatchDeleteConfirm": "确定删除已选中的 {count} 条收藏?",
"collectionBatchModeHint": "请选择要删除的收藏,再次点击垃圾桶执行删除",
"collectionSelectFirst": "请先选择要删除的收藏",
"complaintsAndSuggestions": "投诉建议", "complaintsAndSuggestions": "投诉建议",
"customer-service-phone": "客服电话", "customer-service-phone": "客服电话",
"dial": "拨号", "dial": "拨号",
@@ -261,17 +269,22 @@
"PU": "自取", "PU": "自取",
"accept": "接受", "accept": "接受",
"all": "全部", "all": "全部",
"cancelOrder": "取消订单",
"completed": "已完成", "completed": "已完成",
"confirmReady": "确认/已准备", "confirmReady": "确认/已准备",
"onTheWay": "在路上", "onTheWay": "在路上",
"title": "订单" "title": "订单",
"totalItemCount": "共{count}件"
}, },
"store": { "store": {
"all": "全部" "all": "全部"
}, },
"search": { "search": {
"hot-title": "热", "hot-title": "热门搜索",
"recently": "历史记录", "clear-history": "清空",
"recently": "搜索记录",
"weekly-sales": "周销量",
"member-price-line": "会员价",
"result": { "result": {
"food": "美食", "food": "美食",
"recipe": "菜谱", "recipe": "菜谱",
@@ -364,6 +377,36 @@
"checkout": { "checkout": {
"addAddress": "新增地址", "addAddress": "新增地址",
"addCoupon": "添加优惠券", "addCoupon": "添加优惠券",
"confirmOrder": "确认订单",
"couponInputPlaceholder": "请输入优惠券",
"deliverBefore": "将在 {time} 之前送达",
"deliveryInfo": "配送信息",
"driverTip": "司机小费",
"driverTipNote": "您所支付的小费 100% 归司机所有",
"fillAddressHint": "请填写您的送货地址及联系方式",
"freeTag": "免费",
"localDelivery": "本地配送",
"localDeliverySubtitle": "商品将由 {name} 统一配送!",
"memberThanks": "感谢您成为 {name} 会员。",
"orderTotalLine": "订单总计",
"orderBreakdown": "订单明细",
"pay": "付款",
"payMethodSection": "支付方式",
"pickupSubtitle": "请按预约时间到店取餐。",
"pickupTitle": "门店自取",
"scheduledDeliveryHint": "若未在承诺时间内送达,运费将予以退还。",
"scheduledDeliveryTitle": "定时送达服务",
"scheduledDeliveryWindow": "{time} 前送达",
"scheduledDeliveryAddon": "+ $ 2.99",
"scheduledDeliveryStandardTitle": "标准送达",
"scheduledDeliveryStandardDesc": "按预约时段配送,无加急费用。",
"serviceFee": "服务费",
"shippingFee": "运费",
"subtotalBar": "小计",
"subtotalOneLine": "小计: $ {amount}",
"subtotalWithPieces": "总计({count}件)",
"itemsGoodsTotalWithPrice": "{count}件商品总共${amount}",
"useDiscount": "使用优惠",
"appointmentDelivery": "预约配送", "appointmentDelivery": "预约配送",
"appointmentPickup": "预约自取", "appointmentPickup": "预约自取",
"chooseTime": "选择时间", "chooseTime": "选择时间",
@@ -406,6 +449,7 @@
"deliveryAddress": "配送地址", "deliveryAddress": "配送地址",
"deliveryPhotos": "送达照片", "deliveryPhotos": "送达照片",
"deliveryTime": "送达时间", "deliveryTime": "送达时间",
"beforeDeadline": "前",
"estimatedDeliveryTime": "预计送达时间", "estimatedDeliveryTime": "预计送达时间",
"orderInfo": "订单信息", "orderInfo": "订单信息",
"orderNumber": "订单号", "orderNumber": "订单号",
@@ -435,7 +479,11 @@
"total": "总计", "total": "总计",
"upTime": "预计自取时间", "upTime": "预计自取时间",
"useCode": "核销码", "useCode": "核销码",
"writeOff": "核销" "writeOff": "核销",
"uploadPaidVoucher": "我已支付,上传凭证",
"voucherSubmitSuccess": "凭证已提交",
"voucherSubmitFailed": "提交失败,请重试",
"voucherUploadFailed": "上传失败,请重试"
}, },
"store": { "store": {
"addToCart": "加入购物车", "addToCart": "加入购物车",
@@ -490,6 +538,23 @@
"recommend": "推荐给您", "recommend": "推荐给您",
"required": "必选项", "required": "必选项",
"sales": "销量", "sales": "销量",
"dishDetail": {
"productDetails": "产品详情",
"productIntro": "产品简介",
"origin": "产地",
"unitQty": "单位数量",
"category": "分类",
"brand": "品牌",
"allergen": "过敏源",
"weeklySales": "周销量",
"forYou": "为您推荐",
"memberPriceLabel": "{name}会员价",
"dash": "—",
"lbUnit": "磅",
"hotSalesBadge": "生鲜热销榜",
"stockLabel": "库存",
"loadMoreRecommend": "加载更多"
},
"securityCode": { "securityCode": {
"cantSee": "看不清?换一张", "cantSee": "看不清?换一张",
"confirm": "确定" "confirm": "确定"
@@ -557,7 +622,23 @@
"title": "购物车", "title": "购物车",
"totalPrice": "总计", "totalPrice": "总计",
"viewCart": "查看购物车", "viewCart": "查看购物车",
"viewStore": "查看店铺" "viewStore": "查看店铺",
"localDelivery": "本地配送",
"deliveryUnified": "商品将由 {name} 统一配送!",
"itemsTotalWithPrice": "{count}件商品总共${amount}",
"deliveryFeeLine": "运费",
"serviceFeeLine": "服务费",
"subtotalLine": "小计",
"selectAll": "全选",
"forYou": "为您推荐",
"calculating": "计算中...",
"selectItemsHint": "请选择要结算的商品",
"removeCartTitle": "删除购物车?",
"removeCartDesc": "确定要删除该店铺购物车吗?",
"removeProductTitle": "移除商品?",
"removeProductDesc": "确定要将 {name} 从购物车中移除吗?",
"emptyTitle": "购物车为空",
"emptyAction": "去逛逛餐厅"
}, },
"choosePaymethod": { "choosePaymethod": {
"creditCard": "信用卡支付", "creditCard": "信用卡支付",
@@ -585,6 +666,7 @@
"coupon": { "coupon": {
"all-merchants": "适用于所有商户使用", "all-merchants": "适用于所有商户使用",
"expiry-date": "到期日:", "expiry-date": "到期日:",
"merchant-only": "仅{name}可用",
"merchant-specific": "指定商户使用", "merchant-specific": "指定商户使用",
"no-coupons": "您目前没有任何优惠券", "no-coupons": "您目前没有任何优惠券",
"redeem-now": "立即兑换", "redeem-now": "立即兑换",
+9 -3
View File
@@ -2,8 +2,8 @@
"name" : "CHEFLINK delivery", "name" : "CHEFLINK delivery",
"appid" : "__UNI__06509BE", "appid" : "__UNI__06509BE",
"description" : "", "description" : "",
"versionName" : "1.0.29", "versionName" : "3.1.2",
"versionCode" : 129, "versionCode" : 312,
"transformPx" : false, "transformPx" : false,
/* 5+App */ /* 5+App */
"app-plus" : { "app-plus" : {
@@ -203,5 +203,11 @@
"enable" : false "enable" : false
}, },
"vueVersion" : "3", "vueVersion" : "3",
"locale" : "en" "locale" : "en",
"h5" : {
"router" : {
"mode" : "history",
"base" : "/h5/"
}
}
} }
+373 -346
View File
@@ -1,92 +1,92 @@
<script lang="ts" setup> <script lang="ts" setup>
import * as R from 'ramda' import * as R from 'ramda'
import {z} from "zod"; import {z} from "zod";
import {useLogicStore} from "@/pages-login/store/module/logic"; import {useLogicStore} from "@/pages-login/store/module/logic";
import {Agreement} from "@/constant/enums"; import {Agreement} from "@/constant/enums";
import {debounce} from "throttle-debounce"; import {debounce} from "throttle-debounce";
import Config from "@/config"; import Config from "@/config";
import VerificationCode from "../../components/verification-code.vue"; import VerificationCode from "../../components/verification-code.vue";
import {appUserRegisterPost} from "@/service"; import {appUserRegisterPost} from "@/service";
import {useConfigStore, useUserStore} from "@/store"; import {useConfigStore, useUserStore} from "@/store";
const {t} = useI18n() const {t} = useI18n()
const userStore = useUserStore(); const userStore = useUserStore();
const configStore = useConfigStore() const configStore = useConfigStore()
const logicStore = useLogicStore() const logicStore = useLogicStore()
const invitationCode = ref('')
const areaCode = ref<string>(logicStore.registerForm.areaCode || '+1'); const areaCode = ref<string>(logicStore.registerForm.areaCode || '+1');
const columns = ref<string[]>(Config.phoneCodeList); const columns = ref<string[]>(Config.phoneCodeList);
const isAgreed = ref(false) const isAgreed = ref(false)
const EmailRegisterSchema = z.object({ const EmailRegisterSchema = z.object({
firstName: z.string().min(1, {message: t('pages-login.index.prompt.first-name')}), firstName: z.string().min(1, {message: t('pages-login.index.prompt.first-name')}),
surname: z.string().min(1, {message: t('pages-login.index.prompt.last-name')}), surname: z.string().min(1, {message: t('pages-login.index.prompt.last-name')}),
email: z.string().min(1, {message: t('pages-login.index.prompt.email-address-verify')}).email({message: t('pages-login.index.prompt.email-address-verify')}), email: z.string().min(1, {message: t('pages-login.index.prompt.email-address-verify')}).email({message: t('pages-login.index.prompt.email-address-verify')}),
confirmEmail: z.string().min(1, {message: t('pages-login.index.prompt.email-address-verify')}).email({message: t('pages-login.index.prompt.email-address-verify')}), confirmEmail: z.string().min(1, {message: t('pages-login.index.prompt.email-address-verify')}).email({message: t('pages-login.index.prompt.email-address-verify')}),
loginPwd: z.string().min(1, {message: t('pages-login.index.prompt.password')}), loginPwd: z.string().min(1, {message: t('pages-login.index.prompt.password')}),
confirmLoginPwd: z.string().min(1, {message: t('pages-login.index.prompt.password')}), confirmLoginPwd: z.string().min(1, {message: t('pages-login.index.prompt.password')}),
})
.refine((data) => data.email === data.confirmEmail, {
path: ['confirmEmail'],
message: t('pages-login.index.prompt.confirm-email-verify')
}) })
.refine((data) => data.loginPwd === data.confirmLoginPwd, { .refine((data) => data.email === data.confirmEmail, {
path: ['confirmEmail'],
message: t('pages-login.index.prompt.confirm-email-verify')
})
.refine((data) => data.loginPwd === data.confirmLoginPwd, {
path: ['confirmLoginPwd'],
message: t('pages-login.index.prompt.confirm-password-verify')
})
const PhoneRegisterSchema = z.object({
firstName: z.string().min(1, {message: t('pages-login.index.prompt.first-name')}),
surname: z.string().min(1, {message: t('pages-login.index.prompt.last-name')}),
phone: z.string().min(1, {message: t('pages-login.index.prompt.phone-number')}).regex(/^\d{6,11}$/, {message: t('pages-login.index.prompt.phone-number-verify')}),
loginPwd: z.string().min(1, {message: t('pages-login.index.prompt.password')}),
confirmLoginPwd: z.string().min(1, {message: t('pages-login.index.prompt.password')}),
}).refine((data) => data.loginPwd === data.confirmLoginPwd, {
path: ['confirmLoginPwd'], path: ['confirmLoginPwd'],
message: t('pages-login.index.prompt.confirm-password-verify') message: t('pages-login.index.prompt.confirm-password-verify')
}) })
const PhoneRegisterSchema = z.object({ function checkForm(): boolean {
firstName: z.string().min(1, {message: t('pages-login.index.prompt.first-name')}), const type = logicStore.registerForm.type
surname: z.string().min(1, {message: t('pages-login.index.prompt.last-name')}), const schema = type === 'phone' ? PhoneRegisterSchema : EmailRegisterSchema
phone: z.string().min(1, {message: t('pages-login.index.prompt.phone-number')}).regex(/^\d{6,11}$/, {message: t('pages-login.index.prompt.phone-number-verify')}), const validateFormField = schema.safeParse(logicStore.registerForm)
loginPwd: z.string().min(1, {message: t('pages-login.index.prompt.password')}), if (!validateFormField.success) {
confirmLoginPwd: z.string().min(1, {message: t('pages-login.index.prompt.password')}), const fieldErrorMessage = validateFormField.error.flatten().fieldErrors
}).refine((data) => data.loginPwd === data.confirmLoginPwd, { const errorMessage: string | undefined = R.path([0, 0], R.values(fieldErrorMessage))
path: ['confirmLoginPwd'], errorMessage &&
message: t('pages-login.index.prompt.confirm-password-verify') uni.showToast({
}) title: errorMessage,
icon: 'none',
function checkForm(): boolean { })
const type = logicStore.registerForm.type } else if (!isAgreed.value) {
const schema = type === 'phone' ? PhoneRegisterSchema : EmailRegisterSchema uni.showToast({
const validateFormField = schema.safeParse(logicStore.registerForm) title: `${t('common.prompt.please-carefully-read-and-agree')} ${t('agreement.user-terms-conditions')}${t('pages-login.and')}${t('agreement.privacy-policy')}`,
if (!validateFormField.success) { icon: 'none'
const fieldErrorMessage = validateFormField.error.flatten().fieldErrors })
const errorMessage: string | undefined = R.path([0, 0], R.values(fieldErrorMessage)) }
errorMessage && return validateFormField.success && isAgreed.value
uni.showToast({
title: errorMessage,
icon: 'none',
})
} else if (!isAgreed.value) {
uni.showToast({
title: `${t('common.prompt.please-carefully-read-and-agree')} ${t('agreement.user-terms-conditions')}${t('pages-login.and')}${t('agreement.privacy-policy')}`,
icon: 'none'
})
} }
return validateFormField.success && isAgreed.value
}
const verificationCodeRef = ref() const verificationCodeRef = ref()
const submit = () => { const submit = () => {
// verificationCodeRef.value.onOpen() // verificationCodeRef.value.onOpen()
codeSubmit() codeSubmit()
} }
// 验证码提交 // 验证码提交
const btnLoading = ref(false) const btnLoading = ref(false)
function codeSubmit() { function codeSubmit() {
btnLoading.value = true btnLoading.value = true
// console.log(data) // console.log(data)
const { const {
confirmEmail, confirmEmail,
confirmLoginPwd, confirmLoginPwd,
...rest ...rest
} = logicStore.registerForm as any } = logicStore.registerForm as any
appUserRegisterPost({ const requestBody: Record<string, any> = {
body: {
...rest, ...rest,
// 后端接收“确认密码”字段名为 newPwd // 后端接收“确认密码”字段名为 newPwd
newPwd: confirmLoginPwd, newPwd: confirmLoginPwd,
@@ -94,283 +94,310 @@ function codeSubmit() {
// captcha: data.code, // captcha: data.code,
// uuid: data.uuid, // uuid: data.uuid,
userPort: Config.userPort, // 登录端口2 商户端 userPort: Config.userPort, // 登录端口2 商户端
} as any }
}).then((res) => { if (invitationCode.value) {
userStore.token = res.data.token; requestBody.invitationCode = invitationCode.value
logicStore.reset() }
uni.showToast({ if (requestBody.email == null || requestBody.email === '') {
title: t('pages-login.sign-up.register-success'), delete requestBody.email
icon: 'none', }
}) const phoneVal = requestBody.phone
nextTick(() => { if (
userStore.getUserInfo(); phoneVal == null ||
uni.navigateTo({ phoneVal === '' ||
url: Config.guidePath (typeof phoneVal === 'string' && phoneVal.trim() === '')
) {
delete requestBody.phone
delete requestBody.areaCode
}
appUserRegisterPost({
body: requestBody as any
}).then((res) => {
userStore.token = res.data.token;
logicStore.reset()
uni.showToast({
title: t('pages-login.sign-up.register-success'),
icon: 'none',
}) })
// uni.redirectTo({ nextTick(() => {
// url: '/pages-login/pages/guide-page/location' userStore.getUserInfo();
// }) uni.navigateTo({
// const pages = getCurrentPages() url: Config.guidePath
// if(configStore.isShowedGuidePage) { })
// if(pages.length > 2) { // uni.redirectTo({
// uni.navigateBack({delta: 2}) // url: '/pages-login/pages/guide-page/location'
// } else { // })
// uni.switchTab({ // const pages = getCurrentPages()
// url: Config.indexPath // if(configStore.isShowedGuidePage) {
// }) // if(pages.length > 2) {
// } // uni.navigateBack({delta: 2})
// } else { // } else {
// uni.navigateTo({ // uni.switchTab({
// url: Config.guidePath // url: Config.indexPath
// }) // })
// } // }
// } else {
// uni.navigateTo({
// url: Config.guidePath
// })
// }
})
}).finally(() => {
btnLoading.value = false
}) })
}).finally(() => {
btnLoading.value = false
})
}
// 提交
const handleSubmit = R.when(checkForm, debounce(Config.debounceLongTime, submit, {
atBegin: true
}))
function navigateTo(url: string) {
uni.navigateTo({url});
}
const emailIsReadonly = ref(false)
const phoneIsReadonly = ref(false)
onMounted(() => {
const {email, confirmEmail, phone, type} = logicStore.registerForm
if (type === 'phone') {
phoneIsReadonly.value = !!phone
emailIsReadonly.value = false
} }
if (type === 'email') {
emailIsReadonly.value = !!(email || confirmEmail) // 提交
phoneIsReadonly.value = false const handleSubmit = R.when(checkForm, debounce(Config.debounceLongTime, submit, {
if (!logicStore.registerForm.email && logicStore.registerForm.confirmEmail) { atBegin: true
logicStore.registerForm.email = logicStore.registerForm.confirmEmail }))
function navigateTo(url: string) {
uni.navigateTo({url});
}
const emailIsReadonly = ref(false)
const phoneIsReadonly = ref(false)
onLoad((options?: Record<string, any>) => {
const codeRaw = options?.invitationCode
const code = typeof codeRaw === 'string' ? decodeURIComponent(codeRaw).trim() : ''
invitationCode.value = code
if (code) {
userStore.invitationCode = code
}
})
onMounted(() => {
const {email, confirmEmail, phone, type} = logicStore.registerForm
if (type === 'phone') {
phoneIsReadonly.value = !!phone
emailIsReadonly.value = false
}
if (type === 'email') {
emailIsReadonly.value = !!(email || confirmEmail)
phoneIsReadonly.value = false
if (!logicStore.registerForm.email && logicStore.registerForm.confirmEmail) {
logicStore.registerForm.email = logicStore.registerForm.confirmEmail
}
}
})
</script>
<template>
<view>
<navbar/>
<view class="pt-60rpx px-27rpx pb-80rpx">
<view class="mb-60rpx text-50rpx leading-50rpx font-bold text-#14181B">
{{ t('pages-login.sign-up.title') }}
</view>
<view class="">
<!-- 邮箱 -->
<view v-if="logicStore.registerForm.type !== 'phone'" class="">
<view class="text-32rpx leading-32rpx text-#14181B mb-24rpx">{{ t("common.email") }}</view>
<view class="border-color px-30rpx flex items-center bg-#EFEFEF">
<wd-input
v-model.trim="logicStore.registerForm.email"
:cursorSpacing="20"
:focus-when-clear="false"
:maxlength="40"
:placeholder="t('common.enter')"
:readonly="emailIsReadonly"
clearable
custom-class="flex-1 !bg-transparent"
no-border
placeholderClass="!text-#999 !text-32rpx"
>
</wd-input>
</view>
</view>
<!-- Confirm email -->
<view v-if="logicStore.registerForm.type !== 'phone'" class="mt-36rpx">
<view class="text-32rpx leading-32rpx text-#14181B mb-24rpx">{{
t("pages-login.sign-up.confirm-email")
}}
</view>
<view class="border-color px-30rpx flex items-center bg-#EFEFEF">
<wd-input
v-model.trim="logicStore.registerForm.confirmEmail"
:cursorSpacing="20"
:focus-when-clear="false"
:maxlength="40"
:placeholder="t('common.enter')"
:readonly="emailIsReadonly"
clearable
custom-class="flex-1 !bg-transparent"
no-border
placeholderClass="!text-#999 !text-32rpx"
>
</wd-input>
</view>
</view>
<!-- Password -->
<view class="mt-36rpx ">
<view class="text-32rpx leading-32rpx text-#14181B mb-24rpx">{{ t("pages-login.sign-up.password") }}</view>
<view class="border-color px-30rpx flex items-center bg-#EFEFEF">
<wd-input
v-model.trim="logicStore.registerForm.loginPwd"
:cursorSpacing="20"
:focus-when-clear="false"
:maxlength="20"
:placeholder="t('common.enterPassword')"
clearable
custom-class="flex-1 !bg-transparent"
no-border
placeholderClass="!text-#999 !text-32rpx"
showPassword
>
</wd-input>
</view>
</view>
<!-- Confirm password -->
<view class="mt-36rpx ">
<view class="text-32rpx leading-32rpx text-#14181B mb-24rpx">{{ t("pages-login.sign-up.confirm-password") }}</view>
<view class="border-color px-30rpx flex items-center bg-#EFEFEF">
<wd-input
v-model.trim="logicStore.registerForm.confirmLoginPwd"
:cursorSpacing="20"
:focus-when-clear="false"
:maxlength="20"
:placeholder="t('common.enterPassword')"
clearable
custom-class="flex-1 !bg-transparent"
no-border
placeholderClass="!text-#999 !text-32rpx"
showPassword
>
</wd-input>
</view>
</view>
<!-- First name -->
<view class="mt-36rpx ">
<view class="text-32rpx leading-32rpx text-#14181B mb-24rpx">{{ t("pages-login.sign-up.first-name") }}</view>
<view class="border-color px-30rpx flex items-center bg-#EFEFEF">
<wd-input
v-model.trim="logicStore.registerForm.firstName"
:cursorSpacing="20"
:focus-when-clear="false"
:maxlength="40"
:placeholder="t('common.enter')"
clearable
custom-class="flex-1 !bg-transparent"
no-border
placeholderClass="!text-#999 !text-32rpx"
>
</wd-input>
</view>
</view>
<!-- Last name -->
<view class="mt-36rpx ">
<view class="text-32rpx leading-32rpx text-#14181B mb-24rpx">{{ t("pages-login.sign-up.last-name") }}</view>
<view class="border-color px-30rpx flex items-center bg-#EFEFEF">
<wd-input
v-model.trim="logicStore.registerForm.surname"
:cursorSpacing="20"
:focus-when-clear="false"
:maxlength="40"
:placeholder="t('common.enter')"
clearable
custom-class="flex-1 !bg-transparent"
no-border
placeholderClass="!text-#999 !text-32rpx"
>
</wd-input>
</view>
</view>
<!-- Phone number -->
<view v-if="logicStore.registerForm.type !== 'email'" class="mt-36rpx ">
<view class="text-32rpx leading-32rpx text-#14181B mb-24rpx">{{
t("pages-login.sign-up.phone-number")
}}
</view>
<view class="border-color px-30rpx flex items-center bg-#EFEFEF">
<view class="pr-14rpx text-28rpx">
<wd-picker v-model="areaCode" :columns="columns"/>
</view>
<wd-input
v-model.trim="logicStore.registerForm.phone"
:cursorSpacing="20"
:focus-when-clear="false"
:maxlength="40"
:placeholder="t('common.enter')"
:readonly="phoneIsReadonly"
clearable
custom-class="flex-1 !bg-transparent"
no-border
placeholderClass="!text-#999 !text-32rpx"
>
</wd-input>
</view>
</view>
<view class="mt-54rpx">
<wd-button block custom-class="!h-108rpx !text-36rpx text-#fff font-bold !rounded-16rpx"
@click="handleSubmit" :loading="btnLoading" loading-color="#000">
{{ t('pages-login.sign-up.title') }}
</wd-button>
</view>
<view class="mt-36rpx flex items-start text-28rpx text-primary lh-42rpx" @click="isAgreed = !isAgreed">
<view class="shrink-0 center py-5rpx px-10rpx">
<image v-show="isAgreed" class=" w-28rpx h-28rpx"
src="@img-login/101.png"></image>
<image v-show="!isAgreed" class=" w-28rpx h-28rpx"
src="@img-login/100.png"></image>
</view>
<view class="">
<text class="">{{ t('pages-login.continuing-agree') }}</text>
<text class="text-#00A76D"
@click.stop="navigateTo(`/pages/agreement/index?code=${Agreement.USER_AGREEMENT}`)">
{{ t('agreement.user-terms-conditions') }}
</text>
<text class="text-#00A76D"
@click.stop="navigateTo(`/pages/agreement/index?code=${Agreement.PRIVACY_POLICY}`)">
{{ t('agreement.privacy-policy') }}
</text>
</view>
</view>
</view>
</view>
<verification-code ref="verificationCodeRef" @submit="codeSubmit"/>
</view>
</template>
<style lang="scss">
page {
background-color: #fff;
}
.border-color {
height: 98rpx;
border-radius: 16rpx;
border: 2rpx solid #D4D4D4;
}
:deep(.wd-input__clear) {
background-color: transparent !important;
}
:deep(.wd-input__icon) {
background-color: transparent !important;
}
:deep(.wd-picker__cell) {
background-color: transparent !important;
.wd-picker__value {
margin-right: 8rpx !important;
} }
} }
})
</script>
<template> :deep(.wd-picker-view-column__item) {
<view> line-height: 94rpx !important;
<navbar/>
<view class="pt-60rpx px-27rpx pb-80rpx">
<view class="mb-60rpx text-50rpx leading-50rpx font-bold text-#14181B">
{{ t('pages-login.sign-up.title') }}
</view>
<view class="">
<!-- 邮箱 -->
<view v-if="logicStore.registerForm.type !== 'phone'" class="">
<view class="text-32rpx leading-32rpx text-#14181B mb-24rpx">{{ t("common.email") }}</view>
<view class="border-color px-30rpx flex items-center bg-#EFEFEF">
<wd-input
v-model.trim="logicStore.registerForm.email"
:cursorSpacing="20"
:focus-when-clear="false"
:maxlength="40"
:placeholder="t('common.enter')"
:readonly="emailIsReadonly"
clearable
custom-class="flex-1 !bg-transparent"
no-border
placeholderClass="!text-#999 !text-32rpx"
>
</wd-input>
</view>
</view>
<!-- Confirm email -->
<view v-if="logicStore.registerForm.type !== 'phone'" class="mt-36rpx">
<view class="text-32rpx leading-32rpx text-#14181B mb-24rpx">{{
t("pages-login.sign-up.confirm-email")
}}
</view>
<view class="border-color px-30rpx flex items-center bg-#EFEFEF">
<wd-input
v-model.trim="logicStore.registerForm.confirmEmail"
:cursorSpacing="20"
:focus-when-clear="false"
:maxlength="40"
:placeholder="t('common.enter')"
:readonly="emailIsReadonly"
clearable
custom-class="flex-1 !bg-transparent"
no-border
placeholderClass="!text-#999 !text-32rpx"
>
</wd-input>
</view>
</view>
<!-- Password -->
<view class="mt-36rpx ">
<view class="text-32rpx leading-32rpx text-#14181B mb-24rpx">{{ t("pages-login.sign-up.password") }}</view>
<view class="border-color px-30rpx flex items-center bg-#EFEFEF">
<wd-input
v-model.trim="logicStore.registerForm.loginPwd"
:cursorSpacing="20"
:focus-when-clear="false"
:maxlength="20"
:placeholder="t('common.enterPassword')"
clearable
custom-class="flex-1 !bg-transparent"
no-border
placeholderClass="!text-#999 !text-32rpx"
showPassword
>
</wd-input>
</view>
</view>
<!-- Confirm password -->
<view class="mt-36rpx ">
<view class="text-32rpx leading-32rpx text-#14181B mb-24rpx">{{ t("pages-login.sign-up.confirm-password") }}</view>
<view class="border-color px-30rpx flex items-center bg-#EFEFEF">
<wd-input
v-model.trim="logicStore.registerForm.confirmLoginPwd"
:cursorSpacing="20"
:focus-when-clear="false"
:maxlength="20"
:placeholder="t('common.enterPassword')"
clearable
custom-class="flex-1 !bg-transparent"
no-border
placeholderClass="!text-#999 !text-32rpx"
showPassword
>
</wd-input>
</view>
</view>
<!-- First name -->
<view class="mt-36rpx ">
<view class="text-32rpx leading-32rpx text-#14181B mb-24rpx">{{ t("pages-login.sign-up.first-name") }}</view>
<view class="border-color px-30rpx flex items-center bg-#EFEFEF">
<wd-input
v-model.trim="logicStore.registerForm.firstName"
:cursorSpacing="20"
:focus-when-clear="false"
:maxlength="40"
:placeholder="t('common.enter')"
clearable
custom-class="flex-1 !bg-transparent"
no-border
placeholderClass="!text-#999 !text-32rpx"
>
</wd-input>
</view>
</view>
<!-- Last name -->
<view class="mt-36rpx ">
<view class="text-32rpx leading-32rpx text-#14181B mb-24rpx">{{ t("pages-login.sign-up.last-name") }}</view>
<view class="border-color px-30rpx flex items-center bg-#EFEFEF">
<wd-input
v-model.trim="logicStore.registerForm.surname"
:cursorSpacing="20"
:focus-when-clear="false"
:maxlength="40"
:placeholder="t('common.enter')"
clearable
custom-class="flex-1 !bg-transparent"
no-border
placeholderClass="!text-#999 !text-32rpx"
>
</wd-input>
</view>
</view>
<!-- Phone number -->
<view v-if="logicStore.registerForm.type !== 'email'" class="mt-36rpx ">
<view class="text-32rpx leading-32rpx text-#14181B mb-24rpx">{{
t("pages-login.sign-up.phone-number")
}}
</view>
<view class="border-color px-30rpx flex items-center bg-#EFEFEF">
<view class="pr-14rpx text-28rpx">
<wd-picker v-model="areaCode" :columns="columns"/>
</view>
<wd-input
v-model.trim="logicStore.registerForm.phone"
:cursorSpacing="20"
:focus-when-clear="false"
:maxlength="40"
:placeholder="t('common.enter')"
:readonly="phoneIsReadonly"
clearable
custom-class="flex-1 !bg-transparent"
no-border
placeholderClass="!text-#999 !text-32rpx"
>
</wd-input>
</view>
</view>
<view class="mt-54rpx">
<wd-button block custom-class="!h-108rpx !text-36rpx text-#fff font-bold !rounded-16rpx"
@click="handleSubmit" :loading="btnLoading" loading-color="#000">
{{ t('pages-login.sign-up.title') }}
</wd-button>
</view>
<view class="mt-36rpx flex items-start text-28rpx text-primary lh-42rpx" @click="isAgreed = !isAgreed">
<view class="shrink-0 center py-5rpx px-10rpx">
<image v-show="isAgreed" class=" w-28rpx h-28rpx"
src="@img-login/101.png"></image>
<image v-show="!isAgreed" class=" w-28rpx h-28rpx"
src="@img-login/100.png"></image>
</view>
<view class="">
<text class="">{{ t('pages-login.continuing-agree') }}</text>
<text class="text-#00A76D"
@click.stop="navigateTo(`/pages/agreement/index?code=${Agreement.USER_AGREEMENT}`)">
{{ t('agreement.user-terms-conditions') }}
</text>
<text class="text-#00A76D"
@click.stop="navigateTo(`/pages/agreement/index?code=${Agreement.PRIVACY_POLICY}`)">
{{ t('agreement.privacy-policy') }}
</text>
</view>
</view>
</view>
</view>
<verification-code ref="verificationCodeRef" @submit="codeSubmit"/>
</view>
</template>
<style lang="scss">
page {
background-color: #fff;
}
.border-color {
height: 98rpx;
border-radius: 16rpx;
border: 2rpx solid #D4D4D4;
}
:deep(.wd-input__clear) {
background-color: transparent !important;
}
:deep(.wd-input__icon) {
background-color: transparent !important;
}
:deep(.wd-picker__cell) {
background-color: transparent !important;
.wd-picker__value {
margin-right: 8rpx !important;
} }
}
:deep(.wd-picker-view-column__item) { :deep(.uni-picker-view-indicator) {
line-height: 94rpx !important; height: 94rpx !important;
} }
</style>
:deep(.uni-picker-view-indicator) {
height: 94rpx !important;
}
</style>
+41 -5
View File
@@ -51,11 +51,16 @@ function navigateTo(url: string) {
<view class="grid grid-cols-2 gap-x-30rpx gap-y-46rpx px-32rpx"> <view class="grid grid-cols-2 gap-x-30rpx gap-y-46rpx px-32rpx">
<template v-for="(item,index) in dataList"> <template v-for="(item,index) in dataList">
<view @click="handleClickDish(item)" class="w-330rpx overflow-hidden"> <view @click="handleClickDish(item)" class="w-330rpx overflow-hidden">
<image <view class="search-result-img-wrap">
:src="thumbnailImg(item?.dishImage?.split(',')[0])" <view v-if="item.isNew == 1" class="dish-new-ribbon">
class="w-full h-186rpx rounded-24rpx mb-16rpx" <text class="dish-new-ribbon__text">NEW</text>
mode="aspectFill" </view>
></image> <image
:src="thumbnailImg(item?.dishImage?.split(',')[0])"
class="w-full h-186rpx rounded-24rpx"
mode="aspectFill"
></image>
</view>
<text class="text-30rpx lh-30rpx text-#333 line-clamp-1 tracking-[.04em] font-500" <text class="text-30rpx lh-30rpx text-#333 line-clamp-1 tracking-[.04em] font-500"
>{{ item.dishName }}</text> >{{ item.dishName }}</text>
</view> </view>
@@ -65,5 +70,36 @@ function navigateTo(url: string) {
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.search-result-img-wrap {
position: relative;
overflow: hidden;
border-radius: 24rpx;
margin-bottom: 16rpx;
}
.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> </style>
+317 -49
View File
@@ -1,25 +1,24 @@
<script setup lang="ts"> <script setup lang="ts">
import usePage from "@/hooks/usePage"; import usePage from "@/hooks/usePage";
import {appMerchantRecommendListPost} from "@/service"; import { appMerchantRecommendListPost, type MerchantVo } from "@/service";
import {useUserStore} from "@/store"; import { useUserStore } from "@/store";
import FiltrateTool from "@/components/filtrate-tool/index.vue"; import FiltrateTool from "@/components/filtrate-tool/index.vue";
import PriceChoose from "@/components/filtrate-tool/components/price-choose.vue"; import PriceChoose from "@/components/filtrate-tool/components/price-choose.vue";
import Score from "@/components/filtrate-tool/components/score.vue"; import Score from "@/components/filtrate-tool/components/score.vue";
import FoodBox from "@/pages/home/components/tabbar-home/components/food-box/index.vue";
const userStore = useUserStore();
const {t} = useI18n() const userStore = useUserStore();
const { t } = useI18n();
const props = defineProps({ const props = defineProps({
merchantCategoryIds: { merchantCategoryIds: {
type: [Number, String], type: [Number, String],
default: null default: null,
} },
}) });
const {paging, dataList, queryList} = usePage(getList) const { paging, dataList, queryList } = usePage<MerchantVo>(getList);
function getList(pageNum: number, pageSize: number) { function getList(pageNum: number, pageSize: number) {
return new Promise(resolve => { return new Promise((resolve) => {
appMerchantRecommendListPost({ appMerchantRecommendListPost({
params: { params: {
pageNum: pageNum, pageNum: pageNum,
@@ -28,75 +27,344 @@ function getList(pageNum: number, pageSize: number) {
body: { body: {
lat: userStore.userLocation.latitude, lat: userStore.userLocation.latitude,
lng: userStore.userLocation.longitude, lng: userStore.userLocation.longitude,
selfPickup: selfPickup.value, // 是否自提 selfPickup: selfPickup.value,
discount: discount.value, // 是否有折扣 1是 2 否 discount: discount.value,
scoreRange: scoreRange.value || null, // 评分范围 比如 3-4 scoreRange: scoreRange.value || null,
priceRange: price.value || null, // 价格范围 比如 10-30 priceRange: price.value || null,
merchantCategoryIds: props.merchantCategoryIds ? [props.merchantCategoryIds] : [], // 商家分类id集合(首页中间) merchantCategoryIds: props.merchantCategoryIds
} ? [props.merchantCategoryIds]
}).then(res => { : [],
console.log(res) },
resolve({rows: res.rows}) }).then((res) => {
}) console.log(res);
}) resolve({ rows: res.rows });
});
});
} }
// 是否自提 const selfPickup = ref<number | null>(null);
const selfPickup = ref<number | null>(null)
function togglePickup(value: number) { function togglePickup(value: number) {
selfPickup.value = value; selfPickup.value = value;
paging.value.refresh() paging.value?.refresh();
}
// 是否有折扣
const discount = ref<number | null>(null)
function toggleDiscount(value: number) {
discount.value = value;
paging.value.refresh()
} }
const scoreRef = ref<any>() const discount = ref<number | null>(null);
const scoreRange = ref<string | null>(null) function toggleDiscount(value: number) {
const priceChooseRef = ref<any>() discount.value = value;
const price = ref<string | null>(null) paging.value?.refresh();
}
const scoreRef = ref<any>();
const scoreRange = ref<string | null>(null);
const priceChooseRef = ref<any>();
const price = ref<string | null>(null);
function toggleScore() { function toggleScore() {
if (scoreRef.value) { scoreRef.value?.onOpen();
scoreRef.value.onOpen();
}
} }
function togglePrice() { function togglePrice() {
if (priceChooseRef.value) { priceChooseRef.value?.onOpen();
priceChooseRef.value.onOpen();
}
} }
function applyScore(value: string) { function applyScore(value: string) {
scoreRange.value = value; scoreRange.value = value;
nextTick(() => paging.value?.refresh());
} }
function applyPrice(value: string) { function applyPrice(value: string) {
price.value = value; price.value = value;
nextTick(() => paging.value?.refresh());
}
/** 左侧双图:优先店铺图,不足则用 logo 补齐 */
function merchantImagePair(item: MerchantVo): [string, string] {
const shop = (item.shopImages || "")
.split(",")
.map((s) => s.trim())
.filter(Boolean);
const logo = (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: MerchantVo): string {
const zh = item.merchantCategoryNamesZh;
const en = item.merchantCategoryNamesEn;
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?: number) {
const n = Math.min(5, Math.max(0, Math.round(Number(rating) || 0)));
return n;
}
function ratingText(rating?: number) {
const n = Number(rating);
if (Number.isFinite(n)) return n.toFixed(1);
return "0.0";
}
function openMerchant(item: MerchantVo) {
if (item?.id == null) return;
uni.navigateTo({
url: `/pages-store/pages/store/index?id=${item.id}`,
});
} }
</script> </script>
<template> <template>
<z-paging ref="paging" v-model="dataList" @query="queryList"> <view class="home-store-page">
<z-paging
ref="paging"
v-model="dataList"
bg-color="#F2F3F5"
@query="queryList"
>
<template #top> <template #top>
<view class="bg-white pb-24rpx"> <view class="home-store-header">
<navbar :title="t('pages.home.featured-on')"/> <navbar
<!-- 筛选工具 --> :title="t('pages.home.featured-on')"
<filtrate-tool class="mt-32rpx" @togglePickup="togglePickup" @toggleDiscount="toggleDiscount" @toggleScore="toggleScore" @togglePrice="togglePrice" /> circle-back
custom-class="!bg-white"
/>
</view>
<view class="home-store-filter-strip">
<filtrate-tool
class="home-store-filters"
@togglePickup="togglePickup"
@toggleDiscount="toggleDiscount"
@toggleScore="toggleScore"
@togglePrice="togglePrice"
/>
</view> </view>
</template> </template>
<view class="p-30rpx">
<template v-for="(item, index) in dataList" :key="index"> <view class="home-store-list">
<food-box :item="item" /> <view
</template> v-for="(item, index) in dataList"
:key="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>
</z-paging> </z-paging>
<score @applyScore="applyScore" ref="scoreRef" /> <score @applyScore="applyScore" ref="scoreRef" />
<price-choose @applyPrice="applyPrice" ref="priceChooseRef" /> <price-choose @applyPrice="applyPrice" ref="priceChooseRef" />
</view>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.home-store-page {
min-height: 100vh;
}
.home-store-header {
background: #fff;
}
.home-store-filter-strip {
background: #f2f3f5;
padding-bottom: 8rpx;
}
.home-store-filters {
display: block;
padding: 16rpx 0 20rpx;
}
.home-store-list {
padding: 16rpx 24rpx 40rpx;
box-sizing: border-box;
}
.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;
flex-shrink: 0;
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;
background: #e8eaed;
}
.merchant-card__img--empty {
flex-shrink: 0;
width: 100%;
height: 100%;
}
.merchant-card__body {
flex: 1;
min-width: 0;
padding: 0 20rpx 0 8rpx;
display: flex;
flex-direction: column;
align-items: flex-start;
}
.merchant-card__name {
font-size: 30rpx;
font-weight: 600;
color: #14181b;
line-height: 1.35;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.merchant-card__cate {
margin-top: 8rpx;
font-size: 26rpx;
color: #333;
line-height: 1.3;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.merchant-card__rating {
margin-top: 12rpx;
display: flex;
flex-direction: row;
align-items: center;
flex-wrap: wrap;
gap: 4rpx;
}
.merchant-card__star {
font-size: 22rpx;
line-height: 1;
color: #e0e0e0;
letter-spacing: -2rpx;
}
.merchant-card__star--on {
color: #14181b;
}
.merchant-card__score {
margin-left: 10rpx;
font-size: 26rpx;
font-weight: 600;
color: #14181b;
}
.merchant-card__tag {
margin-top: 10rpx;
font-size: 24rpx;
font-weight: 500;
color: #b8860b;
line-height: 1.3;
}
.merchant-card__go {
width: 56rpx;
height: 56rpx;
border-radius: 50%;
background: #14181b;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.merchant-card__go-icon {
font-size: 28rpx;
color: #fff;
margin-left: 2rpx;
}
</style>
<style>
page {
background-color: #f2f3f5;
}
</style> </style>
+719 -29
View File
@@ -1,61 +1,751 @@
<script setup lang="ts"> <script setup lang="ts">
import {appMerchantRecommendListPost,getDishListByCategoryId} from "@/service"; import { debounce } from "throttle-debounce";
import FoodBox from "@/pages/home/components/tabbar-home/components/food-box/index.vue"; import Config from "@/config";
import {useUserStore} from "@/store"; import usePage from "@/hooks/usePage";
import {
appRecipeCategoryListGet,
appCollectCollectPost,
appMerchantCartAddCartPost,
getDishListByCategoryId,
} from "@/service";
import { CollectionType } from "@/constant/enums";
import {
useCategoryNavStore,
useConfigStore,
useUserStore,
} from "@/store";
import { formatSalesCount } from "@/utils/utils";
import { onShow } from "@dcloudio/uni-app";
const { t } = useI18n();
const configStore = useConfigStore();
const userStore = useUserStore(); const userStore = useUserStore();
const categoryNavStore = useCategoryNavStore();
const props = defineProps<{ /** 菜品瀑布流项(接口字段较多,用宽松类型避免模板断言) */
id?: string type DishGridItem = Record<string, any>;
}>()
/** 菜谱分类(与首页 class-bullet 同源:appRecipeCategoryListGet */
const recipeCategoryList = ref<Record<string, unknown>[]>([]);
/** 当前选中的菜谱分类 id,仅由用户点击大分类或首页写入的 pending 决定,不用路由 options */
const recipeCategoryId = ref<string>("");
/** 横向分类 scroll-view 滚动到选中项(与 :id="'recipe-cat-'+id" 对应) */
const recipeCategoryScrollInto = ref("");
const pageReady = ref(false);
const { paging, dataList, queryList } = usePage(getList);
function getList(pageNum: number, pageSize: number) { function getList(pageNum: number, pageSize: number) {
return new Promise(resolve => { return new Promise<{ rows: unknown[]; total?: number }>((resolve) => {
const rid = recipeCategoryId.value;
if (!rid) {
resolve({ rows: [], total: 0 });
return;
}
getDishListByCategoryId({ getDishListByCategoryId({
params: { params: {
pageNum, pageNum,
pageSize, pageSize,
recipeCategoryId:props.id recipeCategoryId: rid,
}, },
body: { body: {
lat: userStore.userLocation.latitude, pageNum,
lng: userStore.userLocation.longitude, pageSize,
selfPickup: null, // 是否自提 lat: userStore.userLocation.latitude || undefined,
discount: null, // 是否有折扣 1是 2 否 lng: userStore.userLocation.longitude || undefined,
scoreRange: null, // 评分范围 比如 3-4 recipeCategoryId: rid,
priceRange: null, // 价格范围 比如 10-30
merchantCategoryIds: [], merchantCategoryIds: [],
merchantLabelIds: props.id ? [props.id] : [], merchantLabelIds: [],
recipeCategoryId:props.id
}, },
}).then(res => {
resolve(res.data)
}) })
}) .then((res: any) => {
resolve({
rows: res?.rows ?? res?.data?.rows ?? [],
total: res?.total ?? res?.data?.total,
});
})
.catch(() => resolve({ rows: [] }));
});
} }
const {paging, loading, firstLoaded, dataList, queryList} = usePage(getList) const dishColumns = computed(() => {
const list = (dataList.value || []) as DishGridItem[];
return [
list.filter((_, i) => i % 2 === 0),
list.filter((_, i) => i % 2 === 1),
];
});
function getDishPromoLabel(item: DishGridItem): string {
const raw =
item.marketingLabel ??
item.hotSaleTag ??
item.rankTag ??
item.promotionLabel;
return typeof raw === "string" && raw.trim() ? raw.trim() : "";
}
function isSoldOutStock(stockLike: unknown) {
const n = Number(stockLike);
return !Number.isNaN(n) && n <= 0;
}
function navigateToDishes(item: DishGridItem) {
const id = item?.id;
const storeId = item?.merchantId;
if (id == null || storeId == null) return;
uni.navigateTo({
url: `/pages-store/pages/store/dishes?id=${id}&storeId=${storeId}`,
});
}
function goSearch() {
uni.navigateTo({ url: "/pages/search/index" });
}
function goCart() {
uni.navigateTo({ url: "/pages-user/pages/cart/index" });
}
function handleBack() {
const pages = getCurrentPages?.() || [];
if (pages.length <= 1) {
uni.switchTab({ url: "/pages/home/index" });
return;
}
uni.navigateBack();
}
const cartBadgeTotal = computed(() => {
const list = userStore.userCartAllData;
if (!Array.isArray(list) || list.length === 0) return 0;
let n = 0;
for (const m of list) {
n +=
(m as { merchantCartVoList?: unknown[] })?.merchantCartVoList
?.length || 0;
}
return n;
});
const addingDishId = ref<string | number | null>(null);
async function onAddCartClick(item: DishGridItem) {
if (isSoldOutStock(item?.stock)) {
uni.showToast({
title: t("common.prompt.soldOut"),
icon: "none",
});
return;
}
if (!userStore.isLogin) {
uni.showToast({ title: t("common.pleaseLogin"), icon: "none" });
setTimeout(() => uni.navigateTo({ url: Config.loginPath }), 400);
return;
}
const dishId = item?.id;
const merchantId = item?.merchantId;
if (dishId == null || merchantId == null) return;
if (addingDishId.value != null) return;
addingDishId.value = dishId as string | number;
try {
await appMerchantCartAddCartPost({
body: {
merchantId,
dishId,
count: 1,
merchantCartSideDishBoList: [],
} as any,
});
uni.showToast({ title: t("toast.addCartSuccess"), icon: "none" });
userStore.getUserCartAllData();
} finally {
addingDishId.value = null;
}
}
const debouncedCollect = debounce(
1300,
(
isCollected: boolean,
id: string,
type: CollectionType,
cb: () => void
) => {
appCollectCollectPost({
body: { targetId: id, targetType: type },
}).then(() => cb());
},
{ atBegin: true }
);
function handleDishCollectionClick(item: DishGridItem) {
if (!userStore.isLogin) {
uni.showToast({ title: t("common.pleaseLogin"), icon: "none" });
setTimeout(() => uni.navigateTo({ url: Config.loginPath }), 400);
return;
}
const id = String(item?.id ?? "");
if (!id) return;
debouncedCollect(Boolean(item.isCollect), id, CollectionType.DISH, () => {
item.isCollect = !item.isCollect;
});
}
function normalizeCategoryId(v: unknown): string {
if (v === null || v === undefined) return "";
return String(v).trim();
}
/** 与列表项 id 比对(兼容数字 / 字符串) */
function categoryRowIdEquals(
cat: Record<string, unknown>,
target: string
): boolean {
const t = normalizeCategoryId(target);
if (!t) return false;
const a = normalizeCategoryId(cat.id);
if (a === t) return true;
const na = Number(a);
const nt = Number(t);
if (!Number.isNaN(na) && !Number.isNaN(nt) && na === nt) return true;
return false;
}
/** 在接口返回的分类列表中解析应选中的 id(与首页点击项对齐) */
function resolveRecipeCategoryIdFromPending(pendingRaw: string | null): string {
const list = recipeCategoryList.value;
if (!list.length) return "";
const pending = pendingRaw ? normalizeCategoryId(pendingRaw) : "";
if (pending) {
const hit = list.find((c) => categoryRowIdEquals(c, pending));
if (hit != null) return normalizeCategoryId(hit.id);
}
return normalizeCategoryId(list[0].id);
}
function scrollRecipeTabIntoView() {
const id = recipeCategoryId.value;
if (!id) return;
recipeCategoryScrollInto.value = "";
nextTick(() => {
recipeCategoryScrollInto.value = `recipe-cat-${id}`;
});
}
function onSelectRecipe(cat: Record<string, unknown>) {
const id = normalizeCategoryId(cat?.id);
if (!id || id === recipeCategoryId.value) return;
recipeCategoryId.value = id;
nextTick(() => scrollRecipeTabIntoView());
paging.value?.reload();
}
async function loadRecipeCategories() {
const recipeRes: any = await appRecipeCategoryListGet({});
recipeCategoryList.value = recipeRes?.data || [];
}
function resolveInitialRecipeCategoryId() {
const pending = categoryNavStore.consumePendingRecipeCategoryId();
recipeCategoryId.value = resolveRecipeCategoryIdFromPending(pending);
}
onMounted(async () => {
await loadRecipeCategories();
resolveInitialRecipeCategoryId();
pageReady.value = true;
nextTick(() => {
scrollRecipeTabIntoView();
paging.value?.reload();
});
});
onShow(() => {
userStore.getUserCartAllData();
});
</script> </script>
<template> <template>
<z-paging <z-paging
ref="paging" ref="paging"
v-model="dataList" v-model="dataList"
@query="queryList" :auto="false"
bg-color="#fff" @query="queryList"
bg-color="#f5f5f5"
> >
<template #top> <template #top>
<navbar /> <view class="cat-top" :style="{ paddingTop: configStore.statusBarHeight + 'px' }">
<view class="cat-header-row">
<view class="cat-icon-btn" @click="handleBack">
<view class="i-carbon:chevron-left text-40rpx text-#111" />
</view>
<view class="cat-search" @click="goSearch">
<image
src="@img/chef/100222.png"
class="cat-search-icon"
mode="aspectFit"
/>
<text class="cat-search-placeholder">{{
t("components.search.placeholder")
}}</text>
</view>
<view class="cat-icon-btn cat-icon-btn--cart" @click="goCart">
<view class="i-carbon:shopping-cart text-34rpx text-#14181b" />
<view
v-if="userStore.isLogin && cartBadgeTotal > 0"
class="cat-cart-badge"
>{{ cartBadgeTotal > 99 ? "99+" : cartBadgeTotal }}</view
>
</view>
</view>
<!-- 第一行菜谱分类圆图 + 文案 -->
<scroll-view
class="cat-scroll-x"
scroll-x
scroll-with-animation
:scroll-into-view="recipeCategoryScrollInto"
:show-scrollbar="false"
enable-flex
>
<view class="cat-recipe-track">
<view
v-for="cat in recipeCategoryList"
:key="String(cat.id)"
:id="'recipe-cat-' + String(cat.id)"
class="cat-recipe-item"
@click="onSelectRecipe(cat)"
>
<view
class="cat-recipe-ring"
:class="{
'cat-recipe-ring--on': categoryRowIdEquals(
cat,
recipeCategoryId,
),
}"
>
<image
class="cat-recipe-img"
:src="String(cat.categoryImageUrl || cat.categoryImage || '')"
mode="aspectFill"
/>
</view>
<text
class="cat-recipe-label"
:class="{
'cat-recipe-label--on': categoryRowIdEquals(
cat,
recipeCategoryId,
),
}"
>{{ cat.categoryName }}</text
>
</view>
</view>
</scroll-view>
</view>
</template> </template>
<view class="p-32rpx"> <view v-if="pageReady" class="cat-waterfall px-24rpx pb-32rpx">
<template v-for="(item, index) in dataList" :key="index"> <view class="waterfall-row flex gap-16rpx items-start">
<food-box :item="item" /> <view
</template> v-for="(col, colIndex) in dishColumns"
: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) + '-' + String(item.merchantId)"
class="cat-dish-card w-full"
@click="navigateToDishes(item)"
>
<view class="cat-dish-image relative w-full bg-#f0f0f0">
<view v-if="item.isNew == 1" class="dish-new-ribbon">
<text class="dish-new-ribbon__text">NEW</text>
</view>
<view
class="cat-dish-collect"
@click.stop="handleDishCollectionClick(item)"
>
<image
v-if="!item.isCollect"
src="@img-store/1334.png"
mode="aspectFit"
class="cat-dish-collect-icon"
/>
<image
v-else
src="@img-store/1337.png"
mode="aspectFit"
class="cat-dish-collect-icon"
/>
</view>
<view
v-if="isSoldOutStock(item.stock)"
class="cat-dish-soldout"
>{{ t("common.prompt.soldOut") }}</view
>
<image
:src="String(item.dishImage || '').split(',')[0]"
mode="widthFix"
class="w-full block"
/>
</view>
<view class="cat-dish-body">
<view class="cat-dish-meta flex items-start justify-between gap-12rpx mb-14rpx">
<view class="min-w-0 flex-1">
<text class="cat-dish-price"
>US${{ item.discountPrice }}</text
>
<text
v-if="
Number(item.originalPrice) > Number(item.discountPrice)
"
class="cat-dish-original"
>US${{ item.originalPrice }}</text
>
</view>
<text class="cat-dish-sales shrink-0"
>{{ t("pages-store.store.sales") }}:
{{ formatSalesCount(item.salesCount) }}</text
>
</view>
<view class="cat-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="cat-dish-member shrink min-w-0"
>
<text class="cat-dish-member-inner"
>{{ t("pages-store.store.members") }}: US${{
item.memberPrice
}}</text
>
</view>
<view v-else class="flex-1 min-w-0" />
<view
class="cat-dish-add center shrink-0"
:class="{
'cat-dish-add--busy': addingDishId === item.id,
}"
@click.stop="onAddCartClick(item)"
>
<image src="@img/chef/1285.png" class="w-28rpx h-28rpx" />
</view>
</view>
<view
v-if="getDishPromoLabel(item)"
class="cat-dish-promo mt-16rpx"
>
<text class="cat-dish-promo-text">{{
getDishPromoLabel(item)
}}</text>
</view>
</view>
</view>
</view>
</view>
</view> </view>
<template #bottom>
<view class="h-24rpx" />
<view :style="[configStore.iosSafeBottomPlaceholder]" />
</template>
</z-paging> </z-paging>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.cat-top {
background: #f5f5f5;
padding-bottom: 8rpx;
}
.cat-header-row {
display: flex;
align-items: center;
gap: 16rpx;
padding: 12rpx 24rpx 20rpx;
}
.cat-icon-btn {
width: 72rpx;
height: 72rpx;
border-radius: 50%;
background: #fff;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.cat-icon-btn--cart {
position: relative;
}
.cat-cart-badge {
position: absolute;
top: 2rpx;
right: 2rpx;
min-width: 28rpx;
height: 28rpx;
padding: 0 6rpx;
font-size: 18rpx;
line-height: 28rpx;
font-weight: 600;
color: #fff;
text-align: center;
background: #e23636;
border-radius: 999rpx;
}
.cat-search {
flex: 1;
min-width: 0;
height: 72rpx;
border-radius: 36rpx;
background: #fff;
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.06);
display: flex;
align-items: center;
padding: 0 28rpx;
}
.cat-search-icon {
width: 28rpx;
height: 28rpx;
flex-shrink: 0;
}
.cat-search-placeholder {
margin-left: 16rpx;
font-size: 28rpx;
font-weight: 500;
color: #999;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.cat-scroll-x {
width: 100%;
white-space: nowrap;
}
.cat-recipe-track {
display: inline-flex;
flex-direction: row;
padding: 8rpx 24rpx 16rpx;
gap: 36rpx;
}
.cat-recipe-item {
display: inline-flex;
flex-direction: column;
align-items: center;
width: 120rpx;
flex-shrink: 0;
}
.cat-recipe-ring {
width: 102rpx;
height: 102rpx;
border-radius: 50%;
padding: 0;
background: #fff;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.07);
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
}
.cat-recipe-ring--on {
border: 4rpx solid #14181b;
}
.cat-recipe-img {
width: 94rpx;
height: 94rpx;
border-radius: 50%;
}
.cat-recipe-label {
margin-top: 12rpx;
font-size: 22rpx;
line-height: 1.2;
color: #333;
text-align: center;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 500;
}
.cat-recipe-label--on {
color: #14181b;
font-weight: 600;
}
.cat-waterfall {
min-height: 200rpx;
}
.cat-dish-card {
background: #fff;
border-radius: 24rpx;
overflow: hidden;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
}
.cat-dish-collect {
position: absolute;
z-index: 2;
top: 12rpx;
right: 12rpx;
width: 56rpx;
height: 56rpx;
display: flex;
align-items: center;
justify-content: center;
}
.cat-dish-collect-icon {
width: 44rpx;
height: 44rpx;
filter: drop-shadow(0 2rpx 6rpx rgba(0, 0, 0, 0.18));
}
.cat-dish-soldout {
position: absolute;
z-index: 2;
left: 16rpx;
top: 16rpx;
padding: 0 14rpx;
height: 48rpx;
border-radius: 24rpx;
background: rgba(20, 24, 27, 0.75);
color: #fff;
font-size: 24rpx;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
}
.cat-dish-body {
padding: 20rpx 20rpx 22rpx;
}
.cat-dish-price {
color: #e02e24;
font-size: 32rpx;
font-weight: 600;
line-height: 1.2;
}
.cat-dish-original {
margin-left: 10rpx;
color: #b3b3b3;
font-size: 22rpx;
text-decoration: line-through;
vertical-align: baseline;
}
.cat-dish-sales {
color: #999;
font-size: 24rpx;
line-height: 1.35;
max-width: 48%;
text-align: right;
}
.cat-dish-title {
color: #1a1a1a;
font-size: 28rpx;
font-weight: 500;
line-height: 1.45;
}
.cat-dish-member {
display: inline-flex;
align-items: center;
max-width: calc(100% - 88rpx);
}
.cat-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;
max-width: 100%;
}
.cat-dish-add {
width: 56rpx;
height: 56rpx;
border-radius: 50%;
background: #14181b;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.12);
}
.cat-dish-add--busy {
opacity: 0.45;
pointer-events: none;
}
.cat-dish-promo {
padding: 12rpx 16rpx;
border-radius: 12rpx;
background: #fff0f0;
}
.cat-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> </style>
File diff suppressed because it is too large Load Diff
@@ -56,16 +56,18 @@ function handleClickSearch() {
// 已存在:直接选择这个地址,走原有 chooseAddress 流程 // 已存在:直接选择这个地址,走原有 chooseAddress 流程
chooseAddress(exist) chooseAddress(exist)
} else { } else {
// 不存在:走新增地址流程 // 不存在:走新增地址流程(建筑类型首次在保存页以弹窗呈现)
addressStore.clearAddressInfo()
addressStore.setAddressLocation({ addressStore.setAddressLocation({
displayName: data.displayName, displayName: data.displayName,
formattedAddress: data.formattedAddress, formattedAddress: data.formattedAddress,
longitude: data.location.lng, longitude: data.location.lng,
latitude: data.location.lat, latitude: data.location.lat,
}) })
addressStore.pendingIntroBuildingType = true
setTimeout(() => { setTimeout(() => {
uni.navigateTo({ uni.navigateTo({
url: '/pages/address/choose-type' url: '/pages/address/save-address/other'
}) })
}, 300) }, 300)
} }
+422 -119
View File
@@ -1,23 +1,35 @@
<script setup lang="ts"> <script setup lang="ts">
import { import {
appMerchantOrderDetailPost, appMerchantOrderDetailPost,
appCollectCollectPost,
type MerchantOrderVo, type MerchantOrderVo,
appUserCardSelectDefaultPost, appMerchantOrderPayOrderPost, appMerchantOrderCancelOrderPost appUserCardSelectDefaultPost,
appMerchantOrderPayOrderPost,
appMerchantOrderCancelOrderPost,
appMerchantOrderZipPayVoucherPost,
} from "@/service"; } from "@/service";
import { debounce } from 'throttle-debounce' import ChooseImage from '@/components/choose-image/choose-image.vue'
const { t } = useI18n(); const { t } = useI18n();
import OrderProgress from './components/order-progress.vue' import OrderProgress from './components/order-progress.vue'
import OrderDetailSkeleton from './components/order-detail-skeleton.vue' import OrderDetailSkeleton from './components/order-detail-skeleton.vue'
import Collection from "@/components/collection/index.vue";
import PriceDetail from "@/pages-store/pages/order/components/price-detail.vue"; import PriceDetail from "@/pages-store/pages/order/components/price-detail.vue";
import CancelOrder from "@/pages-store/pages/order/components/cancel-order.vue"; import CancelOrder from "@/pages-store/pages/order/components/cancel-order.vue";
import {useConfigStore} from "@/store"; import {useConfigStore} from "@/store";
import {CollectionType, OrderStatus, EventEnum, OrderCancelStatus} from "@/constant/enums"; import {OrderStatus, EventEnum, OrderCancelStatus} from "@/constant/enums";
import {formatTimestampWithMonthName, formatTimestampShort, navGoogleMap, callPhone} from "@/utils/utils"; import {formatTimestampWithMonthName, formatTimestampShort, navGoogleMap, callPhone} from "@/utils/utils";
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>>();
// 打开价格明细 // 打开价格明细
@@ -115,6 +127,45 @@ const orderStatus = computed(() => {
return '' return ''
}) })
// ====== 为设计稿准备的数据结构(商品缩略卡/总件数/总价等)======
const orderDishList = computed(() => {
const list = orderDetail.value?.merchantOrderDishVoList as unknown as Array<any> | null | undefined
if (!Array.isArray(list)) return []
return list
})
const orderTotalItemCount = computed(() => {
return orderDishList.value.reduce((sum, item) => sum + (Number(item?.count) || 0), 0)
})
const orderTotalItemCountText = computed(() => {
return fillI18nParams(t('pages.order.totalItemCount'), {
count: orderTotalItemCount.value,
})
})
const orderTotalPrice = computed(() => {
const n = orderDetail.value?.paidAmount ?? orderDetail.value?.actualPrice ?? 0
return Number(n).toFixed(2)
})
function dishCover(dishItem: any) {
const img = dishItem?.merchantDishVo?.dishImage
if (!img || typeof img !== 'string') return ''
return img.split(',')[0] || ''
}
function dishTitle(dishItem: any) {
return dishItem?.merchantDishVo?.dishName ?? ''
}
const payMethodText = computed(() => {
// 1-信用卡 2-余额(wallet
return orderDetail.value?.payMethod === 1
? t('pages-user.choosePaymethod.creditCard')
: t('pages-user.choosePaymethod.wallet')
})
// 复制订单号 // 复制订单号
const copyOrderNumber = (text: string) => { const copyOrderNumber = (text: string) => {
if(!text) return if(!text) return
@@ -133,29 +184,6 @@ const callPhoneFn = (phone: string) => {
callPhone(phone) callPhone(phone)
} }
// 收藏店铺
function handleCollectionClick() {
debouncedEmit(orderDetail.value?.merchantVo?.id, CollectionType.STORE, ()=> {
if (orderDetail.value?.merchantVo) {
orderDetail.value.merchantVo.isCollect = !orderDetail.value.merchantVo.isCollect
}
})
}
// 防抖处理函数
const debouncedEmit = debounce(1300, (id: any, type: CollectionType, callback: ()=> void) => {
// 收藏接口
appCollectCollectPost({
body: {
targetId: id,
targetType: type
}
}).then(res=> {
callback()
})
}, {
atBegin: true, // 立即触发
})
// 支付参数 // 支付参数
const payMethodOptions = ref({ const payMethodOptions = ref({
orderId: '', orderId: '',
@@ -227,21 +255,141 @@ function openQrCode() {
function navigateTo(url: string) { function navigateTo(url: string) {
uni.navigateTo({ url }) uni.navigateTo({ url })
} }
/** 订单详情顶栏:与设计稿一致的 MM/DD HH:mm */
function formatOrderNavScheduleTime(timestamp?: number | string | null) {
if (timestamp == null || timestamp === '') return ''
const n = Number(timestamp)
if (!Number.isFinite(n)) return ''
return dayjs(n).format('MM/DD HH:mm')
}
function handleOrderDetailBack() {
const pages = getCurrentPages?.() || []
if (pages.length <= 1) {
uni.switchTab({ url: '/pages/home/index' })
return
}
uni.navigateBack()
}
/** 顶栏居中展示「预计送达 / 下单时间」:非取消、非退款申请中、非商家拒绝 */
const showOrderNavTimeBlock = computed(() => {
if (!orderDetail.value) return false
if (orderStatus.value === OrderStatus.CANCELLED) return false
if (orderDetail.value.refundStatus === OrderCancelStatus.APPLIED) return false
if (orderStatus.value === OrderStatus.MERCHANT_REJECTED) return false
return true
})
const orderNavPrimaryLine = computed(() => {
const d = orderDetail.value
if (!d) return ''
const isDelivery = +(d.receiveMethod ?? 0) === 1
const label = isDelivery
? t('pages-store.order.estimatedDeliveryTime')
: t('pages-store.order.upTime')
const timePart = formatOrderNavScheduleTime(d.endScheduledTime) || '--'
const suffix = isDelivery ? t('pages-store.order.beforeDeadline') : ''
return `${label} : ${timePart}${suffix}`
})
const orderNavSecondaryLine = computed(() => {
const d = orderDetail.value
if (!d) return ''
const ct = d.createTime
const timeStr =
formatOrderNavScheduleTime(ct) ||
(ct != null && ct !== '' ? formatTimestampShort(Number(ct)) : '')
return `${t('pages-store.order.orderTime')}: ${timeStr || '--'}`
})
// 已支付上传凭证(Zip 等线下支付)
const voucherChooseRef = ref<InstanceType<typeof ChooseImage>>()
const voucherSubmitting = ref(false)
function openUploadVoucher() {
voucherChooseRef.value?.init()
}
function normalizeVoucherUrl(payload: unknown): string {
if (Array.isArray(payload)) {
const first = payload[0]
return typeof first === 'string' ? first : ''
}
return typeof payload === 'string' ? payload : ''
}
async function onVoucherImageUploaded(urls: unknown) {
const zipPayVoucher = normalizeVoucherUrl(urls)
if (!zipPayVoucher) {
uni.showToast({ title: t('pages-store.order.voucherUploadFailed'), icon: 'none' })
return
}
const id = orderDetail.value?.id
if (id == null || String(id).trim() === '') {
uni.showToast({ title: t('pages-store.order.voucherUploadFailed'), icon: 'none' })
return
}
if (voucherSubmitting.value) return
voucherSubmitting.value = true
try {
await appMerchantOrderZipPayVoucherPost({
body: {
orderId: id,
zipPayVoucher,
},
options: { hideErrorToast: true },
})
uni.showToast({ title: t('pages-store.order.voucherSubmitSuccess'), icon: 'none' })
setTimeout(() => {
appMerchantOrderDetail()
}, 500)
}
catch {
uni.showToast({ title: t('pages-store.order.voucherSubmitFailed'), icon: 'none' })
}
finally {
voucherSubmitting.value = false
}
}
</script> </script>
<template> <template>
<navbar /> <view class="order-detail-page">
<view <wd-navbar
class="animate-in fade-in animate-ease-out animate-duration-300" safeAreaInsetTop
v-show="loading" fixed
> placeholder
<!-- 骨架屏 --> :bordered="false"
<OrderDetailSkeleton /> custom-class="order-detail-navbar !bg-white"
</view> @click-left="handleOrderDetailBack"
<view >
class="animate-in fade-in animate-ease-in animate-duration-300" <template #left>
v-if="!loading" <view class="shrink-0">
> <view class="order-detail-navbar-circle center">
<view class="i-carbon:chevron-left text-44rpx text-#111"></view>
</view>
</view>
</template>
<template #title>
<view v-if="!loading && orderDetail && showOrderNavTimeBlock" class="order-detail-nav-times">
<text class="order-detail-nav-primary">{{ orderNavPrimaryLine }}</text>
<text class="order-detail-nav-secondary">{{ orderNavSecondaryLine }}</text>
</view>
</template>
</wd-navbar>
<view
class="animate-in fade-in animate-ease-out animate-duration-300"
v-show="loading"
>
<!-- 骨架屏 -->
<OrderDetailSkeleton />
</view>
<view
class="animate-in fade-in animate-ease-in animate-duration-300 bg-white"
v-if="!loading"
>
<!-- 已取消-成功取消 --> <!-- 已取消-成功取消 -->
<template v-if="orderStatus === OrderStatus.CANCELLED"> <template v-if="orderStatus === OrderStatus.CANCELLED">
<view class="px-30rpx pt-20rpx pb-50rpx"> <view class="px-30rpx pt-20rpx pb-50rpx">
@@ -262,34 +410,27 @@ function navigateTo(url: string) {
</view> </view>
</template> </template>
<!-- 商家拒绝订单 --> <!-- 商家拒绝订单 -->
<template v-if="orderStatus === OrderStatus.MERCHANT_REJECTED"> <template v-else-if="orderStatus === OrderStatus.MERCHANT_REJECTED">
<view class="px-30rpx pt-20rpx pb-50rpx"> <view class="px-30rpx pt-8rpx pb-40rpx">
<view class="text-40rpx lh-36rpx text-#333 font-500">{{ t('pages-store.order.orderStatus.merchantRejected') }}</view> <view class="text-40rpx lh-36rpx text-#333 font-500">{{ t('pages-store.order.orderStatus.merchantRejected') }}</view>
<view class="text-28rpx lh-28rpx text-#7D7D7D mt-26rpx">{{ t('pages-store.order.orderStatus.merchantRejectedDesc') }}</view> <view class="text-28rpx lh-28rpx text-#7D7D7D mt-26rpx">{{ t('pages-store.order.orderStatus.merchantRejectedDesc') }}</view>
</view> </view>
</template> </template>
<template v-else> <template v-else>
<view class="px-30rpx pt-20rpx pb-50rpx"> <!-- 顶栏已展示预计送达/下单时间此处仅保留提示类副文案 -->
<view class="text-40rpx lh-36rpx text-#333 font-500"> <view class="px-30rpx pt-8rpx pb-24rpx" v-if="orderStatus === OrderStatus.PENDING_PAYMENT || orderDetail?.refundStatus === OrderCancelStatus.REJECTED">
<template v-if="orderDetail?.receiveMethod === 1"> <view class="text-28rpx lh-36rpx text-#7D7D7D" v-if="orderStatus === OrderStatus.PENDING_PAYMENT">
{{ t('pages-store.order.estimatedDeliveryTime') }} {{ t('pages-store.order.autoCancellation') }}
</template>
<template v-else>{{ t('pages-store.order.upTime') }}</template>
:
<!-- 订单预计送达时间-->
{{ formatTimestampShort(orderDetail?.endScheduledTime) }}
</view> </view>
<view class="text-28rpx lh-28rpx text-#7D7D7D mt-26rpx"> <view class="text-28rpx lh-36rpx text-#7D7D7D mt-16rpx" v-if="orderDetail?.refundStatus === OrderCancelStatus.REJECTED">
<view>{{ t('pages-store.order.orderTime') }}: {{ formatTimestampShort(orderDetail?.createTime) }}</view>
<!-- 订单30分钟自动取消--待支付 -->
<view v-if="orderStatus === OrderStatus.PENDING_PAYMENT">{{ t('pages-store.order.autoCancellation') }}</view>
</view>
<view class="text-28rpx lh-28rpx text-#7D7D7D mt-26rpx" v-if="orderDetail?.refundStatus === OrderCancelStatus.REJECTED">
{{ t('pages-store.order.rejectReason') }}{{ orderDetail?.rejectReason }} {{ t('pages-store.order.rejectReason') }}{{ orderDetail?.rejectReason }}
</view> </view>
</view> </view>
<!-- 订单进度 --> <!-- 订单进度紧接顶栏时间信息 -->
<view v-if="orderDetail?.refundStatus !== OrderCancelStatus.APPLIED || orderDetail?.refundStatus !== OrderCancelStatus.APPROVED" class="mb-52rpx px-30rpx"> <view
v-if="orderDetail?.refundStatus !== OrderCancelStatus.APPLIED && orderDetail?.refundStatus !== OrderCancelStatus.APPROVED"
class="order-detail-progress-wrap mb-52rpx px-30rpx"
>
<OrderProgress <OrderProgress
:steps="orderSteps" :steps="orderSteps"
:current-status="orderStatus" :current-status="orderStatus"
@@ -350,58 +491,77 @@ function navigateTo(url: string) {
</view> </view>
<view class="text-30rpx lh-30rpx text-#333 mb-24rpx mt-36rpx">{{ t('pages-store.order.deliveryPhotos') }}</view> <view class="text-30rpx lh-30rpx text-#333 mb-24rpx mt-36rpx">{{ t('pages-store.order.deliveryPhotos') }}</view>
<view class="flex items-center gap-20rpx"> <view class="flex items-center gap-20rpx">
<template v-for="item in orderDetail?.deliveryPhotos?.split(',')"> <view
v-for="(item, idx) in orderDetail?.deliveryPhotos?.split(',')"
:key="`${idx}-${item}`"
>
<wd-img width="158rpx" height="158rpx" radius="16rpx" mode="aspectFill" :src="item" :enable-preview="true" /> <wd-img width="158rpx" height="158rpx" radius="16rpx" mode="aspectFill" :src="item" :enable-preview="true" />
</template> </view>
</view> </view>
</view> </view>
<view class="w-full h-16rpx bg-#F6F6F6"></view> <view class="w-full h-16rpx bg-#F6F6F6"></view>
</view> </view>
<!-- 商品列表 --> <!-- 商品列表 -->
<view class="px-30rpx py-36rpx"> <view class="goods-section px-30rpx py-24rpx">
<!-- 商家信息 --> <view class="goods-layout">
<view @click="navigateTo('/pages-store/pages/store/index?id=' + orderDetail?.merchantVo?.id)" class="flex-center-sb h-80rpx mb-36rpx"> <view class="goods-left">
<view class="flex items-center"> <!-- 门店行:图标 + 店铺名 + 右箭头 -->
<image <view
:src="orderDetail?.merchantVo?.logo" @click="navigateTo('/pages-store/pages/store/index?id=' + orderDetail?.merchantVo?.id)"
mode="aspectFill" class="store-row-detail flex-center-sb mb-20rpx"
class="w-80rpx h-80rpx rounded-full bg-#F2F2F2 mr-24rpx shrink-0" >
/> <view class="flex items-center min-w-0">
<text class="text-30rpx lh-30rpx text-#333 font-500">{{ orderDetail?.merchantVo?.merchantName }}</text> <image
<image src="@img/chef/126.png"
src="@img/chef/142.png" class="w-40rpx h-40rpx shrink-0 mr-16rpx"
class="w-32rpx h-32rpx shrink-0 ml-10rpx" mode="aspectFit"
></image> />
</view> <text class="text-30rpx lh-36rpx text-#333 font-500 line-clamp-1">
<collection :is-collected="orderDetail?.merchantVo?.isCollect" @collectionChange="handleCollectionClick" /> {{ orderDetail?.merchantVo?.merchantName }}
</view> </text>
<view
v-for="item in orderDetail?.merchantOrderDishVoList"
:key="item.id"
class="flex items-start mb-32rpx last:mb-0"
>
<!-- 商品图片 -->
<view class="w-136rpx h-136rpx rounded-16rpx overflow-hidden mr-20rpx shrink-0">
<image
:src="item.merchantDishVo?.dishImage?.split(',')[0]"
mode="aspectFill"
class="w-full h-full bg-#F2F2F2"
/>
</view>
<!-- 商品信息 -->
<view class="flex-1">
<text class="text-30rpx lh-30rpx text-#333 font-500 block mb-20rpx">{{ item.merchantDishVo?.dishName }}</text>
<view class="flex-center-sb text-24rpx lh-24rpx text-#7D7D7D mb-24rpx">
<text class="line-clamp-1">{{ item.merchantSideDishVo?.sideDishName }} {{ item.merchantSideDishItemVo?.name }}</text>
<!-- 数量 -->
<view class="shrink-0 text-28rpx lh-28rpx text-#333 text-right font-500">
X {{ item.count }}
</view> </view>
<image
src="@img/chef/142.png"
class="w-28rpx h-28rpx shrink-0"
mode="aspectFit"
/>
</view> </view>
<text class="text-30rpx lh-30rpx text-#333 font-500">{{ `$${item.merchantDishVo?.discountPrice}` }}</text>
<!-- 商品缩略卡横向滚动 -->
<scroll-view
scroll-x
class="goods-scroll"
:show-scrollbar="false"
:enable-flex="true"
>
<view class="goods-track">
<view
v-for="(item, di) in orderDishList"
:key="item.id ?? di"
class="goods-cell"
>
<view class="goods-img-wrap">
<image
:src="dishCover(item)"
mode="aspectFill"
class="goods-img"
/>
<view v-if="Number(item?.count) > 1" class="goods-qty">
x{{ item?.count }}
</view>
</view>
<text class="goods-caption line-clamp-2">{{ dishTitle(item) }}</text>
</view>
</view>
</scroll-view>
</view>
<view class="goods-price-right">
<text class="goods-price">${{ orderTotalPrice }}</text>
<text class="goods-count">
{{ orderTotalItemCountText }}
</text>
</view> </view>
</view> </view>
</view> </view>
@@ -409,7 +569,7 @@ function navigateTo(url: string) {
<!-- 分隔线 --> <!-- 分隔线 -->
<view class="w-full h-16rpx bg-#F6F6F6"></view> <view class="w-full h-16rpx bg-#F6F6F6"></view>
<view v-if="orderDetail?.receiveMethod === 1" class="pt-36rpx"> <view v-if="orderDetail?.receiveMethod === 1" class="pt-36rpx bg-white">
<view class="text-36rpx lh-36rpx text-#333 font-500 pl-30rpx mb-4rpx">{{ t('pages-store.order.deliveryAddress') }}</view> <view class="text-36rpx lh-36rpx text-#333 font-500 pl-30rpx mb-4rpx">{{ t('pages-store.order.deliveryAddress') }}</view>
<!-- 收货地址 --> <!-- 收货地址 -->
@@ -441,7 +601,6 @@ function navigateTo(url: string) {
{{ orderDetail?.deliveryMethod }} {{ orderDetail?.deliveryMethod }}
</view> </view>
</view> </view>
<!-- 联系电话 -->
<view <view
class="flex items-center py-36rpx px-30rpx" class="flex items-center py-36rpx px-30rpx"
> >
@@ -457,7 +616,7 @@ function navigateTo(url: string) {
</view> </view>
<!-- 分隔线 --> <!-- 分隔线 -->
<view v-else class="px-30rpx pt-36rpx"> <view v-else class="px-30rpx pt-36rpx bg-white">
<view class="text-36rpx lh-36rpx text-#333 font-bold">{{ t('pickupAddress') }}</view> <view class="text-36rpx lh-36rpx text-#333 font-bold">{{ t('pickupAddress') }}</view>
<view class="flex-center-sb py-40rpx"> <view class="flex-center-sb py-40rpx">
<view class="flex items-center"> <view class="flex items-center">
@@ -486,7 +645,7 @@ function navigateTo(url: string) {
<view class="w-full h-16rpx bg-#F6F6F6"></view> <view class="w-full h-16rpx bg-#F6F6F6"></view>
<view class="border-bottom px-30rpx py-36rpx text-36rpx lh-36rpx text-#333 "> <view class="bg-white border-bottom px-30rpx py-36rpx text-36rpx lh-36rpx text-#333 ">
<view class="font-500 text-36rpx lh-36rpx mb-40rpx">{{ t('pages-store.order.orderInfo') }}</view> <view class="font-500 text-36rpx lh-36rpx mb-40rpx">{{ t('pages-store.order.orderInfo') }}</view>
<!-- 订单编号 --> <!-- 订单编号 -->
<view @click="copyOrderNumber(orderDetail?.orderNo)" class="flex-center-sb text-30rpx lh-30rpx mb-40rpx"> <view @click="copyOrderNumber(orderDetail?.orderNo)" class="flex-center-sb text-30rpx lh-30rpx mb-40rpx">
@@ -501,11 +660,10 @@ function navigateTo(url: string) {
</view> </view>
</view> </view>
<!-- 下单时间 mb-40rpx根据支付状态显示 --> <!-- 下单时间 mb-40rpx根据支付状态显示 -->
<view class="flex-center-sb text-30rpx lh-30rpx" :class="[orderStatus !== OrderStatus.PENDING_PAYMENT ? 'mb-40rpx' : '']"> <view class="flex-center-sb text-30rpx lh-30rpx mb-40rpx">
<text>{{ t('pages-store.order.orderTime') }}</text> <text>{{ t('pages-store.order.orderTime') }}</text>
<text>{{ formatTimestampWithMonthName(orderDetail?.createTime) }}</text> <text>{{ formatTimestampWithMonthName(orderDetail?.createTime) }}</text>
</view> </view>
<!-- 待支付 -->
<template v-if="orderStatus !== OrderStatus.PENDING_PAYMENT"> <template v-if="orderStatus !== OrderStatus.PENDING_PAYMENT">
<!-- 支付方式 --> <!-- 支付方式 -->
<view class="flex-center-sb text-30rpx lh-30rpx mb-40rpx"> <view class="flex-center-sb text-30rpx lh-30rpx mb-40rpx">
@@ -516,14 +674,9 @@ function navigateTo(url: string) {
mode="aspectFill" mode="aspectFill"
class="w-44rpx h-44rpx shrink-0 mr-10rpx" class="w-44rpx h-44rpx shrink-0 mr-10rpx"
/> />
<text>{{ orderDetail?.payMethod === 1 ? t('pages-user.choosePaymethod.creditCard') : t('pages-user.choosePaymethod.wallet') }}</text> <text>{{ payMethodText }}</text>
</view> </view>
</view> </view>
<!-- 支付时间 -->
<view class="flex-center-sb text-30rpx lh-30rpx">
<text>{{ t('pages-user.member.payTime') }}</text>
<text>{{ formatTimestampWithMonthName(orderDetail?.payTime) }}</text>
</view>
</template> </template>
</view> </view>
@@ -606,12 +759,21 @@ function navigateTo(url: string) {
</template> </template>
</template> </template>
<template v-else> <template v-else>
<!-- 待支付 -->
<template v-if="orderStatus === OrderStatus.PENDING_PAYMENT"> <template v-if="orderStatus === OrderStatus.PENDING_PAYMENT">
<wd-button @click="goPay" custom-class="!h-108rpx !bg-14181B !text-36rpx !lh-108rpx !text-#fff !rounded-16rpx" block> <!-- 上传支付凭证 -->
<wd-button
:loading="voucherSubmitting"
:disabled="voucherSubmitting"
@click="openUploadVoucher"
custom-class="!h-108rpx !bg-transparent !text-32rpx !font-500 !lh-108rpx !text-#333 !rounded-46rpx !border-#666666 !border-solid !border-1rpx"
block
>
{{ t('pages-store.order.uploadPaidVoucher') }}
</wd-button>
<wd-button @click="goPay" custom-class="!h-108rpx !bg-14181B !text-32rpx !lh-108rpx !text-#fff !rounded-46rpx !mt-22rpx" block>
{{ t('common.goPay') }} {{ t('common.goPay') }}
</wd-button> </wd-button>
<view @click="openCancelOrder" class="text-center mt-52rpx text-36rpx lh-36rpx text-#333 font-500"> <view @click="openCancelOrder" class="text-center mt-22rpx text-28rpx lh-28rpx text-#333 font-500">
{{ t('common.cancel') }} {{ t('common.cancel') }}
</view> </view>
</template> </template>
@@ -678,13 +840,154 @@ function navigateTo(url: string) {
<!-- 支付订单 --> <!-- 支付订单 -->
<password-container @success="payPawSuccess" ref="passwordInputRef" /> <password-container @success="payPawSuccess" ref="passwordInputRef" />
<!-- 核销订单 --> <!-- 核销订单 -->
<use-code ref="useCodeRef" /> <use-code ref="useCodeRef" />
<!-- 支付凭证:相册/拍照,上传后回调真实 URL -->
<choose-image ref="voucherChooseRef" :count="1" @change="onVoucherImageUploaded" />
</view>
</view> </view>
</template> </template>
<style> <style>
page { page {
background-color: #fff; background-color: #F6F6F6;
} }
</style> </style>
<style scoped> <style scoped>
/* 订单详情:商品缩略卡横向滚动(对齐设计稿) */
.goods-section {
background: #fff;
}
.goods-layout {
display: flex;
justify-content: space-between;
align-items: flex-end;
}
.goods-left {
flex: 1;
min-width: 0;
}
.store-row-detail {
align-items: center;
}
.goods-price-right {
flex-shrink: 0;
text-align: right;
padding-left: 24rpx;
padding-bottom: 10rpx;
}
.goods-price {
display: block;
font-size: 32rpx;
line-height: 40rpx;
font-weight: 700;
color: #14181B;
}
.goods-count {
display: block;
margin-top: 8rpx;
font-size: 24rpx;
line-height: 30rpx;
color: #7D7D7D;
font-weight: 500;
}
.goods-scroll {
width: 100%;
}
.goods-track {
display: flex;
flex-direction: row;
gap: 16rpx;
padding-right: 8rpx;
}
.goods-cell {
width: 128rpx;
flex-shrink: 0;
}
.goods-img-wrap {
position: relative;
width: 128rpx;
height: 128rpx;
border-radius: 16rpx;
overflow: hidden;
background: #F2F2F2;
}
.goods-img {
width: 100%;
height: 100%;
}
.goods-qty {
position: absolute;
right: 6rpx;
bottom: 6rpx;
min-width: 36rpx;
padding: 4rpx 10rpx;
background: rgba(20, 24, 27, 0.85);
color: #fff;
font-size: 20rpx;
line-height: 24rpx;
border-radius: 8rpx;
text-align: center;
}
.goods-caption {
display: block;
margin-top: 10rpx;
font-size: 22rpx;
line-height: 28rpx;
color: #333;
}
.order-detail-navbar-circle {
width: 72rpx;
height: 72rpx;
border-radius: 50%;
background-color: #fff;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08);
}
.order-detail-nav-times {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0 8rpx;
max-width: 480rpx;
}
.order-detail-nav-primary {
font-size: 28rpx;
line-height: 36rpx;
font-weight: 600;
color: #14181b;
text-align: center;
}
.order-detail-nav-secondary {
margin-top: 6rpx;
font-size: 24rpx;
line-height: 30rpx;
color: #7d7d7d;
text-align: center;
}
.order-detail-progress-wrap {
padding-top: 8rpx;
}
:deep(.order-detail-navbar.wd-navbar .wd-navbar__title) {
max-width: 520rpx;
overflow: visible;
white-space: normal;
}
</style> </style>
@@ -1,6 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import {useConfigStore} from "@/store"; import {useConfigStore} from "@/store";
import {receiveCouponApi} from "@/pages-user/service"; import {receiveCouponApi} from "@/pages-user/service";
import couponBg from '@img-store/coupon.png'
import couponBg2 from '@img-store/coupon-2.png'
import couponRightBg from '@img-store/coupon-right.png'
import couponRightBg2 from '@img-store/coupon-right-2.png'
const configStore = useConfigStore() const configStore = useConfigStore()
const { t, locale } = useI18n() const { t, locale } = useI18n()
@@ -57,17 +61,16 @@ function formatMoney(value: unknown) {
return n.toFixed(2) return n.toFixed(2)
} }
function couponTitleText(item: any) { function couponAmountText(item: any) {
const type = Number(item?.couponType) const type = Number(item?.couponType)
const discountValue = Number(item?.discountValue) const discountValue = Number(item?.discountValue)
// 标题保持原样:折扣券显示百分比,满减券只显示减免金额(不展示门槛)
if (type === 1) { if (type === 1) {
const pct = Number.isFinite(discountValue) ? Number(discountValue * 100).toFixed(0) : '0' const pct = Number.isFinite(discountValue) ? Number(discountValue * 100).toFixed(0) : '0'
return `${pct}% ${t('pages-store.store.couponOff')}` return `${pct}%`
} }
return `$${formatMoney(discountValue)} ${t('pages-store.store.couponOff')}` return `$${formatMoney(discountValue)}`
} }
function couponBenefitText(item: any) { function couponBenefitText(item: any) {
@@ -91,44 +94,58 @@ function couponBenefitText(item: any) {
</script> </script>
<template> <template>
<wd-popup v-model="show" custom-style="border-radius:0;" position="bottom" @close="handleClose"> <wd-popup v-model="show" custom-style="border-radius: 24rpx 24rpx 0 0;" position="bottom" @close="handleClose">
<view class="bg-#F5F5F5 px-32rpx pt-30rpx"> <view class="coupon-popup-wrap">
<view class="text-40rpx lh-40rpx text-#333 font-bold text-center mb-60rpx">{{ t('pages-store.store.claimCoupon') }}</view> <view class="text-36rpx lh-36rpx text-#333 font-bold text-center pt-40rpx pb-36rpx">
<scroll-view scroll-y class="h-1000rpx"> {{ t('pages-store.store.claimCoupon') }}
<template v-for="item in couponList" :key="item.id"> </view>
<view class="coupon-item h-328rpx flex flex-col mb-30rpx last:mb-0">
<view class="flex-1 pt-40rpx px-58rpx"> <scroll-view scroll-y class="coupon-popup-list">
<view class="line-clamp-1 text-34rpx lh-34rpx text-#333 font-bold"> <view
<!-- couponType 1-折扣券, 2-满减券--> v-for="item in couponList"
{{ couponTitleText(item) }} :key="item.id"
class="coupon-card"
>
<image
class="coupon-card-bg"
:src="item.userCouponVo ? couponBg2 : couponBg"
mode="scaleToFill"
/>
<view class="coupon-card-body">
<view class="coupon-card-info">
<view class="coupon-card-amount">
<text class="coupon-amount-value" :class="{ 'coupon-amount-value--disabled': item.userCouponVo }">{{ couponAmountText(item) }}</text>
<text class="coupon-amount-label" :class="{ 'coupon-amount-label--disabled': item.userCouponVo }">{{ t('pages-store.store.couponOff') }}</text>
</view> </view>
<view class="text-24rpx lh-32rpx text-#999 my-18rpx"> <view class="coupon-card-desc">
{{ t('pages-store.store.validDays') }}: {{ item.validDays }}{{ daySuffix(item.validDays) }}
</view>
<view class="text-24rpx lh-32rpx text-#999 my-18rpx">
{{ item.nameZh }} {{ item.nameZh }}
<text class="ml-16rpx">{{ t('pages-store.store.validDays') }}:{{ item.validDays }}{{ daySuffix(item.validDays) }}</text>
</view> </view>
<view class="text-28rpx lh-28rpx text-#333 flex items-center"> <view class="coupon-card-benefit">
<image <image
class="w-36rpx h-36rpx shrink-0 mr-10rpx" class="coupon-benefit-icon"
src="@img/chef/106.png" src="@img/chef/106.png"
></image> ></image>
{{ t('pages-store.store.get') }} {{ couponBenefitText(item) }} <text>{{ t('pages-store.store.get') }} {{ couponBenefitText(item) }}</text>
<template v-if="Number(item?.couponType) === 1"> {{ t('pages-store.store.couponOff') }}</template>
</view> </view>
</view> </view>
<template v-if="item.userCouponVo">
<view class="h-86rpx bg-#e6e6e6 lh-84rpx text-center text-28rpx text-#fff rounded-br-18rpx rounded-bl-18rpx">
{{ t('pages-store.store.claimed') }}
</view>
</template>
<template v-else>
<view @click="confirmCoupon(item)" class="h-84rpx lh-84rpx text-center text-28rpx text-#fff">
{{ t('pages-store.store.claimNow') }}
</view>
</template>
</view> </view>
</template> <view
v-if="item.userCouponVo"
class="coupon-claim-wrap"
>
<image class="coupon-claim-bg" :src="couponRightBg2" mode="scaleToFill" />
<text class="coupon-claim-text coupon-claim-text--disabled">{{ t('pages-store.store.claimed') }}</text>
</view>
<view
v-else
class="coupon-claim-wrap"
@click="confirmCoupon(item)"
>
<image class="coupon-claim-bg" :src="couponRightBg" mode="scaleToFill" />
<text class="coupon-claim-text">{{ t('pages-store.store.claimNow') }}</text>
</view>
</view>
</scroll-view> </scroll-view>
<view :style="[configStore.iosSafeBottomPlaceholder]" /> <view :style="[configStore.iosSafeBottomPlaceholder]" />
@@ -137,10 +154,126 @@ function couponBenefitText(item: any) {
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.coupon-item { .coupon-popup-wrap {
background-image: url('@img/chef/103.png'); background: #F5F5F5;
background-size: 100% 100%; padding: 0 32rpx;
background-repeat: no-repeat; }
background-position: center;
.coupon-popup-list {
max-height: 860rpx;
padding-bottom: 30rpx;
}
.coupon-card {
position: relative;
margin-bottom: 24rpx;
&:last-child {
margin-bottom: 0;
}
}
.coupon-card-bg {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 0;
}
.coupon-card-body {
position: relative;
z-index: 1;
padding: 32rpx 180rpx 32rpx 32rpx;
}
.coupon-card-info {
min-width: 0;
}
.coupon-card-amount {
display: flex;
align-items: baseline;
margin-bottom: 16rpx;
}
.coupon-amount-value {
font-size: 48rpx;
line-height: 56rpx;
font-weight: bold;
color: #CE7138;
&--disabled {
color: #BFBFBF;
}
}
.coupon-amount-label {
font-size: 26rpx;
line-height: 26rpx;
color: #CE7138;
font-weight: 500;
margin-left: 10rpx;
&--disabled {
color: #BFBFBF;
}
}
.coupon-card-desc {
font-size: 24rpx;
line-height: 32rpx;
color: #999;
margin-bottom: 14rpx;
}
.coupon-card-benefit {
font-size: 24rpx;
line-height: 32rpx;
color: #333;
display: flex;
align-items: center;
}
.coupon-benefit-icon {
width: 32rpx;
height: 32rpx;
flex-shrink: 0;
margin-right: 8rpx;
}
.coupon-claim-wrap {
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 168rpx;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
}
.coupon-claim-bg {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 0;
}
.coupon-claim-text {
position: relative;
z-index: 1;
color: #fff;
font-size: 26rpx;
// font-weight: 600;
text-align: center;
&--disabled {
opacity: 0.6;
}
} }
</style> </style>
@@ -1,94 +1,80 @@
<template> <template>
<view class="store-skeleton"> <view class="store-skeleton">
<!-- 顶部图片区域 --> <!-- 顶部导航栏 -->
<view class="relative h-562rpx"> <view class="fixed top-0 left-0 z-9 w-full pt-6rpx">
<!-- 状态栏 --> <status-bar />
<view class="fixed top-0 left-0 z-9 w-full pt-6rpx"> <view class="nav-bar">
<view class="flex-center-sb px-30rpx"> <view class="nav-btn skeleton-item"></view>
<view class="back-button-skeleton skeleton-item"></view> <view class="nav-btn skeleton-item"></view>
<view class="flex">
<view class="action-button-skeleton skeleton-item mr-20rpx"></view>
<view class="action-button-skeleton skeleton-item mr-20rpx"></view>
<view class="action-button-skeleton skeleton-item"></view>
</view>
</view>
</view> </view>
</view>
<!-- 主图骨架 --> <!-- 占位 -->
<view class="main-image-skeleton skeleton-item w-750rpx h-562rpx absolute top-0 left-0"></view> <status-bar />
<view class="h-88rpx"></view>
<!-- 店铺 Logo -->
<view class="flex justify-center pt-20rpx">
<view class="logo-skeleton skeleton-item"></view>
</view> </view>
<!-- 店铺信息区域 --> <!-- 店铺信息区域 -->
<view class="px-30rpx pt-40rpx pb-42rpx"> <view class="px-30rpx pt-24rpx pb-30rpx">
<!-- 店铺名称 --> <view class="flex flex-col items-center">
<view class="text-center"> <!-- 店铺名称 -->
<view class="store-name-skeleton skeleton-item mx-auto mb-16rpx"></view> <view class="name-skeleton skeleton-item mb-16rpx"></view>
<!-- 评分 + CHEFLINK -->
<!-- 评分和信息 --> <view class="flex items-center mb-12rpx">
<view class="center mb-16rpx"> <view class="rating-skeleton skeleton-item mr-16rpx"></view>
<view class="rating-skeleton skeleton-item mr-20rpx"></view> <view class="cheflink-skeleton skeleton-item"></view>
<view class="chef-link-skeleton skeleton-item mr-20rpx"></view>
<view class="distance-skeleton skeleton-item"></view>
</view> </view>
<!-- 总销量 -->
<!-- 描述信息 --> <view class="sales-text-skeleton skeleton-item"></view>
<view class="description-skeleton skeleton-item mx-auto mb-16rpx"></view>
<view class="description-skeleton skeleton-item mx-auto mb-16rpx"></view>
<!-- 标签 -->
<view class="tag-skeleton skeleton-item mx-auto"></view>
</view> </view>
<!-- 配送方式切换 -->
<view class="delivery-switch-skeleton skeleton-item mt-40rpx"></view>
<!-- 配送信息卡片 --> <!-- 配送信息卡片 -->
<view class="delivery-info-skeleton skeleton-item mt-36rpx"></view> <view class="delivery-card-skeleton skeleton-item mt-30rpx"></view>
</view>
<!-- 标签页导航 --> <!-- 优惠券标签行占位 -->
<view class="tabs-skeleton"> <view class="flex items-center mt-24rpx">
<view class="flex"> <view class="coupon-tag-skeleton skeleton-item mr-16rpx"></view>
<view <view class="coupon-tag-skeleton skeleton-item mr-16rpx"></view>
v-for="i in 5" <view class="coupon-tag-skeleton skeleton-item"></view>
:key="i" <view class="flex-1"></view>
class="tab-item-skeleton skeleton-item mr-40rpx" <view class="claim-skeleton skeleton-item"></view>
></view>
</view> </view>
<view class="tabs-divider skeleton-item"></view>
</view> </view>
<!-- 商品列表区域 --> <!-- 分类胶囊标签 -->
<view class="px-30rpx"> <view class="flex items-center px-30rpx pb-24rpx">
<!-- 分类标题 --> <view
<view class="section-title-skeleton skeleton-item my-36rpx"></view> v-for="i in 4"
:key="i"
class="tab-chip-skeleton skeleton-item"
></view>
</view>
<!-- 商品网格 --> <!-- 商品列表 -->
<view class="grid grid-cols-2 gap-30rpx"> <view class="px-30rpx">
<view class="grid grid-cols-2 gap-24rpx">
<view <view
v-for="i in 6" v-for="i in 4"
:key="i" :key="i"
class="product-item-skeleton" class="product-skeleton"
> >
<!-- 商品图片 --> <!-- 商品图片 -->
<view class="product-image-skeleton skeleton-item mb-28rpx"></view> <view class="product-img-skeleton skeleton-item"></view>
<!-- 价格 + 销量 -->
<!-- 商品名称 --> <view class="flex items-center justify-between mt-16rpx">
<view class="product-name-skeleton skeleton-item mb-12rpx"></view>
<!-- 价格行 -->
<view class="flex-center-sb mb-12rpx">
<view class="price-skeleton skeleton-item"></view> <view class="price-skeleton skeleton-item"></view>
<view class="member-price-skeleton skeleton-item"></view> <view class="sales-skeleton skeleton-item"></view>
</view> </view>
<!-- 商品名称 -->
<!-- 原价和销量 --> <view class="product-name-skeleton skeleton-item mt-8rpx"></view>
<view class="flex-center-sb"> <!-- 会员价 + 加购按钮 -->
<view> <view class="flex items-center justify-between mt-12rpx">
<view class="original-price-skeleton skeleton-item mb-8rpx"></view> <view class="member-skeleton skeleton-item"></view>
<view class="sales-skeleton skeleton-item"></view> <view class="add-btn-skeleton skeleton-item"></view>
</view>
<view class="add-button-skeleton skeleton-item"></view>
</view> </view>
</view> </view>
</view> </view>
@@ -97,25 +83,18 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
// Store页面骨架屏组件
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
// 通用骨架屏样式
.skeleton-item { .skeleton-item {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%; background-size: 200% 100%;
animation: shimmer 1.5s infinite; animation: shimmer 1.5s infinite;
} }
// 闪烁动画
@keyframes shimmer { @keyframes shimmer {
0% { 0% { background-position: -200% 0; }
background-position: -200% 0; 100% { background-position: 200% 0; }
}
100% {
background-position: 200% 0;
}
} }
.store-skeleton { .store-skeleton {
@@ -123,186 +102,126 @@
min-height: 100vh; min-height: 100vh;
} }
// 通用布局类 /* 顶部导航 */
.flex-center-sb { .nav-bar {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 0 30rpx;
height: 88rpx;
} }
.center { .nav-btn {
display: flex;
align-items: center;
justify-content: center;
}
// 状态栏区域
.status-bar-skeleton {
width: 100%;
height: 44rpx;
margin-bottom: 20rpx;
}
.back-button-skeleton {
width: 48rpx; width: 48rpx;
height: 48rpx; height: 48rpx;
border-radius: 24rpx; border-radius: 24rpx;
} }
.action-button-skeleton { /* 店铺 Logo */
width: 68rpx; .logo-skeleton {
height: 68rpx; width: 128rpx;
border-radius: 34rpx; height: 128rpx;
border-radius: 24rpx;
} }
// 主图区域 /* 店铺信息 */
.main-image-skeleton { .name-skeleton {
border-radius: 0; width: 360rpx;
} height: 44rpx;
// 店铺信息区域
.store-name-skeleton {
width: 400rpx;
height: 40rpx;
border-radius: 8rpx; border-radius: 8rpx;
} }
.rating-skeleton { .rating-skeleton {
width: 160rpx;
height: 24rpx;
border-radius: 6rpx;
}
.cheflink-skeleton {
width: 120rpx; width: 120rpx;
height: 24rpx; height: 24rpx;
border-radius: 6rpx; border-radius: 6rpx;
} }
.chef-link-skeleton { .sales-text-skeleton {
width: 100rpx; width: 140rpx;
height: 24rpx; height: 24rpx;
border-radius: 6rpx; border-radius: 6rpx;
} }
.distance-skeleton { /* 配送信息卡片 */
width: 80rpx; .delivery-card-skeleton {
height: 24rpx;
border-radius: 6rpx;
}
.description-skeleton {
width: 500rpx;
height: 24rpx;
border-radius: 6rpx;
}
.tag-skeleton {
width: 300rpx;
height: 48rpx;
border-radius: 8rpx;
}
// 配送方式
.delivery-switch-skeleton {
width: 318rpx;
height: 60rpx;
border-radius: 30rpx;
}
.delivery-info-skeleton {
width: 100%; width: 100%;
height: 164rpx; height: 140rpx;
border-radius: 20rpx; border-radius: 20rpx;
} }
// 标签页导航 /* 优惠券标签 */
.tabs-skeleton { .coupon-tag-skeleton {
padding: 0 30rpx; width: 120rpx;
height: 52rpx;
border-radius: 8rpx;
flex-shrink: 0;
}
.tab-item-skeleton { .claim-skeleton {
width: 120rpx; width: 100rpx;
height: 30rpx; height: 28rpx;
border-radius: 6rpx; border-radius: 6rpx;
} flex-shrink: 0;
}
.tabs-divider { /* 胶囊标签 */
width: 100%; .tab-chip-skeleton {
height: 10rpx; width: 100rpx;
margin-top: 32rpx; height: 60rpx;
border-radius: 0; border-radius: 30rpx;
margin-right: 20rpx;
flex-shrink: 0;
&:last-child {
margin-right: 0;
} }
} }
// 分类标题 /* 商品卡片 */
.section-title-skeleton { .product-skeleton {
width: 200rpx; margin-bottom: 24rpx;
height: 40rpx; }
.product-img-skeleton {
width: 100%;
height: 248rpx;
border-radius: 24rpx;
}
.price-skeleton {
width: 120rpx;
height: 36rpx;
border-radius: 6rpx;
}
.sales-skeleton {
width: 80rpx;
height: 22rpx;
border-radius: 6rpx;
}
.product-name-skeleton {
width: 85%;
height: 34rpx;
border-radius: 6rpx;
}
.member-skeleton {
width: 160rpx;
height: 42rpx;
border-radius: 8rpx; border-radius: 8rpx;
} }
// 商品项 .add-btn-skeleton {
.product-item-skeleton { width: 52rpx;
.product-image-skeleton { height: 52rpx;
width: 100%; border-radius: 26rpx;
height: 248rpx;
border-radius: 24rpx;
}
.product-name-skeleton {
width: 80%;
height: 30rpx;
border-radius: 6rpx;
}
.price-skeleton {
width: 100rpx;
height: 30rpx;
border-radius: 6rpx;
}
.member-price-skeleton {
width: 170rpx;
height: 28rpx;
border-radius: 14rpx;
}
.original-price-skeleton {
width: 80rpx;
height: 28rpx;
border-radius: 6rpx;
}
.sales-skeleton {
width: 100rpx;
height: 28rpx;
border-radius: 6rpx;
}
.add-button-skeleton {
width: 64rpx;
height: 64rpx;
border-radius: 32rpx;
}
}
// 分隔线
.divider-skeleton {
width: 100%;
height: 10rpx;
border-radius: 0;
}
// 底部提示
.bottom-tip-skeleton {
height: 96rpx;
border-radius: 0;
}
// 响应式设计
@media (max-width: 750rpx) {
.grid {
gap: 20rpx;
}
.product-item-skeleton {
.product-image-skeleton {
height: 200rpx;
}
}
} }
</style> </style>
File diff suppressed because it is too large Load Diff
+504 -247
View File
@@ -165,6 +165,20 @@ function getStoreDetail() {
] ]
} }
// 用详情中的首屏菜品初始化列表,并让下一次触底从第 2 页开始,避免仍请求第 1 页导致与详情数据重复
const firstRecords = res.data?.dishPage?.records
if (firstRecords && firstRecords.length > 0) {
dishListByQuery.value = [...firstRecords]
const gotFullPage = firstRecords.length >= pageSize.value
hasMore.value = gotFullPage
pageNum.value = gotFullPage ? 2 : 1
} else if (tabs.value.length > 0) {
dishListByQuery.value = []
hasMore.value = true
pageNum.value = 1
nextTick(() => loadDishList(false))
}
// 商户的经纬度存在,并且用户的经纬度也存在 // 商户的经纬度存在,并且用户的经纬度也存在
if(res.data.latitude && res.data.longitude && userStore.userLocation.latitude && userStore.userLocation.longitude) { if(res.data.latitude && res.data.longitude && userStore.userLocation.latitude && userStore.userLocation.longitude) {
let distance = getDistanceInMiles(res.data.latitude, res.data.longitude, userStore.userLocation.latitude, userStore.userLocation.longitude) let distance = getDistanceInMiles(res.data.latitude, res.data.longitude, userStore.userLocation.latitude, userStore.userLocation.longitude)
@@ -309,15 +323,8 @@ const hasMore = ref(true);
const isLoadingMore = ref(false); const isLoadingMore = ref(false);
const dishListByQuery = ref<any[]>([]); const dishListByQuery = ref<any[]>([]);
// 计算当前显示的商品列表 // 计算当前显示的商品列表(统一走 dishListByQuery,避免「全部」首屏用详情、加载更多再拼第 1 页造成重复)
const currentDishList = computed(() => { const currentDishList = computed(() => dishListByQuery.value || [])
if(tabs.value[activeTab.value].key==''&&pageNum.value===1){
return storeDetail.value?.dishPage?.records
}
console.log(tabs.value[activeTab.value].key=='');
// 使用 dishListByQuery 作为数据源
return dishListByQuery.value || []
})
// 加载菜品列表 // 加载菜品列表
async function loadDishList(isLoadMore = false) { async function loadDishList(isLoadMore = false) {
@@ -352,11 +359,10 @@ async function loadDishList(isLoadMore = false) {
if (res.data && res.data.rows) { if (res.data && res.data.rows) {
if (isLoadMore) { if (isLoadMore) {
// 加载更多,追加数据 // 加载更多:按 id 去重,防止接口分页重叠或重复请求时的重复项
dishListByQuery.value = [ const existingIds = new Set(dishListByQuery.value.map((r: any) => r.id))
...dishListByQuery.value, const nextRows = res.data.rows.filter((r: any) => r != null && !existingIds.has(r.id))
...res.data.rows dishListByQuery.value = [...dishListByQuery.value, ...nextRows]
];
} else { } else {
// 首次加载或刷新 // 首次加载或刷新
dishListByQuery.value = res.data.rows; dishListByQuery.value = res.data.rows;
@@ -417,10 +423,7 @@ function navigateBack() {
function navigateToCart() { function navigateToCart() {
uni.navigateTo({ uni.navigateTo({
url: url: '/pages-user/pages/cart/index',
'/pages-user/pages/cart/store-cart'
+ '?storeId=' + storeID.value
+ '&storeName=' + encodeURIComponent(storeDetail.value.merchantName),
}) })
} }
@@ -455,6 +458,16 @@ function navigateTo(url: string) {
uni.navigateTo({ url }) uni.navigateTo({ url })
} }
function isSoldOutStock(stockLike: unknown) {
const n = Number(stockLike)
return !Number.isNaN(n) && n <= 0
}
function getDishPromoLabel(item: any): string {
const raw = item?.marketingLabel ?? item?.hotSaleTag ?? item?.rankTag ?? item?.promotionLabel
return typeof raw === 'string' && raw.trim() ? raw.trim() : ''
}
// 分享商家 // 分享商家
function handleShare() { function handleShare() {
uni.shareWithSystem({ uni.shareWithSystem({
@@ -472,132 +485,114 @@ function handleShare() {
<template> <template>
<view <view
class="animate-in fade-in animate-ease-out animate-duration-300" class="animate-in fade-in animate-ease-out animate-duration-300"
v-show="loading" v-show="loading"
> >
<!-- 骨架屏 -->
<StoreSkeleton/> <StoreSkeleton/>
</view> </view>
<view <view
class="animate-in fade-in animate-ease-in animate-duration-300" class="animate-in fade-in animate-ease-in animate-duration-300"
v-if="!loading" v-if="!loading"
> >
<!-- 页面内容 -->
<view class="store-box"> <view class="store-box">
<view class="relative"> <!-- 顶部导航栏 -->
<!-- 状态栏 --> <view class="fixed top-0 left-0 z-9 w-full transition-all pt-6rpx" :class="[showStatusBar ? 'bg-#fff' : '']">
<view class="fixed top-0 left-0 z-9 w-full transition-all pt-6rpx" :class="[showStatusBar ? 'bg-#fff' : '']">
<status-bar />
<view class="flex-center-sb px-30rpx">
<image
@click="navigateBack"
src="@img/chef/1327.png"
mode="aspectFill"
class="w-48rpx h-48rpx relative z-1"
/>
<view class="flex items-center">
<image
@click="navigateTo('/pages-store/pages/store/search/index?id=' + storeID)"
src="@img-store/1333.png"
mode="aspectFill"
class="w-68rpx h-68rpx relative z-1 mr-20rpx"
/>
<view @click="handleCollectionClick" class="w-68rpx h-68rpx relative z-1 mr-20rpx">
<image
v-if="!storeDetail?.isCollect"
src="@img-store/1334.png"
mode="aspectFill"
class="w-68rpx h-68rpx"
/>
<image
v-else
src="@img-store/1337.png"
mode="aspectFill"
class="w-68rpx h-68rpx"
/>
</view>
<image
@click="handleShare"
src="@img-store/1335.png"
mode="aspectFill"
class="w-68rpx h-68rpx relative z-1"
/>
</view>
</view>
</view>
<view class="anchors"></view>
<!-- 主图 -->
<!-- <image-->
<!-- :src="storeDetail?.shopImages?.split(',')[0]"-->
<!-- mode="aspectFill"-->
<!-- class="w-750rpx h-562rpx absolute top-0 left-0"-->
<!-- />-->
<status-bar /> <status-bar />
<wd-swiper class="bg-common" v-if="storeDetail && storeDetail?.shopImages?.split(',').length > 0" :list="storeDetail?.shopImages?.split(',')" height="562rpx" autoplay></wd-swiper> <view class="flex-center-sb px-30rpx h-88rpx">
<image
@click="navigateBack"
src="@img/chef/1327.png"
mode="aspectFill"
class="w-48rpx h-48rpx relative z-1"
/>
<image
@click="handleShare"
src="@img-store/1335.png"
mode="aspectFill"
class="w-48rpx h-48rpx relative z-1"
/>
</view>
</view> </view>
<view class="px-30rpx pt-40rpx pb-42rpx"> <!-- 占位 -->
<!-- 店铺信息 --> <status-bar />
<view class="h-44rpx"></view>
<!-- 店铺 Logo -->
<view class="flex justify-center ">
<image
:src="storeDetail?.shopImages?.split(',')[0]"
mode="aspectFill"
class="w-128rpx h-128rpx rounded-24rpx bg-common"
/>
</view>
<!-- 店铺信息区域 -->
<view class="px-30rpx pt-24rpx pb-30rpx">
<view class="text-center"> <view class="text-center">
<view class="text-center text-40rpx lh-40rpx text-#333 font-bold"> <!-- 店铺名称 -->
<view class="text-36rpx lh-44rpx text-#333 font-bold">
{{ storeDetail?.merchantName }} {{ storeDetail?.merchantName }}
</view> </view>
<view class="center text-24rpx lh-24rpx my-16rpx"> <!-- 评分 + CHEFLINK -->
<view class="center text-24rpx lh-24rpx mt-16rpx">
<view class="flex items-center"> <view class="flex items-center">
<text class="text-#333 font-500">{{ storeDetail?.rating }}</text> <text class="text-#333 font-500">{{ storeDetail?.rating }}</text>
<image <image
src="@img/chef/124.png" src="@img/chef/124.png"
class="w-24rpx h-24rpx mx-4rpx" class="w-24rpx h-24rpx mx-4rpx"
></image> ></image>
<text class="text-#7D7D7D">({{ storeDetail?.commentCount }})</text> <text class="text-#7D7D7D">({{ storeDetail?.commentCount }})</text>
</view> </view>
<view class="flex items-center text-#CE7138 px-10rpx"> <view class="flex items-center text-#CE7138 px-10rpx">
<image <image
src="@img-store/1339.png" src="@img-store/1339.png"
class="w-24rpx h-24rpx mr-4rpx" class="w-24rpx h-24rpx mr-4rpx"
></image> ></image>
CHEFLINK CHEFLINK
</view> </view>
<text v-if="+storeDetail?.deliveryService === 1" class="text-#7D7D7D">
{{ storeDetail?.deliveryTime }}{{ daySuffix(storeDetail?.deliveryTime) }}
</text>
</view> </view>
<view class="text-24rpx lh-24rpx text-#7D7D7D text-center mt-16rpx"> <!-- 总销量 -->
{{ t('pages-store.store.title') }} US${{ storeDetail?.minOrderPrice }} <view class="text-24rpx lh-24rpx text-#7D7D7D text-center mt-12rpx">
{{ t('common.sales') }}{{ storeDetail?.totalOrderCount }}
</view> </view>
<!--根据商家营业时间计算处理-->
<view class="text-24rpx lh-24rpx text-#7D7D7D text-center mt-16rpx">
<template v-if="closingInfo.show">
{{ t('pages-store.store.tips') }} {{ closingInfo.minutes }} {{ t('pages-store.store.tips1') }}
</template>
<template v-else>
{{ parseBusinessHoursUtils(storeDetail?.businessHours) }}
</template>
<text v-if="storeDistance" class="ml-10rpx">
{{ t('common.distance') }} {{ storeDistance }} {{ t('common.mile') }}
</text>
</view>
<text v-if="storeDetail?.totalOrderCount > 0" class="mt-16rpx h-48rpx bg-#00A76D/18 rounded-8rpx px-22rpx mx-auto text-24rpx lh-24rpx text-#00A76D mr-20rpx">
{{ t('common.sales') }} {{ storeDetail?.totalOrderCount }}
</text>
<text v-if="storeDetail?.reorderedCount > 0" class="mt-16rpx h-48rpx bg-#00A76D/18 rounded-8rpx px-22rpx mx-auto text-24rpx lh-24rpx text-#00A76D">
{{ storeDetail?.reorderedCount }} {{ t('pages-store.store.tips2') }}
</text>
</view> </view>
<!-- 新功能插入区域 start --> <!-- 配送信息卡片 -->
<!-- 商家优惠券 --> <view v-if="showDeliverySwitch" class="delivery-info-card">
<view class="py-24rpx mt-40rpx border-top border-bottom" v-if="storeCouponList.length"> <template v-if="+storeDetail?.deliveryService === 1 && deliveryMethod === 0">
<view class="flex items-center justify-between mb-24rpx"> <view class="delivery-info-left">
<text class="text-28rpx lh-28rpx font-500 text-#333">{{ t('pages-store.store.merchantDiscounts') }}</text> <view>{{ t('pages-store.store.tips4') }} $ {{ storeDetail?.minOrderPrice }}</view>
<view @click="handleClaimNow" class="flex items-center"> <view>{{ t('pages-store.store.tips5') }} $ {{ storeDetail?.deliveryFee }}{{ t('pages-store.store.start') }}</view>
<text class="text-28rpx lh-28rpx font-500 text-#CE7138 mr-8rpx">{{ t('pages-store.store.claimNow') }}</text>
<i class="i-carbon:chevron-right text-24rpx text-#CE7138"></i>
</view> </view>
</view> <view class="delivery-info-divider"></view>
<view class="delivery-info-right">
<view class="text-32rpx lh-40rpx text-#333 font-500">
{{ storeDetail?.deliveryTime }}{{ daySuffix(storeDetail?.deliveryTime) }}
</view>
<view class="text-24rpx lh-24rpx text-#7D7D7D mt-8rpx">{{ t('pages-store.store.earTime') }}</view>
</view>
</template>
<template v-if="+storeDetail?.selfPickup === 1 && deliveryMethod === 1">
<view class="delivery-info-left">
<view v-if="cartDataList.length > 0 && cartSavingsData && cartSavingsData.savings > 0">
{{ t('pages-store.store.use') }} {{ Config.appName }} {{ t('pages-store.store.tips3') }} ${{ cartSavingsData.savings }} {{ t('pages-store.store.discount') }}
</view>
<view v-else>--</view>
</view>
<view class="delivery-info-divider"></view>
<view class="delivery-info-right">
<view class="text-32rpx lh-40rpx text-#333 font-500">{{ storeDetail?.pickupTime }}{{ t('common.minutes') }}</view>
<view class="text-24rpx lh-24rpx text-#7D7D7D mt-8rpx">{{ t('pages-store.store.earTime') }}</view>
</view>
</template>
</view>
<!-- 优惠券标签行 -->
<view class="mt-24rpx flex items-center" v-if="storeCouponList.length">
<scroll-view <scroll-view
scroll-x scroll-x
class="coupon-scroll" class="coupon-scroll flex-1"
:show-scrollbar="false" :show-scrollbar="false"
enable-flex enable-flex
> >
@@ -613,107 +608,104 @@ function handleShare() {
</view> </view>
</view> </view>
</scroll-view> </scroll-view>
</view> <view @click="handleClaimNow" class="flex items-center shrink-0 ml-16rpx">
<!-- 新功能插入区域 end --> <text class="text-28rpx lh-28rpx font-500 text-#CE7138 mr-4rpx">{{ t('pages-store.store.claimNow') }}</text>
<i class="i-carbon:chevron-right text-24rpx text-#CE7138"></i>
<!-- 自取 配送切换 --> </view>
<!-- <view v-if="showDeliverySwitch" class="w-318rpx mt-40rpx">
<l-segmented :value="deliveryMethod" :options="deliveryMethodOptions" @click="handleClickSegmented" shape="round" bg-color="#F2F2F2" active-color="#333" />
</view> -->
<view v-if="showDeliverySwitch" class="border-#D8D8D8 border-solid border-1px rounded-20rpx min-h-164rpx mt-36rpx px-45rpx py-18rpx text-24rpx lh-24rpx flex-center-sb">
<template v-if="+storeDetail?.deliveryService === 1 && deliveryMethod === 0">
<view class="w-full h-full flex-1 text-center text-#CE7138 pr-44rpx">
<view>{{ t('pages-store.store.tips4') }} ${{ storeDetail?.minOrderPrice }}</view>
<view>{{ t('pages-store.store.tips5') }} ${{ storeDetail?.deliveryFee }}{{ t('pages-store.store.start') }}</view>
</view>
<view class="h-128rpx w-1rpx rotate-0 bg-#D8D8D8"></view>
<view class="w-full flex-1 center flex-col pl-44rpx">
<view class="text-#333 mb-8rpx">
{{ storeDetail?.deliveryTime }}{{ daySuffix(storeDetail?.deliveryTime) }}
</view>
<view class="text-#7D7D7D">{{ t('pages-store.store.earTime') }}</view>
</view>
</template>
<template v-if="+storeDetail?.selfPickup === 1 && deliveryMethod === 1">
<view class="w-full h-full flex-1 text-center text-#CE7138 pr-44rpx">
<view v-if="cartDataList.length > 0 && cartSavingsData && cartSavingsData.savings > 0">
{{ t('pages-store.store.use') }} {{ Config.appName }} {{ t('pages-store.store.tips3') }} ${{ cartSavingsData.savings }} {{ t('pages-store.store.discount') }}
</view>
<view v-else class="">--</view>
</view>
<view class="h-128rpx w-1rpx rotate-0 bg-#D8D8D8"></view>
<view class="w-full flex-1 center flex-col pl-44rpx">
<view class="text-#333 mb-8rpx">{{ storeDetail?.pickupTime }}{{ t('common.minutes') }}</view>
<view class="text-#7D7D7D">{{ t('pages-store.store.earTime') }}</view>
</view>
</template>
</view> </view>
</view> </view>
<wd-tabs v-model="activeTab" slidable="always" autoLineWidth>
<block v-for="item in tabs" :key="item">
<wd-tab :title="item.title">
</wd-tab>
</block>
</wd-tabs>
<view class="box mt--6px"></view>
<view v-if="tabs.length > 0" class="px-30rpx pb-120rpx"> <!-- 分类标签胶囊样式 -->
<view v-if="currentDishList.length > 0" class="text-40rpx lh-40rpx font-500 my-36rpx">{{ t('pages-store.store.recommend') }}</view> <scroll-view scroll-x class="w-full" :show-scrollbar="false" enable-flex>
<view v-if="currentDishList.length > 0" class="grid grid-cols-2 gap-30rpx"> <view class="flex items-center px-30rpx pb-24rpx">
<view
v-for="(item, index) in tabs"
:key="item.key"
@click="activeTab = index"
class="tab-chip"
:class="activeTab === index ? 'tab-chip--active' : ''"
>
{{ item.title }}
</view>
</view>
</scroll-view>
<!-- 商品列表 -->
<view v-if="tabs.length > 0" class="px-30rpx pb-180rpx">
<view v-if="currentDishList.length > 0" class="grid grid-cols-2 gap-24rpx items-start">
<template v-for="item in currentDishList" :key="item.id"> <template v-for="item in currentDishList" :key="item.id">
<view @click="navigateToDishes(item)" class="w-100% mb-10rpx"> <view @click="navigateToDishes(item)" class="dish-card" :class="{ 'dish-card--soldout': isSoldOutStock(item?.stock) }">
<view class="relative h-248rpx rounded-24rpx mb-28rpx"> <!-- 商品图片 -->
<view @click.stop="handleDishCollectionClick(item)" class="w-68rpx h-68rpx absolute z-2 top-0 right-0"> <view class="dish-card-image">
<image <!-- NEW 绑带标签 -->
v-if="!item.isCollect" <view v-if="item.isNew == 1" class="dish-new-ribbon">
src="@img-store/1334.png" <text class="dish-new-ribbon__text">NEW</text>
mode="aspectFill"
class="w-full h-full"
/>
<image
v-else
src="@img-store/1337.png"
mode="aspectFill"
class="w-full h-full"
/>
</view> </view>
<image <view @click.stop="handleDishCollectionClick(item)" class="w-56rpx h-56rpx absolute z-4 top-12rpx right-12rpx center">
:src="item?.dishImage?.split(',')[0]" <image
v-if="!item.isCollect"
src="@img-store/1334.png"
mode="aspectFill" mode="aspectFill"
class="w-full h-full rounded-24rpx bg-common" class="w-44rpx h-44rpx"
v-if="item.dishImage.split(',').length > 0" style="filter: drop-shadow(0 2rpx 6rpx rgba(0,0,0,0.18))"
/> />
<image <image
:src="item?.dishImage"
mode="aspectFill"
class="w-full h-full rounded-24rpx bg-common"
v-else v-else
src="@img-store/1337.png"
mode="aspectFill"
class="w-44rpx h-44rpx"
style="filter: drop-shadow(0 2rpx 6rpx rgba(0,0,0,0.18))"
/>
</view>
<!-- 已售完遮罩 -->
<view v-if="isSoldOutStock(item?.stock)" class="dish-sold-dim"></view>
<!-- 已售完标签 -->
<view v-if="isSoldOutStock(item?.stock)" class="dish-sold-tag">{{ t('common.prompt.soldOut') }}</view>
<image
:src="item?.dishImage?.split(',')[0]"
mode="aspectFill"
class="dish-card-img"
/> />
</view> </view>
<view class="line-clamp-1 text-30rpx text-#333 font-500"> <!-- 卡片信息区 -->
{{ item.dishName }} <view class="dish-card-body">
</view> <!-- 价格 + 销量 -->
<view class="flex-center-sb mt-12rpx"> <view class="flex items-start justify-between gap-12rpx mb-14rpx">
<text class="text-26rpx lh-30rpx text-#333 font-500">US${{ item.discountPrice }}</text> <view class="min-w-0 flex-1">
<view <text class="dish-price">$ {{ item.discountPrice }}</text>
v-if="Number(item.memberPrice) > 0" <text
class="member-price-tag text-[#FBE3C3] font-500 text-28rpx lh-28rpx center pl-6rpx break-all" v-if="Number(item?.originalPrice) > Number(item?.discountPrice)"
> class="dish-original-price"
<text class="!text-24rpx">{{ t('pages-store.store.members') }}: </text> >$ {{ item.originalPrice }}</text>
${{ item.memberPrice }} </view>
<text class="dish-sales shrink-0">{{ t('pages-store.store.sales') }}{{ item.salesCount }}</text>
</view> </view>
</view> <!-- 商品名称 -->
<view class="flex-center-sb mt-12rpx"> <view class="dish-title line-clamp-2 mb-16rpx">
<view class="text-28rpx text-#999"> {{ item.dishName }}
<view class="line-through">US${{ item.originalPrice }}</view>
<view>{{ t('pages-store.store.sales') }}:{{ item.salesCount }}</view>
</view> </view>
<!-- 会员价 + 加购按钮 -->
<view class="center w-64rpx h-64rpx rounded-50% bg-white shadow-lg"> <view class="flex items-center justify-between gap-12rpx">
<image <view
v-if="Number(item.memberPrice) > 0"
class="dish-member shrink min-w-0"
>
<text class="dish-member-inner">{{ t('pages-store.store.members') }}: $ {{ item.memberPrice }}</text>
</view>
<view v-else class="flex-1 min-w-0"></view>
<view class="dish-add-btn center shrink-0">
<image
src="@img/chef/1285.png" src="@img/chef/1285.png"
class="w-30rpx h-30rpx shrink-0" class="w-28rpx h-28rpx"
></image> ></image>
</view>
</view>
<!-- 营销标签 -->
<view
v-if="getDishPromoLabel(item)"
class="dish-promo mt-16rpx"
>
<text class="dish-promo-text">{{ getDishPromoLabel(item) }}</text>
</view> </view>
</view> </view>
</view> </view>
@@ -725,70 +717,91 @@ function handleShare() {
</view> </view>
</template> </template>
</view> </view>
<view @click="navigateTo('/pages-user/pages/member/index')" v-if="cartDataList.length > 0 && cartSavingsData && cartSavingsData.savings > 0" class="h-96rpx bg-#CE7138 pl-56rpx flex items-center fixed bottom-0 left-0 w-full z-9">
<!-- 底部购物车浮窗 -->
<view
v-if="userStore.isLogin && cartDataList.length > 0"
class="store-cart-float"
@click="navigateToCart"
>
<view class="store-cart-float-inner">
<view class="relative mr-16rpx">
<view class="i-carbon:shopping-cart text-44rpx text-#333"></view>
<view class="store-cart-badge">{{ cartDataList.length > 99 ? '99+' : cartDataList.length }}</view>
</view>
<text class="text-28rpx lh-28rpx text-#333 font-500">{{ t('pages-user.cart.viewCart') }}</text>
<view class="text-28rpx text-#999 ml-55rpx"></view>
</view>
</view>
<!-- 会员省钱提示条 -->
<view
@click="navigateTo('/pages-user/pages/member/index')"
v-if="cartDataList.length > 0 && cartSavingsData && cartSavingsData.savings > 0"
class="store-savings-bar"
>
<image <image
src="@img/chef/1289.png" src="@img/chef/1289.png"
class="mr-10rpx w-32rpx h-32rpx shrink-0" class="mr-10rpx w-32rpx h-32rpx shrink-0"
></image> ></image>
<text class="text-[#fff] text-24rpx lh-24rpx"> <text class="text-[#fff] text-24rpx lh-24rpx">
{{ t('pages-store.store.use') }} {{ Config.appName }} {{ t('pages-store.store.tips3') }} ${{ cartSavingsData.savings }} {{ t('pages-store.store.discount') }} {{ t('pages-store.store.use') }} {{ Config.appName }} {{ t('pages-store.store.tips3') }} ${{ cartSavingsData.savings }} {{ t('pages-store.store.discount') }}
</text> </text>
</view> </view>
</view> </view>
<view v-if="cartDataList.length > 0" @click="navigateToCart" class="fixed z-9 bottom-138rpx left-50% translate-x--50% px-26rpx h-88rpx bg-#14181B rounded-44rpx center text-28rpx text-#fff font-500">
<image src="@img/chef/125.png" class="w-28rpx h-28rpx shrink-0"></image>
<view class="ml-10rpx whitespace-nowrap line-clamp-1">{{ storeDetail.merchantName }}</view>
<view class="w-8rpx h-8rpx rounded-50% bg-white mx-8rpx"></view>
<text>{{ cartDataList.length }}</text>
</view>
</view> </view>
<coupon-popup <coupon-popup
ref="couponPopupRef" ref="couponPopupRef"
:coupon-list="storeCouponList" :coupon-list="storeCouponList"
@confirm="getMerchantCouponReceiveList" @confirm="getMerchantCouponReceiveList"
/> />
</template> </template>
<style> <style>
page { page {
background-color: #fff; background-color: #F6F6F6;
} }
</style> </style>
<style scoped lang="scss"> <style scoped lang="scss">
:deep(.wd-swiper__track) { /* ====== 配送信息卡片 ====== */
border-radius: 0; .delivery-info-card {
} display: flex;
:deep(.wd-tabs__nav-container) { align-items: center;
.is-active { justify-content: space-between;
color: #333 !important; margin-top: 30rpx;
font-weight: 500 !important; padding: 24rpx 40rpx;
} border: 2rpx solid #D8D8D8;
} border-radius: 20rpx;
:deep(.wd-tabs__nav-item-text) { min-height: 140rpx;
font-size: 30rpx !important;
//color: #7D7D7D;
padding-bottom: 32rpx !important;
} }
:deep(.wd-tabs__line) { .delivery-info-left {
border-radius: 0 !important; flex: 1;
height: 10rpx !important; text-align: center;
background-color: #333333 !important; font-size: 24rpx;
} line-height: 36rpx;
.box { color: #CE7138;
border-bottom: 10rpx solid #F6F6F6 !important; padding-right: 30rpx;
}
.member-price-tag {
min-width: 190rpx;
height: 42rpx;
background-image: url("/static/images/chef/1282.png");
background-size: 100% 100%;
background-repeat: no-repeat;
} }
.delivery-info-divider {
width: 2rpx;
height: 100rpx;
background: #D8D8D8;
flex-shrink: 0;
}
.delivery-info-right {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding-left: 30rpx;
}
/* ====== 优惠券标签 ====== */
.coupon-scroll { .coupon-scroll {
width: 100%; width: 100%;
} }
@@ -800,7 +813,7 @@ page {
} }
.coupon-item { .coupon-item {
margin-right: 20rpx; margin-right: 15rpx;
flex-shrink: 0; flex-shrink: 0;
&:last-child { &:last-child {
@@ -809,12 +822,12 @@ page {
} }
.coupon-tag { .coupon-tag {
min-width: 120rpx; min-width: 110rpx;
height: 52rpx; height: 52rpx;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 0 28rpx; padding: 0 18rpx;
position: relative; position: relative;
background-image: url("/static/images/5008.png"); background-image: url("/static/images/5008.png");
background-size: 100% 100%; background-size: 100% 100%;
@@ -828,4 +841,248 @@ page {
font-weight: 400; font-weight: 400;
white-space: nowrap; white-space: nowrap;
} }
/* ====== 胶囊标签 ====== */
.tab-chip {
height: 60rpx;
line-height: 60rpx;
padding: 0 32rpx;
border-radius: 30rpx;
font-size: 26rpx;
color: #333;
border: 2rpx solid #E0E0E0;
white-space: nowrap;
flex-shrink: 0;
margin-right: 20rpx;
text-align: center;
&:last-child {
margin-right: 0;
}
&--active {
background: #333;
color: #fff;
border-color: #333;
}
}
/* ====== 商品卡片 ====== */
.dish-card {
width: 100%;
border-radius: 24rpx;
overflow: hidden;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
background: #fff;
&--soldout {
opacity: 0.7;
}
}
.dish-card-image {
position: relative;
width: 100%;
height: 330rpx;
background: #f0f0f0;
overflow: hidden;
}
.dish-card-img {
width: 100%;
height: 100%;
display: block;
z-index: 1;
}
.dish-card-body {
padding: 20rpx 20rpx 22rpx;
}
.dish-price {
color: #e02e24;
font-size: 32rpx;
font-weight: 600;
line-height: 1.2;
}
.dish-original-price {
margin-left: 10rpx;
color: #b3b3b3;
font-size: 22rpx;
font-weight: 400;
text-decoration: line-through;
vertical-align: baseline;
}
.dish-sales {
color: #999;
font-size: 24rpx;
line-height: 1.35;
max-width: 48%;
text-align: right;
}
.dish-title {
color: #1a1a1a;
font-size: 28rpx;
font-weight: 500;
line-height: 1.45;
}
.dish-member {
display: inline-flex;
align-items: center;
max-width: calc(100% - 88rpx);
}
.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;
}
.dish-add-btn {
width: 56rpx;
height: 56rpx;
border-radius: 50%;
background: #14181b;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.12);
}
.dish-promo {
padding: 12rpx 16rpx;
border-radius: 12rpx;
background: #fff0f0;
}
.dish-promo-text {
color: #e02e24;
font-size: 22rpx;
font-weight: 500;
line-height: 1.4;
}
/* NEW 绑带标签 */
.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);
}
}
/* 已售完遮罩 */
.dish-sold-dim {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 2;
background: rgba(20, 24, 27, 0.42);
pointer-events: none;
}
/* 已售完标签 */
.dish-sold-tag {
position: absolute;
z-index: 3;
left: 16rpx;
top: 16rpx;
padding: 0 14rpx;
height: 48rpx;
border-radius: 24rpx;
background: rgba(20, 24, 27, 0.75);
color: #fff;
font-size: 24rpx;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
}
/* ====== 底部购物车浮窗 ====== */
.store-cart-float {
position: fixed;
bottom: calc(40rpx + env(safe-area-inset-bottom));
left: 50%;
width: 90%;
transform: translateX(-50%);
z-index: 9;
}
.store-cart-float-inner {
justify-content: space-between;
display: flex;
align-items: center;
height: 96rpx;
padding: 0 36rpx;
border-radius: 48rpx;
background: rgba(255, 255, 255, 0.75);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 8rpx 40rpx rgba(0, 0, 0, 0.12);
border: 1rpx solid rgba(255, 255, 255, 0.6);
}
.store-cart-badge {
position: absolute;
top: -8rpx;
right: -12rpx;
min-width: 32rpx;
height: 32rpx;
padding: 0 8rpx;
font-size: 20rpx;
line-height: 32rpx;
font-weight: 600;
color: #fff;
text-align: center;
background: #e23636;
border-radius: 999rpx;
}
/* ====== 会员省钱提示条 ====== */
.store-savings-bar {
position: fixed;
bottom: calc(160rpx + env(safe-area-inset-bottom));
left: 50%;
width: 90%;
transform: translateX(-50%);
z-index: 8;
height: 64rpx;
border-radius: 32rpx;
background: #CE7138;
display: flex;
align-items: center;
padding: 0 32rpx;
white-space: nowrap;
}
</style> </style>
+41 -5
View File
@@ -45,11 +45,16 @@ function navigateToDishes(item: any) {
<view class="grid grid-cols-2 gap-x-30rpx gap-y-46rpx pb-40rpx px-30rpx"> <view class="grid grid-cols-2 gap-x-30rpx gap-y-46rpx pb-40rpx px-30rpx">
<template v-for="item in dataList"> <template v-for="item in dataList">
<view @click="navigateToDishes(item)" class="w-330rpx overflow-hidden"> <view @click="navigateToDishes(item)" class="w-330rpx overflow-hidden">
<image <view class="search-result-img-wrap">
:src="item?.dishImage?.split(',')[0]" <view v-if="item.isNew == 1" class="dish-new-ribbon">
class="w-full h-186rpx rounded-24rpx mb-16rpx" <text class="dish-new-ribbon__text">NEW</text>
mode="aspectFill" </view>
></image> <image
:src="item?.dishImage?.split(',')[0]"
class="w-full h-186rpx rounded-24rpx"
mode="aspectFill"
></image>
</view>
<text class="text-30rpx lh-30rpx text-#333 line-clamp-1 tracking-[.04em] font-500" <text class="text-30rpx lh-30rpx text-#333 line-clamp-1 tracking-[.04em] font-500"
>{{ item.dishName }}</text >{{ item.dishName }}</text
> >
@@ -60,5 +65,36 @@ function navigateToDishes(item: any) {
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.search-result-img-wrap {
position: relative;
overflow: hidden;
border-radius: 24rpx;
margin-bottom: 16rpx;
}
.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> </style>
Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

@@ -25,9 +25,9 @@ defineExpose({
@close="handleClose" @close="handleClose"
> >
<view class="px-30rpx text-#333 text-center pt-48rpx pb-60rpx"> <view class="px-30rpx text-#333 text-center pt-48rpx pb-60rpx">
<view class="text-40rpx lh-40rpx font-500">Delete shopping cart?</view> <view class="text-40rpx lh-40rpx font-500">{{ t("pages-user.cart.removeCartTitle") }}</view>
<view class="text-28rpx lh-28rpx mt-36rpx"> <view class="text-28rpx lh-28rpx mt-36rpx">
Are you sure you want to delete the shopping cart? {{ t("pages-user.cart.removeCartDesc") }}
</view> </view>
<view class="mt-70rpx"> <view class="mt-70rpx">
<wd-button @click="handleDelete" custom-class="!h-108rpx !bg-14181B !text-36rpx !lh-108rpx !text-#fff !rounded-16rpx" block> <wd-button @click="handleDelete" custom-class="!h-108rpx !bg-14181B !text-36rpx !lh-108rpx !text-#fff !rounded-16rpx" block>
@@ -4,6 +4,20 @@ 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(() =>
fillI18nParams(t("pages-user.cart.removeProductDesc"), { name: goodsName.value })
);
function onOpen(title: string) { function onOpen(title: string) {
if (title) { if (title) {
goodsName.value = title; goodsName.value = title;
@@ -31,9 +45,9 @@ defineExpose({
@close="handleClose" @close="handleClose"
> >
<view class="px-30rpx text-#333 text-center pt-48rpx pb-60rpx"> <view class="px-30rpx text-#333 text-center pt-48rpx pb-60rpx">
<view class="text-40rpx lh-40rpx font-500">Remove the product?</view> <view class="text-40rpx lh-40rpx font-500">{{ t("pages-user.cart.removeProductTitle") }}</view>
<view class="text-28rpx lh-28rpx mt-36rpx"> <view class="text-28rpx lh-28rpx mt-36rpx">
Are you sure you want to remove {{ goodsName }} from your shopping cart? {{ removeProductDescText }}
</view> </view>
<view class="mt-70rpx"> <view class="mt-70rpx">
<wd-button @click="confirmRemove" custom-class="!h-108rpx !bg-14181B !text-36rpx !lh-108rpx !text-#fff !rounded-16rpx" block> <wd-button @click="confirmRemove" custom-class="!h-108rpx !bg-14181B !text-36rpx !lh-108rpx !text-#fff !rounded-16rpx" block>
File diff suppressed because it is too large Load Diff
+25 -19
View File
@@ -115,20 +115,14 @@ function scheduleCartUpdate(item: MerchantCartVo, targetValue: number) {
pendingUpdateTimers.set(key, timer) pendingUpdateTimers.set(key, timer)
} }
function normalizeInputNumberValue(payload: any): number { /** wd-input-number change 为 { value },兼容 detail.value */
if (typeof payload === 'number') { function normalizeInputNumberValue(payload: unknown): number {
return payload const raw =
} typeof payload === "number"
if (payload && typeof payload === 'object') { ? payload
if (typeof payload.detail?.value === 'number') { : (payload as any)?.value ?? (payload as any)?.detail?.value ?? payload;
return payload.detail.value const n = Number(raw);
} return Number.isFinite(n) ? n : 0;
if (typeof payload.value === 'number') {
return payload.value
}
}
const numeric = Number(payload)
return Number.isNaN(numeric) ? 0 : numeric
} }
const delItemData = ref<MerchantCartVo | null>(null) const delItemData = ref<MerchantCartVo | null>(null)
@@ -247,6 +241,18 @@ function appMerchantCartCalculateSavings() {
}) })
} }
function getCartItemDiscountPrice(item: MerchantCartVo) {
return item.discountPrice ?? item.merchantDishVo?.discountPrice
}
function getCartItemOriginalPrice(item: MerchantCartVo) {
return item.originalPrice ?? item.merchantDishVo?.originalPrice
}
function getCartItemMemberPrice(item: MerchantCartVo) {
return item.memberPrice ?? item.merchantDishVo?.memberPrice
}
function navigateTo(url: string) { function navigateTo(url: string) {
uni.navigateTo({ url }) uni.navigateTo({ url })
} }
@@ -301,21 +307,21 @@ function navigateTo(url: string) {
<view class="price-row flex items-center mt-18rpx"> <view class="price-row flex items-center mt-18rpx">
<text class="current-price text-[#333333] text-30rpx font-normal" <text class="current-price text-[#333333] text-30rpx font-normal"
>${{ item.merchantDishVo.discountPrice }}</text >${{ getCartItemDiscountPrice(item) }}</text
> >
<text <text
v-if="item.merchantDishVo.originalPrice" v-if="getCartItemOriginalPrice(item)"
class="original-price text-[#7D7D7D] text-24rpx font-normal line-through ml-10rpx" class="original-price text-[#7D7D7D] text-24rpx font-normal line-through ml-10rpx"
>${{ item.merchantDishVo.originalPrice }}</text >${{ getCartItemOriginalPrice(item) }}</text
> >
<!-- 会员价标签 --> <!-- 会员价标签 -->
<view <view
v-if="Number(item.merchantDishVo.memberPrice) > 0" v-if="Number(getCartItemMemberPrice(item)) > 0"
class="member-price-tag ml-10rpx center pl-16rpx pr-6rpx pb-2rpx" class="member-price-tag ml-10rpx center pl-16rpx pr-6rpx pb-2rpx"
> >
<text class="text-[#FBE3C3] text-20rpx" <text class="text-[#FBE3C3] text-20rpx"
>{{ t('pages-store.store.members') }}: ${{ item.merchantDishVo.memberPrice }}</text >{{ t('pages-store.store.members') }}: ${{ getCartItemMemberPrice(item) }}</text
> >
</view> </view>
</view> </view>
@@ -1,11 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { debounce } from 'throttle-debounce' import { debounce } from 'throttle-debounce'
import {appCollectListPost, appCollectCollectPost} from "@/service"; import {appCollectListPost, appCollectCollectPost} from "@/service";
import FoodBox from "@/pages/home/components/tabbar-home/components/food-box/index.vue";
import Collection from "@/components/collection/index.vue"; import Collection from "@/components/collection/index.vue";
import {CollectionType} from "@/constant/enums"; import {CollectionType} from "@/constant/enums";
const {t} = useI18n() const {t} = useI18n()
const emit = defineEmits<{
(e: 'batch-delete-success'): void
}>()
const props = defineProps({ const props = defineProps({
currentIndex: { currentIndex: {
type: Number, type: Number,
@@ -15,6 +17,18 @@ const props = defineProps({
type: Number, type: Number,
default: 0, default: 0,
}, },
batchDeleteMode: {
type: Boolean,
default: false,
},
})
const selectedIds = ref<string[]>([])
watch(() => props.batchDeleteMode, (v) => {
if (!v) {
selectedIds.value = []
}
}) })
// (1, "菜谱")(2, "菜品")(3, "配菜")(4, "商家") // (1, "菜谱")(2, "菜品")(3, "配菜")(4, "商家")
@@ -35,7 +49,7 @@ const {paging, dataList, queryList, loading} = usePage<any>((pageNum: number, pa
pageSize, pageSize,
}, },
body: { body: {
targetType: typeList[props.currentIndex] targetType: typeList[props.index]
} }
}), }),
) )
@@ -51,6 +65,106 @@ function navigateToDishes(item: any) {
}) })
} }
function getDishInfo(item: any) {
return item?.merchantDishVo || item || {}
}
function getDishImage(item: any) {
const dish = getDishInfo(item)
return dish?.dishImage?.split?.(',')?.[0] || dish?.dishImage || ''
}
/** 当前列表 tab 对应的收藏目标 id(与 appCollectCollectPost 一致) */
function getCollectTargetId(item: any): string {
const i = props.index
if (i === 0) {
return String(item.merchantVo?.id ?? item.merchantId ?? getDishInfo(item).merchantId ?? item.id ?? '')
}
if (i === 1) {
return String(item.merchantDishVo?.id ?? getDishInfo(item).id ?? item.id ?? '')
}
if (i === 2) {
return String(item.merchantRecipeVo?.id ?? item.id ?? '')
}
return ''
}
function toggleSelect(item: any) {
if (!props.batchDeleteMode) return
const id = getCollectTargetId(item)
if (!id) return
const idx = selectedIds.value.indexOf(id)
if (idx >= 0) {
selectedIds.value.splice(idx, 1)
} else {
selectedIds.value.push(id)
}
}
function isSelected(item: any) {
const id = getCollectTargetId(item)
return id ? selectedIds.value.includes(id) : false
}
function onStoreOrDishRowClick(item: any) {
if (props.batchDeleteMode) {
toggleSelect(item)
return
}
navigateToDishes(getDishInfo(item))
}
function onRecipeCardClick(item: any) {
if (props.batchDeleteMode) {
toggleSelect(item)
return
}
navigateToRecipeDetail(item.merchantRecipeVo?.id ?? item.id)
}
function runBatchDelete() {
if (selectedIds.value.length === 0) {
uni.showToast({
title: t('pages.mine.collectionSelectFirst'),
icon: 'none',
})
return
}
uni.showModal({
title: t('common.prompt.system-prompt'),
content: t('pages.mine.collectionBatchDeleteConfirm', { count: selectedIds.value.length }),
success: (res) => {
if (!res.confirm) return
const targetType = typeList[props.index] as number
const ids = [...selectedIds.value]
;(async () => {
try {
for (const targetId of ids) {
await appCollectCollectPost({
body: {
targetId,
targetType,
},
})
}
selectedIds.value = []
emit('batch-delete-success')
paging.value?.reload()
uni.showToast({
title: t('toast.deleteSuccess'),
icon: 'none',
})
} catch {
uni.showToast({
title: t('common.prompt.request-incorrect'),
icon: 'none',
})
}
})()
},
})
}
// 收藏菜谱 // 收藏菜谱
function handleSubmitCollectRecipe(item:any) { function handleSubmitCollectRecipe(item:any) {
debouncedEmit(item.merchantRecipeVo?.isCollect, item.merchantRecipeVo?.id, CollectionType.RECIPE, ()=> { debouncedEmit(item.merchantRecipeVo?.isCollect, item.merchantRecipeVo?.id, CollectionType.RECIPE, ()=> {
@@ -58,12 +172,6 @@ function handleSubmitCollectRecipe(item:any) {
}) })
} }
// 收藏菜品
function handleDishCollectionClick(item: any) {
debouncedEmit(item.isCollect, item.id, CollectionType.DISH, ()=> {
item.isCollect = !item.isCollect
})
}
// 防抖处理函数 // 防抖处理函数
const debouncedEmit = debounce(1300, (isCollected: boolean, id: string, type: CollectionType, callback: ()=> void) => { const debouncedEmit = debounce(1300, (isCollected: boolean, id: string, type: CollectionType, callback: ()=> void) => {
// 收藏接口 // 收藏接口
@@ -94,6 +202,7 @@ function refresh() {
defineExpose({ defineExpose({
reload, reload,
refresh, refresh,
runBatchDelete,
}) })
</script> </script>
@@ -101,71 +210,66 @@ defineExpose({
<view class="h-full"> <view class="h-full">
<z-paging ref="paging" v-model="dataList" @query="queryList" :fixed="false" :auto="false"> <z-paging ref="paging" v-model="dataList" @query="queryList" :fixed="false" :auto="false">
<view class="p-30rpx"> <view class="p-30rpx">
<template v-if="currentIndex == 0"> <template v-if="currentIndex == 0 || currentIndex == 1">
<!--商家--> <view class="collection-list">
<view v-for="item in dataList" :key="item.id"> <view
<food-box :item="item.merchantVo" /> v-for="item in dataList"
</view> :key="item.id"
</template> class="collection-item"
<template v-if="currentIndex == 1"> @click="onStoreOrDishRowClick(item)"
<!--菜品--> >
<view class="grid grid-cols-2 gap-30rpx"> <view
<template v-for="item in dataList"> v-if="batchDeleteMode"
<view @click="navigateToDishes(item.merchantDishVo)" class="w-100% mb-10rpx rounded-16rpx"> class="collection-item__check"
<view class="relative h-248rpx rounded-24rpx mb-28rpx"> @click.stop="toggleSelect(item)"
<view @click.stop="handleDishCollectionClick(item.merchantDishVo)" class="w-68rpx h-68rpx absolute z-2 top-0 right-0"> >
<image <view class="check-box" :class="{ 'is-on': isSelected(item) }">
v-if="!item.merchantDishVo.isCollect" <text v-if="isSelected(item)" class="check-tick"></text>
src="@img-store/1334.png"
mode="aspectFill"
class="w-full h-full"
/>
<image
v-else
src="@img-store/1337.png"
mode="aspectFill"
class="w-full h-full"
/>
</view>
<image
:src="item.merchantDishVo?.dishImage?.split(',')[0]"
mode="aspectFill"
class="w-full h-full rounded-24rpx"
/>
</view>
<view class="line-clamp-1 text-30rpx lh-30rpx text-#333 font-500 mb-12rpx">
{{ item.merchantDishVo.dishName }}
</view>
<view class="flex-center-sb">
<text class="text-30rpx lh-30rpx text-#333 font-500">US${{ item.merchantDishVo.discountPrice }}</text>
<view
v-if="Number(item.merchantDishVo.memberPrice) > 0"
class="member-price-tag text-[#FBE3C3] text-18rpx center pl-6rpx"
>
{{ t('pages-store.store.members') }}: ${{ item.merchantDishVo.memberPrice }}
</view>
</view>
<view class="flex-center-sb mt-12rpx">
<view class="text-28rpx text-#999">
<view class="line-through">US${{ item.merchantDishVo.originalPrice }}</view>
<view>{{ t('pages-store.store.sales') }}:{{ item.merchantDishVo.salesCount }}</view>
</view>
<view class="center w-64rpx h-64rpx rounded-50% bg-white shadow-lg">
<image
src="@img/chef/1285.png"
class="w-30rpx h-30rpx shrink-0"
></image>
</view>
</view> </view>
</view> </view>
</template> <image
:src="getDishImage(item)"
mode="aspectFill"
class="collection-item__image"
/>
<view class="collection-item__content">
<view class="collection-item__name line-clamp-1">{{ getDishInfo(item).dishName }}</view>
<view class="collection-item__price-row mt-10rpx">
<text class="collection-item__price">${{ getDishInfo(item).discountPrice }}</text>
<text
v-if="Number(getDishInfo(item).originalPrice) > 0"
class="collection-item__original ml-10rpx"
>
${{ getDishInfo(item).originalPrice }}
</text>
</view>
<view v-if="Number(getDishInfo(item).memberPrice) > 0" class="collection-item__member mt-12rpx">
<image src="@img-store/1339.png" class="w-22rpx h-22rpx mr-6rpx" />
{{ t('pages.browse.brandTag') }} {{ t('pages-store.store.members') }}: ${{ getDishInfo(item).memberPrice }}
</view>
<view class="collection-item__sales mt-10rpx">
{{ t('pages-store.store.sales') }} : {{ getDishInfo(item).salesCount || 0 }}
</view>
</view>
<view v-if="!batchDeleteMode" class="collection-item__add center">
<text class="text-42rpx text-#333">+</text>
</view>
</view>
</view> </view>
</template> </template>
<template v-if="currentIndex == 2"> <template v-if="currentIndex == 2">
<view class="grid grid-cols-2 gap-30rpx"> <view class="grid grid-cols-2 gap-30rpx">
<view v-for="item in dataList"> <view v-for="item in dataList" :key="item.id" class="recipe-card-wrap">
<view @click="navigateToRecipeDetail(item.id)" class="w-312rpx"> <view
v-if="batchDeleteMode"
class="recipe-card__check"
@click.stop="toggleSelect(item)"
>
<view class="check-box" :class="{ 'is-on': isSelected(item) }">
<text v-if="isSelected(item)" class="check-tick"></text>
</view>
</view>
<view @click="onRecipeCardClick(item)" class="w-312rpx">
<image <image
:src="item.merchantRecipeVo?.recipeImage?.split(',')[0]" :src="item.merchantRecipeVo?.recipeImage?.split(',')[0]"
class="w-310rpx h-296rpx rounded-24rpx mb-26rpx" class="w-310rpx h-296rpx rounded-24rpx mb-26rpx"
@@ -175,7 +279,7 @@ defineExpose({
<text class="text-30rpx lh-30rpx text-#333 line-clamp-1 tracking-[.04em] font-500" <text class="text-30rpx lh-30rpx text-#333 line-clamp-1 tracking-[.04em] font-500"
>{{ item.merchantRecipeVo?.recipeName }}</text >{{ item.merchantRecipeVo?.recipeName }}</text
> >
<view class="w-40rpx h-40rpx ml-14rpx shrink-0"> <view v-if="!batchDeleteMode" class="w-40rpx h-40rpx ml-14rpx shrink-0">
<collection <collection
:is-collected="item.merchantRecipeVo?.isCollect" :is-collected="item.merchantRecipeVo?.isCollect"
@collectionChange="handleSubmitCollectRecipe(item)" @collectionChange="handleSubmitCollectRecipe(item)"
@@ -192,5 +296,123 @@ defineExpose({
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.collection-list {
.collection-item {
display: flex;
align-items: center;
padding: 24rpx 0;
border-bottom: 1rpx solid #f1f1f1;
}
.collection-item__image {
width: 168rpx;
height: 168rpx;
border-radius: 20rpx;
background: #f4f4f4;
flex-shrink: 0;
}
.collection-item__content {
flex: 1;
margin-left: 20rpx;
overflow: hidden;
}
.collection-item__name {
font-size: 30rpx;
line-height: 38rpx;
color: #222;
font-weight: 500;
}
.collection-item__price-row {
display: flex;
align-items: baseline;
}
.collection-item__price {
color: #e7362f;
font-size: 42rpx;
line-height: 42rpx;
font-weight: 600;
}
.collection-item__original {
color: #a5a5a5;
font-size: 28rpx;
line-height: 28rpx;
text-decoration: line-through;
}
.collection-item__member {
display: inline-flex;
align-items: center;
color: #c5883f;
font-size: 26rpx;
line-height: 26rpx;
}
.collection-item__sales {
width: fit-content;
height: 42rpx;
padding: 0 14rpx;
border-radius: 10rpx;
background: #f1f2f4;
color: #9b9b9b;
font-size: 24rpx;
line-height: 42rpx;
}
.collection-item__add {
width: 56rpx;
height: 56rpx;
border-radius: 50%;
border: 1rpx solid #e6e6e6;
margin-left: 14rpx;
flex-shrink: 0;
}
.collection-item__check {
width: 44rpx;
margin-right: 12rpx;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
}
}
.check-box {
width: 40rpx;
height: 40rpx;
border-radius: 50%;
border: 2rpx solid #c8c8c8;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
}
.check-box.is-on {
border-color: #111;
background: #111;
}
.check-tick {
font-size: 24rpx;
line-height: 24rpx;
color: #fff;
font-weight: 700;
}
.recipe-card-wrap {
position: relative;
}
.recipe-card__check {
position: absolute;
z-index: 3;
left: 8rpx;
top: 8rpx;
}
</style> </style>
+189 -9
View File
@@ -1,6 +1,26 @@
<script setup lang="ts"> <script setup lang="ts">
import OrderSwiperList from "./components/order-swiper-list/order-swiper-list.vue"; import OrderSwiperList from "./components/order-swiper-list/order-swiper-list.vue";
import { useUserStore } from "@/store";
import { onShow } from "@dcloudio/uni-app";
const {t} = useI18n() const {t} = useI18n()
const userStore = useUserStore();
const cartBadgeTotal = computed(() => {
const list = userStore.userCartAllData;
if (!Array.isArray(list) || list.length === 0) return 0;
let n = 0;
for (const m of list) {
n +=
(m as { merchantCartVoList?: unknown[] })?.merchantCartVoList
?.length || 0;
}
return n;
});
onShow(() => {
userStore.getUserCartAllData();
});
const segmentedValue = ref(0); const segmentedValue = ref(0);
const segmentedList = [ const segmentedList = [
'Store', 'Store',
@@ -8,26 +28,106 @@ const segmentedList = [
'Recipe', 'Recipe',
] ]
function handleSwiperChange(e) { function handleSwiperChange(e: any) {
segmentedValue.value = e.detail.current; segmentedValue.value = e.detail.current;
} }
const orderSwiperListRef = ref() const orderSwiperListRef = ref()
const batchDeleteMode = ref(false)
onMounted(()=> { onMounted(()=> {
nextTick(()=> { nextTick(()=> {
orderSwiperListRef.value[segmentedValue.value].reload() orderSwiperListRef.value[segmentedValue.value].reload()
}) })
}) })
function exitBatchDeleteMode() {
batchDeleteMode.value = false
}
function handleBatchDeleteSuccess() {
batchDeleteMode.value = false
}
function navigateBack() {
uni.navigateBack({
delta: 1,
})
}
function handleDeleteCollection() {
if (!batchDeleteMode.value) {
batchDeleteMode.value = true
uni.showToast({
title: t('pages.mine.collectionBatchModeHint'),
icon: 'none',
})
return
}
const inst = orderSwiperListRef.value?.[segmentedValue.value]
inst?.runBatchDelete?.()
}
function navigateToCart() {
uni.navigateTo({
url: '/pages-user/pages/cart/index'
})
}
</script> </script>
<template> <template>
<view> <view class="collection-page">
<z-paging-swiper> <z-paging-swiper>
<template #top> <template #top>
<navbar/> <status-bar />
<view class="px-30rpx"> <view class="header-wrap px-30rpx pt-14rpx">
<view class="text-46rpx lh-46rpx text-#333 font-bold mb-52rpx">{{ t('pages.mine.collection') }}</view> <view class="flex items-center justify-between mb-34rpx">
<l-segmented v-model="segmentedValue" :options="segmentedList" shape="round" bg-color="#F2F2F2" active-color="#333" /> <image
@click="navigateBack"
src="@img/chef/1327.png"
mode="aspectFill"
class="w-48rpx h-48rpx"
/>
<view class="text-38rpx lh-38rpx text-#111 font-600">{{ t('pages.mine.collection') }}</view>
<view class="flex items-center gap-16rpx">
<text
v-if="batchDeleteMode"
class="text-28rpx text-#666"
@click="exitBatchDeleteMode"
>{{ t('common.cancel') }}</text>
<view
v-if="!batchDeleteMode"
class="collection-header-cart w-66rpx h-66rpx rounded-50% bg-#F5F5F5 center shrink-0"
@click="navigateToCart"
>
<view class="i-carbon:shopping-cart text-36rpx text-#14181b"></view>
<view
v-if="userStore.isLogin && cartBadgeTotal > 0"
class="collection-header-cart-badge"
>{{ cartBadgeTotal > 99 ? "99+" : cartBadgeTotal }}</view
>
</view>
<view
v-if="!batchDeleteMode"
class="w-66rpx h-66rpx rounded-50% bg-#F5F5F5 center"
@click="handleDeleteCollection"
>
<image src="@img/chef/1278.png" mode="aspectFill" class="w-66rpx h-66rpx" />
</view>
</view>
</view>
<l-segmented
v-model="segmentedValue"
:options="segmentedList"
shape="round"
bg-color="#F2F3F5"
active-color="#fff"
color="#5E5E5E"
padding="0rpx"
slider-color="#000"
height="62rpx"
/>
</view> </view>
</template> </template>
@@ -35,15 +135,95 @@ onMounted(()=> {
:current="segmentedValue" :current="segmentedValue"
@change="handleSwiperChange"> @change="handleSwiperChange">
<swiper-item class="swiper-item" v-for="(item, index) in segmentedList" :key="index"> <swiper-item class="swiper-item" v-for="(item, index) in segmentedList" :key="index">
<order-swiper-list ref="orderSwiperListRef" :currentIndex="segmentedValue" :index="index"></order-swiper-list> <order-swiper-list
ref="orderSwiperListRef"
:currentIndex="segmentedValue"
:index="index"
:batch-delete-mode="batchDeleteMode"
@batch-delete-success="handleBatchDeleteSuccess"
/>
</swiper-item> </swiper-item>
</swiper> </swiper>
</z-paging-swiper> </z-paging-swiper>
<view class="cart-bar-wrap px-24rpx pb-24rpx" v-if="batchDeleteMode">
<view class="delete-bar">
<view class="text-30rpx text-#1d1d1d ml-20rpx" style="color: #fff;">{{ t('common.delete') || '删除' }}</view>
</view>
</view>
</view> </view>
</template> </template>
<style> <style scoped lang="scss">
page { page {
background-color: white; background-color: #fff;
}
.collection-page {
min-height: 100vh;
background: #fff;
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
.header-wrap {
background: #fff;
}
:deep(.l-segmented) {
height: 62rpx !important;
border-radius: 34rpx !important;
}
:deep(.l-segmented-item) {
font-size: 30rpx !important;
color: #666 !important;
}
:deep(.l-segmented-item--active) {
color: #fff !important;
font-weight: 600 !important;
background: #111 !important;
}
.cart-bar-wrap {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 20;
background: linear-gradient(to top, #fff 65%, rgba(255, 255, 255, 0));
}
.collection-header-cart {
position: relative;
}
.collection-header-cart-badge {
position: absolute;
top: 2rpx;
right: 2rpx;
min-width: 28rpx;
height: 28rpx;
padding: 0 6rpx;
font-size: 18rpx;
line-height: 28rpx;
font-weight: 600;
color: #fff;
text-align: center;
background: #e23636;
border-radius: 999rpx;
}
.delete-bar {
height: 96rpx;
border-radius: 48rpx;
background: #000;
box-shadow: 0 -2rpx 16rpx rgba(0, 0, 0, 0.06);
border: 1rpx solid #efefef;
padding: 0 26rpx;
display: flex;
align-items: center;
justify-content: center;
} }
</style> </style>
+21 -1
View File
@@ -42,6 +42,26 @@ 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) {
const name = String(item?.merchantVo?.merchantName || '').trim()
if (name) {
return fillI18nParams(t('pages-user.coupon.merchant-only'), { name })
}
return item?.snapshotMerchantId
? t('pages-user.coupon.merchant-specific')
: t('pages-user.coupon.all-merchants')
}
function getList(pageNum: number, pageSize: number) { function getList(pageNum: number, pageSize: number) {
return appCouponUserCouponListPost({ return appCouponUserCouponListPost({
params: { params: {
@@ -150,7 +170,7 @@ function handleSubmit() {
{{ item.snapshotNameZh }} {{ item.snapshotNameZh }}
</view> </view>
<view class="text-24rpx lh-32rpx text-#999 my-18rpx"> <view class="text-24rpx lh-32rpx text-#999 my-18rpx">
{{ item.snapshotMerchantId ? t('pages-user.coupon.merchant-specific') : t('pages-user.coupon.all-merchants') }} {{ formatCouponMerchantText(item) }}
</view> </view>
<view class="text-24rpx lh-32rpx text-#999 my-18rpx"> <view class="text-24rpx lh-32rpx text-#999 my-18rpx">
{{ dayjs(Number(item.snapshotValidEnd)).format('YYYY-MM-DD HH:mm') }}{{ isEnLocale() ? ' expires' : '到期' }} {{ dayjs(Number(item.snapshotValidEnd)).format('YYYY-MM-DD HH:mm') }}{{ isEnLocale() ? ' expires' : '到期' }}
+33 -2
View File
@@ -42,8 +42,13 @@ function handleClickLeft() {
<view class="i-carbon:chevron-left text-50rpx text-#3D3D3D ml-[-10rpx]"></view> <view class="i-carbon:chevron-left text-50rpx text-#3D3D3D ml-[-10rpx]"></view>
</view> </view>
</template> </template>
<template #title>
<view class="invite-navbar-title-wrap">
<text class="invite-navbar-title">{{ t('pages.mine.the-person-invited') }}</text>
<text class="invite-navbar-count">({{ dataList.length }})</text>
</view>
</template>
</wd-navbar> </wd-navbar>
<view class="mb-46rpx mt-18rpx pl-30rpx text-#333 text-46rpx lh-46rpx font-bold">{{ t('pages.mine.the-person-invited') }} ({{ dataList.length }})</view>
</template> </template>
<view class="px-18rpx"> <view class="px-18rpx">
<view <view
@@ -111,4 +116,30 @@ function handleClickLeft() {
</view> </view>
</template> </template>
<style lang="scss" scoped></style> <style lang="scss" scoped>
.invite-navbar-title-wrap {
display: flex;
align-items: baseline;
align-items: center;
align-content: center;
height: 100%;
justify-content: center;
width: 100%;
padding-left: 8rpx;
gap: 8rpx;
}
.invite-navbar-title {
font-size: 34rpx;
line-height: 42rpx;
font-weight: 700;
color: #333;
}
.invite-navbar-count {
font-size: 28rpx;
line-height: 34rpx;
font-weight: 600;
color: #666;
}
</style>
+25 -2
View File
@@ -15,6 +15,29 @@ import {formatTimestamp, formatTimestampWithMonthName} from "@/utils/utils";
import {useUserStore, useConfigStore} from "@/store"; import {useUserStore, useConfigStore} from "@/store";
const userStore = useUserStore() const userStore = useUserStore()
const configStore = useConfigStore() const configStore = useConfigStore()
function normalizeTimestamp(input: unknown): number | null {
if (input == null || input === '') return null
const raw = Number(input)
if (!Number.isFinite(raw) || raw <= 0) return null
// 10位秒级、13位毫秒级、16位微秒级都做兼容
if (raw < 1e11) return Math.trunc(raw * 1000)
if (raw > 1e14) return Math.trunc(raw / 1000)
return Math.trunc(raw)
}
function formatRecipeTime(value: unknown): string {
const ts = normalizeTimestamp(value)
if (!ts) return '--'
return formatTimestamp(ts)
}
function formatCommentTime(value: unknown): string {
const ts = normalizeTimestamp(value)
if (!ts) return '--'
return formatTimestampWithMonthName(ts)
}
// 加载状态 // 加载状态
const loading = ref(true); const loading = ref(true);
// 获取菜谱详情 // 获取菜谱详情
@@ -108,7 +131,7 @@ function getCommentList() {
user_name: userInfo.user_name, // 用户名 user_name: userInfo.user_name, // 用户名
user_avatar: userInfo.user_avatar, // 用户头像地址 user_avatar: userInfo.user_avatar, // 用户头像地址
user_content: item.content, // 用户评论内容 user_content: item.content, // 用户评论内容
create_time: formatTimestampWithMonthName(item.createTime), // 创建时间 create_time: formatCommentTime(item.createTime), // 创建时间
} }
}) })
tableTotal.value = res.total tableTotal.value = res.total
@@ -192,7 +215,7 @@ function handleSend() {
>{{ recipeDetail?.recipeName || '' }}</view >{{ recipeDetail?.recipeName || '' }}</view
> >
<view class="flex-center-sb text-28rpx text-#fff"> <view class="flex-center-sb text-28rpx text-#fff">
<text>{{ formatTimestamp(recipeDetail?.createTime) }}</text> <text>{{ formatRecipeTime(recipeDetail?.createTime) }}</text>
<view class="flex items-center"> <view class="flex items-center">
<image <image
src="@img/chef/1326.png" src="@img/chef/1326.png"
+1
View File
@@ -6,6 +6,7 @@ export const useLogicStore = defineStore('store-list-logic', () => {
const searchLoading = ref(false) const searchLoading = ref(false)
const setPlacesList = (list: any) => { const setPlacesList = (list: any) => {
console.log('setPlacesList', list)
if (Array.isArray(list)) { if (Array.isArray(list)) {
placesList.value = list placesList.value = list
} else { } else {
+4 -1
View File
@@ -229,7 +229,10 @@
"path": "pages/store/search/result" "path": "pages/store/search/result"
}, },
{ {
"path": "pages/store/dishes" "path": "pages/store/dishes",
"style": {
"onReachBottomDistance": 80
}
}, },
{ {
"path": "pages/order/checkout" "path": "pages/order/checkout"
+3 -1
View File
@@ -18,15 +18,17 @@ function handleClickSearch() {
chooseAddress: (data: any) => { chooseAddress: (data: any) => {
console.log('搜索的地址信息', data) console.log('搜索的地址信息', data)
if (data) { if (data) {
addressStore.clearAddressInfo()
addressStore.setAddressLocation({ addressStore.setAddressLocation({
displayName: data.displayName, displayName: data.displayName,
formattedAddress: data.formattedAddress, formattedAddress: data.formattedAddress,
longitude: data.location.lng, longitude: data.location.lng,
latitude: data.location.lat latitude: data.location.lat
}) })
addressStore.pendingIntroBuildingType = true
setTimeout(()=> { setTimeout(()=> {
uni.navigateTo({ uni.navigateTo({
url: '/pages/address/choose-type' url: '/pages/address/save-address/other'
}) })
}, 300) }, 300)
} }
+149 -101
View File
@@ -137,13 +137,12 @@ const isDateSelectable = (date: Date): boolean => {
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();
// 生成连续的5天日期(包括不营业的日期) for (let i = 0; i < 7; i++) {
for (let i = 0; i < 5; i++) {
const date = new Date(today); const date = new Date(today);
date.setDate(today.getDate() + i); date.setDate(today.getDate() + i);
dates.push(date); dates.push(date);
@@ -152,6 +151,9 @@ const dateOptions = computed(() => {
return dates; return dates;
}); });
const thisWeekDates = computed(() => dateOptions.value.slice(0, 5));
const nextWeekDates = computed(() => dateOptions.value.slice(5, 7));
// 状态管理 - 初始化为第一个营业日期 // 状态管理 - 初始化为第一个营业日期
const selectedDate = ref<Date>(); const selectedDate = ref<Date>();
@@ -246,7 +248,6 @@ const initializeSelectedDate = () => {
); );
const firstAllowed = firstOpen || dateOptions.value.find((d) => isAllowedDay(d)); const firstAllowed = firstOpen || dateOptions.value.find((d) => isAllowedDay(d));
selectedDate.value = firstAllowed || dateOptions.value[0]; selectedDate.value = firstAllowed || dateOptions.value[0];
nextTick(() => updateScrollPosition());
return; return;
} }
@@ -254,9 +255,6 @@ const initializeSelectedDate = () => {
for (const date of dateOptions.value) { for (const date of dateOptions.value) {
if (isDateSelectable(date)) { if (isDateSelectable(date)) {
selectedDate.value = date; selectedDate.value = date;
nextTick(() => {
updateScrollPosition();
});
return; return;
} }
} }
@@ -266,7 +264,6 @@ const initializeSelectedDate = () => {
if (nextBusinessDate) { if (nextBusinessDate) {
selectedDate.value = nextBusinessDate; selectedDate.value = nextBusinessDate;
nextTick(() => { nextTick(() => {
updateScrollPosition();
uni.showToast({ uni.showToast({
title: t('pages.address.reservationTime.currentTimeExpired'), title: t('pages.address.reservationTime.currentTimeExpired'),
icon: "none", icon: "none",
@@ -275,9 +272,6 @@ const initializeSelectedDate = () => {
}); });
} else { } else {
selectedDate.value = dateOptions.value[0]; selectedDate.value = dateOptions.value[0];
nextTick(() => {
updateScrollPosition();
});
} }
}; };
@@ -304,59 +298,15 @@ watch(
); );
const selectedTimeSlot = ref<string>(""); const selectedTimeSlot = ref<string>("");
// 横向滚动距离 /** 圆圈内上行:星期(与全局 dayjs 语言一致) */
const scrollLeft = ref<number>(0); const formatWeekdayCircle = (date: Date) => dayjs(date).format("dddd");
// 计算并设置横向滚动距离 /** 圆圈内下行:MM/DD;不可选时显示文案 */
const updateScrollPosition = () => { const formatCircleSubLine = (date: Date) => {
if (!selectedDate.value) return;
// 找到选中日期在 dateOptions 中的索引
const selectedIndex = dateOptions.value.findIndex(date =>
dayjs(date).isSame(dayjs(selectedDate.value), 'day')
);
if (selectedIndex === -1) return;
// 每个日期卡片的宽度:240rpx + 28rpx 间距 = 268rpx
// 但第一个卡片没有左边距,所以需要特殊处理
const cardWidth = 240; // rpx
const cardMargin = 28; // rpx
// 计算滚动距离,让选中的卡片尽量居中显示
let scrollDistance = 0;
if (selectedIndex > 0) {
// 第一个卡片没有左边距,从第二个开始每个卡片占用 240 + 28 = 268rpx
scrollDistance = selectedIndex * (cardWidth + cardMargin);
// 减去一些距离让选中项更居中(可根据屏幕宽度调整)
scrollDistance = Math.max(0, scrollDistance - 100);
}
scrollLeft.value = scrollDistance;
};
// 格式化日期显示
const formatDateDisplay = (date: Date) => {
const today = dayjs();
const targetDate = dayjs(date);
if (targetDate.isSame(today, "day")) {
return "Today";
} else if (targetDate.isSame(today.add(1, "day"), "day")) {
return "Tomorrow";
} else {
// 返回星期几
return targetDate.format("dddd");
}
};
// 格式化日期为月份和日期(不包含年份),不可选择日期显示"不营业"
const formatDateOnly = (date: Date) => {
if (!isDateSelectable(date)) { if (!isDateSelectable(date)) {
return t('pages.address.reservationTime.notAvailable') return t("pages.address.reservationTime.notAvailable");
} }
return dayjs(date).format('MMMM D') return dayjs(date).format("MM/DD");
}; };
/** /**
@@ -536,8 +486,16 @@ const selectTimeSlot = (timeSlot: string) => {
selectedTimeSlot.value = timeSlot; selectedTimeSlot.value = timeSlot;
}; };
const isDateSelected = (date: Date) =>
!!selectedDate.value && dayjs(selectedDate.value).isSame(dayjs(date), "day");
// 提交预约 // 提交预约
const submitReservation = () => { const submitReservation = () => {
const dateVal = selectedDate.value;
if (!dateVal) {
return;
}
// 非仅选日期模式,需要选择时间段 // 非仅选日期模式,需要选择时间段
if (!onlySelectDay.value) { if (!onlySelectDay.value) {
if (!selectedTimeSlot.value) { if (!selectedTimeSlot.value) {
@@ -550,13 +508,13 @@ const submitReservation = () => {
} }
// 计算开始/结束时间 // 计算开始/结束时间
const selectedDateDayjs = dayjs(selectedDate.value); 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(selectedDate.value); const bh = getBusinessHoursForDate(dateVal);
if (bh) { if (bh) {
const [startHour, startMinute] = bh.startTime.split(':').map(Number); const [startHour, startMinute] = bh.startTime.split(':').map(Number);
const [endHour, endMinute] = bh.endTime.split(':').map(Number); const [endHour, endMinute] = bh.endTime.split(':').map(Number);
@@ -592,14 +550,14 @@ const submitReservation = () => {
} }
console.log("预约信息:", { console.log("预约信息:", {
date: selectedDate.value, date: dateVal,
timeSlot: onlySelectDay.value ? '' : selectedTimeSlot.value, timeSlot: onlySelectDay.value ? '' : selectedTimeSlot.value,
startTime: startTime.valueOf(), startTime: startTime.valueOf(),
endTime: endTime.valueOf(), endTime: endTime.valueOf(),
}); });
uni.$emit(EventEnum.CHOOSE_APPOINTMENT_TIME, { uni.$emit(EventEnum.CHOOSE_APPOINTMENT_TIME, {
date: selectedDate.value, date: dateVal,
timeSlot: onlySelectDay.value ? '' : selectedTimeSlot.value, timeSlot: onlySelectDay.value ? '' : selectedTimeSlot.value,
startTime: startTime.valueOf(), startTime: startTime.valueOf(),
endTime: endTime.valueOf(), endTime: endTime.valueOf(),
@@ -633,45 +591,52 @@ onLoad((options: any) => {
</script> </script>
<template> <template>
<view class=""> <view class="reservation-time-page min-h-screen pb-180rpx">
<navbar /> <navbar
<view class="mt-20rpx px-30rpx text-46rpx lh-46rpx text-#333 font-bold"> :title="t('pages.address.reservationTime.pageTitle')"
{{ t("pages.address.appTime") }} circle-back
</view> custom-class="reservation-time-navbar"
<view class="px-30rpx pt-52rpx pb-50rpx w-screen bg-white"> />
<scroll-view class="w-full whitespace-nowrap" scroll-x="true" :scroll-left="scrollLeft"> <view class="reservation-time-body px-40rpx pt-40rpx">
<template v-for="(item, index) in dateOptions" :key="index"> <view class="date-section">
<text class="section-label">{{ t("pages.address.reservationTime.thisWeek") }}</text>
<view class="date-row">
<view <view
@click="selectDate(item)" v-for="(item, index) in thisWeekDates"
:class="[ :key="index"
index === 0 ? '' : 'ml-28rpx', class="date-circle"
selectedDate && dayjs(selectedDate).isSame(dayjs(item), 'day') :class="{
? 'border-#333' 'date-circle--selected': isDateSelected(item),
: 'border-#D8D8D8', 'date-circle--disabled': !isDateSelectable(item),
!isDateSelectable(item) }"
? 'opacity-50 cursor-not-allowed' @click="selectDate(item)"
: 'cursor-pointer',
]"
class="inline-block border-solid border-1px w-240rpx h-140rpx rounded-20rpx px-32rpx py-36rpx"
> >
<view <text class="date-circle__weekday">{{ formatWeekdayCircle(item) }}</text>
:class="!isDateSelectable(item) ? 'text-#999' : 'text-#333'" <text class="date-circle__sub">{{ formatCircleSubLine(item) }}</text>
class="text-28rpx lh-28rpx mb-12rpx"
>
{{ formatDateDisplay(item) }}
</view>
<view
:class="!isDateSelectable(item) ? 'text-#CCC' : 'text-#7D7D7D'"
class="text-28rpx"
>
{{ formatDateOnly(item) }}
</view>
</view> </view>
</template> </view>
</scroll-view> </view>
<view v-if="nextWeekDates.length" class="date-section date-section--next">
<text class="section-label">{{ t("pages.address.reservationTime.nextWeek") }}</text>
<view class="date-row">
<view
v-for="(item, index) in nextWeekDates"
:key="index"
class="date-circle"
:class="{
'date-circle--selected': isDateSelected(item),
'date-circle--disabled': !isDateSelectable(item),
}"
@click="selectDate(item)"
>
<text class="date-circle__weekday">{{ formatWeekdayCircle(item) }}</text>
<text class="date-circle__sub">{{ formatCircleSubLine(item) }}</text>
</view>
</view>
</view>
</view> </view>
<!-- 时间段选择区域在仅选日期模式下隐藏 --> <!-- 时间段选择区域在仅选日期模式下隐藏 -->
<view v-if="!onlySelectDay" class="pb-138rpx"> <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"
:key="index" :key="index"
@@ -705,8 +670,91 @@ onLoad((options: any) => {
</view> </view>
</template> </template>
<style> <style scoped lang="scss">
page { .reservation-time-body {
box-sizing: border-box;
}
.date-section {
margin-bottom: 48rpx;
&--next {
margin-bottom: 0;
}
}
.section-label {
display: block;
font-size: 32rpx;
line-height: 44rpx;
color: #111;
font-weight: 500;
margin-bottom: 24rpx;
}
.date-row {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
gap: 20rpx;
justify-content: flex-start;
}
.date-circle {
width: 118rpx;
height: 118rpx;
border-radius: 50%;
background-color: #fff; background-color: #fff;
box-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.06);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex-shrink: 0;
box-sizing: border-box;
}
.date-circle__weekday {
font-size: 26rpx;
line-height: 32rpx;
color: #111;
font-weight: 500;
}
.date-circle__sub {
font-size: 22rpx;
line-height: 28rpx;
margin-top: 6rpx;
color: #111;
opacity: 0.88;
}
.date-circle--selected {
background-color: #111;
box-shadow: none;
.date-circle__weekday,
.date-circle__sub {
color: #fff;
opacity: 1;
}
}
.date-circle--disabled {
opacity: 0.45;
}
.date-circle--disabled.date-circle--selected {
opacity: 1;
}
</style>
<style lang="scss">
page {
background-color: #f7f7f9;
}
.reservation-time-page :deep(.reservation-time-navbar.wd-navbar) {
background-color: #f7f7f9 !important;
} }
</style> </style>
+12 -1
View File
@@ -99,6 +99,13 @@ onLoad(()=> {
} }
}) })
}) })
onReady(() => {
if (addressStore.pendingIntroBuildingType && !addressStore.addressInfo.id) {
buildingTypeRef.value?.openIntroSheet?.()
}
})
onUnload(()=> { onUnload(()=> {
if(!isSwitch.value) { if(!isSwitch.value) {
addressStore.clearAddressInfo() addressStore.clearAddressInfo()
@@ -176,7 +183,11 @@ function chooseStateConfirm(data: any) {
<view class="mb-20rpx text-36rpx lh-36rpx text-#333 font-500">{{ t('pages.address.choose-type.navTitle') }}</view> <view class="mb-20rpx text-36rpx lh-36rpx text-#333 font-500">{{ t('pages.address.choose-type.navTitle') }}</view>
<view @click="openBuildingType" class="bg-#F6F6F6 rounded-16rpx px-24rpx h-98rpx flex-center-sb"> <view @click="openBuildingType" class="bg-#F6F6F6 rounded-16rpx px-24rpx h-98rpx flex-center-sb">
<text class="text-30rpx lh-30rpx text-#333"> <text class="text-30rpx lh-30rpx text-#333">
{{ `${t(`pages.address.choose-type.${addressStore.addressInfo.type}`)}` }} {{
addressStore.addressInfo.type
? t(`pages.address.choose-type.${addressStore.addressInfo.type}`)
: t('common.placeholder.pleaseSelect')
}}
</text> </text>
<image src="@img/chef/142.png" class="w-32rpx h-32rpx shrink-0 rotate-90"></image> <image src="@img/chef/142.png" class="w-32rpx h-32rpx shrink-0 rotate-90"></image>
</view> </view>
@@ -1,70 +1,228 @@
<script setup lang="ts"> <script setup lang="ts">
import {UserAddressType} from "@/constant/enums"; import { UserAddressType } from '@/constant/enums';
import { useAddressStore } from '@/pages/address/store/address';
const { t } = useI18n(); const { t } = useI18n();
const show = ref(false); const addressStore = useAddressStore();
const value = ref('house')
const emit = defineEmits(['submit']) /** 新地址首次进入保存页:与原先 choose-type 全页等价的介绍弹窗 */
const showIntro = ref(false);
/** 点击表单「建筑类型」行:原有滚轮选择器 */
const showPicker = ref(false);
const emit = defineEmits<{
submit: [value: string];
}>();
const typeList = computed(() => [
{
label: t('pages.address.choose-type.house'),
desc: t('pages.address.choose-type.houseDescription'),
value: UserAddressType.HOUSE,
icon: '/static/images/chef/147.png',
},
{
label: t('pages.address.choose-type.apartment'),
desc: t('pages.address.choose-type.apartmentDescription'),
value: UserAddressType.APARTMENT,
icon: '/static/images/chef/148.png',
},
{
label: t('pages.address.choose-type.office'),
desc: t('pages.address.choose-type.officeDescription'),
value: UserAddressType.OFFICE,
icon: '/static/images/chef/149.png',
},
{
label: t('pages.address.choose-type.hotel'),
desc: t('pages.address.choose-type.hotelDescription'),
value: UserAddressType.HOTEL,
icon: '/static/images/chef/150.png',
},
{
label: t('pages.address.choose-type.other'),
desc: t('pages.address.choose-type.otherDescription'),
value: UserAddressType.OTHER,
icon: '/static/images/chef/151.png',
},
]);
const columns = computed(() => [
{ label: t('pages.address.choose-type.house'), value: UserAddressType.HOUSE },
{ label: t('pages.address.choose-type.apartment'), value: UserAddressType.APARTMENT },
{ label: t('pages.address.choose-type.office'), value: UserAddressType.OFFICE },
{ label: t('pages.address.choose-type.hotel'), value: UserAddressType.HOTEL },
{ label: t('pages.address.choose-type.other'), value: UserAddressType.OTHER },
]);
const pickerValue = ref<string>(UserAddressType.HOUSE);
function routeForType(type: string): string {
switch (type) {
case UserAddressType.HOUSE:
return 'pages/address/save-address/house';
case UserAddressType.APARTMENT:
return 'pages/address/save-address/apartment';
case UserAddressType.OFFICE:
return 'pages/address/save-address/office';
case UserAddressType.HOTEL:
return 'pages/address/save-address/hotel';
case UserAddressType.OTHER:
return 'pages/address/save-address/other';
default:
return '';
}
}
function currentSaveAddressRoute(): string {
const pages = getCurrentPages();
return pages[pages.length - 1]?.route ?? '';
}
/** 点击表单行:滚轮选择器(原效果) */
function onOpen() { function onOpen() {
show.value = true; pickerValue.value =
} addressStore.addressInfo.type && `${addressStore.addressInfo.type}`.length > 0
function handleClose() { ? (addressStore.addressInfo.type as string)
show.value = false; : UserAddressType.HOUSE;
} showPicker.value = true;
function handleSubmit() {
console.log('value', value.value)
emit('submit', value.value)
handleClose()
} }
const columns = ref( function handlePickerClose() {
[ showPicker.value = false;
{ }
label: t('pages.address.choose-type.house'),
value: UserAddressType.HOUSE, function handlePickerCancel() {
}, showPicker.value = false;
{ }
label: t('pages.address.choose-type.apartment'),
value: UserAddressType.APARTMENT, function handlePickerConfirm() {
}, addressStore.addressInfo.type = pickerValue.value;
{ showPicker.value = false;
label: t('pages.address.choose-type.office'), const next = routeForType(pickerValue.value);
value: UserAddressType.OFFICE, if (next && currentSaveAddressRoute() !== next) {
}, emit('submit', pickerValue.value);
{ }
label: t('pages.address.choose-type.hotel'), }
value: UserAddressType.HOTEL,
}, /** 新地址首次进入页面时由父级调用 */
{ function openIntroSheet() {
label: t('pages.address.choose-type.other'), showIntro.value = true;
value: UserAddressType.OTHER, }
},
] function handleIntroClose() {
) showIntro.value = false;
function onChange({picker, value, index}) { }
/** 蒙层关闭且尚未完成选择时,与「跳过」一致 */
function onIntroPopupClosed() {
if (!addressStore.pendingIntroBuildingType) return;
addressStore.pendingIntroBuildingType = false;
addressStore.addressInfo.type = UserAddressType.OTHER;
const next = routeForType(UserAddressType.OTHER);
if (next && currentSaveAddressRoute() !== next) {
emit('submit', UserAddressType.OTHER);
}
}
function chooseIntroType(item: { value: string }) {
addressStore.pendingIntroBuildingType = false;
addressStore.addressInfo.type = item.value;
showIntro.value = false;
const next = routeForType(item.value);
if (next && currentSaveAddressRoute() !== next) {
emit('submit', item.value);
}
}
function handleIntroSkip() {
addressStore.pendingIntroBuildingType = false;
addressStore.addressInfo.type = UserAddressType.OTHER;
showIntro.value = false;
const next = routeForType(UserAddressType.OTHER);
if (next && currentSaveAddressRoute() !== next) {
emit('submit', UserAddressType.OTHER);
}
} }
defineExpose({ defineExpose({
onOpen, onOpen,
openIntroSheet,
}); });
</script> </script>
<template> <template>
<!-- 新地址首次建筑类型介绍底部弹窗 -->
<wd-popup <wd-popup
v-model="show" v-model="showIntro"
position="bottom" position="bottom"
@close="handleClose" :z-index="120"
custom-style="background: transparent;"
@close="onIntroPopupClosed"
>
<view class="bg-white rounded-t-32rpx overflow-hidden pb-[calc(24rpx+env(safe-area-inset-bottom))]">
<view class="relative px-30rpx pt-40rpx pb-8rpx">
<text
class="absolute right-30rpx top-40rpx z-1 text-32rpx lh-32rpx text-#CE7138"
@click="handleIntroSkip"
>
{{ t('common.skip') }}
</text>
<view class="text-center text-36rpx lh-44rpx text-#333 font-500 pr-100rpx pl-100rpx">
{{ t('pages.address.choose-type.title') }}
</view>
<view class="mt-24rpx text-center text-28rpx lh-36rpx text-#666">
{{ t('pages.address.choose-type.description') }}
</view>
</view>
<scroll-view scroll-y class="box-border max-h-[62vh] px-30rpx pb-8rpx pt-28rpx">
<view
v-for="(item, index) in typeList"
:key="item.value"
:class="[
index === 0 ? '' : 'mt-28rpx',
item.value === addressStore.addressInfo.type
? 'border-#000 border-4rpx'
: 'border-#DFDFDF border-2rpx',
'border-solid rounded-16rpx px-28rpx py-34rpx flex items-center',
]"
@click="chooseIntroType(item)"
>
<image :src="item.icon" class="h-48rpx mr-28rpx shrink-0 w-48rpx" />
<view class="min-w-0 flex-1">
<view class="text-36rpx lh-40rpx text-#333 font-500">
{{ item.label }}
</view>
<view class="mt-18rpx text-28rpx lh-32rpx text-#6D6D6D">
{{ item.desc }}
</view>
</view>
</view>
</scroll-view>
</view>
</wd-popup>
<!-- 点击建筑类型行原滚轮选择 -->
<wd-popup
v-model="showPicker"
position="bottom"
:z-index="121"
@close="handlePickerClose"
> >
<view> <view>
<view class="flex-center-sb h-102rpx bg-#F7F7F7 px-30rpx"> <view class="flex-center-sb h-102rpx bg-#F7F7F7 px-30rpx">
<view @click="handleClose" class="text-30rpx text-#999">{{ t('common.cancel') }}</view> <view class="text-30rpx text-#999" @click="handlePickerCancel">{{ t('common.cancel') }}</view>
<view class="text-34rpx text-#333">{{ t('common.buildingType') }}</view> <view class="text-34rpx text-#333">{{ t('common.buildingType') }}</view>
<view @click="handleSubmit" class="text-30rpx text-#FF6106">{{ t('common.confirm') }}</view> <view class="text-30rpx text-#FF6106" @click="handlePickerConfirm">{{ t('common.confirm') }}</view>
</view> </view>
<view class="bg-#fff px-54rpx py-56rpx"> <view class="bg-#fff px-54rpx py-56rpx">
<wd-picker-view :columns="columns" v-model="value" @change="onChange" label-key="label" value-key="value" /> <wd-picker-view
:columns="columns"
v-model="pickerValue"
label-key="label"
value-key="value"
/>
</view> </view>
</view> </view>
</wd-popup> </wd-popup>
+12 -1
View File
@@ -98,6 +98,13 @@ onLoad(()=> {
} }
}) })
}) })
onReady(() => {
if (addressStore.pendingIntroBuildingType && !addressStore.addressInfo.id) {
buildingTypeRef.value?.openIntroSheet?.()
}
})
onUnload(()=> { onUnload(()=> {
if(!isSwitch.value) { if(!isSwitch.value) {
addressStore.clearAddressInfo() addressStore.clearAddressInfo()
@@ -173,7 +180,11 @@ function chooseStateConfirm(data: any) {
<view class="mb-20rpx text-36rpx lh-36rpx text-#333 font-500">{{ t('pages.address.choose-type.navTitle') }}</view> <view class="mb-20rpx text-36rpx lh-36rpx text-#333 font-500">{{ t('pages.address.choose-type.navTitle') }}</view>
<view @click="openBuildingType" class="bg-#F6F6F6 rounded-16rpx px-24rpx h-98rpx flex-center-sb"> <view @click="openBuildingType" class="bg-#F6F6F6 rounded-16rpx px-24rpx h-98rpx flex-center-sb">
<text class="text-30rpx lh-30rpx text-#333"> <text class="text-30rpx lh-30rpx text-#333">
{{ `${t(`pages.address.choose-type.${addressStore.addressInfo.type}`)}` }} {{
addressStore.addressInfo.type
? t(`pages.address.choose-type.${addressStore.addressInfo.type}`)
: t('common.placeholder.pleaseSelect')
}}
</text> </text>
<image src="@img/chef/142.png" class="w-32rpx h-32rpx shrink-0 rotate-90"></image> <image src="@img/chef/142.png" class="w-32rpx h-32rpx shrink-0 rotate-90"></image>
</view> </view>
+12 -1
View File
@@ -98,6 +98,13 @@ onLoad(()=> {
} }
}) })
}) })
onReady(() => {
if (addressStore.pendingIntroBuildingType && !addressStore.addressInfo.id) {
buildingTypeRef.value?.openIntroSheet?.()
}
})
onUnload(()=> { onUnload(()=> {
if(!isSwitch.value) { if(!isSwitch.value) {
addressStore.clearAddressInfo() addressStore.clearAddressInfo()
@@ -173,7 +180,11 @@ function chooseStateConfirm(data: any) {
<view class="mb-20rpx text-36rpx lh-36rpx text-#333 font-500">{{ t('pages.address.choose-type.navTitle') }}</view> <view class="mb-20rpx text-36rpx lh-36rpx text-#333 font-500">{{ t('pages.address.choose-type.navTitle') }}</view>
<view @click="openBuildingType" class="bg-#F6F6F6 rounded-16rpx px-24rpx h-98rpx flex-center-sb"> <view @click="openBuildingType" class="bg-#F6F6F6 rounded-16rpx px-24rpx h-98rpx flex-center-sb">
<text class="text-30rpx lh-30rpx text-#333"> <text class="text-30rpx lh-30rpx text-#333">
{{ `${t(`pages.address.choose-type.${addressStore.addressInfo.type}`)}` }} {{
addressStore.addressInfo.type
? t(`pages.address.choose-type.${addressStore.addressInfo.type}`)
: t('common.placeholder.pleaseSelect')
}}
</text> </text>
<image src="@img/chef/142.png" class="w-32rpx h-32rpx shrink-0 rotate-90"></image> <image src="@img/chef/142.png" class="w-32rpx h-32rpx shrink-0 rotate-90"></image>
</view> </view>
+12 -1
View File
@@ -98,6 +98,13 @@ onLoad(()=> {
} }
}) })
}) })
onReady(() => {
if (addressStore.pendingIntroBuildingType && !addressStore.addressInfo.id) {
buildingTypeRef.value?.openIntroSheet?.()
}
})
onUnload(()=> { onUnload(()=> {
if(!isSwitch.value) { if(!isSwitch.value) {
addressStore.clearAddressInfo() addressStore.clearAddressInfo()
@@ -173,7 +180,11 @@ function chooseStateConfirm(data: any) {
<view class="mb-20rpx text-36rpx lh-36rpx text-#333 font-500">{{ t('pages.address.choose-type.navTitle') }}</view> <view class="mb-20rpx text-36rpx lh-36rpx text-#333 font-500">{{ t('pages.address.choose-type.navTitle') }}</view>
<view @click="openBuildingType" class="bg-#F6F6F6 rounded-16rpx px-24rpx h-98rpx flex-center-sb"> <view @click="openBuildingType" class="bg-#F6F6F6 rounded-16rpx px-24rpx h-98rpx flex-center-sb">
<text class="text-30rpx lh-30rpx text-#333"> <text class="text-30rpx lh-30rpx text-#333">
{{ `${t(`pages.address.choose-type.${addressStore.addressInfo.type}`)}` }} {{
addressStore.addressInfo.type
? t(`pages.address.choose-type.${addressStore.addressInfo.type}`)
: t('common.placeholder.pleaseSelect')
}}
</text> </text>
<image src="@img/chef/142.png" class="w-32rpx h-32rpx shrink-0 rotate-90"></image> <image src="@img/chef/142.png" class="w-32rpx h-32rpx shrink-0 rotate-90"></image>
</view> </view>
+12 -1
View File
@@ -98,6 +98,13 @@ onLoad(()=> {
} }
}) })
}) })
onReady(() => {
if (addressStore.pendingIntroBuildingType && !addressStore.addressInfo.id) {
buildingTypeRef.value?.openIntroSheet?.()
}
})
onUnload(()=> { onUnload(()=> {
if(!isSwitch.value) { if(!isSwitch.value) {
addressStore.clearAddressInfo() addressStore.clearAddressInfo()
@@ -173,7 +180,11 @@ function chooseStateConfirm(data: any) {
<view class="mb-20rpx text-36rpx lh-36rpx text-#333 font-500">{{ t('pages.address.choose-type.navTitle') }}</view> <view class="mb-20rpx text-36rpx lh-36rpx text-#333 font-500">{{ t('pages.address.choose-type.navTitle') }}</view>
<view @click="openBuildingType" class="bg-#F6F6F6 rounded-16rpx px-24rpx h-98rpx flex-center-sb"> <view @click="openBuildingType" class="bg-#F6F6F6 rounded-16rpx px-24rpx h-98rpx flex-center-sb">
<text class="text-30rpx lh-30rpx text-#333"> <text class="text-30rpx lh-30rpx text-#333">
{{ `${t(`pages.address.choose-type.${addressStore.addressInfo.type}`)}` }} {{
addressStore.addressInfo.type
? t(`pages.address.choose-type.${addressStore.addressInfo.type}`)
: t('common.placeholder.pleaseSelect')
}}
</text> </text>
<image src="@img/chef/142.png" class="w-32rpx h-32rpx shrink-0 rotate-90"></image> <image src="@img/chef/142.png" class="w-32rpx h-32rpx shrink-0 rotate-90"></image>
</view> </view>
+5
View File
@@ -1,6 +1,9 @@
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import type { UserAddressBo } from '@/service/types'; import type { UserAddressBo } from '@/service/types';
export const useAddressStore = defineStore('store-address', () => { export const useAddressStore = defineStore('store-address', () => {
/** 从地图搜索新增地址进入保存页时,首次需弹出建筑类型介绍层(与原先进入 choose-type 页等价) */
const pendingIntroBuildingType = ref(false)
const addressInfo = ref<UserAddressBo>({ const addressInfo = ref<UserAddressBo>({
type: '', type: '',
/** 配送类型 1-亲自送达 2-放门口,默认放门口 */ /** 配送类型 1-亲自送达 2-放门口,默认放门口 */
@@ -57,6 +60,7 @@ export const useAddressStore = defineStore('store-address', () => {
} }
function clearAddressInfo() { function clearAddressInfo() {
pendingIntroBuildingType.value = false
addressInfo.value = { addressInfo.value = {
id: '', id: '',
type: '', type: '',
@@ -102,6 +106,7 @@ export const useAddressStore = defineStore('store-address', () => {
return { return {
addressInfo, addressInfo,
addressLocation, addressLocation,
pendingIntroBuildingType,
setAddressLocation, setAddressLocation,
clearAddressInfo clearAddressInfo
} }
@@ -1,23 +1,17 @@
<script setup lang="ts"> <script setup lang="ts">
import useEventEmit from "@/hooks/useEventEmit";
import {CollectionType, EventEnum} from "@/constant/enums";
import { debounce } from 'throttle-debounce'
import Search from "../tabbar-home/components/search.vue"; import Search from "../tabbar-home/components/search.vue";
import { useConfigStore, useUserStore } from "@/store"; import { useConfigStore, useUserStore } from "@/store";
import MsgBox from "../tabbar-home/components/msg-box.vue";
import Collection from "@/components/collection/index.vue";
import BrowseSkeleton from "./components/browse-skeleton.vue"; import BrowseSkeleton from "./components/browse-skeleton.vue";
import { import {
appMerchantDishNearbyListPost, appMerchantDishNearbyListPost,
appSearchSearchRecipePost, appSearchSearchRecipePost,
appCollectCollectPost,
} from "@/service"; } from "@/service";
import {thumbnailImg} from "@/utils/utils"; import {thumbnailImg} from "@/utils/utils";
const configStore = useConfigStore(); const configStore = useConfigStore();
const userStore = useUserStore(); const userStore = useUserStore();
const emit = defineEmits(["toggleNotOpen"]);
const loading = ref(false); const loading = ref(false);
const recipePreviewLimit = 4;
const { t } = useI18n(); const { t } = useI18n();
@@ -29,10 +23,6 @@ function navigateTo(url: string) {
} }
} }
function toggleNotOpen() {
emit("toggleNotOpen");
}
async function initData() { async function initData() {
if(!recipeData.value) { if(!recipeData.value) {
loading.value = true; loading.value = true;
@@ -43,13 +33,14 @@ async function initData() {
} }
// 获取菜谱数据 // 获取菜谱数据
const recipeData = ref([]); const recipeData = ref<any[]>([]);
function getRecipeData() { function getRecipeData() {
appSearchSearchRecipePost({ appSearchSearchRecipePost({
body: { params: {
pageNum: 1, pageNum: 1,
pageSize: 10, pageSize: 10,
} },
body: {}
}).then(res=> { }).then(res=> {
console.log('菜谱数据', res) console.log('菜谱数据', res)
recipeData.value = res.rows; recipeData.value = res.rows;
@@ -57,30 +48,16 @@ function getRecipeData() {
loading.value = false; loading.value = false;
}) })
} }
// 收藏菜品
function handleSubmitCollectRecipe(item: any) {
collectRecipe(item)
}
// 防抖处理函数
const collectRecipe = debounce(1000, (item: any) => {
appCollectCollectPost({
body: {
targetId: item.id,
targetType: CollectionType.RECIPE
}
}).then(res=> {
item.isCollect = !item.isCollect;
})
}, {
atBegin: true, // 立即触发
});
function navigateToRecipeDetail(id: string | number) { function navigateToRecipeDetail(id: string | number) {
navigateTo(`/pages-user/pages/recipe/index?id=${id}`) navigateTo(`/pages-user/pages/recipe/index?id=${id}`)
} }
function navigateToRecipeList() {
navigateTo('/pages-user/pages/recipe/list')
}
// 获取附近的菜品 // 获取附近的菜品
const dishData = ref([]); const dishData = ref<any[]>([]);
function appMerchantDishNearbyList() { function appMerchantDishNearbyList() {
appMerchantDishNearbyListPost({ appMerchantDishNearbyListPost({
params: { params: {
@@ -88,8 +65,8 @@ function appMerchantDishNearbyList() {
pageSize: 10, pageSize: 10,
}, },
body: { body: {
lat: userStore.userLocation.latitude, lat: String(userStore.userLocation.latitude ?? ''),
lng: userStore.userLocation.longitude, lng: String(userStore.userLocation.longitude ?? ''),
} }
}).then(res=> { }).then(res=> {
console.log('菜品数据', res) console.log('菜品数据', res)
@@ -100,6 +77,27 @@ function handleClickDish(item: any) {
navigateTo(`/pages-store/pages/store/index?id=${item.merchantId}`) navigateTo(`/pages-store/pages/store/index?id=${item.merchantId}`)
} }
function getMerchantName(item: any) {
return item?.merchantVo?.merchantName || item?.merchantName||item?.dishName || '--'
}
function getMerchantLogo(item: any) {
return item?.merchantVo?.logo || item?.logo || item?.dishImage?.split?.(',')?.[0] || item?.dishImage || ''
}
function getMerchantRate(item: any) {
const rating = Number(item?.merchantVo?.rating ?? item?.rating ?? 0)
return Number.isFinite(rating) && rating > 0 ? rating.toFixed(1) : '5.0'
}
function getPreviewRecipeList() {
return recipeData.value.slice(0, recipePreviewLimit)
}
function showRecipeMore() {
return recipeData.value.length > recipePreviewLimit
}
async function getPlatformDefaultStoreInfo() {} async function getPlatformDefaultStoreInfo() {}
defineExpose({ defineExpose({
@@ -131,65 +129,57 @@ defineExpose({
v-show="!loading" v-show="!loading"
class="animate-in fade-in animate-ease-in animate-duration-300" class="animate-in fade-in animate-ease-in animate-duration-300"
> >
<view class="flex-center-sb px-30rpx pt-16rpx"> <view class="px-24rpx pt-16rpx">
<view class="text-56rpx text-#333 lh-56rpx font-bold">{{
t("tabBar.browse")
}}</view>
<msg-box @toggleNotOpen="toggleNotOpen" />
</view>
<view class="px-30rpx mt-44rpx">
<search /> <search />
</view> </view>
<view class="mt-50rpx px-30rpx"> <view class="browse-wrap px-24rpx">
<view @click="navigateTo('/pages-user/pages/recipe/list')" class="flex-center-sb"> <view class="section-title mt-36rpx">{{ t("pages.browse.titleRecipes") }}</view>
<text class="text-36rpx lh-36rpx text-#333 font-bold">{{ <view class="mt-28rpx">
t("pages.browse.titleRecipes") <scroll-view scroll-x class="recipe-scroll" :show-scrollbar="false" :enable-flex="true">
}}</text> <view class="recipe-track">
<image src="@img/chef/116.png" class="w-64rpx h-64rpx"></image> <view
</view> v-for="item in getPreviewRecipeList()"
<scroll-view scroll-x="true" class="mt-16rpx"> :key="item.id"
<view class="flex gap-30rpx"> class="recipe-item"
<template v-for="item in recipeData"> @click="navigateToRecipeDetail(item.id)"
<view @click="navigateToRecipeDetail(item.id)" class="w-312rpx"> >
<image <image
:src="thumbnailImg(item?.recipeImage?.split(',')[0])" :src="thumbnailImg(item?.recipeImage?.split(',')[0])"
class="w-310rpx h-296rpx rounded-24rpx mb-26rpx bg-common" class="recipe-avatar"
mode="aspectFill" mode="aspectFill"
></image> />
<view class="flex-center-sb"> <text class="recipe-name line-clamp-1">{{ item.recipeName }}</text>
<text class="text-30rpx lh-30rpx text-#333 line-clamp-1 tracking-[.04em] font-500" </view>
>{{ item.recipeName }}</text <view v-if="showRecipeMore()" class="recipe-more" @click="navigateToRecipeList">
> <view class="recipe-more__text-wrap">
<view class="w-40rpx h-40rpx ml-14rpx shrink-0"> <text class="recipe-more__text">{{ t("pages.browse.moreRecipes") }}</text>
<collection </view>
:is-collected="item.isCollect" <i class="i-carbon:chevron-right recipe-more__icon"></i>
@collectionChange="handleSubmitCollectRecipe(item)" </view>
/> </view>
</view> </scroll-view>
</view>
<view class="section-title mt-54rpx">{{ t("pages.browse.titleCuisine") }}</view>
<scroll-view scroll-x class="store-scroll mt-28rpx pb-40rpx" :show-scrollbar="false" :enable-flex="true">
<view class="store-track">
<view v-for="item in dishData" :key="item.id" @click="handleClickDish(item)" class="store-card">
<image
:src="thumbnailImg(getMerchantLogo(item))"
class="store-card__cover"
mode="aspectFill"
/>
<view class="store-card__right">
<view class="store-card__name line-clamp-2">{{ getMerchantName(item) }}</view>
<view class="store-card__rating"> {{ getMerchantRate(item) }}</view>
<view class="store-card__brand">{{ t("pages.browse.brandTag") }}</view>
<view class="store-card__arrow center">
<i class="i-carbon:chevron-right text-30rpx text-white"></i>
</view> </view>
</view> </view>
</template> </view>
</view> </view>
</scroll-view> </scroll-view>
<view
class="mt-50rpx mb-28rpx text-36rpx lh-36rpx text-#333 font-bold"
>{{ t("pages.browse.titleCuisine") }}</view
>
<view class="grid grid-cols-2 gap-x-30rpx gap-y-46rpx pb-40rpx">
<template v-for="item in dishData">
<view @click="handleClickDish(item)" class="w-330rpx overflow-hidden">
<image
:src="thumbnailImg(item?.dishImage?.split(',')[0])"
class="w-full h-186rpx rounded-24rpx mb-16rpx bg-common"
mode="aspectFill"
></image>
<text class="text-30rpx lh-30rpx text-#333 line-clamp-1 tracking-[.04em] font-500"
>{{ item.dishName }}</text
>
</view>
</template>
</view>
</view> </view>
</view> </view>
@@ -201,4 +191,148 @@ defineExpose({
</view> </view>
</template> </template>
<style scoped lang="scss"></style> <style scoped lang="scss">
.browse-wrap {
background: #fff;
}
.section-title {
font-size: 32rpx;
line-height: 48rpx;
// font-weight: 700;
color: #1c1c1c;
}
.recipe-scroll,
.store-scroll {
width: 100%;
}
.recipe-track {
display: flex;
align-items: flex-start;
gap: 22rpx;
padding-right: 24rpx;
}
.recipe-item {
flex-shrink: 0;
display: flex;
flex-direction: column;
align-items: center;
}
.recipe-avatar {
width: 112rpx;
height: 112rpx;
border-radius: 56rpx;
background: #f1f1f1;
}
.recipe-name {
max-width: 140rpx;
margin-top: 14rpx;
text-align: center;
font-size: 26rpx;
line-height: 32rpx;
color: #222;
font-weight: 500;
}
.recipe-more {
flex-shrink: 0;
height: 112rpx;
display: flex;
align-items: center;
justify-content: space-between;
gap: 4rpx;
padding: 4rpx 8rpx;
}
.recipe-more__text-wrap {
display: flex;
flex-direction: column;
justify-content: center;
gap: 6rpx;
}
.recipe-more__text {
font-size: 24rpx;
line-height: 28rpx;
color: #8a8a8a;
font-weight: 500;
writing-mode: vertical-rl;
text-orientation: upright;
letter-spacing: 2rpx;
white-space: nowrap;
}
.recipe-more__icon {
font-size: 20rpx;
color: #8a8a8a;
}
.store-track {
display: flex;
align-items: stretch;
gap: 22rpx;
padding-right: 24rpx;
}
.store-card {
width: 404rpx;
height: 220rpx;
border-radius: 20rpx;
overflow: hidden;
background: #fff;
flex-shrink: 0;
display: flex;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
}
.store-card__cover {
width: 184rpx;
height: 220rpx;
background: #f2f2f2;
flex-shrink: 0;
}
.store-card__right {
flex: 1;
padding: 16rpx 14rpx 14rpx 16rpx;
position: relative;
}
.store-card__name {
font-size: 28rpx;
line-height: 34rpx;
color: #191919;
font-weight: 600;
}
.store-card__rating {
margin-top: 8rpx;
color: #111;
font-size: 26rpx;
line-height: 30rpx;
font-weight: 500;
}
.store-card__brand {
margin-top: 12rpx;
color: #d39a48;
font-size: 24rpx;
line-height: 28rpx;
font-weight: 500;
}
.store-card__arrow {
position: absolute;
right: 10rpx;
bottom: 12rpx;
width: 52rpx;
height: 52rpx;
border-radius: 50%;
background: #111;
}
</style>
@@ -3,7 +3,7 @@
<!-- 第一行自动缓慢滚动 + 可手动滑动 --> <!-- 第一行自动缓慢滚动 + 可手动滑动 -->
<view class="scroll-row first-row"> <view class="scroll-row first-row">
<scroll-view <scroll-view
class="ab-scroll mb-22rpx" class="ab-scroll mb-10rpx"
scroll-x scroll-x
show-scrollbar="false" show-scrollbar="false"
:scroll-left="scrollLeft1" :scroll-left="scrollLeft1"
@@ -77,6 +77,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, onMounted, onUnmounted, nextTick } from 'vue' import { computed, ref, onMounted, onUnmounted, nextTick } from 'vue'
import { useCategoryNavStore } from '@/store'
// 定义分类项接口(与模板字段一致) // 定义分类项接口(与模板字段一致)
interface CategoryItem { interface CategoryItem {
@@ -97,6 +98,8 @@ const props = withDefaults(defineProps<Props>(), {
categories: () => [] categories: () => []
}) })
const categoryNavStore = useCategoryNavStore()
// 定义事件 // 定义事件
const emit = defineEmits<{ const emit = defineEmits<{
/** 点击分类项事件 */ /** 点击分类项事件 */
@@ -241,10 +244,11 @@ function onScroll2(e: any) {
} }
} }
// 点击处理 // 点击处理(列表页菜谱 id 来自 store,不放在 URL query
const handleItemClick = (item: CategoryItem) => { const handleItemClick = (item: CategoryItem) => {
emit('itemClick', item) emit('itemClick', item)
navigateTo('/pages-store/pages/list/index?id=' + item.id) categoryNavStore.setPendingRecipeCategoryId(item.id)
navigateTo('/pages-store/pages/list/index')
} }
function navigateTo(url: string) { function navigateTo(url: string) {
@@ -301,14 +305,16 @@ onUnmounted(() => {
min-width: 120rpx; min-width: 120rpx;
height: 60rpx; height: 60rpx;
padding: 0 20rpx; padding: 0 20rpx;
border: 1px solid #C8C8C8; background: #fff;
border: none;
border-radius: 30rpx; border-radius: 30rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all 0.2s ease;
flex-shrink: 0; flex-shrink: 0;
&:active { &:active {
transform: translateY(0) scale(0.95); transform: translateY(0) scale(0.96);
} }
.category-icon { .category-icon {
@@ -1,40 +1,277 @@
<script setup lang="ts"> <script setup lang="ts">
import {thumbnailImg} from "@/utils/utils"; import { thumbnailImg } from '@/utils/utils'
const props = defineProps<{ const props = defineProps<{
list: any[]; list: any[]
}>(); }>()
const { t } = useI18n(); const { t } = useI18n()
function handleClickFood(item: any) { function handleClickFood(item: any) {
uni.navigateTo({ uni.navigateTo({
url: '/pages-store/pages/store/index?id=' + item.id url: '/pages-store/pages/store/index?id=' + item.id,
}) })
} }
function getCardImages(item: any): string[] {
const raw = item?.shopImages
let urls: string[] = []
if (raw && typeof raw === 'string') {
urls = raw
.split(',')
.map((s: string) => s.trim())
.filter(Boolean)
.map((u: string) => thumbnailImg(u) || u)
}
if (urls.length === 0 && item?.logo) {
const logo = thumbnailImg(item.logo) || item.logo
urls = logo ? [logo] : []
}
return urls
}
function getTitleLines(item: any): { line1: string; line2: string } {
const en = item?.merchantCategoryNamesEn?.[0]
const zh = item?.merchantCategoryNamesZh?.[0]
const name = String(item?.merchantName || '').trim()
if (en && zh) return { line1: String(en), line2: String(zh) }
if (en && !zh) return { line1: String(en), line2: name && name !== String(en) ? name : '' }
if (zh && !en) return { line1: String(zh), line2: name && name !== String(zh) ? name : '' }
const parts = name.split(/\n|\r|\|/).map((s) => s.trim()).filter(Boolean)
if (parts.length >= 2) return { line1: parts[0], line2: parts[1] }
return { line1: name, line2: '' }
}
function formatMerchantRating(item: any): string {
const r = Number(item?.rating)
if (Number.isFinite(r) && r > 0) return r.toFixed(1)
return '5.0'
}
function subtitleLine(item: any): string {
const comments = item?.commentCount ?? 0
const dt = item?.deliveryTime
if (dt === undefined || dt === null || dt === '') {
return `(${comments})`
}
const n = Number(dt)
if (Number.isFinite(n) && n > 0) {
const unit = n === 1 ? t('common.day') : t('common.days')
return `(${comments}) · ${n}${unit}`
}
return `(${comments}) · ${dt}`
}
</script> </script>
<template> <template>
<scroll-view scroll-x="true"> <scroll-view class="featured-scroll" scroll-x enable-flex :show-scrollbar="false">
<view class="flex"> <view class="featured-track">
<view class="w-30rpx shrink-0"></view> <view class="featured-track__pad" />
<template v-for="(item, index) in list" :key="item?.id ?? index"> <view
<view @click="handleClickFood(item)" :class="[index === 0 ? '' : 'ml-28rpx']"> v-for="(item, index) in list"
<image :src="thumbnailImg(item?.shopImages?.split(',')[0])" class="w-448rpx h-252rpx rounded-24rpx mb-20rpx bg-common" mode="aspectFill"></image> :key="item?.id ?? index"
<text class="text-30rpx lh-30rpx text-#333 font-500 line-clamp-1">{{ item?.merchantName }}</text> class="featured-card"
<view v-if="+item.deliveryService === 1" class="text-#CE7138 text-24rpx lh-24rpx mt-12rpx"> :class="{ 'featured-card--first': index === 0 }"
@click="handleClickFood(item)"
>
<!-- 左侧图单图或与示例一致的双图上下分栏 -->
<view class="featured-card__media">
<template v-if="getCardImages(item).length >= 2">
<image
:src="getCardImages(item)[0]"
class="featured-card__media-half featured-card__media-half--top"
mode="aspectFill"
/>
</template>
<image
v-else-if="getCardImages(item).length >= 1"
:src="getCardImages(item)[0]"
class="featured-card__media-single"
mode="aspectFill"
/>
<view v-else class="featured-card__media-single featured-card__media-placeholder" />
</view>
<view class="featured-card__body">
<view class="featured-card__titles">
<text v-if="getTitleLines(item).line1" class="featured-card__name featured-card__name--primary">{{
getTitleLines(item).line1
}}</text>
<text v-if="getTitleLines(item).line2" class="featured-card__name featured-card__name--secondary">{{
getTitleLines(item).line2
}}</text>
</view>
<view
v-if="+item.deliveryService === 1"
class="featured-card__fee"
>
{{ t('pages-store.store.tips5') }} ${{ item.deliveryFee }}{{ t('pages-store.store.start') }} {{ t('pages-store.store.tips5') }} ${{ item.deliveryFee }}{{ t('pages-store.store.start') }}
</view> </view>
<view class="text-24rpx lh-24rpx flex items-center mt-12rpx">
<text class="text-#333 font-500">{{ item.rating }}</text> <view class="featured-card__rating">
<image src="@img/chef/124.png" class="w-24rpx h-24rpx mx-4rpx mt-2rpx"></image> <text class="featured-card__stars"></text>
<text class="text-#7D7D7D">({{ item.commentCount }}) {{ item.deliveryTime }} {{ Number(item.deliveryTime) === 1 ? t('common.day') : t('common.days') }}</text> <text class="featured-card__rate-num">{{ formatMerchantRating(item) }}</text>
</view>
<text class="featured-card__sub">{{ subtitleLine(item) }}</text>
<view class="featured-card__arrow center">
<view class="i-carbon:chevron-right text-32rpx text-white"></view>
</view> </view>
</view> </view>
</template> </view>
<view class="w-30rpx shrink-0 op-0">1</view> <view class="featured-track__pad featured-track__pad--end" />
</view> </view>
</scroll-view> </scroll-view>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.featured-scroll {
width: 100%;
}
.featured-track {
display: flex;
align-items: stretch;
padding-bottom: 4rpx;
}
.featured-track__pad {
flex-shrink: 0;
width: 6rpx;
}
.featured-track__pad--end {
width: 24rpx;
}
.featured-card {
flex-shrink: 0;
display: flex;
flex-direction: row;
width: 620rpx;
height: 256rpx;
min-height: 256rpx;
margin-left: 16rpx;
background: #fff;
border-radius: 24rpx;
overflow: hidden;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
}
.featured-card--first {
margin-left: 0;
}
.featured-card__media {
width: 232rpx;
flex-shrink: 0;
background: #f0f0f0;
display: flex;
flex-direction: column;
position: relative;
overflow: hidden;
}
.featured-card__media-single {
width: 100%;
height: 100%;
min-height: 256rpx;
display: block;
}
.featured-card__media-half {
width: 100%;
flex: 1;
min-height: 0;
display: block;
}
.featured-card__media-half--top {
border-bottom: 3rpx solid #fff;
}
.featured-card__body {
flex: 1;
min-width: 0;
padding: 20rpx 20rpx 18rpx 20rpx;
position: relative;
display: flex;
flex-direction: column;
}
.featured-card__titles {
display: flex;
flex-direction: column;
gap: 6rpx;
}
.featured-card__name {
font-size: 28rpx;
line-height: 34rpx;
font-weight: 600;
color: #1a1a1a;
}
.featured-card__name--primary {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.featured-card__name--secondary {
font-size: 28rpx;
line-height: 34rpx;
color: #1a1a1a;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.featured-card__media-placeholder {
min-height: 256rpx;
background: #e8e8e8;
}
.featured-card__fee {
margin-top: 10rpx;
font-size: 24rpx;
line-height: 30rpx;
font-weight: 500;
color: #d48806;
}
.featured-card__rating {
margin-top: 14rpx;
display: flex;
align-items: center;
gap: 8rpx;
}
.featured-card__stars {
font-size: 22rpx;
line-height: 1;
color: #1a1a1a;
letter-spacing: 2rpx;
}
.featured-card__rate-num {
font-size: 26rpx;
line-height: 30rpx;
font-weight: 600;
color: #1a1a1a;
}
.featured-card__sub {
margin-top: 8rpx;
font-size: 22rpx;
line-height: 28rpx;
color: #999;
}
.featured-card__arrow {
position: absolute;
right: 16rpx;
bottom: 16rpx;
width: 56rpx;
height: 56rpx;
border-radius: 50%;
background: #14181b;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.12);
}
</style> </style>
@@ -61,11 +61,16 @@ function handleCollectionChange(value: boolean) {
</script> </script>
<template> <template>
<view @click="handleClickFood" class="mb-52rpx"> <view @click="handleClickFood" class="mb-52rpx">
<image <view class="food-box-img-wrap">
:src="item?.dishImage?.split(',')[0]||item?.logo" <view v-if="item.isNew == 1" class="dish-new-ribbon">
mode="aspectFill" <text class="dish-new-ribbon__text">NEW</text>
class="w-100% h-400rpx rounded-24rpx bg-common" </view>
></image> <image
:src="item?.dishImage?.split(',')[0]||item?.logo"
mode="aspectFill"
class="w-100% h-400rpx rounded-24rpx bg-common"
></image>
</view>
<view class="flex justify-between items-start mt-14rpx"> <view class="flex justify-between items-start mt-14rpx">
<view> <view>
<text class="text-30rpx lh-30rpx text-#333 font-500 line-clamp-1" <text class="text-30rpx lh-30rpx text-#333 font-500 line-clamp-1"
@@ -86,3 +91,37 @@ function handleCollectionChange(value: boolean) {
</view> </view>
</view> </view>
</template> </template>
<style scoped lang="scss">
.food-box-img-wrap {
position: relative;
overflow: hidden;
border-radius: 24rpx;
}
.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>
@@ -1,5 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { useUserStore } from "@/store"; import { useUserStore } from "@/store";
const props = withDefaults(
defineProps<{
/** 首页顶栏紧凑模式:更小图标与间距 */
compact?: boolean
}>(),
{ compact: false }
)
const emit = defineEmits(['toggleNotOpen']); const emit = defineEmits(['toggleNotOpen']);
const userStore = useUserStore(); const userStore = useUserStore();
function navigateTo(url: string) { function navigateTo(url: string) {
@@ -12,12 +19,25 @@ function navigateTo(url: string) {
</script> </script>
<template> <template>
<view class="flex items-center"> <view class="flex items-center" :class="compact ? 'gap-20rpx' : ''">
<view @click="navigateTo('/pages-user/pages/message/index')" class="w-40rpx h-40rpx mr-42rpx relative"> <view
<view v-if="userStore.isLogin && userStore.unreadMessageCount > 0" class="w-32rpx h-32rpx bg-#E23636 absolute z-2 top--16rpx right--16rpx rounded-50% text-24rpx text-#fff text-center line-height-32rpx">{{ userStore.unreadMessageCount }}</view> @click="navigateTo('/pages-user/pages/message/index')"
<image src="@img/chef/114.png" class="w-40rpx h-40rpx"></image> :class="compact ? 'w-34rpx h-34rpx mr-0' : 'w-40rpx h-40rpx mr-42rpx'"
class="relative shrink-0"
>
<view
v-if="userStore.isLogin && userStore.unreadMessageCount > 0"
:class="compact ? 'h-26rpx top--10rpx right--10rpx text-20rpx px-8rpx' : 'w-32rpx h-32rpx top--16rpx right--16rpx text-24rpx line-height-32rpx'"
class="bg-#E23636 absolute z-2 rounded-50% text-#fff text-center font-500"
>{{ userStore.unreadMessageCount }}</view>
<image src="@img/chef/114.png" :class="compact ? 'w-34rpx h-34rpx' : 'w-40rpx h-40rpx'"></image>
</view> </view>
<image @click="emit('toggleNotOpen')" src="@img/chef/115.png" class="w-40rpx h-40rpx"></image> <image
@click="emit('toggleNotOpen')"
src="@img/chef/115.png"
:class="compact ? 'w-34rpx h-34rpx' : 'w-40rpx h-40rpx'"
class="shrink-0"
></image>
</view> </view>
</template> </template>
@@ -23,12 +23,22 @@ function handleClickSearch() {
</script> </script>
<template> <template>
<view @click="handleClickSearch" class="flex items-center h-88rpx bg-#F2F3F6 rounded-44rpx pl-36rpx"> <view
<image src="@img/chef/100222.png" class="w-28rpx h-28rpx"></image> @click="handleClickSearch"
<text class="text-30rpx text-#434343 ml-16rpx tracking-[.04em] font-500">{{ t('components.search.placeholder') }}</text> class="home-search-bar flex items-center h-88rpx rounded-44rpx pl-36rpx pr-28rpx bg-white"
</view> >
<image src="@img/chef/100222.png" class="w-28rpx h-28rpx shrink-0"></image>
<text class="home-search-placeholder text-28rpx ml-16rpx tracking-[.04em] font-500 truncate">{{
t('components.search.placeholder')
}}</text>
</view>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.home-search-bar {
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.06);
}
.home-search-placeholder {
color: #9a9a9a;
}
</style> </style>
@@ -75,8 +75,11 @@ function selectTab(item: any) {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.2s; transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.2s;
width: 132rpx; width: 102rpx;
height: 132rpx; height: 102rpx;
border-radius: 50%;
background: #fff;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.07);
} }
.img-selected { .img-selected {
border: 4rpx solid #ce7138; border: 4rpx solid #ce7138;
@@ -86,8 +89,8 @@ function selectTab(item: any) {
} }
.tab-img { .tab-img {
width: 112rpx; width: 98rpx;
height: 112rpx; height: 98rpx;
border-radius: 50%; border-radius: 50%;
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1); transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
} }
@@ -25,6 +25,7 @@ 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 { formatSalesCount } from "@/utils/utils";
const configStore = useConfigStore(); const configStore = useConfigStore();
const userStore = useUserStore(); const userStore = useUserStore();
const props = defineProps<{ const props = defineProps<{
@@ -41,6 +42,19 @@ function isSoldOutStock(stockLike: unknown) {
return !Number.isNaN(n) && n <= 0 return !Number.isNaN(n) && n <= 0
} }
/** 底部营销条文案(接口若返回 marketingLabel / hotSaleTag 等则展示) */
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 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()) {
uni.navigateTo({ uni.navigateTo({
@@ -209,6 +223,30 @@ const isShowMerchant = computed(()=> {
} }
}) })
// 精选菜品瀑布流:按序均分到两列(0,2,4… / 1,3,5…),分页追加后仍交错排列
const featuredDishColumns = computed(() => {
const list = dataList.value
return [
list.filter((_, i) => i % 2 === 0),
list.filter((_, i) => i % 2 === 1),
]
})
/** 顶栏购物车角标(多商家购物车汇总件数) */
const cartBadgeTotal = computed(() => {
const list = userStore.userCartAllData
if (!Array.isArray(list) || list.length === 0) return 0
let n = 0
for (const m of list) {
n += (m as { merchantCartVoList?: unknown[] })?.merchantCartVoList?.length || 0
}
return n
})
function goCart() {
navigateTo('/pages-user/pages/cart/index')
}
// 手动触发下拉刷新了 // 手动触发下拉刷新了
function onRefresh() { function onRefresh() {
console.log('手动触发下拉刷新了') console.log('手动触发下拉刷新了')
@@ -221,7 +259,7 @@ function handleClickSwiper(item: any) {
console.log(item, '点击轮播图') console.log(item, '点击轮播图')
switch (Number(item.activityType)) { switch (Number(item.activityType)) {
case 1: // 商家列表 case 1: // 商家列表
navigateTo('/pages-store/pages/list/index?id=') navigateTo('/pages-store/pages/list/index')
break break
case 2: // 活动菜品列表 case 2: // 活动菜品列表
navigateTo('/pages-store/pages/dishes/index?id=' + item.id) navigateTo('/pages-store/pages/dishes/index?id=' + item.id)
@@ -261,7 +299,7 @@ const debouncedEmit = debounce(1300, (isCollected: boolean, id: string, type: Co
<template> <template>
<view <view
class="bg-#fff" class="home-page-root"
:style="[ :style="[
{ {
height: configStore.windowHeight + 'px', height: configStore.windowHeight + 'px',
@@ -271,20 +309,34 @@ const debouncedEmit = debounce(1300, (isCollected: boolean, id: string, type: Co
<z-paging @onRefresh="onRefresh" ref="paging" v-model="dataList" :auto="false" @query="queryList" @scroll="onPageScroll" :refresher-enabled="true" :auto-show-back-to-top="false"> <z-paging @onRefresh="onRefresh" ref="paging" v-model="dataList" :auto="false" @query="queryList" @scroll="onPageScroll" :refresher-enabled="true" :auto-show-back-to-top="false">
<template #top> <template #top>
<status-bar /> <status-bar />
<view class="flex items-center pt-18rpx px-30rpx pb-20rpx"> <!-- 设计稿品牌行 + 右侧地址胶囊消息/客服购物车 -->
<!-- <text class="text-52rpx lh-52rpx text-#333 font-bold shrink-0">{{Config.appName}}</text>--> <view class="home-top-header px-24rpx pt-12rpx pb-8rpx">
<image <view class="flex items-center justify-between gap-12rpx">
src="@img/logo.png" <view class="flex items-center gap-14rpx min-w-0 flex-1">
class="w-52rpx h-52rpx shrink-0" <image src="@img/logo.png" class="w-52rpx h-52rpx shrink-0" style="border-radius: 50%;"></image>
></image> <text class="text-32rpx lh-36rpx text-#1a1a1a font-bold tracking-tight shrink-0">{{ Config.appName }}</text>
<view class="bg-#D8D8D8 w-1rpx h-40rpx mx-14rpx"></view> </view>
<view @click="navigateTo('/pages/address/index')" class="text-#00A76D text-28rpx lh-28rpx flex items-center"> <view class="flex items-center gap-10rpx shrink-0">
<text v-if="userStore.appointmentTimeShow">{{ t('pages.address.appTime') }}:{{ userStore.appointmentTimeShow }}</text> <view class="home-loc-pill" @click="navigateTo('/pages-user/pages/search-address/index')">
<text v-else>{{ t('pages.address.reservation') }}</text> <!-- <image src="@img/chef/101.png" class="home-loc-pill__pin w-22rpx h-22rpx shrink-0"></image> -->
<image <text class="home-loc-pill__text line-clamp-1">{{
src="@img/chef/119.png" userStore.userLocation.location || t('pages.home.default-location')
class="w-24rpx h-24rpx ml-6rpx mt-4rpx shrink-0" }}</text>
></image> <image src="@img/chef/119.png" class="home-loc-pill__arrow w-20rpx h-20rpx shrink-0 op-50 mt-2rpx"></image>
</view>
<view class="home-cart-btn" @click="goCart">
<view class="i-carbon:shopping-cart text-36rpx text-#14181b"></view>
<view v-if="userStore.isLogin && cartBadgeTotal > 0" class="home-cart-badge">{{ cartBadgeTotal > 99 ? '99+' : cartBadgeTotal }}</view>
</view>
</view>
</view>
<view class="home-delivery-actions-row mt-14rpx flex items-center justify-between gap-16rpx">
<view @click="navigateTo('/pages/address/index')" class="home-delivery-row flex items-center min-w-0 flex-1 text-26rpx lh-32rpx text-#00A76D">
<text v-if="userStore.appointmentTimeShow">{{ t('pages.address.appTime') }}: {{ userStore.appointmentTimeShow }}</text>
<text v-else>{{ t('pages.address.reservation') }}</text>
<image src="@img/chef/119.png" class="w-22rpx h-22rpx ml-8rpx shrink-0"></image>
</view>
<msg-box compact class="shrink-0" @toggleNotOpen="toggleNotOpen" />
</view> </view>
</view> </view>
</template> </template>
@@ -294,169 +346,415 @@ const debouncedEmit = debounce(1300, (isCollected: boolean, id: string, type: Co
> >
<home-skeleton /> <home-skeleton />
</view> </view>
<view class="flex-center-sb px-30rpx pt-34rpx"> <view class="px-24rpx pt-12rpx pb-8rpx">
<!--展示用户的定位城市如果用户没有使用定位则展示选择的城市用户选择城市后需要更新定位城市-->
<view @click="navigateTo('/pages-user/pages/search-address/index')" class="flex items-center text-30rpx text-#333 font-500">
<text class="line-clamp-1">
{{ userStore.userLocation.location || t('pages.home.default-location') }}
</text>
<image
src="@img/chef/101.png"
class="w-24rpx h-24rpx ml-10rpx mt-6rpx shrink-0"
></image>
</view>
<view class="shrink-0 ml-40rpx">
<msg-box @toggleNotOpen="toggleNotOpen" />
</view>
</view>
<view class="px-30rpx mt-32rpx pb-22rpx">
<search /> <search />
<!-- 分类滚动区域 --> <!-- 分类标签双行横滑 -->
<view class="mt-40rpx" 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>
<swiper <swiper
class="card-swiper" class="home-promo-swiper card-swiper"
:circular="true" :circular="true"
:autoplay="true" :autoplay="true"
previous-margin="60rpx" previous-margin="48rpx"
next-margin="60rpx" next-margin="48rpx"
> >
<template v-for="item in swiperList" :key="item.id"> <swiper-item
<swiper-item @click="handleClickSwiper(item)" class=""> v-for="(item, sIdx) in swiperList"
<image :key="item.id ?? sIdx"
:src="item.activityImage" @click="handleClickSwiper(item)"
class="swiper-item-content w-full h-100% rounded-24rpx bg-common" >
></image> <image
</swiper-item> :src="item.activityImage"
</template> class="swiper-item-content w-full h-100% rounded-32rpx bg-common"
></image>
</swiper-item>
</swiper> </swiper>
<!-- 分类滚动区域 --> <!-- 快捷入口圆形分类 -->
<tabs-type @changeType="tabsTypeChange" v-if="appMerchantCategoryList.length > 0" :list="appMerchantCategoryList" :currentId="currentCategory" class="mt-22rpx" /> <tabs-type @changeType="tabsTypeChange" v-if="appMerchantCategoryList.length > 0" :list="appMerchantCategoryList" :currentId="currentCategory" class="mt-28rpx home-tabs-quick" />
<!-- 筛选工具 --> <!-- 筛选工具 -->
<!-- <filtrate-tool class="mt-32rpx" @togglePickup="togglePickup" @toggleDiscount="toggleDiscount" @toggleScore="toggleScore" @togglePrice="togglePrice" />--> <!-- <filtrate-tool class="mt-32rpx" @togglePickup="togglePickup" @toggleDiscount="toggleDiscount" @toggleScore="toggleScore" @togglePrice="togglePrice" />-->
<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="mt-56rpx"> <view v-if="featuredList.length > 0" class="featured-merchants-section mt-36rpx px-24rpx pt-8rpx pb-16rpx">
<view <view class="mb-24rpx px-6rpx text-32rpx lh-44rpx text-#1a1a1a">
class="mb-30rpx pl-30rpx text-36rpx lh-36rpx text-#333 font-bold" {{ t('pages.home.featured-on') }}
>{{ t('pages.home.featured-on') }}</view> </view>
<featured-on :list="featuredList" /> <featured-on :list="featuredList" />
</view> </view>
<!-- Nearby Merchants 附近商家 --> <!-- Nearby Merchants 附近商家 -->
<view v-if="nearbyList.length > 0" class="mt-56rpx"> <view v-if="nearbyList.length > 0" class="nearby-merchants-block mt-36rpx px-24rpx pb-16rpx">
<view <view class="mb-24rpx px-6rpx text-32rpx lh-44rpx text-#1a1a1a ">
class="mb-32rpx pl-30rpx text-36rpx lh-36rpx text-#333 font-bold" {{ t('pages.home.nearby-merchants') }}
>{{ t('pages.home.nearby-merchants') }}</view> </view>
<nearby-merchants :list="nearbyList" /> <nearby-merchants :list="nearbyList" />
</view> </view>
</view> </view>
<!-- List --> <!-- List 精选菜品瀑布流浅底 + 白卡片 + 阴影结构对齐设计稿 -->
<view class="mt-56rpx px-30rpx"> <view class="featured-dishes-section mt-36rpx px-24rpx pb-40rpx">
<view class="mb-32rpx text-36rpx lh-36rpx text-#333 font-bold" <view class="mb-24rpx px-6rpx text-32rpx lh-44rpx text-#1a1a1a "
>{{ t('pages.home.featured-dishes') }}</view> >{{ t('pages.home.featured-dishes') }}</view>
<template v-for="(item, index) in dataList" :key="index"> <view class="waterfall-row flex gap-16rpx items-start">
<view @click="navigateToDishes(item)" class="w-100% mb-30rpx"> <view
<view class="relative h-448rpx rounded-24rpx mb-28rpx"> v-for="(col, colIndex) in featuredDishColumns"
<view @click.stop="handleDishCollectionClick(item)" class="w-68rpx h-68rpx absolute z-2 top-0 right-0"> :key="colIndex"
class="waterfall-col flex-1 min-w-0 flex flex-col gap-16rpx"
>
<view
v-for="item in col"
:key="item.id || String(item.merchantId) + '-' + item.dishName"
@click="navigateToDishes(item)"
class="featured-dish-card w-full"
>
<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 <image
v-if="!item.isCollect" :src="item?.dishImage?.split(',')[0]"
src="@img-store/1334.png"
mode="aspectFill" mode="aspectFill"
class="w-full h-full" class="featured-dish-img"
/>
<view
v-if="isSoldOutStock(item?.stock)"
class="featured-dish-sold-dim"
/> />
<image <image
v-else v-if="isSoldOutStock(item?.stock)"
src="@img-store/1337.png" src="/static/app/images/SoldOut.png"
mode="aspectFill"
class="w-full h-full"
/>
</view>
<view
v-if="isSoldOutStock(item?.stock)"
class="absolute z-2 left-20rpx top-20rpx px-16rpx h-52rpx rounded-26rpx bg-#14181B/70 center text-26rpx text-#fff font-500"
>
已售完
</view>
<image
:src="item?.dishImage?.split(',')[0]"
mode="aspectFill" mode="aspectFill"
class="w-full h-full rounded-24rpx bg-common" class="featured-dish-sold-overlay"
/> />
</view> <view
<view class="line-clamp-1 text-30rpx text-#333 font-500"> @click.stop="handleDishCollectionClick(item)"
{{ item?.dishName }} class="featured-dish-collect w-56rpx h-56rpx absolute z-4 top-12rpx right-12rpx center"
</view> >
<view class="flex-center-sb mt-12rpx"> <image
<text class="text-32rpx lh-30rpx text-#333 font-500">US${{ item?.discountPrice }}</text> v-if="!item.isCollect"
<view src="@img-store/1334.png"
v-if="Number(item?.memberPrice) > 0" mode="aspectFill"
class="member-price-tag text-[#FBE3C3] font-500 text-30rpx lh-30rpx center pl-6rpx break-all" class="w-44rpx h-44rpx featured-dish-collect-icon"
> />
<text>{{ t('pages-store.store.members') }}: </text> <image
${{ item?.memberPrice }} v-else
src="@img-store/1337.png"
mode="aspectFill"
class="w-44rpx h-44rpx featured-dish-collect-icon"
/>
</view>
</view> </view>
</view> <view class="featured-dish-body">
<view class="flex-center-sb mt-12rpx"> <view class="featured-dish-meta flex items-start justify-between gap-12rpx mb-14rpx">
<view class="text-28rpx text-#999"> <view class="min-w-0 flex-1">
<view class="line-through">US${{ item?.originalPrice }}</view> <text class="featured-dish-price">US${{ getFeaturedDishDisplayPrice(item) }}</text>
<view>{{ t('pages-store.store.sales') }}:{{ item?.salesCount }}</view> <!-- <text
</view> v-if="Number(item?.originalPrice) > Number(item?.discountPrice)"
class="featured-dish-original"
<view class="center w-64rpx h-64rpx rounded-50% bg-white shadow-lg"> >US${{ item?.originalPrice }}</text> -->
<image </view>
src="@img/chef/1285.png" <text class="featured-dish-sales shrink-0">{{ t('pages-store.store.sales') }}: {{ formatSalesCount(item?.salesCount) }}</text>
class="w-30rpx h-30rpx shrink-0" </view>
></image> <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 as Record<string, unknown>)"
class="featured-dish-promo mt-16rpx"
>
<text class="featured-dish-promo-text">{{ getDishPromoLabel(item as Record<string, unknown>) }}</text>
</view>
</view> </view>
</view> </view>
</view> </view>
</template> </view>
</view> </view>
<template #bottom> <template #bottom>
<view class="h-50px"></view> <view class="h-50px"></view>
<view :style="[configStore.iosSafeBottomPlaceholder]"></view> <view :style="[configStore.iosSafeBottomPlaceholder]"></view>
</template> </template>
</z-paging> </z-paging>
<view v-if="userStore.isLogin && userStore.userCartAllData.length > 0" @click="navigateTo('/pages-user/pages/cart/index')" class="fixed bottom-138rpx left-50% translate-x--50% px-26rpx h-88rpx bg-#14181B rounded-44rpx center text-28rpx text-#fff font-500"> <!-- 回到顶部购物车已并入顶栏不再使用底部浮条 -->
<image src="@img/chef/125.png" class="w-28rpx h-28rpx shrink-0"></image> <view v-if="showBackToTop" @click="scrollToTop" class="home-back-top fixed left-30rpx w-88rpx h-88rpx bg-#14181B rounded-50% center shadow-lg">
<view class="ml-10rpx whitespace-nowrap">{{ userStore.userCartAllData[0]?.merchantName }}</view>
<view class="w-8rpx h-8rpx rounded-50% bg-white mx-8rpx"></view>
<text>{{ userStore.userCartAllData[0]?.merchantCartVoList?.length || 0 }}</text>
</view>
<!-- 回到顶部按钮 -->
<view v-if="showBackToTop" @click="scrollToTop" class="fixed bottom-148rpx left-30rpx w-88rpx h-88rpx bg-#14181B rounded-50% center shadow-lg">
<image src="@img/chef/119.png" class="w-40rpx h-40rpx shrink-0 rotate-180"></image> <image src="@img/chef/119.png" class="w-40rpx h-40rpx shrink-0 rotate-180"></image>
</view> </view>
</view> </view>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.home-page-root {
background: #f2f2f2;
}
.home-top-header {
background: #f2f2f2;
}
.home-loc-pill {
display: flex;
align-items: center;
gap: 8rpx;
max-width: 200rpx;
padding: 12rpx 18rpx;
background: #fff;
border-radius: 999rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
}
.home-loc-pill__text {
flex: 1;
min-width: 0;
font-size: 22rpx;
line-height: 28rpx;
font-weight: 500;
color: #333;
}
.home-cart-btn {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 72rpx;
height: 72rpx;
flex-shrink: 0;
background: #fff;
border-radius: 50%;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.07);
}
.home-cart-badge {
position: absolute;
top: 4rpx;
right: 4rpx;
min-width: 28rpx;
height: 28rpx;
padding: 0 6rpx;
font-size: 18rpx;
line-height: 28rpx;
font-weight: 600;
color: #fff;
text-align: center;
background: #e23636;
border-radius: 999rpx;
}
.home-delivery-row {
padding-left: 2rpx;
}
.home-promo-swiper {
margin-top: 8rpx;
}
.home-tabs-quick {
padding-left: 8rpx;
padding-right: 8rpx;
}
.nearby-merchants-block {
background: #f2f2f2;
}
/* 为底栏 Tab 预留空间,避免被遮挡 */
.home-back-top {
bottom: calc(100rpx + env(safe-area-inset-bottom, 0px));
}
.card-swiper { .card-swiper {
height: 420rpx; height: 400rpx;
} }
.swiper-item-content { .swiper-item-content {
width: 100%; width: 100%;
height: 100%; height: 100%;
transform: scale(0.95); transform: scale(0.94);
border-radius: 20rpx; border-radius: 32rpx;
transition: transform 0.3s; box-shadow: 0 8rpx 28rpx rgba(0, 0, 0, 0.08);
transition: transform 0.3s ease;
} }
.swiper-item-active .swiper-item-content { .swiper-item-active .swiper-item-content {
transform: scale(1); transform: scale(1);
} }
.member-price-tag {
min-width: 220rpx; /* 精选商家 / 精选菜品 区块(与页面背景统一) */
height: 42rpx; .featured-merchants-section,
background-image: url("/static/images/chef/1282.png"); .featured-dishes-section {
background-size: 100% 100%; background: #f2f2f2;
background-repeat: no-repeat; }
.featured-dish-collect-icon {
filter: drop-shadow(0 2rpx 6rpx rgba(0, 0, 0, 0.18));
}
/* 瀑布流商品图:固定 1:1 区域,便于「已售完」等角标统一 */
.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(20, 24, 27, 0.42);
pointer-events: none;
}
.featured-dish-sold-overlay {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
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-original {
margin-left: 10rpx;
color: #b3b3b3;
font-size: 22rpx;
font-weight: 400;
text-decoration: line-through;
vertical-align: baseline;
}
.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> </style>
@@ -194,7 +194,7 @@ const poster = computed(() => ({
type: 'view', type: 'view',
}, },
{ {
text: 'http://192.168.5.118:8011/h5/'+ `pages-login/pages/sign-up/index?invitationCode=${userStore.userInfo.invitationCode}`, text: 'http://www.howhowfresh.com/h5/'+ `pages-login/pages/sign-up/index?invitationCode=${userStore.userInfo.invitationCode}`,
type: 'qrcode', type: 'qrcode',
css: { css: {
width: '124rpx', width: '124rpx',
@@ -83,10 +83,16 @@ defineExpose({
</view> </view>
<view class="shrink-0 ml-20rpx" v-if="userStore.userInfo.invitationCode"> <view class="shrink-0 ml-20rpx" v-if="userStore.userInfo.invitationCode">
<uqrcode ref="uqrcode" canvas-id="qrcode" <uqrcode ref="uqrcode" canvas-id="qrcode"
:value="'http://192.168.5.118:8011/h5/' + `pages-login/pages/sign-up/index?invitationCode=${userStore.userInfo.invitationCode}`" :value="'http://www.howhowfresh.com/h5/' + `pages-login/pages/sign-up/index?invitationCode=${userStore.userInfo.invitationCode}`"
:size="124" :size="124"
sizeUnit="rpx" sizeUnit="rpx"
:options="{}"></uqrcode> :options="{}"></uqrcode>
<!-- <uqrcode ref="uqrcode" canvas-id="qrcode"
:value="'http://192.168.5.118:8011/h5/' + `pages-login/pages/sign-up/index?invitationCode=${userStore.userInfo.invitationCode}`"
:size="124"
sizeUnit="rpx"
:options="{}"></uqrcode> -->
</view> </view>
</view> </view>
</view> </view>
@@ -499,8 +499,8 @@ margin: 0 30rpx;
} }
&__icon { &__icon {
width: 44rpx; width: 38rpx;
height: 44rpx; height: 38rpx;
flex-shrink: 0; flex-shrink: 0;
} }
@@ -3,10 +3,11 @@ import {
appMerchantOrderOrderListPost, appMerchantOrderOrderListPost,
type MerchantOrderVo type MerchantOrderVo
} from "@/service"; } from "@/service";
import {callPhone, formatTimestampWithWeekday} from "@/utils/utils"; import {callPhone} from "@/utils/utils";
import {OrderCancelStatus, OrderStatus} from "@/constant/enums"; import {OrderCancelStatus, OrderStatus} from "@/constant/enums";
import {useUserStore} from "@/store"; import {useUserStore} from "@/store";
import HomeSkeleton from "@/pages/home/components/tabbar-home/components/home-skeleton.vue"; import {dayjs} from "@/plugin";
const userStore = useUserStore(); const userStore = useUserStore();
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps<{ const props = defineProps<{
@@ -31,7 +32,7 @@ const {paging, dataList, loading, queryList, firstLoaded} = usePage<MerchantOrde
}, },
body: { body: {
userPort: 1, userPort: 1,
orderStatusList: props.status ? [Number(props.status + 1)] : [], orderStatusList: props.status !== null && props.status !== '' ? [Number(props.status)] : [],
} }
}) })
} else { } else {
@@ -41,7 +42,118 @@ const {paging, dataList, loading, queryList, firstLoaded} = usePage<MerchantOrde
} }
}) })
function handleClick(item: any) { 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 {
if (input == null || input === '') return null
let value: number
if (typeof input === 'string') {
const trimmed = input.trim()
if (!trimmed) return null
value = Number(trimmed)
} else {
value = Number(input)
}
if (!Number.isFinite(value) || value <= 0) return null
// 兼容后端可能返回秒级时间戳
if (value < 1e12) {
value = value * 1000
}
return value
}
function formatOrderCardTime(item: MerchantOrderVo) {
// 优先使用下单时间 createTime;没有则回退到预约结束时间
const ts = normalizeTimestamp((item as any)?.createTime ?? item?.endScheduledTime)
if (!ts) return '--'
return dayjs(ts).format('YYYY/MM/DD HH:mm')
}
function formatOrderPrice(item: MerchantOrderVo) {
const raw = item?.paidAmount ?? item?.actualPrice ?? 0
if (raw == null || raw === '') return '0.00'
if (typeof raw === 'number') return raw.toFixed(2)
const str = String(raw).trim()
if (!str) return '0.00'
if (!str.includes('.')) return `${str}.00`
const [intPart, decimalPart = ''] = str.split('.')
return `${intPart}.${(decimalPart + '00').slice(0, 2)}`
}
function getOrderDishes(item: MerchantOrderVo): Array<Record<string, any>> {
const list = item.merchantOrderDishVoList as unknown as Array<Record<string, any>> | null | undefined
if (!list || !Array.isArray(list)) return []
return list
}
function dishCover(dish: Record<string, any>) {
const img = dish?.merchantDishVo?.dishImage
if (!img || typeof img !== 'string') return ''
return img.split(',')[0] || ''
}
function dishTitle(dish: Record<string, any>) {
return dish?.merchantDishVo?.dishName ?? ''
}
function getTotalDishCount(item: MerchantOrderVo) {
return getOrderDishes(item).reduce((s, d) => s + (d.count || 0), 0)
}
function getTotalDishCountText(item: MerchantOrderVo) {
return fillI18nParams(t('pages.order.totalItemCount'), {
count: getTotalDishCount(item),
})
}
function isSameCode(value: unknown, code: unknown) {
return String(value ?? '') === String(code ?? '')
}
function isRefundApplied(item: MerchantOrderVo) {
return isSameCode(item.refundStatus, OrderCancelStatus.APPLIED)
}
function isRefundApproved(item: MerchantOrderVo) {
return isSameCode(item.refundStatus, OrderCancelStatus.APPROVED)
}
function isRefundRejected(item: MerchantOrderVo) {
return isSameCode(item.refundStatus, OrderCancelStatus.REJECTED)
}
function getOrderStatusText(item: MerchantOrderVo) {
if (isRefundApplied(item)) return t('pages-store.order.orderStatus.refund')
if (isRefundApproved(item)) return t('pages-store.order.orderStatus.agreeRefund')
if (isRefundRejected(item)) return t('pages-store.order.orderStatus.rejectRefund')
if (isSameCode(item.orderStatus, OrderStatus.CANCELLED)) return t('pages-store.order.orderStatus.cancelled')
if (isSameCode(item.orderStatus, OrderStatus.PENDING_PAYMENT)) return t('pages-store.order.orderStatus.pendingPayment')
if (isSameCode(item.orderStatus, OrderStatus.HAS_PENDING_PAYMENT)) return t('pages-store.order.orderStatus.hasPendingPayment')
if (isSameCode(item.orderStatus, OrderStatus.MERCHANT_ACCEPTED)) return t('pages-store.order.orderStatus.received')
if (isSameCode(item.receiveMethod, 1) && isSameCode(item.orderStatus, OrderStatus.DELIVERING)) return t('pages-store.order.orderStatus.delivering')
if (isSameCode(item.orderStatus, OrderStatus.COMPLETED)) return t('pages-store.order.orderStatus.delivered')
if (isSameCode(item.orderStatus, OrderStatus.MERCHANT_REJECTED)) return t('pages-store.store.orderStatus.rejected')
return '--'
}
function handleClick(item: MerchantOrderVo) {
uni.navigateTo({
url: '/pages-store/pages/order/index?id=' + item.id
})
}
function handleCancelClick(item: MerchantOrderVo) {
uni.navigateTo({ uni.navigateTo({
url: '/pages-store/pages/order/index?id=' + item.id url: '/pages-store/pages/order/index?id=' + item.id
}) })
@@ -66,121 +178,132 @@ defineExpose({
</script> </script>
<template> <template>
<view class="h-full"> <view class="list-root h-full">
<z-paging ref="paging" v-model="dataList" @query="queryList" :fixed="false" :auto="false"> <z-paging ref="paging" v-model="dataList" @query="queryList" :fixed="false" :auto="false">
<view class="p-30rpx"> <view class="page-pad">
<view <view
v-show="loading" v-show="loading"
class="animate-in fade-in animate-ease-out animate-duration-300" class="animate-in fade-in animate-ease-out animate-duration-300"
> >
<template v-for="item in 3"> <view v-for="sk in 3" :key="'sk-' + sk" class="order-card order-card--skeleton mb-24rpx">
<view class="w-full px-50rpx pt-43rpx pb-36rpx bg-white rounded-36rpx mb-30rpx last:mb-0"> <view class="flex-center-sb mb-24rpx">
<!-- 头部时间和状态骨架屏 --> <view class="w-280rpx h-32rpx skeleton-item rounded-8rpx"></view>
<view class="flex-center-sb mb-28rpx"> <view class="w-120rpx h-32rpx skeleton-item rounded-8rpx"></view>
<view class="w-280rpx h-34rpx skeleton-item rounded-8rpx"></view>
<view class="w-120rpx h-34rpx skeleton-item rounded-8rpx"></view>
</view> </view>
<view class="flex items-center mb-24rpx">
<!-- 商家信息骨架屏 -->
<view class="flex items-center my-28rpx">
<view class="w-32rpx h-32rpx skeleton-item rounded-6rpx mr-10rpx"></view> <view class="w-32rpx h-32rpx skeleton-item rounded-6rpx mr-10rpx"></view>
<view class="w-200rpx h-30rpx skeleton-item rounded-6rpx"></view> <view class="w-240rpx h-28rpx skeleton-item rounded-6rpx"></view>
<view class="w-60rpx h-32rpx skeleton-item rounded-6rpx ml-20rpx"></view> <view class="w-72rpx h-28rpx skeleton-item rounded-20rpx ml-16rpx"></view>
</view> </view>
<view class="flex gap-16rpx mb-24rpx">
<!-- 轮播图骨架屏 --> <view v-for="i in 3" :key="i" class="sk-thumb">
<view class="mb-30rpx"> <view class="sk-thumb-img skeleton-item"></view>
<view class="w-100% h-508rpx skeleton-item rounded-36rpx mb-10rpx"></view> <view class="w-100rpx h-22rpx skeleton-item rounded-4rpx mt-10rpx"></view>
<view class="w-180rpx h-30rpx skeleton-item rounded-6rpx"></view> </view>
</view> <view class="flex-1"></view>
<view class="w-100rpx flex flex-col items-end gap-12rpx pt-8rpx">
<!-- 底部按钮区域骨架屏 --> <view class="w-88rpx h-30rpx skeleton-item rounded-4rpx"></view>
<view class="flex-center-sb"> <view class="w-72rpx h-22rpx skeleton-item rounded-4rpx"></view>
<view class="w-56rpx h-56rpx skeleton-item rounded-28rpx"></view>
<view class="flex items-center gap-20rpx">
<view class="w-170rpx h-64rpx skeleton-item rounded-64rpx"></view>
<view class="w-170rpx h-64rpx skeleton-item rounded-64rpx"></view>
</view> </view>
</view> </view>
</view> <view class="flex justify-end">
</template> <view class="sk-pill skeleton-item"></view>
</view>
</view>
</view> </view>
<view <view
class="animate-in fade-in animate-ease-in animate-duration-300" class="animate-in fade-in animate-ease-in animate-duration-300"
v-if="!loading" v-if="!loading"
> >
<template v-for="(item,index) in dataList"> <view
<view @click="handleClick(item)" :class="[index === 0 ? '' : 'mt-30rpx']" class="w-full px-50rpx pt-43rpx pb-36rpx bg-white rounded-36rpx"> v-for="(item,index) in dataList"
<view class="flex-center-sb text-34rpx lh-34rpx font-bold"> :key="item.id"
<text class="text-#333 tracking-[.04em]">{{ formatTimestampWithWeekday(item.endScheduledTime) }}</text> :class="[index === 0 ? '' : 'mt-24rpx']"
<text class="text-#00A76D tracking-[.04em] shrink-0"> class="order-card"
<template v-if=" @click="handleClick(item)"
+item.refundStatus === OrderCancelStatus.APPLIED || >
+item.refundStatus === OrderCancelStatus.APPROVED || <view class="flex-center-sb items-start gap-16rpx">
+item.refundStatus === OrderCancelStatus.REJECTED <text class="order-time">{{ formatOrderCardTime(item) }}</text>
"> <text class="order-status shrink-0" :class="{ 'text-#FF6106': isSameCode(item.orderStatus, OrderStatus.MERCHANT_REJECTED) }">
<template v-if="+item.refundStatus === OrderCancelStatus.APPLIED"> {{ getOrderStatusText(item) }}
{{ t('pages-store.order.orderStatus.refund') }}
</template>
<template v-else-if="+item.refundStatus === OrderCancelStatus.APPROVED">
{{ t('pages-store.order.orderStatus.agreeRefund') }}
</template>
<template v-else-if="+item.refundStatus === OrderCancelStatus.REJECTED">
{{ t('pages-store.order.orderStatus.rejectRefund') }}
</template>
</template>
<template v-else>
<template v-if="item.orderStatus === OrderStatus.CANCELLED">{{ t('pages-store.order.orderStatus.cancelled') }}</template>
<template v-if="item.orderStatus === OrderStatus.PENDING_PAYMENT">{{ t('pages-store.order.orderStatus.pendingPayment') }}</template>
<template v-if="item.orderStatus === OrderStatus.HAS_PENDING_PAYMENT">{{ t('pages-store.order.orderStatus.hasPendingPayment') }}</template>
<!--配送订单-->
<template v-if="item.orderStatus === OrderStatus.MERCHANT_ACCEPTED">{{ t('pages-store.order.orderStatus.received') }}</template>
<template v-if="+item.receiveMethod === 1 && item.orderStatus === OrderStatus.DELIVERING">{{ t('pages-store.order.orderStatus.delivering') }}</template>
<template v-if="item.orderStatus === OrderStatus.COMPLETED">{{ t('pages-store.order.orderStatus.delivered') }}</template>
<!--商家拒绝接单-->
<template v-if="item.orderStatus === OrderStatus.MERCHANT_REJECTED">
<text class="text-#FF6106">{{ t('pages-store.store.orderStatus.rejected') }}</text>
</template>
</template>
</text> </text>
</view> </view>
<view class="text-30rpx lh-30rpx text-#333 font-500 flex items-center my-28rpx ">
<image src="@img/chef/126.png" class="w-32rpx h-32rpx mr-10rpx"></image> <view class="store-row">
{{ item.merchantVo.merchantName }} <image src="@img/chef/126.png" class="store-icon" mode="aspectFit"></image>
<!--收货方式(1-派送 2-自取)--> <text class="store-name line-clamp-1">{{ item.merchantVo?.merchantName }}</text>
<view v-if="+item.receiveMethod === 1" class="bg-#FF6106 rounded-6rpx h-32rpx text-#fff text-22rpx center px-10rpx ml-20rpx"> <view v-if="isSameCode(item.receiveMethod, 1)" class="recv-tag recv-tag--del">
{{ t('pages.order.DEL') }} {{ t('pages.order.DEL') }}
</view> </view>
<view v-if="+item.receiveMethod === 2" class="bg-#00A76D rounded-6rpx h-32rpx text-#fff text-22rpx center px-10rpx ml-20rpx"> <view v-if="isSameCode(item.receiveMethod, 2)" class="recv-tag recv-tag--pu">
{{ t('pages.order.PU') }} {{ t('pages.order.PU') }}
</view> </view>
</view> </view>
<swiper class="h-568rpx mb-30rpx" :autoplay="true">
<swiper-item v-for="img in item.merchantOrderDishVoList"> <view class="goods-row">
<image <scroll-view scroll-x class="goods-scroll" :show-scrollbar="false" :enable-flex="true">
:src="img.merchantDishVo.dishImage?.split(',')[0]" <view class="goods-track">
mode="aspectFill" <view
class="w-100% h-508rpx rounded-36rpx bg-common" v-for="(dish, di) in getOrderDishes(item)"
></image> :key="dish.id ?? di"
<view class="mt-10rpx text-30rpx font-500 line-clamp-1">{{ img.merchantDishVo.dishName }}</view> class="goods-cell"
</swiper-item> >
</swiper> <view class="goods-img-wrap">
<view class="flex-center-sb"> <image
<view> :src="dishCover(dish)"
<image v-if="+item.receiveMethod === 2" src="@img/chef/127.png" class="w-56rpx h-56rpx"></image> mode="aspectFill"
<image @click.stop="callPhone(item.merchantVo.phone)" v-if="item.orderStatus >= OrderStatus.MERCHANT_ACCEPTED && +item.receiveMethod === 1" src="@img/chef/128.png" class="w-56rpx h-56rpx"></image> class="goods-img"
/>
<view
v-if="dish.count > 1"
class="goods-qty"
>x{{ dish.count }}</view>
</view>
<text class="goods-caption">{{ dishTitle(dish) }}</text>
</view>
</view>
</scroll-view>
<view class="price-block shrink-0">
<text class="price-num">${{ formatOrderPrice(item) }}</text>
<text class="price-meta">{{ getTotalDishCountText(item) }}</text>
</view> </view>
<view class="flex items-center gap-20rpx font-500"> </view>
<template v-if="item.refundStatus !== OrderCancelStatus.APPLIED && item.refundStatus !== OrderCancelStatus.APPROVED">
<view v-if="item.orderStatus !== OrderStatus.CANCELLED && +item.orderStatus !== OrderStatus.COMPLETED && item.orderStatus !== OrderStatus.MERCHANT_REJECTED" class="w-170rpx h-64rpx center rounded-64rpx border-solid border-#666666 border-1rpx">{{ t('common.cancel') }}</view> <view class="action-row" @click.stop>
<view v-if="+item.receiveMethod === 2 && +item.orderStatus === OrderStatus.MERCHANT_ACCEPTED" class="w-170rpx h-64rpx center rounded-64rpx bg-#14181B text-28rpx text-#fff">{{ t('pages-store.order.writeOff') }}</view> <view class="action-left">
<view v-if="+item.orderStatus === OrderStatus.COMPLETED && item?.dishReviewVoList.length === 0" class="w-170rpx h-64rpx center rounded-64rpx bg-#14181B text-28rpx text-#fff">{{ t('common.evaluate') }}</view> <image
v-if="isSameCode(item.receiveMethod, 2) && isSameCode(item.orderStatus, OrderStatus.MERCHANT_ACCEPTED)"
src="@img/chef/127.png"
class="action-icon"
/>
<image
v-if="!isSameCode(item.orderStatus, OrderStatus.PENDING_PAYMENT) && !isSameCode(item.orderStatus, OrderStatus.CANCELLED) && isSameCode(item.receiveMethod, 1)"
src="@img/chef/128.png"
class="action-icon"
@click="callPhone(item.merchantVo?.phone)"
/>
</view>
<view class="action-btns">
<template v-if="!isRefundApplied(item) && !isRefundApproved(item)">
<view
v-if="!isSameCode(item.orderStatus, OrderStatus.CANCELLED) && !isSameCode(item.orderStatus, OrderStatus.COMPLETED) && !isSameCode(item.orderStatus, OrderStatus.MERCHANT_REJECTED)"
class="btn-outline"
@click="handleCancelClick(item)"
>{{ t('pages.order.cancelOrder') }}</view>
<view
v-if="isSameCode(item.receiveMethod, 2) && isSameCode(item.orderStatus, OrderStatus.MERCHANT_ACCEPTED)"
class="btn-primary"
@click="handleClick(item)"
>{{ t('pages-store.order.writeOff') }}</view>
<view
v-if="isSameCode(item.orderStatus, OrderStatus.COMPLETED) && item?.dishReviewVoList?.length === 0"
class="btn-primary"
@click="handleClick(item)"
>{{ t('common.evaluate') }}</view>
</template> </template>
</view> </view>
</view> </view>
</view> </view>
</template>
</view> </view>
</view> </view>
</z-paging> </z-paging>
@@ -188,5 +311,233 @@ defineExpose({
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.list-root {
background: #F5F5F5;
}
.page-pad {
padding: 24rpx 24rpx 32rpx;
}
.order-card {
background: #fff;
border-radius: 24rpx;
padding: 28rpx 24rpx 24rpx;
box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.04);
}
.order-card--skeleton {
box-shadow: none;
}
.order-time {
font-size: 28rpx;
line-height: 36rpx;
color: #14181B;
font-weight: 500;
}
.order-status {
font-size: 28rpx;
line-height: 36rpx;
color: #00A76D;
font-weight: 500;
}
.store-row {
display: flex;
align-items: center;
margin-top: 20rpx;
gap: 0;
}
.store-icon {
width: 32rpx;
height: 32rpx;
margin-right: 10rpx;
flex-shrink: 0;
}
.store-name {
flex: 1;
min-width: 0;
font-size: 28rpx;
line-height: 36rpx;
color: #14181B;
font-weight: 500;
}
.recv-tag {
flex-shrink: 0;
margin-left: 16rpx;
padding: 4rpx 14rpx;
border-radius: 8rpx;
font-size: 22rpx;
line-height: 28rpx;
color: #fff;
font-weight: 500;
}
.recv-tag--del {
background: #FF6106;
}
.recv-tag--pu {
background: #00A76D;
}
.goods-row {
display: flex;
align-items: flex-start;
margin-top: 24rpx;
gap: 16rpx;
}
.goods-scroll {
flex: 1;
min-width: 0;
align-self: flex-start;
height: auto;
}
.goods-track {
display: flex;
flex-direction: row;
gap: 16rpx;
padding-right: 8rpx;
}
.goods-cell {
width: 128rpx;
flex-shrink: 0;
}
.goods-img-wrap {
position: relative;
width: 128rpx;
height: 128rpx;
border-radius: 16rpx;
overflow: hidden;
background: #F2F2F2;
}
.goods-img {
width: 100%;
height: 100%;
}
.goods-qty {
position: absolute;
right: 6rpx;
bottom: 6rpx;
min-width: 36rpx;
padding: 4rpx 10rpx;
background: rgba(20, 24, 27, 0.85);
color: #fff;
font-size: 20rpx;
line-height: 24rpx;
border-radius: 8rpx;
text-align: center;
}
.goods-caption {
display: block;
margin-top: 10rpx;
font-size: 22rpx;
line-height: 28rpx;
color: #333;
min-height: 56rpx;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
word-break: break-word;
}
.price-block {
text-align: right;
padding-top: 8rpx;
max-width: 200rpx;
}
.price-num {
display: block;
font-size: 32rpx;
line-height: 40rpx;
font-weight: 700;
color: #14181B;
}
.price-meta {
display: block;
margin-top: 8rpx;
font-size: 22rpx;
line-height: 28rpx;
color: #999;
}
.action-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 24rpx;
}
.action-left {
display: flex;
align-items: center;
gap: 16rpx;
}
.action-icon {
width: 56rpx;
height: 56rpx;
}
.action-btns {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 16rpx;
flex-wrap: wrap;
}
.btn-outline {
padding: 16rpx 32rpx;
border-radius: 999rpx;
border: 2rpx solid #14181B;
font-size: 26rpx;
font-weight: 500;
color: #14181B;
background: #fff;
line-height: 32rpx;
}
.btn-primary {
padding: 16rpx 32rpx;
border-radius: 999rpx;
background: #14181B;
font-size: 26rpx;
font-weight: 500;
color: #fff;
line-height: 32rpx;
}
.sk-thumb {
flex-shrink: 0;
width: 128rpx;
}
.sk-thumb-img {
width: 128rpx;
height: 128rpx;
border-radius: 16rpx;
}
.sk-pill {
width: 176rpx;
height: 60rpx;
border-radius: 9999rpx;
}
</style> </style>
@@ -67,32 +67,30 @@ defineExpose({
<z-paging-swiper> <z-paging-swiper>
<template #top> <template #top>
<status-bar/> <status-bar/>
<view class="pl-30rpx pt-18rpx text-56rpx lh-56rpx text-#333 font-bold tracking-[.04em]">{{ t('pages.order.title') }}</view> <!-- <view class="pl-30rpx pt-18rpx text-56rpx lh-56rpx text-#333 font-bold tracking-[.04em]">{{ t('pages.order.title') }}</view> -->
<view class="tab pl-20rpx mt-24rpx"> <view class="tab mt-16rpx bg-white">
<wd-tabs <wd-tabs
slidable="always" slidable="always"
key="tab" key="tab"
color="#333" color="#14181B"
inactiveColor="#999" inactiveColor="#B0B0B0"
:line-height="3" :line-height="6"
:line-width="30" :line-width="48"
v-model="currentIndex" v-model="currentIndex"
@click="handleClickTab" @click="handleClickTab"
> >
<template v-for="(item, index) in tabList" :key="index"> <wd-tab v-for="(item, index) in tabList" :key="index" :title="item.name"></wd-tab>
<wd-tab :title="item.name"></wd-tab>
</template>
</wd-tabs> </wd-tabs>
<view class="px-20rpx mt--2rpx"> <!-- <view class="px-20rpx mt--2rpx">
<view class="border-b-solid border-b-4rpx border-b-#999"></view> <view class="border-b-solid border-b-4rpx border-b-#999"></view>
</view> </view> -->
</view> </view>
</template> </template>
<swiper class="h-full" <swiper class="h-full order-swiper"
:current="currentIndex" :current="currentIndex"
@change="handleSwiperChange"> @change="handleSwiperChange">
<swiper-item class="swiper-item" v-for="(item, index) in tabList" :key="index"> <swiper-item class="swiper-item" v-for="(item, index) in tabList" :key="index">
<order-swiper-list ref="orderSwiperListRef" :currentIndex="currentIndex" :status="index"></order-swiper-list> <order-swiper-list ref="orderSwiperListRef" :currentIndex="currentIndex" :status="item.value"></order-swiper-list>
</swiper-item> </swiper-item>
</swiper> </swiper>
<template #bottom> <template #bottom>
@@ -106,22 +104,44 @@ defineExpose({
<style scoped lang="scss"> <style scoped lang="scss">
.tab { .tab {
:deep(.wd-tabs) { :deep(.wd-tabs) {
background: #fff !important;
.wd-tabs__line { .wd-tabs__line {
bottom: 0 !important; bottom: 0 !important;
z-index: 99 !important; z-index: 99 !important;
border-radius: 0 !important; border-radius: 2rpx !important;
background: #14181B !important;
} }
.wd-tabs__nav-item { .wd-tabs__nav-item {
padding: 0 22rpx !important; padding: 0 !important;
flex: 1 1 0 !important;
display: flex !important;
justify-content: center !important;
} }
.wd-tabs__nav-item-text{ .wd-tabs__nav-item-text {
font-size: 32rpx !important; font-size: 26rpx !important;
text-overflow: unset !important; text-overflow: unset !important;
font-family: 'UberMove', sans-serif !important; font-family: 'UberMove', sans-serif !important;
// font-weight: 500 !important;
width: 100%;
text-align: center;
} }
.wd-tabs__nav-item.is-active .wd-tabs__nav-item-text {
font-weight: 700 !important;
color: #14181B !important;
}
}
:deep(.wd-tabs__nav-container) {
display: flex !important;
width: 100% !important;
justify-content: space-between !important;
} }
} }
.bg { .bg {
background: linear-gradient(180deg, #FFFFFF 0%, #FFFFFF 51%, rgba(255, 255, 255, 0) 100%); background: linear-gradient(180deg, #FFFFFF 0%, #FFFFFF 40%, rgba(245, 245, 245, 0) 100%);
}
.order-swiper {
background-color: #F5F5F5;
} }
</style> </style>
@@ -0,0 +1,230 @@
<script setup lang="ts">
import { computed } from 'vue'
import Config from '@/config/index'
import { formatSalesCount } from '@/utils/utils'
const { t } = useI18n()
type DishItem = {
id: number | string
merchantId?: number | string
dishName?: string
dishImage?: string
discountPrice?: number | string
originalPrice?: number | string
memberPrice?: number | string
salesCount?: number | string
[key: string]: unknown
}
const props = defineProps<{
item: DishItem
}>()
function goDetail() {
const { item } = props
if (item.merchantId) {
uni.navigateTo({
url: `/pages-store/pages/store/dishes?id=${item.id}&storeId=${item.merchantId}`,
})
} else {
uni.navigateTo({
url: `/pages-store/pages/store/index?id=${item.id}`,
})
}
}
const cover = computed(() => {
const raw = props.item?.dishImage
if (typeof raw === 'string' && raw) return raw.split(',')[0]
return ''
})
</script>
<template>
<view class="search-dish-item" @click="goDetail">
<view class="search-dish-item__img-wrap">
<view v-if="item.isNew == 1" class="dish-new-ribbon">
<text class="dish-new-ribbon__text">NEW</text>
</view>
<image :src="cover" mode="aspectFill" class="search-dish-item__img" />
</view>
<view class="search-dish-item__main">
<text class="search-dish-item__title line-clamp-2">{{ item.dishName || '' }}</text>
<view class="search-dish-item__price-row">
<text class="search-dish-item__price-current">
${{ item.discountPrice ?? item.originalPrice }}
</text>
<text
v-if="
Number(item.originalPrice) > 0 &&
Number(item.originalPrice) > Number(item.discountPrice ?? item.originalPrice)
"
class="search-dish-item__price-old"
>${{ item.originalPrice }}</text>
</view>
<view
v-if="Number(item.memberPrice) > 0"
class="search-dish-item__member"
>
<text class="search-dish-item__member-diamond">◆</text>
<text class="search-dish-item__member-text">
{{ Config.appName }} {{ t('pages.search.member-price-line') }}: ${{ item.memberPrice }}
</text>
</view>
<view class="search-dish-item__sales-wrap">
<text class="search-dish-item__sales-tag">
{{ t('pages.search.weekly-sales') }}{{ formatSalesCount(item.salesCount) }}
</text>
</view>
</view>
<view class="search-dish-item__add center" @click.stop="goDetail">
<image src="@img/chef/1285.png" class="search-dish-item__add-icon" mode="aspectFit" />
</view>
</view>
</template>
<style scoped lang="scss">
.search-dish-item {
display: flex;
flex-direction: row;
align-items: stretch;
padding: 28rpx 24rpx;
background: #fff;
border-bottom: 1rpx solid #ebebeb;
}
.search-dish-item__img {
width: 200rpx;
height: 200rpx;
background: #f2f2f2;
display: block;
}
.search-dish-item__main {
flex: 1;
min-width: 0;
margin-left: 24rpx;
margin-right: 16rpx;
display: flex;
flex-direction: column;
justify-content: flex-start;
}
.search-dish-item__title {
font-size: 28rpx;
line-height: 40rpx;
font-weight: 500;
color: #1a1a1a;
}
.search-dish-item__price-row {
display: flex;
flex-direction: row;
align-items: baseline;
flex-wrap: wrap;
gap: 12rpx;
margin-top: 12rpx;
}
.search-dish-item__price-current {
font-size: 32rpx;
line-height: 36rpx;
font-weight: 700;
color: #e02e24;
}
.search-dish-item__price-old {
font-size: 24rpx;
line-height: 28rpx;
color: #999;
text-decoration: line-through;
}
.search-dish-item__member {
display: flex;
flex-direction: row;
align-items: center;
margin-top: 12rpx;
padding: 6rpx 0;
gap: 6rpx;
}
.search-dish-item__member-diamond {
font-size: 20rpx;
line-height: 1;
color: #b8860b;
}
.search-dish-item__member-text {
font-size: 24rpx;
line-height: 32rpx;
font-weight: 500;
color: #8b6914;
}
.search-dish-item__sales-wrap {
margin-top: auto;
padding-top: 12rpx;
}
.search-dish-item__sales-tag {
display: inline-block;
padding: 8rpx 18rpx;
border-radius: 999rpx;
background: #f5f5f5;
font-size: 22rpx;
line-height: 28rpx;
color: #888;
}
.search-dish-item__add {
align-self: flex-end;
width: 64rpx;
height: 64rpx;
flex-shrink: 0;
border-radius: 50%;
background: #f2f2f2;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
}
.search-dish-item__add-icon {
width: 30rpx;
height: 30rpx;
}
.search-dish-item__img-wrap {
position: relative;
width: 200rpx;
height: 200rpx;
border-radius: 20rpx;
overflow: hidden;
flex-shrink: 0;
}
.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>
@@ -1,56 +0,0 @@
<template>
<view class="pt-52rpx pl-30rpx bg-#fff" v-if="searchStore.historyList?.length">
<view class="text-40rpx lh-40rpx text-#333 font-bold mb-24rpx tracking-[.04em]">{{ t('pages.search.recently') }}</view>
<view class="">
<template v-for="(item, index) in searchStore.historyList" :key="index">
<wd-swipe-action>
<view @click="handleSearch(item,index)" class="w-full h-110rpx flex items-center">
<image src="@img/chef/130.png" class="w-28rpx h-28rpx mr-44rpx"></image>
<view class="flex-1 border-b-solid border-b-1rpx border-b-#DFDFDF h-full flex items-center text-30rpx text-primary font-500 tracking-[.06em] line-clamp-1">{{ item.text }}</view>
</view>
<template #right>
<view @click="handleRemove(index)" class="w-110rpx h-110rpx bg-#FF4848 center">
<image src="@img/chef/129.png" class="w-44rpx h-50rpx"></image>
</view>
</template>
</wd-swipe-action>
</template>
</view>
</view>
<wd-message-box/>
</template>
<script lang="ts" setup>
import {useSearchStore} from "@/store";
import {useMessage,} from "wot-design-uni";
const {t} = useI18n()
const message = useMessage()
const searchStore = useSearchStore()
const isRemove = ref(false)
function handleSearch(item: { text: string }, index: number) {
if (isRemove.value) {
handleRemove(index)
return
}
console.log(item)
nextTick(() => {
searchStore.setHistoryList(item.text)
uni.navigateTo({
url: `/pages/search/result?keyword=${item.text}`,
})
})
}
function handleRemove(index: number) {
searchStore.historyList.splice(index, 1)
if (searchStore.historyList.length === 0) {
isRemove.value = false
}
}
</script>
<style lang="scss" scoped></style>
+60 -86
View File
@@ -1,49 +1,38 @@
<template> <template>
<view class="search-skeleton pt-88rpx"> <view class="search-skeleton">
<!-- 头部区域 --> <view class="search-skeleton__header px-24rpx pt-8rpx pb-12rpx flex items-center justify-between gap-16rpx">
<view class="header-section"> <view class="search-skeleton__circle skeleton-item"></view>
<view class="back-button-skeleton skeleton-item"></view> <view class="search-skeleton__pill flex-1 skeleton-item"></view>
<view class="search-bar-skeleton skeleton-item"></view> <view class="search-skeleton__circle skeleton-item"></view>
</view> </view>
<view class="search-skeleton__section">
<!-- 最近搜索区域 --> <view class="search-skeleton__title-row">
<view class="recent-section"> <view class="search-skeleton__title skeleton-item"></view>
<view class="section-title-skeleton skeleton-item"></view> <view class="search-skeleton__clear skeleton-item"></view>
<view class="recent-list"> </view>
<view <view class="search-skeleton__tags">
v-for="i in 3" <view v-for="i in 5" :key="'r-' + i" class="search-skeleton__tag skeleton-item"></view>
:key="i"
class="recent-item-skeleton skeleton-item"
></view>
</view> </view>
</view> </view>
<view class="search-skeleton__section">
<!-- 热门分类区域 --> <view class="search-skeleton__title skeleton-item search-skeleton__title--wide"></view>
<view class="popular-section"> <view class="search-skeleton__tags">
<view class="section-title-skeleton skeleton-item"></view> <view v-for="i in 8" :key="'h-' + i" class="search-skeleton__tag skeleton-item"></view>
<template v-for="item in 6"> </view>
<view class="flex items-center h-144rpx">
<view class="w-64rpx h-64rpx mr-28rpx rounded-50% skeleton-item"></view>
<view class="w-420rpx h-60rpx rounded-10rpx skeleton-item"></view>
</view>
</template>
</view> </view>
</view> </view>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
// 搜索页面骨架屏组件
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
// 通用骨架屏样式
.skeleton-item { .skeleton-item {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); background: linear-gradient(90deg, #ececec 25%, #e0e0e0 50%, #ececec 75%);
background-size: 200% 100%; background-size: 200% 100%;
animation: shimmer 1.5s infinite; animation: shimmer 1.5s infinite;
} }
// 闪烁动画
@keyframes shimmer { @keyframes shimmer {
0% { 0% {
background-position: -200% 0; background-position: -200% 0;
@@ -54,75 +43,60 @@
} }
.search-skeleton { .search-skeleton {
background-color: #fff;
min-height: 100vh; min-height: 100vh;
background-color: #f7f8fa;
padding-top: env(safe-area-inset-top, 0px);
} }
// 状态栏 .search-skeleton__circle {
.status-bar { width: 72rpx;
height: 88rpx; height: 72rpx;
width: 100%; border-radius: 50%;
flex-shrink: 0;
} }
// 头部区域 .search-skeleton__pill {
.header-section { height: 80rpx;
padding: 0 30rpx 40rpx; border-radius: 999rpx;
}
.search-skeleton__section {
padding: 28rpx 24rpx 8rpx;
}
.search-skeleton__title-row {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 20rpx; justify-content: space-between;
margin-bottom: 28rpx;
.back-button-skeleton {
width: 44rpx;
height: 44rpx;
border-radius: 22rpx;
}
.search-bar-skeleton {
flex: 1;
height: 88rpx;
border-radius: 44rpx;
}
} }
// 最近搜索区域 .search-skeleton__title {
.recent-section { width: 180rpx;
padding: 0 30rpx 40rpx; height: 40rpx;
border-radius: 8rpx;
.section-title-skeleton { }
width: 200rpx; .search-skeleton__title--wide {
height: 40rpx; width: 220rpx;
border-radius: 8rpx; margin-bottom: 28rpx;
margin-bottom: 30rpx;
}
.recent-list {
display: flex;
flex-direction: column;
gap: 20rpx;
.recent-item-skeleton {
height: 60rpx;
border-radius: 8rpx;
}
}
} }
// 热门分类区域 .search-skeleton__clear {
.popular-section { width: 56rpx;
padding: 40rpx 30rpx 0; height: 32rpx;
border-radius: 8rpx;
.section-title-skeleton {
width: 300rpx;
height: 40rpx;
border-radius: 8rpx;
margin-bottom: 30rpx;
}
} }
// 响应式设计 .search-skeleton__tags {
@media (max-width: 750rpx) { display: flex;
.category-grid { flex-direction: row;
grid-template-columns: 1fr; flex-wrap: wrap;
} gap: 16rpx 14rpx;
}
.search-skeleton__tag {
width: 132rpx;
height: 60rpx;
border-radius: 999rpx;
} }
</style> </style>
@@ -0,0 +1,134 @@
<script setup lang="ts">
interface SearchTagItem {
key: string | number
text: string
/** 左侧「热门」火焰标 */
hot?: boolean
/** 显示右侧删除(搜索记录) */
removable?: boolean
}
const props = withDefaults(
defineProps<{
title: string
tags: SearchTagItem[]
/** 标题右侧「清空」 */
showClear?: boolean
}>(),
{
showClear: false,
},
)
const emit = defineEmits<{
select: [item: SearchTagItem]
remove: [index: number]
clear: []
}>()
const { t } = useI18n()
</script>
<template>
<view v-if="tags.length > 0" class="search-tag-cloud">
<view class="search-tag-cloud__head">
<text class="search-tag-cloud__title">{{ title }}</text>
<text v-if="showClear" class="search-tag-cloud__clear" @click="emit('clear')">{{ t('pages.search.clear-history') }}</text>
</view>
<view class="search-tag-cloud__wrap">
<view
v-for="(item, index) in tags"
:key="item.key"
class="search-tag-cloud__pill"
@click="emit('select', item)"
>
<view v-if="item.hot" class="i-carbon:fire search-tag-cloud__flame shrink-0"></view>
<text class="search-tag-cloud__text">{{ item.text }}</text>
<view
v-if="item.removable"
class="search-tag-cloud__remove center shrink-0"
@click.stop="emit('remove', index)"
>
<text class="search-tag-cloud__remove-x">×</text>
</view>
</view>
</view>
</view>
</template>
<style scoped lang="scss">
.search-tag-cloud {
padding: 36rpx 24rpx 8rpx;
}
.search-tag-cloud__head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 28rpx;
}
.search-tag-cloud__title {
font-size: 32rpx;
line-height: 40rpx;
font-weight: 700;
color: #1a1a1a;
}
.search-tag-cloud__clear {
font-size: 26rpx;
line-height: 32rpx;
color: #999;
}
.search-tag-cloud__wrap {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 16rpx 14rpx;
}
.search-tag-cloud__pill {
display: inline-flex;
flex-direction: row;
align-items: center;
max-width: 100%;
padding: 14rpx 22rpx;
background: #fff;
border-radius: 999rpx;
box-shadow: 0 4rpx 14rpx rgba(0, 0, 0, 0.06);
}
.search-tag-cloud__flame {
margin-right: 8rpx;
font-size: 26rpx;
color: #e02020;
}
.search-tag-cloud__text {
font-size: 28rpx;
line-height: 32rpx;
font-weight: 500;
color: #333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 320rpx;
}
.search-tag-cloud__remove {
margin-left: 10rpx;
width: 36rpx;
height: 36rpx;
border-radius: 50%;
background: rgba(0, 0, 0, 0.06);
}
.search-tag-cloud__remove-x {
font-size: 32rpx;
line-height: 32rpx;
color: #666;
font-weight: 300;
margin-top: -4rpx;
}
</style>
@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import FiltrateTool from "@/components/filtrate-tool/index.vue"; import FiltrateTool from "@/components/filtrate-tool/index.vue";
import FoodBox from "@/pages/home/components/tabbar-home/components/food-box/index.vue"; import SearchDishResultItem from "@/pages/search/components/search-dish-result-item.vue";
import TabsType from "@/pages/home/components/tabbar-home/components/tabs-type.vue"; import TabsType from "@/pages/home/components/tabbar-home/components/tabs-type.vue";
import AnimatedButton from "../animated-button/animated-button.vue"; import AnimatedButton from "../animated-button/animated-button.vue";
import Collection from "@/components/collection/index.vue"; import Collection from "@/components/collection/index.vue";
@@ -189,14 +189,11 @@ defineExpose({
> >
<!-- 筛选工具 --> <!-- 筛选工具 -->
<!-- <filtrate-tool class="my-36rpx" @togglePickup="togglePickup" @toggleDiscount="toggleDiscount" @toggleScore="emit('toggleScore')" @togglePrice="emit('togglePrice')" /> --> <!-- <filtrate-tool class="my-36rpx" @togglePickup="togglePickup" @toggleDiscount="toggleDiscount" @toggleScore="emit('toggleScore')" @togglePrice="emit('togglePrice')" /> -->
<view <view class="search-food-toolbar">
class="pl-30rpx pb-36rpx text-36rpx lh-36rpx text-#333 font-500 tracking-[.04em]" <text class="search-food-toolbar__count">{{ foodTotal }} {{ t('pages.search.result.result') }}</text>
>{{ foodTotal }} {{ t('pages.search.result.result') }}</view </view>
> <view class="search-food-list">
<view class="px-30rpx"> <search-dish-result-item v-for="item in dataList" :key="String(item.id)" :item="item" />
<template v-for="item in dataList">
<food-box :item="item" />
</template>
</view> </view>
</view> </view>
</template> </template>
@@ -282,6 +279,22 @@ defineExpose({
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.search-food-toolbar {
padding: 20rpx 24rpx 8rpx;
background: #fff;
}
.search-food-toolbar__count {
font-size: 26rpx;
line-height: 34rpx;
color: #999;
font-weight: 400;
}
.search-food-list {
background: #fff;
}
.search-food-list :deep(.search-dish-item:last-child) {
border-bottom: none;
}
// 确保过渡动画平滑 // 确保过渡动画平滑
.transition-transform { .transition-transform {
transition: transform 0.6s ease-in-out; transition: transform 0.6s ease-in-out;
+115 -41
View File
@@ -1,79 +1,139 @@
<script setup lang="ts"> <script setup lang="ts">
import SearchHistory from './components/search-history/index.vue'
import SearchSkeleton from './components/search-skeleton.vue' import SearchSkeleton from './components/search-skeleton.vue'
import {useSearchStore} from '@/store' import SearchTagCloud from './components/search-tag-cloud.vue'
import {appSearchListPost,appRecipeCategoryListGet} from '@/service' import { useSearchStore } from '@/store'
import { appRecipeCategoryListGet } from '@/service'
import { decodeRouteQueryValue } from '@/utils/utils'
const {t} = useI18n() const { t } = useI18n()
const searchStore = useSearchStore() const searchStore = useSearchStore()
const keyword = ref('') const keyword = ref('')
function handleSearch() { function handleSearch() {
nextTick(() => { nextTick(() => {
if (!keyword.value) { if (!keyword.value) {
return uni.showToast({title: t('common.prompt.please-enter-keyword-search'), icon: 'none'}) return uni.showToast({
title: t('common.prompt.please-enter-keyword-search'),
icon: 'none',
})
} }
searchStore.setHistoryList(keyword.value) const kw = decodeRouteQueryValue(keyword.value)
if (!kw) {
return uni.showToast({
title: t('common.prompt.please-enter-keyword-search'),
icon: 'none',
})
}
searchStore.setHistoryList(kw)
uni.navigateTo({ uni.navigateTo({
url: `/pages/search/result?keyword=${keyword.value}`, url: `/pages/search/result?keyword=${encodeURIComponent(kw)}`,
}) })
keyword.value = '' keyword.value = ''
}) })
} }
function handleHotSearch(item: any) { function goResultWithKeyword(k: string) {
nextTick(() => { const kw = decodeRouteQueryValue(k)
searchStore.setHistoryList(item.categoryName) if (!kw) return
uni.navigateTo({ searchStore.setHistoryList(kw)
url: `/pages/search/result?keyword=${item.categoryName}`, uni.navigateTo({
}) url: `/pages/search/result?keyword=${encodeURIComponent(kw)}`,
}) })
} }
function handleHotSearch(item: Record<string, unknown>) {
const name = String(item?.categoryName ?? '')
goResultWithKeyword(name)
}
function onHistorySelect(tag: { text: string }) {
goResultWithKeyword(tag.text)
}
function onHistoryRemove(index: number) {
searchStore.historyList.splice(index, 1)
}
function onHistoryClear() {
searchStore.clearHistory()
}
const loading = ref(true) const loading = ref(true)
onMounted(() => { onMounted(() => {
loading.value = true loading.value = true
// 查询热门搜索词列表
getHotSearchList() getHotSearchList()
}) })
const hotSearchList = ref([]) const hotSearchList = ref<Record<string, unknown>[]>([])
function getHotSearchList() { function getHotSearchList() {
appRecipeCategoryListGet({}).then(res=> { appRecipeCategoryListGet({})
console.log('热门搜索词列表', res) .then((res) => {
hotSearchList.value = res.data hotSearchList.value = (res.data as Record<string, unknown>[]) || []
}).finally(() => { })
loading.value = false .finally(() => {
}) loading.value = false
})
}
const historyTags = computed(() =>
searchStore.historyList.map((h, i) => ({
key: `${h.time}-${i}`,
text: decodeRouteQueryValue(h.text),
removable: true,
})),
)
const hotTags = computed(() =>
hotSearchList.value.map((item, i) => ({
key: String(item.id ?? `hot-${i}`),
text: String(item.categoryName ?? ''),
hot: i < 3,
})),
)
function onHotSelect(tag: { key: string | number }) {
const idx = hotTags.value.findIndex((x) => x.key === tag.key)
const raw = idx >= 0 ? hotSearchList.value[idx] : null
if (raw) handleHotSearch(raw)
} }
</script> </script>
<template> <template>
<view class=""> <view class="search-page">
<view <view
class="animate-in fade-in animate-ease-out animate-duration-300" class="animate-in fade-in animate-ease-out animate-duration-300"
v-show="loading" v-show="loading"
> >
<search-skeleton /> <search-skeleton />
</view> </view>
<view <view
class="animate-in fade-in animate-ease-in animate-duration-300" class="animate-in fade-in animate-ease-in animate-duration-300 search-page__content"
v-show="!loading" v-show="!loading"
> >
<header-search focus class="" v-model="keyword" :placeholder="t('components.search.placeholder')" @search="handleSearch"/> <header-search
<search-history/> v-model="keyword"
<view class="pl-30rpx mt-52rpx"> focus
<view class="text-40rpx lh-40rpx text-#333 font-bold mb-24rpx tracking-[.04em]"> trailing-cart
{{ t('pages.search.hot-title') }} class="search-page__header"
</view> :placeholder="t('components.search.placeholder')"
<template v-for="item in hotSearchList"> @search="handleSearch"
<view @click="handleHotSearch(item)" class="w-full h-144rpx flex items-center"> />
<image :src="item?.categoryImage" class="w-64rpx h-64rpx mr-28rpx rounded-50%" mode="aspectFill"></image> <view class="search-page__body">
<view class="flex-1 border-b-solid border-b-1rpx border-b-#DFDFDF h-full flex items-center text-30rpx text-primary font-500 tracking-[.06em]"> <search-tag-cloud
{{ item?.categoryName }} v-if="historyTags.length > 0"
</view> :title="t('pages.search.recently')"
</view> :tags="historyTags"
</template> show-clear
@select="onHistorySelect"
@remove="onHistoryRemove"
@clear="onHistoryClear"
/>
<search-tag-cloud
v-if="hotTags.length > 0"
:title="t('pages.search.hot-title')"
:tags="hotTags"
@select="onHotSelect"
/>
</view> </view>
</view> </view>
</view> </view>
@@ -81,6 +141,20 @@ function getHotSearchList() {
<style lang="scss"> <style lang="scss">
page { page {
background-color: #fff; background-color: #f7f8fa;
}
</style>
<style scoped lang="scss">
.search-page {
min-height: 100vh;
background: #f7f8fa;
}
.search-page__content {
min-height: 100vh;
background: #f7f8fa;
}
.search-page__body {
padding-bottom: 48rpx;
} }
</style> </style>
+32 -5
View File
@@ -1,12 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { useSearchStore } from "@/store"; import { useSearchStore } from "@/store";
import TabSwitcher from "./components/tab-switcher.vue"; import { decodeRouteQueryValue } from "@/utils/utils";
import SwiperList from "./components/swiper-list/swiper-list.vue"; import SwiperList from "./components/swiper-list/swiper-list.vue";
import Score from "@/components/filtrate-tool/components/score.vue"; import Score from "@/components/filtrate-tool/components/score.vue";
import PriceChoose from "@/components/filtrate-tool/components/price-choose.vue"; import PriceChoose from "@/components/filtrate-tool/components/price-choose.vue";
import SortPopup from "@/pages/search/components/sort-popup.vue"; import SortPopup from "@/pages/search/components/sort-popup.vue";
const props = defineProps<{ const _props = defineProps<{
/** 少数构建下可能由路由注入;实际以 onLoad 的 query 为准 */
keyword?: string; keyword?: string;
}>(); }>();
@@ -14,7 +15,15 @@ const { t } = useI18n();
const searchStore = useSearchStore(); const searchStore = useSearchStore();
const keyword = ref(props.keyword || ""); const keyword = ref(decodeRouteQueryValue(_props.keyword) || '');
/** uni-app?keyword= 只在 onLoad 的 options 里;部分端会对已编码参数再编一层,需循环 decode */
onLoad((options?: Record<string, string | undefined>) => {
const q = options?.keyword
if (q == null || String(q).trim() === '') return
const plain = decodeRouteQueryValue(q)
if (plain) keyword.value = plain
})
function handleSearch() { function handleSearch() {
nextTick(() => { nextTick(() => {
@@ -99,12 +108,15 @@ onMounted(()=> {
</script> </script>
<template> <template>
<view class=""> <view class="search-result-page">
<z-paging-swiper> <z-paging-swiper>
<template #top> <template #top>
<header-search <header-search
class="pb-42rpx"
v-model="keyword" v-model="keyword"
trailing-cart
trailing-surface="white"
class="search-result-page__header"
:placeholder="t('components.search.placeholder')"
@search="handleSearch" @search="handleSearch"
/> />
<!-- <tab-switcher <!-- <tab-switcher
@@ -147,7 +159,22 @@ onMounted(()=> {
</view> </view>
</template> </template>
<style lang="scss">
page {
background-color: #fff;
}
</style>
<style scoped lang="scss"> <style scoped lang="scss">
.search-result-page {
min-height: 100vh;
background: #fff;
}
.search-result-page__header {
padding-bottom: 8rpx;
}
:deep(.wd-tabs__nav-item) { :deep(.wd-tabs__nav-item) {
font-size: 40rpx; font-size: 40rpx;
color: #666666; color: #666666;
+18
View File
@@ -467,6 +467,24 @@ export async function appMerchantOrderPayOrderBatchPost({
}); });
} }
/** 提交 Zip 支付凭证 POST /app/merchantOrder/zipPayVoucher */
export async function appMerchantOrderZipPayVoucherPost({
body,
options,
}: {
body: { orderId: number; zipPayVoucher: string };
options?: CustomRequestOptions;
}) {
return request<API.R>('/app/merchantOrder/zipPayVoucher', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: body,
...(options || {}),
});
}
/** /**
* PayOrderBatchBo - 批量订单支付参数 * PayOrderBatchBo - 批量订单支付参数
*/ */
+3
View File
@@ -2898,6 +2898,8 @@
'merchantVo'?: MerchantVo; 'merchantVo'?: MerchantVo;
/** 份数 */ /** 份数 */
'copies'?: number; 'copies'?: number;
'actualSalePrice'?: number;
} }
@@ -3031,6 +3033,7 @@
'dishList'?: MerchantDishVo[]; 'dishList'?: MerchantDishVo[];
/** 商家视图对象 t_merchant */ /** 商家视图对象 t_merchant */
'merchantVo'?: MerchantVo; 'merchantVo'?: MerchantVo;
'isNew'?: number;
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

+1
View File
@@ -17,3 +17,4 @@ export { Pinia, store }
export * from './module/user' export * from './module/user'
export * from './module/search' export * from './module/search'
export * from './module/config' export * from './module/config'
export * from './module/categoryNav'
+30
View File
@@ -0,0 +1,30 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
/**
* id query
*/
export const useCategoryNavStore = defineStore('categoryNav', () => {
const pendingRecipeCategoryId = ref<string | null>(null)
function setPendingRecipeCategoryId(id: string | number | null | undefined) {
if (id == null || id === '') {
pendingRecipeCategoryId.value = null
return
}
pendingRecipeCategoryId.value = String(id)
}
/** 读取并清空,保证仅消费一次 */
function consumePendingRecipeCategoryId(): string | null {
const v = pendingRecipeCategoryId.value
pendingRecipeCategoryId.value = null
return v
}
return {
pendingRecipeCategoryId,
setPendingRecipeCategoryId,
consumePendingRecipeCategoryId,
}
})
+4
View File
@@ -34,10 +34,14 @@ export const useSearchStore = defineStore(
historyList.value = historyListCopy historyList.value = historyListCopy
} }
function clearHistory() {
historyList.value = []
}
return { return {
historyList, historyList,
setHistoryList, setHistoryList,
clearHistory,
} }
}, },
{ {
+48 -4
View File
@@ -5,6 +5,38 @@ import {dayjs} from "@/plugin";
import {useConfigStore} from "@/store"; import {useConfigStore} from "@/store";
/**
* /query URL navigate %25E8%25...
* %XX decode
*/
export function decodeRouteQueryValue(raw: unknown): string {
let s = String(raw ?? '').trim()
if (!s) return ''
for (let i = 0; i < 8; i++) {
if (!/%[0-9A-Fa-f]{2}/i.test(s)) break
try {
const next = decodeURIComponent(s.replace(/\+/g, ' '))
if (next === s) break
s = next
} catch {
break
}
}
return s
}
/** 销量展示:千级 k+、万级 w+(与首页精选菜品一致) */
export function formatSalesCount(stockLike: unknown): string {
const n = Number(stockLike)
if (!Number.isFinite(n) || n < 0) return '0'
if (n >= 10000) {
const w = n / 10000
return (w >= 10 ? `${Math.round(w)}w+` : `${w.toFixed(1).replace(/\.0$/, '')}w+`)
}
if (n >= 1000) return `${Math.floor(n / 1000)}k+`
return String(Math.floor(n))
}
// 缩略图 // 缩略图
export function thumbnailImg(url: string, width = 400, height = 400) { export function thumbnailImg(url: string, width = 400, height = 400) {
if (!url || !url.startsWith("http") || !url.startsWith("https")) { if (!url || !url.startsWith("http") || !url.startsWith("https")) {
@@ -175,14 +207,26 @@ export function navGoogleMap(latLng: string) {
} }
/** /**
* "MMM.DD YYYY" Mar.06 2025 *
* - M月D日 YYYY48 2026
* - MMM D, YYYYApr 8, 2026
* 使 MMM.DD YYYY zh-cn MMM 44.08
* @param {number} timestamp - * @param {number} timestamp -
* @returns {string} * @returns {string}
*/ */
export function formatTimestamp(timestamp: number): string { export function formatTimestamp(timestamp: number): string {
return dayjs(Number(timestamp)) const locale = uni.getLocale()
.format('MMM.DD YYYY') // 核心格式化指令 const date = dayjs(Number(timestamp))
.replace(/\b([a-z])/, match => match.toUpperCase()); // 确保月份首字母大写
if (locale === 'zh-Hans') {
date.locale('zh-cn')
return date.format('M月D日 YYYY')
}
date.locale('en')
return date
.format('MMM D, YYYY')
.replace(/\b([a-z])/, match => match.toUpperCase())
} }
/** /**