1270 lines
35 KiB
Vue
1270 lines
35 KiB
Vue
<template>
|
|
<div class="product-group-container">
|
|
<!-- 主容器 -->
|
|
<el-card class="main-container" shadow="never">
|
|
<!-- Tab切换 -->
|
|
<el-tabs v-model="activeTab" class="main-tabs">
|
|
<el-tab-pane label="商品分组" name="group">
|
|
<!-- 操作栏 -->
|
|
<div class="filter-section">
|
|
<div class="filter-content">
|
|
<el-form :inline="true" :model="queryParams" class="search-form">
|
|
<el-form-item label="层级筛选">
|
|
<el-select v-model="queryParams.level" placeholder="全部层级" clearable style="width: 120px" @change="fetchGroupList">
|
|
<el-option label="一级" :value="1" />
|
|
<el-option label="二级" :value="2" />
|
|
<el-option label="三级" :value="3" />
|
|
</el-select>
|
|
</el-form-item>
|
|
<el-form-item label="状态筛选">
|
|
<el-select v-model="queryParams.disable" placeholder="全部状态" clearable style="width: 120px" @change="fetchGroupList">
|
|
<el-option label="启用" :value="false" />
|
|
<el-option label="禁用" :value="true" />
|
|
</el-select>
|
|
</el-form-item>
|
|
<el-form-item label="关键词">
|
|
<el-input v-model="queryParams.key" placeholder="搜索分组名称" clearable style="width: 180px" @keyup.enter="fetchGroupList" />
|
|
</el-form-item>
|
|
<el-form-item>
|
|
<el-button type="primary" @click="fetchGroupList">
|
|
<el-icon><Search /></el-icon>查询
|
|
</el-button>
|
|
<el-button @click="resetQuery">重置</el-button>
|
|
</el-form-item>
|
|
</el-form>
|
|
<div class="action-bar">
|
|
<el-button type="primary" @click="handleAdd(null)">
|
|
<el-icon><Plus /></el-icon>新增顶级分组
|
|
</el-button>
|
|
<el-button type="success" @click="fetchGroupList">
|
|
<el-icon><Refresh /></el-icon>刷新
|
|
</el-button>
|
|
<el-button :type="viewMode === 'tree' ? 'primary' : 'default'" @click="viewMode = 'tree'">
|
|
树形视图
|
|
</el-button>
|
|
<el-button :type="viewMode === 'list' ? 'primary' : 'default'" @click="viewMode = 'list'">
|
|
列表视图
|
|
</el-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 商品分组列表 -->
|
|
<div class="table-section">
|
|
<!-- 骨架屏 -->
|
|
<div v-if="loading" class="skeleton-container">
|
|
<div v-for="i in 5" :key="i" class="skeleton-row">
|
|
<div class="skeleton-cell skeleton-id"></div>
|
|
<div class="skeleton-cell skeleton-name"></div>
|
|
<div class="skeleton-cell skeleton-note"></div>
|
|
<div class="skeleton-cell skeleton-level"></div>
|
|
<div class="skeleton-cell skeleton-status"></div>
|
|
<div class="skeleton-cell skeleton-action"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 树形表格视图 -->
|
|
<el-table
|
|
v-else-if="viewMode === 'tree'"
|
|
:data="treeData"
|
|
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">
|
|
<el-avatar v-if="row.cover" :size="32" :src="row.cover" />
|
|
<el-avatar v-else :size="32" style="background: #409eff">
|
|
<el-icon><Folder /></el-icon>
|
|
</el-avatar>
|
|
<span class="group-name">{{ row.name }}</span>
|
|
</div>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="id" label="ID" width="80" />
|
|
<el-table-column label="层级" width="100">
|
|
<template #default="{ row }">
|
|
<el-tag :type="getLevelType(row.level)" size="small">
|
|
{{ getLevelText(row.level) }}
|
|
</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="note" label="备注" min-width="200" show-overflow-tooltip />
|
|
<el-table-column label="状态" width="100">
|
|
<template #default="{ row }">
|
|
<el-switch
|
|
v-model="row.disable"
|
|
:active-value="false"
|
|
:inactive-value="true"
|
|
@change="(val) => handleStatusChange(row, val)"
|
|
/>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="操作" width="220" 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>
|
|
</div>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
|
|
<!-- 列表视图 -->
|
|
<el-table
|
|
v-else
|
|
:data="groupList"
|
|
style="width: 100%"
|
|
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
|
>
|
|
<el-table-column prop="id" label="分组ID" width="100" />
|
|
<el-table-column prop="name" label="分组名称" min-width="200">
|
|
<template #default="{ row }">
|
|
<div class="group-name-cell">
|
|
<el-avatar v-if="row.cover" :size="32" :src="row.cover" />
|
|
<span class="group-name">{{ row.name }}</span>
|
|
</div>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="层级" width="100">
|
|
<template #default="{ row }">
|
|
<el-tag :type="getLevelType(row.level)" size="small">
|
|
{{ getLevelText(row.level) }}
|
|
</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="父级分组" width="150">
|
|
<template #default="{ row }">
|
|
<span v-if="row.parentId">{{ getParentName(row.parentId) }}</span>
|
|
<span v-else class="text-muted">-</span>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="note" label="备注" min-width="200" show-overflow-tooltip />
|
|
<el-table-column label="状态" width="100">
|
|
<template #default="{ row }">
|
|
<el-switch
|
|
v-model="row.disable"
|
|
:active-value="false"
|
|
:inactive-value="true"
|
|
@change="(val) => handleStatusChange(row, val)"
|
|
/>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="操作" width="180" fixed="right">
|
|
<template #default="{ row }">
|
|
<div class="action-buttons">
|
|
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
|
|
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
|
|
</div>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
|
|
<!-- 分页 -->
|
|
<el-pagination
|
|
v-if="viewMode === 'list'"
|
|
v-model:current-page="queryParams.page"
|
|
v-model:page-size="queryParams.count"
|
|
:page-sizes="[10, 20, 50, 100]"
|
|
layout="total, sizes, prev, pager, next, jumper"
|
|
:total="total"
|
|
@size-change="handleSizeChange"
|
|
@current-change="handleCurrentChange"
|
|
background
|
|
class="pagination"
|
|
/>
|
|
</div>
|
|
</el-tab-pane>
|
|
|
|
<!-- 分组标签管理 -->
|
|
<el-tab-pane label="分组标签" name="tag">
|
|
<div class="filter-section">
|
|
<div class="filter-content">
|
|
<el-form :inline="true" :model="tagQueryParams" class="search-form">
|
|
<el-form-item label="关键词">
|
|
<el-input v-model="tagQueryParams.key" placeholder="搜索标签名称" clearable style="width: 180px" @keyup.enter="fetchTagList" />
|
|
</el-form-item>
|
|
<el-form-item>
|
|
<el-button type="primary" @click="fetchTagList">
|
|
<el-icon><Search /></el-icon>查询
|
|
</el-button>
|
|
<el-button @click="resetTagQuery">重置</el-button>
|
|
</el-form-item>
|
|
</el-form>
|
|
<div class="action-bar">
|
|
<el-button type="primary" @click="handleAddTag">
|
|
<el-icon><Plus /></el-icon>新增标签
|
|
</el-button>
|
|
<el-button type="success" @click="fetchTagList">
|
|
<el-icon><Refresh /></el-icon>刷新
|
|
</el-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="table-section">
|
|
<el-table
|
|
v-loading="tagLoading"
|
|
:data="tagList"
|
|
style="width: 100%"
|
|
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
|
>
|
|
<el-table-column prop="id" label="标签ID" width="100" />
|
|
<el-table-column prop="name" label="标签名称" min-width="200" />
|
|
<el-table-column label="操作" width="180" fixed="right">
|
|
<template #default="{ row }">
|
|
<div class="action-buttons">
|
|
<el-button type="primary" link @click="handleEditTag(row)">编辑</el-button>
|
|
<el-button type="danger" link @click="handleDeleteTag(row)">删除</el-button>
|
|
</div>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
<el-pagination
|
|
v-model:current-page="tagQueryParams.page"
|
|
v-model:page-size="tagQueryParams.count"
|
|
:page-sizes="[10, 20, 50, 100]"
|
|
layout="total, sizes, prev, pager, next, jumper"
|
|
:total="tagTotal"
|
|
@size-change="handleTagSizeChange"
|
|
@current-change="handleTagCurrentChange"
|
|
background
|
|
class="pagination"
|
|
/>
|
|
</div>
|
|
</el-tab-pane>
|
|
</el-tabs>
|
|
</el-card>
|
|
|
|
<!-- 商品分组表单对话框 -->
|
|
<el-dialog
|
|
v-model="dialogVisible"
|
|
:title="dialogTitle"
|
|
width="600px"
|
|
append-to-body
|
|
>
|
|
<el-form
|
|
ref="groupFormRef"
|
|
:model="groupForm"
|
|
:rules="groupRules"
|
|
label-width="100px"
|
|
>
|
|
<el-form-item label="分组名称" prop="name">
|
|
<el-input v-model="groupForm.name" placeholder="请输入分组名称" />
|
|
</el-form-item>
|
|
<el-form-item label="父级分组" prop="parent_id">
|
|
<div class="recommend-user-selector">
|
|
<el-input
|
|
:model-value="selectedParentName"
|
|
placeholder="无父级(顶级分组)"
|
|
readonly
|
|
@click="showParentSelector = true"
|
|
>
|
|
<template #append>
|
|
<el-button @click="showParentSelector = true">
|
|
<el-icon><Search /></el-icon>
|
|
</el-button>
|
|
</template>
|
|
</el-input>
|
|
<el-button
|
|
v-if="groupForm.parent_id"
|
|
type="danger"
|
|
link
|
|
@click="clearParent"
|
|
class="clear-btn"
|
|
>
|
|
清除
|
|
</el-button>
|
|
</div>
|
|
</el-form-item>
|
|
<el-form-item label="分组层级" prop="level">
|
|
<el-select v-model="groupForm.level" placeholder="请选择层级" style="width: 100%" disabled>
|
|
<el-option label="一级" :value="1" />
|
|
<el-option label="二级" :value="2" />
|
|
<el-option label="三级" :value="3" />
|
|
</el-select>
|
|
<div class="form-tip">层级根据父级分组自动计算</div>
|
|
</el-form-item>
|
|
<el-form-item label="分组封面" prop="cover_id">
|
|
<div class="recommend-user-selector">
|
|
<el-input
|
|
:model-value="groupForm.cover_id ? `文件ID: ${groupForm.cover_id}` : ''"
|
|
placeholder="点击选择封面图片"
|
|
readonly
|
|
@click="showCoverSelector = true"
|
|
>
|
|
<template #append>
|
|
<el-button @click="showCoverSelector = true">
|
|
<el-icon><Search /></el-icon>
|
|
</el-button>
|
|
</template>
|
|
</el-input>
|
|
<el-button
|
|
v-if="groupForm.cover_id"
|
|
type="danger"
|
|
link
|
|
@click="groupForm.cover_id = undefined"
|
|
class="clear-btn"
|
|
>
|
|
清除
|
|
</el-button>
|
|
</div>
|
|
</el-form-item>
|
|
<el-form-item label="分组标签" prop="tag_id">
|
|
<div class="recommend-user-selector">
|
|
<el-input
|
|
:model-value="selectedTagName"
|
|
placeholder="点击选择分组标签"
|
|
readonly
|
|
@click="showTagSelector = true"
|
|
>
|
|
<template #append>
|
|
<el-button @click="showTagSelector = true">
|
|
<el-icon><Search /></el-icon>
|
|
</el-button>
|
|
</template>
|
|
</el-input>
|
|
<el-button
|
|
v-if="groupForm.tag_id"
|
|
type="danger"
|
|
link
|
|
@click="clearTag"
|
|
class="clear-btn"
|
|
>
|
|
清除
|
|
</el-button>
|
|
</div>
|
|
</el-form-item>
|
|
<el-form-item label="备注" prop="note">
|
|
<el-input v-model="groupForm.note" type="textarea" :rows="4" placeholder="请输入备注" />
|
|
</el-form-item>
|
|
<el-form-item label="状态" prop="disable">
|
|
<el-radio-group v-model="groupForm.disable">
|
|
<el-radio :value="false">启用</el-radio>
|
|
<el-radio :value="true">禁用</el-radio>
|
|
</el-radio-group>
|
|
</el-form-item>
|
|
</el-form>
|
|
<template #footer>
|
|
<div class="dialog-footer">
|
|
<el-button @click="dialogVisible = false">取消</el-button>
|
|
<el-button type="primary" @click="submitForm">确定</el-button>
|
|
</div>
|
|
</template>
|
|
</el-dialog>
|
|
|
|
<!-- 父级分组选择弹窗 -->
|
|
<el-dialog
|
|
v-model="showParentSelector"
|
|
title="选择父级分组"
|
|
width="600px"
|
|
append-to-body
|
|
>
|
|
<el-table
|
|
:data="parentOptions"
|
|
highlight-current-row
|
|
@current-change="handleParentSelect"
|
|
:height="400"
|
|
>
|
|
<el-table-column prop="id" label="ID" width="80" />
|
|
<el-table-column prop="name" label="分组名称" min-width="150" />
|
|
<el-table-column label="层级" width="100">
|
|
<template #default="{ row }">
|
|
<el-tag :type="getLevelType(row.level)" size="small">
|
|
{{ getLevelText(row.level) }}
|
|
</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
<template #footer>
|
|
<el-button @click="showParentSelector = false">取消</el-button>
|
|
<el-button type="primary" @click="confirmParentSelect">确定</el-button>
|
|
</template>
|
|
</el-dialog>
|
|
|
|
<!-- 封面选择器 -->
|
|
<AvatarSelector
|
|
v-model="showCoverSelector"
|
|
:user-id="1"
|
|
:current-cover-id="groupForm.cover_id"
|
|
@confirm="handleCoverSelect"
|
|
/>
|
|
|
|
<!-- 分组标签表单对话框 -->
|
|
<el-dialog
|
|
v-model="tagDialogVisible"
|
|
:title="tagDialogType === 'add' ? '新增分组标签' : '编辑分组标签'"
|
|
width="500px"
|
|
append-to-body
|
|
>
|
|
<el-form
|
|
ref="tagFormRef"
|
|
:model="tagForm"
|
|
:rules="tagRules"
|
|
label-width="100px"
|
|
>
|
|
<el-form-item label="标签名称" prop="name">
|
|
<el-input v-model="tagForm.name" placeholder="请输入标签名称" />
|
|
</el-form-item>
|
|
</el-form>
|
|
<template #footer>
|
|
<div class="dialog-footer">
|
|
<el-button @click="tagDialogVisible = false">取消</el-button>
|
|
<el-button type="primary" @click="submitTagForm">确定</el-button>
|
|
</div>
|
|
</template>
|
|
</el-dialog>
|
|
|
|
<!-- 分组标签选择弹窗 -->
|
|
<el-dialog
|
|
v-model="showTagSelector"
|
|
title="选择分组标签"
|
|
width="600px"
|
|
append-to-body
|
|
>
|
|
<div class="tag-selector-header">
|
|
<el-input
|
|
v-model="tagSelectorSearch"
|
|
placeholder="搜索标签名称"
|
|
clearable
|
|
style="width: 200px"
|
|
@keyup.enter="fetchTagOptionsForSelector"
|
|
>
|
|
<template #prefix>
|
|
<el-icon><Search /></el-icon>
|
|
</template>
|
|
</el-input>
|
|
<el-button type="primary" @click="fetchTagOptionsForSelector">搜索</el-button>
|
|
</div>
|
|
<el-table
|
|
v-loading="tagSelectorLoading"
|
|
:data="tagOptionsForSelector"
|
|
highlight-current-row
|
|
@current-change="handleTagSelect"
|
|
:height="350"
|
|
>
|
|
<el-table-column prop="id" label="ID" width="80" />
|
|
<el-table-column prop="name" label="标签名称" min-width="150">
|
|
<template #default="{ row }">
|
|
<el-tag type="primary">{{ row.name }}</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
<template #footer>
|
|
<el-button @click="showTagSelector = false">取消</el-button>
|
|
<el-button type="primary" @click="confirmTagSelect">确定</el-button>
|
|
</template>
|
|
</el-dialog>
|
|
</div>
|
|
</template>
|
|
|
|
<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 {
|
|
getProductGroupList,
|
|
createProductGroup,
|
|
updateProductGroup,
|
|
deleteProductGroup,
|
|
hideProductGroup,
|
|
startProductGroup,
|
|
getProductGroupTagList,
|
|
createProductGroupTag,
|
|
updateProductGroupTag,
|
|
deleteProductGroupTag
|
|
} from '@/api/admin/product'
|
|
import AvatarSelector from '@/components/admin/AvatarSelector.vue'
|
|
|
|
// Tab切换
|
|
const activeTab = ref('group')
|
|
|
|
// 视图模式
|
|
const viewMode = ref('tree')
|
|
|
|
// 查询参数
|
|
const queryParams = reactive({
|
|
page: 1,
|
|
count: 10, // 列表视图分页
|
|
level: undefined,
|
|
disable: undefined,
|
|
key: ''
|
|
})
|
|
|
|
// 监听视图模式变化
|
|
watch(viewMode, (newVal) => {
|
|
if (newVal === 'tree') {
|
|
// 树形视图获取全部数据
|
|
queryParams.count = 1000
|
|
} else {
|
|
// 列表视图使用分页
|
|
queryParams.count = 10
|
|
queryParams.page = 1
|
|
}
|
|
fetchGroupList()
|
|
})
|
|
|
|
// 商品分组表单
|
|
const groupForm = reactive({
|
|
id: undefined,
|
|
name: '',
|
|
note: '',
|
|
disable: false,
|
|
level: 1,
|
|
parent_id: undefined,
|
|
cover_id: undefined,
|
|
tag_id: undefined
|
|
})
|
|
|
|
const groupRules = {
|
|
name: [
|
|
{ required: true, message: '请输入分组名称', trigger: 'blur' }
|
|
]
|
|
}
|
|
|
|
// 状态数据
|
|
const loading = ref(false)
|
|
const groupList = ref([])
|
|
const allGroupList = ref([]) // 用于树形构建
|
|
const total = ref(0)
|
|
const dialogVisible = ref(false)
|
|
const dialogType = ref('add')
|
|
const dialogTitle = computed(() => {
|
|
if (dialogType.value === 'add') {
|
|
return groupForm.parent_id ? '添加子级分组' : '新增顶级分组'
|
|
}
|
|
return '编辑商品分组'
|
|
})
|
|
const groupFormRef = ref(null)
|
|
|
|
// 父级选择相关
|
|
const showParentSelector = ref(false)
|
|
const showCoverSelector = ref(false)
|
|
const selectedParent = ref(null)
|
|
const selectedParentName = computed(() => {
|
|
if (groupForm.parent_id) {
|
|
const parent = allGroupList.value.find(g => g.id === groupForm.parent_id)
|
|
return parent ? `${parent.name} (ID: ${parent.id})` : `分组ID: ${groupForm.parent_id}`
|
|
}
|
|
return ''
|
|
})
|
|
const parentOptions = computed(() => {
|
|
// 只能选择层级1或2的作为父级
|
|
return allGroupList.value.filter(g => g.level < 3 && g.id !== groupForm.id)
|
|
})
|
|
|
|
// 标签选择相关
|
|
const showTagSelector = ref(false)
|
|
const tagSelectorLoading = ref(false)
|
|
const tagSelectorSearch = ref('')
|
|
const tagOptionsForSelector = ref([])
|
|
const selectedTag = ref(null)
|
|
const allTagOptions = ref([]) // 存储所有标签用于显示名称
|
|
|
|
const selectedTagName = computed(() => {
|
|
if (groupForm.tag_id) {
|
|
const tag = allTagOptions.value.find(t => t.id === groupForm.tag_id)
|
|
return tag ? `${tag.name} (ID: ${tag.id})` : `标签ID: ${groupForm.tag_id}`
|
|
}
|
|
return ''
|
|
})
|
|
|
|
// 获取标签选项(用于选择器)
|
|
const fetchTagOptionsForSelector = async () => {
|
|
tagSelectorLoading.value = true
|
|
try {
|
|
const params = { page: 1, count: 100 }
|
|
if (tagSelectorSearch.value) {
|
|
params.key = tagSelectorSearch.value
|
|
}
|
|
const res = await getProductGroupTagList(params)
|
|
if (res.data.code === 200) {
|
|
const data = res.data.data
|
|
if (Array.isArray(data)) {
|
|
tagOptionsForSelector.value = data
|
|
} else if (data && data.list) {
|
|
tagOptionsForSelector.value = data.list
|
|
} else if (data && data.data) {
|
|
tagOptionsForSelector.value = data.data
|
|
} else {
|
|
tagOptionsForSelector.value = []
|
|
}
|
|
// 同时更新allTagOptions
|
|
if (!tagSelectorSearch.value) {
|
|
allTagOptions.value = tagOptionsForSelector.value
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('获取标签列表失败:', error)
|
|
} finally {
|
|
tagSelectorLoading.value = false
|
|
}
|
|
}
|
|
|
|
// 初始化获取所有标签
|
|
const fetchAllTagOptions = async () => {
|
|
try {
|
|
const res = await getProductGroupTagList({ page: 1, count: 1000 })
|
|
if (res.data.code === 200) {
|
|
const data = res.data.data
|
|
if (Array.isArray(data)) {
|
|
allTagOptions.value = data
|
|
} else if (data && data.list) {
|
|
allTagOptions.value = data.list
|
|
} else if (data && data.data) {
|
|
allTagOptions.value = data.data
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('获取标签列表失败:', error)
|
|
}
|
|
}
|
|
|
|
// 标签选择
|
|
const handleTagSelect = (row) => {
|
|
selectedTag.value = row
|
|
}
|
|
|
|
const confirmTagSelect = () => {
|
|
if (selectedTag.value) {
|
|
groupForm.tag_id = selectedTag.value.id
|
|
showTagSelector.value = false
|
|
selectedTag.value = null
|
|
}
|
|
}
|
|
|
|
const clearTag = () => {
|
|
groupForm.tag_id = undefined
|
|
}
|
|
|
|
// 监听标签选择弹窗打开
|
|
watch(showTagSelector, (val) => {
|
|
if (val) {
|
|
tagSelectorSearch.value = ''
|
|
fetchTagOptionsForSelector()
|
|
}
|
|
})
|
|
|
|
// 构建树形数据
|
|
const treeData = computed(() => {
|
|
const list = allGroupList.value
|
|
const map = new Map()
|
|
const roots = []
|
|
|
|
// 创建映射
|
|
list.forEach(item => {
|
|
map.set(item.id, { ...item, children: [] })
|
|
})
|
|
|
|
// 构建树
|
|
list.forEach(item => {
|
|
const node = map.get(item.id)
|
|
if (item.parentId && map.has(item.parentId)) {
|
|
map.get(item.parentId).children.push(node)
|
|
} else {
|
|
roots.push(node)
|
|
}
|
|
})
|
|
|
|
return roots
|
|
})
|
|
|
|
// 获取层级文本
|
|
const getLevelText = (level) => {
|
|
const map = { 1: '一级', 2: '二级', 3: '三级' }
|
|
return map[level] || `${level}级`
|
|
}
|
|
|
|
// 获取层级类型
|
|
const getLevelType = (level) => {
|
|
const map = { 1: 'primary', 2: 'success', 3: 'warning' }
|
|
return map[level] || 'info'
|
|
}
|
|
|
|
// 获取父级名称
|
|
const getParentName = (parentId) => {
|
|
const parent = allGroupList.value.find(g => g.id === parentId)
|
|
return parent ? parent.name : `ID: ${parentId}`
|
|
}
|
|
|
|
// 获取商品分组列表
|
|
const fetchGroupList = async () => {
|
|
loading.value = true
|
|
try {
|
|
const params = { ...queryParams }
|
|
if (params.level === undefined) delete params.level
|
|
if (params.disable === undefined) delete params.disable
|
|
if (!params.key) delete params.key
|
|
|
|
const res = await getProductGroupList(params)
|
|
if (res.data.code === 200) {
|
|
const data = res.data.data.data || []
|
|
allGroupList.value = data
|
|
groupList.value = data
|
|
total.value = res.data.data.total || data.length
|
|
}
|
|
} catch (error) {
|
|
ElMessage.error('获取商品分组列表失败')
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
// 重置查询
|
|
const resetQuery = () => {
|
|
queryParams.level = undefined
|
|
queryParams.disable = undefined
|
|
queryParams.key = ''
|
|
queryParams.page = 1
|
|
fetchGroupList()
|
|
}
|
|
|
|
// 分页
|
|
const handleSizeChange = (size) => {
|
|
queryParams.count = size
|
|
fetchGroupList()
|
|
}
|
|
|
|
const handleCurrentChange = (page) => {
|
|
queryParams.page = page
|
|
fetchGroupList()
|
|
}
|
|
|
|
// 新增商品分组
|
|
const handleAdd = (parentRow) => {
|
|
dialogType.value = 'add'
|
|
dialogVisible.value = true
|
|
|
|
if (parentRow) {
|
|
// 添加子级
|
|
Object.assign(groupForm, {
|
|
id: undefined,
|
|
name: '',
|
|
note: '',
|
|
disable: false,
|
|
level: parentRow.level + 1,
|
|
parent_id: parentRow.id,
|
|
cover_id: undefined,
|
|
tag_id: undefined
|
|
})
|
|
} else {
|
|
// 添加顶级
|
|
Object.assign(groupForm, {
|
|
id: undefined,
|
|
name: '',
|
|
note: '',
|
|
disable: false,
|
|
level: 1,
|
|
parent_id: undefined,
|
|
cover_id: undefined,
|
|
tag_id: undefined
|
|
})
|
|
}
|
|
groupFormRef.value?.resetFields()
|
|
}
|
|
|
|
// 编辑商品分组
|
|
const handleEdit = (row) => {
|
|
dialogType.value = 'edit'
|
|
dialogVisible.value = true
|
|
Object.assign(groupForm, {
|
|
id: row.id,
|
|
name: row.name,
|
|
note: row.note,
|
|
disable: row.disable,
|
|
level: row.level || 1,
|
|
parent_id: row.parentId,
|
|
cover_id: row.coverId,
|
|
tag_id: row.tagId
|
|
})
|
|
}
|
|
|
|
// 清除父级
|
|
const clearParent = () => {
|
|
groupForm.parent_id = undefined
|
|
groupForm.level = 1
|
|
}
|
|
|
|
// 父级选择
|
|
const handleParentSelect = (row) => {
|
|
selectedParent.value = row
|
|
}
|
|
|
|
const confirmParentSelect = () => {
|
|
if (selectedParent.value) {
|
|
groupForm.parent_id = selectedParent.value.id
|
|
groupForm.level = selectedParent.value.level + 1
|
|
showParentSelector.value = false
|
|
selectedParent.value = null
|
|
}
|
|
}
|
|
|
|
// 封面选择
|
|
const handleCoverSelect = (file) => {
|
|
groupForm.cover_id = file.cover_id
|
|
}
|
|
|
|
// 状态变化
|
|
const handleStatusChange = async (row, disable) => {
|
|
try {
|
|
let res
|
|
if (disable === false) {
|
|
res = await startProductGroup({ id: row.id })
|
|
} else {
|
|
res = await hideProductGroup({ id: row.id })
|
|
}
|
|
if (res.data.code === 200) {
|
|
ElMessage.success('状态修改成功')
|
|
}
|
|
} catch (error) {
|
|
ElMessage.error('状态修改失败')
|
|
row.disable = !disable
|
|
}
|
|
}
|
|
|
|
// 删除商品分组
|
|
const handleDelete = (row) => {
|
|
// 检查是否有子级
|
|
const hasChildren = allGroupList.value.some(g => g.parentId === row.id)
|
|
if (hasChildren) {
|
|
ElMessage.warning('该分组下有子分组,请先删除子分组')
|
|
return
|
|
}
|
|
|
|
ElMessageBox.confirm(`确认删除商品分组 ${row.name} 吗?`, '警告', {
|
|
confirmButtonText: '确定',
|
|
cancelButtonText: '取消',
|
|
type: 'warning'
|
|
}).then(async () => {
|
|
try {
|
|
const res = await deleteProductGroup({ id: row.id })
|
|
if (res.data.code === 200) {
|
|
ElMessage.success('删除成功')
|
|
fetchGroupList()
|
|
}
|
|
} catch (error) {
|
|
ElMessage.error('删除失败')
|
|
}
|
|
}).catch(() => {})
|
|
}
|
|
|
|
// 提交表单
|
|
const submitForm = () => {
|
|
groupFormRef.value?.validate(async (valid) => {
|
|
if (valid) {
|
|
// 检查必填字段
|
|
if (!groupForm.name || !groupForm.name.trim()) {
|
|
ElMessage.warning('请输入分组名称')
|
|
return
|
|
}
|
|
|
|
try {
|
|
const submitData = {
|
|
name: groupForm.name.trim(),
|
|
note: groupForm.note || '',
|
|
disable: groupForm.disable,
|
|
level: String(groupForm.level || 1)
|
|
}
|
|
// 只有有值时才传递这些字段,避免外键约束错误
|
|
if (groupForm.parent_id) {
|
|
submitData.parent_id = groupForm.parent_id
|
|
}
|
|
if (groupForm.cover_id) {
|
|
submitData.cover_id = groupForm.cover_id
|
|
}
|
|
if (groupForm.tag_id) {
|
|
submitData.tag_id = groupForm.tag_id
|
|
}
|
|
console.log('提交数据:', submitData)
|
|
let res
|
|
if (dialogType.value === 'add') {
|
|
res = await createProductGroup(submitData)
|
|
} else {
|
|
submitData.id = groupForm.id
|
|
res = await updateProductGroup(submitData)
|
|
}
|
|
if (res.data.code === 200) {
|
|
ElMessage.success(dialogType.value === 'add' ? '新增成功' : '修改成功')
|
|
dialogVisible.value = false
|
|
fetchGroupList()
|
|
}
|
|
} catch (error) {
|
|
ElMessage.error('操作失败')
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// ==================== 分组标签管理 ====================
|
|
const tagQueryParams = reactive({
|
|
page: 1,
|
|
count: 10,
|
|
key: ''
|
|
})
|
|
|
|
const tagLoading = ref(false)
|
|
const tagList = ref([])
|
|
const tagTotal = ref(0)
|
|
const tagDialogVisible = ref(false)
|
|
const tagDialogType = ref('add')
|
|
const tagFormRef = ref(null)
|
|
const tagForm = reactive({
|
|
id: undefined,
|
|
name: ''
|
|
})
|
|
|
|
const tagRules = {
|
|
name: [{ required: true, message: '请输入标签名称', trigger: 'blur' }]
|
|
}
|
|
|
|
// 获取标签列表
|
|
const fetchTagList = async () => {
|
|
tagLoading.value = true
|
|
try {
|
|
const params = { ...tagQueryParams }
|
|
if (!params.key) delete params.key
|
|
|
|
const res = await getProductGroupTagList(params)
|
|
if (res.data.code === 200) {
|
|
const data = res.data.data
|
|
if (Array.isArray(data)) {
|
|
tagList.value = data
|
|
tagTotal.value = data.length
|
|
} else if (data && data.list) {
|
|
tagList.value = data.list
|
|
tagTotal.value = data.all_count || data.total || data.list.length
|
|
} else if (data && data.data) {
|
|
tagList.value = data.data
|
|
tagTotal.value = data.total || data.data.length
|
|
} else {
|
|
tagList.value = []
|
|
tagTotal.value = 0
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('获取标签列表失败:', error)
|
|
ElMessage.error('获取标签列表失败')
|
|
} finally {
|
|
tagLoading.value = false
|
|
}
|
|
}
|
|
|
|
// 重置标签查询
|
|
const resetTagQuery = () => {
|
|
tagQueryParams.key = ''
|
|
tagQueryParams.page = 1
|
|
fetchTagList()
|
|
}
|
|
|
|
// 标签分页
|
|
const handleTagSizeChange = (size) => {
|
|
tagQueryParams.count = size
|
|
fetchTagList()
|
|
}
|
|
|
|
const handleTagCurrentChange = (page) => {
|
|
tagQueryParams.page = page
|
|
fetchTagList()
|
|
}
|
|
|
|
// 新增标签
|
|
const handleAddTag = () => {
|
|
tagDialogType.value = 'add'
|
|
tagDialogVisible.value = true
|
|
Object.assign(tagForm, {
|
|
id: undefined,
|
|
name: ''
|
|
})
|
|
tagFormRef.value?.resetFields()
|
|
}
|
|
|
|
// 编辑标签
|
|
const handleEditTag = (row) => {
|
|
tagDialogType.value = 'edit'
|
|
tagDialogVisible.value = true
|
|
Object.assign(tagForm, {
|
|
id: row.id,
|
|
name: row.name
|
|
})
|
|
}
|
|
|
|
// 删除标签
|
|
const handleDeleteTag = (row) => {
|
|
ElMessageBox.confirm(`确认删除标签 ${row.name} 吗?`, '警告', {
|
|
confirmButtonText: '确定',
|
|
cancelButtonText: '取消',
|
|
type: 'warning'
|
|
}).then(async () => {
|
|
try {
|
|
const res = await deleteProductGroupTag({ id: row.id })
|
|
if (res.data.code === 200) {
|
|
ElMessage.success('删除成功')
|
|
fetchTagList()
|
|
}
|
|
} catch (error) {
|
|
ElMessage.error('删除失败')
|
|
}
|
|
}).catch(() => {})
|
|
}
|
|
|
|
// 提交标签表单
|
|
const submitTagForm = () => {
|
|
tagFormRef.value?.validate(async (valid) => {
|
|
if (valid) {
|
|
try {
|
|
let res
|
|
if (tagDialogType.value === 'add') {
|
|
res = await createProductGroupTag({ name: tagForm.name })
|
|
} else {
|
|
res = await updateProductGroupTag({ id: tagForm.id, name: tagForm.name })
|
|
}
|
|
if (res.data.code === 200) {
|
|
ElMessage.success(tagDialogType.value === 'add' ? '新增成功' : '修改成功')
|
|
tagDialogVisible.value = false
|
|
fetchTagList()
|
|
}
|
|
} catch (error) {
|
|
ElMessage.error('操作失败')
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// 监听Tab切换
|
|
watch(activeTab, (newVal) => {
|
|
if (newVal === 'tag') {
|
|
fetchTagList()
|
|
}
|
|
})
|
|
|
|
// 初始化
|
|
onMounted(() => {
|
|
fetchGroupList()
|
|
fetchAllTagOptions() // 获取所有标签用于显示名称
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.product-group-container {
|
|
padding: 0;
|
|
}
|
|
|
|
.main-container {
|
|
border: 1px solid #e1e8ed;
|
|
background: #ffffff;
|
|
}
|
|
|
|
.main-tabs {
|
|
padding: 0 20px;
|
|
}
|
|
|
|
.tag-selector-header {
|
|
display: flex;
|
|
gap: 12px;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
:deep(.el-tabs__header) {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
:deep(.el-tabs__content) {
|
|
padding: 0;
|
|
}
|
|
|
|
.filter-section {
|
|
padding: 0;
|
|
border-bottom: 1px solid #e1e8ed;
|
|
background: #fafbfc;
|
|
}
|
|
|
|
.filter-content {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 16px 20px;
|
|
gap: 20px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.search-form {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
flex-wrap: wrap;
|
|
margin: 0;
|
|
}
|
|
|
|
.search-form :deep(.el-form-item) {
|
|
margin-bottom: 0;
|
|
margin-right: 0;
|
|
}
|
|
|
|
.action-bar {
|
|
display: flex;
|
|
gap: 12px;
|
|
flex-shrink: 0;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.table-section {
|
|
padding: 0;
|
|
}
|
|
|
|
.group-name-cell {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.group-name {
|
|
font-weight: 500;
|
|
}
|
|
|
|
.action-buttons {
|
|
display: flex;
|
|
gap: 8px;
|
|
align-items: center;
|
|
}
|
|
|
|
.pagination {
|
|
margin-top: 20px;
|
|
padding: 16px 20px;
|
|
border-top: 1px solid #e1e8ed;
|
|
background: #fafbfc;
|
|
justify-content: flex-end;
|
|
}
|
|
|
|
.dialog-footer {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 12px;
|
|
padding: 0;
|
|
}
|
|
|
|
.text-muted {
|
|
color: #909399;
|
|
}
|
|
|
|
.form-tip {
|
|
font-size: 12px;
|
|
color: #909399;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
/* 推介人选择器样式 */
|
|
.recommend-user-selector {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
width: 100%;
|
|
}
|
|
|
|
.recommend-user-selector .el-input {
|
|
flex: 1;
|
|
}
|
|
|
|
.recommend-user-selector .clear-btn {
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
/* 表格样式优化 */
|
|
:deep(.el-table) {
|
|
border: none;
|
|
color: #2c3e50;
|
|
}
|
|
|
|
:deep(.el-table__header) {
|
|
background: #f8f9fa;
|
|
}
|
|
|
|
:deep(.el-table th) {
|
|
background: #f8f9fa !important;
|
|
border-bottom: 2px solid #e1e8ed;
|
|
color: #2c3e50;
|
|
font-weight: 600;
|
|
font-size: 13px;
|
|
}
|
|
|
|
:deep(.el-table td) {
|
|
border-bottom: 1px solid #f0f2f5;
|
|
color: #34495e;
|
|
}
|
|
|
|
:deep(.el-table tr:hover > td) {
|
|
background-color: #f8f9fa !important;
|
|
}
|
|
|
|
:deep(.el-card__body) {
|
|
padding: 0;
|
|
}
|
|
|
|
/* 骨架屏样式 */
|
|
.skeleton-container {
|
|
padding: 20px;
|
|
}
|
|
|
|
.skeleton-row {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 16px 0;
|
|
border-bottom: 1px solid #f0f0f0;
|
|
gap: 16px;
|
|
}
|
|
|
|
.skeleton-row:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.skeleton-cell {
|
|
height: 20px;
|
|
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
|
background-size: 200% 100%;
|
|
animation: skeleton-loading 1.5s ease-in-out infinite;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.skeleton-id { width: 80px; }
|
|
.skeleton-name { width: 200px; }
|
|
.skeleton-note { flex: 1; min-width: 150px; }
|
|
.skeleton-level { width: 100px; }
|
|
.skeleton-status { width: 100px; }
|
|
.skeleton-action { width: 180px; height: 32px; }
|
|
|
|
@keyframes skeleton-loading {
|
|
0% { background-position: 200% 0; }
|
|
100% { background-position: -200% 0; }
|
|
}
|
|
|
|
/* 移动端适配 */
|
|
@media (max-width: 768px) {
|
|
.filter-content {
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
gap: 12px;
|
|
}
|
|
|
|
.search-form {
|
|
flex-direction: column;
|
|
width: 100%;
|
|
}
|
|
|
|
.search-form :deep(.el-form-item) {
|
|
width: 100%;
|
|
}
|
|
|
|
.search-form :deep(.el-input),
|
|
.search-form :deep(.el-select) {
|
|
width: 100% !important;
|
|
}
|
|
|
|
.action-bar {
|
|
width: 100%;
|
|
justify-content: flex-start;
|
|
}
|
|
}
|
|
</style>
|