修改效果

This commit is contained in:
2026-06-12 15:06:59 +08:00
parent 8ed9bf2e1a
commit eeffab9a06
13 changed files with 705 additions and 45 deletions
+8 -1
View File
@@ -241,7 +241,8 @@
"mustEatList": "CHEFLINK Must-Eat",
"newCalendar": "New Arrival Calendar",
"newCalendarNav": "Today's New",
"freshSeafoodToday": "Fresh Seafood Today"
"freshSeafoodToday": "Fresh Seafood Today",
"energyMeal": "Energy Meals"
},
"mustEatListTabs": {
"merchant": "Merchant Rank",
@@ -589,6 +590,12 @@
"voucherUploadFailed": "Upload failed, please try again",
"pleaseBindCreditCard": "No bank card linked. Please add a card before paying."
},
"energyMeal": {
"title": "Energy Meals",
"addAllToCart": "Add all to cart",
"empty": "No energy meals yet",
"unavailable": "This bundle is unavailable"
},
"store": {
"addToCart": "Add to cart",
"appetizers": "Appetizers",
+8 -1
View File
@@ -241,7 +241,8 @@
"mustEatList": "CHEFLINK必吃榜",
"newCalendar": "上新日历",
"newCalendarNav": "今日上新",
"freshSeafoodToday": "今日现打海鲜"
"freshSeafoodToday": "今日现打海鲜",
"energyMeal": "能量餐"
},
"mustEatListTabs": {
"merchant": "商家榜单",
@@ -589,6 +590,12 @@
"voucherUploadFailed": "上传失败,请重试",
"pleaseBindCreditCard": "未绑定银行卡,请先绑定后再支付"
},
"energyMeal": {
"title": "能量餐",
"addAllToCart": "一键加入购物车",
"empty": "暂无能量餐",
"unavailable": "套餐暂不可购买"
},
"store": {
"addToCart": "加入购物车",
"appetizers": "开胃菜",
+2 -2
View File
@@ -2,8 +2,8 @@
"name" : "CHEFLINK delivery",
"appid" : "__UNI__06509BE",
"description" : "",
"versionName" : "3.2.4",
"versionCode" : 324,
"versionName" : "3.2.5",
"versionCode" : 325,
"transformPx" : false,
/* 5+App */
"app-plus" : {
@@ -13,8 +13,10 @@ export type FeaturedDishQueryBody = {
/** 运营入口固定参数(精选菜品列表 operationalEntry */
export const OPERATIONAL_ENTRY = {
FEATURED: 'featured',
FRESH_SEAFOOD_TODAY: 'fresh-seafood-today',
LIMITED_AIR_SEAFOOD: 'limited-air-seafood',
NEW_DISH_CALENDAR: 'new-dish-calendar',
} as const
export type RouteQueryMap = Record<string, string | undefined>
@@ -63,41 +65,10 @@ export function buildMustEatListQuery(routeQuery?: RouteQueryMap): FeaturedDishQ
return mergeRouteQueryIntoBody({ salesSort: 'desc' }, routeQuery)
}
/** 当天 00:00:00 ~ 23:59:59(本地时区,10 位秒级时间戳 */
export function getTodayCreateTimeRange(): { createBeginTime: number; createEndTime: number } {
const now = new Date()
const createBeginTime = Math.floor(
new Date(
now.getFullYear(),
now.getMonth(),
now.getDate(),
0,
0,
0,
0,
).getTime() / 1000,
)
const createEndTime = Math.floor(
new Date(
now.getFullYear(),
now.getMonth(),
now.getDate(),
23,
59,
59,
0,
).getTime() / 1000,
)
return { createBeginTime, createEndTime }
}
/** 上新日历:当天创建 + 新品(isNew = 1 */
/** 今日上新/上新日历:运营入口 new-dish-calendar(全量新品,按上架时间倒序 */
export function buildNewCalendarQuery(routeQuery?: RouteQueryMap): FeaturedDishQueryBody {
return mergeRouteQueryIntoBody(
{
...getTodayCreateTimeRange(),
isNew: 1,
},
{ operationalEntry: OPERATIONAL_ENTRY.NEW_DISH_CALENDAR },
routeQuery,
)
}
@@ -32,6 +32,7 @@ export function resolveQuickTopicFromMerchantCategory(
/** 快捷入口 topic 与 operationalEntry 固定映射 */
export const TOPIC_OPERATIONAL_ENTRY: Partial<Record<QuickTopicSlug, string>> = {
'live-seafood-air': 'limited-air-seafood',
'new-calendar': 'new-dish-calendar',
'fresh-seafood-today': 'fresh-seafood-today',
}
@@ -0,0 +1,139 @@
<template>
<view class="energy-meal-skeleton">
<view v-for="i in 2" :key="i" class="energy-meal-skeleton__card">
<view class="energy-meal-skeleton__head">
<view class="energy-meal-skeleton__title skeleton-item"></view>
<view class="energy-meal-skeleton__merchant skeleton-item"></view>
</view>
<view
v-for="j in 2"
:key="j"
class="energy-meal-skeleton__row"
:class="{ 'energy-meal-skeleton__row--last': j === 2 }"
>
<view class="energy-meal-skeleton__img skeleton-item"></view>
<view class="energy-meal-skeleton__main">
<view class="energy-meal-skeleton__name skeleton-item"></view>
<view class="energy-meal-skeleton__price skeleton-item"></view>
<view class="energy-meal-skeleton__member skeleton-item"></view>
<view class="energy-meal-skeleton__sales skeleton-item"></view>
</view>
</view>
<view class="energy-meal-skeleton__action">
<view class="energy-meal-skeleton__btn skeleton-item"></view>
</view>
</view>
</view>
</template>
<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;
}
}
.energy-meal-skeleton {
padding: 24rpx;
}
.energy-meal-skeleton__card {
margin-bottom: 24rpx;
border-radius: 20rpx;
background: #fff;
overflow: hidden;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
}
.energy-meal-skeleton__head {
padding: 24rpx 24rpx 8rpx;
}
.energy-meal-skeleton__title {
width: 280rpx;
height: 36rpx;
border-radius: 8rpx;
}
.energy-meal-skeleton__merchant {
width: 180rpx;
height: 28rpx;
border-radius: 8rpx;
margin-top: 12rpx;
}
.energy-meal-skeleton__row {
display: flex;
padding: 28rpx 24rpx;
border-bottom: 1rpx solid #ebebeb;
}
.energy-meal-skeleton__row--last {
border-bottom: none;
}
.energy-meal-skeleton__img {
width: 200rpx;
height: 200rpx;
border-radius: 20rpx;
flex-shrink: 0;
}
.energy-meal-skeleton__main {
flex: 1;
min-width: 0;
margin-left: 24rpx;
display: flex;
flex-direction: column;
}
.energy-meal-skeleton__name {
width: 100%;
height: 72rpx;
border-radius: 8rpx;
}
.energy-meal-skeleton__price {
width: 160rpx;
height: 32rpx;
border-radius: 8rpx;
margin-top: 12rpx;
}
.energy-meal-skeleton__member {
width: 320rpx;
height: 28rpx;
border-radius: 8rpx;
margin-top: 12rpx;
}
.energy-meal-skeleton__sales {
width: 180rpx;
height: 44rpx;
border-radius: 999rpx;
margin-top: auto;
}
.energy-meal-skeleton__action {
display: flex;
justify-content: flex-end;
padding: 8rpx 24rpx 24rpx;
}
.energy-meal-skeleton__btn {
width: 280rpx;
height: 72rpx;
border-radius: 16rpx;
}
</style>
+436
View File
@@ -0,0 +1,436 @@
<script setup lang="ts">
import Config from '@/config/index'
import { useUserStore } from '@/store'
import EnergyMealSkeleton from './components/energy-meal-skeleton.vue'
import {
appEnergyMealAddCartPost,
appEnergyMealListPost,
type EnergyMealItemVo,
type EnergyMealVo,
} from '@/service'
import { formatSalesCount, thumbnailImg } from '@/utils/utils'
const { t } = useI18n()
const userStore = useUserStore()
const filterMerchantId = ref<string>('')
const mealList = ref<EnergyMealVo[]>([])
const paging = ref<ZPagingInstance | null>(null)
const addingMealId = ref<string | number | null>(null)
const loading = ref(true)
onLoad((options: Record<string, string | undefined>) => {
if (options?.merchantId) {
filterMerchantId.value = String(options.merchantId)
}
})
function firstImage(raw?: string) {
if (typeof raw !== 'string' || !raw.trim()) return ''
return raw.split(',')[0].trim()
}
function sortedItems(meal: EnergyMealVo): EnergyMealItemVo[] {
const list = Array.isArray(meal.itemList) ? [...meal.itemList] : []
return list.sort((a, b) => Number(a.sort ?? 0) - Number(b.sort ?? 0))
}
function canAddMeal(meal: EnergyMealVo) {
return sortedItems(meal).some((item) => item.merchantDishVo?.id)
}
function dishCover(item: EnergyMealItemVo) {
return firstImage(item.merchantDishVo?.dishImage)
}
function dishName(item: EnergyMealItemVo) {
const name = String(item.merchantDishVo?.dishName ?? '').trim()
const qty = Number(item.quantity) || 1
if (!name) return '--'
if (qty > 1) {
return `${name} × ${qty}`
}
return name
}
function dishPrice(item: EnergyMealItemVo) {
const dish = item.merchantDishVo ?? {}
const discount = Number(dish.discountPrice)
const original = Number(dish.originalPrice)
if (Number.isFinite(discount) && discount > 0) return discount.toFixed(2)
if (Number.isFinite(original) && original > 0) return original.toFixed(2)
return '0.00'
}
function dishOriginalPrice(item: EnergyMealItemVo) {
const dish = item.merchantDishVo ?? {}
const discount = Number(dish.discountPrice)
const original = Number(dish.originalPrice)
if (
Number.isFinite(original) &&
original > 0 &&
Number.isFinite(discount) &&
original > discount
) {
return original.toFixed(2)
}
return ''
}
function dishMemberPrice(item: EnergyMealItemVo) {
const member = Number(item.merchantDishVo?.memberPrice)
return Number.isFinite(member) && member > 0 ? member.toFixed(2) : ''
}
function openDishDetail(item: EnergyMealItemVo, meal: EnergyMealVo) {
const dishId = item.merchantDishVo?.id ?? item.dishId
const merchantId = meal.merchantId ?? item.merchantDishVo?.merchantId
if (!dishId || !merchantId) return
uni.navigateTo({
url: `/pages-store/pages/store/dishes?id=${dishId}&storeId=${merchantId}`,
})
}
async function onQuery(pageNum: number, pageSize: number) {
if (pageNum === 1) {
loading.value = true
}
try {
const body: Record<string, string> = {}
if (filterMerchantId.value) {
body.merchantId = filterMerchantId.value
}
const res = await appEnergyMealListPost({
params: { pageNum, pageSize },
body,
})
const rows = Array.isArray(res.rows) ? res.rows : []
const total = Number(res.total ?? rows.length)
await paging.value?.completeByTotal(rows, total)
} catch {
await paging.value?.complete(false)
} finally {
if (pageNum === 1) {
loading.value = false
}
}
}
async function handleAddAllToCart(meal: EnergyMealVo) {
if (!userStore.checkLogin()) return
if (!meal.id) return
if (!canAddMeal(meal)) {
uni.showToast({
title: t('pages-store.energyMeal.unavailable'),
icon: 'none',
})
return
}
if (addingMealId.value != null) return
addingMealId.value = meal.id
try {
await appEnergyMealAddCartPost({
body: {
energyMealId: meal.id,
count: 1,
},
options: { hideErrorToast: true },
})
uni.showToast({
title: t('toast.addCartSuccess'),
icon: 'none',
})
userStore.getUserCartAllData()
} catch (err: any) {
uni.showToast({
title: err?.msg || err?.message || t('common.prompt.request-failed-please-try-again-later'),
icon: 'none',
})
} finally {
if (addingMealId.value === meal.id) {
addingMealId.value = null
}
}
}
</script>
<template>
<z-paging
ref="paging"
v-model="mealList"
bg-color="#f2f2f2"
:auto="true"
:hide-empty-view="loading"
@query="onQuery"
>
<template #top>
<navbar :title="t('pages-store.energyMeal.title')" circle-back />
</template>
<view
v-show="loading"
class="animate-in fade-in animate-ease-out animate-duration-300"
>
<energy-meal-skeleton />
</view>
<view
v-show="!loading"
class="animate-in fade-in animate-ease-in animate-duration-300 energy-meal-page"
>
<view
v-for="meal in mealList"
:key="meal.id"
class="energy-meal-card"
>
<view v-if="meal.mealName" class="energy-meal-card__head">
<text class="energy-meal-card__title line-clamp-1">{{ meal.mealName }}</text>
<text
v-if="meal.merchant?.merchantName"
class="energy-meal-card__merchant line-clamp-1"
>
{{ meal.merchant.merchantName }}
</text>
</view>
<view
v-for="(item, index) in sortedItems(meal)"
:key="item.id || `${meal.id}-${index}`"
class="energy-dish-row"
:class="{ 'energy-dish-row--last': index === sortedItems(meal).length - 1 }"
@click="openDishDetail(item, meal)"
>
<view class="energy-dish-row__img-wrap">
<image
:src="thumbnailImg(dishCover(item))"
mode="aspectFill"
class="energy-dish-row__img"
/>
</view>
<view class="energy-dish-row__main">
<text class="energy-dish-row__name line-clamp-2">{{ dishName(item) }}</text>
<view class="energy-dish-row__price-row">
<text class="energy-dish-row__price-current">${{ dishPrice(item) }}</text>
<text
v-if="dishOriginalPrice(item)"
class="energy-dish-row__price-old"
>
${{ dishOriginalPrice(item) }}
</text>
</view>
<view
v-if="dishMemberPrice(item)"
class="energy-dish-row__member"
>
<text class="energy-dish-row__member-diamond"></text>
<text class="energy-dish-row__member-text">
{{ Config.appName }} {{ t('pages.search.member-price-line') }}: ${{ dishMemberPrice(item) }}
</text>
</view>
<view class="energy-dish-row__sales-wrap">
<text class="energy-dish-row__sales-tag">
{{ t('pages.search.weekly-sales') }}{{ formatSalesCount(item.merchantDishVo?.salesCount) }}
</text>
</view>
</view>
</view>
<view
v-if="canAddMeal(meal)"
class="energy-meal-card__action"
>
<view
class="energy-meal-card__btn"
:class="{ 'energy-meal-card__btn--loading': addingMealId === meal.id }"
@click.stop="handleAddAllToCart(meal)"
>
{{ t('pages-store.energyMeal.addAllToCart') }}
</view>
</view>
</view>
</view>
<template #empty>
<view v-if="!loading" class="energy-meal-empty">
<image class="energy-meal-empty__img" src="@img/chef/100.png" mode="aspectFit" />
<text class="energy-meal-empty__text">{{ t('pages-store.energyMeal.empty') }}</text>
</view>
</template>
</z-paging>
</template>
<style scoped lang="scss">
.energy-meal-page {
padding: 24rpx;
}
.energy-meal-card {
margin-bottom: 24rpx;
border-radius: 20rpx;
background: #fff;
overflow: hidden;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
}
.energy-meal-card__head {
padding: 24rpx 24rpx 8rpx;
}
.energy-meal-card__title {
display: block;
font-size: 30rpx;
line-height: 42rpx;
font-weight: 600;
color: #1a1a1a;
}
.energy-meal-card__merchant {
display: block;
margin-top: 6rpx;
font-size: 24rpx;
line-height: 34rpx;
color: #888;
}
.energy-dish-row {
display: flex;
align-items: stretch;
padding: 28rpx 24rpx;
border-bottom: 1rpx solid #ebebeb;
}
.energy-dish-row--last {
border-bottom: none;
}
.energy-dish-row__img-wrap {
width: 200rpx;
height: 200rpx;
border-radius: 20rpx;
overflow: hidden;
flex-shrink: 0;
background: #f2f2f2;
}
.energy-dish-row__img {
width: 100%;
height: 100%;
display: block;
}
.energy-dish-row__main {
flex: 1;
min-width: 0;
margin-left: 24rpx;
display: flex;
flex-direction: column;
}
.energy-dish-row__name {
font-size: 28rpx;
line-height: 40rpx;
font-weight: 500;
color: #1a1a1a;
}
.energy-dish-row__price-row {
display: flex;
align-items: baseline;
flex-wrap: wrap;
gap: 12rpx;
margin-top: 12rpx;
}
.energy-dish-row__price-current {
font-size: 32rpx;
line-height: 36rpx;
font-weight: 700;
color: #e02e24;
}
.energy-dish-row__price-old {
font-size: 24rpx;
line-height: 28rpx;
color: #999;
text-decoration: line-through;
}
.energy-dish-row__member {
display: flex;
align-items: center;
gap: 6rpx;
margin-top: 12rpx;
}
.energy-dish-row__member-diamond {
font-size: 20rpx;
line-height: 1;
color: #b8860b;
}
.energy-dish-row__member-text {
font-size: 24rpx;
line-height: 32rpx;
font-weight: 500;
color: #8b6914;
}
.energy-dish-row__sales-wrap {
margin-top: auto;
padding-top: 12rpx;
}
.energy-dish-row__sales-tag {
display: inline-block;
padding: 8rpx 18rpx;
border-radius: 999rpx;
background: #f5f5f5;
font-size: 22rpx;
line-height: 28rpx;
color: #888;
}
.energy-meal-card__action {
display: flex;
justify-content: flex-end;
padding: 8rpx 24rpx 24rpx;
}
.energy-meal-card__btn {
min-width: 280rpx;
height: 72rpx;
padding: 0 32rpx;
border-radius: 16rpx;
background: #111;
color: #fff;
font-size: 28rpx;
line-height: 72rpx;
font-weight: 500;
text-align: center;
}
.energy-meal-card__btn--loading {
opacity: 0.65;
}
.energy-meal-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 48rpx;
}
.energy-meal-empty__img {
width: 250rpx;
height: 250rpx;
}
.energy-meal-empty__text {
margin-top: 24rpx;
font-size: 28rpx;
line-height: 40rpx;
color: #8a8a8a;
}
</style>
+6
View File
@@ -284,6 +284,12 @@
},
{
"path": "pages/home-store/index"
},
{
"path": "pages/energy-meal/index",
"style": {
"onReachBottomDistance": 80
}
}
]
}
@@ -3,7 +3,7 @@ import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
const props = defineProps({
/** 外部列表(菜谱页等);首页不传则使用固定项 */
/** 外部列表(菜谱页等);首页不传则使用固定项 */
list: {
type: Array,
default: () => [],
@@ -55,6 +55,11 @@ const fixedTabs = [
nameKey: 'pages.home.quickTabs.freshSeafoodToday',
logoUrl: '/static/app/images/home/xiandahaixian.png',
},
{
id: 'energy-meal',
nameKey: 'pages.home.quickTabs.energyMeal',
logoUrl: '/static/app/images/home/nengliangcan.png',
},
]
const useFixedTabs = computed(() => !props.list || props.list.length === 0)
@@ -63,10 +68,6 @@ function selectTab(item: Record<string, unknown>) {
emit('changeType', item[props.valueKey])
}
function imageWidthByIndex(index: number) {
return (index + 1) % 2 === 1 ? '104rpx' : '210rpx'
}
function getTabName(item: Record<string, unknown>) {
if (useFixedTabs.value && item.nameKey) {
return t(String(item.nameKey))
@@ -97,8 +98,7 @@ const displayList = computed(() =>
<image
class="tab-img"
:src="getImage(item as Record<string, unknown>)"
mode="scaleToFill"
:style="{ width: imageWidthByIndex(index) }"
mode="heightFix"
/>
<text class="tab-label line-clamp-1">{{ getTabName(item as Record<string, unknown>) }}</text>
</view>
@@ -122,7 +122,6 @@ const displayList = computed(() =>
.tab-img {
height: 144rpx;
width: auto;
display: block;
margin-bottom: 10rpx;
}
@@ -236,6 +236,10 @@ function handleItemClick(e) {
}
function tabsTypeChange(id: string | number) {
const topic = String(id)
if (topic === 'energy-meal') {
navigateTo('/pages-store/pages/energy-meal/index')
return
}
if (!isQuickTopicSlug(topic)) {
return
}
@@ -310,6 +314,9 @@ function handleClickSwiper(item: any) {
case 3: // 会员
navigateTo('/pages-user/pages/member/index')
break
case 5: // 钱包
navigateTo('/pages-user/pages/balance/index')
break
// case 4:
// navigateTo('/pages/ai/chat/index')
// break
+86
View File
@@ -0,0 +1,86 @@
/* eslint-disable */
// @ts-ignore
import request from '@/http/vue-query';
import type { CustomRequestOptions } from '@/http/types';
export interface EnergyMealListQueryBo {
merchantId?: number | string;
mealName?: string;
merchantName?: string;
}
export interface EnergyMealItemVo {
id?: number | string;
energyMealId?: number | string;
dishId?: number | string;
quantity?: number;
sort?: number;
delFlag?: number;
merchantDishVo?: Record<string, any>;
}
export interface EnergyMealVo {
id?: number | string;
merchantId?: number | string;
mealName?: string;
coverImage?: string;
sort?: number;
delFlag?: number;
remark?: string;
merchant?: {
id?: number | string;
merchantName?: string;
logo?: string;
merchantAddress?: string;
};
itemList?: EnergyMealItemVo[];
}
export interface EnergyMealAddCartBo {
energyMealId: number | string;
count?: number;
}
/** 能量餐分页列表 POST /app/energyMeal/list */
export async function appEnergyMealListPost({
params,
body,
options,
}: {
params: { pageNum: number; pageSize: number };
body?: EnergyMealListQueryBo;
options?: CustomRequestOptions;
}) {
return request<{ rows?: EnergyMealVo[]; total?: number }>(
'/app/energyMeal/list',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
params: {
...params,
},
data: body ?? {},
...(options || {}),
}
);
}
/** 能量餐一键加入购物车 POST /app/energyMeal/addCart */
export async function appEnergyMealAddCartPost({
body,
options,
}: {
body: EnergyMealAddCartBo;
options?: CustomRequestOptions;
}) {
return request<{ data?: number[] }>('/app/energyMeal/addCart', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: body,
...(options || {}),
});
}
+1
View File
@@ -44,3 +44,4 @@ export * from './automaticCookingMachine';
export * from './userCoupon';
export * from './marketingActivity';
export * from './marketActivity';
export * from './energyMeal';
Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB