diff --git a/internal/lint-configs/eslint-config/src/custom-config.ts b/internal/lint-configs/eslint-config/src/custom-config.ts index 60820f69..2deb2854 100644 --- a/internal/lint-configs/eslint-config/src/custom-config.ts +++ b/internal/lint-configs/eslint-config/src/custom-config.ts @@ -15,10 +15,17 @@ const customConfig: Linter.Config[] = [ }, }, { - files: ['packages/effects/**/**', 'packages/types/**/**'], + files: [ + 'apps/**/**', + 'packages/effects/**/**', + 'packages/utils/**/**', + 'packages/types/**/**', + 'packages/locales/**/**', + ], ignores: restrictedImportIgnores, rules: { 'perfectionist/sort-interfaces': 'off', + 'perfectionist/sort-objects': 'off', }, }, { diff --git a/packages/@core/base/shared/src/utils/__tests__/util.test.ts b/packages/@core/base/shared/src/utils/__tests__/util.test.ts index 992145ca..0d87b318 100644 --- a/packages/@core/base/shared/src/utils/__tests__/util.test.ts +++ b/packages/@core/base/shared/src/utils/__tests__/util.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { bindMethods } from '../util'; +import { bindMethods, getNestedValue } from '../util'; class TestClass { public value: string; @@ -78,3 +78,79 @@ describe('bindMethods', () => { expect(value).toBe('test'); }); }); + +describe('getNestedValue', () => { + interface UserProfile { + age: number; + name: string; + } + + interface UserSettings { + theme: string; + } + + interface Data { + user: { + profile: UserProfile; + settings: UserSettings; + }; + } + + const data: Data = { + user: { + profile: { + age: 25, + name: 'Alice', + }, + settings: { + theme: 'dark', + }, + }, + }; + + it('should get a nested value when the path is valid', () => { + const result = getNestedValue(data, 'user.profile.name'); + expect(result).toBe('Alice'); + }); + + it('should return undefined for non-existent property', () => { + const result = getNestedValue(data, 'user.profile.gender'); + expect(result).toBeUndefined(); + }); + + it('should return undefined when accessing a non-existent deep path', () => { + const result = getNestedValue(data, 'user.nonexistent.field'); + expect(result).toBeUndefined(); + }); + + it('should return undefined if a middle level is undefined', () => { + const result = getNestedValue({ user: undefined }, 'user.profile.name'); + expect(result).toBeUndefined(); + }); + + it('should return the correct value for a nested setting', () => { + const result = getNestedValue(data, 'user.settings.theme'); + expect(result).toBe('dark'); + }); + + it('should work for a single-level path', () => { + const result = getNestedValue({ a: 1, b: 2 }, 'b'); + expect(result).toBe(2); + }); + + it('should return the entire object if path is empty', () => { + expect(() => getNestedValue(data, '')()).toThrow(); + }); + + it('should handle paths with array indexes', () => { + const complexData = { list: [{ name: 'Item1' }, { name: 'Item2' }] }; + const result = getNestedValue(complexData, 'list.1.name'); + expect(result).toBe('Item2'); + }); + + it('should return undefined when accessing an out-of-bounds array index', () => { + const complexData = { list: [{ name: 'Item1' }] }; + const result = getNestedValue(complexData, 'list.2.name'); + expect(result).toBeUndefined(); + }); +}); diff --git a/packages/@core/base/shared/src/utils/merge.ts b/packages/@core/base/shared/src/utils/merge.ts index c63733ab..4bf79eb5 100644 --- a/packages/@core/base/shared/src/utils/merge.ts +++ b/packages/@core/base/shared/src/utils/merge.ts @@ -1 +1,10 @@ +import { createDefu } from 'defu'; + export { createDefu as createMerge, defu as merge } from 'defu'; + +export const mergeWithArrayOverride = createDefu((originObj, key, updates) => { + if (Array.isArray(originObj[key]) && Array.isArray(updates)) { + originObj[key] = updates; + return true; + } +}); diff --git a/packages/@core/base/shared/src/utils/util.ts b/packages/@core/base/shared/src/utils/util.ts index 7f9a620c..885eeaa6 100644 --- a/packages/@core/base/shared/src/utils/util.ts +++ b/packages/@core/base/shared/src/utils/util.ts @@ -17,3 +17,28 @@ export function bindMethods(instance: T): void { } }); } + +/** + * 获取嵌套对象的字段值 + * @param obj - 要查找的对象 + * @param path - 用于查找字段的路径,使用小数点分隔 + * @returns 字段值,或者未找到时返回 undefined + */ +export function getNestedValue(obj: T, path: string): any { + if (typeof path !== 'string' || path.length === 0) { + throw new Error('Path must be a non-empty string'); + } + // 把路径字符串按 "." 分割成数组 + const keys = path.split('.') as (number | string)[]; + + let current: any = obj; + + for (const key of keys) { + if (current === null || current === undefined) { + return undefined; + } + current = current[key as keyof typeof current]; + } + + return current; +} diff --git a/packages/@core/ui-kit/form-ui/__tests__/form-api.test.ts b/packages/@core/ui-kit/form-ui/__tests__/form-api.test.ts index d61627d9..69126420 100644 --- a/packages/@core/ui-kit/form-ui/__tests__/form-api.test.ts +++ b/packages/@core/ui-kit/form-ui/__tests__/form-api.test.ts @@ -1,24 +1,7 @@ -// 假设这个文件为 FormApi.ts import { beforeEach, describe, expect, it, vi } from 'vitest'; import { FormApi } from '../src/form-api'; -vi.mock('@vben-core/shared/utils', () => ({ - bindMethods: vi.fn(), - createMerge: vi.fn((mergeFn) => { - return (stateOrFn: any, prev: any) => { - mergeFn(prev, 'key', stateOrFn); - return { ...prev, ...stateOrFn }; - }; - }), - isFunction: (fn: any) => typeof fn === 'function', - StateHandler: vi.fn().mockImplementation(() => ({ - reset: vi.fn(), - setConditionTrue: vi.fn(), - waitForCondition: vi.fn().mockResolvedValue(true), - })), -})); - describe('formApi', () => { let formApi: FormApi; @@ -128,7 +111,6 @@ describe('formApi', () => { it('should unmount form and reset state', () => { formApi.unmounted(); expect(formApi.isMounted).toBe(false); - expect(formApi.stateHandler.reset).toHaveBeenCalled(); }); it('should validate form', async () => { diff --git a/packages/@core/ui-kit/form-ui/src/form-api.ts b/packages/@core/ui-kit/form-ui/src/form-api.ts index 3b78e039..df8bdf63 100644 --- a/packages/@core/ui-kit/form-ui/src/form-api.ts +++ b/packages/@core/ui-kit/form-ui/src/form-api.ts @@ -12,20 +12,13 @@ import { toRaw } from 'vue'; import { Store } from '@vben-core/shared/store'; import { bindMethods, - createMerge, isFunction, + mergeWithArrayOverride, StateHandler, } from '@vben-core/shared/utils'; import { objectPick } from '@vueuse/core'; -const merge = createMerge((originObj, key, updates) => { - if (Array.isArray(originObj[key]) && Array.isArray(updates)) { - originObj[key] = updates; - return true; - } -}); - function getDefaultState(): VbenFormProps { return { actionWrapperClass: '', @@ -218,10 +211,10 @@ export class FormApi { ) { if (isFunction(stateOrFn)) { this.store.setState((prev) => { - return merge(stateOrFn(prev), prev); + return mergeWithArrayOverride(stateOrFn(prev), prev); }); } else { - this.store.setState((prev) => merge(stateOrFn, prev)); + this.store.setState((prev) => mergeWithArrayOverride(stateOrFn, prev)); } } @@ -287,7 +280,10 @@ export class FormApi { currentSchema.forEach((schema, index) => { const updatedData = updatedMap[schema.fieldName]; if (updatedData) { - currentSchema[index] = merge(updatedData, schema) as FormSchema; + currentSchema[index] = mergeWithArrayOverride( + updatedData, + schema, + ) as FormSchema; } }); this.setState({ schema: currentSchema });