新增营销系统、分销系统、会员功能、门店、提现功能

This commit is contained in:
Shaw
2024-02-08 21:01:37 +08:00
parent 68b3f2dcc3
commit 17c043348a
1398 changed files with 81279 additions and 56269 deletions

View File

@ -13,7 +13,7 @@
>
<uv-image
class="attr-image"
:src="curAttr.image || detailData.storeInfo.image"
:src="curAttr.image || storeInfo.image"
width="150rpx"
height="150rpx"
></uv-image>
@ -22,8 +22,44 @@
{{ storeInfo.storeName }}
</view>
<view class="attr-info-bottom">
<view class="price">¥{{ curAttr.price }}</view>
<view class="stock">库存{{ curAttr.stock }}</view>
<!-- 普通SKU -->
<template v-if="actionType==='singleBuy' || !curAttr.campaignType">
<view class="price">¥{{ curAttr.price }}</view>
<view class="stock">库存{{ curAttr.stock }}</view>
</template>
<!-- 活动SKU -->
<template v-else>
<view class="flex flex-ai__end">
<view
class="price-name"
v-if="curAttr.campaignState===1">{{ activityName }}
</view>
<view class="price primary-color">¥{{ curAttr.campaignPrice }}</view>
<view class="old-price">{{ curAttr.price }}</view>
</view>
<view class="stock">库存{{ curAttr.campaignStock }}</view>
</template>
</view>
</view>
</view>
<!-- 没有选中sku -->
<view
class="goodAttrSelect-goods"
v-else
>
<uv-image
class="attr-image"
:src="storeInfo.image"
width="150rpx"
height="150rpx"
></uv-image>
<view class="attr-info">
<view class="name">
{{ storeInfo.storeName }}
</view>
<view class="attr-info-bottom">
<view class="price">¥{{ storeInfo.price }}</view>
<view class="stock">库存{{ storeInfo.stock }}</view>
</view>
</view>
</view>
@ -59,6 +95,7 @@
inputmode="numeric"
v-model="storeNum"
@blur="(e)=>handleCartNumberInputChange(e,curAttr)"
@input="(e)=>cartNumberInput(e,curAttr)"
>
</view>
<view
@ -73,11 +110,6 @@
></uv-icon>
</view>
</view>
<!-- <uv-number-box
v-model="storeNum"
min="1"
:max="curAttr.stock"
></uv-number-box>-->
</view>
</view>
<div class="line"></div>
@ -91,9 +123,11 @@
{{ item.attrName }}
</view>
<view class="goodAttrSelect-attr-content">
<space wrap="warp" gap>
<space
wrap="warp"
gap>
<view
:class="{ attr: true, check: selectedAttr[index] === attr }"
:class="{ attr: true, check: selectedAttr[index] === attr,disabled:checkAttrDisable(index,attr)}"
v-for="(attr, attrIndex) in item.attrValueArr"
:key="attrIndex"
@tap="handleSelectAttr(index, attr)"
@ -104,8 +138,7 @@
</view>
<view class="goodAttrSelect-action">
<uv-button
v-if="curAttr"
:disabled="curAttr.stock===0 || storeNum===0"
:disabled="!curAttr || curAttr&&curAttr.stock===0 || storeNum===0"
round
block
type="primary"
@ -119,104 +152,146 @@
</template>
<script setup>
import { ref, unref } from 'vue';
import { getProductDetail } from '@/api/product'
import {computed, nextTick, ref, toRefs} from 'vue';
import { useInterface } from "@/hooks/useInterface";
import Popup from '@/components/Popup/index.vue';
const props = defineProps(["id"])
const emit = defineEmits(['onSelect', 'submit', 'close'])
const popupRef = ref()
const selectedAttr = ref([])
const visible = ref(false)
const detailData = ref(null)
const storeInfo = ref(null)
const productAttr = ref(null)
const productValue = ref(null)
const storeNum = ref(1)
const curAttr = ref(null)
const defaultSelectAttrStr = ref(undefined)
const selectAttrPanel = ref(false)
const customStyle = {
width: '40rpx',
height: '40rpx',
padding: '0px',
border: '1rpx solid #cccccc',
}
const handleGetDetail = async (id) => {
const detail = await getProductDetail(id)
if (detail) {
detailData.value = detail
storeInfo.value = detail.storeInfo
productAttr.value = detail.productAttr
productValue.value = detail.productValue
if (!defaultSelectAttrStr.value) {
// 设置默认选中
let attr = []
detail.productAttr.forEach((item, i) => {
attr[i] = item.attrValueArr[0]
})
selectedAttr.value = attr
let selectedAttrStr = selectedAttr.value.join(',')
curAttr.value = productValue.value[selectedAttrStr]
} else {
selectedAttr.value = unref(defaultSelectAttrStr).split(',')
curAttr.value = productValue.value[defaultSelectAttrStr.value]
}
if(storeNum.value>curAttr.value.stock){
storeNum.value = curAttr.value.stock
}
}
}
const handleSelectAttr = (index, attr) => {
selectedAttr.value[index] = attr
let selectedAttrStr = selectedAttr.value.join(',')
curAttr.value = productValue.value[selectedAttrStr]
if(storeNum.value === 0 && curAttr.value.stock >0){
storeNum.value = 1
}
if(storeNum.value>curAttr.value.stock){
storeNum.value = curAttr.value.stock
}
}
const handleSubmit = () => {
// let value = []
// productAttr.value.map(item => {
// value.push(selectedAttr.value[item.attrName] || '')
// })
// if (value.includes('')) {
// uni.showToast({
// title: '请选择规格',
// icon: 'none',
// duration: 2000,
// })
// return
// }
if(curAttr.value.stock!==0 && storeNum.value!==0){
let selectedAttrStr = selectedAttr.value.join(',')
emit('submit', {
store: productValue.value[selectedAttrStr],
num: storeNum.value
})
emit('select', {
store: productValue.value[selectedAttrStr],
num: storeNum.value
})
emit('onSelect', {
store: productValue.value[selectedAttrStr],
num: storeNum.value
})
}
}
// ====================== hooks ============================
const {toast} = useInterface()
// ====================== 组件相关 ============================
const emit = defineEmits(['select', 'close'])
/**
* @property goodsDetail 商品详情
* @property skuId sku打开时默认sku
*/
const props = defineProps({
goodsDetail: {
type: Object,
},
skuId: {
type: [String, Number],
}
})
const {goodsDetail, skuId} = toRefs(props)
// ======================= 数据相关 ==============================
const storeInfo = computed(() => goodsDetail.value.storeInfo) // 商品信息
const productAttr = computed(() => goodsDetail.value.productAttr) // 商品sku attr细腻些
const productValue = computed(() => goodsDetail.value.productValue) // 商品sku value信息
const activityName = computed(() => {
if (!curAttr.value) return ''
return ['拼团价', '秒杀价', '折扣价'][curAttr.value.campaignType - 1]
})
// ======================= sku 操作相关 ==========================
const selectedAttr = ref([]) // 当前选中sku的attr key信息
const curAttr = ref(null) // 当前选中的sku
/**
* 检查是否为disable
* @param attrRowIndex
* @param attr
* @return {boolean}
*/
function checkAttrDisable(attrRowIndex, attr) {
// 模拟当前值选中
const selectedAttrMock = JSON.parse(JSON.stringify(selectedAttr.value))
selectedAttrMock[attrRowIndex] = attr
const filterAttrMock = selectedAttrMock.filter(i => i);
// 如果不是最后一次选择属性构不成sku比较直接return false
if (filterAttrMock.length < productAttr.value.length) return false
const mockNameStr = filterAttrMock.filter(i => i)
for (const skuName in productValue.value) {
if (skuName.includes(mockNameStr)) {
const sku = productValue.value[skuName]
const trueStock = sku.campaignState === 1 ? sku.campaignStock : sku.stock // 真实库存 (区分活动和普通)
// 从有库存的开始查不要从没库存的直接break
// 只要有一条includes且库存大于0就直接break
if (trueStock > 0) return false
}
}
return true
}
/**
* 设置默认sku选中
*/
function handleSetDefaultSkuSelect() {
const skuList = productValue.value
// 遍历sku的对象数组匹配到id相同的项
if (skuId.value) {
for (const skuName in skuList) {
const sku = skuList[skuName]
const trueStock = sku.campaignState === 1 ? sku.campaignStock : sku.stock // 真实库存 (区分活动和普通)
if (sku.id === skuId.value && trueStock > 0) {
// 设置默认选中
selectedAttr.value = skuName.split(',')
curAttr.value = sku
break;
}
}
}
// 如果没有找到那就默认选中sku的第一个、
if (selectedAttr.value.filter(i => i).length <= 0) {
const skuNameList = Reflect.ownKeys(skuList)
if (skuNameList.length > 0) {
let defaultSku, defaultSkuName
for (const skuName of skuNameList) {
const sku = skuList[skuName];
const trueStock = sku.campaignState === 1 ? sku.campaignStock : sku.stock // 真实库存 (区分活动和普通)
if (trueStock > 0) {
defaultSku = sku
defaultSkuName = skuName
break;
}
}
if (defaultSku) {
selectedAttr.value = defaultSkuName.split(',')
curAttr.value = defaultSku
}
}
}
handleSetStoreNum()
}
/**
* sku选中
* @param attrRowIndex sku属性row索引
* @param attrItemName sku选中的item的名称object的key
*/
const handleSelectAttr = (attrRowIndex, attrItemName) => {
if (checkAttrDisable(attrRowIndex, attrItemName)) return toast({title: "商品抢完啦~看看其他的吧"})
curAttr.value = undefined
if (selectedAttr.value[attrRowIndex] && selectedAttr.value[attrRowIndex] === attrItemName) {
selectedAttr.value[attrRowIndex] = ''
return;
}
selectedAttr.value[attrRowIndex] = attrItemName
if (selectedAttr.value.filter(i => i).length === productAttr.value.length) {
curAttr.value = productValue.value[selectedAttr.value.join(',')]
// 设置库存
handleSetStoreNum()
}
}
/**
* 提交
*/
const handleSubmit = () => {
if (!curAttr.value) return;
if (curAttr.value.stock === 0 && storeNum.value === 0) return
emit('select', {
store: curAttr.value,
num: storeNum.value
})
close()
}
// ========================== 数量相关 ===========================
const storeNum = ref(1) // 数量
/**
* 用户手动输入改变数量
* @param e
@ -225,15 +300,26 @@ const {toast} = useInterface()
*/
function handleCartNumberInputChange(e, item) {
const value = Number(e.detail.value)
const trueStock = item.campaignState === 1 ? item.campaignStock : item.stock // 真实库存 (区分活动和普通)
if (value <= 0) {
storeNum.value = 1
} else if (value > item.stock) {
storeNum.value = item.stock
} else if (value > trueStock) {
storeNum.value = trueStock
} else {
storeNum.value = value
storeNum.value = value.toString().replace(/^0+/, '')
}
}
/**
* 购买数量验证
*/
function cartNumberInput(e, item){
const pattern = /^0+|[.]*/g;
nextTick(() => {
storeNum.value = e.detail.value.replace(pattern,'');
})
}
/**
* 用户点击购物车+-改变数量
* @param item
@ -241,15 +327,16 @@ function handleCartNumberInputChange(e, item) {
* @returns {*}
*/
function handleCartNumberChange(item, type = 'plus') {
const trueStock = item.campaignState === 1 ? item.campaignStock : item.stock // 真实库存 (区分活动和普通)
if (type === 'plus') {
if (storeNum.value + 1 > item.stock){
storeNum.value =item.stock
} else{
if (storeNum.value + 1 > trueStock) {
storeNum.value = trueStock
} else {
storeNum.value += 1
}
} else {
if (storeNum.value - 1 <= 0){
if (storeNum.value - 1 <= 0) {
storeNum.value = 1
} else {
storeNum.value -= 1
@ -257,20 +344,43 @@ function handleCartNumberChange(item, type = 'plus') {
}
}
/**
* 设置选中数量
* 当sku发生改变的时候需要调用
*/
const handleSetStoreNum = () => {
if (!curAttr.value) return storeNum.value = 0
if (storeNum.value === 0 && curAttr.value.stock > 0) {
storeNum.value = 1
}
if (storeNum.value > curAttr.value.stock) {
storeNum.value = curAttr.value.stock
}
}
const open = (cartNum = 1, selectAttrStr = undefined) => {
defaultSelectAttrStr.value = selectAttrStr
storeNum.value = cartNum
handleGetDetail(props.id)
// ========================= 弹窗相关 ============================
const popupRef = ref()
const actionType = ref('select') // select:普通选择 cart:购物车选择 buy:普通下单 activeBuy:活动下单 singleBuy:单独购买
/**
* 打开弹窗
* @param cartNum
* @param action 打开弹窗类型
*/
const open = (cartNum = 1, action = 'select') => {
actionType.value = action
storeNum.value = cartNum || 1
const attrLength = productAttr.value.length
selectedAttr.value = new Array(attrLength).fill('')
handleSetDefaultSkuSelect()
popupRef.value.show()
}
const close = () => {
popupRef.value.close()
curAttr.value = null
emit('close')
popupRef.value.close()
}
defineExpose({
open,
close
@ -296,6 +406,7 @@ defineExpose({
flex-direction: column;
justify-content: space-between;
margin-left: 30rpx;
.name {
font-size: 28rpx;
line-height: 40rpx;
@ -314,7 +425,24 @@ defineExpose({
.price {
font-size: 30rpx;
line-height: 42rpx;
color: #333333;
color: #ee6d46;
font-weight: bold;
}
.price-name {
background: #ee6d46;
color: #fff;
border-radius: 10rpx;
font-size: 24rpx;
font-style: italic;
padding: 3rpx 10rpx;
margin-right: 5rpx;
}
.old-price {
font-size: 22rpx;
color: grey;
text-decoration: line-through;
}
.stock {
@ -346,7 +474,7 @@ defineExpose({
}
&-title {
margin:0 0 30rpx 30rpx;
margin: 0 0 30rpx 30rpx;
}
&-content {
@ -371,10 +499,17 @@ defineExpose({
justify-content: center;
align-items: center;
margin: 0 0 30rpx 30rpx;
&.check {
background: #333333;
color: #fff;
}
&.disabled {
border: none;
background: #f5f7fa;
color: #ccced3;
}
}
.cart-num {
@ -393,6 +528,7 @@ defineExpose({
.button {
font-size: 32rpx;
width: 40rpx;
height: 40rpx;
aspect-ratio: 1/1;
border-radius: 5rpx;
box-sizing: border-box;
@ -405,11 +541,14 @@ defineExpose({
&:active {
scale: 1.2;
}
&.disabled{
&.disabled {
border-color: #dddddd;
:deep(.uv-icon__icon){
:deep(.uv-icon__icon) {
color: #dddddd !important;
}
&:active {
scale: 1;
}
@ -417,7 +556,7 @@ defineExpose({
}
}
.gap{
.gap {
gap: 20rpx;
}