Files
lilishop-uniapp/pages/product/shopPage.vue
Yer11214 349ffa4f34 refactor: 重构多个组件以支持 Vue 3 语法和功能
- 将多个组件转换为 `<script setup>` 语法,提升可读性和性能
- 优化状态管理和事件处理逻辑,简化代码结构
- 更新样式和布局以适应新组件结构
- 添加新功能和修复已知问题,提升用户体验
2026-07-08 18:37:11 +08:00

877 lines
22 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view>
<!-- 楼层装修店铺信息 -->
<div>
<!-- 基础店铺返回 + 搜索楼层装修无顶部搜索时仅返回 -->
<u-navbar
v-if="!enablePageData || !floorHasTopSearch"
class="shop-navbar"
:border="false"
:fixed="true"
:placeholder="true"
left-icon=""
:auto-back="false"
>
<template #left>
<view class="shop-navbar-inner">
<view class="nav-back" @click.stop="back">
<u-icon name="arrow-left" size="20"></u-icon>
</view>
<view v-if="!enablePageData" class="search-wrap" @click.stop>
<u-search
v-model="keyword"
@search="goToSearch"
@click="goToSearch"
placeholder="请输入搜索"
:show-action="false"
></u-search>
</view>
</view>
</template>
</u-navbar>
<div class="wrapper">
<!-- 店铺信息模块 -->
<div class="store">
<view class="store-logo">
<u-image
border-radius="10"
width="100"
height="100"
:src="storeInfo.storeLogo || config.logo"
mode="aspectFit"
>
</u-image>
</view>
<view class="store-main">
<view class="store-top">
<view class="store-name" @click="getStoreLicencePhoto">
<text class="store-name-text">{{ storeInfo.storeName || "" }}</text>
</view>
<view class="collection">
<view class="collection-btn" @click.stop="whetherCollection">
{{ isCollection ? "已关注" : "+ 关注" }}
</view>
</view>
</view>
<view class="store-message">
<view class="store-stat">
<text class="stat-num">{{ storeInfo.collectionNum || 0 }}</text>
<text class="stat-label">关注</text>
</view>
<view class="store-stat">
<text class="stat-num">{{ storeInfo.goodsNum || 0 }}</text>
<text class="stat-label">件商品</text>
</view>
</view>
</view>
</div>
<!-- 店铺简介 -->
<div class="store-desc wes-2">
{{ storeInfo.storeDesc || '' }}
</div>
<!-- 联系客服 -->
<div class="kefu" @click="talk">
<u-icon name="kefu-ermai"></u-icon>
联系客服
</div>
</div>
<!-- 优惠券 -->
<scroll-view
scroll-x="true"
show-scrollbar="false"
class="discount"
v-if="couponList.length > 0"
>
<view class="card-box" v-for="(item, index) in couponList" :key="index">
<view class="card" @click="getCoupon(item)">
<view class="money">
<view>
<span v-if="item.couponType == 'DISCOUNT'"
>{{ item.couponDiscount }}</span
>
<span v-else>{{ item.price }}</span>
</view>
</view>
<view class="xian"></view>
<view class="text">
<text>{{ "领取优惠券" }}</text>
<text>{{unitPrice(item.consumeThreshold) }}元可用</text>
</view>
</view>
</view>
</scroll-view>
<!-- 基础店铺模式 -->
<div v-if="basePageData">
<u-tabs
:list="tabs"
:lineColor="mainColor"
:activeStyle="{ color: mainColor }"
:scrollable="false"
v-model:current="current"
></u-tabs>
<div class="content" v-if="current == 0">
<u-empty style='margin-top:100rpx' v-if="goodsList.length == 0" class="empty" text='暂无商品信息'></u-empty>
<goodsTemplate style="width:100%;" v-else :res="goodsList" :storeName="false" />
</div>
<!-- 全部分类 -->
<div class="category" v-if="current == 1">
<div class="category-item" v-for="(item,index) in categoryList" :key="index">
<div class="flex" @click="getCategoryGoodsList(item)">
<div>{{item.labelName}}</div>
<div>
<u-icon color="#999" name="arrow-right"></u-icon>
</div>
</div>
<!-- 分类子级 -->
<div class="child-list" v-if="item.children && item.children.length!=0">
<div class="child" @click="getCategoryGoodsList(child)" :key='i' v-for="(child,i) in item.children">{{child.labelName}}
</div>
</div>
</div>
</div>
</div>
<!-- 楼层装修模式 -->
<view v-if="enablePageData" class="floor-page">
<!-- uni 中不能使用 vue component 所以用if判断每个组件 -->
<view
v-for="(item, index) in pageData.list || []"
:key="`${item.type}-${index}`"
class="floor-block"
>
<!-- 搜索栏如果在楼层装修顶部则会自动浮动否则不浮动 -->
<u-navbar
class="navbar shop-navbar"
v-if="item.type == 'search'"
:fixed="index === 1 ? false : true"
:placeholder="index === 1 ? false : true"
left-icon=""
:auto-back="false"
>
<template #left>
<view class="shop-navbar-inner">
<view class="nav-back" @click.stop="back">
<u-icon name="arrow-left" size="20"></u-icon>
</view>
<view class="floor-search-wrap" @click.stop>
<search style="width: 100%" :res="item.options" :storeId="storeId" />
</view>
</view>
</template>
</u-navbar>
<carousel v-if="item.type == 'carousel'" :res="item.options" />
<titleLayout v-if="item.type == 'title'" :res="item.options" />
<leftOneRightTwo
v-if="item.type == 'leftOneRightTwo'"
:res="item.options"
/>
<leftTwoRightOne
v-if="item.type == 'leftTwoRightOne'"
:res="item.options"
/>
<topOneBottomTwo
v-if="item.type == 'topOneBottomTwo'"
:res="item.options"
/>
<topTwoBottomOne
v-if="item.type == 'topTwoBottomOne'"
:res="item.options"
/>
<flexThree v-if="item.type == 'flexThree'" :res="item.options" />
<flexFive v-if="item.type == 'flexFive'" :res="item.options" />
<flexFour v-if="item.type == 'flexFour'" :res="item.options" />
<flexTwo v-if="item.type == 'flexTwo'" :res="item.options" />
<textPicture v-if="item.type == 'textPicture'" :res="item.options" />
<menuLayout v-if="item.type == 'menu'" :res="item.options" />
<flexOne v-if="item.type == 'flexOne'" :res="item.options" />
<GoodsFloor v-if="item.type == 'goods'" :res="item.options" />
<group v-if="item.type == 'group'" :res="item.options" />
<!-- <joinGroup v-if="item.type == 'joinGroup'" :res="item.options" /> -->
<!-- <integral v-if="item.type == 'integral'" :res="item.options" /> -->
<!-- <spike v-if="item.type == 'spike'" :res="item.options" /> -->
</view>
</view>
<u-no-network></u-no-network>
</div>
</view>
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch, onMounted } from 'vue'
import {
onLoad,
onPageScroll,
onPullDownRefresh,
onReachBottom,
} from '@dcloudio/uni-app'
import { useStore } from '@/store'
import carousel from '@/pages/tabbar/home/template/tpl_banner.vue'
import titleLayout from '@/pages/tabbar/home/template/tpl_title.vue'
import leftOneRightTwo from '@/pages/tabbar/home/template/tpl_left_one_right_two.vue'
import leftTwoRightOne from '@/pages/tabbar/home/template/tpl_left_two_right_one.vue'
import topOneBottomTwo from '@/pages/tabbar/home/template/tpl_top_one_bottom_two.vue'
import topTwoBottomOne from '@/pages/tabbar/home/template/tpl_top_two_bottom_one.vue'
import flexOne from '@/pages/tabbar/home/template/tpl_flex_one.vue'
import flexTwo from '@/pages/tabbar/home/template/tpl_flex_two.vue'
import flexThree from '@/pages/tabbar/home/template/tpl_flex_three.vue'
import flexFive from '@/pages/tabbar/home/template/tpl_flex_five.vue'
import flexFour from '@/pages/tabbar/home/template/tpl_flex_four.vue'
import textPicture from '@/pages/tabbar/home/template/tpl_text_picture.vue'
import menuLayout from '@/pages/tabbar/home/template/tpl_menu.vue'
import search from '@/pages/tabbar/home/template/tpl_search.vue'
import group from '@/pages/tabbar/home/template/tpl_group.vue'
import GoodsFloor from '@/pages/tabbar/home/template/tpl_goods.vue'
import goodsTemplate from '@/components/m-goods-list/list.vue'
import { getStoreBaseInfo, getStoreCategory } from '@/api/store.js'
import {
receiveCoupons,
deleteStoreCollection,
collectionStore,
getStoreIsCollect,
} from '@/api/members.js'
import config from '@/config/config'
import { getGoodsList } from '@/api/goods.js'
import { getAllCoupons } from '@/api/promotions.js'
import { getFloorStoreData } from '@/api/home'
import {
isLogin,
navigateToLogin,
talkIm,
unitPrice,
} from '@/utils/filters.js'
import storage from '@/utils/storage.js'
const store = useStore()
const mainColor = computed(() => store.getters.mainColor)
const pageData = ref<{ list: any[] }>({ list: [] })
const enablePageData = ref(false)
const basePageData = ref(false)
const scrollTop = ref(0)
const current = ref(0)
const tabs = [
{ name: '全部商品' },
{ name: '分类查看' },
]
const storeId = ref('')
const keyword = ref('')
const storeInfo = ref<any>({})
const isCollection = ref(false)
const goodsList = ref<any[]>([])
const couponList = ref<any[]>([])
const categoryList = ref<any[]>([])
const couponParams = reactive({
pageNumber: 1,
pageSize: 50,
storeId: '',
})
const goodsParams = reactive({
pageNumber: 1,
pageSize: 10,
storeId: '',
})
const floorHasTopSearch = computed(() => {
const list = pageData.value && pageData.value.list
if (!list || !list.length) return false
return list.some((item, index) => item.type === 'search' && index !== 1)
})
watch(current, (val) => {
if (val == 0) {
goodsList.value = []
getGoodsData()
} else {
getCategoryData()
}
})
function talk() {
talkIm(storeInfo.value.storeId)
}
function back() {
const pages = getCurrentPages()
if (pages.length > 1) {
uni.navigateBack({ delta: 1 })
} else {
uni.switchTab({ url: '/pages/tabbar/home/index' })
}
}
function isPageDataSuccess(payload: any) {
return payload?.success === true || payload?.code === 200
}
function shouldUseFloorPage(pageShow: unknown) {
return pageShow === '1' || pageShow === 1 || pageShow === true
}
function initPageData() {
getFloorStoreData({
pageType: 'STORE',
num: storeId.value,
})
.then((res) => {
const payload = res?.data ?? res
const result = payload?.result
const pageDataStr = result?.pageData
if (!isPageDataSuccess(payload) || !pageDataStr) {
fallbackToBasePage()
return
}
try {
const parsed = JSON.parse(pageDataStr)
pageData.value =
parsed && Array.isArray(parsed.list) ? parsed : { list: [] }
enablePageData.value = pageData.value.list.length > 0
if (!enablePageData.value) {
fallbackToBasePage()
}
} catch (e) {
fallbackToBasePage()
}
})
.catch(() => {
fallbackToBasePage()
})
}
function fallbackToBasePage() {
enablePageData.value = false
basePageData.value = true
pageData.value = { list: [] }
getGoodsData()
getCategoryData()
}
function getStoreLicencePhoto() {
uni.navigateTo({
url: `/pages/product/licencePhoto?id=${storeId.value}`,
})
}
function init() {
goodsList.value = []
categoryList.value = []
couponList.value = []
goodsParams.pageNumber = 1
if (isLogin('auth')) {
enableGoodsIsCollect()
}
getStoreData()
}
function linkKefuDetail() {
// 客服
// #ifdef MP-WEIXIN
const params = {
// originalPrice: goodsDetail.original || goodsDetail.price,
uuid: storage.getUuid(),
token: storage.getAccessToken(),
sign: storeInfo.value.yzfSign,
mpSign: storeInfo.value.yzfMpSign,
}
uni.navigateTo({
url: '/pages/mine/im/index',
})
// uni.navigateTo({
// url:
// "/pages/product/customerservice/index?params=" +
// encodeURIComponent(JSON.stringify(params)),
// });
// // #endif
// // #ifndef MP-WEIXIN
// const sign = storeInfo.yzfSign;
// uni.navigateTo({
// url:
// "/pages/tabbar/home/web-view?src=https://yzf.qq.com/xv/web/static/chat/index.html?sign=" +
// sign,
// });
// #endif
}
async function getCategoryData() {
const res = await getStoreCategory(storeId.value)
if (res.data.success) {
categoryList.value = res.data.result
}
}
async function enableGoodsIsCollect() {
const res = await getStoreIsCollect('STORE', storeId.value)
if (res.data.success) {
isCollection.value = res.data.result
}
}
function getCategoryGoodsList(val: any) {
uni.navigateTo({
url: `/pages/product/shopPageGoods?title=${val.labelName}&id=${val.id}&storeId=${storeId.value}`,
})
}
function goToSearch() {
uni.navigateTo({
url: `/pages/navigation/search/searchPage?storeId=${storeId.value}&keyword=${keyword.value}`,
})
}
async function getStoreData() {
const res = await getStoreBaseInfo(storeId.value)
if (res.data.success) {
storeInfo.value = res.data.result
getCouponsData()
if (shouldUseFloorPage(res.data.result.pageShow)) {
initPageData()
} else {
getGoodsData()
getCategoryData()
basePageData.value = true
}
} else {
uni.reLaunch({
url: '/',
})
}
}
async function getGoodsData() {
const res = await getGoodsList(goodsParams)
if (res.data.success) {
goodsList.value.push(...res.data.result.records)
}
}
async function getCouponsData() {
couponParams.storeId = storeId.value
const res = await getAllCoupons(couponParams)
if (res.data.success) {
couponList.value.push(...res.data.result.records)
}
}
function whetherCollection() {
if (isCollection.value) {
deleteStoreCollection(storeId.value).then((res) => {
if (res.data.success) {
isCollection.value = false
uni.showToast({
icon: 'none',
duration: 3000,
title: '取消关注成功!',
})
}
})
} else {
collectionStore(storeId.value).then((res) => {
if (res.data.success) {
isCollection.value = true
uni.showToast({
icon: 'none',
duration: 3000,
title: '关注成功!',
})
}
})
}
}
function getCoupon(item: any) {
if (!isLogin('auth')) {
uni.showToast({
icon: 'none',
duration: 3000,
title: '请先登录!',
})
navigateToLogin('redirectTo')
return false
}
receiveCoupons(item.id).then((res) => {
if (res.data.success) {
uni.showToast({
icon: 'none',
duration: 3000,
title: '领取成功!',
})
}
})
}
onLoad((options: any) => {
storeId.value = options.id
console.log(storeId.value, 'this.storeId')
goodsParams.storeId = options.id
couponParams.storeId = options.id
})
onPageScroll((e) => {
scrollTop.value = e.scrollTop
})
onPullDownRefresh(() => {
init()
})
onReachBottom(() => {
goodsParams.pageNumber++
getGoodsData()
})
onMounted(() => {
// #ifdef MP-WEIXIN
uni.showShareMenu({
withShareTicket: true,
})
// #endif
init()
})
</script>
<style lang="scss" scoped>
.wrapper {
background: #fff;
padding: 32rpx;
}
.store {
display: flex;
align-items: center;
.store-logo {
flex-shrink: 0;
width: 100rpx;
height: 100rpx;
overflow: hidden;
:deep(.u-image),
:deep(.u-image__image) {
width: 100rpx !important;
height: 100rpx !important;
}
}
.store-main {
flex: 1;
min-width: 0;
margin-left: 20rpx;
}
.store-top {
position: relative;
display: flex;
align-items: center;
min-height: 56rpx;
}
.store-name {
width: 100%;
font-size: 34rpx;
color: #333;
letter-spacing: 1rpx;
font-weight: bold;
text-align: center;
}
.store-name-text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: block;
padding: 0 150rpx;
box-sizing: border-box;
}
.store-message {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: nowrap;
margin-top: 12rpx;
font-size: 24rpx;
color: #999;
}
.store-stat {
display: flex;
align-items: baseline;
white-space: nowrap;
margin-right: 24rpx;
font-size: 26rpx;
color: #999;
}
.stat-num {
font-size: 26rpx;
font-weight: bold;
color: #333;
margin-right: 4rpx;
}
.stat-label {
font-size: 26rpx;
color: #999;
}
}
.collection {
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
flex-shrink: 0;
}
.collection-btn {
background: $main-color;
padding: 8rpx 0;
width: 140rpx;
font-size: 24rpx;
color: #fff;
border-radius: 100px;
text-align: center;
line-height: 1.2;
}
.store-desc {
margin: 40rpx 0 0 0;
color: #999;
}
.content {
margin-top: 20rpx;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
> .empty {
width: 100%;
display: flex;
justify-content: center;
margin-top: 40rpx;
}
.item {
overflow: hidden;
background: #fff;
width: 49%;
height: 484rpx;
font-size: 26rpx;
display: flex;
flex-direction: column;
border: 1px solid #f8f8f8;
margin-bottom: 20rpx;
.name {
text-align: left !important;
color: #333;
padding: 0 20rpx;
margin-top: 20rpx;
height: 80rpx;
overflow: hidden;
}
.price {
font-weight: 500;
color: $main-color;
font-size: 30rpx;
padding: 0 20rpx;
margin-top: 20rpx;
white-space: nowrap;
}
.buyCount {
display: flex;
padding: 0 20rpx;
font-size: 24upx;
justify-content: space-between;
color: #999;
}
}
}
.discount {
height: 154rpx;
border-top: 1px solid #f6f6f6;
border-bottom: 18rpx solid #f6f6f6;
background: #f6f6f6;
overflow: hidden;
white-space: nowrap;
.card-box {
display: inline-block;
padding-top: 25rpx;
}
.card {
width: 324rpx;
height: 116rpx;
background: #fff;
margin-left: 20rpx;
border-radius: 5px;
display: flex;
align-items: center;
.money {
width: 45%;
color: #fd6466;
font-weight: 500;
text-align: center;
text {
font-size: 50rpx;
}
}
.xian {
height: 66rpx;
border: 1px dashed #f6f6f6;
position: relative;
&:before,
&:after {
content: "";
width: 22rpx;
height: 12rpx;
position: absolute;
background: #f6f6f6;
}
&:before {
border-radius: 0 0 22rpx 22rpx;
top: -30rpx;
left: -10rpx;
}
&:after {
border-radius: 22rpx 22rpx 0 0;
bottom: -30rpx;
left: -10rpx;
}
}
.text {
flex: 1;
color: $aider-light-color;
font-size: 24rpx;
display: flex;
flex-direction: column;
margin-left: 14rpx;
text:nth-of-type(2) {
color: #ccc;
}
.cur:nth-of-type(1) {
color: #ccc;
}
}
}
}
.category-item {
background: #fff;
padding: 22rpx;
margin: 20rpx 10rpx;
> .flex {
color: #666;
justify-content: space-between;
}
> .child-list {
display: flex;
margin: 20rpx 0;
flex-wrap: wrap;
> .child {
justify-content: center;
margin: 1% 0;
display: flex;
width: 48%;
font-size: 24rpx;
color: #999;
margin-right: 1%;
border: 1rpx solid #ededed;
box-sizing: border-box;
height: 70rpx;
text-align: center;
line-height: 70rpx;
}
}
}
.kefu {
background: #f7f7f7;
height: 70rpx;
display: flex;
align-items: center;
justify-content: center;
margin-top: 32rpx;
border-radius: 10rpx;
font-size: 24rpx;
color: #999;
}
.shop-navbar {
:deep(.u-navbar__content__left) {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 2;
width: auto !important;
padding: 0 24rpx 0 8rpx !important;
box-sizing: border-box;
}
}
.shop-navbar-inner {
display: flex;
align-items: center;
width: 100%;
height: 100%;
box-sizing: border-box;
}
.search-wrap {
flex: 1;
min-width: 0;
box-sizing: border-box;
}
.floor-search-wrap {
flex: 1;
min-width: 0;
box-sizing: border-box;
}
.nav-back {
display: flex;
align-items: center;
justify-content: center;
width: 64rpx;
height: 100%;
flex-shrink: 0;
}
.floor-page {
width: 100%;
background: #f9f9f9;
}
.floor-block {
width: 100%;
display: block;
}
</style>