feat: 商品详情
This commit is contained in:
parent
263fbb3633
commit
3f18c2ea0e
@ -2,21 +2,41 @@
|
||||
<div class="max-w-1/2">
|
||||
<dl v-for="item in list" :key="item.typeId" class="flex mb-1 items-center">
|
||||
<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>
|
||||
</dt>
|
||||
<dd class="flex">
|
||||
<img :src="item.img" alt="" />
|
||||
<p>
|
||||
<span class="text-xs text-[#666]">数量:</span
|
||||
><el-input v-model="item.num" size="small" class="w-20 mr-2"></el-input>
|
||||
<span class="text-xs text-[#666]">库存:</span
|
||||
><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>
|
||||
<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>
|
||||
<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>
|
||||
</dd>
|
||||
</dl>
|
||||
@ -24,5 +44,65 @@
|
||||
</template>
|
||||
|
||||
<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>
|
||||
|
||||
@ -55,31 +55,41 @@
|
||||
</form-wrap>
|
||||
</el-card>
|
||||
<el-card class="mt-2 sku-info">
|
||||
<h4 class="font-bold mb-2">SKU信息:</h4>
|
||||
<el-form-item label="后台类目">
|
||||
<el-tree-select
|
||||
show-checkbox
|
||||
v-model="$dialog.data.categoryId"
|
||||
:props="defaultAdminCategoryTreeProps"
|
||||
:render-after-expand="false"
|
||||
@check="
|
||||
(checkedNode: any, checkedStatus: any) =>
|
||||
handleCheckChange(checkedNode, checkedStatus, 'admin')
|
||||
"
|
||||
:load="handleLoadNode"
|
||||
lazy
|
||||
style="width: 240px"
|
||||
>
|
||||
</el-tree-select>
|
||||
</el-form-item>
|
||||
<h4 class="font-bold mb-2">属性配置:</h4>
|
||||
<el-form inline>
|
||||
<el-form-item label="后台类目">
|
||||
<el-tree-select
|
||||
show-checkbox
|
||||
v-model="$dialog.data.categoryId"
|
||||
:props="defaultAdminCategoryTreeProps"
|
||||
:render-after-expand="false"
|
||||
@check="
|
||||
(checkedNode: any, checkedStatus: any) =>
|
||||
handleCheckChange(checkedNode, checkedStatus, 'admin')
|
||||
"
|
||||
:load="handleLoadNode"
|
||||
lazy
|
||||
style="width: 240px"
|
||||
>
|
||||
</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">
|
||||
<dl v-for="(item, index) in categoryData" :key="item.typeId" class="flex items-center">
|
||||
<dt class="text-sm text-[#666] mb-2">{{ item.typeName }}:</dt>
|
||||
<dl v-for="(item, index) in categoryData" :key="item.id" class="flex items-center">
|
||||
<dt class="text-sm text-[#666] mb-2">{{ item.categoryPropertyName }}:</dt>
|
||||
<dd class="flex mb-2">
|
||||
<div v-for="child in item.options" :key="item.typeId + '-' + child.id">
|
||||
<el-tag closable effect="plain" :type="colors[index % 4]" class="mr-1">{{
|
||||
child.name
|
||||
}}</el-tag>
|
||||
<div v-for="child in item.vvPropertyValueList" :key="item.id + '-' + child.id">
|
||||
<el-tag
|
||||
closable
|
||||
effect="plain"
|
||||
:type="colors[index % 4]"
|
||||
class="mr-1"
|
||||
@close="onDeletePropertyValue(index, child.id)"
|
||||
>{{ child.categoryPropertyValue }}</el-tag
|
||||
>
|
||||
</div>
|
||||
<el-input
|
||||
v-if="inputVisibles[index]"
|
||||
@ -87,14 +97,14 @@
|
||||
v-model="inputValue"
|
||||
class="w-20"
|
||||
size="small"
|
||||
@keyup.enter="handleInputConfirm(index)"
|
||||
@keyup.enter="handleInputConfirm(index, skuList)"
|
||||
@blur="handleInputConfirm(index)"
|
||||
/>
|
||||
<el-button
|
||||
v-else
|
||||
class="button-new-tag ml-1"
|
||||
size="small"
|
||||
@click="onAddCategoryDataOption(index)"
|
||||
@click="onAddPropertyValue(index)"
|
||||
>
|
||||
+ 新增
|
||||
</el-button>
|
||||
@ -102,13 +112,13 @@
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<h4 class="my-1">价格配置:</h4>
|
||||
<h4 class="my-1">sku配置:</h4>
|
||||
<div
|
||||
class="flex w-full flex-wrap justify-between p-2 rounded-[5px]"
|
||||
style="border: 1px dashed #bbb"
|
||||
>
|
||||
<category-config :list="list.slice(0, Math.floor(list.length / 2))" />
|
||||
<category-config :list="list.slice(Math.floor(list.length / 2))" />
|
||||
<category-config :list="skuList.slice(0, Math.floor(skuList.length / 2))" />
|
||||
<category-config :list="skuList.slice(Math.floor(skuList.length / 2))" />
|
||||
</div>
|
||||
</el-card>
|
||||
<el-card>
|
||||
@ -126,10 +136,10 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { initConfig } from './config'
|
||||
import { categoryDataMock, categoryOptionsMock } from './mock'
|
||||
import { categoryDataMock } from './mock'
|
||||
import {
|
||||
generateTargetData,
|
||||
useCategoryAddOption,
|
||||
generateSkuList,
|
||||
usePropertyValue,
|
||||
colors,
|
||||
useDrag,
|
||||
handleLoadNode,
|
||||
@ -161,18 +171,27 @@ const {
|
||||
onClickChooseResourceBtn
|
||||
} = useImportFile()
|
||||
|
||||
const categoryOptions123 = ref(categoryOptionsMock)
|
||||
const isTest = ref(0)
|
||||
|
||||
const categoryData = ref(categoryDataMock)
|
||||
const { inputValue, InputRefs, inputVisibles, onAddCategoryDataOption, handleInputConfirm } =
|
||||
useCategoryAddOption(categoryData)
|
||||
const {
|
||||
inputValue,
|
||||
InputRefs,
|
||||
inputVisibles,
|
||||
onAddPropertyValue,
|
||||
onClosePropertyValue,
|
||||
handleInputConfirm
|
||||
} = usePropertyValue(categoryData.value)
|
||||
|
||||
const { dragRef, createDrag } = useDrag(categoryData)
|
||||
createDrag()
|
||||
const list = computed(() => generateTargetData(categoryData.value))
|
||||
console.warn('----- my data is list.value: ', list.value)
|
||||
const skuList = generateSkuList(categoryData.value)
|
||||
|
||||
const htmlContent = ref('自定义')
|
||||
|
||||
const onDeletePropertyValue = (index: number, id: number) =>
|
||||
onClosePropertyValue(index, id, skuList)
|
||||
|
||||
const onSubmit = () => {
|
||||
console.warn('----- my data is htmlContent: ', htmlContent.value)
|
||||
}
|
||||
|
||||
@ -37,7 +37,7 @@ export const categoryOptionsMock = [
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
/*
|
||||
// 类目配置项
|
||||
export const categoryDataMock = [
|
||||
{
|
||||
@ -66,6 +66,145 @@ export const categoryDataMock = [
|
||||
{ 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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
//
|
||||
|
||||
@ -2,43 +2,49 @@ import { useDraggable } from 'vue-draggable-plus'
|
||||
// 定义源数据的结构类型
|
||||
interface Option {
|
||||
id: number
|
||||
name: string
|
||||
categoryPropertyValue: string
|
||||
isAdd?: boolean
|
||||
}
|
||||
|
||||
interface Category {
|
||||
typeName: string
|
||||
typeId: number
|
||||
options: Option[]
|
||||
vvPropertyValueList: Option[]
|
||||
}
|
||||
|
||||
type CategoryDataMock = Category[]
|
||||
|
||||
// 定义目标数据的结构类型
|
||||
interface TargetData {
|
||||
interface TSkuList {
|
||||
serialNo: string
|
||||
name: string
|
||||
sale: string
|
||||
img: string
|
||||
num: number
|
||||
amount: string
|
||||
salePrice: string
|
||||
imageUrl: string
|
||||
stock: number
|
||||
originPrice: number
|
||||
promotionPrice: string
|
||||
vvSkuPropertyValueList: any[]
|
||||
}
|
||||
|
||||
export const generateTargetData = (categoryData: CategoryDataMock): TargetData[] => {
|
||||
const result: TargetData[] = []
|
||||
const optionGroups: Option[][] = categoryData.map((category) => category.options)
|
||||
// 创建sku列表
|
||||
export const generateSkuList = (categoryData: CategoryDataMock): Ref<TSkuList[]> => {
|
||||
const result: TSkuList[] = []
|
||||
const optionGroups: Option[][] = categoryData.map((category) => category.vvPropertyValueList)
|
||||
|
||||
// 使用递归生成所有可能的组合
|
||||
const combine = (index: number, current: Option[]) => {
|
||||
if (index === optionGroups.length) {
|
||||
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({
|
||||
serialNo,
|
||||
name,
|
||||
sale: '',
|
||||
num: 0,
|
||||
img: '',
|
||||
amount: ''
|
||||
salePrice: '',
|
||||
stock: 0,
|
||||
originPrice: 0,
|
||||
imageUrl: '',
|
||||
promotionPrice: '',
|
||||
vvSkuPropertyValueList: current
|
||||
})
|
||||
return
|
||||
}
|
||||
@ -48,33 +54,99 @@ export const generateTargetData = (categoryData: CategoryDataMock): TargetData[]
|
||||
}
|
||||
|
||||
combine(0, [])
|
||||
return result
|
||||
return ref(result)
|
||||
}
|
||||
|
||||
// 添加类目
|
||||
export const useCategoryAddOption = (categoryData: any) => {
|
||||
const inputVisibles = ref(Array.from({ length: categoryData.value.length }, (_) => false))
|
||||
// 增加属性、删除属性
|
||||
export const usePropertyValue = (categoryData: CategoryDataMock) => {
|
||||
const inputVisibles = ref(Array.from({ length: categoryData.length }, (_) => false))
|
||||
|
||||
const InputRefs = ref()
|
||||
const inputValue = ref('')
|
||||
const onAddCategoryDataOption = (index: number) => {
|
||||
const onAddPropertyValue = (index: number) => {
|
||||
inputVisibles.value[index] = true
|
||||
nextTick(() => {
|
||||
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) {
|
||||
categoryData.value[index].options.push({
|
||||
id: inputValue.value,
|
||||
name: inputValue.value,
|
||||
const id = Math.random()
|
||||
categoryData[lineIndex].vvPropertyValueList.push({
|
||||
id,
|
||||
categoryPropertyValue: inputValue.value,
|
||||
isAdd: true
|
||||
})
|
||||
skuList.push(
|
||||
...generateAddSkuList(
|
||||
lineIndex,
|
||||
{ id, categoryPropertyValue: inputValue.value },
|
||||
categoryData
|
||||
)
|
||||
)
|
||||
}
|
||||
inputVisibles.value[index] = false
|
||||
inputVisibles.value[lineIndex] = false
|
||||
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']
|
||||
@ -120,7 +192,7 @@ export const handleLoadNode = (node: any, resolve: any) => {
|
||||
})
|
||||
}
|
||||
|
||||
// 树结构懒加载后台类目
|
||||
// 树结构懒加载app类目
|
||||
export const handleLoadNode2 = (node: any, resolve: any) => {
|
||||
api.commodity.getAppCategoryList.post!<any>({ parentId: node.data.value || 0 }).then((res) => {
|
||||
resolve(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user