fix: 修复点击退出图标确认后不能退出 && 图标排序等数组字段需顺序敏感比较 (#8179)
* chore: 调整 sortablejs 的位置,只在用到的模块引入 * fix: non-null assertion lint error * fix: 修复点击退出图标确认后不能退出 * fix: 图标排序等数组字段需顺序敏感比较 * feat: 配置中增加 refresh 并统一位置
This commit is contained in:
@@ -70,7 +70,6 @@
|
|||||||
"@changesets/cli": "catalog:",
|
"@changesets/cli": "catalog:",
|
||||||
"@tsdown/css": "catalog:",
|
"@tsdown/css": "catalog:",
|
||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
"@types/sortablejs": "catalog:",
|
|
||||||
"@vben/commitlint-config": "workspace:*",
|
"@vben/commitlint-config": "workspace:*",
|
||||||
"@vben/eslint-config": "workspace:*",
|
"@vben/eslint-config": "workspace:*",
|
||||||
"@vben/oxfmt-config": "workspace:*",
|
"@vben/oxfmt-config": "workspace:*",
|
||||||
@@ -107,8 +106,5 @@
|
|||||||
"node": "^22.18.0 || ^24.12.0",
|
"node": "^22.18.0 || ^24.12.0",
|
||||||
"pnpm": ">=11.0.0"
|
"pnpm": ">=11.0.0"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@11.15.1",
|
"packageManager": "pnpm@11.15.1"
|
||||||
"dependencies": {
|
|
||||||
"sortablejs": "catalog:"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
import { diff } from '../diff';
|
import { diff, diffStrict } from '../diff';
|
||||||
|
|
||||||
describe('diff function', () => {
|
describe('diff function', () => {
|
||||||
it('should return an empty object when comparing identical objects', () => {
|
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] });
|
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', () => {
|
it('should handle added keys', () => {
|
||||||
const obj1 = { a: 1 };
|
const obj1 = { a: 1 };
|
||||||
const obj2 = { a: 1, b: 2 };
|
const obj2 = { a: 1, b: 2 };
|
||||||
@@ -51,3 +57,31 @@ describe('diff function', () => {
|
|||||||
expect(diff(obj1, obj2)).toEqual({ a: 1 });
|
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 } });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// type Diff<T = any> = T;
|
// type Diff<T = any> = T;
|
||||||
|
|
||||||
// 比较两个数组是否相等
|
// 比较两个数组是否相等(忽略顺序)
|
||||||
|
|
||||||
function arraysEqual<T>(a: T[], b: T[]): boolean {
|
function arraysEqual<T>(a: T[], b: T[]): boolean {
|
||||||
if (a.length !== b.length) return false;
|
if (a.length !== b.length) return false;
|
||||||
@@ -18,6 +18,11 @@ function arraysEqual<T>(a: T[], b: T[]): boolean {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 比较两个数组是否相等(顺序敏感)
|
||||||
|
function arraysStrictEqual<T>(a: T[], b: T[]): boolean {
|
||||||
|
return a.length === b.length && a.every((value, index) => value === b[index]);
|
||||||
|
}
|
||||||
|
|
||||||
// 深度对比两个值
|
// 深度对比两个值
|
||||||
// function deepEqual<T>(oldVal: T, newVal: T): boolean {
|
// function deepEqual<T>(oldVal: T, newVal: T): boolean {
|
||||||
// if (
|
// if (
|
||||||
@@ -59,38 +64,51 @@ type DiffResult<T> = Partial<{
|
|||||||
[K in keyof T]: T[K] extends object ? DiffResult<T[K]> : T[K];
|
[K in keyof T]: T[K] extends object ? DiffResult<T[K]> : T[K];
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
function diff<T extends Record<string, any>>(obj1: T, obj2: T): DiffResult<T> {
|
type ArrayComparator = (a: any[], b: any[]) => boolean;
|
||||||
function findDifferences(o1: any, o2: any): any {
|
|
||||||
if (Array.isArray(o1) && Array.isArray(o2)) {
|
|
||||||
if (!arraysEqual(o1, o2)) {
|
|
||||||
return o2;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
function createDiff(arrayEquals: ArrayComparator) {
|
||||||
typeof o1 === 'object' &&
|
return function <T extends Record<string, any>>(
|
||||||
typeof o2 === 'object' &&
|
obj1: T,
|
||||||
o1 !== null &&
|
obj2: T,
|
||||||
o2 !== null
|
): DiffResult<T> {
|
||||||
) {
|
function findDifferences(o1: any, o2: any): any {
|
||||||
const diffResult: any = {};
|
if (Array.isArray(o1) && Array.isArray(o2)) {
|
||||||
|
if (!arrayEquals(o1, o2)) {
|
||||||
const keys = new Set([...Object.keys(o1), ...Object.keys(o2)]);
|
return o2;
|
||||||
keys.forEach((key) => {
|
|
||||||
const valueDiff = findDifferences(o1[key], o2[key]);
|
|
||||||
if (valueDiff !== undefined) {
|
|
||||||
diffResult[key] = valueDiff;
|
|
||||||
}
|
}
|
||||||
});
|
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 };
|
||||||
|
|||||||
@@ -146,6 +146,18 @@ exports[`defaultPreferences immutability test > should not modify the config obj
|
|||||||
"logoutButtonPosition": "header",
|
"logoutButtonPosition": "header",
|
||||||
"notification": true,
|
"notification": true,
|
||||||
"notificationButtonPosition": "header",
|
"notificationButtonPosition": "header",
|
||||||
|
"order": [
|
||||||
|
"globalSearch",
|
||||||
|
"preferences",
|
||||||
|
"themeToggle",
|
||||||
|
"languageToggle",
|
||||||
|
"timezone",
|
||||||
|
"fullscreen",
|
||||||
|
"refresh",
|
||||||
|
"notification",
|
||||||
|
"lockScreenBtn",
|
||||||
|
"logoutBtn",
|
||||||
|
],
|
||||||
"refresh": true,
|
"refresh": true,
|
||||||
"refreshButtonPosition": "header",
|
"refreshButtonPosition": "header",
|
||||||
"sidebarToggle": true,
|
"sidebarToggle": true,
|
||||||
|
|||||||
@@ -161,6 +161,7 @@ const defaultPreferences: Preferences = {
|
|||||||
'languageToggle',
|
'languageToggle',
|
||||||
'timezone',
|
'timezone',
|
||||||
'fullscreen',
|
'fullscreen',
|
||||||
|
'refresh',
|
||||||
'notification',
|
'notification',
|
||||||
'lockScreenBtn',
|
'lockScreenBtn',
|
||||||
'logoutBtn',
|
'logoutBtn',
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
import { diff } from '@vben-core/shared/utils';
|
import { diff, diffStrict } from '@vben-core/shared/utils';
|
||||||
|
|
||||||
import { preferencesManager } from './preferences';
|
import { preferencesManager } from './preferences';
|
||||||
import { isDarkTheme } from './update-css-variables';
|
import { isDarkTheme } from './update-css-variables';
|
||||||
@@ -16,9 +16,10 @@ function usePreferences() {
|
|||||||
);
|
);
|
||||||
/**
|
/**
|
||||||
* @zh_CN 计算偏好设置的变化
|
* @zh_CN 计算偏好设置的变化
|
||||||
|
* @zh_CN 使用 diffStrict:图标排序等数组字段需顺序敏感比较
|
||||||
*/
|
*/
|
||||||
const diffPreference = computed(() => {
|
const diffPreference = computed(() => {
|
||||||
return diff(initialPreferences, preferences);
|
return diffStrict(initialPreferences, preferences);
|
||||||
});
|
});
|
||||||
|
|
||||||
const diffCustomPreference = computed(() => {
|
const diffCustomPreference = computed(() => {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@types/sortablejs": "catalog:",
|
||||||
"@vben-core/composables": "workspace:*",
|
"@vben-core/composables": "workspace:*",
|
||||||
"@vben-core/design": "workspace:*",
|
"@vben-core/design": "workspace:*",
|
||||||
"@vben-core/form-ui": "workspace:*",
|
"@vben-core/form-ui": "workspace:*",
|
||||||
@@ -38,6 +39,7 @@
|
|||||||
"@vben/types": "workspace:*",
|
"@vben/types": "workspace:*",
|
||||||
"@vben/utils": "workspace:*",
|
"@vben/utils": "workspace:*",
|
||||||
"@vueuse/core": "catalog:",
|
"@vueuse/core": "catalog:",
|
||||||
|
"sortablejs": "catalog:",
|
||||||
"vue": "catalog:",
|
"vue": "catalog:",
|
||||||
"vue-router": "catalog:"
|
"vue-router": "catalog:"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -163,6 +163,12 @@ const rightSlots = computed(() => {
|
|||||||
preferences.widget.fullscreenButtonPosition === 'header',
|
preferences.widget.fullscreenButtonPosition === 'header',
|
||||||
slotName: 'fullscreen',
|
slotName: 'fullscreen',
|
||||||
},
|
},
|
||||||
|
refresh: {
|
||||||
|
visible:
|
||||||
|
preferences.widget.refresh &&
|
||||||
|
preferences.widget.refreshButtonPosition === 'header',
|
||||||
|
slotName: 'refresh',
|
||||||
|
},
|
||||||
notification: {
|
notification: {
|
||||||
visible:
|
visible:
|
||||||
preferences.widget.notification &&
|
preferences.widget.notification &&
|
||||||
@@ -179,12 +185,6 @@ const rightSlots = computed(() => {
|
|||||||
visible: preferences.widget.logoutButtonPosition === 'header',
|
visible: preferences.widget.logoutButtonPosition === 'header',
|
||||||
slotName: 'logout-btn',
|
slotName: 'logout-btn',
|
||||||
},
|
},
|
||||||
refresh: {
|
|
||||||
visible:
|
|
||||||
preferences.widget.refresh &&
|
|
||||||
preferences.widget.refreshButtonPosition === 'header',
|
|
||||||
slotName: 'refresh',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const key of preferences.widget.order) {
|
for (const key of preferences.widget.order) {
|
||||||
|
|||||||
@@ -44,15 +44,16 @@ const hiddenList = computed(() =>
|
|||||||
props.items.filter((item) => item.position === 'none'),
|
props.items.filter((item) => item.position === 'none'),
|
||||||
);
|
);
|
||||||
function initSortable() {
|
function initSortable() {
|
||||||
if (!listRef.value) return;
|
const container = listRef.value;
|
||||||
|
if (!container) return;
|
||||||
sortableInstance?.destroy();
|
sortableInstance?.destroy();
|
||||||
sortableInstance = Sortable.create(listRef.value, {
|
sortableInstance = Sortable.create(container, {
|
||||||
animation: 200,
|
animation: 200,
|
||||||
handle: '.drag-handle',
|
handle: '.drag-handle',
|
||||||
onEnd() {
|
onEnd() {
|
||||||
// Sortable 已经改了 DOM,但 sortableList computed 还是旧顺序。
|
// Sortable 已经改了 DOM,但 sortableList computed 还是旧顺序。
|
||||||
// 直接从 DOM 读 children 的 data-key 拿新顺序,再追加 hidden 部分。
|
// 直接从 DOM 读 children 的 data-key 拿新顺序,再追加 hidden 部分。
|
||||||
const newOrder = [...listRef.value!.children]
|
const newOrder = [...container.children]
|
||||||
.map((el) => (el as HTMLElement).dataset.key)
|
.map((el) => (el as HTMLElement).dataset.key)
|
||||||
.filter(Boolean) as string[];
|
.filter(Boolean) as string[];
|
||||||
emit('updateOrder', [...newOrder, ...hiddenList.value.map((i) => i.key)]);
|
emit('updateOrder', [...newOrder, ...hiddenList.value.map((i) => i.key)]);
|
||||||
|
|||||||
@@ -107,10 +107,10 @@ const labelMap: Record<string, string> = {
|
|||||||
languageToggle: 'preferences.widget.languageToggle',
|
languageToggle: 'preferences.widget.languageToggle',
|
||||||
timezone: 'preferences.widget.timezone',
|
timezone: 'preferences.widget.timezone',
|
||||||
fullscreen: 'preferences.widget.fullscreen',
|
fullscreen: 'preferences.widget.fullscreen',
|
||||||
|
refresh: 'preferences.widget.refresh',
|
||||||
notification: 'preferences.widget.notification',
|
notification: 'preferences.widget.notification',
|
||||||
lockScreenBtn: 'ui.widgets.lockScreen.title',
|
lockScreenBtn: 'ui.widgets.lockScreen.title',
|
||||||
logoutBtn: 'common.logout',
|
logoutBtn: 'common.logout',
|
||||||
refresh: 'preferences.widget.refresh',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const draggableItems = computed(() =>
|
const draggableItems = computed(() =>
|
||||||
|
|||||||
@@ -239,8 +239,11 @@ onBeforeMount(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<BasicLayout
|
<BasicLayout
|
||||||
|
:avatar
|
||||||
|
:text="userStore.userInfo?.realName"
|
||||||
@clear-preferences-and-logout="handleLogout"
|
@clear-preferences-and-logout="handleLogout"
|
||||||
@click-logo="handleClickLogo"
|
@click-logo="handleClickLogo"
|
||||||
|
@logout="handleLogout"
|
||||||
>
|
>
|
||||||
<template #user-dropdown>
|
<template #user-dropdown>
|
||||||
<UserDropdown
|
<UserDropdown
|
||||||
|
|||||||
1096
pnpm-lock.yaml
generated
1096
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user