import { type ActiveRecord, type ActiveRecords, getRelationFieldKey, isRelatedField, ModelCache, QueryService, RequestHelper, type RuntimeModel, type RuntimeModelField, type RuntimeRelationField, translateValueByKey } from '@oinone/kunlun-engine'; import { type Entity, type IModel, isEmptyValue, ModelType } from '@oinone/kunlun-meta'; import { Condition, type ObjectValue } from '@oinone/kunlun-request'; import { DEFAULT_TRUE_CONDITION, IQueryPageOption, IQueryPageResult, queryOne } from '@oinone/kunlun-service'; import { CastHelper, NumberHelper } from '@oinone/kunlun-shared'; import { autoFillSelectedValueToOptions, autoFillSelectedValueToOptionsByLabel, buildSelectSearchCondition } from '@oinone/kunlun-vue-admin-layout'; import { PageSizeEnum, WidgetTrigger } from '@oinone/kunlun-vue-ui-common'; import { Widget } from '@oinone/kunlun-vue-widget'; import { isEmpty, isNil, isNumber, isPlainObject, isString, toInteger } from 'lodash-es'; import { isValidatorSuccess, type ValidatorInfo } from '../../../../typing'; import type { FormComplexFieldProps } from '../FormComplexFieldWidget'; import { BaseSelectFieldWidget } from './BaseSelectFieldWidget'; /** * 关系字段下拉选的抽象类 */ export abstract class FormSelectComplexFieldWidget< Value extends ActiveRecords = ActiveRecords, Field extends RuntimeRelationField = RuntimeRelationField, Props extends FormComplexFieldProps = FormComplexFieldProps > extends BaseSelectFieldWidget { protected selectedValue!: Record; // 单选选中的值 protected selectedOption!: Record; // 单选选中的选项 protected selectedValues!: Record[]; // 多选选中的值 protected timeout; @Widget.Reactive() protected searchValue = ''; protected queryFieldName = 'name'; protected computeQueryOneDefaultKey() { return 'id'; } @Widget.Reactive() public get computeQueryOneKey() { return this.getDsl().computeQueryOneKey; } @Widget.Reactive() protected showMoreButton = false; @Widget.Reactive() protected isInitOptions = false; protected totalPages = 10000; @Widget.Reactive() protected total = 0; @Widget.Reactive() protected currentPage = 1; @Widget.Reactive() protected pageSize = PageSizeEnum.OPTION_2; @Widget.Reactive() protected options: Record[] = []; protected dataList: Record[] = []; protected defaultConstructDataTrigger() { return [WidgetTrigger.CHANGE]; } protected defaultClearFieldsTrigger() { return [WidgetTrigger.CHANGE]; } @Widget.Watch('value', { deep: true }) protected watchValue() { if (this.field.multi) { this.selectedValues = this.value as any; } else { this.selectedValue = this.value as any; } if (!isEmptyValue(this.value)) { this.fillOptions(this.dataList || []); } else { if (this.oldDomain && this.oldDomain !== this.domain) { // 解决其他字段联动后当前字段domain发生了改变,且配置了clearFields清空了当前字段value的时候,之前已选中的对象还在options的问题 // 之前watch domain 属性是没有用的,domain发生改变的时候value还是老的数据,所以需要在domain和value同时明确发生改变后重新获取一次options this.currentPage = 1; this.initLoadOptions(); this.oldDomain = ''; } } } protected async fillOptions(dataList: Record[], insetDefaultValue = true) { this.field.multi ? this.fillOptionsForMulti(dataList) : this.fillOptionsForSingle(dataList, insetDefaultValue); } public x2oChange(value) { if (value == null) { super.change(null as any); this.handleEmpty(); return; } const selectedValue = this.dataList.find((d) => d[this.relationFieldKey] === value.value)! || value; super.change(selectedValue as any); } protected x2mChange(value) { if (value == null) { super.change(value); this.handleEmpty(); } else { if (!value.length) { this.handleEmpty(); } const submitData = this.filterX2mChangeValue(value); super.change(submitData); } } protected filterX2mChangeValue(value) { // focus的时候才会查询数据,这时候dataList为空,如果开始有value,会导致剩下的已选数据匹配不到值 const list = isEmpty(this.dataList) ? this.value || [] : this.dataList; return value .map((item) => { return (list as any[])?.find((d) => d[this.relationFieldKey] === item.value); }) .filter((a) => !!a); } public async loadMetadata() {} @Widget.Method() protected generatorSelectOption(optionDataList: Record[]) { return this.handleSelectOption(optionDataList, this.referencesModel); } protected handleSelectOption( optionDataList: Record[], referencesModel: RuntimeModel | undefined ): Entity[] { const { separator = ' ' } = this.getDsl(); const xmlOptionLabel = this.optionLabel; const metaModelLabel = referencesModel?.label; if (isEmptyValue(xmlOptionLabel) && isEmptyValue(metaModelLabel)) { let labelFields: string[] = []; labelFields = referencesModel?.labelFields || []; return autoFillSelectedValueToOptions( this.relationFieldKey, this.value as Entity, optionDataList, labelFields, separator ); } return autoFillSelectedValueToOptionsByLabel( this.relationFieldKey, this.value as Entity, optionDataList, xmlOptionLabel, metaModelLabel, this.optionLabelContextArgs ); } protected setQueryFieldName(queryFieldName: string) { this.queryFieldName = queryFieldName; } protected async fillOptionsForMulti(dataList: Record[]) { const pk = this.referencesModel?.pks; if (!pk) { console.error(`current model is not found pks. field: ${this.field.data}`); return; } if (this.selectedValues) { for (let j = 0; j < this.selectedValues.length; j++) { const selected = this.selectedValues[j]; if (!selected) { continue; } const selectedOptionIndex = dataList.findIndex((option) => { for (let i = 0; i < pk.length; i++) { if (selected[pk[i]] !== option[pk[i]]) { return false; } } return true; }); if (selectedOptionIndex < 0) { dataList = [selected, ...dataList]; } else { const secondSelectedOptionIndex = dataList.slice(selectedOptionIndex + 1).findIndex((option) => { for (let i = 0; i < pk.length; i++) { if (selected[pk[i]] !== option[pk[i]]) { return false; } } return true; }); if (secondSelectedOptionIndex >= 0) { dataList.splice(secondSelectedOptionIndex + 1, 1); } } } } this.dataList = dataList; this.options = this.handleSelectOption(this.dataList, this.referencesModel); } protected async fillOptionsForSingle(dataList: Record[], insetDefaultValue = true) { const pk = this.referencesModel?.pks || []; let valid = true; for (let i = 0; i < pk.length; i++) { if (this.selectedValue && this.selectedValue[pk[i]] == null) { valid = false; break; } } if (this.selectedValue && valid) { const selectedOptionIndex = dataList.findIndex((option) => { for (let i = 0; i < pk.length; i++) { if (this.selectedValue && this.selectedValue[pk[i]] !== option[pk[i]]) { return false; } } return true; }); if (selectedOptionIndex < 0 && insetDefaultValue) { dataList = [this.selectedValue, ...dataList]; } else { const secondSelectedOptionIndex = dataList.slice(selectedOptionIndex + 1).findIndex((option) => { for (let i = 0; i < pk.length; i++) { if (this.selectedValue && this.selectedValue[pk[i]] !== option[pk[i]]) { return false; } } return true; }); if (secondSelectedOptionIndex >= 0) { dataList.splice(secondSelectedOptionIndex + 1, 1); } } } this.dataList = dataList; this.options = this.handleSelectOption(this.dataList, this.referencesModel); } @Widget.Reactive() protected get relationFieldKey() { return getRelationFieldKey(this.field, this.referencesModel); } @Widget.Method() protected async loadMore() { this.currentPage += 1; if (this.currentPage > this.totalPages) { this.showMoreButton = false; return; } this.loadMoreLoading = true; const iQueryPageResult = await this.loadOptions({ condition: buildSelectSearchCondition(this.referencesModel, this.queryFieldName, this.searchValue, this.domain), currentPage: this.currentPage }); const options = [...this.dataList, ...iQueryPageResult.content]; await this.fillOptions(options, true); this.loadMoreLoading = false; } @Widget.Method() protected async onSelect(e) { this.selectedOption = e; if (this.searchValue) { this.search(''); } } @Widget.Reactive() protected get maxDepth(): number { let maxDepth = NumberHelper.toNumber(this.getDsl().maxDepth); if (isNil(maxDepth) || (maxDepth !== -1 && maxDepth <= 0)) { maxDepth = 1; } return maxDepth; } protected async loadOriginValue() { const { value } = this; if (Array.isArray(value) && value.length) { const ids = value.map((val) => val[this.relationFieldKey]); const item = this.options.filter((o) => ids.includes(o.value)); const { references } = this.field; if (!item.length && references) { // const cond = new Condition('id').in(ids).toString(); // const condition = this.domain ? `${this.domain} and ${cond}` : cond; // const opts = await queryPage(references, { condition }); const res = this.handleSelectOption(this.dataList, this.referencesModel); this.options.unshift(...res); this.dataList.unshift(...res); } } } /** * 如果当前字段是m2m select, 并且有值 ,那么发起查询获取数据 * * 否则就在字段获取焦点的时候查询 */ @Widget.Reactive() protected get needInitOptions() { if (Array.isArray(this.value)) { return !!this.value.length; } if (this.readonly || this.disabled) { return false; } return false; } protected mountedProcess() { super.mountedProcess(); const { searchFields } = this; if (searchFields.length) { this.setQueryFieldName(searchFields.join(',')); } if (this.needInitOptions) { this.initLoadOptions(); } // if (!this.readonly && !this.disabled) { // if (!this.options || !this.options.length) { // this.loadMoreLoading = true; // } // } } protected async initLoadOptions() { this.loadMoreLoading = true; try { const data = await this.loadOptions({ condition: buildSelectSearchCondition(this.referencesModel, this.queryFieldName, this.searchValue, this.domain), currentPage: this.currentPage }); if (data) { await this.fillOptions(data.content, true); this.isInitOptions = true; } } catch (e) { console.error('initLoadOptions exp', e); } finally { this.loadMoreLoading = false; } } protected async loadOptions(param: IQueryPageOption = {}) { /** * @description 如果当前字段是非存储,并且它的关联模型是(STORE[`存储模型`] || PROXY[`代理模型`])才需要 `queryPage` */ const { references } = this.field; const option = { condition: this.domain, queryData: this.queryData, ...param, pageSize: this.pageSize }; let { type: referencesType } = this.referencesModel!; const referencesModelFields = (this.referencesModel?.modelFields || []) as unknown as RuntimeModelField[]; let content; // fixme @zbh 20221129 改版 if (false) { // if (this.field.optionsLoadApi) { // content = queryFieldDataList4Options( // CastHelper.cast(this.field), // CastHelper.cast(this.field), // [], // this.getSelfViewWidget()!.getDsl(), // this.formData, // [this.formData], // this.rootData, // useMatched().matched.segmentParams?.page?.scene // ); } else if (references) { if (!referencesType) { const referenceModel = this.referencesModel!; referencesType = referenceModel.type; } if (referencesType === ModelType.STORE || referencesType === ModelType.PROXY) { // 这里暂时只能做到两层, 否则中间层的模型字段无处定义 const mainModel = this.model; const elseQueryFields: RuntimeModelField[] = []; const mainModelFields = mainModel.modelFields; if (mainModelFields) { for (const modelField of mainModelFields) { if ( modelField && isRelatedField(modelField) && modelField.relatedTtype && modelField.related && modelField.related.length > 1 ) { const mainFieldName = modelField.related[0]; const elseFieldName = modelField.related[1]; if (this.field!.name === mainFieldName) { elseQueryFields.push({ ...modelField, model: references, name: elseFieldName, ttype: modelField.relatedTtype }); } } } } for (const elseQueryField of elseQueryFields) { const index = referencesModelFields?.findIndex((_f) => _f.name === elseQueryField.name); if (index < 0) { referencesModelFields?.push(elseQueryField); } } const iQueryPageResult = await this.innerQueryPage(references, option, referencesModelFields!, undefined, { maxDepth: this.maxDepth }); this.total = toInteger(iQueryPageResult.totalElements); this.totalPages = FormSelectComplexFieldWidget.getRealPages(iQueryPageResult.totalPages); this.showMoreButton = this.currentPage < this.totalPages; content = iQueryPageResult; } } else { content = { content: [], totalPages: 0, totalElements: 0, size: 0 } as IQueryPageResult>; } if (content && !content.content) { content.content = []; } return content; } /** * * 多选的搜索下拉框关闭的时候,需要重新获取数据, 前置条件:没有value的时候 * * 操作: 用户用多选的select搜索的时候,输入搜索内容查询了多条数据,但是用户不选中任何一条, * 直接失去焦点,这个时候在重新获取焦点,下拉的数据是上次搜索出来的数据, * 所以需要单独处理 * */ @Widget.Method() protected dropdownVisibleChange(open: boolean) { const hasObjValue = this.value && isPlainObject(this.value) && Object.keys(this.value).length; const hasArrValue = this.value && Array.isArray(this.value) && this.value.length; const hasValue = this.value && (hasObjValue || hasArrValue); if (!open && !hasValue) { this.search('', true); } if (open && !this.options?.length) { this.initLoadOptions(); } } @Widget.Method() protected async search(searchValue: string, forceSearch = false) { if (this.timeout) { clearTimeout(this.timeout); this.timeout = null; } if (!forceSearch) { if (this.searchValue === searchValue) { return; } } this.searchValue = searchValue; this.currentPage = 1; this.timeout = setTimeout(delaySearch, 400); // eslint-disable-next-line @typescript-eslint/no-this-alias const self = this; async function delaySearch() { const iQueryPageResult = await self.loadOptions({ condition: buildSelectSearchCondition(self.referencesModel, self.queryFieldName, self.searchValue, self.domain), currentPage: self.currentPage }); if (iQueryPageResult) { self.totalPages = FormSelectComplexFieldWidget.getRealPages(iQueryPageResult.totalPages); await self.fillOptions(iQueryPageResult.content, false); } } } protected oldDomain = ''; @Widget.Watch('domain') protected async onDomainChange(newDomain, oldDomain) { if (newDomain !== oldDomain) { this.oldDomain = oldDomain; this.currentPage = 1; this.initLoadOptions(); } } @Widget.Watch('queryData') protected async onQueryChange(newData, oldData) { if (JSON.stringify(newData) !== JSON.stringify(oldData)) { this.currentPage = 1; this.initLoadOptions(); } } @Widget.Reactive() protected get maxNumber() { const _maxNumber = this.getDsl().maxNumber; if (_maxNumber) { return _maxNumber; } return Infinity; } @Widget.Reactive() protected get minNumber() { const _minNumber = this.getDsl().minNumber; if (_minNumber) { return _minNumber; } return 0; } /** * 值是否与选项列表是否一致 * 如果为 true,那么是编辑页数据回填,在获取焦点的时候需要查询列表 */ @Widget.Reactive() protected get valueEqualOptions() { let valueEqualOptions = false; if (this.value) { const value = (this.field.multi ? (this.value ?? []) : [this.value]) as ActiveRecord[]; valueEqualOptions = this.options.length === value.length && this.options.every((opt) => { return !!value.find((v) => v[this.relationFieldKey] === opt.value); }); } return valueEqualOptions; } public async validator(): Promise { const res = await super.validator(); if (!isValidatorSuccess(res)) { return res; } const { value } = this; if (!isNil(value)) { if (this.field.multi) { const values = CastHelper.cast(value); const { length } = values; if (length > 0) { if (!isNil(this.maxNumber) && length > this.maxNumber) { return this.validatorError( `${this.field.label} ${translateValueByKey('最多选择')}${this.maxNumber}${translateValueByKey('个')}` ); } if (!isNil(this.minNumber) && length < this.minNumber) { return this.validatorError( `${this.field.label} ${translateValueByKey('最少选择')}${this.minNumber}${translateValueByKey('个')}` ); } } } } return this.validatorSuccess(); } @Widget.Method() protected handleEmpty(forceSearch = false) { // value置空说明是触发了清除, 这个时候搜索关键词应该也要清空, 并且下拉候选项也要恢复初始值 this.selectedOption = {}; this.search(null as any, forceSearch); } @Widget.Method() public focus() { super.focus(); if (this.readonly || this.disabled) { return; } if (!this.options.length || this.valueEqualOptions) { this.initLoadOptions(); } else if (!this.isInitOptions) { this.isInitOptions = true; } } @Widget.Method() public blur() { // 若此时搜索结果为空, 那么需要将候选值恢复初始值, 以防下次下拉的时候还是空 if (!this.options.length) { this.handleEmpty(true); } super.blur(); } @Widget.Method() protected changeSearchValue(val: string) { this.searchValue = val; } private static getRealPages(val) { return isNil(val) ? 1000 : val; } @Widget.Reactive() @Widget.Provide() protected get queryData() { return this.buildQueryData(); } protected buildQueryData() { return this.genQueryData(); } protected genQueryData() { return this.generatorQueryData(); } public async updateM2oValue() { const val = this.getValue(); const _compute = this.getCompute(this.formData); if (_compute != null && _compute !== '') { const computeResult = this.executeExpression(_compute, null); let queryOneKey = this.computeQueryOneKey; const _computeList = _compute.split('.'); if (_computeList[0] === 'activeRecord' && _computeList[1] && _computeList[2] && !queryOneKey) { const currentModel = (await ModelCache.get(this.field!.model)) as unknown as IModel; const relatedField = currentModel?.modelFields?.find((_f) => _f.name === _computeList[1]); if (relatedField && relatedField.references) { const relatedFieldModel = (await ModelCache.get(relatedField.references)) as unknown as IModel; if (relatedFieldModel && relatedFieldModel.modelFields) { const finalField = relatedFieldModel.modelFields.find( (_f) => _f.relationFields && _f.relationFields.includes(_computeList[2]) ); if (finalField) { const keyIndex = finalField.relationFields?.findIndex((_r) => _r === _computeList[2]); queryOneKey = finalField.referenceFields?.[keyIndex!] || 'id'; } } } } if (!queryOneKey) { queryOneKey = this.computeQueryOneDefaultKey(); } const param = {}; param[queryOneKey] = computeResult; if (isString(computeResult) || isNumber(computeResult)) { if (typeof val === 'object' && !isEmptyValue(val)) { if (val[queryOneKey] !== computeResult) { const data = await queryOne(this.field!.references!, param); this.setValue(data); } } else { const data = await queryOne(this.field!.references!, param); this.setValue(data); } } else if (typeof computeResult === 'object') { if (JSON.stringify(computeResult) !== JSON.stringify(this.value)) { this.setValue(computeResult); } } else if (isNil(computeResult)) { this.setValue(null); } } } protected async innerQueryPage>( modelModel: string, option: IQueryPageOption, fields?: RuntimeModelField[], variables?: ObjectValue, context: ObjectValue = {} ): Promise> { let { condition } = option; if (!isEmptyValue(option.queryData)) { const { queryData } = option; if (typeof condition === 'string') { condition = new Condition(condition); } if (!condition) { condition = new Condition(DEFAULT_TRUE_CONDITION); } condition.setConditionBodyData({ ...(condition.getConditionBodyData() || {}), ...queryData }); } return QueryService.queryPage(this.referencesModel!, { fun: this.loadFunctionFun, currentPage: option.currentPage!, pageSize: option.pageSize!, condition, requestFields: RequestHelper.convertRequestFields(fields || []), variables, context }) as unknown as IQueryPageResult; } /** * @deprecated invalid prop */ @Widget.Reactive() protected get renderOnParent() { const _renderOnParent = this.getDsl().renderOnParent; if (isNil(_renderOnParent)) { return false; } return _renderOnParent; } /** * @deprecated invalid prop */ @Widget.Reactive() protected get clearBackFillSelected() { const _clearBackFillSelected = this.getDsl().clearBackFillSelected; if (isNil(_clearBackFillSelected)) { return false; } return _clearBackFillSelected; } }