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

View File

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

View File

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

View File

@ -1,3 +1,4 @@
import { Atrans$DialogRes } from '@/utils/page/config'
import { useDraggable } from 'vue-draggable-plus' import { useDraggable } from 'vue-draggable-plus'
// 定义源数据的结构类型 // 定义源数据的结构类型
interface Option { interface Option {
@ -15,7 +16,7 @@ interface Category {
type CategoryDataMock = Category[] type CategoryDataMock = Category[]
// 定义目标数据的结构类型 // 定义目标数据的结构类型
interface TSkuList { export interface TSkuList {
serialNo: string serialNo: string
name: string name: string
salePrice: string salePrice: string
@ -27,9 +28,9 @@ interface TSkuList {
} }
// 创建sku列表 // 创建sku列表
export const generateSkuList = (categoryData: CategoryDataMock): Ref<TSkuList[]> => { export const generateSkuList = (adminCategoryData: CategoryDataMock): TSkuList[] => {
const result: 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[]) => { const combine = (index: number, current: Option[]) => {
@ -54,24 +55,29 @@ export const generateSkuList = (categoryData: CategoryDataMock): Ref<TSkuList[]>
} }
combine(0, []) combine(0, [])
return ref(result) return result
} }
// 增加属性、删除属性 // 增加属性、删除属性
export const usePropertyValue = (categoryData: CategoryDataMock) => { export const usePropertyValue = () => {
const inputVisibles = ref(Array.from({ length: categoryData.length }, (_) => false)) 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 InputRefs = ref()
const inputValue = ref('') const inputValue = ref('')
const onAddPropertyValue = (index: number) => { const onAddPropertyValue = (index: number) => {
inputVisibles.value[index] = true inputVisibles.value[index] = true
nextTick(() => { nextTick(() => {
InputRefs.value[index].input!.focus() InputRefs.value[0].input!.focus()
}) })
} }
const onClosePropertyValue = (index: number, id: number, skuList: Ref<TSkuList[]>) => { const onClosePropertyValue = (index: number, id: number, skuList: Ref<TSkuList[]>) => {
const i = categoryData[index].vvPropertyValueList.findIndex((item: any) => item.id === id) const i = adminCategoryData[index].vvPropertyValueList.findIndex((item: any) => item.id === id)
categoryData[index].vvPropertyValueList.splice(i, 1) adminCategoryData[index].vvPropertyValueList.splice(i, 1)
const deleteIndexs: number[] = [] const deleteIndexs: number[] = []
skuList.value.forEach((item: TSkuList, skuIndex: number) => { skuList.value.forEach((item: TSkuList, skuIndex: number) => {
if (item.vvSkuPropertyValueList.find((it) => it.id === id)) { if (item.vvSkuPropertyValueList.find((it) => it.id === id)) {
@ -86,7 +92,7 @@ export const usePropertyValue = (categoryData: CategoryDataMock) => {
const handleInputConfirm = (lineIndex: number, skuList: TSkuList[] = []) => { const handleInputConfirm = (lineIndex: number, skuList: TSkuList[] = []) => {
if (inputValue.value) { if (inputValue.value) {
const id = Math.random() const id = Math.random()
categoryData[lineIndex].vvPropertyValueList.push({ adminCategoryData[lineIndex].vvPropertyValueList.push({
id, id,
categoryPropertyValue: inputValue.value, categoryPropertyValue: inputValue.value,
isAdd: true isAdd: true
@ -95,7 +101,7 @@ export const usePropertyValue = (categoryData: CategoryDataMock) => {
...generateAddSkuList( ...generateAddSkuList(
lineIndex, lineIndex,
{ id, categoryPropertyValue: inputValue.value }, { id, categoryPropertyValue: inputValue.value },
categoryData adminCategoryData
) )
) )
} }
@ -107,16 +113,16 @@ export const usePropertyValue = (categoryData: CategoryDataMock) => {
const generateAddSkuList = ( const generateAddSkuList = (
lineIndex: number, lineIndex: number,
addData: Option, addData: Option,
categoryData: CategoryDataMock adminCategoryData: CategoryDataMock
): TSkuList[] => { ): TSkuList[] => {
const result: TSkuList[] = [] const result: TSkuList[] = []
const optionGroups: Option[][] = categoryData const optionGroups: Option[][] = adminCategoryData
.filter((_, index) => index !== lineIndex) .filter((_, index) => index !== lineIndex)
.map((category) => category.vvPropertyValueList) .map((category) => category.vvPropertyValueList)
// 使用递归生成所有可能的组合 // 使用递归生成所有可能的组合
const combine = (index: number, current: Option[]) => { const combine = (index: number, current: Option[]) => {
if (index === categoryData.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.categoryPropertyValue).join('-') const name = current.map((opt) => opt.categoryPropertyValue).join('-')
result.push({ 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 result
} }
return { return {
inputVisibles, inputVisibles,
InputRefs, InputRefs,
inputValue, inputValue,
initPropertyData,
onAddPropertyValue, onAddPropertyValue,
onClosePropertyValue, onClosePropertyValue,
handleInputConfirm 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 } // checkedStatus: { checkedKeys, checkedNodes, halfCheckedKeys, halfCheckedNodes }
checkedNodes.value[type] = [...checkedStatus.halfCheckedKeys, ...checkedStatus.checkedKeys] data[type + 'CategoryIds'] = [...checkedStatus.halfCheckedKeys, ...checkedStatus.checkedKeys]
console.warn('----- my data is checkedNodes: ', checkedNodes) }
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[]
} }