|
|
@@ -1,3 +1,229 @@
|
|
|
+<script lang="ts" setup>
|
|
|
+import type { TreeProps } from 'ant-design-vue';
|
|
|
+
|
|
|
+import type { PropType } from 'vue';
|
|
|
+
|
|
|
+import type { ApiConfig } from '#/components/form/types';
|
|
|
+
|
|
|
+import { computed, reactive, ref, unref, watch, watchEffect } from 'vue';
|
|
|
+
|
|
|
+import { isFunction } from '@vben/utils';
|
|
|
+
|
|
|
+import { Input, Tree } from 'ant-design-vue';
|
|
|
+
|
|
|
+import { createApiFunction } from '#/components/form/helper';
|
|
|
+import { Icon } from '#/components/icon';
|
|
|
+import { filter, get, treeToList } from '#/utils';
|
|
|
+
|
|
|
+const props = defineProps({
|
|
|
+ multiple: {
|
|
|
+ type: Boolean as PropType<boolean>,
|
|
|
+ default: true,
|
|
|
+ },
|
|
|
+ title: {
|
|
|
+ type: String as PropType<string>,
|
|
|
+ default: '',
|
|
|
+ },
|
|
|
+ api: {
|
|
|
+ type: Object as PropType<ApiConfig>,
|
|
|
+ default: () => ({
|
|
|
+ type: 'none',
|
|
|
+ method: 'get',
|
|
|
+ params: {},
|
|
|
+ result: '',
|
|
|
+ url: null,
|
|
|
+ }),
|
|
|
+ },
|
|
|
+ fieldNames: {
|
|
|
+ type: Object as PropType<{ children: any; label: string; value: string }>,
|
|
|
+ default: () => ({ label: 'label', value: 'value', children: null }),
|
|
|
+ },
|
|
|
+ expandOnSearch: {
|
|
|
+ type: Boolean,
|
|
|
+ default: true,
|
|
|
+ },
|
|
|
+ immediate: {
|
|
|
+ type: Boolean,
|
|
|
+ default: true,
|
|
|
+ },
|
|
|
+});
|
|
|
+
|
|
|
+const emit = defineEmits(['update:searchValue']);
|
|
|
+
|
|
|
+const state = reactive<{
|
|
|
+ autoExpandParent: boolean;
|
|
|
+ expandedKeys: (number | string)[];
|
|
|
+}>({
|
|
|
+ expandedKeys: [],
|
|
|
+ autoExpandParent: true,
|
|
|
+});
|
|
|
+
|
|
|
+const searchListData = ref('');
|
|
|
+const searchState = reactive({
|
|
|
+ startSearch: false,
|
|
|
+ searchText: '',
|
|
|
+ searchData: [] as TreeProps['treeData'],
|
|
|
+});
|
|
|
+
|
|
|
+const treeDataRef = ref<TreeProps['treeData']>([]);
|
|
|
+
|
|
|
+const loading = ref(false);
|
|
|
+const isFirstLoad = ref(true);
|
|
|
+const handleExpand = (keys: string[]) => {
|
|
|
+ state.expandedKeys = keys;
|
|
|
+ state.autoExpandParent = false;
|
|
|
+};
|
|
|
+
|
|
|
+// const getParentKey = (
|
|
|
+// key: number | string,
|
|
|
+// tree: TreeProps['treeData'],
|
|
|
+// ): number | string | undefined => {
|
|
|
+// let parentKey;
|
|
|
+// tree &&
|
|
|
+// tree.forEach((node) => {
|
|
|
+// 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;
|
|
|
+// };
|
|
|
+
|
|
|
+const fetch = async () => {
|
|
|
+ const api = createApiFunction({ ...props.api });
|
|
|
+ if (!api || !isFunction(api)) return;
|
|
|
+ try {
|
|
|
+ loading.value = true;
|
|
|
+ const res = await api(props.api.params);
|
|
|
+ if (Array.isArray(res)) {
|
|
|
+ treeDataRef.value = res;
|
|
|
+ } else {
|
|
|
+ treeDataRef.value = props.api.result ? get(res, props.api.result) : [];
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.warn(error);
|
|
|
+ } finally {
|
|
|
+ loading.value = false;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+watchEffect(() => {
|
|
|
+ props.immediate && fetch();
|
|
|
+});
|
|
|
+
|
|
|
+watch(
|
|
|
+ () => props.api.params,
|
|
|
+ () => {
|
|
|
+ !unref(isFirstLoad) && fetch();
|
|
|
+ },
|
|
|
+ { deep: true },
|
|
|
+);
|
|
|
+
|
|
|
+const setExpandedKeys = (keys: (number | string)[]) => {
|
|
|
+ state.expandedKeys = keys;
|
|
|
+};
|
|
|
+
|
|
|
+// const getExpandedKeys = () => {
|
|
|
+// return state.expandedKeys;
|
|
|
+// };
|
|
|
+
|
|
|
+const handleSearch = (searchValue: string) => {
|
|
|
+ if (searchValue !== searchState.searchText)
|
|
|
+ searchState.searchText = searchValue;
|
|
|
+ emit('update:searchValue', searchValue);
|
|
|
+ if (!searchValue) {
|
|
|
+ searchState.startSearch = false;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ searchState.startSearch = true;
|
|
|
+ const { fieldNames, expandOnSearch } = props;
|
|
|
+
|
|
|
+ const matchedKeys: string[] = [];
|
|
|
+ searchState.searchData = filter(
|
|
|
+ unref(treeDataRef),
|
|
|
+ (node) => {
|
|
|
+ const result = node[fieldNames.label]?.includes(searchValue) ?? false;
|
|
|
+ if (result) {
|
|
|
+ matchedKeys.push(node[fieldNames.value]);
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ },
|
|
|
+ fieldNames,
|
|
|
+ );
|
|
|
+
|
|
|
+ if (expandOnSearch) {
|
|
|
+ const expandKeys = treeToList(searchState.searchData).map((val: any) => {
|
|
|
+ return val[fieldNames.value];
|
|
|
+ });
|
|
|
+ if (expandKeys && expandKeys.length > 0) {
|
|
|
+ setExpandedKeys(expandKeys);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // if (checkOnSearch && checkable && matchedKeys.length > 0) {
|
|
|
+ // setCheckedKeys(matchedKeys);
|
|
|
+ // }
|
|
|
+
|
|
|
+ // if (selectedOnSearch && matchedKeys.length > 0) {
|
|
|
+ // setSelectedKeys(matchedKeys);
|
|
|
+ // }
|
|
|
+};
|
|
|
+function handleKeyChange(v: any) {
|
|
|
+ handleSearch(v.target.value);
|
|
|
+}
|
|
|
+
|
|
|
+const getTreeData = computed((): TreeProps['treeData'] =>
|
|
|
+ searchState.startSearch ? searchState.searchData : unref(treeDataRef),
|
|
|
+);
|
|
|
+</script>
|
|
|
<template>
|
|
|
- <div>TRee</div>
|
|
|
+ <div class="h-full">
|
|
|
+ <div class="flex h-[30px] items-center justify-between">
|
|
|
+ <div>
|
|
|
+ {{ title }}
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <Icon icon="uil:ellipsis-v" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div style="height: calc(100% - 30px)" class="overflow-y-auto">
|
|
|
+ <div class="h-[35px]">
|
|
|
+ <Input
|
|
|
+ style="height: 35px; padding: 3px 2px 3px 10px"
|
|
|
+ v-model:value="searchListData"
|
|
|
+ @change="handleKeyChange"
|
|
|
+ placeholder="输入关键字搜索"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <Tree
|
|
|
+ :expanded-keys="state.expandedKeys"
|
|
|
+ :auto-expand-parent="state.autoExpandParent"
|
|
|
+ :tree-data="getTreeData"
|
|
|
+ :field-names="{
|
|
|
+ title: fieldNames.label,
|
|
|
+ key: fieldNames.value,
|
|
|
+ children: fieldNames.children,
|
|
|
+ }"
|
|
|
+ style="height: calc(100% - 35px)"
|
|
|
+ @expand="handleExpand"
|
|
|
+ >
|
|
|
+ <template #title="{ title }">
|
|
|
+ <span v-if="title.indexOf(searchListData) > -1">
|
|
|
+ {{ title.substring(0, title.indexOf(searchListData)) }}
|
|
|
+ <span style="color: #f50">{{ searchListData }}</span>
|
|
|
+ {{
|
|
|
+ title.substring(
|
|
|
+ title.indexOf(searchListData) + searchListData.length,
|
|
|
+ )
|
|
|
+ }}
|
|
|
+ </span>
|
|
|
+ <span v-else>{{ title }}</span>
|
|
|
+ </template>
|
|
|
+ </Tree>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</template>
|