feat(@vben/web-antd): AreaTree 组件增强支持多选模式并新增 AreaFilterDrawer

AreaTree 新增 checkable、checkedKeys、selectedKeys 属性支持复选框多选,
暴露 getAreaName 方法;新增 AreaFilterDrawer 区域筛选抽屉组件。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
lzh
2026-03-13 11:14:40 +08:00
parent 805b0bfcf7
commit bf13067812
2 changed files with 209 additions and 4 deletions

View File

@@ -0,0 +1,166 @@
<script lang="ts" setup>
import type { OpsAreaApi } from '#/api/ops/area';
import { computed, ref, watch } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { Button, Drawer } from 'ant-design-vue';
import AreaTree from './AreaTree.vue';
const props = withDefaults(
defineProps<{
/** 控制抽屉显示 */
open?: boolean;
/** 外部已确认的区域 ID */
modelValue?: number | undefined;
}>(),
{ open: false, modelValue: undefined },
);
const emit = defineEmits<{
'update:open': [value: boolean];
'update:modelValue': [id: number | undefined];
/** 确认选择后触发 */
confirm: [id: number | undefined];
}>();
const areaTreeRef = ref<InstanceType<typeof AreaTree>>();
/** 临时选中的区域(未确认前) */
const tempSelectedArea = ref<null | OpsAreaApi.BusArea>(null);
/** 打开时同步外部已确认的值 */
watch(
() => props.open,
(isOpen) => {
if (isOpen && props.modelValue) {
// 保留上次确认的选择;若无外部值则清空
// 具体节点信息会在树加载后由 Tree 组件高亮
} else if (isOpen) {
tempSelectedArea.value = null;
}
},
);
/** 选中区域的完整路径名称 */
const selectedAreaPath = computed(() => {
if (!tempSelectedArea.value?.id) return '';
return areaTreeRef.value?.getAreaPath(tempSelectedArea.value.id) ?? tempSelectedArea.value.areaName;
});
/** 树节点选中 */
function handleSelect(area: null | OpsAreaApi.BusArea) {
tempSelectedArea.value = area;
}
/** 确认选择 */
function handleConfirm() {
const id = tempSelectedArea.value?.id ?? undefined;
emit('update:modelValue', id);
emit('confirm', id);
emit('update:open', false);
}
/** 重置 */
function handleReset() {
tempSelectedArea.value = null;
emit('update:modelValue', undefined);
emit('confirm', undefined);
emit('update:open', false);
}
/** 关闭抽屉 */
function handleClose() {
emit('update:open', false);
}
</script>
<template>
<Drawer
:open="open"
title="区域筛选"
placement="right"
:width="320"
:body-style="{ padding: '12px 16px', display: 'flex', flexDirection: 'column' }"
:header-style="{ padding: '12px 16px' }"
:footer-style="{ padding: '10px 16px' }"
@close="handleClose"
>
<!-- 当前选中提示 -->
<div v-if="tempSelectedArea" class="selected-hint">
<IconifyIcon icon="solar:map-point-bold" class="hint-icon" />
<span class="hint-text">{{ selectedAreaPath }}</span>
</div>
<!-- 区域树单选模式 -->
<div class="tree-container">
<AreaTree
ref="areaTreeRef"
:selected-keys="tempSelectedArea?.id ? [tempSelectedArea.id] : []"
@select="handleSelect"
/>
</div>
<!-- Footer -->
<template #footer>
<div class="drawer-footer">
<Button @click="handleReset">
<IconifyIcon icon="solar:restart-bold" class="btn-icon" />
重置
</Button>
<Button type="primary" :disabled="!tempSelectedArea" @click="handleConfirm">
<IconifyIcon icon="solar:check-circle-bold" class="btn-icon" />
确认
</Button>
</div>
</template>
</Drawer>
</template>
<style scoped lang="scss">
.selected-hint {
display: flex;
gap: 6px;
align-items: center;
padding: 8px 12px;
margin-bottom: 12px;
color: var(--ant-color-primary);
background: var(--ant-color-primary-bg);
border: 1px solid var(--ant-color-primary-border);
border-radius: 6px;
}
.hint-icon {
font-size: 16px;
flex-shrink: 0;
}
.hint-text {
font-size: 13px;
font-weight: 500;
}
.tree-container {
flex: 1;
overflow-y: auto;
}
.drawer-footer {
display: flex;
gap: 8px;
justify-content: flex-end;
}
.btn-icon {
margin-right: 4px;
}
html.dark {
.selected-hint {
background: rgb(22 119 255 / 10%);
border-color: rgb(22 119 255 / 30%);
}
}
</style>

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup>
import type { OpsAreaApi } from '#/api/ops/area';
import { onMounted, ref } from 'vue';
import { onMounted, ref, watch } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { handleTree } from '@vben/utils';
@@ -14,15 +14,35 @@ const props = withDefaults(
defineProps<{
/** 仅查询启用的区域 */
activeOnly?: boolean;
/** 是否启用复选框多选模式 */
checkable?: boolean;
/** 外部控制勾选的 key 列表 */
checkedKeys?: number[];
/** 外部控制选中的 key 列表(单选模式) */
selectedKeys?: number[];
}>(),
{ activeOnly: true },
{ activeOnly: true, checkable: false, checkedKeys: () => [], selectedKeys: undefined },
);
const emit = defineEmits<{
/** 选中区域节点,传出完整节点对象;取消选中时传 null */
select: [area: null | OpsAreaApi.BusArea];
/** checkable 模式下勾选变化 */
check: [checkedKeys: number[], halfCheckedKeys: number[]];
/** 同步 checkedKeys 到父组件 */
'update:checkedKeys': [keys: number[]];
}>();
/** 内部勾选状态 */
const internalCheckedKeys = ref<number[]>([...props.checkedKeys]);
watch(
() => props.checkedKeys,
(keys) => {
internalCheckedKeys.value = [...keys];
},
);
const areaList = ref<OpsAreaApi.BusArea[]>([]);
const areaTree = ref<any[]>([]);
const loading = ref(false);
@@ -58,7 +78,7 @@ function handleSearch(e: any) {
areaTree.value = handleTree(filtered, 'id', 'parentId', 'children');
}
/** 选中节点 */
/** 选中节点(单选模式) */
function handleSelect(selectedKeys: any[], info: any) {
if (selectedKeys.length === 0) {
emit('select', null);
@@ -68,6 +88,21 @@ function handleSelect(selectedKeys: any[], info: any) {
emit('select', node);
}
/** 勾选节点checkable 模式) */
function handleCheck(checked: any, info: any) {
const keys: number[] = Array.isArray(checked) ? checked : checked.checked;
const halfKeys: number[] = info.halfCheckedKeys ?? [];
internalCheckedKeys.value = keys;
emit('update:checkedKeys', keys);
emit('check', keys, halfKeys);
}
/** 根据 id 获取区域名称 */
function getAreaName(id: number): string {
const area = areaList.value.find((a) => a.id === id);
return area?.areaName ?? '';
}
/** 加载区域树 */
async function loadTree() {
loading.value = true;
@@ -120,7 +155,7 @@ function getDescendantIds(id: number): number[] {
}
/** 暴露刷新方法和路径查询供父组件调用 */
defineExpose({ refresh: loadTree, getAreaPath, getDescendantIds });
defineExpose({ refresh: loadTree, getAreaPath, getDescendantIds, getAreaName });
onMounted(loadTree);
</script>
@@ -144,12 +179,16 @@ onMounted(loadTree);
class="pt-2"
:tree-data="areaTree"
:default-expand-all="true"
:checkable="checkable"
:checked-keys="checkable ? internalCheckedKeys : undefined"
:selected-keys="selectedKeys"
:field-names="{
title: 'areaName',
key: 'id',
children: 'children',
}"
@select="handleSelect"
@check="handleCheck"
/>
<div v-else-if="!loading" class="py-4 text-center text-gray-500">
暂无区域数据