商城分包相关内容

This commit is contained in:
陈善美 2025-08-28 11:58:40 +08:00
parent 65c88c24eb
commit 6bf2a1ada9
16 changed files with 3579 additions and 0 deletions

View File

@ -378,6 +378,7 @@
}
}]
},
//
{
"root": "pageCompany",
"pages": [{
@ -390,6 +391,7 @@
}
}]
},
//
{
"root": "pagesCustomer",
"pages": [{
@ -399,6 +401,77 @@
"navigationStyle": "custom"
}
}]
},
//
{
"root": "pagesShop",
"pages": [{
"path": "pages/shopMall/index",
"style": {
"navigationBarTitleText": "大贸商城",
// #ifdef WEB
"navigationStyle": "default"
// #endif
}
},
{
"path": "pages/shopGoods/goods/goods",
"style": {
"navigationBarTitleText": "商品详情",
// #ifdef WEB
"navigationStyle": "default"
// #endif
}
},
{
"path": "pages/shopGoods/list/list",
"style": {
"navigationBarTitleText": "商品列表",
// #ifdef WEB
"navigationStyle": "default"
// #endif
}
},
{
"path": "pages/shopGoods/evaluate/evaluate",
"style": {
"navigationBarTitleText": "商品评价",
// #ifdef WEB
"navigationStyle": "default"
// #endif
}
},
{
"path": "pages/shopGoods/search/search",
"style": {
"navigationBarTitleText": "搜索商品",
// #ifdef WEB
"navigationStyle": "default"
// #endif
}
},
//
{
"path": "pages/shopCart/cart/cart",
"style": {
"navigationBarTitleText": "购物车",
// #ifdef WEB
"navigationStyle": "default"
// #endif
}
},
//
{
"path": "pages/shopCategory/category/category",
"style": {
"navigationBarTitleText": "分类",
// #ifdef WEB
"navigationStyle": "default"
// #endif
}
}
]
}
],
"preloadRule": {

View File

@ -0,0 +1,687 @@
<script setup lang="ts">
import { useCartStore, useMemberStore, useRefreshStore } from '@/stores'
import { nextTick, ref, watch } from 'vue'
import type { cartItem } from '@/types/cart'
import { deleteCartApi, getCartApi, putCartApi, putCartSelectedAllApi } from '@/api/cart'
import type { InputNumberBoxEvent } from '@/components/vk-data-input-number-box/vk-data-input-number-box'
import { computed } from 'vue'
import { debounce } from 'lodash'
import { executeWithLoading, fullUrl, onShowRefreshData } from '@/utils/common'
// zpaging使使
import useZPagingComp from '@/uni_modules/z-paging/components/z-paging/js/hooks/useZPagingComp.js'
import { pageUrl, safeBottom } from '@/utils/constants'
import { onLoad, onShow } from '@dcloudio/uni-app'
const memberStore = useMemberStore()
const cartStore = useCartStore()
const paging = ref()
// 使useZPagingComp
defineExpose({ ...useZPagingComp(paging) })
const props = defineProps<{
type?: string
}>()
//
const toolbarSafeAreaInsets = computed(() => {
if (props.type === 'tabBar') return 0
return safeBottom
})
//
const cartList = ref<cartItem[]>()
const queryList = (page: number, page_size: number) => {
getCartApi({
page,
page_size,
}).then((res) => {
paging.value.complete(res.result.data)
cartStore.setCartTotalNum(res.result.total_num)
})
}
//
const onDeleteCart = (id: string[] | number[]) => {
const content = id.length > 1 ? '确定要删除选中的商品吗' : '确定要删除该商品吗'
uni.showModal({
title: '提示',
content: content,
showCancel: true,
success: async ({ confirm, cancel }) => {
if (confirm) {
await deleteCartApi({
id,
})
uni.showToast({
title: '删除成功',
icon: 'success',
mask: true,
})
setTimeout(() => {
paging.value.reload()
}, 1000)
}
},
})
}
//
const onDeleteCartBatch = () => {
if (!selectedList.value || selectedList.value.length === 0) {
return uni.showToast({
icon: 'none',
title: '请选择要删除的商品',
})
}
onDeleteCart(selectedList.value.map((item) => item.id))
}
//
const removeInvalidCart = () => {
if (!cartList.value || cartList.value.length === 0) return
const invalidList = cartList.value.filter((item) => item.disabled)
if (invalidList.length === 0) {
return uni.showToast({
icon: 'none',
title: '购物车内商品都是有效的噢,快去结算吧~',
})
}
uni.showModal({
title: '提示',
content: '是否快速清理无效商品',
showCancel: true,
success: ({ confirm, cancel }) => {
if (confirm) {
uni.showLoading({
title: '快速清理中…',
mask: true,
})
setTimeout(() => {
deleteCartApi({
id: invalidList.map((item) => item.id),
})
uni.showToast({
title: '清理完成',
icon: 'success',
mask: true,
})
uni.hideLoading()
paging.value.reload()
}, 1500)
}
},
})
}
//
const onChangeCart = debounce((event: InputNumberBoxEvent) => {
putCartApi({
id: Number(event.index),
num: event.value,
})
}, 500)
//
const onChangeSelected = debounce((item: cartItem) => {
if (item.disabled) return
//
item.selected = !item.selected
putCartApi({
id: item.id,
selected: item.selected,
})
}, 500)
//
const onChangeSelectedAll = debounce(() => {
if (!cartList.value || cartList.value.length === 0) return
if (!effectiveCartList.value || effectiveCartList.value.length === 0) return
//
const isSelectedAllOld = isSelectedAll.value
//
cartList.value?.forEach((item) => {
item.selected = !isSelectedAllOld
})
//
putCartSelectedAllApi({
selected: !isSelectedAllOld,
})
}, 500)
//
const selectedList = computed(() => {
return cartList.value?.filter((item) => item.selected && !item.disabled)
})
//
const effectiveCartList = computed(() => {
return cartList.value?.filter((item) => !item.disabled)
})
//
const isSelectedAll = computed(() => {
if (!selectedList.value || !cartList.value || selectedList.value.length <= 0) {
return false
}
return selectedList.value.length === effectiveCartList.value?.length
})
//
const selectedCount = computed(() => {
if (!selectedList.value) return 0
const count = selectedList.value?.reduce((sum, item) => (sum += item.num), 0)
return count > 99 ? '99+' : count
})
//
const selectedPrice = computed(() => {
return selectedList.value?.reduce((sum, item) => (sum += item.sku.price * item.num), 0).toFixed(2)
})
//
const gotoPayment = () => {
if (selectedCount.value === 0) {
return uni.showToast({
icon: 'none',
title: '请选择结算商品',
})
}
uni.navigateTo({ url: `${pageUrl['order-create']}` })
}
const formatPrice = (price: string | number) => {
let [integer, decimal] = price.toString().split('.')
return {
integer,
decimal,
}
}
//
const goToIndex = (event: any) => {
event(false)
uni.redirectTo({
url: `${pageUrl['shopping-mall']}`,
})
}
const totalNum = computed(() => {
return cartList.value?.reduce((sum, item) => (sum += item.num), 0) || 0
})
onShow(() => {
if (memberStore.profile) {
nextTick(() => {
paging.value?.refresh()
})
}
})
watch(
() => totalNum.value,
(newVal) => {
cartStore.setCartTotalNum(newVal)
//
cartStore.setCartTabBadge()
},
{ immediate: true, deep: true },
)
</script>
<template>
<template v-if="memberStore.profile">
<view class="cart-list">
<z-paging
ref="paging"
v-model="cartList"
:auto="false"
@query="queryList"
empty-view-text="购物车空空如也,快去逛逛看吧~"
show-empty-view-reload
empty-view-reload-text="去逛逛"
@emptyViewReload="goToIndex"
:paging-style="{
backgroundColor: '#f7f7f8',
}"
:empty-view-reload-style="{
backgroundColor: '#ff5555',
color: 'white',
borderRadius: '10px',
padding: '10px 20px',
}"
>
<view class="cart-operate" v-if="cartList && cartList.length > 0">
<text class="operate-item icon icon-delete" @tap="onDeleteCartBatch">批量删除</text>
<text class="operate-item icon icon-clear" @tap="removeInvalidCart">快速清理</text>
</view>
<view class="cart-list-item">
<uni-swipe-action>
<uni-swipe-action-item
v-for="item in cartList"
:key="item.sku_id"
class="cart-swipe"
:class="{ disabled: item.disabled }"
>
<view class="goods">
<text v-if="item.disabled" class="disabled">{{ item.disabled_text }}</text>
<text
v-else
class="checkbox"
:class="{ checked: item.selected }"
@tap="onChangeSelected(item)"
></text>
<navigator
:url="`${pageUrl['shopping-mall-goods-detail']}?id=${item.goods_id}`"
hover-class="none"
class="navigator"
>
<image mode="aspectFill" class="picture" :src="fullUrl(item.sku.image)"></image>
<view class="meta">
<view class="name ellipsis">{{ item.goods.name }}</view>
<view class="attrsText ellipsis">{{ item.sku.spec_text }}</view>
<view class="price">
<span class="integer">{{ formatPrice(item.sku.price).integer }}</span>
<span class="decimal">.{{ formatPrice(item.sku.price).decimal }}</span>
</view>
<view
class="price-diff"
v-if="item.old_price && item.old_price - item.sku.price > 0"
>
<text>加购后降</text>
<text class="diff-amount">
{{ (item.old_price - item.sku.price).toFixed(2) }}
</text>
</view>
</view>
</navigator>
<view class="num" v-if="!item.disabled">
<vk-data-input-number-box
v-model="item.num"
:index="item.id"
:min="1"
:max="item.sku.stock"
@plus="onChangeCart($event)"
@minus="onChangeCart($event)"
@blur="onChangeCart($event)"
/>
</view>
</view>
<template #right>
<view class="cart-swipe-right">
<button class="button delete-button" @tap="onDeleteCart([item.id])">删除</button>
</view>
</template>
</uni-swipe-action-item>
</uni-swipe-action>
</view>
<template #bottom>
<view class="toolbar" :style="{ paddingBottom: toolbarSafeAreaInsets + 'px' }">
<text class="all" :class="{ checked: isSelectedAll }" @tap="onChangeSelectedAll"
>全选</text
>
<view class="total-price">
<text class="text">总计:</text>
<text class="amount">{{ selectedPrice ?? '0.00' }}</text>
</view>
<view class="button-grounp">
<view
class="button payment-button"
:class="{ disabled: selectedCount === 0 }"
@tap="gotoPayment"
>
去结算({{ selectedCount }})
</view>
</view>
</view>
<view class="toolbar-height"></view>
</template>
</z-paging>
</view>
</template>
<!-- 未登录: 提示登录 -->
<view class="login-blank" v-else>
<text class="text">登录后可查看购物车中的商品</text>
<navigator :url="pageUrl['login']" hover-class="none">
<button class="button">去登录</button>
</navigator>
</view>
</template>
<style lang="scss">
//
:host {
height: 100%;
display: flex;
flex-direction: column;
background-color: #f7f7f8;
}
.cart-operate {
padding-top: 20rpx;
padding-left: 20rpx;
display: flex;
justify-content: flex-end;
.operate-item {
padding-right: 20rpx;
font-size: 28rpx;
}
}
.cart-list {
.tips {
// display: flex;
align-items: center;
line-height: 1;
margin: 30rpx 10rpx;
font-size: 26rpx;
color: #666;
.label {
color: #fff;
padding: 7rpx 15rpx 5rpx;
border-radius: 4rpx;
font-size: 24rpx;
background-color: #ff5f3c;
margin-right: 10rpx;
}
}
.cart-list-item {
padding: 20rpx;
}
.cart-swipe.disabled {
opacity: 0.5;
filter: grayscale(100%);
pointer-events: none;
}
//
.goods {
display: flex;
padding: 3% 5% 3% 8%;
// border-radius: 10rpx;
background-color: #fff;
position: relative;
border-bottom: 1rpx solid #ededed;
.navigator {
display: flex;
}
.disabled {
position: absolute;
top: 40%;
left: 1%;
width: 50rpx;
font-size: 18rpx;
text-align: center;
background-color: #000000;
color: white;
padding: 3px;
border-radius: 10px;
}
.checkbox {
position: absolute;
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
width: 50rpx;
height: 100%;
&::before {
content: '\e6cd';
font-family: 'iconfont' !important;
font-size: 40rpx;
}
&.checked::before {
content: '\e6cc';
color: #ff5f3c;
}
}
.picture {
width: 180rpx;
height: 180rpx;
}
.meta {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-around;
margin-left: 20rpx;
}
.name {
font-size: 26rpx;
color: #444;
align-items: center;
}
.attrsText {
line-height: 1.8;
margin-top: 10rpx;
padding: 0 15rpx;
font-size: 24rpx;
align-self: flex-start;
border-radius: 4rpx;
color: #888;
background-color: #f7f7f8;
}
.price {
line-height: 2;
color: #e33333;
margin-bottom: 2rpx;
display: flex;
align-items: baseline;
&::before {
content: '¥';
font-size: 20rpx;
vertical-align: sub;
}
.integer {
font-size: 30rpx;
}
.decimal {
font-size: 20rpx;
}
}
.price-diff {
line-height: 1;
color: orange; //
font-size: 20rpx;
}
//
.num {
position: absolute;
bottom: 25rpx;
right: 10rpx;
.text {
height: 100%;
padding: 0 20rpx;
font-size: 32rpx;
color: #444;
}
.input {
height: 100%;
text-align: center;
border-radius: 4rpx;
font-size: 24rpx;
color: #444;
background-color: #f6f6f6;
}
}
}
.cart-swipe {
display: block;
margin: 20rpx 0;
}
.cart-swipe-right {
display: flex;
height: 100%;
.button {
display: flex;
justify-content: center;
align-items: center;
width: 50px;
padding: 6px;
line-height: 1.5;
color: #fff;
font-size: 26rpx;
border-radius: 0;
}
.delete-button {
background-color: #cf4444;
}
}
}
//
.cart-blank,
.login-blank {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
height: 60vh;
.image {
width: 400rpx;
height: 281rpx;
}
.text {
color: #444;
font-size: 26rpx;
margin: 20rpx 0;
}
.button {
width: 240rpx !important;
height: 60rpx;
line-height: 60rpx;
margin-top: 20rpx;
font-size: 26rpx;
border-radius: 60rpx;
color: #fff;
background-color: #ff5f3c;
}
}
//
.toolbar {
position: fixed;
left: 0;
right: 0;
bottom: var(--window-bottom);
z-index: 100;
height: 100rpx;
// padding: 0 20rpx;
display: flex;
align-items: center;
border-top: 1rpx solid #ededed;
border-bottom: 1rpx solid #ededed;
background-color: #fff;
box-sizing: content-box;
.all {
margin-left: 15rpx;
font-size: 15px;
color: #444;
display: flex;
align-items: center;
}
.all::before {
font-family: 'iconfont' !important;
content: '\e6cd';
font-size: 40rpx;
margin-right: 8rpx;
}
.checked::before {
content: '\e6cc';
color: #ff5f3c;
}
.text {
margin-right: 8rpx;
margin-left: 12rpx;
color: #444;
font-size: 14px;
}
.amount {
font-size: 20px;
color: #cf4444;
.decimal {
font-size: 12px;
}
&::before {
content: '¥';
font-size: 12px;
}
}
.button-grounp {
// margin-left: auto;
display: flex;
justify-content: space-between;
text-align: center;
line-height: 72rpx;
font-size: 13px;
color: #fff;
.button {
width: 240rpx;
margin: 0 10rpx;
border-radius: 72rpx;
}
.payment-button {
background-color: #ff5f3c;
&.disabled {
opacity: 0.6;
}
}
}
.total-price {
//
margin-left: auto;
}
}
//
.toolbar-height {
height: 100rpx;
}
</style>

View File

@ -0,0 +1,378 @@
<script setup lang="ts">
import { getCategoryApi, getClassifyDetailApi } from '@/api/catogory'
import type { categoryTopItem } from '@/types/category'
import { onLoad, onShow } from '@dcloudio/uni-app'
import { computed, nextTick, watch } from 'vue'
import { ref } from 'vue'
import { fullUrl } from '@/utils/common'
import { pageUrl } from '@/utils/constants'
//tab
const activeIndex = ref(0)
//
const categoryList = ref<categoryTopItem[]>([])
//
const fetchTopCategory = async () => {
const res = await getCategoryApi()
categoryList.value = res.result
}
//
const onChangeCategory = (index: number) => {
activeIndex.value = index
//
secondaryTabIndex.value = 0
uni.setStorageSync('activeCategoryIndex', index)
}
//ref
const allSecondaryRef = ref()
const showAllSecondary = () => {
allSecondaryRef.value.open()
}
const hiddenAllSecondary = () => {
allSecondaryRef.value.close()
}
//tab
const secondaryTabIndex = ref(0)
const onChangeSecondary = (index: number) => {
secondaryTabIndex.value = index
}
const selectSecondary = (index: number) => {
if (categoryList.value[activeIndex.value].children.length <= 0) return
secondaryTabIndex.value = index
allSecondaryRef.value.close()
}
//
const paging = ref()
const secondaryData = ref()
const querySecondary = (page: number, page_size: number) => {
if (categoryList.value.length <= 0) return
if (categoryList.value[activeIndex.value]?.children.length <= 0) return
const item = categoryList.value[activeIndex.value].children[secondaryTabIndex.value]
if (item?.classify_id === undefined || item.classify_id === '') {
console.warn('没有分类ID')
return
}
getClassifyDetailApi({
page,
page_size,
classify_id: item.classify_id,
}).then((res) => {
paging.value.complete(res.result.data)
})
}
const activeTabIndex = computed(() => {
return activeIndex.value + '-' + secondaryTabIndex.value
})
//
watch(activeIndex, () => {
secondaryTabIndex.value = 0
})
watch(activeTabIndex, (newVal) => {
if (newVal) {
nextTick(() => {
paging.value?.reload()
})
}
})
onLoad(() => {
uni.setStorageSync('activeCategoryIndex', 0)
})
onShow(() => {
activeIndex.value = uni.getStorageSync('activeCategoryIndex') || 0
fetchTopCategory()
})
</script>
<template>
<view class="viewport">
<z-paging
:pagingStyle="{
backgroud: '#fff',
}"
empty-view-text="该分类下暂无商品,看看别的分类吧"
:empty-view-fixed="false"
>
<template #top>
<shop-goods-search />
</template>
<template #left>
<view class="categories">
<!-- 左侧一级分类 -->
<scroll-view class="primary" scroll-y>
<view
v-for="(item, index) in categoryList"
:key="index"
class="item"
@tap="onChangeCategory(index)"
:class="{ active: index === activeIndex }"
>
<text class="name"> {{ item.name }} </text>
</view>
</scroll-view>
</view>
</template>
<scroll-view class="secondary" scroll-y v-if="categoryList[activeIndex]?.children.length > 0">
<z-tabs
:list="categoryList[activeIndex].children"
@change="onChangeSecondary"
:current="secondaryTabIndex"
:scroll-count="1"
>
<template #right>
<view class="all" @tap="showAllSecondary">
<text class="icon icon-more"></text>
<text class="text">全部</text>
</view>
</template>
</z-tabs>
<!-- 展开全部二级 -->
<view class="all-secondary">
<uni-drawer ref="allSecondaryRef" mode="right" :mask-click="false">
<view class="all-secondary-title">
{{ categoryList[activeIndex].name }}
</view>
<scroll-view style="height: 100%" scroll-y>
<view class="seconday-panel">
<view
v-for="(item, index) in categoryList[activeIndex].children"
class="seconday-panel-item"
:class="{ active: index === secondaryTabIndex }"
:key="index"
@tap="selectSecondary(index)"
>
<view class="title">{{ item.name }}</view>
<image class="icon" :src="fullUrl(item.icon)" />
</view>
</view>
<button class="seconday-panel-button" type="warn" @tap="hiddenAllSecondary">
点击收起
</button>
</scroll-view>
</uni-drawer>
</view>
<z-paging
ref="paging"
v-model="secondaryData"
@query="querySecondary"
:use-page-scroll="true"
:pagingStyle="{
backgroud: '#fff',
}"
empty-view-text="该分类下暂无商品,看看别的分类吧"
>
<view class="panel">
<view class="section" v-for="(item, index) in secondaryData" :key="index">
<navigator
class="goods"
hover-class="none"
:url="`${pageUrl['shopping-mall-goods-detail']}?id=${item.id}`"
>
<view>
<image class="image" :src="fullUrl(item.images[0])" />
</view>
<view class="info">
<view class="name ellipsis">{{ item.name }}</view>
<view class="price">
<view class="origin">
<text class="symbol">¥</text>
<text class="number">{{ item.max_origin_price }}</text>
</view>
<view class="now">
<text class="symbol">¥</text>
<text class="number">{{ item.min_price }}</text>
</view>
</view>
</view>
</navigator>
</view>
</view>
</z-paging>
</scroll-view>
<z-paging-empty-view v-else empty-view-text="该分类下暂无商品,看看别的分类吧" />
</z-paging>
</view>
</template>
<style lang="scss">
page {
height: 100%;
overflow: hidden;
}
.viewport {
height: 100%;
display: flex;
flex-direction: column;
}
/* 分类 */
.categories {
flex: 1;
display: flex;
height: 100vh;
}
/* 一级分类 */
.primary {
overflow: hidden;
width: 180rpx;
flex: none;
background-color: #f6f6f6;
.item {
display: flex;
justify-content: center;
align-items: center;
height: 96rpx;
font-size: 26rpx;
color: #595c63;
position: relative;
&::after {
content: '';
position: absolute;
left: 42rpx;
bottom: 0;
width: 96rpx;
border-top: 1rpx solid #e3e4e7;
}
}
.active {
background-color: #fff;
&::before {
content: '';
position: absolute;
left: 0;
top: 0;
width: 8rpx;
height: 100%;
background-color: #002fa8;
}
}
}
.primary .item:last-child::after,
.primary .active::after {
display: none;
}
/* 二级分类 */
.secondary {
background-color: #fff;
.carousel {
height: 200rpx;
margin: 0 30rpx 20rpx;
border-radius: 4rpx;
overflow: hidden;
}
.panel {
margin: 0 30rpx 0rpx;
}
.title {
height: 60rpx;
line-height: 60rpx;
color: #333;
font-size: 28rpx;
.more {
float: right;
padding-left: 20rpx;
font-size: 24rpx;
color: #999;
}
}
.all {
color: #939393;
.text {
font-size: 30rpx;
padding: 10rpx;
}
}
.all-secondary-title {
text-align: center;
margin: 40rpx;
}
.seconday-panel {
display: flex;
flex-wrap: wrap;
padding-left: 10rpx;
}
.seconday-panel-item {
display: flex;
flex-direction: column;
align-items: center;
min-width: 180rpx;
&.active {
border: 5rpx solid #f00;
border-radius: 10rpx;
}
}
.seconday-panel-item .title {
font-size: 28rpx;
}
.seconday-panel-item .icon {
height: 100rpx;
width: 100rpx;
margin-bottom: 10rpx;
}
.seconday-panel-button {
font-size: 20rpx;
width: 40%;
margin-top: 50rpx;
border-radius: 30rpx;
}
.section {
width: 100%;
display: flex;
flex-wrap: wrap;
padding: 20rpx 0;
.goods {
width: 100%;
display: flex;
margin-bottom: 20rpx;
.image {
width: 150rpx;
height: 150rpx;
margin-right: 10rpx;
}
.info {
display: flex;
flex-direction: column;
justify-content: space-around;
width: 70%;
.name {
font-size: 24rpx;
}
.price {
display: flex;
flex-direction: column;
margin-top: 10rpx;
.origin {
font-size: 22rpx;
margin-right: 10rpx;
text-decoration: line-through;
}
.now {
color: red;
}
.symbol {
font-size: 18rpx;
}
}
}
}
}
}
.z-tabs-item-title {
font-size: 24rpx !important;
}
</style>

View File

@ -0,0 +1,219 @@
<script setup lang="ts">
import { getOrderEvaluateListApi, getOrderEvaluateTabsApi } from '@/api/order'
import { ref } from 'vue'
// 使使
import { onPageScroll, onReachBottom } from '@dcloudio/uni-app'
import useZPaging from '@/uni_modules/z-paging/components/z-paging/js/hooks/useZPaging.js'
import type { orderEvaluateListResult } from '@/types/order'
import type { TabItem } from '@/types/global'
import { fullUrl, onShowRefreshData, previewImg } from '@/utils/common'
const props = defineProps<{
goods_id: string
}>()
const paging = ref()
// mixinsrefpagingpagingpaging.value
useZPaging(paging)
const tabList = ref<TabItem[]>([])
const tabIndex = ref(0)
const tabsChange = (index: number) => {
tabIndex.value = index
paging.value.reload()
}
const dataList = ref<orderEvaluateListResult[]>([])
const queryList = async (page: number, page_size: number) => {
const res = await getOrderEvaluateListApi({
page,
page_size,
goods_id: props.goods_id,
type: tabList.value[tabIndex.value]?.value ?? '',
})
paging.value.complete(res.result.data)
}
const getTabList = async () => {
const res = await getOrderEvaluateTabsApi({
goods_id: props.goods_id,
})
tabList.value = res.result.map((item) => ({
...item,
disabled: false,
badge: { count: item.count },
}))
}
const groupedImages = (imageList: string[]) => {
if (imageList.length <= 0) return []
const groups = []
const images = imageList.map((item) => fullUrl(item))
for (let i = 0; i < images.length; i += 3) {
groups.push(images.slice(i, i + 3))
}
return groups
}
onShowRefreshData(() => {
getTabList()
})
</script>
<template>
<z-paging ref="paging" v-model="dataList" @query="queryList" :safe-area-inset-bottom="true">
<template #top>
<z-tabs
:list="tabList"
@change="tabsChange"
:current="tabIndex"
:badge-style="{ backgroundColor: '#c3c3c3' }"
/>
</template>
<view class="evaluate panel" v-for="(item, index) in dataList" :key="index">
<view class="top">
<shop-user-avatar class="portrait" :url="item.user_avatar" width="70" />
<text class="name">{{ item.user_name }}</text>
<text class="time">{{ item.create_time }}</text>
</view>
<view class="rate">
<uni-rate :value="item.score" :size="18" readonly />
<text class="spec">{{ item.goods_spec }}</text>
</view>
<text class="content">{{ item.content }}</text>
<view class="content-img">
<view class="image-group" v-for="(group, index) in groupedImages(item.images)" :key="index">
<image
v-for="(img, imgIndex) in group"
:key="imgIndex"
:src="img"
mode="aspectFill"
@click="previewImg(img, item.images)"
:class="{
'first-image': imgIndex === 0 && group.length > 1,
'last-image': imgIndex === group.length - 1 && group.length > 1,
'single-image': group.length === 1,
'middle-image': imgIndex !== 0 && imgIndex !== group.length - 1,
}"
/>
</view>
</view>
<view class="bot"> </view>
</view>
</z-paging>
</template>
<style lang="scss">
page {
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
background-color: #f4f4f4;
}
.panel {
margin-top: 10rpx;
background-color: #fff;
padding: 20rpx;
margin: 20rpx;
border-radius: 20rpx;
.top {
display: flex;
align-items: center;
margin-bottom: 10rpx;
.portrait {
width: 70rpx;
height: 70rpx;
border-radius: 50%;
margin-right: 10px;
}
.name {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 28rpx;
}
.time {
font-size: 24rpx;
}
}
.rate {
display: flex;
align-items: center;
margin-bottom: 10rpx;
.spec {
::before {
content: '|';
margin: 0 10rpx;
}
color: #666;
font-size: 20rpx;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
}
}
.content {
display: flex;
padding-left: 7rpx;
font-size: 26rpx;
margin: 20rpx 0rpx;
}
.content-img {
display: flex;
flex-direction: column;
margin-top: 10rpx;
}
.image-group {
display: flex;
width: 100%;
}
.image-group image {
height: 200rpx;
width: 33%;
margin-right: 1.5%;
margin-bottom: 10rpx;
object-fit: cover;
border-radius: 0;
}
.image-group image.single-image {
border-radius: 20rpx;
}
.image-group image.first-image {
border-top-left-radius: 20rpx;
border-bottom-left-radius: 20rpx;
}
.image-group image.last-image {
border-top-right-radius: 20rpx;
border-bottom-right-radius: 20rpx;
}
.image-group image.middle-image {
border-radius: 0;
}
.image-group image:last-child {
margin-right: 0;
}
}
</style>

View File

@ -0,0 +1,162 @@
<script setup lang="ts">
import { useAddressStore } from '@/stores/modules/address'
import type { addressItem } from '@/types/address'
import { pageUrl } from '@/utils/constants'
const emit = defineEmits<{
(event: 'close'): void
}>()
//
const props = defineProps<{
list?: addressItem[]
}>()
const addressStore = useAddressStore()
//
const onSelectAddress = (item: addressItem) => {
addressStore.changeSelectedAddress(item)
emit('close')
}
const toAddressList = () => {
uni.navigateTo({
url: `${pageUrl['address-form']}`,
})
emit('close')
}
useAddressStore().changeScene('order')
</script>
<template>
<view class="address-panel">
<text class="close icon-close" @tap="emit('close')"></text>
<view class="title">配送至</view>
<view class="content">
<view v-if="props.list!.length > 0">
<view class="item" v-for="item in props.list" :key="item.id" @tap="onSelectAddress(item)">
<view class="user">{{ item.name }} {{ item.phone }}</view>
<view class="address">{{ item.full_location }} {{ item.address }}</view>
<text
class="icon"
:class="item.id === addressStore.selectedAddress?.id ? 'icon-checked' : 'icon-check'"
></text>
</view>
</view>
<view v-else>
<z-paging-empty-view
empty-view-text="暂无收货地址"
:empty-view-style="{
top: '50rpx',
}"
/>
</view>
</view>
</view>
<view class="footer">
<view class="button primary">
<view open-type="navigate" hover-class="navigator-hover" @tap="toAddressList">
新增收货地址
</view>
</view>
</view>
</template>
<style lang="scss">
.address-panel {
padding: 0 30rpx;
border-radius: 10rpx 10rpx 0 0;
position: relative;
background-color: #fff;
}
.title {
line-height: 1;
padding: 40rpx 0;
text-align: center;
font-size: 32rpx;
font-weight: normal;
border-bottom: 1rpx solid #ddd;
color: #444;
}
.close {
position: absolute;
right: 24rpx;
top: 24rpx;
font-size: 40rpx;
}
.content {
min-height: 300rpx;
max-height: 540rpx;
overflow: auto;
padding: 20rpx;
.item {
padding: 30rpx 50rpx 30rpx 60rpx;
background-size: 40rpx;
background-repeat: no-repeat;
background-position: 0 center;
background-image: url('~@/static/images/locate.png');
position: relative;
}
.icon {
color: #999;
font-size: 40rpx;
transform: translateY(-50%);
position: absolute;
top: 50%;
right: 0;
}
.icon-checked {
color: #ff5f3c;
}
.icon-ring {
color: #444;
}
.user {
font-size: 28rpx;
color: #444;
font-weight: 500;
}
.address {
font-size: 26rpx;
color: #666;
}
.empty-text {
text-align: center;
}
}
.footer {
display: flex;
justify-content: space-between;
padding: 20rpx 0 40rpx;
font-size: 28rpx;
color: #444;
.button {
flex: 1;
height: 72rpx;
text-align: center;
line-height: 72rpx;
margin: 0 20rpx;
color: #fff;
border-radius: 72rpx;
}
.primary {
color: #fff;
background-color: #ff5f3c;
}
.secondary {
background-color: #ffa868;
}
}
</style>

View File

@ -0,0 +1,87 @@
<script setup lang="ts">
import type { goodsServiceItem } from '@/types/goods'
const emit = defineEmits<{
(event: 'close'): void
}>()
//
const props = defineProps<{
list?: goodsServiceItem[]
}>()
</script>
<template>
<view class="service-panel">
<text class="close icon-close" @tap="emit('close')"></text>
<view class="title">服务说明</view>
<view class="content">
<view class="item" v-for="item in props.list" :key="item.id">
<view class="dt">{{ item.name }}</view>
<view class="dd">
{{ item.content }}
</view>
</view>
</view>
</view>
</template>
<style lang="scss">
.service-panel {
padding: 0 30rpx;
border-radius: 10rpx 10rpx 0 0;
position: relative;
background-color: #fff;
}
.title {
line-height: 1;
padding: 40rpx 0;
text-align: center;
font-size: 32rpx;
font-weight: normal;
border-bottom: 1rpx solid #ddd;
color: #444;
}
.close {
position: absolute;
right: 24rpx;
top: 24rpx;
font-size: 40rpx;
}
.content {
padding: 20rpx 20rpx 150rpx 20rpx;
.item {
margin-top: 20rpx;
}
.dt {
margin-bottom: 10rpx;
font-size: 28rpx;
color: #333;
font-weight: 500;
position: relative;
&::before {
content: '';
width: 10rpx;
height: 10rpx;
border-radius: 50%;
background-color: #eaeaea;
transform: translateY(-50%);
position: absolute;
top: 50%;
left: -20rpx;
}
}
.dd {
line-height: 1.6;
font-size: 26rpx;
color: #999;
}
}
</style>

View File

@ -0,0 +1,153 @@
<script setup lang="ts">
import type { goodsListItem } from '@/types/goods'
import { getShareUrl } from './sharePoster'
const emits = defineEmits<{
(event: 'close'): void
(event: 'showPoster'): void
}>()
const props = defineProps<{
goods: goodsListItem
}>()
const onCpoyLink = () => {
const shareUrl = getShareUrl(props.goods.id)
uni.setClipboardData({
data: shareUrl,
success: function () {
uni.showToast({
title: '复制成功',
icon: 'none',
})
},
})
emits('close')
}
const onShowSharePoster = () => {
emits('close')
emits('showPoster')
}
</script>
<template v-if="path == ''">
<view class="panel">
<view class="content">
<!-- #ifdef MP-WEIXIN -->
<button class="content-item" open-type="share">
<image class="icon" src="/static/icons/wechat.svg" />
<view class="text">微信好友</view>
</button>
<!-- #endif -->
<button class="content-item" @tap="onShowSharePoster">
<image class="icon" src="/static/icons/poster.svg" />
<view class="text">生成海报</view>
</button>
<button class="content-item" @tap="onCpoyLink">
<image class="icon" src="/static/icons/link.svg" />
<view class="text">复制链接</view>
</button>
</view>
<view class="footer">
<view class="button" @tap="emits('close')"> 取消分享 </view>
</view>
</view>
</template>
<style lang="scss">
.panel {
padding: 0 30rpx;
border-radius: 10rpx 10rpx 0 0;
position: relative;
background-color: #fff;
}
.title {
line-height: 1;
padding: 40rpx 0;
text-align: center;
font-size: 32rpx;
font-weight: normal;
border-bottom: 1rpx solid #ddd;
color: #444;
}
.close {
position: absolute;
right: 24rpx;
top: 24rpx;
font-size: 40rpx;
}
.content {
display: flex;
justify-content: cneter;
.content-item {
margin-top: 20rpx;
background-color: transparent;
border: none;
}
::after {
border: none;
}
.icon {
width: 80rpx;
height: 80rpx;
}
.text {
font-size: 30rpx;
}
}
.footer {
display: flex;
justify-content: space-between;
padding: 20rpx 0 20px;
font-size: 28rpx;
color: #444;
.button {
flex: 1;
height: 72rpx;
text-align: center;
line-height: 72rpx;
margin: 0 20rpx;
border-radius: 72rpx;
color: #fff;
background-color: #ff5f3c;
}
}
.action {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx;
background: #f5f5f5;
.button {
padding: 16rpx 32rpx;
margin: 0 10rpx;
background: #ff784d;
color: #ffffff;
border-radius: 40rpx;
&:active {
opacity: 0.8;
}
}
.cancel {
background: #b3b3b3;
color: #ffffff;
}
}
</style>

View File

@ -0,0 +1,9 @@
import { useMemberStore, useSettingStore } from '@/stores'
import { pageUrl } from '@/utils/constants'
export const getShareUrl = (goodsId: number | string) => {
const settingData = useSettingStore().data
const profileData = useMemberStore().profile
return `${settingData.h5_domain}${pageUrl['shopping-mall-goods-detail']}?id=${goodsId}&sharer=${profileData?.id}`
}

View File

@ -0,0 +1,231 @@
<script setup lang="ts">
import { useMemberStore } from '@/stores'
import type { goodsListItem } from '@/types/goods'
import { ref, reactive } from 'vue'
import { fullUrl, checkImage, getUserDefaultAvatar } from '@/utils/common'
import { getShareInfoApi } from '@/api/goods'
import { getShareUrl } from './sharePoster'
import { usePopupStore } from '@/stores'
const props = defineProps<{
goods: goodsListItem
currentPrice: string
currentOriginPrice: string
goodsImage: string
}>()
const sharePosterRef = ref()
const painter = ref()
const state = reactive({
posterImgPath: '',
showPoster: false,
userAvatarImg: useMemberStore().profile!.avatar,
qrcodeUrl: '',
})
const userImage = ref('')
const open = async () => {
userImage.value = fullUrl(state.userAvatarImg)
await checkImage(userImage.value)
.then((url) => {
userImage.value = url
})
.catch(() => {
userImage.value = getUserDefaultAvatar()
})
// #ifdef MP-WEIXIN
const shareInfo = await getShareInfoApi(props.goods.id)
state.qrcodeUrl = shareInfo.result.qrcode_url
// #endif
await sharePosterRef.value?.open()
state.showPoster = true
state.posterImgPath = ''
await uni.showLoading({
title: '海报生成中…',
mask: true,
})
}
const close = () => {
sharePosterRef.value.close()
state.showPoster = false
}
const onDone = () => {}
const onSuccess = (imgPath: string) => {
state.posterImgPath = imgPath
uni.hideLoading()
}
const onSave = () => {
// #ifdef MP-WEIXIN
painter.value.canvasToTempFilePathSync({
fileType: 'jpg',
pathType: 'url',
quality: 1,
success: (res: any) => {
console.log(res.tempFilePath)
uni.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success: () => {
uni.showToast({
title: '保存成功',
icon: 'none',
})
},
fail: (err) => {
console.log(err)
if (err.errMsg == 'saveImageToPhotosAlbum:fail cancel') {
return uni.showToast({
title: '取消保存',
icon: 'none',
})
} else {
uni.showToast({
title: '保存失败',
icon: 'none',
})
}
},
complete: () => {
close()
},
})
},
})
// #endif
}
defineExpose({ sharePosterRef, open, close })
</script>
<template>
<uni-popup
ref="sharePosterRef"
type="center"
:animation="false"
:mask-click="false"
@change="usePopupStore().onChangePopupProps"
>
<image :src="state.posterImgPath" mode="widthFix" style="width: 600rpx" />
<view>
<view class="poster">
<l-painter
ref="painter"
isCanvasToTempFilePath
@success="onSuccess"
css="width: 750rpx; padding-bottom: 40rpx; background: #edca88; object-fit: contain;"
hidden
@done="onDone"
:after-delay="500"
>
<l-painter-image
:src="userImage"
css="margin-left: 40rpx; margin-top: 40rpx; width: 100rpx; height: 100rpx; border-radius: 50%;"
/>
<l-painter-view css="margin-top: 40rpx; padding-left: 20rpx; display: inline-block">
<l-painter-text
:text="useMemberStore().profile?.nickname || ''"
css="display: block; padding-bottom: 20rpx; color: #000; font-size: 32rpx; fontWeight: bold"
/>
<l-painter-text text="推荐了一个好物给您" css="color: #000; font-size: 24rpx" />
</l-painter-view>
<l-painter-view
css="margin-left: 40rpx; margin-top: 30rpx; padding: 32rpx; box-sizing: border-box; background: #fff; border-radius: 16rpx; width: 670rpx; box-shadow: 0 20rpx 58rpx rgba(0,0,0,.15)"
>
<l-painter-image
:src="fullUrl(props.goodsImage)"
css="object-fit: fill; width: 600rpx; max-height: 550rpx; border-radius: 12rpx;"
/>
<l-painter-view
css="margin-top: 32rpx; color: #FF0000; font-weight: bold; font-size: 28rpx; line-height: 1em;"
>
<l-painter-text text="¥" css="vertical-align: bottom" />
<l-painter-text
:text="`${props.currentPrice.split('.')[0]}`"
css="vertical-align: bottom; font-size: 58rpx"
/>
<l-painter-text
:text="`.${props.currentPrice.split('.')[1]}`"
css="vertical-align: bottom"
/>
<l-painter-text
:text="`¥${props.currentOriginPrice}`"
css="vertical-align: bottom; padding-left: 10rpx; font-weight: normal; text-decoration: line-through; color: #999999"
/>
</l-painter-view>
<l-painter-view css="margin-top: 30rpx">
<l-painter-text
:text="props.goods.name"
css="line-clamp: 2; color: #333333; line-height: 1.5em; width: 620rpx; font-size: 30rpx; padding-right:32rpx; box-sizing: border-box"
></l-painter-text>
</l-painter-view>
<l-painter-view css="margin-top: 30rpx; display: flex; flex-wrap: nowrap;">
<l-painter-text
text="长按或扫一扫识别二维码"
css="float: left; padding-bottom: 10rpx; color: #999999; font-size: 26rpx; margin-top: 60rpx; margin-right: 160rpx"
/>
<!-- #ifdef MP-WEIXIN -->
<l-painter-image
v-if="state.qrcodeUrl"
:src="state.qrcodeUrl"
css="float: right; width: 150rpx; height: 150rpx;"
/>
<!-- #endif -->
<!-- #ifdef WEB -->
<l-painter-qrcode
:text="getShareUrl(props.goods.id)"
css="float: right; width: 150rpx; height: 150rpx;"
/>
<!-- #endif -->
</l-painter-view>
</l-painter-view>
</l-painter>
</view>
<view class="action" v-if="state.posterImgPath">
<view class="button cancel" @tap="close()">取消分享</view>
<!-- #ifdef WEB -->
<view>长按图片进行保存</view>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<view class="button" @tap="onSave()">保存图片</view>
<!-- #endif -->
</view>
</view>
</uni-popup>
</template>
<style lang="scss" scoped>
.action {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx;
background: #f5f5f5;
.button {
padding: 16rpx 32rpx;
margin: 0 10rpx;
background: #ff784d;
color: #ffffff;
border-radius: 40rpx;
&:active {
opacity: 0.8;
}
}
.cancel {
background: #b3b3b3;
color: #ffffff;
}
}
</style>

View File

@ -0,0 +1,864 @@
<script setup lang="ts">
import { getGoodsByIdApi } from '@/api/goods'
import { onLoad } from '@dcloudio/uni-app'
import { ref, watchEffect, reactive, computed } from 'vue'
import type { goodsResult, goodsServiceItem, skuItem } from '@/types/goods'
import type {
SkuPopupEvent,
SkuPopupInstance,
SkuPopupLocaldata,
} from '@/components/vk-data-goods-sku-popup/vk-data-goods-sku-popup'
import { addCartApi } from '@/api/cart'
import { useAddressStore } from '@/stores/modules/address'
import { previewImg, fullUrl, arrayFullUrl, onShowRefreshData } from '@/utils/common'
import { pageUrl, safeBottom, pageMateStyle, isIOSWithHomeIndicator } from '@/utils/constants'
import { useCartStore } from '@/stores'
import { postAddFavoriteApi, postCancelFavoriteApi } from '@/api/favorite'
import _, { debounce } from 'lodash'
import { onShareAppMessage } from '@dcloudio/uni-app'
import { useMemberStore } from '@/stores'
import { getAddressApi } from '@/api/address'
import type { addressItem } from '@/types/address'
import { computeConversion } from '@/utils/common'
import servicePanel from './components/servicePanel.vue'
import addressPanel from './components/addressPanel.vue'
import sharePanel from './components/sharePanel.vue'
import sharePoster from './components/sharePoster.vue'
import { usePopupStore } from '@/stores'
const props = defineProps<{
id: string | number
scene?: string
}>()
//
const query = reactive({
id: props.id,
scene: props.scene,
})
const cartStore = useCartStore()
//sku
const isShowSku = ref(false)
//sku
const skuData = ref({} as SkuPopupLocaldata)
//sku
enum skuskuModeType {
both = 1,
cart = 2,
buy = 3,
}
const skuMode = ref<skuskuModeType>(skuskuModeType.both)
//skuref
const skuPopupRef = ref<SkuPopupInstance>()
//sku
const selectAttrText = computed(() => {
return skuPopupRef.value?.selectArr?.join(' ').trim() || '请选择商品规格'
})
//
const selectedPrice = ref(0)
const currentPrice = computed(() => selectedPrice.value || goods.value?.min_price || 0)
// 线
const selectedOriginPrice = ref(0)
const currentOriginPrice = computed(
() => selectedOriginPrice.value || goods.value?.max_origin_price || 0,
)
//sku
const openSkuPopup = (mode: skuskuModeType) => {
isShowSku.value = true
skuMode.value = mode
}
const goodsServiceList = ref<goodsServiceItem[]>([])
const goodsServiceText = ref('')
//
const goods = ref<goodsResult>()
const getGoodsDataById = async () => {
const res = await getGoodsByIdApi(Number(query.id))
goods.value = res.result
//sku
skuData.value = {
_id: res.result.id.toString(),
name: res.result.name,
goods_thumb: fullUrl(res.result.images[0]),
spec_list: res.result.spec_list.map((spec) => {
return {
name: spec.name,
list: spec.value,
}
}),
sku_list: res.result.sku_list.map((sku) => {
return {
_id: sku.id.toString(),
goods_id: res.result.id.toString(),
goods_name: res.result.name,
image: fullUrl(sku.image),
price: sku.price,
origin_price: sku.origin_price,
sku_name_arr: sku.spec?.map((sku_name) => sku_name.value),
stock: sku.stock,
}
}),
}
//
goodsServiceList.value = goods.value.goods_service
goodsServiceText.value = goods.value.goods_service.map((item) => item.name).join(' ')
}
//
const currentIndex = ref(0)
const updateIndex: UniHelper.SwiperOnChange = (event) => {
currentIndex.value = event.detail.current
}
//ref
const popup = ref<{
open: (type?: UniHelper.UniPopupType) => void
close: () => void
}>()
//
const popupName = ref<'address' | 'service' | 'share' | 'sharePoster'>()
const openPopup = (name: typeof popupName.value) => {
popupName.value = name
popup.value?.open()
}
//
const onAddCart = (event: SkuPopupEvent) => {
addCartApi({
sku_id: event._id,
num: event.buy_num,
}).then((res) => {
cartStore.setCartTotalNum(res.result.total_num)
cartInfo.value = res.result.total_num
isShowSku.value = false
uni.showToast({
title: '加入购物车成功',
icon: 'success',
mask: true,
})
})
}
//
const onBuyNow = (event: SkuPopupEvent) => {
console.log(event)
isShowSku.value = false
uni.navigateTo({
url: `${pageUrl['order-create']}?sku_id=${event._id}&count=${event.buy_num}`,
})
}
//
const onSpecSelected = (skuData: skuItem) => {
selectedPrice.value = skuData.price
selectedOriginPrice.value = skuData.origin_price
}
//
const addressStore = useAddressStore()
const selectAddressText = computed(() => {
return addressStore.selectedAddress
? addressStore.selectedAddress.full_location
: '请选择收货地址'
})
const addressList = ref<addressItem[]>([])
const openAddressPopup = () => {
getAddressApi().then((res) => {
addressList.value = res.result.data
openPopup('address')
})
}
const getMoreEvaluate = () => {
uni.navigateTo({
url: `${pageUrl['shopping-mall-goods-evaluate']}?goods_id=${query.id}`,
})
}
const cartInfo = ref(0)
const menuButtons = computed<UniHelper.UniGoodsNavOption[]>(() => [
{
icon: 'shop',
text: '商城',
},
{
icon: 'cart',
text: '购物车',
info: cartInfo.value,
},
{
icon: 'redo',
text: '分享',
},
])
const menuClick = (event: UniHelper.UniGoodsNavOnClickEvent) => {
switch (event.index) {
case 0:
uni.redirectTo({
url: `${pageUrl['shopping-mall']}`,
})
break
case 1:
uni.redirectTo({
url: `${pageUrl['shopping-mall-cart']}`,
})
break
case 2:
showSharePopup()
break
}
}
const operateButtons: UniHelper.UniGoodsNavButton[] = [
{
text: '加入购物车',
backgroundColor: '#ffa200',
color: '#fff',
},
{
text: '立即购买',
backgroundColor: '#ff0000',
color: '#fff',
},
]
const buttonClick = (event: UniHelper.UniGoodsNavOnButtonClickEvent) => {
switch (event.index) {
case 0:
openSkuPopup(skuskuModeType.cart)
break
case 1:
openSkuPopup(skuskuModeType.buy)
break
}
}
const changeFavoriteStatus = debounce((goods) => {
if (goods.favorite === true) {
postCancelFavoriteApi({
goods_id: goods.id,
}).then((res) => {
if (res.result) {
goods.favorite = false
uni.showToast({
title: '商品取消收藏',
icon: 'none',
})
}
})
} else {
postAddFavoriteApi({
goods_id: goods.id,
})
.then((res) => {
if (res.result) {
goods.favorite = true
uni.showToast({
title: '商品收藏成功',
icon: 'none',
})
}
})
.catch((err) => {
console.log(err)
})
}
}, 500)
watchEffect(() => {
cartInfo.value = useCartStore().cartTotalNum
})
const sharePopupRef = ref()
//
const showSharePopup = () => {
if (!useMemberStore().profile?.id) {
return uni.showModal({
title: '温馨提示',
content: '登录解锁更多精彩,是否继续?',
confirmText: '去登录',
cancelText: '再看看',
success: function (res) {
if (res.confirm) {
uni.navigateTo({ url: pageUrl['login'] })
}
},
})
}
openPopup('share')
}
const sharePosterRef = ref()
const goodsImage = ref('')
const onShowPoster = () => {
sharePosterRef.value.open()
goodsImage.value = goods.value!.images[currentIndex.value] || goods.value!.images[0]
}
onShareAppMessage(() => {
sharePopupRef.value.close()
const sharePath = `${pageUrl['shopping-mall-goods-detail']}?id=${goods.value?.id}&sharer=${
useMemberStore().profile?.id
}`
console.log(sharePath)
return {
title: goods.value?.name,
path: sharePath,
imageUrl: fullUrl(goods.value!.images[0]),
}
})
onShowRefreshData(() => {
if (props.scene) {
const params = decodeURIComponent(props.scene)
.split('&')
.reduce((acc: { [key: string]: any }, curr) => {
const [key, value] = curr.split('=')
acc[key] = value
return acc
}, {})
console.log('解析scene', params)
if (params.id) {
query.id = params.id
}
}
getGoodsDataById()
})
onLoad(() => {
// #ifdef WEB
// IOS
if (isIOSWithHomeIndicator()) {
document.documentElement.classList.add('ios-device')
}
// #endif
// #ifdef MP-WEIXIN
uni.showShareMenu({
withShareTicket: true,
menus: ['shareAppMessage'],
})
// #endif
})
</script>
<template>
<!-- #ifdef MP-WEIXIN -->
<page-meta :page-style="pageMateStyle">
<!-- #endif -->
<scroll-view scroll-y class="viewport" :style="safeBottom" v-if="goods">
<!-- 基本信息 -->
<view class="goods">
<!-- 商品主图 -->
<view class="preview">
<swiper circular @change="updateIndex" autoplay>
<swiper-item v-for="item in goods?.images" :key="item">
<image
class="image"
:src="fullUrl(item)"
@tap="previewImg(item, arrayFullUrl(goods!.images))"
/>
</swiper-item>
</swiper>
<view class="indicator">
<text class="current">{{ currentIndex + 1 }}</text>
<text class="split">/</text>
<text class="total">{{ goods?.images.length }}</text>
</view>
</view>
</view>
<view class="panel">
<view class="meta">
<view class="meta-top">
<view class="price">
<view class="origin">¥{{ currentOriginPrice }} </view>
<view class="current">
<text class="symbol">¥</text>
<text class="number">{{ currentPrice }}</text>
</view>
</view>
<view class="operate">
<view class="operate-item" @tap="changeFavoriteStatus(goods!)" v-if="goods.favorite">
<button class="operate-item-btn">
<text class="icon icon-favorite-checked" />
</button>
<text class="icon-text">已收藏</text>
</view>
<view class="operate-item" @tap="changeFavoriteStatus(goods!)" v-else>
<button class="operate-item-btn">
<text class="icon icon-favorite-default" />
</button>
<text class="icon-text">收藏</text>
</view>
<!-- #ifdef MP-WEIXIN -->
<view class="operate-item">
<button open-type="contact" class="operate-item-btn">
<text class="icon-handset"></text>
</button>
<view class="icon-text">
<text>客服</text>
</view>
</view>
<!-- #endif -->
</view>
</view>
<view class="name-box">
<view class="name ellipsis">{{ goods?.name }} </view>
<view class="sales">
已售{{ computeConversion(goods.sales_init + goods.sales_real) }}</view
>
</view>
</view>
</view>
<view class="panel">
<view class="action">
<view class="item arrow" @tap="openSkuPopup(skuskuModeType.both)">
<text class="label">已选</text>
<text class="text ellipsis">{{ selectAttrText }}</text>
</view>
<view class="item arrow" @tap="openAddressPopup">
<text class="label">送至</text>
<text class="text ellipsis">{{ selectAddressText }}</text>
</view>
<view class="item arrow" @tap="openPopup('service')">
<text class="label">服务</text>
<text class="text ellipsis">{{ goodsServiceText }}</text>
</view>
</view>
<!-- 弹出层 -->
<uni-popup
ref="popup"
type="bottom"
background-color="#fff"
@change="usePopupStore().onChangePopupProps"
>
<address-panel
v-if="popupName === 'address'"
@close="popup?.close()"
:list="addressList"
/>
<service-panel
v-if="popupName === 'service'"
:list="goodsServiceList"
@close="popup?.close()"
/>
<share-panel
v-if="popupName === 'share'"
@close="popup?.close()"
@showPoster="onShowPoster"
:goods="goods"
/>
</uni-popup>
</view>
<!-- 商品评价 -->
<view class="panel">
<view v-if="goods?.evaluate_count > 0" @click="getMoreEvaluate">
<uni-section :title="`评价 (${computeConversion(goods?.evaluate_count)})`" type="line">
<template #right>
<view class="tip">
<text> 好评率 {{ goods?.positive_rate }}%</text>
<uni-icons type="forward" size="14" color="#909399" class="icon"></uni-icons>
</view>
</template>
<view class="eva-box" v-for="(evaluate, index) in goods?.evaluate" :key="index">
<view class="top">
<shop-user-avatar
:url="evaluate.user_avatar"
width="50"
style="padding-right: 10rpx"
/>
<text class="name">{{ evaluate.user_name }}</text>
<view class="rate">
<uni-rate :value="evaluate.score" :size="20" readonly />
</view>
</view>
<text class="content">{{ evaluate.content }}</text>
<scroll-view class="scroll-view-container" :scroll-x="true">
<image
class="content-img"
mode="aspectFill"
v-for="(item, index) in arrayFullUrl(evaluate.images)"
:key="index"
:src="item"
@click.stop="previewImg(item, evaluate.images)"
/>
</scroll-view>
<view class="bot"> </view>
</view>
</uni-section>
</view>
<view v-else>
<uni-section title="评价 (0)" type="line">
<z-paging-empty-view empty-view-text="期待您的评价" :empty-view-fixed="false" />
</uni-section>
</view>
</view>
<view class="panel">
<uni-section title="详情" type="line">
<view class="properties">
<view class="item" v-for="item in goods?.params" :key="item.key">
<text class="label">{{ item.key }}</text>
<text class="value">{{ item.value }}</text>
</view>
</view>
</uni-section>
<view v-if="goods.detail_images.length > 0">
<shop-image :src="goods?.detail_images" />
</view>
</view>
<view :style="{ paddingBottom: '5rpx' }"> </view>
<!-- sku弹窗 -->
<vk-data-goods-sku-popup
v-model="isShowSku"
:localdata="skuData"
:mode="skuMode"
buy-now-background-color="#ff0000"
add-cart-background-color="#ffa200"
:amountType="0"
ref="skuPopupRef"
:actived-style="{
color: '#ff0000',
backgroundColor: '#fee8e6',
borderColor: '#ff0000',
}"
@add-cart="onAddCart"
@buy-now="onBuyNow"
@spec-selected="onSpecSelected"
class="sku-popup"
/>
<!-- 海报弹窗 -->
<sharePoster
ref="sharePosterRef"
:goods="goods"
:currentPrice="currentPrice"
:currentOriginPrice="currentOriginPrice"
:goodsImage="goodsImage"
/>
<!-- 用户操作 -->
<view class="goods-nav" :style="safeBottom" v-show="goods">
<uni-goods-nav
:fill="true"
:options="menuButtons"
:buttonGroup="operateButtons"
@click="menuClick"
@buttonClick="buttonClick"
/>
</view>
</scroll-view>
<!-- #ifdef MP-WEIXIN -->
</page-meta>
<!-- #endif -->
</template>
<style lang="scss">
swiper {
height: 100%;
}
page {
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
}
.viewport {
background-color: #f4f4f4;
z-index: 999;
margin-bottom: 40px;
}
.panel {
background-color: #fff;
border-radius: 10rpx;
margin: 15rpx 15rpx 20rpx;
padding: 10rpx;
}
.arrow {
&::after {
position: absolute;
top: 50%;
right: 30rpx;
content: '\e6c2';
color: #ccc;
font-family: 'iconfont' !important;
font-size: 32rpx;
transform: translateY(-50%);
}
}
/* 商品信息 */
.goods {
background-color: #fff;
margin: 15rpx 15rpx 20rpx;
.preview {
height: 750rpx;
position: relative;
.image {
width: 750rpx;
height: 750rpx;
}
.indicator {
height: 40rpx;
padding: 0 24rpx;
line-height: 40rpx;
border-radius: 30rpx;
color: #fff;
font-family: Arial, Helvetica, sans-serif;
background-color: rgba(0, 0, 0, 0.3);
position: absolute;
bottom: 30rpx;
right: 30rpx;
.current {
font-size: 26rpx;
}
.split {
font-size: 24rpx;
margin: 0 1rpx 0 2rpx;
}
.total {
font-size: 24rpx;
}
}
}
}
.meta {
.meta-top {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.operate {
display: flex;
.operate-item {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.operate-item-btn {
width: 80rpx;
height: 80rpx;
background-color: transparent;
&::after {
border: none;
}
}
.icon {
text-align: center;
}
.icon-text {
font-size: 20rpx;
color: #9a9a9a;
}
.icon-favorite-checked {
color: red;
}
}
}
.price {
height: 130rpx;
padding: 0rpx 20rpx;
color: red;
font-size: 34rpx;
box-sizing: border-box;
background-color: #fff;
display: flex;
flex-direction: column;
.origin {
margin-top: 15rpx;
color: #888;
text-decoration: line-through;
::after {
content: ;
}
}
}
.number {
font-size: 50rpx;
}
.brand {
width: 160rpx;
height: 80rpx;
overflow: hidden;
position: absolute;
top: 26rpx;
right: 30rpx;
}
.name {
max-height: 88rpx;
line-height: 1.4;
margin: 20rpx;
font-size: 32rpx;
color: #333;
}
.desc {
line-height: 1;
padding: 0 20rpx 30rpx;
font-size: 24rpx;
color: #cf4444;
}
.sales {
margin: 20rpx;
font-size: 24rpx;
color: #888;
}
}
.action {
padding-left: 10rpx;
background: #fff;
.item {
height: 90rpx;
padding-right: 60rpx;
border-bottom: 1rpx solid #eaeaea;
font-size: 26rpx;
color: #333;
position: relative;
display: flex;
align-items: center;
&:last-child {
border-bottom: 0 none;
}
}
.label {
width: 60rpx;
color: #898b94;
margin: 0 16rpx 0 10rpx;
}
.text {
flex: 1;
-webkit-line-clamp: 1;
}
}
.tip {
font-size: 24rpx;
color: #9a9a9a;
.icon {
padding-left: 10rpx;
}
}
.eva-box {
padding: 10rpx;
.top {
display: flex;
align-items: center;
padding-bottom: 20rpx;
.name {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 26rpx;
padding-bottom: 10rpx;
}
}
.rate {
margin-right: 10rpx;
}
.content {
margin: 0rpx 10rpx 10rpx;
max-height: 100rpx;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
font-size: 24rpx;
}
.scroll-view-container {
width: 100%;
white-space: nowrap;
overflow-x: scroll;
-webkit-overflow-scrolling: touch;
.content-img {
margin-left: 10rpx;
height: 150rpx;
width: 150rpx;
margin-bottom: 10rpx;
margin-right: 10rpx;
border-radius: 10rpx;
margin-top: 10rpx;
scroll-snap-align: start;
}
}
}
.properties {
padding: 0 20rpx;
.item {
display: flex;
line-height: 2;
padding: 10rpx;
font-size: 26rpx;
color: #333;
border-bottom: 1rpx dashed #ccc;
}
.label {
width: 200rpx;
}
.value {
flex: 1;
}
}
/* 底部工具栏 */
.goods-nav {
position: fixed;
bottom: 0;
width: 100%;
background-color: #fff;
}
/* #ifdef WEB */
// ios-device
.ios-device {
.viewport {
padding-bottom: 34px !important;
}
.goods-nav {
padding-bottom: 34px !important;
}
}
/* #endif */
</style>

View File

@ -0,0 +1,107 @@
<script setup lang="ts">
import { reactive, ref } from 'vue'
import type { shopGoodsListInsatnce } from '@/types/component'
//
const props = withDefaults(
defineProps<{
classify_id?: string
name?: string
coupon_id?: string
empty_text?: string
}>(),
{
classify_id: '',
name: '',
coupon_id: '',
empty_text: '暂无商品数据',
},
)
const query = reactive({
classify_id: props.classify_id,
name: props.name,
coupon_id: props.coupon_id,
sort_field: '',
sort_by: '',
})
const shopGoodsListRef = ref<shopGoodsListInsatnce>()
const queryOptions = ref([
{ title: '综合排序', value: 'all', type: 'click' },
{ title: '销量优先', value: 'sales', type: 'click' },
{
title: '价格排序',
value: '',
type: 'sort',
options: [{ value: 'asc' }, { value: 'desc' }],
},
])
const onChange = (data: any, index: number) => {
console.log(data, index)
}
const onConfirm = (data: any) => {
const { value, type } = data
if (value === 'all') {
Object.assign(query, { sort_field: data.value })
} else if (value === 'sales') {
Object.assign(query, { sort_field: data.value, sort_by: 'desc' })
} else if (type === 'sort') {
Object.assign(query, { sort_field: 'price', sort_by: value })
}
shopGoodsListRef.value?.reload()
}
</script>
<template>
<shop-goods-list
:query="query"
:safe-area-inset-bottom="true"
ref="shopGoodsListRef"
:options="{
emptyViewText: empty_text,
backGroundColor: '#fff',
}"
>
<template #top>
<view v-if="query">
<le-dropdown
v-model:menuList="queryOptions"
themeColor="#3185FF"
:duration="300"
:isCeiling="false"
@onConfirm="onConfirm"
@onChange="onChange"
></le-dropdown>
</view>
</template>
</shop-goods-list>
</template>
<style lang="scss" scoped>
page {
background-color: #ececec;
}
.toolbar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 1;
background-color: #fff;
height: 100rpx;
padding: 0 20rpx var(--window-bottom);
border-top: 1rpx solid #eaeaea;
display: flex;
justify-content: space-between;
align-items: center;
box-sizing: content-box;
}
//
.toolbar-height {
height: 190rpx;
}
</style>

View File

@ -0,0 +1,147 @@
<script setup lang="ts">
import { pageUrl } from '@/utils/constants'
import { ref } from 'vue'
let name = ref('')
const history = ref<string[]>(uni.getStorageSync('search') || [])
const storeHistroy = () => {
name.value = name.value.trim()
if (name.value == '') return
//
const index = history.value.findIndex((item) => item === name.value)
if (index !== -1) {
history.value.splice(index, 1)
}
history.value.unshift(name.value)
uni.setStorageSync('search', history.value)
}
const search = async () => {
await uni.navigateTo({
url: `${pageUrl['shopping-mall-goods-list']}?name=${name.value}`,
})
storeHistroy()
}
const onTapHistory = async (keyword: string) => {
await uni.navigateTo({
url: `${pageUrl['shopping-mall-goods-list']}?name=${keyword}`,
})
name.value = keyword
storeHistroy()
}
const onCancel = () => {
uni.navigateBack()
}
const onClearHistory = () => {
uni.showModal({
title: '',
content: '确定要清空历史搜索吗',
showCancel: true,
success: ({ confirm, cancel }) => {
if (confirm) {
uni.removeStorageSync('search')
history.value = []
}
},
})
}
const onDeleteHistory = (name: string) => {
uni.showModal({
title: '',
content: '确定要删除该条历史搜索吗',
showCancel: true,
success: ({ confirm, cancel }) => {
if (confirm) {
const index = history.value.findIndex((item) => item === name)
if (index !== -1) {
history.value.splice(index, 1)
uni.setStorageSync('search', history.value)
}
}
},
})
}
</script>
<template>
<view class="search">
<uni-search-bar
@confirm="search"
:focus="true"
v-model="name"
radius="100"
@cancel="onCancel"
/>
</view>
<view class="search-history">
<text>历史搜索</text>
<view class="trash">
<uni-icons type="trash" color="" size="24" @tap="onClearHistory" />
</view>
</view>
<view class="keyword">
<button
v-for="(item, index) in history"
:key="index"
@tap="onTapHistory(item)"
class="keyword-button"
aria-readonly
@longtap="onDeleteHistory(item)"
>
{{ item }}
</button>
</view>
</template>
<style lang="scss">
page {
padding-left: 20rpx;
}
.search {
padding-right: 20rpx;
}
.search-history {
display: flex;
justify-content: space-between;
margin-top: 10px;
font-size: 14px;
.trash {
display: flex;
align-items: center;
margin-right: 50rpx;
}
}
.keyword {
display: flex;
flex-wrap: wrap;
margin-top: 20px;
float: left;
button {
background-color: #f2f2f2;
border-radius: 25rpx;
color: #333333;
font-size: 24rpx;
align-items: center;
margin: 0 10rpx 15rpx 15rpx;
justify-content: flex-start;
// padding-left: 10px;
display: flex;
.delete-icon {
margin-left: 5px;
margin-right: -10rpx;
}
}
}
</style>

View File

@ -0,0 +1,90 @@
<script setup lang="ts">
import { computed } from 'vue'
import { fullUrl } from '@/utils/common'
import type { categoryPanelItem } from '@/types/home'
import { pageUrl } from '@/utils/constants'
const props = defineProps<{
list: categoryPanelItem[]
}>()
const change = (event: UniHelper.UniGridOnChangeEvent) => {
uni.setStorageSync('activeCategoryIndex', event.detail.index)
uni.navigateTo({
url: pageUrl['shopping-mall-category'],
})
}
const groupedCategoryList = computed(() => {
if (!props.list) return []
const chunkSize = 5
const groups = []
for (let i = 0; i < props.list.length; i += chunkSize) {
groups.push(props.list.slice(i, i + chunkSize))
}
return groups
})
</script>
<template>
<swiper class="swiper" :indicator-dots="true" v-if="props.list.length > 0">
<swiper-item v-for="(group, groupIndex) in groupedCategoryList" :key="groupIndex">
<uni-grid :column="5" :highlight="true" @change="change" :showBorder="false" :square="false">
<uni-grid-item v-for="(item, index) in group" :index="index" :key="index">
<view class="grid-item-box">
<image :src="fullUrl(item.icon)" class="image" mode="aspectFill" />
<text class="text">{{ item.name }}</text>
</view>
</uni-grid-item>
</uni-grid>
</swiper-item>
</swiper>
</template>
<style lang="scss" scoped>
.swiper {
background-color: white;
}
.image {
width: 75rpx;
height: 75rpx;
margin-top: 10rpx;
}
.text {
font-size: 24rpx;
margin-top: 10rpx;
}
.grid-dynamic-box {
margin-bottom: 10rpx;
}
.grid-item-box {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 10rpx 0;
}
.grid-item-box-row {
flex: 1;
flex-direction: row;
align-items: center;
justify-content: center;
padding: 10rpx 0;
}
.grid-dot {
position: absolute;
top: 5rpx;
right: 15rpx;
}
.swiper {
height: 190rpx;
margin-top: 15rpx;
}
</style>

View File

@ -0,0 +1,137 @@
<script setup lang="ts">
import type { hotRecommendItem } from '@/types/home'
import { fullUrl } from '@/utils/common'
import { pageUrl } from '@/utils/constants'
const props = withDefaults(
defineProps<{
list: hotRecommendItem[]
}>(),
{
list: () => [],
},
)
//
const jumpTo = (item: hotRecommendItem) => {
if (item.goods_id) {
return uni.navigateTo({
url: `${pageUrl['shopping-mall-goods-detail']}?id=${item.goods_id}`,
})
}
if (item.classify_id) {
return uni.navigateTo({
url: `${pageUrl['shopping-mall-goods-list']}?classify_id=${item.classify_id}`,
})
}
}
</script>
<template>
<view v-if="props.list.length > 0" class="recommend">
<uni-section title="热卖推荐" type="line">
<view class="section">
<uni-grid :column="2" :showBorder="false" :square="false" :highlight="false">
<uni-grid-item v-for="(item, index) in props.list" :key="index">
<view class="grid-item" @tap="jumpTo(item)">
<view class="titles">
<text class="title ellipsis">{{ item.title }}</text>
<text class="sub-title ellipsis">{{ item.sub_title }}</text>
</view>
<view class="content">
<view class="tag-container">
<view class="tag" v-for="(tag, tagIndex) in item.tags" :key="tagIndex">
<text>{{ tag }}</text>
</view>
</view>
<view>
<image class="image" :src="fullUrl(item.image)"></image>
</view>
</view>
</view>
</uni-grid-item>
</uni-grid>
</view>
</uni-section>
</view>
</template>
<style lang="scss" scoped>
.recommend {
margin-top: 10rpx;
}
.section {
background-color: #f9f9f9;
}
.grid-item {
margin: 0rpx 5rpx 10rpx;
padding: 0rpx 10rpx;
flex-direction: column;
background-color: #fff;
}
.titles {
display: flex;
flex-direction: column;
padding-bottom: 20rpx;
.title {
font-size: 30rpx;
font-weight: bold;
}
.sub-title {
font-size: 24rpx;
color: #666;
min-height: 35rpx;
line-height: 40rpx;
}
}
.content {
display: flex;
justify-content: space-between;
}
.tag-container {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 5rpx;
}
.tag {
display: flex;
align-items: center;
padding: 5rpx 15rpx;
font-size: 20rpx;
color: #fff;
background-color: #ff6347;
border-radius: 10rpx;
border: none;
box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
height: 40rpx;
max-width: 150rpx;
margin-bottom: 10rpx;
}
.tag {
text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.image {
width: 150rpx;
height: 150rpx;
}
.ellipsis {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
}
</style>

View File

@ -0,0 +1,130 @@
<script setup lang="ts">
import { pageUrl } from '@/utils/constants'
type list = {
title: string
content: string
}[]
const props = withDefaults(
defineProps<{
list: list
color?: string
bgColor?: string
switchTime?: number
}>(),
{
list: () => [],
color: '#000',
bgColor: '#fff',
switchTime: 3,
},
)
const showContent = (content: string) => {
if (!content) return
content = encodeURIComponent(content)
uni.navigateTo({ url: `${pageUrl['rich-text']}?title=消息公告&content=${content}` })
}
</script>
<template>
<view v-if="props.list.length > 0" style="margin-top: 20rpx">
<view class="notice-box" :style="'background-color: ' + bgColor + ';'">
<view class="notice-icon">
<image src="/static/images/notice_bar.png" style="width: 200rpx" />
</view>
<scroll-view class="notice-content">
<swiper
class="swiper"
:autoplay="true"
:interval="switchTime * 1000"
:duration="1500"
:circular="true"
:vertical="true"
>
<swiper-item v-for="(item, index) in list" :key="index" class="notice-content-item">
<view class="swiper-item">
<view
class="notice-content-item-text-wrapper"
:style="'color: ' + color + ';'"
@tap="showContent(item.content)"
>
<text class="notice-content-item-text">{{ item.title }}</text>
<text class="icon-right"></text>
</view>
</view>
</swiper-item>
</swiper>
</scroll-view>
</view>
</view>
</template>
<style lang="scss" scoped>
.swiper {
height: 60rpx !important;
padding-bottom: 20rpx;
}
scroll-view {
flex: 1;
height: 100%;
overflow: hidden;
}
.notice-box {
width: 100%;
height: 60rpx;
padding: 0 10rpx;
overflow: hidden;
display: flex;
justify-content: flex-start;
}
.notice-icon {
display: flex;
flex-direction: column;
justify-content: center;
width: 140rpx;
margin-left: 10rpx;
}
.notice-content {
width: calc(100% - 220rpx);
position: relative;
font-size: 14px;
}
.notice-content-item {
width: 100%;
height: 60rpx;
text-align: left;
line-height: 60rpx;
flex: 1;
}
.notice-content-item-text-wrapper {
display: flex;
justify-content: space-between;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.notice-content-item-text {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@keyframes anotice {
0% {
transform: translateY(100%);
}
30% {
transform: translateY(0);
}
70% {
transform: translateY(0);
}
100% {
transform: translateY(-100%);
}
}
</style>

View File

@ -0,0 +1,105 @@
<script setup lang="ts">
import { nextTick, ref } from 'vue'
import { getBannerApi, getHotRecommendApi, getNoticeBartApi } from '@/api/home'
import type { bannerItem, noticeBarItem } from '@/types/home'
import { onLoad, onShow } from '@dcloudio/uni-app'
import categoryGroup from './components/categoryGroup.vue'
import noticeBar from './components/notice-bar.vue'
import hotRecommend from './components/hotRecommend.vue'
import { getTopCategoryApi } from '@/api/catogory'
import { useCartStore, useMemberStore } from '@/stores'
import { getCartTotalNumApi } from '@/api/cart'
//
const bannerList = ref<bannerItem[]>([])
const getBannerListData = async () => {
const res = await getBannerApi()
bannerList.value = res.result
}
//
const noticeBarList = ref<noticeBarItem[]>([])
const getNoticeBarData = async () => {
const res = await getNoticeBartApi()
noticeBarList.value = res.result
}
//
const categoryList = ref()
const getTopCategoryListData = async () => {
const res = await getTopCategoryApi()
categoryList.value = res.result
}
//
const hotRecommendList = ref()
const getHotRecommend = async () => {
const res = await getHotRecommendApi()
hotRecommendList.value = res.result
}
//
const shopGoodsListRef = ref()
const getIndexData = () => {
return Promise.all([
getBannerListData(),
getNoticeBarData(),
getTopCategoryListData(),
getHotRecommend(),
])
}
onLoad(() => {
getIndexData()
//
// #ifdef MP-WEIXIN
uni.showShareMenu({
withShareTicket: true,
menus: ['shareAppMessage'],
})
// #endif
})
onShow(() => {
nextTick(() => {
shopGoodsListRef.value.refresh()
})
const cartStore = useCartStore()
//
if (useMemberStore().profile) {
getCartTotalNumApi().then((res) => {
cartStore.setCartTotalNum(res.result)
})
} else {
cartStore.setCartTotalNum(0)
}
cartStore.setCartTabBadge()
})
const handleRefresh = async (status : number) => {
//
if (status === 2) {
getIndexData()
}
}
</script>
<template>
<shop-goods-list ref="shopGoodsListRef" @refresher-status-changed="handleRefresh" :options="{
auto: false,
}">
<template #top>
<shop-goods-search />
</template>
<template #middle>
<shop-swiper :list="bannerList" v-if="bannerList" />
<noticeBar :list="noticeBarList" />
<category-group :list="categoryList" v-if="categoryList" />
<hot-recommend :list="hotRecommendList" />
<uni-section title="商品列表" type="line"></uni-section>
</template>
</shop-goods-list>
</template>
<style lang="scss">
page {
background-color: #f9f9f9;
}
</style>