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:
166
apps/web-antd/src/views/ops/components/AreaFilterDrawer.vue
Normal file
166
apps/web-antd/src/views/ops/components/AreaFilterDrawer.vue
Normal 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>
|
||||
@@ -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">
|
||||
暂无区域数据
|
||||
|
||||
Reference in New Issue
Block a user