Commit 3de9a072 by 周海峰

优化

parent 9f535b90
部署地址:10.12.9.9 /home/docker-nginx/nginx_auth/html-new/platform-auth
## 功能与特点
......
<template>
<div style="position: relative;">
<!-- Loading遮罩层 - 覆盖树的半透明效果 -->
<div
class="tree-loading-overlay"
:class="{ 'is-loading': checkLoading }"
>
<div class="tree-loading-content">
<q-spinner-orbit color="primary" size="40px" />
<span class="tree-loading-text">处理中...</span>
</div>
</div>
<div class="tree-wrapper" :class="{ 'is-dimmed': checkLoading }">
<div class="search-container">
<div class="search-wrapper">
<q-input
v-model="searchText"
clearable
class="search-input"
label="搜索(支持两字以上的名字搜索)"
placeholder="输入搜索并选择"
@input="onSearchInput"
@focus="showSearchMenu = true"
@blur="hideSearchMenu"
>
<template v-slot:prepend>
<q-icon name="search" class="search-icon" />
</template>
<template v-slot:append>
<q-btn
flat
dense
round
icon="search"
class="search-btn"
@click="manualSearch"
/>
</template>
</q-input>
<!-- 搜索菜单 -->
<div
v-show="showSearchMenu"
class="search-menu"
>
<div class="search-results" v-if="searchOptions.length > 0">
<div
v-for="option in searchOptions"
:key="option.key"
class="search-result-item"
@click="selectSearchOption(option)"
>
<div class="search-result-avatar">
<q-icon
:name="option.type === 'department' ? 'business' : 'person'"
:color="option.type === 'department' ? 'primary' : 'positive'"
size="sm"
/>
</div>
<div class="search-result-content">
<div class="search-result-label">
{{ option.label }}
</div>
<div class="search-result-caption">
{{ option.type === 'department' ? '部门' : '人员' }}
<span v-if="option.key < 1000000 && option.key" class="search-result-id">
ID: {{ option.key }}
</span>
</div>
</div>
<div class="search-result-side">
<q-icon name="chevron_right" size="sm" color="grey-5" />
</div>
</div>
</div>
<!-- 无搜索结果 -->
<div v-else-if="searchText && searchText.length > 0" class="no-results">
<div class="no-result-item">
<div class="no-result-avatar">
<q-icon name="info" color="grey-5" size="sm" />
</div>
<div class="no-result-content">
<div class="no-results-text">
没有找到匹配的人员或部门
</div>
<div class="no-results-hint">
请尝试其他关键词
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div style="padding-top: 6vh;">
<Tree
v-if="treeData.length > 0"
ref="tree"
checkable
:defaultExpandedKeys="defaultExpandedKeys"
:checkedKeys="checkedKeys"
:treeData="treeData"
@check="onCheck"
>
<template slot="title" slot-scope="node">
<span style="color: rgb(33, 186, 69);" v-if="checkedKeys.indexOf(node.key)>-1" ></span>
<span v-if="node.title.indexOf(searchValue) > -1">
{{node.title.substr(0, node.title.indexOf(searchValue))}}
<span style="color: #f50">{{searchValue }}</span>
{{node.title.substr(node.title.indexOf(searchValue) + searchValue.length) }}
<span style="color: #64b5fe">{{ node.no ? node.no:'' }}</span>
</span>
<span v-else>
{{ node.title }}
<span style="color: #64b5fe;">{{ node.no ? node.no:'' }}</span>
</span>
</template>
</Tree>
</div>
</div>
<div v-show="!treeData.length > 0">
正在加载组织机构组件...
</div>
</div>
</template>
<script>
import Tree from 'ant-design-vue/lib/tree';
import 'ant-design-vue/dist/antd.css';
import AInputSearch from "ant-design-vue/es/input/Search";
import {deptall} from "@/service/user/user";
const dataList = []
const generateList = (data) => {
for (let i = 0; i < data.length; i++) {//高首骊
const node = data[i]
// const key = node.key
dataList.push({//key,
title: node.title,
key: node.key,
no:node.no,
avatar:node.avatar,
id: node.id,
label: node.label,
pid: node.pid,
scopedSlots: {title: 'title'}
})
if (node.children) {
generateList(node.children, node.key)
}
node.scopedSlots = {title: 'title'}
}
}
const getParentKey = (key, tree) => {
let parentKey
for (let i = 0; i < tree.length; i++) {
const node = tree[i]
if (node.children) {
if (node.children.some(item => item.key === key)) {
parentKey = node.key
} else if (getParentKey(key, node.children)) {
parentKey = getParentKey(key, node.children)
}
}
}
return parentKey
}
export default {
components: {
AInputSearch,
Tree
},
name: "organization-ant",
data() {
return {
treeData: [],
searchValue: '',
searchText: '',// 自定义
expandedKeys: [1000001],
defaultExpandedKeys: [1000001],
autoExpandParent: true,
// 复选模式下的选中key(包含部门和人员)
checkedKeys: [],
// 准备提交的选中的部门
checkedDepartment: [],
// 准备提交的选中的人类对象
checkedPeople: [],
// 节点缓存,用于快速查找
nodeCache: null,
// 搜索相关
searchOptions: [],
showSearchMenu: false,
// 勾选操作loading状态
checkLoading: false,
// 保存body原始样式
originalBodyStyle: null
};
},
props: {
value: {
type: Array
}
},
computed: {
getTreeData() {
// this.initTree();
// generateList(this.$store.state.orgTreeData)
// console.log(this.treeData)
if (this.treeData.length > 0) {
console.log('计算属性,组织机构,ok');
this.$emit('ok', 'ok')
}
return this.treeData;
},
getselectKeysStr(){
return this.checkedKeys.map(item=>item).join(",")
}
},
created() {
this.initTree();
},
mounted() {
},
methods: {
// 初始化树
async initTree(){
let Res = await deptall();
let resdata = Res.data.data;
this.treeData.push(resdata)
generateList(this.treeData);
// 构建节点缓存
this.nodeCache = new Map();
this.buildNodeCache(this.treeData);
},
onExpand(expandedKeys) {
// console.log('onExpand', expandedKeys)
// if not set autoExpandParent to false, if children expanded, parent can not collapse.
// or, you can remove all expanded children keys.
this.expandedKeys = expandedKeys
this.autoExpandParent = false
},
onCheck(checkedKeys, e) {
console.log('onCheck', checkedKeys, e);
// 显示loading
this.checkLoading = true;
console.log('Loading set to true:', this.checkLoading);
// 防止页面滚动
this.preventBodyScroll();
// 强制Vue重新渲染
this.$forceUpdate();
// 使用setTimeout强制分离到下一个事件循环,确保loading先显示
setTimeout(() => {
console.log('Processing check operation...');
this.processCheckOperation(checkedKeys, e);
}, 0);
},
// 分离的处理逻辑
processCheckOperation(checkedKeys, e) {
try {
// 更新本地状态
this.checkedKeys = checkedKeys;
// 更新选中的人员列表
this.updateSelectedStaff();
// 双向绑定会通过watch自动处理
// 处理完成后,延迟一点时间再隐藏loading,确保用户能看到
setTimeout(() => {
this.checkLoading = false;
// 恢复页面滚动
this.restoreBodyScroll();
}, 500); // 最少显示200ms,让用户看到反馈
} catch (error) {
console.error('勾选处理出错:', error);
this.checkLoading = false; // 出错时也要隐藏loading
this.restoreBodyScroll(); // 恢复页面滚动
}
},
// 防止body滚动
preventBodyScroll() {
// 保存原始样式
if (!this.originalBodyStyle) {
this.originalBodyStyle = {
overflow: document.body.style.overflow,
position: document.body.style.position
};
}
// 防止滚动
document.body.style.overflow = 'hidden';
document.body.style.position = 'fixed';
document.body.style.width = '100%';
},
// 恢复body滚动
restoreBodyScroll() {
// 恢复原始样式
if (this.originalBodyStyle) {
document.body.style.overflow = this.originalBodyStyle.overflow || '';
document.body.style.position = this.originalBodyStyle.position || '';
document.body.style.width = '';
this.originalBodyStyle = null;
}
},
onSelect(selectedKeys, info) {
// 在复选模式下,onSelect 可以用来处理一些辅助功能,比如展开/折叠
// console.log('onSelect', selectedKeys, info);
},
// 搜索输入处理
onSearchInput(value) {
this.searchValue = value || '';
if (value && value.length > 0) {
// 过滤搜索选项
this.searchOptions = dataList
.filter(item => item.title == value || item.no == value)
.map(item => ({
key: item.key,
label: item.title,
displayLabel: `${item.title}${item.no ? ' (' + item.no + ')' : ''}${item.key >= 1000000 ? ' [部门]' : ' [人员]'}`,
type: item.key >= 1000000 ? 'department' : 'person'
}))
.slice(0, 10); // 限制最多显示10个结果
this.showSearchMenu = true;
} else {
this.searchOptions = [];
this.showSearchMenu = false;
}
},
// 手动搜索
manualSearch() {
if (this.searchText && this.searchText.length > 0) {
this.onSearchInput(this.searchText);
}
},
// 隐藏搜索菜单
hideSearchMenu() {
// 延迟隐藏,以便点击选项时能正常触发
setTimeout(() => {
this.showSearchMenu = false;
}, 500);
},
// 选择搜索选项
selectSearchOption(option) {
this.checkLoading = true;
// 强制Vue重新渲染
this.$forceUpdate();
console.log(option,'selectSearchOption=======')
// 清空搜索状态
this.searchText = '';
this.searchOptions = [];
this.showSearchMenu = false;
setTimeout(() => {
// 勾选选中的节点
if (!this.checkedKeys.includes(option.key)) {
this.checkedKeys = [option.key, ...this.checkedKeys];
}else{
//根据 key 移除
this.checkedKeys = this.checkedKeys.filter(item => item !== option.key);
}
this.updateSelectedStaff();
this.checkLoading = false;
}, 0);
},
// 获取节点到根节点的路径
getParentKeys(key, treeData, parents = []) {
for (let node of treeData) {
if (node.key === key) {
return parents;
}
if (node.children) {
const found = this.getParentKeys(key, node.children, [...parents, node.key]);
if (found.length > parents.length) {
return found;
}
}
}
return parents;
},
// 原有的搜索方法,现在主要用于高亮显示
onChange(value) {
if (!value) {
this.searchValue = '';
return;
}
this.searchValue = value;
// 展开包含搜索结果的节点
// const expandedKeys = dataList.map((item) => {
// if (item.title.indexOf(value) > -1) {
// return getParentKey(item.key, this.getTreeData)
// }
// return null
// }).filter((item, i, self) => item && self.indexOf(item) === i)
// if (expandedKeys.length > 0) {
// this.expandedKeys = [...new Set([...this.expandedKeys, ...expandedKeys])];
// this.autoExpandParent = true;
// }
},
// 获取节点下的所有叶子节点key
getAllLeafKeys(nodes) {
let leafKeys = [];
nodes.forEach(node => {
if (node.children && node.children.length > 0) {
// 如果有子节点,递归获取
leafKeys = leafKeys.concat(this.getAllLeafKeys(node.children));
} else {
// 如果是叶子节点(人员),添加到结果中
if (node.key < 1000000) { // 小于1000000的是人员
leafKeys.push(node.key);
}
}
});
return leafKeys;
},
// 更新选中的人员列表
updateSelectedStaff() {
this.checkedPeople = dataList.filter(node => node.key < 1000000 && this.checkedKeys.includes(node.key))
// 向父组件发送选中的人员列表
this.$emit('staffNode', this.checkedPeople);
},
// 构建节点缓存,避免每次查找都遍历树
buildNodeCache(nodes) {
nodes.forEach(node => {
this.nodeCache.set(node.key, node);
if (node.children && node.children.length > 0) {
this.buildNodeCache(node.children);
}
});
},
orderStaffNode(){
//对选择的人根据选择顺序排序
let staffNode = []
for(var i=0;i<this.checkedKeys.length;i++){
let key = this.checkedKeys[i]
if(key<1000000){//小于百万的为人员。大于为机构
let node =this.checkedPeople.filter(item =>item.key==key)[0]
if(node){
staffNode.push(node);
}
}
}
return staffNode
}
},
watch: {
checkedKeys: {
handler(val) {
// 当 checkedKeys 改变时,同步更新 value,但避免循环
const filteredKeys = val.filter(key => key < 1000000); // 只传递人员key给父组件
// 只有当过滤后的key与当前value不同时才emit,避免循环
if (JSON.stringify(filteredKeys) !== JSON.stringify(this.value || [])) {
this.$emit('input', filteredKeys);
}
},
deep: true
},
value: {
handler(newValue) {
// 只有当树已经加载完成且新值与当前checkedKeys中的人员key不同时才更新,避免循环
if (this.treeData.length > 0) {
const currentStaffKeys = this.checkedKeys.filter(key => key < 1000000);
if (JSON.stringify(newValue || []) !== JSON.stringify(currentStaffKeys)) {
this.checkedKeys = newValue || [];
if (this.checkedKeys.length > 0) {
this.updateSelectedStaff();
}
}
}
},
immediate: true
}
},
}
// 筛选方法 结束
</script>
<style scoped>
.search-container {
position: fixed;
width: 17vw;
z-index: 9999;
padding: 0 1vh;
}
.search-wrapper {
position: relative;
}
.search-input {
padding-top: 10px;
padding-left: 5px;
border-radius: 4px !important;
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
/* box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); */
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.search-input:hover {
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12);
transform: translateY(-1px);
}
.search-input:focus-within {
box-shadow: 0 8px 25px rgba(33, 150, 243, 0.15);
transform: translateY(-2px);
}
.search-input >>> .q-field__control {
border-radius: 12px;
padding: 8px 16px;
}
.search-input >>> .q-field__label {
font-weight: 500;
color: #666;
font-size: 12px;
}
.search-input >>> .q-field__native {
font-size: 14px;
font-weight: 400;
padding-left: 12px !important;
padding-top: 2px !important;
padding-bottom: 2px !important;
}
.search-input >>> .q-field__native::placeholder {
font-size: 14px;
color: #999;
position: relative;
left: 0px;
top: 0px;
transform: none;
}
.search-icon {
color: #999;
transition: color 0.3s ease;
}
.search-input:hover .search-icon {
color: #1976d2;
}
.search-btn {
margin-right: 4px;
transition: all 0.3s ease;
}
.search-btn:hover {
background-color: rgba(25, 118, 210, 0.1);
transform: scale(1.05);
}
.search-menu {
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
border: 1px solid rgba(255, 255, 255, 0.2);
backdrop-filter: blur(8px);
background: rgba(255, 255, 255, 0.95);
min-width: 16vw;
max-width: 16vw;
max-height: 300px;
overflow-y: auto;
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 1000;
margin-top: 8px;
}
.search-results {
padding: 8px 0;
}
.search-result-item {
display: flex;
align-items: center;
padding: 12px 16px;
border-radius: 8px;
margin: 2px 8px;
cursor: pointer;
transition: all 0.2s ease;
border: 1px solid transparent;
}
.search-result-item:hover {
background-color: rgba(25, 118, 210, 0.08);
transform: translateX(2px);
border-color: rgba(25, 118, 210, 0.1);
}
.search-result-avatar {
flex-shrink: 0;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background-color: rgba(0, 0, 0, 0.04);
margin-right: 12px;
}
.search-result-content {
flex: 1;
min-width: 0;
}
.search-result-side {
flex-shrink: 0;
margin-left: 8px;
opacity: 0.6;
}
.search-result-label {
font-weight: 500;
color: #333;
font-size: 14px;
}
.search-result-caption {
font-size: 12px;
color: #666;
display: flex;
align-items: center;
gap: 8px;
}
.search-result-id {
background-color: rgba(102, 102, 102, 0.1);
padding: 2px 6px;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
}
.no-results {
padding: 16px;
text-align: center;
}
.no-result-item {
display: flex;
align-items: center;
padding: 12px 16px;
justify-content: center;
}
.no-result-avatar {
flex-shrink: 0;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background-color: rgba(0, 0, 0, 0.04);
margin-right: 12px;
}
.no-result-content {
flex: 1;
text-align: left;
}
.no-results-text {
color: #666;
font-weight: 500;
font-size: 14px;
}
.no-results-hint {
color: #999;
font-size: 12px;
margin-top: 4px;
}
/* 滚动条美化 */
.search-menu::-webkit-scrollbar {
width: 6px;
}
.search-menu::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.05);
border-radius: 3px;
}
.search-menu::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.2);
border-radius: 3px;
}
.search-menu::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.3);
}
/* 树组件的半透明Loading遮罩层 */
.tree-loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.75);
backdrop-filter: blur(2px);
z-index: 100;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
opacity: 0;
visibility: hidden;
transition: opacity 0.2s ease, visibility 0.2s ease;
}
.tree-loading-overlay.is-loading {
opacity: 1;
visibility: visible;
}
.tree-wrapper {
transition: opacity 0.2s ease;
}
.tree-wrapper.is-dimmed {
opacity: 0.3;
}
.tree-loading-text {
color: #666;
font-size: 14px;
font-weight: 500;
}
/* 响应式设计 */
@media (max-width: 768px) {
.search-container {
width: 90vw;
}
.search-menu {
min-width: 90vw;
max-width: 90vw;
}
}
</style>
<template>
<div style="position: relative;">
<div class="tree-wrapper">
<div class="search-container">
<div class="search-wrapper">
<q-input
v-model="searchText"
clearable
class="search-input"
label="搜索部门"
placeholder="输入部门名称搜索"
@input="onSearchInput"
@focus="showSearchMenu = true"
@blur="hideSearchMenu"
>
<template v-slot:prepend>
<q-icon name="search" class="search-icon" />
</template>
</q-input>
<!-- 搜索结果菜单 -->
<div v-show="showSearchMenu" class="search-menu">
<div class="search-results" v-if="searchResults.length > 0">
<div
v-for="item in searchResults"
:key="item.key"
class="search-result-item"
@click="onSearchResultClick(item)"
>
<div class="search-result-avatar">
<q-icon name="business" color="primary" size="sm" />
</div>
<div class="search-result-content">
<div class="search-result-label">{{ item.title }}</div>
</div>
<div class="search-result-side">
<q-icon name="keyboard_arrow_right" size="sm" color="grey-5" />
</div>
</div>
</div>
<div v-else-if="searchText && searchText.length > 0" class="no-results">
没有找到匹配的部门
</div>
</div>
</div>
</div>
<div style="padding-top: 6vh;">
<Tree
v-if="treeData.length > 0"
:expandedKeys="expandedKeys"
:autoExpandParent="autoExpandParent"
:treeData="treeData"
@expand="onExpand"
>
<template slot="title" slot-scope="node">
<span style="display: inline-flex; align-items: center; width: 100%;">
<span style="flex: 1;">{{ node.title }}</span>
<!-- 编辑按钮 -->
<q-btn
flat
round
dense
icon="edit"
size="sm"
class="dept-edit-btn"
@click.stop="onDeptBtnClick(node)"
/>
</span>
</template>
</Tree>
</div>
</div>
<div v-show="!treeData.length">
正在加载组织机构...
</div>
<!-- 部门用户选择弹窗 -->
<q-modal v-model="deptModal" maximized>
<q-modal-layout>
<q-toolbar slot="header" color="primary">
<q-btn flat round dense @click="deptModal = false" icon="reply" />
<q-toolbar-title>
{{ currentDept ? currentDept.title : '' }} — 部门用户
</q-toolbar-title>
</q-toolbar>
<q-toolbar slot="footer">
<q-toolbar-title></q-toolbar-title>
<q-btn round color="red" @click="confirmDeptSelection">确认</q-btn>
<q-btn round @click="deptModal = false">取消</q-btn>
</q-toolbar>
<div class="q-pa-md">
<q-input
v-model="deptUserSearchText"
clearable
outlined
dense
placeholder="搜索部门用户..."
class="q-mb-md"
>
<template v-slot:prepend>
<q-icon name="search" />
</template>
</q-input>
<div class="select-all-row">
<q-checkbox
:value="filteredDeptUsers.length > 0 && filteredDeptUsers.every(u => getCurrentCheckedSet().has(u.key))"
:indeterminate="filteredDeptUsers.some(u => getCurrentCheckedSet().has(u.key)) && !filteredDeptUsers.every(u => getCurrentCheckedSet().has(u.key))"
@input="toggleSelectAll"
/>
<span class="select-all-label">全选</span>
</div>
<div v-if="deptUserLoading" class="text-center q-pa-md text-grey-6">
<q-spinner-dots size="32px" /> 加载中...
</div>
<div v-else-if="filteredDeptUsers.length === 0" class="text-center q-pa-md text-grey-5">
该部门下暂无用户
</div>
<div v-else class="dept-user-list">
<div
v-for="user in filteredDeptUsers"
:key="user.key"
class="dept-user-item"
@click="toggleTempUser(user.key)"
>
<q-checkbox
:value="deptUserSearchText ? searchCheckedUsers.has(user.key) : tempCheckedUsers.has(user.key)"
@input="toggleTempUser(user.key)"
/>
<div class="dept-user-info">
<div class="dept-user-name">{{ user.title }}</div>
<div class="dept-user-no" v-if="user.no">{{ user.no }}</div>
</div>
</div>
</div>
<div class="text-grey-6 text-center q-mt-md">
已选 {{ tempCheckedUsers.size }} 人
</div>
</div>
</q-modal-layout>
</q-modal>
</div>
</template>
<script>
import Tree from 'ant-design-vue/lib/tree';
import 'ant-design-vue/dist/antd.css';
import {deptTree, getDeptUsers} from "@/service/user/user";
let cachedTreeData = null;
export default {
name: "organization-dept-panel",
components: {Tree},
props: {
value: {
type: Array,
default: () => []
}
},
data() {
return {
treeData: [],
expandedKeys: [],
autoExpandParent: true,
searchText: '',
showSearchMenu: false,
searchResults: [],
// 部门弹窗
deptModal: false,
currentDept: null,
deptUsers: [], // 部门下的所有用户
deptUserSearchText: '',
deptUserLoading: false,
tempCheckedUsers: new Set(), // 弹窗内临时勾选状态
searchCheckedUsers: new Set(), // 搜索框内临时勾选状态
// 已选用户集合(主数据源,同步 orgList)
selectedUserIds: new Set(),
};
},
computed: {
filteredDeptUsers() {
if (!this.deptUserSearchText) return this.deptUsers;
const kw = this.deptUserSearchText.toLowerCase();
return this.deptUsers.filter(u =>
(u.title || '').toLowerCase().includes(kw) ||
(u.no || '').toLowerCase().includes(kw)
);
}
},
watch: {
value: {
handler(newVal) {
this.selectedUserIds = new Set(newVal || []);
},
immediate: true
}
},
created() {
this.initTree();
},
methods: {
async initTree() {
if (cachedTreeData) {
this.treeData = cachedTreeData;
this.expandedKeys = this.treeData.length > 0 ? [this.treeData[0].key] : [];
return;
}
let Res = await deptTree();
cachedTreeData = [Res.data.data];
this.treeData = cachedTreeData;
this.expandedKeys = this.treeData.length > 0 ? [this.treeData[0].key] : [];
},
onExpand(expandedKeys) {
this.expandedKeys = expandedKeys;
this.autoExpandParent = false;
},
// 搜索部门
onSearchInput(value) {
if (!value || value.length < 1) {
this.searchResults = [];
return;
}
const kw = value.toLowerCase();
const results = [];
const searchTree = (nodes) => {
for (const node of nodes) {
if ((node.title || '').toLowerCase().includes(kw)) {
results.push({key: node.key, title: node.title});
}
if (node.children) searchTree(node.children);
}
};
searchTree(this.treeData);
this.searchResults = results.slice(0, 20);
},
hideSearchMenu() {
setTimeout(() => { this.showSearchMenu = false; }, 200);
},
onSearchResultClick(item) {
this.searchText = item.title;
this.showSearchMenu = false;
// 展开到该节点
if (!this.expandedKeys.includes(item.key)) {
this.expandedKeys.push(item.key);
}
this.onDeptBtnClick(item);
},
// 点击部门编辑按钮
async onDeptBtnClick(dept) {
this.currentDept = dept;
this.deptUserSearchText = '';
this.deptModal = true;
// 懒加载部门用户
if (dept.key >= 1000000) {
const realDeptId = dept.key - 1000000;
this.deptUserLoading = true;
this.deptUsers = [];
try {
let res = await getDeptUsers(realDeptId, 1, 100);
let users = (res.data.data.list || res.data.data) || [];
this.deptUsers = users.map(u => ({
key: u.id,
title: u.title || u.label || '',
no: u.no || ''
}));
} catch (e) {
console.error('loadDeptUsers error:', e);
this.deptUsers = [];
} finally {
this.deptUserLoading = false;
// deptUsers 填充后再初始化勾选状态
this.tempCheckedUsers = new Set(
[...this.selectedUserIds].filter(id => this.deptUsers.some(u => u.key === id))
);
}
} else {
// 部门key < 1000000,直接从树节点 children
this.deptUsers = (dept.children || []).map(u => ({
key: u.key,
title: u.title || u.label || '',
no: u.no || ''
}));
// deptUsers 填充后再初始化勾选状态
this.tempCheckedUsers = new Set(
[...this.selectedUserIds].filter(id => this.deptUsers.some(u => u.key === id))
);
}
},
toggleTempUser(userKey) {
const currentSet = this.deptUserSearchText ? this.searchCheckedUsers : this.tempCheckedUsers;
const newSet = new Set(currentSet);
if (newSet.has(userKey)) {
newSet.delete(userKey);
} else {
newSet.add(userKey);
}
if (this.deptUserSearchText) {
this.searchCheckedUsers = newSet;
} else {
this.tempCheckedUsers = newSet;
}
},
getCurrentCheckedSet() {
return this.deptUserSearchText ? this.searchCheckedUsers : this.tempCheckedUsers;
},
toggleSelectAll(checked) {
const currentSet = this.deptUserSearchText ? this.searchCheckedUsers : this.tempCheckedUsers;
if (checked) {
this.filteredDeptUsers.forEach(u => currentSet.add(u.key));
} else {
this.filteredDeptUsers.forEach(u => currentSet.delete(u.key));
}
if (this.deptUserSearchText) {
this.searchCheckedUsers = new Set(currentSet);
} else {
this.tempCheckedUsers = new Set(currentSet);
}
},
confirmDeptSelection() {
const prevInDept = new Set([...this.selectedUserIds].filter(id => this.deptUsers.some(u => u.key === id)));
// 添加本次新增的
const newSelected = new Set([...this.selectedUserIds]);
this.tempCheckedUsers.forEach(id => newSelected.add(id));
this.searchCheckedUsers.forEach(id => newSelected.add(id));
// 移除本次取消的
prevInDept.forEach(id => {
if (!this.tempCheckedUsers.has(id) && !this.searchCheckedUsers.has(id)) {
newSelected.delete(id);
}
});
this.selectedUserIds = newSelected;
this.$emit('change', [...this.selectedUserIds]);
this.deptModal = false;
}
}
};
</script>
<style scoped>
.search-container {
position: fixed;
width: 17vw;
z-index: 9999;
padding: 0 1vh;
}
.search-wrapper {
position: relative;
}
.search-input {
padding-top: 10px;
padding-left: 5px;
border-radius: 4px !important;
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.search-input:hover {
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12);
transform: translateY(-1px);
}
.search-input:focus-within {
box-shadow: 0 8px 25px rgba(33, 150, 243, 0.15);
transform: translateY(-2px);
}
.search-input >>> .q-field__control {
border-radius: 12px;
padding: 8px 16px;
}
.search-input >>> .q-field__label {
font-weight: 500;
color: #666;
font-size: 12px;
}
.search-input >>> .q-field__native {
font-size: 14px;
font-weight: 400;
padding-left: 12px !important;
padding-top: 2px !important;
padding-bottom: 2px !important;
}
.search-input >>> .q-field__native::placeholder {
font-size: 14px;
color: #999;
}
.search-icon {
color: #999;
transition: color 0.3s ease;
}
.search-input:hover .search-icon {
color: #1976d2;
}
.search-menu {
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
border: 1px solid rgba(255, 255, 255, 0.2);
backdrop-filter: blur(8px);
background: rgba(255, 255, 255, 0.95);
min-width: 16vw;
max-width: 16vw;
max-height: 300px;
overflow-y: auto;
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 1000;
margin-top: 8px;
}
.search-results {
padding: 8px 0;
}
.search-result-item {
display: flex;
align-items: center;
padding: 12px 16px;
border-radius: 8px;
margin: 2px 8px;
cursor: pointer;
transition: all 0.2s ease;
border: 1px solid transparent;
}
.search-result-item:hover {
background-color: rgba(25, 118, 210, 0.08);
transform: translateX(2px);
border-color: rgba(25, 118, 210, 0.1);
}
.search-result-avatar {
flex-shrink: 0;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background-color: rgba(0, 0, 0, 0.04);
margin-right: 12px;
}
.search-result-content {
flex: 1;
min-width: 0;
}
.search-result-side {
flex-shrink: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
opacity: 0.5;
}
.search-result-label {
font-weight: 500;
color: #333;
font-size: 14px;
}
.no-results {
padding: 16px;
text-align: center;
color: #999;
font-size: 14px;
}
.dept-edit-btn {
transition: opacity 0.2s;
}
.dept-user-list {
max-height: 60vh;
overflow-y: auto;
border: 1px solid #e0e0e0;
border-radius: 8px;
}
.select-all-row {
display: flex;
align-items: center;
padding: 6px 0;
margin-bottom: 4px;
}
.select-all-label {
font-size: 14px;
color: #333;
user-select: none;
}
.dept-user-item {
display: flex;
align-items: center;
padding: 12px 16px;
cursor: pointer;
border-bottom: 1px solid #f0f0f0;
transition: background-color 0.15s;
}
.dept-user-item:last-child {
border-bottom: none;
}
.dept-user-item:hover {
background-color: #f5f5f5;
}
.dept-user-info {
margin-left: 12px;
flex: 1;
}
.dept-user-name {
font-size: 14px;
color: #333;
}
.dept-user-no {
font-size: 12px;
color: #999;
margin-top: 2px;
}
.dept-edit-btn {
transition: opacity 0.2s;
}
:deep(.ant-tree-title:hover) .dept-edit-btn {
opacity: 1;
}
.tree-wrapper {
transition: opacity 0.2s ease;
}
</style>
......@@ -40,28 +40,51 @@
<div class="col-2 q-ml-lg" style="height:80vh;overflow:auto;">
</div>
<div class="col-4 q-mt-lg" style="height:80vh;overflow:auto;">
<organizationAnt v-model="orgList" @staffNode="staffNode" />
<organizationDeptPanel :value="orgList" @change="onOrgListChange" />
</div>
<div class="col-4 q-mt-lg" style="height:80vh;overflow:auto;" >
<div class="q-mb-lg q-mt-lg">
显示已选择的成员,共{{selectStaff.length}}人
</div>
<div v-if="!selectStaff || selectStaff.length ===0" class="text-grey-5">
没有选择任何成员
<div class="col-4 q-mt-lg" style="height:80vh;overflow:auto;" ref="staffListEl" @scroll="onStaffScroll">
<div class="q-mb-md">
<q-input
v-model="selectedSearchText"
clearable
outlined
dense
placeholder="搜索已选成员..."
>
<template v-slot:prepend>
<q-icon name="search" />
</template>
</q-input>
<div class="q-mt-xs text-grey-6" style="font-size:12px;">
共 {{ filteredStaff.length }} 人{{ selectedSearchText ? '(已过滤)' : '' }}
</div>
</div>
<div v-else >
<div class="row shadow-1" v-for="node in selectStaff" :key="node.id" >
<div class="col" style="display:flex;align-items: center;">
<q-icon name="person" size="16px"/>
<div v-if="visibleStaff.length > 0">
<div class="row shadow-1 q-mb-xs" v-for="node in visibleStaff" :key="node.key">
<div class="col" style="display:flex;align-items:center;">
<q-icon name="person" size="16px" />
<div>
{{' '+node.title}}&nbsp;<span style="color: #64b5fe">{{ node.no ? node.no:'' }}</span>
{{ ' ' + node.title }}
<span style="color:#64b5fe">{{ node.no ? ' ' + node.no : '' }}</span>
</div>
</div>
<div class="col">
<q-btn class=" float-right" flat color="primary" icon="clear" @click="deleteNode(node)"/>
<div class="col" style="display:flex;align-items:center;justify-content:flex-end;">
<q-btn flat color="primary" icon="clear" @click="deleteNode(node)" />
</div>
</div>
</div>
<div v-if="staffLoading" class="text-center q-pa-sm text-grey-6">
加载中...
</div>
<div v-else-if="!selectedSearchText && filteredStaff.length === 0" class="text-grey-5 q-mt-md">
没有选择任何成员
</div>
<div v-else-if="selectedSearchText && filteredStaff.length === 0" class="text-grey-5 q-mt-md">
没有匹配的成员
</div>
</div>
</div>
</q-modal-layout>
......@@ -70,15 +93,21 @@
</template>
<script>
import { getRolePagedList } from "@/service/permission/role";
import { getUserRoleList, editRoleUser,editUserRoleList} from "@/service/user/user";
import organizationAnt from '@/components/organization-ant'
import { getUserRoleList, editRoleUser, editUserRoleList, listByIds} from "@/service/user/user";
import organizationDeptPanel from '@/components/organization-dept-panel'
export default {
components: {organizationAnt},
components: { organizationDeptPanel},
// name: "roleuser",
data() {
return {
orgList:[],//选中的树节点
selectStaff:[],//选中的人
selectedSearchText: '', // 右侧已选列表搜索
visibleStaff: [], // 当前可见的已选成员(分页)
staffPageSize: 50, // 每页加载条数
staffPage: 1, // 当前页
staffLoading: false, // 加载中
staffListEl: null, // 滚动容器 ref
treeKey:[],
resdata:[],
expandedKeys: [],
......@@ -190,9 +219,41 @@ export default {
}
};
},
computed: {
filteredStaff() {
if (!this.selectedSearchText) return this.selectStaff;
const kw = this.selectedSearchText.toLowerCase();
return this.selectStaff.filter(s =>
(s.title || '').toLowerCase().includes(kw) ||
(s.no || '').toLowerCase().includes(kw)
);
}
},
watch: {
filteredStaff() {
this.staffPage = 1;
this.visibleStaff = this.filteredStaff.slice(0, this.staffPageSize);
}
},
methods: {
staffNode(nodeList) {//组织机构树人员选择后组件的回调方法
this.selectStaff = nodeList
onOrgListChange(selectedIds) {//新组件选中后回调
this.orgList = selectedIds;
if (selectedIds.length > 0) {
listByIds(selectedIds).then(res => {
let users = res.data.data || [];
this.selectStaff = users.map(u => ({
key: u.id,
title: u.title || u.label || '',
no: u.no || ''
}));
this.visibleStaff = this.selectStaff.slice(0, this.staffPageSize);
this.staffPage = 1;
});
} else {
this.selectStaff = [];
this.visibleStaff = [];
this.staffPage = 1;
}
},
// checkStaffNode(nodeList){//组织机构树人员选择后组件的回调方法-返回选中的node
// this.selectStaff = nodeList
......@@ -207,7 +268,30 @@ export default {
}
var staff = this.selectStaff.filter(staff => staff.key !== node.key);
this.selectStaff = staff;
this.orgList = this.selectStaff.map(item=>item.key)
this.orgList = this.selectStaff.map(item=>item.key);
this.visibleStaff = this.filteredStaff.slice(0, this.staffPage * this.staffPageSize);
},
onStaffScroll(e) {
const el = e.target;
if (el.scrollHeight - el.scrollTop - el.clientHeight < 50) {
this.loadMoreStaff();
}
},
loadMoreStaff() {
if (this.staffLoading) return;
const total = this.filteredStaff.length;
const next = (this.staffPage + 1) * this.staffPageSize;
if (next >= total) {
this.visibleStaff = this.filteredStaff;
this.staffLoading = false;
} else {
this.staffLoading = true;
setTimeout(() => {
this.staffPage++;
this.visibleStaff = this.filteredStaff.slice(0, this.staffPage * this.staffPageSize);
this.staffLoading = false;
}, 100);
}
},
async request(props) {
this.loading = true;
......@@ -234,15 +318,31 @@ export default {
this.roleId = props.value;
this.roleName = props.row.namezh;
this.editModal = true;
let query = { role: this.roleId};
let query = { role: this.roleId };
let dataRes = await getUserRoleList(query);
let data = dataRes.data.data;
this.orgList = data.map(item=>item.userId);
this.orgList = data || [];
if (this.orgList.length > 0) {
let idsRes = await listByIds(this.orgList);
let users = idsRes.data.data || [];
this.selectStaff = users.map(u => ({
key: u.id,
title: u.title || u.label || '',
no: u.no || ''
}));
this.visibleStaff = this.selectStaff.slice(0, this.staffPageSize);
this.staffPage = 1;
} else {
this.selectStaff = [];
}
},
closeEditModal(){//关闭弹层,清空树
this.editModal=false
this.orgList = []
this.selectStaff = []
this.visibleStaff = []
this.staffPage = 1
this.selectedSearchText = ''
},
search() {
this.request({
......
<template>
<div style="padding:10px">
<q-card inline class="fit shadow-6">
<div>
</div>
<q-card-main style="height:80%">
<q-table ref="table" color="primary" :data="serverData" :columns="columns" separator="cell" selection="none" row-key="id" :rows-per-page-options="[5,10,20,30,40,50,60,200,500]" :pagination.sync="serverPagination"
@request="request" :loading="loading" :rows-per-page-label="$t('Rows per page')" :no-data-label="$t('No data')">
<template slot="top-left" slot-scope="props">
<q-input v-model="filter.namezh" type="text" :prefix="$t('Role name') + ':'" />&nbsp;&nbsp;
<q-input v-model="filter.name" type="text" :prefix="$t('Role code') + ':'" />&nbsp;&nbsp;
<q-btn push dense color="primary" icon="search" @click="search">{{$t('Search')}}</q-btn>&nbsp;&nbsp;
</template>
<template slot="top-right" slot-scope="props">
<q-btn flat round dense :icon="props.inFullscreen ? 'fullscreen_exit' : 'fullscreen'" @click="props.toggleFullscreen" />
</template>
<q-td slot="body-cell-id" slot-scope="props" :props="props" style="width:150px">
<q-btn glossy color="secondary" :label="$t('User list')" @click="editRoleUser(props)"></q-btn>
</q-td>
</q-table>
</q-card-main>
</q-card>
<q-modal v-model="editModal" maximized>
<q-modal-layout>
<q-toolbar slot="header">
<q-btn flat round dense @click="closeEditModal" icon="reply" />
<q-toolbar-title>
{{$t('Editing role')}}&nbsp;
<q-chip small>{{roleName}}</q-chip>&nbsp;{{$t('User under')}}
</q-toolbar-title>
</q-toolbar>
<q-toolbar slot="footer">
<q-toolbar-title>
</q-toolbar-title>
<q-btn round color="red" @click="modifyRoleUser">{{$t('Save')}}</q-btn>
<q-btn round @click="closeEditModal">{{$t('Cancel')}}</q-btn>
</q-toolbar>
<!-- 选择个人 -->
<div class=" row q-mb-lg q-ml-lg q-mr-lg" >
<div class="col-2 q-ml-lg" style="height:80vh;overflow:auto;">
</div>
<div class="col-4 q-mt-lg" style="height:80vh;overflow:auto;">
<organizationAnt v-model="orgList" @staffNode="staffNode" />
</div>
<div class="col-4 q-mt-lg" style="height:80vh;overflow:auto;" >
<div class="q-mb-lg q-mt-lg">
显示已选择的成员,共{{selectStaff.length}}人
</div>
<div v-if="!selectStaff || selectStaff.length ===0" class="text-grey-5">
没有选择任何成员
</div>
<div v-else >
<div class="row shadow-1" v-for="node in selectStaff" :key="node.id" >
<div class="col" style="display:flex;align-items: center;">
<q-icon name="person" size="16px"/>
<div>
{{' '+node.title}}&nbsp;<span style="color: #64b5fe">{{ node.no ? node.no:'' }}</span>
</div>
</div>
<div class="col">
<q-btn class=" float-right" flat color="primary" icon="clear" @click="deleteNode(node)"/>
</div>
</div>
</div>
</div>
</div>
</q-modal-layout>
</q-modal>
</div>
</template>
<script>
import { getRolePagedList } from "@/service/permission/role";
import { getUserRoleList, editRoleUser,editUserRoleList} from "@/service/user/user";
import organizationAnt from '@/components/organization-ant'
export default {
components: {organizationAnt},
// name: "roleuser",
data() {
return {
orgList:[],//选中的树节点
selectStaff:[],//选中的人
treeKey:[],
resdata:[],
expandedKeys: [],
autoExpandParent: true,
checkedKeys: [],
selectedKeys: [],
namefilter:"",
leaderSelected: [],
leaderList: [],
tempFunction:{
contactList:[{}],
},
serverData: [],
serverPagination: {
page: 1,
rowsNumber: 0, // specifying this determines pagination is server-side
rowsPerPage: 10 // current rows per page being displayed
},
columns: [
{
name: "namezh",
required: true,
label: this.$t("Role name"),
align: "left",
field: "namezh",
sortable: true
},
{
name: "name",
label: this.$t("Role code"),
field: "name",
sortable: true,
align: "left",
},
{
name: "id",
required: true,
label: this.$t("ID"),
align: "left",
field: "id"
}
],
filter: {
name: "",
namezh: ""
},
loading: false,
editModal: false,
roleId: 0,
roleName: "",
roleUser: {
serverData: [],
serverPagination: {
page: 1,
rowsNumber: 0, // specifying this determines pagination is server-side
rowsPerPage: 10 // current rows per page being displayed
},
columns: [
{
name: "username",
required: true,
label: this.$t("Name"),
align: "left",
field: "username",
sortable: true
},
{
name: "account",
label: this.$t("Account"),
field: "account",
sortable: true,
align: "left"
},
{
name: "email",
label: this.$t("Email"),
field: "email",
sortable: true,
align: "left"
},
{
name: "phoneNumber",
label: this.$t("Phone"),
field: "phoneNumber",
sortable: true,
align: "left"
},
{
name: "position",
required: true,
label: this.$t("Position"),
align: "left",
field: "position"
},
{
name: "id",
required: true,
label: this.$t("ID"),
align: "left",
field: "id"
}
],
filter: {
account: "",
username: "",
roleId: ""
},
loading: false
}
};
},
methods: {
staffNode(nodeList) {//组织机构树人员选择后组件的回调方法
this.selectStaff = nodeList
},
// checkStaffNode(nodeList){//组织机构树人员选择后组件的回调方法-返回选中的node
// this.selectStaff = nodeList
// },
// checkStaffKey(keyList){//组织机构树人员选择后组件的回调方法-返回key
// this.selectStaff = keyList
// },
// 删除节点事件
deleteNode(node) {
if (!node) {
return false;
}
var staff = this.selectStaff.filter(staff => staff.key !== node.key);
this.selectStaff = staff;
this.orgList = this.selectStaff.map(item=>item.key)
},
async request(props) {
this.loading = true;
this.serverPagination = props.pagination;
let table = this.$refs.table,
{ page, rowsPerPage, sortBy, descending } = props.pagination;
let query = {
pageNum: page,
pageSize: rowsPerPage,
sortBy: sortBy,
descending: descending,
name: this.filter.name,
namezh: this.filter.namezh
};
let dataRes = await getRolePagedList(query);
let data = dataRes.data.data;
this.serverPagination.rowsNumber = data.total;
this.serverData = data.list;
setTimeout(() => {
this.loading = false;
}, 500);
},
async editRoleUser(props) {
this.roleId = props.value;
this.roleName = props.row.namezh;
this.editModal = true;
let query = { role: this.roleId};
let dataRes = await getUserRoleList(query);
let data = dataRes.data.data;
this.orgList = data.map(item=>item.userId);
},
closeEditModal(){//关闭弹层,清空树
this.editModal=false
this.orgList = []
this.selectStaff = []
},
search() {
this.request({
pagination: this.serverPagination,
filter: this.filter
});
},
searchRoleUser() {
this.roleUserRequest({
pagination: this.roleUser.serverPagination,
filter: this.roleUser.filter
});
},
async modifyRoleUser() {
//保存用户角色信息,并刷新角色用户列表
let userIds = this.selectStaff.map(item=>item.key).join(",")
let res = await editUserRoleList({
roleId: this.roleId,
userIds: userIds
});
this.closeEditModal()
this.$q.notify({
type: "positive",
message: this.$t("Added successfully"),
position: "bottom-right"
});
this.request({
pagination: this.serverPagination,
filter: this.filter
});
}
},
mounted() {
this.request({
pagination: this.serverPagination,
filter: this.filter
});
}
};
</script>
......@@ -189,6 +189,41 @@ export function getweixinisleaderindeptByaccount(){
})
}
export function deptTree() {
return request({
url: '/user/dept_tree',
method: 'get',
loading: 'gears'
});
}
export function searchUsers(keyword, pageNum, pageSize) {
return request({
url: '/user/search',
method: 'get',
params: { keyword, pageNum, pageSize },
loading: 'gears'
});
}
export function getDeptUsers(deptId, pageNum, pageSize) {
return request({
url: '/user/dept_users/' + deptId,
method: 'get',
params: { pageNum, pageSize },
loading: 'gears'
});
}
export function listByIds(ids) {
return request({
url: '/user/list_by_ids',
method: 'post',
data: ids,
loading: 'gears'
});
}
export function editUserRoleList(params) {
return request({
url: '/roleuser/editUserRoleList',
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论