From 6b6708bcf28aa9dd87c53c879c7dfd73708c915d Mon Sep 17 00:00:00 2001 From: xingyu Date: Wed, 22 Jul 2026 17:44:10 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E7=82=B9=E5=87=BB?= =?UTF-8?q?=E9=80=80=E5=87=BA=E5=9B=BE=E6=A0=87=E7=A1=AE=E8=AE=A4=E5=90=8E?= =?UTF-8?q?=E4=B8=8D=E8=83=BD=E9=80=80=E5=87=BA=20&&=20=E5=9B=BE=E6=A0=87?= =?UTF-8?q?=E6=8E=92=E5=BA=8F=E7=AD=89=E6=95=B0=E7=BB=84=E5=AD=97=E6=AE=B5?= =?UTF-8?q?=E9=9C=80=E9=A1=BA=E5=BA=8F=E6=95=8F=E6=84=9F=E6=AF=94=E8=BE=83?= =?UTF-8?q?=20(#8179)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: 调整 sortablejs 的位置,只在用到的模块引入 * fix: non-null assertion lint error * fix: 修复点击退出图标确认后不能退出 * fix: 图标排序等数组字段需顺序敏感比较 * feat: 配置中增加 refresh 并统一位置 --- package.json | 6 +- .../shared/src/utils/__tests__/diff.test.ts | 36 +- packages/@core/base/shared/src/utils/diff.ts | 76 +- .../__snapshots__/config.test.ts.snap | 12 + packages/@core/preferences/src/config.ts | 1 + .../@core/preferences/src/use-preferences.ts | 5 +- packages/effects/layouts/package.json | 2 + .../layouts/src/basic/header/header.vue | 12 +- .../preferences/blocks/draggable-list.vue | 7 +- .../preferences/blocks/layout/widget.vue | 2 +- playground/src/layouts/basic.vue | 3 + pnpm-lock.yaml | 1096 ++++++++--------- 12 files changed, 663 insertions(+), 595 deletions(-) diff --git a/package.json b/package.json index 65ff86f4..3ecf8b37 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,6 @@ "@changesets/cli": "catalog:", "@tsdown/css": "catalog:", "@types/node": "catalog:", - "@types/sortablejs": "catalog:", "@vben/commitlint-config": "workspace:*", "@vben/eslint-config": "workspace:*", "@vben/oxfmt-config": "workspace:*", @@ -107,8 +106,5 @@ "node": "^22.18.0 || ^24.12.0", "pnpm": ">=11.0.0" }, - "packageManager": "pnpm@11.15.1", - "dependencies": { - "sortablejs": "catalog:" - } + "packageManager": "pnpm@11.15.1" } diff --git a/packages/@core/base/shared/src/utils/__tests__/diff.test.ts b/packages/@core/base/shared/src/utils/__tests__/diff.test.ts index cb3227b5..edcaa050 100644 --- a/packages/@core/base/shared/src/utils/__tests__/diff.test.ts +++ b/packages/@core/base/shared/src/utils/__tests__/diff.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { diff } from '../diff'; +import { diff, diffStrict } from '../diff'; describe('diff function', () => { it('should return an empty object when comparing identical objects', () => { @@ -27,6 +27,12 @@ describe('diff function', () => { expect(diff(obj1, obj2)).toEqual({ a: [1, 2, 4] }); }); + it('should ignore array order changes', () => { + const obj1 = { a: [1, 2, 3] }; + const obj2 = { a: [3, 2, 1] }; + expect(diff(obj1, obj2)).toEqual(undefined); + }); + it('should handle added keys', () => { const obj1 = { a: 1 }; const obj2 = { a: 1, b: 2 }; @@ -51,3 +57,31 @@ describe('diff function', () => { expect(diff(obj1, obj2)).toEqual({ a: 1 }); }); }); + +describe('diffStrict function', () => { + it('should return undefined when comparing identical objects', () => { + const obj1 = { a: 1, b: { c: 2 }, d: [1, 2, 3] }; + const obj2 = { a: 1, b: { c: 2 }, d: [1, 2, 3] }; + expect(diffStrict(obj1, obj2)).toEqual(undefined); + }); + + it('should detect array order changes', () => { + const obj1 = { a: ['search', 'theme', 'logout'] }; + const obj2 = { a: ['logout', 'theme', 'search'] }; + expect(diffStrict(obj1, obj2)).toEqual({ + a: ['logout', 'theme', 'search'], + }); + }); + + it('should detect array element changes', () => { + const obj1 = { a: [1, 2, 3] }; + const obj2 = { a: [1, 2, 4] }; + expect(diffStrict(obj1, obj2)).toEqual({ a: [1, 2, 4] }); + }); + + it('should detect nested object changes', () => { + const obj1 = { a: 1, b: { c: 2, d: 4 } }; + const obj2 = { a: 1, b: { c: 3, d: 4 } }; + expect(diffStrict(obj1, obj2)).toEqual({ b: { c: 3 } }); + }); +}); diff --git a/packages/@core/base/shared/src/utils/diff.ts b/packages/@core/base/shared/src/utils/diff.ts index 449214d7..1b1c8b2c 100644 --- a/packages/@core/base/shared/src/utils/diff.ts +++ b/packages/@core/base/shared/src/utils/diff.ts @@ -1,6 +1,6 @@ // type Diff = T; -// 比较两个数组是否相等 +// 比较两个数组是否相等(忽略顺序) function arraysEqual(a: T[], b: T[]): boolean { if (a.length !== b.length) return false; @@ -18,6 +18,11 @@ function arraysEqual(a: T[], b: T[]): boolean { return true; } +// 比较两个数组是否相等(顺序敏感) +function arraysStrictEqual(a: T[], b: T[]): boolean { + return a.length === b.length && a.every((value, index) => value === b[index]); +} + // 深度对比两个值 // function deepEqual(oldVal: T, newVal: T): boolean { // if ( @@ -59,38 +64,51 @@ type DiffResult = Partial<{ [K in keyof T]: T[K] extends object ? DiffResult : T[K]; }>; -function diff>(obj1: T, obj2: T): DiffResult { - function findDifferences(o1: any, o2: any): any { - if (Array.isArray(o1) && Array.isArray(o2)) { - if (!arraysEqual(o1, o2)) { - return o2; - } - return undefined; - } +type ArrayComparator = (a: any[], b: any[]) => boolean; - if ( - typeof o1 === 'object' && - typeof o2 === 'object' && - o1 !== null && - o2 !== null - ) { - const diffResult: any = {}; - - const keys = new Set([...Object.keys(o1), ...Object.keys(o2)]); - keys.forEach((key) => { - const valueDiff = findDifferences(o1[key], o2[key]); - if (valueDiff !== undefined) { - diffResult[key] = valueDiff; +function createDiff(arrayEquals: ArrayComparator) { + return function >( + obj1: T, + obj2: T, + ): DiffResult { + function findDifferences(o1: any, o2: any): any { + if (Array.isArray(o1) && Array.isArray(o2)) { + if (!arrayEquals(o1, o2)) { + return o2; } - }); + return undefined; + } - return Object.keys(diffResult).length > 0 ? diffResult : undefined; + if ( + typeof o1 === 'object' && + typeof o2 === 'object' && + o1 !== null && + o2 !== null + ) { + const diffResult: any = {}; + + const keys = new Set([...Object.keys(o1), ...Object.keys(o2)]); + keys.forEach((key) => { + const valueDiff = findDifferences(o1[key], o2[key]); + if (valueDiff !== undefined) { + diffResult[key] = valueDiff; + } + }); + + return Object.keys(diffResult).length > 0 ? diffResult : undefined; + } + + return o1 === o2 ? undefined : o2; } - return o1 === o2 ? undefined : o2; - } - - return findDifferences(obj1, obj2); + return findDifferences(obj1, obj2); + }; } -export { arraysEqual, diff }; +// 数组比较(不含顺序) +const diff = createDiff(arraysEqual); + +// 数组比较(含顺序) +const diffStrict = createDiff(arraysStrictEqual); + +export { arraysEqual, arraysStrictEqual, diff, diffStrict }; diff --git a/packages/@core/preferences/__tests__/__snapshots__/config.test.ts.snap b/packages/@core/preferences/__tests__/__snapshots__/config.test.ts.snap index f3ff080c..4a7ff7a0 100644 --- a/packages/@core/preferences/__tests__/__snapshots__/config.test.ts.snap +++ b/packages/@core/preferences/__tests__/__snapshots__/config.test.ts.snap @@ -146,6 +146,18 @@ exports[`defaultPreferences immutability test > should not modify the config obj "logoutButtonPosition": "header", "notification": true, "notificationButtonPosition": "header", + "order": [ + "globalSearch", + "preferences", + "themeToggle", + "languageToggle", + "timezone", + "fullscreen", + "refresh", + "notification", + "lockScreenBtn", + "logoutBtn", + ], "refresh": true, "refreshButtonPosition": "header", "sidebarToggle": true, diff --git a/packages/@core/preferences/src/config.ts b/packages/@core/preferences/src/config.ts index 0c47ad1b..ededeb7e 100644 --- a/packages/@core/preferences/src/config.ts +++ b/packages/@core/preferences/src/config.ts @@ -161,6 +161,7 @@ const defaultPreferences: Preferences = { 'languageToggle', 'timezone', 'fullscreen', + 'refresh', 'notification', 'lockScreenBtn', 'logoutBtn', diff --git a/packages/@core/preferences/src/use-preferences.ts b/packages/@core/preferences/src/use-preferences.ts index 2a075f64..1231efa1 100644 --- a/packages/@core/preferences/src/use-preferences.ts +++ b/packages/@core/preferences/src/use-preferences.ts @@ -1,6 +1,6 @@ import { computed } from 'vue'; -import { diff } from '@vben-core/shared/utils'; +import { diff, diffStrict } from '@vben-core/shared/utils'; import { preferencesManager } from './preferences'; import { isDarkTheme } from './update-css-variables'; @@ -16,9 +16,10 @@ function usePreferences() { ); /** * @zh_CN 计算偏好设置的变化 + * @zh_CN 使用 diffStrict:图标排序等数组字段需顺序敏感比较 */ const diffPreference = computed(() => { - return diff(initialPreferences, preferences); + return diffStrict(initialPreferences, preferences); }); const diffCustomPreference = computed(() => { diff --git a/packages/effects/layouts/package.json b/packages/effects/layouts/package.json index e59c33f3..4a647f24 100644 --- a/packages/effects/layouts/package.json +++ b/packages/effects/layouts/package.json @@ -20,6 +20,7 @@ } }, "dependencies": { + "@types/sortablejs": "catalog:", "@vben-core/composables": "workspace:*", "@vben-core/design": "workspace:*", "@vben-core/form-ui": "workspace:*", @@ -38,6 +39,7 @@ "@vben/types": "workspace:*", "@vben/utils": "workspace:*", "@vueuse/core": "catalog:", + "sortablejs": "catalog:", "vue": "catalog:", "vue-router": "catalog:" } diff --git a/packages/effects/layouts/src/basic/header/header.vue b/packages/effects/layouts/src/basic/header/header.vue index 2db47df5..9df8acb9 100644 --- a/packages/effects/layouts/src/basic/header/header.vue +++ b/packages/effects/layouts/src/basic/header/header.vue @@ -163,6 +163,12 @@ const rightSlots = computed(() => { preferences.widget.fullscreenButtonPosition === 'header', slotName: 'fullscreen', }, + refresh: { + visible: + preferences.widget.refresh && + preferences.widget.refreshButtonPosition === 'header', + slotName: 'refresh', + }, notification: { visible: preferences.widget.notification && @@ -179,12 +185,6 @@ const rightSlots = computed(() => { visible: preferences.widget.logoutButtonPosition === 'header', slotName: 'logout-btn', }, - refresh: { - visible: - preferences.widget.refresh && - preferences.widget.refreshButtonPosition === 'header', - slotName: 'refresh', - }, }; for (const key of preferences.widget.order) { diff --git a/packages/effects/layouts/src/widgets/preferences/blocks/draggable-list.vue b/packages/effects/layouts/src/widgets/preferences/blocks/draggable-list.vue index db85f45f..2af8ddad 100644 --- a/packages/effects/layouts/src/widgets/preferences/blocks/draggable-list.vue +++ b/packages/effects/layouts/src/widgets/preferences/blocks/draggable-list.vue @@ -44,15 +44,16 @@ const hiddenList = computed(() => props.items.filter((item) => item.position === 'none'), ); function initSortable() { - if (!listRef.value) return; + const container = listRef.value; + if (!container) return; sortableInstance?.destroy(); - sortableInstance = Sortable.create(listRef.value, { + sortableInstance = Sortable.create(container, { animation: 200, handle: '.drag-handle', onEnd() { // Sortable 已经改了 DOM,但 sortableList computed 还是旧顺序。 // 直接从 DOM 读 children 的 data-key 拿新顺序,再追加 hidden 部分。 - const newOrder = [...listRef.value!.children] + const newOrder = [...container.children] .map((el) => (el as HTMLElement).dataset.key) .filter(Boolean) as string[]; emit('updateOrder', [...newOrder, ...hiddenList.value.map((i) => i.key)]); diff --git a/packages/effects/layouts/src/widgets/preferences/blocks/layout/widget.vue b/packages/effects/layouts/src/widgets/preferences/blocks/layout/widget.vue index e0b084a9..d3cafc41 100644 --- a/packages/effects/layouts/src/widgets/preferences/blocks/layout/widget.vue +++ b/packages/effects/layouts/src/widgets/preferences/blocks/layout/widget.vue @@ -107,10 +107,10 @@ const labelMap: Record = { languageToggle: 'preferences.widget.languageToggle', timezone: 'preferences.widget.timezone', fullscreen: 'preferences.widget.fullscreen', + refresh: 'preferences.widget.refresh', notification: 'preferences.widget.notification', lockScreenBtn: 'ui.widgets.lockScreen.title', logoutBtn: 'common.logout', - refresh: 'preferences.widget.refresh', }; const draggableItems = computed(() => diff --git a/playground/src/layouts/basic.vue b/playground/src/layouts/basic.vue index 50381b57..1dc0b319 100644 --- a/playground/src/layouts/basic.vue +++ b/playground/src/layouts/basic.vue @@ -239,8 +239,11 @@ onBeforeMount(() => {