feat: 商品详情

This commit is contained in:
zc 2025-10-24 23:34:45 +08:00
parent 263fbb3633
commit 3f18c2ea0e
4 changed files with 380 additions and 70 deletions

View File

@ -2,21 +2,41 @@
<div class="max-w-1/2"> <div class="max-w-1/2">
<dl v-for="item in list" :key="item.typeId" class="flex mb-1 items-center"> <dl v-for="item in list" :key="item.typeId" class="flex mb-1 items-center">
<dt> <dt>
<el-upload
ref="uploadRef"
:auto-upload="false"
:limit="1"
:on-change="handleFileChange"
:on-exceed="handleExceed"
accept="image/*"
list-type="picture-card"
class="w-10 h-10"
>
<el-icon><Plus /></el-icon>
<template #file="{ file }">
<div class="relative w-10 h-10">
<img class="w-10 h-10" :src="file.url" alt="" />
</div>
</template>
</el-upload>
<el-button type="primary" plain size="small" class="mr-2">{{ item.name }}</el-button> <el-button type="primary" plain size="small" class="mr-2">{{ item.name }}</el-button>
</dt> </dt>
<dd class="flex"> <dd class="flex">
<img :src="item.img" alt="" />
<p> <p>
<span class="text-xs text-[#666]">数量</span <span class="text-xs text-[#666]">库存</span
><el-input v-model="item.num" size="small" class="w-20 mr-2"></el-input> ><el-input v-model="item.stock" size="small" class="w-20 mr-2"></el-input>
</p>
<p>
<span class="text-xs text-[#666]">成本价</span
><el-input v-model="item.originPrice" size="small" class="w-20 mr-2"></el-input>
</p> </p>
<p> <p>
<span class="text-xs text-[#666]">原价</span <span class="text-xs text-[#666]">原价</span
><el-input v-model="item.sale" size="small" class="w-20 mr-2"></el-input> ><el-input v-model="item.salePrice" size="small" class="w-20 mr-2"></el-input>
</p> </p>
<p> <p>
<span class="text-xs text-[#666]">售价</span <span class="text-xs text-[#666]">售价</span
><el-input v-model="item.amount" size="small" class="w-20 mr-2"></el-input> ><el-input v-model="item.promotionPrice" size="small" class="w-20 mr-2"></el-input>
</p> </p>
</dd> </dd>
</dl> </dl>
@ -24,5 +44,65 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const props = defineProps<{ list: any[] }>() import { ref } from 'vue'
import { ElMessage } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
interface Props {
list: any[]
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const props = defineProps<Props>()
const uploadRef = ref()
//
const handleFileChange = (file: any, fileList: any[]) => {
//
if (fileList.length > 1) {
fileList.splice(0, 1)
}
}
//
const handleExceed = () => {
ElMessage.warning('只能选择一张图片')
}
//
const uploadImage = async (item: any) => {
if (!uploadRef.value) return
const uploadInstance = uploadRef.value
const fileList = uploadInstance.fileList
if (fileList.length === 0) {
ElMessage.warning('请选择要上传的图片')
return
}
try {
// FormData
const form = new FormData()
form.append('files', fileList[0].raw)
// API
const res = await (globalThis as any).api.resource.uploadFile.post!(form)
if (res.data && res.data.length > 0) {
// URL
item.imageUrl = res.data[0].url
ElMessage.success('图片上传成功')
//
uploadInstance.clearFiles()
} else {
ElMessage.error('图片上传失败')
}
} catch (error) {
console.error('上传失败:', error)
ElMessage.error('图片上传失败')
}
}
</script> </script>

View File

@ -55,31 +55,41 @@
</form-wrap> </form-wrap>
</el-card> </el-card>
<el-card class="mt-2 sku-info"> <el-card class="mt-2 sku-info">
<h4 class="font-bold mb-2">SKU信息:</h4> <h4 class="font-bold mb-2">属性配置:</h4>
<el-form-item label="后台类目"> <el-form inline>
<el-tree-select <el-form-item label="后台类目">
show-checkbox <el-tree-select
v-model="$dialog.data.categoryId" show-checkbox
:props="defaultAdminCategoryTreeProps" v-model="$dialog.data.categoryId"
:render-after-expand="false" :props="defaultAdminCategoryTreeProps"
@check=" :render-after-expand="false"
(checkedNode: any, checkedStatus: any) => @check="
handleCheckChange(checkedNode, checkedStatus, 'admin') (checkedNode: any, checkedStatus: any) =>
" handleCheckChange(checkedNode, checkedStatus, 'admin')
:load="handleLoadNode" "
lazy :load="handleLoadNode"
style="width: 240px" lazy
> style="width: 240px"
</el-tree-select> >
</el-form-item> </el-tree-select>
</el-form-item>
<el-form-item label="测试商品">
<el-switch v-model="isTest" active-value="1" inactive-value="0" />
</el-form-item>
</el-form>
<div ref="dragRef"> <div ref="dragRef">
<dl v-for="(item, index) in categoryData" :key="item.typeId" class="flex items-center"> <dl v-for="(item, index) in categoryData" :key="item.id" class="flex items-center">
<dt class="text-sm text-[#666] mb-2">{{ item.typeName }}</dt> <dt class="text-sm text-[#666] mb-2">{{ item.categoryPropertyName }}</dt>
<dd class="flex mb-2"> <dd class="flex mb-2">
<div v-for="child in item.options" :key="item.typeId + '-' + child.id"> <div v-for="child in item.vvPropertyValueList" :key="item.id + '-' + child.id">
<el-tag closable effect="plain" :type="colors[index % 4]" class="mr-1">{{ <el-tag
child.name closable
}}</el-tag> effect="plain"
:type="colors[index % 4]"
class="mr-1"
@close="onDeletePropertyValue(index, child.id)"
>{{ child.categoryPropertyValue }}</el-tag
>
</div> </div>
<el-input <el-input
v-if="inputVisibles[index]" v-if="inputVisibles[index]"
@ -87,14 +97,14 @@
v-model="inputValue" v-model="inputValue"
class="w-20" class="w-20"
size="small" size="small"
@keyup.enter="handleInputConfirm(index)" @keyup.enter="handleInputConfirm(index, skuList)"
@blur="handleInputConfirm(index)" @blur="handleInputConfirm(index)"
/> />
<el-button <el-button
v-else v-else
class="button-new-tag ml-1" class="button-new-tag ml-1"
size="small" size="small"
@click="onAddCategoryDataOption(index)" @click="onAddPropertyValue(index)"
> >
+ 新增 + 新增
</el-button> </el-button>
@ -102,13 +112,13 @@
</dl> </dl>
</div> </div>
<h4 class="my-1">价格配置</h4> <h4 class="my-1">sku配置</h4>
<div <div
class="flex w-full flex-wrap justify-between p-2 rounded-[5px]" class="flex w-full flex-wrap justify-between p-2 rounded-[5px]"
style="border: 1px dashed #bbb" style="border: 1px dashed #bbb"
> >
<category-config :list="list.slice(0, Math.floor(list.length / 2))" /> <category-config :list="skuList.slice(0, Math.floor(skuList.length / 2))" />
<category-config :list="list.slice(Math.floor(list.length / 2))" /> <category-config :list="skuList.slice(Math.floor(skuList.length / 2))" />
</div> </div>
</el-card> </el-card>
<el-card> <el-card>
@ -126,10 +136,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { initConfig } from './config' import { initConfig } from './config'
import { categoryDataMock, categoryOptionsMock } from './mock' import { categoryDataMock } from './mock'
import { import {
generateTargetData, generateSkuList,
useCategoryAddOption, usePropertyValue,
colors, colors,
useDrag, useDrag,
handleLoadNode, handleLoadNode,
@ -161,18 +171,27 @@ const {
onClickChooseResourceBtn onClickChooseResourceBtn
} = useImportFile() } = useImportFile()
const categoryOptions123 = ref(categoryOptionsMock) const isTest = ref(0)
const categoryData = ref(categoryDataMock) const categoryData = ref(categoryDataMock)
const { inputValue, InputRefs, inputVisibles, onAddCategoryDataOption, handleInputConfirm } = const {
useCategoryAddOption(categoryData) inputValue,
InputRefs,
inputVisibles,
onAddPropertyValue,
onClosePropertyValue,
handleInputConfirm
} = usePropertyValue(categoryData.value)
const { dragRef, createDrag } = useDrag(categoryData) const { dragRef, createDrag } = useDrag(categoryData)
createDrag() createDrag()
const list = computed(() => generateTargetData(categoryData.value)) const skuList = generateSkuList(categoryData.value)
console.warn('----- my data is list.value: ', list.value)
const htmlContent = ref('自定义') const htmlContent = ref('自定义')
const onDeletePropertyValue = (index: number, id: number) =>
onClosePropertyValue(index, id, skuList)
const onSubmit = () => { const onSubmit = () => {
console.warn('----- my data is htmlContent: ', htmlContent.value) console.warn('----- my data is htmlContent: ', htmlContent.value)
} }

View File

@ -37,7 +37,7 @@ export const categoryOptionsMock = [
] ]
} }
] ]
/*
// 类目配置项 // 类目配置项
export const categoryDataMock = [ export const categoryDataMock = [
{ {
@ -66,6 +66,145 @@ export const categoryDataMock = [
{ id: 33, name: '黄' } { id: 33, name: '黄' }
] ]
} }
] */
// 类目配置项
export const categoryDataMock = [
{
id: 1,
isDelete: 0,
createTime: 1756023528000,
modifyTime: 1756023528000,
categoryId: 16,
categoryName: '项链',
categoryPropertyName: '珍珠直径',
defaultSort: -1,
vvPropertyValueList: [
{
id: 1,
isDelete: 0,
createTime: 1756015260000,
modifyTime: 1756015260000,
categoryPropertyId: 1,
categoryPropertyValue: '6-7mm',
defaultSort: -1
},
{
id: 3,
isDelete: 0,
createTime: 1756015260000,
modifyTime: 1756015260000,
categoryPropertyId: 1,
categoryPropertyValue: '8-9mm',
defaultSort: 2
},
{
id: 4,
isDelete: 0,
createTime: 1756015260000,
modifyTime: 1756015260000,
categoryPropertyId: 1,
categoryPropertyValue: '9-10mm',
defaultSort: 3
},
{
id: 2,
isDelete: 0,
createTime: 1756015260000,
modifyTime: 1756015260000,
categoryPropertyId: 1,
categoryPropertyValue: '7-8mm',
defaultSort: 4
},
{
id: 5,
isDelete: 0,
createTime: 1756015260000,
modifyTime: 1756015260000,
categoryPropertyId: 1,
categoryPropertyValue: '10-11mm',
defaultSort: 5
},
{
id: 6,
isDelete: 0,
createTime: 1756015260000,
modifyTime: 1756015260000,
categoryPropertyId: 1,
categoryPropertyValue: '11-12mm',
defaultSort: 6
}
]
},
{
id: 3,
isDelete: 0,
createTime: 1756023528000,
modifyTime: 1756023528000,
categoryId: 16,
categoryName: '项链',
categoryPropertyName: '颜色分类',
defaultSort: 2,
vvPropertyValueList: [
{
id: 12,
isDelete: 0,
createTime: 1756015420000,
modifyTime: 1756015420000,
categoryPropertyId: 3,
categoryPropertyValue: '极光正圆【配鉴定证书】',
defaultSort: 1
}
]
},
{
id: 2,
isDelete: 0,
createTime: 1756023528000,
modifyTime: 1756023528000,
categoryId: 16,
categoryName: '项链',
categoryPropertyName: '项链长度',
defaultSort: 3,
vvPropertyValueList: [
{
id: 8,
isDelete: 0,
createTime: 1756015381000,
modifyTime: 1756015381000,
categoryPropertyId: 2,
categoryPropertyValue: '43cm',
defaultSort: 1
},
{
id: 9,
isDelete: 0,
createTime: 1756015381000,
modifyTime: 1756015381000,
categoryPropertyId: 2,
categoryPropertyValue: '45cm',
defaultSort: 2
},
{
id: 10,
isDelete: 0,
createTime: 1756015381000,
modifyTime: 1756015381000,
categoryPropertyId: 2,
categoryPropertyValue: '47cm',
defaultSort: 3
},
{
id: 11,
isDelete: 0,
createTime: 1756015381000,
modifyTime: 1756015381000,
categoryPropertyId: 2,
categoryPropertyValue: '50cm',
defaultSort: 4
}
]
}
] ]
// //

View File

@ -2,43 +2,49 @@ import { useDraggable } from 'vue-draggable-plus'
// 定义源数据的结构类型 // 定义源数据的结构类型
interface Option { interface Option {
id: number id: number
name: string categoryPropertyValue: string
isAdd?: boolean
} }
interface Category { interface Category {
typeName: string typeName: string
typeId: number typeId: number
options: Option[] vvPropertyValueList: Option[]
} }
type CategoryDataMock = Category[] type CategoryDataMock = Category[]
// 定义目标数据的结构类型 // 定义目标数据的结构类型
interface TargetData { interface TSkuList {
serialNo: string serialNo: string
name: string name: string
sale: string salePrice: string
img: string imageUrl: string
num: number stock: number
amount: string originPrice: number
promotionPrice: string
vvSkuPropertyValueList: any[]
} }
export const generateTargetData = (categoryData: CategoryDataMock): TargetData[] => { // 创建sku列表
const result: TargetData[] = [] export const generateSkuList = (categoryData: CategoryDataMock): Ref<TSkuList[]> => {
const optionGroups: Option[][] = categoryData.map((category) => category.options) const result: TSkuList[] = []
const optionGroups: Option[][] = categoryData.map((category) => category.vvPropertyValueList)
// 使用递归生成所有可能的组合 // 使用递归生成所有可能的组合
const combine = (index: number, current: Option[]) => { const combine = (index: number, current: Option[]) => {
if (index === optionGroups.length) { if (index === optionGroups.length) {
const serialNo = current.map((opt) => opt.id.toString()).join('-') const serialNo = current.map((opt) => opt.id.toString()).join('-')
const name = current.map((opt) => opt.name).join('-') const name = current.map((opt) => opt.categoryPropertyValue).join('-')
result.push({ result.push({
serialNo, serialNo,
name, name,
sale: '', salePrice: '',
num: 0, stock: 0,
img: '', originPrice: 0,
amount: '' imageUrl: '',
promotionPrice: '',
vvSkuPropertyValueList: current
}) })
return return
} }
@ -48,33 +54,99 @@ export const generateTargetData = (categoryData: CategoryDataMock): TargetData[]
} }
combine(0, []) combine(0, [])
return result return ref(result)
} }
// 添加类目 // 增加属性、删除属性
export const useCategoryAddOption = (categoryData: any) => { export const usePropertyValue = (categoryData: CategoryDataMock) => {
const inputVisibles = ref(Array.from({ length: categoryData.value.length }, (_) => false)) const inputVisibles = ref(Array.from({ length: categoryData.length }, (_) => false))
const InputRefs = ref() const InputRefs = ref()
const inputValue = ref('') const inputValue = ref('')
const onAddCategoryDataOption = (index: number) => { const onAddPropertyValue = (index: number) => {
inputVisibles.value[index] = true inputVisibles.value[index] = true
nextTick(() => { nextTick(() => {
InputRefs.value[index].input!.focus() InputRefs.value[index].input!.focus()
}) })
} }
const handleInputConfirm = (index: number) => { const onClosePropertyValue = (index: number, id: number, skuList: Ref<TSkuList[]>) => {
const i = categoryData[index].vvPropertyValueList.findIndex((item: any) => item.id === id)
categoryData[index].vvPropertyValueList.splice(i, 1)
const deleteIndexs: number[] = []
skuList.value.forEach((item: TSkuList, skuIndex: number) => {
if (item.vvSkuPropertyValueList.find((it) => it.id === id)) {
deleteIndexs.push(skuIndex)
}
})
skuList.value = skuList.value.filter(
(_: TSkuList, index: number) => !deleteIndexs.includes(index)
)
}
// 新增回车确认
const handleInputConfirm = (lineIndex: number, skuList: TSkuList[] = []) => {
if (inputValue.value) { if (inputValue.value) {
categoryData.value[index].options.push({ const id = Math.random()
id: inputValue.value, categoryData[lineIndex].vvPropertyValueList.push({
name: inputValue.value, id,
categoryPropertyValue: inputValue.value,
isAdd: true isAdd: true
}) })
skuList.push(
...generateAddSkuList(
lineIndex,
{ id, categoryPropertyValue: inputValue.value },
categoryData
)
)
} }
inputVisibles.value[index] = false inputVisibles.value[lineIndex] = false
inputValue.value = '' inputValue.value = ''
} }
return { inputVisibles, InputRefs, inputValue, onAddCategoryDataOption, handleInputConfirm }
// 新增属性的时候创建数据
const generateAddSkuList = (
lineIndex: number,
addData: Option,
categoryData: CategoryDataMock
): TSkuList[] => {
const result: TSkuList[] = []
const optionGroups: Option[][] = categoryData
.filter((_, index) => index !== lineIndex)
.map((category) => category.vvPropertyValueList)
// 使用递归生成所有可能的组合
const combine = (index: number, current: Option[]) => {
if (index === categoryData.length) {
const serialNo = current.map((opt) => opt.id.toString()).join('-')
const name = current.map((opt) => opt.categoryPropertyValue).join('-')
result.push({
serialNo,
name,
salePrice: '',
stock: 0,
originPrice: 0,
imageUrl: '',
promotionPrice: '',
vvSkuPropertyValueList: current
})
return
}
for (const option of optionGroups[index]) {
combine(index + 1, [...current, option])
}
}
combine(1, [addData])
return result
}
return {
inputVisibles,
InputRefs,
inputValue,
onAddPropertyValue,
onClosePropertyValue,
handleInputConfirm
}
} }
export const colors = ['primary', 'success', 'warning', 'danger', 'info'] export const colors = ['primary', 'success', 'warning', 'danger', 'info']
@ -120,7 +192,7 @@ export const handleLoadNode = (node: any, resolve: any) => {
}) })
} }
// 树结构懒加载后台类目 // 树结构懒加载app类目
export const handleLoadNode2 = (node: any, resolve: any) => { export const handleLoadNode2 = (node: any, resolve: any) => {
api.commodity.getAppCategoryList.post!<any>({ parentId: node.data.value || 0 }).then((res) => { api.commodity.getAppCategoryList.post!<any>({ parentId: node.data.value || 0 }).then((res) => {
resolve( resolve(