feat(商品)

This commit is contained in:
Ankkaya 2024-06-12 18:33:39 +08:00
parent 1c71006dc5
commit d5e3d34e0f
13 changed files with 569 additions and 450 deletions

View File

@ -12,14 +12,35 @@
"path": "pages/index/redirect"
},
{
"path": "pages/index/order",
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "订单"
"navigationBarTitleText": "首页"
},
"meta": {
"auth": true
}
},
{
"path": "pages/index/order",
"style": {
"navigationBarTitleText": "订单",
"enablePullDownRefresh": true
},
"meta": {
"auth": true
}
},
{
"path": "pages/index/product",
"style": {
"navigationBarTitleText": "产品",
"enablePullDownRefresh": true
},
"meta": {
"auth": false
}
},
{
"path": "pages/index/my",
"style": {
@ -38,15 +59,6 @@
"auth": false
}
},
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "首页"
},
"meta": {
"auth": true
}
},
{
"path": "pages/index/login",
"style": {
@ -55,15 +67,6 @@
"meta": {
"auth": false
}
},
{
"path": "pages/index/product",
"style": {
"navigationBarTitleText": "产品"
},
"meta": {
"auth": false
}
}
],
"subPackages": [

View File

@ -2,7 +2,7 @@
<pb-layout navbar="inner" tabbar="/pages/index/index" :bgStyle="bgStyle">
<view class="dashboard-module ss-p-x-30">
<view class="merchant-info flex align-center">
<image class="logo" :src="merchantInfo.logo" mode="aspectFill"></image>
<image class="logo" :src="merchantInfo?.logo" mode="aspectFill"></image>
<view class="detail flex flex-column justify-center gap-10">
<view class="name ss-font-26">{{ merchantInfo.name }}</view>

View File

@ -42,7 +42,7 @@
<script setup>
import { ref, computed } from 'vue'
import $store from '@/peach/store'
import AuthUtil from '@/peach/api/member/auth'
import peach from '@/peach'
const bgStyle = {
@ -76,7 +76,7 @@ const state = ref({
],
})
const userStore = $store('user')
const userStore = peach.$store('user')
const remain = computed(() => {
return userStore.userWallet?.balance
@ -99,7 +99,7 @@ function logOut() {
return
}
await AuthUtil.logout()
peach.$store('user').logout()
userStore.logOut()
peach.$router.go('/pages/index/login')
},
})

View File

@ -124,7 +124,7 @@
<script setup>
import { ref } from 'vue'
import OrderApi from '@/peach/api/trade/order'
import { onLoad, onReachBottom, onPullDownRefresh } from '@dcloudio/uni-app'
import { onLoad, onShow, onReachBottom, onPullDownRefresh } from '@dcloudio/uni-app'
import { fen2yuan, formatOrderColor, formatOrderStatus, handleOrderButtons } from '@/peach/hooks/useGoods'
import peach from '@/peach'
import _, { isEmpty } from 'lodash'
@ -223,6 +223,10 @@ onLoad(async (options) => {
if (options.type) {
state.value.currentTab = options.type
}
})
onShow(async () => {
resetPagination(state.value.pagination)
await getOrderList()
})

View File

@ -15,6 +15,7 @@
:data="item"
:topRadius="10"
:bottomRadius="10"
@refresh="refresh"
@click="peach.$router.go('/pages/product/manageGoods', { id: item.id, mark: 'detail' })"
/>
</view>
@ -42,7 +43,7 @@
<script setup>
import { ref } from 'vue'
import { onLoad, onReachBottom } from '@dcloudio/uni-app'
import { onLoad, onShow, onReachBottom, onPullDownRefresh } from '@dcloudio/uni-app'
import GoodApi from '@/peach/api/trade/goods'
import peach from '@/peach'
import _ from 'lodash'
@ -93,6 +94,7 @@ function addGoods() {
selectedProperty: null,
goodsInfo: null,
skus: null,
specType: false,
})
peach.$router.go('/pages/product/manageGoods', {
title: '添加商品',
@ -107,13 +109,28 @@ function loadMore() {
getList()
}
onLoad(async (options) => {
function refresh() {
resetPagination(state.value.pagination)
getList()
}
onShow(async () => {
resetPagination(state.value.pagination)
await getList()
})
onReachBottom(() => {
loadMore()
})
//
onPullDownRefresh(() => {
resetPagination(state.value.pagination)
getList()
setTimeout(function () {
uni.stopPullDownRefresh()
}, 800)
})
</script>
<style lang="scss" scoped>
@ -124,6 +141,7 @@ onReachBottom(() => {
bottom: 70px;
right: 20px;
font-size: 80rpx;
z-index: 999;
}
.goods-list-box {

View File

@ -97,7 +97,7 @@ const props = defineProps({
},
})
const formData = ref({})
const specType = computed(() => peach.$store('trade').goodsInfo?.specType || false)
const specType = computed(() => peach.$store('trade').specType)
watch(
() => props.skus,

View File

@ -1,108 +1,94 @@
import { ref, computed } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import peach from "@/peach";
import GoodsApi from "@/peach/api/trade/goods";
import { SPEC_TYPE, SKU_RULE_CONFIG } from "./config";
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import peach from '@/peach'
import GoodsApi from '@/peach/api/trade/goods'
import { SPEC_TYPE, SKU_RULE_CONFIG } from './config'
const pickerRef = ref(null);
const pickerRef = ref(null)
// 多属性商品 sku 列表
const skus = ref([]);
const skus = ref([])
const propertyList = ref([]);
// 已选中商品规格的规格属性和值
const propertyList = ref([])
const goodsPropertyList = ref([]);
const goodsPropertyList = ref([])
const propertyListRef = ref(null);
const propertyListRef = ref(null)
const canEdit = computed(() => peach.$store("trade").canEdit);
const canEdit = computed(() => peach.$store('trade').canEdit)
const formData = ref({
specType: true,
specText: SPEC_TYPE[0].label,
});
})
async function showPropertyList() {
await getGoodsProperty();
propertyListRef.value.onOpen();
await getGoodsProperty()
propertyListRef.value.onOpen()
}
function onRDPickerConfirm(e) {
formData.value.specType = SPEC_TYPE[e.value[0]].value;
formData.value.specText = SPEC_TYPE[e.value[0]].label;
formData.value.specType = SPEC_TYPE[e.value[0]].value
formData.value.specText = SPEC_TYPE[e.value[0]].label
// 如果商品规格不一致,则需要重新初始化 sku 列表
initSku();
peach.$store("trade").specType = SPEC_TYPE[e.value[0]].value;
initSku()
}
function pickerProperty() {
if (canEdit.value) {
let index = formData.value.specType ? 1 : 0;
let index = formData.value.specType ? 1 : 0
console.log(index);
pickerRef.value.onOpen([index]);
pickerRef.value.onOpen([index])
}
}
async function onPropertyConfirm(e) {
await getGoodsProperty();
console.log(e);
await getGoodsProperty()
console.log(e)
}
function onConfirm() {
console.log(skus.value);
console.log(skus.value)
}
async function getGoodsProperty() {
let { data } = await GoodsApi.getHistoryProperty();
let { data } = await GoodsApi.getHistoryProperty()
// 此处处理商品详情情况下,多规格属性
// 把 propertyList 中 id 相同的属性合并,并去重
propertyList.value = peach.$store("trade").selectedProperty;
propertyList.value = peach.$store('trade').selectedProperty || []
console.log(propertyList.value);
if (propertyList.value) {
// 根据已经选择数据,设置默认选中
data.forEach((item) => {
// 判断属性是否已经选中
let propertyParent = propertyList.value.find(
(sitem) => sitem?.id === item.id
);
let propertyParent = propertyList.value.find((sitem) => sitem?.id === item.id)
item.checked = propertyParent ? true : false;
item.checked = propertyParent ? true : false
// 如果属性已经选中,查询子类中是否有选中
if (item.checked) {
item.propertyValues.forEach((child) => {
let childResult = propertyParent?.children.some(
(schild) => schild === child.id
);
child.checked = childResult ? true : false;
});
let childResult = propertyParent?.children.some((schild) => schild === child.id)
child.checked = childResult ? true : false
})
}
});
})
goodsPropertyList.value = data;
} else {
goodsPropertyList.value = [];
}
goodsPropertyList.value = data
console.log(goodsPropertyList.value);
console.log(goodsPropertyList.value)
}
function changeSubProperty() {
// 修改子属性状态,需要同步更新 skus 的显示
console.log(goodsPropertyList.value);
console.log(goodsPropertyList.value)
// 过滤父属性 checked 选项,深拷贝避免后面循环改变元数据内容
let temp = JSON.parse(
JSON.stringify(goodsPropertyList.value.filter((item) => item.checked))
);
let temp = JSON.parse(JSON.stringify(goodsPropertyList.value.filter((item) => item.checked)))
temp.forEach((item) => {
item.propertyValues = item.propertyValues.filter((child) => child.checked);
});
item.propertyValues = item.propertyValues.filter((child) => child.checked)
})
let result = temp.map((item) => {
return item.propertyValues.map((child) => ({
@ -110,15 +96,15 @@ function changeSubProperty() {
propertyName: item.name,
valueId: child.id,
valueName: child.name,
}));
});
}))
})
let tempSkus = [];
let tempSkus = []
for (let item of reduceArr(result)) {
let obj = {
picUrl: "",
barCode: "",
picUrl: '',
barCode: '',
price: 0,
marketPrice: 0,
costPrice: 0,
@ -126,11 +112,11 @@ function changeSubProperty() {
weight: 0,
volume: 0,
properties: item,
};
tempSkus.push(obj);
}
tempSkus.push(obj)
}
skus.value = tempSkus;
skus.value = tempSkus
}
/**
@ -143,8 +129,8 @@ function initSku() {
// 单规格
if (!formData.value.specType) {
let obj = {
picUrl: "",
barCode: "",
picUrl: '',
barCode: '',
price: 0,
marketPrice: 0,
costPrice: 0,
@ -154,17 +140,20 @@ function initSku() {
properties: [
{
propertyId: 0,
propertyName: "默认",
propertyName: '默认',
valueId: 0,
valueName: "默认",
valueName: '默认',
},
],
};
}
skus.value = [obj];
skus.value = [obj]
} else {
// 初始化已选中规格,属性和格式化后的结果
propertyList.value = []
goodsPropertyList.value = []
// 多规格
skus.value = [];
skus.value = []
}
}
@ -176,108 +165,109 @@ function initSku() {
*/
function submitProperty() {
try {
validateSku(skus.value);
peach.$store("trade").skus = skus.value;
peach.$router.back();
validateSku(skus.value)
peach.$store('trade').$patch({
skus: skus.value,
specType: formData.value.specType,
})
peach.$router.back()
} catch (e) {
console.log(skus.value);
console.log(e, "校验失败");
console.log(skus.value)
console.log(e, '校验失败')
}
}
function validateSku(skus) {
let warningInfo = "请检查商品各行相关属性配置,";
let validateStatue = true;
let skusValue = skus ?? peach.$store("trade").skus;
let warningInfo = '请检查商品各行相关属性配置,'
let validateStatue = true
let skusValue = skus ?? peach.$store('trade').skus
// 判断如果是多规格,并且 skusValue 为空 [],则提示
if (formData.value.specType && skusValue.length < 1) {
uni.showToast({
title: "请选择商品规格",
icon: "none",
title: '请选择商品规格',
icon: 'none',
duration: 4000,
});
throw new Error("请选择商品规格");
})
throw new Error('请选择商品规格')
}
for (const sku of skusValue) {
for (const rule of SKU_RULE_CONFIG) {
const arg = getValue(sku, rule.name);
const arg = getValue(sku, rule.name)
if (!rule.rule(arg)) {
validateStatue = false;
warningInfo += rule.message;
break;
validateStatue = false
warningInfo += rule.message
break
}
}
if (!validateStatue) {
uni.showToast({
title: warningInfo,
icon: "none",
icon: 'none',
duration: 4000,
});
throw new Error(warningInfo);
})
throw new Error(warningInfo)
}
}
}
function getValue(obj, arg) {
const keys = arg.split(".");
let value = obj;
const keys = arg.split('.')
let value = obj
for (const key of keys) {
if (value && typeof value === "object" && key in value) {
value = value[key];
if (value && typeof value === 'object' && key in value) {
value = value[key]
} else {
value = undefined;
break;
value = undefined
break
}
}
return value;
return value
}
function reduceArr(arr) {
return arr.reduce((acc, cur) => {
let tempAcc = [];
let tempAcc = []
if (acc.length < 1) {
cur.forEach((item, index) => {
if (tempAcc[index]) {
tempAcc[index].push(item);
tempAcc[index].push(item)
} else {
tempAcc[index] = [item];
tempAcc[index] = [item]
}
});
})
} else {
acc.forEach((item, index) => {
cur.forEach((sitem, sindex) => {
tempAcc.push([...item, sitem]);
});
});
tempAcc.push([...item, sitem])
})
})
if (cur.length < 1) {
tempAcc = acc;
tempAcc = acc
}
}
return tempAcc;
}, []);
return tempAcc
}, [])
}
const specType = computed(
() => peach.$store("trade").goodsInfo?.specType || false
);
const specType = computed(() => peach.$store('trade').specType)
function initial() {
onLoad(() => {
formData.value.specType = specType.value ? true : false;
formData.value.specText = SPEC_TYPE[specType.value ? 1 : 0].label;
skus.value = JSON.parse(JSON.stringify(peach.$store("trade").skus));
formData.value.specType = specType.value
formData.value.specText = SPEC_TYPE[specType.value ? 1 : 0].label
skus.value = JSON.parse(JSON.stringify(peach.$store('trade').skus))
// 如果新增商品 sku并且是单规格初始化 sku
if (!skus.value) {
initSku();
initSku()
}
if (specType.value) {
getGoodsProperty();
getGoodsProperty()
}
});
})
}
export {
@ -297,4 +287,4 @@ export {
showPropertyList,
goodsPropertyList,
changeSubProperty,
};
}

View File

@ -120,7 +120,6 @@
placeholderStyle="color:#8a8a8a"
:inputBorder="false"
v-model="formData.deliveryText"
:disabled="!canEdit"
placeholder="请选择配送方式"
disabled
>
@ -129,9 +128,18 @@
</template>
</uni-easyinput>
</uni-forms-item>
<uni-forms-item label="商品详情">
<uni-forms-item label="虚拟销量" name="virtualSalesCount" required>
<uni-easyinput
v-model="formData.virtualSalesCount"
type="number"
:disabled="!canEdit"
placeholder="请输入虚拟销量"
/>
</uni-forms-item>
<uni-forms-item label="商品详情" required>
<piaoyiEditor
:values="richValues"
@changes="saveContens"
:maxlength="3000"
:readOnly="richReadOnly"
:photoUrl="photoUrl"
@ -161,7 +169,7 @@ import GoodsApi from '@/peach/api/trade/goods'
import piaoyiEditor from '@/uni_modules/piaoyi-editor/components/piaoyi-editor/piaoyi-editor.vue'
import peach from '@/peach'
import { baseUrl, apiPath } from '@/peach/config'
import { handleTree } from '@/peach/utils'
import { handleTree, floatToFixed2, convertToInteger } from '@/peach/utils'
import { validateSku } from './js/sku'
const bgStyle = {
@ -198,12 +206,16 @@ const formData = ref({
'http://101.43.181.163:9001/mall-backend/8f11e372520501531d06bfce15ea97bbecead41c5e4a36d15d7e40af85729ff3.png',
],
name: '测试商品',
categoryId: [],
categoryId: 91,
categoryText: '',
brandId: '',
brandId: 4,
keyword: '香酥鸭,但家',
deliveryTypes: [],
deliveryTypes: [3],
deliveryText: '',
sort: 0,
giveIntegral: 0,
virtualSalesCount: 0,
subCommissionType: false,
introduction: '但家贵阳香酥鸭现榨香酥鸭无任何添加剂香酥鸭但家贵阳香酥鸭现榨香酥鸭无任何添加剂香酥鸭',
})
@ -362,7 +374,8 @@ function getProduct(id) {
GoodsApi.getProduct({ id }).then((res) => {
formData.value = res.data
richValues.value = res.data.description
//
richValues.value = res.data.description ? res.data.description + '<p><br/></p>' : ''
// categoryList formData.value.categoryId
let tempCategory = categoryList.value.find((item) => {
@ -390,18 +403,28 @@ function getProduct(id) {
return item.value === formData.value.deliveryTypes[0]
}).label
formData.value.skus.forEach((item) => {
item.price = floatToFixed2(item.price)
item.marketPrice = floatToFixed2(item.marketPrice)
item.costPrice = floatToFixed2(item.costPrice)
})
peach.$store('trade').$patch({
goodsInfo: formData.value,
skus: formData.value.skus,
specType: formData.value.specType,
})
})
}
function saveContens(e) {
formData.value.description = e.html
}
function onSubmit() {
console.log('res', formData.value)
console.log('richtext', richValues.value)
formData.value.description = richValues.value
formData.value.skus = peach.$store('trade').skus
formRef.value
@ -414,22 +437,42 @@ function onSubmit() {
tempObj.skus.forEach((item) => {
item.name = formData.value.name
item.price = convertToInteger(item.price)
item.marketPrice = convertToInteger(item.marketPrice)
item.costPrice = convertToInteger(item.costPrice)
})
tempObj.specType = peach.$store('trade').specType
let msg = ''
if (formData.value.id) {
tempObj.id = formData.value.id
await GoodsApi.editProduct(tempObj)
msg = '修改成功'
} else {
await GoodsApi.addProduct(tempObj)
msg = '添加成功'
}
})
.catch((err) => {
uni.showToast({
title: err[0].errorMessage,
title: msg,
icon: 'none',
duration: 4000,
})
setTimeout(() => {
peach.$router.redirect('/pages/index/product')
}, 1000)
})
.catch((err) => {
console.log('err', err)
if (err) {
uni.showToast({
title: err[0]?.errorMessage,
icon: 'none',
duration: 4000,
})
}
})
}
@ -452,7 +495,7 @@ onLoad(async (options) => {
goodsTitle.value = options.title
//
richReadOnly.value = true
// richReadOnly.value = true
/**
* todo 滚动一定距离后修改富文本状态和 canEdit 一致

View File

@ -33,6 +33,14 @@ const GoodsApi = {
data,
})
},
// 删除商品
delProduct: (data) => {
return request({
url: '/product/spu/delete',
method: 'DELETE',
params: data,
})
},
// 商品分类
getGoodsCategory: (data) => {
return request({

View File

@ -59,6 +59,7 @@
import { ref, computed } from 'vue'
import { isArray } from 'lodash'
import peach from '@/peach'
import GoodsApi from '@/peach/api/trade/goods'
import { fen2yuan, formatSales, formatStock } from '@/peach/hooks/useGoods'
import { unix } from 'dayjs'
@ -114,7 +115,7 @@ const props = defineProps({
},
})
const emits = defineEmits(['click'])
const emits = defineEmits(['click', 'refresh'])
function onClick() {
emits('click')
@ -155,6 +156,7 @@ function clickGoods(mark) {
selectedProperty: null,
goodsInfo: null,
skus: null,
specType: false,
})
peach.$router.go('/pages/product/manageGoods', {
id: props.data.id,
@ -165,8 +167,16 @@ function clickGoods(mark) {
uni.showModal({
title: '提示',
content: '是否删除该商品?',
success: (res) => {
success: async (res) => {
if (res.confirm) {
await GoodsApi.delProduct({ id: props.data.id })
uni.showToast({
title: '删除成功',
icon: 'none',
})
emits('refresh')
}
},
})

View File

@ -1,21 +1,24 @@
import { ref, computed } from "vue";
import { defineStore } from "pinia";
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
const useTradeStore = defineStore("trade", () => {
const useTradeStore = defineStore('trade', () => {
// 已选择规格类型
const selectedProperty = ref(null);
const selectedProperty = ref(null)
// 商品信息
const goodsInfo = ref(null);
const goodsInfo = ref(null)
// 详情标记
const detailTag = ref("edit");
const detailTag = ref('edit')
// 规格类型,默认单规格
const specType = ref(false)
// 商品属性
const skus = ref(null);
const skus = ref(null)
// 商品是否可编辑
const canEdit = computed(() => (detailTag.value === "detail" ? false : true));
const canEdit = computed(() => (detailTag.value === 'detail' ? false : true))
return {
selectedProperty,
@ -23,7 +26,8 @@ const useTradeStore = defineStore("trade", () => {
skus,
canEdit,
detailTag,
};
});
specType,
}
})
export default useTradeStore;
export default useTradeStore

View File

@ -56,6 +56,45 @@ export const handleTree = (data, id, parentId, children) => {
return tree
}
export const convertToInteger = (num) => {
if (typeof num === 'undefined') return 0
const parsedNumber = typeof num === 'string' ? parseFloat(num) : num
// TODO 分转元后还有小数则四舍五入
return Math.round(parsedNumber * 100)
}
/**
* 将一个整数转换为分数保留两位小数
* @param num
*/
export const formatToFraction = (num) => {
if (typeof num === 'undefined') return '0.00'
const parsedNumber = typeof num === 'string' ? parseFloat(num) : num
return (parsedNumber / 100.0).toFixed(2)
}
export const floatToFixed2 = (num) => {
let str = '0.00'
if (typeof num === 'undefined') {
return str
}
const f = formatToFraction(num)
const decimalPart = f.toString().split('.')[1]
const len = decimalPart ? decimalPart.length : 0
switch (len) {
case 0:
str = f.toString() + '.00'
break
case 1:
str = f.toString() + '0'
break
case 2:
str = f.toString()
break
}
return str
}
export function resetPagination(pagination) {
pagination.list = []
pagination.total = 0

View File

@ -263,7 +263,7 @@ export default {
},
clear() {
this.editorCtx.clear()
this.$emit()
this.$emit('update:modelValue', '')
},
insertDate() {
const date = new Date()