feate:添加分组标签
Build and Deploy Vue3 / build (push) Successful in 4m59s
Build and Deploy Vue3 / deploy (push) Successful in 1m23s

This commit is contained in:
2026-01-29 17:43:45 +08:00
parent 127d54eaa6
commit 043be60f4f
5 changed files with 206 additions and 46 deletions
+192 -35
View File
@@ -45,6 +45,14 @@
<el-button :type="viewMode === 'list' ? 'primary' : 'default'" @click="viewMode = 'list'">
列表视图
</el-button>
<template v-if="viewMode === 'tree'">
<el-button type="info" plain @click="expandAll">
<el-icon><ArrowDown /></el-icon>全部展开
</el-button>
<el-button type="info" plain @click="collapseAll">
<el-icon><ArrowUp /></el-icon>全部收起
</el-button>
</template>
</div>
</div>
</div>
@@ -66,18 +74,17 @@
<!-- 树形表格视图 -->
<el-table
v-else-if="viewMode === 'tree'"
:data="treeData"
ref="treeTableRef"
:data="flattenedTreeData"
style="width: 100%"
row-key="id"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
default-expand-all
>
<el-table-column prop="name" label="分组名称" min-width="250">
<template #default="{ row }">
<div class="group-name-cell">
<div class="group-name-cell" :style="{ paddingLeft: (row.level - 1) * 24 + 'px' }">
<el-avatar v-if="row.cover" :size="32" :src="row.cover" />
<el-avatar v-else :size="32" style="background: #409eff">
<el-avatar v-else :size="32" :style="{ background: getLevelColor(row.level) }">
<el-icon><Folder /></el-icon>
</el-avatar>
<span class="group-name">{{ row.name }}</span>
@@ -103,12 +110,20 @@
/>
</template>
</el-table-column>
<el-table-column label="操作" width="220" fixed="right">
<el-table-column label="操作" width="320" fixed="right">
<template #default="{ row }">
<div class="action-buttons">
<el-button type="success" link @click="handleAdd(row)" v-if="row.level < 3">添加子级</el-button>
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
<el-button
v-if="row.children && row.children.length > 0"
type="info"
link
@click="toggleRowExpand(row)"
>
{{ isRowExpanded(row) ? '收起' : '展开' }}
</el-button>
</div>
</template>
</el-table-column>
@@ -259,18 +274,19 @@
<div class="recommend-user-selector">
<el-input
:model-value="selectedParentName"
placeholder="无父级(顶级分组)"
:placeholder="isAddingChild ? '已自动设置父级分组' : '无父级(顶级分组)'"
readonly
@click="showParentSelector = true"
:disabled="isAddingChild"
@click="!isAddingChild && (showParentSelector = true)"
>
<template #append>
<el-button @click="showParentSelector = true">
<el-button @click="!isAddingChild && (showParentSelector = true)" :disabled="isAddingChild">
<el-icon><Search /></el-icon>
</el-button>
</template>
</el-input>
<el-button
v-if="groupForm.parent_id"
v-if="groupForm.parent_id && !isAddingChild"
type="danger"
link
@click="clearParent"
@@ -279,6 +295,9 @@
清除
</el-button>
</div>
<div v-if="isAddingChild" class="form-tip">
添加子级时自动继承父级分组不可修改
</div>
</el-form-item>
<el-form-item label="分组层级" prop="level">
<el-select v-model="groupForm.level" placeholder="请选择层级" style="width: 100%" disabled>
@@ -464,7 +483,7 @@
<script setup>
import { ref, reactive, computed, onMounted, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Refresh, Search, Folder } from '@element-plus/icons-vue'
import { Plus, Refresh, Search, Folder, ArrowDown, ArrowUp } from '@element-plus/icons-vue'
import {
getProductGroupList,
createProductGroup,
@@ -488,7 +507,7 @@ const viewMode = ref('tree')
// 查询参数
const queryParams = reactive({
page: 1,
count: 10, // 列表视图分页
count: 100, // 列表视图分页
level: undefined,
disable: undefined,
key: ''
@@ -498,10 +517,10 @@ const queryParams = reactive({
watch(viewMode, (newVal) => {
if (newVal === 'tree') {
// 树形视图获取全部数据
queryParams.count = 1000
queryParams.count = 100
} else {
// 列表视图使用分页
queryParams.count = 10
queryParams.count = 100
queryParams.page = 1
}
fetchGroupList()
@@ -538,8 +557,58 @@ const dialogTitle = computed(() => {
}
return '编辑商品分组'
})
// 是否正在添加子级分组(用于禁用父级选择)
const isAddingChild = computed(() => {
return dialogType.value === 'add' && groupForm.parent_id
})
const groupFormRef = ref(null)
// 树形表格展开控制
const treeTableRef = ref(null)
const expandedRowIds = ref(new Set()) // 记录展开的行ID
// 切换单行展开状态
const toggleRowExpand = (row) => {
const newSet = new Set(expandedRowIds.value)
if (newSet.has(row.id)) {
newSet.delete(row.id)
} else {
newSet.add(row.id)
}
expandedRowIds.value = newSet
}
// 判断行是否展开
const isRowExpanded = (row) => {
return expandedRowIds.value.has(row.id)
}
// 全部展开
const expandAll = () => {
const newSet = new Set()
const addAllWithChildren = (rows) => {
rows.forEach(row => {
if (row.children && row.children.length > 0) {
newSet.add(row.id)
addAllWithChildren(row.children)
}
})
}
addAllWithChildren(treeData.value)
expandedRowIds.value = newSet
}
// 全部收起
const collapseAll = () => {
expandedRowIds.value = new Set()
}
// 初始化展开状态(默认全部展开)
const initExpandedState = () => {
expandAll()
}
// 父级选择相关
const showParentSelector = ref(false)
const showCoverSelector = ref(false)
@@ -564,10 +633,21 @@ const tagOptionsForSelector = ref([])
const selectedTag = ref(null)
const allTagOptions = ref([]) // 存储所有标签用于显示名称
// 存储当前编辑行的标签信息(用于在allTagOptions未加载时显示)
const currentEditTag = ref(null)
const selectedTagName = computed(() => {
if (groupForm.tag_id) {
// 优先从allTagOptions中查找
const tag = allTagOptions.value.find(t => t.id === groupForm.tag_id)
return tag ? `${tag.name} (ID: ${tag.id})` : `标签ID: ${groupForm.tag_id}`
if (tag) {
return `${tag.name} (ID: ${tag.id})`
}
// 如果没找到,使用当前编辑行的标签信息
if (currentEditTag.value && currentEditTag.value.id === groupForm.tag_id) {
return `${currentEditTag.value.name} (ID: ${currentEditTag.value.id})`
}
return `标签ID: ${groupForm.tag_id}`
}
return ''
})
@@ -607,7 +687,7 @@ const fetchTagOptionsForSelector = async () => {
// 初始化获取所有标签
const fetchAllTagOptions = async () => {
try {
const res = await getProductGroupTagList({ page: 1, count: 1000 })
const res = await getProductGroupTagList({ page: 1, count: 100 })
if (res.data.code === 200) {
const data = res.data.data
if (Array.isArray(data)) {
@@ -672,6 +752,24 @@ const treeData = computed(() => {
return roots
})
// 将树形数据扁平化(根据展开状态)
const flattenedTreeData = computed(() => {
const result = []
const flatten = (nodes) => {
nodes.forEach(node => {
result.push(node)
// 如果有子节点且当前节点是展开状态,则继续展开
if (node.children && node.children.length > 0 && isRowExpanded(node)) {
flatten(node.children)
}
})
}
flatten(treeData.value)
return result
})
// 获取层级文本
const getLevelText = (level) => {
const map = { 1: '一级', 2: '二级', 3: '三级' }
@@ -684,6 +782,12 @@ const getLevelType = (level) => {
return map[level] || 'info'
}
// 获取层级颜色(用于头像背景)
const getLevelColor = (level) => {
const map = { 1: '#409eff', 2: '#67c23a', 3: '#e6a23c' }
return map[level] || '#909399'
}
// 获取父级名称
const getParentName = (parentId) => {
const parent = allGroupList.value.find(g => g.id === parentId)
@@ -704,7 +808,13 @@ const fetchGroupList = async () => {
const data = res.data.data.data || []
allGroupList.value = data
groupList.value = data
total.value = res.data.data.total || data.length
total.value = res.data.data.all_count || data.length
// 初始化展开状态(默认全部展开)
// 使用nextTick确保treeData已更新
setTimeout(() => {
initExpandedState()
}, 0)
}
} catch (error) {
ElMessage.error('获取商品分组列表失败')
@@ -736,10 +846,16 @@ const handleCurrentChange = (page) => {
// 新增商品分组
const handleAdd = (parentRow) => {
dialogType.value = 'add'
dialogVisible.value = true
// 先重置表单,再设置值
groupFormRef.value?.resetFields()
if (parentRow) {
// 添加子级
// 添加子级 - 自动获取父级分组信息
// 继承父级标签
const parentTagId = parentRow.tag?.id || parentRow.tagId
currentEditTag.value = parentRow.tag || null // 保存父级标签信息用于显示
Object.assign(groupForm, {
id: undefined,
name: '',
@@ -748,10 +864,12 @@ const handleAdd = (parentRow) => {
level: parentRow.level + 1,
parent_id: parentRow.id,
cover_id: undefined,
tag_id: undefined
tag_id: parentTagId || undefined
})
console.log('添加子级,父级信息:', parentRow.name, 'ID:', parentRow.id, 'Level:', parentRow.level, '标签:', parentRow.tag)
} else {
// 添加顶级
currentEditTag.value = null
Object.assign(groupForm, {
id: undefined,
name: '',
@@ -763,23 +881,36 @@ const handleAdd = (parentRow) => {
tag_id: undefined
})
}
groupFormRef.value?.resetFields()
dialogVisible.value = true
}
// 编辑商品分组
const handleEdit = (row) => {
dialogType.value = 'edit'
dialogVisible.value = true
// 保存当前行的标签信息(用于显示)
currentEditTag.value = row.tag || null
// 处理数据映射
// API返回: cover(URL), tag(对象{id, name}), parentId, coverId
// 表单需要: cover_id, tag_id, parent_id
// disable: true 表示禁用,false 表示启用
Object.assign(groupForm, {
id: row.id,
name: row.name,
note: row.note,
disable: row.disable,
note: row.note || '',
disable: row.disable === true, // 确保是布尔值
level: row.level || 1,
parent_id: row.parentId,
cover_id: row.coverId,
tag_id: row.tagId
parent_id: row.parentId || undefined,
cover_id: row.coverId || undefined,
tag_id: row.tag?.id || row.tagId || undefined
})
dialogVisible.value = true
console.log('编辑分组数据:', row, '原始disable:', row.disable)
console.log('表单数据:', groupForm, '表单disable:', groupForm.disable)
}
// 清除父级
@@ -865,18 +996,34 @@ const submitForm = () => {
const submitData = {
name: groupForm.name.trim(),
note: groupForm.note || '',
disable: groupForm.disable,
level: String(groupForm.level || 1)
disable: groupForm.disable
}
// 只有有值时才传递这些字段,避免外键约束错误
// 处理 level 和 parent_id
// 如果有 parent_id,则设置 level 为父级+1,否则为顶级(level=1)
if (groupForm.parent_id) {
submitData.parent_id = groupForm.parent_id
submitData.parent_id = Number(groupForm.parent_id)
// 根据父级计算level
const parentGroup = allGroupList.value.find(g => g.id === groupForm.parent_id)
console.log('父级分组:', parentGroup)
if (parentGroup && typeof parentGroup.level === 'number') {
submitData.level = String(parentGroup.level + 1)
} else {
// 如果找不到父级或父级没有level,使用表单中的level
submitData.level = String(groupForm.level || 2)
}
console.log('计算得到的level:', submitData.level, '父级level:', parentGroup?.level)
} else {
// 顶级分组,level=1,不传parent_id
submitData.level = '1'
}
// 只有有值时才传递这些字段,避免外键约束错误
if (groupForm.cover_id) {
submitData.cover_id = groupForm.cover_id
submitData.cover_id = Number(groupForm.cover_id)
}
if (groupForm.tag_id) {
submitData.tag_id = groupForm.tag_id
submitData.tag_id = Number(groupForm.tag_id)
}
console.log('提交数据:', submitData)
let res
@@ -901,7 +1048,7 @@ const submitForm = () => {
// ==================== 分组标签管理 ====================
const tagQueryParams = reactive({
page: 1,
count: 10,
count: 100,
key: ''
})
@@ -1126,8 +1273,13 @@ onMounted(() => {
.action-buttons {
display: flex;
gap: 8px;
gap: 4px;
align-items: center;
flex-wrap: nowrap;
}
.action-buttons .el-button {
padding: 4px 8px;
}
.pagination {
@@ -1177,6 +1329,11 @@ onMounted(() => {
color: #2c3e50;
}
/* 隐藏el-table自带的树形展开图标 */
:deep(.el-table__expand-icon) {
display: none !important;
}
:deep(.el-table__header) {
background: #f8f9fa;
}
+7 -7
View File
@@ -638,14 +638,15 @@ const coverPreviewUrl = ref('')
const fetchProductList = async () => {
loading.value = true
try {
const res = await getProductList(queryParams)
// 添加 delete=false 参数,让后端只返回未删除的数据
const params = { ...queryParams, delete: false }
const res = await getProductList(params)
if (res.data.code === 200) {
const allData = res.data.data.data || []
// 过滤掉已删除的数据
productList.value = allData.filter(item => item.delete == false)
// 计算未删除数据的总数(API返回的all_count包含已删除的,需要减去已删除的数量)
const deletedCount = allData.filter(item => item.delete == true).length
total.value = (res.data.data.data.length || 0) - deletedCount
// 数据已经是未删除的,直接使用
productList.value = allData
// 使用后端返回的总数
total.value = res.data.data.all_count || allData.length
// 异步获取所有商品的封面图片
const imagePromises = productList.value.map(async (item) => {
@@ -665,7 +666,6 @@ const fetchProductList = async () => {
// 等待所有图片加载完成
await Promise.all(imagePromises)
console.log('productList', productList.value)
}
} catch (error) {
ElMessage.error('获取商品列表失败')