feat: 偏好设置小部件支持位置选择与拖拽排序
- 顶栏各小部件按钮支持选择位置(header/user-dropdown/none),偏好设置按钮额外保留 auto/fixed 智能兜底,移动端/全屏内容模式下自动切 fixed 浮动保命,避免偏好入口死锁 - draggable-list.vue 的 listRef 未在模板绑定,Sortable 永不初始化导致小部件无法拖拽;加 ref 绑定与 data-key 让 onEnd 从 DOM 读真实新顺序 - preferences.ts 的 updatePreferences 用 defu(merge) 合并数组,对 widget.order 是 concat 语义,每次拖拽触发更新都会把当前 order 追加一次,localStorage 里膨胀到上百条;改用 mergeWithArrayOverride 整体替换,加载缓存时 sanitizeCachedArray 去重已存在的脏数据 - preferences 按钮之前在 widget.order 里因 labelMap 未登记显示成裸字符串,且 widgetChecks 缺 refresh 导致刷新按钮不按 order 渲染;统一 preferences 与其它 widget 的拖拽排序与位置 select,refresh 补进 rightSlots.widgetChecks 并补模板渲染 - 补 sortablejs 依赖、WidgetPreferences.order 类型字段、preferences-drawer.vue 的 widgetOrder 绑定
This commit is contained in:
@@ -121,71 +121,77 @@ type SlotItem = { index: number; name: string };
|
||||
|
||||
const rightSlots = computed(() => {
|
||||
const list: Array<SlotItem> = [];
|
||||
// 全局搜索
|
||||
if (preferences.widget.globalSearch) {
|
||||
list.push({
|
||||
index: REFERENCE_VALUE,
|
||||
name: 'global-search',
|
||||
});
|
||||
}
|
||||
// 偏好设置快捷功能
|
||||
if (preferencesButtonPosition.value.header) {
|
||||
list.push({
|
||||
index: REFERENCE_VALUE + 10,
|
||||
name: 'preferences',
|
||||
});
|
||||
}
|
||||
// 主题、语言、时区等子功能由各自的 widget 开关独立控制,
|
||||
// 不应跟随偏好设置按钮的显示与否(如 enablePreferences 为 false 或按钮位于其他位置时)
|
||||
if (preferences.widget.themeToggle) {
|
||||
list.push({
|
||||
index: REFERENCE_VALUE + 20,
|
||||
name: 'theme-toggle',
|
||||
});
|
||||
}
|
||||
if (preferences.widget.languageToggle) {
|
||||
list.push({
|
||||
index: REFERENCE_VALUE + 30,
|
||||
name: 'language-toggle',
|
||||
});
|
||||
}
|
||||
if (preferences.widget.timezone) {
|
||||
list.push({
|
||||
index: REFERENCE_VALUE + 40,
|
||||
name: 'timezone',
|
||||
});
|
||||
}
|
||||
// 全屏
|
||||
if (preferences.widget.fullscreen) {
|
||||
list.push({
|
||||
index: REFERENCE_VALUE + 50,
|
||||
name: 'fullscreen',
|
||||
});
|
||||
}
|
||||
// 消息通知
|
||||
if (preferences.widget.notification) {
|
||||
list.push({
|
||||
index: REFERENCE_VALUE + 60,
|
||||
name: 'notification',
|
||||
});
|
||||
}
|
||||
// 锁定屏幕
|
||||
if (preferences.widget.lockScreen) {
|
||||
list.push({
|
||||
index: REFERENCE_VALUE + 70,
|
||||
name: 'lock-screen-btn',
|
||||
});
|
||||
}
|
||||
// 退出登录
|
||||
list.push({
|
||||
index: REFERENCE_VALUE + 80,
|
||||
name: 'logout-btn',
|
||||
});
|
||||
|
||||
// 按 widget.order 顺序遍历,检查每个部件是否应在 header 中显示
|
||||
const widgetChecks: Record<string, { slotName: string; visible: boolean }> = {
|
||||
globalSearch: {
|
||||
visible:
|
||||
preferences.widget.globalSearch &&
|
||||
preferences.widget.globalSearchButtonPosition === 'header',
|
||||
slotName: 'global-search',
|
||||
},
|
||||
preferences: {
|
||||
visible: preferencesButtonPosition.value.header,
|
||||
slotName: 'preferences',
|
||||
},
|
||||
themeToggle: {
|
||||
visible:
|
||||
preferences.widget.themeToggle &&
|
||||
preferences.widget.themeToggleButtonPosition === 'header',
|
||||
slotName: 'theme-toggle',
|
||||
},
|
||||
languageToggle: {
|
||||
visible:
|
||||
preferences.widget.languageToggle &&
|
||||
preferences.widget.languageToggleButtonPosition === 'header',
|
||||
slotName: 'language-toggle',
|
||||
},
|
||||
timezone: {
|
||||
visible:
|
||||
preferences.widget.timezone &&
|
||||
preferences.widget.timezoneButtonPosition === 'header',
|
||||
slotName: 'timezone',
|
||||
},
|
||||
fullscreen: {
|
||||
visible:
|
||||
preferences.widget.fullscreen &&
|
||||
preferences.widget.fullscreenButtonPosition === 'header',
|
||||
slotName: 'fullscreen',
|
||||
},
|
||||
notification: {
|
||||
visible:
|
||||
preferences.widget.notification &&
|
||||
preferences.widget.notificationButtonPosition === 'header',
|
||||
slotName: 'notification',
|
||||
},
|
||||
lockScreenBtn: {
|
||||
visible:
|
||||
preferences.widget.lockScreen &&
|
||||
preferences.widget.lockScreenButtonPosition === 'header',
|
||||
slotName: 'lock-screen-btn',
|
||||
},
|
||||
logoutBtn: {
|
||||
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) {
|
||||
const check = widgetChecks[key];
|
||||
if (check?.visible) {
|
||||
list.push({ index: REFERENCE_VALUE + list.length, name: check.slotName });
|
||||
}
|
||||
}
|
||||
|
||||
// 用户插槽(header-right-N)追加在后面
|
||||
Object.keys(slots).forEach((key) => {
|
||||
// 适配插槽名称,例如第一个插槽名:header-right-1
|
||||
if (key.startsWith('header-right')) {
|
||||
// 取第三个占位的数字,若是第三个占位不是数字,则自动分配排序索引
|
||||
const slotIndex = Number(key.split('-')[2]);
|
||||
const index = Number.isNaN(slotIndex) ? nextIndex(list) : slotIndex;
|
||||
list.push({ index, name: key });
|
||||
@@ -200,13 +206,6 @@ const rightSlots = computed(() => {
|
||||
|
||||
const leftSlots = computed(() => {
|
||||
const list: Array<SlotItem> = [];
|
||||
// 刷新
|
||||
if (preferences.widget.refresh) {
|
||||
list.push({
|
||||
index: 0,
|
||||
name: 'refresh',
|
||||
});
|
||||
}
|
||||
|
||||
Object.keys(slots).forEach((key) => {
|
||||
// 适配插槽名称,例如第一个插槽名:header-left-1
|
||||
@@ -331,6 +330,15 @@ function clearPreferencesAndLogout() {
|
||||
<LogOut class="size-4" />
|
||||
</VbenIconButton>
|
||||
</template>
|
||||
<template v-else-if="slot.name === 'refresh'">
|
||||
<VbenIconButton
|
||||
class="my-0 mr-1 rounded-md"
|
||||
:tooltip="$t('preferences.widget.refresh')"
|
||||
@click="refresh"
|
||||
>
|
||||
<RotateCw class="size-4" />
|
||||
</VbenIconButton>
|
||||
</template>
|
||||
</slot>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user