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/management/spu.ts b/src/api/mall/product/management/spu.ts deleted file mode 100644 index 07d7103e..00000000 --- a/src/api/mall/product/management/spu.ts +++ /dev/null @@ -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}` }) -} diff --git a/src/api/mall/product/management/type/skuType.ts b/src/api/mall/product/management/type/skuType.ts deleted file mode 100644 index 42889dc4..00000000 --- a/src/api/mall/product/management/type/skuType.ts +++ /dev/null @@ -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 -} diff --git a/src/api/mall/product/management/type/spuType.ts b/src/api/mall/product/management/type/spuType.ts deleted file mode 100644 index 11c3c888..00000000 --- a/src/api/mall/product/management/type/spuType.ts +++ /dev/null @@ -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 // 是否优品 -} diff --git a/src/api/mall/product/spu.ts b/src/api/mall/product/spu.ts new file mode 100644 index 00000000..ace7e417 --- /dev/null +++ b/src/api/mall/product/spu.ts @@ -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 }) +} diff --git a/src/components/UploadFile/src/UploadImgs.vue b/src/components/UploadFile/src/UploadImgs.vue index 82e5030c..857959bf 100644 --- a/src/components/UploadFile/src/UploadImgs.vue +++ b/src/components/UploadFile/src/UploadImgs.vue @@ -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 多图上传组件成功后只是把保存成功后的url替换掉组件选图时的文件路径,所以返回的fileList包含的是一个包含文件信息的对象列表 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; diff --git a/src/router/modules/remaining.ts b/src/router/modules/remaining.ts index 4f5a16bd..e530c410 100644 --- a/src/router/modules/remaining.ts +++ b/src/router/modules/remaining.ts @@ -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' } } ] diff --git a/src/utils/index.ts b/src/utils/index.ts index e016c1e2..134bdf40 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -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) +} diff --git a/src/utils/object.ts b/src/utils/object.ts deleted file mode 100644 index 6612da74..00000000 --- a/src/utils/object.ts +++ /dev/null @@ -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) -} diff --git a/src/views/mall/product/spu/addForm.vue b/src/views/mall/product/spu/addForm.vue index 28fc414d..bc0941cd 100644 --- a/src/views/mall/product/spu/addForm.vue +++ b/src/views/mall/product/spu/addForm.vue @@ -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) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 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 - // 多规格情况移除skus相关属性值value - 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> diff --git a/src/views/mall/product/spu/components/BasicInfoForm.vue b/src/views/mall/product/spu/components/BasicInfoForm.vue index 249a3830..eac3de9c 100644 --- a/src/views/mall/product/spu/components/BasicInfoForm.vue +++ b/src/views/mall/product/spu/components/BasicInfoForm.vue @@ -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 @puhui999:attributeList 改成 propertyList,会更统一一点 -const attributeList = ref([]) // 商品属性列表 -/** 添加商品属性 */ // TODO @puhui999:propFormData 算出来 -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 @puhui999:attributeList 改成 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> diff --git a/src/views/mall/product/spu/components/DescriptionForm.vue b/src/views/mall/product/spu/components/DescriptionForm.vue index 0a7f522b..fbae9a86 100644 --- a/src/views/mall/product/spu/components/DescriptionForm.vue +++ b/src/views/mall/product/spu/components/DescriptionForm.vue @@ -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') diff --git a/src/views/mall/product/spu/components/OtherSettingsForm.vue b/src/views/mall/product/spu/components/OtherSettingsForm.vue index c0fc5122..8189f9c7 100644 --- a/src/views/mall/product/spu/components/OtherSettingsForm.vue +++ b/src/views/mall/product/spu/components/OtherSettingsForm.vue @@ -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:是不是可以遍历 recommend,然后进行是否选中;fix + 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') diff --git a/src/views/mall/product/spu/components/ProductAttributes.vue b/src/views/mall/product/spu/components/ProductAttributes.vue index 73e8c992..8ab88be3 100644 --- a/src/views/mall/product/spu/components/ProductAttributes.vue +++ b/src/views/mall/product/spu/components/ProductAttributes.vue @@ -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 - // 因为组件在ref中所以需要用索引获取对应的Ref - 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 缺少国际化 } diff --git a/src/views/mall/product/spu/components/ProductAttributesAddForm.vue b/src/views/mall/product/spu/components/ProductAttributesAddForm.vue index bd715dde..06987b0a 100644 --- a/src/views/mall/product/spu/components/ProductAttributesAddForm.vue +++ b/src/views/mall/product/spu/components/ProductAttributesAddForm.vue @@ -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) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 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 diff --git a/src/views/mall/product/spu/components/SkuList.vue b/src/views/mall/product/spu/components/SkuList.vue index 9e1c666f..3f1a9542 100644 --- a/src/views/mall/product/spu/components/SkuList.vue +++ b/src/views/mall/product/spu/components/SkuList.vue @@ -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: 使用map替换多重for循环 + 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) => { // name加属性项index区分属性值 - 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> diff --git a/src/views/mall/product/spu/index.vue b/src/views/mall/product/spu/index.vue index b3a04c88..6fd3f5f1 100644 --- a/src/views/mall/product/spu/index.vue +++ b/src/views/mall/product/spu/index.vue @@ -8,7 +8,7 @@ class="-mb-15px" label-width="68px" > - <!-- TODO @puhui999:https://admin.java.crmeb.net/store/index,参考,使用分类 + 标题搜索 --> + <!-- TODO @puhui999:https://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 @puhui999:managementApi=》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:可以改成 handleTabClick:更准确一点;fix +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 深拷贝对象status被赋值为0 + ? `确认要将[${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 @puhui999:这个是必须加的么?fix: 因为编辑表单是以路由的方式打开,保存表单后列表不会刷新 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>