feat(商品管理)

This commit is contained in:
Ankkaya 2024-06-05 18:58:12 +08:00
parent 30f3a4c2c4
commit 50e03907e7
11 changed files with 380 additions and 116 deletions

View File

@ -1,28 +1,28 @@
{ {
"name": "mall-app-t", "name" : "mall-app-t",
"appid": "__UNI__B201544", "appid" : "__UNI__B201544",
"description": "", "description" : "",
"versionName": "1.0.0", "versionName" : "1.0.0",
"versionCode": "100", "versionCode" : "100",
"transformPx": false, "transformPx" : false,
/* 5+App */ /* 5+App */
"app-plus": { "app-plus" : {
"usingComponents": true, "usingComponents" : true,
"nvueStyleCompiler": "uni-app", "nvueStyleCompiler" : "uni-app",
"compilerVersion": 3, "compilerVersion" : 3,
"splashscreen": { "splashscreen" : {
"alwaysShowBeforeRender": true, "alwaysShowBeforeRender" : true,
"waiting": true, "waiting" : true,
"autoclose": true, "autoclose" : true,
"delay": 0 "delay" : 0
}, },
/* */ /* */
"modules": {}, "modules" : {},
/* */ /* */
"distribute": { "distribute" : {
/* android */ /* android */
"android": { "android" : {
"permissions": [ "permissions" : [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>", "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>", "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>", "<uses-permission android:name=\"android.permission.VIBRATE\"/>",
@ -41,32 +41,32 @@
] ]
}, },
/* ios */ /* ios */
"ios": {}, "ios" : {},
/* SDK */ /* SDK */
"sdkConfigs": {} "sdkConfigs" : {}
} }
}, },
/* */ /* */
"quickapp": {}, "quickapp" : {},
/* */ /* */
"mp-weixin": { "mp-weixin" : {
"appid": "", "appid" : "wx64387dc8bba916ec",
"setting": { "setting" : {
"urlCheck": false "urlCheck" : false
}, },
"usingComponents": true "usingComponents" : true
}, },
"mp-alipay": { "mp-alipay" : {
"usingComponents": true "usingComponents" : true
}, },
"mp-baidu": { "mp-baidu" : {
"usingComponents": true "usingComponents" : true
}, },
"mp-toutiao": { "mp-toutiao" : {
"usingComponents": true "usingComponents" : true
}, },
"uniStatistics": { "uniStatistics" : {
"enable": false "enable" : false
}, },
"vueVersion": "3" "vueVersion" : "3"
} }

View File

@ -15,7 +15,7 @@
:data="item" :data="item"
:topRadius="10" :topRadius="10"
:bottomRadius="10" :bottomRadius="10"
@click="peach.$router.go('/pages/goods/index', { id: item.id })" @click="peach.$router.go('/pages/product/manageGoods', { id: item.id, mark: 'detail' })"
/> />
</view> </view>
</view> </view>

View File

@ -0,0 +1,78 @@
<template>
<view class="property-detail">
<template v-for="item in goodsPropertyList" :key="item.id">
<view class="property-item ss-p-40 ss-gap-40" v-if="item.checked">
<view class="property-name ss-flex ss-gap-40 ss-m-b-20">
<view class="property-name-text">规格名</view>
<view class="property-name-value">{{ item.name }}</view>
</view>
<view class="property-value ss-flex ss-gap-40">
<view class="property-value-text">规格值</view>
<view class="property-value-value ss-flex ss-gap-10">
<view
v-for="sitem in item.propertyValues"
@tap="chooseProperty(item)"
:class="['item', item.checked ? 'active' : '']"
>{{ sitem.name }}</view
>
</view>
</view>
</view>
</template>
</view>
</template>
<script setup>
import { defineProps, ref, computed } from 'vue'
const props = defineProps({
modelValue: {
type: Array,
default: () => [],
},
goodsPropertyList: {
type: Array,
default: () => [],
},
})
function chooseProperty(item) {
item.checked = !item.checked
}
</script>
<style lang="scss" scoped>
.property-detail {
.property-item {
margin: 0 40rpx 20rpx;
background-color: #f9f9f9;
border-radius: 10px;
.property-name {
.property-name-text {
color: #606266;
}
}
.property-value {
.property-value-text {
color: #606266;
}
.property-value-value {
.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);
}
}
}
}
}
</style>

View File

@ -7,12 +7,12 @@
</view> </view>
<view class="popup-content"> <view class="popup-content">
<view <view
v-for="item in propertyList" v-for="item in goodsPropertyList"
:key="item.id" :key="item.id"
:class="['property-item', item.checked ? 'active' : '']" :class="['property-item', item.checked ? 'active' : '']"
@tap="chooseProperty(item)" @tap="chooseProperty(item)"
> >
{{ item.label }} {{ item.name }}
</view> </view>
</view> </view>
</uni-popup> </uni-popup>
@ -21,6 +21,7 @@
<script setup> <script setup>
import { ref, computed, defineEmits, defineProps, defineExpose } from 'vue' import { ref, computed, defineEmits, defineProps, defineExpose } from 'vue'
import peach from '@/peach'
/** /**
* todo 底部高度配置 * todo 底部高度配置
@ -32,28 +33,15 @@ const props = defineProps({
required: true, required: true,
type: Array, type: Array,
}, },
goodsPropertyList: {
default: () => [],
required: true,
type: Array,
},
}) })
const emit = defineEmits(['update:modelValue']) 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 propertyListPopupRef = ref()
const onClosePopup = () => { const onClosePopup = () => {
@ -61,28 +49,30 @@ const onClosePopup = () => {
} }
function chooseProperty(item) { function chooseProperty(item) {
const index = props.modelValue.findIndex((v) => v.id === item.id)
item.checked = !item.checked item.checked = !item.checked
if (index === -1) {
props.modelValue.push(item)
} else {
props.modelValue.splice(index, 1)
}
} }
function onConfirmPopup() { function onConfirmPopup() {
emit('update:modelValue', propertyList.value.filter((item) => item.checked).map((item) => item.id) ?? []) let resut = props.goodsPropertyList
.filter((item) => {
if (item.checked) {
return item.propertyValues.filter((sitem) => sitem.checked)
}
})
.map((item) => {
let children = item.propertyValues.filter((sitem) => sitem.checked).map((titem) => titem.id)
return {
id: item.id,
children: children,
}
})
console.log(resut)
// emit('update:modelValue', props.goodsPropertyList.filter((item) => item.checked).map((item) => item.id) ?? [])
onClosePopup() onClosePopup()
} }
function onOpen() { 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') propertyListPopupRef.value.open('bottom')
} }

View File

@ -1,9 +1,23 @@
import { ref } from 'vue' 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 } from './config' import { SPEC_TYPE } from './config'
const pickerRef = ref(null) const pickerRef = ref(null)
const propertyList = ref([1, 2]) const propertyList = ref([
{
id: 36,
children: [68],
},
{
id: 37,
children: [],
},
])
const goodsPropertyList = ref([])
const propertyListRef = ref(null) const propertyListRef = ref(null)
@ -12,13 +26,64 @@ const formData = ref({
specText: SPEC_TYPE[0].label, specText: SPEC_TYPE[0].label,
}) })
async function showPropertyList() {
await getGoodsProperty()
propertyListRef.value.onOpen()
}
function onRDPickerConfirm(e) { function onRDPickerConfirm(e) {
peach.$store('trade').specType = SPEC_TYPE[e.value[0]].value
formData.value.specText = SPEC_TYPE[e.value[0]].label formData.value.specText = SPEC_TYPE[e.value[0]].label
formData.value.specType = SPEC_TYPE[e.value[0]].value formData.value.specType = SPEC_TYPE[e.value[0]].value
} }
function pickerProperty() {
let index = specType ? 1 : 0
pickerRef.value.onOpen([index])
}
function onPropertyConfirm(e) { function onPropertyConfirm(e) {
console.log(e) console.log(e)
} }
export { pickerRef, propertyListRef, formData, onRDPickerConfirm, onPropertyConfirm, propertyList } async function getGoodsProperty() {
let { data } = await GoodsApi.getHistoryProperty()
// 根据已经选择数据,设置默认选中
data.forEach((item) => {
// 判断属性是否已经选中
let propertyParent = propertyList.value.find((sitem) => sitem?.id === item.id)
item.checked = propertyParent ? true : false
// 如果属性已经选中,查询子类中是否有选中
if (item.checked) {
item.propertyValues.forEach((child) => {
let childResult = propertyParent?.children.some((schild) => schild?.id === child.id)
child.checked = childResult ? true : false
})
}
})
goodsPropertyList.value = data
}
const specType = computed(() => peach.$store('trade').specType)
function initial() {
onLoad(() => {
formData.value.specType = specType ? true : false
formData.value.specText = SPEC_TYPE[specType ? 1 : 0].label
})
}
export {
initial,
pickerRef,
pickerProperty,
propertyListRef,
formData,
onRDPickerConfirm,
onPropertyConfirm,
propertyList,
showPropertyList,
goodsPropertyList,
}

View File

@ -53,10 +53,16 @@
</template> </template>
</uni-easyinput> </uni-easyinput>
</uni-forms-item> </uni-forms-item>
<uni-forms-item label="商品品牌" name="brandId" label-position="left" required> <uni-forms-item
label="商品品牌"
name="brandId"
label-position="left"
required
@tap="openPicker('brand', 'single')"
>
<uni-easyinput <uni-easyinput
type="text" type="text"
v-model="formData.brandId" v-model="formData.brandText"
:styles="selfStyles" :styles="selfStyles"
placeholderStyle="color:#8a8a8a" placeholderStyle="color:#8a8a8a"
:clearable="false" :clearable="false"
@ -69,19 +75,10 @@
</template> </template>
</uni-easyinput> </uni-easyinput>
</uni-forms-item> </uni-forms-item>
<uni-forms-item label="商品规格" name="skus" label-position="left" required> <uni-forms-item label="商品规格" name="skus" required label-position="left">
<uni-easyinput <view class="btn-group">
type="text" <button class="ss-reset-button ss-set-property" @tap="clickSetProperty">规格设置</button>
v-model="formData.skus" </view>
: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>
<uni-forms-item label="商品关键词" name="keyword" required> <uni-forms-item label="商品关键词" name="keyword" required>
<uni-easyinput type="text" v-model="formData.keyword" placeholder="请输入商品关键词" /> <uni-easyinput type="text" v-model="formData.keyword" placeholder="请输入商品关键词" />
@ -255,6 +252,7 @@ const formRef = ref(null)
const pickerMode = ref('single') const pickerMode = ref('single')
const popMark = ref('') const popMark = ref('')
const categoryList = ref([]) const categoryList = ref([])
const brandList = ref([])
const optionsCols = ref([]) const optionsCols = ref([])
function openPicker(mark, mode) { function openPicker(mark, mode) {
@ -266,6 +264,9 @@ function openPicker(mark, mode) {
} else if (mark === 'category') { } else if (mark === 'category') {
optionsCols.value = categoryList.value optionsCols.value = categoryList.value
pickerRef.value.onOpen([0, 0]) pickerRef.value.onOpen([0, 0])
} else if (mark === 'brand') {
optionsCols.value = brandList.value
pickerRef.value.onOpen([0])
} }
} }
@ -277,15 +278,53 @@ function onRDPickerConfirm(e) {
} }
if (popMark.value === 'category') { if (popMark.value === 'category') {
formData.value.categoryId = e.value formData.value.categoryId = categoryList.value[e.value[0]].children[e.value[1]].id
formData.value.categoryText = formData.value.categoryText =
categoryList.value[e.value[0]].name + '/' + categoryList.value[e.value[0]].children[e.value[1]].name categoryList.value[e.value[0]].name + '/' + categoryList.value[e.value[0]].children[e.value[1]].name
} }
if (popMark.value === 'brand') {
formData.value.brandId = brandList.value[e.value[0]].id
}
} }
function getProduct() { function clickSetProperty() {
GoodsApi.getProduct(formData.value.id).then((res) => { peach.$store('trade').$patch({
formData.value = res selectedProperty: formData.value.skus,
goodsInfo: formData.value,
})
peach.$router.go('/pages/product/sku')
}
function getProduct(id) {
GoodsApi.getProduct({ id }).then((res) => {
formData.value = res.data
// categoryList formData.value.categoryId
let tempCategory = categoryList.value.find((item) => {
return item.children.find((child) => {
return child.id === formData.value.categoryId
})
})
formData.value.categoryText =
tempCategory.name +
'/' +
tempCategory.children.find((item) => {
return item.id === formData.value.categoryId
}).name
// brandList, formData.value.brandId
let tempBrand = brandList.value.find((item) => {
return item.id === formData.value.brandId
})
formData.value.brandText = tempBrand.name
// DELIVERY_TYPES formData.value.deliveryTypes
formData.value.deliveryText = DELIVERY_TYPES.find((item) => {
return item.value === formData.value.deliveryTypes[0]
}).label
}) })
} }
@ -309,9 +348,6 @@ function onSubmit() {
}) })
} }
//
function getGoodsInfo() {}
// //
async function getCategoryList() { async function getCategoryList() {
let { data } = await GoodsApi.getGoodsCategory() let { data } = await GoodsApi.getGoodsCategory()
@ -320,14 +356,15 @@ async function getCategoryList() {
// //
async function getBrandList() { async function getBrandList() {
let { data } = await GoodsApi.getBrandList() let { data } = await GoodsApi.getBrand()
console.log(data) brandList.value = data
} }
onLoad((options) => { onLoad(async (options) => {
getCategoryList() await getCategoryList()
await getBrandList()
if (options.id) { if (options.id) {
getProduct() getProduct(options.id)
} }
}) })
</script> </script>
@ -369,6 +406,25 @@ onLoad((options) => {
text-align: right; text-align: right;
} }
} }
.btn-group {
height: 100%;
display: flex;
justify-content: flex-end;
align-items: center;
background-color: #f9f9f9;
border-radius: 0 10px 10px 0;
.ss-set-property {
width: 80px;
height: 60rpx;
line-height: normal;
background: var(--ui-BG-Main);
border-radius: 28rpx;
font-size: 26rpx;
font-weight: 500;
color: #fff;
}
}
} }
} }
</style> </style>

View File

@ -10,13 +10,7 @@
> >
<view class="property"> <view class="property">
<uni-forms ref="formRef" v-model="formData" :rules="rules" label-position="top" label-width="160"> <uni-forms ref="formRef" v-model="formData" :rules="rules" label-position="top" label-width="160">
<uni-forms-item <uni-forms-item label="商品规格" @tap="pickerProperty" name="specType" label-position="left" required>
label="商品规格"
@tap="pickerRef.onOpen([0])"
name="specType"
label-position="left"
required
>
<uni-easyinput <uni-easyinput
type="text" type="text"
:clearable="false" :clearable="false"
@ -35,7 +29,12 @@
</uni-forms> </uni-forms>
</view> </view>
<template v-if="formData.specType"> <template v-if="formData.specType">
<button class="ss-reset-button add-property" @tap="propertyListRef.onOpen()">+添加规格</button> <button class="ss-reset-button add-property" @tap="showPropertyList">+添加规格</button>
<property-detail
v-if="propertyList.length > 0"
v-model="propertyList"
:goodsPropertyList="goodsPropertyList"
></property-detail>
</template> </template>
<template v-else> <template v-else>
@ -44,21 +43,40 @@
<p-picker ref="pickerRef" mode="single" :options-cols="SPEC_TYPE" @confirm="onRDPickerConfirm"></p-picker> <p-picker ref="pickerRef" mode="single" :options-cols="SPEC_TYPE" @confirm="onRDPickerConfirm"></p-picker>
<PropertyList ref="propertyListRef" v-model="propertyList" @confirm="onPropertyConfirm" /> <PropertyList
ref="propertyListRef"
v-model="propertyList"
:goodsPropertyList="goodsPropertyList"
@confirm="onPropertyConfirm"
/>
</pb-layout> </pb-layout>
</template> </template>
<script setup> <script setup>
import SkuItem from './components/item' import SkuItem from './components/item'
import PropertyList from './components/propertyList' import PropertyList from './components/propertyList'
import PropertyDetail from './components/propertyDetail'
import { SPEC_TYPE } from './js/config' import { SPEC_TYPE } from './js/config'
import { pickerRef, propertyListRef, onRDPickerConfirm, formData, propertyList, onPropertyConfirm } from './js/sku' import {
initial,
pickerRef,
propertyListRef,
onRDPickerConfirm,
formData,
propertyList,
onPropertyConfirm,
showPropertyList,
goodsPropertyList,
pickerProperty,
} from './js/sku'
const bgStyle = { const bgStyle = {
backgroundImage: '', backgroundImage: '',
backgroundColor: '#fff', backgroundColor: '#fff',
description: '', description: '',
} }
initial()
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -12,7 +12,7 @@ const GoodsApi = {
// 商品详情 spuIds // 商品详情 spuIds
getProduct: (data) => { getProduct: (data) => {
return request({ return request({
url: '/product/spu/list', url: '/product/spu/get-detail',
method: 'GET', method: 'GET',
params: data, params: data,
}) })
@ -42,6 +42,27 @@ const GoodsApi = {
}) })
}, },
// 商品品牌 // 商品品牌
getBrand: (data) => {
return request({
url: '/product/brand/list',
method: 'GET',
params: data,
})
},
// 历史属性
getHistoryProperty: () => {
return request({
url: '/product/property/history-list',
method: 'GET',
})
},
getPropertyList: (data) => {
return request({
url: '/product/property/get',
method: 'GET',
params: data,
})
},
} }
export default GoodsApi export default GoodsApi

View File

@ -48,9 +48,9 @@
</view> </view>
</view> </view>
<view class="ss-flex ss-row-around" :style="btnStyles"> <view class="ss-flex ss-row-around" :style="btnStyles">
<button class="ss-reset-button btn-group">详情</button> <button class="ss-reset-button btn-group" @click="clickGoods('detail')">详情</button>
<button class="ss-reset-button btn-group">编辑</button> <button class="ss-reset-button btn-group" @click="clickGoods('edit')">编辑</button>
<button class="ss-reset-button btn-group btn-del">删除</button> <button class="ss-reset-button btn-group btn-del" @click="clickGoods('del')">删除</button>
</view> </view>
</view> </view>
</template> </template>
@ -60,6 +60,7 @@ import peach from '@/peach'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { isArray } from 'lodash' import { isArray } from 'lodash'
import { fen2yuan, formatSales, formatStock } from '@/peach/hooks/useGoods' import { fen2yuan, formatSales, formatStock } from '@/peach/hooks/useGoods'
import { unix } from 'dayjs'
const props = defineProps({ const props = defineProps({
goodsFields: { goodsFields: {
@ -147,6 +148,24 @@ const salesAndStock = computed(() => {
} }
return text.join(' | ') return text.join(' | ')
}) })
function clickGoods(mark) {
if (mark === 'detail' || mark === 'edit') {
peach.$router.go('/pages/product/manageGoods', {
id: props.data.id,
mark: mark,
})
} else if (mark === 'del') {
uni.showModal({
title: '提示',
content: '是否删除该商品?',
success: (res) => {
if (res.confirm) {
}
},
})
}
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -106,7 +106,7 @@ http.interceptors.request.use(
*/ */
http.interceptors.response.use( http.interceptors.response.use(
(response) => { (response) => {
console.log('response', response) // console.log('response', response)
// 约定:如果是 /auth/ 下的 URL 地址,并且返回了 accessToken 说明是登录相关的接口,则自动设置登陆令牌 // 约定:如果是 /auth/ 下的 URL 地址,并且返回了 accessToken 说明是登录相关的接口,则自动设置登陆令牌
if (response.config.url.indexOf('/member/auth/') >= 0 && response.data?.data?.accessToken) { if (response.config.url.indexOf('/member/auth/') >= 0 && response.data?.data?.accessToken) {
$store('user').setToken(response.data.data.accessToken, response.data.data.refreshToken) $store('user').setToken(response.data.data.accessToken, response.data.data.refreshToken)

17
peach/store/trade.js Normal file
View File

@ -0,0 +1,17 @@
import { ref } from 'vue'
import { defineStore } from 'pinia'
const useTradeStore = defineStore('trade', () => {
// 已选择规格类型
const selectedProperty = ref(null)
// 商品信息
const goodsInfo = ref(null)
return {
selectedProperty,
goodsInfo,
}
})
export default useTradeStore