Files
klp-oa/klp-ui/src/views/wms/post/index.vue
砂糖 5473fbf28f feat(wms): 新增岗位管理与岗位职责管理功能
1. 新增依赖包vis-network用于可视化岗位树
2. 新增岗位相关API接口,包含增删改查功能
3. 新增岗位职责相关API接口,包含增删改查功能
4. 新增岗位管理页面,支持岗位树可视化、岗位CRUD以及关联的岗位职责管理
2026-06-16 13:42:12 +08:00

764 lines
22 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-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="岗位名称" prop="postName">
<el-input
v-model="queryParams.postName"
placeholder="请输入岗位名称"
clearable
@keyup.enter.native="handleQuery"
/>
</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-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="el-icon-plus"
size="mini"
@click="handleAdd"
>新增岗位</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<!-- 左右主体区域 -->
<el-row :gutter="16" class="post-content" v-loading="loading">
<!-- 左侧ECharts 岗位树 + 岗位信息 -->
<el-col :span="18">
<div class="tree-panel">
<div class="panel-header">
<span class="panel-title">岗位结构树</span>
<span v-if="!currentPost" class="panel-hint">点击节点查看详情</span>
</div>
<div ref="treeChart" class="chart-container"></div>
<!-- 岗位信息及操作 -->
<div v-if="currentPost" class="post-info-bar">
<span class="post-name">{{ currentPost.postName }}</span>
<div class="post-actions">
<el-button type="primary" size="mini" icon="el-icon-edit" @click="handleUpdate(currentPost)">编辑</el-button>
<el-button type="success" size="mini" icon="el-icon-plus" @click="handleAdd(currentPost)">添加子级</el-button>
<el-button type="danger" size="mini" icon="el-icon-delete" @click="handleDelete(currentPost)">删除</el-button>
</div>
</div>
</div>
</el-col>
<!-- 右侧岗位职责管理 -->
<el-col :span="6">
<div class="duty-panel">
<div class="panel-header">
<span class="panel-title">岗位职责</span>
<span v-if="currentPost" class="panel-sub-title">{{ currentPost.postName }}</span>
<span v-else class="panel-hint">请点击左侧岗位节点</span>
</div>
<!-- 自定义职责列表 -->
<div v-if="currentPost" class="duty-list-wrap">
<div class="duty-toolbar">
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAddDuty">新增职责</el-button>
</div>
<div v-loading="dutyLoading" class="duty-list">
<div v-for="item in dutyList" :key="item.dutyId" class="duty-item">
<div class="duty-item-header">
<span class="duty-name">{{ item.dutyName }}</span>
<!-- <span class="duty-sort">排序: {{ item.sortOrder }}</span> -->
</div>
<div class="duty-content">{{ item.dutyContent }}</div>
<div v-if="item.remark" class="duty-remark">备注{{ item.remark }}</div>
<div class="duty-item-actions">
<el-button type="text" size="mini" icon="el-icon-edit" @click="handleUpdateDuty(item)">修改</el-button>
<el-button type="text" size="mini" icon="el-icon-delete" style="color: #F56C6C" @click="handleDeleteDuty(item)">删除</el-button>
</div>
</div>
<el-empty v-if="!dutyLoading && dutyList.length === 0" description="暂无职责" :image-size="60" />
</div>
</div>
<!-- 未选择岗位时的提示 -->
<div v-else class="empty-hint">
<i class="el-icon-info"></i>
<p>请点击左侧树图中的岗位节点以查看和管理其岗位职责</p>
</div>
</div>
</el-col>
</el-row>
<!-- 添加或修改岗位对话框 -->
<el-dialog :title="title" :visible.sync="open" width="600px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
<el-form-item label="上级岗位" prop="parentId">
<treeselect v-model="form.parentId" :options="postOptions" :normalizer="normalizer" placeholder="请选择上级岗位(留空为顶级)" />
</el-form-item>
<el-form-item label="岗位名称" prop="postName">
<el-input v-model="form.postName" placeholder="请输入岗位名称" />
</el-form-item>
<!-- <el-form-item label="岗位类型" prop="postType">
<el-select v-model="form.postType" placeholder="请选择岗位类型">
<el-option label="生产岗" value="PRODUCTION" />
<el-option label="质检岗" value="QUALITY" />
<el-option label="维修岗" value="MAINTENANCE" />
<el-option label="技术岗" value="TECHNICAL" />
<el-option label="管理岗" value="MANAGEMENT" />
</el-select>
</el-form-item>
<el-form-item label="岗位级别" prop="postLevel">
<el-select v-model="form.postLevel" placeholder="请选择岗位级别">
<el-option label="初级" value="JUNIOR" />
<el-option label="中级" value="MIDDLE" />
<el-option label="高级" value="SENIOR" />
<el-option label="班长" value="LEAD" />
<el-option label="经理" value="MANAGER" />
</el-select>
</el-form-item> -->
<el-form-item label="显示顺序" prop="postSort">
<el-input-number v-model="form.postSort" :min="0" :max="999" controls-position="right" />
</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 :loading="buttonLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</el-dialog>
<!-- 添加或修改岗位职责对话框 -->
<el-dialog :title="dutyTitle" :visible.sync="dutyOpen" width="600px" append-to-body>
<el-form ref="dutyForm" :model="dutyForm" :rules="dutyRules" label-width="100px">
<el-form-item label="职责名称" prop="dutyName">
<el-input v-model="dutyForm.dutyName" placeholder="请输入职责名称" />
</el-form-item>
<el-form-item label="职责内容" prop="dutyContent">
<el-input v-model="dutyForm.dutyContent" type="textarea" :rows="4" placeholder="请输入职责内容" />
</el-form-item>
<!-- <el-form-item label="职责类型" prop="dutyType">
<el-select v-model="dutyForm.dutyType" placeholder="请选择职责类型">
<el-option label="主要职责" value="MAIN" />
<el-option label="次要职责" value="SECONDARY" />
<el-option label="安全职责" value="SAFETY" />
<el-option label="质量职责" value="QUALITY" />
</el-select>
</el-form-item> -->
<!-- <el-form-item label="排序" prop="sortOrder">
<el-input-number v-model="dutyForm.sortOrder" :min="0" :max="999" controls-position="right" />
</el-form-item> -->
<el-form-item label="备注" prop="remark">
<el-input v-model="dutyForm.remark" type="textarea" placeholder="请输入备注" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button :loading="dutyButtonLoading" type="primary" @click="submitDutyForm"> </el-button>
<el-button @click="cancelDuty"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listPost, getPost, addPost, updatePost, delPost } from "@/api/wms/post";
import { listPostDuty, getPostDuty, addPostDuty, updatePostDuty, delPostDuty } from "@/api/wms/postDuty";
import Treeselect from "@riophae/vue-treeselect";
import "@riophae/vue-treeselect/dist/vue-treeselect.css";
import { Network } from 'vis-network/peer/umd/vis-network.min.js';
export default {
name: "WmsPost",
components: {
Treeselect
},
data() {
return {
// 岗位相关
buttonLoading: false,
loading: true,
showSearch: true,
postList: [],
postOptions: [],
title: "",
open: false,
queryParams: {
postName: undefined
},
form: {},
rules: {
postName: [
{ required: true, message: "岗位名称不能为空", trigger: "blur" }
],
postSort: [
{ required: true, message: "显示顺序不能为空", trigger: "blur" }
]
},
// 岗位职责相关
currentPost: null,
dutyLoading: false,
dutyList: [],
dutyTitle: "",
dutyOpen: false,
dutyButtonLoading: false,
dutyForm: {},
dutyRules: {
dutyName: [
{ required: true, message: "职责名称不能为空", trigger: "blur" }
],
dutyContent: [
{ required: true, message: "职责内容不能为空", trigger: "blur" }
]
},
// vis.js 实例
network: null,
// 岗位类型颜色映射
postTypeColors: {
'PRODUCTION': '#409EFF',
'QUALITY': '#67C23A',
'MAINTENANCE': '#E6A23C',
'TECHNICAL': '#909399',
'MANAGEMENT': '#F56C6C'
}
};
},
created() {
this.getList();
},
beforeDestroy() {
if (this._resizeHandler) {
window.removeEventListener('resize', this._resizeHandler);
this._resizeHandler = null;
}
if (this.network) {
this.network.destroy();
this.network = null;
}
},
methods: {
/** 查询岗位列表 */
getList() {
this.loading = true;
listPost(this.queryParams).then(response => {
this.postList = this.handleTree(response.data, "postId", "parentId");
this.loading = false;
this.$nextTick(() => {
this.initChart();
});
});
},
/** 初始化 vis.js 树图 */
initChart() {
if (!this.$refs.treeChart) return;
if (this.network) {
this.network.destroy();
}
// 构建扁平化的 nodes 和 edges
const nodesData = [];
const edgesData = [];
this.buildVisData(this.postList, null, nodesData, edgesData);
if (nodesData.length === 0) {
nodesData.push({ id: 0, label: '暂无岗位', font: { color: '#909399' }, shape: 'box' });
}
const that = this;
const options = {
layout: {
hierarchical: {
enabled: true,
direction: 'UD',
sortMethod: 'directed',
levelSeparation: 100,
nodeSpacing: 150,
treeSpacing: 200
}
},
physics: {
enabled: false
},
interaction: {
hover: false,
hoverConnectedEdges: false,
selectable: false,
dragNodes: false,
zoomView: true,
dragView: true
},
edges: {
smooth: { type: 'curvedCW', roundness: 0.2 },
color: '#bbb',
width: 2,
arrows: { to: { enabled: true, scaleFactor: 0.5 } }
},
nodes: {
shape: 'box',
size: 40,
font: {
color: '#fff',
size: 12,
face: 'Microsoft YaHei',
multi: false
},
borderWidth: 0,
margin: {
top: 8,
bottom: 8,
left: 14,
right: 14
},
shadow: false
}
};
const container = this.$refs.treeChart;
this.network = new Network(container, { nodes: nodesData, edges: edgesData }, options);
// 节点点击事件
this.network.on('click', function(params) {
const nodeId = that.network.getNodeAt(params.pointer.DOM);
if (nodeId) {
const post = that.findPostById(that.postList, nodeId);
if (post) {
that.handleTreeNodeClick(post);
}
}
});
// 窗口大小变化自适应
const resizeHandler = () => {
if (that.network) {
that.network.fit({ animation: false });
}
};
window.addEventListener('resize', resizeHandler);
this._resizeHandler = resizeHandler;
},
/** 构建 vis.js 扁平化节点和边数据 */
buildVisData(nodes, parentId, nodesData, edgesData) {
if (!nodes) return;
nodes.forEach(node => {
const color = this.postTypeColors[node.postType] || '#409EFF';
nodesData.push({
id: node.postId,
label: node.postName || '未命名',
color: { background: color, border: color }
});
if (parentId !== null) {
edgesData.push({
from: parentId,
to: node.postId
});
}
if (node.children && node.children.length > 0) {
this.buildVisData(node.children, node.postId, nodesData, edgesData);
}
});
},
/** 树节点点击 */
handleTreeNodeClick(post) {
this.currentPost = post;
this.loadDutyList(post.postId);
},
/** 递归查找岗位 */
findPostById(nodes, postId) {
for (let node of nodes) {
if (node.postId === postId) {
return node;
}
if (node.children && node.children.length > 0) {
const found = this.findPostById(node.children, postId);
if (found) return found;
}
}
return null;
},
/** 转换岗位数据结构Treeselect 用) */
normalizer(node) {
if (node.children && !node.children.length) {
delete node.children;
}
return {
id: node.postId,
label: node.postName,
children: node.children
};
},
/** 查询岗位下拉树结构 */
getTreeselect() {
listPost().then(response => {
this.postOptions = [];
const data = { postId: 0, postName: '顶级节点', children: [] };
data.children = this.handleTree(response.data, "postId", "parentId");
this.postOptions.push(data);
});
},
/** 加载岗位职责列表 */
loadDutyList(postId) {
this.dutyLoading = true;
listPostDuty({ postId: postId }).then(response => {
this.dutyList = response.rows || [];
this.dutyLoading = false;
});
},
// ========== 岗位 CRUD ==========
handleQuery() {
this.getList();
},
resetQuery() {
this.resetForm("queryForm");
this.handleQuery();
},
cancel() {
this.open = false;
this.reset();
},
reset() {
this.form = {
postId: null,
parentId: null,
postName: null,
postType: null,
postLevel: null,
postSort: 0,
status: 1,
remark: null
};
this.resetForm("form");
},
handleAdd(row) {
this.reset();
this.getTreeselect();
if (row != null && row.postId) {
this.form.parentId = row.postId;
}
this.open = true;
this.title = "添加岗位";
},
handleUpdate(row) {
this.loading = true;
this.reset();
this.getTreeselect();
getPost(row.postId).then(response => {
this.loading = false;
this.form = response.data;
this.open = true;
this.title = "修改岗位";
});
},
submitForm() {
this.$refs["form"].validate(valid => {
if (valid) {
this.buttonLoading = true;
if (this.form.postId != null) {
updatePost(this.form).then(response => {
this.$modal.msgSuccess("修改成功");
this.open = false;
this.getList();
if (this.currentPost && this.currentPost.postId === this.form.postId) {
// 更新当前选中岗位的显示信息
Object.assign(this.currentPost, {
postName: this.form.postName,
postType: this.form.postType,
postLevel: this.form.postLevel,
status: this.form.status,
remark: this.form.remark
});
}
}).finally(() => {
this.buttonLoading = false;
});
} else {
addPost(this.form).then(response => {
this.$modal.msgSuccess("新增成功");
this.open = false;
this.getList();
}).finally(() => {
this.buttonLoading = false;
});
}
}
});
},
handleDelete(row) {
this.$modal.confirm('是否确认删除岗位"' + row.postName + '"的数据项?').then(() => {
this.loading = true;
return delPost(row.postId);
}).then(() => {
this.loading = false;
if (this.currentPost && this.currentPost.postId === row.postId) {
this.currentPost = null;
this.dutyList = [];
}
this.getList();
this.$modal.msgSuccess("删除成功");
}).catch(() => {
}).finally(() => {
this.loading = false;
});
},
// ========== 岗位职责 CRUD ==========
cancelDuty() {
this.dutyOpen = false;
this.resetDuty();
},
resetDuty() {
this.dutyForm = {
dutyId: null,
postId: this.currentPost ? this.currentPost.postId : null,
dutyName: null,
dutyContent: null,
dutyType: null,
sortOrder: 0,
remark: null
};
this.resetForm("dutyForm");
},
handleAddDuty() {
this.resetDuty();
this.dutyOpen = true;
this.dutyTitle = "添加岗位职责";
},
handleUpdateDuty(row) {
this.resetDuty();
getPostDuty(row.dutyId).then(response => {
this.dutyForm = response.data;
this.dutyOpen = true;
this.dutyTitle = "修改岗位职责";
});
},
submitDutyForm() {
this.$refs["dutyForm"].validate(valid => {
if (valid) {
this.dutyButtonLoading = true;
this.dutyForm.postId = this.currentPost.postId;
if (this.dutyForm.dutyId != null) {
updatePostDuty(this.dutyForm).then(response => {
this.$modal.msgSuccess("修改成功");
this.dutyOpen = false;
this.loadDutyList(this.currentPost.postId);
}).finally(() => {
this.dutyButtonLoading = false;
});
} else {
addPostDuty(this.dutyForm).then(response => {
this.$modal.msgSuccess("新增成功");
this.dutyOpen = false;
this.loadDutyList(this.currentPost.postId);
}).finally(() => {
this.dutyButtonLoading = false;
});
}
}
});
},
handleDeleteDuty(row) {
this.$modal.confirm('是否确认删除职责"' + row.dutyName + '"的数据项?').then(() => {
this.dutyLoading = true;
return delPostDuty(row.dutyId);
}).then(() => {
this.dutyLoading = false;
this.loadDutyList(this.currentPost.postId);
this.$modal.msgSuccess("删除成功");
}).catch(() => {
}).finally(() => {
this.dutyLoading = false;
});
}
}
};
</script>
<style scoped>
.post-content {
margin-top: 8px;
}
.tree-panel,
.duty-panel {
border: 1px solid #EBEEF5;
border-radius: 4px;
background: #fff;
}
.tree-panel {
min-height: 600px;
}
.duty-panel {
min-height: 600px;
display: flex;
flex-direction: column;
}
.panel-header {
padding: 10px 14px;
border-bottom: 1px solid #EBEEF5;
background: #FAFAFA;
display: flex;
align-items: center;
justify-content: space-between;
}
.panel-title {
font-size: 14px;
font-weight: 600;
color: #303133;
}
.panel-hint {
font-size: 12px;
color: #909399;
}
.panel-sub-title {
font-size: 12px;
color: #606266;
max-width: 140px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.chart-container {
width: 100%;
height: 540px;
}
.post-info-bar {
padding: 10px 14px;
display: flex;
align-items: center;
gap: 8px;
border-top: 1px solid #EBEEF5;
background: #FAFAFA;
flex-wrap: wrap;
}
.post-info-bar .post-name {
font-size: 14px;
font-weight: 600;
color: #303133;
margin-right: 4px;
}
.post-info-bar .post-actions {
margin-left: auto;
display: flex;
gap: 6px;
}
/* 自定义职责列表样式 */
.duty-list-wrap {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.duty-toolbar {
padding: 10px 14px;
border-bottom: 1px solid #EBEEF5;
}
.duty-list {
flex: 1;
overflow-y: auto;
padding: 8px 14px 14px;
}
.duty-item {
border: 1px solid #EBEEF5;
border-radius: 4px;
padding: 10px 12px;
margin-bottom: 8px;
transition: box-shadow 0.2s;
}
.duty-item:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.duty-item-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6px;
}
.duty-name {
font-size: 13px;
font-weight: 600;
color: #303133;
}
.duty-sort {
font-size: 11px;
color: #909399;
}
.duty-content {
font-size: 12px;
color: #606266;
line-height: 1.6;
margin-bottom: 4px;
word-break: break-all;
}
.duty-remark {
font-size: 11px;
color: #909399;
margin-bottom: 6px;
}
.duty-item-actions {
display: flex;
gap: 8px;
justify-content: flex-end;
padding-top: 6px;
border-top: 1px dashed #EBEEF5;
}
.empty-hint {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex: 1;
color: #C0C4CC;
min-height: 300px;
}
.empty-hint i {
font-size: 48px;
margin-bottom: 12px;
}
.empty-hint p {
font-size: 13px;
color: #909399;
}
</style>