diff --git a/src/api/mall/product/brand.ts b/src/api/mall/product/brand.ts index dc8acc2a..94d53704 100644 --- a/src/api/mall/product/brand.ts +++ b/src/api/mall/product/brand.ts @@ -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' }) +} diff --git a/src/api/mall/product/spu.ts b/src/api/mall/product/spu.ts index 7bc645e0..ace7e417 100644 --- a/src/api/mall/product/spu.ts +++ b/src/api/mall/product/spu.ts @@ -34,6 +34,7 @@ export interface SpuType { sliderPicUrls?: string[] // 商品轮播图 introduction?: string // 商品简介 deliveryTemplateId?: number | null // 运费模版 + brandId?: number | null // 商品品牌编号 specType?: boolean // 商品规格 subCommissionType?: boolean // 分销类型 skus: SkuType[] // sku数组 diff --git a/src/utils/index.ts b/src/utils/index.ts index d74bbe99..134bdf40 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -173,3 +173,24 @@ export const copyValueToTarget = (target, source) => { // 更新目标对象值 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) +} diff --git a/src/views/mall/product/spu/addForm.vue b/src/views/mall/product/spu/addForm.vue index a968eefb..e081387d 100644 --- a/src/views/mall/product/spu/addForm.vue +++ b/src/views/mall/product/spu/addForm.vue @@ -38,6 +38,7 @@ import { BasicInfoForm, DescriptionForm, OtherSettingsForm } from './components' // 业务api import * as ProductSpuApi from '@/api/mall/product/spu' import * as PropertyApi from '@/api/mall/product/property' +import { convertToInteger, formatToFraction } from '@/utils' const { t } = useI18n() // 国际化 const message = useMessage() // 消息弹窗 @@ -60,6 +61,7 @@ const formData = ref<ProductSpuApi.SpuType>({ sliderPicUrls: [], // 商品轮播图 introduction: '', // 商品简介 deliveryTemplateId: 1, // 运费模版 + brandId: null, // 商品品牌 specType: false, // 商品规格 subCommissionType: false, // 分销类型 skus: [ @@ -94,14 +96,34 @@ const getDetail = async () => { formLoading.value = true try { 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) + // 只有是多规格才处理 + if (res.specType) { + // TODO @puhui999:可以直接拿 propertyName 拼接处规格 id + 属性,可以看下商品 uniapp 详情的做法 + // fix: 考虑到 sku 数量和通过属性算出来的sku不一致的情况 + const propertyIds = [] + res.skus.forEach((sku) => + sku.properties + ?.map((property) => property.propertyId) + .forEach((propertyId) => { + if (propertyIds.indexOf(propertyId) === -1) { + propertyIds.push(propertyId) + } + }) + ) + const properties = await PropertyApi.getPropertyListAndValue({ propertyIds }) + await nextTick() + // 回显商品属性 + basicInfoRef.value.addAttribute(properties) + } } finally { formLoading.value = false } @@ -119,10 +141,26 @@ const submitForm = async () => { await unref(descriptionRef)?.validate() await unref(otherSettingsRef)?.validate() const deepCopyFormData = cloneDeep(unref(formData.value)) // 深拷贝一份 fix:这样最终 server 端不满足,不需要恢复, - // 处理掉一些无关数据 + // TODO 兜底处理 sku 空数据详见 SkuList TODO + 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 = [] @@ -148,34 +186,6 @@ const submitForm = async () => { } } -/** - * 重置表单 - * fix:先注释保留,如果后期没有使用到则移除 - */ -// 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 = () => { delView(unref(currentRoute)) diff --git a/src/views/mall/product/spu/components/BasicInfoForm.vue b/src/views/mall/product/spu/components/BasicInfoForm.vue index 2219008e..d2c7bbe1 100644 --- a/src/views/mall/product/spu/components/BasicInfoForm.vue +++ b/src/views/mall/product/spu/components/BasicInfoForm.vue @@ -59,13 +59,23 @@ </el-col> <el-col :span="12"> <el-form-item label="运费模板" prop="deliveryTemplateId"> - <el-select v-model="formData.deliveryTemplateId" class="w-1/1" placeholder="请选择"> + <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"> @@ -108,14 +118,15 @@ </template> <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/spu' import { UploadImg, UploadImgs } from '@/components/UploadFile' import { ProductAttributes, ProductAttributesAddForm, SkuList } from './index' import * as ProductCategoryApi from '@/api/mall/product/category' -import { propTypes } from '@/utils/propTypes' -import { copyValueToTarget } from '@/utils' +import { getSimpleBrandList } from '@/api/mall/product/brand' const message = useMessage() // 消息弹窗 @@ -135,15 +146,20 @@ const propertyList = ref([]) // 商品属性列表 const addAttribute = (property: any) => { Array.isArray(property) ? (propertyList.value = property) : propertyList.value.push(property) } +/** 调用 SkuList generateTableData 方法*/ +// const generateSkus(propertyList){ +// skuList.value.generateTableData() +// } const formData = reactive<SpuType>({ name: '', // 商品名称 - categoryId: undefined, // 商品分类 + categoryId: null, // 商品分类 keyword: '', // 关键字 unit: '', // 单位 picUrl: '', // 商品封面图 sliderPicUrls: [], // 商品轮播图 introduction: '', // 商品简介 deliveryTemplateId: 1, // 运费模版 + brandId: null, // 商品品牌 specType: false, // 商品规格 subCommissionType: false, // 分销类型 skus: [] @@ -157,6 +173,7 @@ const rules = reactive({ picUrl: [required], sliderPicUrls: [required], // deliveryTemplateId: [required], + brandId: [required], specType: [required], subCommissionType: [required] }) @@ -232,10 +249,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> diff --git a/src/views/mall/product/spu/components/SkuList.vue b/src/views/mall/product/spu/components/SkuList.vue index 914414ad..838e1f43 100644 --- a/src/views/mall/product/spu/components/SkuList.vue +++ b/src/views/mall/product/spu/components/SkuList.vue @@ -34,17 +34,29 @@ <!-- 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%" /> + <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"> <template #default="{ row }"> - <el-input-number v-model="row.marketPrice" :min="0" class="w-100%" /> + <el-input-number + v-model="row.marketPrice" + :min="0" + :precision="2" + :step="0.1" + class="w-100%" + /> </template> </el-table-column> <el-table-column align="center" label="成本价(元)" min-width="168"> <template #default="{ row }"> - <el-input-number v-model="row.costPrice" :min="0" class="w-100%" /> + <el-input-number + v-model="row.costPrice" + :min="0" + :precision="2" + :step="0.1" + class="w-100%" + /> </template> </el-table-column> <el-table-column align="center" label="库存" min-width="168"> @@ -54,42 +66,54 @@ </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%" /> + <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%" /> + <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"> <template #default="{ row }"> - <el-input-number v-model="row.subCommissionFirstPrice" :min="0" class="w-100%" /> + <el-input-number + v-model="row.subCommissionFirstPrice" + :min="0" + :precision="2" + :step="0.1" + class="w-100%" + /> </template> </el-table-column> <el-table-column align="center" label="二级返佣(元)" min-width="168"> <template #default="{ row }"> - <el-input-number v-model="row.subCommissionSecondPrice" :min="0" class="w-100%" /> + <el-input-number + v-model="row.subCommissionSecondPrice" + :min="0" + :precision="2" + :step="0.1" + class="w-100%" + /> </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 type { Property, SkuType, SpuType } from '@/api/mall/product/spu' -import { propTypes } from '@/utils/propTypes' import { copyValueToTarget } from '@/utils' +import { propTypes } from '@/utils/propTypes' +import { UploadImg } from '@/components/UploadFile' +import type { Property, SkuType, SpuType } from '@/api/mall/product/spu' const props = defineProps({ propFormData: { @@ -124,7 +148,14 @@ const batchAdd = () => { copyValueToTarget(item, skuList.value[0]) }) } - +/** 删除 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 }[]>([]) // 多属性表头 /** @@ -142,11 +173,11 @@ watch( } ) -// TODO @芋艿:看看 chatgpt 可以进一步下面几个方法的实现不 fix +// TODO @芋艿:看看 chatgpt 可以进一步下面几个方法的实现不 fix: 添加相关处理逻辑解决编辑表单时或查看详情时数据回显问题 /** 生成表数据 */ -const generateTableData = (data: any[]) => { +const generateTableData = (propertyList: any[]) => { // 构建数据结构 fix: 使用map替换多重for循环 - const propertiesItemList = data.map((item) => + const propertiesItemList = propertyList.map((item) => item.values.map((v) => ({ propertyId: item.id, propertyName: item.name, @@ -155,19 +186,14 @@ const generateTableData = (data: any[]) => { })) ) const buildList = build(propertiesItemList) - // 如果构建后的组合数跟sku数量一样的话则不用处理,添加新属性没有属性值也不做处理 - // fix: 解决编辑表单时或查看详情时数据回显问题 - 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: Array.isArray(item) ? item : [item], + properties: Array.isArray(item) ? item : [item], // 如果只有一个属性的话返回的是一个property对象 price: 0, marketPrice: 0, costPrice: 0, @@ -179,8 +205,36 @@ const generateTableData = (data: any[]) => { subCommissionFirstPrice: 0, subCommissionSecondPrice: 0 } + const index = formData.value!.skus.findIndex( + (sku) => JSON.stringify(sku.properties) === JSON.stringify(row.properties) + ) + // 如果存在属性相同的 sku 则不做处理 + if (index !== -1) { + continue + } + /** + * TODO 嗯。。有一个问题回显数据时已删除的 sku 会被重新添加暂时没想到好办法,保存时先手动重新删除一下因为是一条空数据很好辨别 不手动删也没是提交表单时会检测删除空sku来兜底 + * + */ 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 = (propertyValuesList: Property[][]) => { @@ -237,6 +291,10 @@ watch( // name加属性项index区分属性值 tableHeaders.value.push({ prop: `name${index}`, label: item.name }) }) + // 如果回显的 sku 属性和添加的属性一致则不处理 + if (validateData(propertyList)) return + // 添加新属性没有属性值也不做处理 + if (propertyList.some((item) => item.values.length === 0)) return generateTableData(propertyList) }, { diff --git a/src/views/mall/product/spu/index.vue b/src/views/mall/product/spu/index.vue index c46bd4a3..6fd3f5f1 100644 --- a/src/views/mall/product/spu/index.vue +++ b/src/views/mall/product/spu/index.vue @@ -75,10 +75,10 @@ <template #default="{ row }"> <el-form class="demo-table-expand" inline label-position="left"> <el-form-item label="市场价:"> - <span>{{ row.marketPrice }}</span> + <span>{{ formatToFraction(row.marketPrice) }}</span> </el-form-item> <el-form-item label="成本价:"> - <span>{{ row.costPrice }}</span> + <span>{{ formatToFraction(row.costPrice) }}</span> </el-form-item> <el-form-item label="虚拟销量:"> <span>{{ row.virtualSalesCount }}</span> @@ -99,7 +99,11 @@ </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" /> @@ -129,9 +133,12 @@ </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']" @@ -151,7 +158,9 @@ </el-button> </template> <template v-else> + <!-- 只有不是上架和回收站的商品可以编辑 --> <el-button + v-if="queryParams.tabType !== 0" v-hasPermi="['product:spu:update']" link type="primary" @@ -189,6 +198,7 @@ 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() // 国际化 @@ -357,7 +367,12 @@ const openForm = (id?: number) => { // 新增 push('/product/productSpuAdd') } - +/** + * 查看商品详情 + */ +const openDetail = () => { + message.alert('查看详情未完善!!!') +} // 监听路由变化更新列表 TODO @puhui999:这个是必须加的么?fix: 因为编辑表单是以路由的方式打开,保存表单后列表不会刷新 watch( () => currentRoute.value,