feat(商品管理)

This commit is contained in:
Ankkaya 2024-06-04 18:43:13 +08:00
parent 0a2d2e4601
commit 30f3a4c2c4
10 changed files with 892 additions and 225 deletions

View File

@ -163,6 +163,15 @@
"meta": {
"auth": false
}
},
{
"path": "sku",
"style": {
"navigationBarTitleText": "商品属性"
},
"meta": {
"auth": false
}
}
]
},

View File

@ -0,0 +1,48 @@
<template>
<div class="sku-item">
<uni-forms label-width="160" label-position="top">
<uni-forms-item label="商品封面图" name="picUrl">
<p-uploader
v-model:url="formData.picUrl"
fileMediatype="image"
limit="1"
mode="grid"
:imageStyles="{ width: '168rpx', height: '168rpx' }"
/>
</uni-forms-item>
<uni-forms-item label="商品条码" name="barCode">
<uni-easyinput type="text" trim="all" v-model="formData.barCode" placeholder="请输入商品条码" />
</uni-forms-item>
<uni-forms-item label="销售价" name="price">
<uni-easyinput type="number" trim="all" v-model="formData.price" placeholder="请输入商品销售价" />
</uni-forms-item>
<uni-forms-item label="市场价" name="marketPrice">
<uni-easyinput type="number" trim="all" v-model="formData.marketPrice" placeholder="请输入商品销售价" />
</uni-forms-item>
<uni-forms-item label="成本价" name="costPrice">
<uni-easyinput type="number" trim="all" v-model="formData.costPrice" placeholder="请输入商品销售价" />
</uni-forms-item>
<uni-forms-item label="库存" name="stock">
<uni-easyinput type="number" trim="all" v-model="formData.stock" placeholder="请输入商品库存" />
</uni-forms-item>
<uni-forms-item label="重量kg" name="weight">
<uni-easyinput type="number" trim="all" v-model="formData.weight" placeholder="请输入商品重量" />
</uni-forms-item>
<uni-forms-item label="体积" name="volume">
<uni-easyinput type="number" trim="all" v-model="formData.volume" placeholder="请输入商品体积" />
</uni-forms-item>
</uni-forms>
</div>
</template>
<script setup>
import { ref } from 'vue'
const formData = ref({})
</script>
<style lang="scss" scoped>
.sku-item {
margin: 40rpx;
}
</style>

View File

@ -0,0 +1,132 @@
<template>
<view class="property-list">
<uni-popup type="bottom" ref="propertyListPopupRef" background-color="#fff">
<view class="popup-header">
<view class="button-cancel" @click="onClosePopup">取消</view>
<view class="button-link" @click="onConfirmPopup">确定</view>
</view>
<view class="popup-content">
<view
v-for="item in propertyList"
:key="item.id"
:class="['property-item', item.checked ? 'active' : '']"
@tap="chooseProperty(item)"
>
{{ item.label }}
</view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import { ref, computed, defineEmits, defineProps, defineExpose } from 'vue'
/**
* todo 底部高度配置
*/
const props = defineProps({
modelValue: {
default: () => [],
required: true,
type: Array,
},
})
const emit = defineEmits(['update:modelValue'])
const propertyList = ref([
{
label: '红色',
id: 1,
checked: false,
},
{
label: '蓝色',
id: 2,
checked: false,
},
{
label: '白色',
id: 3,
checked: false,
},
])
const propertyListPopupRef = ref()
const onClosePopup = () => {
propertyListPopupRef.value.close()
}
function chooseProperty(item) {
const index = props.modelValue.findIndex((v) => v.id === item.id)
item.checked = !item.checked
if (index === -1) {
props.modelValue.push(item)
} else {
props.modelValue.splice(index, 1)
}
}
function onConfirmPopup() {
emit('update:modelValue', propertyList.value.filter((item) => item.checked).map((item) => item.id) ?? [])
onClosePopup()
}
function onOpen() {
propertyList.value.map((item) => {
item.checked = false
return item
})
propertyList.value = propertyList.value.map((item) =>
props.modelValue.some((id) => id === item.id) ? { ...item, checked: true } : item
)
propertyListPopupRef.value.open('bottom')
}
defineExpose({
onOpen,
})
</script>
<style lang="scss" scoped>
.property-list {
.popup-content {
height: 500rpx;
padding: 40rpx;
display: flex;
flex-wrap: wrap;
gap: 20rpx;
justify-content: flex-start;
.property-item {
text-align: center;
line-height: 60rpx;
height: 60rpx;
padding: 0 10px;
border-radius: 10px;
background-color: var(--ui-BG-4);
}
.active {
color: #fff;
background-color: var(--ui-BG-Main);
}
}
.button-link {
color: #1892ea;
font-size: 28rpx;
}
.button-cancel {
color: #888;
font-size: 28rpx;
}
.popup-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 38rpx 0;
}
}
</style>

View File

@ -0,0 +1,10 @@
export const SPEC_TYPE = [
{
label: '单规格',
value: false,
},
{
label: '多规格',
value: true,
},
]

24
pages/product/js/sku.js Normal file
View File

@ -0,0 +1,24 @@
import { ref } from 'vue'
import { SPEC_TYPE } from './config'
const pickerRef = ref(null)
const propertyList = ref([1, 2])
const propertyListRef = ref(null)
const formData = ref({
specType: true,
specText: SPEC_TYPE[0].label,
})
function onRDPickerConfirm(e) {
formData.value.specText = SPEC_TYPE[e.value[0]].label
formData.value.specType = SPEC_TYPE[e.value[0]].value
}
function onPropertyConfirm(e) {
console.log(e)
}
export { pickerRef, propertyListRef, formData, onRDPickerConfirm, onPropertyConfirm, propertyList }

View File

@ -1,50 +1,117 @@
<template>
<pb-layout class="manage-goods" title="发布商品" leftIcon="leftIcon" navbar="normal" :bgStyle="bgStyle"
opacityBgUi="bg-white" color="black">
<pb-layout
class="manage-goods"
title="发布商品"
leftIcon="leftIcon"
navbar="normal"
:bgStyle="bgStyle"
opacityBgUi="bg-white"
color="black"
>
<view class="goods-form">
<uni-forms ref="formRef" v-model="formData" :rules="rules" label-position="top" label-width="160">
<uni-forms-item label="商品封面图" name="picUrl" required>
<p-uploader v-model:url="formData.picUrl" fileMediatype="image" limit="1" mode="grid"
:imageStyles="{ width: '168rpx', height: '168rpx' }" />
<p-uploader
v-model:url="formData.picUrl"
fileMediatype="image"
limit="1"
mode="grid"
:imageStyles="{ width: '168rpx', height: '168rpx' }"
/>
</uni-forms-item>
<uni-forms-item label="商品轮播图" name="sliderPicUrls" required>
<p-uploader v-model:url="formData.sliderPicUrls" fileMediatype="image" limit="6" mode="grid"
:imageStyles="{ width: '168rpx', height: '168rpx' }" />
<p-uploader
v-model:url="formData.sliderPicUrls"
fileMediatype="image"
limit="6"
mode="grid"
:imageStyles="{ width: '168rpx', height: '168rpx' }"
/>
</uni-forms-item>
<uni-forms-item label="商品名称" name="name" required>
<uni-easyinput type="text" trim="all" v-model="formData.name" placeholder="请输入商品名称" />
</uni-forms-item>
<uni-forms-item label="商品分类" name="categoryId" label-position="left" required>
<uni-easyinput type="text" v-model="formData.categoryId" :styles="selfStyles" placeholderStyle="color:#8a8a8a"
:clearable="false" :inputBorder="false" placeholder="请选择商品分类" disabled>
<uni-forms-item
label="商品分类"
@tap="openPicker('category', 'multiple')"
name="categoryId"
label-position="left"
required
>
<uni-easyinput
type="text"
v-model="formData.categoryText"
:styles="selfStyles"
placeholderStyle="color:#8a8a8a"
:clearable="false"
:inputBorder="false"
placeholder="请选择商品分类"
disabled
>
<template v-slot:right>
<uni-icons type="right" />
</template>
</uni-easyinput>
</uni-forms-item>
<uni-forms-item label="商品品牌" name="brandId" label-position="left" required>
<uni-easyinput type="text" v-model="formData.brandId" :styles="selfStyles" placeholderStyle="color:#8a8a8a"
:clearable="false" :inputBorder="false" placeholder="请选择商品品牌" disabled>
<uni-easyinput
type="text"
v-model="formData.brandId"
:styles="selfStyles"
placeholderStyle="color:#8a8a8a"
:clearable="false"
:inputBorder="false"
placeholder="请选择商品品牌"
disabled
>
<template v-slot:right>
<uni-icons type="right" />
</template>
</uni-easyinput>
</uni-forms-item>
<uni-forms-item label="商品规格" name="skus" label-position="left" required>
<uni-easyinput type="text" v-model="formData.skus" :styles="selfStyles" placeholderStyle="color:#8a8a8a"
:clearable="false" :inputBorder="false" placeholder="请添加商品规格" disabled>
<template v-slot:right>
<uni-icons type="right" /> </template></uni-easyinput>
<uni-easyinput
type="text"
v-model="formData.skus"
:styles="selfStyles"
placeholderStyle="color:#8a8a8a"
:clearable="false"
:inputBorder="false"
placeholder="请添加商品规格"
disabled
>
<template v-slot:right> <uni-icons type="right" /> </template
></uni-easyinput>
</uni-forms-item>
<uni-forms-item label="商品关键词" name="keyword" required>
<uni-easyinput type="text" v-model="formData.keyword" placeholder="请输入商品关键词" />
</uni-forms-item>
<uni-forms-item label="商品简介" name="introduction" required>
<uni-easyinput type="textarea" trim="all" autoHeight v-model="formData.introduction" placeholder="请输入商品简介" />
<uni-easyinput
type="textarea"
trim="all"
autoHeight
v-model="formData.introduction"
placeholder="请输入商品简介"
/>
</uni-forms-item>
<uni-forms-item label="物流设置" @click="openPicker" name="deliveryTypes" label-position="left" required>
<uni-easyinput type="text" :clearable="false" :styles="selfStyles" placeholderStyle="color:#8a8a8a"
:inputBorder="false" v-model="formData.keyword" placeholder="请选择配送方式" disabled>
<uni-forms-item
label="物流设置"
@tap="openPicker('delivery', 'single')"
name="deliveryTypes"
label-position="left"
required
>
<uni-easyinput
type="text"
:clearable="false"
:styles="selfStyles"
placeholderStyle="color:#8a8a8a"
:inputBorder="false"
v-model="formData.deliveryText"
placeholder="请选择配送方式"
disabled
>
<template v-slot:right>
<uni-icons type="right" />
</template>
@ -53,15 +120,12 @@
</uni-forms>
</view>
<uni-popup ref="popupRef" type="bottom" border-radius="10px 10px 0 0">
<picker-view v-if="visible" :indicator-style="indicatorStyle" :value="value" @change="bindChange"
class="picker-view">
<picker-view-column>
<view class="item" v-for="(item, index) in DELIVERY_TYPES" :key="index">{{ item.label }}</view>
</picker-view-column>
</picker-view>
</uni-popup>
<p-picker
ref="pickerRef"
:mode="pickerMode"
:options-cols="optionsCols"
@confirm="onRDPickerConfirm"
></p-picker>
</pb-layout>
</template>
@ -69,6 +133,7 @@
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import peach from '@/peach'
import { handleTree } from '@/peach/utils'
import GoodsApi from '@/peach/api/trade/goods'
import _ from 'lodash'
@ -78,21 +143,22 @@ const bgStyle = {
description: '',
}
const DELIVERY_TYPES = [{
const DELIVERY_TYPES = [
{
value: 3,
label: '到店核销'
}, {
label: '到店核销',
},
{
value: 4,
label: '商家配送'
}]
const indicatorStyle = 'height: 50px'
label: '商家配送',
},
]
const selfStyles = {
backgroundColor: '#f9f9f9',
}
const popupRef = ref()
const pickerRef = ref()
const formData = ref({
picUrl: 'http://101.43.181.163:9001/mall-backend/8f11e372520501531d06bfce15ea97bbecead41c5e4a36d15d7e40af85729ff3.png',
sliderPicUrls: [
@ -101,10 +167,12 @@ const formData = ref({
'http://101.43.181.163:9001/mall-backend/8f11e372520501531d06bfce15ea97bbecead41c5e4a36d15d7e40af85729ff3.png',
],
name: '测试商品',
categoryId: 91,
brandId: 4,
categoryId: [],
categoryText: '',
brandId: '',
keyword: '香酥鸭,但家',
deliveryTypes: [3],
deliveryTypes: [],
deliveryText: '',
introduction: '但家贵阳香酥鸭现榨香酥鸭无任何添加剂香酥鸭但家贵阳香酥鸭现榨香酥鸭无任何添加剂香酥鸭',
})
@ -184,10 +252,41 @@ const rules = {
}
const formRef = ref(null)
const pickerMode = ref('single')
const popMark = ref('')
const categoryList = ref([])
const optionsCols = ref([])
function openPicker() {
console.log('123')
popupRef.value.open()
function openPicker(mark, mode) {
pickerMode.value = mode
popMark.value = mark
if (mark === 'delivery') {
optionsCols.value = DELIVERY_TYPES
pickerRef.value.onOpen([0])
} else if (mark === 'category') {
optionsCols.value = categoryList.value
pickerRef.value.onOpen([0, 0])
}
}
function onRDPickerConfirm(e) {
if (popMark.value === 'delivery') {
formData.value.deliveryTypes = []
formData.value.deliveryText = DELIVERY_TYPES[e.value[0]].label
formData.value.deliveryTypes.push(DELIVERY_TYPES[e.value[0]].value)
}
if (popMark.value === 'category') {
formData.value.categoryId = e.value
formData.value.categoryText =
categoryList.value[e.value[0]].name + '/' + categoryList.value[e.value[0]].children[e.value[1]].name
}
}
function getProduct() {
GoodsApi.getProduct(formData.value.id).then((res) => {
formData.value = res
})
}
function onSubmit() {
@ -211,9 +310,22 @@ function onSubmit() {
}
//
function getGoodsInfo() { }
function getGoodsInfo() {}
//
async function getCategoryList() {
let { data } = await GoodsApi.getGoodsCategory()
categoryList.value = handleTree(data)
}
//
async function getBrandList() {
let { data } = await GoodsApi.getBrandList()
console.log(data)
}
onLoad((options) => {
getCategoryList()
if (options.id) {
getProduct()
}
@ -258,16 +370,5 @@ onLoad((options) => {
}
}
}
.picker-view {
width: 750rpx;
height: 600rpx;
margin-top: 20rpx;
}
.item {
line-height: 100rpx;
text-align: center;
}
}
</style>

111
pages/product/sku.vue Normal file
View File

@ -0,0 +1,111 @@
<template>
<pb-layout
class="goods-property"
title="商品属性"
leftIcon="leftIcon"
navbar="normal"
:bgStyle="bgStyle"
opacityBgUi="bg-white"
color="black"
>
<view class="property">
<uni-forms ref="formRef" v-model="formData" :rules="rules" label-position="top" label-width="160">
<uni-forms-item
label="商品规格"
@tap="pickerRef.onOpen([0])"
name="specType"
label-position="left"
required
>
<uni-easyinput
type="text"
:clearable="false"
:styles="selfStyles"
placeholderStyle="color:#8a8a8a"
:inputBorder="false"
v-model="formData.specText"
placeholder="请选择商品规格"
disabled
>
<template v-slot:right>
<uni-icons type="right" />
</template>
</uni-easyinput>
</uni-forms-item>
</uni-forms>
</view>
<template v-if="formData.specType">
<button class="ss-reset-button add-property" @tap="propertyListRef.onOpen()">+添加规格</button>
</template>
<template v-else>
<SkuItem />
</template>
<p-picker ref="pickerRef" mode="single" :options-cols="SPEC_TYPE" @confirm="onRDPickerConfirm"></p-picker>
<PropertyList ref="propertyListRef" v-model="propertyList" @confirm="onPropertyConfirm" />
</pb-layout>
</template>
<script setup>
import SkuItem from './components/item'
import PropertyList from './components/propertyList'
import { SPEC_TYPE } from './js/config'
import { pickerRef, propertyListRef, onRDPickerConfirm, formData, propertyList, onPropertyConfirm } from './js/sku'
const bgStyle = {
backgroundImage: '',
backgroundColor: '#fff',
description: '',
}
</script>
<style lang="scss" scoped>
.goods-property {
.property {
margin: 40rpx;
:deep() {
.uni-easyinput__content-input {
font-size: 28rpx !important;
color: #333333 !important;
line-height: normal !important;
}
.uni-easyinput__placeholder-class {
font-size: 14px;
}
.is-direction-left {
.uni-forms-item__label {
padding-left: 10px;
background-color: #f9f9f9;
border-radius: 10px 0 0 10px;
}
uni-icons {
margin-right: 10px;
}
.uni-easyinput__content {
border-radius: 0 10px 10px 0;
}
}
.is-disabled {
color: #333333;
text-align: right;
}
}
}
.add-property {
margin: 40rpx;
border: 1px dotted var(--ui-BG-Main);
color: var(--ui-BG-Main);
border-radius: 10px;
text-align: center;
}
}
</style>

View File

@ -33,6 +33,15 @@ const GoodsApi = {
data,
})
},
// 商品分类
getGoodsCategory: (data) => {
return request({
url: '/product/category/list',
method: 'GET',
params: data,
})
},
// 商品品牌
}
export default GoodsApi

View File

@ -0,0 +1,167 @@
<template>
<view class="custom-picker">
<!-- 普通弹窗 -->
<uni-popup type="bottom" ref="pickerPopupRef" background-color="#fff">
<view class="popup-header">
<view class="button-cancel" @click="onClosePopup">取消</view>
<view class="button-link" @click="onConfirmPopup">确定</view>
</view>
<view class="popup-content">
<picker-view
:indicator-style="indicatorStyle"
:value="pickerViewValue"
@change="bindChange"
class="picker-view"
>
<template v-if="mode === 'single'">
<picker-view-column>
<view
class="item"
v-for="(item, index) in props.optionsCols"
:key="`${item.value}-${index}`"
>{{ item.label }}</view
>
</picker-view-column>
</template>
<template v-else>
<picker-view-column>
<view
class="item"
v-for="(item, index) in props.optionsCols"
:key="`${item.value}-${index}`"
>{{ item.name }}</view
>
</picker-view-column>
<picker-view-column>
<view class="item" v-for="(item, index) in childrenList" :key="`${item.value}-${index}`">{{
item.name
}}</view>
</picker-view-column>
</template>
</picker-view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import { defineEmits, defineProps, ref, onMounted, computed, defineExpose } from 'vue'
const pickerViewValue = ref([])
const indicatorStyle = `height: 50px`
const props = defineProps({
// pickerview value label
optionsCols: {
default: () => [],
required: true,
type: Array,
},
mode: {
type: String,
default: 'single',
},
})
const childrenList = computed(() => {
return props.optionsCols[pickerViewValue.value[0]]?.children
})
// confirm change
const emit = defineEmits(['confirm', 'change'])
// ref
const pickerPopupRef = ref(null)
/**
* @author: joey wong
* @description: 打开弹窗
* @param {Array} defaultValue 默认选中index的值,[0,1]
* @return {*}
*/
const onOpen = (defaultValue) => {
pickerViewValue.value = defaultValue
pickerPopupRef.value.open('bottom')
}
/**
* @author: joey wong
* @description: 滚动改变事件
* @param {*} e
* @return {*}
*/
const bindChange = (e) => {
pickerViewValue.value = e.detail.value
if (props.mode === 'multiple') {
if (pickerViewValue.value[0] !== e.detail.value[0]) {
pickerViewValue.value[1] = 0
}
}
emit('change', {
value: pickerViewValue.value,
})
}
/**
* @author: joey wong
* @description: 关闭模态框事件
* @return {*}
*/
const onClosePopup = () => {
pickerPopupRef.value.close()
}
/**
* @author: joey wong
* @description: 确认事件
* @return {*}
*/
const onConfirmPopup = () => {
emit('confirm', {
value: pickerViewValue.value,
})
onClosePopup()
}
defineExpose({
onOpen,
})
</script>
<style lang="scss" scoped>
.custom-picker {
display: flex;
align-items: center;
&-text {
margin-right: 5.7rpx;
}
&-icon {
width: 19rpx;
height: 12rpx;
margin-bottom: 3rpx;
}
.popup-content {
height: 500rpx;
.item {
text-align: center;
line-height: 50px;
}
}
.picker-view {
width: 100%;
height: 100%;
margin-top: 20rpx;
}
.button-link {
color: #1892ea;
font-size: 28rpx;
}
.button-cancel {
color: #888;
font-size: 28rpx;
}
.popup-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 38rpx 0;
}
}
</style>

View File

@ -1,5 +1,61 @@
import dayjs from 'dayjs'
/**
* 构造树型结构数据
* @param {*} data 数据源
* @param {*} id id字段 默认 'id'
* @param {*} parentId 父节点字段 默认 'parentId'
* @param {*} children 孩子节点字段 默认 'children'
*/
export const handleTree = (data, id, parentId, children) => {
if (!Array.isArray(data)) {
console.warn('data must be an array')
return []
}
const config = {
id: id || 'id',
parentId: parentId || 'parentId',
childrenList: children || 'children',
}
const childrenListMap = {}
const nodeIds = {}
const tree = []
for (const d of data) {
const parentId = d[config.parentId]
if (childrenListMap[parentId] == null) {
childrenListMap[parentId] = []
}
nodeIds[d[config.id]] = d
childrenListMap[parentId].push(d)
}
for (const d of data) {
const parentId = d[config.parentId]
if (nodeIds[parentId] == null) {
tree.push(d)
}
}
for (const t of tree) {
adaptToChildrenList(t)
}
function adaptToChildrenList(o) {
if (childrenListMap[o[config.id]] !== null) {
o[config.childrenList] = childrenListMap[o[config.id]]
}
if (o[config.childrenList]) {
for (const c of o[config.childrenList]) {
adaptToChildrenList(c)
}
}
}
return tree
}
export function resetPagination(pagination) {
pagination.list = []
pagination.total = 0