<template>
	<div class="tree-transfer">
		<div class="tree-transfer-panel">
			<div class="tree-transfer-header">
				<span>{{ titles[0] }}</span>
				<div class="tree-transfer-header-actions">
					<span>已选 {{ sourceCheckedCount }}/{{ sourceCount }} 项</span>
					<el-button text type="primary" size="small" :disabled="sourceCount === 0"
						@click="toggleSourceAll()">
						{{ sourceAllChecked ? '取消全选' : '全选' }}
					</el-button>
				</div>
			</div>
			<div class="tree-transfer-search">
				<el-input v-model="sourceFilter" clearable placeholder="" prefix-icon="el-icon-search"
					@input="filterSourceTree"></el-input>
			</div>
			<div class="tree-transfer-body" :style="{ height: height }">
				<el-tree ref="sourceTree" :data="sourceTreeData" :props="treeProps" :node-key="keyField" show-checkbox
					default-expand-all :filter-node-method="filterNode" @check="handleSourceCheck">
					<template #default="{ data: nodeData }">
						<div class="tree-transfer-node">
							<span class="tree-transfer-node-label">{{ nodeData[labelField] }}</span>
						</div>
					</template>
				</el-tree>
				<div v-if="sourceCount === 0" class="tree-transfer-empty">暂无可选数据</div>
			</div>
		</div>

		<div class="tree-transfer-actions">
			<el-button type="primary" icon="el-icon-arrow-right" title="移入" @click="moveToTarget">添加</el-button>
			<el-button type="primary" icon="el-icon-arrow-left" title="移出" @click="moveToSource">移出</el-button>
		</div>

		<div class="tree-transfer-panel">
			<div class="tree-transfer-header">
				<span>{{ titles[1] }}</span>
				<div class="tree-transfer-header-actions">
					<span>已选 {{ targetCheckedCount }}/{{ targetCount }} 项</span>
					<el-button text type="primary" size="small" :disabled="targetCount === 0"
						@click="toggleTargetAll()">
						{{ targetAllChecked ? '取消全选' : '全选' }}
					</el-button>
				</div>
			</div>
			<div class="tree-transfer-search">
				<el-input v-model="targetFilter" clearable placeholder="" prefix-icon="el-icon-search"
					@input="filterTargetTree"></el-input>
			</div>
			<div class="tree-transfer-body" :style="{ height: height }">
				<el-tree ref="targetTree" :data="targetTreeData" :props="treeProps" :node-key="keyField" show-checkbox
					default-expand-all :filter-node-method="filterNode" @check="handleTargetCheck">
					<template #default="{ data: nodeData }">
						<div class="tree-transfer-node">
							<span class="tree-transfer-node-label">{{ nodeData[labelField] }}</span>
						</div>
					</template>
				</el-tree>
				<div v-if="targetCount === 0" class="tree-transfer-empty">暂无已选数据</div>
			</div>
		</div>
	</div>
</template>

<script>
export default {
	name: 'TreeTransfer',
	props: {
		modelValue: {
			type: Array,
			default: () => []
		},
		data: {
			type: Array,
			default: () => []
		},
		titles: {
			type: Array,
			default: () => ['可选数据', '已选数据']
		},
		keyField: {
			type: String,
			default: 'id'
		},
		labelField: {
			type: String,
			default: 'label'
		},
		childrenField: {
			type: String,
			default: 'children'
		},
		leafField: {
			type: String,
			default: 'isLeaf'
		},
		height: {
			type: String,
			default: '420px'
		}
	},
	emits: ['update:modelValue', 'change'],
	data() {
		return {
			sourceFilter: '',
			targetFilter: '',
			sourceCheckedCount: 0,
			targetCheckedCount: 0
		}
	},
	computed: {
		treeProps() {
			return {
				label: this.labelField,
				children: this.childrenField,
				disabled: 'disabled'
			}
		},
		selectedKeySet() {
			return new Set(this.modelValue || [])
		},
		sourceTreeData() {
			return this.filterTree(this.data, false)
		},
		targetTreeData() {
			return this.filterTree(this.data, true)
		},
		sourceCount() {
			return this.countLeaves(this.sourceTreeData)
		},
		targetCount() {
			return this.countLeaves(this.targetTreeData)
		},
		sourceAllChecked() {
			return this.sourceCount > 0 && this.sourceCheckedCount === this.sourceCount
		},
		targetAllChecked() {
			return this.targetCount > 0 && this.targetCheckedCount === this.targetCount
		}
	},
	watch: {
		sourceTreeData() {
			this.$nextTick(() => this.filterSourceTree(this.sourceFilter))
		},
		targetTreeData() {
			this.$nextTick(() => this.filterTargetTree(this.targetFilter))
		}
	},
	methods: {
		isLeaf(node) {
			if (Object.prototype.hasOwnProperty.call(node, this.leafField)) {
				return node[this.leafField] === true
			}
			return !node[this.childrenField] || node[this.childrenField].length === 0
		},
		filterTree(nodes, selected) {
			return (nodes || []).reduce((result, node) => {
				if (this.isLeaf(node)) {
					if (this.selectedKeySet.has(node[this.keyField]) === selected) {
						result.push({ ...node })
					}
					return result
				}
				const children = this.filterTree(node[this.childrenField] || [], selected)
				if (children.length > 0) {
					result.push({
						...node,
						[this.childrenField]: children
					})
				}
				return result
			}, [])
		},
		countLeaves(nodes) {
			return (nodes || []).reduce((count, node) => {
				if (this.isLeaf(node)) {
					return count + 1
				}
				return count + this.countLeaves(node[this.childrenField] || [])
			}, 0)
		},
		collectLeafKeys(nodes) {
			return (nodes || []).reduce((keys, node) => {
				if (this.isLeaf(node)) {
					if (!node.disabled) {
						keys.push(node[this.keyField])
					}
					return keys
				}
				return keys.concat(this.collectLeafKeys(node[this.childrenField] || []))
			}, [])
		},
		toggleSourceAll() {
			this.toggleAll(this.$refs.sourceTree, this.sourceTreeData, this.sourceAllChecked, 'source')
		},
		toggleTargetAll() {
			this.toggleAll(this.$refs.targetTree, this.targetTreeData, this.targetAllChecked, 'target')
		},
		toggleAll(tree, treeData, allChecked, side) {
			if (!tree) {
				return
			}
			tree.setCheckedKeys(allChecked ? [] : this.collectLeafKeys(treeData))
			this.$nextTick(() => this.refreshCheckedCount(side))
		},
		handleSourceCheck() {
			this.refreshCheckedCount('source')
		},
		handleTargetCheck() {
			this.refreshCheckedCount('target')
		},
		refreshCheckedCount(side) {
			const tree = side === 'source' ? this.$refs.sourceTree : this.$refs.targetTree
			const count = this.getCheckedLeafKeys(tree).length
			if (side === 'source') {
				this.sourceCheckedCount = count
			} else {
				this.targetCheckedCount = count
			}
		},
		moveToTarget() {
			const keys = this.getCheckedLeafKeys(this.$refs.sourceTree)
			if (keys.length === 0) {
				this.$message.warning('请选择要添加的数据')
				return
			}
			this.updateValue([...this.modelValue, ...keys])
		},
		moveToSource() {
			const keys = this.getCheckedLeafKeys(this.$refs.targetTree)
			if (keys.length === 0) {
				this.$message.warning('请选择要移出的数据')
				return
			}
			const removeKeySet = new Set(keys)
			this.updateValue((this.modelValue || []).filter(key => !removeKeySet.has(key)))
		},
		getCheckedLeafKeys(tree) {
			if (!tree) {
				return []
			}
			return tree.getCheckedNodes(true, false).map(node => node[this.keyField])
		},
		updateValue(keys) {
			const value = [...new Set(keys)]
			this.$emit('update:modelValue', value)
			this.$emit('change', value)
			this.$nextTick(() => {
				if (this.$refs.sourceTree) {
					this.$refs.sourceTree.setCheckedKeys([])
				}
				if (this.$refs.targetTree) {
					this.$refs.targetTree.setCheckedKeys([])
				}
				this.sourceCheckedCount = 0
				this.targetCheckedCount = 0
			})
		},
		filterSourceTree(value) {
			if (this.$refs.sourceTree) {
				this.$refs.sourceTree.filter(value)
			}
		},
		filterTargetTree(value) {
			if (this.$refs.targetTree) {
				this.$refs.targetTree.filter(value)
			}
		},
		filterNode(value, data, node) {
			if (!value) {
				return true
			}
			const keyword = String(value).toLowerCase()
			if (String(data[this.labelField] || '').toLowerCase().includes(keyword)) {
				return true
			}
			let parent = node && node.parent
			while (parent && parent.data) {
				if (String(parent.data[this.labelField] || '').toLowerCase().includes(keyword)) {
					return true
				}
				parent = parent.parent
			}
			return false
		}
	}
}
</script>

<style scoped>
.tree-transfer {
	display: grid;
	grid-template-columns: minmax(0, 1fr) 80px minmax(0, 1fr);
	gap: 12px;
	align-items: center;
}

.tree-transfer-panel {
	min-width: 0;
	border: 1px solid #dcdfe6;
	border-radius: 6px;
	overflow: hidden;
	background: #fff;
}

.tree-transfer-header {
	height: 42px;
	display: flex;
	align-items: center;
	justify-content: space-between;
	padding: 0 12px;
	border-bottom: 1px solid #ebeef5;
	background: #f5f7fa;
	font-weight: 600;
	color: #303133;
}

.tree-transfer-header-actions {
	display: flex;
	align-items: center;
	gap: 8px;
}

.tree-transfer-header-actions>span {
	font-size: 12px;
	font-weight: 400;
	color: #909399;
}

.tree-transfer-header-actions .el-button {
	padding: 0;
}

.tree-transfer-search {
	padding: 10px;
	border-bottom: 1px solid #ebeef5;
}

.tree-transfer-body {
	position: relative;
	overflow: auto;
	padding: 8px;
}

.tree-transfer-actions {
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 12px;
	margin: 0 8px;
}

.tree-transfer-actions .el-button+.el-button {
	margin-left: 0;
}

.tree-transfer-node {
	min-width: 0;
	flex: 1;
	display: flex;
	align-items: center;
	gap: 8px;
}

.tree-transfer-node-label {
	min-width: 0;
	overflow: hidden;
	white-space: nowrap;
	text-overflow: ellipsis;
}

.tree-transfer-empty {
	position: absolute;
	inset: 0;
	display: flex;
	align-items: center;
	justify-content: center;
	color: #909399;
	pointer-events: none;
}

@media (max-width: 900px) {
	.tree-transfer {
		grid-template-columns: 1fr;
	}

	.tree-transfer-actions {
		flex-direction: row;
		justify-content: center;
	}

	.tree-transfer-actions .el-button:first-child {
		transform: rotate(90deg);
	}

	.tree-transfer-actions .el-button:last-child {
		transform: rotate(90deg);
	}
}
</style>
