Files
jnpf_app/components/Jnpf/OrganizeSelect/SelectPopup.vue
2026-01-19 17:34:15 +08:00

502 lines
13 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>
<u-popup class="jnpf-tree-select-popup" mode="right" v-model="showPopup" width="100%" @close="close">
<view class="jnpf-tree-select-body">
<view class="jnpf-tree-select-title">
<text class="icon-ym icon-ym-report-icon-preview-pagePre backIcon" @tap="close()"></text>
<view class="title">选择申请人单位</view>
</view>
<view class="jnpf-tree-select-search">
<u-search :placeholder="$t('app.apply.pleaseKeyword')" v-model="keyword" height="72"
:show-action="false" @change="handleSearch" bg-color="#f0f2f6" shape="square">
</u-search>
</view>
<view class="jnpf-tree-selected">
<view class="jnpf-tree-selected-head">
<view>{{$t('component.jnpf.common.selected')}}({{selectedList.length||0}})</view>
<view v-if="multiple" class="clear-btn" @click="setCheckAll">
{{$t('component.jnpf.common.clearAll')}}
</view>
</view>
<view class="jnpf-tree-selected-box">
<scroll-view scroll-y="true" class="select-list">
<u-tag closeable @close="delSelect(index)" v-for="(item,index) in selectedList" :key="index"
:text="item.deptName" class="u-selectTag" />
</scroll-view>
</view>
</view>
<view class="jnpf-tree-selected-line"></view>
<!-- <view class="jnpf-tree-selected-tabs">
<view class="tab-item" :class="{'tab-item-active':activeKey==='1'}" @click="toggloActive('1')">组织构架</view>
<view class="tab-item" :class="{'tab-item-active':activeKey==='2'}" @click="toggloActive('2')"
v-if="selectType === 'all'">
当前组织
</view>
</view> -->
<view class="jnpf-tree-select-tree">
<!-- 树形结构区域 -->
<scroll-view :scroll-y="true" style="height: 100%" v-if="activeKey==='1' && !hasPage">
<ly-tree v-if="selectType !== 'all'" :tree-data="options" :node-key="realProps.value"
default-expand-all :props="realProps" :filter-node-method="filterNode"
child-visible-for-filter-node show-node-icon @node-click="handleTreeNodeClick"
:show-checkbox="multiple" :show-radio="!multiple" :expandOnClickNode="false"
:checkOnClickNode="true" :expandOnCheckNode="false" checkStrictly ref="tree">
</ly-tree>
<ly-tree ref="tree" v-if="selectType === 'all'" :props="realProps" :node-key="realProps.value"
:load="loadNode" lazy :tree-data="lazyOptions" show-node-icon :defaultExpandAll='false'
@node-click="handleTreeNodeClick" :show-checkbox="multiple" :show-radio="!multiple"
:expandOnClickNode="false" :checkOnClickNode="true" :expandOnCheckNode="false" checkStrictly />
</scroll-view>
<!-- 列表/搜索结果区域 -->
<scroll-view :scroll-y="true" style="height: 100%" :refresher-enabled="false" :refresher-threshold="100"
:scroll-with-animation='true' :refresher-triggered="triggered" @scrolltolower="handleScrollToLower"
v-if="activeKey==='2'|| (activeKey==='1' && hasPage)">
<view class="jnpf-selcet-list" v-if="list.length">
<view class="jnpf-selcet-cell" v-for="item in list" :key="item.deptId"
@click.stop="handleNodeClick(item)">
<view class="jnpf-selcet-cell-action">
<lyCheckbox :type="multiple ? 'checkbox' : 'radio'"
:checked="selectedIds.includes(item.deptId)" />
</view>
<view class="jnpf-selcet-cell-name">
{{item.deptName}}
</view>
</view>
</view>
<Empty class="h-full" v-else />
</scroll-view>
</view>
<!-- 底部按钮 -->
<view class="jnpf-tree-select-actions">
<u-button class="buttom-btn" @click="close()">{{$t('common.cancelText')}}</u-button>
<u-button class="buttom-btn" type="primary"
@click.stop="handleConfirm()">{{$t('common.okText')}}</u-button>
</view>
</view>
</u-popup>
</template>
<script>
import {
getOrgByOrganizeCondition,
getOrganizeSelector
} from '@/api/common'
import lyCheckbox from '@/components/ly-tree/components/ly-checkbox.vue';
import Empty from '../Empty/index.vue'
// 适配接口返回的字段
const defaultProps = {
label: 'deptName', // 对应接口的deptName
value: 'deptId', // 对应接口的deptId
icon: 'icon',
children: 'children',// 子节点字段
isLeaf: 'isLeaf' // 叶子节点标识
}
export default {
props: {
selectedData: {
type: Array,
default: () => []
},
ableIds: {
type: Array,
default: () => []
},
selectType: {
type: String,
default: 'all'
},
type: {
type: String,
default: 'all'
},
modelValue: {
type: Boolean,
default: false
},
zIndex: {
type: [String, Number],
default: 0
},
props: {
type: Object,
default: () => ({})
},
multiple: {
type: Boolean,
default: false
}
},
components: {
lyCheckbox,
Empty
},
data() {
return {
moving: false,
selectedList: [],
selectedIds: [],
keyword: '',
showPopup: false,
options: [],
lazyOptions: [{}], // 懒加载根节点占位
activeKey: '1',
hasPage: false,
pagination: {
hasPage: 1,
currentPage: 1,
pageSize: 20
},
triggered: false,
finish: false,
list: []
};
},
watch: {
modelValue: {
handler(val) {
this.showPopup = val
if (!val) this.activeKey = ''
if (val) setTimeout(() => this.init(), 10);
},
immediate: true
},
selectedList: {
handler(val) {
// 适配deptId字段
this.selectedIds = val.map((o) => o.deptId);
this.$refs.tree && this.$refs.tree.setCheckedKeys(this.selectedIds)
},
deep: true
},
},
computed: {
uZIndex() {
return this.zIndex ? this.zIndex : this.$u.zIndex.popup;
},
realProps() {
return {
...defaultProps,
...this.props
}
},
getCurrOrgList() {
const userInfo = uni.getStorageSync('userInfo') || {}
const list = (userInfo.organizeList || []).map((o) => ({
...o,
deptName: o.treeName || o.deptName,
deptId: o.id || o.deptId
}))
return list
}
},
methods: {
init() {
this.keyword = ""
this.hasPage = 0
this.activeKey = '1'
this.finish = false
this.pagination.currentPage = 1
this.$nextTick(() => {
this.triggered = false
})
// 深拷贝选中数据
this.selectedList = JSON.parse(JSON.stringify(this.selectedData)).map(item => ({
deptId: item.id || item.deptId,
deptName: item.orgNameTree || item.deptName || item.fullName,
...item
}))
// 如果是全量选择,预加载根节点
if (this.selectType === 'all') {
this.preloadRootNodes()
} else {
this.getConditionOptions()
}
},
preloadRootNodes() {
const data = {
type: this.type,
}
getOrganizeSelector(data).then(res => {
const list = res.data || []
// 构建根节点树
this.options = this.buildTree(list, 'deptId', 'deptPid', '0')
console.log('预加载的根节点:', this.options)
// 设置初始选中状态
this.$nextTick(() => {
if (this.$refs.tree && this.selectedIds.length > 0) {
this.$refs.tree.setCheckedKeys(this.selectedIds)
}
})
})
},
getConditionOptions() {
if (!this.ableIds.length) return
const query = {
ids: this.ableIds
}
getOrgByOrganizeCondition(query).then(res => {
// 适配接口返回的扁平数据为树形结构
this.options = this.buildTree(res.data.list || [], 'deptId', 'deptPid', '0')
this.$refs.tree && this.$refs.tree.setCheckedKeys(this.selectedIds)
})
},
// 扁平数组转树形结构
buildTree(data, idKey, pidKey, rootPid) {
const result = []
const map = {}
// 先构建ID映射
data.forEach(item => {
map[item[idKey]] = { ...item, children: [] }
})
// 组装父子关系
data.forEach(item => {
const parent = map[item[pidKey]]
if (item[pidKey] === rootPid) {
result.push(map[item[idKey]])
} else if (parent) {
parent.children.push(map[item[idKey]])
}
})
return result
},
// 树形节点过滤
filterNode(value, data) {
if (!value) return true;
return data.deptName && data.deptName.indexOf(value) !== -1;
},
// 懒加载节点
loadNode(node, resolve) {
const parentId = node.key || '0'
// 获取该节点的深度信息
const level = node.level || 0
console.log('加载节点:', { parentId, level, node })
// 如果是根节点level为0直接使用已经构建好的options
if (level === 0 && this.options.length > 0) {
resolve(this.options)
return
}
const data = {
type: this.type,
}
getOrganizeSelector(data).then(res => {
const list = res.data || []
// 将返回的扁平数据转换为树形节点
const treeData = this.buildTree(list, 'deptId', 'deptPid', parentId)
// 设置节点的isLeaf属性根据是否有子节点判断
treeData.forEach(item => {
// 判断该节点是否还有子节点(根据实际情况调整)
// 如果知道接口返回是否有子节点的字段,可以替换这个判断
item.isLeaf = !item.children || item.children.length === 0
})
console.log(`加载父节点 ${parentId} 的子节点:`, treeData)
resolve(treeData)
}).catch(() => {
resolve([]) // 异常时返回空数组,避免组件报错
})
},
// 滚动加载(仅搜索列表使用)
handleScrollToLower() {
if (this.finish || this.activeKey === '2' || !this.keyword) return
this.getTreeData()
},
// 获取搜索列表数据
getTreeData() {
const data = {
type: this.type,
// keyword: this.keyword,
// parentId: '0',
// ...this.pagination
}
getOrganizeSelector(data).then(res => {
const list = res.data || []
if (list.length < this.pagination.pageSize) this.finish = true;
this.list = this.list.concat(list);
this.pagination.currentPage++
})
},
// 树形节点点击
handleTreeNodeClick(item) {
const data = item.data || item
this.handleNodeClick(data)
},
// 列表节点点击
handleNodeClick(data) {
// 适配deptId字段
const index = this.selectedList.findIndex((o) => o.deptId === data.deptId);
if (index !== -1) {
this.selectedList.splice(index, 1);
} else {
this.multiple ? this.selectedList.push(data) : (this.selectedList = [data]);
}
},
// 删除选中项
delSelect(index) {
this.selectedList.splice(index, 1);
},
// 清空选中
setCheckAll() {
this.selectedIds = []
this.selectedList = []
},
// 确认选择
handleConfirm() {
// 转换回原始字段格式,兼容父组件
const resultList = this.selectedList.map(item => ({
id: item.deptId,
orgNameTree: item.deptName,
...item
}))
const resultIds = resultList.map(item => item.id)
console.log(resultIds,resultList,'数据')
this.$emit('confirm', resultList, resultIds);
this.close();
},
// 关闭弹窗
close() {
this.$emit('update:modelValue', false); // 双向绑定规范
this.$emit('close', false);
},
// 切换标签
toggloActive(key) {
if (this.activeKey === key) return
this.keyword = ''
this.$nextTick(() => {
this.activeKey = key
if (this.activeKey === '2') {
this.list = this.getCurrOrgList
} else {
this.list = []
}
})
},
// 搜索处理
handleSearch(val) {
this.keyword = val.trim()
// 非全量选择时,使用树形过滤
if (this.selectType !== 'all') {
this.$refs.tree && this.$refs.tree.filter(this.keyword)
return
}
// 有搜索关键词时,切换为列表展示
this.hasPage = !!this.keyword
this.list = []
this.finish = false
this.pagination.currentPage = 1
this.activeKey = '1'
if (this.keyword) {
this.$u.debounce(this.getTreeData, 300)() // 执行防抖函数
}
},
}
};
</script>
<style scoped>
/* 保留原有样式,如需补充可添加 */
.jnpf-tree-select-body {
height: 100vh;
display: flex;
flex-direction: column;
background: #fff;
}
.jnpf-tree-select-title {
display: flex;
align-items: center;
padding: 16rpx;
border-bottom: 1rpx solid #eee;
}
.backIcon {
font-size: 32rpx;
margin-right: 16rpx;
}
.title {
font-size: 32rpx;
font-weight: 500;
}
.jnpf-tree-select-search {
padding: 16rpx;
}
.jnpf-tree-selected {
padding: 0 16rpx;
}
.jnpf-tree-selected-head {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12rpx 0;
}
.clear-btn {
color: #007aff;
font-size: 28rpx;
}
.select-list {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
padding: 8rpx 0;
max-height: 200rpx;
}
.u-selectTag {
margin-bottom: 8rpx;
}
.jnpf-tree-selected-line {
height: 1rpx;
background: #eee;
margin: 8rpx 0;
}
.jnpf-tree-selected-tabs {
display: flex;
}
.tab-item {
flex: 1;
text-align: center;
padding: 16rpx;
font-size: 28rpx;
}
.tab-item-active {
color: #007aff;
border-bottom: 2rpx solid #007aff;
}
.jnpf-tree-select-tree {
flex: 1;
padding: 16rpx;
}
.jnpf-selcet-cell {
display: flex;
align-items: center;
padding: 16rpx 0;
border-bottom: 1rpx solid #f5f5f5;
}
.jnpf-selcet-cell-action {
margin-right: 16rpx;
}
.jnpf-tree-select-actions {
display: flex;
padding: 16rpx;
gap: 16rpx;
}
.buttom-btn {
flex: 1;
height: 88rpx;
line-height: 88rpx;
}
.h-full {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
</style>