2025-08-18 12:00:55 +08:00

720 lines
24 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div>
<!-- 调试信息 -->
<div style="margin-bottom: 20px; padding: 15px; background: #f5f7fa; border-radius: 8px;">
<el-button size="small" @click="showDebugInfo">查看调试信息</el-button>
<el-button size="small" @click="showRawData">查看原始数据</el-button>
<el-button size="small" type="primary" @click="testTreeStructure">测试树形结构</el-button>
<el-button size="small" type="success" @click="getCategories">重新加载数据</el-button>
<span style="margin-left: 20px; color: #666;">
数据总数: {{ tableData.length }} |
树形层级: {{ getMaxLevel() }}
</span>
</div>
<el-table :data="tableData" style="width: 100%;margin-bottom: 20px;" row-key="id" border v-loading="loading"
element-loading-text="加载中..." element-loading-spinner="el-icon-loading"
:tree-props="{ children: 'children' }" :default-expand-all="false" :expand-on-click-node="false">
<el-table-column prop="name" label="名称" min-width="120">
</el-table-column>
<el-table-column prop="id" label="id" min-width="200">
</el-table-column>
<el-table-column prop="level" label="层级" min-width="120">
</el-table-column>
<el-table-column label="操作" width="280" fixed="right">
<template slot-scope="scope">
<el-button type="text" size="small" @click="addNode(scope.row, 'sibling')">新增同级</el-button>
<el-button type="text" size="small" @click="addNode(scope.row, 'child')">新增子级</el-button>
<el-button type="text" size="small" @click="editNode(scope.row)">编辑</el-button>
<el-button type="text" size="small" @click="deleteNode(scope.row)"
style="color: #F56C6C;">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-drawer title="菜单管理" :visible.sync="drawer" :direction="direction">
<div style="margin: 20px;"></div>
<el-form style="margin: 10px;" :label-position="labelPosition" label-width="100px" :model="formLabelAlign" :rules="formRules" ref="menuForm">
<el-form-item label="菜单名称" prop="name">
<el-input v-model="formLabelAlign.name" placeholder="请输入菜单名称"></el-input>
</el-form-item>
<el-form-item v-if="!formLabelAlign.parentid" label="菜单图标" prop="icon">
<div class="icon-upload-container">
<!-- 图片预览区域 -->
<div v-if="formLabelAlign.icon" class="icon-preview">
<div class="preview-wrapper">
<!-- SVG图片显示 -->
<img v-if="isSvgImage(formLabelAlign.icon)"
:src="formLabelAlign.icon"
class="preview-image svg-image"
alt="菜单图标">
<!-- 普通图片显示 -->
<img v-else
:src="formLabelAlign.icon"
class="preview-image"
alt="菜单图标">
<div class="preview-actions">
<el-button type="text" size="mini" @click="removeIcon" icon="el-icon-delete" style="color: #F56C6C;">删除</el-button>
</div>
</div>
</div>
<!-- 上传组件 -->
<el-upload
v-if="!formLabelAlign.icon"
class="icon-uploader"
action="#"
:http-request="handleIconUpload"
:show-file-list="false"
:before-upload="beforeIconUpload"
accept=".svg,.png,.jpg"
drag>
<i class="el-icon-plus uploader-icon"></i>
<div class="el-upload__text">将文件拖到此处<em>点击上传</em></div>
<!-- <div class="el-upload__tip" slot="tip">
<i class="el-icon-info"></i>
支持 SVGPNGJPG格式文件大小不超过 5MB
</div> -->
</el-upload>
</div>
</el-form-item>
<el-form-item label="菜单排序" style="margin-top: 10px!important;" prop="poriority">
<el-input-number :controls="false" v-model="formLabelAlign.poriority" :step="1" :min="0" :max="100" placeholder="请输入排序值"></el-input-number>
</el-form-item>
<el-form-item label="菜单来源" prop="source">
<el-input v-model="formLabelAlign.source" placeholder="请输入菜单来源"></el-input>
</el-form-item>
<!-- 操作按钮 -->
<el-form-item>
<el-button type="primary" @click="submitForm">保存</el-button>
<el-button @click="resetForm">重置</el-button>
<el-button @click="drawer = false">取消</el-button>
</el-form-item>
</el-form>
</el-drawer>
</div>
</template>
<script>
import { reqNcMatchMenu,reqAddProductMenu } from '@/api/ncmatch';
export default {
name: 'menuMangement',
data() {
return {
loading: false,
categories: [],
tableData: [], // 移除硬编码的测试数据
drawer: false,
direction: 'rtl',
labelPosition: 'right',
formLabelAlign: {
name: '',
icon: '',
iconFile: null, // 存储二进制文件对象
url_link: '',
parentid: '',
poriority: '',
source: '',
},
formRules: {
name: [
{ required: true, message: '请输入菜单名称', trigger: 'blur' }
],
poriority: [
{ required: true, message: '请输入菜单排序', trigger: 'blur' }
]
}
}
},
created() {
this.getCategories();
},
methods: {
// 处理图标上传
handleIconUpload(options) {
const file = options.file;
console.log('上传文件:', file);
// 创建本地预览URL
const reader = new FileReader();
reader.onload = (e) => {
this.formLabelAlign.icon = e.target.result;
this.$message.success('图片上传成功!');
};
reader.readAsDataURL(file);
// 保存文件对象到表单数据中,用于后续提交
this.formLabelAlign.iconFile = file;
},
// 上传前验证
beforeIconUpload(file) {
// 检查文件类型
const isValidType = this.isValidImageType(file.type);
if (!isValidType) {
this.$message.error('只支持 SVG、PNG、JPG格式的图片');
return false;
}
// 检查文件大小 (5MB)
const isLt5M = file.size / 1024 / 1024 < 5;
if (!isLt5M) {
this.$message.error('图片大小不能超过 5MB');
return false;
}
return true;
},
// 验证图片类型
isValidImageType(type) {
const validTypes = [
'image/svg+xml',
'image/png',
'image/jpeg',
'image/jpg',
'image/gif',
'image/webp'
];
return validTypes.includes(type);
},
// 判断是否为SVG图片
isSvgImage(src) {
return src && (src.includes('.svg') || src.startsWith('data:image/svg+xml'));
},
// 删除图标
removeIcon() {
this.$confirm('确定要删除这个图标吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.formLabelAlign.icon = '';
this.formLabelAlign.iconFile = null;
this.$message.success('图标已删除');
}).catch(() => {});
},
// 提交表单
submitForm() {
this.$refs.menuForm.validate((valid) => {
if (valid) {
// 创建FormData对象
const formData = new FormData();
// 添加基本表单数据
formData.append('name', this.formLabelAlign.name);
formData.append('poriority', this.formLabelAlign.poriority);
formData.append('source', this.formLabelAlign.source);
formData.append('url_link', window.location.href);
// 只有当parentid为null时才传递图片
if (!this.formLabelAlign.parentid || this.formLabelAlign.parentid === '' || this.formLabelAlign.parentid === '0') {
if (this.formLabelAlign.iconFile) {
formData.append('icon', this.formLabelAlign.iconFile);
console.log('包含图片上传');
} else if (this.formLabelAlign.icon) {
// 如果有现有图片URL也传递
formData.append('iconUrl', this.formLabelAlign.icon);
console.log('包含现有图片URL');
}
} else {
console.log('跳过图片上传parentid不为null');
}
// 添加parentid
if (this.formLabelAlign.parentid) {
formData.append('parentid', this.formLabelAlign.parentid);
}
// 打印FormData内容调试用
for (let [key, value] of formData.entries()) {
console.log(`${key}:`, value);
}
// 这里调用API保存数据
this.saveMenuData(formData);
} else {
this.$message.error('请检查表单信息!');
return false;
}
});
},
// 保存菜单数据
saveMenuData(formData) {
// 这里添加实际的API调用
// 例如:
reqAddProductMenu(formData).then(res => {
if (res.status) {
this.$message.success('保存成功!');
this.drawer = false;
this.getCategories(); // 刷新数据
}else{
this.$message.error(res.msg);
}
}).catch(error => {
this.$message.error('保存失败:' + error.message);
});
// // 临时模拟成功
// this.$message.success('保存成功!');
// this.drawer = false;
// console.log('FormData数据已准备就绪可以发送到服务器');
},
// 重置表单
resetForm() {
this.$refs.menuForm.resetFields();
this.formLabelAlign.icon = '';
this.formLabelAlign.iconFile = null;
},
addNode(row, type) {
this.drawer = true;
this.resetForm();
if (type === 'child') {
this.formLabelAlign.parentid = row.id;
} else if (type === 'sibling') {
this.formLabelAlign.parentid = row.parentid;
}
console.log('新增节点:', row, '类型:', type);
},
getCategories() {
this.loading = true;
reqNcMatchMenu({ url_link: window.location.href }).then(res => {
this.loading = false;
if (res.status) {
this.categories = this.buildSimpleTree(res.data);
this.tableData = this.categories; // 使用精简的树形结构
console.log("构建的精简树结构:", this.categories);
}
}).catch(error => {
this.loading = false;
console.error('获取菜单数据失败:', error);
});
},
editNode(row) {
this.drawer = true;
this.formLabelAlign = { ...row };
// 编辑时清除iconFile避免重复上传
this.formLabelAlign.iconFile = null;
console.log('编辑节点:', row);
},
deleteNode(row) {
this.$confirm('确定要删除这个菜单吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
console.log('删除节点:', row);
this.$message.success('删除成功!');
}).catch(() => {});
},
buildTree(data, parentId = null) {
if (!Array.isArray(data)) {
console.log('数据不是数组:', data);
return [];
}
console.log('构建树结构当前parentId:', parentId, '数据长度:', data.length);
// 找出当前层级的节点
const currentNodes = data.filter(item => {
// 处理顶级节点parentid 为 null 或空字符串)
if (parentId === null) {
return !item.parentid || item.parentid === '' || item.parentid === '0';
}
return item.parentid === parentId;
});
console.log('当前层级节点数量:', currentNodes.length, '节点:', currentNodes);
if (currentNodes.length === 0) return [];
// 处理每个节点
return currentNodes.map(node => {
const resultNode = {
id: node.id,
name: node.name || '未命名',
parentid: node.parentid
};
// 递归处理子节点
const children = this.buildSimpleTree(data, node.id);
// 只有当有子节点时才添加 children 字段
if (children && children.length > 0) {
resultNode.children = children;
}
return resultNode;
});
},
// 精简的树形结构构建方法
buildSimpleTree(data, parentId = null) {
if (!Array.isArray(data)) {
return [];
}
// 找出当前层级的节点
const currentNodes = data.filter(item => {
if (parentId === null) {
return !item.parentid || item.parentid === '' || item.parentid === '0';
}
return item.parentid === parentId;
});
if (currentNodes.length === 0) return [];
// 处理每个节点,只保留必要字段
return currentNodes.map(node => {
const resultNode = {
id: node.id,
name: node.name || '未命名',
parentid: node.parentid
};
// 递归处理子节点
const children = this.buildSimpleTree(data, node.id);
// 只有当有子节点时才添加 children 字段
if (children && children.length > 0) {
resultNode.children = children;
}
return resultNode;
});
},
// 计算节点层级
calculateLevel(data, nodeId) {
let level = 1;
let currentId = nodeId;
while (true) {
const parent = data.find(item => item.id === currentId);
if (!parent || !parent.parentid || parent.parentid === '' || parent.parentid === '0') {
break;
}
level++;
currentId = parent.parentid;
}
return level;
},
showDebugInfo() {
console.log("当前 tableData 的结构:", this.tableData);
console.log("当前 tableData 的层级分布:", this.getLevelDistribution());
console.log("当前 tableData 的最大层级:", this.getMaxLevel());
// 检查树形结构
this.checkTreeStructure(this.tableData);
},
// 检查树形结构
checkTreeStructure(nodes, level = 0) {
if (!Array.isArray(nodes)) return;
nodes.forEach(node => {
const indent = ' '.repeat(level);
console.log(`${indent}📁 ${node.name} (ID: ${node.id})`);
console.log(`${indent} - hasChildren: ${node.hasChildren}`);
console.log(`${indent} - children.length: ${node.children ? node.children.length : 'undefined'}`);
console.log(`${indent} - level: ${node.level}`);
if (node.children && node.children.length > 0) {
this.checkTreeStructure(node.children, level + 1);
}
});
},
showRawData() {
console.log("原始数据:", this.categories);
},
// 测试树形结构
testTreeStructure() {
const testData = [
{
id: 1,
name: '一级菜单1',
parentid: null
},
{
id: 2,
name: '一级菜单2',
parentid: null
},
{
id: 3,
name: '一级菜单3',
parentid: null,
children: [
{
id: 31,
name: '二级菜单3-1',
parentid: 3
},
{
id: 32,
name: '二级菜单3-2',
parentid: 3,
children: [
{
id: 321,
name: '三级菜单3-2-1',
parentid: 32
}
]
}
]
}
];
console.log("精简测试数据:", testData);
this.tableData = testData;
},
getLevelDistribution() {
const levels = {};
this.tableData.forEach(node => {
const level = node.level;
if (levels[level]) {
levels[level]++;
} else {
levels[level] = 1;
}
});
return levels;
},
getMaxLevel() {
let maxLevel = 0;
this.tableData.forEach(node => {
if (node.level > maxLevel) {
maxLevel = node.level;
}
});
return maxLevel;
}
}
}
</script>
<style scoped>
.el-table {
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.el-table th {
background-color: #f5f7fa;
color: #606266;
font-weight: 600;
}
.el-table td {
padding: 12px 0;
}
.el-button--text {
padding: 4px 8px;
margin: 0 2px;
}
.el-button--text:hover {
background-color: #f5f7fa;
border-radius: 4px;
}
.el-tag {
border-radius: 4px;
}
/* 树形表格的缩进样式 */
.el-table__expand-icon {
margin-right: 8px;
}
/* 加载状态样式 */
.el-loading-mask {
background-color: rgba(255, 255, 255, 0.8);
}
.license-uploader {
border: 2px dashed #d1d9e0;
border-radius: 12px;
cursor: pointer;
position: relative;
overflow: hidden;
width:150px;
height: 150px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&:hover {
border-color: #667eea;
background: linear-gradient(135deg, #f0f4ff 0%, #e8f0ff 100%);
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.2);
}
.license-uploader-icon {
font-size: 28px;
color: #667eea;
width: 100%;
height: 160px;
line-height: 160px;
text-align: center;
transition: all 0.3s ease;
}
.license-image {
width: 100%;
height: 160px;
display: block;
object-fit: cover;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
}
/* 图标上传容器样式 */
.icon-upload-container {
width: 100%;
}
/* 图标预览样式 */
.icon-preview {
margin-bottom: 15px;
}
.preview-wrapper {
position: relative;
display: inline-block;
border: 2px dashed #d9d9d9;
border-radius: 8px;
padding: 10px;
background: #fafafa;
transition: all 0.3s;
}
.preview-wrapper:hover {
border-color: #409eff;
background: #f0f9ff;
}
.preview-image {
max-width: 120px;
max-height: 120px;
object-fit: contain;
border-radius: 4px;
display: block;
}
.svg-image {
background: linear-gradient(45deg, #f0f0f0 25%, transparent 25%),
linear-gradient(-45deg, #f0f0f0 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #f0f0f0 75%),
linear-gradient(-45deg, transparent 75%, #f0f0f0 75%);
background-size: 20px 20px;
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
}
.preview-actions {
margin-top: 10px;
text-align: center;
}
.preview-actions .el-button {
margin: 0 5px;
}
/* 上传组件样式 */
.icon-uploader {
width: 150px;
height: 150px;
}
.icon-uploader .el-upload {
width: 100%;
}
::v-deep .icon-uploader .el-upload-dragger {
width: 150px!important;
height: 150px!important;
border: 2px dashed #d9d9d9;
border-radius: 8px;
background: #fafafa;
transition: all 0.3s;
display: flex ;
flex-direction: column;
align-items: center;
justify-content:center;
}
.icon-uploader .el-upload-dragger:hover {
border-color: #409eff;
background: #f0f9ff;
}
.uploader-icon {
font-size: 28px;
color: #8c939d;
margin-bottom: 10px;
}
.el-upload__text {
color: #606266;
font-size: 14px;
margin-bottom: 5px;
}
.el-upload__text em {
color: #409eff;
font-style: normal;
}
.el-upload__tip {
font-size: 12px;
color: #909399;
margin-top: 10px;
}
/* 表单样式优化 */
.el-form-item {
margin-bottom: 20px;
}
.el-form-item__label {
font-weight: 500;
color: #606266;
}
/* 响应式设计 */
@media (max-width: 768px) {
.preview-image {
max-width: 80px;
max-height: 80px;
}
.icon-uploader .el-upload-dragger {
height: 100px;
}
}
</style>