fix: 完善 SPU 查看详情

(cherry picked from commit 39c92cb944)
This commit is contained in:
puhui999 2023-05-31 14:42:45 +08:00 committed by shizhong
parent 9e7fe34ae8
commit d35ac547ac
10 changed files with 387 additions and 37 deletions

View File

@ -37,7 +37,7 @@ export interface Spu {
brandId?: number | null // 商品品牌编号
specType?: boolean // 商品规格
subCommissionType?: boolean // 分销类型
skus: Sku[] // sku数组
skus?: Sku[] // sku数组
description?: string // 商品详情
sort?: number // 商品排序
giveIntegral?: number // 赠送积分

View File

@ -379,6 +379,19 @@ const remainingRouter: AppRouteRecordRaw[] = [
title: '编辑商品',
activeMenu: '/product/product-spu'
}
},
{
path: 'productSpuDetail/:spuId(\\d+)',
component: () => import('@/views/mall/product/spu/addForm.vue'),
name: 'productSpuDetail',
meta: {
noCache: true,
hidden: true,
canTo: true,
icon: 'ep:view',
title: '商品详情',
activeMenu: '/product/product-spu'
}
}
]
}

View File

@ -310,26 +310,30 @@ export const handleTree2 = (data, id, parentId, children, rootId) => {
* @param nodeId
* @param level ,
*/
export const checkSelectedNode = (tree: any[], nodeId, level = 2) => {
export const checkSelectedNode = (tree: any[], nodeId: any, level = 2): boolean => {
if (typeof tree === 'undefined' || !Array.isArray(tree) || tree.length === 0) {
console.warn('tree must be an array')
return false
}
// 校验是否是一级节点
if (tree.some((item) => item.id === nodeId)) {
return false
}
// 递归计数
let count = 1
// 深层次校验
function performAThoroughValidation(arr) {
function performAThoroughValidation(arr: any[]): boolean {
count += 1
for (const item of arr) {
if (item.id === nodeId) {
return true
} else if (typeof item.children !== 'undefined' && item.children.length !== 0) {
performAThoroughValidation(item.children)
if (performAThoroughValidation(item.children)) {
return true
}
}
}
return false
@ -339,11 +343,15 @@ export const checkSelectedNode = (tree: any[], nodeId, level = 2) => {
count = 1
if (performAThoroughValidation(item.children)) {
// 找到后对比是否是期望的层级
if (count >= level) return true
if (count >= level) {
return true
}
}
}
return false
}
/**
*
* @param tree
@ -367,7 +375,10 @@ export const treeToString = (tree: any[], nodeId) => {
str += `/${item.name}`
return true
} else if (typeof item.children !== 'undefined' && item.children.length !== 0) {
performAThoroughValidation(item.children)
str += `/${item.name}`
if (performAThoroughValidation(item.children)) {
return true
}
}
}
return false

View File

@ -5,6 +5,7 @@
<BasicInfoForm
ref="basicInfoRef"
v-model:activeName="activeName"
:is-detail="isDetail"
:propFormData="formData"
/>
</el-tab-pane>
@ -12,6 +13,7 @@
<DescriptionForm
ref="descriptionRef"
v-model:activeName="activeName"
:is-detail="isDetail"
:propFormData="formData"
/>
</el-tab-pane>
@ -19,6 +21,7 @@
<OtherSettingsForm
ref="otherSettingsRef"
v-model:activeName="activeName"
:is-detail="isDetail"
:propFormData="formData"
/>
</el-tab-pane>
@ -42,11 +45,12 @@ import { convertToInteger, formatToFraction } from '@/utils'
const { t } = useI18n() //
const message = useMessage() //
const { push, currentRoute } = useRouter() //
const { params } = useRoute() //
const { params, name } = useRoute() //
const { delView } = useTagsViewStore() //
const formLoading = ref(false) // 12
const activeName = ref('basicInfo') // Tag
const isDetail = ref(false) //
const basicInfoRef = ref<ComponentRef<typeof BasicInfoForm>>() // Ref
const descriptionRef = ref<ComponentRef<typeof DescriptionForm>>() // Ref
const otherSettingsRef = ref<ComponentRef<typeof OtherSettingsForm>>() // Ref
@ -90,12 +94,13 @@ const formData = ref<ProductSpuApi.Spu>({
/** 获得详情 */
const getDetail = async () => {
console.log(name)
const id = params.spuId as number
if (id) {
formLoading.value = true
try {
const res = (await ProductSpuApi.getSpu(id)) as ProductSpuApi.Spu
res.skus.forEach((item) => {
res.skus!.forEach((item) => {
//
item.price = formatToFraction(item.price)
item.marketPrice = formatToFraction(item.marketPrice)
@ -123,7 +128,7 @@ const submitForm = async () => {
// , server
const deepCopyFormData = cloneDeep(unref(formData.value))
// sku
formData.value.skus.forEach((sku) => {
formData.value.skus!.forEach((sku) => {
//
if (sku.barCode === '') {
const index = deepCopyFormData.skus.findIndex(
@ -171,7 +176,6 @@ const close = () => {
delView(unref(currentRoute))
push('/product/product-spu')
}
/** 初始化 */
onMounted(async () => {
await getDetail()

View File

@ -1,5 +1,11 @@
<template>
<el-form ref="productSpuBasicInfoRef" :model="formData" :rules="rules" label-width="120px">
<el-form
v-if="!isDetail"
ref="productSpuBasicInfoRef"
:model="formData"
:rules="rules"
label-width="120px"
>
<el-row>
<el-col :span="12">
<el-form-item label="商品名称" prop="name">
@ -115,18 +121,72 @@
</el-row>
</el-form>
<ProductAttributesAddForm ref="attributesAddFormRef" :propertyList="propertyList" />
<!-- 详情跟表单放在一块可以共用已有功能再抽离成组件有点过度封装的感觉 -->
<Descriptions v-if="isDetail" :data="formData" :schema="allSchemas.detailSchema">
<template #categoryId="{ row }"> {{ categoryString(row.categoryId) }}</template>
<template #brandId="{ row }">
{{ brandList.find((item) => item.id === row.brandId)?.name }}
</template>
<template #specType="{ row }">
{{ row.specType ? '多规格' : '单规格' }}
</template>
<template #subCommissionType="{ row }">
{{ row.subCommissionType ? '自行设置' : '默认设置' }}
</template>
<template #picUrl="{ row }">
<el-image :src="row.picUrl" class="w-60px h-60px" @click="imagePreview(row.picUrl)" />
</template>
<template #sliderPicUrls="{ row }">
<el-image
v-for="(item, index) in row.sliderPicUrls"
:key="index"
:src="item.url"
class="w-60px h-60px mr-10px"
@click="imagePreview(row.sliderPicUrls)"
/>
</template>
<template #skus>
<SkuList
ref="skuDetailListRef"
:is-detail="isDetail"
:prop-form-data="formData"
:propertyList="propertyList"
/>
</template>
</Descriptions>
</template>
<script lang="ts" name="ProductSpuBasicInfoForm" setup>
import { PropType } from 'vue'
import { copyValueToTarget } from '@/utils'
import { propTypes } from '@/utils/propTypes'
import { checkSelectedNode, defaultProps, handleTree } from '@/utils/tree'
import { checkSelectedNode, defaultProps, handleTree, treeToString } from '@/utils/tree'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import type { Spu } from '@/api/mall/product/spu'
import { UploadImg, UploadImgs } from '@/components/UploadFile'
import { ProductAttributes, ProductAttributesAddForm, SkuList } from './index'
import { basicInfoSchema } from './spu.data'
import { createImageViewer } from '@/components/ImageViewer'
import * as ProductCategoryApi from '@/api/mall/product/category'
import { getSimpleBrandList } from '@/api/mall/product/brand'
import { isArray } from '@/utils/is'
// ====== ======
const { allSchemas } = useCrudSchemas(basicInfoSchema)
/** 商品图预览 */
const imagePreview = (args) => {
const urlList = []
if (isArray(args)) {
args.forEach((item) => {
urlList.push(item.url)
})
} else {
urlList.push(args)
}
createImageViewer({
urlList
})
}
// ====== end ======
const message = useMessage() //
@ -135,7 +195,8 @@ const props = defineProps({
type: Object as PropType<Spu>,
default: () => {}
},
activeName: propTypes.string.def('')
activeName: propTypes.string.def(''),
isDetail: propTypes.bool.def(false) //
})
const attributesAddFormRef = ref() //
const productSpuBasicInfoRef = ref() // Ref
@ -149,11 +210,11 @@ const formData = reactive<Spu>({
name: '', //
categoryId: null, //
keyword: '', //
unit: '', //
unit: null, //
picUrl: '', //
sliderPicUrls: [], //
introduction: '', //
deliveryTemplateId: 1, //
deliveryTemplateId: null, //
brandId: null, //
specType: false, //
subCommissionType: false, //
@ -273,9 +334,16 @@ const categoryList = ref([]) // 分类树
const nodeClick = () => {
if (!checkSelectedNode(categoryList.value, formData.categoryId)) {
formData.categoryId = null
message.warning('必须选择二级节点!!')
message.warning('必须选择二级及以下节点!!')
}
}
/**
* 获取分类的节点的完整结构
* @param categoryId 分类id
*/
const categoryString = (categoryId) => {
return treeToString(categoryList.value, categoryId)
}
const brandList = ref([]) //
onMounted(async () => {
//

View File

@ -1,10 +1,27 @@
<template>
<el-form ref="descriptionFormRef" :model="formData" :rules="rules" label-width="120px">
<el-form
v-if="!isDetail"
ref="descriptionFormRef"
:model="formData"
:rules="rules"
label-width="120px"
>
<!--富文本编辑器组件-->
<el-form-item label="商品详情" prop="description">
<Editor v-model:modelValue="formData.description" />
</el-form-item>
</el-form>
<Descriptions
v-if="isDetail"
:data="formData"
:schema="allSchemas.detailSchema"
class="descriptionFormDescriptions"
>
<!-- 展示 HTML 内容 -->
<template #description="{ row }">
<div style="width: 600px" v-html="row.description"></div>
</template>
</Descriptions>
</template>
<script lang="ts" name="DescriptionForm" setup>
import type { Spu } from '@/api/mall/product/spu'
@ -12,6 +29,9 @@ import { Editor } from '@/components/Editor'
import { PropType } from 'vue'
import { propTypes } from '@/utils/propTypes'
import { copyValueToTarget } from '@/utils'
import { descriptionSchema } from './spu.data'
const { allSchemas } = useCrudSchemas(descriptionSchema)
const message = useMessage() //
const props = defineProps({
@ -19,7 +39,8 @@ const props = defineProps({
type: Object as PropType<Spu>,
default: () => {}
},
activeName: propTypes.string.def('')
activeName: propTypes.string.def(''),
isDetail: propTypes.bool.def(false) //
})
const descriptionFormRef = ref() // Ref
const formData = ref<Spu>({

View File

@ -1,5 +1,11 @@
<template>
<el-form ref="otherSettingsFormRef" :model="formData" :rules="rules" label-width="120px">
<el-form
v-if="!isDetail"
ref="otherSettingsFormRef"
:model="formData"
:rules="rules"
label-width="120px"
>
<el-row>
<el-col :span="24">
<el-row :gutter="20">
@ -50,12 +56,38 @@
</el-col>
</el-row>
</el-form>
<Descriptions v-if="isDetail" :data="formData" :schema="allSchemas.detailSchema">
<template #recommendHot="{ row }">
{{ row.recommendHot ? '是' : '否' }}
</template>
<template #recommendBenefit="{ row }">
{{ row.recommendBenefit ? '是' : '否' }}
</template>
<template #recommendBest="{ row }">
{{ row.recommendBest ? '是' : '否' }}
</template>
<template #recommendNew="{ row }">
{{ row.recommendNew ? '是' : '否' }}
</template>
<template #recommendGood="{ row }">
{{ row.recommendGood ? '是' : '否' }}
</template>
<template #activityOrders>
<el-tag>默认</el-tag>
<el-tag class="ml-2" type="success">秒杀</el-tag>
<el-tag class="ml-2" type="info">砍价</el-tag>
<el-tag class="ml-2" type="warning">拼团</el-tag>
</template>
</Descriptions>
</template>
<script lang="ts" name="OtherSettingsForm" setup>
import type { Spu } from '@/api/mall/product/spu'
import { PropType } from 'vue'
import { propTypes } from '@/utils/propTypes'
import { copyValueToTarget } from '@/utils'
import { otherSettingsSchema } from './spu.data'
const { allSchemas } = useCrudSchemas(otherSettingsSchema)
const message = useMessage() //
@ -64,7 +96,8 @@ const props = defineProps({
type: Object as PropType<Spu>,
default: () => {}
},
activeName: propTypes.string.def('')
activeName: propTypes.string.def(''),
isDetail: propTypes.bool.def(false) //
})
const otherSettingsFormRef = ref() // Ref

View File

@ -1,5 +1,6 @@
<template>
<el-table
v-if="!isDetail"
:data="isBatch ? skuList : formData!.skus"
border
class="tabNumWidth"
@ -21,8 +22,8 @@
min-width="120"
>
<template #default="{ row }">
<!-- TODO puhui999展示成蓝色有点区分度哈 fix: 字体加粗颜色使用 #99a9bf 蓝色有点不好看哈哈-->
<span style="font-weight: bold; color: #99a9bf">
<!-- TODO puhui999展示成蓝色有点区分度哈 fix-->
<span style="font-weight: bold; color: #40aaff">
{{ row.properties[index]?.valueName }}
</span>
</template>
@ -108,6 +109,84 @@
</template>
</el-table-column>
</el-table>
<el-table
v-if="isDetail"
:data="formData!.skus"
border
max-height="500"
size="small"
style="width: 99%"
>
<el-table-column align="center" label="图片" min-width="80">
<template #default="{ row }">
<el-image :src="row.picUrl" class="w-60px h-60px" @click="imagePreview(row.picUrl)" />
</template>
</el-table-column>
<template v-if="formData!.specType && !isBatch">
<!-- 根据商品属性动态添加 -->
<el-table-column
v-for="(item, index) in tableHeaders"
:key="index"
:label="item.label"
align="center"
min-width="80"
>
<template #default="{ row }">
<!-- TODO puhui999展示成蓝色有点区分度哈 fix-->
<span style="font-weight: bold; color: #40aaff">
{{ row.properties[index]?.valueName }}
</span>
</template>
</el-table-column>
</template>
<el-table-column align="center" label="商品条码" min-width="100">
<template #default="{ row }">
{{ row.barCode }}
</template>
</el-table-column>
<el-table-column align="center" label="销售价(元)" min-width="80">
<template #default="{ row }">
{{ row.price }}
</template>
</el-table-column>
<el-table-column align="center" label="市场价(元)" min-width="80">
<template #default="{ row }">
{{ row.marketPrice }}
</template>
</el-table-column>
<el-table-column align="center" label="成本价(元)" min-width="80">
<template #default="{ row }">
{{ row.costPrice }}
</template>
</el-table-column>
<el-table-column align="center" label="库存" min-width="80">
<template #default="{ row }">
{{ row.stock }}
</template>
</el-table-column>
<el-table-column align="center" label="重量(kg)" min-width="80">
<template #default="{ row }">
{{ row.weight }}
</template>
</el-table-column>
<el-table-column align="center" label="体积(m^3)" min-width="80">
<template #default="{ row }">
{{ row.volume }}
</template>
</el-table-column>
<template v-if="formData!.subCommissionType">
<el-table-column align="center" label="一级返佣(元)" min-width="80">
<template #default="{ row }">
{{ row.subCommissionFirstPrice }}
</template>
</el-table-column>
<el-table-column align="center" label="二级返佣(元)" min-width="80">
<template #default="{ row }">
{{ row.subCommissionSecondPrice }}
</template>
</el-table-column>
</template>
</el-table>
</template>
<script lang="ts" name="SkuList" setup>
import { PropType, Ref } from 'vue'
@ -115,6 +194,7 @@ import { copyValueToTarget } from '@/utils'
import { propTypes } from '@/utils/propTypes'
import { UploadImg } from '@/components/UploadFile'
import type { Property, Sku, Spu } from '@/api/mall/product/spu'
import { createImageViewer } from '@/components/ImageViewer'
const props = defineProps({
propFormData: {
@ -125,7 +205,8 @@ const props = defineProps({
type: Array,
default: () => []
},
isBatch: propTypes.bool.def(false) //
isBatch: propTypes.bool.def(false), //
isDetail: propTypes.bool.def(false) // sku
})
const formData: Ref<Spu | undefined> = ref<Spu>() //
const skuList = ref<Sku[]>([
@ -143,20 +224,27 @@ const skuList = ref<Sku[]>([
}
]) //
/** 商品图预览 */
const imagePreview = (imgUrl: string) => {
createImageViewer({
urlList: [imgUrl]
})
}
/** 批量添加 */
const batchAdd = () => {
formData.value!.skus.forEach((item) => {
formData.value!.skus!.forEach((item) => {
copyValueToTarget(item, skuList.value[0])
})
}
/** 删除 sku */
const deleteSku = (row) => {
const index = formData.value!.skus.findIndex(
const index = formData.value!.skus!.findIndex(
//
(sku) => JSON.stringify(sku.properties) === JSON.stringify(row.properties)
)
formData.value!.skus.splice(index, 1)
formData.value!.skus!.splice(index, 1)
}
const tableHeaders = ref<{ prop: string; label: string }[]>([]) //
/**
@ -221,13 +309,13 @@ const generateTableData = (propertyList: any[]) => {
subCommissionSecondPrice: 0
}
// sku
const index = formData.value!.skus.findIndex(
const index = formData.value!.skus!.findIndex(
(sku) => JSON.stringify(sku.properties) === JSON.stringify(row.properties)
)
if (index !== -1) {
continue
}
formData.value!.skus.push(row)
formData.value!.skus!.push(row)
}
}
@ -236,7 +324,7 @@ const generateTableData = (propertyList: any[]) => {
*/
const validateData = (propertyList: any[]) => {
const skuPropertyIds = []
formData.value!.skus.forEach((sku) =>
formData.value!.skus!.forEach((sku) =>
sku.properties
?.map((property) => property.propertyId)
.forEach((propertyId) => {

View File

@ -0,0 +1,105 @@
import { CrudSchema } from '@/hooks/web/useCrudSchemas'
export const basicInfoSchema = reactive<CrudSchema[]>([
{
label: '商品名称',
field: 'name'
},
{
label: '关键字',
field: 'keyword'
},
{
label: '商品简介',
field: 'introduction'
},
{
label: '商品分类',
field: 'categoryId'
},
{
label: '商品品牌',
field: 'brandId'
},
{
label: '商品封面图',
field: 'picUrl'
},
{
label: '商品轮播图',
field: 'sliderPicUrls'
},
{
label: '商品视频',
field: 'videoUrl'
},
{
label: '单位',
field: 'unit',
dictType: DICT_TYPE.PRODUCT_UNIT
},
{
label: '规格类型',
field: 'specType'
},
{
label: '分销类型',
field: 'subCommissionType'
},
{
label: '物流模版',
field: 'deliveryTemplateId'
},
{
label: '商品属性列表',
field: 'skus'
}
])
export const descriptionSchema = reactive<CrudSchema[]>([
{
label: '商品详情',
field: 'description'
}
])
export const otherSettingsSchema = reactive<CrudSchema[]>([
{
label: '商品排序',
field: 'sort'
},
{
label: '赠送积分',
field: 'giveIntegral'
},
{
label: '虚拟销量',
field: 'virtualSalesCount'
},
{
label: '是否热卖推荐',
field: 'recommendHot'
},
{
label: '是否优惠推荐',
field: 'recommendBenefit'
},
{
label: '是否精品推荐',
field: 'recommendBest'
},
{
label: '是否新品推荐',
field: 'recommendNew'
},
{
label: '是否优品推荐',
field: 'recommendGood'
},
{
label: '赠送的优惠劵',
field: 'giveCouponTemplateIds'
},
{
label: '活动显示排序',
field: 'activityOrders'
}
])

View File

@ -171,8 +171,13 @@
</el-table-column>
<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">
<!-- TODO @puhui999详情可以后面点做哈 fix-->
<el-button
v-hasPermi="['product:spu:update']"
link
type="primary"
@click="openDetail(row.id)"
>
详情
</el-button>
<template v-if="queryParams.tabType === 4">
@ -284,12 +289,14 @@ const getTabsCount = async () => {
const queryParams = ref({
pageNo: 1,
pageSize: 10,
tabType: 0
tabType: 0,
name: '',
categoryId: null
}) //
const queryFormRef = ref() // Ref
const handleTabClick = (tab: TabsPaneContext) => {
queryParams.value.tabType = tab.paneName
queryParams.value.tabType = tab.paneName as number
getList()
}
@ -400,8 +407,8 @@ const openForm = (id?: number) => {
/**
* 查看商品详情
*/
const openDetail = () => {
message.alert('查看详情未完善!!!')
const openDetail = (id?: number) => {
push('/product/productSpuDetail' + id)
}
/** 导出按钮操作 */
@ -436,12 +443,12 @@ const categoryString = (categoryId) => {
return treeToString(categoryList.value, categoryId)
}
/**
* 校验所选是否为二级节点
* 校验所选是否为二级及以下节点
*/
const nodeClick = () => {
if (!checkSelectedNode(categoryList.value, queryParams.value.categoryId)) {
queryParams.value.categoryId = null
message.warning('必须选择二级节点!!')
message.warning('必须选择二级及以下节点!!')
}
}
/** 初始化 **/