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

791 lines
26 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="locationTree"
:data="locationTree"
: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.locationCode" placeholder="请输入位置编码" clearable size="small" />
</el-form-item>
<el-form-item label="位置名称">
<el-input v-model="queryParams.locationName" 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="locationList"
border
stripe
style="width: 100%"
@selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" />
<el-table-column prop="locationCode" label="位置编码" width="150" />
<el-table-column prop="locationName" 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="levelCode" label="层级编码" width="120" /> -->
<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="locationCode">
<el-input v-model="form.locationCode" placeholder="请输入位置编码" />
</el-form-item>
<el-form-item label="位置名称" prop="locationName">
<el-input v-model="form.locationName" placeholder="请输入位置名称" />
</el-form-item>
<el-form-item label="上级位置" prop="parentId">
<el-cascader
v-model="form.parentId"
:options="locationTreeOptions"
: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="levelCode">
<el-input v-model="form.levelCode" placeholder="请输入层级编码" />
</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 { listLocation, getLocation, addLocation, updateLocation, delLocation, enableLocation, disableLocation, getLocationTree, checkLocationInUse, getChildLocations, delLocationBatch, checkLocationCode } from '@/api/asset/location'
export default {
name: 'AssetLocation',
data() {
return {
// 遮罩层
loading: false,
// 总条数
total: 0,
// 位置列表
locationList: [],
// 位置树形数据
locationTree: [],
// 处理后的树形选项数据(用于级联选择器)
locationTreeOptions: [],
// 默认展开的节点
defaultExpandedKeys: [],
// 树形配置
defaultProps: {
children: 'children',
label: 'label'
},
// 弹出层标题
dialogTitle: '',
// 是否显示弹出层
dialogVisible: false,
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
locationCode: undefined,
locationName: undefined,
status: undefined,
parentId: undefined
},
// 表单参数
form: {},
// 表单校验
rules: {
locationCode: [
{ required: true, message: '请输入位置编码', trigger: 'blur' },
{ min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' },
{ validator: this.validateLocationCode, trigger: 'blur' }
],
locationName: [
{ 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: {
/** 检验位置编码唯一性 */
validateLocationCode(rule, value, callback) {
if (value === '') {
callback()
return
}
checkLocationCode(this.form.id, value).then(res => {
if (res.code === '000000') {
if (res.data) {
callback()
} else {
callback(new Error('该位置编码已存在'))
}
} else {
callback()
}
}).catch(() => {
callback()
})
},
/** 获取位置树形结构 */
getTree() {
this.loading = true
const params = {
status: undefined // 获取所有状态的位置
}
getLocationTree(params).then(res => {
if (res.code === '000000') {
// 处理返回的树形数据,确保字段名称正确
this.locationTree = this.processTreeData(res.data)
// 准备级联选择器的数据源
this.locationTreeOptions = this.prepareTreeSelectOptions(this.locationTree)
// 如果之前已经有展开的节点,保持这些节点展开
// 否则只展开第一级有子节点的节点
if (!this.defaultExpandedKeys || this.defaultExpandedKeys.length === 0) {
this.defaultExpandedKeys = this.getFirstLevelKeys(this.locationTree)
}
// 强制刷新树组件
this.$nextTick(() => {
if (this.$refs.locationTree) {
this.$refs.locationTree.$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.locationName || node.label || ''}`,
// 驼峰命名
locationName: node.locationName || node.label || '',
locationCode: node.code || '',
status: node.status || '1',
parentId: node.parentId || null,
levelCode: node.levelCode || '',
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
},
/** 转换下划线命名为驼峰命名 */
convertSnakeToCamel(list) {
if (!list || !Array.isArray(list)) return []
return list.map(item => {
return {
id: item.id,
locationCode: item.locationCode || '',
locationName: item.locationName || '',
parentId: item.parentId || null,
levelCode: item.levelCode || '',
status: item.status || '1',
remark: item.remark || '',
createTime: item.createTime || '',
createUserId: item.createUserId || '',
lastModUserId: item.lastModUserId || '',
lastModTime: item.lastModTime || ''
}
})
},
/** 查询位置列表 */
getList() {
this.loading = true
listLocation(this.queryParams).then(res => {
if (res.code === '000000') {
// 转换API返回数据结构为驼峰命名
this.locationList = 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
})
},
/** 节点单击事件 */
handleNodeClick(data) {
this.queryParams.parentId = data.id
// 使用新的获取子位置API
this.loading = true
getChildLocations(data.id, {
pageNum: this.queryParams.pageNum,
pageSize: this.queryParams.pageSize
}).then(res => {
if (res.code === '000000') {
this.locationList = 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,
locationCode: undefined,
locationName: undefined,
status: undefined,
parentId: undefined
}
this.getList()
},
/** 新增按钮操作 */
handleAdd() {
this.dialogTitle = '新增位置'
this.form = {
locationCode: '',
locationName: '',
parentId: this.queryParams.parentId || '', // 如果当前有选中节点,则默认为父节点
levelCode: '',
status: '1',
remark: '',
createUserId: this.$store.getters.userId || ''
}
this.dialogVisible = true
this.$nextTick(() => {
if (this.$refs.form) {
this.$refs.form.clearValidate()
}
})
},
/** 修改按钮操作 */
handleEdit(row) {
const id = row.id
getLocation(id).then(res => {
if (res.code === '000000') {
// 将API响应数据映射到表单字段使用驼峰命名
this.form = {
id: res.data.id,
locationCode: res.data.locationCode,
locationName: res.data.locationCode,
parentId: res.data.parentId || '',
levelCode: res.data.levelCode || '',
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,
locationCode: this.form.locationCode,
locationName: this.form.locationName,
parentId: this.form.parentId,
levelCode: this.form.levelCode,
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.form.createUserId || this.$store.getters.userId || ''
}
const method = this.form.id ? updateLocation : addLocation
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(() => {
// 先检查位置是否被使用
checkLocationInUse(row.id).then(res => {
if (res.code === '000000' && res.data) {
this.$message.error('该位置已被使用,无法删除')
return
}
// 未被使用,执行删除操作
const lastModUserId = this.$store.getters.userId || ''
delLocation(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' ? disableLocation : enableLocation
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.locationCode + '-' + node.locationName;
}
if (node.children && node.children.length > 0) {
const found = findParentInTree(node.children, id);
if (found) return found;
}
}
return null;
};
const parentName = findParentInTree(this.locationTree, parentId);
if (parentName) return parentName;
// 如果在树中没找到,可能在列表数据中
const parent = this.locationList.find(item => item.id === parentId);
return parent ? (parent.locationCode + '-' + parent.locationName) : '未知';
},
/** 多选选中项改变 */
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
delLocationBatch(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(() => {})
},
/** 展开所有节点 */
expandAll() {
const allKeys = this.getExpandedKeys(this.locationTree)
this.defaultExpandedKeys = allKeys
this.$nextTick(() => {
if (this.$refs.locationTree) {
this.$refs.locationTree.$forceUpdate()
}
})
},
/** 收起所有节点 */
collapseAll() {
this.defaultExpandedKeys = []
this.$nextTick(() => {
if (this.$refs.locationTree) {
this.$refs.locationTree.$forceUpdate()
}
})
}
}
}
</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>