This commit is contained in:
砂糖
2026-02-07 18:01:13 +08:00
commit 8015759c65
2110 changed files with 269866 additions and 0 deletions

View File

@@ -0,0 +1,194 @@
<template>
<goods-sku-popup :value="value" @input="onChangeValue" border-radius="20" :custom-action="findGoodsInfo"
:mode="skuMode" :defaultPrice="goods.price" :defaultStock="goods.stock" :maskCloseAble="true"
@open="openSkuPopup" @close="closeSkuPopup" @add-cart="addCart" @buy-now="buyNow" />
</template>
<script>
import { setCartTotalNum } from '@/utils/app'
import * as CartApi from '@/api/cart'
import * as GoodsApi from '@/api/goods'
import GoodsSkuPopup from '@/components/goods-sku-popup'
let goodsInfo;
export default {
components: {
GoodsSkuPopup
},
model: {
prop: 'value',
event: 'input'
},
props: {
// true 组件显示 false 组件隐藏
value: {
Type: Boolean,
default: false
},
// 模式 1:都显示 2:只显示购物车 3:只显示立即购买
skuMode: {
type: Number,
default: 1
},
// 商品详情信息
goods: {
type: Object,
default: {}
}
},
data() {
return {}
},
created() {
const app = this
const { goods } = app
goodsInfo = {
_id: goods.goodsId,
name: goods.name,
goods_thumb: goods.logo,
sku_list: app.getSkuList(),
spec_list: app.getSpecList()
}
},
methods: {
// 监听组件显示隐藏
onChangeValue(val) {
this.$emit('input', val)
},
/**
* 获取商品信息
* 这里可以看到每次打开SKU都会去重新请求商品信息,为的是每次打开SKU组件可以实时看到剩余库存
*/
findGoodsInfo() {
return new Promise((resolve, reject) => {
resolve(goodsInfo)
})
},
// 整理商品SKU列表
getSkuList() {
const app = this
const { goods: { name, logo, skuList } } = app
const skuData = []
if (skuList) {
skuList.forEach(item => {
skuData.push({
_id: item.id,
sku_id: item.skuId,
goods_id: item.goodsId,
name: name,
image: item.logo ? item.logo : logo,
price: item.price,
stock: item.stock,
spec_value_ids: item.specIds,
sku_name_arr: app.getSkuNameArr(item.specIds)
})
})
}
return skuData
},
// 获取sku记录的规格值列表
getSkuNameArr(specValueIds) {
const app = this
const defaultData = ['默认']
const skuNameArr = []
if (specValueIds) {
specValueIds.forEach((valueId, specId) => {
const specValueName = app.getSpecValueName(valueId, specId)
skuNameArr.push(specValueName)
})
}
return skuNameArr.length ? skuNameArr : defaultData
},
// 获取指定的规格值名称
getSpecValueName(valueId, specId) {
const app = this
const { goods: { specList } } = app
const res = specList[specId].valueList.find(specValue => {
return specValue.specValueId == valueId
})
if (res) {
return res.specValue
} else {
return ""
}
},
// 整理规格数据
getSpecList() {
const { goods: { specList } } = this
const defaultData = [{ name: '默认', list: [{ name: '默认' }] }]
const specData = []
if (specList) {
specList.forEach(group => {
const children = []
group.valueList.forEach(specValue => {
children.push({ name: specValue.specValue })
})
specData.push({
name: group.name,
list: children
})
})
}
return specData.length ? specData : defaultData
},
// sku组件 开始-----------------------------------------------------------
openSkuPopup() {
// console.log("监听 - 打开sku组件")
},
closeSkuPopup() {
// console.log("监听 - 关闭sku组件")
},
// 加入购物车按钮
addCart(selectShop) {
const app = this
console.log(selectShop)
const { goods_id, sku_id, buy_num } = selectShop
CartApi.save(goods_id, '+', sku_id, buy_num)
.then(result => {
// 隐藏当前弹窗
app.onChangeValue(false)
// 购物车商品总数量
const cartTotal = result.data.cartTotal
// 缓存购物车数量
setCartTotalNum(cartTotal)
// 传递给父级
app.$emit('addCart', cartTotal)
})
},
// 立即购买
buyNow(selectShop) {
// 跳转到订单结算页
this.$navTo('pages/settlement/goods', {
mode: 'buyNow',
goodsId: selectShop.goods_id,
skuId: selectShop.sku_id,
buyNum: selectShop.buy_num
})
// 隐藏当前弹窗
this.onChangeValue(false)
}
}
}
</script>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,587 @@
<template>
<view class="container">
<!--店铺切换-->
<Location v-if="storeInfo" :storeInfo="storeInfo"/>
<!-- 搜索框 -->
<Search tips="请输入搜索关键字..." @event="$navTo('pages/search/index')" />
<view class="cate-content dis-flex" v-if="list.length > 0">
<!-- 左侧 分类 -->
<scroll-view class="cate-left f-28" :scroll-y="true" :style="{ height: `${scrollHeight}px` }">
<view v-for="(item, index) in list" :key="index">
<text class="cart-badge" v-if="item.total">{{ item.total }}</text>
<view class="type-nav" :class="{ selected: curIndex == index }" @click="handleSelectNav(index)">
<image class="logo" lazy-load :lazy-load-margin="0" v-if="item.logo" :src="item.logo"></image>
<view class="name">{{ item.name }}</view>
</view>
</view>
</scroll-view>
<!-- 右侧 商品 -->
<scroll-view class="cate-right b-f" :scroll-top="scrollTop" :scroll-y="true" :style="{ height: `${scrollHeight}px` }">
<view v-if="list[curIndex]">
<view class="cate-right-cont">
<view class="cate-two-box">
<view v-if="list[curIndex].goodsList.length" class="cate-cont-box">
<view class="flex-five item" v-for="(item, idx) in list[curIndex].goodsList" :key="idx">
<view class="cate-img">
<image v-if="item.logo" lazy-load :lazy-load-margin="0" :src="item.logo" @click="onTargetGoods(item.id)"></image>
</view>
<view class="cate-info">
<view class="base">
<text class="name text">{{ item.name }}</text>
<text class="stock text">库存:{{ item.stock ? item.stock : 0 }} 已售:{{ item.initSale ? item.initSale : 0 }}</text>
</view>
<view class="action">
<text class="price">{{ item.price ? item.price : 0 }}</text>
<view class="cart">
<view v-if="item.isSingleSpec === 'Y'" class="singleSpec">
<view class="ii do-minus" v-if="item.buyNum" @click="onSaveCart(item.id, '-')"></view>
<view class="ii num" v-if="item.buyNum">{{ (item.buyNum != undefined) ? item.buyNum : 0 }}</view>
<view class="ii do-add" v-if="item.stock > 0" @click="onSaveCart(item.id, '+')"></view>
</view>
<view v-if="item.isSingleSpec === 'N'" class="multiSpec">
<text class="num-badge" v-if="item.buyNum">{{ item.buyNum }}</text>
<view class="select-spec" @click="onShowSkuPopup(2, item.id)">选规格</view>
</view>
</view>
</view>
</view>
</view>
</view>
<empty v-if="!list[curIndex].goodsList.length" :isLoading="isLoading" tips="暂无商品~"></empty>
</view>
</view>
</view>
</scroll-view>
</view>
<!-- 商品SKU弹窗 -->
<SkuPopup v-if="!isLoading" v-model="showSkuPopup" :skuMode="skuMode" :goods="goods" @addCart="onAddCart"/>
<view class="flow-fixed-footer b-f m-top10">
<view class="dis-flex chackout-box">
<view class="chackout-left pl-12">
<view class="col-amount-do">总金额<text class="amount">{{ totalPrice.toFixed(2) }}</text></view>
<view class="col-amount-view">共计{{ totalNum }} </view>
</view>
<view class="chackout-right" @click="doSubmit()">
<view class="flow-btn f-32">去结算</view>
</view>
</view>
</view>
<empty v-if="!list.length" :isLoading="isLoading" />
</view>
</template>
<script>
import { setCartTabBadge, setCartTotalNum } from '@/utils/app'
import * as CartApi from '@/api/cart'
import * as GoodsApi from '@/api/goods'
import * as settingApi from '@/api/setting'
import Search from '@/components/search'
import Empty from '@/components/empty'
import SkuPopup from './components/SkuPopup'
import Location from '@/components/page/location'
const App = getApp()
export default {
components: {
Search,
SkuPopup,
Empty,
Location
},
data() {
return {
goodsCart: [],
totalNum: 0,
totalPrice: 0.00,
// 列表高度
scrollHeight: 500,
// 一级分类:指针
curIndex: 0,
// 内容区竖向滚动条位置
scrollTop: 0,
// 分类列表
list: [],
// 正在加载中
isLoading: true,
showSkuPopup: false,
skuMode: 1,
goods: {},
storeInfo: null
}
},
/**
* 生命周期函数--监听页面加载
*/
onLoad() {
const app = this
// 设置分类列表高度
app.setListHeight()
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
const app = this;
// 获取页面数据
app.getPageData();
app.onGetStoreInfo();
uni.getLocation({
type: 'gcj02',
success(res){
uni.setStorageSync('latitude', res.latitude);
uni.setStorageSync('longitude', res.longitude);
app.onGetStoreInfo();
},
fail(e) {
// empty
}
})
},
methods: {
/**
* 获取页面数据
*/
getPageData() {
const app = this
app.isLoading = true
Promise.all([
// 获取分类列表
GoodsApi.cateList(),
// 获取购物车列表
CartApi.list()
])
.then(result => {
// 初始化分类列表数据
app.list = result[0].data;
app.totalNum = result[1].data.totalNum;
app.goodsCart = result[1].data.list;
setCartTotalNum(app.totalNum);
setCartTabBadge();
})
.finally(() => {
app.isLoading = false
app.totalPrice = 0
app.list.forEach(function(item, index) {
let total = 0
item.goodsList.forEach(function(goods, key) {
let totalBuyNum = 0
app.goodsCart.forEach(function(cart){
if (goods.id == cart.goodsId) {
total = total + cart.num
totalBuyNum = totalBuyNum + cart.num
app.totalPrice = app.totalPrice + (cart.goodsInfo.price * cart.num)
}
})
app.$set(app.list[index].goodsList[key], 'buyNum', totalBuyNum)
})
app.$set(app.list[index], 'total', total)
})
})
},
/**
* 获取默认店铺
* */
onGetStoreInfo() {
const app = this
settingApi.systemConfig()
.then(result => {
app.storeInfo = result.data.storeInfo
})
},
/**
* 跳转商品详情
*/
onTargetGoods(goodsId) {
this.$navTo(`pages/goods/detail`, { goodsId })
},
/**
* 设置分类列表高度
*/
setListHeight() {
const app = this
uni.getSystemInfo({
success(res) {
app.scrollHeight = res.windowHeight - 120
}
})
},
// 一级分类:选中分类
handleSelectNav(index) {
const app = this;
app.curIndex = index;
app.scrollTop = 0;
},
// 更新购物车
onSaveCart(goodsId, action) {
const app = this
return new Promise((resolve, reject) => {
CartApi.save(goodsId, action)
.then(result => {
app.getPageData();
resolve(result);
})
.catch(err => {
console.log(err);
})
})
},
// 更新购物车数量
onAddCart(total) {
this.getPageData();
this.$toast("添加购物车成功");
},
// 结算
doSubmit() {
if (this.totalPrice > 0) {
this.$navTo('pages/cart/index')
} else {
this.$error("请先选择商品")
}
},
onShowSkuPopup(skuMode, goodsId) {
const app = this
app.isLoading = true
return new Promise((resolve, reject) => {
GoodsApi.detail(goodsId)
.then(result => {
const goodsData = result.data
if (goodsData.skuList) {
goodsData.skuList.forEach(function(sku, index) {
goodsData.skuList[index].specIds = sku.specIds.split('-')
goodsData.skuList[index].skuId = sku.id
})
}
app.goods = goodsData
app.skuMode = skuMode
app.showSkuPopup = !app.showSkuPopup
console.log(app.skuMode)
app.isLoading = false
resolve(result)
})
.catch(err => reject(err))
})
},
},
/**
* 设置分享内容
*/
onShareAppMessage() {
const app = this
return {
title: _this.templet.shareTitle,
path: '/pages/category/index?' + app.$getShareUrlParams()
}
},
/**
* 分享到朋友圈
* 本接口为 Beta 版本,暂只在 Android 平台支持,详见分享到朋友圈 (Beta)
* https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/share-timeline.html
*/
onShareTimeline() {
const app = this
return {
title: _this.templet.shareTitle,
path: '/pages/category/index?' + app.$getShareUrlParams()
}
}
}
</script>
<style>
page {
background: #fff;
}
</style>
<style lang="scss" scoped>
.cate-content {
background: #fff;
margin-top: 118rpx;
/* #ifdef H5 */
margin-top: 124rpx;
/* #endif */
}
.cate-wrapper {
padding: 0 20rpx 20rpx 20rpx;
box-sizing: border-box;
}
/* 分类内容 */
.cate-content {
width: 100%;
}
.cate-left {
flex-direction: column;
display: flex;
width: 200rpx;
color: #444;
height: 100%;
background: #f8f8f8;
margin-bottom: 120rpx;
.cart-badge {
position: absolute;
right: 1rpx;
margin-top: 10rpx;
margin-right: 5rpx;
font-size: 18rpx;
background: #fa5151;
z-index: 999;
text-align: center;
line-height: 28rpx;
color: #ffffff;
border-radius: 50%;
min-width: 32rpx;
padding: 5rpx 13rpx 5rpx 13rpx;
}
}
.cate-right {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
overflow: hidden;
margin-bottom: 80rpx;
}
.cate-right-cont {
width: 100%;
display: flex;
flex-flow: row wrap;
align-content: flex-start;
padding-top: 10rpx;
}
.type-nav {
position: relative;
height: 140rpx;
text-align: center;
z-index: 10;
display: block;
font-size: 26rpx;
padding: 20rpx 0rpx 126rpx 0rpx;
.logo {
width: 60rpx;
height: 60rpx;
border-radius: 60rpx;
margin: 0rpx;
padding: 0rpx;
}
.name {
margin-top: 2rpx;
width: 100%;
overflow-x: hidden;
height: 40rpx;
line-height: 40rpx;
text-align: center;
}
}
.type-nav.selected {
color: #666666;
background: #ffffff;
border-right: none;
border-left: solid 10rpx #f03c3c;
font-weight: bold;
font-size: 28rpx;
}
.cate-cont-box {
margin-bottom: 10rpx;
padding-bottom: 10rpx;
overflow: hidden;
height: auto;
display: block;
.item {
height: 220rpx;
display: block;
padding-top: 5rpx;
border-radius: 3rpx;
margin-bottom: 5rpx;
}
}
.cate-cont-box .cate-img {
padding: 13rpx 10rpx 4rpx 10rpx;
display: block;
}
.cate-cont-box .cate-img image {
width: 160rpx;
height: 150rpx;
float: left;
border-radius: 5rpx;
display: block;
margin-top: 5rpx;
}
.cate-cont-box .cate-info {
text-align: left;
display: flex;
flex-direction: column;
font-size: 26rpx;
margin-left: 168rpx;
padding-bottom: 14rpx;
color: #444;
padding: 0 15rpx 30rpx 15rpx;
.base {
height: 100%;
display: block;
.text {
display: block;
float: left;
width: 100%;
}
.name {
font-weight: bold;
width: 100%;
font-size: 26rpx;
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.stock {
margin-top: 10rpx;
color: #999;
}
}
.action {
display: block;
height: 50rpx;
.price {
margin-top: 20rpx;
color: #f03c3c;
float: left;
font-size: 32rpx;
font-weight: bold;
}
.cart {
margin-top: 20rpx;
float: right;
font-size: 30rpx;
height: 60rpx;
.ii {
float: left;
text-align: center;
width: 60rpx;
cursor: pointer;
}
.do-add {
background: url('~@/static/icon/add.png') no-repeat;
background-size: 100% 100%;
width: 45rpx;
height: 45rpx;
}
.do-minus {
background-image: url('~@/static/icon/minus.png');
background-size: 100% 100%;
width: 45rpx;
height: 45rpx;
}
.multiSpec {
.num-badge {
position: absolute;
margin-top: 10rpx;
margin-right: 25rpx;
font-size: 18rpx;
background: #f03c3c;
text-align: center;
line-height: 36rpx;
color: #ffffff;
border-radius: 50%;
min-width: 36rpx;
padding: 2rpx;
}
.select-spec {
border: solid 1rpx $fuint-theme;
padding: 10rpx 20rpx 10rpx 36rpx;
font-size: 25rpx;
border-radius: 5rpx;
color: #ffffff;
background: $fuint-theme;
}
}
}
}
}
.cate-two-box {
width: 100%;
padding: 0 2px;
}
// 底部操作栏
.flow-fixed-footer {
position: fixed;
bottom: var(--window-bottom);
width: 100%;
background: #fff;
border-top: 1px solid #eee;
z-index: 11;
padding-top: 8rpx;
.chackout-left {
font-size: 28rpx;
height: 98rpx;
color: #777;
flex: 4;
padding-left: 12px;
text-align: right;
padding-right: 40rpx;
.col-amount-do {
font-size: 35rpx;
margin-top: 5rpx;
margin-bottom:5rpx;
.amount {
color: #f03c3c;
font-weight: bold;
}
}
}
.chackout-right {
font-size: 34rpx;
flex: 2;
}
// 提交按钮
.flow-btn {
background: linear-gradient(to right, $fuint-theme, $fuint-theme);
color: #fff;
text-align: center;
line-height: 92rpx;
display: block;
font-size: 28rpx;
border-radius: 5rpx;
margin-right: 20rpx;
// 禁用按钮
&.disabled {
background: #ff9779;
}
}
}
</style>