push
This commit is contained in:
20
lib/component/form/select/index.ts
Normal file
20
lib/component/form/select/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { App } from 'vue';
|
||||
import nsSelect from './select.vue';
|
||||
import nsSelectApi from './select-api.vue';
|
||||
import nsSelectOption from './select-option.vue';
|
||||
import nsSelectOptGroup from './select-opt-group.vue';
|
||||
import nsSelectTreeApi from './select-tree-api.vue';
|
||||
import nsSelectApiV2 from './select-api-v2.vue';
|
||||
import nsSelectTree from './selectTree.vue';
|
||||
import { Select } from 'ant-design-vue';
|
||||
|
||||
export const NsSelect = function (app: App) {
|
||||
app.component(nsSelect.name, nsSelect);
|
||||
app.component(nsSelectApi.name, nsSelectApi);
|
||||
app.component(nsSelectApiV2.name, nsSelectApiV2);
|
||||
app.component(nsSelectTree.name, nsSelectTree);
|
||||
app.component(nsSelectTreeApi.name, nsSelectTreeApi);
|
||||
app.component(nsSelectOption.name, nsSelect.Option);
|
||||
app.component(nsSelectOptGroup.name, Select.OptGroup);
|
||||
return app;
|
||||
};
|
||||
440
lib/component/form/select/select-api-v2.vue
Normal file
440
lib/component/form/select/select-api-v2.vue
Normal file
@@ -0,0 +1,440 @@
|
||||
<!-- @format -->
|
||||
|
||||
<template>
|
||||
<a-select
|
||||
v-bind="getBindValues"
|
||||
:options="getOptions"
|
||||
@dropdown-visible-change="dropdownHandle"
|
||||
@popupScroll="onPopupScroll"
|
||||
v-model:value="modelValue"
|
||||
@search="onSearch"
|
||||
@clear="onClear"
|
||||
:filterOption="onFilterOption">
|
||||
<template #[item]="data" v-for="item in Object.keys($slots)" :key="item">
|
||||
<slot :name="item" v-bind="data || {}"></slot>
|
||||
</template>
|
||||
</a-select>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, nextTick, PropType, ref, unref, watch } from 'vue';
|
||||
import {
|
||||
cloneDeep,
|
||||
get,
|
||||
isArray,
|
||||
isEqual,
|
||||
isFunction,
|
||||
isString,
|
||||
isUndefined,
|
||||
isEmpty,
|
||||
} from 'lodash-es';
|
||||
import { HttpRequestConfig, useApi } from '/nerv-lib/use/use-api';
|
||||
import { useParams } from '/nerv-lib/use/use-params';
|
||||
type ChangeValue = string | number | undefined | string[] | number[];
|
||||
declare type Recordable<T = any> = Record<string, T>;
|
||||
export default defineComponent({
|
||||
name: 'NsSelectApiV2',
|
||||
props: {
|
||||
api: {
|
||||
type: [String, Object, Function] as PropType<string | Function | HttpRequestConfig>,
|
||||
required: true,
|
||||
},
|
||||
value: [String, Object, Array, Number],
|
||||
field: String,
|
||||
params: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
resultField: {
|
||||
type: String,
|
||||
default: 'data.data',
|
||||
},
|
||||
firstOption: {
|
||||
type: Object,
|
||||
},
|
||||
numberToString: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
immediate: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
labelField: {
|
||||
type: [String, Function],
|
||||
default: 'label',
|
||||
},
|
||||
valueField: {
|
||||
type: String,
|
||||
default: 'value',
|
||||
},
|
||||
customRequest: {
|
||||
type: [Function],
|
||||
},
|
||||
autoSelectFirst: {
|
||||
// 与autoClearValue同时使用,只会执行autoSelectFirst
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
autoClearValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
//数据筛选函数
|
||||
filterData: {
|
||||
type: Function,
|
||||
},
|
||||
// 开启前端筛选默认label包含
|
||||
autoSearch: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
//后端筛选字段
|
||||
filterFiled: {
|
||||
type: String,
|
||||
},
|
||||
//是否开启滚动加载
|
||||
scrollLoad: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
//分页字段
|
||||
pageField: {
|
||||
type: String,
|
||||
default: 'page',
|
||||
},
|
||||
//第一页
|
||||
defaultPage: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
dropdownReload: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
dynamicParams: {
|
||||
type: String || Array || Function || Object,
|
||||
},
|
||||
formModel: {
|
||||
type: Object as PropType<Recordable>,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
emits: ['change', 'validateChange'],
|
||||
setup(props, { attrs, emit }) {
|
||||
const options = ref([]);
|
||||
const isLoad = ref(false);
|
||||
const changeValue = ref<any>(undefined);
|
||||
const filterFiledRef = ref<string | undefined>(undefined);
|
||||
let isFirstLoad = !!props.api; // 是否第一次加载
|
||||
const { getParams } = useParams();
|
||||
// eslint-disable-next-line vue/no-setup-props-destructure
|
||||
let page = props.defaultPage; //
|
||||
let isLoading = false;
|
||||
const getBindValues = computed(() => {
|
||||
const selectProps: Recordable = {};
|
||||
if (props.filterFiled || props.autoSearch) {
|
||||
selectProps['showSearch'] = true;
|
||||
}
|
||||
return {
|
||||
...attrs,
|
||||
...selectProps,
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* 设置filterFiled时注册search事件
|
||||
*/
|
||||
const onSearch = computed(() => {
|
||||
if (props.filterFiled) {
|
||||
return (input: string) => {
|
||||
if (filterFiledRef.value !== input) {
|
||||
filterFiledRef.value = input;
|
||||
page = props.defaultPage;
|
||||
fetch();
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
/**
|
||||
* 设置filterFiled时注册filterOption事件
|
||||
*/
|
||||
const onFilterOption = computed(() => {
|
||||
if (props.autoSearch) {
|
||||
return (input: string, option: any) => {
|
||||
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
|
||||
};
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
/**
|
||||
* 根据请求得到的data获取options
|
||||
*/
|
||||
const getOptions = computed(() => {
|
||||
const { firstOption, labelField, valueField, numberToString } = props;
|
||||
const sourceOptions = [];
|
||||
firstOption && sourceOptions.push(firstOption);
|
||||
return unref(options).reduce((acc, next: Recordable) => {
|
||||
if (isString(next)) {
|
||||
//值是字符串类型
|
||||
const option = {
|
||||
label: next,
|
||||
value: next,
|
||||
};
|
||||
acc.push(option);
|
||||
return acc;
|
||||
} else {
|
||||
const value = get(next, valueField);
|
||||
const option = {
|
||||
...next,
|
||||
label: isFunction(labelField) ? labelField(next) : get(next, labelField as string),
|
||||
value: numberToString ? `${value}` : value,
|
||||
};
|
||||
acc.push(option);
|
||||
return acc;
|
||||
}
|
||||
}, sourceOptions);
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取数据
|
||||
*/
|
||||
const fetch = () => {
|
||||
console.log('requiredParams', getBindValues.value.checkRequiredParams);
|
||||
if (getBindValues.value.checkRequiredParams === false) {
|
||||
options.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading = true;
|
||||
const requestConfig: HttpRequestConfig = { method: 'get' };
|
||||
const {
|
||||
api,
|
||||
params: _params,
|
||||
resultField,
|
||||
filterData,
|
||||
filterFiled,
|
||||
pageField,
|
||||
scrollLoad,
|
||||
defaultPage,
|
||||
dynamicParams,
|
||||
customRequest,
|
||||
} = props;
|
||||
let params: Recordable = cloneDeep(_params);
|
||||
if (dynamicParams) {
|
||||
params = getParams(attrs.record, dynamicParams, { ...params });
|
||||
}
|
||||
|
||||
if (filterFiled && filterFiledRef.value) {
|
||||
params[filterFiled] = filterFiledRef.value;
|
||||
// options.value = [];
|
||||
}
|
||||
if (scrollLoad) {
|
||||
params[pageField] = page;
|
||||
if (page === defaultPage) {
|
||||
options.value = [];
|
||||
}
|
||||
}
|
||||
const success = (res: Recordable) => {
|
||||
emit('validateChange', { help: undefined });
|
||||
if (resultField) {
|
||||
let data = get(res, resultField) || [];
|
||||
// data = data.splice(Math.floor(Math.random() * 3), Math.floor(Math.random() * 5 + 5));
|
||||
if (isFunction(filterData)) {
|
||||
data = data.filter(filterData);
|
||||
}
|
||||
if (scrollLoad) {
|
||||
options.value = [...options.value, ...data];
|
||||
} else {
|
||||
options.value = data;
|
||||
}
|
||||
}
|
||||
isLoad.value = true;
|
||||
isLoading = false;
|
||||
};
|
||||
const errror = (error: any) => {
|
||||
if (error?.response?.status === 403) {
|
||||
emit('validateChange', { help: '暂无权限', validateStatus: 'error' });
|
||||
nextTick(() => {
|
||||
//清空编辑初始值
|
||||
modelValue.value = undefined;
|
||||
});
|
||||
}
|
||||
options.value = [];
|
||||
isLoading = false;
|
||||
};
|
||||
if (customRequest) {
|
||||
customRequest({ api, params, requestConfig })
|
||||
.then((res: Recordable) => {
|
||||
success(res);
|
||||
})
|
||||
.catch((error: any) => {
|
||||
errror(error);
|
||||
});
|
||||
} else {
|
||||
const { httpRequest } = useApi();
|
||||
httpRequest({ api, params, requestConfig })
|
||||
.then((res: Recordable) => {
|
||||
success(res);
|
||||
})
|
||||
.catch((error: any) => {
|
||||
errror(error);
|
||||
});
|
||||
}
|
||||
};
|
||||
let lastParams: any = undefined;
|
||||
/**
|
||||
** 延迟获取数据
|
||||
*/
|
||||
async function dropdownHandle(open: boolean) {
|
||||
if (!open) return;
|
||||
if ((!props.immediate && !isLoad.value) || props.dropdownReload) {
|
||||
await fetch();
|
||||
}
|
||||
}
|
||||
/**
|
||||
** 绑定value
|
||||
*/
|
||||
const modelValue = computed({
|
||||
set(value: ChangeValue) {
|
||||
if (isEqual(value, changeValue.value)) return;
|
||||
changeValue.value = value;
|
||||
triggerChange(value);
|
||||
},
|
||||
get() {
|
||||
return changeValue.value;
|
||||
},
|
||||
});
|
||||
/**
|
||||
* 传入值需要
|
||||
* 前提option已获取到数据
|
||||
*/
|
||||
watch(
|
||||
() => props.value,
|
||||
() => {
|
||||
if (isLoad.value) {
|
||||
modelValue.value = props.value;
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
watch(
|
||||
() => getOptions.value,
|
||||
() => {
|
||||
const { value, autoSelectFirst, autoClearValue } = props;
|
||||
// 首次加载如果有值则选中值
|
||||
if (isFirstLoad && !isUndefined(value)) {
|
||||
modelValue.value = value;
|
||||
} else if (!filterFiledRef.value) {
|
||||
if (autoSelectFirst) {
|
||||
modelValue.value = getOptions.value[0]?.value;
|
||||
} else if (autoClearValue) {
|
||||
modelValue.value = undefined;
|
||||
}
|
||||
}
|
||||
isFirstLoad = false;
|
||||
},
|
||||
);
|
||||
/**
|
||||
* 重写change ant 初始化数据、删除数据时不触发change
|
||||
* @param value
|
||||
*/
|
||||
function triggerChange(value: ChangeValue) {
|
||||
if (isUndefined(value)) {
|
||||
emit('change', value, undefined);
|
||||
} else if (isArray(value)) {
|
||||
const options: Record<string, any>[] = [];
|
||||
value.forEach((v) => {
|
||||
getOptions.value.forEach((option) => {
|
||||
if (option.value === v) {
|
||||
options.push(option);
|
||||
}
|
||||
});
|
||||
});
|
||||
emit('change', value, options);
|
||||
} else {
|
||||
let op = {};
|
||||
getOptions.value.forEach((option) => {
|
||||
if (option.value === value) {
|
||||
op = option;
|
||||
}
|
||||
});
|
||||
emit('change', value, op);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
** 联动 immediate 为是否主动获取数据
|
||||
*/
|
||||
watch(
|
||||
() => attrs.record,
|
||||
async (value: object) => {
|
||||
if (isEmpty(value)) return;
|
||||
let { params, dynamicParams, defaultParams = {} } = props;
|
||||
params = getParams(value, dynamicParams, { ...params, ...defaultParams });
|
||||
if (!isEqual(lastParams, params)) {
|
||||
if (props.immediate || true) {
|
||||
//todo 暂时全设为主动获取
|
||||
lastParams = cloneDeep(params);
|
||||
await fetch();
|
||||
}
|
||||
isLoad.value = false; // 设置成false 点击下拉才会触发
|
||||
}
|
||||
},
|
||||
{ deep: true, immediate: props.immediate },
|
||||
);
|
||||
|
||||
/**
|
||||
** 联动 immediate 为是否主动获取数据
|
||||
*/
|
||||
watch(
|
||||
[() => props.params, () => props.api],
|
||||
async () => {
|
||||
const { params } = props;
|
||||
if (!isEqual(lastParams, params)) {
|
||||
if (props.immediate || true) {
|
||||
//todo 暂时全设为主动获取
|
||||
lastParams = cloneDeep(params);
|
||||
page = props.defaultPage;
|
||||
await fetch();
|
||||
}
|
||||
isLoad.value = false; // 设置成false 点击下拉才会触发
|
||||
}
|
||||
},
|
||||
{ deep: true, immediate: props.immediate },
|
||||
);
|
||||
|
||||
const onPopupScroll = computed(() => {
|
||||
if (props.scrollLoad) {
|
||||
return (e) => {
|
||||
const { scrollHeight, scrollTop, clientHeight } = e.target;
|
||||
if (scrollHeight - scrollTop - 80 < clientHeight && !isLoading) {
|
||||
page++;
|
||||
fetch();
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
const onClear = () => {
|
||||
filterFiledRef.value = undefined;
|
||||
};
|
||||
return {
|
||||
onPopupScroll,
|
||||
dropdownHandle,
|
||||
getOptions,
|
||||
getBindValues,
|
||||
modelValue,
|
||||
onSearch,
|
||||
onFilterOption,
|
||||
onClear,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped></style>
|
||||
371
lib/component/form/select/select-api.vue
Normal file
371
lib/component/form/select/select-api.vue
Normal file
@@ -0,0 +1,371 @@
|
||||
<!-- @format -->
|
||||
|
||||
<template>
|
||||
<a-select
|
||||
v-bind="getBindValues"
|
||||
:options="getOptions"
|
||||
:getPopupContainer="(triggerNode) => triggerNode.parentNode"
|
||||
@dropdown-visible-change="dropdownHandle"
|
||||
@popupScroll="onPopupScroll"
|
||||
v-model:value="modelValue"
|
||||
@search="onSearch"
|
||||
@clear="onClear"
|
||||
:filterOption="onFilterOption">
|
||||
<template #[item]="data" v-for="item in Object.keys($slots)" :key="item">
|
||||
<slot :name="item" v-bind="data || {}"></slot>
|
||||
</template>
|
||||
</a-select>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, nextTick, PropType, ref, unref, watch } from 'vue';
|
||||
import { cloneDeep, get, isArray, isEqual, isFunction, isString, isUndefined } from 'lodash-es';
|
||||
import { HttpRequestConfig, useApi } from '/nerv-lib/use/use-api';
|
||||
type ChangeValue = string | number | undefined | string[] | number[];
|
||||
export default defineComponent({
|
||||
name: 'NsSelectApi',
|
||||
props: {
|
||||
api: {
|
||||
type: [String, Object, Function] as PropType<string | Function | HttpRequestConfig>,
|
||||
required: true,
|
||||
},
|
||||
value: [String, Object, Array, Number],
|
||||
field: String,
|
||||
params: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
resultField: {
|
||||
type: String,
|
||||
default: 'data.data',
|
||||
},
|
||||
firstOption: {
|
||||
type: Object,
|
||||
},
|
||||
numberToString: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
immediate: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
labelField: {
|
||||
type: [String, Function],
|
||||
default: 'label',
|
||||
},
|
||||
valueField: {
|
||||
type: String,
|
||||
default: 'value',
|
||||
},
|
||||
autoSelectFirst: {
|
||||
// 与autoClearValue同时使用,只会执行autoSelectFirst
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
autoClearValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
//数据筛选函数
|
||||
filterData: {
|
||||
type: Function,
|
||||
},
|
||||
// 开启前端筛选默认label包含
|
||||
autoSearch: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
//后端筛选字段
|
||||
filterFiled: {
|
||||
type: String,
|
||||
},
|
||||
//是否开启滚动加载
|
||||
scrollLoad: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
//分页字段
|
||||
pageField: {
|
||||
type: String,
|
||||
default: 'page',
|
||||
},
|
||||
//第一页
|
||||
defaultPage: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
dropdownReload: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['change', 'validateChange'],
|
||||
setup(props, { attrs, emit }) {
|
||||
const options = ref([]);
|
||||
const isLoad = ref(false);
|
||||
const changeValue = ref<any>(undefined);
|
||||
const filterFiledRef = ref<string | undefined>(undefined);
|
||||
let isFirstLoad = !!props.api; // 是否第一次加载
|
||||
// eslint-disable-next-line vue/no-setup-props-destructure
|
||||
let page = props.defaultPage; //
|
||||
let isLoading = false;
|
||||
const getBindValues = computed(() => {
|
||||
const selectProps: Recordable = {};
|
||||
if (props.filterFiled || props.autoSearch) {
|
||||
selectProps['showSearch'] = true;
|
||||
}
|
||||
return {
|
||||
...attrs,
|
||||
...selectProps,
|
||||
};
|
||||
});
|
||||
/**
|
||||
* 设置filterFiled时注册search事件
|
||||
*/
|
||||
const onSearch = computed(() => {
|
||||
if (props.filterFiled) {
|
||||
return (input: string) => {
|
||||
if (filterFiledRef.value !== input) {
|
||||
filterFiledRef.value = input;
|
||||
page = props.defaultPage;
|
||||
fetch();
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
/**
|
||||
* 设置filterFiled时注册filterOption事件
|
||||
*/
|
||||
const onFilterOption = computed(() => {
|
||||
if (props.autoSearch) {
|
||||
return (input: string, option: any) => {
|
||||
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
|
||||
};
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
/**
|
||||
* 根据请求得到的data获取options
|
||||
*/
|
||||
const getOptions = computed(() => {
|
||||
const { firstOption, labelField, valueField, numberToString } = props;
|
||||
const sourceOptions = [];
|
||||
firstOption && sourceOptions.push(firstOption);
|
||||
return unref(options).reduce((acc, next: Recordable) => {
|
||||
if (isString(next)) {
|
||||
//值是字符串类型
|
||||
const option = {
|
||||
label: next,
|
||||
value: next,
|
||||
};
|
||||
acc.push(option);
|
||||
return acc;
|
||||
} else {
|
||||
const value = get(next, valueField);
|
||||
const option = {
|
||||
...next,
|
||||
label: isFunction(labelField) ? labelField(next) : get(next, labelField as string),
|
||||
value: numberToString ? `${value}` : value,
|
||||
};
|
||||
acc.push(option);
|
||||
return acc;
|
||||
}
|
||||
}, sourceOptions);
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取数据
|
||||
*/
|
||||
const fetch = () => {
|
||||
console.log('requiredParams', getBindValues.value.checkRequiredParams);
|
||||
if (getBindValues.value.checkRequiredParams === false) {
|
||||
options.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading = true;
|
||||
const requestConfig: HttpRequestConfig = { method: 'get' };
|
||||
const {
|
||||
api,
|
||||
params: _params,
|
||||
resultField,
|
||||
filterData,
|
||||
filterFiled,
|
||||
pageField,
|
||||
scrollLoad,
|
||||
defaultPage,
|
||||
} = props;
|
||||
const params: Recordable = cloneDeep(_params);
|
||||
if (filterFiled && filterFiledRef.value) {
|
||||
params[filterFiled] = filterFiledRef.value;
|
||||
}
|
||||
if (scrollLoad) {
|
||||
params[pageField] = page;
|
||||
if (page === defaultPage) {
|
||||
options.value = [];
|
||||
}
|
||||
}
|
||||
const { httpRequest } = useApi();
|
||||
httpRequest({ api, params, requestConfig })
|
||||
.then((res: Recordable) => {
|
||||
emit('validateChange', { help: undefined });
|
||||
if (resultField) {
|
||||
let data = get(res, resultField) || [];
|
||||
// data = data.splice(Math.floor(Math.random() * 3), Math.floor(Math.random() * 5 + 5));
|
||||
if (isFunction(filterData)) {
|
||||
data = data.filter(filterData);
|
||||
}
|
||||
if (scrollLoad) {
|
||||
options.value = [...options.value, ...data];
|
||||
} else {
|
||||
options.value = data;
|
||||
}
|
||||
}
|
||||
isLoad.value = true;
|
||||
isLoading = false;
|
||||
})
|
||||
.catch((error: any) => {
|
||||
if (error?.response?.status === 403) {
|
||||
emit('validateChange', { help: '暂无权限', validateStatus: 'error' });
|
||||
nextTick(() => {
|
||||
//清空编辑初始值
|
||||
modelValue.value = undefined;
|
||||
});
|
||||
}
|
||||
options.value = [];
|
||||
isLoading = false;
|
||||
});
|
||||
};
|
||||
let lastParams: any = undefined;
|
||||
/**
|
||||
** 延迟获取数据
|
||||
*/
|
||||
async function dropdownHandle(open: boolean) {
|
||||
if (!open) return;
|
||||
if ((!props.immediate && !isLoad.value) || props.dropdownReload) {
|
||||
await fetch();
|
||||
}
|
||||
}
|
||||
/**
|
||||
** 绑定value
|
||||
*/
|
||||
const modelValue = computed({
|
||||
set(value: ChangeValue) {
|
||||
if (isEqual(value, changeValue.value)) return;
|
||||
changeValue.value = value;
|
||||
triggerChange(value);
|
||||
},
|
||||
get() {
|
||||
return changeValue.value;
|
||||
},
|
||||
});
|
||||
/**
|
||||
* 传入值需要
|
||||
* 前提option已获取到数据
|
||||
*/
|
||||
watch(
|
||||
() => props.value,
|
||||
() => {
|
||||
if (isLoad.value) {
|
||||
modelValue.value = props.value;
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
watch(
|
||||
() => getOptions.value,
|
||||
() => {
|
||||
const { value, autoSelectFirst, autoClearValue } = props;
|
||||
// 首次加载如果有值则选中值
|
||||
if (isFirstLoad && !isUndefined(value)) {
|
||||
modelValue.value = value;
|
||||
} else if (!filterFiledRef.value) {
|
||||
if (autoSelectFirst) {
|
||||
modelValue.value = getOptions.value[0]?.value;
|
||||
} else if (autoClearValue) {
|
||||
modelValue.value = undefined;
|
||||
}
|
||||
}
|
||||
isFirstLoad = false;
|
||||
},
|
||||
);
|
||||
/**
|
||||
* 重写change ant 初始化数据、删除数据时不触发change
|
||||
* @param value
|
||||
*/
|
||||
function triggerChange(value: ChangeValue) {
|
||||
if (isUndefined(value)) {
|
||||
emit('change', value, undefined);
|
||||
} else if (isArray(value)) {
|
||||
const options: Record<string, any>[] = [];
|
||||
value.forEach((v) => {
|
||||
getOptions.value.forEach((option) => {
|
||||
if (option.value === v) {
|
||||
options.push(option);
|
||||
}
|
||||
});
|
||||
});
|
||||
emit('change', value, options);
|
||||
} else {
|
||||
let op = {};
|
||||
getOptions.value.forEach((option) => {
|
||||
if (option.value === value) {
|
||||
op = option;
|
||||
}
|
||||
});
|
||||
emit('change', value, op);
|
||||
}
|
||||
}
|
||||
/**
|
||||
** 联动 immediate 为是否主动获取数据
|
||||
*/
|
||||
watch(
|
||||
[() => props.params, () => props.api],
|
||||
async () => {
|
||||
const { params } = props;
|
||||
if (!isEqual(lastParams, params)) {
|
||||
if (props.immediate || true) {
|
||||
//todo 暂时全设为主动获取
|
||||
lastParams = cloneDeep(params);
|
||||
await fetch();
|
||||
}
|
||||
isLoad.value = false; // 设置成false 点击下拉才会触发
|
||||
}
|
||||
},
|
||||
{ deep: true, immediate: props.immediate },
|
||||
);
|
||||
const onPopupScroll = computed(() => {
|
||||
if (props.scrollLoad) {
|
||||
return (e) => {
|
||||
const { scrollHeight, scrollTop, clientHeight } = e.target;
|
||||
if (scrollHeight - scrollTop - 80 < clientHeight && !isLoading) {
|
||||
page++;
|
||||
fetch();
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
const onClear = () => {
|
||||
filterFiledRef.value = undefined;
|
||||
};
|
||||
return {
|
||||
onPopupScroll,
|
||||
dropdownHandle,
|
||||
getOptions,
|
||||
getBindValues,
|
||||
modelValue,
|
||||
onSearch,
|
||||
onFilterOption,
|
||||
onClear,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped></style>
|
||||
14
lib/component/form/select/select-opt-group.vue
Normal file
14
lib/component/form/select/select-opt-group.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<a-select-opt-group>
|
||||
<slot></slot>
|
||||
</a-select-opt-group>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NsSelectOptGroup',
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped></style>
|
||||
12
lib/component/form/select/select-option.vue
Normal file
12
lib/component/form/select/select-option.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<a-select-option><slot></slot></a-select-option>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NsSelectOption',
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped></style>
|
||||
375
lib/component/form/select/select-tree-api.vue
Normal file
375
lib/component/form/select/select-tree-api.vue
Normal file
@@ -0,0 +1,375 @@
|
||||
<!-- @format -->
|
||||
|
||||
<template>
|
||||
<a-tree-select
|
||||
v-bind="getBindValues"
|
||||
:tree-data="getOptions"
|
||||
@dropdown-visible-change="dropdownHandle"
|
||||
v-model:value="modelValue"
|
||||
@search="onSearch"
|
||||
:filterOption="onFilterOption"
|
||||
:multiple="multiple"
|
||||
:treeDefaultExpandedKeys="treeDefaultExpandedKeys">
|
||||
<template #[item]="data" v-for="item in Object.keys($slots)" :key="item">
|
||||
<slot :name="item" v-bind="data || {}"></slot>
|
||||
</template>
|
||||
</a-tree-select>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, nextTick, PropType, ref, unref, watch } from 'vue';
|
||||
import { cloneDeep, get, isArray, isEqual, isFunction, isString, isUndefined } from 'lodash-es';
|
||||
import { HttpRequestConfig, useApi } from '/nerv-lib/use/use-api';
|
||||
|
||||
type ChangeValue = string | number | undefined | string[] | number[];
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NsSelectTreeApi',
|
||||
props: {
|
||||
api: {
|
||||
type: [String, Object, Function] as PropType<string | Function | HttpRequestConfig>,
|
||||
required: true,
|
||||
},
|
||||
value: [String, Object, Array, Number],
|
||||
field: String,
|
||||
params: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
resultField: {
|
||||
type: String,
|
||||
default: 'data.data',
|
||||
},
|
||||
firstOption: {
|
||||
type: Object,
|
||||
},
|
||||
numberToString: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
immediate: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
requiredParams: {
|
||||
type: [Boolean, Object],
|
||||
default: false,
|
||||
},
|
||||
labelField: {
|
||||
type: [String, Function],
|
||||
default: 'label',
|
||||
},
|
||||
valueField: {
|
||||
type: String,
|
||||
default: 'value',
|
||||
},
|
||||
autoSelectFirst: {
|
||||
// 与autoClearValue同时使用,只会执行autoSelectFirst
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
autoClearValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
//数据筛选函数
|
||||
filterData: {
|
||||
type: Function,
|
||||
},
|
||||
// 开启前端筛选默认label包含
|
||||
autoSearch: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
//后端筛选字段
|
||||
filterFiled: {
|
||||
type: String,
|
||||
},
|
||||
dropdownReload: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// 多选模式
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
unSelectable: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
emits: ['change', 'validateChange'],
|
||||
setup(props, { attrs, emit }) {
|
||||
const options = ref([]);
|
||||
const isLoad = ref(false);
|
||||
const changeValue = ref<any>(undefined);
|
||||
const filterFiledRef = ref<string | undefined>(undefined);
|
||||
let isFirstLoad = !!props.api; // 是否第一次加载
|
||||
let treeDefaultExpandedKeys = ref<any>([]); //展开默认选中值
|
||||
|
||||
const getBindValues = computed(() => {
|
||||
const selectProps: Recordable = {};
|
||||
if (props.filterFiled || props.autoSearch) {
|
||||
selectProps['showSearch'] = true;
|
||||
}
|
||||
|
||||
return {
|
||||
...attrs,
|
||||
...selectProps,
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* 设置filterFiled时注册search事件
|
||||
*/
|
||||
const onSearch = computed(() => {
|
||||
if (props.filterFiled) {
|
||||
return (input: string) => {
|
||||
if (filterFiledRef.value !== input) {
|
||||
filterFiledRef.value = input;
|
||||
fetch();
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 设置filterFiled时注册filterOption事件
|
||||
*/
|
||||
const onFilterOption = computed(() => {
|
||||
if (props.autoSearch) {
|
||||
return (input: string, option: any) => {
|
||||
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
|
||||
};
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 根据请求得到的data获取options
|
||||
*/
|
||||
const getOptions = computed(() => {
|
||||
const { firstOption, labelField, valueField, numberToString } = props;
|
||||
const sourceOptions = [];
|
||||
firstOption && sourceOptions.push(firstOption);
|
||||
let acc: any[] = [];
|
||||
unref(options).forEach((next: Recordable) => {
|
||||
const args = { labelField, valueField };
|
||||
transferTreeData(next, null, acc, { ...args });
|
||||
});
|
||||
return acc;
|
||||
});
|
||||
/** 转换树形数据 */
|
||||
function transferTreeData(next: Recordable, parent: any, acc: any, args: any) {
|
||||
const { labelField, valueField, numberToString } = args;
|
||||
let node = null;
|
||||
if (isString(next)) {
|
||||
//值是字符串类型
|
||||
node = {
|
||||
title: next,
|
||||
value: next,
|
||||
key: next,
|
||||
children: [],
|
||||
};
|
||||
} else {
|
||||
const value = get(next, valueField);
|
||||
node = {
|
||||
title: isFunction(labelField) ? labelField(next) : get(next, labelField as string),
|
||||
value: numberToString ? `${value}` : value,
|
||||
key: value,
|
||||
children: [],
|
||||
};
|
||||
}
|
||||
if (props.unSelectable) {
|
||||
const level = props.unSelectable.level;
|
||||
if (next['level'] == level) {
|
||||
node['selectable'] = false;
|
||||
}
|
||||
}
|
||||
if (!parent) {
|
||||
// 根节点
|
||||
acc.push(node);
|
||||
} else {
|
||||
parent.children?.push(node);
|
||||
}
|
||||
if (next.children?.length > 0) {
|
||||
for (const el of next['children']) {
|
||||
transferTreeData(el, node, acc, { ...args });
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据
|
||||
*/
|
||||
const fetch = () => {
|
||||
const requestConfig: HttpRequestConfig = { method: 'get' };
|
||||
const { api, params: _params, resultField, filterData } = props;
|
||||
const params: Recordable = cloneDeep(_params);
|
||||
if (props.filterFiled && filterFiledRef.value) {
|
||||
params[props.filterFiled] = filterFiledRef.value;
|
||||
}
|
||||
const { httpRequest } = useApi();
|
||||
httpRequest({ api, params, requestConfig })
|
||||
.then((res: Recordable) => {
|
||||
emit('validateChange', { help: undefined });
|
||||
let data = [];
|
||||
if (resultField) {
|
||||
data = get(res, resultField) || [];
|
||||
// data = data.splice(Math.floor(Math.random() * 3), Math.floor(Math.random() * 5 + 5));
|
||||
} else {
|
||||
data = res;
|
||||
}
|
||||
if (isFunction(filterData)) {
|
||||
options.value = data.filter(filterData);
|
||||
} else {
|
||||
options.value = data;
|
||||
}
|
||||
isLoad.value = true;
|
||||
})
|
||||
.catch((error: any) => {
|
||||
if (error?.response?.status === 403) {
|
||||
emit('validateChange', { help: '暂无权限', validateStatus: 'error' });
|
||||
nextTick(() => {
|
||||
//清空编辑初始值
|
||||
modelValue.value = undefined;
|
||||
});
|
||||
}
|
||||
options.value = [];
|
||||
});
|
||||
};
|
||||
|
||||
let lastParams: any = undefined;
|
||||
|
||||
/**
|
||||
** 延迟获取数据
|
||||
*/
|
||||
async function dropdownHandle(open: boolean) {
|
||||
if (!open) return;
|
||||
if ((!props.immediate && !isLoad.value) || props.dropdownReload) {
|
||||
await fetch();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
** 绑定value
|
||||
*/
|
||||
const modelValue = computed({
|
||||
set(value: ChangeValue) {
|
||||
if (isEqual(value, changeValue.value)) return;
|
||||
changeValue.value = value;
|
||||
triggerChange(value);
|
||||
},
|
||||
get() {
|
||||
return changeValue.value;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 传入值需要
|
||||
* 前提option已获取到数据
|
||||
*/
|
||||
watch(
|
||||
() => props.value,
|
||||
() => {
|
||||
if (isLoad.value) {
|
||||
modelValue.value = props.value;
|
||||
}
|
||||
// 默认选中值
|
||||
if (props.multiple) {
|
||||
treeDefaultExpandedKeys.value = props.value;
|
||||
} else {
|
||||
treeDefaultExpandedKeys.value = [props.value];
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => getOptions.value,
|
||||
() => {
|
||||
const { value, autoSelectFirst, autoClearValue } = props;
|
||||
|
||||
// 首次加载如果有值则选中值
|
||||
if (isFirstLoad && !isUndefined(value)) {
|
||||
modelValue.value = value;
|
||||
} else if (!filterFiledRef.value) {
|
||||
if (autoSelectFirst) {
|
||||
modelValue.value = getOptions.value[0]?.value;
|
||||
} else if (autoClearValue) {
|
||||
modelValue.value = undefined;
|
||||
}
|
||||
}
|
||||
isFirstLoad = false;
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* 重写change ant 初始化数据、删除数据时不触发change
|
||||
* @param value
|
||||
*/
|
||||
function triggerChange(value: ChangeValue) {
|
||||
if (isUndefined(value)) {
|
||||
emit('change', value, undefined);
|
||||
} else if (isArray(value)) {
|
||||
const options: Record<string, any>[] = [];
|
||||
value.forEach((v) => {
|
||||
getOptions.value.forEach((option) => {
|
||||
if (option.value === v) {
|
||||
options.push(option);
|
||||
}
|
||||
});
|
||||
});
|
||||
emit('change', value, options);
|
||||
} else {
|
||||
let op = {};
|
||||
getOptions.value.forEach((option) => {
|
||||
if (option.value === value) {
|
||||
op = option;
|
||||
}
|
||||
});
|
||||
emit('change', value, op);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
** 联动 immediate 为是否主动获取数据
|
||||
*/
|
||||
watch(
|
||||
[() => props.params, () => props.api],
|
||||
async () => {
|
||||
const { params } = props;
|
||||
if (!isEqual(lastParams, params)) {
|
||||
if (props.immediate || true) {
|
||||
//todo 暂时全设为主动获取
|
||||
lastParams = cloneDeep(params);
|
||||
|
||||
await fetch();
|
||||
}
|
||||
isLoad.value = false; // 设置成false 点击下拉才会触发
|
||||
}
|
||||
},
|
||||
{ deep: true, immediate: props.immediate },
|
||||
);
|
||||
|
||||
return {
|
||||
dropdownHandle,
|
||||
getOptions,
|
||||
getBindValues,
|
||||
modelValue,
|
||||
onSearch,
|
||||
onFilterOption,
|
||||
treeDefaultExpandedKeys,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped></style>
|
||||
18
lib/component/form/select/select.vue
Normal file
18
lib/component/form/select/select.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<a-select :getPopupContainer="(triggerNode) => triggerNode.parentNode">
|
||||
<template #[item]="data" v-for="item in Object.keys($slots)" :key="item">
|
||||
<slot :name="item" v-bind="data || {}"></slot>
|
||||
</template>
|
||||
</a-select>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NsSelect',
|
||||
props: {},
|
||||
setup(props) {},
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped></style>
|
||||
18
lib/component/form/select/selectTree.vue
Normal file
18
lib/component/form/select/selectTree.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<a-tree-select>
|
||||
<template #[item]="data" v-for="item in Object.keys($slots)" :key="item">
|
||||
<slot :name="item" v-bind="data || {}"></slot>
|
||||
</template>
|
||||
</a-tree-select>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NsSelectTree',
|
||||
props: {},
|
||||
setup(props) {},
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped></style>
|
||||
Reference in New Issue
Block a user