最近做的一个qms项目,多个地方需要选择会签人员,然后希望实现的效果图如下,这是我们产品经理画的原型图
这是我实现的效果图
会签组件效果
实现思路如下
点击外面的框
,显示一个弹窗
,弹窗左侧显示部门
,中间显示待选人员列表
(默认展示所有人员
),右侧展示已选人员列表
,点击部门
,待选人员列表
展示该部门下的人员
,点击后的待选人员
在待选人员列表删除
,同时在已选人员列表新增
,每个部门的人员最少并且只允许选中一个
代码如下
countersign.vue
<template>
<div>
<div @click="selectClick()" style="cursor: pointer;">
<a-input v-model:value="selectName" disabled style="pointer-events:none;width:300px;" placeholder="请选择" ref="searchInput" autocomplete="off">
</a-input>
</div>
<a-modal v-model:visible="visible" @ok="handleOk" @cancel="handleCancel" width="700px" height="400px">
<template #title>
<div style="font-weight: 600; font-size: 16px">人员</div>
</template>
<div class="tree-container">
<div>
<div class="tree-title">单位组织机构</div>
<div class="tree-section01">
<div class="section01-left">
<a-tree
:tree-data="treeData"
:selectedKeys="selectedKeys"
@select="selectOrgTreeNode"
>
</a-tree>
</div>
<div class="section01-right">
<a-divider type="vertical" style="height: 100%;"/>
</div>
</div>
</div>
<div>
<div class="tree-title">待选人员</div>
<div class="tree-section02">
<div class="section-left">
<a-input-search v-model:value="searchValue" style="margin-bottom: 8px" placeholder="搜索" @search="onSearch"/>
<div v-if="userLoading">
<div @click="changeTag(tag)" v-for="(tag,index) in selectedUserData" :key="index" :style="tag.isEdit?{height:'30px',cursor: 'pointer'}:{height:'30px',cursor: 'not-allowed'}">
{{ tag.label }}<span v-show="tag.departName">({{tag.departName}})</span>
</div>
</div>
<div v-else>
加载中
</div>
</div>
<div class="section-right">
<a-divider type="vertical" style="height: 100%;"/>
</div>
</div>
</div>
<div>
<div class="tree-title">已选人员</div>
<div class="tree-section03">
<div class="tag-container">
<div @click="closeTag(tag)" v-for="(tag,index) in tags" :key="index" style="height:30px;cursor: pointer;">
{{ tag.label }}<span v-show="tag.departName">({{tag.departName}})</span>
</div>
</div>
</div>
</div>
</div>
</a-modal>
</div>
</template>
<script setup>
import {onMounted, ref,defineEmits,defineProps, onBeforeUnmount, watch, nextTick} from 'vue';
import {defHttp } from '/@/utils/http/axios';
import { useMessage } from '/@/hooks/web/useMessage';
import qs from 'qs'
const { createMessage } = useMessage();
const visible = ref(false);
const emits = defineEmits(['check']);
const props = defineProps({
checked: {
type: Array,
default: [],
}, //字段
});
const checkUser=[];
checkUser.value=props.checked;
// 组织树数据源
const treeData = ref([]);
// 所有用户数据
const userDataOptions = ref([]);
const selectedUserData = ref([]);
// 全局selectedKeys
const selectedKeys = ref([]);
const tags = ref([])
const selectName=ref('');//选中返回值
const departList=ref([]);//部门列表
const userList=ref([]);//用户列表
const userLoading=ref(false);//加载用户数据
onMounted(() => {
// console.log(checkUser.value,'回显值')
getDepartList();
getAllUserList();
});
function getComponentData(url,jsonData,carNo){
return new Promise((resolve,reject)=>{
getAction(url,{ trainCode: carNo}).then(res => {
if (res.success) {
resolve(JSON.parse(res.result))
} else {
resolve(jsonData);
}
})
})
}
//获取部门列表
const pid=ref('');
async function getDepartList(){
departList.value=[];
pid.value = 'c6d7cb4deeac411cb3384b1b31278596';//眉山制动id
let url = '/sys/sysDepart/queryDepartTreeSync';
let params = { pid: pid.value, };
//对获取到的部门列表进行筛选,只获取其中四个部门
// ('1744927552985948161','1744927780283670530','1744927844313915394','1744927935921709057')
await defHttp.get({ url, params }, { isTransformResponse: false }).then((res) => {
if (res.success) {
for(let i=0;i<res.result.length;i++){
if(res.result[i].id=='1744927552985948161'||res.result[i].id=='1744927780283670530'||res.result[i].id=='1744927844313915394'||res.result[i].id=='1744927935921709057'){
departList.value.push(res.result[i]);
}
}
// console.log(departList.value,'部门')
} else {
createMessage.error(res.message);
}
});
queryData();
}
//判断一个数组是否含有另一个数组中的值
function getIncludes(arr1, arr2) {
return arr1.filter((item) => {
return arr2.includes(item)
})
}
//点击部门获取该部门下的人员列表
async function getUserList(departIds){
userLoading.value=false;
selectedUserData.value=[];
userList.value=[];
let url = '/sys/user/listAll';
// let url = '/subassembly/listAll';
let params = { departIds:departIds,pageNo:1,pageSize:200};
// params=qs.stringify(params)
userDataOptions.value = [];
await defHttp.get({ url, params,paramsSerializer: params => {
return qs.stringify(params, { indices: false })
}, }, { isTransformResponse: false}).then((res) => {
if (res.success) {
userLoading.value=true;
userList.value=res.result.records;
// 对获取到的数据进行排序,防止后面顺序错乱
userList.value.forEach((user,i) => {
let option = {
label: user.realname,
value: user.username,
departName:user.departIds_dictText,
isEdit:true,
px:i
}
userDataOptions.value.push(option)
})
sessionStorage.setItem("inData",JSON.stringify(userDataOptions.value));
selectedUserData.value = userDataOptions.value.filter(item => !tags.value.some(obj => obj.value === item.value));
// console.log(userDataOptions.value,'userDataOptions用户')
// console.log(userList.value,'用户')
} else {
createMessage.error(res.message);
}
});
}
//数据回显时,获取所有人员列表
async function getAllUserList(){
userLoading.value=false;
selectedUserData.value=[];
userList.value=[];
// let url = '/sys/user/listAll';
let url='/subassembly/listAll';
let params = { order: 'desc'};
userDataOptions.value = [];
await defHttp.get({ url, params }, { isTransformResponse: false,timeout: 60000 }).then((res) => {
if (res.success) {
userLoading.value=true;
userList.value=res.result;
// 对获取到的数据进行排序,防止后面顺序错乱
userList.value.forEach((user,i) => {
let option = {
label: user.realname,
value: user.username,
departName:user.orgName,
isEdit:true,
px:i
}
userDataOptions.value.push(option)
})
let newArr=[];
//选中人员回显;
for(let i=0;i<userDataOptions.value.length;i++){
for(let j=0;j<checkUser.value.length;j++){
if(userDataOptions.value[i].value==checkUser.value[j]){
newArr.push(userDataOptions.value[i])
}
}
}
// console.log(newArr,'newArr')
if(tags.value.length>0){
for(let i=0;i<newArr.length;i++){
for(let j=0;j<tags.value.length;j++){
if(newArr[i].value!=tags.value[j].value){
tags.value.push(newArr[i])
}
}
}
}else{
tags.value=newArr;
}
sessionStorage.setItem("tagsData",JSON.stringify(tags.value));
// console.log(tags.value,'tags')
selectName.value=tags.value.map(item=>{
return item.label
});
sessionStorage.setItem("inData",JSON.stringify(userDataOptions.value));
selectedUserData.value = userDataOptions.value.filter(item => !tags.value.some(obj => obj.value === item.value));
// console.log(userDataOptions.value,'userDataOptions用户')
// console.log(userList.value,'用户')
} else {
createMessage.error(res.message);
}
});
}
// 组织架构树搜索
const searchValue = ref('');
const onSearch = (searchInputValue) => {
let newArr = [];
let inData = JSON.parse(sessionStorage.getItem('inData'));
if (inData) {
if (searchValue.value) {
for (let i = 0; i < inData.length; i++) {
if (inData[i].label.indexOf(searchValue.value)!=-1) {
newArr.push(inData[i]);
}
}
} else {
newArr = inData;
}
selectedUserData.value = newArr.filter(item => !tags.value.some(obj => obj.value === item.value));
}
};
function selectClick(){
visible.value=true;
// if(visible.value){
// getAllUserList();
// }
}
// 点击左侧组织架构树根据组织code获取所属用户
function selectOrgTreeNode(departIds, e){
getUserList(departIds);
userDataOptions.value = [];
// 对获取到的数据进行排序,防止后面顺序错乱
userList.value.forEach((user,i) => {
let option = {
label: user.realname,
value: user.username,
px:i
}
userDataOptions.value.push(option)
})
sessionStorage.setItem("inData",JSON.stringify(userDataOptions.value));
selectedUserData.value = userDataOptions.value.filter(item => !tags.value.some(obj => obj.value === item.value));
// console.log(selectedUserData.value,'selectedUserData')
// console.log(userDataOptions.value,'userDataOptions点击部门')
}
//已选择人员点击删除,同时待选择人员添加
const closeTag = (tag) => {
let isIn=false;
// console.log(tag,'tag点击')
let index='';
for(let i=0;i<tags.value.length;i++){
if(tag.value==tags.value[i].value){
index=i;
}
}
for(let i=0;i<selectedUserData.value.length;i++){
if(selectedUserData.value[i].departName==tag.departName){
isIn=true;
}
}
searchValue.value='';
if(isIn){
selectedUserData.value.push(tag);
selectedUserData.value=selectedUserData.value.sort(function(a,b){
return a.px-b.px
});
tags.value.splice(index,1);
}else{
for(let i=0;i<tags.value.length;i++){
if(tag.value==tags.value[i].value){
index=i;
}
}
tags.value.splice(index,1);
}
}
//待选择人员点击删除,同时已选择人员添加
const changeTag= (tag) => {
searchValue.value='';
//判断待选择人员是否在已选择人员中,在的话isIn为true,不在等于为false
let isIn=false;
for(let i=0;i<tags.value.length;i++){
if(tags.value[i].departName==tag.departName){
isIn=true;
}
}
// console.log('isIn',isIn)
if(isIn){
selectedUserData.value.map(item=>{
item.isEdit=false
})
}else{
tags.value.push(tag);
tags.value=tags.value.sort(function(a,b){
return a.px-b.px
});
selectedUserData.value = userDataOptions.value.filter(item => !tags.value.some(obj => obj.value === item.value));
}
// console.log(selectedUserData.value,'selectedUserData')
}
// 左侧组织架构树
const queryData = async () => {
let officeUserData = [];
officeUserData=departList.value;
localStorage.setItem('officeUserData', JSON.stringify(officeUserData));
recursive(officeUserData);
}
// 根据数据源构建架构树
const recursive = (newList) => {
// console.log(newList,'newList')
newList.map((item => {
item.title = item.departName;
item.key = item.id;
// if (item.children !== undefined) {
// recursive(item.children)
// }
}))
treeData.value = newList;
};
const handleCancel = (e) => {
visible.value = false;
//选中人员回显;
tags.value=JSON.parse(sessionStorage.getItem("tagsData"));
}
const handleOk = (e) => {
// console.log(tags.value,'选中值')
if(tags.value.length==4){
visible.value = false;
selectName.value=tags.value.map(item=>{
return item.label
});
emits('check', tags.value); //把列表选中的值传过去
}else{
createMessage.warning('每个部门至少选择一个人员~');
}
};
onBeforeUnmount(() => {
})
</script>
<style lang="less" scoped>
.tree-container {
display: flex;
flex-direction: row;
min-height: 400px;
max-height: 650px;
box-sizing: border-box;
.tree-section01 {
overflow-y: scroll;
// margin-right: 20px;
padding-left:5px;
display: flex;
max-height: 400px;
}
.tree-section02 {
display: flex;
overflow-y: scroll;
max-height: 400px;
// margin-right: 20px;
// padding-top: 35px;
padding-left:5px;
flex: 3;
& :deep(.ant-checkbox-group) {
display: flex !important;
flex-direction: column !important;
}
}
.tree-section03 {
padding-left:5px;
// padding-top: 30px;
flex: 3;
max-height: 400px;
}
.section-left {
width: 100%;
}
.tree-title {
font-size: 14px;
height: 22px;
line-height: 22px;
font-weight: 600;
color: rgba(21, 22, 24, 0.92);
margin-left:10px;
}
.tag-container {
.tag-content {
width: 100%;
display: flex;
flex-wrap: wrap;
.tag-title {
font-size: 14px;
height: 22px;
line-height: 22px;
font-weight: 600;
color: red;
}
.tag-item {
margin: 2px 2px;
}
}
}
}
</style>
调用countersign组件
<template>
<a-card :bordered="false">
<a-tabs defaultActiveKey="1">
<a-tab-pane tab="基础示例" key="1">
<a-form :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<a-row :gutter="24">
<a-col :span="8">
<a-form-item label="label名称:">
<countersign :checked='checkUser' @check='getCheckUser'></countersign>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-tab-pane>
</a-tabs>
</a-card>
</template>
<script setup>
import {onMounted, ref,defineEmits,defineProps, onBeforeUnmount, watch, nextTick} from 'vue';
import countersign from '/@/components/countersign.vue';
const checkUser=ref(['010800007567']);//传过去的人员账号
const checkedUser=ref([]);//回显的数据
function getCheckUser(val){
console.log(val,'点击确定返回数据')
if(val){
checkedUser.value=val.map(item=>{
return item.value
})
}
console.log(checkedUser.value,'选中人员账号')
}
</script>