!153 提交商品管理第二版

Merge pull request !153 from puhui999/dev-to-dev
This commit is contained in:
芋道源码 2023-05-21 16:01:53 +00:00 committed by Gitee
commit 855e4d3d02
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
17 changed files with 705 additions and 683 deletions

View File

@ -54,3 +54,8 @@ export const getBrand = (id: number) => {
export const getBrandParam = (params: PageParam) => {
return request.get({ url: '/product/brand/page', params })
}
// 获得商品品牌精简信息列表
export const getSimpleBrandList = () => {
return request.get({ url: '/product/brand/list-all-simple' })
}

View File

@ -1,39 +0,0 @@
import request from '@/config/axios'
import type { SpuType } from './type/spuType' // TODO @puhui999: type 和 api 一起放,简单一点哈~
// TODO @puhui999中英文之间有空格
// 获得spu列表 TODO @puhui999这个是 getSpuPage 哈
export const getSpuList = (params: PageParam) => {
return request.get({ url: '/product/spu/page', params })
}
// 获得spu列表tabsCount
export const getTabsCount = () => {
return request.get({ url: '/product/spu/tabsCount' })
}
// 创建商品spu
export const createSpu = (data: SpuType) => {
return request.post({ url: '/product/spu/create', data })
}
// 更新商品spu
export const updateSpu = (data: SpuType) => {
return request.put({ url: '/product/spu/update', data })
}
// 更新商品spu status
export const updateStatus = (data: { id: number; status: number }) => {
return request.put({ url: '/product/spu/updateStatus', data })
}
// 获得商品 spu
export const getSpu = (id: number) => {
return request.get({ url: `/product/spu/get-detail?id=${id}` })
}
// 删除商品Spu
export const deleteSpu = (id: number) => {
return request.delete({ url: `/product/spu/delete?id=${id}` })
}

View File

@ -1,79 +0,0 @@
export interface Property {
/**
*
*
* {@link ProductPropertyDO#getId()}
*/
propertyId?: number
/**
*
*
* {@link ProductPropertyValueDO#getId()}
*/
valueId?: number
/**
*
*/
valueName?: string
}
export interface SkuType {
/**
* SKU
*/
id?: number
/**
* SPU
*/
spuId?: number
/**
* JSON
*/
properties?: Property[]
/**
*
*/
price?: number
/**
*
*/
marketPrice?: number
/**
*
*/
costPrice?: number
/**
*
*/
barCode?: string
/**
*
*/
picUrl?: string
/**
*
*/
stock?: number
/**
* kg
*/
weight?: number
/**
* m^3
*/
volume?: number
/**
*
*/
subCommissionFirstPrice?: number
/**
*
*/
subCommissionSecondPrice?: number
/**
*
*/
salesCount?: number
}

View File

@ -1,25 +0,0 @@
import { SkuType } from './skuType'
export interface SpuType {
id?: number
name?: string // 商品名称
categoryId?: number | null // 商品分类
keyword?: string // 关键字
unit?: number | null // 单位
picUrl?: string // 商品封面图
sliderPicUrls?: string[] // 商品轮播图
introduction?: string // 商品简介
deliveryTemplateId?: number // 运费模版
specType?: boolean // 商品规格
subCommissionType?: boolean // 分销类型
skus: SkuType[] // sku数组
description?: string // 商品详情
sort?: string // 商品排序
giveIntegral?: number // 赠送积分
virtualSalesCount?: number // 虚拟销量
recommendHot?: boolean // 是否热卖
recommendBenefit?: boolean // 是否优惠
recommendBest?: boolean // 是否精品
recommendNew?: boolean // 是否新品
recommendGood?: boolean // 是否优品
}

View File

@ -0,0 +1,90 @@
import request from '@/config/axios'
export interface Property {
propertyId?: number // 属性编号
propertyName?: string // 属性名称
valueId?: number // 属性值编号
valueName?: string // 属性值名称
}
export interface SkuType {
id?: number // 商品 SKU 编号
spuId?: number // SPU 编号
properties?: Property[] // 属性数组
price?: number // 商品价格
marketPrice?: number // 市场价
costPrice?: number // 成本价
barCode?: string // 商品条码
picUrl?: string // 图片地址
stock?: number // 库存
weight?: number // 商品重量单位kg 千克
volume?: number // 商品体积单位m^3 平米
subCommissionFirstPrice?: number // 一级分销的佣金
subCommissionSecondPrice?: number // 二级分销的佣金
salesCount?: number // 商品销量
}
export interface SpuType {
id?: number
name?: string // 商品名称
categoryId?: number | null // 商品分类
keyword?: string // 关键字
unit?: number | null // 单位
picUrl?: string // 商品封面图
sliderPicUrls?: string[] // 商品轮播图
introduction?: string // 商品简介
deliveryTemplateId?: number | null // 运费模版
brandId?: number | null // 商品品牌编号
specType?: boolean // 商品规格
subCommissionType?: boolean // 分销类型
skus: SkuType[] // sku数组
description?: string // 商品详情
sort?: string // 商品排序
giveIntegral?: number // 赠送积分
virtualSalesCount?: number // 虚拟销量
recommendHot?: boolean // 是否热卖
recommendBenefit?: boolean // 是否优惠
recommendBest?: boolean // 是否精品
recommendNew?: boolean // 是否新品
recommendGood?: boolean // 是否优品
}
// 获得 Spu 列表
export const getSpuPage = (params: PageParam) => {
return request.get({ url: '/product/spu/page', params })
}
// 获得 Spu 列表 tabsCount
export const getTabsCount = () => {
return request.get({ url: '/product/spu/get-count' })
}
// 创建商品 Spu
export const createSpu = (data: SpuType) => {
return request.post({ url: '/product/spu/create', data })
}
// 更新商品 Spu
export const updateSpu = (data: SpuType) => {
return request.put({ url: '/product/spu/update', data })
}
// 更新商品 Spu status
export const updateStatus = (data: { id: number; status: number }) => {
return request.put({ url: '/product/spu/update-status', data })
}
// 获得商品 Spu
export const getSpu = (id: number) => {
return request.get({ url: `/product/spu/get-detail?id=${id}` })
}
// 删除商品 Spu
export const deleteSpu = (id: number) => {
return request.delete({ url: `/product/spu/delete?id=${id}` })
}
// 导出商品 Spu
export const exportUser = (params) => {
return request.download({ url: '/product/spu/export', params })
}

View File

@ -1,19 +1,19 @@
<template>
<div class="upload-box">
<el-upload
:action="updateUrl"
list-type="picture-card"
:class="['upload', drag ? 'no-border' : '']"
v-model:file-list="fileList"
:multiple="true"
:limit="limit"
:headers="uploadHeaders"
:accept="fileType.join(',')"
:action="updateUrl"
:before-upload="beforeUpload"
:class="['upload', drag ? 'no-border' : '']"
:drag="drag"
:headers="uploadHeaders"
:limit="limit"
:multiple="true"
:on-error="uploadError"
:on-exceed="handleExceed"
:on-success="uploadSuccess"
:on-error="uploadError"
:drag="drag"
:accept="fileType.join(',')"
list-type="picture-card"
>
<div class="upload-empty">
<slot name="empty">
@ -40,15 +40,15 @@
</div>
<el-image-viewer
v-if="imgViewVisible"
@close="imgViewVisible = false"
:url-list="[viewImageUrl]"
@close="imgViewVisible = false"
/>
</div>
</template>
<script setup lang="ts" name="UploadImgs">
<script lang="ts" name="UploadImgs" setup>
import { PropType } from 'vue'
import type { UploadFile, UploadProps, UploadUserFile } from 'element-plus'
import { ElNotification } from 'element-plus'
import type { UploadProps, UploadFile, UploadUserFile } from 'element-plus'
import { propTypes } from '@/utils/propTypes'
import { getAccessToken, getTenantId } from '@/utils/auth'
@ -88,8 +88,19 @@ const uploadHeaders = ref({
'tenant-id': getTenantId()
})
const fileList = ref<UploadUserFile[]>(props.modelValue)
const fileList = ref<UploadUserFile[]>()
// fix:
watch(
() => props.modelValue,
(data) => {
if (!data) return
fileList.value = data
},
{
deep: true,
immediate: true
}
)
/**
* @description 文件上传之前判断
* @param rawFile 上传的文件
@ -116,9 +127,11 @@ const beforeUpload: UploadProps['beforeUpload'] = (rawFile) => {
interface UploadEmits {
(e: 'update:modelValue', value: UploadUserFile[]): void
}
const emit = defineEmits<UploadEmits>()
const uploadSuccess = (response, uploadFile: UploadFile) => {
if (!response) return
// TODO urlfileList
uploadFile.url = response.data
emit('update:modelValue', fileList.value)
message.success('上传成功')
@ -159,35 +172,40 @@ const handlePictureCardPreview: UploadProps['onPreview'] = (uploadFile) => {
}
</script>
<style scoped lang="scss">
<style lang="scss" scoped>
.is-error {
.upload {
:deep(.el-upload--picture-card),
:deep(.el-upload-dragger) {
border: 1px dashed var(--el-color-danger) !important;
&:hover {
border-color: var(--el-color-primary) !important;
}
}
}
}
:deep(.disabled) {
.el-upload--picture-card,
.el-upload-dragger {
cursor: not-allowed;
background: var(--el-disabled-bg-color) !important;
border: 1px dashed var(--el-border-color-darker);
&:hover {
border-color: var(--el-border-color-darker) !important;
}
}
}
.upload-box {
.no-border {
:deep(.el-upload--picture-card) {
border: none !important;
}
}
:deep(.upload) {
.el-upload-dragger {
display: flex;
@ -199,14 +217,17 @@ const handlePictureCardPreview: UploadProps['onPreview'] = (uploadFile) => {
overflow: hidden;
border: 1px dashed var(--el-border-color-darker);
border-radius: v-bind(borderRadius);
&:hover {
border: 1px dashed var(--el-color-primary);
}
}
.el-upload-dragger.is-dragover {
background-color: var(--el-color-primary-light-9);
border: 2px dashed var(--el-color-primary) !important;
}
.el-upload-list__item,
.el-upload--picture-card {
width: v-bind(width);
@ -214,11 +235,13 @@ const handlePictureCardPreview: UploadProps['onPreview'] = (uploadFile) => {
background-color: transparent;
border-radius: v-bind(borderRadius);
}
.upload-image {
width: 100%;
height: 100%;
object-fit: contain;
}
.upload-handle {
position: absolute;
top: 0;
@ -233,6 +256,7 @@ const handlePictureCardPreview: UploadProps['onPreview'] = (uploadFile) => {
background: rgb(0 0 0 / 60%);
opacity: 0;
transition: var(--el-transition-duration-fast);
.handle-icon {
display: flex;
flex-direction: column;
@ -240,15 +264,18 @@ const handlePictureCardPreview: UploadProps['onPreview'] = (uploadFile) => {
justify-content: center;
padding: 0 6%;
color: aliceblue;
.el-icon {
margin-bottom: 15%;
font-size: 140%;
}
span {
font-size: 100%;
}
}
}
.el-upload-list__item {
&:hover {
.upload-handle {
@ -256,6 +283,7 @@ const handlePictureCardPreview: UploadProps['onPreview'] = (uploadFile) => {
}
}
}
.upload-empty {
display: flex;
flex-direction: column;
@ -263,12 +291,14 @@ const handlePictureCardPreview: UploadProps['onPreview'] = (uploadFile) => {
font-size: 12px;
line-height: 30px;
color: var(--el-color-info);
.el-icon {
font-size: 28px;
color: var(--el-text-color-secondary);
}
}
}
.el-upload__tip {
line-height: 15px;
text-align: center;

View File

@ -349,22 +349,35 @@ const remainingRouter: AppRouteRecordRaw[] = [
{
path: '/product',
component: Layout,
name: 'ProductManagementEdit',
name: 'Product',
meta: {
hidden: true
},
children: [
{
path: 'productManagementAdd', // TODO @puhui999最好拆成 add 和 edit 两个路由;添加商品;修改商品
path: 'productSpuAdd', // TODO @puhui999最好拆成 add 和 edit 两个路由;添加商品;修改商品 fix
component: () => import('@/views/mall/product/spu/addForm.vue'),
name: 'ProductManagementAdd',
name: 'ProductSpuAdd',
meta: {
noCache: true,
hidden: true,
canTo: true,
icon: 'ep:edit',
title: '添加商品',
activeMenu: '/product/product-management'
activeMenu: '/product/product-spu'
}
},
{
path: 'productSpuEdit/:spuId(\\d+)',
component: () => import('@/views/mall/product/spu/addForm.vue'),
name: 'productSpuEdit',
meta: {
noCache: true,
hidden: true,
canTo: true,
icon: 'ep:edit',
title: '编辑商品',
activeMenu: '/product/product-spu'
}
}
]

View File

@ -155,3 +155,42 @@ export const fileSizeFormatter = (row, column, cellValue) => {
const sizeStr = size.toFixed(2) //保留的小数位数
return sizeStr + ' ' + unitArr[index]
}
/**
* target: {a:1} source:{a:2,b:3} {a:2}
* @param target
* @param source
*/
export const copyValueToTarget = (target, source) => {
const newObj = Object.assign({}, target, source)
// 删除多余属性
Object.keys(newObj).forEach((key) => {
// 如果不是target中的属性则删除
if (Object.keys(target).indexOf(key) === -1) {
delete newObj[key]
}
})
// 更新目标对象值
Object.assign(target, newObj)
}
/**
*
* @param num
*/
export const formatToFraction = (num: number | string | undefined): number => {
if (typeof num === 'undefined') return 0
const parsedNumber = typeof num === 'string' ? parseFloat(num) : num
return parseFloat((parsedNumber / 100).toFixed(2))
}
/**
*
* @param num
*/
export const convertToInteger = (num: number | string | undefined): number => {
if (typeof num === 'undefined') return 0
const parsedNumber = typeof num === 'string' ? parseFloat(num) : num
// TODO 分转元后还有小数则四舍五入
return Math.round(parsedNumber * 100)
}

View File

@ -1,18 +0,0 @@
// TODO @puhui999这个方法可以考虑放到 index.js
/**
* target: {a:1} source:{a:2,b:3} {a:2}
* @param target
* @param source
*/
export const copyValueToTarget = (target, source) => {
const newObj = Object.assign({}, target, source)
// 删除多余属性
Object.keys(newObj).forEach((key) => {
// 如果不是target中的属性则删除
if (Object.keys(target).indexOf(key) === -1) {
delete newObj[key]
}
})
// 更新目标对象值
Object.assign(target, newObj)
}

View File

@ -3,21 +3,21 @@
<el-tabs v-model="activeName">
<el-tab-pane label="商品信息" name="basicInfo">
<BasicInfoForm
ref="BasicInfoRef"
ref="basicInfoRef"
v-model:activeName="activeName"
:propFormData="formData"
/>
</el-tab-pane>
<el-tab-pane label="商品详情" name="description">
<DescriptionForm
ref="DescriptionRef"
ref="descriptionRef"
v-model:activeName="activeName"
:propFormData="formData"
/>
</el-tab-pane>
<el-tab-pane label="其他设置" name="otherSettings">
<OtherSettingsForm
ref="OtherSettingsRef"
ref="otherSettingsRef"
v-model:activeName="activeName"
:propFormData="formData"
/>
@ -31,88 +31,56 @@
</el-form>
</ContentWrap>
</template>
<script lang="ts" name="ProductManagementForm" setup>
<script lang="ts" name="ProductSpuForm" setup>
import { cloneDeep } from 'lodash-es'
import { useTagsViewStore } from '@/store/modules/tagsView'
import { BasicInfoForm, DescriptionForm, OtherSettingsForm } from './components'
import type { SpuType } from '@/api/mall/product/management/type/spuType' // api
import * as managementApi from '@/api/mall/product/management/spu'
import * as PropertyApi from '@/api/mall/product/property'
// api
import * as ProductSpuApi from '@/api/mall/product/spu'
import { convertToInteger, formatToFraction } from '@/utils'
const { t } = useI18n() //
const message = useMessage() //
const { push, currentRoute } = useRouter() //
const { query } = useRoute() //
const { params } = useRoute() //
const { delView } = useTagsViewStore() //
const formLoading = ref(false) // 12
const activeName = ref('basicInfo') // Tag
const BasicInfoRef = ref<ComponentRef<typeof BasicInfoForm>>() // Ref
const DescriptionRef = ref<ComponentRef<typeof DescriptionForm>>() // Ref
const OtherSettingsRef = ref<ComponentRef<typeof OtherSettingsForm>>() // Ref
const formData = ref<SpuType>({
name: '213', //
const basicInfoRef = ref<ComponentRef<typeof BasicInfoForm>>() // Ref
const descriptionRef = ref<ComponentRef<typeof DescriptionForm>>() // Ref
const otherSettingsRef = ref<ComponentRef<typeof OtherSettingsForm>>() // Ref
// spu
const formData = ref<ProductSpuApi.SpuType>({
name: '', //
categoryId: null, //
keyword: '213', //
keyword: '', //
unit: null, //
picUrl:
'http://127.0.0.1:48080/admin-api/infra/file/4/get/6ffdf8f5dfc03f7ceec1ff1ebc138adb8b721a057d505ccfb0e61a8783af1371.png', //
sliderPicUrls: [
{
name: 'http://127.0.0.1:48080/admin-api/infra/file/4/get/6ffdf8f5dfc03f7ceec1ff1ebc138adb8b721a057d505ccfb0e61a8783af1371.png',
url: 'http://127.0.0.1:48080/admin-api/infra/file/4/get/6ffdf8f5dfc03f7ceec1ff1ebc138adb8b721a057d505ccfb0e61a8783af1371.png'
}
], //
introduction: '213', //
deliveryTemplateId: 0, //
picUrl: '', //
sliderPicUrls: [], //
introduction: '', //
deliveryTemplateId: 1, //
brandId: null, //
specType: false, //
subCommissionType: false, //
skus: [
{
/**
* 商品价格单位 TODO @puhui999注释放在尾巴哈简洁一点~
*/
price: 0,
/**
* 市场价单位
*/
marketPrice: 0,
/**
* 成本价单位
*/
costPrice: 0,
/**
* 商品条码
*/
barCode: '',
/**
* 图片地址
*/
picUrl: '',
/**
* 库存
*/
stock: 0,
/**
* 商品重量单位kg 千克
*/
weight: 0,
/**
* 商品体积单位m^3 平米
*/
volume: 0,
/**
* 一级分销的佣金单位
*/
subCommissionFirstPrice: 0,
/**
* 二级分销的佣金单位
*/
subCommissionSecondPrice: 0
price: 0, //
marketPrice: 0, //
costPrice: 0, //
barCode: '', //
picUrl: '', //
stock: 0, //
weight: 0, //
volume: 0, //
subCommissionFirstPrice: 0, //
subCommissionSecondPrice: 0 //
}
],
description: '5425', //
sort: 1, //
giveIntegral: 1, //
virtualSalesCount: 1, //
description: '', //
sort: 0, //
giveIntegral: 0, //
virtualSalesCount: 0, //
recommendHot: false, //
recommendBenefit: false, //
recommendBest: false, //
@ -122,19 +90,20 @@ const formData = ref<SpuType>({
/** 获得详情 */
const getDetail = async () => {
const id = query.id as unknown as number
const id = params.spuId as number
if (id) {
formLoading.value = true
try {
const res = (await managementApi.getSpu(id)) as SpuType
const res = (await ProductSpuApi.getSpu(id)) as ProductSpuApi.SpuType
res.skus.forEach((item) => {
//
item.price = formatToFraction(item.price)
item.marketPrice = formatToFraction(item.marketPrice)
item.costPrice = formatToFraction(item.costPrice)
item.subCommissionFirstPrice = formatToFraction(item.subCommissionFirstPrice)
item.subCommissionSecondPrice = formatToFraction(item.subCommissionSecondPrice)
})
formData.value = res
// id
// TODO @puhui999 propertyName id + uniapp
const propertyIds = res.skus[0]?.properties.map((item) => item.propertyId)
const PropertyS = await PropertyApi.getPropertyListAndValue({ propertyIds })
await nextTick()
//
BasicInfoRef.value.addAttribute(PropertyS)
} finally {
formLoading.value = false
}
@ -145,96 +114,66 @@ const getDetail = async () => {
const submitForm = async () => {
//
formLoading.value = true
const newSkus = JSON.parse(JSON.stringify(formData.value.skus)) //skus使
// TODO
//
//
try {
await unref(BasicInfoRef)?.validate()
await unref(DescriptionRef)?.validate()
await unref(OtherSettingsRef)?.validate()
// TODO @puhui server
//
formData.value.skus.forEach((item) => {
// sku name
item.name = formData.value.name
// skusvalue
if (formData.value.specType) {
item.properties.forEach((item2) => {
delete item2.valueName
})
await unref(basicInfoRef)?.validate()
await unref(descriptionRef)?.validate()
await unref(otherSettingsRef)?.validate()
const deepCopyFormData = cloneDeep(unref(formData.value)) // fix: server
// TODO sku
formData.value.skus.forEach((sku) => {
//
if (sku.barCode === '') {
const index = deepCopyFormData.skus.findIndex(
(item) => JSON.stringify(item.properties) === JSON.stringify(sku.properties)
)
// sku
deepCopyFormData.skus.splice(index, 1)
}
})
deepCopyFormData.skus.forEach((item) => {
// sku name
item.name = deepCopyFormData.name
// sku
item.price = convertToInteger(item.price)
item.marketPrice = convertToInteger(item.marketPrice)
item.costPrice = convertToInteger(item.costPrice)
item.subCommissionFirstPrice = convertToInteger(item.subCommissionFirstPrice)
item.subCommissionSecondPrice = convertToInteger(item.subCommissionSecondPrice)
})
//
const newSliderPicUrls = []
formData.value.sliderPicUrls.forEach((item) => {
deepCopyFormData.sliderPicUrls.forEach((item) => {
//
// TODO @puhui999 object
if (typeof item === 'object') {
newSliderPicUrls.push(item.url)
} else {
newSliderPicUrls.push(item)
}
// TODO @puhui999 object fix
typeof item === 'object' ? newSliderPicUrls.push(item.url) : newSliderPicUrls.push(item)
})
formData.value.sliderPicUrls = newSliderPicUrls
deepCopyFormData.sliderPicUrls = newSliderPicUrls
//
const data = formData.value as SpuType
// skus.
const id = query.id as unknown as number
const data = deepCopyFormData as ProductSpuApi.SpuType
const id = params.spuId as number
if (!id) {
await managementApi.createSpu(data)
await ProductSpuApi.createSpu(data)
message.success(t('common.createSuccess'))
} else {
await managementApi.updateSpu(data)
await ProductSpuApi.updateSpu(data)
message.success(t('common.updateSuccess'))
}
close()
} catch (e) {
// ,skus
if (typeof e === 'string') {
formData.value.skus = newSkus
}
} finally {
formLoading.value = false
}
}
/**
* 重置表单
*/
const resetForm = async () => {
formData.value = {
name: '', //
categoryId: 0, //
keyword: '', //
unit: '', //
picUrl: '', //
sliderPicUrls: [], //
introduction: '', //
deliveryTemplateId: 0, //
selectRule: '',
specType: false, //
subCommissionType: false, //
description: '', //
sort: 1, //
giveIntegral: 1, //
virtualSalesCount: 1, //
recommendHot: false, //
recommendBenefit: false, //
recommendBest: false, //
recommendNew: false, //
recommendGood: false //
}
}
/** 关闭按钮 */
const close = () => {
// TODO @puhui999 reset close
resetForm()
delView(unref(currentRoute))
push('/product/product-management')
push('/product/product-spu')
}
/** 初始化 */
onMounted(() => {
getDetail()
onMounted(async () => {
await getDetail()
})
</script>

View File

@ -1,5 +1,5 @@
<template>
<el-form ref="ProductManagementBasicInfoRef" :model="formData" :rules="rules" label-width="120px">
<el-form ref="productSpuBasicInfoRef" :model="formData" :rules="rules" label-width="120px">
<el-row>
<el-col :span="12">
<el-form-item label="商品名称" prop="name">
@ -14,9 +14,9 @@
:data="categoryList"
:props="defaultProps"
check-strictly
class="w-1/1"
node-key="id"
placeholder="请选择商品分类"
class="w-1/1"
/>
</el-form-item>
</el-col>
@ -27,7 +27,7 @@
</el-col>
<el-col :span="12">
<el-form-item label="单位" prop="unit">
<el-select v-model="formData.unit" placeholder="请选择单位" class="w-1/1">
<el-select v-model="formData.unit" class="w-1/1" placeholder="请选择单位">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.PRODUCT_UNIT)"
:key="dict.value"
@ -54,18 +54,28 @@
</el-col>
<el-col :span="24">
<el-form-item label="商品轮播图" prop="sliderPicUrls">
<UploadImgs v-model="formData.sliderPicUrls" />
<UploadImgs v-model:modelValue="formData.sliderPicUrls" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="运费模板" prop="deliveryTemplateId">
<el-select v-model="formData.deliveryTemplateId" placeholder="请选择" class="w-1/1">
<el-select v-model="formData.deliveryTemplateId" placeholder="请选择">
<el-option v-for="item in []" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
<el-button class="ml-20px">运费模板</el-button>
</el-form-item>
</el-col>
<el-col :span="12">
<el-button class="ml-20px">运费模板</el-button>
<el-form-item label="品牌" prop="brandId">
<el-select v-model="formData.brandId" placeholder="请选择">
<el-option
v-for="item in brandList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="商品规格" props="specType">
@ -86,36 +96,38 @@
<!-- 多规格添加-->
<el-col :span="24">
<el-form-item v-if="formData.specType" label="商品属性">
<!-- TODO @puhui999参考 https://admin.java.crmeb.net/store/list/creatProduct -->
<el-button class="mr-15px mb-10px" @click="AttributesAddFormRef.open">添加规格</el-button>
<ProductAttributes :attribute-data="attributeList" />
<!-- TODO @puhui999参考 https://admin.java.crmeb.net/store/list/creatProduct fix-->
<el-button class="mr-15px mb-10px" @click="attributesAddFormRef.open">添加规格</el-button>
<ProductAttributes :propertyList="propertyList" @success="generateSkus" />
</el-form-item>
<template v-if="formData.specType && attributeList.length > 0">
<template v-if="formData.specType && propertyList.length > 0">
<el-form-item label="批量设置">
<SkuList :attributeList="attributeList" :is-batch="true" :prop-form-data="formData" />
<SkuList :is-batch="true" :prop-form-data="formData" :propertyList="propertyList" />
</el-form-item>
<el-form-item label="属性列表">
<SkuList :attributeList="attributeList" :prop-form-data="formData" />
<SkuList ref="skuListRef" :prop-form-data="formData" :propertyList="propertyList" />
</el-form-item>
</template>
<el-form-item v-if="!formData.specType">
<SkuList :attributeList="attributeList" :prop-form-data="formData" />
<SkuList :prop-form-data="formData" :propertyList="propertyList" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<ProductAttributesAddForm ref="AttributesAddFormRef" @success="addAttribute" />
<ProductAttributesAddForm ref="attributesAddFormRef" :propertyList="propertyList" />
</template>
<script lang="ts" name="ProductManagementBasicInfoForm" setup>
<script lang="ts" name="ProductSpuBasicInfoForm" setup>
import { PropType } from 'vue'
import { copyValueToTarget } from '@/utils'
import { propTypes } from '@/utils/propTypes'
import { defaultProps, handleTree } from '@/utils/tree'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import type { SpuType } from '@/api/mall/product/management/type/spuType'
import type { SpuType } from '@/api/mall/product/spu'
import { UploadImg, UploadImgs } from '@/components/UploadFile'
import { copyValueToTarget } from '@/utils/object'
import { ProductAttributes, ProductAttributesAddForm, SkuList } from './index'
import * as ProductCategoryApi from '@/api/mall/product/category'
import { propTypes } from '@/utils/propTypes'
import { getSimpleBrandList } from '@/api/mall/product/brand'
const message = useMessage() //
const props = defineProps({
@ -125,27 +137,25 @@ const props = defineProps({
},
activeName: propTypes.string.def('')
})
const AttributesAddFormRef = ref() // TODO @puhui999
const ProductManagementBasicInfoRef = ref() // Ref TODO @puhui999
// TODO @puhui999attributeList propertyList
const attributeList = ref([]) //
/** 添加商品属性 */ // TODO @puhui999propFormData
const addAttribute = (property: any) => {
if (Array.isArray(property)) {
attributeList.value = property
return
}
attributeList.value.push(property)
const attributesAddFormRef = ref() // TODO @puhui999 fix
const productSpuBasicInfoRef = ref() // Ref TODO @puhui999 fix
// TODO @puhui999attributeList propertyList fix
const propertyList = ref([]) //
const skuListRef = ref() // Ref
/** 调用 SkuList generateTableData 方法*/
const generateSkus = (propertyList) => {
skuListRef.value.generateTableData(propertyList)
}
const formData = reactive<SpuType>({
name: '', //
categoryId: undefined, //
categoryId: null, //
keyword: '', //
unit: '', //
picUrl: '', //
sliderPicUrls: [], //
introduction: '', //
deliveryTemplateId: 1, //
brandId: null, //
specType: false, //
subCommissionType: false, //
skus: []
@ -159,6 +169,7 @@ const rules = reactive({
picUrl: [required],
sliderPicUrls: [required],
// deliveryTemplateId: [required],
brandId: [required],
specType: [required],
subCommissionType: [required]
})
@ -170,10 +181,35 @@ watch(
() => props.propFormData,
(data) => {
if (!data) return
// fix使 copyValueToTarget 使 formData.value = data
copyValueToTarget(formData, data)
// fix: url
formData.sliderPicUrls = data['sliderPicUrls'].map((item) => ({
url: item
}))
//
if (formData.specType) {
// TODO @puhui999 propertyName id + uniapp
// fix: skus propertyList
const properties = []
formData.skus.forEach((sku) => {
sku.properties.forEach(({ propertyId, propertyName, valueId, valueName }) => {
//
if (!properties.some((item) => item.id === propertyId)) {
properties.push({ id: propertyId, name: propertyName, values: [] })
}
//
const index = properties.findIndex((item) => item.id === propertyId)
if (!properties[index].values.some((value) => value.id === valueId)) {
properties[index].values.push({ id: valueId, name: valueName })
}
})
})
propertyList.value = properties
}
},
{
deep: true,
// fix: ,
immediate: true
}
)
@ -184,8 +220,8 @@ watch(
const emit = defineEmits(['update:activeName'])
const validate = async () => {
//
if (!ProductManagementBasicInfoRef) return
return await unref(ProductManagementBasicInfoRef).validate((valid) => {
if (!productSpuBasicInfoRef) return
return await unref(productSpuBasicInfoRef).validate((valid) => {
if (!valid) {
message.warning('商品信息未完善!!')
emit('update:activeName', 'basicInfo')
@ -197,7 +233,7 @@ const validate = async () => {
}
})
}
defineExpose({ validate, addAttribute })
defineExpose({ validate })
/** 分销类型 */
const changeSubCommissionType = () => {
@ -211,7 +247,7 @@ const changeSubCommissionType = () => {
/** 选择规格 */
const onChangeSpec = () => {
//
attributeList.value = []
propertyList.value = []
// sku
formData.skus = [
{
@ -229,10 +265,13 @@ const onChangeSpec = () => {
]
}
const categoryList = ref() //
const categoryList = ref([]) //
const brandList = ref([]) //
onMounted(async () => {
//
const data = await ProductCategoryApi.getCategoryList({})
categoryList.value = handleTree(data, 'id', 'parentId')
//
brandList.value = await getSimpleBrandList()
})
</script>

View File

@ -1,5 +1,5 @@
<template>
<el-form ref="DescriptionFormRef" :model="formData" :rules="rules" label-width="120px">
<el-form ref="descriptionFormRef" :model="formData" :rules="rules" label-width="120px">
<!--富文本编辑器组件-->
<el-form-item label="商品详情" prop="description">
<Editor v-model:modelValue="formData.description" />
@ -7,11 +7,11 @@
</el-form>
</template>
<script lang="ts" name="DescriptionForm" setup>
import type { SpuType } from '@/api/mall/product/management/type/spuType'
import type { SpuType } from '@/api/mall/product/spu'
import { Editor } from '@/components/Editor'
import { PropType } from 'vue'
import { copyValueToTarget } from '@/utils/object'
import { propTypes } from '@/utils/propTypes'
import { copyValueToTarget } from '@/utils'
const message = useMessage() //
const props = defineProps({
@ -21,7 +21,7 @@ const props = defineProps({
},
activeName: propTypes.string.def('')
})
const DescriptionFormRef = ref() // Ref
const descriptionFormRef = ref() // Ref
const formData = ref<SpuType>({
description: '' //
})
@ -29,7 +29,6 @@ const formData = ref<SpuType>({
const rules = reactive({
description: [required]
})
/**
* 富文本编辑器如果输入过再清空会有残留需再重置一次
*/
@ -45,7 +44,6 @@ watch(
immediate: true
}
)
/**
* 将传进来的值赋值给formData
*/
@ -53,10 +51,11 @@ watch(
() => props.propFormData,
(data) => {
if (!data) return
// fix使 copyValueToTarget 使 formData.value = data
copyValueToTarget(formData.value, data)
},
{
deep: true,
// fix: ,
immediate: true
}
)
@ -67,8 +66,8 @@ watch(
const emit = defineEmits(['update:activeName'])
const validate = async () => {
//
if (!DescriptionFormRef) return
return unref(DescriptionFormRef).validate((valid) => {
if (!descriptionFormRef) return
return await unref(descriptionFormRef).validate((valid) => {
if (!valid) {
message.warning('商品详情为完善!!')
emit('update:activeName', 'description')

View File

@ -1,32 +1,34 @@
<template>
<el-form ref="OtherSettingsFormRef" :model="formData" :rules="rules" label-width="120px">
<el-form ref="otherSettingsFormRef" :model="formData" :rules="rules" label-width="120px">
<el-row>
<!-- TODO @puhui999横着三个哈 -->
<!-- TODO @puhui999横着三个哈 fix-->
<el-col :span="24">
<el-col :span="8">
<el-form-item label="商品排序" prop="sort">
<el-input-number v-model="formData.sort" :min="0" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="赠送积分" prop="giveIntegral">
<el-input-number v-model="formData.giveIntegral" :min="0" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="虚拟销量" prop="virtualSalesCount">
<el-input-number
v-model="formData.virtualSalesCount"
:min="0"
placeholder="请输入虚拟销量"
/>
</el-form-item>
</el-col>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="商品排序" prop="sort">
<el-input-number v-model="formData.sort" :min="0" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="赠送积分" prop="giveIntegral">
<el-input-number v-model="formData.giveIntegral" :min="0" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="虚拟销量" prop="virtualSalesCount">
<el-input-number
v-model="formData.virtualSalesCount"
:min="0"
placeholder="请输入虚拟销量"
/>
</el-form-item>
</el-col>
</el-row>
</el-col>
<el-col :span="24">
<el-form-item label="商品推荐">
<el-checkbox-group v-model="checkboxGroup" @change="onChangeGroup">
<el-checkbox v-for="(item, index) in recommend" :key="index" :label="item.value">
<el-checkbox v-for="(item, index) in recommendOptions" :key="index" :label="item.value">
{{ item.name }}
</el-checkbox>
</el-checkbox-group>
@ -51,10 +53,11 @@
</el-form>
</template>
<script lang="ts" name="OtherSettingsForm" setup>
import type { SpuType } from '@/api/mall/product/management/type/spuType'
import type { SpuType } from '@/api/mall/product/spu'
import { PropType } from 'vue'
import { copyValueToTarget } from '@/utils/object'
import { propTypes } from '@/utils/propTypes'
import { copyValueToTarget } from '@/utils'
const message = useMessage() //
const props = defineProps({
@ -64,35 +67,8 @@ const props = defineProps({
},
activeName: propTypes.string.def('')
})
// TODO @puhui999 recommendOptions
const recommend = [
{ name: '是否热卖', value: 'recommendHot' },
{ name: '是否优惠', value: 'recommendBenefit' },
{ name: '是否精品', value: 'recommendBest' },
{ name: '是否新品', value: 'recommendNew' },
{ name: '是否优品', value: 'recommendGood' }
]
const checkboxGroup = ref<string[]>(['recommendHot']) //
/** 选择商品后赋值 */
const onChangeGroup = () => {
// TODO @puhui999 recommend
checkboxGroup.value.includes('recommendHot')
? (formData.value.recommendHot = true)
: (formData.value.recommendHot = false)
checkboxGroup.value.includes('recommendBenefit')
? (formData.value.recommendBenefit = true)
: (formData.value.recommendBenefit = false)
checkboxGroup.value.includes('recommendBest')
? (formData.value.recommendBest = true)
: (formData.value.recommendBest = false)
checkboxGroup.value.includes('recommendNew')
? (formData.value.recommendNew = true)
: (formData.value.recommendNew = false)
checkboxGroup.value.includes('recommendGood')
? (formData.value.recommendGood = true)
: (formData.value.recommendGood = false)
}
const OtherSettingsFormRef = ref() // Ref
const otherSettingsFormRef = ref() // Ref
//
const formData = ref<SpuType>({
sort: 1, //
@ -110,6 +86,23 @@ const rules = reactive({
giveIntegral: [required],
virtualSalesCount: [required]
})
// TODO @puhui999 recommendOptions fix
const recommendOptions = [
{ name: '是否热卖', value: 'recommendHot' },
{ name: '是否优惠', value: 'recommendBenefit' },
{ name: '是否精品', value: 'recommendBest' },
{ name: '是否新品', value: 'recommendNew' },
{ name: '是否优品', value: 'recommendGood' }
] //
const checkboxGroup = ref<string[]>([]) //
/** 选择商品后赋值 */
const onChangeGroup = () => {
// TODO @puhui999 recommendfix
recommendOptions.forEach(({ value }) => {
formData.value[value] = checkboxGroup.value.includes(value)
})
}
/**
* 将传进来的值赋值给formData
@ -118,29 +111,28 @@ watch(
() => props.propFormData,
(data) => {
if (!data) return
// fix使 copyValueToTarget 使 formData.value = data
copyValueToTarget(formData.value, data)
// TODO
checkboxGroup.value = []
formData.value.recommendHot ? checkboxGroup.value.push('recommendHot') : ''
formData.value.recommendBenefit ? checkboxGroup.value.push('recommendBenefit') : ''
formData.value.recommendBest ? checkboxGroup.value.push('recommendBest') : ''
formData.value.recommendNew ? checkboxGroup.value.push('recommendNew') : ''
formData.value.recommendGood ? checkboxGroup.value.push('recommendGood') : ''
recommendOptions.forEach(({ value }) => {
// TODO fix:
if (formData.value[value] && !checkboxGroup.value.includes(value)) {
checkboxGroup.value.push(value)
}
})
},
{
deep: true,
// fix: ,
immediate: true
}
)
/**
* 表单校验
*/
const emit = defineEmits(['update:activeName'])
const validate = async () => {
//
if (!OtherSettingsFormRef) return
return await unref(OtherSettingsFormRef).validate((valid) => {
if (!otherSettingsFormRef) return
return await unref(otherSettingsFormRef).validate((valid) => {
if (!valid) {
message.warning('商品其他设置未完善!!')
emit('update:activeName', 'otherSettings')

View File

@ -2,23 +2,25 @@
<el-col v-for="(item, index) in attributeList" :key="index">
<div>
<el-text class="mx-1">属性名</el-text>
<el-text class="mx-1">{{ item.name }}</el-text>
<el-tag class="mx-1" closable type="success" @close="handleCloseProperty(index)"
>{{ item.name }}
</el-tag>
</div>
<div>
<el-text class="mx-1">属性值</el-text>
<el-tag
v-for="(value, valueIndex) in item.values"
:key="value.id"
:disable-transitions="false"
class="mx-1"
closable
@close="handleClose(index, valueIndex)"
@close="handleCloseValue(index, valueIndex)"
>
{{ value.name }}
</el-tag>
<el-input
v-show="inputVisible(index)"
ref="InputRef"
:id="`input${index}`"
:ref="setInputRef"
v-model="inputValue"
class="!w-20"
size="small"
@ -51,17 +53,25 @@ const inputVisible = computed(() => (index) => {
if (attributeIndex.value === null) return false
if (attributeIndex.value === index) return true
})
const InputRef = ref() //Ref
const inputRef = ref([]) //Ref
/** 解决 ref 在 v-for 中的获取问题*/
const setInputRef = (el) => {
if (el === null || typeof el === 'undefined') return
// id
if (!inputRef.value.some((item) => item.input?.attributes.id === el.input?.attributes.id)) {
inputRef.value.push(el)
}
}
const attributeList = ref([]) //
const props = defineProps({
attributeData: {
propertyList: {
type: Array,
default: () => {}
}
})
watch(
() => props.attributeData,
() => props.propertyList,
(data) => {
if (!data) return
attributeList.value = data
@ -72,18 +82,22 @@ watch(
}
)
/** 删除标签 tagValue 标签值*/
const handleClose = (index, valueIndex) => {
/** 删除属性值*/
const handleCloseValue = (index, valueIndex) => {
attributeList.value[index].values?.splice(valueIndex, 1)
}
/** 删除属性*/
const handleCloseProperty = (index) => {
attributeList.value?.splice(index, 1)
}
/** 显示输入框并获取焦点 */
const showInput = async (index) => {
attributeIndex.value = index
// refRef
InputRef.value[index]!.input!.focus()
inputRef.value[index].focus()
}
const emit = defineEmits(['success']) // success
/** 输入框失去焦点或点击回车时触发 */
const handleInputConfirm = async (index, propertyId) => {
if (inputValue.value) {
@ -92,6 +106,7 @@ const handleInputConfirm = async (index, propertyId) => {
const id = await PropertyApi.createPropertyValue({ propertyId, name: inputValue.value })
attributeList.value[index].values.push({ id, name: inputValue.value })
message.success(t('common.createSuccess'))
emit('success', attributeList.value)
} catch {
message.error('添加失败,请重试') // TODO
}

View File

@ -7,12 +7,9 @@
:rules="formRules"
label-width="80px"
>
<el-form-item label="名称" prop="name">
<el-form-item label="属性名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入名称" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" placeholder="请输入内容" type="textarea" />
</el-form-item>
</el-form>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
@ -30,14 +27,31 @@ const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('添加商品属性') //
const formLoading = ref(false) // 12
const formData = ref({
name: '',
remark: ''
name: ''
})
const formRules = reactive({
name: [{ required: true, message: '名称不能为空', trigger: 'blur' }]
})
const formRef = ref() // Ref
const attributeList = ref([]) //
const props = defineProps({
propertyList: {
type: Array,
default: () => {}
}
})
watch(
() => props.propertyList,
(data) => {
if (!data) return
attributeList.value = data
},
{
deep: true,
immediate: true
}
)
/** 打开弹窗 */
const open = async () => {
dialogVisible.value = true
@ -46,7 +60,6 @@ const open = async () => {
defineExpose({ open }) // open
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
//
if (!formRef) return
@ -60,12 +73,12 @@ const submitForm = async () => {
const res = await PropertyApi.getPropertyListAndValue({ name: data.name })
if (res.length === 0) {
const propertyId = await PropertyApi.createProperty(data)
emit('success', { id: propertyId, ...formData.value, values: [] })
attributeList.value.push({ id: propertyId, ...formData.value, values: [] })
} else {
if (res[0].values === null) {
res[0].values = []
}
emit('success', res[0]) //
attributeList.value.push(res[0]) //
}
message.success(t('common.createSuccess'))
dialogVisible.value = false

View File

@ -1,6 +1,6 @@
<template>
<el-table
:data="isBatch ? SkuData : formData.skus"
:data="isBatch ? skuList : formData.skus"
border
class="tabNumWidth"
max-height="500"
@ -14,7 +14,7 @@
<template v-if="formData.specType && !isBatch">
<!-- 根据商品属性动态添加 -->
<el-table-column
v-for="(item, index) in tableHeaderList"
v-for="(item, index) in tableHeaders"
:key="index"
:label="item.label"
align="center"
@ -25,162 +25,141 @@
</template>
</el-table-column>
</template>
<!-- TODO @puhui999 controls-position="right" 可以去掉哈不然太长了手动输入更方便 -->
<!-- TODO @puhui999 controls-position=" " 可以去掉哈不然太长了手动输入更方便 fix -->
<el-table-column align="center" label="商品条码" min-width="168">
<template #default="{ row }">
<el-input v-model="row.barCode" class="w-100%" />
</template>
</el-table-column>
<!-- TODO @puhui999用户输入的时候是按照元分主要是我们自己用 -->
<el-table-column align="center" label="销售价()" min-width="168">
<!-- TODO @puhui999用户输入的时候是按照元分主要是我们自己用fix -->
<el-table-column align="center" label="销售价()" min-width="168">
<template #default="{ row }">
<el-input-number v-model="row.price" :min="0" class="w-100%" controls-position="right" />
<el-input-number v-model="row.price" :min="0" :precision="2" :step="0.1" class="w-100%" />
</template>
</el-table-column>
<el-table-column align="center" label="市场价()" min-width="168">
<el-table-column align="center" label="市场价()" min-width="168">
<template #default="{ row }">
<el-input-number
v-model="row.marketPrice"
:min="0"
:precision="2"
:step="0.1"
class="w-100%"
controls-position="right"
/>
</template>
</el-table-column>
<el-table-column align="center" label="成本价()" min-width="168">
<el-table-column align="center" label="成本价()" min-width="168">
<template #default="{ row }">
<el-input-number
v-model="row.costPrice"
:min="0"
:precision="2"
:step="0.1"
class="w-100%"
controls-position="right"
/>
</template>
</el-table-column>
<el-table-column align="center" label="库存" min-width="168">
<template #default="{ row }">
<el-input-number v-model="row.stock" :min="0" class="w-100%" controls-position="right" />
<el-input-number v-model="row.stock" :min="0" class="w-100%" />
</template>
</el-table-column>
<el-table-column align="center" label="重量(kg)" min-width="168">
<template #default="{ row }">
<el-input-number v-model="row.weight" :min="0" class="w-100%" controls-position="right" />
<el-input-number v-model="row.weight" :min="0" :precision="2" :step="0.1" class="w-100%" />
</template>
</el-table-column>
<el-table-column align="center" label="体积(m^3)" min-width="168">
<template #default="{ row }">
<el-input-number v-model="row.volume" :min="0" class="w-100%" controls-position="right" />
<el-input-number v-model="row.volume" :min="0" :precision="2" :step="0.1" class="w-100%" />
</template>
</el-table-column>
<template v-if="formData.subCommissionType">
<el-table-column align="center" label="一级返佣()" min-width="168">
<el-table-column align="center" label="一级返佣()" min-width="168">
<template #default="{ row }">
<el-input-number
v-model="row.subCommissionFirstPrice"
:min="0"
:precision="2"
:step="0.1"
class="w-100%"
controls-position="right"
/>
</template>
</el-table-column>
<el-table-column align="center" label="二级返佣()" min-width="168">
<el-table-column align="center" label="二级返佣()" min-width="168">
<template #default="{ row }">
<el-input-number
v-model="row.subCommissionSecondPrice"
:min="0"
:precision="2"
:step="0.1"
class="w-100%"
controls-position="right"
/>
</template>
</el-table-column>
</template>
<el-table-column v-if="formData.specType" align="center" fixed="right" label="操作" width="80">
<template #default>
<template #default="{ row }">
<el-button v-if="isBatch" link size="small" type="primary" @click="batchAdd">
批量添加
</el-button>
<el-button v-else link size="small" type="primary">删除</el-button>
<el-button v-else link size="small" type="primary" @click="deleteSku(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</template>
<script lang="ts" name="SkuList" setup>
import { UploadImg } from '@/components/UploadFile'
import { PropType } from 'vue'
import { SpuType } from '@/api/mall/product/management/type/spuType'
import { copyValueToTarget } from '@/utils'
import { propTypes } from '@/utils/propTypes'
import { SkuType } from '@/api/mall/product/management/type/skuType'
import { copyValueToTarget } from '@/utils/object'
import { UploadImg } from '@/components/UploadFile'
import type { Property, SkuType, SpuType } from '@/api/mall/product/spu'
const props = defineProps({
propFormData: {
type: Object as PropType<SpuType>,
default: () => {}
},
attributeList: {
propertyList: {
type: Array,
default: () => []
},
isBatch: propTypes.bool.def(false) //
isBatch: propTypes.bool.def(false) //
})
const formData = ref<SpuType>() //
// TODO @puhui999
const SkuData = ref<SkuType[]>([
const skuList = ref<SkuType[]>([
{
/**
* 商品价格单位
*/
price: 0,
/**
* 市场价单位
*/
marketPrice: 0,
/**
* 成本价单位
*/
costPrice: 0,
/**
* 商品条码
*/
barCode: '',
/**
* 图片地址
*/
picUrl: '',
/**
* 库存
*/
stock: 0,
/**
* 商品重量单位kg 千克
*/
weight: 0,
/**
* 商品体积单位m^3 平米
*/
volume: 0,
/**
* 一级分销的佣金单位
*/
subCommissionFirstPrice: 0,
/**
* 二级分销的佣金单位
*/
subCommissionSecondPrice: 0
price: 0, //
marketPrice: 0, //
costPrice: 0, //
barCode: '', //
picUrl: '', //
stock: 0, //
weight: 0, //
volume: 0, //
subCommissionFirstPrice: 0, //
subCommissionSecondPrice: 0 //
}
])
]) //
/** 批量添加 */
const batchAdd = () => {
formData.value.skus.forEach((item) => {
copyValueToTarget(item, SkuData.value[0])
copyValueToTarget(item, skuList.value[0])
})
}
const tableHeaderList = ref<{ prop: string; label: string }[]>([])
/** 删除 sku */
const deleteSku = (row) => {
const index = formData.value.skus.findIndex(
//
(sku) => JSON.stringify(sku.properties) === JSON.stringify(row.properties)
)
formData.value.skus.splice(index, 1)
}
const tableHeaders = ref<{ prop: string; label: string }[]>([]) //
/**
* 将传进来的值赋值给SkuData
* 将传进来的值赋值给skuList
*/
watch(
() => props.propFormData,
@ -194,35 +173,27 @@ watch(
}
)
// TODO @ chatgpt
// TODO @ chatgpt fix:
/** 生成表数据 */
const generateTableData = (data: any[]) => {
//
const propertiesItemList = []
for (const item of data) {
const objList = []
for (const v of item.values) {
const obj = { propertyId: 0, valueId: 0, valueName: '' }
obj.propertyId = item.id
obj.valueId = v.id
obj.valueName = v.name
objList.push(obj)
}
propertiesItemList.push(objList)
}
const generateTableData = (propertyList: any[]) => {
// fix: 使mapfor
const propertiesItemList = propertyList.map((item) =>
item.values.map((v) => ({
propertyId: item.id,
propertyName: item.name,
valueId: v.id,
valueName: v.name
}))
)
const buildList = build(propertiesItemList)
// sku,
if (
buildList.length === formData.value.skus.length ||
data.some((item) => item.values.length === 0)
) {
return
// sku skus
if (!validateData(propertyList)) {
// sku
formData.value!.skus = []
}
//
formData.value!.skus = []
buildList.forEach((item) => {
for (const item of buildList) {
const row = {
properties: [],
properties: Array.isArray(item) ? item : [item], // property
price: 0,
marketPrice: 0,
costPrice: 0,
@ -234,32 +205,49 @@ const generateTableData = (data: any[]) => {
subCommissionFirstPrice: 0,
subCommissionSecondPrice: 0
}
//
if (Array.isArray(item)) {
row.properties = item
} else {
row.properties.push(item)
const index = formData.value!.skus.findIndex(
(sku) => JSON.stringify(sku.properties) === JSON.stringify(row.properties)
)
// sku
if (index !== -1) {
continue
}
formData.value.skus.push(row)
})
}
}
/**
* 生成 skus 前置校验
*/
const validateData = (propertyList: any[]) => {
const skuPropertyIds = []
formData.value.skus.forEach((sku) =>
sku.properties
?.map((property) => property.propertyId)
.forEach((propertyId) => {
if (skuPropertyIds.indexOf(propertyId) === -1) {
skuPropertyIds.push(propertyId)
}
})
)
const propertyIds = propertyList.map((item) => item.id)
return skuPropertyIds.length === propertyIds.length
}
/** 构建所有排列组合 */
const build = (list: any[]) => {
if (list.length === 0) {
const build = (propertyValuesList: Property[][]) => {
if (propertyValuesList.length === 0) {
return []
} else if (list.length === 1) {
return list[0]
} else if (propertyValuesList.length === 1) {
return propertyValuesList[0]
} else {
const result = []
const rest = build(list.slice(1))
for (let i = 0; i < list[0].length; i++) {
const result: Property[][] = []
const rest = build(propertyValuesList.slice(1))
for (let i = 0; i < propertyValuesList[0].length; i++) {
for (let j = 0; j < rest.length; j++) {
//
if (Array.isArray(rest[j])) {
result.push([list[0][i], ...rest[j]])
result.push([propertyValuesList[0][i], ...rest[j]])
} else {
result.push([list[0][i], rest[j]])
result.push([propertyValuesList[0][i], rest[j]])
}
}
}
@ -269,13 +257,13 @@ const build = (list: any[]) => {
/** 监听属性列表生成相关参数和表头 */
watch(
() => props.attributeList,
(data) => {
() => props.propertyList,
(propertyList) => {
//
if (!formData.value.specType) return
// 使
if (props.isBatch) {
SkuData.value = [
skuList.value = [
{
price: 0,
marketPrice: 0,
@ -291,19 +279,25 @@ watch(
]
}
//
if (JSON.stringify(data) === '[]') return
if (JSON.stringify(propertyList) === '[]') return
//
tableHeaderList.value = []
tableHeaders.value = []
//
data.forEach((item, index) => {
propertyList.forEach((item, index) => {
// nameindex
tableHeaderList.value.push({ prop: `name${index}`, label: item.name })
tableHeaders.value.push({ prop: `name${index}`, label: item.name })
})
generateTableData(data)
// sku
if (validateData(propertyList)) return
//
if (propertyList.some((item) => item.values.length === 0)) return
generateTableData(propertyList)
},
{
deep: true,
immediate: true
}
)
// sku fix: spu skus sku
defineExpose({ generateTableData })
</script>

View File

@ -8,7 +8,7 @@
class="-mb-15px"
label-width="68px"
>
<!-- TODO @puhui999https://admin.java.crmeb.net/store/index使 + -->
<!-- TODO @puhui999https://admin.java.crmeb.net/store/index使 + fix-->
<el-form-item label="品牌名称" prop="name">
<el-input
v-model="queryParams.name"
@ -18,15 +18,17 @@
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" class="!w-240px" clearable placeholder="请选择状态">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
<!-- TODO 分类只能选择二级分类目前还没做还是先以联调通顺为主 -->
<el-form-item label="商品分类" prop="categoryId">
<el-tree-select
v-model="queryParams.categoryId"
:data="categoryList"
:props="defaultProps"
check-strictly
class="w-1/1"
node-key="id"
placeholder="请选择商品分类"
/>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
<el-date-picker
@ -59,7 +61,7 @@
<!-- 列表 -->
<ContentWrap>
<el-tabs v-model="queryParams.tabType" @tab-click="handleClick">
<el-tabs v-model="queryParams.tabType" @tab-click="handleTabClick">
<el-tab-pane
v-for="item in tabsData"
:key="item.type"
@ -68,35 +70,40 @@
/>
</el-tabs>
<el-table v-loading="loading" :data="list">
<!-- TODO puhui999: ID 编号的展示 -->
<!-- TODO 暂时不做折叠数据 -->
<!-- <el-table-column type="expand">-->
<!-- <template #default="{ row }">-->
<!-- <el-form inline label-position="left">-->
<!-- <el-form-item label="市场价:">-->
<!-- <span>{{ row.marketPrice }}</span>-->
<!-- </el-form-item>-->
<!-- <el-form-item label="成本价:">-->
<!-- <span>{{ row.costPrice }}</span>-->
<!-- </el-form-item>-->
<!-- <el-form-item label="虚拟销量:">-->
<!-- <span>{{ row.virtualSalesCount }}</span>-->
<!-- </el-form-item>-->
<!-- </el-form>-->
<!-- </template>-->
<!-- </el-table-column>-->
<!-- TODO 折叠数据按需增加暂定以下三个 -->
<el-table-column type="expand" width="30">
<template #default="{ row }">
<el-form class="demo-table-expand" inline label-position="left">
<el-form-item label="市场价:">
<span>{{ formatToFraction(row.marketPrice) }}</span>
</el-form-item>
<el-form-item label="成本价:">
<span>{{ formatToFraction(row.costPrice) }}</span>
</el-form-item>
<el-form-item label="虚拟销量:">
<span>{{ row.virtualSalesCount }}</span>
</el-form-item>
</el-form>
</template>
</el-table-column>
<!-- TODO puhui999: ID 编号的展示 fix-->
<el-table-column key="id" align="center" label="商品编号" prop="id" />
<el-table-column label="商品图" min-width="80">
<template #default="{ row }">
<el-image
:src="row.picUrl"
style="width: 36px; height: 36px"
style="width: 30px; height: 30px"
@click="imagePreview(row.picUrl)"
/>
</template>
</el-table-column>
<el-table-column :show-overflow-tooltip="true" label="商品名称" min-width="300" prop="name" />
<!-- TODO 价格 / 100.0 -->
<el-table-column align="center" label="商品售价" min-width="90" prop="price" />
<el-table-column align="center" label="商品售价" min-width="90" prop="price">
<template #default="{ row }">
{{ formatToFraction(row.price) }}
</template>
</el-table-column>
<el-table-column align="center" label="销量" min-width="90" prop="salesCount" />
<el-table-column align="center" label="库存" min-width="90" prop="stock" />
<el-table-column align="center" label="排序" min-width="70" prop="sort" />
@ -107,24 +114,31 @@
prop="createTime"
width="180"
/>
<el-table-column fixed="right" label="状态" min-width="80">
<el-table-column align="center" label="状态" min-width="80">
<template #default="{ row }">
<!-- TODO @puhui是不是不用 Number(row.status) 去比较哈直接 row.status < 0 -->
<el-switch
v-model="row.status"
:active-value="1"
:disabled="Number(row.status) < 0"
:inactive-value="0"
active-text="上架"
inactive-text="下架"
inline-prompt
@change="changeStatus(row)"
/>
<!-- fix: 修复打开回收站时商品误触改变商品状态的事件因为el-switch只用0和1没有-1所以当商品状态为-1时状态使用el-tag显示 -->
<template v-if="row.status >= 0">
<el-switch
v-model="row.status"
:active-value="1"
:inactive-value="0"
active-text="上架"
inactive-text="下架"
inline-prompt
@change="changeStatus(row)"
/>
</template>
<template v-else>
<el-tag type="info">回收站</el-tag>
</template>
</template>
</el-table-column>
<el-table-column align="center" fixed="right" label="操作" min-width="150">
<el-table-column align="center" fixed="right" label="操作" min-width="200">
<template #default="{ row }">
<!-- TODO @puhui999详情可以后面点做哈 -->
<el-button v-hasPermi="['product:spu:update']" link type="primary" @click="openDetail">
详情
</el-button>
<template v-if="queryParams.tabType === 4">
<el-button
v-hasPermi="['product:spu:delete']"
@ -138,13 +152,15 @@
v-hasPermi="['product:spu:update']"
link
type="primary"
@click="addToTrash(row, ProductSpuStatusEnum.DISABLE.status)"
@click="changeStatus(row, ProductSpuStatusEnum.DISABLE.status)"
>
恢复到仓库
</el-button>
</template>
<template v-else>
<!-- 只有不是上架和回收站的商品可以编辑 -->
<el-button
v-if="queryParams.tabType !== 0"
v-hasPermi="['product:spu:update']"
link
type="primary"
@ -156,7 +172,7 @@
v-hasPermi="['product:spu:update']"
link
type="primary"
@click="addToTrash(row, ProductSpuStatusEnum.RECYCLE.status)"
@click="changeStatus(row, ProductSpuStatusEnum.RECYCLE.status)"
>
加入回收站
</el-button>
@ -172,21 +188,18 @@
@pagination="getList"
/>
</ContentWrap>
<!-- https://kailong110120130.gitee.io/vue-element-plus-admin-doc/components/image-viewer.html -->
<!-- 必须在表格外面展示不然单元格会遮挡图层 -->
<el-image-viewer
v-if="imgViewVisible"
:url-list="imageViewerList"
@close="imgViewVisible = false"
/>
</template>
<script lang="ts" name="ProductList" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
// TODO @puhui999managementApi=ProductSpuApi
import * as managementApi from '@/api/mall/product/management/spu'
import { ProductSpuStatusEnum } from '@/utils/constants'
<script lang="ts" name="ProductSpu" setup>
import { TabsPaneContext } from 'element-plus'
import { cloneDeep } from 'lodash-es'
import { createImageViewer } from '@/components/ImageViewer'
import { dateFormatter } from '@/utils/formatTime'
import { defaultProps, handleTree } from '@/utils/tree'
import { ProductSpuStatusEnum } from '@/utils/constants'
import * as ProductSpuApi from '@/api/mall/product/spu'
import * as ProductCategoryApi from '@/api/mall/product/category'
import { formatToFraction } from '@/utils'
const message = useMessage() //
const { t } = useI18n() //
const { currentRoute, push } = useRouter() //
@ -225,26 +238,21 @@ const tabsData = ref([
/** 获得每个 Tab 的数量 */
const getTabsCount = async () => {
// TODO @puhui999 try catch
try {
const res = await managementApi.getTabsCount()
for (let objName in res) {
tabsData.value[Number(objName)].count = res[objName]
}
} catch {}
// TODO @puhui999 try catch fix
const res = await ProductSpuApi.getTabsCount()
for (let objName in res) {
tabsData.value[Number(objName)].count = res[objName]
}
}
const imgViewVisible = ref(false) //
const imageViewerList = ref<string[]>([]) //
const queryParams = ref({
pageNo: 1,
pageSize: 10,
tabType: 0
})
const queryFormRef = ref() //
}) //
const queryFormRef = ref() // Ref
// TODO @puhui999 handleTabClick
const handleClick = (tab: TabsPaneContext) => {
// TODO @puhui999 handleTabClickfix
const handleTabClick = (tab: TabsPaneContext) => {
queryParams.value.tabType = tab.paneName
getList()
}
@ -253,7 +261,7 @@ const handleClick = (tab: TabsPaneContext) => {
const getList = async () => {
loading.value = true
try {
const data = await managementApi.getSpuList(queryParams.value)
const data = await ProductSpuApi.getSpuPage(queryParams.value)
list.value = data.list
total.value = data.total
} finally {
@ -261,7 +269,7 @@ const getList = async () => {
}
}
// TODO @puhui999 changeStatus addToTrash
// TODO @puhui999 changeStatus addToTrash fix
/**
* 更改 SPU 状态
*
@ -269,10 +277,12 @@ const getList = async () => {
* @param status 更改前的值
*/
const changeStatus = async (row, status?: number) => {
// TODO
// fix: changeStatus
const deepCopyValue = cloneDeep(unref(row))
if (typeof status !== 'undefined') deepCopyValue.status = status
try {
let text = ''
switch (row.status) {
switch (deepCopyValue.status) {
case ProductSpuStatusEnum.DISABLE.status:
text = ProductSpuStatusEnum.DISABLE.name
break
@ -283,21 +293,21 @@ const changeStatus = async (row, status?: number) => {
text = `加入${ProductSpuStatusEnum.RECYCLE.name}`
break
}
// fix:
await message.confirm(
row.status === -1 ? `确认要将[${row.name}]${text}吗?` : `确认要${text}[${row.name}]吗?`
deepCopyValue.status === -1
? `确认要将[${row.name}]${text}吗?`
: row.status === -1 // -1: status-1 status0
? `确认要将[${row.name}]恢复到仓库吗?`
: `确认要${text}[${row.name}]吗?`
)
await managementApi.updateStatus({ id: row.id, status: row.status })
await ProductSpuApi.updateStatus({ id: deepCopyValue.id, status: deepCopyValue.status })
message.success('更新状态成功')
// tabs
await getTabsCount()
//
await getList()
} catch {
//
if (typeof status !== 'undefined') {
row.status = status
return
}
//
row.status =
row.status === ProductSpuStatusEnum.DISABLE.status
@ -306,26 +316,13 @@ const changeStatus = async (row, status?: number) => {
}
}
/**
* 加入回收站
*
* @param row
* @param status
*/
const addToTrash = (row, status) => {
//
const num = Number(`${row.status}`)
row.status = status
changeStatus(row, num)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
//
await message.delConfirm()
//
await managementApi.deleteSpu(id)
await ProductSpuApi.deleteSpu(id)
message.success(t('common.delSuccess'))
// tabs
await getTabsCount()
@ -339,8 +336,10 @@ const handleDelete = async (id: number) => {
* @param imgUrl
*/
const imagePreview = (imgUrl: string) => {
imageViewerList.value = [imgUrl]
imgViewVisible.value = true
// fix: https://kailong110120130.gitee.io/vue-element-plus-admin-doc/components/image-viewer.html
createImageViewer({
urlList: [imgUrl]
})
}
/** 搜索按钮操作 */
@ -362,27 +361,43 @@ const resetQuery = () => {
const openForm = (id?: number) => {
//
if (typeof id === 'number') {
push('/product/productManagementAdd?id=' + id)
push('/product/productSpuEdit/' + id)
return
}
//
push('/product/productManagementAdd')
push('/product/productSpuAdd')
}
// TODO @puhui999
/**
* 查看商品详情
*/
const openDetail = () => {
message.alert('查看详情未完善!!!')
}
// TODO @puhui999fix:
watch(
() => currentRoute.value,
() => {
getList()
},
{
immediate: true
}
)
const categoryList = ref() //
/** 初始化 **/
onMounted(() => {
getTabsCount()
getList()
onMounted(async () => {
await getTabsCount()
await getList()
//
const data = await ProductCategoryApi.getCategoryList({})
categoryList.value = handleTree(data, 'id', 'parentId')
})
</script>
<style lang="scss" scoped>
.demo-table-expand {
padding-left: 42px;
:deep(.el-form-item__label) {
width: 82px;
font-weight: bold;
color: #99a9bf;
}
}
</style>