2025-04-07 22:28:03 +08:00

778 lines
25 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 class="app-container">
<el-row :gutter="20">
<!-- 左侧树形结构 -->
<el-col :span="6">
<div class="tree-container">
<div class="tree-header">
<span class="tree-title">资产分类</span>
</div>
<el-divider class="tree-divider"></el-divider>
<el-tree
ref="classTree"
:data="classTree"
:props="defaultProps"
node-key="id"
:expand-on-click-node="false"
highlight-current
@node-click="handleNodeClick"
:default-expanded-keys="defaultExpandedKeys">
<span class="custom-tree-node" slot-scope="{ node, data }">
<span>{{ node.label }}</span>
<span v-if="data.children && data.children.length > 0" class="node-count">
({{ data.children.length }})
</span>
</span>
</el-tree>
</div>
</el-col>
<!-- 右侧列表 -->
<el-col :span="18">
<el-card class="box-card">
<div slot="header" class="clearfix">
<span>分类列表</span>
<el-button
style="float: right; margin-left: 10px;"
type="primary"
icon="el-icon-plus"
size="mini"
@click="handleAdd">新增分类</el-button>
<el-button
style="float: right;"
type="danger"
icon="el-icon-delete"
size="mini"
:disabled="multipleSelection.length === 0"
@click="handleBatchDelete">批量删除</el-button>
</div>
<!-- 搜索表单 -->
<el-form :inline="true" :model="queryParams" class="demo-form-inline">
<el-form-item label="分类编码">
<el-input v-model="queryParams.classificationCode" placeholder="请输入分类编码" clearable size="small" />
</el-form-item>
<el-form-item label="分类名称">
<el-input v-model="queryParams.classificationName" placeholder="请输入分类名称" clearable size="small" />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable size="small">
<el-option label="启用" value="1" />
<el-option label="禁用" value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-table
v-loading="loading"
:data="classList"
border
stripe
style="width: 100%"
@selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" />
<el-table-column prop="classificationCode" label="分类编码" width="150" />
<el-table-column prop="classificationName" label="分类名称" width="200" />
<el-table-column label="上级分类" width="200">
<template slot-scope="scope">
<span>{{ getParentName(scope.row.parentId) }}</span>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template slot-scope="scope">
<el-tag :type="scope.row.status === '1' ? 'success' : 'info'">
{{ scope.row.status === '1' ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" />
<el-table-column label="操作" width="180" fixed="right">
<template slot-scope="scope">
<el-button size="mini" type="text" @click="handleEdit(scope.row)">修改</el-button>
<el-button
size="mini"
type="text"
@click="handleStatusChange(scope.row)">
{{ scope.row.status === '1' ? '禁用' : '启用' }}
</el-button>
<el-button
size="mini"
type="text"
style="color: #F56C6C"
@click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="queryParams.pageNum"
:page-sizes="[10, 20, 50, 100]"
:page-size="queryParams.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
class="pagination">
</el-pagination>
</el-card>
</el-col>
</el-row>
<!-- 新增/编辑对话框 -->
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="500px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
<el-form-item label="分类编码" prop="classificationCode">
<el-input v-model="form.classificationCode" placeholder="请输入分类编码" />
</el-form-item>
<el-form-item label="分类名称" prop="classificationName">
<el-input v-model="form.classificationName" placeholder="请输入分类名称" />
</el-form-item>
<el-form-item label="上级分类" prop="parentId">
<el-cascader
v-model="form.parentId"
:options="classTreeOptions"
:props="{
checkStrictly: true,
emitPath: false,
expandTrigger: 'hover',
value: 'id',
label: 'label'
}"
clearable
filterable
placeholder="请选择上级分类"
style="width: 100%">
</el-cascader>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio label="1">启用</el-radio>
<el-radio label="0">禁用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false"> </el-button>
<el-button type="primary" @click="submitForm"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listClassification, getClassification, addClassification, updateClassification, delClassification, enableClassification, disableClassification, getClassificationTree, checkClassificationInUse, getChildClassifications, delClassificationBatch } from '@/api/asset/classification'
export default {
name: 'AssetClassification',
data() {
return {
// 遮罩层
loading: false,
// 总条数
total: 0,
// 分类列表
classList: [],
// 分类树形数据
classTree: [],
// 处理后的树形选项数据(用于级联选择器)
classTreeOptions: [],
// 默认展开的节点
defaultExpandedKeys: [],
// 树形配置(用于树组件)
defaultProps: {
children: 'children',
label: 'label'
},
// 弹出层标题
dialogTitle: '',
// 是否显示弹出层
dialogVisible: false,
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
classificationCode: undefined,
classificationName: undefined,
status: undefined,
parentId: undefined
},
// 表单参数
form: {},
// 表单校验
rules: {
classificationCode: [
{ required: true, message: '请输入分类编码', trigger: 'blur' },
{ min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' }
],
classificationName: [
{ required: true, message: '请输入分类名称', trigger: 'blur' },
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
],
parentId: [
{ required: false, message: '请选择上级分类', trigger: 'change' }
],
remark: [
{ max: 200, message: '长度不能超过200个字符', trigger: 'blur' }
]
},
// 多选选中项
multipleSelection: []
}
},
created() {
this.getTree()
this.getList()
},
methods: {
/** 展开所有节点 */
expandAll() {
const allKeys = this.getExpandedKeys(this.classTree)
this.defaultExpandedKeys = allKeys
this.$refs.classTree.setCheckedKeys([]) // 清空选中状态
this.$nextTick(() => {
if (this.$refs.classTree) {
this.$refs.classTree.$forceUpdate()
}
})
},
/** 收起所有节点 */
collapseAll() {
this.defaultExpandedKeys = []
this.$refs.classTree.setCheckedKeys([]) // 清空选中状态
this.$nextTick(() => {
if (this.$refs.classTree) {
this.$refs.classTree.$forceUpdate()
}
})
},
/** 获取分类树形结构 */
getTree() {
this.loading = true
const params = {
status: undefined // 获取所有状态的分类,包括启用和禁用
}
getClassificationTree(params).then(res => {
if (res.code === '000000') {
// 处理返回的树形数据,确保字段名称正确
this.classTree = this.processTreeData(res.data)
// 准备级联选择器的数据源
this.classTreeOptions = this.prepareTreeSelectOptions(this.classTree)
// 如果之前已经有展开的节点,保持这些节点展开
// 否则只展开第一级有子节点的节点
if (!this.defaultExpandedKeys || this.defaultExpandedKeys.length === 0) {
this.defaultExpandedKeys = this.getFirstLevelKeys(this.classTree)
}
// 强制刷新树组件
this.$nextTick(() => {
if (this.$refs.classTree) {
this.$refs.classTree.$forceUpdate()
}
})
} else {
this.$message.error(res.msg || '获取分类树失败')
}
this.loading = false
}).catch(error => {
console.error('获取分类树失败', error)
this.$message.error('系统错误,获取分类树失败')
this.loading = false
})
},
/** 获取第一级节点的key仅展开有子节点的 */
getFirstLevelKeys(tree) {
if (!tree || !Array.isArray(tree)) return []
return tree
.filter(node => node.children && node.children.length > 0)
.map(node => node.id)
},
/** 获取所有节点key用于全部展开 */
getExpandedKeys(tree, keys = []) {
for (const node of tree) {
if (node.children && node.children.length > 0) {
keys.push(node.id)
this.getExpandedKeys(node.children, keys)
}
}
return keys
},
/** 处理树形数据,确保字段名称正确 */
processTreeData(data) {
if (!data || !Array.isArray(data)) return []
const processNode = (node) => {
// 创建一个新对象,使用驼峰命名
const processedNode = {
id: node.id,
// 转换label格式为"编码-名称"
label: `${node.code || ''}-${node.classificationName || node.label || ''}`,
// 驼峰命名
classificationName: node.classificationName || node.label || '',
classificationCode: node.code || '',
status: node.status || '1',
parentId: node.parentId || null,
children: []
}
// 递归处理子节点
if (node.children && Array.isArray(node.children) && node.children.length > 0) {
processedNode.children = node.children.map(child => processNode(child))
}
return processedNode
}
return data.map(item => processNode(item))
},
/** 准备级联选择器的数据源 */
prepareTreeSelectOptions(data) {
if (!data || !Array.isArray(data)) return []
// 创建一个无上级选项
const options = [{
id: '',
label: '无上级分类',
children: []
}]
// 添加所有分类节点
const cloneData = JSON.parse(JSON.stringify(data))
options.push(...cloneData)
return options
},
/** 查询分类列表 */
getList() {
this.loading = true
listClassification(this.queryParams).then(res => {
if (res.code === '000000') {
// 转换API返回数据结构为驼峰命名
this.classList = this.convertSnakeToCamel(res.data.list)
this.total = res.data.total
} else {
this.$message.error(res.msg || '获取分类列表失败')
}
this.loading = false
}).catch(error => {
console.error('获取分类列表失败', error)
this.$message.error('系统错误,获取分类列表失败')
this.loading = false
})
},
/** 转换下划线命名为驼峰命名 */
convertSnakeToCamel(list) {
if (!list || !Array.isArray(list)) return []
return list.map(item => {
return {
id: item.id,
classificationCode: item.classification_code || item.classificationCode || '',
classificationName: item.classification_name || item.classificationName || '',
parentId: item.parent_id || item.parentId || null,
status: item.status || '1',
remark: item.remark || '',
createTime: item.create_time || item.createTime || '',
createUserId: item.create_user_id || item.createUserId || '',
lastModUserId: item.last_mod_user_id || item.lastModUserId || '',
lastModTime: item.last_mod_time || item.lastModTime || ''
}
})
},
/** 将驼峰命名转换为下划线命名(API请求前的转换) */
convertCamelToSnake(data) {
const result = {}
Object.keys(data).forEach(key => {
// 将驼峰转换为下划线
const newKey = key.replace(/([A-Z])/g, '_$1').toLowerCase()
result[newKey] = data[key]
})
return result
},
/** 节点单击事件 */
handleNodeClick(data) {
this.queryParams.parentId = data.id
// 使用新的获取子分类API
this.loading = true
getChildClassifications(data.id, {
pageNum: this.queryParams.pageNum,
pageSize: this.queryParams.pageSize
}).then(res => {
if (res.code === '000000') {
this.classList = this.convertSnakeToCamel(res.data.list)
this.total = res.data.total
} else {
this.$message.error(res.msg || '获取子分类列表失败')
}
this.loading = false
}).catch(error => {
console.error('获取子分类列表失败', error)
this.$message.error('系统错误,获取子分类列表失败')
this.loading = false
})
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1
this.queryParams.parentId = undefined
this.getList()
},
/** 重置按钮操作 */
resetQuery() {
this.queryParams = {
pageNum: 1,
pageSize: 10,
classificationCode: undefined,
classificationName: undefined,
status: undefined,
parentId: undefined
}
this.getList()
},
/** 新增按钮操作 */
handleAdd() {
this.dialogTitle = '新增分类'
this.form = {
classificationCode: '',
classificationName: '',
parentId: this.queryParams.parentId || '', // 如果当前有选中节点,则默认为父节点
status: '1',
remark: ''
}
this.dialogVisible = true
this.$nextTick(() => {
if (this.$refs.form) {
this.$refs.form.clearValidate()
}
})
},
/** 修改按钮操作 */
handleEdit(row) {
const id = row.id
getClassification(id).then(res => {
if (res.code === '000000') {
// 将API响应数据映射到表单字段使用驼峰命名
this.form = {
id: res.data.id,
classificationCode: res.data.classificationCode,
classificationName: res.data.classificationName,
parentId: res.data.parentId || '',
status: res.data.status,
remark: res.data.remark,
// 添加最后修改人
lastModUserId: this.$store.getters.userId || ''
}
this.dialogTitle = '编辑分类'
this.dialogVisible = true
// 重置表单验证
this.$nextTick(() => {
if (this.$refs.form) {
this.$refs.form.clearValidate()
}
})
} else {
this.$message.error(res.msg || '获取分类详情失败')
}
}).catch(error => {
console.error('获取分类详情失败', error)
this.$message.error('系统错误,获取分类详情失败')
})
},
/** 提交按钮 */
submitForm() {
this.$refs['form'].validate(valid => {
if (valid) {
// A. 构建提交数据映射表单字段到API字段
const submitData = {
id: this.form.id,
classificationCode: this.form.classificationCode,
classificationName: this.form.classificationName,
parentId: this.form.parentId,
status: this.form.status,
remark: this.form.remark
}
// B. 添加创建/修改用户信息
if (this.form.id) {
submitData.lastModUserId = this.form.lastModUserId || this.$store.getters.userId || ''
} else {
submitData.createUserId = this.$store.getters.userId || ''
}
const method = this.form.id ? updateClassification : addClassification
method(submitData).then(res => {
if (res.code === '000000') {
this.$message.success('操作成功')
this.dialogVisible = false
// 重新获取树形结构,确保更新多级分类
this.getTree()
// 如果是新增子分类,更新当前列表
if (this.queryParams.parentId && !this.form.id) {
this.handleNodeClick({ id: this.queryParams.parentId })
} else {
this.getList()
}
} else {
this.$message.error(res.msg || '操作失败')
}
}).catch(error => {
console.error('提交表单失败', error)
this.$message.error('系统错误,提交失败')
})
}
})
},
/** 删除按钮操作 */
handleDelete(row) {
this.$confirm('是否确认删除该分类?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 先检查分类是否被使用
checkClassificationInUse(row.id).then(res => {
if (res.code === '000000' && res.data) {
this.$message.error('该分类已被使用,无法删除')
return
}
// 未被使用,执行删除操作
const lastModUserId = this.$store.getters.userId || ''
delClassification(row.id, lastModUserId).then(res => {
if (res.code === '000000') {
this.$message.success('删除成功')
// 重新获取树形结构,确保更新多级分类
this.getTree()
this.getList()
} else {
this.$message.error(res.msg || '删除失败')
}
}).catch(error => {
console.error('删除操作失败', error)
this.$message.error('系统错误,删除失败')
})
})
}).catch(() => {})
},
/** 状态修改按钮操作 */
handleStatusChange(row) {
const action = row.status === '1' ? '禁用' : '启用'
this.$confirm(`是否确认${action}该分类?`, '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 获取当前用户ID
const lastModUserId = this.$store.getters.userId || ''
const method = row.status === '1' ? disableClassification : enableClassification
method(row.id, lastModUserId).then(res => {
if (res.code === '000000') {
this.$message.success(`${action}成功`)
// 状态变更后,可能会影响树的显示,所以也需要刷新树
this.getTree()
this.getList()
} else {
this.$message.error(res.msg || `${action}失败`)
}
}).catch(error => {
console.error(`${action}操作失败`, error)
this.$message.error(`系统错误,${action}失败`)
})
}).catch(() => {})
},
/** 分页大小改变 */
handleSizeChange(val) {
this.queryParams.pageSize = val
this.getList()
},
/** 分页页码改变 */
handleCurrentChange(val) {
this.queryParams.pageNum = val
this.getList()
},
/** 获取上级分类名称 */
getParentName(parentId) {
if (!parentId) return '无';
// 先在树形数据中查找
const findParentInTree = (tree, id) => {
for (const node of tree) {
if (node.id === id) {
return node.classificationCode + '-' + node.classificationName;
}
if (node.children && node.children.length > 0) {
const found = findParentInTree(node.children, id);
if (found) return found;
}
}
return null;
};
const parentName = findParentInTree(this.classTree, parentId);
if (parentName) return parentName;
// 如果在树中没找到,可能在列表数据中
const parent = this.classList.find(item => item.id === parentId);
return parent ? (parent.classificationCode + '-' + parent.classificationName) : '未知';
},
/** 多选选中项改变 */
handleSelectionChange(val) {
this.multipleSelection = val;
},
/** 批量删除按钮操作 */
handleBatchDelete() {
if (this.multipleSelection.length === 0) {
this.$message.warning('请至少选择一个分类')
return
}
this.$confirm('是否确认删除选中的分类?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 获取选中的分类ID列表
const selectedIds = this.multipleSelection.map(item => item.id)
// 获取当前用户ID
const lastModUserId = this.$store.getters.userId || ''
// 执行批量删除操作
this.loading = true
delClassificationBatch(selectedIds, lastModUserId).then(res => {
if (res.code === '000000') {
this.$message.success('批量删除成功')
// 重新获取树形结构,确保更新多级分类
this.getTree()
this.getList()
} else {
this.$message.error(res.msg || '批量删除失败')
}
this.loading = false
}).catch(error => {
console.error('批量删除操作失败', error)
this.$message.error('系统错误,批量删除失败')
this.loading = false
})
}).catch(() => {})
}
}
}
</script>
<style lang="scss" scoped>
.app-container {
.tree-container {
background-color: #fff;
padding: 15px;
margin-bottom: 20px;
border-radius: 4px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
height: calc(100vh - 140px);
overflow-y: auto;
}
.tree-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
}
.tree-title {
font-size: 15px;
font-weight: 500;
color: #303133;
}
.tree-actions {
display: flex;
gap: 8px;
}
.tree-divider {
margin: 5px 0 15px;
}
.box-card {
margin-bottom: 20px;
}
.custom-tree-node {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
padding-right: 8px;
}
.node-count {
font-size: 12px;
color: #909399;
margin-left: 5px;
}
.pagination {
margin-top: 15px;
text-align: right;
}
/* 让所有输入框和下拉框保持一致宽度 */
.el-input, .el-select, .el-textarea {
width: 100%;
}
/* 确保所有表单元素的高度和边距一致 */
::v-deep .el-form-item {
margin-bottom: 18px;
.el-form-item__content {
line-height: 36px;
}
}
/* 确保el-select和普通输入框宽度一致 */
::v-deep .el-select, ::v-deep .el-tree-select {
width: 100%;
}
/* 树形选择器样式优化 */
::v-deep .el-tree-select .el-select-dropdown__item {
height: auto;
padding: 0;
}
/* 树节点样式优化 */
::v-deep .el-tree-node__content {
height: 32px;
}
::v-deep .el-tree-node__expand-icon {
padding: 6px;
}
}
</style>