feat: 类目联调

This commit is contained in:
zc 2025-10-22 09:45:10 +08:00
parent 51bbd8c170
commit 3ff3977ed0
14 changed files with 308 additions and 374 deletions

View File

@ -1,9 +1,22 @@
const login = {
/**
*
*
*/
getCommodityList: ['/getCommodityList'], // 获取商品列表
delCommodity: ['/delCommodity'] // 删除商品
getHomeCommodityList: ['/front/manager/list'], // 获取首页商品数据
sortHomeCommodity: ['/front/manager/sort'], // 首页排序
/**
*
*/
getCategoryList: ['/category/list'], // 获取类目列表
sortCategory: ['/category/sort'], // 排序
updateCategory: ['/category/insertOrUpdate'], // 插入或更新
/**
*
*/
getCommodityList: ['/product/list'], // 获取商品列表
getCommodityDetail: ['/product/detail'], // 获取商品详情
addOrUpdateCommodity: [''] // 修改商品详情
}
export default login

View File

@ -0,0 +1,150 @@
<template>
<div class="resource-upload">
<div class="resource-upload__wrapper">
<el-button type="primary" class="relative !text-white">
选择文件
<input
ref="inputRef"
class="absolute top-0 left-0 cursor-pointer opacity-0"
type="file"
multiple
accept="image/*,video/*"
@change="onFilesChange"
/>
</el-button>
</div>
<div v-if="fileList.length" class="image-list">
<div v-for="item in fileList" :key="item.id" class="image-item">
<img
v-if="item.fileType === 'image'"
:src="item.url"
class="image-item-media"
alt="preview"
@click="handlePictureCardPreview(item)"
/>
<video
v-else
:src="item.url"
class="image-item-media"
@click="handlePictureCardPreview(item)"
/>
<div class="meta">
<span class="name" :title="item.name">{{ item.name }}</span>
<el-button link type="danger" size="small" @click="remove(item.id)">移除</el-button>
</div>
</div>
</div>
<el-dialog v-model="showReviewImgDialog" @close="onDialogClose">
<img w-full v-if="reviewImg.fileType === 'image'" :src="reviewImg.url" alt="Preview Image" />
<video v-else ref="reviewVideoRef" :src="reviewImg.url" autoplay />
</el-dialog>
</div>
</template>
<script setup lang="ts">
interface PreviewItem {
id: number
name: string
url: string
fileType: 'image' | 'video'
}
const emit = defineEmits<{
(e: 'change', files: File[]): void
}>()
const inputRef = ref<HTMLInputElement | null>(null)
const fileList = ref<PreviewItem[]>([])
const formData = ref([])
const onFilesChange = (e: Event) => {
const input = e.target as HTMLInputElement
const files = Array.from(input.files || [])
if (!files.length) return
files.forEach((file: any) => {
const url = URL.createObjectURL(file)
formData.value.push(file)
const fileType: 'image' | 'video' = file.type.startsWith('image') ? 'image' : 'video'
fileList.value.push({ id: Math.random(), name: file.name, url, fileType })
})
// reset input so selecting the same files again will still trigger change
if (inputRef.value) inputRef.value.value = ''
emit('change', files)
}
const remove = (id: number) => {
const idx = fileList.value.findIndex((x) => x.id === id)
if (idx === -1) return
const [removed] = fileList.value.splice(idx, 1)
formData.value.splice(idx, 1)
if (removed?.url && objectUrls.has(removed.url)) {
URL.revokeObjectURL(removed.url)
}
}
const showReviewImgDialog = ref(false)
const reviewImg = ref({ url: '', fileType: '' })
const handlePictureCardPreview = (item: { url: string; fileType: 'image' | 'video' }) => {
reviewImg.value = item
showReviewImgDialog.value = true
}
const reviewVideoRef = ref<HTMLVideoElement | null>(null)
const onDialogClose = () => {
const video = reviewVideoRef.value
if (video) {
try {
video.pause()
video.currentTime = 0
} catch (e) {
console.error(e)
}
}
}
</script>
<style scoped lang="scss">
.resource-upload {
.image-list {
margin-top: 12px;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 12px;
}
.image-item {
border: 1px solid #ebeef5;
border-radius: 6px;
background: #fff;
padding: 8px;
display: flex;
flex-direction: column;
gap: 6px;
.image-item-media {
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
border-radius: 4px;
background: #f5f7fa;
}
.meta {
display: flex;
align-items: center;
gap: 8px;
.name {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 12px;
color: #606266;
}
}
}
}
</style>

View File

@ -18,6 +18,12 @@ export const constantRoutes: Array<RouteRecordRaw> = [
name: 'errorpage',
component: () => import('@/views/home/errorpage.vue'),
meta: { title: '错误页', hidden: true }
},
{
path: '/goods/detail',
name: '/goods/detail',
component: () => import('@/views/goods/commodity/detail-dialog/index.vue'),
meta: { title: '商品详情', hidden: true }
}
]
},

View File

@ -2,11 +2,17 @@
<div id="category-tree" class="p-2 bg-white">
<p>类目展示</p>
<el-tree
ref="treeRef"
style="max-width: 600px"
:data="dataSource"
:props="defaultProps"
node-key="id"
draggable
default-expand-all
:render-after-expand="false"
check-strictly
:load="handleLoadNode"
lazy
highlight-current
@node-click="onClick"
:expand-on-click-node="false"
@node-drag-start="handleDragStart"
@node-drag-enter="handleDragEnter"
@ -17,7 +23,7 @@
>
<template #default="{ node, data }">
<div class="category-tree-node">
<span v-if="curId !== data.id">{{ data.label }}</span>
<span v-if="curId !== data.id">{{ data.name }}</span>
<el-input
ref="inputRef"
v-else
@ -28,7 +34,7 @@
@blur="handleInputCancel(node, data)"
/>
<div>
<el-button type="primary" link @click="edit(data)" size="small">编辑 </el-button>
<el-button type="primary" link @click="onEdit(data)" size="small">编辑 </el-button>
<el-button type="primary" link @click="append(data)" size="small"> 添加 </el-button>
<el-button type="danger" size="small" link @click="remove(node, data)">
删除
@ -43,7 +49,6 @@
<script lang="ts" setup>
import { ElButton } from 'element-plus'
import type { RenderContentContext } from 'element-plus'
import { dataSource } from './mock'
import {
handleDragStart,
handleDragEnter,
@ -55,17 +60,21 @@ import {
interface Tree {
id: number
label: string
name: string
children?: Tree[]
}
type Node = RenderContentContext['node']
type Data = RenderContentContext['data']
const curId = ref(NaN)
const curParentId = ref(NaN)
// const curId = ref(NaN)
const inputRef = ref()
const treeRef = ref()
const edit = (data: Data) => {
data.label2 = data.label
//
const onEdit = (data: Data) => {
data.label2 = data.name
curId.value = data.id
nextTick(() => {
inputRef.value.focus()
@ -75,11 +84,11 @@ const edit = (data: Data) => {
//
const handleInputConfirm = (data: any) => {
if (curId.value === Infinity) {
//
api.commodity.updateCategory.post!({ ...data, name: data.label2, parentId: curParentId.value })
} else {
//
api.commodity.updateCategory.post!({ ...data, name: data.label2 })
}
data.label = data.label2
data.name = data.label2
data.label2 = ''
curId.value = NaN
}
@ -92,25 +101,63 @@ const handleInputCancel = (node: Node, data: Data) => {
curId.value = NaN
}
const append = (data: Data) => {
//
const append = async (data: Data) => {
const node = treeRef.value.getNode(data.id)
curId.value = Infinity
const newChild = { id: curId.value, label2: 'test', children: [] }
if (!data.children) {
data.children = []
curParentId.value = data.id
const newChild = { id: curId.value, name: '', label2: 'test', hasChild: false }
if (!node.expanded) {
await api.commodity.getCategoryList.post!<any>({ parentId: data.id || 0 }).then((res) => {
res.data.forEach((item: any) => {
treeRef.value.append(
{
...item,
children: item.hasChild ? [] : undefined
},
data
)
})
})
}
data.children.push(newChild)
dataSource.value = [...dataSource.value]
treeRef.value.append(newChild, data)
node.expanded = true
nextTick(() => {
inputRef.value.focus()
})
}
//
const remove = (node: Node, data: Data) => {
const parent = node.parent
const children: Tree[] = parent?.data.children || parent?.data
const index = children.findIndex((d) => d.id === data.id)
children.splice(index, 1)
dataSource.value = [...dataSource.value]
}
const defaultProps = {
label: 'name',
children: 'children',
disabled: 'disabled',
isLeaf: (data: any) => {
return !data.hasChild
}
}
//
const handleLoadNode = (node, resolve) => {
api.commodity.getCategoryList.post!<any>({ parentId: node.data.id || 0 }).then((res) => {
resolve(
res.data.map((item: any) => ({
...item,
children: item.hasChild ? [] : undefined
}))
)
})
}
const onClick = (data) => {
console.warn('----- my data is data111: ', data)
}
</script>

View File

@ -2,14 +2,15 @@ const configData = ref()
export const initConfig = () => {
configData.value = pageConfig({
search: {
comTitle: { label: '标题', clearable: true },
title: { label: '标题', clearable: true },
comCategoryId: { label: '类目', option: [], clearable: true }
},
table: {
id: { label: '产品ID' },
comTitle: { label: '标题' },
sales: { label: '销量' },
price: { label: '价格' },
title: { label: '标题' },
showSaleCount: { label: '销量' },
showPromotionPrice: { label: '价格' },
showSalePrice: { label: '促销价' },
btn: {
types: ['primary', 'info', 'warning', 'success', 'danger'],
names: ['编辑', '复制', '加入首页', '上下架', '删除'],

View File

@ -4,10 +4,11 @@ export const initConfig = () => {
configData.value = pageConfig({
dialog: [
{
comTitle: { label: '商品标题' },
comCategoryId: { label: '类目', slot: 'comCategoryId' },
video: { label: '视频', slot: 'video', class: '!w-full' },
mainImg: { label: '主图', slot: 'mainImg', class: '!w-full' }
title: { label: '商品标题' },
comCategoryId: { label: 'app类目', slot: 'comCategoryId' },
mainImageUrl: { label: '主图', slot: 'file' },
videoUrl: { label: '视频', slot: 'file' },
subImageUrl: { label: '副图', slot: 'file' }
}
]
})

View File

@ -9,11 +9,8 @@
:data="categoryOptions"
></el-tree-select>
</template>
<template #video>
<span>sdfs</span>
</template>
<template #mainImg>
<p>123</p>
<template #file>
<resource-upload v-model="fileList" />
</template>
</form-wrap>
</el-card>
@ -72,9 +69,15 @@ import { categoryDataMock, categoryOptionsMock } from './mock'
import { generateTargetData, useCategoryAddOption, colors, useDrag } from './use-method'
import categoryConfig from './category-config.vue'
import wanEditor from './editor.vue'
import resourceUpload from '@/components/ResourceUpload/index.vue'
const route = useRoute()
const init = async () => {
await api.commodity.getCommodityDetail.post!({ productId: route.query.id })
}
const { $dialog, dialog1 } = toRefs(initConfig().value!)
$dialog.value.config = dialog1.value
$dialog.value['item-width'] = '100%'
const categoryOptions = ref(categoryOptionsMock)
const categoryData = ref(categoryDataMock)
@ -88,9 +91,16 @@ console.warn('----- my data is list.value: ', list.value)
const htmlContent = ref('自定义')
const fileList = ref([])
const onGetResource = (v) => {
console.warn('----- my data is v: ', v)
}
const onSubmit = () => {
console.warn('----- my data is htmlContent: ', htmlContent.value)
}
init()
</script>
<style scoped lang="scss">

View File

@ -24,21 +24,47 @@
import { initConfig } from './config'
import { useCommodityType } from './use-method'
const router = useRouter()
/** 新增&编辑 */
const onAddOrEdit = (type: string, row: any) => {}
const onAddOrEdit = (type: string, row: any) => {
router.push({ name: '/goods/detail', query: { type, id: row.id } })
}
/** 复制 */
const onCopy = (row: any) => {}
//
const onAddHome = (row: any) => {}
const onAddHome = (row: any) => {
handleMessageBox({
msg: `是否加入首页?`,
success: api.commodity.addOrUpdateCommodity.post!,
data: { id: row.id, frontPage: 1 }
}).then(() => {
table.value.$onGetData(table.value)
})
}
//
const onDelete = (row: any) => {
handleMessageBox({
msg: `是否确认删除吗?`,
success: api.commodity.delCommodity.post!,
data: { id: row.id }
success: api.commodity.addOrUpdateCommodity.post!,
data: { id: row.id, status: 'delete' }
}).then(() => {
table.value.$onGetData(table.value)
})
}
//
const onChangeStatus = (row: any) => {
const type = row.status === 'online' ? '下架' : '上架'
handleMessageBox({
msg: `是否确认${type}吗?`,
success: api.commodity.addOrUpdateCommodity.post!,
data: {
id: row.id,
status: row.status === 'online' ? 'down' : 'online'
}
}).then(() => {
table.value.$onGetData(table.value)
})
@ -50,6 +76,7 @@ const { search, table } = handleInit(ConfigData, api.commodity.getCommodityList.
onAddOrEdit.bind(null, 'edit'),
onCopy,
onAddHome,
onChangeStatus,
onDelete
])
const { commodityType, onChangeCommodityType, commodityTypeOptions } = useCommodityType(

View File

@ -37,7 +37,7 @@ const onDelete = (row: any) => {
/** 初始化页面 */
const ConfigData = initConfig()
const { search, table } = handleInit(ConfigData, api.commodity.getCommodityList.post, [
const { search, table } = handleInit(ConfigData, api.commodity.getHomeCommodityList.post, [
onAddOrEdit.bind(null, 'edit'),
onCopy,
onAddHome,

View File

@ -1,131 +0,0 @@
<template>
<div id="category-tree" class="p-2 bg-white">
<p>资源展示</p>
<el-tree
style="max-width: 600px"
:data="transformedDataSource"
node-key="id"
draggable
show-checkbox
:default-expanded-keys="[2, 3]"
:default-checked-keys="[5]"
default-expand-all
:expand-on-click-node="false"
@node-drag-start="handleDragStart"
@node-drag-enter="handleDragEnter"
@node-drag-leave="handleDragLeave"
@node-drag-over="handleDragOver"
@node-drag-end="handleDragEnd"
@node-drop="handleDrop"
>
<template #default="{ node, data }">
<div class="category-tree-node">
<span v-if="curId !== data.id">{{ data.fileName }}</span>
<el-input
ref="inputRef"
v-else
v-model="data.label2"
size="small"
class="w-40"
@keyup.enter="handleInputConfirm(data)"
@blur="handleInputCancel(node, data)"
/>
<div>
<el-button type="primary" link @click="edit(data)" size="small">编辑 </el-button>
<el-button type="primary" link @click="append(data)" size="small"> 添加 </el-button>
<el-button type="danger" size="small" link @click="remove(node, data)">
删除
</el-button>
</div>
</div>
</template>
</el-tree>
</div>
</template>
<script lang="ts" setup>
import { ElButton } from 'element-plus'
import type { RenderContentContext } from 'element-plus'
import { transformedDataSource } from './mock'
import {
handleDragStart,
handleDragEnter,
handleDragOver,
handleDragLeave,
handleDragEnd,
handleDrop
} from './use-drag'
interface Tree {
id: number
label: string
children?: Tree[]
}
type Node = RenderContentContext['node']
type Data = RenderContentContext['data']
const curId = ref(NaN)
const inputRef = ref()
const edit = (data: Data) => {
data.label2 = data.label
curId.value = data.id
nextTick(() => {
inputRef.value.focus()
})
}
//
const handleInputConfirm = (data: any) => {
if (curId.value === Infinity) {
//
} else {
//
}
data.label = data.label2
data.label2 = ''
curId.value = NaN
}
//
const handleInputCancel = (node: Node, data: Data) => {
if (data.id === Infinity) {
remove(node, data)
}
curId.value = NaN
}
const append = (data: Data) => {
curId.value = Infinity
const newChild = { id: curId.value, label2: 'test', children: [] }
if (!data.children) {
data.children = []
}
data.children.push(newChild)
transformedDataSource.value = [...transformedDataSource.value]
nextTick(() => {
inputRef.value.focus()
})
}
const remove = (node: Node, data: Data) => {
const parent = node.parent
const children: Tree[] = parent?.data.children || parent?.data
const index = children.findIndex((d) => d.id === data.id)
children.splice(index, 1)
transformedDataSource.value = [...transformedDataSource.value]
}
</script>
<style lang="scss" scoped>
#category-tree {
.category-tree-node {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
padding-right: 8px;
}
}
</style>

View File

@ -1,162 +0,0 @@
interface Tree {
id: number
resourceUrl: string
type: string
fileName: string
defaultSort: number
children?: Tree[]
}
interface OriginDataItem {
id: number
resourceUrl: string
fileName: string
type: string
defaultSort: number
parentId?: number
}
export const originData: OriginDataItem[] = [
{
id: 12,
resourceUrl: 'bbb',
type: 'image',
fileName: '张三',
parentId: 10,
defaultSort: 2
},
{
id: 78,
resourceUrl: 'bbb',
type: 'image',
fileName: '张三',
parentId: 77,
defaultSort: 1
},
{
id: 80,
resourceUrl: 'bbb',
type: 'image',
fileName: '张三',
parentId: 78,
defaultSort: 4
},
{
id: 82,
resourceUrl: 'bbb',
type: 'image',
fileName: '张三',
parentId: 81,
defaultSort: 1
},
{
id: 84,
resourceUrl: 'bbb',
type: 'image',
fileName: '张三',
parentId: 83,
defaultSort: 1
},
{
id: 86,
resourceUrl: 'bbb',
type: 'image',
fileName: '张三',
parentId: 85,
defaultSort: 1
},
{
id: 88,
resourceUrl: 'bbb',
type: 'image',
fileName: '张三',
parentId: 87,
defaultSort: 1
},
{
id: 90,
resourceUrl: 'bbb',
type: 'image',
fileName: '张三',
parentId: 89,
defaultSort: 1
},
{
id: 92,
resourceUrl: 'bbb',
type: 'image',
fileName: '张三',
parentId: 91,
defaultSort: 1
},
{
id: 94,
resourceUrl: 'bbb',
type: 'image',
fileName: '张三',
parentId: 93,
defaultSort: 1
}
]
/**
* originData dataSource
* @param data
* @returns
*/
export function transformToTreeData(data: OriginDataItem[]): Tree[] {
// 创建映射表,用于快速查找
const itemMap = new Map<number, Tree>()
const result: Tree[] = []
// 首先创建所有节点
data.forEach((item) => {
itemMap.set(item.id, {
id: item.id,
resourceUrl: item.resourceUrl,
type: item.type,
fileName: item.fileName,
defaultSort: item.defaultSort,
children: []
})
})
// 构建树形结构
data.forEach((item) => {
const treeNode = itemMap.get(item.id)!
if (item.parentId) {
const parentNode = itemMap.get(item.parentId)
if (parentNode) {
parentNode.children!.push(treeNode)
} else {
// 如果找不到父节点,作为根节点处理
result.push(treeNode)
}
} else {
// 没有父节点,作为根节点
result.push(treeNode)
}
})
// 递归排序函数
function sortChildren(node: Tree) {
if (node.children && node.children.length > 0) {
// 重新排列 children
node.children = node.children.sort((a, b) => a.defaultSort - b.defaultSort)
// 递归处理子节点
node.children.forEach((child) => sortChildren(child))
}
}
// 重新排列根节点
const sortedResult = result.sort((a, b) => a.defaultSort - b.defaultSort)
// 递归排序所有子节点
sortedResult.forEach((node) => sortChildren(node))
return sortedResult
}
// 使用示例
export const transformedDataSource = ref(transformToTreeData(originData))

View File

@ -1,39 +0,0 @@
import type { DragEvents } from 'element-plus/es/components/tree/src/model/useDragNode'
import type { NodeDropType, RenderContentContext } from 'element-plus'
type Node = RenderContentContext['node']
// 开始拖拽
export const handleDragStart = (node: Node, ev: DragEvents) => {
console.log('drag start', node)
}
// 拖拽进入其他节点时触发的事件
export const handleDragEnter = (draggingNode: Node, dropNode: Node, ev: DragEvents) => {
console.log('tree drag enter:', dropNode.label)
}
// 拖拽离开某个节点时触发的事件
export const handleDragLeave = (draggingNode: Node, dropNode: Node, ev: DragEvents) => {
console.log('tree drag leave:', dropNode.label)
}
// 在拖拽节点时触发的事件(类似浏览器的 mouseover 事件)
export const handleDragOver = (draggingNode: Node, dropNode: Node, ev: DragEvents) => {
console.log('tree drag over:', dropNode.label)
}
// 拖拽结束时(可能未成功)触发的事件
export const handleDragEnd = (
draggingNode: Node,
dropNode: Node,
dropType: NodeDropType,
ev: DragEvents
) => {
console.log('tree drag end:', dropNode && dropNode.label, dropType)
}
// 拖拽成功完成时触发的事件
export const handleDrop = (
draggingNode: Node,
dropNode: Node,
dropType: NodeDropType,
ev: DragEvents
) => {
console.log('tree drop:', dropNode.label, dropType)
}

View File

@ -11,7 +11,7 @@ export const initConfig = () => {
pathname: {
label: '路径',
width: 160,
formatter: (row: any) => row.locations.map((item: any) => item.fileName).join('/')
formatter: (row: any) => row.locations?.map((item: any) => item.fileName).join('/')
},
modifyTime: {
label: '修改时间',

View File

@ -1,6 +1,6 @@
<template>
<div>
<search-module :search="search" :table="table">
<search-module :search="search" :table="table" @search="onSearch">
<template #btn>
<el-button @click="onAddFolder" type="success">新增文件夹</el-button>
<el-button @click="onAddFiles" type="success">添加文件</el-button>
@ -136,7 +136,8 @@ const onAddFolder = () => {
parentId: search.value.$data.parentId,
defaultSort: 0,
type: 'file',
fileName: ''
fileName: '',
modifyTime: new Date()
})
}
@ -314,6 +315,16 @@ const initDraggable = () => {
}, 100)
}
const onSearch = () => {
table.value.$onGetData(
table.value,
1,
search.value.$data.fileName
? { parentId: null, fileName: search.value.$data.fileName }
: search.value.$data
)
}
//
watch(
() => table.value.$data,