Files
ApiServer-Web-admin_dashboa…/src/views/product/ProductGroup.vue
T
lin 127d54eaa6
Build and Deploy Vue3 / build (push) Successful in 1m11s
Build and Deploy Vue3 / deploy (push) Successful in 1m26s
feate:新增套餐管理
2026-01-29 15:18:08 +08:00

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>