first commit
This commit is contained in:
@@ -0,0 +1,329 @@
|
||||
<template>
|
||||
<view class="class-bullet-container">
|
||||
<!-- 第一行(可拖动 + 自动滚动) -->
|
||||
<view class="scroll-row first-row">
|
||||
<scroll-view
|
||||
class="ab-scroll mb-22rpx"
|
||||
scroll-x
|
||||
show-scrollbar="false"
|
||||
id="sv-1"
|
||||
style="--ab-scroll-timeout: 50s"
|
||||
:scroll-left="scrollLeft1"
|
||||
@touchstart="onTouchStart1"
|
||||
@touchend="onTouchEnd1"
|
||||
@touchcancel="onTouchEnd1"
|
||||
@scroll="onScroll1"
|
||||
>
|
||||
<view id="sv-inner-1" :class="['sv-inner', { 'paused': isDragging1 }]">
|
||||
<!-- 一份内容 -->
|
||||
<view
|
||||
v-for="(item, idx) in categories"
|
||||
:key="item.id + '-a-' + idx"
|
||||
class="category-item"
|
||||
@click="handleItemClick(item)"
|
||||
>
|
||||
<image :src="item.logoUrl" class="category-icon" mode="aspectFit" />
|
||||
<text class="category-text">{{ item.name }}</text>
|
||||
</view>
|
||||
<!-- 第二份重复内容(用于无缝循环) -->
|
||||
<view
|
||||
v-for="(item, idx) in categories"
|
||||
:key="item.id + '-b-' + idx"
|
||||
class="category-item"
|
||||
@click="handleItemClick(item)"
|
||||
>
|
||||
<image :src="item.logoUrl" class="category-icon" mode="aspectFit" />
|
||||
<text class="category-text">{{ item.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 第二行(可拖动 + 自动滚动,速度不同) -->
|
||||
<view class="scroll-row">
|
||||
<scroll-view
|
||||
class="ab-scroll"
|
||||
scroll-x
|
||||
show-scrollbar="false"
|
||||
id="sv-2"
|
||||
style="--ab-scroll-timeout: 70s"
|
||||
:scroll-left="scrollLeft2"
|
||||
@touchstart="onTouchStart2"
|
||||
@touchend="onTouchEnd2"
|
||||
@touchcancel="onTouchEnd2"
|
||||
@scroll="onScroll2"
|
||||
>
|
||||
<view id="sv-inner-2" :class="['sv-inner', { 'paused': isDragging2 }]">
|
||||
<view
|
||||
v-for="(item, idx) in categoriesReversed"
|
||||
:key="item.id + '-c-' + idx"
|
||||
class="category-item"
|
||||
@click="handleItemClick(item)"
|
||||
>
|
||||
<image :src="item.logoUrl" class="category-icon" mode="aspectFit" />
|
||||
<text class="category-text">{{ item.name }}</text>
|
||||
</view>
|
||||
<view
|
||||
v-for="(item, idx) in categoriesReversed"
|
||||
:key="item.id + '-d-' + idx"
|
||||
class="category-item"
|
||||
@click="handleItemClick(item)"
|
||||
>
|
||||
<image :src="item.logoUrl" class="category-icon" mode="aspectFit" />
|
||||
<text class="category-text">{{ item.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, onMounted, nextTick, getCurrentInstance } from 'vue'
|
||||
|
||||
// 定义分类项接口(与模板字段一致)
|
||||
interface CategoryItem {
|
||||
id: number
|
||||
logoUrl: string
|
||||
name: string
|
||||
}
|
||||
|
||||
// 定义组件属性
|
||||
interface Props {
|
||||
/** 分类数据数组 */
|
||||
categories?: CategoryItem[]
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
categories: () => []
|
||||
})
|
||||
|
||||
// 定义事件
|
||||
const emit = defineEmits<{
|
||||
/** 点击分类项事件 */
|
||||
'itemClick': [item: CategoryItem]
|
||||
}>()
|
||||
|
||||
// 模拟数据
|
||||
const mockCategories: CategoryItem[] = [
|
||||
{ id: 1, logoUrl: '/src/static/images/ls/6.png', name: '外卖点餐' },
|
||||
{ id: 2, logoUrl: '/src/static/images/ls/7.png', name: '净菜商城' },
|
||||
{ id: 3, logoUrl: '/src/static/images/ls/8.png', name: '生鲜冷链' },
|
||||
{ id: 4, logoUrl: '/src/static/images/ls/9.png', name: '肉类' },
|
||||
{ id: 5, logoUrl: '/src/static/images/ls/10.png', name: '调料' },
|
||||
{ id: 6, logoUrl: '/src/static/images/ls/11.png', name: '零食' },
|
||||
{ id: 7, logoUrl: '/src/static/images/ls/12.png', name: '海鲜' }
|
||||
]
|
||||
|
||||
// 使用传入的数据或默认数据
|
||||
const categories = computed(() => {
|
||||
return props.categories.length > 0 ? props.categories : mockCategories
|
||||
})
|
||||
// 第二行使用反向顺序以形成与第一行相反的视觉效果
|
||||
const categoriesReversed = computed(() => {
|
||||
const list = categories.value || []
|
||||
return list.slice().reverse()
|
||||
})
|
||||
|
||||
// 自动滚动(CSS 动画)+ 手动拖动时暂停
|
||||
const isDragging1 = ref(false)
|
||||
const isDragging2 = ref(false)
|
||||
const RESUME_DELAY_MS = 1200
|
||||
let resumeTimer1: any
|
||||
let resumeTimer2: any
|
||||
|
||||
// 中心定位与边界重置,避免滚动到底卡住
|
||||
const scrollLeft1 = ref(0)
|
||||
const scrollLeft2 = ref(0)
|
||||
const copyWidth1Px = ref(0)
|
||||
const copyWidth2Px = ref(0)
|
||||
const viewportWidth1Px = ref(0)
|
||||
const viewportWidth2Px = ref(0)
|
||||
|
||||
function measureAndInit() {
|
||||
const ins = getCurrentInstance()
|
||||
const q = uni.createSelectorQuery().in(ins?.proxy as any)
|
||||
q.select('#sv-1').boundingClientRect()
|
||||
.select('#sv-inner-1').boundingClientRect()
|
||||
.select('#sv-2').boundingClientRect()
|
||||
.select('#sv-inner-2').boundingClientRect()
|
||||
.exec((res) => {
|
||||
const sv1 = res?.[0]
|
||||
const inner1 = res?.[1]
|
||||
const sv2 = res?.[2]
|
||||
const inner2 = res?.[3]
|
||||
if (sv1?.width) viewportWidth1Px.value = sv1.width
|
||||
if (inner1?.width) copyWidth1Px.value = inner1.width / 2
|
||||
if (sv2?.width) viewportWidth2Px.value = sv2.width
|
||||
if (inner2?.width) copyWidth2Px.value = inner2.width / 2
|
||||
// 初始化到中间位置
|
||||
if (copyWidth1Px.value) scrollLeft1.value = copyWidth1Px.value
|
||||
if (copyWidth2Px.value) scrollLeft2.value = copyWidth2Px.value
|
||||
})
|
||||
}
|
||||
|
||||
function recenter1(currentLeft?: number) {
|
||||
const cw = copyWidth1Px.value
|
||||
const vw = viewportWidth1Px.value
|
||||
if (!cw || !vw) return
|
||||
const left = currentLeft ?? scrollLeft1.value
|
||||
const mod = left % cw
|
||||
scrollLeft1.value = cw + mod
|
||||
}
|
||||
|
||||
function recenter2(currentLeft?: number) {
|
||||
const cw = copyWidth2Px.value
|
||||
const vw = viewportWidth2Px.value
|
||||
if (!cw || !vw) return
|
||||
const left = currentLeft ?? scrollLeft2.value
|
||||
const mod = left % cw
|
||||
scrollLeft2.value = cw + mod
|
||||
}
|
||||
|
||||
function onTouchStart1() {
|
||||
isDragging1.value = true
|
||||
if (resumeTimer1) clearTimeout(resumeTimer1)
|
||||
}
|
||||
function onTouchEnd1() {
|
||||
// 手指松开后,若在边界附近,立即重置到中间位置,保持无缝
|
||||
recenter1(scrollLeft1.value)
|
||||
resumeTimer1 = setTimeout(() => {
|
||||
isDragging1.value = false
|
||||
}, RESUME_DELAY_MS)
|
||||
}
|
||||
function onScroll1(e: any) {
|
||||
isDragging1.value = true
|
||||
if (resumeTimer1) clearTimeout(resumeTimer1)
|
||||
const left = e?.detail?.scrollLeft ?? 0
|
||||
const cw = copyWidth1Px.value
|
||||
const vw = viewportWidth1Px.value
|
||||
if (cw && vw) {
|
||||
const total = cw * 2
|
||||
// 临界阈值,靠近起点或终点时重置
|
||||
const threshold = 10
|
||||
if (left <= threshold || left >= total - vw - threshold) {
|
||||
recenter1(left)
|
||||
}
|
||||
}
|
||||
resumeTimer1 = setTimeout(() => {
|
||||
isDragging1.value = false
|
||||
}, RESUME_DELAY_MS)
|
||||
}
|
||||
function onTouchStart2() {
|
||||
isDragging2.value = true
|
||||
if (resumeTimer2) clearTimeout(resumeTimer2)
|
||||
}
|
||||
function onTouchEnd2() {
|
||||
recenter2(scrollLeft2.value)
|
||||
resumeTimer2 = setTimeout(() => {
|
||||
isDragging2.value = false
|
||||
}, RESUME_DELAY_MS)
|
||||
}
|
||||
function onScroll2(e: any) {
|
||||
isDragging2.value = true
|
||||
if (resumeTimer2) clearTimeout(resumeTimer2)
|
||||
const left = e?.detail?.scrollLeft ?? 0
|
||||
const cw = copyWidth2Px.value
|
||||
const vw = viewportWidth2Px.value
|
||||
if (cw && vw) {
|
||||
const total = cw * 2
|
||||
const threshold = 10
|
||||
if (left <= threshold || left >= total - vw - threshold) {
|
||||
recenter2(left)
|
||||
}
|
||||
}
|
||||
resumeTimer2 = setTimeout(() => {
|
||||
isDragging2.value = false
|
||||
}, RESUME_DELAY_MS)
|
||||
}
|
||||
|
||||
// 点击处理
|
||||
const handleItemClick = (item: CategoryItem) => {
|
||||
emit('itemClick', item)
|
||||
navigateTo('/pages-store/pages/list/index?id=' + item.id)
|
||||
}
|
||||
|
||||
function navigateTo(url: string) {
|
||||
uni.navigateTo({ url })
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
measureAndInit()
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.class-bullet-container {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
.scroll-row {
|
||||
margin-bottom: 20rpx;
|
||||
overflow: hidden;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ab-scroll {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sv-inner {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 20rpx;
|
||||
animation: ab-move-marquee var(--ab-scroll-timeout) linear infinite;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.category-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 120rpx;
|
||||
height: 60rpx;
|
||||
padding: 0 16rpx;
|
||||
border: 1px solid #C8C8C8;
|
||||
border-radius: 30rpx;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:active {
|
||||
transform: translateY(0) scale(0.95);
|
||||
}
|
||||
|
||||
.category-icon {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.category-text {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GPU 动画,无缝循环(两份内容)
|
||||
@keyframes ab-move-marquee {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
.paused {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
import {thumbnailImg} from "@/utils/utils";
|
||||
const props = defineProps<{
|
||||
list: object[];
|
||||
}>();
|
||||
const { t } = useI18n();
|
||||
|
||||
function handleClickFood(item: any) {
|
||||
uni.navigateTo({
|
||||
url: '/pages-store/pages/store/index?id=' + item.id
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<scroll-view scroll-x="true">
|
||||
<view class="flex">
|
||||
<view class="w-30rpx shrink-0"></view>
|
||||
<template v-for="(item, index) in list">
|
||||
<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">${{ item.deliveryFee }} {{ t('pages.home.deliveryFee') }}</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 }} {{ t('common.minutes') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<view class="w-30rpx shrink-0 op-0">1</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,54 @@
|
||||
<script setup lang="ts">
|
||||
import Collection from "@/components/collection/index.vue";
|
||||
import {appCollectCollectPost} from "@/service";
|
||||
import {CollectionType} from "@/constant/enums";
|
||||
const { t } = useI18n();
|
||||
const props = defineProps<{
|
||||
item: object;
|
||||
}>();
|
||||
|
||||
function handleClickFood() {
|
||||
uni.navigateTo({
|
||||
url: '/pages-store/pages/store/index?id=' + props.item.id
|
||||
})
|
||||
}
|
||||
|
||||
function handleCollectionChange(value: boolean) {
|
||||
appCollectCollectPost({
|
||||
body: {
|
||||
targetId: props.item.id,
|
||||
targetType: CollectionType.STORE
|
||||
}
|
||||
}).then(res=> {
|
||||
props.item.isCollect = value;
|
||||
}).catch(err=> {
|
||||
props.item.isCollect = !value;
|
||||
})
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<view @click="handleClickFood" class="mb-52rpx">
|
||||
<image
|
||||
:src="item?.shopImages?.split(',')[0]"
|
||||
mode="aspectFill"
|
||||
class="w-100% h-636rpx rounded-24rpx bg-common"
|
||||
></image>
|
||||
<view class="flex justify-between items-start mt-14rpx">
|
||||
<view>
|
||||
<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">${{ item.deliveryFee }} {{ t('pages.home.deliveryFee') }}</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 }}{{ t('common.minutes') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<collection :is-collected="item.isCollect" @collectionChange="handleCollectionChange" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -0,0 +1,322 @@
|
||||
<template>
|
||||
<view class="home-skeleton">
|
||||
<!-- 头部区域 -->
|
||||
<view class="header-section">
|
||||
<view class="app-title-skeleton skeleton-item"></view>
|
||||
<view class="delivery-info-skeleton skeleton-item"></view>
|
||||
</view>
|
||||
|
||||
<!-- 位置和通知区域 -->
|
||||
<view class="location-notification">
|
||||
<view class="location-skeleton skeleton-item"></view>
|
||||
<view class="notification-skeleton skeleton-item"></view>
|
||||
</view>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<view class="search-section">
|
||||
<view class="search-bar-skeleton skeleton-item"></view>
|
||||
</view>
|
||||
|
||||
<!-- 分类滚动区域 -->
|
||||
<view class="category-section">
|
||||
<view class="category-list">
|
||||
<view
|
||||
v-for="i in 7"
|
||||
:key="i"
|
||||
class="category-item-skeleton skeleton-item"
|
||||
></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 轮播图区域 -->
|
||||
<view class="swiper-section">
|
||||
<view class="swiper-skeleton skeleton-item"></view>
|
||||
</view>
|
||||
|
||||
<!-- 食品类型分类 -->
|
||||
<view class="food-type-section">
|
||||
<view class="food-type-list">
|
||||
<view
|
||||
v-for="i in 5"
|
||||
:key="i"
|
||||
class="food-type-item-skeleton skeleton-item"
|
||||
></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 筛选工具 -->
|
||||
<view class="filter-section">
|
||||
<view class="filter-list">
|
||||
<view
|
||||
v-for="i in 4"
|
||||
:key="i"
|
||||
class="filter-item-skeleton skeleton-item"
|
||||
></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Featured on ChefLink 区域 -->
|
||||
<view class="featured-section">
|
||||
<view class="section-title-skeleton skeleton-item"></view>
|
||||
<view class="featured-cards">
|
||||
<view class="featured-card-skeleton skeleton-item"></view>
|
||||
<view class="featured-card-skeleton skeleton-item"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Nearby Merchants 区域 -->
|
||||
<view class="nearby-section">
|
||||
<view class="section-title-skeleton skeleton-item"></view>
|
||||
<view class="nearby-list">
|
||||
<view
|
||||
v-for="i in 4"
|
||||
:key="i"
|
||||
class="nearby-item-skeleton skeleton-item"
|
||||
></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- All Merchants 区域 -->
|
||||
<view class="all-merchants-section">
|
||||
<view class="section-title-skeleton skeleton-item"></view>
|
||||
<view class="merchant-card-skeleton skeleton-item"></view>
|
||||
</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-size: 200% 100%;
|
||||
animation: shimmer 1.5s infinite;
|
||||
}
|
||||
|
||||
// 闪烁动画
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
.home-skeleton {
|
||||
background-color: #fff;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
// 头部区域
|
||||
.header-section {
|
||||
padding: 18rpx 30rpx 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14rpx;
|
||||
|
||||
.app-title-skeleton {
|
||||
width: 241rpx;
|
||||
height: 52rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.delivery-info-skeleton {
|
||||
width: 266rpx;
|
||||
height: 28rpx;
|
||||
border-radius: 6rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// 位置和通知区域
|
||||
.location-notification {
|
||||
padding: 22rpx 30rpx 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.location-skeleton {
|
||||
width: 329rpx;
|
||||
height: 30rpx;
|
||||
border-radius: 6rpx;
|
||||
}
|
||||
|
||||
.notification-skeleton {
|
||||
width: 132rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 30rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索栏
|
||||
.search-section {
|
||||
padding: 32rpx 30rpx 0;
|
||||
|
||||
.search-bar-skeleton {
|
||||
width: 690rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 44rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// 分类滚动区域
|
||||
.category-section {
|
||||
padding: 40rpx 30rpx 22rpx;
|
||||
|
||||
.category-list {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
overflow-x: auto;
|
||||
|
||||
.category-item-skeleton {
|
||||
flex-shrink: 0;
|
||||
width: 214rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 33rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 轮播图区域
|
||||
.swiper-section {
|
||||
padding: 0 30rpx;
|
||||
|
||||
.swiper-skeleton {
|
||||
width: 690rpx;
|
||||
height: 420rpx;
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// 食品类型分类
|
||||
.food-type-section {
|
||||
padding: 22rpx 30rpx;
|
||||
|
||||
.food-type-list {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
overflow-x: auto;
|
||||
|
||||
.food-type-item-skeleton {
|
||||
flex-shrink: 0;
|
||||
width: 112rpx;
|
||||
height: 112rpx;
|
||||
border-radius: 56rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 筛选工具
|
||||
.filter-section {
|
||||
padding: 32rpx 30rpx;
|
||||
|
||||
.filter-list {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
|
||||
.filter-item-skeleton {
|
||||
flex: 1;
|
||||
height: 64rpx;
|
||||
border-radius: 32rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Featured on ChefLink 区域
|
||||
.featured-section {
|
||||
padding: 56rpx 30rpx 0;
|
||||
|
||||
.section-title-skeleton {
|
||||
width: 300rpx;
|
||||
height: 36rpx;
|
||||
border-radius: 8rpx;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.featured-cards {
|
||||
display: flex;
|
||||
gap: 28rpx;
|
||||
|
||||
.featured-card-skeleton {
|
||||
flex: 1;
|
||||
height: 252rpx;
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Nearby Merchants 区域
|
||||
.nearby-section {
|
||||
padding: 56rpx 30rpx 0;
|
||||
|
||||
.section-title-skeleton {
|
||||
width: 303rpx;
|
||||
height: 36rpx;
|
||||
border-radius: 8rpx;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.nearby-list {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
overflow-x: auto;
|
||||
|
||||
.nearby-item-skeleton {
|
||||
flex-shrink: 0;
|
||||
width: 156rpx;
|
||||
height: 156rpx;
|
||||
border-radius: 78rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// All Merchants 区域
|
||||
.all-merchants-section {
|
||||
padding: 56rpx 30rpx 62rpx;
|
||||
|
||||
.section-title-skeleton {
|
||||
width: 224rpx;
|
||||
height: 36rpx;
|
||||
border-radius: 8rpx;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.merchant-card-skeleton {
|
||||
width: 690rpx;
|
||||
height: 766rpx;
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// 底部导航栏
|
||||
.bottom-nav {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 108rpx;
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
padding: 20rpx 0;
|
||||
box-shadow: 0 -6rpx 16rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 750rpx) {
|
||||
.category-list,
|
||||
.food-type-list,
|
||||
.nearby-list {
|
||||
padding-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.featured-cards {
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import { useUserStore } from "@/store";
|
||||
const emit = defineEmits(['toggleNotOpen']);
|
||||
const userStore = useUserStore();
|
||||
function navigateTo(url: string) {
|
||||
if(userStore.checkLogin()) {
|
||||
uni.navigateTo({
|
||||
url,
|
||||
});
|
||||
}
|
||||
}
|
||||
</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>
|
||||
<image @click="emit('toggleNotOpen')" src="@img/chef/115.png" class="w-40rpx h-40rpx"></image>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,37 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
list: object[];
|
||||
}>();
|
||||
const { t } = useI18n();
|
||||
|
||||
function handleClickFood(item: any) {
|
||||
uni.navigateTo({
|
||||
url: '/pages-store/pages/store/index?id=' + item.id
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<scroll-view :scroll-x="true">
|
||||
<view class="flex items-center">
|
||||
<view class="shrink-0 w-30rpx"></view>
|
||||
<template v-for="(item, index) in list" :key="index">
|
||||
<view @click="handleClickFood(item)" :class="[index === 0 ? '' : 'ml-42rpx']">
|
||||
<image
|
||||
class="w-156rpx h-156rpx rounded-50% bg-common"
|
||||
:src="item.logo"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
<view class="w-156rpx text-center line-clamp-1 text-#333 text-28rpx lh-28rpx mt-12rpx font-500">
|
||||
{{ item.merchantName }}
|
||||
</view>
|
||||
<view v-if="+item.deliveryService === 1" class="mt-12rpx text-center text-24rpx lh-24rpx text-#7D7D7D">{{ item.deliveryTime }}{{ t('common.minutes') }}</view>
|
||||
</view>
|
||||
</template>
|
||||
<view class="shrink-0 w-30rpx op-0">1</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
</style>
|
||||
@@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
const { t } = useI18n();
|
||||
const show = ref(false);
|
||||
|
||||
function onOpen() {
|
||||
show.value = true;
|
||||
}
|
||||
function handleClose() {
|
||||
show.value = false;
|
||||
}
|
||||
defineExpose({
|
||||
onOpen,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<wd-popup
|
||||
v-model="show"
|
||||
position="center"
|
||||
@close="handleClose"
|
||||
custom-style="border-radius:20rpx;"
|
||||
>
|
||||
<!-- #ifdef APP-PLUS -->
|
||||
<view class="w-590rpx rounded-20rpx relative">
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef APP-PLUS -->
|
||||
<view class="w-590rpx h-570rpx rounded-20rpx relative">
|
||||
<!-- #endif -->
|
||||
<view class="flex flex-col items-center mb-34rpx">
|
||||
<image src="@img/chef/100.png" class="w-318rpx h-318rpx"></image>
|
||||
<view class="text-40rpx lh-40rpx text-#333 font-500 mt--20rpx mb-22rpx">{{ t('pages.shop.tips') }}</view>
|
||||
<view class="text-28rpx text-#333 text-center px-40rpx tracking-[.04em]">
|
||||
{{ t('components.noOpen.title') }}
|
||||
</view>
|
||||
</view>
|
||||
<view @click="handleClose" class="absolute bottom-0 left-0 w-full border-top h-98rpx center text-30rpx text-#333">
|
||||
{{ t('common.gotIt') }}
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps({
|
||||
isAutoJump: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
const emit = defineEmits<{
|
||||
(e: 'clickSearch'): void
|
||||
}>()
|
||||
|
||||
function handleClickSearch() {
|
||||
if(props.isAutoJump) {
|
||||
uni.navigateTo({
|
||||
url: '/pages/search/index',
|
||||
})
|
||||
} else {
|
||||
emit('clickSearch')
|
||||
}
|
||||
}
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,94 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
list: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
currentId: {
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
},
|
||||
labelKey: {
|
||||
type: String,
|
||||
default: 'name',
|
||||
},
|
||||
valueKey: {
|
||||
type: String,
|
||||
default: 'id',
|
||||
},
|
||||
imgKey: {
|
||||
type: String,
|
||||
default: 'logoUrl',
|
||||
},
|
||||
})
|
||||
const emit = defineEmits(['changeType']);
|
||||
watchEffect(() => {
|
||||
if (props.currentId) {
|
||||
selectedIndex.value = props.currentId;
|
||||
}
|
||||
});
|
||||
|
||||
const selectedIndex = ref();
|
||||
|
||||
function selectTab(item: any) {
|
||||
selectedIndex.value = item[props.valueKey];
|
||||
// 触发父组件事件
|
||||
console.log('selectTab', item[props.valueKey]);
|
||||
emit('changeType', item[props.valueKey]);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<scroll-view :scroll-x="true">
|
||||
<view class="flex items-center">
|
||||
<view class="shrink-0 w-30rpx"></view>
|
||||
<template v-for="(item, index) in list" :key="index">
|
||||
<view
|
||||
:class="[index === 0 ? '' : 'ml-40rpx']"
|
||||
class="w-112rpx flex flex-col items-center"
|
||||
@click="selectTab(item)"
|
||||
>
|
||||
<view
|
||||
:class="['img-wrap', selectedIndex == item[props.valueKey] ? 'img-selected' : '']"
|
||||
>
|
||||
<image
|
||||
class="tab-img rounded-50% overflow-hidden bg-common"
|
||||
:src="item[props.imgKey]"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
</view>
|
||||
<text
|
||||
:class="selectedIndex == item[props.valueKey] ? 'text-#CE7138' : 'text-#333'"
|
||||
class="line-clamp-1 text-20rpx lh-20rpx mt-12rpx font-500"
|
||||
>{{ item[props.labelKey] }}</text
|
||||
>
|
||||
</view>
|
||||
</template>
|
||||
<view class="shrink-0 w-30rpx op-0">1</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.img-wrap {
|
||||
display: flex;
|
||||
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;
|
||||
}
|
||||
.img-selected {
|
||||
border: 4rpx solid #ce7138;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tab-img {
|
||||
width: 112rpx;
|
||||
height: 112rpx;
|
||||
border-radius: 50%;
|
||||
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,424 @@
|
||||
<script setup lang="ts">
|
||||
import useEventEmit from "@/hooks/useEventEmit";
|
||||
import {CollectionType, EventEnum} from "@/constant/enums";
|
||||
import { useConfigStore, useUserStore } from "@/store";
|
||||
import Config from '@/config/index'
|
||||
import { debounce } from 'throttle-debounce'
|
||||
import MsgBox from "@/pages/home/components/tabbar-home/components/msg-box.vue";
|
||||
import Search from "@/pages/home/components/tabbar-home/components/search.vue";
|
||||
import ClassBullet from "./components/class-bullet.vue";
|
||||
import TabsType from "./components/tabs-type.vue";
|
||||
import FeaturedOn from "./components/featured-on/index.vue";
|
||||
import FiltrateTool from "@/components/filtrate-tool/index.vue";
|
||||
import NearbyMerchants from "./components/nearby-merchants/index.vue";
|
||||
import FoodBox from "./components/food-box/index.vue";
|
||||
import HomeSkeleton from "./components/home-skeleton.vue";
|
||||
import {
|
||||
appMarketActivityListPost,
|
||||
appMerchantCartListMerchantPost,
|
||||
appMerchantCategoryListGet,
|
||||
appMerchantFeaturedListPost,
|
||||
appMerchantLabelListGet,
|
||||
appMerchantNearbyListPost, appMerchantRecommendListPost,
|
||||
appCollectCollectPost
|
||||
} from "@/service";
|
||||
import usePage from "@/hooks/usePage";
|
||||
import {getFeaturedDishList} from "@/pages-store/service";
|
||||
const configStore = useConfigStore();
|
||||
const userStore = useUserStore();
|
||||
const props = defineProps<{
|
||||
scoreRange?: string;
|
||||
price?: Array<string> | string;
|
||||
}>();
|
||||
const emit = defineEmits(["toggleScore", "togglePrice", "toggleNotOpen"]);
|
||||
const { t } = useI18n();
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
function navigateTo(url: string) {
|
||||
if(userStore.checkLogin()) {
|
||||
uni.navigateTo({
|
||||
url,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const swiperList = ref([]);
|
||||
const currentSwiper = ref(0);
|
||||
|
||||
async function initData() {
|
||||
// 只在首次加载时显示骨架屏,避免切换时的白屏
|
||||
if(featuredList.value.length === 0) {
|
||||
loading.value = true
|
||||
}
|
||||
appMarketActivityList()
|
||||
getAppMerchantLabelList()
|
||||
getAppMerchantCategoryList()
|
||||
getAppFeaturedList()
|
||||
getAppNearbyListPost()
|
||||
// 获取当前用户购物车信息
|
||||
userStore.getUserCartAllData()
|
||||
}
|
||||
|
||||
// 筛选事件触发
|
||||
function toggleScore() {
|
||||
emit("toggleScore");
|
||||
}
|
||||
function togglePrice() {
|
||||
emit("togglePrice");
|
||||
}
|
||||
function toggleNotOpen() {
|
||||
emit("toggleNotOpen");
|
||||
}
|
||||
|
||||
async function getIndexList() {
|
||||
// 切换时立即刷新列表,无需等待nextTick
|
||||
if (paging.value) {
|
||||
paging.value.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
initData,
|
||||
init: getIndexList,
|
||||
});
|
||||
|
||||
// 获取轮播图列表
|
||||
function appMarketActivityList() {
|
||||
appMarketActivityListPost({}).then(res=> {
|
||||
console.log('互动列表', res)
|
||||
swiperList.value = res.data
|
||||
})
|
||||
}
|
||||
|
||||
// 滚动信息获取 APP-商家标签分类控制器(用户端首页上面左右滚动的)
|
||||
const appMerchantLabelList = ref([])
|
||||
function getAppMerchantLabelList() {
|
||||
appMerchantLabelListGet({}).then(res => {
|
||||
console.log('滚动信息获取 APP-商家标签分类控制器(用户端首页上面左右滚动的)', res)
|
||||
appMerchantLabelList.value = res.data || []
|
||||
})
|
||||
}
|
||||
// 查询所有商家分类数据
|
||||
const appMerchantCategoryList = ref([])
|
||||
const currentCategory = ref('')
|
||||
function getAppMerchantCategoryList() {
|
||||
appMerchantCategoryListGet({}).then((res: any) => {
|
||||
console.log('查询所有商家分类数据', res)
|
||||
appMerchantCategoryList.value = res.data || []
|
||||
})
|
||||
}
|
||||
|
||||
// 查询精选商家列表(首页)
|
||||
const featuredList = ref([])
|
||||
function getAppFeaturedList() {
|
||||
appMerchantFeaturedListPost({
|
||||
body: {
|
||||
lat: userStore.userLocation.latitude,
|
||||
lng: userStore.userLocation.longitude,
|
||||
}
|
||||
}).then(res=> {
|
||||
featuredList.value = res.data || []
|
||||
}).finally(()=> {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
// 查询附近商家列表(首页) /app/merchant/nearbyList
|
||||
const nearbyList = ref([])
|
||||
function getAppNearbyListPost() {
|
||||
appMerchantNearbyListPost({
|
||||
body: {
|
||||
lat: userStore.userLocation.latitude,
|
||||
lng: userStore.userLocation.longitude,
|
||||
}
|
||||
}).then(res=> {
|
||||
nearbyList.value = res.data || []
|
||||
})
|
||||
}
|
||||
|
||||
// 是否自提
|
||||
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()
|
||||
}
|
||||
const {paging, dataList, queryList} = usePage(getList)
|
||||
function getList(pageNum: number, pageSize: number) {
|
||||
return new Promise(resolve => {
|
||||
getFeaturedDishList({
|
||||
pageNum,
|
||||
pageSize,
|
||||
}).then(res => {
|
||||
console.log('查询精选菜品列表', res)
|
||||
resolve({rows: res.rows})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 点击头部分类
|
||||
const merchantLabelId = ref('')
|
||||
function handleItemClick(e) {
|
||||
console.log(e, '点击头部分类')
|
||||
merchantLabelId.value = e.id
|
||||
// paging.value.refresh()
|
||||
}
|
||||
function tabsTypeChange(id: string) {
|
||||
currentCategory.value = id
|
||||
navigateTo('/pages-store/pages/home-store/index?merchantCategoryIds=' + id)
|
||||
// console.log('分类切换', id)
|
||||
// paging.value.refresh()
|
||||
}
|
||||
|
||||
// 是否展示精选商家和附近商家 true 显示 false隐藏
|
||||
const isShowMerchant = computed(()=> {
|
||||
if(!selfPickup.value && !discount.value && !props.scoreRange && !props.price && !currentCategory.value) {
|
||||
return true // 没有筛选条件时显示
|
||||
} else {
|
||||
return false // 有筛选条件时隐藏
|
||||
}
|
||||
})
|
||||
|
||||
// 手动触发下拉刷新了
|
||||
function onRefresh() {
|
||||
console.log('手动触发下拉刷新了')
|
||||
merchantLabelId.value = ''
|
||||
currentCategory.value = ''
|
||||
paging.value.refresh()
|
||||
}
|
||||
|
||||
function handleClickSwiper(item: any) {
|
||||
console.log(item, '点击轮播图')
|
||||
switch (Number(item.activityType)) {
|
||||
case 1: // 商家列表
|
||||
navigateTo('/pages-store/pages/list/index?id=')
|
||||
break
|
||||
case 2: // 活动菜品列表
|
||||
navigateTo('/pages-store/pages/dishes/index?id=' + item.id)
|
||||
break
|
||||
case 3: // 会员
|
||||
navigateTo('/pages-user/pages/member/index')
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function navigateToDishes(item: any) {
|
||||
uni.navigateTo({
|
||||
url: '/pages-store/pages/store/dishes?id=' + item.id + '&storeId=' + item.merchantId,
|
||||
})
|
||||
}
|
||||
// 收藏菜品
|
||||
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) => {
|
||||
// 收藏接口
|
||||
appCollectCollectPost({
|
||||
body: {
|
||||
targetId: id,
|
||||
targetType: type
|
||||
}
|
||||
}).then(res=> {
|
||||
callback()
|
||||
})
|
||||
}, {
|
||||
atBegin: true, // 立即触发
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view
|
||||
class="bg-#fff"
|
||||
:style="[
|
||||
{
|
||||
height: configStore.windowHeight + 'px',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<z-paging @onRefresh="onRefresh" ref="paging" v-model="dataList" :auto="false" @query="queryList" :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">
|
||||
<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>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<view
|
||||
class="animate-in fade-in animate-ease-out animate-duration-300"
|
||||
v-show="loading && featuredList.length === 0"
|
||||
>
|
||||
<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">
|
||||
<search />
|
||||
<!-- 分类滚动区域 -->
|
||||
<view class="mt-40rpx" v-if="appMerchantLabelList.length > 0">
|
||||
<class-bullet :categories="appMerchantLabelList" @itemClick="handleItemClick" />
|
||||
</view>
|
||||
</view>
|
||||
<swiper
|
||||
class="card-swiper"
|
||||
:circular="true"
|
||||
:autoplay="true"
|
||||
previous-margin="60rpx"
|
||||
next-margin="60rpx"
|
||||
>
|
||||
<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"
|
||||
></image>
|
||||
</swiper-item>
|
||||
</template>
|
||||
</swiper>
|
||||
|
||||
<!-- 分类滚动区域 -->
|
||||
<tabs-type @changeType="tabsTypeChange" v-if="appMerchantCategoryList.length > 0" :list="appMerchantCategoryList" :currentId="currentCategory" class="mt-22rpx" />
|
||||
|
||||
<!-- 筛选工具 -->
|
||||
<!-- <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 :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>
|
||||
<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"
|
||||
>{{ 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">
|
||||
<image
|
||||
v-if="!item.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?.dishImage?.split(',')[0]"
|
||||
mode="aspectFill"
|
||||
class="w-full h-full rounded-24rpx bg-common"
|
||||
/>
|
||||
</view>
|
||||
<view class="line-clamp-1 text-30rpx text-#333 font-500">
|
||||
{{ 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="member-price-tag text-[#FBE3C3] font-500 text-30rpx lh-30rpx center pl-6rpx break-all">
|
||||
<text>{{ t('pages-store.store.members') }}: </text>
|
||||
${{ item?.memberPrice }}
|
||||
</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">
|
||||
<image
|
||||
src="@img/chef/1285.png"
|
||||
class="w-30rpx h-30rpx shrink-0"
|
||||
></image>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.card-swiper {
|
||||
height: 420rpx;
|
||||
}
|
||||
.swiper-item-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform: scale(0.95);
|
||||
border-radius: 20rpx;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user