feat: app类目完成+商品编辑后台类目自测

This commit is contained in:
zc 2025-10-31 18:03:18 +08:00
parent 5c8b7bcccf
commit 163030aef1
14 changed files with 316 additions and 129 deletions

View File

@ -31,7 +31,9 @@ const login = {
*/
getCommodityList: ['/product/list'], // 获取商品列表
getCommodityDetail: ['/product/detail'], // 获取商品详情
addOrUpdateCommodity: ['/product/insertOrUpadate'] // 修改商品详情
addOrUpdateCommodity: ['/product/insertOrUpadate'], // 修改商品详情
changeCommodityInfo: ['/product/onlyUpdateProduct'], // 修改商品信息(列表页)
copyCommodity: ['/product/copy'] // 复制商品
}
export default login

View File

@ -73,7 +73,7 @@
import { ElMessage } from 'element-plus'
import { Folder } from '@element-plus/icons-vue'
interface FileItem {
export interface FileItem {
id: number
fileName: string
type: 'file' | 'image' | 'video'

View File

@ -1,7 +1,12 @@
<template>
<div class="resource-upload">
<div class="resource-upload__wrapper">
<el-button type="primary" :size="size" class="relative !text-white">
<el-button
type="primary"
:link="isLink"
:size="size"
:class="['relative', { '!text-white': !isLink }]"
>
{{ buttonText }}
<input
ref="inputRef"
@ -20,6 +25,7 @@
interface Props {
/** 是否允许多选文件 */
multiple?: boolean
isLink?: boolean
size?: 'small' | 'default'
/** 接受的文件类型 */
accept?: 'image/*,video/*' | 'image/*' | 'video/*'
@ -27,15 +33,22 @@ interface Props {
buttonText?: string
}
export interface FileItem {
fileName: string
resourceUrl: string
type: 'image' | 'video'
}
withDefaults(defineProps<Props>(), {
multiple: false,
accept: 'image/*,video/*',
buttonText: '选择文件',
isLink: false,
size: 'default'
})
const emit = defineEmits<{
(e: 'change', files: File[]): void
(e: 'change', files: FileItem[]): void
}>()
const inputRef = ref<HTMLInputElement | null>(null)

View File

@ -11,7 +11,7 @@ const useAppStore = defineStore({
state: (): AppState => ({
device: 'desktop',
sidebar: {
opened: sidebarStatus ? sidebarStatus : true,
opened: sidebarStatus,
withoutAnimation: false
},
size: localStorage.getItem('size') || 'default'

View File

@ -1,9 +1,26 @@
<template>
<div id="category-tree" class="p-2 bg-white">
<p>类目展示</p>
<p>
app类目展示
<el-input
v-if="addRootCategory.showInput"
class="w-20 !h-[22px]"
v-model="addRootCategory.inputValue"
ref="addRootCategoryInputRef"
@keyup.enter="onAddRootCategory"
@blur="addRootCategory.showInput = false"
></el-input>
<el-button
v-else
class="w-20 !h-[22px]"
type="primary"
size="small"
@click="onClickAddRootCategory"
>添加根类目</el-button
>
</p>
<el-tree
ref="treeRef"
style="max-width: 600px"
:props="defaultProps"
node-key="id"
draggable
@ -13,115 +30,98 @@
lazy
highlight-current
:expand-on-click-node="false"
@node-click="handleNodeClick"
@node-drag-start="handleDragStart"
@node-drop="handleDropFinish"
>
<template #default="{ node, data }">
<div class="category-tree-node">
<span v-if="curId !== data.id">{{ data.name }}</span>
<el-input
ref="inputRef"
v-else
v-model="data.label2"
size="small"
class="w-40"
@keyup.enter="handleInputConfirm(data)"
@blur="treeRef.remove(node)"
/>
<div>
<el-button type="primary" link @click="onEdit(data)" size="small">编辑 </el-button>
<el-button type="primary" link @click="append(data)" size="small"> 添加 </el-button>
<img :src="data.imageUrl" alt="" class="w-5 h-5" />
<span v-if="curEditId !== data.id">{{ data.name }}</span>
<el-input
ref="editInputRef"
v-else
v-model="data.label2"
size="small"
class="w-40"
@keyup.enter="handleEditInputConfirm(node, data)"
@blur="curEditId = NaN"
/>
</div>
<div>
<el-button type="primary" link @click="onEdit(node, data)" size="small"
>编辑
</el-button>
<el-input
ref="addInputRef"
class="w-20"
size="small"
v-if="data.id === curAddAppCategoryId"
v-model="addInputValue"
@keyup.enter="onAddInputConfirm(node, data)"
@blur="curAddAppCategoryId = NaN"
></el-input>
<el-button
v-else
type="warning"
link
@click="onAppendChildBtn(node, data)"
size="small"
>
添加子类目
</el-button>
<file-upload-btn
class="inline-block"
isLink
size="small"
accept="image/*"
@click="curFileData = data"
@change="(val) => handleChooseResourceFileCallback(val, curFileData)"
/>
<el-button
type="success"
link
size="small"
class="ml-2"
@click="onClickChooseResourceBtn(data)"
>资源库导入</el-button
>
<el-button type="danger" size="small" link @click="onDelete(node)"> 删除 </el-button>
</div>
</div>
</template>
</el-tree>
<FileExplorerDialog
v-model="showFileExplorer"
v-model:initPathArray="currentPathArray"
@select="(files: FileItem[]) => handleChooseResourceFileCallback(files, curFileData)"
/>
</div>
</template>
<script lang="ts" setup>
import FileExplorerDialog, { FileItem } from '@/components/FileExplorerDialog/index.vue'
import fileUploadBtn from '@/components/FileUploadBtn/index.vue'
import { ElButton, ElMessage } from 'element-plus'
import type { RenderContentContext } from 'element-plus'
import { handleDragStart, handleDropFinish } from './use-drag'
interface Tree {
id: number
name: string
children?: Tree[]
}
type Node = RenderContentContext['node']
type Data = RenderContentContext['data']
const curId = ref(NaN)
const curParentId = ref(NaN)
const inputRef = ref()
const treeRef = ref()
//
const onEdit = (data: Data) => {
data.label2 = data.name
curId.value = data.id
nextTick(() => {
inputRef.value.focus()
})
}
//
const handleInputConfirm = async (data: any) => {
if (curId.value === Infinity) {
await api.commodity.updateAppCategory.post!({
...data,
name: data.label2,
parentId: curParentId.value
})
} else {
await api.commodity.updateAppCategory.post!({ ...data, name: data.label2 })
}
ElMessage.success('更新成功')
data.name = data.label2
data.label2 = ''
curId.value = NaN
}
//
const append = async (data: Data) => {
const node = treeRef.value.getNode(data.id)
curId.value = Infinity
curParentId.value = data.id
const newChild = { id: curId.value, name: '', label2: 'test', hasChild: false }
if (!node.expanded) {
await api.commodity.getAppCategoryList.post!<any>({ parentId: data.id || 0 }).then((res) => {
res.data.forEach((item: any) => {
treeRef.value.append(
{
...item,
children: item.hasChild ? [] : undefined
},
data
)
})
})
}
treeRef.value.append(newChild, data)
node.expanded = true
nextTick(() => {
const timer = setTimeout(() => {
clearTimeout(timer)
inputRef.value.focus()
}, 500)
})
}
//
const onDelete = async (node: Node) => {
await api.commodity.updateAppCategory.post!({
id: node.data.id,
isDelete: 1
})
treeRef.value.remove(node)
ElMessage.success('删除成功')
}
import {
treeRef,
onEdit,
onAddInputConfirm,
handleEditInputConfirm,
curEditId,
curAddAppCategoryId,
editInputRef,
addInputRef,
addInputValue,
onAppendChildBtn,
onDelete,
addRootCategory,
addRootCategoryInputRef,
onClickAddRootCategory,
onAddRootCategory,
Data
} from './init-method'
const defaultProps = {
label: 'name',
@ -144,9 +144,16 @@ const handleLoadNode = (node, resolve) => {
})
}
//
const handleNodeClick = (node: Node, data: Data) => {
console.log('click', node, data)
//
const curFileData = ref({})
const currentPathArray = ref<{ name: string; id: number }[]>([{ name: '根目录', id: 0 }])
const showFileExplorer = ref(false)
const onClickChooseResourceBtn = (data: Data) => {
curFileData.value = data
showFileExplorer.value = true
}
const handleChooseResourceFileCallback = (files: FileItem[], curFileData: Data) => {
curFileData.imageUrl = files[0].resourceUrl
}
</script>

View File

@ -0,0 +1,97 @@
import { ElMessage, type RenderContentContext } from 'element-plus'
export type Node = RenderContentContext['node']
export type Data = RenderContentContext['data']
export const curEditId = ref(NaN)
export const editInputRef = ref()
export const treeRef = ref()
// 编辑类目名称
export const onEdit = (node: Node, data: Data) => {
data.label2 = data.name
curEditId.value = data.id
nextTick(() => {
editInputRef.value.focus()
})
}
// 编辑类目回车确认
export const handleEditInputConfirm = async (node: Node, data: Data) => {
if (data.label2) {
await api.commodity.updateAppCategory.post!({ ...data, name: data.label2 })
ElMessage.success('更新成功')
data.name = data.label2
data.label2 = ''
curEditId.value = NaN
}
}
export const curAddAppCategoryId = ref(NaN)
export const addInputRef = ref()
export const addInputValue = ref('')
// 增加子类目按钮
export const onAppendChildBtn = async (node: Node, data: Data) => {
curAddAppCategoryId.value = data.id
nextTick(() => {
addInputRef.value.focus()
})
}
// 增加子类目回车确认
export const onAddInputConfirm = async (node: Node, data: Data) => {
if (addInputValue.value) {
await api.commodity.updateAppCategory.post!({
parentId: data.id,
name: addInputValue.value
})
ElMessage.success('添加成功')
addInputValue.value = ''
curAddAppCategoryId.value = NaN
node.loaded = false
node.expand()
}
}
// 删除类目
export const onDelete = async (node: Node) => {
handleMessageBox({
msg: `是否删除类目?`,
success: api.commodity.updateAppCategory.post!,
data: { id: node.data.id, isDelete: 1 }
}).then(() => {
treeRef.value.remove(node)
})
}
/**
*
*/
export const addRootCategory = reactive({ showInput: false, inputValue: '' })
export const addRootCategoryInputRef = ref()
// 点击添加根类目按钮
export const onClickAddRootCategory = () => {
addRootCategory.showInput = true
nextTick(() => {
addRootCategoryInputRef.value.input!.focus()
})
}
// 添加根类目回车确定
export const onAddRootCategory = async () => {
const newRoot = { parentId: 0, name: addRootCategory.inputValue }
const res = await api.commodity.updateAppCategory.post!<{ id: number }>(newRoot)
treeRef.value.insertAfter(
{
...newRoot,
children: [],
id: res.data.id,
hasChild: 0
},
treeRef.value.store.root.childNodes.slice(-1)[0]
)
addRootCategory.showInput = false
addRootCategory.inputValue = ''
ElMessage.success('添加成功')
}

View File

@ -18,7 +18,7 @@ export const handleDropFinish = (draggingNode: Node, dropNode: Node, dropType: N
// 获取拖拽到的位置索引
const sortIds = getSortIds()
if (!sortIds.length) return
api.commodity.sortCategory.post!({ categoryIds: sortIds }).then(() => {
api.commodity.sortAppCategory.post!({ categoryIds: sortIds }).then(() => {
console.log('tree drop:', draggingNode, dropNode.label, dropType)
})
}

View File

@ -50,10 +50,12 @@ export const handleAddChildCategoryConfirm = async (node: Node, data: any) => {
// 编辑类目回车确认
export const handleEditChildCategoryConfirm = async (node: Node, data: any) => {
await api.commodity.updateCategory.post!({ ...data, name: data.label2 })
ElMessage.success('更新成功')
data.name = data.label2
editChildCategoryId.value = NaN
if (data.label2) {
await api.commodity.updateCategory.post!({ ...data, name: data.label2 })
ElMessage.success('更新成功')
data.name = data.label2
editChildCategoryId.value = NaN
}
}
// 删除类目

View File

@ -1,6 +1,7 @@
<template>
<div class="max-w-1/2 sku-config">
<dl v-for="item in list" :key="item.typeId" class="flex mb-1 items-center">
<dl v-for="(item, index) in list" :key="item.typeId" class="flex mb-1 items-center">
<span class="text-sm text-[#666] mr-1">{{ (startIndex || 0) + index + 1 }}</span>
<dt class="flex items-center">
<el-upload
ref="uploadRef"
@ -46,6 +47,7 @@ import { Plus } from '@element-plus/icons-vue'
interface Props {
list: any[]
startIndex?: number
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars

View File

@ -85,13 +85,19 @@ export const handleGetNewAdminCategoryData = (data: any) => {
// 编辑的时候对sku进行重组
export const handleGetNewSkuList = (data: any) => {
return data.map((item: any) => ({
...item,
name: item.vvSkuPropertyValueList.reduce((str: string, cur: any, index: number) => {
cur.categoryPropertyName = cur.productPropertyName
cur.categoryPropertyValue = cur.productPropertyValue
str += index === 0 ? cur.productPropertyValue : '-' + cur.productPropertyValue
return str
}, '')
}))
return data.map((item: any) => {
let categoryPropertyNames = ''
let name = ''
item.vvSkuPropertyValueList.forEach((child: any, index: number) => {
child.categoryPropertyName = child.productPropertyName
child.categoryPropertyValue = child.productPropertyValue
categoryPropertyNames += (index === 0 ? '' : '-') + child.categoryPropertyName
name += (index === 0 ? '' : '-') + child.categoryPropertyValue
})
return {
...item,
name,
categoryPropertyNames
}
})
}

View File

@ -182,7 +182,12 @@
type="danger"
class="button-new-tag ml-1 !text-white"
size="small"
@click="onDeletePropertyTypeBtn(index, adminCategoryData, skuList)"
@click="
() => {
onDeletePropertyTypeBtn(index, adminCategoryData, skuList)
skuList = generateSkuList(adminCategoryData)
}
"
>
删除属性
</el-button>
@ -196,7 +201,10 @@
style="border: 1px dashed #bbb"
>
<category-config :list="skuList.slice(0, Math.floor(skuList.length / 2))" />
<category-config :list="skuList.slice(Math.floor(skuList.length / 2))" />
<category-config
:startIndex="Math.floor(skuList.length / 2)"
:list="skuList.slice(Math.floor(skuList.length / 2))"
/>
</div>
</el-card>
<el-card>

View File

@ -15,7 +15,6 @@ export const handleEditPropertyTypeConfirm = async (item: any) => {
export const onDeletePropertyTypeBtn = (index: number, adminCategoryData: any, skuList: any) => {
adminCategoryData.splice(index, 1)
skuList.length = 0
}
export const curPropertyValueId = ref('')
@ -31,7 +30,44 @@ export const onClickPropertyValue = (item: any, child: any) => {
}
export const onEditPropertyValue = (child: any, skuList: any) => {
const oldCategoryPropertyValue = child.categoryPropertyValue
child.categoryPropertyValue = inputEditPropertyValue.value
handleEditSkuListPropertyValue(
child.categoryPropertyName,
oldCategoryPropertyValue,
skuList,
inputEditPropertyValue.value
)
curPropertyValueId.value = ''
inputEditPropertyValue.value = ''
}
// 修改skuList中的属性值名称
const handleEditSkuListPropertyValue = (
categoryPropertyName: string,
oldCategoryPropertyValue: string,
skuList: any,
inputEditPropertyValue: string
) => {
const newPropertyValue = inputEditPropertyValue
skuList.forEach((item: any) => {
const index = (item.categoryPropertyNames || '')
.split('-')
.findIndex((item: string) => item === categoryPropertyName)
if (index !== -1) {
const names = (item.name || '').split('-')
if (names[index] === oldCategoryPropertyValue) {
names[index] = newPropertyValue
item.name = names.join('-')
}
}
item.vvSkuPropertyValueList.forEach((item: any) => {
if (
item.categoryPropertyName === categoryPropertyName &&
item.categoryPropertyValue === oldCategoryPropertyValue
) {
item.categoryPropertyValue = newPropertyValue
}
})
})
}

View File

@ -19,6 +19,7 @@ type CategoryDataMock = Category[]
export interface TSkuList {
serialNo: string
name: string
categoryPropertyNames: string
salePrice: string
imageUrl: string
stock: number
@ -40,9 +41,11 @@ export const generateSkuList = (adminCategoryData: CategoryDataMock): TSkuList[]
if (index === optionGroups.length) {
const serialNo = current.map((opt) => opt.id.toString()).join('-')
const name = current.map((opt) => opt.categoryPropertyValue).join('-')
const categoryPropertyNames = current.map((opt) => opt.categoryPropertyName).join('-')
result.push({
serialNo,
name,
categoryPropertyNames,
salePrice: '',
stock: 0,
originPrice: 0,
@ -116,8 +119,9 @@ export const usePropertyValue = () => {
item.name += '-' + inputValue.value
item.vvSkuPropertyValueList.push({ ...newData })
})
} else {
skuList.push(...generateAddSkuList(lineIndex, { ...newData }, adminCategoryData))
}
skuList.push(...generateAddSkuList(lineIndex, { ...newData }, adminCategoryData))
}
inputVisibles.value[lineIndex] = false
inputValue.value = ''
@ -130,18 +134,23 @@ export const usePropertyValue = () => {
adminCategoryData: CategoryDataMock
): TSkuList[] => {
const result: TSkuList[] = []
const optionGroups: Option[][] = adminCategoryData
.filter((_, index) => index !== lineIndex)
.map((category) => category.vvPropertyValueList)
const optionGroups: Option[][] = adminCategoryData.map((category, i) => {
if (i === lineIndex) {
return category.vvPropertyValueList.filter((item) => item.id === addData.id)
}
return 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.categoryPropertyValue).join('-')
const categoryPropertyNames = current.map((opt) => opt.categoryPropertyName).join('-')
result.push({
serialNo,
name,
categoryPropertyNames,
salePrice: '',
stock: 0,
originPrice: 0,
@ -156,7 +165,7 @@ export const usePropertyValue = () => {
}
}
combine(0, [addData])
combine(0, [])
//bug
console.warn('----- my data is result111: ', result)
return result

View File

@ -21,6 +21,7 @@
</template>
<script lang="ts" setup>
import { ElMessage } from 'element-plus'
import { initConfig } from './config'
import { useCommodityType } from './use-method'
@ -35,13 +36,17 @@ const onAddOrEdit = (type: string, row: any) => {
}
/** 复制 */
const onCopy = (row: any) => {}
const onCopy = async (row: any) => {
await api.commodity.copyCommodity.post!({ productId: row.id })
ElMessage.success('复制成功')
table.value.$onGetData(table.value)
}
//
const onAddHome = (row: any) => {
handleMessageBox({
msg: `是否加入首页?`,
success: api.commodity.addOrUpdateCommodity.post!,
success: api.commodity.changeCommodityInfo.post!,
data: { id: row.id, frontPage: 1 }
}).then(() => {
table.value.$onGetData(table.value)
@ -52,8 +57,8 @@ const onAddHome = (row: any) => {
const onDelete = (row: any) => {
handleMessageBox({
msg: `是否确认删除吗?`,
success: api.commodity.addOrUpdateCommodity.post!,
data: { id: row.id, status: 'delete' }
success: api.commodity.changeCommodityInfo.post!,
data: { id: row.id, isDelete: 1 }
}).then(() => {
table.value.$onGetData(table.value)
})