bdf6dd9382
- 合并优惠码/代金券为商品管理下优惠管理页面,卡片化展示与过期遮罩 - 用户组新增优惠绑定,商品关联改用懒加载树选择器 - 商品/套餐表单新增 renew_price、renew_recommend_rebate、renew_fixed_price Co-authored-by: Cursor <cursoragent@cursor.com>
2487 lines
71 KiB
Vue
2487 lines
71 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.tag" placeholder="全部标签" clearable style="width: 140px" @change="fetchGroupList">
|
||
<el-option
|
||
v-for="tag in allTagOptions"
|
||
:key="tag.id"
|
||
:label="tag.name"
|
||
:value="tag.name"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="层级筛选">
|
||
<el-select v-model="queryParams.level" placeholder="全部层级" clearable style="width: 120px" @change="fetchGroupList">
|
||
<el-option v-for="n in 10" :key="n" :label="`${n}级`" :value="n" />
|
||
</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="handleAddProduct">
|
||
<el-icon><Plus /></el-icon>新增商品
|
||
</el-button>
|
||
<el-button type="success" @click="fetchGroupList">
|
||
<el-icon><Refresh /></el-icon>刷新
|
||
</el-button>
|
||
<div class="view-switch">
|
||
<el-radio-group v-model="viewMode" size="default">
|
||
<el-radio-button value="tree">
|
||
<el-icon><Grid /></el-icon>
|
||
<span>树形视图</span>
|
||
</el-radio-button>
|
||
<el-radio-button value="list">
|
||
<el-icon><List /></el-icon>
|
||
<span>列表视图</span>
|
||
</el-radio-button>
|
||
</el-radio-group>
|
||
</div>
|
||
</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'"
|
||
ref="treeTableRef"
|
||
:data="treeDisplayData"
|
||
style="width: 100%"
|
||
row-key="id"
|
||
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||
>
|
||
<el-table-column prop="name" label="名称" min-width="320">
|
||
<template #default="{ row }">
|
||
<div
|
||
class="group-name-cell"
|
||
:class="{ 'is-clickable': row.isGroup || row.isProduct }"
|
||
:style="{ paddingLeft: row.isProduct ? ((row.level - 1) * 24 + 24) + 'px' : (row.level - 1) * 24 + 'px' }"
|
||
@click="handleNameCellClick(row)"
|
||
>
|
||
<!-- 分组的展开/收起按钮 -->
|
||
<span
|
||
v-if="row.isGroup"
|
||
class="expand-icon"
|
||
>
|
||
<el-icon v-if="row._loading"><Loading /></el-icon>
|
||
<el-icon v-else :class="{ 'is-expanded': row._expanded }"><ArrowRight /></el-icon>
|
||
</span>
|
||
<span v-else class="expand-placeholder"></span>
|
||
|
||
<!-- 类型图标 -->
|
||
<el-avatar v-if="row.isGroup && row.cover" :size="32" :src="row.cover" />
|
||
<el-avatar v-else-if="row.isGroup" :size="32" :style="{ background: getLevelColor(row.level) }">
|
||
<el-icon><Folder /></el-icon>
|
||
</el-avatar>
|
||
<el-avatar v-else-if="row.isProduct && row.data?.cover" :size="32" :src="row.data.cover" />
|
||
<el-avatar v-else-if="row.isProduct" :size="32" :style="{ background: '#409eff' }">
|
||
<el-icon><Document /></el-icon>
|
||
</el-avatar>
|
||
|
||
<!-- 类型标签 -->
|
||
<el-tag v-if="row.isGroup" type="primary" size="small" style="margin-left: 8px;">分组</el-tag>
|
||
<el-tag v-else-if="row.isProduct" type="success" size="small" style="margin-left: 8px;">商品</el-tag>
|
||
|
||
<span class="group-name">{{ row.name }}</span>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="ID" width="100">
|
||
<template #default="{ row }">
|
||
<span v-if="row.isProduct">{{ row.data?.id }}</span>
|
||
<span v-else>{{ row.id }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="标签" width="120">
|
||
<template #default="{ row }">
|
||
<el-tag v-if="row.isGroup && row.tag" size="small">{{ row.tag.name }}</el-tag>
|
||
<el-tag v-else-if="row.isProduct && row.data?.tag" size="small" type="success">{{ row.data.tag }}</el-tag>
|
||
<span v-else class="text-muted">-</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="层级" width="80">
|
||
<template #default="{ row }">
|
||
<el-tag v-if="row.isGroup" :type="getLevelType(row.level)" size="small">
|
||
{{ getLevelText(row.level) }}
|
||
</el-tag>
|
||
<span v-else class="text-muted">-</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="排序" width="80">
|
||
<template #default="{ row }">
|
||
<el-tag type="info" size="small">{{ row.index || 0 }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="note" label="备注/价格" min-width="320" show-overflow-tooltip>
|
||
<template #default="{ row }">
|
||
<span v-if="row.isGroup">{{ row.note || '-' }}</span>
|
||
<div v-else-if="row.isProduct" class="product-info-inline">
|
||
<span class="product-price">¥{{ (row.data.price / 100).toFixed(2) }}</span>
|
||
<el-tag v-if="row.data.inventoryControl" type="info" size="small">库存: {{ row.data.inventory }}</el-tag>
|
||
<el-tag v-if="row.data.recommend" type="warning" size="small">推荐</el-tag>
|
||
<el-tag v-if="row.data.expireTime && row.data.expireTime > 0" type="info" size="small">有效期: {{ row.data.expireTime }}天</el-tag>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="状态" width="80">
|
||
<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="260" fixed="right">
|
||
<template #default="{ row }">
|
||
<div class="action-buttons">
|
||
<template v-if="row.isGroup">
|
||
<el-button type="success" link @click="handleAdd(row)">添加子级</el-button>
|
||
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
|
||
</template>
|
||
<template v-else-if="row.isProduct">
|
||
<el-button type="primary" link @click="handleEditProduct(row.data, row.parentId)">编辑</el-button>
|
||
<el-button type="success" link @click="handlePlan(row.data)">套餐</el-button>
|
||
<el-button type="warning" link @click="handleParameter(row.data)">参数</el-button>
|
||
<el-button type="danger" link @click="handleDeleteProduct(row.data)">删除</el-button>
|
||
</template>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<template #empty>
|
||
<el-empty description="暂无分组数据" :image-size="100" />
|
||
</template>
|
||
</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>
|
||
<template #empty>
|
||
<el-empty description="暂无分组数据" :image-size="100" />
|
||
</template>
|
||
</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">
|
||
<GroupTagManager ref="groupTagManagerRef" />
|
||
</el-tab-pane>
|
||
</el-tabs>
|
||
</el-card>
|
||
|
||
<!-- 商品分组表单对话框 -->
|
||
<el-dialog
|
||
v-model="dialogVisible"
|
||
:title="dialogTitle"
|
||
width="860px"
|
||
append-to-body
|
||
class="product-form-dialog"
|
||
>
|
||
<el-form
|
||
ref="groupFormRef"
|
||
:model="groupForm"
|
||
:rules="groupRules"
|
||
label-position="top"
|
||
class="product-form"
|
||
>
|
||
<!-- 顶部:封面 + 基本信息 -->
|
||
<div class="product-form-header">
|
||
<!-- 左侧封面 -->
|
||
<div class="cover-uploader" @click="showCoverSelector = true">
|
||
<img v-if="groupForm.cover_url" :src="groupForm.cover_url" class="cover-image" alt="分组封面" />
|
||
<div v-else class="cover-placeholder">
|
||
<el-icon class="cover-placeholder-icon"><Folder /></el-icon>
|
||
<span class="cover-placeholder-text">点击选择封面</span>
|
||
</div>
|
||
<div class="cover-mask">
|
||
<el-icon><Picture /></el-icon>
|
||
<span>更换封面</span>
|
||
</div>
|
||
<el-button
|
||
v-if="groupForm.cover_id"
|
||
type="danger"
|
||
size="small"
|
||
circle
|
||
class="cover-clear-btn"
|
||
@click.stop="clearGroupCover"
|
||
>
|
||
<el-icon><Delete /></el-icon>
|
||
</el-button>
|
||
</div>
|
||
|
||
<!-- 右侧基本信息 -->
|
||
<div class="basic-fields">
|
||
<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="group-picker"
|
||
:class="{ 'is-empty': !selectedParentName, 'is-disabled': isAddingChild }"
|
||
@click="!isAddingChild && (showParentSelector = true)"
|
||
>
|
||
<el-icon class="group-picker-icon"><Folder /></el-icon>
|
||
<span class="group-picker-text">
|
||
{{ selectedParentName || (isAddingChild ? '已自动设置父级分组' : '无父级(顶级分组)') }}
|
||
</span>
|
||
<el-button
|
||
v-if="groupForm.parent_id && !isAddingChild"
|
||
type="danger"
|
||
link
|
||
size="small"
|
||
class="group-picker-clear"
|
||
@click.stop="clearParent"
|
||
>
|
||
清除
|
||
</el-button>
|
||
<el-icon v-if="!isAddingChild" class="group-picker-arrow"><ArrowRight /></el-icon>
|
||
</div>
|
||
<div v-if="isAddingChild" class="form-tip">
|
||
添加子级时自动继承父级分组,不可修改
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="分组标签" prop="tag_id">
|
||
<div
|
||
class="group-picker"
|
||
:class="{ 'is-empty': !selectedTagName }"
|
||
@click="showTagSelector = true"
|
||
>
|
||
<el-icon class="group-picker-icon"><CollectionTag /></el-icon>
|
||
<span class="group-picker-text">{{ selectedTagName || '点击选择分组标签' }}</span>
|
||
<el-button
|
||
v-if="groupForm.tag_id"
|
||
type="danger"
|
||
link
|
||
size="small"
|
||
class="group-picker-clear"
|
||
@click.stop="clearTag"
|
||
>
|
||
清除
|
||
</el-button>
|
||
<el-icon class="group-picker-arrow"><ArrowRight /></el-icon>
|
||
</div>
|
||
</el-form-item>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 基本属性(层级 / 排序 / 状态) -->
|
||
<div class="form-section">
|
||
<div class="form-section-title">基本属性</div>
|
||
<div class="form-grid">
|
||
<el-form-item label="分组层级" prop="level">
|
||
<el-input :model-value="`${groupForm.level} 级`" disabled style="width: 100%" />
|
||
<div class="form-tip">层级根据父级分组自动计算</div>
|
||
</el-form-item>
|
||
<el-form-item label="排序索引" prop="index">
|
||
<el-input-number
|
||
v-model="groupForm.index"
|
||
:min="0"
|
||
:max="9999"
|
||
controls-position="right"
|
||
placeholder="数值越小越靠前"
|
||
style="width: 100%"
|
||
/>
|
||
<div class="form-tip">数值越小排序越靠前,相同索引按创建时间排序</div>
|
||
</el-form-item>
|
||
<el-form-item label="状态" prop="disable" class="group-status-item">
|
||
<el-radio-group v-model="groupForm.disable">
|
||
<el-radio-button :value="false">启用</el-radio-button>
|
||
<el-radio-button :value="true">禁用</el-radio-button>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 备注 -->
|
||
<el-form-item prop="note" label="备注" class="note-form-item">
|
||
<el-input
|
||
v-model="groupForm.note"
|
||
type="textarea"
|
||
:rows="4"
|
||
placeholder="请输入备注(可选)"
|
||
resize="none"
|
||
/>
|
||
</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>
|
||
<template #empty>
|
||
<el-empty description="暂无可选分组" :image-size="60" />
|
||
</template>
|
||
</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="showTagSelector"
|
||
title="选择分组标签"
|
||
width="650px"
|
||
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-column label="操作" width="100" fixed="right">
|
||
<template #default="{ row }">
|
||
<el-button type="danger" link size="small" @click.stop="handleDeleteTagFromSelector(row)">删除</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
<template #empty>
|
||
<el-empty description="暂无标签数据" :image-size="60" />
|
||
</template>
|
||
</el-table>
|
||
<template #footer>
|
||
<el-button @click="showTagSelector = false">取消</el-button>
|
||
<el-button type="primary" @click="confirmTagSelect">确定</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 商品表单对话框 -->
|
||
<el-dialog
|
||
v-model="productDialogVisible"
|
||
:title="productDialogType === 'add' ? '新增商品' : '编辑商品'"
|
||
width="860px"
|
||
append-to-body
|
||
class="product-form-dialog"
|
||
>
|
||
<el-form
|
||
ref="productFormRef"
|
||
:model="productForm"
|
||
:rules="productRules"
|
||
label-position="top"
|
||
class="product-form"
|
||
>
|
||
<!-- 顶部:封面 + 基本信息 -->
|
||
<div class="product-form-header">
|
||
<!-- 左侧封面 -->
|
||
<div class="cover-uploader" @click="coverSelectorVisible = true">
|
||
<img v-if="productForm.cover_url" :src="productForm.cover_url" class="cover-image" alt="商品封面" />
|
||
<div v-else class="cover-placeholder">
|
||
<el-icon class="cover-placeholder-icon"><Picture /></el-icon>
|
||
<span class="cover-placeholder-text">点击选择封面</span>
|
||
</div>
|
||
<div class="cover-mask">
|
||
<el-icon><Picture /></el-icon>
|
||
<span>更换封面</span>
|
||
</div>
|
||
<el-button
|
||
v-if="productForm.cover_id"
|
||
type="danger"
|
||
size="small"
|
||
circle
|
||
class="cover-clear-btn"
|
||
@click.stop="clearProductCover"
|
||
>
|
||
<el-icon><Delete /></el-icon>
|
||
</el-button>
|
||
</div>
|
||
|
||
<!-- 右侧基本信息 -->
|
||
<div class="basic-fields">
|
||
<el-form-item label="商品名称" prop="name">
|
||
<el-input v-model="productForm.name" placeholder="请输入商品名称" />
|
||
</el-form-item>
|
||
<el-form-item label="所属分组" prop="good_group_id">
|
||
<div
|
||
class="group-picker"
|
||
:class="{ 'is-empty': !selectedGroupName }"
|
||
@click="showProductGroupSelector = true"
|
||
>
|
||
<el-icon class="group-picker-icon"><Folder /></el-icon>
|
||
<span class="group-picker-text">{{ selectedGroupName || '点击选择商品分组' }}</span>
|
||
<el-button
|
||
v-if="productForm.good_group_id"
|
||
type="danger"
|
||
link
|
||
size="small"
|
||
class="group-picker-clear"
|
||
@click.stop="clearProductGroup"
|
||
>
|
||
清除
|
||
</el-button>
|
||
<el-icon class="group-picker-arrow"><ArrowRight /></el-icon>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="所属表" prop="table">
|
||
<el-input v-model="productForm.table" placeholder="请输入商品所属表(如 kvm_service)" />
|
||
</el-form-item>
|
||
<el-form-item label="商品介绍" prop="content">
|
||
<el-input
|
||
v-model="productForm.content"
|
||
type="textarea"
|
||
:rows="4"
|
||
placeholder="请输入商品介绍"
|
||
resize="none"
|
||
/>
|
||
</el-form-item>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 封面选择器弹窗 -->
|
||
<AvatarSelector
|
||
v-model="coverSelectorVisible"
|
||
:user-id="1"
|
||
:current-cover-id="productForm.cover_id"
|
||
title="选择封面"
|
||
@confirm="handleProductCoverSelect"
|
||
/>
|
||
|
||
<!-- Tab 栏管理 -->
|
||
<el-tabs v-model="productFormTab" class="product-form-tabs">
|
||
<el-tab-pane label="价格与销售" name="sales">
|
||
<div class="form-grid">
|
||
<el-form-item prop="price">
|
||
<template #label>
|
||
<span>商品价格<span class="unit-suffix">(元)</span></span>
|
||
</template>
|
||
<el-input-number
|
||
v-model="productForm.price"
|
||
:min="0"
|
||
:precision="2"
|
||
:step="0.01"
|
||
placeholder="请输入价格"
|
||
controls-position="right"
|
||
style="width: 100%"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="单个商品数量" prop="pay_num">
|
||
<el-input-number
|
||
v-model="productForm.pay_num"
|
||
:min="1"
|
||
placeholder="请输入数量"
|
||
controls-position="right"
|
||
style="width: 100%"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item prop="expire_time">
|
||
<template #label>
|
||
<span>有效期<span class="unit-suffix">(天,0 为永久)</span></span>
|
||
</template>
|
||
<el-input-number
|
||
v-model="productForm.expire_time"
|
||
:min="0"
|
||
placeholder="请输入有效期"
|
||
controls-position="right"
|
||
style="width: 100%"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="允许续费" prop="can_renew">
|
||
<el-switch
|
||
v-model="productForm.can_renew"
|
||
active-text="允许"
|
||
inactive-text="禁止"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item prop="renew_price" v-if="productForm.can_renew">
|
||
<template #label>
|
||
<span>续费价格<span class="unit-suffix">(元,0 沿用商品价格)</span></span>
|
||
</template>
|
||
<el-input-number
|
||
v-model="productForm.renew_price"
|
||
:min="0"
|
||
:precision="2"
|
||
:step="0.01"
|
||
placeholder="续费基础价格"
|
||
controls-position="right"
|
||
style="width: 100%"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="购买类型" prop="arg_type">
|
||
<el-select
|
||
v-model="productForm.arg_type"
|
||
placeholder="请选择购买类型"
|
||
style="width: 100%"
|
||
>
|
||
<el-option label="所有类型" value="all" />
|
||
<el-option label="仅套餐" value="plan" />
|
||
<el-option label="仅自定义参数" value="customize" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="需要实名" prop="require_real_name">
|
||
<el-switch
|
||
v-model="productForm.require_real_name"
|
||
active-text="需要"
|
||
inactive-text="不需要"
|
||
/>
|
||
<div style="font-size:12px;color:#909399;margin-top:4px">启用后,用户购买/续费/升级此商品前须完成实名认证</div>
|
||
</el-form-item>
|
||
</div>
|
||
</el-tab-pane>
|
||
|
||
<el-tab-pane label="库存管理" name="inventory">
|
||
<div class="form-grid">
|
||
<el-form-item label="库存控制" prop="inventory_control">
|
||
<el-switch
|
||
v-model="productForm.inventory_control"
|
||
active-text="启用"
|
||
inactive-text="禁用"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="库存数量" prop="inventory">
|
||
<el-input-number
|
||
v-model="productForm.inventory"
|
||
:min="0"
|
||
:disabled="!productForm.inventory_control"
|
||
placeholder="请输入库存"
|
||
controls-position="right"
|
||
style="width: 100%"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="售罄状态" prop="sold_out">
|
||
<el-switch
|
||
v-model="productForm.sold_out"
|
||
active-text="售罄"
|
||
inactive-text="正常"
|
||
active-color="#f56c6c"
|
||
/>
|
||
<div style="font-size:12px;color:#909399;margin-top:4px">开启后用户端将无法选购该商品</div>
|
||
</el-form-item>
|
||
<el-form-item label="购买限制" prop="max_per_user">
|
||
<el-input-number
|
||
v-model="productForm.max_per_user"
|
||
:min="0"
|
||
placeholder="0 表示不限"
|
||
controls-position="right"
|
||
style="width: 100%"
|
||
/>
|
||
<div style="font-size:12px;color:#909399;margin-top:4px">限制单用户最大购买数量,0 表示不限制</div>
|
||
</el-form-item>
|
||
</div>
|
||
</el-tab-pane>
|
||
|
||
<el-tab-pane label="推荐与通知" name="promotion">
|
||
<div class="form-grid">
|
||
<el-form-item label="推荐" prop="recommend">
|
||
<el-switch
|
||
v-model="productForm.recommend"
|
||
active-text="启用"
|
||
inactive-text="禁用"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item prop="recommend_rebate">
|
||
<template #label>
|
||
<span>推荐返还<span class="unit-suffix">(%)</span></span>
|
||
</template>
|
||
<el-input-number
|
||
v-model="productForm.recommend_rebate"
|
||
:min="0"
|
||
:max="100"
|
||
:disabled="!productForm.recommend"
|
||
placeholder="返还百分比"
|
||
controls-position="right"
|
||
style="width: 100%"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item prop="renew_recommend_rebate">
|
||
<template #label>
|
||
<span>续费推介返还<span class="unit-suffix">(%,0 沿用推荐返还)</span></span>
|
||
</template>
|
||
<el-input-number
|
||
v-model="productForm.renew_recommend_rebate"
|
||
:min="0"
|
||
:max="100"
|
||
:disabled="!productForm.recommend"
|
||
placeholder="续费推介返还百分比"
|
||
controls-position="right"
|
||
style="width: 100%"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="购买通知" prop="send_notice">
|
||
<el-switch
|
||
v-model="productForm.send_notice"
|
||
active-text="发送"
|
||
inactive-text="不发送"
|
||
/>
|
||
<div style="font-size:12px;color:#909399;margin:4px">用户购买后是否发送通知给管理员</div>
|
||
</el-form-item>
|
||
</div>
|
||
</el-tab-pane>
|
||
</el-tabs>
|
||
</el-form>
|
||
<template #footer>
|
||
<div class="dialog-footer">
|
||
<el-button @click="productDialogVisible = false">取消</el-button>
|
||
<el-button type="primary" @click="submitProductForm">确定</el-button>
|
||
</div>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 商品分组选择弹窗 -->
|
||
<el-dialog
|
||
v-model="showProductGroupSelector"
|
||
title="选择商品分组"
|
||
width="600px"
|
||
append-to-body
|
||
>
|
||
<el-table
|
||
:data="allGroupList"
|
||
highlight-current-row
|
||
@current-change="handleProductGroupSelect"
|
||
style="width: 100%"
|
||
max-height="400px"
|
||
>
|
||
<el-table-column prop="name" label="分组名称" />
|
||
<el-table-column prop="level" label="层级" width="80">
|
||
<template #default="{ row }">
|
||
<el-tag :type="getLevelType(row.level)" size="small">
|
||
{{ getLevelText(row.level) }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<template #empty>
|
||
<el-empty description="暂无可选分组" :image-size="60" />
|
||
</template>
|
||
</el-table>
|
||
<template #footer>
|
||
<el-button @click="showProductGroupSelector = false">取消</el-button>
|
||
<el-button type="primary" @click="confirmProductGroupSelect">确定</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 商品参数管理(子组件) -->
|
||
<ProductParameterManager
|
||
v-model:visible="paramDialogVisible"
|
||
:good-id="currentProductId"
|
||
/>
|
||
|
||
<!-- 商品套餐管理(子组件) -->
|
||
<ProductPlanManager
|
||
v-model:visible="planDialogVisible"
|
||
:good-id="currentPlanProductId"
|
||
:good-name="currentPlanProductName"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, reactive, computed, onMounted, watch } from 'vue'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import { Plus, Refresh, Search, Folder, ArrowRight, Loading, Grid, List, Document, Picture, Delete, CollectionTag } from '@element-plus/icons-vue'
|
||
import {
|
||
getProductGroupList,
|
||
createProductGroup,
|
||
updateProductGroup,
|
||
deleteProductGroup,
|
||
hideProductGroup,
|
||
startProductGroup,
|
||
getProductGroupTagList,
|
||
deleteProductGroupTag,
|
||
getProductList,
|
||
createProduct,
|
||
updateProduct,
|
||
deleteProduct,
|
||
} from '@/api/admin/product'
|
||
import AvatarSelector from '@/components/admin/AvatarSelector.vue'
|
||
import GroupTagManager from './components/GroupTagManager.vue'
|
||
import ProductParameterManager from './components/ProductParameterManager.vue'
|
||
import ProductPlanManager from './components/ProductPlanManager.vue'
|
||
|
||
// Tab切换
|
||
const activeTab = ref('group')
|
||
|
||
// 视图模式
|
||
const viewMode = ref('tree')
|
||
|
||
// 查询参数
|
||
const queryParams = reactive({
|
||
page: 1,
|
||
count: 10,
|
||
tag: undefined,
|
||
level: undefined,
|
||
disable: undefined,
|
||
key: ''
|
||
})
|
||
|
||
// 监听视图模式变化
|
||
watch(viewMode, (newVal) => {
|
||
if (newVal === 'tree') {
|
||
fetchGroupList()
|
||
} else {
|
||
queryParams.page = 1
|
||
fetchGroupList()
|
||
}
|
||
})
|
||
|
||
// 商品分组表单
|
||
const groupForm = reactive({
|
||
id: undefined,
|
||
name: '',
|
||
note: '',
|
||
disable: false,
|
||
level: 1,
|
||
parent_id: undefined,
|
||
cover_id: undefined,
|
||
cover_url: '',
|
||
tag_id: undefined,
|
||
index: 0
|
||
})
|
||
|
||
const groupRules = {
|
||
name: [
|
||
{ required: true, message: '请输入分组名称', trigger: 'blur' }
|
||
]
|
||
}
|
||
|
||
// 状态数据
|
||
const loading = ref(false)
|
||
const groupList = ref([])
|
||
const allGroupList = ref([])
|
||
const treeDataMap = ref(new Map())
|
||
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 productList = ref([])
|
||
const productLoading = ref(false)
|
||
const expandedGroups = ref(new Set())
|
||
const loadedProductGroups = ref(new Set())
|
||
const groupProductsMap = ref(new Map())
|
||
|
||
// 商品表单
|
||
const productForm = reactive({
|
||
id: undefined,
|
||
name: '',
|
||
table: '',
|
||
tag: '',
|
||
content: '',
|
||
cover_id: undefined,
|
||
cover_url: '',
|
||
good_group_id: undefined,
|
||
inventory_control: false,
|
||
inventory: 0,
|
||
price: 0,
|
||
pay_num: 1,
|
||
expire_time: 0,
|
||
recommend: false,
|
||
recommend_rebate: 0,
|
||
arg_type: 'all',
|
||
can_renew: true,
|
||
require_real_name: false,
|
||
sold_out: false,
|
||
max_per_user: 0,
|
||
send_notice: false,
|
||
renew_price: 0,
|
||
renew_recommend_rebate: 0
|
||
})
|
||
|
||
const productRules = {
|
||
name: [
|
||
{ required: true, message: '请输入商品名称', trigger: 'blur' }
|
||
],
|
||
content: [
|
||
{ required: true, message: '请输入商品内容', trigger: 'blur' }
|
||
],
|
||
good_group_id: [
|
||
{ required: true, message: '请选择商品分组', trigger: 'change' }
|
||
],
|
||
price: [
|
||
{ required: true, message: '请输入商品价格', trigger: 'blur' },
|
||
{ type: 'number', message: '请输入数字', trigger: 'blur' }
|
||
]
|
||
}
|
||
|
||
// 商品对话框状态
|
||
const productDialogVisible = ref(false)
|
||
const productDialogType = ref('add')
|
||
const productFormTab = ref('sales')
|
||
const productFormRef = ref(null)
|
||
const coverSelectorVisible = ref(false)
|
||
|
||
// 商品分组选择相关
|
||
const showProductGroupSelector = ref(false)
|
||
const selectedProductGroup = ref(null)
|
||
const selectedGroupName = computed(() => {
|
||
return selectedProductGroup.value?.name || ''
|
||
})
|
||
|
||
// 是否正在添加子级分组
|
||
const isAddingChild = computed(() => {
|
||
return dialogType.value === 'add' && groupForm.parent_id
|
||
})
|
||
const groupFormRef = ref(null)
|
||
|
||
// 树形表格展开控制
|
||
const treeTableRef = ref(null)
|
||
|
||
// 根据ID在树中查找原始分组对象
|
||
const findGroupById = (id) => {
|
||
const rootItems = treeDataMap.value.get(0) || []
|
||
const search = (items) => {
|
||
for (const item of items) {
|
||
if (item.id === id) return item
|
||
if (item._children && item._children.length > 0) {
|
||
const found = search(item._children)
|
||
if (found) return found
|
||
}
|
||
}
|
||
return null
|
||
}
|
||
return search(rootItems)
|
||
}
|
||
|
||
const treeVersion = ref(0)
|
||
|
||
// 树形显示数据
|
||
const treeDisplayData = computed(() => {
|
||
const _v = treeVersion.value
|
||
const result = []
|
||
const rootItems = treeDataMap.value.get(0) || []
|
||
const productsMap = groupProductsMap.value
|
||
|
||
const processGroup = (group, displayLevel) => {
|
||
result.push({
|
||
...group,
|
||
_origRef: group,
|
||
level: displayLevel,
|
||
isGroup: true
|
||
})
|
||
|
||
if (group._expanded) {
|
||
if (expandedGroups.value.has(group.id) && !loadedProductGroups.value.has(group.id)) {
|
||
loadProductsForGroup(group.id)
|
||
}
|
||
|
||
if (group._children && group._children.length > 0) {
|
||
for (const child of group._children) {
|
||
processGroup(child, displayLevel + 1)
|
||
}
|
||
}
|
||
|
||
const products = productsMap.get(group.id)
|
||
if (products && products.length > 0) {
|
||
for (const product of products) {
|
||
result.push({
|
||
...product,
|
||
level: displayLevel + 1,
|
||
isProduct: true
|
||
})
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
for (const item of rootItems) {
|
||
if (item.isGroup) {
|
||
processGroup(item, item.level || 1)
|
||
}
|
||
}
|
||
|
||
return result
|
||
})
|
||
|
||
const clearProductCache = () => {
|
||
loadedProductGroups.value.clear()
|
||
groupProductsMap.value = new Map()
|
||
console.log('清除所有商品加载缓存')
|
||
}
|
||
|
||
const loadProductsForGroup = async (groupId) => {
|
||
if (loadedProductGroups.value.has(groupId)) {
|
||
return
|
||
}
|
||
|
||
try {
|
||
const res = await getProductList({
|
||
good_group_id: groupId,
|
||
page: 1,
|
||
count: 10,
|
||
delete: false
|
||
})
|
||
|
||
if (res.data.code === 200) {
|
||
const products = res.data.data.data || []
|
||
console.log(`加载分组 ${groupId} 的商品:`, products)
|
||
|
||
loadedProductGroups.value.add(groupId)
|
||
|
||
const productItems = products.map(product => ({
|
||
id: `product_${product.id}`,
|
||
type: 'product',
|
||
isProduct: true,
|
||
data: product,
|
||
hasChildren: false,
|
||
existSub: false,
|
||
_expanded: false,
|
||
_children: [],
|
||
_loading: false,
|
||
name: product.name,
|
||
price: product.price,
|
||
image: product.image,
|
||
inventoryControl: product.inventoryControl,
|
||
inventory: product.inventory,
|
||
recommend: product.recommend,
|
||
disable: product.disable,
|
||
parentId: groupId
|
||
}))
|
||
|
||
const newMap = new Map(groupProductsMap.value)
|
||
newMap.set(groupId, productItems)
|
||
groupProductsMap.value = newMap
|
||
|
||
console.log(`成功加载 ${productItems.length} 个商品到分组 ${groupId}`)
|
||
}
|
||
} catch (error) {
|
||
console.error('加载商品失败:', error)
|
||
ElMessage.error('加载商品失败')
|
||
}
|
||
}
|
||
|
||
const handleNameCellClick = (row) => {
|
||
if (row.isGroup) {
|
||
toggleExpand(row)
|
||
} else if (row.isProduct) {
|
||
handleEditProduct(row.data, row.parentId)
|
||
}
|
||
}
|
||
|
||
const toggleExpand = async (row) => {
|
||
const group = row._origRef || findGroupById(row.id)
|
||
if (!group || group._loading) return
|
||
|
||
if (group._expanded) {
|
||
group._expanded = false
|
||
expandedGroups.value.delete(group.id)
|
||
|
||
loadedProductGroups.value.delete(group.id)
|
||
|
||
if (groupProductsMap.value.has(group.id)) {
|
||
const newMap = new Map(groupProductsMap.value)
|
||
newMap.delete(group.id)
|
||
groupProductsMap.value = newMap
|
||
}
|
||
} else {
|
||
group._expanded = true
|
||
expandedGroups.value.add(group.id)
|
||
|
||
if (group.isGroup) {
|
||
if (group.existSub && (!group._children || group._children.length === 0)) {
|
||
group._loading = true
|
||
treeVersion.value++
|
||
try {
|
||
const childLevel = (group.level || 1) + 1
|
||
const res = await getProductGroupList({
|
||
parent_id: group.id,
|
||
level: childLevel,
|
||
count: 10
|
||
})
|
||
if (res.data.code === 200) {
|
||
const children = res.data.data.data || []
|
||
console.log(`加载分组 ${group.name} 的子级分组:`, children)
|
||
group._children = children.map(child => ({
|
||
...child,
|
||
type: 'group',
|
||
isGroup: true,
|
||
_expanded: false,
|
||
_children: [],
|
||
_loading: false,
|
||
existSub: child.existSub || false
|
||
}))
|
||
allGroupList.value = [...allGroupList.value, ...children]
|
||
}
|
||
} catch (error) {
|
||
console.error('加载子级分组失败:', error)
|
||
ElMessage.error('加载子级分组失败')
|
||
} finally {
|
||
group._loading = false
|
||
}
|
||
}
|
||
|
||
loadProductsForGroup(group.id)
|
||
}
|
||
}
|
||
|
||
treeVersion.value++
|
||
}
|
||
|
||
const expandAll = async () => {
|
||
const loadAndExpand = async (items) => {
|
||
for (const item of items) {
|
||
if (item.existSub) {
|
||
if (!item._children || item._children.length === 0) {
|
||
item._origRef = item
|
||
await toggleExpand(item)
|
||
} else {
|
||
item._expanded = true
|
||
expandedGroups.value.add(item.id)
|
||
loadProductsForGroup(item.id)
|
||
}
|
||
if (item._children && item._children.length > 0) {
|
||
await loadAndExpand(item._children)
|
||
}
|
||
} else {
|
||
item._expanded = true
|
||
expandedGroups.value.add(item.id)
|
||
loadProductsForGroup(item.id)
|
||
}
|
||
}
|
||
}
|
||
|
||
const rootItems = treeDataMap.value.get(0) || []
|
||
await loadAndExpand(rootItems)
|
||
treeVersion.value++
|
||
}
|
||
|
||
const collapseAll = () => {
|
||
const collapse = (items) => {
|
||
items.forEach(item => {
|
||
item._expanded = false
|
||
if (item._children && item._children.length > 0) {
|
||
collapse(item._children)
|
||
}
|
||
})
|
||
}
|
||
|
||
const rootItems = treeDataMap.value.get(0) || []
|
||
collapse(rootItems)
|
||
expandedGroups.value.clear()
|
||
clearProductCache()
|
||
treeVersion.value++
|
||
}
|
||
|
||
// 父级选择相关
|
||
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(() => {
|
||
return allGroupList.value.filter(g => 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 currentEditTag = ref(null)
|
||
|
||
const selectedTagName = computed(() => {
|
||
if (groupForm.tag_id) {
|
||
const tag = allTagOptions.value.find(t => t.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 ''
|
||
})
|
||
|
||
const fetchTagOptionsForSelector = async () => {
|
||
tagSelectorLoading.value = true
|
||
try {
|
||
const params = { page: 1, count: 10 }
|
||
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 = []
|
||
}
|
||
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: 10 })
|
||
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
|
||
}
|
||
|
||
const handleDeleteTagFromSelector = (row) => {
|
||
ElMessageBox.confirm(`确定删除标签「${row.name}」吗?删除后使用该标签的分组将失去关联。`, '删除确认', { type: 'warning' })
|
||
.then(async () => {
|
||
try {
|
||
const res = await deleteProductGroupTag({ id: row.id })
|
||
if (res?.data?.code === 200) {
|
||
ElMessage.success('标签已删除')
|
||
fetchTagOptionsForSelector()
|
||
fetchAllTagOptions()
|
||
} else {
|
||
ElMessage.error(res?.data?.message || '删除失败')
|
||
}
|
||
} catch (e) {
|
||
ElMessage.error(e?.response?.data?.message || '删除失败')
|
||
}
|
||
})
|
||
.catch(() => {})
|
||
}
|
||
|
||
watch(showTagSelector, (val) => {
|
||
if (val) {
|
||
tagSelectorSearch.value = ''
|
||
fetchTagOptionsForSelector()
|
||
}
|
||
})
|
||
|
||
const getLevelText = (level) => {
|
||
return `${level}级`
|
||
}
|
||
|
||
const getLevelType = (level) => {
|
||
const types = ['primary', 'success', 'warning', 'danger', 'info']
|
||
return types[(level - 1) % types.length] || 'info'
|
||
}
|
||
|
||
const getLevelColor = (level) => {
|
||
const colors = ['#409eff', '#67c23a', '#e6a23c', '#f56c6c', '#909399', '#9c27b0', '#00bcd4', '#ff9800']
|
||
return colors[(level - 1) % colors.length] || '#909399'
|
||
}
|
||
|
||
const getParentName = (parentId) => {
|
||
const parent = allGroupList.value.find(g => g.id === parentId)
|
||
return parent ? parent.name : `ID: ${parentId}`
|
||
}
|
||
|
||
const fetchGroupList = async () => {
|
||
loading.value = true
|
||
clearProductCache()
|
||
|
||
try {
|
||
const params = { ...queryParams }
|
||
const hasFilter = params.tag || params.key || params.disable !== undefined || params.level !== undefined
|
||
|
||
if (viewMode.value === 'tree' && !hasFilter) {
|
||
params.level = 1
|
||
delete params.page
|
||
} else if (viewMode.value === 'tree' && hasFilter) {
|
||
delete params.page
|
||
}
|
||
|
||
if (!params.tag) delete params.tag
|
||
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 || []
|
||
|
||
if (viewMode.value === 'tree') {
|
||
if (hasFilter) {
|
||
const treeResult = buildTreeFromFilteredData(data)
|
||
treeDataMap.value.set(0, treeResult)
|
||
} else {
|
||
const rootItems = data.map(item => ({
|
||
...item,
|
||
type: 'group',
|
||
isGroup: true,
|
||
_expanded: false,
|
||
_children: [],
|
||
_loading: false
|
||
}))
|
||
treeDataMap.value.set(0, rootItems)
|
||
}
|
||
allGroupList.value = data
|
||
} else {
|
||
groupList.value = data
|
||
allGroupList.value = data
|
||
}
|
||
|
||
total.value = res.data.data.all_count || data.length
|
||
}
|
||
} catch (error) {
|
||
ElMessage.error('获取商品分组列表失败')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
const buildTreeFromFilteredData = (data) => {
|
||
const roots = []
|
||
const nodeMap = new Map()
|
||
data.forEach(item => {
|
||
nodeMap.set(item.id, {
|
||
...item,
|
||
type: 'group',
|
||
isGroup: true,
|
||
_expanded: true,
|
||
_children: [],
|
||
_loading: false
|
||
})
|
||
})
|
||
|
||
data.forEach(item => {
|
||
const node = nodeMap.get(item.id)
|
||
const parentId = item.parentId || 0
|
||
|
||
if (parentId === 0 || !nodeMap.has(parentId)) {
|
||
roots.push(node)
|
||
} else {
|
||
const parent = nodeMap.get(parentId)
|
||
parent._children.push(node)
|
||
parent.existSub = true
|
||
}
|
||
})
|
||
|
||
roots.sort((a, b) => a.level - b.level)
|
||
return roots
|
||
}
|
||
|
||
const resetQuery = () => {
|
||
queryParams.tag = undefined
|
||
queryParams.level = undefined
|
||
queryParams.disable = undefined
|
||
queryParams.key = ''
|
||
queryParams.page = 1
|
||
treeDataMap.value.clear()
|
||
fetchGroupList()
|
||
}
|
||
|
||
const handleSizeChange = (size) => {
|
||
queryParams.count = size
|
||
fetchGroupList()
|
||
}
|
||
|
||
const handleCurrentChange = (page) => {
|
||
queryParams.page = page
|
||
fetchGroupList()
|
||
}
|
||
|
||
const handleAdd = (parentRow) => {
|
||
dialogType.value = 'add'
|
||
groupFormRef.value?.resetFields()
|
||
|
||
if (parentRow) {
|
||
const parentTagId = parentRow.tag?.id || parentRow.tagId
|
||
currentEditTag.value = parentRow.tag || null
|
||
|
||
Object.assign(groupForm, {
|
||
id: undefined,
|
||
name: '',
|
||
note: '',
|
||
disable: false,
|
||
level: parentRow.level + 1,
|
||
parent_id: parentRow.id,
|
||
cover_id: undefined,
|
||
cover_url: '',
|
||
tag_id: parentTagId || undefined,
|
||
index: 0
|
||
})
|
||
console.log('添加子级,父级信息:', parentRow.name, 'ID:', parentRow.id, 'Level:', parentRow.level, '标签:', parentRow.tag)
|
||
} else {
|
||
currentEditTag.value = null
|
||
Object.assign(groupForm, {
|
||
id: undefined,
|
||
name: '',
|
||
note: '',
|
||
disable: false,
|
||
level: 1,
|
||
parent_id: undefined,
|
||
cover_id: undefined,
|
||
cover_url: '',
|
||
tag_id: undefined,
|
||
index: 0
|
||
})
|
||
}
|
||
|
||
dialogVisible.value = true
|
||
}
|
||
|
||
const handleEdit = (row) => {
|
||
dialogType.value = 'edit'
|
||
currentEditTag.value = row.tag || null
|
||
|
||
Object.assign(groupForm, {
|
||
id: row.id,
|
||
name: row.name,
|
||
note: row.note || '',
|
||
disable: row.disable === true,
|
||
level: row.level || 1,
|
||
parent_id: row.parentId || undefined,
|
||
cover_id: row.coverId || undefined,
|
||
cover_url: row.cover || '',
|
||
tag_id: row.tag?.id || row.tagId || undefined,
|
||
index: row.index || 0
|
||
})
|
||
|
||
dialogVisible.value = true
|
||
|
||
console.log('编辑分组数据:', row, '原始disable:', row.disable)
|
||
console.log('表单数据:', groupForm, '表单disable:', groupForm.disable)
|
||
}
|
||
|
||
const handleDeleteProduct = async (product) => {
|
||
try {
|
||
await ElMessageBox.confirm(
|
||
`确定要删除商品"${product.name}"吗?此操作不可恢复!`,
|
||
'删除确认',
|
||
{
|
||
confirmButtonText: '确定删除',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
}
|
||
)
|
||
|
||
const res = await deleteProduct({ id: product.id })
|
||
if (res.data.code === 200) {
|
||
ElMessage.success('删除商品成功')
|
||
|
||
const index = productList.value.findIndex(p => p.id === product.id)
|
||
if (index > -1) {
|
||
productList.value.splice(index, 1)
|
||
}
|
||
|
||
const groupId = product.good_group_id || product.goodGroupId
|
||
if (groupId) {
|
||
loadedProductGroups.value.delete(groupId)
|
||
const newMap = new Map(groupProductsMap.value)
|
||
newMap.delete(groupId)
|
||
groupProductsMap.value = newMap
|
||
loadProductsForGroup(groupId)
|
||
}
|
||
}
|
||
} catch (error) {
|
||
if (error !== 'cancel') {
|
||
console.error('删除商品失败:', error)
|
||
ElMessage.error('删除商品失败')
|
||
}
|
||
}
|
||
}
|
||
|
||
const handleAddProduct = () => {
|
||
productDialogType.value = 'add'
|
||
productFormRef.value?.resetFields()
|
||
|
||
Object.assign(productForm, {
|
||
id: undefined,
|
||
name: '',
|
||
table: '',
|
||
tag: '',
|
||
content: '',
|
||
cover_id: undefined,
|
||
cover_url: '',
|
||
good_group_id: undefined,
|
||
inventory_control: false,
|
||
inventory: 0,
|
||
price: 0,
|
||
pay_num: 1,
|
||
expire_time: 0,
|
||
recommend: false,
|
||
recommend_rebate: 0,
|
||
arg_type: 'all',
|
||
require_real_name: false
|
||
})
|
||
|
||
selectedProductGroup.value = null
|
||
productDialogVisible.value = true
|
||
}
|
||
|
||
const handleEditProduct = (product, parentGroupId) => {
|
||
productDialogType.value = 'edit'
|
||
|
||
const groupId = parentGroupId || product.good_group_id || product.goodGroupId
|
||
|
||
if (groupId) {
|
||
selectedProductGroup.value = allGroupList.value.find(g => g.id === groupId)
|
||
}
|
||
|
||
Object.assign(productForm, {
|
||
id: product.id,
|
||
name: product.name || '',
|
||
table: product.table || '',
|
||
tag: typeof product.tag === 'string' ? product.tag : '',
|
||
content: product.content || '',
|
||
cover_id: product.coverId,
|
||
cover_url: product.cover || '',
|
||
good_group_id: groupId,
|
||
inventory_control: product.inventoryControl,
|
||
inventory: product.inventory,
|
||
price: product.price / 100,
|
||
pay_num: product.payNum,
|
||
expire_time: product.expireTime,
|
||
recommend: product.recommend,
|
||
recommend_rebate: product.recommendRebate,
|
||
arg_type: product.argType || 'all',
|
||
can_renew: product.canRenew !== undefined ? product.canRenew : (product.can_renew !== undefined ? product.can_renew : true),
|
||
require_real_name: product.requireRealName ?? product.require_real_name ?? false,
|
||
sold_out: !!product.soldOut,
|
||
max_per_user: product.maxPerUser ?? product.max_per_user ?? 0,
|
||
send_notice: !!product.sendNotice,
|
||
renew_price: (product.renewPrice ?? product.renew_price ?? 0) / 100,
|
||
renew_recommend_rebate: product.renewRecommendRebate ?? product.renew_recommend_rebate ?? 0
|
||
})
|
||
|
||
productDialogVisible.value = true
|
||
}
|
||
|
||
const submitProductForm = () => {
|
||
productFormRef.value?.validate(async (valid) => {
|
||
if (valid) {
|
||
try {
|
||
const submitData = {
|
||
name: (productForm.name || '').trim(),
|
||
table: (productForm.table || '').trim(),
|
||
tag: (typeof productForm.tag === 'string' ? productForm.tag : '').trim(),
|
||
content: (productForm.content || '').trim(),
|
||
cover_id: productForm.cover_id,
|
||
good_group_id: productForm.good_group_id,
|
||
inventory_control: productForm.inventory_control,
|
||
inventory: Number(productForm.inventory) || 0,
|
||
price: Number(productForm.price) || 0,
|
||
pay_num: Number(productForm.pay_num) || 1,
|
||
expire_time: Number(productForm.expire_time) || 0,
|
||
recommend: productForm.recommend,
|
||
recommend_rebate: Number(productForm.recommend_rebate) || 0,
|
||
arg_type: productForm.arg_type,
|
||
can_renew: productForm.can_renew,
|
||
require_real_name: productForm.require_real_name,
|
||
sold_out: productForm.sold_out === true,
|
||
max_per_user: Number(productForm.max_per_user) || 0,
|
||
send_notice: productForm.send_notice === true,
|
||
renew_price: Number(productForm.renew_price) || 0,
|
||
renew_recommend_rebate: Number(productForm.renew_recommend_rebate) || 0
|
||
}
|
||
|
||
let res
|
||
if (productDialogType.value === 'add') {
|
||
res = await createProduct(submitData)
|
||
} else {
|
||
submitData.id = productForm.id
|
||
res = await updateProduct(submitData)
|
||
}
|
||
|
||
if (res.data.code === 200) {
|
||
ElMessage.success(productDialogType.value === 'add' ? '新增成功' : '修改成功')
|
||
productDialogVisible.value = false
|
||
|
||
if (productForm.good_group_id) {
|
||
loadedProductGroups.value.delete(productForm.good_group_id)
|
||
const newMap = new Map(groupProductsMap.value)
|
||
newMap.delete(productForm.good_group_id)
|
||
groupProductsMap.value = newMap
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('提交失败:', error)
|
||
ElMessage.error('操作失败')
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
const handleProductGroupSelect = (row) => {
|
||
selectedProductGroup.value = row
|
||
}
|
||
|
||
const confirmProductGroupSelect = () => {
|
||
if (selectedProductGroup.value) {
|
||
productForm.good_group_id = selectedProductGroup.value.id
|
||
showProductGroupSelector.value = false
|
||
}
|
||
}
|
||
|
||
const clearProductGroup = () => {
|
||
productForm.good_group_id = undefined
|
||
selectedProductGroup.value = null
|
||
}
|
||
|
||
const handleProductCoverSelect = (file) => {
|
||
productForm.cover_id = file.cover_id
|
||
productForm.cover_url = file.url || ''
|
||
coverSelectorVisible.value = false
|
||
}
|
||
|
||
const clearProductCover = () => {
|
||
productForm.cover_id = undefined
|
||
productForm.cover_url = ''
|
||
}
|
||
|
||
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
|
||
groupForm.cover_url = file.url || ''
|
||
}
|
||
|
||
const clearGroupCover = () => {
|
||
groupForm.cover_id = undefined
|
||
groupForm.cover_url = ''
|
||
}
|
||
|
||
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,
|
||
index: Number(groupForm.index) || 0
|
||
}
|
||
|
||
if (groupForm.parent_id) {
|
||
submitData.parent_id = Number(groupForm.parent_id)
|
||
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 {
|
||
submitData.level = String(groupForm.level || 2)
|
||
}
|
||
console.log('计算得到的level:', submitData.level, '父级level:', parentGroup?.level)
|
||
} else {
|
||
submitData.level = '1'
|
||
}
|
||
|
||
if (groupForm.cover_id) {
|
||
submitData.cover_id = Number(groupForm.cover_id)
|
||
}
|
||
if (groupForm.tag_id) {
|
||
submitData.tag_id = Number(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 paramDialogVisible = ref(false)
|
||
const currentProductId = ref(null)
|
||
|
||
const handleParameter = (row) => {
|
||
currentProductId.value = row.id
|
||
paramDialogVisible.value = true
|
||
}
|
||
|
||
// ==================== 商品套餐管理(子组件通信) ====================
|
||
const planDialogVisible = ref(false)
|
||
const currentPlanProductId = ref(null)
|
||
const currentPlanProductName = ref('')
|
||
|
||
const handlePlan = (row) => {
|
||
currentPlanProductId.value = row.id
|
||
currentPlanProductName.value = row.name
|
||
planDialogVisible.value = true
|
||
}
|
||
|
||
// 监听Tab切换
|
||
watch(activeTab, (newVal) => {
|
||
if (newVal === 'tag') {
|
||
groupTagManagerRef.value?.fetchTagList()
|
||
}
|
||
})
|
||
|
||
const groupTagManagerRef = ref(null)
|
||
|
||
// 初始化
|
||
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;
|
||
align-items: center;
|
||
}
|
||
|
||
.view-switch {
|
||
margin-left: 8px;
|
||
}
|
||
|
||
.view-switch :deep(.el-radio-button__inner) {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
padding: 8px 16px;
|
||
}
|
||
|
||
.view-switch :deep(.el-icon) {
|
||
font-size: 14px;
|
||
}
|
||
|
||
.table-section {
|
||
padding: 0;
|
||
}
|
||
|
||
.group-name-cell {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
border-radius: 4px;
|
||
transition: background-color 0.2s;
|
||
}
|
||
|
||
.group-name-cell.is-clickable {
|
||
cursor: pointer;
|
||
user-select: none;
|
||
}
|
||
|
||
.group-name-cell.is-clickable:hover {
|
||
background-color: #f0f7ff;
|
||
}
|
||
|
||
.group-name-cell.is-clickable:hover .expand-icon {
|
||
color: #409eff;
|
||
}
|
||
|
||
.group-name {
|
||
font-weight: 500;
|
||
}
|
||
|
||
.expand-icon {
|
||
width: 20px;
|
||
height: 20px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
cursor: pointer;
|
||
flex-shrink: 0;
|
||
color: #909399;
|
||
transition: transform 0.2s;
|
||
}
|
||
|
||
.expand-icon:hover {
|
||
color: #409eff;
|
||
}
|
||
|
||
.expand-icon .el-icon {
|
||
transition: transform 0.2s;
|
||
}
|
||
|
||
.expand-icon .el-icon.is-expanded {
|
||
transform: rotate(90deg);
|
||
}
|
||
|
||
.expand-placeholder {
|
||
width: 20px;
|
||
height: 20px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.text-muted {
|
||
color: #c0c4cc;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.action-buttons {
|
||
display: flex;
|
||
gap: 4px;
|
||
align-items: center;
|
||
flex-wrap: nowrap;
|
||
}
|
||
|
||
.action-buttons .el-button {
|
||
padding: 4px 8px;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.form-tip {
|
||
font-size: 12px;
|
||
color: #909399;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.unit-input-row { display: flex; align-items: center; gap: 6px; width: 100%; }
|
||
.unit-text { font-size: 13px; color: #606266; flex-shrink: 0; white-space: nowrap; }
|
||
|
||
.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;
|
||
color: #f56c6c;
|
||
}
|
||
|
||
/* 商品表单优化布局 */
|
||
.product-form-dialog :deep(.el-dialog__body) {
|
||
padding-top: 16px;
|
||
}
|
||
|
||
.product-form {
|
||
--section-gap: 20px;
|
||
}
|
||
|
||
.product-form-header {
|
||
display: flex;
|
||
gap: 24px;
|
||
padding: 16px;
|
||
background: #fafbfc;
|
||
border: 1px solid #ebeef5;
|
||
border-radius: 8px;
|
||
margin-bottom: var(--section-gap);
|
||
}
|
||
|
||
.cover-uploader {
|
||
position: relative;
|
||
width: 180px;
|
||
height: 180px;
|
||
flex-shrink: 0;
|
||
border: 1px dashed #dcdfe6;
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
cursor: pointer;
|
||
background: #fff;
|
||
transition: border-color 0.2s;
|
||
}
|
||
|
||
.cover-uploader:hover {
|
||
border-color: #409eff;
|
||
}
|
||
|
||
.cover-image {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
display: block;
|
||
}
|
||
|
||
.cover-placeholder {
|
||
width: 100%;
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
color: #909399;
|
||
}
|
||
|
||
.cover-placeholder-icon {
|
||
font-size: 40px;
|
||
}
|
||
|
||
.cover-placeholder-text {
|
||
font-size: 13px;
|
||
}
|
||
|
||
.cover-mask {
|
||
position: absolute;
|
||
inset: 0;
|
||
background: rgba(0, 0, 0, 0.45);
|
||
color: #fff;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 6px;
|
||
opacity: 0;
|
||
transition: opacity 0.2s;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.cover-mask .el-icon {
|
||
font-size: 22px;
|
||
}
|
||
|
||
.cover-uploader:hover .cover-mask {
|
||
opacity: 1;
|
||
}
|
||
|
||
.cover-clear-btn {
|
||
position: absolute;
|
||
top: 8px;
|
||
right: 8px;
|
||
z-index: 2;
|
||
opacity: 0;
|
||
transition: opacity 0.2s;
|
||
}
|
||
|
||
.cover-uploader:hover .cover-clear-btn {
|
||
opacity: 1;
|
||
}
|
||
|
||
.basic-fields {
|
||
flex: 1;
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.basic-fields :deep(.el-form-item) {
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.basic-fields :deep(.el-form-item:last-child) {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.group-picker {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
width: 100%;
|
||
height: 32px;
|
||
padding: 0 11px;
|
||
border: 1px solid #dcdfe6;
|
||
border-radius: 4px;
|
||
background: #fff;
|
||
cursor: pointer;
|
||
color: #303133;
|
||
font-size: 14px;
|
||
transition: border-color 0.2s;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.group-picker:hover {
|
||
border-color: #c0c4cc;
|
||
}
|
||
|
||
.group-picker.is-empty .group-picker-text {
|
||
color: #a8abb2;
|
||
}
|
||
|
||
.group-picker.is-disabled {
|
||
background: #f5f7fa;
|
||
cursor: not-allowed;
|
||
color: #a8abb2;
|
||
}
|
||
|
||
.group-picker.is-disabled:hover {
|
||
border-color: #dcdfe6;
|
||
}
|
||
|
||
.group-picker.is-disabled .group-picker-text,
|
||
.group-picker.is-disabled .group-picker-icon {
|
||
color: #a8abb2;
|
||
}
|
||
|
||
.note-form-item {
|
||
margin-bottom: 0 !important;
|
||
padding: 16px;
|
||
border: 1px solid #ebeef5;
|
||
border-radius: 8px;
|
||
background: #fff;
|
||
}
|
||
|
||
.group-status-item :deep(.el-radio-button__inner) {
|
||
padding: 6px 18px;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.group-picker-icon {
|
||
color: #909399;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.group-picker-text {
|
||
flex: 1;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.group-picker-clear {
|
||
flex-shrink: 0;
|
||
padding: 0 4px;
|
||
}
|
||
|
||
.group-picker-arrow {
|
||
color: #c0c4cc;
|
||
font-size: 12px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.product-form-tabs {
|
||
margin-top: 16px;
|
||
}
|
||
|
||
.product-form-tabs :deep(.el-tabs__header) {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.product-form-tabs :deep(.el-tab-pane) {
|
||
padding: 4px 0;
|
||
}
|
||
|
||
.form-section {
|
||
margin-bottom: var(--section-gap);
|
||
padding: 16px;
|
||
border: 1px solid #ebeef5;
|
||
border-radius: 8px;
|
||
background: #fff;
|
||
}
|
||
|
||
.form-section:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.form-section-title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #303133;
|
||
margin-bottom: 12px;
|
||
padding-left: 8px;
|
||
border-left: 3px solid #409eff;
|
||
line-height: 1.2;
|
||
}
|
||
|
||
.form-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 4px 24px;
|
||
}
|
||
|
||
.form-grid :deep(.el-form-item) {
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.form-grid .arg-type-item {
|
||
grid-column: span 2;
|
||
}
|
||
|
||
.unit-suffix {
|
||
color: #909399;
|
||
font-weight: normal;
|
||
font-size: 12px;
|
||
margin-left: 2px;
|
||
}
|
||
|
||
.product-form :deep(.el-form-item__label) {
|
||
font-weight: 500;
|
||
padding-bottom: 4px !important;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
@media (max-width: 720px) {
|
||
.product-form-header {
|
||
flex-direction: column;
|
||
align-items: center;
|
||
}
|
||
.form-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
.form-grid .arg-type-item {
|
||
grid-column: span 1;
|
||
}
|
||
}
|
||
|
||
.product-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
}
|
||
|
||
.product-info .price {
|
||
font-weight: 600;
|
||
color: #e74c3c;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.product-info .table-name {
|
||
font-size: 12px;
|
||
color: #606266;
|
||
}
|
||
|
||
.product-info .inventory {
|
||
font-size: 12px;
|
||
color: #909399;
|
||
}
|
||
|
||
.product-info .recommend {
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.product-info .expire-time {
|
||
font-size: 11px;
|
||
color: #909399;
|
||
}
|
||
|
||
.product-info-inline {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.product-info-inline .product-price {
|
||
font-weight: 600;
|
||
color: #e74c3c;
|
||
font-size: 14px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.product-tags {
|
||
display: flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 4px;
|
||
}
|
||
|
||
.product-tags .text-muted {
|
||
font-size: 12px;
|
||
color: #c0c4cc;
|
||
}
|
||
|
||
:deep(.el-table) {
|
||
border: none;
|
||
color: #2c3e50;
|
||
}
|
||
|
||
:deep(.el-table__expand-icon) {
|
||
display: none !important;
|
||
}
|
||
|
||
: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>
|