first commit

This commit is contained in:
2026-02-26 09:32:03 +08:00
commit 36a8e4c51b
845 changed files with 116474 additions and 0 deletions
@@ -0,0 +1,180 @@
<template>
<view class="browse-skeleton">
<!-- 头部标题区域 -->
<view class="header-section">
<view class="title-skeleton skeleton-item"></view>
<view class="header-actions">
<view class="action-icon-skeleton skeleton-item"></view>
<view class="action-icon-skeleton skeleton-item"></view>
</view>
</view>
<!-- 搜索栏 -->
<view class="search-section">
<view class="search-bar-skeleton skeleton-item"></view>
</view>
<!-- 精选食谱区域 -->
<view class="selected-recipes-section">
<view class="section-header">
<view class="section-title-skeleton skeleton-item"></view>
<view class="section-action-skeleton skeleton-item"></view>
</view>
<view class="recipes-grid">
<view
v-for="i in 3"
:key="i"
class="recipe-card-skeleton skeleton-item"
></view>
</view>
</view>
<!-- 附近美食区域 -->
<view class="nearby-cuisine-section">
<view class="section-title-skeleton skeleton-item"></view>
<view class="cuisine-grid">
<view
v-for="i in 8"
:key="i"
class="cuisine-card-skeleton skeleton-item"
></view>
</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;
}
}
.browse-skeleton {
background-color: #fff;
min-height: 100vh;
padding: 0 30rpx;
}
// 头部区域
.header-section {
padding: 16rpx 0 0;
display: flex;
justify-content: space-between;
align-items: center;
.title-skeleton {
width: 200rpx;
height: 56rpx;
border-radius: 8rpx;
}
.header-actions {
display: flex;
gap: 20rpx;
.action-icon-skeleton {
width: 40rpx;
height: 40rpx;
border-radius: 20rpx;
}
}
}
// 搜索栏
.search-section {
padding: 48rpx 0 0;
.search-bar-skeleton {
width: 690rpx;
height: 88rpx;
border-radius: 44rpx;
}
}
// 精选食谱区域
.selected-recipes-section {
padding: 56rpx 0 0;
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32rpx;
.section-title-skeleton {
width: 292rpx;
height: 36rpx;
border-radius: 8rpx;
}
.section-action-skeleton {
width: 64rpx;
height: 64rpx;
border-radius: 32rpx;
}
}
.recipes-grid {
display: flex;
gap: 30rpx;
overflow-x: auto;
.recipe-card-skeleton {
flex-shrink: 0;
width: 310rpx;
height: 296rpx;
border-radius: 24rpx;
}
}
}
// 附近美食区域
.nearby-cuisine-section {
padding: 62rpx 0 0;
.section-title-skeleton {
width: 252rpx;
height: 36rpx;
border-radius: 8rpx;
margin-bottom: 32rpx;
}
.cuisine-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30rpx;
.cuisine-card-skeleton {
height: 186rpx;
border-radius: 24rpx;
}
}
}
// 响应式设计
@media (max-width: 750rpx) {
.recipes-grid {
padding-bottom: 20rpx;
}
.cuisine-grid {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,204 @@
<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 { t } = useI18n();
function navigateTo(url: string) {
if(userStore.checkLogin()) {
uni.navigateTo({
url,
});
}
}
function toggleNotOpen() {
emit("toggleNotOpen");
}
async function initData() {
if(!recipeData.value) {
loading.value = true;
}
getRecipeData()
// 获取菜品数据
appMerchantDishNearbyList()
}
// 获取菜谱数据
const recipeData = ref([]);
function getRecipeData() {
appSearchSearchRecipePost({
body: {
pageNum: 1,
pageSize: 10,
}
}).then(res=> {
console.log('菜谱数据', res)
recipeData.value = res.rows;
}).finally(()=> {
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}`)
}
// 获取附近的菜品
const dishData = ref([]);
function appMerchantDishNearbyList() {
appMerchantDishNearbyListPost({
params: {
pageNum: 1,
pageSize: 10,
},
body: {
lat: userStore.userLocation.latitude,
lng: userStore.userLocation.longitude,
}
}).then(res=> {
console.log('菜品数据', res)
dishData.value = res.rows;
})
}
function handleClickDish(item: any) {
navigateTo(`/pages-store/pages/store/index?id=${item.merchantId}`)
}
async function getPlatformDefaultStoreInfo() {}
defineExpose({
initData,
init: getPlatformDefaultStoreInfo,
});
</script>
<template>
<view
class="bg-#fff"
:style="[
{
height: configStore.windowHeight + 'px',
},
]"
>
<z-paging ref="paging">
<template #top>
<status-bar />
</template>
<view
v-show="loading"
class="animate-in fade-in animate-ease-out animate-duration-300"
>
<browse-skeleton />
</view>
<view
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">
<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">
<image
:src="thumbnailImg(item?.recipeImage?.split(',')[0])"
class="w-310rpx h-296rpx rounded-24rpx mb-26rpx bg-common"
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)"
/>
</view>
</view>
</view>
</template>
</view>
</scroll-view>
<view
class="mt-50rpx mb-28rpx text-36rpx lh-36rpx text-#333 font-bold"
>{{ t("pages.browse.titleCuisine") }}</view
>
<view class="grid grid-cols-2 gap-x-30rpx gap-y-46rpx pb-40rpx">
<template v-for="item in dishData">
<view @click="handleClickDish(item)" class="w-330rpx overflow-hidden">
<image
:src="thumbnailImg(item?.dishImage?.split(',')[0])"
class="w-full h-186rpx rounded-24rpx mb-16rpx bg-common"
mode="aspectFill"
></image>
<text class="text-30rpx lh-30rpx text-#333 line-clamp-1 tracking-[.04em] font-500"
>{{ item.dishName }}</text
>
</view>
</template>
</view>
</view>
</view>
<template #bottom>
<view class="h-50px"></view>
<view :style="[configStore.iosSafeBottomPlaceholder]"></view>
</template>
</z-paging>
</view>
</template>
<style scoped lang="scss"></style>
@@ -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>
@@ -0,0 +1,55 @@
<script setup lang="ts">
import {appCustomerServiceGetInfoGet} from "@/service";
import {callPhone} from "@/utils/utils";
const {t, locale} = useI18n();
const show = ref(false);
const serviceData = ref({})
function init() {
appCustomerServiceGetInfoGet({}).then(res=> {
console.log('客服信息', res)
serviceData.value = res.data
})
show.value = true;
}
function close() {
show.value = false;
}
function handleDial() {
callPhone(serviceData.value.servicePhone)
}
defineExpose({
show,
init,
close,
});
</script>
<template>
<wd-popup
custom-class="!bg-transparent"
position="bottom"
safe-area-inset-bottom
v-model="show"
@close="close"
>
<view class="p-[40rpx+20rpx]">
<view
class="mb-18rpx py-32rpx text-center bg-white shadow-[0rpx_3rpx_6rpx_rgba(0,0,0,0.16)] rounded-16rpx"
>
<view class="text-30rpx text-#333 font-bold mb-22rpx">{{ t("pages.mine.customer-service-phone") }}: {{ serviceData.servicePhone }}</view>
<view class="text-28rpx text-#999">{{ t("pages.mine.work-time") }}: {{ locale === 'en' ? serviceData.workingHoursEn : serviceData.workingHoursZh }}</view>
</view>
<view
class="h-100rpx flex items-center justify-center text-28rpx text-primary font-bold shadow-[0rpx_3rpx_6rpx_rgba(0,0,0,0.16)] bg-white rounded-16rpx"
@click="handleDial"
>
{{ t("pages.mine.dial") }}
</view>
</view>
</wd-popup>
</template>
<style scoped lang="scss"></style>
@@ -0,0 +1,286 @@
<script setup lang="ts">
import {useUserStore} from '@/store'
import Config from "@/config";
const emits = defineEmits<{
success: []
}>()
const {t} = useI18n()
const userStore = useUserStore()
const painterRef = ref(null)
const path = ref('')
const poster = computed(() => ({
css: {
width: '658rpx',
height: '1052rpx',
},
views: [
{
src: '/static/images/50@2x.png',
type: 'image',
css: {
position: 'absolute',
left: 0,
top: 0,
width: '100%',
height: '100%',
},
views: [
{
type: 'view',
css: {
height: '706rpx',
},
views: [
{
// src: '/static/images/1334@2x.png',
src: Config.shareImage,
type: 'image',
css: {
position: 'absolute',
left: 0,
top: 0,
width: '100%',
height: '100%',
},
},
{
type: 'view',
css: {
position: 'absolute',
left: 0,
right: 0,
top: '14rpx',
display: 'flex',
"justifyContent": 'center'
},
views: [
// {
// src: '/static/images/img_word@2x.png',
// type: 'image',
// css: {
// width: '592rpx',
// height: '238rpx',
// },
// },
],
},
],
},
{
views: [
{
css: {
padding: '24rpx 24rpx',
},
type: 'view',
views: [
{
views: [
{
text: `${t('pages.mine.activity-description')}`,
type: 'text',
css: {
color: '#000',
fontSize: '28rpx',
fontWeight: 'bold',
lineHeight: '42rpx'
},
},
],
type: 'view',
},
{
css: {
marginTop: '8rpx',
},
views: [
{
text: Config.shareDesc,
type: 'text',
css: {
color: '#666',
fontSize: '24rpx',
fontWeight: 'bold',
lineHeight: '34rpx'
},
},
],
type: 'view',
},
]
},
{
css: {
marginTop: '2rpx',
borderTop: '1rpx dashed #D1D1D1',
},
type: 'view',
},
{
css: {
padding: '24rpx 24rpx',
display: 'flex',
'align-items': 'center',
},
views: [
{
css: {
flex: '1',
display: 'flex',
'align-items': 'center',
},
views: [
{
src: userStore.userInfo.avatar,
type: 'image',
css: {
width: '94rpx',
height: '94rpx',
borderRadius: '50%',
},
},
{
css: {
display: 'inline-block',
marginLeft: '24rpx',
},
views: [
{
views: [
{
text: `${userStore.userInfo.firstName} ${userStore.userInfo.surname}`,
type: 'text',
css: {
color: '#000',
fontSize: '30rpx',
fontWeight: 'bold',
lineHeight: '42rpx'
},
},
],
type: 'view',
},
{
css: {
marginTop: '8rpx',
},
views: [
{
text: `${t('pages.mine.invitation-code')}${userStore.userInfo.invitationCode}`,
type: 'text',
css: {
color: '#999',
fontSize: '24rpx',
fontWeight: 'bold',
lineHeight: '34rpx'
},
},
],
type: 'view',
},
],
type: 'view',
},
],
type: 'view',
},
{
text: Config.shareLink + `pages-login/pages/sign-up/index?invitationCode=${userStore.userInfo.invitationCode}`,
type: 'qrcode',
css: {
width: '124rpx',
height: '124rpx',
},
},
],
type: 'view',
},
],
type: 'view',
},
],
},
],
}))
function save() {
onAppSaveToPhone()
}
function onAppSaveToPhone() {
uni.saveImageToPhotosAlbum({
filePath: path.value,
success: function () {
console.log('save success')
uni.showToast({
title: t('common.prompt.save-successfully'),
icon: 'none',
})
emits('success')
},
fail(error) {
console.log(error)
uni.showToast({
title: t('common.prompt.save-failed'),
icon: 'none',
})
},
complete() {
uni.hideLoading()
}
})
}
function init() {
console.log('init')
console.log(userStore.userInfo)
uni.showLoading({
title: t('common.loading') + '...',
})
painterRef.value?.render(poster.value)
painterRef.value?.canvasToTempFilePathSync({
fileType: 'png',
// 如果返回的是base64是无法使用 saveImageToPhotosAlbum,需要设置 pathType为url
pathType: 'url',
success: (res: any) => {
console.log('res', res)
path.value = res.tempFilePath
save()
},
fail(error: Error) {
console.log(error)
uni.hideLoading()
uni.showToast({
title: t('common.failure'),
icon: 'none',
})
},
})
}
defineOptions({
name: "InvitePoster",
});
defineExpose({
path,
init,
})
</script>
<template>
<l-painter ref="painterRef" path-type="url" hidden custom-style="position: fixed; left: 200%"/>
</template>
<style scoped lang="scss"></style>
@@ -0,0 +1,113 @@
<script setup lang="ts">
import InvitePoster from "@/pages/home/components/tabbar-mine/components/invite-poster/invite-poster.vue";
import {throttle} from "throttle-debounce";
import Config from "@/config";
import {useUserStore} from "@/store";
import {setClipboardData} from "@/utils/utils";
const {t} = useI18n();
const userStore = useUserStore()
const invitePosterRef = ref<InstanceType<typeof InvitePoster>>()
const show = ref(false);
function init() {
show.value = true;
}
function close() {
show.value = false;
}
function navigateTo(url: string) {
uni.navigateTo({
url,
})
}
const handleSave = throttle(Config.throttleTime, () => {
invitePosterRef.value?.init()
}, {})
function copyLink() {
setClipboardData(Config.shareLink + `pages-login/pages/sign-up/index?invitationCode=${userStore.userInfo.invitationCode}`)
}
defineOptions({
name: "InviteUser",
});
defineExpose({
show,
init,
close,
});
</script>
<template>
<wd-popup
custom-class="!bg-transparent"
safe-area-inset-bottom
v-model="show"
@close="close"
>
<view class="box-border px-46rpx w-750rpx">
<view class="flex justify-end">
<image class="w-66rpx h-66rpx" src="@img/14952@2x.png" @click="close"></image>
</view>
<view class="mt-36rpx relative h-1052rpx">
<image class="absolute top-0 left-0 w-full h-full" src="@img/50@2x.png"></image>
<!-- <view class="z-9 absolute top-14rpx left-0 right-0 center text-0rpx">-->
<!-- <image class="w-592rpx h-238rpx" src="@img/img_word@2x.png"></image>-->
<!-- </view>-->
<view class="relative z-2 flex flex-col">
<!-- <image class="w-full h-706rpx" src="@img/1334@2x.png"></image>-->
<image class="w-full h-706rpx" :src="Config.shareImage"></image>
<view class="py-24rpx px-24rpx">
<view class="text-28-bold">{{ t('pages.mine.activity-description') }}</view>
<view class="mt-8rpx text-24rpx text-#666 font-bold lh-34rpx">
{{ Config.shareDesc }}
</view>
</view>
<view class="mt-2rpx border-1rpx border-dashed border-#D1D1D1"></view>
<view class="py-24rpx px-24rpx flex items-center justify-between">
<view class="flex items-center">
<image class="shrink-0 mr-24rpx w-94rpx h-94rpx rounded-full" :src="userStore.userInfo.avatar"></image>
<view class="">
<view class="text-30-bold">{{ `${userStore.userInfo.firstName} ${userStore.userInfo.surname}` }}</view>
<view class="mt-8rpx text-24rpx text-#999 font-bold lh-34rpx">
<text>{{ t('pages.mine.invitation-code') }}</text>
<text>{{ userStore.userInfo.invitationCode }}</text>
</view>
</view>
</view>
<view class="shrink-0 ml-20rpx" v-if="userStore.userInfo.invitationCode">
<uqrcode ref="uqrcode" canvas-id="qrcode"
:value="Config.shareLink + `pages-login/pages/sign-up/index?invitationCode=${userStore.userInfo.invitationCode}`"
:size="124"
sizeUnit="rpx"
:options="{}"></uqrcode>
</view>
</view>
</view>
</view>
<view class="mt-34rpx flex justify-between items-center ">
<wd-button
custom-class="!box-border shrink-0 !min-w-auto !h-98rpx !w-316rpx !text-#333 !text-30rpx !lh-42rpx !rounded-16rpx !bg-#fff"
@click="copyLink"
>
{{ t('common.copyLink') }}
</wd-button>
<wd-button
custom-class="!box-border shrink-0 !min-w-auto !h-98rpx !w-316rpx !text-30rpx !lh-42rpx !rounded-16rpx"
@click="handleSave"
>
{{ t('pages.mine.save-picture') }}
</wd-button>
</view>
</view>
</wd-popup>
<invite-poster ref="invitePosterRef" @success="close"/>
</template>
<style scoped lang="scss"></style>
@@ -0,0 +1,230 @@
<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>
<!-- 统计卡片区域 -->
<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>
</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>
<!-- 菜单列表区域 -->
<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>
</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;
}
}
.mine-skeleton {
background-color: #fff;
padding: 0 30rpx;
}
// 用户信息区域
.user-info-section {
padding: 36rpx 0 0;
display: flex;
align-items: center;
gap: 20rpx;
.user-avatar-skeleton {
width: 130rpx;
height: 130rpx;
border-radius: 50%;
}
.user-name-skeleton {
width: 233rpx;
height: 56rpx;
border-radius: 8rpx;
}
}
// 统计卡片区域
.stats-section {
padding: 90rpx 0 0;
.stats-list {
display: flex;
gap: 36rpx;
.stat-card-skeleton {
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;
}
.stat-label-skeleton {
width: 120rpx;
height: 28rpx;
border-radius: 6rpx;
}
}
}
}
// 会员横幅区域
.member-banner-section {
padding: 40rpx 0 0;
.member-banner-skeleton {
width: 690rpx;
height: 152rpx;
border-radius: 24rpx;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 28rpx;
.banner-content {
display: flex;
flex-direction: column;
gap: 12rpx;
.banner-title-skeleton {
width: 457rpx;
height: 57rpx;
border-radius: 8rpx;
}
.banner-subtitle-skeleton {
width: 497rpx;
height: 48rpx;
border-radius: 6rpx;
}
}
.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 {
display: flex;
align-items: center;
padding: 22rpx 0;
// border-bottom: 1rpx solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.menu-icon-skeleton {
width: 44rpx;
height: 44rpx;
border-radius: 8rpx;
margin-right: 20rpx;
}
.menu-text-skeleton {
flex: 1;
height: 30rpx;
border-radius: 6rpx;
}
.menu-arrow-skeleton {
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;
}
}
}
</style>
@@ -0,0 +1,331 @@
<script setup lang="ts">
import * as R from "ramda";
import dayjs from 'dayjs'
import useEventEmit from "@/hooks/useEventEmit";
import { EventEnum } from "@/constant/enums";
import { useConfigStore, useUserStore } from "@/store";
import MineSkeleton from "./components/mine-skeleton.vue";
const configStore = useConfigStore();
const userStore = useUserStore();
import { Agreement } from "@/constant/enums";
import Config from '@/config/index'
import {formatTimestampWithMonthName, loadWeixinService} from "@/utils/utils";
const { t } = useI18n();
const loading = ref(true);
const emits = defineEmits<{
chooseLanguage: [];
logOut: [];
inviteUser: [];
customerService: [];
changeOrder: [];
}>();
interface Tab {
iconPath: string;
path: string;
text: string;
code: string;
isLogin: boolean;
}
const tabBarList = ref<Tab[]>([
{
iconPath: "/static/images/chef/100201.png",
path: "/pages-user/pages/coupon/index",
text: t("pages.mine.discount"),
code: "discount",
isLogin: true,
},
{
iconPath: "/static/images/chef/100200.png",
path: "/pages-user/pages/faqs/index",
text: t("pages.mine.help"),
code: "help",
isLogin: true,
},
{
iconPath: "/static/images/chef/100199.png",
path: "/pages-user/pages/coupon/index",
text: t("pages.mine.inviteFriends"),
code: "inviteFriends",
isLogin: true,
},
{
iconPath: "/static/images/100203.png",
path: "/pages-user/pages/invited-person/index",
text: t('pages.mine.my-invitations'),
code: "myInvitations",
isLogin: true,
},
{
iconPath: "/static/images/chef/100198.png",
path: "/pages-user/pages/store-settle-in/index",
text: t("pages.mine.storeSettled"),
code: "storeSettled",
isLogin: false,
},
{
iconPath: "/static/images/chef/100197.png",
path: "/pages-user/pages/coupon/index",
text: t("pages.mine.support"),
code: "support",
isLogin: true,
},
{
iconPath: "/static/images/chef/100196.png",
path: "/pages/agreement/index?code=" + Agreement.CHEF_PLATFORM_AGREEMENT,
text: t("pages.mine.platformAgreement"),
code: "platformAgreement",
isLogin: true,
},
{
iconPath: "/static/images/chef/100195.png",
path: "/pages/agreement/index?code=" + Agreement.PRIVACY_POLICY,
text: t("pages.mine.privacyPolicy"),
code: "privacyPolicy",
isLogin: true,
},
{
iconPath: "/static/images/chef/100194.png",
path: "/pages-user/pages/complaints/index",
text: t("pages.mine.complaintsAndSuggestions"),
code: "complaintsAndSuggestions",
isLogin: true,
},
{
iconPath: "/static/images/chef/100192.png",
path: "/pages-user/pages/setting/index",
text: t("pages.mine.set"),
code: "set",
isLogin: true,
},
]);
function navigateTo(url: string) {
if(userStore.checkLogin()) {
uni.navigateTo({
url,
});
}
}
function checkNeedLogin(isNeedLogin: boolean) {
return isNeedLogin ? userStore.checkLogin() : true;
}
// 用户会员状态是否已开通
const isUserMember = computed(()=> {
if(!userStore.userInfo.userMembershipVo) return false
if(userStore.userInfo.userMembershipVo && userStore.userInfo.userMembershipVo.expireTime ){
return dayjs().isBefore(dayjs(Number(userStore.userInfo.userMembershipVo.expireTime)))
} else {
return false
}
})
function handleTabClick(item: Tab) {
switch (item.code) {
case "inviteFriends": {
// R.both(checkNeedLogin, R.pipe(() => emits("inviteUser"), R.T))(item.isLogin)
emits("inviteUser");
break;
}
case "support": {
// emits("customerService");
loadWeixinService()
break;
}
default: {
// navigateTo(item.path)
if (item.code === "set") {
navigateTo("/pages-user/pages/setting/index");
} else {
R.both(
checkNeedLogin,
R.pipe(() => navigateTo(item.path), R.T)
)(item.isLogin);
}
break;
}
}
}
function changeOrderFn() {
emits("changeOrder");
}
async function initData() {
loading.value = false;
// setTimeout(() => {
// loading.value = false;
// }, 300);
userStore.getUserInfo()
}
async function getPlatformDefaultStoreInfo() {}
defineExpose({
initData,
init: getPlatformDefaultStoreInfo,
});
</script>
<template>
<view
class="bg-#fff"
:style="[
{
height: configStore.windowHeight + 'px',
},
]"
>
<z-paging ref="paging">
<template #top>
<status-bar />
</template>
<view
v-show="loading"
class="animate-in fade-in animate-ease-out animate-duration-300"
>
<mine-skeleton />
</view>
<view
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]"
>
{{
userStore.isLogin
? ([userStore.userInfo.firstName, userStore.userInfo.surname].filter(Boolean).join(' ') || t('common.unknownUser'))
: t('common.pleaseLogin')
}}
</text>
<image
v-if="userStore.isLogin"
:src="userStore.userInfo.avatar"
class="w-130rpx h-130rpx rounded-50%"
mode="aspectFill"
/>
<image
v-else
class="w-130rpx h-130rpx rounded-50%"
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>
<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>
<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>
</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]">
{{ 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">
<!--用户没有试用过会员-->
<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>
</view>
</template>
<view
class="flex-center-sb py-28rpx bg-#fff"
v-for="(item, index) in tabBarList"
:key="item.code"
:class="[
index === tabBarList.length - 1 ? 'mb-58rpx' : 'border-bottom',
]"
@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>
<view class="flex items-center shrink-0 ml-20rpx">
<image class="w-22rpx h-30rpx" src="@img/chef/100202.png"></image>
</view>
</view>
</view>
</view>
<template #bottom>
<view class="h-50px"></view>
<view :style="[configStore.iosSafeBottomPlaceholder]"></view>
</template>
</z-paging>
</view>
</template>
<style scoped lang="scss">
.border-bottom {
border-bottom: 1rpx solid #dfdfdf;
}
</style>
@@ -0,0 +1,192 @@
<script setup lang="ts">
import {
appMerchantOrderOrderListPost,
type MerchantOrderVo
} from "@/service";
import {callPhone, formatTimestampWithWeekday} 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";
const userStore = useUserStore();
const { t } = useI18n();
const props = defineProps<{
currentIndex: number
status: null | number | string
}>()
watch(() => props.currentIndex, (newVal, oldVal) => {
if (newVal === props.status && !firstLoaded.value) {
paging.value?.refresh()
}
}, {
deep: true,
})
const {paging, dataList, loading, queryList, firstLoaded} = usePage<MerchantOrderVo[]>((pageNum, pageSize)=>{
if(userStore.isLogin) {
return appMerchantOrderOrderListPost({
params: {
pageNum,
pageSize,
},
body: {
userPort: 1,
orderStatusList: props.status ? [Number(props.status + 1)] : [],
}
})
} else {
return new Promise(resolve => {
resolve([])
})
}
})
function handleClick(item: any) {
uni.navigateTo({
url: '/pages-store/pages/order/index?id=' + item.id
})
}
function reload() {
paging.value?.reload()
}
function refresh() {
paging.value?.refresh()
}
defineOptions({
name: 'OrderSwiperList',
})
defineExpose({
reload,
refresh,
})
</script>
<template>
<view class="h-full">
<z-paging ref="paging" v-model="dataList" @query="queryList" :fixed="false" :auto="false">
<view class="p-30rpx">
<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>
<!-- 商家信息骨架屏 -->
<view class="flex items-center my-28rpx">
<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>
<!-- 轮播图骨架屏 -->
<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>
<!-- 底部按钮区域骨架屏 -->
<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>
</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>
</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">
{{ 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">
{{ t('pages.order.PU') }}
</view>
</view>
<swiper class="h-568rpx mb-30rpx" :autoplay="true">
<swiper-item v-for="img in item.merchantOrderDishVoList">
<image
:src="img.merchantDishVo.dishImage?.split(',')[0]"
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>
</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>
</template>
</view>
</view>
</view>
</template>
</view>
</view>
</z-paging>
</view>
</template>
<style scoped lang="scss">
</style>
@@ -0,0 +1,127 @@
<script setup lang="ts">
import {OrderStatus} from "@/constant/enums";
import OrderSwiperList from "./components/order-swiper-list/order-swiper-list.vue";
import {useConfigStore} from "@/store";
import {debounce} from "throttle-debounce";
const configStore = useConfigStore()
const {t} = useI18n()
const orderSwiperListRef = ref(null)
// async function initData() {
// orderSwiperListRef.value[currentIndex.value].reload()
// }
const initData = debounce(500, () => {
nextTick(() => {
// listItem.value[currentTab.value].reload()
orderSwiperListRef.value[currentIndex.value].reload()
})
}, {atBegin: true})
async function getPlatformDefaultStoreInfo() {}
const currentIndex = ref(0)
const tabList = ref([
{
name: t('pages.order.all'),
value: ''
},
{
name: t('pages.order.confirmReady'), // 待接单待取餐
value: OrderStatus.HAS_PENDING_PAYMENT,
},
{
name: t('pages.order.accept'), // 已接单
value: OrderStatus.MERCHANT_ACCEPTED,
},
{
name: t('pages.order.onTheWay'), // 配送中
value: OrderStatus.DELIVERING,
},
{
name: t('pages.order.completed'), // 已送达
value: OrderStatus.COMPLETED,
},
])
function handleClickTab(event: any) {
currentIndex.value = event.index
}
function handleSwiperChange(event: any){
currentIndex.value = event.detail.current
}
defineExpose({
initData,
init: getPlatformDefaultStoreInfo,
})
</script>
<template>
<view
:style="[{
height: configStore.windowHeight+'px',
}]"
>
<view class="bg h-360rpx fixed top-0 left-0 right-0"></view>
<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">
<wd-tabs
slidable="always"
key="tab"
color="#333"
inactiveColor="#999"
:line-height="3"
:line-width="30"
v-model="currentIndex"
@click="handleClickTab"
>
<template v-for="(item, index) in tabList" :key="index">
<wd-tab :title="item.name"></wd-tab>
</template>
</wd-tabs>
<view class="px-20rpx mt--2rpx">
<view class="border-b-solid border-b-4rpx border-b-#999"></view>
</view>
</view>
</template>
<swiper class="h-full"
: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>
</swiper-item>
</swiper>
<template #bottom>
<view class="h-50px"></view>
<view :style="[configStore.iosSafeBottomPlaceholder]"></view>
</template>
</z-paging-swiper>
</view>
</template>
<style scoped lang="scss">
.tab {
:deep(.wd-tabs) {
.wd-tabs__line {
bottom: 0 !important;
z-index: 99 !important;
border-radius: 0 !important;
}
.wd-tabs__nav-item {
padding: 0 22rpx !important;
}
.wd-tabs__nav-item-text{
font-size: 32rpx !important;
text-overflow: unset !important;
font-family: 'UberMove', sans-serif !important;
}
}
}
.bg {
background: linear-gradient(180deg, #FFFFFF 0%, #FFFFFF 51%, rgba(255, 255, 255, 0) 100%);
}
</style>
@@ -0,0 +1,33 @@
<script setup lang="ts">
import {useConfigStore} from "@/store";
const configStore = useConfigStore()
const {t} = useI18n()
</script>
<template>
<view class="bg-#fff"
:style="[{
height: configStore.windowHeight+'px',
}]"
>
<z-paging ref="paging">
<template #top>
<status-bar/>
</template>
<view class="text-56rpx text-#333 lh-56rpx font-bold pl-30rpx pt-16rpx">{{ t('tabBar.global') }}</view>
<view class="flex flex-col items-center mt-306rpx">
<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-108rpx tracking-[.04em]">{{ t('pages.shop.description') }}</view>
</view>
<template #bottom>
<view class="h-50px"></view>
<view :style="[configStore.iosSafeBottomPlaceholder]"></view>
</template>
</z-paging>
</view>
</template>
<style scoped lang="scss">
</style>
+652
View File
@@ -0,0 +1,652 @@
<script setup lang="ts">
import * as R from "ramda";
import { useConfigStore, useUserStore } from "@/store";
import { appUpdate } from "@/utils/update";
import TabbarHome from "@/pages/home/components/tabbar-home/tabbar-home.vue";
import TabbarShop from "@/pages/home/components/tabbar-shop/tabbar-shop.vue";
import TabbarOrder from "@/pages/home/components/tabbar-order/tabbar-order.vue";
import TabbarMine from "@/pages/home/components/tabbar-mine/tabbar-mine.vue";
import TabbarBrowse from "@/pages/home/components/tabbar-browse/tabbar-browse.vue";
import InviteUser from "@/pages/home/components/tabbar-mine/components/invite-user/invite-user.vue";
import CustomerService from "@/pages/home/components/tabbar-mine/components/customer-service/customer-service.vue";
import NotOpen from "@/pages/home/components/tabbar-home/components/not-open.vue";
import { useMessage } from "wot-design-uni";
import { debounce } from "throttle-debounce";
import Config from "@/config";
import { EventEnum } from "@/constant/enums";
import type UseCode from "@/components/use-code/use-code.vue";
import Score from "@/components/filtrate-tool/components/score.vue";
import PriceChoose from "@/components/filtrate-tool/components/price-choose.vue";
import useEventEmit from "@/hooks/useEventEmit";
import {
appAppointmentTimeQueryAppointmentTimePost,
appMessageUnreadCountPost,
} from "@/service";
import SwiperList from "@/pages/search/components/swiper-list/swiper-list.vue";
import { getDictFineList } from "@/pages-store/service";
import {getCouponReceiveListApi, receiveAllCouponApi} from "@/pages-user/service";
// useEventEmit(EventEnum.STAR_RATING_FILTER, ()=> {
// scoreRef.value.onOpen()
// })
// useEventEmit(EventEnum.PRICE_FILTER, ()=> {
// priceChooseRef.value.onOpen()
// })
const userStore = useUserStore();
const configStore = useConfigStore();
const message = useMessage();
const { t } = useI18n();
const tabbarHomeRef = ref<InstanceType<typeof TabbarHome>>();
const tabbarShopRef = ref<InstanceType<typeof TabbarShop>>();
const tabbarOrderRef = ref<InstanceType<typeof TabbarOrder>>();
const tabBarMineRef = ref<InstanceType<typeof TabbarMine>>();
const tabbarBrowseRef = ref<InstanceType<typeof TabbarBrowse>>();
const scoreRef = ref<InstanceType<typeof Score>>();
const priceChooseRef = ref<InstanceType<typeof PriceChoose>>();
const notOpenRef = ref<InstanceType<typeof NotOpen>>();
const showCoupon = ref(false);
const couponList = ref([]);
const inviteUserRef = ref<InstanceType<typeof InviteUser>>();
const useCodeRef = ref<InstanceType<typeof UseCode>>();
const customerServiceRef = ref<InstanceType<typeof CustomerService>>();
// 字体加载状态管理
const fontLoadingState = ref({
isLoading: false,
isLoaded: false,
loadedFonts: new Set(),
retryCount: 0,
maxRetries: 3,
});
// 接口调用状态管理
const apiLoadingState = ref({
isLoadingUserData: false,
lastLoadTime: 0,
debounceDelay: 1000,
});
const activeIndex = ref("home");
const tabBarList = ref([
{
iconPath: "/static/tabbar/home.png",
selectedIconPath: "/static/tabbar/homeHL.png",
text: t("tabBar.home"),
code: "home",
},
{
iconPath: "/static/tabbar/global.png",
selectedIconPath: "/static/tabbar/globalHL.png",
text: t("tabBar.global"),
code: "global",
},
{
iconPath: "/static/tabbar/browse.png",
selectedIconPath: "/static/tabbar/browseHL.png",
text: t("tabBar.browse"),
code: "browse",
},
{
iconPath: "/static/tabbar/order.png",
selectedIconPath: "/static/tabbar/orderHL.png",
text: t("tabBar.order"),
code: "order",
},
{
iconPath: "/static/tabbar/user.png",
selectedIconPath: "/static/tabbar/userHL.png",
text: t("tabBar.mine"),
code: "mine",
},
]);
function handleInviteUser() {
if (inviteUserRef.value) {
inviteUserRef.value.init();
}
}
function handleCustomerService() {
if (customerServiceRef.value) {
customerServiceRef.value.init();
}
}
// 组件初始化配置
const tabbarInitConfig = {
home: {
ref: tabbarHomeRef,
methods: ["init", "initData"], // init优先执行,快速显示已有内容
},
browse: {
ref: tabbarBrowseRef,
methods: ["init", "initData"],
},
global: {
ref: tabbarShopRef,
methods: [],
// methods: ['initData', 'init']
},
order: {
ref: tabbarOrderRef,
methods: ["initData"],
},
mine: {
ref: tabBarMineRef,
methods: ["init", "initData"],
},
};
// 统一的组件初始化函数
function initTabbarComponent(tabType: string) {
const config = tabbarInitConfig[tabType as keyof typeof tabbarInitConfig];
if (!config) {
console.warn(`未找到 ${tabType} 对应的初始化配置`);
return;
}
const componentRef = config.ref.value;
if (!componentRef) {
console.warn(`${tabType} 组件引用不存在`);
return;
}
// 对于home组件,优先执行init方法以快速显示内容
if (tabType === "home") {
// 立即执行init方法,无延迟
if (typeof componentRef.init === "function") {
componentRef.init();
}
// 然后执行initData方法
if (typeof componentRef.initData === "function") {
componentRef.initData();
}
} else {
// 其他组件按原有顺序执行
config.methods.forEach((method) => {
if (typeof componentRef[method] === "function") {
componentRef[method]();
} else {
console.warn(`${tabType} 组件不存在 ${method} 方法`);
}
});
}
}
function handleTabbarChange({ value }: { value: string }) {
initTabbarComponent(value);
}
const scoreRange = ref(""); // 星级评分
const price = ref(""); // 价格范围
function toggleScore() {
if (scoreRef.value) {
scoreRef.value.onOpen();
}
}
function applyScore(value: string) {
console.log("applyScore", value);
scoreRange.value = value;
tabbarHomeRef.value?.init();
}
function togglePrice() {
if (priceChooseRef.value) {
priceChooseRef.value.onOpen();
}
}
function applyPrice(value: string) {
console.log("applyPrice", value);
price.value = value;
nextTick(() => {
tabbarHomeRef.value?.init();
});
}
function toggleNotOpen() {
if (notOpenRef.value) {
notOpenRef.value.onOpen();
}
}
function mineChangeOrder() {
console.log("执行了");
activeIndex.value = "order";
handleTabbarChange({ value: "order" });
}
function handleClose() {
showCoupon.value = false;
}
function receiveAllCoupon() {
if (couponList.value.length === 0) {
return handleClose()
}
receiveAllCouponApi().then(res => {
if (res.code === 200) {
uni.showToast({
title: t('common.prompt.claimCouponSuccessfully'),
icon: 'none'
})
handleClose()
}
})
}
function getFontUrl(fontRelativeUrl: string): string {
let fontUrl = `url(/static/fonts/${fontRelativeUrl})`;
// #ifdef APP-PLUS
fontUrl = `url(${plus.io.convertLocalFileSystemURL(`_www/static/fonts/${fontRelativeUrl}`)})`;
// #endif
return fontUrl;
}
// 优化后的字体加载函数
function loadFonts() {
// 防止重复加载
if (fontLoadingState.value.isLoading || fontLoadingState.value.isLoaded) {
return Promise.resolve();
}
fontLoadingState.value.isLoading = true;
const fontConfigs = [
{ name: "UberMoveRegular", file: "UberMoveRegular.ttf", weight: "normal" },
{ name: "UberMoveMedium", file: "UberMoveMedium.otf", weight: "500" },
{ name: "UberMoveBold", file: "UberMoveBold.otf", weight: "700" },
];
const loadPromises = fontConfigs.map((config) => loadSingleFont(config));
return Promise.allSettled(loadPromises)
.then((results) => {
const successCount = results.filter(
(result) => result.status === "fulfilled"
).length;
const failedCount = results.length - successCount;
if (successCount > 0) {
fontLoadingState.value.isLoaded = true;
console.log(`字体加载完成: ${successCount}/${results.length} 成功`);
}
if (
failedCount > 0 &&
fontLoadingState.value.retryCount < fontLoadingState.value.maxRetries
) {
fontLoadingState.value.retryCount++;
console.log(
`字体加载失败 ${failedCount} 个,准备重试 (${fontLoadingState.value.retryCount}/${fontLoadingState.value.maxRetries})`
);
setTimeout(() => {
fontLoadingState.value.isLoading = false;
loadFonts();
}, 1000 * fontLoadingState.value.retryCount);
}
})
.finally(() => {
fontLoadingState.value.isLoading = false;
});
}
// 加载单个字体的函数
function loadSingleFont(config: {
name: string;
file: string;
weight: string;
}) {
return new Promise((resolve, reject) => {
// 检查是否已加载
if (fontLoadingState.value.loadedFonts.has(config.name)) {
resolve(config.name);
return;
}
const fontOptions: any = {
family: "UberMove",
source: getFontUrl(config.file),
success: () => {
fontLoadingState.value.loadedFonts.add(config.name);
console.log(`字体加载成功: ${config.name}`);
resolve(config.name);
},
fail: (error: any) => {
console.error(`字体加载失败: ${config.name}`, error);
reject(error);
},
};
if (config.weight !== "normal") {
fontOptions.desc = { weight: config.weight };
}
uni.loadFontFace(fontOptions);
});
}
useNetworkStatusChange(() => {
if (!userStore.location.location) {
userStore.getLocation();
}
nextTick(() => {
// tabbarHomeRef.value?.initData()
// tabbarShopRef.value?.initData()
// tabbarShopRef.value?.init()
// tabbarOrderRef.value?.initData()
// tabBarMineRef.value?.init()
});
});
onLoad(async () => {
uni.hideTabBar();
// 并行执行初始化任务
const initTasks = [loadFonts(), userStore.getLocation(), getInviteInfo()];
try {
await Promise.allSettled(initTasks);
console.log("页面初始化完成");
} catch (error) {
console.error("页面初始化失败:", error);
}
// #ifdef APP-PLUS
setTimeout(() => {
appUpdate();
}, 3000);
// #endif
});
onShow(() => {
if (configStore.tabbarIndex) {
activeIndex.value = configStore.tabbarIndex;
}
nextTick(() => {
handleTabbarChange({ value: activeIndex.value });
});
if (userStore.isLogin) {
loadUserDataWithDebounce();
// 获取用户信息
userStore.getUserInfo();
if(+userStore.userInfo.hasPop !== 2) {
getCouponReceiveListApi().then(res=> {
if(res.code === 200 && res.data?.length > 0) {
showCoupon.value = true;
couponList.value = res.data || [];
}
})
}
// getCouponReceiveListApi().then(res=> {
// showCoupon.value = true;
// couponList.value = res.data || [];
// })
}
});
// 防抖加载用户数据
const loadUserDataWithDebounce = debounce(1000, () => {
loadUserData();
});
// 合并的用户数据加载函数
async function loadUserData() {
if (apiLoadingState.value.isLoadingUserData) {
return;
}
const now = Date.now();
if (
now - apiLoadingState.value.lastLoadTime <
apiLoadingState.value.debounceDelay
) {
return;
}
apiLoadingState.value.isLoadingUserData = true;
apiLoadingState.value.lastLoadTime = now;
try {
// 并行执行多个接口调用
const promises = [getUnreadMessageCount(), userStore.getAppointmentTime()];
await Promise.allSettled(promises);
console.log("用户数据加载完成");
} catch (error) {
console.error("用户数据加载失败:", error);
} finally {
apiLoadingState.value.isLoadingUserData = false;
}
}
// 获取未读消息数量
function getUnreadMessageCount() {
return appMessageUnreadCountPost({})
.then((res) => {
userStore.unreadMessageCount = res.data || 0;
})
.catch((error) => {
console.error("获取未读消息数量失败:", error);
});
}
useEventEmit(EventEnum.CHOOSE_ADDRESS, (data) => {
console.log("设置用户的定位地址信息", data);
if (data) {
userStore.userSetLocation = {
location: data.displayName,
longitude: data.location.lng,
latitude: data.location.lat,
};
}
});
// 获取邀请信息配置
function getInviteInfo() {
if (userStore.isLogin) {
getDictFineList({
dictType: "invite_poster",
}).then((res) => {
console.log("获取邀请信息", res);
if (res.data.length > 0) {
res.data.map((item) => {
if (item.dictValue === "bg_image") {
Config.shareImage = item.dictLabel;
}
if (item.dictValue === "invite_url") {
Config.shareLink = item.dictLabel;
}
if (item.dictValue === "desc_text") {
Config.shareDesc = item.dictLabel;
}
if (item.dictValue === "weixinServiceUrl") {
Config.weixinServiceUrl = item.dictLabel;
}
});
}
});
// 获取支付相关配置
getDictFineList({
dictType: "app_config",
}).then((res) => {
res.data.map((item) => {
if (item.dictValue === "stripeKey") {
Config.stripeKey = item.remark;
}
});
});
}
}
</script>
<template>
<view class="page">
<tabbar-home
ref="tabbarHomeRef"
:scoreRange="scoreRange"
:price="price"
@toggleScore="toggleScore"
@togglePrice="togglePrice"
@toggleNotOpen="toggleNotOpen"
v-show="activeIndex === 'home'"
/>
<tabbar-shop ref="tabbarShopRef" v-show="activeIndex === 'global'" />
<tabbar-order ref="tabbarOrderRef" v-show="activeIndex === 'order'" />
<tabbar-browse
ref="tabbarBrowseRef"
@toggleNotOpen="toggleNotOpen"
v-show="activeIndex === 'browse'"
/>
<tabbar-mine
ref="tabBarMineRef"
v-show="activeIndex === 'mine'"
@inviteUser="handleInviteUser"
@customerService="handleCustomerService"
@changeOrder="mineChangeOrder"
/>
<wd-tabbar
custom-class="bg-#fff shadow-[0rpx_-6rpx_24rpx_rgba(0,0,0,0.1)]"
fixed
safeAreaInsetBottom
placeholder
v-model="activeIndex"
@change="handleTabbarChange"
activeColor="#333"
inactiveColor="#999"
>
<wd-tabbar-item
v-for="item in tabBarList"
:key="item.code"
:name="item.code"
:title="item.text"
>
<template #icon>
<view class="relative center">
<wd-img
class="absolute"
custom-class="animate-in fade-in-50 zoom-in-80"
height="46rpx"
width="46rpx"
v-show="activeIndex === item.code"
:src="item.selectedIconPath"
></wd-img>
<wd-img
class="absolute"
custom-class="animate-in fade-in-50"
height="46rpx"
width="46rpx"
:src="item.iconPath"
v-show="activeIndex !== item.code"
></wd-img>
</view>
</template>
</wd-tabbar-item>
</wd-tabbar>
</view>
<invite-user ref="inviteUserRef" />
<use-code ref="useCodeRef" />
<customer-service ref="customerServiceRef" />
<!-- 星级筛选弹窗 -->
<score @applyScore="applyScore" ref="scoreRef" />
<price-choose @applyPrice="applyPrice" ref="priceChooseRef" />
<not-open ref="notOpenRef" />
<!-- 优惠券弹窗 -->
<wd-popup
v-model="showCoupon"
position="center"
custom-style="background:transparent;overflow:visible;padding:0rpx;"
@close="handleClose"
>
<view class="relative w-640rpx h-874rpx">
<image
class="w-full h-874rpx absolute top-0 left-0"
src="/static/images/5005.png"
/>
<view
class="w-full h-874rpx absolute top-0 left-0"
>
<view
class="w-full h-250rpx px-74rpx pt-30rpx text-#6d2d00 text-36rpx font-700 leading-[1.35]"
>
<view class="">{{ t('pages-store.store.tips-1') }}</view>
<view class="mt-4rpx">{{ t('pages-store.store.tips-2') }}</view>
</view>
<view
class="w-full"
>
<view
class="text-center text-30rpx text-#b36f2c font-600 tracking-wide flex-center-sb px-70rpx"
>
<image
class="w-92rpx h-5rpx"
src="@img/5006.png"
/>
{{ t('pages-store.store.congratulations') }}
<image
class="w-92rpx h-5rpx rotate-180"
src="@img/5006.png"
/>
</view>
<scroll-view scroll-y class="mt-32rpx h-420rpx px-62rpx box-border">
<view
v-for="item in couponList"
:key="item.id"
class="relative mb-24rpx rounded-24rpx border-2rpx border-#f5c78c bg-#fff7ed px-32rpx h-136rpx shadow-[0_8rpx_20rpx_rgba(0,0,0,0.08)]"
>
<image
class="w-full h-full absolute top-0 left-0"
src="/static/images/5007.png"
/>
<view class="w-full px-32rpx py-10rpx box-border absolute top-0 left-0">
<view class="text-32rpx font-700 text-#c16a1c">
<!-- 1-折扣券, 2-满减券-->
<text v-if="+item.couponType === 1" class="">
{{ Number(item.discountValue * 100).toFixed(0) }}% {{ t('pages-store.store.couponOff') }}
</text>
<text v-if="+item.couponType === 2" class="">
${{ item.discountValue }} {{ t('pages-store.store.couponOff') }}
</text>
</view>
<view class="mt-4rpx text-24rpx text-#a0611d">
{{ t('pages-store.store.validDays') }}: {{ item.validDays }} {{ t('pages-store.store.days') }}</view>
<view class="mt-8rpx text-24rpx text-#a0611d">
{{ item?.merchantVo?.merchantName }}
</view>
</view>
</view>
<view v-if="couponList.length === 0" class="pt-60rpx center">
<image class="w-250rpx h-250rpx" src="@img/16033@2x.png"></image>
</view>
</scroll-view>
<view class="px-70rpx mt-12rpx">
<view
class="h-96rpx rounded-full bg-gradient-to-b from-[#7b3f1b] to-[#3f2210] text-#f7e5c6 text-32rpx font-700 center"
@click="receiveAllCoupon"
>
{{ t('pages-store.store.tips-3') }}
</view>
</view>
</view>
</view>
<view
class="absolute left-1/2 -bottom-120rpx -translate-x-1/2 w-88rpx h-88rpx center text-68rpx text-#fff"
@click="handleClose"
>
<view class="i-carbon:close-outline"></view>
</view>
</view>
</wd-popup>
<!-- 优惠券弹窗 -->
</template>
<style lang="scss"></style>