7 Commits

Author SHA1 Message Date
pcwl_yancheng 0274df566b !2 修复bug
Merge pull request !2 from ISFP_T/new_ui
2026-04-15 01:27:52 +00:00
pcwl_yancheng 02243a0b88 修复bug 2026-04-15 09:26:48 +08:00
pcwl_yancheng a89610c245 修复bug 2026-04-14 17:45:41 +08:00
pcwl_yancheng d2077f5844 ui调整 2026-04-14 15:02:00 +08:00
pcwl_yancheng ec9282a64f 修改样式 2026-04-11 11:55:03 +08:00
pcwl_yancheng ef9210a567 !1 修复bug
Merge pull request !1 from ISFP_T/now_prod
2026-04-02 03:11:46 +00:00
pcwl_yancheng c83f52dfad 用户中心样式调整 2026-04-01 18:10:14 +08:00
79 changed files with 12009 additions and 3271 deletions
+2 -2
View File
@@ -4,7 +4,7 @@ NODE_ENV=development
VITE_DELETE_CONSOLE=false
#本地环境
#VITE_SERVER_BASEURL=https://howhowfresh.com/prod-api
VITE_SERVER_BASEURL=http://192.168.5.23:8080
VITE_SERVER_BASEURL=https://howhowfresh.com/prod-api
#VITE_SERVER_BASEURL=http://192.168.5.4:8080
#VITE_SERVER_BASEURL=http://192.168.0.148:8888
#VITE_SERVER_BASEURL=http://liuyao.nat100.top/meiguowaimai
+35 -7
View File
@@ -1,6 +1,5 @@
<script lang="ts" setup>
// import {upload} from '@/utils/upload/alioss'
import { uploadToS3 } from '@/utils/upload/ymx'
import { upload } from '@/utils/upload/alioss'
const props = withDefaults(
defineProps<{
@@ -59,8 +58,7 @@ function chooseImage(type: 'album' | 'camera') {
// })
//
// for (let i = 0; i < files.length; i++) {
// // asyncList.push(upload(files[i]))
// asyncList.push(uploadToS3(files[i]))
// asyncList.push(upload(files[i]))
// console.log(asyncList)
// }
//
@@ -101,9 +99,7 @@ function chooseImage(type: 'album' | 'camera') {
mask: true,
})
for (let i = 0; i < files.length; i++) {
// asyncList.push(upload(files[i]))
asyncList.push(uploadToS3(files[i]))
console.log(asyncList)
asyncList.push(upload(files[i]))
}
Promise.all(asyncList)
.then((results) => {
@@ -116,6 +112,38 @@ function chooseImage(type: 'album' | 'camera') {
}
})
// #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">
import { computed, onMounted } from 'vue'
import Config from '@/config'
import {throttle} from 'throttle-debounce'
import {useUserStore} from '@/store'
const {t} = useI18n()
const userStore = useUserStore()
const props = withDefaults(
defineProps<{
@@ -10,11 +13,17 @@ const props = withDefaults(
disabled?: boolean
focus?: boolean
placeholder?: string
/** 搜索落地页:右侧圆形购物车,与返回键对称 */
trailingCart?: boolean
/** trailingCart 为 true 时:gray=搜索首页浅底;white=结果页白底(与设计稿一致) */
trailingSurface?: 'gray' | 'white'
}>(),
{
modelValue: '',
disabled: false,
focus: false,
trailingCart: false,
trailingSurface: 'gray',
},
)
@@ -39,6 +48,35 @@ function handleClickLeft() {
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({
name: 'HeaderSearch',
@@ -46,10 +84,52 @@ defineOptions({
</script>
<template>
<view
class="relative z-1 bg-#fff"
:class="headerRootClass"
>
<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="i-fluent:ios-arrow-ltr-24-filled text-36rpx text-#333"></view>
</view>
@@ -79,6 +159,56 @@ defineOptions({
</template>
<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) {
background: transparent !important;
}
+23 -2
View File
@@ -4,9 +4,12 @@ const props = withDefaults(defineProps<{
fixed?: boolean
showLeft?: boolean
customClass?: string
/** 左侧返回为白底圆形+阴影(用于预约日期等浅色页) */
circleBack?: boolean
}>(), {
fixed: true,
showLeft: true
showLeft: true,
circleBack: false
});
function handleClickLeft() {
@@ -31,7 +34,17 @@ function handleClickLeft() {
@click-left="handleClickLeft">
<template #left>
<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>
</template>
<template #right>
@@ -41,6 +54,14 @@ function handleClickLeft() {
</template>
<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) {
z-index: 2 !important;
.wd-navbar__title {
+2
View File
@@ -23,6 +23,8 @@ const Config = {
shareLink: "https://www.howhowfresh.com/h5/",
shareImage: "https://hanguomcn.oss-ap-northeast-2.aliyuncs.com/images/20250901105756_1756695476183_56ac07ac.png",
shareDesc: "分享专属二维码邀请新用户注册即邀请成功。一起来注册体验点餐功能。",
// Zelle 支付二维码图片地址
zellePayPath: "https://www.howhowfresh.com/minio/ruoyi/2026/04/14/ef120c97e50c419da2263a59d6679229.png",
// 登录端口
userPort: 1,
// 手机区号数组
+93 -8
View File
@@ -171,7 +171,7 @@
"officeDescription": "Workplaces with entry restrictions",
"other": "Other",
"otherDescription": "Hospitals, parks, outdoors, etc",
"title": "Select building type"
"title": "Building type selection"
},
"deliveryInstructions": "Delivery Instructions",
"deliveryPointInfo": "Delivery Point Information",
@@ -206,14 +206,19 @@
"dateNotSelectable": "This date is not selectable",
"noAvailableTime": "No available time for current date, automatically selected next business day",
"notAvailable": "Not available",
"nextWeek": "Next week",
"pageTitle": "Delivery date",
"reservationSuccess": "Reservation successful",
"selectTimeSlot": "Please select a time slot"
"selectTimeSlot": "Please select a time slot",
"thisWeek": "This week"
},
"savedAddresses": "Saved addresses",
"title": "Address",
"titleDetail": "Address Details"
},
"browse": {
"brandTag": "CHEFLINK Selected",
"moreRecipes": "More Recipes",
"titleCuisine": "Nearby Cuisine",
"titleRecipes": "Selected Recipes"
},
@@ -228,7 +233,10 @@
"mine": {
"activity-description": "Activity description",
"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",
"customer-service-phone": "Customer service phone",
"dial": "Dial",
@@ -240,8 +248,10 @@
"join": "Join",
"log-out-successfully": "Log out successfully",
"login-out-tip": "Are you sure you want to log out of your current login account?",
"member": "Member",
"member-desc": "Unlock members to enjoy more discounts and promotions.",
"member-title": "Free trial of CHEFLINK",
"openMember": "Activate",
"order": "Order",
"platformAgreement": "Platform Agreement",
"privacyPolicy": "Privacy Policy",
@@ -259,17 +269,22 @@
"PU": "PU",
"accept": "ACPT",
"all": "ALL",
"cancelOrder": "Cancel",
"completed": "DCOMP",
"confirmReady": "CONF/READY",
"onTheWay": "OTW",
"title": "History"
"title": "History",
"totalItemCount": "{count} items"
},
"store": {
"all": "All"
},
"search": {
"hot-title": "Popular Categories",
"recently": "Recently",
"hot-title": "Hot searches",
"clear-history": "Clear",
"recently": "Search history",
"weekly-sales": "Weekly sales",
"member-price-line": "Member",
"result": {
"food": "Food",
"recipe": "Recipe",
@@ -362,6 +377,36 @@
"checkout": {
"addAddress": "Add address",
"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",
"appointmentPickup": "Appointment pickup",
"chooseTime": "Choose the time",
@@ -404,6 +449,7 @@
"deliveryAddress": "Delivery address",
"deliveryPhotos": "Delivery of photos",
"deliveryTime": "Delivery time",
"beforeDeadline": " before",
"estimatedDeliveryTime": "Estimated delivery time",
"orderInfo": "Ordering Information",
"orderNumber": "Order number",
@@ -433,7 +479,11 @@
"total": "Total",
"upTime": "Estimated self-pickup time",
"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": {
"addToCart": "Add to cart",
@@ -488,6 +538,23 @@
"recommend": "Recommended for you",
"required": "Required",
"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": {
"cantSee": "Can't see clearly? Change one",
"confirm": "Confirm"
@@ -555,7 +622,23 @@
"title": "Cart",
"totalPrice": "Total price",
"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": {
"creditCard": "Credit card payment",
@@ -565,6 +648,7 @@
},
"complaints": {
"contact-information": "Contact information",
"contact-information-placeholder": "Please enter your contact phone",
"contact-information-tip": "Your contact information helps us communicate and solve problems, only visible to staff",
"description": "You are welcome to give us feedback on the use of our products and suggestions.",
"feedback-content": "Feedback content",
@@ -582,6 +666,7 @@
"coupon": {
"all-merchants": "Applicable to all merchants",
"expiry-date": "Expiry date: ",
"merchant-only": "Only available at {name}",
"merchant-specific": "For specific merchant use",
"no-coupons": "You currently do not have any coupons",
"redeem-now": "Redeem now",
+94 -9
View File
@@ -30,7 +30,7 @@
"goSettle": "去结算",
"google-map": "谷歌地图",
"googleLoginFailed": "谷歌登录授权失败,请重试",
"gotIt": "明白了",
"gotIt": "我知道了",
"loading": "加载中",
"mile": "英里",
"minutes": "分钟",
@@ -171,7 +171,7 @@
"officeDescription": "有进入限制的工作场所",
"other": "其他",
"otherDescription": "医院、公园、户外等",
"title": "选择建筑类型"
"title": "建筑类型选择"
},
"deliveryInstructions": "配送说明",
"deliveryPointInfo": "交货点信息",
@@ -206,14 +206,19 @@
"dateNotSelectable": "该日期不可选择",
"noAvailableTime": "当前日期无可用时间,已自动选择下一个营业日",
"notAvailable": "不营业",
"nextWeek": "下周",
"pageTitle": "送货日",
"reservationSuccess": "预约成功",
"selectTimeSlot": "请选择时间段"
"selectTimeSlot": "请选择时间段",
"thisWeek": "本周"
},
"savedAddresses": "保存的地址",
"title": "地址管理",
"titleDetail": "地址详情"
},
"browse": {
"brandTag": "CHEFLINK 严选",
"moreRecipes": "更多食谱",
"titleCuisine": "附近的美食",
"titleRecipes": "选择食谱"
},
@@ -228,7 +233,10 @@
"mine": {
"activity-description": "活动说明:",
"activity-description-tip": "分享专属二维码邀请新用户注册即邀请成功。一起来注册体验点餐功能。",
"collection": "收藏",
"collection": "我的收藏",
"collectionBatchDeleteConfirm": "确定删除已选中的 {count} 条收藏?",
"collectionBatchModeHint": "请选择要删除的收藏,再次点击垃圾桶执行删除",
"collectionSelectFirst": "请先选择要删除的收藏",
"complaintsAndSuggestions": "投诉建议",
"customer-service-phone": "客服电话",
"dial": "拨号",
@@ -240,8 +248,10 @@
"join": "加入",
"log-out-successfully": "退出登录成功",
"login-out-tip": "是否确定退出当前登录账号",
"member": "会员",
"member-desc": "解锁会员,享受更多折扣和促销活动。",
"member-title": "CHEFLINK 免费试用",
"openMember": "去开通",
"order": "订单",
"platformAgreement": "平台协议",
"privacyPolicy": "隐私政策",
@@ -259,17 +269,22 @@
"PU": "自取",
"accept": "接受",
"all": "全部",
"cancelOrder": "取消订单",
"completed": "已完成",
"confirmReady": "确认/已准备",
"onTheWay": "在路上",
"title": "订单"
"title": "订单",
"totalItemCount": "共{count}件"
},
"store": {
"all": "全部"
},
"search": {
"hot-title": "热",
"recently": "历史记录",
"hot-title": "热门搜索",
"clear-history": "清空",
"recently": "搜索记录",
"weekly-sales": "周销量",
"member-price-line": "会员价",
"result": {
"food": "美食",
"recipe": "菜谱",
@@ -362,6 +377,36 @@
"checkout": {
"addAddress": "新增地址",
"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": "预约配送",
"appointmentPickup": "预约自取",
"chooseTime": "选择时间",
@@ -404,6 +449,7 @@
"deliveryAddress": "配送地址",
"deliveryPhotos": "送达照片",
"deliveryTime": "送达时间",
"beforeDeadline": "前",
"estimatedDeliveryTime": "预计送达时间",
"orderInfo": "订单信息",
"orderNumber": "订单号",
@@ -433,7 +479,11 @@
"total": "总计",
"upTime": "预计自取时间",
"useCode": "核销码",
"writeOff": "核销"
"writeOff": "核销",
"uploadPaidVoucher": "我已支付,上传凭证",
"voucherSubmitSuccess": "凭证已提交",
"voucherSubmitFailed": "提交失败,请重试",
"voucherUploadFailed": "上传失败,请重试"
},
"store": {
"addToCart": "加入购物车",
@@ -488,6 +538,23 @@
"recommend": "推荐给您",
"required": "必选项",
"sales": "销量",
"dishDetail": {
"productDetails": "产品详情",
"productIntro": "产品简介",
"origin": "产地",
"unitQty": "单位数量",
"category": "分类",
"brand": "品牌",
"allergen": "过敏源",
"weeklySales": "周销量",
"forYou": "为您推荐",
"memberPriceLabel": "{name}会员价",
"dash": "—",
"lbUnit": "磅",
"hotSalesBadge": "生鲜热销榜",
"stockLabel": "库存",
"loadMoreRecommend": "加载更多"
},
"securityCode": {
"cantSee": "看不清?换一张",
"confirm": "确定"
@@ -555,7 +622,23 @@
"title": "购物车",
"totalPrice": "总计",
"viewCart": "查看购物车",
"viewStore": "查看店铺"
"viewStore": "查看店铺",
"localDelivery": "本地配送",
"deliveryUnified": "商品将由 {name} 统一配送!",
"itemsTotalWithPrice": "{count}件商品总共${amount}",
"deliveryFeeLine": "运费",
"serviceFeeLine": "服务费",
"subtotalLine": "小计",
"selectAll": "全选",
"forYou": "为您推荐",
"calculating": "计算中...",
"selectItemsHint": "请选择要结算的商品",
"removeCartTitle": "删除购物车?",
"removeCartDesc": "确定要删除该店铺购物车吗?",
"removeProductTitle": "移除商品?",
"removeProductDesc": "确定要将 {name} 从购物车中移除吗?",
"emptyTitle": "购物车为空",
"emptyAction": "去逛逛餐厅"
},
"choosePaymethod": {
"creditCard": "信用卡支付",
@@ -565,6 +648,7 @@
},
"complaints": {
"contact-information": "联系电话",
"contact-information-placeholder": "请输入您的联系电话",
"contact-information-tip": "您的联系方式有助于我们沟通解决问题,仅工作人员可见",
"description": "您好,欢迎您给我们反馈产品的使用感受和建议。",
"feedback-content": "反馈内容",
@@ -582,6 +666,7 @@
"coupon": {
"all-merchants": "适用于所有商户使用",
"expiry-date": "到期日:",
"merchant-only": "仅{name}可用",
"merchant-specific": "指定商户使用",
"no-coupons": "您目前没有任何优惠券",
"redeem-now": "立即兑换",
+9 -3
View File
@@ -2,8 +2,8 @@
"name" : "CHEFLINK delivery",
"appid" : "__UNI__06509BE",
"description" : "",
"versionName" : "2.0.1",
"versionCode" : 201,
"versionName" : "3.1.2",
"versionCode" : 312,
"transformPx" : false,
/* 5+App */
"app-plus" : {
@@ -203,5 +203,11 @@
"enable" : false
},
"vueVersion" : "3",
"locale" : "en"
"locale" : "en",
"h5" : {
"router" : {
"mode" : "history",
"base" : "/h5/"
}
}
}
+13
View File
@@ -14,6 +14,7 @@
const userStore = useUserStore();
const configStore = useConfigStore()
const logicStore = useLogicStore()
const invitationCode = ref('')
const areaCode = ref<string>(logicStore.registerForm.areaCode || '+1');
const columns = ref<string[]>(Config.phoneCodeList);
@@ -94,6 +95,9 @@
// uuid: data.uuid,
userPort: Config.userPort, // 登录端口2 商户端
}
if (invitationCode.value) {
requestBody.invitationCode = invitationCode.value
}
if (requestBody.email == null || requestBody.email === '') {
delete requestBody.email
}
@@ -155,6 +159,15 @@
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') {
+37 -1
View File
@@ -51,11 +51,16 @@ function navigateTo(url: string) {
<view class="grid grid-cols-2 gap-x-30rpx gap-y-46rpx px-32rpx">
<template v-for="(item,index) in dataList">
<view @click="handleClickDish(item)" class="w-330rpx overflow-hidden">
<view class="search-result-img-wrap">
<view v-if="item.isNew == 1" class="dish-new-ribbon">
<text class="dish-new-ribbon__text">NEW</text>
</view>
<image
:src="thumbnailImg(item?.dishImage?.split(',')[0])"
class="w-full h-186rpx rounded-24rpx mb-16rpx"
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"
>{{ item.dishName }}</text>
</view>
@@ -65,5 +70,36 @@ function navigateTo(url: string) {
</template>
<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>
+316 -48
View File
@@ -1,25 +1,24 @@
<script setup lang="ts">
import usePage from "@/hooks/usePage";
import {appMerchantRecommendListPost} from "@/service";
import { appMerchantRecommendListPost, type MerchantVo } from "@/service";
import { useUserStore } from "@/store";
import FiltrateTool from "@/components/filtrate-tool/index.vue";
import PriceChoose from "@/components/filtrate-tool/components/price-choose.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({
merchantCategoryIds: {
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) {
return new Promise(resolve => {
return new Promise((resolve) => {
appMerchantRecommendListPost({
params: {
pageNum: pageNum,
@@ -28,75 +27,344 @@ function getList(pageNum: number, pageSize: number) {
body: {
lat: userStore.userLocation.latitude,
lng: userStore.userLocation.longitude,
selfPickup: selfPickup.value, // 是否自提
discount: discount.value, // 是否有折扣 1是 2 否
scoreRange: scoreRange.value || null, // 评分范围 比如 3-4
priceRange: price.value || null, // 价格范围 比如 10-30
merchantCategoryIds: props.merchantCategoryIds ? [props.merchantCategoryIds] : [], // 商家分类id集合(首页中间)
}
}).then(res => {
console.log(res)
resolve({rows: res.rows})
})
})
selfPickup: selfPickup.value,
discount: discount.value,
scoreRange: scoreRange.value || null,
priceRange: price.value || null,
merchantCategoryIds: props.merchantCategoryIds
? [props.merchantCategoryIds]
: [],
},
}).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) {
selfPickup.value = value;
paging.value.refresh()
}
// 是否有折扣
const discount = ref<number | null>(null)
function toggleDiscount(value: number) {
discount.value = value;
paging.value.refresh()
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() {
if (scoreRef.value) {
scoreRef.value.onOpen();
const discount = ref<number | null>(null);
function toggleDiscount(value: number) {
discount.value = value;
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() {
scoreRef.value?.onOpen();
}
function togglePrice() {
if (priceChooseRef.value) {
priceChooseRef.value.onOpen();
}
priceChooseRef.value?.onOpen();
}
function applyScore(value: string) {
scoreRange.value = value;
nextTick(() => paging.value?.refresh());
}
function applyPrice(value: string) {
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>
<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>
<view class="bg-white pb-24rpx">
<navbar :title="t('pages.home.featured-on')"/>
<!-- 筛选工具 -->
<filtrate-tool class="mt-32rpx" @togglePickup="togglePickup" @toggleDiscount="toggleDiscount" @toggleScore="toggleScore" @togglePrice="togglePrice" />
<view class="home-store-header">
<navbar
:title="t('pages.home.featured-on')"
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>
</template>
<view class="p-30rpx">
<template v-for="(item, index) in dataList" :key="index">
<food-box :item="item" />
</template>
<view class="home-store-list">
<view
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>
</z-paging>
<score @applyScore="applyScore" ref="scoreRef" />
<price-choose @applyPrice="applyPrice" ref="priceChooseRef" />
</view>
</template>
<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>
+715 -25
View File
@@ -1,61 +1,751 @@
<script setup lang="ts">
import {appMerchantRecommendListPost,getDishListByCategoryId} from "@/service";
import FoodBox from "@/pages/home/components/tabbar-home/components/food-box/index.vue";
import {useUserStore} from "@/store";
import { debounce } from "throttle-debounce";
import Config from "@/config";
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 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) {
return new Promise(resolve => {
return new Promise<{ rows: unknown[]; total?: number }>((resolve) => {
const rid = recipeCategoryId.value;
if (!rid) {
resolve({ rows: [], total: 0 });
return;
}
getDishListByCategoryId({
params: {
pageNum,
pageSize,
recipeCategoryId:props.id
recipeCategoryId: rid,
},
body: {
lat: userStore.userLocation.latitude,
lng: userStore.userLocation.longitude,
selfPickup: null, // 是否自提
discount: null, // 是否有折扣 1是 2 否
scoreRange: null, // 评分范围 比如 3-4
priceRange: null, // 价格范围 比如 10-30
pageNum,
pageSize,
lat: userStore.userLocation.latitude || undefined,
lng: userStore.userLocation.longitude || undefined,
recipeCategoryId: rid,
merchantCategoryIds: [],
merchantLabelIds: props.id ? [props.id] : [],
recipeCategoryId:props.id
merchantLabelIds: [],
},
}).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>
<template>
<z-paging
ref="paging"
v-model="dataList"
:auto="false"
@query="queryList"
bg-color="#fff"
bg-color="#f5f5f5"
>
<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>
<view class="p-32rpx">
<template v-for="(item, index) in dataList" :key="index">
<food-box :item="item" />
</template>
<view v-if="pageReady" class="cat-waterfall px-24rpx pb-32rpx">
<view class="waterfall-row flex gap-16rpx items-start">
<view
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>
<template #bottom>
<view class="h-24rpx" />
<view :style="[configStore.iosSafeBottomPlaceholder]" />
</template>
</z-paging>
</template>
<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>
File diff suppressed because it is too large Load Diff
@@ -56,16 +56,18 @@ function handleClickSearch() {
// 已存在:直接选择这个地址,走原有 chooseAddress 流程
chooseAddress(exist)
} else {
// 不存在:走新增地址流程
// 不存在:走新增地址流程(建筑类型首次在保存页以弹窗呈现)
addressStore.clearAddressInfo()
addressStore.setAddressLocation({
displayName: data.displayName,
formattedAddress: data.formattedAddress,
longitude: data.location.lng,
latitude: data.location.lat,
})
addressStore.pendingIntroBuildingType = true
setTimeout(() => {
uni.navigateTo({
url: '/pages/address/choose-type'
url: '/pages/address/save-address/other'
})
}, 300)
}
+404 -101
View File
@@ -1,23 +1,35 @@
<script setup lang="ts">
import {
appMerchantOrderDetailPost,
appCollectCollectPost,
type MerchantOrderVo,
appUserCardSelectDefaultPost, appMerchantOrderPayOrderPost, appMerchantOrderCancelOrderPost
appUserCardSelectDefaultPost,
appMerchantOrderPayOrderPost,
appMerchantOrderCancelOrderPost,
appMerchantOrderZipPayVoucherPost,
} from "@/service";
import { debounce } from 'throttle-debounce'
import ChooseImage from '@/components/choose-image/choose-image.vue'
const { t } = useI18n();
import OrderProgress from './components/order-progress.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 CancelOrder from "@/pages-store/pages/order/components/cancel-order.vue";
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 dayjs from 'dayjs'
import useEventEmit from "@/hooks/useEventEmit";
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>>();
// 打开价格明细
@@ -115,6 +127,45 @@ const orderStatus = computed(() => {
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) => {
if(!text) return
@@ -133,29 +184,6 @@ const callPhoneFn = (phone: string) => {
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({
orderId: '',
@@ -227,10 +255,130 @@ function openQrCode() {
function navigateTo(url: string) {
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>
<template>
<navbar />
<view class="order-detail-page">
<wd-navbar
safeAreaInsetTop
fixed
placeholder
:bordered="false"
custom-class="order-detail-navbar !bg-white"
@click-left="handleOrderDetailBack"
>
<template #left>
<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"
@@ -239,7 +387,7 @@ function navigateTo(url: string) {
<OrderDetailSkeleton />
</view>
<view
class="animate-in fade-in animate-ease-in animate-duration-300"
class="animate-in fade-in animate-ease-in animate-duration-300 bg-white"
v-if="!loading"
>
<!-- 已取消-成功取消 -->
@@ -262,34 +410,27 @@ function navigateTo(url: string) {
</view>
</template>
<!-- 商家拒绝订单 -->
<template v-if="orderStatus === OrderStatus.MERCHANT_REJECTED">
<view class="px-30rpx pt-20rpx pb-50rpx">
<template v-else-if="orderStatus === OrderStatus.MERCHANT_REJECTED">
<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-28rpx lh-28rpx text-#7D7D7D mt-26rpx">{{ t('pages-store.order.orderStatus.merchantRejectedDesc') }}</view>
</view>
</template>
<template v-else>
<view class="px-30rpx pt-20rpx pb-50rpx">
<view class="text-40rpx lh-36rpx text-#333 font-500">
<template v-if="orderDetail?.receiveMethod === 1">
{{ t('pages-store.order.estimatedDeliveryTime') }}
</template>
<template v-else>{{ t('pages-store.order.upTime') }}</template>
:
<!-- 订单预计送达时间-->
{{ formatTimestampShort(orderDetail?.endScheduledTime) }}
<!-- 顶栏已展示预计送达/下单时间此处仅保留提示类副文案 -->
<view class="px-30rpx pt-8rpx pb-24rpx" v-if="orderStatus === OrderStatus.PENDING_PAYMENT || orderDetail?.refundStatus === OrderCancelStatus.REJECTED">
<view class="text-28rpx lh-36rpx text-#7D7D7D" v-if="orderStatus === OrderStatus.PENDING_PAYMENT">
{{ t('pages-store.order.autoCancellation') }}
</view>
<view class="text-28rpx lh-28rpx text-#7D7D7D mt-26rpx">
<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">
<view class="text-28rpx lh-36rpx text-#7D7D7D mt-16rpx" v-if="orderDetail?.refundStatus === OrderCancelStatus.REJECTED">
{{ t('pages-store.order.rejectReason') }}{{ orderDetail?.rejectReason }}
</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
:steps="orderSteps"
:current-status="orderStatus"
@@ -350,58 +491,77 @@ function navigateTo(url: string) {
</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">
<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" />
</template>
</view>
</view>
</view>
<view class="w-full h-16rpx bg-#F6F6F6"></view>
</view>
<!-- 商品列表 -->
<view class="px-30rpx py-36rpx">
<!-- 商家信息 -->
<view @click="navigateTo('/pages-store/pages/store/index?id=' + orderDetail?.merchantVo?.id)" class="flex-center-sb h-80rpx mb-36rpx">
<view class="flex items-center">
<view class="goods-section px-30rpx py-24rpx">
<view class="goods-layout">
<view class="goods-left">
<!-- 门店行:图标 + 店铺名 + 右箭头 -->
<view
@click="navigateTo('/pages-store/pages/store/index?id=' + orderDetail?.merchantVo?.id)"
class="store-row-detail flex-center-sb mb-20rpx"
>
<view class="flex items-center min-w-0">
<image
:src="orderDetail?.merchantVo?.logo"
mode="aspectFill"
class="w-80rpx h-80rpx rounded-full bg-#F2F2F2 mr-24rpx shrink-0"
src="@img/chef/126.png"
class="w-40rpx h-40rpx shrink-0 mr-16rpx"
mode="aspectFit"
/>
<text class="text-30rpx lh-30rpx text-#333 font-500">{{ orderDetail?.merchantVo?.merchantName }}</text>
<text class="text-30rpx lh-36rpx text-#333 font-500 line-clamp-1">
{{ orderDetail?.merchantVo?.merchantName }}
</text>
</view>
<image
src="@img/chef/142.png"
class="w-32rpx h-32rpx shrink-0 ml-10rpx"
></image>
</view>
<collection :is-collected="orderDetail?.merchantVo?.isCollect" @collectionChange="handleCollectionClick" />
</view>
<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"
class="w-28rpx h-28rpx shrink-0"
mode="aspectFit"
/>
</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 }}
<!-- 商品缩略卡横向滚动 -->
<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="text-30rpx lh-30rpx text-#333 font-500">{{ `$${item.merchantDishVo?.discountPrice}` }}</text>
<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>
@@ -409,7 +569,7 @@ function navigateTo(url: string) {
<!-- 分隔线 -->
<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>
<!-- 收货地址 -->
@@ -441,7 +601,6 @@ function navigateTo(url: string) {
{{ orderDetail?.deliveryMethod }}
</view>
</view>
<!-- 联系电话 -->
<view
class="flex items-center py-36rpx px-30rpx"
>
@@ -457,7 +616,7 @@ function navigateTo(url: string) {
</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="flex-center-sb py-40rpx">
<view class="flex items-center">
@@ -486,7 +645,7 @@ function navigateTo(url: string) {
<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 @click="copyOrderNumber(orderDetail?.orderNo)" class="flex-center-sb text-30rpx lh-30rpx mb-40rpx">
@@ -501,11 +660,10 @@ function navigateTo(url: string) {
</view>
</view>
<!-- 下单时间 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>{{ formatTimestampWithMonthName(orderDetail?.createTime) }}</text>
</view>
<!-- 待支付 -->
<template v-if="orderStatus !== OrderStatus.PENDING_PAYMENT">
<!-- 支付方式 -->
<view class="flex-center-sb text-30rpx lh-30rpx mb-40rpx">
@@ -516,14 +674,9 @@ function navigateTo(url: string) {
mode="aspectFill"
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 class="flex-center-sb text-30rpx lh-30rpx">
<text>{{ t('pages-user.member.payTime') }}</text>
<text>{{ formatTimestampWithMonthName(orderDetail?.payTime) }}</text>
</view>
</template>
</view>
@@ -606,12 +759,21 @@ function navigateTo(url: string) {
</template>
</template>
<template v-else>
<!-- 待支付 -->
<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') }}
</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') }}
</view>
</template>
@@ -679,12 +841,153 @@ function navigateTo(url: string) {
<password-container @success="payPawSuccess" ref="passwordInputRef" />
<!-- 核销订单 -->
<use-code ref="useCodeRef" />
<!-- 支付凭证:相册/拍照,上传后回调真实 URL -->
<choose-image ref="voucherChooseRef" :count="1" @change="onVoucherImageUploaded" />
</view>
</view>
</template>
<style>
page {
background-color: #fff;
background-color: #F6F6F6;
}
</style>
<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>
@@ -1,6 +1,10 @@
<script setup lang="ts">
import {useConfigStore} from "@/store";
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 { t, locale } = useI18n()
@@ -57,17 +61,16 @@ function formatMoney(value: unknown) {
return n.toFixed(2)
}
function couponTitleText(item: any) {
function couponAmountText(item: any) {
const type = Number(item?.couponType)
const discountValue = Number(item?.discountValue)
// 标题保持原样:折扣券显示百分比,满减券只显示减免金额(不展示门槛)
if (type === 1) {
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) {
@@ -91,44 +94,58 @@ function couponBenefitText(item: any) {
</script>
<template>
<wd-popup v-model="show" custom-style="border-radius:0;" position="bottom" @close="handleClose">
<view class="bg-#F5F5F5 px-32rpx pt-30rpx">
<view class="text-40rpx lh-40rpx text-#333 font-bold text-center mb-60rpx">{{ t('pages-store.store.claimCoupon') }}</view>
<scroll-view scroll-y class="h-1000rpx">
<template v-for="item in couponList" :key="item.id">
<view class="coupon-item h-328rpx flex flex-col mb-30rpx last:mb-0">
<view class="flex-1 pt-40rpx px-58rpx">
<view class="line-clamp-1 text-34rpx lh-34rpx text-#333 font-bold">
<!-- couponType 1-折扣券, 2-满减券-->
{{ couponTitleText(item) }}
<wd-popup v-model="show" custom-style="border-radius: 24rpx 24rpx 0 0;" position="bottom" @close="handleClose">
<view class="coupon-popup-wrap">
<view class="text-36rpx lh-36rpx text-#333 font-bold text-center pt-40rpx pb-36rpx">
{{ t('pages-store.store.claimCoupon') }}
</view>
<view class="text-24rpx lh-32rpx text-#999 my-18rpx">
{{ t('pages-store.store.validDays') }}: {{ item.validDays }}{{ daySuffix(item.validDays) }}
</view>
<view class="text-24rpx lh-32rpx text-#999 my-18rpx">
{{ item.nameZh }}
</view>
<view class="text-28rpx lh-28rpx text-#333 flex items-center">
<scroll-view scroll-y class="coupon-popup-list">
<view
v-for="item in couponList"
:key="item.id"
class="coupon-card"
>
<image
class="w-36rpx h-36rpx shrink-0 mr-10rpx"
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 class="coupon-card-desc">
{{ item.nameZh }}
<text class="ml-16rpx">{{ t('pages-store.store.validDays') }}:{{ item.validDays }}{{ daySuffix(item.validDays) }}</text>
</view>
<view class="coupon-card-benefit">
<image
class="coupon-benefit-icon"
src="@img/chef/106.png"
></image>
{{ t('pages-store.store.get') }} {{ couponBenefitText(item) }}
<template v-if="Number(item?.couponType) === 1"> {{ t('pages-store.store.couponOff') }}</template>
<text>{{ t('pages-store.store.get') }} {{ couponBenefitText(item) }}</text>
</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
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>
</template>
</view>
</template>
</scroll-view>
<view :style="[configStore.iosSafeBottomPlaceholder]" />
@@ -137,10 +154,126 @@ function couponBenefitText(item: any) {
</template>
<style scoped lang="scss">
.coupon-item {
background-image: url('@img/chef/103.png');
background-size: 100% 100%;
background-repeat: no-repeat;
background-position: center;
.coupon-popup-wrap {
background: #F5F5F5;
padding: 0 32rpx;
}
.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>
@@ -1,94 +1,80 @@
<template>
<view class="store-skeleton">
<!-- 顶部图片区域 -->
<view class="relative h-562rpx">
<!-- 状态栏 -->
<!-- 顶部导航栏 -->
<view class="fixed top-0 left-0 z-9 w-full pt-6rpx">
<view class="flex-center-sb px-30rpx">
<view class="back-button-skeleton 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>
<status-bar />
<view class="nav-bar">
<view class="nav-btn skeleton-item"></view>
<view class="nav-btn skeleton-item"></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 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="center mb-16rpx">
<view class="rating-skeleton skeleton-item mr-20rpx"></view>
<view class="chef-link-skeleton skeleton-item mr-20rpx"></view>
<view class="distance-skeleton skeleton-item"></view>
<view class="name-skeleton skeleton-item mb-16rpx"></view>
<!-- 评分 + CHEFLINK -->
<view class="flex items-center mb-12rpx">
<view class="rating-skeleton skeleton-item mr-16rpx"></view>
<view class="cheflink-skeleton skeleton-item"></view>
</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 class="sales-text-skeleton skeleton-item"></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 class="flex items-center mt-24rpx">
<view class="coupon-tag-skeleton skeleton-item mr-16rpx"></view>
<view class="coupon-tag-skeleton skeleton-item mr-16rpx"></view>
<view class="coupon-tag-skeleton skeleton-item"></view>
<view class="flex-1"></view>
<view class="claim-skeleton skeleton-item"></view>
</view>
</view>
<!-- 标签页导航 -->
<view class="tabs-skeleton">
<view class="flex">
<!-- 分类胶囊标签 -->
<view class="flex items-center px-30rpx pb-24rpx">
<view
v-for="i in 5"
v-for="i in 4"
:key="i"
class="tab-item-skeleton skeleton-item mr-40rpx"
class="tab-chip-skeleton skeleton-item"
></view>
</view>
<view class="tabs-divider skeleton-item"></view>
</view>
<!-- 商品列表区域 -->
<!-- 商品列表 -->
<view class="px-30rpx">
<!-- 分类标题 -->
<view class="section-title-skeleton skeleton-item my-36rpx"></view>
<!-- 商品网格 -->
<view class="grid grid-cols-2 gap-30rpx">
<view class="grid grid-cols-2 gap-24rpx">
<view
v-for="i in 6"
v-for="i in 4"
:key="i"
class="product-item-skeleton"
class="product-skeleton"
>
<!-- 商品图片 -->
<view class="product-image-skeleton skeleton-item mb-28rpx"></view>
<!-- 商品名称 -->
<view class="product-name-skeleton skeleton-item mb-12rpx"></view>
<!-- 价格行 -->
<view class="flex-center-sb mb-12rpx">
<view class="product-img-skeleton skeleton-item"></view>
<!-- 价格 + 销量 -->
<view class="flex items-center justify-between mt-16rpx">
<view class="price-skeleton skeleton-item"></view>
<view class="member-price-skeleton skeleton-item"></view>
</view>
<!-- 原价和销量 -->
<view class="flex-center-sb">
<view>
<view class="original-price-skeleton skeleton-item mb-8rpx"></view>
<view class="sales-skeleton skeleton-item"></view>
</view>
<view class="add-button-skeleton skeleton-item"></view>
<!-- 商品名称 -->
<view class="product-name-skeleton skeleton-item mt-8rpx"></view>
<!-- 会员价 + 加购按钮 -->
<view class="flex items-center justify-between mt-12rpx">
<view class="member-skeleton skeleton-item"></view>
<view class="add-btn-skeleton skeleton-item"></view>
</view>
</view>
</view>
@@ -97,25 +83,18 @@
</template>
<script setup lang="ts">
// Store页面骨架屏组件
</script>
<style scoped lang="scss">
// 通用骨架屏样式
.skeleton-item {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
// 闪烁动画
@keyframes shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
.store-skeleton {
@@ -123,186 +102,126 @@
min-height: 100vh;
}
// 通用布局类
.flex-center-sb {
/* 顶部导航 */
.nav-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 30rpx;
height: 88rpx;
}
.center {
display: flex;
align-items: center;
justify-content: center;
}
// 状态栏区域
.status-bar-skeleton {
width: 100%;
height: 44rpx;
margin-bottom: 20rpx;
}
.back-button-skeleton {
.nav-btn {
width: 48rpx;
height: 48rpx;
border-radius: 24rpx;
}
.action-button-skeleton {
width: 68rpx;
height: 68rpx;
border-radius: 34rpx;
/* 店铺 Logo */
.logo-skeleton {
width: 128rpx;
height: 128rpx;
border-radius: 24rpx;
}
// 主图区域
.main-image-skeleton {
border-radius: 0;
}
// 店铺信息区域
.store-name-skeleton {
width: 400rpx;
height: 40rpx;
/* 店铺信息 */
.name-skeleton {
width: 360rpx;
height: 44rpx;
border-radius: 8rpx;
}
.rating-skeleton {
width: 160rpx;
height: 24rpx;
border-radius: 6rpx;
}
.cheflink-skeleton {
width: 120rpx;
height: 24rpx;
border-radius: 6rpx;
}
.chef-link-skeleton {
width: 100rpx;
.sales-text-skeleton {
width: 140rpx;
height: 24rpx;
border-radius: 6rpx;
}
.distance-skeleton {
width: 80rpx;
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 {
/* 配送信息卡片 */
.delivery-card-skeleton {
width: 100%;
height: 164rpx;
height: 140rpx;
border-radius: 20rpx;
}
// 标签页导航
.tabs-skeleton {
padding: 0 30rpx;
.tab-item-skeleton {
/* 优惠券标签 */
.coupon-tag-skeleton {
width: 120rpx;
height: 30rpx;
border-radius: 6rpx;
}
.tabs-divider {
width: 100%;
height: 10rpx;
margin-top: 32rpx;
border-radius: 0;
}
}
// 分类标题
.section-title-skeleton {
width: 200rpx;
height: 40rpx;
height: 52rpx;
border-radius: 8rpx;
flex-shrink: 0;
}
// 商品项
.product-item-skeleton {
.product-image-skeleton {
.claim-skeleton {
width: 100rpx;
height: 28rpx;
border-radius: 6rpx;
flex-shrink: 0;
}
/* 胶囊标签 */
.tab-chip-skeleton {
width: 100rpx;
height: 60rpx;
border-radius: 30rpx;
margin-right: 20rpx;
flex-shrink: 0;
&:last-child {
margin-right: 0;
}
}
/* 商品卡片 */
.product-skeleton {
margin-bottom: 24rpx;
}
.product-img-skeleton {
width: 100%;
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;
width: 120rpx;
height: 36rpx;
border-radius: 6rpx;
}
.sales-skeleton {
width: 100rpx;
height: 28rpx;
width: 80rpx;
height: 22rpx;
border-radius: 6rpx;
}
.add-button-skeleton {
width: 64rpx;
height: 64rpx;
border-radius: 32rpx;
}
.product-name-skeleton {
width: 85%;
height: 34rpx;
border-radius: 6rpx;
}
// 分隔线
.divider-skeleton {
width: 100%;
height: 10rpx;
border-radius: 0;
.member-skeleton {
width: 160rpx;
height: 42rpx;
border-radius: 8rpx;
}
// 底部提示
.bottom-tip-skeleton {
height: 96rpx;
border-radius: 0;
}
// 响应式设计
@media (max-width: 750rpx) {
.grid {
gap: 20rpx;
}
.product-item-skeleton {
.product-image-skeleton {
height: 200rpx;
}
}
.add-btn-skeleton {
width: 52rpx;
height: 52rpx;
border-radius: 26rpx;
}
</style>
File diff suppressed because it is too large Load Diff
+438 -187
View File
@@ -423,10 +423,7 @@ function navigateBack() {
function navigateToCart() {
uni.navigateTo({
url:
'/pages-user/pages/cart/store-cart'
+ '?storeId=' + storeID.value
+ '&storeName=' + encodeURIComponent(storeDetail.value.merchantName),
url: '/pages-user/pages/cart/index',
})
}
@@ -461,6 +458,16 @@ function navigateTo(url: string) {
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() {
uni.shareWithSystem({
@@ -481,74 +488,54 @@ function handleShare() {
class="animate-in fade-in animate-ease-out animate-duration-300"
v-show="loading"
>
<!-- 骨架屏 -->
<StoreSkeleton/>
</view>
<view
class="animate-in fade-in animate-ease-in animate-duration-300"
v-if="!loading"
>
<!-- 页面内容 -->
<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' : '']">
<status-bar />
<view class="flex-center-sb px-30rpx">
<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"
/>
<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"
class="w-48rpx h-48rpx 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 />
<wd-swiper class="bg-common" v-if="storeDetail && storeDetail?.shopImages?.split(',').length > 0" :list="storeDetail?.shopImages?.split(',')" height="562rpx" autoplay></wd-swiper>
<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-40rpx pb-42rpx">
<!-- 店铺信息 -->
<!-- 店铺信息区域 -->
<view class="px-30rpx pt-24rpx pb-30rpx">
<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 }}
</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">
<text class="text-#333 font-500">{{ storeDetail?.rating }}</text>
<image
@@ -564,46 +551,48 @@ function handleShare() {
></image>
CHEFLINK
</view>
<text v-if="+storeDetail?.deliveryService === 1" class="text-#7D7D7D">
{{ storeDetail?.deliveryTime }}{{ daySuffix(storeDetail?.deliveryTime) }}
</text>
</view>
<view class="text-24rpx lh-24rpx text-#7D7D7D text-center mt-16rpx">
{{ t('pages-store.store.title') }} US${{ storeDetail?.minOrderPrice }}
</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">
<!-- 总销量 -->
<view class="text-24rpx lh-24rpx text-#7D7D7D text-center mt-12rpx">
{{ 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 class="py-24rpx mt-40rpx border-top border-bottom" v-if="storeCouponList.length">
<view class="flex items-center justify-between mb-24rpx">
<text class="text-28rpx lh-28rpx font-500 text-#333">{{ t('pages-store.store.merchantDiscounts') }}</text>
<view @click="handleClaimNow" class="flex items-center">
<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 v-if="showDeliverySwitch" class="delivery-info-card">
<template v-if="+storeDetail?.deliveryService === 1 && deliveryMethod === 0">
<view class="delivery-info-left">
<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="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-x
class="coupon-scroll"
class="coupon-scroll flex-1"
:show-scrollbar="false"
enable-flex
>
@@ -619,109 +608,106 @@ function handleShare() {
</view>
</view>
</scroll-view>
<view @click="handleClaimNow" class="flex items-center shrink-0 ml-16rpx">
<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>
</view>
<!-- 新功能插入区域 end -->
<!-- 自取 配送切换 -->
<!-- <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>
<!-- 分类标签胶囊样式 -->
<scroll-view scroll-x class="w-full" :show-scrollbar="false" enable-flex>
<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>
<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>
</scroll-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>
<view v-if="currentDishList.length > 0" class="grid grid-cols-2 gap-30rpx">
<!-- 商品列表 -->
<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">
<view @click="navigateToDishes(item)" class="w-100% mb-10rpx">
<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 @click="navigateToDishes(item)" class="dish-card" :class="{ 'dish-card--soldout': isSoldOutStock(item?.stock) }">
<!-- 商品图片 -->
<view class="dish-card-image">
<!-- NEW 绑带标签 -->
<view v-if="item.isNew == 1" class="dish-new-ribbon">
<text class="dish-new-ribbon__text">NEW</text>
</view>
<view @click.stop="handleDishCollectionClick(item)" class="w-56rpx h-56rpx absolute z-4 top-12rpx right-12rpx center">
<image
v-if="!item.isCollect"
src="@img-store/1334.png"
mode="aspectFill"
class="w-full h-full"
class="w-44rpx h-44rpx"
style="filter: drop-shadow(0 2rpx 6rpx rgba(0,0,0,0.18))"
/>
<image
v-else
src="@img-store/1337.png"
mode="aspectFill"
class="w-full h-full"
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="w-full h-full rounded-24rpx bg-common"
v-if="item.dishImage.split(',').length > 0"
/>
<image
:src="item?.dishImage"
mode="aspectFill"
class="w-full h-full rounded-24rpx bg-common"
v-else
class="dish-card-img"
/>
</view>
<view class="line-clamp-1 text-30rpx text-#333 font-500">
<!-- 卡片信息区 -->
<view class="dish-card-body">
<!-- 价格 + 销量 -->
<view class="flex items-start justify-between gap-12rpx mb-14rpx">
<view class="min-w-0 flex-1">
<text class="dish-price">$ {{ item.discountPrice }}</text>
<text
v-if="Number(item?.originalPrice) > Number(item?.discountPrice)"
class="dish-original-price"
>$ {{ item.originalPrice }}</text>
</view>
<text class="dish-sales shrink-0">{{ t('pages-store.store.sales') }}{{ item.salesCount }}</text>
</view>
<!-- 商品名称 -->
<view class="dish-title line-clamp-2 mb-16rpx">
{{ item.dishName }}
</view>
<view class="flex-center-sb mt-12rpx">
<text class="text-26rpx lh-30rpx text-#333 font-500">US${{ item.discountPrice }}</text>
<!-- 会员价 + 加购按钮 -->
<view class="flex items-center justify-between gap-12rpx">
<view
v-if="Number(item.memberPrice) > 0"
class="member-price-tag text-[#FBE3C3] font-500 text-28rpx lh-28rpx center pl-6rpx break-all"
class="dish-member shrink min-w-0"
>
<text class="!text-24rpx">{{ t('pages-store.store.members') }}: </text>
${{ item.memberPrice }}
<text class="dish-member-inner">{{ t('pages-store.store.members') }}: $ {{ item.memberPrice }}</text>
</view>
</view>
<view class="flex-center-sb mt-12rpx">
<view class="text-28rpx text-#999">
<view class="line-through">US${{ item.originalPrice }}</view>
<view>{{ t('pages-store.store.sales') }}:{{ item.salesCount }}</view>
</view>
<view class="center w-64rpx h-64rpx rounded-50% bg-white shadow-lg">
<view v-else class="flex-1 min-w-0"></view>
<view class="dish-add-btn center shrink-0">
<image
src="@img/chef/1285.png"
class="w-30rpx h-30rpx shrink-0"
class="w-28rpx h-28rpx"
></image>
</view>
</view>
<!-- 营销标签 -->
<view
v-if="getDishPromoLabel(item)"
class="dish-promo mt-16rpx"
>
<text class="dish-promo-text">{{ getDishPromoLabel(item) }}</text>
</view>
</view>
</view>
</template>
</view>
@@ -731,7 +717,29 @@ function handleShare() {
</view>
</template>
</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
src="@img/chef/1289.png"
class="mr-10rpx w-32rpx h-32rpx shrink-0"
@@ -741,16 +749,7 @@ function handleShare() {
</text>
</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>
<coupon-popup
ref="couponPopupRef"
@@ -758,43 +757,51 @@ function handleShare() {
@confirm="getMerchantCouponReceiveList"
/>
</template>
<style>
page {
background-color: #fff;
background-color: #F6F6F6;
}
</style>
<style scoped lang="scss">
:deep(.wd-swiper__track) {
border-radius: 0;
}
:deep(.wd-tabs__nav-container) {
.is-active {
color: #333 !important;
font-weight: 500 !important;
}
}
:deep(.wd-tabs__nav-item-text) {
font-size: 30rpx !important;
//color: #7D7D7D;
padding-bottom: 32rpx !important;
/* ====== 配送信息卡片 ====== */
.delivery-info-card {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 30rpx;
padding: 24rpx 40rpx;
border: 2rpx solid #D8D8D8;
border-radius: 20rpx;
min-height: 140rpx;
}
:deep(.wd-tabs__line) {
border-radius: 0 !important;
height: 10rpx !important;
background-color: #333333 !important;
}
.box {
border-bottom: 10rpx solid #F6F6F6 !important;
}
.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-left {
flex: 1;
text-align: center;
font-size: 24rpx;
line-height: 36rpx;
color: #CE7138;
padding-right: 30rpx;
}
.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 {
width: 100%;
}
@@ -806,7 +813,7 @@ page {
}
.coupon-item {
margin-right: 20rpx;
margin-right: 15rpx;
flex-shrink: 0;
&:last-child {
@@ -815,12 +822,12 @@ page {
}
.coupon-tag {
min-width: 120rpx;
min-width: 110rpx;
height: 52rpx;
display: flex;
align-items: center;
justify-content: center;
padding: 0 28rpx;
padding: 0 18rpx;
position: relative;
background-image: url("/static/images/5008.png");
background-size: 100% 100%;
@@ -834,4 +841,248 @@ page {
font-weight: 400;
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>
+37 -1
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">
<template v-for="item in dataList">
<view @click="navigateToDishes(item)" class="w-330rpx overflow-hidden">
<view class="search-result-img-wrap">
<view v-if="item.isNew == 1" class="dish-new-ribbon">
<text class="dish-new-ribbon__text">NEW</text>
</view>
<image
:src="item?.dishImage?.split(',')[0]"
class="w-full h-186rpx rounded-24rpx mb-16rpx"
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"
>{{ item.dishName }}</text
>
@@ -60,5 +65,36 @@ function navigateToDishes(item: any) {
</template>
<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>
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

+1 -1
View File
@@ -106,7 +106,7 @@ onShow(()=> {
<view class="bg-white rounded-36rpx overflow-hidden mt-18rpx py-30rpx">
<view class="flex-center-sb pr-20rpx mb-8rpx">
<view class="flex items-center">
<view class="w-8rpx h-30rpx bg-#00A76D"></view>
<view class="w-8rpx h-40rpx bg-#00A76D" style="margin-left: 10rpx;border-radius: 20rpx;"></view>
<text class="ml-10rpx text-34rpx text-#333">{{ t('pages-user.balance.detail-list') }}</text>
</view>
<view class="flex items-center">
@@ -25,9 +25,9 @@ defineExpose({
@close="handleClose"
>
<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">
Are you sure you want to delete the shopping cart?
{{ t("pages-user.cart.removeCartDesc") }}
</view>
<view class="mt-70rpx">
<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 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) {
if (title) {
goodsName.value = title;
@@ -31,9 +45,9 @@ defineExpose({
@close="handleClose"
>
<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">
Are you sure you want to remove {{ goodsName }} from your shopping cart?
{{ removeProductDescText }}
</view>
<view class="mt-70rpx">
<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)
}
function normalizeInputNumberValue(payload: any): number {
if (typeof payload === 'number') {
return payload
}
if (payload && typeof payload === 'object') {
if (typeof payload.detail?.value === 'number') {
return payload.detail.value
}
if (typeof payload.value === 'number') {
return payload.value
}
}
const numeric = Number(payload)
return Number.isNaN(numeric) ? 0 : numeric
/** wd-input-number change 为 { value },兼容 detail.value */
function normalizeInputNumberValue(payload: unknown): number {
const raw =
typeof payload === "number"
? payload
: (payload as any)?.value ?? (payload as any)?.detail?.value ?? payload;
const n = Number(raw);
return Number.isFinite(n) ? n : 0;
}
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) {
uni.navigateTo({ url })
}
@@ -301,21 +307,21 @@ function navigateTo(url: string) {
<view class="price-row flex items-center mt-18rpx">
<text class="current-price text-[#333333] text-30rpx font-normal"
>${{ item.merchantDishVo.discountPrice }}</text
>${{ getCartItemDiscountPrice(item) }}</text
>
<text
v-if="item.merchantDishVo.originalPrice"
v-if="getCartItemOriginalPrice(item)"
class="original-price text-[#7D7D7D] text-24rpx font-normal line-through ml-10rpx"
>${{ item.merchantDishVo.originalPrice }}</text
>${{ getCartItemOriginalPrice(item) }}</text
>
<!-- 会员价标签 -->
<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"
>
<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>
@@ -1,11 +1,13 @@
<script setup lang="ts">
import { debounce } from 'throttle-debounce'
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 {CollectionType} from "@/constant/enums";
const {t} = useI18n()
const emit = defineEmits<{
(e: 'batch-delete-success'): void
}>()
const props = defineProps({
currentIndex: {
type: Number,
@@ -15,6 +17,18 @@ const props = defineProps({
type: Number,
default: 0,
},
batchDeleteMode: {
type: Boolean,
default: false,
},
})
const selectedIds = ref<string[]>([])
watch(() => props.batchDeleteMode, (v) => {
if (!v) {
selectedIds.value = []
}
})
// (1, "菜谱")(2, "菜品")(3, "配菜")(4, "商家")
@@ -35,7 +49,7 @@ const {paging, dataList, queryList, loading} = usePage<any>((pageNum: number, pa
pageSize,
},
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) {
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) => {
// 收藏接口
@@ -94,6 +202,7 @@ function refresh() {
defineExpose({
reload,
refresh,
runBatchDelete,
})
</script>
@@ -101,71 +210,66 @@ defineExpose({
<view class="h-full">
<z-paging ref="paging" v-model="dataList" @query="queryList" :fixed="false" :auto="false">
<view class="p-30rpx">
<template v-if="currentIndex == 0">
<!--商家-->
<view v-for="item in dataList" :key="item.id">
<food-box :item="item.merchantVo" />
</view>
</template>
<template v-if="currentIndex == 1">
<!--菜品-->
<view class="grid grid-cols-2 gap-30rpx">
<template v-for="item in dataList">
<view @click="navigateToDishes(item.merchantDishVo)" class="w-100% mb-10rpx rounded-16rpx">
<view class="relative h-248rpx rounded-24rpx mb-28rpx">
<view @click.stop="handleDishCollectionClick(item.merchantDishVo)" class="w-68rpx h-68rpx absolute z-2 top-0 right-0">
<image
v-if="!item.merchantDishVo.isCollect"
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>
<template v-if="currentIndex == 0 || currentIndex == 1">
<view class="collection-list">
<view
v-if="Number(item.merchantDishVo.memberPrice) > 0"
class="member-price-tag text-[#FBE3C3] text-18rpx center pl-6rpx"
v-for="item in dataList"
:key="item.id"
class="collection-item"
@click="onStoreOrDishRowClick(item)"
>
{{ t('pages-store.store.members') }}: ${{ item.merchantDishVo.memberPrice }}
<view
v-if="batchDeleteMode"
class="collection-item__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 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>
: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>
</template>
</view>
</template>
<template v-if="currentIndex == 2">
<view class="grid grid-cols-2 gap-30rpx">
<view v-for="item in dataList">
<view @click="navigateToRecipeDetail(item.id)" class="w-312rpx">
<view v-for="item in dataList" :key="item.id" class="recipe-card-wrap">
<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
:src="item.merchantRecipeVo?.recipeImage?.split(',')[0]"
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"
>{{ 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
:is-collected="item.merchantRecipeVo?.isCollect"
@collectionChange="handleSubmitCollectRecipe(item)"
@@ -192,5 +296,123 @@ defineExpose({
</template>
<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>
+189 -9
View File
@@ -1,6 +1,26 @@
<script setup lang="ts">
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 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 segmentedList = [
'Store',
@@ -8,26 +28,106 @@ const segmentedList = [
'Recipe',
]
function handleSwiperChange(e) {
function handleSwiperChange(e: any) {
segmentedValue.value = e.detail.current;
}
const orderSwiperListRef = ref()
const batchDeleteMode = ref(false)
onMounted(()=> {
nextTick(()=> {
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>
<template>
<view>
<view class="collection-page">
<z-paging-swiper>
<template #top>
<navbar/>
<view class="px-30rpx">
<view class="text-46rpx lh-46rpx text-#333 font-bold mb-52rpx">{{ t('pages.mine.collection') }}</view>
<l-segmented v-model="segmentedValue" :options="segmentedList" shape="round" bg-color="#F2F2F2" active-color="#333" />
<status-bar />
<view class="header-wrap px-30rpx pt-14rpx">
<view class="flex items-center justify-between mb-34rpx">
<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>
</template>
@@ -35,15 +135,95 @@ onMounted(()=> {
:current="segmentedValue"
@change="handleSwiperChange">
<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>
</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>
</template>
<style>
<style scoped lang="scss">
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>
+229 -63
View File
@@ -24,9 +24,37 @@ const createValidationSchema = () => {
}
const chooseImageRef = ref()
const showNoticePopup = ref(true)
const confirmCountdown = ref(5)
let countdownTimer: ReturnType<typeof setInterval> | null = null
// 校验单个字段
const validateField = (field: keyof typeof form.value) => {
function startNoticeCountdown() {
if (countdownTimer) {
clearInterval(countdownTimer)
}
confirmCountdown.value = 5
countdownTimer = setInterval(() => {
if (confirmCountdown.value <= 1) {
confirmCountdown.value = 0
if (countdownTimer) {
clearInterval(countdownTimer)
countdownTimer = null
}
return
}
confirmCountdown.value -= 1
}, 1000)
}
function handleCloseNoticePopup() {
if (confirmCountdown.value > 0) {
return
}
showNoticePopup.value = false
}
// 校验单个字段(只校验 schema 中存在的字段)
const validateField = (field: 'content' | 'contactPhone') => {
try {
const schema = createValidationSchema()
const fieldSchema = schema.shape[field]
@@ -88,79 +116,217 @@ const handleChooseImage = () => {
function onImageChange(files: string[]) {
form.value.images = files[0]
}
onLoad(() => {
startNoticeCountdown()
})
onUnload(() => {
if (countdownTimer) {
clearInterval(countdownTimer)
countdownTimer = null
}
})
</script>
<template>
<view class="complaints-root">
<navbar :title="t('pages-user.complaints.title')" />
<view class="px-18rpx">
<view class="mb-27rpx mt-32rpx text-28rpx text-#333">
<view class="complaints-page">
<view class="form-card">
<view class="field">
<view class="field__label">{{ t('pages-user.complaints.feedback-content') }}</view>
<view class="field__box field__box--textarea">
<wd-textarea v-model="form.content" :maxlength="500" custom-class="!bg-transparent"
custom-textarea-container-class="!bg-transparent" no-border auto-height
:placeholder="t('pages-user.complaints.feedback-content-placeholder')" @blur="validateField('content')" />
</view>
</view>
<view class="field field--mt">
<view class="field__label">{{ t('pages-user.complaints.image') }}</view>
<view class="upload" @click="handleChooseImage">
<image v-if="form.images" src="@img/chef/113.png" class="upload__remove"></image>
<image v-if="!form.images" src="@img/chef/112.png" class="upload__placeholder"></image>
<image v-else :src="form.images" class="upload__image" mode="aspectFill"></image>
</view>
</view>
<view class="field field--mt">
<view class="field__label">{{ t('pages-user.complaints.contact-information') }}</view>
<view class="field__box field__box--input">
<wd-input v-model.trim="form.contactPhone" no-border custom-class="!p-0 !bg-transparent"
custom-input-class="complaints-input" :placeholder="t('pages-user.complaints.contact-information-placeholder')" :maxlength="50"
@blur="validateField('contactPhone')" />
</view>
</view>
</view>
<view class="complaints-page__desc">
{{ t('pages-user.complaints.description') }}
</view>
<view class="bg-white rounded-14rpx px-18rpx pt-30rpx pb-24rpx">
<view class="text-28rpx text-#333 mb-28rpx">{{ t('pages-user.complaints.feedback-content') }}</view>
<view
class="min-h-234rpx box-border bg-#F6F6F6 rounded-14rpx overflow-hidden px-18rpx py-10rpx"
>
<wd-textarea
v-model="form.content"
:maxlength="500"
custom-class="!bg-#F6F6F6"
custom-textarea-container-class="!bg-#F6F6F6"
no-border
auto-height
:placeholder="t('pages-user.complaints.feedback-content-placeholder')"
@blur="validateField('content')"
/>
<!-- 底部固定提交按钮 -->
<view class="bottom-actions">
<wd-button custom-class="submit-btn" block @click="handleSubmit">
{{ t('common.submit') }}
</wd-button>
</view>
<ChooseImage ref="chooseImageRef" @change="onImageChange" />
<view>
<view class="text-28rpx text-#333 mt-32rpx mb-24rpx">{{ t('pages-user.complaints.image') }}:</view>
<view @click="handleChooseImage" class="relative w-210rpx h-210rpx">
<image
v-if="form.images"
src="@img/chef/113.png"
class="absolute top--10rpx right--10rpx z-10 w-36rpx h-36rpx"
></image>
<image
v-if="!form.images"
src="@img/chef/112.png"
class="w-210rpx h-210rpx"
></image>
<image
v-else
:src="form.images"
class="w-210rpx h-210rpx rounded-16rpx"
mode="aspectFill"
></image>
</view>
</view>
</view>
<view class="mt-14rpx flex-center-sb bg-white h-86rpx rounded-14rpx px-18rpx">
<view class="text-28rpx text-#333 ">{{ t('pages-user.complaints.contact-information') }}:</view>
<wd-input
no-border
custom-class="!p-[12rpx+20rpx] rounded-10rpx"
v-model.trim="form.contactPhone"
placeholderStyle="font-size: 28rpx;line-height: 40rpx;color: #B1B1B1; text-align: right;"
:placeholder="t('common.enter')"
:maxlength="50"
custom-input-class="text-right"
@blur="validateField('contactPhone')"
/>
</view>
<view class="mt-38rpx text-28rpx text-#999">
<wd-popup v-model="showNoticePopup" :close-on-click-modal="false" custom-class="!bg-transparent">
<view class="notice-dialog">
<view class="notice-dialog__title">{{ t('pages-user.complaints.title') }}</view>
<view class="notice-dialog__content">
{{ t('pages-user.complaints.contact-information-tip') }}
</view>
<fixed-bottom-large-btn
class="z-100"
fixed
:text="`${t('common.submit')}`"
@click="handleSubmit"
/>
<ChooseImage ref="chooseImageRef" @change="onImageChange" />
<wd-button
custom-class="notice-dialog__btn"
block
:disabled="confirmCountdown > 0"
@click="handleCloseNoticePopup"
>
{{ confirmCountdown > 0 ? `${t('common.gotIt')} (${confirmCountdown}s)` : t('common.gotIt') }}
</wd-button>
</view>
</wd-popup>
</view>
</view>
</template>
<style lang="scss" scoped>
.complaints-page {
padding: 32rpx 30rpx calc(160rpx + env(safe-area-inset-bottom));
background: #fff;
min-height: 100vh;
box-sizing: border-box;
}
.complaints-page__desc {
font-size: 22rpx;
line-height: 36rpx;
color: #333;
margin-bottom: 18rpx;
text-align: center;
}
.form-card {
background: #fff;
border-radius: 24rpx;
padding: 26rpx 24rpx 18rpx;
}
.field__label {
font-size: 26rpx;
line-height: 36rpx;
color: #333;
font-weight: 700;
margin-bottom: 16rpx;
}
.field--mt {
margin-top: 26rpx;
}
.field__box {
border: 1rpx solid #ececec;
border-radius: 18rpx;
background: #fff;
overflow: hidden;
}
.field__box--textarea {
padding: 14rpx 16rpx;
min-height: 240rpx;
box-sizing: border-box;
}
.field__box--input {
padding: 18rpx 16rpx;
box-sizing: border-box;
}
:deep(.complaints-input) {
text-align: left;
font-size: 26rpx;
line-height: 36rpx;
color: #333;
}
.upload {
width: 176rpx;
height: 176rpx;
border: 1rpx solid #ececec;
border-radius: 18rpx;
overflow: hidden;
position: relative;
background: #fff;
}
.upload__placeholder,
.upload__image {
width: 176rpx;
height: 176rpx;
}
.upload__remove {
position: absolute;
top: -10rpx;
right: -10rpx;
z-index: 2;
width: 36rpx;
height: 36rpx;
}
.bottom-actions {
position: fixed;
left: 30rpx;
right: 30rpx;
bottom: calc(36rpx + env(safe-area-inset-bottom));
z-index: 20;
}
:deep(.submit-btn) {
height: 108rpx !important;
border-radius: 999rpx !important;
background: #111 !important;
color: #fff !important;
font-size: 32rpx !important;
font-weight: 800 !important;
letter-spacing: 0.06em;
}
.notice-dialog {
width: 620rpx;
background: #fff;
border-radius: 24rpx;
padding: 36rpx 28rpx 28rpx;
box-sizing: border-box;
}
.notice-dialog__title {
text-align: center;
font-size: 30rpx;
line-height: 40rpx;
color: #222;
font-weight: 700;
}
.notice-dialog__content {
margin-top: 18rpx;
margin-bottom: 28rpx;
font-size: 26rpx;
line-height: 38rpx;
color: #666;
// text-align: center;
}
:deep(.notice-dialog__btn) {
height: 84rpx !important;
border-radius: 999rpx !important;
font-size: 28rpx !important;
font-weight: 700 !important;
}
</style>
+27 -6
View File
@@ -42,6 +42,26 @@ function formatCouponDetail(item: any) {
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) {
return appCouponUserCouponListPost({
params: {
@@ -101,11 +121,11 @@ function handleSubmit() {
bg-color="#fff"
>
<template #top>
<navbar />
<navbar :title="t('pages-user.coupon.title')" />
<view class="px-30rpx pb-42rpx">
<view class="text-46rpx text-#333 font-bold lh-46rpx mb-36rpx mt-10rpx">
<!-- <view class="text-46rpx text-#333 font-bold lh-46rpx mb-36rpx mt-10rpx">
{{ t("pages-user.coupon.title") }}</view
>
> -->
<view
class="flex items-center h-88rpx px-30rpx bg-#F2F3F6 rounded-88rpx box-border"
>
@@ -131,10 +151,11 @@ function handleSubmit() {
</view>
</template>
<view class="px-30rpx my-38rpx" v-if="isSearch">
<view class="px-30rpx my-34rpx" style="position: fixed; bottom: 10rpx; left: 0; right: 0; z-index: 100;" v-if="isSearch">
<wd-button
block
custom-class="!h-108rpx !text-36rpx !rounded-16rpx"
custom-class="!h-98rpx !text-32rpx !rounded-46rpx"
style="background-color: #000;"
:disabled="isDisabled"
@click="handleSubmit"
>
@@ -149,7 +170,7 @@ function handleSubmit() {
{{ item.snapshotNameZh }}
</view>
<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 class="text-24rpx lh-32rpx text-#999 my-18rpx">
{{ dayjs(Number(item.snapshotValidEnd)).format('YYYY-MM-DD HH:mm') }}{{ isEnLocale() ? ' expires' : '到期' }}
+16 -27
View File
@@ -1,3 +1,11 @@
<!--
* @Author: ISFP_T 68358856@qq.com
* @Date: 2026-02-25 10:02:44
* @LastEditors: ISFP_T 68358856@qq.com
* @LastEditTime: 2026-04-01 15:51:38
* @FilePath: \chef-link-uniapp\src\pages-user\pages\edit-nickname\index.vue
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
-->
<script lang="ts" setup>
import Config from '@/config'
import { useUserStore } from '@/store'
@@ -61,43 +69,24 @@ const handleSubmit = R.when(checkForm, debounce(Config.debounceLongTime, submit,
<view class="py-36rpx px-30rpx bg-#fff">
<view class="">
<view class="text-28-bold">{{ t("pages-login.sign-up.first-name") }}</view>
<view
class="mt-20rpx flex px-30rpx items-center h-88rpx border-2rpx border-solid border-#D4D4D4 rounded-20rpx">
<wd-input
no-border
clearable
:focus-when-clear="false"
:cursorSpacing="20"
:maxlength="40"
v-model.trim="form.firstName"
custom-class="flex-1"
placeholder=""
>
<view class="mt-20rpx flex px-30rpx items-center h-88rpx border-2rpx border-solid border-#D4D4D4 rounded-20rpx">
<wd-input no-border clearable :focus-when-clear="false" :cursorSpacing="20" :maxlength="40"
v-model.trim="form.firstName" custom-class="flex-1" placeholder="">
</wd-input>
</view>
</view>
<view class="mt-36rpx">
<view class="text-28-bold">{{ t("pages-login.sign-up.last-name") }}</view>
<view
class="mt-20rpx flex px-30rpx items-center h-88rpx border-2rpx border-solid border-#D4D4D4 rounded-20rpx">
<wd-input
no-border
clearable
:focus-when-clear="false"
use-prefix-slot
:maxlength="40"
:cursorSpacing="20"
v-model.trim="form.surname"
custom-class="flex-1"
placeholder=""
>
<view class="mt-20rpx flex px-30rpx items-center h-88rpx border-2rpx border-solid border-#D4D4D4 rounded-20rpx">
<wd-input no-border clearable :focus-when-clear="false" use-prefix-slot :maxlength="40" :cursorSpacing="20"
v-model.trim="form.surname" custom-class="flex-1" placeholder="">
</wd-input>
</view>
</view>
</view>
<view class="mt-318rpx px-30rpx">
<wd-button custom-class="!h-108rpx !text-36rpx font-bold !rounded-16rpx" block @click="handleSubmit">
<view class="mt-318rpx px-30rpx" style="position: fixed; bottom: 30rpx; left: 0; right: 0; z-index: 100;">
<wd-button custom-class="!h-108rpx !text-36rpx font-bold !rounded-46rpx" block @click="handleSubmit">
{{ t('common.save') }}
</wd-button>
</view>
+2 -2
View File
@@ -59,9 +59,9 @@ onLoad(()=> {
:auto="false"
>
<template #top>
<navbar />
<navbar :title="t('pages.mine.help')" />
<view class="px-30rpx pb-42rpx">
<view class="text-46rpx text-#333 font-bold lh-46rpx mb-36rpx mt-10rpx">{{ t("pages.mine.help") }}</view>
<!-- <view class="text-46rpx text-#333 font-bold lh-46rpx mb-36rpx mt-10rpx">{{ t("pages.mine.help") }}</view> -->
<view
class="flex items-center h-88rpx px-30rpx bg-#F2F3F6 rounded-88rpx box-border"
>
+33 -2
View File
@@ -42,8 +42,13 @@ function handleClickLeft() {
<view class="i-carbon:chevron-left text-50rpx text-#3D3D3D ml-[-10rpx]"></view>
</view>
</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>
<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>
<view class="px-18rpx">
<view
@@ -111,4 +116,30 @@ function handleClickLeft() {
</view>
</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";
const userStore = useUserStore()
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);
// 获取菜谱详情
@@ -108,7 +131,7 @@ function getCommentList() {
user_name: userInfo.user_name, // 用户名
user_avatar: userInfo.user_avatar, // 用户头像地址
user_content: item.content, // 用户评论内容
create_time: formatTimestampWithMonthName(item.createTime), // 创建时间
create_time: formatCommentTime(item.createTime), // 创建时间
}
})
tableTotal.value = res.total
@@ -192,7 +215,7 @@ function handleSend() {
>{{ recipeDetail?.recipeName || '' }}</view
>
<view class="flex-center-sb text-28rpx text-#fff">
<text>{{ formatTimestamp(recipeDetail?.createTime) }}</text>
<text>{{ formatRecipeTime(recipeDetail?.createTime) }}</text>
<view class="flex items-center">
<image
src="@img/chef/1326.png"
@@ -0,0 +1,464 @@
<script setup lang="ts">
import { computed } from 'vue'
type PayPwdMode = 'set' | 'change'
const props = defineProps<{
t: (key: string) => string
isSend: number
payPwdMode: PayPwdMode
showLoginPwdPopup: boolean
showLoginPwdForgetPopup: boolean
showPayPwdPopup: boolean
showPayPwdForgetPopup: boolean
loginPwdForm: { oldLoginPwd: string; newLoginPwd: string; confirmPwd: string }
loginPwdForgetForm: { phone: string; captcha: string; newLoginPwd: string; confirmPwd: string }
payPwdSetForm: { phone: string; captcha: string; payPwd: string; confirmPwd: string }
payPwdChangeForm: { oldPayPwd: string; newPayPwd: string; confirmPwd: string }
payPwdForgetForm: { phone: string; captcha: string; payPwd: string; confirmPwd: string }
}>()
const emit = defineEmits<{
(e: 'update:showLoginPwdPopup', value: boolean): void
(e: 'update:showLoginPwdForgetPopup', value: boolean): void
(e: 'update:showPayPwdPopup', value: boolean): void
(e: 'update:showPayPwdForgetPopup', value: boolean): void
(e: 'submit-login-pwd'): void
(e: 'submit-login-pwd-forget'): void
(e: 'submit-pay-pwd-set'): void
(e: 'submit-pay-pwd-change'): void
(e: 'submit-pay-pwd-forget'): void
(e: 'request-login-forget-code'): void
(e: 'request-pay-set-code'): void
(e: 'request-pay-forget-code'): void
}>()
const loginPwdPopupVisible = computed({
get: () => props.showLoginPwdPopup,
set: (value: boolean) => emit('update:showLoginPwdPopup', value),
})
const loginPwdForgetPopupVisible = computed({
get: () => props.showLoginPwdForgetPopup,
set: (value: boolean) => emit('update:showLoginPwdForgetPopup', value),
})
const payPwdPopupVisible = computed({
get: () => props.showPayPwdPopup,
set: (value: boolean) => emit('update:showPayPwdPopup', value),
})
const payPwdForgetPopupVisible = computed({
get: () => props.showPayPwdForgetPopup,
set: (value: boolean) => emit('update:showPayPwdForgetPopup', value),
})
function openLoginPwdForget() {
emit('update:showLoginPwdPopup', false)
emit('update:showLoginPwdForgetPopup', true)
}
function openPayPwdForget() {
emit('update:showPayPwdPopup', false)
emit('update:showPayPwdForgetPopup', true)
}
</script>
<template>
<view>
<wd-popup
v-model="loginPwdPopupVisible"
:close-on-click-modal="false"
position="bottom"
custom-class="ios-popup-mask"
>
<view class="ios-dialog">
<view class="ios-dialog__header">
<view class="ios-dialog__title">{{ t('navbar-change-password') }}</view>
<view class="ios-dialog__close" @click="emit('update:showLoginPwdPopup', false)">×</view>
</view>
<view class="ios-dialog__body">
<wd-input
v-model.trim="loginPwdForm.oldLoginPwd"
no-border
show-password
clearable
:maxlength="20"
:placeholder="t('pages-user.pay-password.input-placeholder.enter-old-password')"
/>
<view class="ios-dialog__divider" />
<wd-input
v-model.trim="loginPwdForm.newLoginPwd"
no-border
show-password
clearable
:maxlength="16"
:placeholder="t('pages-user.pay-password.input-placeholder.enter-new-password')"
/>
<view class="ios-dialog__divider" />
<wd-input
v-model.trim="loginPwdForm.confirmPwd"
no-border
show-password
clearable
:maxlength="16"
:placeholder="t('pages-user.pay-password.input-placeholder.enter-new-password-again')"
/>
<view class="ios-dialog__link-row">
<text class="ios-dialog__link" @click="openLoginPwdForget">
{{ t('navbar-forget-password') }}?
</text>
</view>
</view>
<view class="ios-dialog__footer">
<wd-button custom-class="ios-dialog__btn ios-dialog__btn--primary" block @click="emit('submit-login-pwd')">
{{ t('common.confirm') }}
</wd-button>
</view>
</view>
</wd-popup>
<wd-popup
v-model="loginPwdForgetPopupVisible"
:close-on-click-modal="false"
position="bottom"
custom-class="ios-popup-mask"
>
<view class="ios-dialog">
<view class="ios-dialog__header">
<view class="ios-dialog__title">{{ t('navbar-forget-password') }}</view>
<view class="ios-dialog__close" @click="emit('update:showLoginPwdForgetPopup', false)">×</view>
</view>
<view class="ios-dialog__body">
<wd-input :model-value="loginPwdForgetForm.phone" disabled disabledColor="transparent" no-border />
<view class="ios-dialog__divider" />
<wd-input
v-model.trim="loginPwdForgetForm.captcha"
style="color:#000 !important;"
no-border
clearable
:maxlength="6"
:placeholder="t('pages-user.pay-password.input-placeholder.enter-verification-code')"
>
<template #suffix>
<wd-button :disabled="!!isSend" custom-class="ios-code-btn" @click="emit('request-login-forget-code')">
{{ isSend ? isSend + 'S' : t('common.obtain') }}
</wd-button>
</template>
</wd-input>
<view class="ios-dialog__divider" />
<wd-input
v-model.trim="loginPwdForgetForm.newLoginPwd"
style="color:#000 !important;"
no-border
show-password
clearable
:maxlength="20"
:placeholder="t('pages-user.pay-password.input-placeholder.enter-new-password')"
/>
<view class="ios-dialog__divider" />
<wd-input
v-model.trim="loginPwdForgetForm.confirmPwd"
no-border
show-password
clearable
:maxlength="20"
:placeholder="t('pages-user.pay-password.input-placeholder.enter-new-password-again')"
/>
</view>
<view class="ios-dialog__footer">
<wd-button custom-class="ios-dialog__btn ios-dialog__btn--primary" block @click="emit('submit-login-pwd-forget')">
{{ t('common.confirm') }}
</wd-button>
</view>
</view>
</wd-popup>
<wd-popup
v-model="payPwdPopupVisible"
:close-on-click-modal="false"
position="bottom"
custom-class="ios-popup-mask"
>
<view class="ios-dialog">
<view class="ios-dialog__header">
<view class="ios-dialog__title">
{{ payPwdMode === 'set' ? t('navbar-set-payment-password') : t('navbar-change-payment-password') }}
</view>
<view class="ios-dialog__close" @click="emit('update:showPayPwdPopup', false)">×</view>
</view>
<view class="ios-dialog__body" v-if="payPwdMode === 'set'">
<wd-input :model-value="payPwdSetForm.phone" disabled disabledColor="transparent" no-border />
<view class="ios-dialog__divider" />
<wd-input
v-model.trim="payPwdSetForm.captcha"
no-border
clearable
:maxlength="6"
:placeholder="t('pages-user.pay-password.input-placeholder.enter-verification-code')"
>
<template #suffix>
<wd-button :disabled="!!isSend" custom-class="ios-code-btn" @click="emit('request-pay-set-code')">
{{ isSend ? isSend + 'S' : t('common.obtain') }}
</wd-button>
</template>
</wd-input>
<view class="ios-dialog__divider" />
<wd-input
v-model.trim="payPwdSetForm.payPwd"
no-border
show-password
clearable
:maxlength="6"
:placeholder="t('pages-user.pay-password.input-placeholder.enter-new-password')"
/>
<view class="ios-dialog__divider" />
<wd-input
v-model.trim="payPwdSetForm.confirmPwd"
no-border
show-password
clearable
:maxlength="6"
:placeholder="t('pages-user.pay-password.input-placeholder.enter-new-password-again')"
/>
</view>
<view class="ios-dialog__body" v-else>
<wd-input
v-model.trim="payPwdChangeForm.oldPayPwd"
no-border
show-password
clearable
:maxlength="6"
:placeholder="t('pages-user.pay-password.input-placeholder.enter-old-password')"
/>
<view class="ios-dialog__divider" />
<wd-input
v-model.trim="payPwdChangeForm.newPayPwd"
no-border
show-password
clearable
:maxlength="6"
:placeholder="t('pages-user.pay-password.input-placeholder.enter-new-password')"
/>
<view class="ios-dialog__divider" />
<wd-input
v-model.trim="payPwdChangeForm.confirmPwd"
no-border
show-password
clearable
:maxlength="6"
:placeholder="t('pages-user.pay-password.input-placeholder.enter-new-password-again')"
/>
<view class="ios-dialog__link-row">
<text class="ios-dialog__link" @click="openPayPwdForget">
{{ t('navbar-forget-password') }}?
</text>
</view>
</view>
<view class="ios-dialog__footer">
<wd-button
custom-class="ios-dialog__btn ios-dialog__btn--primary"
block
@click="payPwdMode === 'set' ? emit('submit-pay-pwd-set') : emit('submit-pay-pwd-change')"
>
{{ t('common.confirm') }}
</wd-button>
</view>
</view>
</wd-popup>
<wd-popup
v-model="payPwdForgetPopupVisible"
:close-on-click-modal="false"
position="bottom"
custom-class="ios-popup-mask"
>
<view class="ios-dialog">
<view class="ios-dialog__header">
<view class="ios-dialog__title">{{ t('navbar-forget-payment-password') }}</view>
<view class="ios-dialog__close" @click="emit('update:showPayPwdForgetPopup', false)">×</view>
</view>
<view class="ios-dialog__body">
<wd-input :model-value="payPwdForgetForm.phone" disabled disabledColor="transparent" no-border />
<view class="ios-dialog__divider" />
<wd-input
v-model.trim="payPwdForgetForm.captcha"
style="color:#000 !important;"
no-border
clearable
:maxlength="6"
:placeholder="t('pages-user.pay-password.input-placeholder.enter-verification-code')"
>
<template #suffix>
<wd-button :disabled="!!isSend" custom-class="ios-code-btn" @click="emit('request-pay-forget-code')">
{{ isSend ? isSend + 'S' : t('common.obtain') }}
</wd-button>
</template>
</wd-input>
<view class="ios-dialog__divider" />
<wd-input
v-model.trim="payPwdForgetForm.payPwd"
no-border
show-password
clearable
:maxlength="6"
:placeholder="t('pages-user.pay-password.input-placeholder.enter-new-password')"
/>
<view class="ios-dialog__divider" />
<wd-input
v-model.trim="payPwdForgetForm.confirmPwd"
no-border
show-password
clearable
:maxlength="6"
:placeholder="t('pages-user.pay-password.input-placeholder.enter-new-password-again')"
/>
</view>
<view class="ios-dialog__footer">
<wd-button custom-class="ios-dialog__btn ios-dialog__btn--primary" block @click="emit('submit-pay-pwd-forget')">
{{ t('common.confirm') }}
</wd-button>
</view>
</view>
</wd-popup>
</view>
</template>
<style scoped lang="scss">
.ios-dialog {
width: 100vw;
max-height: 88vh;
background: #fff;
border-radius: 40rpx 40rpx 0 0;
overflow: hidden;
padding-bottom: env(safe-area-inset-bottom);
box-sizing: border-box;
}
.ios-dialog__header {
height: 96rpx;
padding: 0 28rpx;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.ios-dialog__title {
text-align: center;
font-size: 36rpx;
line-height: 44rpx;
color: #1c1c1e;
font-weight: 600;
}
.ios-dialog__close {
position: absolute;
right: 28rpx;
top: 50%;
transform: translateY(-50%);
width: 56rpx;
height: 56rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 44rpx;
line-height: 44rpx;
color: #222;
}
.ios-dialog__body {
margin: 0 24rpx 12rpx;
background: #fff;
}
.ios-dialog__divider {
display: none;
}
.ios-dialog__link-row {
padding: 12rpx 8rpx 0;
display: flex;
justify-content: flex-end;
}
.ios-dialog__link {
font-size: 34rpx;
line-height: 34rpx;
color: #3c3c43;
font-weight: 500;
}
.ios-dialog__footer {
padding: 210rpx 24rpx 24rpx;
}
:deep(.ios-dialog__btn) {
height: 88rpx !important;
border-radius: 999rpx !important;
font-size: 34rpx !important;
font-weight: 600 !important;
}
:deep(.ios-dialog__btn--primary) {
background: #000 !important;
color: #fff !important;
}
:deep(.ios-code-btn) {
min-width: 92rpx !important;
height: 52rpx !important;
padding: 0 20rpx !important;
border-radius: 16rpx !important;
font-size: 26rpx !important;
font-weight: 400 !important;
line-height: 52rpx !important;
background: #000 !important;
color: #fff !important;
border: none !important;
}
:deep(.ios-code-btn.is-disabled) {
background: #8e8e93 !important;
color: #fff !important;
opacity: 1 !important;
}
:deep(.wd-input) {
height: 84rpx !important;
border: 1rpx solid #dedede !important;
border-radius: 24rpx !important;
padding: 0 24rpx !important;
box-sizing: border-box !important;
display: flex;
align-items: center;
margin-bottom: 14rpx;
}
:deep(.wd-input__inner) {
flex: 1;
min-width: 0;
border: none !important;
margin-bottom: 0 !important;
padding: 0 !important;
}
:deep(.wd-input__suffix) {
display: flex;
align-items: center;
flex-shrink: 0;
margin-left: 12rpx;
}
:deep(.ios-popup-mask) {
background: transparent !important;
}
:deep(.ios-popup-mask.wd-popup--bottom) {
border-radius: 40rpx 40rpx 0 0 !important;
}
</style>
+378 -58
View File
@@ -1,21 +1,260 @@
<script setup lang="ts">
import {useMessage} from "wot-design-uni";
import {useConfigStore, useUserStore} from "@/store";
import {conversionMobile} from "@/utils";
import {Agreement} from "@/constant/enums";
import ChooseLanguage from "./components/choose-language/choose-language.vue";
import Logout from "./components/log-out/log-out.vue";
import PasswordDialogs from "./components/password-dialogs/password-dialogs.vue";
import Config from "@/config";
import {appUserLogOffPost} from "@/service";
import {appUserLogOffPost, appUserEditLoginPwdPost, appUserForgetPwdPost} from "@/service";
import { setPayPwd, editPayPwd, forgetPayPwd } from "@/pages-user/service";
import { SmsType } from "@/constant/enums";
import { z } from "zod";
import useGetMsgCode from "@/hooks/useGetMsgCode";
const {t} = useI18n();
const {locale} = useI18n();
const userStore = useUserStore();
const configStore = useConfigStore()
const message = useMessage();
const currentVersion = ref(configStore.appVersion)
const chooseLanguageRef = ref<InstanceType<typeof ChooseLanguage>>()
const logoutRef = ref<InstanceType<typeof Logout>>()
const currentLanguageLabel = computed(() => (locale.value === 'en' ? 'English' : '中文'))
const { isSend, getMsgCode } = useGetMsgCode()
const showLoginPwdPopup = ref(false)
const showLoginPwdForgetPopup = ref(false)
const showPayPwdPopup = ref(false)
const showPayPwdForgetPopup = ref(false)
const payPwdMode = ref<'set' | 'change'>('set')
const loginPwdForm = ref({
oldLoginPwd: '',
newLoginPwd: '',
confirmPwd: '',
})
const loginPwdSchema = computed(() =>
z
.object({
oldLoginPwd: z.string().min(1, { message: t('pages-user.pay-password.input-placeholder.enter-old-password') }),
newLoginPwd: z
.string()
.min(8, { message: t('pages-user.pay-password.password-length-limit') })
.max(16, { message: t('pages-user.pay-password.password-length-limit') }),
confirmPwd: z.string().min(1, { message: t('pages-user.pay-password.input-placeholder.enter-new-password-again') }),
})
.refine((data) => data.newLoginPwd === data.confirmPwd, {
path: ['confirmPwd'],
message: t('pages-user.pay-password.two-passwords-inconsistent'),
})
)
function validateBySchema(schema: z.ZodTypeAny, data: any) {
const res = schema.safeParse(data)
if (!res.success) {
const errors = res.error.flatten().fieldErrors
const first = Object.values(errors).find((arr) => Array.isArray(arr) && arr.length)?.[0]
if (first) uni.showToast({ title: String(first), icon: 'none' })
return false
}
return true
}
async function submitLoginPwd() {
if (!validateBySchema(loginPwdSchema.value, loginPwdForm.value)) return
await appUserEditLoginPwdPost({ body: { ...loginPwdForm.value } })
uni.showToast({ title: t('pages-user.password.change-password-successfully'), icon: 'none' })
showLoginPwdPopup.value = false
loginPwdForm.value = { oldLoginPwd: '', newLoginPwd: '', confirmPwd: '' }
setTimeout(() => {
userStore.clear()
uni.navigateTo({ url: Config.loginPath })
}, 1000)
}
const loginPwdForgetForm = ref({
areaCode: userStore.userInfo?.areaCode || '',
phone: userStore.userInfo?.phone || '',
captcha: '',
confirmPwd: '',
newLoginPwd: '',
})
const loginPwdForgetSchema = computed(() =>
z
.object({
captcha: z.string().min(1, { message: t('pages-user.pay-password.input-placeholder.enter-verification-code') }),
confirmPwd: z
.string()
.min(1, { message: t('pages-user.pay-password.input-placeholder.enter-new-password') })
.regex(/^\d{6}$/, { message: t('pages-user.pay-password.enter-6-digit-password') }),
newLoginPwd: z.string().min(1, { message: t('pages-user.pay-password.input-placeholder.enter-new-password-again') }),
})
.refine((data) => data.confirmPwd === data.newLoginPwd, {
path: ['newLoginPwd'],
message: t('pages-user.pay-password.two-passwords-inconsistent'),
})
)
function openLoginPwdPopup() {
showLoginPwdPopup.value = true
}
function openPayPwdPopup() {
payPwdMode.value = userStore.userInfo?.payPwd ? 'change' : 'set'
showPayPwdPopup.value = true
}
function requestLoginForgetCode() {
if (isSend.value > 0) return
getMsgCode({
type: SmsType.USER_FORGET_PASSWORD,
phone: loginPwdForgetForm.value.phone,
areaCode: loginPwdForgetForm.value.areaCode,
})
}
async function submitLoginPwdForget() {
if (!validateBySchema(loginPwdForgetSchema.value, loginPwdForgetForm.value)) return
await appUserForgetPwdPost({ body: { ...loginPwdForgetForm.value } })
uni.showToast({ title: t('pages-user.password.forget-password-successfully'), icon: 'none' })
await userStore.getUserInfo()
showLoginPwdForgetPopup.value = false
loginPwdForgetForm.value = {
areaCode: userStore.userInfo?.areaCode || '',
phone: userStore.userInfo?.phone || '',
captcha: '',
confirmPwd: '',
newLoginPwd: '',
}
}
const payPwdSetForm = ref({
areaCode: userStore.userInfo?.areaCode || '',
phone: userStore.userInfo?.phone || '',
captcha: '',
payPwd: '',
confirmPwd: '',
})
const payPwdSetSchema = computed(() =>
z
.object({
captcha: z.string().min(1, { message: t('pages-user.pay-password.input-placeholder.enter-verification-code') }),
payPwd: z
.string()
.min(1, { message: t('pages-user.pay-password.input-placeholder.enter-new-password') })
.regex(/^\d{6}$/, { message: t('pages-user.pay-password.enter-6-digit-password') }),
confirmPwd: z.string().min(1, { message: t('pages-user.pay-password.input-placeholder.enter-new-password-again') }),
})
.refine((data) => data.payPwd === data.confirmPwd, {
path: ['confirmPwd'],
message: t('pages-user.pay-password.two-passwords-inconsistent'),
})
)
function requestPaySetCode() {
if (isSend.value > 0) return
getMsgCode({
type: SmsType.USER_SET_PAYMENT_PASSWORD,
phone: payPwdSetForm.value.phone,
areaCode: payPwdSetForm.value.areaCode,
})
}
async function submitPayPwdSet() {
if (!validateBySchema(payPwdSetSchema.value, payPwdSetForm.value)) return
await setPayPwd(payPwdSetForm.value as any)
uni.showToast({ title: t('pages-user.pay-password.set-payment-password-successfully'), icon: 'none' })
await userStore.getUserInfo()
showPayPwdPopup.value = false
payPwdSetForm.value = {
areaCode: userStore.userInfo?.areaCode || '',
phone: userStore.userInfo?.phone || '',
captcha: '',
payPwd: '',
confirmPwd: '',
}
}
const payPwdChangeForm = ref({
oldPayPwd: '',
newPayPwd: '',
confirmPwd: '',
})
const payPwdChangeSchema = computed(() =>
z
.object({
oldPayPwd: z.string().min(1, { message: t('pages-user.pay-password.input-placeholder.enter-old-password') }),
newPayPwd: z
.string()
.min(1, { message: t('pages-user.pay-password.input-placeholder.enter-new-password') })
.regex(/^\d{6}$/, { message: t('pages-user.pay-password.enter-6-digit-password') }),
confirmPwd: z.string().min(1, { message: t('pages-user.pay-password.input-placeholder.enter-new-password-again') }),
})
.refine((data) => data.newPayPwd === data.confirmPwd, {
path: ['confirmPwd'],
message: t('pages-user.pay-password.two-passwords-inconsistent'),
})
)
async function submitPayPwdChange() {
if (!validateBySchema(payPwdChangeSchema.value, payPwdChangeForm.value)) return
await editPayPwd(payPwdChangeForm.value as any)
uni.showToast({ title: t('pages-user.pay-password.change-payment-password-successfully'), icon: 'none' })
await userStore.getUserInfo()
showPayPwdPopup.value = false
payPwdChangeForm.value = { oldPayPwd: '', newPayPwd: '', confirmPwd: '' }
}
const payPwdForgetForm = ref({
areaCode: userStore.userInfo?.areaCode || '',
phone: userStore.userInfo?.phone || '',
captcha: '',
payPwd: '',
confirmPwd: '',
})
const payPwdForgetSchema = computed(() =>
z
.object({
captcha: z.string().min(1, { message: t('pages-user.pay-password.input-placeholder.enter-verification-code') }),
payPwd: z
.string()
.min(1, { message: t('pages-user.pay-password.input-placeholder.enter-new-password') })
.regex(/^\d{6}$/, { message: t('pages-user.pay-password.enter-6-digit-password') }),
confirmPwd: z.string().min(1, { message: t('pages-user.pay-password.input-placeholder.enter-new-password-again') }),
})
.refine((data) => data.payPwd === data.confirmPwd, {
path: ['confirmPwd'],
message: t('pages-user.pay-password.two-passwords-inconsistent'),
})
)
function requestPayForgetCode() {
if (isSend.value > 0) return
getMsgCode({
type: SmsType.USER_FORGET_PAYMENT_PASSWORD,
phone: payPwdForgetForm.value.phone,
areaCode: payPwdForgetForm.value.areaCode,
})
}
async function submitPayPwdForget() {
if (!validateBySchema(payPwdForgetSchema.value, payPwdForgetForm.value)) return
await forgetPayPwd(payPwdForgetForm.value as any)
uni.showToast({ title: t('pages-user.pay-password.forget-payment-password-successfully'), icon: 'none' })
await userStore.getUserInfo()
showPayPwdForgetPopup.value = false
payPwdForgetForm.value = {
areaCode: userStore.userInfo?.areaCode || '',
phone: userStore.userInfo?.phone || '',
captcha: '',
payPwd: '',
confirmPwd: '',
}
}
function handleChooseLanguage() {
if (chooseLanguageRef.value) {
@@ -30,10 +269,7 @@ function navigateTo(url: string) {
function handleSetOrUpdatePassword() {
if (userStore.userInfo?.payPwd) {
return navigateTo(`/pages-user/pages/pay-password/change/index`)
}
navigateTo(`/pages-user/pages/pay-password/set/index`)
openPayPwdPopup()
}
function handleLogout() {
@@ -74,76 +310,160 @@ function handleLogoutAccount() {
</script>
<template>
<view class="setting-root">
<navbar :title="t('navbar-settings')"/>
<view class="pt-20rpx">
<view class="text-30-bold bg-#fff">
<view class="setting-page">
<view class="setting-card">
<view
class="flex justify-between items-center border-bottom font-bold p-[40rpx+20rpx]"
@click="navigateTo('/pages-user/pages/password/change/index')"
class="setting-row setting-row--border"
@click="openLoginPwdPopup"
>
<view>{{ t("pages-user.setting.modification") }}</view>
<image
src="@img/chef/100202.png"
class="shrink-0 ml-16rpx w-22rpx h-30rpx"
></image>
<text class="setting-row__label">{{ t("pages-user.setting.modification") }}</text>
<image src="@img/chef/100202.png" class="setting-row__arrow"></image>
</view>
<view
@click="
handleSetOrUpdatePassword
"
class="flex justify-between items-center p-[40rpx+20rpx] border-bottom"
>
<view>{{ t("pages-user.setting.payPwd") }}</view>
<view class="flex items-center">
<text>{{ t('navbar-settings') }}</text>
<image
src="@img/chef/100202.png"
class="shrink-0 ml-16rpx w-22rpx h-30rpx"
></image>
<view class="setting-row" @click="handleSetOrUpdatePassword">
<text class="setting-row__label">{{ t("pages-user.setting.payPwd") }}</text>
<view class="setting-row__right">
<text class="setting-row__value">{{ t('pages.mine.set') }}</text>
<image src="@img/chef/100202.png" class="setting-row__arrow"></image>
</view>
</view>
</view>
<view
class="flex justify-between items-center p-[40rpx+20rpx] border-bottom before:!bg-common"
@click="handleChooseLanguage"
>
<view>{{ t("pages-user.setting.language") }}</view>
<view class="flex items-center">
<text>{{ locale === 'en' ? 'English' : '中文'}}</text>
<image
src="@img/chef/100202.png"
class="shrink-0 ml-16rpx w-22rpx h-30rpx"
></image>
<view class="setting-card setting-card--gap">
<view class="setting-row" @click="handleChooseLanguage">
<text class="setting-row__label">{{ t("pages-user.setting.language") }}</text>
<view class="setting-row__right">
<text class="setting-row__value">{{ currentLanguageLabel }}</text>
<image src="@img/chef/100202.png" class="setting-row__arrow"></image>
</view>
</view>
</view>
<view
class="flex justify-between items-center font-bold p-[40rpx+20rpx]"
@click="handleLogoutAccount"
>
<view>{{ t('pages-user.setting.cancelAccount') }}</view>
<image
src="@img/chef/100202.png"
class="shrink-0 ml-16rpx w-22rpx h-30rpx"
></image>
<view class="setting-card setting-card--gap">
<view class="setting-row" @click="handleLogoutAccount">
<text class="setting-row__label setting-row__label--strong">{{ t('pages-user.setting.cancelAccount') }}</text>
<!-- <image src="@img/chef/100202.png" class="setting-row__arrow"></image> -->
</view>
</view>
<view class="px-30rpx mt-180rpx">
<wd-button
custom-class="!h-108rpx !text-36rpx !text-white !bg-#14181B !rounded-16rpx"
block
@click="handleLogout"
>
<view class="logout-actions">
<wd-button custom-class="logout-btn" block @click="handleLogout">
{{ t('pages-user.setting.logout') }}
</wd-button>
</view>
</view>
<choose-language ref="chooseLanguageRef"/>
<logout ref="logoutRef"/>
<password-dialogs
:t="t"
:is-send="isSend"
:pay-pwd-mode="payPwdMode"
:show-login-pwd-popup="showLoginPwdPopup"
:show-login-pwd-forget-popup="showLoginPwdForgetPopup"
:show-pay-pwd-popup="showPayPwdPopup"
:show-pay-pwd-forget-popup="showPayPwdForgetPopup"
:login-pwd-form="loginPwdForm"
:login-pwd-forget-form="loginPwdForgetForm"
:pay-pwd-set-form="payPwdSetForm"
:pay-pwd-change-form="payPwdChangeForm"
:pay-pwd-forget-form="payPwdForgetForm"
@update:show-login-pwd-popup="showLoginPwdPopup = $event"
@update:show-login-pwd-forget-popup="showLoginPwdForgetPopup = $event"
@update:show-pay-pwd-popup="showPayPwdPopup = $event"
@update:show-pay-pwd-forget-popup="showPayPwdForgetPopup = $event"
@submit-login-pwd="submitLoginPwd"
@submit-login-pwd-forget="submitLoginPwdForget"
@submit-pay-pwd-set="submitPayPwdSet"
@submit-pay-pwd-change="submitPayPwdChange"
@submit-pay-pwd-forget="submitPayPwdForget"
@request-login-forget-code="requestLoginForgetCode"
@request-pay-set-code="requestPaySetCode"
@request-pay-forget-code="requestPayForgetCode"
/>
</view>
</template>
<style scoped></style>
<style scoped lang="scss">
.setting-root {
min-height: 100vh;
background: #f5f5f5;
}
.setting-page {
padding: 20rpx 30rpx calc(160rpx + env(safe-area-inset-bottom));
box-sizing: border-box;
}
.setting-card {
background: #fff;
border-radius: 24rpx;
overflow: hidden;
}
.setting-card--gap {
margin-top: 24rpx;
}
.setting-row {
height: 102rpx;
padding: 0 28rpx;
display: flex;
align-items: center;
justify-content: space-between;
&--border {
border-bottom: 1rpx solid #ededed;
}
&__label {
font-size: 34rpx;
line-height: 34rpx;
color: #2c2c2c;
// font-weight: 700;
}
&__label--strong {
width: 100%;
text-align: center;
}
&__right {
display: flex;
align-items: center;
gap: 12rpx;
}
&__value {
font-size: 30rpx;
line-height: 30rpx;
color: #2c2c2c;
// font-weight: 600;
}
&__arrow {
width: 22rpx;
height: 30rpx;
flex-shrink: 0;
}
}
.logout-actions {
position: fixed;
left: 30rpx;
right: 30rpx;
bottom: calc(36rpx + env(safe-area-inset-bottom));
z-index: 20;
}
:deep(.logout-btn) {
height: 108rpx !important;
border-radius: 22rpx !important;
background: #111 !important;
color: #fff !important;
font-size: 36rpx !important;
font-weight: 700 !important;
}
</style>
+4 -10
View File
@@ -125,10 +125,7 @@ onBackPress(() => {
<view class="shrink-0 mr-20rpx text-30rpx text-primary">{{ t('pages-user.user-info.head-portrait') }}</view>
<view class="flex items-center" @click="handleAddImg">
<image :src="form.avatar" class="shrink-0 w-80rpx h-80rpx rounded-full"></image>
<image
src="@img/chef/100202.png"
class="shrink-0 ml-16rpx w-22rpx h-30rpx"
></image>
<image src="@img/chef/100202.png" class="shrink-0 ml-16rpx w-22rpx h-30rpx"></image>
</view>
</view>
<view class="px-18rpx py-37rpx flex items-center justify-between"
@@ -136,17 +133,14 @@ onBackPress(() => {
<view class="shrink-0 mr-20rpx text-30rpx text-primary">{{ t('pages-user.user-info.nickname') }}</view>
<view class="flex-1 flex items-center justify-end text-30rpx text-primary">
<text>{{ `${userStore.userInfo.firstName} ${userStore.userInfo.surname}` }}</text>
<image
src="@img/chef/100202.png"
class="shrink-0 ml-16rpx w-22rpx h-30rpx"
></image>
<image src="@img/chef/100202.png" class="shrink-0 ml-16rpx w-22rpx h-30rpx"></image>
</view>
</view>
</view>
<view class="mt-318rpx px-30rpx">
<wd-button custom-class="!h-108rpx !text-36rpx font-bold !rounded-16rpx" block @click="handleSubmit">
<view class="mt-318rpx px-30rpx" style="position: fixed; bottom: 30rpx; left: 0; right: 0; z-index: 100;">
<wd-button custom-class="!h-108rpx !text-36rpx font-bold !rounded-46rpx" block @click="handleSubmit">
{{ t('common.save') }}
</wd-button>
</view>
+1
View File
@@ -6,6 +6,7 @@ export const useLogicStore = defineStore('store-list-logic', () => {
const searchLoading = ref(false)
const setPlacesList = (list: any) => {
console.log('setPlacesList', list)
if (Array.isArray(list)) {
placesList.value = list
} else {
+4 -1
View File
@@ -229,7 +229,10 @@
"path": "pages/store/search/result"
},
{
"path": "pages/store/dishes"
"path": "pages/store/dishes",
"style": {
"onReachBottomDistance": 80
}
},
{
"path": "pages/order/checkout"
+3 -1
View File
@@ -18,15 +18,17 @@ function handleClickSearch() {
chooseAddress: (data: any) => {
console.log('搜索的地址信息', data)
if (data) {
addressStore.clearAddressInfo()
addressStore.setAddressLocation({
displayName: data.displayName,
formattedAddress: data.formattedAddress,
longitude: data.location.lng,
latitude: data.location.lat
})
addressStore.pendingIntroBuildingType = true
setTimeout(()=> {
uni.navigateTo({
url: '/pages/address/choose-type'
url: '/pages/address/save-address/other'
})
}, 300)
}
+145 -97
View File
@@ -137,13 +137,12 @@ const isDateSelectable = (date: Date): boolean => {
return hasAvailableTimeSlots(date);
};
// 生成未来的日期(显示所有日期,但标记营业状态)
// 生成未来 7 天:设计稿为「本周」5 个圆 +「下周」2 个圆
const dateOptions = computed(() => {
const dates: Date[] = [];
const today = new Date();
// 生成连续的5天日期(包括不营业的日期)
for (let i = 0; i < 5; i++) {
for (let i = 0; i < 7; i++) {
const date = new Date(today);
date.setDate(today.getDate() + i);
dates.push(date);
@@ -152,6 +151,9 @@ const dateOptions = computed(() => {
return dates;
});
const thisWeekDates = computed(() => dateOptions.value.slice(0, 5));
const nextWeekDates = computed(() => dateOptions.value.slice(5, 7));
// 状态管理 - 初始化为第一个营业日期
const selectedDate = ref<Date>();
@@ -246,7 +248,6 @@ const initializeSelectedDate = () => {
);
const firstAllowed = firstOpen || dateOptions.value.find((d) => isAllowedDay(d));
selectedDate.value = firstAllowed || dateOptions.value[0];
nextTick(() => updateScrollPosition());
return;
}
@@ -254,9 +255,6 @@ const initializeSelectedDate = () => {
for (const date of dateOptions.value) {
if (isDateSelectable(date)) {
selectedDate.value = date;
nextTick(() => {
updateScrollPosition();
});
return;
}
}
@@ -266,7 +264,6 @@ const initializeSelectedDate = () => {
if (nextBusinessDate) {
selectedDate.value = nextBusinessDate;
nextTick(() => {
updateScrollPosition();
uni.showToast({
title: t('pages.address.reservationTime.currentTimeExpired'),
icon: "none",
@@ -275,9 +272,6 @@ const initializeSelectedDate = () => {
});
} else {
selectedDate.value = dateOptions.value[0];
nextTick(() => {
updateScrollPosition();
});
}
};
@@ -304,59 +298,15 @@ watch(
);
const selectedTimeSlot = ref<string>("");
// 横向滚动距离
const scrollLeft = ref<number>(0);
/** 圆圈内上行:星期(与全局 dayjs 语言一致) */
const formatWeekdayCircle = (date: Date) => dayjs(date).format("dddd");
// 计算并设置横向滚动距离
const updateScrollPosition = () => {
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) => {
/** 圆圈内下行:MM/DD;不可选时显示文案 */
const formatCircleSubLine = (date: 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;
};
const isDateSelected = (date: Date) =>
!!selectedDate.value && dayjs(selectedDate.value).isSame(dayjs(date), "day");
// 提交预约
const submitReservation = () => {
const dateVal = selectedDate.value;
if (!dateVal) {
return;
}
// 非仅选日期模式,需要选择时间段
if (!onlySelectDay.value) {
if (!selectedTimeSlot.value) {
@@ -550,13 +508,13 @@ const submitReservation = () => {
}
// 计算开始/结束时间
const selectedDateDayjs = dayjs(selectedDate.value);
const selectedDateDayjs = dayjs(dateVal);
let startTime: dayjs.Dayjs;
let endTime: dayjs.Dayjs;
if (onlySelectDay.value) {
// 仅选日期:优先使用营业时间范围;若无营业时间限制,则使用当天起止
const bh = getBusinessHoursForDate(selectedDate.value);
const bh = getBusinessHoursForDate(dateVal);
if (bh) {
const [startHour, startMinute] = bh.startTime.split(':').map(Number);
const [endHour, endMinute] = bh.endTime.split(':').map(Number);
@@ -592,14 +550,14 @@ const submitReservation = () => {
}
console.log("预约信息:", {
date: selectedDate.value,
date: dateVal,
timeSlot: onlySelectDay.value ? '' : selectedTimeSlot.value,
startTime: startTime.valueOf(),
endTime: endTime.valueOf(),
});
uni.$emit(EventEnum.CHOOSE_APPOINTMENT_TIME, {
date: selectedDate.value,
date: dateVal,
timeSlot: onlySelectDay.value ? '' : selectedTimeSlot.value,
startTime: startTime.valueOf(),
endTime: endTime.valueOf(),
@@ -633,45 +591,52 @@ onLoad((options: any) => {
</script>
<template>
<view class="">
<navbar />
<view class="mt-20rpx px-30rpx text-46rpx lh-46rpx text-#333 font-bold">
{{ t("pages.address.appTime") }}
</view>
<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">
<template v-for="(item, index) in dateOptions" :key="index">
<view class="reservation-time-page min-h-screen pb-180rpx">
<navbar
:title="t('pages.address.reservationTime.pageTitle')"
circle-back
custom-class="reservation-time-navbar"
/>
<view class="reservation-time-body px-40rpx pt-40rpx">
<view class="date-section">
<text class="section-label">{{ t("pages.address.reservationTime.thisWeek") }}</text>
<view class="date-row">
<view
v-for="(item, index) in thisWeekDates"
:key="index"
class="date-circle"
:class="{
'date-circle--selected': isDateSelected(item),
'date-circle--disabled': !isDateSelectable(item),
}"
@click="selectDate(item)"
:class="[
index === 0 ? '' : 'ml-28rpx',
selectedDate && dayjs(selectedDate).isSame(dayjs(item), 'day')
? 'border-#333'
: 'border-#D8D8D8',
!isDateSelectable(item)
? 'opacity-50 cursor-not-allowed'
: 'cursor-pointer',
]"
class="inline-block border-solid border-1px w-240rpx h-140rpx rounded-20rpx px-32rpx py-36rpx"
>
<text class="date-circle__weekday">{{ formatWeekdayCircle(item) }}</text>
<text class="date-circle__sub">{{ formatCircleSubLine(item) }}</text>
</view>
</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
:class="!isDateSelectable(item) ? 'text-#999' : 'text-#333'"
class="text-28rpx lh-28rpx mb-12rpx"
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)"
>
{{ formatDateDisplay(item) }}
</view>
<view
:class="!isDateSelectable(item) ? 'text-#CCC' : 'text-#7D7D7D'"
class="text-28rpx"
>
{{ formatDateOnly(item) }}
<text class="date-circle__weekday">{{ formatWeekdayCircle(item) }}</text>
<text class="date-circle__sub">{{ formatCircleSubLine(item) }}</text>
</view>
</view>
</view>
</template>
</scroll-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
v-for="(timeSlot, index) in timeSlots"
:key="index"
@@ -705,8 +670,91 @@ onLoad((options: any) => {
</view>
</template>
<style>
page {
<style scoped lang="scss">
.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;
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>
+12 -1
View File
@@ -99,6 +99,13 @@ onLoad(()=> {
}
})
})
onReady(() => {
if (addressStore.pendingIntroBuildingType && !addressStore.addressInfo.id) {
buildingTypeRef.value?.openIntroSheet?.()
}
})
onUnload(()=> {
if(!isSwitch.value) {
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 @click="openBuildingType" class="bg-#F6F6F6 rounded-16rpx px-24rpx h-98rpx flex-center-sb">
<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>
<image src="@img/chef/142.png" class="w-32rpx h-32rpx shrink-0 rotate-90"></image>
</view>
@@ -1,70 +1,228 @@
<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 show = ref(false);
const value = ref('house')
const addressStore = useAddressStore();
const emit = defineEmits(['submit'])
/** 新地址首次进入保存页:与原先 choose-type 全页等价的介绍弹窗 */
const showIntro = ref(false);
/** 点击表单「建筑类型」行:原有滚轮选择器 */
const showPicker = ref(false);
function onOpen() {
show.value = true;
}
function handleClose() {
show.value = false;
}
function handleSubmit() {
console.log('value', value.value)
emit('submit', value.value)
handleClose()
}
const emit = defineEmits<{
submit: [value: string];
}>();
const columns = ref(
[
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',
},
]
)
function onChange({picker, value, index}) {
]);
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() {
pickerValue.value =
addressStore.addressInfo.type && `${addressStore.addressInfo.type}`.length > 0
? (addressStore.addressInfo.type as string)
: UserAddressType.HOUSE;
showPicker.value = true;
}
function handlePickerClose() {
showPicker.value = false;
}
function handlePickerCancel() {
showPicker.value = false;
}
function handlePickerConfirm() {
addressStore.addressInfo.type = pickerValue.value;
showPicker.value = false;
const next = routeForType(pickerValue.value);
if (next && currentSaveAddressRoute() !== next) {
emit('submit', pickerValue.value);
}
}
/** 新地址首次进入页面时由父级调用 */
function openIntroSheet() {
showIntro.value = true;
}
function handleIntroClose() {
showIntro.value = false;
}
/** 蒙层关闭且尚未完成选择时,与「跳过」一致 */
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({
onOpen,
openIntroSheet,
});
</script>
<template>
<!-- 新地址首次建筑类型介绍底部弹窗 -->
<wd-popup
v-model="show"
v-model="showIntro"
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 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 @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 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>
</wd-popup>
+12 -1
View File
@@ -98,6 +98,13 @@ onLoad(()=> {
}
})
})
onReady(() => {
if (addressStore.pendingIntroBuildingType && !addressStore.addressInfo.id) {
buildingTypeRef.value?.openIntroSheet?.()
}
})
onUnload(()=> {
if(!isSwitch.value) {
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 @click="openBuildingType" class="bg-#F6F6F6 rounded-16rpx px-24rpx h-98rpx flex-center-sb">
<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>
<image src="@img/chef/142.png" class="w-32rpx h-32rpx shrink-0 rotate-90"></image>
</view>
+12 -1
View File
@@ -98,6 +98,13 @@ onLoad(()=> {
}
})
})
onReady(() => {
if (addressStore.pendingIntroBuildingType && !addressStore.addressInfo.id) {
buildingTypeRef.value?.openIntroSheet?.()
}
})
onUnload(()=> {
if(!isSwitch.value) {
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 @click="openBuildingType" class="bg-#F6F6F6 rounded-16rpx px-24rpx h-98rpx flex-center-sb">
<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>
<image src="@img/chef/142.png" class="w-32rpx h-32rpx shrink-0 rotate-90"></image>
</view>
+12 -1
View File
@@ -98,6 +98,13 @@ onLoad(()=> {
}
})
})
onReady(() => {
if (addressStore.pendingIntroBuildingType && !addressStore.addressInfo.id) {
buildingTypeRef.value?.openIntroSheet?.()
}
})
onUnload(()=> {
if(!isSwitch.value) {
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 @click="openBuildingType" class="bg-#F6F6F6 rounded-16rpx px-24rpx h-98rpx flex-center-sb">
<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>
<image src="@img/chef/142.png" class="w-32rpx h-32rpx shrink-0 rotate-90"></image>
</view>
+12 -1
View File
@@ -98,6 +98,13 @@ onLoad(()=> {
}
})
})
onReady(() => {
if (addressStore.pendingIntroBuildingType && !addressStore.addressInfo.id) {
buildingTypeRef.value?.openIntroSheet?.()
}
})
onUnload(()=> {
if(!isSwitch.value) {
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 @click="openBuildingType" class="bg-#F6F6F6 rounded-16rpx px-24rpx h-98rpx flex-center-sb">
<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>
<image src="@img/chef/142.png" class="w-32rpx h-32rpx shrink-0 rotate-90"></image>
</view>
+5
View File
@@ -1,6 +1,9 @@
import { defineStore } from "pinia";
import type { UserAddressBo } from '@/service/types';
export const useAddressStore = defineStore('store-address', () => {
/** 从地图搜索新增地址进入保存页时,首次需弹出建筑类型介绍层(与原先进入 choose-type 页等价) */
const pendingIntroBuildingType = ref(false)
const addressInfo = ref<UserAddressBo>({
type: '',
/** 配送类型 1-亲自送达 2-放门口,默认放门口 */
@@ -57,6 +60,7 @@ export const useAddressStore = defineStore('store-address', () => {
}
function clearAddressInfo() {
pendingIntroBuildingType.value = false
addressInfo.value = {
id: '',
type: '',
@@ -102,6 +106,7 @@ export const useAddressStore = defineStore('store-address', () => {
return {
addressInfo,
addressLocation,
pendingIntroBuildingType,
setAddressLocation,
clearAddressInfo
}
@@ -1,23 +1,17 @@
<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 { 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 {
appMerchantDishNearbyListPost,
appSearchSearchRecipePost,
appCollectCollectPost,
} from "@/service";
import {thumbnailImg} from "@/utils/utils";
const configStore = useConfigStore();
const userStore = useUserStore();
const emit = defineEmits(["toggleNotOpen"]);
const loading = ref(false);
const recipePreviewLimit = 4;
const { t } = useI18n();
@@ -29,10 +23,6 @@ function navigateTo(url: string) {
}
}
function toggleNotOpen() {
emit("toggleNotOpen");
}
async function initData() {
if(!recipeData.value) {
loading.value = true;
@@ -43,13 +33,14 @@ async function initData() {
}
// 获取菜谱数据
const recipeData = ref([]);
const recipeData = ref<any[]>([]);
function getRecipeData() {
appSearchSearchRecipePost({
body: {
params: {
pageNum: 1,
pageSize: 10,
}
},
body: {}
}).then(res=> {
console.log('菜谱数据', res)
recipeData.value = res.rows;
@@ -57,30 +48,16 @@ function getRecipeData() {
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) {
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() {
appMerchantDishNearbyListPost({
params: {
@@ -88,8 +65,8 @@ function appMerchantDishNearbyList() {
pageSize: 10,
},
body: {
lat: userStore.userLocation.latitude,
lng: userStore.userLocation.longitude,
lat: String(userStore.userLocation.latitude ?? ''),
lng: String(userStore.userLocation.longitude ?? ''),
}
}).then(res=> {
console.log('菜品数据', res)
@@ -100,6 +77,27 @@ function handleClickDish(item: any) {
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() {}
defineExpose({
@@ -131,67 +129,59 @@ defineExpose({
v-show="!loading"
class="animate-in fade-in animate-ease-in animate-duration-300"
>
<view class="flex-center-sb px-30rpx 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">
<view class="px-24rpx pt-16rpx">
<search />
</view>
<view class="mt-50rpx px-30rpx">
<view @click="navigateTo('/pages-user/pages/recipe/list')" class="flex-center-sb">
<text class="text-36rpx lh-36rpx text-#333 font-bold">{{
t("pages.browse.titleRecipes")
}}</text>
<image src="@img/chef/116.png" class="w-64rpx h-64rpx"></image>
</view>
<scroll-view scroll-x="true" class="mt-16rpx">
<view class="flex gap-30rpx">
<template v-for="item in recipeData">
<view @click="navigateToRecipeDetail(item.id)" class="w-312rpx">
<view class="browse-wrap px-24rpx">
<view class="section-title mt-36rpx">{{ t("pages.browse.titleRecipes") }}</view>
<view class="mt-28rpx">
<scroll-view scroll-x class="recipe-scroll" :show-scrollbar="false" :enable-flex="true">
<view class="recipe-track">
<view
v-for="item in getPreviewRecipeList()"
:key="item.id"
class="recipe-item"
@click="navigateToRecipeDetail(item.id)"
>
<image
:src="thumbnailImg(item?.recipeImage?.split(',')[0])"
class="w-310rpx h-296rpx rounded-24rpx mb-26rpx bg-common"
class="recipe-avatar"
mode="aspectFill"
></image>
<view class="flex-center-sb">
<text class="text-30rpx lh-30rpx text-#333 line-clamp-1 tracking-[.04em] font-500"
>{{ item.recipeName }}</text
>
<view class="w-40rpx h-40rpx ml-14rpx shrink-0">
<collection
:is-collected="item.isCollect"
@collectionChange="handleSubmitCollectRecipe(item)"
/>
<text class="recipe-name line-clamp-1">{{ item.recipeName }}</text>
</view>
<view v-if="showRecipeMore()" class="recipe-more" @click="navigateToRecipeList">
<view class="recipe-more__text-wrap">
<text class="recipe-more__text">{{ t("pages.browse.moreRecipes") }}</text>
</view>
<i class="i-carbon:chevron-right recipe-more__icon"></i>
</view>
</template>
</view>
</scroll-view>
</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">
<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(item?.dishImage?.split(',')[0])"
class="w-full h-186rpx rounded-24rpx mb-16rpx bg-common"
:src="thumbnailImg(getMerchantLogo(item))"
class="store-card__cover"
mode="aspectFill"
></image>
<text class="text-30rpx lh-30rpx text-#333 line-clamp-1 tracking-[.04em] font-500"
>{{ item.dishName }}</text
>
/>
<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>
</template>
</view>
</view>
</view>
</scroll-view>
</view>
</view>
<template #bottom>
<view class="h-50px"></view>
@@ -201,4 +191,148 @@ defineExpose({
</view>
</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">
<scroll-view
class="ab-scroll mb-22rpx"
class="ab-scroll mb-10rpx"
scroll-x
show-scrollbar="false"
:scroll-left="scrollLeft1"
@@ -77,6 +77,7 @@
<script setup lang="ts">
import { computed, ref, onMounted, onUnmounted, nextTick } from 'vue'
import { useCategoryNavStore } from '@/store'
// 定义分类项接口(与模板字段一致)
interface CategoryItem {
@@ -97,6 +98,8 @@ const props = withDefaults(defineProps<Props>(), {
categories: () => []
})
const categoryNavStore = useCategoryNavStore()
// 定义事件
const emit = defineEmits<{
/** 点击分类项事件 */
@@ -241,10 +244,11 @@ function onScroll2(e: any) {
}
}
// 点击处理
// 点击处理(列表页菜谱 id 来自 store,不放在 URL query
const handleItemClick = (item: CategoryItem) => {
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) {
@@ -301,14 +305,16 @@ onUnmounted(() => {
min-width: 120rpx;
height: 60rpx;
padding: 0 20rpx;
border: 1px solid #C8C8C8;
background: #fff;
border: none;
border-radius: 30rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
cursor: pointer;
transition: all 0.2s ease;
flex-shrink: 0;
&:active {
transform: translateY(0) scale(0.95);
transform: translateY(0) scale(0.96);
}
.category-icon {
@@ -1,40 +1,277 @@
<script setup lang="ts">
import {thumbnailImg} from "@/utils/utils";
import { thumbnailImg } from '@/utils/utils'
const props = defineProps<{
list: any[];
}>();
const { t } = useI18n();
list: any[]
}>()
const { t } = useI18n()
function handleClickFood(item: any) {
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>
<template>
<scroll-view scroll-x="true">
<view class="flex">
<view class="w-30rpx shrink-0"></view>
<template v-for="(item, index) in list" :key="item?.id ?? index">
<view @click="handleClickFood(item)" :class="[index === 0 ? '' : 'ml-28rpx']">
<image :src="thumbnailImg(item?.shopImages?.split(',')[0])" class="w-448rpx h-252rpx rounded-24rpx mb-20rpx bg-common" mode="aspectFill"></image>
<text class="text-30rpx lh-30rpx text-#333 font-500 line-clamp-1">{{ item?.merchantName }}</text>
<view v-if="+item.deliveryService === 1" class="text-#CE7138 text-24rpx lh-24rpx mt-12rpx">
<scroll-view class="featured-scroll" scroll-x enable-flex :show-scrollbar="false">
<view class="featured-track">
<view class="featured-track__pad" />
<view
v-for="(item, index) in list"
:key="item?.id ?? index"
class="featured-card"
: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') }}
</view>
<view class="text-24rpx lh-24rpx flex items-center mt-12rpx">
<text class="text-#333 font-500">{{ item.rating }}</text>
<image src="@img/chef/124.png" class="w-24rpx h-24rpx mx-4rpx mt-2rpx"></image>
<text class="text-#7D7D7D">({{ item.commentCount }}) {{ item.deliveryTime }} {{ Number(item.deliveryTime) === 1 ? t('common.day') : t('common.days') }}</text>
<view class="featured-card__rating">
<text class="featured-card__stars"></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>
</template>
<view class="w-30rpx shrink-0 op-0">1</view>
</view>
<view class="featured-track__pad featured-track__pad--end" />
</view>
</scroll-view>
</template>
<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>
@@ -61,11 +61,16 @@ function handleCollectionChange(value: boolean) {
</script>
<template>
<view @click="handleClickFood" class="mb-52rpx">
<view class="food-box-img-wrap">
<view v-if="item.isNew == 1" class="dish-new-ribbon">
<text class="dish-new-ribbon__text">NEW</text>
</view>
<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>
<text class="text-30rpx lh-30rpx text-#333 font-500 line-clamp-1"
@@ -86,3 +91,37 @@ function handleCollectionChange(value: boolean) {
</view>
</view>
</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">
import { useUserStore } from "@/store";
const props = withDefaults(
defineProps<{
/** 首页顶栏紧凑模式:更小图标与间距 */
compact?: boolean
}>(),
{ compact: false }
)
const emit = defineEmits(['toggleNotOpen']);
const userStore = useUserStore();
function navigateTo(url: string) {
@@ -12,12 +19,25 @@ function navigateTo(url: string) {
</script>
<template>
<view class="flex items-center">
<view @click="navigateTo('/pages-user/pages/message/index')" class="w-40rpx h-40rpx mr-42rpx relative">
<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>
<image src="@img/chef/114.png" class="w-40rpx h-40rpx"></image>
<view class="flex items-center" :class="compact ? 'gap-20rpx' : ''">
<view
@click="navigateTo('/pages-user/pages/message/index')"
: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>
<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>
</template>
@@ -23,12 +23,22 @@ function handleClickSearch() {
</script>
<template>
<view @click="handleClickSearch" class="flex items-center h-88rpx bg-#F2F3F6 rounded-44rpx pl-36rpx">
<image src="@img/chef/100222.png" class="w-28rpx h-28rpx"></image>
<text class="text-30rpx text-#434343 ml-16rpx tracking-[.04em] font-500">{{ t('components.search.placeholder') }}</text>
<view
@click="handleClickSearch"
class="home-search-bar flex items-center h-88rpx rounded-44rpx pl-36rpx pr-28rpx bg-white"
>
<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>
<style scoped lang="scss">
.home-search-bar {
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.06);
}
.home-search-placeholder {
color: #9a9a9a;
}
</style>
@@ -75,8 +75,11 @@ function selectTab(item: any) {
align-items: center;
justify-content: center;
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.2s;
width: 132rpx;
height: 132rpx;
width: 102rpx;
height: 102rpx;
border-radius: 50%;
background: #fff;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.07);
}
.img-selected {
border: 4rpx solid #ce7138;
@@ -86,8 +89,8 @@ function selectTab(item: any) {
}
.tab-img {
width: 112rpx;
height: 112rpx;
width: 98rpx;
height: 98rpx;
border-radius: 50%;
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
@@ -25,6 +25,7 @@ import {
} from "@/service";
import usePage from "@/hooks/usePage";
import {getFeaturedDishList} from "@/pages-store/service";
import { formatSalesCount } from "@/utils/utils";
const configStore = useConfigStore();
const userStore = useUserStore();
const props = defineProps<{
@@ -41,6 +42,19 @@ function isSoldOutStock(stockLike: unknown) {
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) {
if(userStore.checkLogin()) {
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() {
console.log('手动触发下拉刷新了')
@@ -221,7 +259,7 @@ function handleClickSwiper(item: any) {
console.log(item, '点击轮播图')
switch (Number(item.activityType)) {
case 1: // 商家列表
navigateTo('/pages-store/pages/list/index?id=')
navigateTo('/pages-store/pages/list/index')
break
case 2: // 活动菜品列表
navigateTo('/pages-store/pages/dishes/index?id=' + item.id)
@@ -261,7 +299,7 @@ const debouncedEmit = debounce(1300, (isCollected: boolean, id: string, type: Co
<template>
<view
class="bg-#fff"
class="home-page-root"
:style="[
{
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">
<template #top>
<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>-->
<image
src="@img/logo.png"
class="w-52rpx h-52rpx shrink-0"
></image>
<view class="bg-#D8D8D8 w-1rpx h-40rpx mx-14rpx"></view>
<view @click="navigateTo('/pages/address/index')" class="text-#00A76D text-28rpx lh-28rpx flex items-center">
<!-- 设计稿品牌行 + 右侧地址胶囊消息/客服购物车 -->
<view class="home-top-header px-24rpx pt-12rpx pb-8rpx">
<view class="flex items-center justify-between gap-12rpx">
<view class="flex items-center gap-14rpx min-w-0 flex-1">
<image src="@img/logo.png" class="w-52rpx h-52rpx shrink-0" style="border-radius: 50%;"></image>
<text class="text-32rpx lh-36rpx text-#1a1a1a font-bold tracking-tight shrink-0">{{ Config.appName }}</text>
</view>
<view class="flex items-center gap-10rpx shrink-0">
<view class="home-loc-pill" @click="navigateTo('/pages-user/pages/search-address/index')">
<!-- <image src="@img/chef/101.png" class="home-loc-pill__pin w-22rpx h-22rpx shrink-0"></image> -->
<text class="home-loc-pill__text line-clamp-1">{{
userStore.userLocation.location || t('pages.home.default-location')
}}</text>
<image src="@img/chef/119.png" class="home-loc-pill__arrow w-20rpx h-20rpx shrink-0 op-50 mt-2rpx"></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-24rpx h-24rpx ml-6rpx mt-4rpx shrink-0"
></image>
<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>
</template>
@@ -294,169 +346,415 @@ const debouncedEmit = debounce(1300, (isCollected: boolean, id: string, type: Co
>
<home-skeleton />
</view>
<view class="flex-center-sb px-30rpx pt-34rpx">
<!--展示用户的定位城市如果用户没有使用定位则展示选择的城市用户选择城市后需要更新定位城市-->
<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">
<view class="px-24rpx pt-12rpx pb-8rpx">
<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" />
</view>
</view>
<swiper
class="card-swiper"
class="home-promo-swiper card-swiper"
:circular="true"
:autoplay="true"
previous-margin="60rpx"
next-margin="60rpx"
previous-margin="48rpx"
next-margin="48rpx"
>
<swiper-item
v-for="(item, sIdx) in swiperList"
:key="item.id ?? sIdx"
@click="handleClickSwiper(item)"
>
<template v-for="item in swiperList" :key="item.id">
<swiper-item @click="handleClickSwiper(item)" class="">
<image
:src="item.activityImage"
class="swiper-item-content w-full h-100% rounded-24rpx bg-common"
class="swiper-item-content w-full h-100% rounded-32rpx bg-common"
></image>
</swiper-item>
</template>
</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" />-->
<view class="animate-in fade-in animate-ease-in animate-duration-300" v-if="isShowMerchant">
<!-- Featured on ChefLink 精选商家 -->
<view v-if="featuredList.length > 0" class="mt-56rpx">
<view
class="mb-30rpx pl-30rpx text-36rpx lh-36rpx text-#333 font-bold"
>{{ t('pages.home.featured-on') }}</view>
<!-- Featured on ChefLink 精选商家浅底 + 横向卡片对齐设计稿 -->
<view v-if="featuredList.length > 0" class="featured-merchants-section mt-36rpx px-24rpx pt-8rpx pb-16rpx">
<view class="mb-24rpx px-6rpx text-32rpx lh-44rpx text-#1a1a1a">
{{ t('pages.home.featured-on') }}
</view>
<featured-on :list="featuredList" />
</view>
<!-- Nearby Merchants 附近商家 -->
<view v-if="nearbyList.length > 0" class="mt-56rpx">
<view
class="mb-32rpx pl-30rpx text-36rpx lh-36rpx text-#333 font-bold"
>{{ t('pages.home.nearby-merchants') }}</view>
<view v-if="nearbyList.length > 0" class="nearby-merchants-block mt-36rpx px-24rpx pb-16rpx">
<view class="mb-24rpx px-6rpx text-32rpx lh-44rpx text-#1a1a1a ">
{{ t('pages.home.nearby-merchants') }}
</view>
<nearby-merchants :list="nearbyList" />
</view>
</view>
<!-- List -->
<view class="mt-56rpx px-30rpx">
<view class="mb-32rpx text-36rpx lh-36rpx text-#333 font-bold"
<!-- List 精选菜品瀑布流浅底 + 白卡片 + 阴影结构对齐设计稿 -->
<view class="featured-dishes-section mt-36rpx px-24rpx pb-40rpx">
<view class="mb-24rpx px-6rpx text-32rpx lh-44rpx text-#1a1a1a "
>{{ t('pages.home.featured-dishes') }}</view>
<template v-for="(item, index) in dataList" :key="index">
<view @click="navigateToDishes(item)" class="w-100% mb-30rpx">
<view class="relative h-448rpx rounded-24rpx mb-28rpx">
<view @click.stop="handleDishCollectionClick(item)" class="w-68rpx h-68rpx absolute z-2 top-0 right-0">
<view class="waterfall-row flex gap-16rpx items-start">
<view
v-for="(col, colIndex) in featuredDishColumns"
:key="colIndex"
class="waterfall-col flex-1 min-w-0 flex flex-col gap-16rpx"
>
<view
v-for="item in col"
:key="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
:src="item?.dishImage?.split(',')[0]"
mode="aspectFill"
class="featured-dish-img"
/>
<view
v-if="isSoldOutStock(item?.stock)"
class="featured-dish-sold-dim"
/>
<image
v-if="isSoldOutStock(item?.stock)"
src="/static/app/images/SoldOut.png"
mode="aspectFill"
class="featured-dish-sold-overlay"
/>
<view
@click.stop="handleDishCollectionClick(item)"
class="featured-dish-collect w-56rpx h-56rpx absolute z-4 top-12rpx right-12rpx center"
>
<image
v-if="!item.isCollect"
src="@img-store/1334.png"
mode="aspectFill"
class="w-full h-full"
class="w-44rpx h-44rpx featured-dish-collect-icon"
/>
<image
v-else
src="@img-store/1337.png"
mode="aspectFill"
class="w-full h-full"
class="w-44rpx h-44rpx featured-dish-collect-icon"
/>
</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"
class="w-full h-full rounded-24rpx bg-common"
/>
<view class="featured-dish-body">
<view class="featured-dish-meta flex items-start justify-between gap-12rpx mb-14rpx">
<view class="min-w-0 flex-1">
<text class="featured-dish-price">US${{ getFeaturedDishDisplayPrice(item) }}</text>
<!-- <text
v-if="Number(item?.originalPrice) > Number(item?.discountPrice)"
class="featured-dish-original"
>US${{ item?.originalPrice }}</text> -->
</view>
<view class="line-clamp-1 text-30rpx text-#333 font-500">
<text class="featured-dish-sales shrink-0">{{ t('pages-store.store.sales') }}: {{ formatSalesCount(item?.salesCount) }}</text>
</view>
<view class="featured-dish-title line-clamp-2 mb-16rpx">
{{ item?.dishName }}
</view>
<view class="flex-center-sb mt-12rpx">
<text class="text-32rpx lh-30rpx text-#333 font-500">US${{ item?.discountPrice }}</text>
<view class="flex items-center justify-between gap-12rpx">
<view
v-if="Number(item?.memberPrice) > 0"
class="member-price-tag text-[#FBE3C3] font-500 text-30rpx lh-30rpx center pl-6rpx break-all"
class="featured-dish-member shrink min-w-0"
>
<text>{{ t('pages-store.store.members') }}: </text>
${{ item?.memberPrice }}
<text class="featured-dish-member-inner">{{ t('pages-store.store.members') }}: US${{ item?.memberPrice }}</text>
</view>
</view>
<view class="flex-center-sb mt-12rpx">
<view class="text-28rpx text-#999">
<view class="line-through">US${{ item?.originalPrice }}</view>
<view>{{ t('pages-store.store.sales') }}:{{ item?.salesCount }}</view>
</view>
<view class="center w-64rpx h-64rpx rounded-50% bg-white shadow-lg">
<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-30rpx h-30rpx shrink-0"
></image>
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>
</template>
</view>
<template #bottom>
<view class="h-50px"></view>
<view :style="[configStore.iosSafeBottomPlaceholder]"></view>
</template>
</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 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">
<!-- 回到顶部购物车已并入顶栏不再使用底部浮条 -->
<view v-if="showBackToTop" @click="scrollToTop" class="home-back-top fixed 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>
</view>
</view>
</template>
<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 {
height: 420rpx;
height: 400rpx;
}
.swiper-item-content {
width: 100%;
height: 100%;
transform: scale(0.95);
border-radius: 20rpx;
transition: transform 0.3s;
transform: scale(0.94);
border-radius: 32rpx;
box-shadow: 0 8rpx 28rpx rgba(0, 0, 0, 0.08);
transition: transform 0.3s ease;
}
.swiper-item-active .swiper-item-content {
transform: scale(1);
}
.member-price-tag {
min-width: 220rpx;
height: 42rpx;
background-image: url("/static/images/chef/1282.png");
background-size: 100% 100%;
background-repeat: no-repeat;
/* 精选商家 / 精选菜品 区块(与页面背景统一) */
.featured-merchants-section,
.featured-dishes-section {
background: #f2f2f2;
}
.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>
@@ -194,7 +194,7 @@ const poster = computed(() => ({
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',
css: {
width: '124rpx',
@@ -83,10 +83,16 @@ defineExpose({
</view>
<view class="shrink-0 ml-20rpx" v-if="userStore.userInfo.invitationCode">
<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"
sizeUnit="rpx"
: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>
@@ -1,48 +1,41 @@
<template>
<view class="mine-skeleton">
<!-- 用户信息区域 -->
<view class="user-info-section flex-center-sb">
<view class="user-name-skeleton skeleton-item"></view>
<view class="user-avatar-skeleton skeleton-item"></view>
<view class="mine-skeleton-page">
<!-- 顶部用户信息对应新布局 -->
<view class="sk-header">
<view class="sk-header__left">
<view class="sk-name skeleton-item"></view>
<view class="sk-sub-row">
<view class="sk-sub-text skeleton-item"></view>
<view class="sk-badge skeleton-item"></view>
</view>
<view class="sk-date skeleton-item"></view>
</view>
<view class="sk-avatar skeleton-item"></view>
</view>
<!-- 统计卡片区域 -->
<view class="stats-section">
<view class="stats-list">
<view
v-for="i in 3"
:key="i"
class="stat-card-skeleton skeleton-item"
>
<view class="stat-number-skeleton skeleton-item"></view>
<view class="stat-label-skeleton skeleton-item"></view>
</view>
<!-- 三项数据胶囊卡 -->
<view class="sk-stats skeleton-item">
<view class="sk-stats__cell" v-for="i in 3" :key="i">
<view class="sk-num skeleton-item"></view>
<view class="sk-label skeleton-item"></view>
</view>
<view class="sk-divider" v-if="true"></view>
</view>
<!-- 会员横幅区域 -->
<view class="member-banner-section">
<view class="member-banner-skeleton skeleton-item">
<view class="banner-content">
<view class="banner-title-skeleton skeleton-item"></view>
<view class="banner-subtitle-skeleton skeleton-item"></view>
</view>
<view class="banner-icon-skeleton skeleton-item"></view>
</view>
<!-- 会员横幅 -->
<view class="sk-banner skeleton-item">
<view class="sk-banner__title skeleton-item"></view>
<view class="sk-banner__sub skeleton-item"></view>
</view>
<!-- 菜单列表区域 -->
<view class="menu-section">
<view class="menu-list">
<view
v-for="i in 9"
:key="i"
class="menu-item-skeleton"
>
<view class="menu-icon-skeleton skeleton-item"></view>
<view class="menu-text-skeleton skeleton-item"></view>
<view class="menu-arrow-skeleton skeleton-item"></view>
<!-- 菜单列表卡片 -->
<view class="sk-menu">
<view class="sk-menu__row" v-for="i in 9" :key="i">
<view class="sk-menu__left">
<view class="sk-menu__icon skeleton-item"></view>
<view class="sk-menu__text skeleton-item"></view>
</view>
<view class="sk-menu__arrow skeleton-item"></view>
</view>
</view>
</view>
@@ -70,161 +63,166 @@
}
}
.mine-skeleton {
background-color: #fff;
padding: 0 30rpx;
.mine-skeleton-page {
background-color: #f5f5f5;
padding: 0 30rpx 30rpx;
}
// 用户信息区域
.user-info-section {
padding: 36rpx 0 0;
.sk-header {
padding-top: 32rpx;
padding-bottom: 28rpx;
display: flex;
align-items: center;
gap: 20rpx;
justify-content: space-between;
gap: 24rpx;
.user-avatar-skeleton {
&__left {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
}
}
.sk-name {
width: 420rpx;
height: 44rpx;
border-radius: 10rpx;
}
.sk-sub-row {
margin-top: 12rpx;
display: flex;
align-items: center;
gap: 12rpx;
}
.sk-sub-text {
width: 200rpx;
height: 22rpx;
border-radius: 999rpx;
}
.sk-badge {
width: 120rpx;
height: 22rpx;
border-radius: 999rpx;
}
.sk-date {
margin-top: 10rpx;
width: 360rpx;
height: 22rpx;
border-radius: 8rpx;
}
.sk-avatar {
width: 130rpx;
height: 130rpx;
border-radius: 50%;
flex-shrink: 0;
}
.user-name-skeleton {
width: 233rpx;
height: 56rpx;
border-radius: 8rpx;
}
}
// 统计卡片区域
.stats-section {
padding: 90rpx 0 0;
.stats-list {
.sk-stats {
margin-top: 24rpx;
height: 120rpx;
border-radius: 60rpx;
background: #fff;
box-shadow: 0 12rpx 30rpx rgba(0, 0, 0, 0.06);
display: flex;
gap: 36rpx;
align-items: center;
padding: 0 20rpx;
position: relative;
}
.stat-card-skeleton {
.sk-stats__cell {
flex: 1;
height: 194rpx;
border-radius: 24rpx;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 20rpx;
.stat-number-skeleton {
width: 80rpx;
height: 44rpx;
border-radius: 6rpx;
gap: 10rpx;
}
.stat-label-skeleton {
width: 120rpx;
height: 28rpx;
border-radius: 6rpx;
}
}
}
.sk-num {
width: 70rpx;
height: 30rpx;
border-radius: 8rpx;
}
// 会员横幅区域
.member-banner-section {
padding: 40rpx 0 0;
.sk-label {
width: 90rpx;
height: 20rpx;
border-radius: 8rpx;
}
.member-banner-skeleton {
width: 690rpx;
.sk-divider {
display: none;
}
.sk-banner {
margin-top: 24rpx;
height: 152rpx;
border-radius: 24rpx;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 28rpx;
.banner-content {
overflow: hidden;
background: #fff;
padding: 22rpx 28rpx;
display: flex;
flex-direction: column;
gap: 12rpx;
justify-content: space-between;
}
.banner-title-skeleton {
width: 457rpx;
height: 57rpx;
.sk-banner__title {
width: 420rpx;
height: 32rpx;
border-radius: 10rpx;
}
.sk-banner__sub {
width: 520rpx;
height: 20rpx;
border-radius: 8rpx;
}
.banner-subtitle-skeleton {
width: 497rpx;
height: 48rpx;
border-radius: 6rpx;
}
.sk-menu {
margin-top: 14rpx;
background: #fff;
border-radius: 24rpx;
overflow: hidden;
}
.banner-icon-skeleton {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
}
}
}
// 菜单列表区域
.menu-section {
padding: 80rpx 0 0;
.menu-list {
display: flex;
flex-direction: column;
gap: 0;
.menu-item-skeleton {
.sk-menu__row {
padding: 28rpx 24rpx;
display: flex;
align-items: center;
padding: 22rpx 0;
// border-bottom: 1rpx solid #f0f0f0;
justify-content: space-between;
border-bottom: 1rpx solid #efefef;
&:last-child {
border-bottom: none;
}
}
.menu-icon-skeleton {
.sk-menu__left {
display: flex;
align-items: center;
gap: 18rpx;
}
.sk-menu__icon {
width: 44rpx;
height: 44rpx;
border-radius: 8rpx;
margin-right: 20rpx;
border-radius: 10rpx;
}
.menu-text-skeleton {
flex: 1;
height: 30rpx;
border-radius: 6rpx;
.sk-menu__text {
width: 240rpx;
height: 26rpx;
border-radius: 10rpx;
}
.menu-arrow-skeleton {
.sk-menu__arrow {
width: 22rpx;
height: 30rpx;
border-radius: 4rpx;
margin-left: 20rpx;
}
}
}
}
// 响应式设计
@media (max-width: 750rpx) {
.stats-list {
flex-direction: column;
gap: 20rpx;
}
.member-banner-skeleton {
flex-direction: column;
gap: 20rpx;
padding: 20rpx;
.banner-content {
align-items: center;
text-align: center;
}
}
border-radius: 8rpx;
}
</style>
@@ -173,7 +173,7 @@ defineExpose({
<template>
<view
class="bg-#fff"
class="view-bg"
:style="[
{
height: configStore.windowHeight + 'px',
@@ -194,127 +194,103 @@ defineExpose({
v-show="!loading"
class="animate-in fade-in animate-ease-in animate-duration-300"
>
<view class="px-30rpx">
<view
@click="navigateTo('/pages-user/pages/user-info/index')"
class="flex-center-sb mt-32rpx mb-82rpx"
>
<!-- <text class="text-56rpx text-#333 leading-56rpx font-bold tracking-[.04em]"-->
<!-- >{{ userStore.isLogin ? `${userStore.userInfo.firstName} ${userStore.userInfo.surname}` : t('common.pleaseLogin') }}-->
<!-- </text>-->
<text
class="text-56rpx text-#333 leading-56rpx font-bold tracking-[.04em]"
>
<view class="mine-page ">
<!-- 顶部用户信息 -->
<view class="mine-header" @click="navigateTo('/pages-user/pages/user-info/index')">
<view class="mine-header__left">
<text class="mine-header__name">
{{
userStore.isLogin
? ([userStore.userInfo.firstName, userStore.userInfo.surname].filter(Boolean).join(' ') || t('common.unknownUser'))
? ([userStore.userInfo.firstName, userStore.userInfo.surname].filter(Boolean).join(' ') ||
t('common.unknownUser'))
: t('common.pleaseLogin')
}}
</text>
<view class="mine-header__sub" v-if="userStore.isLogin">
<text class="mine-header__member">{{ Config.appName }} {{ t('pages.mine.member') }}</text>
<view class="mine-header__badge" v-if="!isUserMember">
{{ t('pages.mine.openMember') }}
</view>
<text class="mine-header__chev"></text>
</view>
<text class="mine-header__date" v-if="isUserMember && userStore.userInfo.userMembershipVo?.expireTime">
{{ t('common.expireTime') }}{{ formatTimestampWithMonthName(userStore.userInfo.userMembershipVo?.expireTime) }}
</text>
</view>
<image
v-if="userStore.isLogin"
:src="userStore.userInfo.avatar"
class="w-130rpx h-130rpx rounded-50%"
class="mine-header__avatar"
mode="aspectFill"
/>
<image
v-else
class="w-130rpx h-130rpx rounded-50%"
mode="aspectFill"
src="@img/chef/default_avatar.png"
/>
<image v-else class="mine-header__avatar" mode="aspectFill" src="@img/chef/default_avatar.png" />
</view>
<view class="flex-center-sb mb-36rpx">
<view
@click="navigateTo('/pages-user/pages/collection/index')"
class="flex-1 flex-col center bg-#F6F6F6 rounded-24rpx h-194rpx"
>
<view
class="text-44rpx text-#333 leading-44rpx font-bold mb-24rpx"
>{{ userStore.userInfo.collectNum || 0 }}</view
>
<view class="text-28rpx text-#333 leading-28rpx tracking-[.04em]">{{
t("pages.mine.collection")
}}</view>
<!-- 三项数据胶囊卡 -->
<view class="mine-stats">
<view class="mine-stats__item" @click="navigateTo('/pages-user/pages/collection/index')">
<text class="mine-stats__num">{{ userStore.userInfo.collectNum || 0 }}</text>
<text class="mine-stats__label">{{ t('pages.mine.collection') }}</text>
</view>
<view
@click="navigateTo('/pages-user/pages/balance/index')"
class="flex-1 mx-32rpx flex-col center bg-#F6F6F6 rounded-24rpx h-194rpx"
>
<view
class="text-44rpx text-#333 leading-44rpx font-bold mb-24rpx"
>{{ userStore.userInfo?.balance || 0 }}</view
>
<view class="text-28rpx text-#333 leading-28rpx tracking-[.04em]">{{
t("pages.mine.wallet")
}}</view>
<view class="mine-stats__divider" />
<view class="mine-stats__item" @click="navigateTo('/pages-user/pages/balance/index')">
<text class="mine-stats__num">{{ userStore.userInfo?.balance || 0 }}</text>
<text class="mine-stats__label">{{ t('pages.mine.wallet') }}</text>
</view>
<view
@click="changeOrderFn"
class="flex-1 flex-col center bg-#F6F6F6 rounded-24rpx h-194rpx"
>
<view
class="text-44rpx text-#333 leading-44rpx font-bold mb-24rpx"
>{{ userStore.userInfo.orderNum || 0 }}</view
>
<view class="text-28rpx text-#333 leading-28rpx tracking-[.04em]">{{
t("pages.mine.order")
}}</view>
<view class="mine-stats__divider" />
<view class="mine-stats__item" @click="changeOrderFn">
<text class="mine-stats__num">{{ userStore.userInfo.orderNum || 0 }}</text>
<text class="mine-stats__label">{{ t('pages.mine.order') }}</text>
</view>
</view>
<template v-if="isUserMember">
<view @click="navigateTo('/pages-user/pages/member/index')" class="w-full h-152rpx relative mb-52rpx">
<image src="@img/chef/100203.png" class="w-full h-full absolute top-0 left-0"></image>
<view class="pl-28rpx py-22rpx pr-165rpx relative z-1 h-full flex flex-col justify-between">
<view class="text-40rpx text-#333 font-bold">
{{ Config.appName }}
</view>
<view class="text-24rpx lh-24rpx text-#935D04 tracking-[.08em]">
<view @click="navigateTo('/pages-user/pages/member/index')" class="member-banner">
<image src="@img/chef/100203.png" class="member-banner__bg"></image>
<view class="member-banner__content">
<view class="member-banner__title">{{ Config.appName }}</view>
<view class="member-banner__sub">
{{ t('common.expireTime') }}{{ formatTimestampWithMonthName(userStore.userInfo.userMembershipVo?.expireTime) }}
</view>
</view>
</view>
</template>
<template v-else>
<view @click="navigateTo('/pages-user/pages/member/index')" class="w-full h-152rpx relative mb-52rpx">
<image src="@img/chef/100203.png" class="w-full h-full absolute top-0 left-0"></image>
<view class="pl-28rpx py-22rpx pr-165rpx relative z-1 h-full flex flex-col justify-between">
<view class="text-40rpx text-#333 font-bold">
<!--用户没有试用过会员-->
<view @click="navigateTo('/pages-user/pages/member/index')" class="member-banner">
<image src="@img/chef/100203.png" class="member-banner__bg"></image>
<view class="member-banner__content">
<view class="member-banner__title">
<template v-if="!userStore.userInfo.userMembershipVo">{{ t('pages.mine.member-title') }}</template>
<template v-else>{{ t('pages.mine.join') }} {{ Config.appName }}</template>
</view>
<view class="text-24rpx lh-24rpx text-#935D04 tracking-[.08em]">{{ t('pages.mine.member-desc') }}</view>
<view class="member-banner__sub">{{ t('pages.mine.member-desc') }}</view>
</view>
</view>
</template>
<view style="height: 30rpx;background-color: #f5f5f5;" />
<!-- 菜单列表卡片 -->
<view class="mine-menu">
<view
class="flex-center-sb py-28rpx bg-#fff"
class="mine-menu__row"
v-for="(item, index) in tabBarList"
:key="item.code"
:class="[
index === tabBarList.length - 1 ? 'mb-58rpx' : 'border-bottom',
]"
:class="[index === tabBarList.length - 1 ? 'mine-menu__row--last' : '']"
@click="handleTabClick(item)"
>
<view class="flex items-center">
<image
class="w-44rpx h-44rpx shrink-0 mr-18rpx"
:src="item.iconPath"
></image>
<text class="text-30rpx text-primary font-500 lh-30rpx tracking-[.04em]">{{
item.text
}}</text>
<view class="mine-menu__left">
<image class="mine-menu__icon" :src="item.iconPath"></image>
<text class="mine-menu__text">{{ item.text }}</text>
</view>
<view class="flex items-center shrink-0 ml-20rpx">
<image class="w-22rpx h-30rpx" src="@img/chef/100202.png"></image>
<image class="mine-menu__arrow" src="@img/chef/100202.png"></image>
</view>
</view>
<view class="mine-bottom-spacer" />
</view>
</view>
<template #bottom>
<view class="h-50px"></view>
@@ -325,7 +301,229 @@ defineExpose({
</template>
<style scoped lang="scss">
.border-bottom {
border-bottom: 1rpx solid #dfdfdf;
.view-bg {
background-color: #fff;
}
.mine-page {
padding-bottom: 20rpx;
}
.mine-header {
margin: 0 30rpx;
padding: 32rpx 0 28rpx;
display: flex;
align-items: center;
justify-content: space-between;
&__left {
min-width: 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
&__name {
font-size: 38rpx;
line-height: 40rpx;
font-weight: 700;
color: #333;
letter-spacing: 0.04em;
max-width: 520rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&__sub {
margin-top: 12rpx;
display: inline-flex;
align-items: center;
gap: 10rpx;
color: #b68b3e;
font-size: 22rpx;
line-height: 22rpx;
font-weight: 600;
}
&__member {
color: #b68b3e;
}
&__badge {
padding: 8rpx 16rpx;
border-radius: 999rpx;
background: rgba(182, 139, 62, 0.12);
color: #b68b3e;
font-size: 20rpx;
line-height: 20rpx;
font-weight: 700;
letter-spacing: 0.02em;
}
&__chev {
transform: translateY(-1rpx);
color: #b68b3e;
font-size: 24rpx;
line-height: 22rpx;
}
&__date {
margin-top: 10rpx;
font-size: 22rpx;
line-height: 22rpx;
color: #999;
font-weight: 500;
}
&__avatar {
width: 90rpx;
height: 90rpx;
border-radius: 50%;
flex-shrink: 0;
background-color: #eee;
}
}
.mine-stats {
margin: 15rpx 35rpx;
// margin-top: 24rpx;
background: #fff;
border-radius: 36rpx;
height: 120rpx;
padding: 0 20rpx;
display: flex;
align-items: center;
box-shadow: 0 12rpx 30rpx rgba(10, 10, 10, 0.06);
&__item {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8rpx;
}
&__num {
font-size: 32rpx;
line-height: 30rpx;
font-weight: 700;
color: #333;
}
&__label {
font-size: 26rpx;
line-height: 28rpx;
color: #666;
font-weight: 700;
letter-spacing: 0.04em;
// font-weight: 500;
}
&__divider {
width: 1rpx;
height: 56rpx;
background: #eaeaea;
flex-shrink: 0;
}
}
.member-banner {
margin: 15rpx 30rpx;
height: 152rpx;
position: relative;
border-radius: 36rpx;
overflow: hidden;
&__bg {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
&__content {
position: relative;
z-index: 1;
height: 100%;
padding: 22rpx 165rpx 22rpx 28rpx;
display: flex;
flex-direction: column;
justify-content: space-between;
}
&__title {
font-size: 32rpx;
line-height: 32rpx;
font-weight: 800;
color: #333;
}
&__sub {
font-size: 20rpx;
line-height: 20rpx;
font-weight: 600;
color: #935d04;
letter-spacing: 0.08em;
}
}
.mine-menu {
margin:20rpx 30rpx 30rpx;
background: #fff;
border-radius: 24rpx;
overflow: hidden;
&__row {
padding: 28rpx 24rpx;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1rpx solid #efefef;
}
&__row--last {
border-bottom: none;
}
&__left {
min-width: 0;
display: flex;
align-items: center;
gap: 18rpx;
}
&__icon {
width: 38rpx;
height: 38rpx;
flex-shrink: 0;
}
&__text {
font-size: 26rpx;
line-height: 26rpx;
color: #333;
// font-weight: 600;
letter-spacing: 0.04em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&__arrow {
width: 22rpx;
height: 30rpx;
flex-shrink: 0;
opacity: 0.9;
}
}
.mine-bottom-spacer {
height: 58rpx;
}
</style>
@@ -3,10 +3,11 @@ import {
appMerchantOrderOrderListPost,
type MerchantOrderVo
} from "@/service";
import {callPhone, formatTimestampWithWeekday} from "@/utils/utils";
import {callPhone} from "@/utils/utils";
import {OrderCancelStatus, OrderStatus} from "@/constant/enums";
import {useUserStore} from "@/store";
import HomeSkeleton from "@/pages/home/components/tabbar-home/components/home-skeleton.vue";
import {dayjs} from "@/plugin";
const userStore = useUserStore();
const { t } = useI18n();
const props = defineProps<{
@@ -31,7 +32,7 @@ const {paging, dataList, loading, queryList, firstLoaded} = usePage<MerchantOrde
},
body: {
userPort: 1,
orderStatusList: props.status ? [Number(props.status + 1)] : [],
orderStatusList: props.status !== null && props.status !== '' ? [Number(props.status)] : [],
}
})
} 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({
url: '/pages-store/pages/order/index?id=' + item.id
})
@@ -66,121 +178,132 @@ defineExpose({
</script>
<template>
<view class="h-full">
<view class="list-root h-full">
<z-paging ref="paging" v-model="dataList" @query="queryList" :fixed="false" :auto="false">
<view class="p-30rpx">
<view class="page-pad">
<view
v-show="loading"
class="animate-in fade-in animate-ease-out animate-duration-300"
>
<template v-for="item in 3">
<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-28rpx">
<view class="w-280rpx h-34rpx skeleton-item rounded-8rpx"></view>
<view class="w-120rpx h-34rpx skeleton-item rounded-8rpx"></view>
<view v-for="sk in 3" :key="'sk-' + sk" class="order-card order-card--skeleton mb-24rpx">
<view class="flex-center-sb mb-24rpx">
<view class="w-280rpx h-32rpx skeleton-item rounded-8rpx"></view>
<view class="w-120rpx h-32rpx skeleton-item rounded-8rpx"></view>
</view>
<!-- 商家信息骨架屏 -->
<view class="flex items-center my-28rpx">
<view class="flex items-center mb-24rpx">
<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-60rpx h-32rpx skeleton-item rounded-6rpx ml-20rpx"></view>
<view class="w-240rpx h-28rpx skeleton-item rounded-6rpx"></view>
<view class="w-72rpx h-28rpx skeleton-item rounded-20rpx ml-16rpx"></view>
</view>
<!-- 轮播图骨架屏 -->
<view class="mb-30rpx">
<view class="w-100% h-508rpx skeleton-item rounded-36rpx mb-10rpx"></view>
<view class="w-180rpx h-30rpx skeleton-item rounded-6rpx"></view>
<view class="flex gap-16rpx mb-24rpx">
<view v-for="i in 3" :key="i" class="sk-thumb">
<view class="sk-thumb-img skeleton-item"></view>
<view class="w-100rpx h-22rpx skeleton-item rounded-4rpx mt-10rpx"></view>
</view>
<!-- 底部按钮区域骨架屏 -->
<view class="flex-center-sb">
<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 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="w-72rpx h-22rpx skeleton-item rounded-4rpx"></view>
</view>
</view>
<view class="flex justify-end">
<view class="sk-pill skeleton-item"></view>
</view>
</view>
</template>
</view>
<view
class="animate-in fade-in animate-ease-in animate-duration-300"
v-if="!loading"
>
<template v-for="(item,index) in dataList">
<view @click="handleClick(item)" :class="[index === 0 ? '' : 'mt-30rpx']" class="w-full px-50rpx pt-43rpx pb-36rpx bg-white rounded-36rpx">
<view class="flex-center-sb text-34rpx lh-34rpx font-bold">
<text class="text-#333 tracking-[.04em]">{{ formatTimestampWithWeekday(item.endScheduledTime) }}</text>
<text class="text-#00A76D tracking-[.04em] shrink-0">
<template v-if="
+item.refundStatus === OrderCancelStatus.APPLIED ||
+item.refundStatus === OrderCancelStatus.APPROVED ||
+item.refundStatus === OrderCancelStatus.REJECTED
">
<template v-if="+item.refundStatus === OrderCancelStatus.APPLIED">
{{ 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>
<view
v-for="(item,index) in dataList"
:key="item.id"
:class="[index === 0 ? '' : 'mt-24rpx']"
class="order-card"
@click="handleClick(item)"
>
<view class="flex-center-sb items-start gap-16rpx">
<text class="order-time">{{ formatOrderCardTime(item) }}</text>
<text class="order-status shrink-0" :class="{ 'text-#FF6106': isSameCode(item.orderStatus, OrderStatus.MERCHANT_REJECTED) }">
{{ getOrderStatusText(item) }}
</text>
</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>
{{ item.merchantVo.merchantName }}
<!--收货方式(1-派送 2-自取)-->
<view v-if="+item.receiveMethod === 1" class="bg-#FF6106 rounded-6rpx h-32rpx text-#fff text-22rpx center px-10rpx ml-20rpx">
<view class="store-row">
<image src="@img/chef/126.png" class="store-icon" mode="aspectFit"></image>
<text class="store-name line-clamp-1">{{ item.merchantVo?.merchantName }}</text>
<view v-if="isSameCode(item.receiveMethod, 1)" class="recv-tag recv-tag--del">
{{ t('pages.order.DEL') }}
</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') }}
</view>
</view>
<swiper class="h-568rpx mb-30rpx" :autoplay="true">
<swiper-item v-for="img in item.merchantOrderDishVoList">
<view class="goods-row">
<scroll-view scroll-x class="goods-scroll" :show-scrollbar="false" :enable-flex="true">
<view class="goods-track">
<view
v-for="(dish, di) in getOrderDishes(item)"
:key="dish.id ?? di"
class="goods-cell"
>
<view class="goods-img-wrap">
<image
:src="img.merchantDishVo.dishImage?.split(',')[0]"
:src="dishCover(dish)"
mode="aspectFill"
class="w-100% h-508rpx rounded-36rpx bg-common"
></image>
<view class="mt-10rpx text-30rpx font-500 line-clamp-1">{{ img.merchantDishVo.dishName }}</view>
</swiper-item>
</swiper>
<view class="flex-center-sb">
<view>
<image v-if="+item.receiveMethod === 2" src="@img/chef/127.png" class="w-56rpx h-56rpx"></image>
<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>
<view class="flex items-center gap-20rpx font-500">
<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 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 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>
<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="action-row" @click.stop>
<view class="action-left">
<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>
</view>
</view>
</view>
</template>
</view>
</view>
</z-paging>
@@ -188,5 +311,233 @@ defineExpose({
</template>
<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>
@@ -67,32 +67,30 @@ defineExpose({
<z-paging-swiper>
<template #top>
<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="tab pl-20rpx mt-24rpx">
<!-- <view class="pl-30rpx pt-18rpx text-56rpx lh-56rpx text-#333 font-bold tracking-[.04em]">{{ t('pages.order.title') }}</view> -->
<view class="tab mt-16rpx bg-white">
<wd-tabs
slidable="always"
key="tab"
color="#333"
inactiveColor="#999"
:line-height="3"
:line-width="30"
color="#14181B"
inactiveColor="#B0B0B0"
:line-height="6"
:line-width="48"
v-model="currentIndex"
@click="handleClickTab"
>
<template v-for="(item, index) in tabList" :key="index">
<wd-tab :title="item.name"></wd-tab>
</template>
<wd-tab v-for="(item, index) in tabList" :key="index" :title="item.name"></wd-tab>
</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>
</view> -->
</view>
</template>
<swiper class="h-full"
<swiper class="h-full order-swiper"
:current="currentIndex"
@change="handleSwiperChange">
<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>
<template #bottom>
@@ -106,22 +104,44 @@ defineExpose({
<style scoped lang="scss">
.tab {
:deep(.wd-tabs) {
background: #fff !important;
.wd-tabs__line {
bottom: 0 !important;
z-index: 99 !important;
border-radius: 0 !important;
border-radius: 2rpx !important;
background: #14181B !important;
}
.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 {
font-size: 32rpx !important;
font-size: 26rpx !important;
text-overflow: unset !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 {
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>
@@ -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>
+56 -82
View File
@@ -1,49 +1,38 @@
<template>
<view class="search-skeleton pt-88rpx">
<!-- 头部区域 -->
<view class="header-section">
<view class="back-button-skeleton skeleton-item"></view>
<view class="search-bar-skeleton skeleton-item"></view>
<view class="search-skeleton">
<view class="search-skeleton__header px-24rpx pt-8rpx pb-12rpx flex items-center justify-between gap-16rpx">
<view class="search-skeleton__circle skeleton-item"></view>
<view class="search-skeleton__pill flex-1 skeleton-item"></view>
<view class="search-skeleton__circle skeleton-item"></view>
</view>
<!-- 最近搜索区域 -->
<view class="recent-section">
<view class="section-title-skeleton skeleton-item"></view>
<view class="recent-list">
<view
v-for="i in 3"
:key="i"
class="recent-item-skeleton skeleton-item"
></view>
<view class="search-skeleton__section">
<view class="search-skeleton__title-row">
<view class="search-skeleton__title skeleton-item"></view>
<view class="search-skeleton__clear skeleton-item"></view>
</view>
<view class="search-skeleton__tags">
<view v-for="i in 5" :key="'r-' + i" class="search-skeleton__tag skeleton-item"></view>
</view>
</view>
<!-- 热门分类区域 -->
<view class="popular-section">
<view class="section-title-skeleton skeleton-item"></view>
<template v-for="item in 6">
<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 class="search-skeleton__section">
<view class="search-skeleton__title skeleton-item search-skeleton__title--wide"></view>
<view class="search-skeleton__tags">
<view v-for="i in 8" :key="'h-' + i" class="search-skeleton__tag skeleton-item"></view>
</view>
</template>
</view>
</view>
</template>
<script setup lang="ts">
// 搜索页面骨架屏组件
</script>
<style scoped lang="scss">
// 通用骨架屏样式
.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%;
animation: shimmer 1.5s infinite;
}
// 闪烁动画
@keyframes shimmer {
0% {
background-position: -200% 0;
@@ -54,75 +43,60 @@
}
.search-skeleton {
background-color: #fff;
min-height: 100vh;
background-color: #f7f8fa;
padding-top: env(safe-area-inset-top, 0px);
}
// 状态栏
.status-bar {
height: 88rpx;
width: 100%;
.search-skeleton__circle {
width: 72rpx;
height: 72rpx;
border-radius: 50%;
flex-shrink: 0;
}
// 头部区域
.header-section {
padding: 0 30rpx 40rpx;
.search-skeleton__pill {
height: 80rpx;
border-radius: 999rpx;
}
.search-skeleton__section {
padding: 28rpx 24rpx 8rpx;
}
.search-skeleton__title-row {
display: flex;
align-items: center;
gap: 20rpx;
.back-button-skeleton {
width: 44rpx;
height: 44rpx;
border-radius: 22rpx;
justify-content: space-between;
margin-bottom: 28rpx;
}
.search-bar-skeleton {
flex: 1;
height: 88rpx;
border-radius: 44rpx;
}
}
// 最近搜索区域
.recent-section {
padding: 0 30rpx 40rpx;
.section-title-skeleton {
width: 200rpx;
.search-skeleton__title {
width: 180rpx;
height: 40rpx;
border-radius: 8rpx;
margin-bottom: 30rpx;
}
.search-skeleton__title--wide {
width: 220rpx;
margin-bottom: 28rpx;
}
.recent-list {
.search-skeleton__clear {
width: 56rpx;
height: 32rpx;
border-radius: 8rpx;
}
.search-skeleton__tags {
display: flex;
flex-direction: column;
gap: 20rpx;
flex-direction: row;
flex-wrap: wrap;
gap: 16rpx 14rpx;
}
.recent-item-skeleton {
.search-skeleton__tag {
width: 132rpx;
height: 60rpx;
border-radius: 8rpx;
}
}
}
// 热门分类区域
.popular-section {
padding: 40rpx 30rpx 0;
.section-title-skeleton {
width: 300rpx;
height: 40rpx;
border-radius: 8rpx;
margin-bottom: 30rpx;
}
}
// 响应式设计
@media (max-width: 750rpx) {
.category-grid {
grid-template-columns: 1fr;
}
border-radius: 999rpx;
}
</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">
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 AnimatedButton from "../animated-button/animated-button.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')" /> -->
<view
class="pl-30rpx pb-36rpx text-36rpx lh-36rpx text-#333 font-500 tracking-[.04em]"
>{{ foodTotal }} {{ t('pages.search.result.result') }}</view
>
<view class="px-30rpx">
<template v-for="item in dataList">
<food-box :item="item" />
</template>
<view class="search-food-toolbar">
<text class="search-food-toolbar__count">{{ foodTotal }} {{ t('pages.search.result.result') }}</text>
</view>
<view class="search-food-list">
<search-dish-result-item v-for="item in dataList" :key="String(item.id)" :item="item" />
</view>
</view>
</template>
@@ -282,6 +279,22 @@ defineExpose({
</template>
<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 0.6s ease-in-out;
+107 -33
View File
@@ -1,8 +1,9 @@
<script setup lang="ts">
import SearchHistory from './components/search-history/index.vue'
import SearchSkeleton from './components/search-skeleton.vue'
import SearchTagCloud from './components/search-tag-cloud.vue'
import { useSearchStore } from '@/store'
import {appSearchListPost,appRecipeCategoryListGet} from '@/service'
import { appRecipeCategoryListGet } from '@/service'
import { decodeRouteQueryValue } from '@/utils/utils'
const { t } = useI18n()
const searchStore = useSearchStore()
@@ -11,45 +12,94 @@ const keyword = ref('')
function handleSearch() {
nextTick(() => {
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({
url: `/pages/search/result?keyword=${keyword.value}`,
url: `/pages/search/result?keyword=${encodeURIComponent(kw)}`,
})
keyword.value = ''
})
}
function handleHotSearch(item: any) {
nextTick(() => {
searchStore.setHistoryList(item.categoryName)
function goResultWithKeyword(k: string) {
const kw = decodeRouteQueryValue(k)
if (!kw) return
searchStore.setHistoryList(kw)
uni.navigateTo({
url: `/pages/search/result?keyword=${item.categoryName}`,
})
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)
onMounted(() => {
loading.value = true
// 查询热门搜索词列表
getHotSearchList()
})
const hotSearchList = ref([])
const hotSearchList = ref<Record<string, unknown>[]>([])
function getHotSearchList() {
appRecipeCategoryListGet({}).then(res=> {
console.log('热门搜索词列表', res)
hotSearchList.value = res.data
}).finally(() => {
appRecipeCategoryListGet({})
.then((res) => {
hotSearchList.value = (res.data as Record<string, unknown>[]) || []
})
.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>
<template>
<view class="">
<view class="search-page">
<view
class="animate-in fade-in animate-ease-out animate-duration-300"
v-show="loading"
@@ -57,23 +107,33 @@ function getHotSearchList() {
<search-skeleton />
</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"
>
<header-search focus class="" v-model="keyword" :placeholder="t('components.search.placeholder')" @search="handleSearch"/>
<search-history/>
<view class="pl-30rpx mt-52rpx">
<view class="text-40rpx lh-40rpx text-#333 font-bold mb-24rpx tracking-[.04em]">
{{ t('pages.search.hot-title') }}
</view>
<template v-for="item in hotSearchList">
<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="flex-1 border-b-solid border-b-1rpx border-b-#DFDFDF h-full flex items-center text-30rpx text-primary font-500 tracking-[.06em]">
{{ item?.categoryName }}
</view>
</view>
</template>
<header-search
v-model="keyword"
focus
trailing-cart
class="search-page__header"
:placeholder="t('components.search.placeholder')"
@search="handleSearch"
/>
<view class="search-page__body">
<search-tag-cloud
v-if="historyTags.length > 0"
:title="t('pages.search.recently')"
:tags="historyTags"
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>
@@ -81,6 +141,20 @@ function getHotSearchList() {
<style lang="scss">
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>
+32 -5
View File
@@ -1,12 +1,13 @@
<script setup lang="ts">
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 Score from "@/components/filtrate-tool/components/score.vue";
import PriceChoose from "@/components/filtrate-tool/components/price-choose.vue";
import SortPopup from "@/pages/search/components/sort-popup.vue";
const props = defineProps<{
const _props = defineProps<{
/** 少数构建下可能由路由注入;实际以 onLoad 的 query 为准 */
keyword?: string;
}>();
@@ -14,7 +15,15 @@ const { t } = useI18n();
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() {
nextTick(() => {
@@ -99,12 +108,15 @@ onMounted(()=> {
</script>
<template>
<view class="">
<view class="search-result-page">
<z-paging-swiper>
<template #top>
<header-search
class="pb-42rpx"
v-model="keyword"
trailing-cart
trailing-surface="white"
class="search-result-page__header"
:placeholder="t('components.search.placeholder')"
@search="handleSearch"
/>
<!-- <tab-switcher
@@ -147,7 +159,22 @@ onMounted(()=> {
</view>
</template>
<style lang="scss">
page {
background-color: #fff;
}
</style>
<style scoped lang="scss">
.search-result-page {
min-height: 100vh;
background: #fff;
}
.search-result-page__header {
padding-bottom: 8rpx;
}
:deep(.wd-tabs__nav-item) {
font-size: 40rpx;
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 - 批量订单支付参数
*/
+3
View File
@@ -2898,6 +2898,8 @@
'merchantVo'?: MerchantVo;
/** 份数 */
'copies'?: number;
'actualSalePrice'?: number;
}
@@ -3031,6 +3033,7 @@
'dishList'?: MerchantDishVo[];
/** 商家视图对象 t_merchant */
'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/search'
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
}
function clearHistory() {
historyList.value = []
}
return {
historyList,
setHistoryList,
clearHistory,
}
},
{
+48 -4
View File
@@ -5,6 +5,38 @@ import {dayjs} from "@/plugin";
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) {
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 -
* @returns {string}
*/
export function formatTimestamp(timestamp: number): string {
return dayjs(Number(timestamp))
.format('MMM.DD YYYY') // 核心格式化指令
.replace(/\b([a-z])/, match => match.toUpperCase()); // 确保月份首字母大写
const locale = uni.getLocale()
const date = dayjs(Number(timestamp))
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())
}
/**