feat: 商品详情编辑

This commit is contained in:
zc 2025-10-25 19:56:01 +08:00
parent e1f682b95c
commit c198ca35ea
5 changed files with 278 additions and 48 deletions

View File

@ -1,5 +1,12 @@
interface PreviewItem {
fileName: string
name: string
resourceUrl: string
type: 'image' | 'video'
}
// 选择资源库图片
export const useImportFile = () => {
export const useImportFile = (fileList: Ref<PreviewItem[]>) => {
const showFileExplorer = ref(false)
const fileType = {
mainImageUrl: '主图',
@ -10,8 +17,8 @@ export const useImportFile = () => {
const currentPathArray = ref<{ name: string; id: number }[]>([{ name: '根目录', id: 0 }])
// 处理文件选择
const handleChooseResourceFileCallback = (files: any[] = []) => {
handleChooseFiles(curFileType.value, files)
const handleChooseResourceFileCallback = (files: PreviewItem[] = []) => {
handleChooseFiles(curFileType.value, files, fileList.value)
}
const onClickChooseResourceBtn = (type: 'mainImageUrl' | 'videoUrl' | 'subImage') => {
@ -26,10 +33,10 @@ export const useImportFile = () => {
}
}
export const fileList = ref<any[]>([])
export const handleChooseFiles = (
fileType: 'mainImageUrl' | 'videoUrl' | 'subImage',
files: any = []
files: any = [],
fileList: PreviewItem[]
) => {
const fileNameMap = {
mainImageUrl: '主图',
@ -37,12 +44,10 @@ export const handleChooseFiles = (
subImage: '副图'
}
if (['mainImageUrl', 'videoUrl'].includes(fileType)) {
const index = fileList.value.findIndex((item: any) => item.fileType === fileType)
const index = fileList.findIndex((item: any) => item.fileType === fileType)
if (index !== -1) {
fileList.value.splice(index, 1)
fileList.splice(index, 1)
}
}
fileList.value.push(
...files.map((item: any) => ({ ...item, fileType, name: fileNameMap[fileType] }))
)
fileList.push(...files.map((item: any) => ({ ...item, fileType, name: fileNameMap[fileType] })))
}

View File

@ -5,7 +5,7 @@ export const initConfig = () => {
dialog: [
{
title: { label: '商品标题', class: '!w-full' },
appCategoryId: { label: 'app类目', slot: 'appCategoryId' },
appCategoryIds: { label: 'app类目', slot: 'appCategoryId' },
mainImageUrl: { label: '主图', slot: 'mainFile' },
videoUrl: { label: '视频', slot: 'videoFile' },
subImageUrl: { label: '副图', slot: 'subFile' },

View File

@ -9,9 +9,10 @@
v-model="$dialog.data.appCategoryId"
:props="defaultAdminCategoryTreeProps"
:render-after-expand="false"
:default-expanded-keys="defaultCheckedNodes.app"
@check="
(checkedNode: any, checkedStatus: any) =>
handleCheckChange(checkedNode, checkedStatus, 'app')
handleCheckChange(checkedNode, checkedStatus, 'app', $dialog.data)
"
:load="handleLoadNode2"
lazy
@ -22,7 +23,7 @@
<file-upload-btn
accept="image/*"
size="normal"
@change="(val) => handleChooseFiles('mainImageUrl', val)"
@change="(val) => handleChooseFiles('mainImageUrl', val, fileList)"
/>
<el-button class="ml-2" @click="onClickChooseResourceBtn('mainImageUrl')"
>资源库导入</el-button
@ -32,7 +33,7 @@
<file-upload-btn
size="normal"
accept="video/*"
@change="(val) => handleChooseFiles('videoUrl', val)"
@change="(val) => handleChooseFiles('videoUrl', val, fileList)"
/>
<el-button class="ml-2" @click="onClickChooseResourceBtn('videoUrl')"
>资源库导入</el-button
@ -43,7 +44,7 @@
size="normal"
multiple
accept="image/*"
@change="(val) => handleChooseFiles('subImage', val)"
@change="(val) => handleChooseFiles('subImage', val, fileList)"
/>
<el-button class="ml-2" @click="onClickChooseResourceBtn('subImage')"
>资源库导入</el-button
@ -60,9 +61,10 @@
<el-form-item label="后台类目">
<el-tree-select
show-checkbox
v-model="$dialog.data.categoryId"
v-model="$dialog.data.adminCategoryId"
:props="defaultAdminCategoryTreeProps"
:render-after-expand="false"
:default-expanded-keys="defaultCheckedNodes.admin"
@check="
(checkedNode: any, checkedStatus: any) =>
handleCheckChange(checkedNode, checkedStatus, 'admin')
@ -78,7 +80,7 @@
</el-form-item>
</el-form>
<div ref="dragRef">
<dl v-for="(item, index) in categoryData" :key="item.id" class="flex items-center">
<dl v-for="(item, index) in adminCategoryData" :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.vvPropertyValueList" :key="item.id + '-' + child.id">
@ -146,44 +148,58 @@ import {
handleLoadNode2,
handleCheckChange,
defaultAdminCategoryTreeProps,
checkedNodes
defaultCheckedNodes,
handleGetDialogData,
fileList,
TSkuList
} from './use-method'
import categoryConfig from './category-config.vue'
import wanEditor from './editor.vue'
import fileUploadBtn from '@/components/FileUploadBtn/index.vue'
import resourceReview from '@/components/ResourceReview/index.vue'
import { useImportFile, fileList, handleChooseFiles } from './choose-file-method'
import { useImportFile, handleChooseFiles } from './choose-file-method'
import FileExplorerDialog from '@/components/FileExplorerDialog/index.vue'
import { Atrans$DialogRes } from '@/utils/page/config'
const route = useRoute()
const $dialog = ref<Atrans$DialogRes>({} as Atrans$DialogRes)
const init = async () => {
checkedNodes.value = { admin: [], app: [] }
await api.commodity.getCommodityDetail.post!({ productId: route.query.id })
defaultCheckedNodes.value = { admin: [], app: [] }
const data = initConfig().value!
$dialog.value = data.$dialog
$dialog.value.config = data.dialog1
$dialog.value.data = { appCategoryId: '', adminCategoryId: '', isTest: 0, title: '' }
adminCategoryData.value = []
if (route.query.id) {
const res = await api.commodity.getCommodityDetail.post!<any>({ productId: route.query.id })
handleGetDialogData(res.data, $dialog, fileList)
adminCategoryData.value = categoryDataMock
initPropertyData(adminCategoryData.value)
skuList.value = generateSkuList(adminCategoryData.value)
}
}
const { $dialog, dialog1 } = toRefs(initConfig().value!)
$dialog.value.config = dialog1.value
const {
showFileExplorer,
currentPathArray,
handleChooseResourceFileCallback,
onClickChooseResourceBtn
} = useImportFile()
} = useImportFile(fileList)
const categoryData = ref(categoryDataMock)
const adminCategoryData = ref([])
const {
inputValue,
InputRefs,
inputVisibles,
initPropertyData,
onAddPropertyValue,
onClosePropertyValue,
handleInputConfirm
} = usePropertyValue(categoryData.value)
} = usePropertyValue()
const { dragRef, createDrag } = useDrag(categoryData)
const { dragRef, createDrag } = useDrag(adminCategoryData)
createDrag()
const skuList = generateSkuList(categoryData.value)
const skuList = ref<TSkuList[]>([])
const htmlContent = ref('自定义')

View File

@ -207,6 +207,152 @@ export const categoryDataMock = [
}
]
/* export const categoryDataMock = [
{
id: 7,
isDelete: 0,
createTime: 1755940247000,
modifyTime: 1755940247000,
productId: 5,
productPropertyName: '珍珠直径',
defalutSort: 1,
vvProductPropertyValueList: [
{
id: 21,
isDelete: 0,
createTime: 1755940416000,
modifyTime: 1755940416000,
productId: 5,
productPropertyName: '珍珠直径',
productPropertyValue: '7-8mm',
productPropertyId: 7,
defalutSort: 1
},
{
id: 20,
isDelete: 0,
createTime: 1755940416000,
modifyTime: 1755940416000,
productId: 5,
productPropertyName: '珍珠直径',
productPropertyValue: '8-9mm',
productPropertyId: 7,
defalutSort: 2
},
{
id: 19,
isDelete: 0,
createTime: 1755940416000,
modifyTime: 1755940416000,
productId: 5,
productPropertyName: '珍珠直径',
productPropertyValue: '9-10mm',
productPropertyId: 7,
defalutSort: 3
},
{
id: 18,
isDelete: 0,
createTime: 1755940416000,
modifyTime: 1755940416000,
productId: 5,
productPropertyName: '珍珠直径',
productPropertyValue: '10-11mm',
productPropertyId: 7,
defalutSort: 4
}
]
},
{
id: 6,
isDelete: 0,
createTime: 1755940247000,
modifyTime: 1755940247000,
productId: 5,
productPropertyName: '项链长度',
defalutSort: 2,
vvProductPropertyValueList: [
{
id: 17,
isDelete: 0,
createTime: 1755940428000,
modifyTime: 1755940428000,
productId: 5,
productPropertyName: '项链长度',
productPropertyValue: '43cm',
productPropertyId: 6,
defalutSort: 1
},
{
id: 16,
isDelete: 0,
createTime: 1755940519000,
modifyTime: 1755940519000,
productId: 5,
productPropertyName: '项链长度',
productPropertyValue: '45cm',
productPropertyId: 6,
defalutSort: 2
},
{
id: 15,
isDelete: 0,
createTime: 1755940519000,
modifyTime: 1755940519000,
productId: 5,
productPropertyName: '项链长度',
productPropertyValue: '47cm',
productPropertyId: 6,
defalutSort: 3
},
{
id: 14,
isDelete: 0,
createTime: 1755940519000,
modifyTime: 1755940519000,
productId: 5,
productPropertyName: '项链长度',
productPropertyValue: '50cm',
productPropertyId: 6,
defalutSort: 4
}
]
},
{
id: 5,
isDelete: 0,
createTime: 1755940247000,
modifyTime: 1755940247000,
productId: 5,
productPropertyName: '颜色分类',
defalutSort: 3,
vvProductPropertyValueList: [
{
id: 13,
isDelete: 0,
createTime: 1755940570000,
modifyTime: 1755940570000,
productId: 5,
productPropertyName: '颜色分类',
productPropertyValue: '【极光白透粉】大几乎无瑕大配证书',
productPropertyId: 5,
defalutSort: 1
},
{
id: 12,
isDelete: 0,
createTime: 1755940570000,
modifyTime: 1755940570000,
productId: 5,
productPropertyName: '颜色分类',
productPropertyValue: '【极光冷白】大几乎无瑕大配证书',
productPropertyId: 5,
defalutSort: 2
}
]
}
] */
//
export const a = [
{

View File

@ -1,3 +1,4 @@
import { Atrans$DialogRes } from '@/utils/page/config'
import { useDraggable } from 'vue-draggable-plus'
// 定义源数据的结构类型
interface Option {
@ -15,7 +16,7 @@ interface Category {
type CategoryDataMock = Category[]
// 定义目标数据的结构类型
interface TSkuList {
export interface TSkuList {
serialNo: string
name: string
salePrice: string
@ -27,9 +28,9 @@ interface TSkuList {
}
// 创建sku列表
export const generateSkuList = (categoryData: CategoryDataMock): Ref<TSkuList[]> => {
export const generateSkuList = (adminCategoryData: CategoryDataMock): TSkuList[] => {
const result: TSkuList[] = []
const optionGroups: Option[][] = categoryData.map((category) => category.vvPropertyValueList)
const optionGroups: Option[][] = adminCategoryData.map((category) => category.vvPropertyValueList)
// 使用递归生成所有可能的组合
const combine = (index: number, current: Option[]) => {
@ -54,24 +55,29 @@ export const generateSkuList = (categoryData: CategoryDataMock): Ref<TSkuList[]>
}
combine(0, [])
return ref(result)
return result
}
// 增加属性、删除属性
export const usePropertyValue = (categoryData: CategoryDataMock) => {
const inputVisibles = ref(Array.from({ length: categoryData.length }, (_) => false))
export const usePropertyValue = () => {
let adminCategoryData: CategoryDataMock = []
const inputVisibles = ref<boolean[]>([])
const initPropertyData = (initData: CategoryDataMock) => {
inputVisibles.value = Array.from({ length: initData.length }, (_) => false)
adminCategoryData = initData
}
const InputRefs = ref()
const inputValue = ref('')
const onAddPropertyValue = (index: number) => {
inputVisibles.value[index] = true
nextTick(() => {
InputRefs.value[index].input!.focus()
InputRefs.value[0].input!.focus()
})
}
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 i = adminCategoryData[index].vvPropertyValueList.findIndex((item: any) => item.id === id)
adminCategoryData[index].vvPropertyValueList.splice(i, 1)
const deleteIndexs: number[] = []
skuList.value.forEach((item: TSkuList, skuIndex: number) => {
if (item.vvSkuPropertyValueList.find((it) => it.id === id)) {
@ -86,7 +92,7 @@ export const usePropertyValue = (categoryData: CategoryDataMock) => {
const handleInputConfirm = (lineIndex: number, skuList: TSkuList[] = []) => {
if (inputValue.value) {
const id = Math.random()
categoryData[lineIndex].vvPropertyValueList.push({
adminCategoryData[lineIndex].vvPropertyValueList.push({
id,
categoryPropertyValue: inputValue.value,
isAdd: true
@ -95,7 +101,7 @@ export const usePropertyValue = (categoryData: CategoryDataMock) => {
...generateAddSkuList(
lineIndex,
{ id, categoryPropertyValue: inputValue.value },
categoryData
adminCategoryData
)
)
}
@ -107,16 +113,16 @@ export const usePropertyValue = (categoryData: CategoryDataMock) => {
const generateAddSkuList = (
lineIndex: number,
addData: Option,
categoryData: CategoryDataMock
adminCategoryData: CategoryDataMock
): TSkuList[] => {
const result: TSkuList[] = []
const optionGroups: Option[][] = categoryData
const optionGroups: Option[][] = adminCategoryData
.filter((_, index) => index !== lineIndex)
.map((category) => category.vvPropertyValueList)
// 使用递归生成所有可能的组合
const combine = (index: number, current: Option[]) => {
if (index === categoryData.length) {
if (index === optionGroups.length) {
const serialNo = current.map((opt) => opt.id.toString()).join('-')
const name = current.map((opt) => opt.categoryPropertyValue).join('-')
result.push({
@ -136,13 +142,16 @@ export const usePropertyValue = (categoryData: CategoryDataMock) => {
}
}
combine(1, [addData])
combine(0, [addData])
//bug
console.warn('----- my data is result111: ', result)
return result
}
return {
inputVisibles,
InputRefs,
inputValue,
initPropertyData,
onAddPropertyValue,
onClosePropertyValue,
handleInputConfirm
@ -207,9 +216,63 @@ export const handleLoadNode2 = (node: any, resolve: any) => {
})
}
export const checkedNodes = ref<{ admin: number[]; app: number[] }>({ admin: [], app: [] })
export const handleCheckChange = (_: any, checkedStatus: any, type: 'admin' | 'app') => {
// 图片预览列表
export const fileList = ref<PreviewItem[]>([])
// 选中后台类目和app类目获取父子id
export const defaultCheckedNodes = ref<{ admin: number[]; app: number[] }>({ admin: [], app: [] })
export const handleCheckChange = (_: any, checkedStatus: any, type: 'admin' | 'app', data: any) => {
// checkedStatus: { checkedKeys, checkedNodes, halfCheckedKeys, halfCheckedNodes }
checkedNodes.value[type] = [...checkedStatus.halfCheckedKeys, ...checkedStatus.checkedKeys]
console.warn('----- my data is checkedNodes: ', checkedNodes)
data[type + 'CategoryIds'] = [...checkedStatus.halfCheckedKeys, ...checkedStatus.checkedKeys]
}
interface PreviewItem {
fileName: string
name: string
resourceUrl: string
type: 'image' | 'video'
}
// 编辑的时候从接口响应数据提取$dialog数据
export const handleGetDialogData = (
data: any,
$dialog: Ref<Atrans$DialogRes>,
fileList: Ref<PreviewItem[]>
) => {
const result = { ...$dialog.value.data }
result.appCategoryId = data.appCategoryId2
result.adminCategoryId = data.adminCategoryId
defaultCheckedNodes.value.admin = extractAdminCategoryIds(data)
defaultCheckedNodes.value.app = [data.appCategoryId1, data.appCategoryId2]
result.isTest = data.isTest
result.title = data.title
const files = [
{ fileName: '主图', name: '主图', resourceUrl: data.mainImageUrl, type: 'image' },
{ fileName: '视频', name: '视频', resourceUrl: data.videoUrl, type: 'video' }
]
files.push(
...data.vvProductDetailList
.filter((item: any) => item.type === 1)
.map((item: any) => ({
fileName: '副图',
name: '副图',
resourceUrl: item.resourceUrl,
type: 'image'
}))
)
fileList.value = files as PreviewItem[]
$dialog.value.data = result
}
// 将adminCategoryId1, adminCategoryId2, ... 提取出来并排序
function extractAdminCategoryIds(obj: Record<string, any>): number[] {
return Object.entries(obj)
.filter(([key]) => key.startsWith('adminCategoryId'))
.sort(([keyA], [keyB]) => {
// 按数字部分排序 (adminCategoryId1 < adminCategoryId2)
const numA = parseInt(keyA.replace('adminCategoryId', ''))
const numB = parseInt(keyB.replace('adminCategoryId', ''))
return numA - numB
})
.map(([, value]) => value)
.filter((val) => ![undefined, null].includes(val)) as number[]
}