mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-15 17:30:13 +08:00
feat: 插件有新版本时置顶显示(可开关) (#7665)
* feat: add pinUpdatesOnTop option to always show plugin updates at top * fix: add missing pinUpdatesOnTop destructuring in InstalledPluginsTab * fix: address AI review suggestions - add localStorage persistence and increase switch width * fix: harden extension preference storage * fix: refine extension preference sorting * fix: simplify extension preference sorting * refactor: simplify extension preference storage access --------- Co-authored-by: Test User <test@test.com> Co-authored-by: 邹永赫 <1259085392@qq.com>
This commit is contained in:
@@ -144,7 +144,8 @@
|
||||
"updated": "Last Updated",
|
||||
"updateStatus": "Update Status",
|
||||
"ascending": "Ascending",
|
||||
"descending": "Descending"
|
||||
"descending": "Descending",
|
||||
"pinUpdatesOnTop": "Pin Updates on Top"
|
||||
},
|
||||
"tags": {
|
||||
"danger": "Danger"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
{
|
||||
"title": "Плагины",
|
||||
"subtitle": "Управление и настройка расширений системы",
|
||||
"tabs": {
|
||||
@@ -143,7 +143,8 @@
|
||||
"updated": "Дате обновления",
|
||||
"updateStatus": "Статусу обновления",
|
||||
"ascending": "По возрастанию",
|
||||
"descending": "По убыванию"
|
||||
"descending": "По убыванию",
|
||||
"pinUpdatesOnTop": "Обновления сверху"
|
||||
},
|
||||
"tags": {
|
||||
"danger": "Опасно"
|
||||
|
||||
@@ -147,7 +147,8 @@
|
||||
"updated": "更新时间",
|
||||
"updateStatus": "更新状态",
|
||||
"ascending": "升序",
|
||||
"descending": "降序"
|
||||
"descending": "降序",
|
||||
"pinUpdatesOnTop": "有更新置顶"
|
||||
},
|
||||
"tags": {
|
||||
"danger": "危险"
|
||||
|
||||
@@ -55,6 +55,7 @@ const {
|
||||
installedStatusFilter,
|
||||
installedSortBy,
|
||||
installedSortOrder,
|
||||
pinUpdatesOnTop,
|
||||
loading_,
|
||||
currentPage,
|
||||
dangerConfirmDialog,
|
||||
@@ -354,6 +355,15 @@ const pinnedPlugins = computed(() => {
|
||||
:show-order="installedSortUsesOrder"
|
||||
@update:order="installedSortOrder = $event"
|
||||
/>
|
||||
<v-switch
|
||||
v-model="pinUpdatesOnTop"
|
||||
:label="tm('sort.pinUpdatesOnTop')"
|
||||
color="primary"
|
||||
density="compact"
|
||||
hide-details
|
||||
class="ml-4"
|
||||
style="max-width: 200px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
|
||||
84
dashboard/src/views/extension/extensionPreferenceStorage.mjs
Normal file
84
dashboard/src/views/extension/extensionPreferenceStorage.mjs
Normal file
@@ -0,0 +1,84 @@
|
||||
export const SHOW_RESERVED_PLUGINS_STORAGE_KEY = "showReservedPlugins";
|
||||
export const PLUGIN_LIST_VIEW_MODE_STORAGE_KEY = "pluginListViewMode";
|
||||
export const PIN_UPDATES_ON_TOP_STORAGE_KEY = "pinUpdatesOnTop";
|
||||
|
||||
/**
|
||||
* Resolve the storage backend for reading preferences.
|
||||
* Pass `null` to explicitly disable storage access in callers/tests.
|
||||
*/
|
||||
const getStorageForRead = (storageOverride) => {
|
||||
if (storageOverride === null) {
|
||||
return null;
|
||||
}
|
||||
if (storageOverride !== undefined) {
|
||||
return typeof storageOverride?.getItem === "function"
|
||||
? storageOverride
|
||||
: null;
|
||||
}
|
||||
if (typeof window === "undefined") {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const localStorage = window.localStorage ?? null;
|
||||
return typeof localStorage?.getItem === "function" ? localStorage : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolve the storage backend for writing preferences.
|
||||
* Pass `null` to explicitly disable storage access in callers/tests.
|
||||
*/
|
||||
const getStorageForWrite = (storageOverride) => {
|
||||
if (storageOverride === null) {
|
||||
return null;
|
||||
}
|
||||
if (storageOverride !== undefined) {
|
||||
return typeof storageOverride?.setItem === "function"
|
||||
? storageOverride
|
||||
: null;
|
||||
}
|
||||
if (typeof window === "undefined") {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const localStorage = window.localStorage ?? null;
|
||||
return typeof localStorage?.setItem === "function" ? localStorage : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const readBooleanPreference = (key, fallback, storage) => {
|
||||
const targetStorage = getStorageForRead(storage);
|
||||
if (!targetStorage) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
try {
|
||||
const saved = targetStorage.getItem(key);
|
||||
if (saved === "true") {
|
||||
return true;
|
||||
}
|
||||
if (saved === "false") {
|
||||
return false;
|
||||
}
|
||||
return fallback;
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
};
|
||||
|
||||
export const writeBooleanPreference = (key, value, storage) => {
|
||||
const targetStorage = getStorageForWrite(storage);
|
||||
if (!targetStorage) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
targetStorage.setItem(key, String(value));
|
||||
} catch {
|
||||
// Ignore restricted storage environments.
|
||||
}
|
||||
};
|
||||
@@ -14,6 +14,13 @@ import {
|
||||
getValidHashTab,
|
||||
replaceTabRoute,
|
||||
} from "@/utils/hashRouteTabs.mjs";
|
||||
import {
|
||||
PIN_UPDATES_ON_TOP_STORAGE_KEY,
|
||||
PLUGIN_LIST_VIEW_MODE_STORAGE_KEY,
|
||||
SHOW_RESERVED_PLUGINS_STORAGE_KEY,
|
||||
readBooleanPreference,
|
||||
writeBooleanPreference,
|
||||
} from "./extensionPreferenceStorage.mjs";
|
||||
import { ref, computed, onMounted, onUnmounted, reactive, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { useDisplay } from "vuetify";
|
||||
@@ -124,11 +131,7 @@ export const useExtensionPage = () => {
|
||||
|
||||
// 从 localStorage 恢复显示系统插件的状态,默认为 false(隐藏)
|
||||
const getInitialShowReserved = () => {
|
||||
if (typeof window !== "undefined" && window.localStorage) {
|
||||
const saved = localStorage.getItem("showReservedPlugins");
|
||||
return saved === "true";
|
||||
}
|
||||
return false;
|
||||
return readBooleanPreference(SHOW_RESERVED_PLUGINS_STORAGE_KEY, false);
|
||||
};
|
||||
const showReserved = ref(getInitialShowReserved());
|
||||
const snack_message = ref("");
|
||||
@@ -178,16 +181,20 @@ export const useExtensionPage = () => {
|
||||
// 新增变量支持列表视图
|
||||
// 从 localStorage 恢复显示模式,默认为 false(卡片视图)
|
||||
const getInitialListViewMode = () => {
|
||||
if (typeof window !== "undefined" && window.localStorage) {
|
||||
return localStorage.getItem("pluginListViewMode") === "true";
|
||||
}
|
||||
return false;
|
||||
return readBooleanPreference(PLUGIN_LIST_VIEW_MODE_STORAGE_KEY, false);
|
||||
};
|
||||
const isListView = ref(getInitialListViewMode());
|
||||
const pluginSearch = ref("");
|
||||
const installedStatusFilter = ref("all");
|
||||
const installedSortBy = ref("default");
|
||||
const installedSortOrder = ref("desc");
|
||||
const getInitialPinUpdatesOnTop = () => {
|
||||
return readBooleanPreference(PIN_UPDATES_ON_TOP_STORAGE_KEY, true);
|
||||
};
|
||||
const pinUpdatesOnTop = ref(getInitialPinUpdatesOnTop());
|
||||
watch(pinUpdatesOnTop, (val) => {
|
||||
writeBooleanPreference(PIN_UPDATES_ON_TOP_STORAGE_KEY, val);
|
||||
});
|
||||
const loading_ = ref(false);
|
||||
|
||||
// 分页相关
|
||||
@@ -426,6 +433,17 @@ export const useExtensionPage = () => {
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
};
|
||||
|
||||
const compareInstalledFallback = (left, right) => {
|
||||
const nameCompare = compareInstalledPluginNames(left.plugin, right.plugin);
|
||||
return nameCompare !== 0 ? nameCompare : left.index - right.index;
|
||||
};
|
||||
|
||||
const compareInstalledUpdatePinning = (left, right) => {
|
||||
const leftHasUpdate = left.plugin?.has_update ? 1 : 0;
|
||||
const rightHasUpdate = right.plugin?.has_update ? 1 : 0;
|
||||
return rightHasUpdate - leftHasUpdate;
|
||||
};
|
||||
|
||||
const sortInstalledPlugins = (plugins) => {
|
||||
return plugins
|
||||
.map((plugin, index) => ({
|
||||
@@ -434,19 +452,24 @@ export const useExtensionPage = () => {
|
||||
installedAtTimestamp: getInstalledAtTimestamp(plugin),
|
||||
}))
|
||||
.sort((left, right) => {
|
||||
const fallbackNameCompare = compareInstalledPluginNames(
|
||||
left.plugin,
|
||||
right.plugin,
|
||||
);
|
||||
const fallbackResult =
|
||||
fallbackNameCompare !== 0 ? fallbackNameCompare : left.index - right.index;
|
||||
if (
|
||||
pinUpdatesOnTop.value &&
|
||||
installedSortBy.value !== "update_status"
|
||||
) {
|
||||
// Pinning updates is a primary grouping; the selected sort order still
|
||||
// applies within the "has update" and "no update" groups below.
|
||||
const pinCompare = compareInstalledUpdatePinning(left, right);
|
||||
if (pinCompare !== 0) {
|
||||
return pinCompare;
|
||||
}
|
||||
}
|
||||
|
||||
if (installedSortBy.value === "install_time") {
|
||||
const leftTimestamp = left.installedAtTimestamp;
|
||||
const rightTimestamp = right.installedAtTimestamp;
|
||||
|
||||
if (leftTimestamp == null && rightTimestamp == null) {
|
||||
return fallbackResult;
|
||||
return compareInstalledFallback(left, right);
|
||||
}
|
||||
if (leftTimestamp == null) {
|
||||
return 1;
|
||||
@@ -459,7 +482,9 @@ export const useExtensionPage = () => {
|
||||
installedSortOrder.value === "desc"
|
||||
? rightTimestamp - leftTimestamp
|
||||
: leftTimestamp - rightTimestamp;
|
||||
return timeDiff !== 0 ? timeDiff : fallbackResult;
|
||||
return timeDiff !== 0
|
||||
? timeDiff
|
||||
: compareInstalledFallback(left, right);
|
||||
}
|
||||
|
||||
if (installedSortBy.value === "name") {
|
||||
@@ -469,7 +494,7 @@ export const useExtensionPage = () => {
|
||||
? -nameCompare
|
||||
: nameCompare;
|
||||
}
|
||||
return left.index - right.index;
|
||||
return compareInstalledFallback(left, right);
|
||||
}
|
||||
|
||||
if (installedSortBy.value === "author") {
|
||||
@@ -482,20 +507,20 @@ export const useExtensionPage = () => {
|
||||
? -authorCompare
|
||||
: authorCompare;
|
||||
}
|
||||
return fallbackResult;
|
||||
return compareInstalledFallback(left, right);
|
||||
}
|
||||
|
||||
if (installedSortBy.value === "update_status") {
|
||||
const leftHasUpdate = left.plugin?.has_update ? 1 : 0;
|
||||
const rightHasUpdate = right.plugin?.has_update ? 1 : 0;
|
||||
const updateDiff =
|
||||
installedSortOrder.value === "desc"
|
||||
? rightHasUpdate - leftHasUpdate
|
||||
: leftHasUpdate - rightHasUpdate;
|
||||
return updateDiff !== 0 ? updateDiff : fallbackResult;
|
||||
? compareInstalledUpdatePinning(left, right)
|
||||
: compareInstalledUpdatePinning(right, left);
|
||||
return updateDiff !== 0
|
||||
? updateDiff
|
||||
: compareInstalledFallback(left, right);
|
||||
}
|
||||
|
||||
return fallbackResult;
|
||||
return compareInstalledFallback(left, right);
|
||||
})
|
||||
.map((item) => item.plugin);
|
||||
};
|
||||
@@ -636,9 +661,7 @@ export const useExtensionPage = () => {
|
||||
const toggleShowReserved = () => {
|
||||
showReserved.value = !showReserved.value;
|
||||
// 保存到 localStorage
|
||||
if (typeof window !== "undefined" && window.localStorage) {
|
||||
localStorage.setItem("showReservedPlugins", showReserved.value.toString());
|
||||
}
|
||||
writeBooleanPreference(SHOW_RESERVED_PLUGINS_STORAGE_KEY, showReserved.value);
|
||||
};
|
||||
|
||||
const toast = (message, success) => {
|
||||
@@ -1603,9 +1626,7 @@ export const useExtensionPage = () => {
|
||||
|
||||
// 监听显示模式变化并保存到 localStorage
|
||||
watch(isListView, (newVal) => {
|
||||
if (typeof window !== "undefined" && window.localStorage) {
|
||||
localStorage.setItem("pluginListViewMode", String(newVal));
|
||||
}
|
||||
writeBooleanPreference(PLUGIN_LIST_VIEW_MODE_STORAGE_KEY, newVal);
|
||||
});
|
||||
|
||||
watch(
|
||||
@@ -1695,6 +1716,7 @@ export const useExtensionPage = () => {
|
||||
installedStatusFilter,
|
||||
installedSortBy,
|
||||
installedSortOrder,
|
||||
pinUpdatesOnTop,
|
||||
loading_,
|
||||
currentPage,
|
||||
marketCategoryFilter,
|
||||
|
||||
75
dashboard/tests/extensionPreferenceStorage.test.mjs
Normal file
75
dashboard/tests/extensionPreferenceStorage.test.mjs
Normal file
@@ -0,0 +1,75 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
import {
|
||||
PIN_UPDATES_ON_TOP_STORAGE_KEY,
|
||||
readBooleanPreference,
|
||||
writeBooleanPreference,
|
||||
} from '../src/views/extension/extensionPreferenceStorage.mjs';
|
||||
|
||||
test("readBooleanPreference returns fallback when storage access throws", () => {
|
||||
const storage = {
|
||||
getItem() {
|
||||
throw new Error("SecurityError");
|
||||
},
|
||||
};
|
||||
|
||||
assert.equal(
|
||||
readBooleanPreference(PIN_UPDATES_ON_TOP_STORAGE_KEY, true, storage),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("readBooleanPreference parses stored boolean strings", () => {
|
||||
const storage = {
|
||||
getItem(key) {
|
||||
return key === PIN_UPDATES_ON_TOP_STORAGE_KEY ? "false" : null;
|
||||
},
|
||||
};
|
||||
|
||||
assert.equal(
|
||||
readBooleanPreference(PIN_UPDATES_ON_TOP_STORAGE_KEY, true, storage),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("readBooleanPreference treats explicit null storage as unavailable", () => {
|
||||
assert.equal(
|
||||
readBooleanPreference(PIN_UPDATES_ON_TOP_STORAGE_KEY, true, null),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("readBooleanPreference treats invalid storage overrides as unavailable", () => {
|
||||
assert.equal(
|
||||
readBooleanPreference(PIN_UPDATES_ON_TOP_STORAGE_KEY, true, {}),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("writeBooleanPreference stores boolean strings and swallows storage errors", () => {
|
||||
const writes = [];
|
||||
const storage = {
|
||||
setItem(key, value) {
|
||||
writes.push([key, value]);
|
||||
throw new Error("QuotaExceededError");
|
||||
},
|
||||
};
|
||||
|
||||
assert.doesNotThrow(() =>
|
||||
writeBooleanPreference(PIN_UPDATES_ON_TOP_STORAGE_KEY, true, storage),
|
||||
);
|
||||
assert.deepEqual(writes, [[PIN_UPDATES_ON_TOP_STORAGE_KEY, "true"]]);
|
||||
});
|
||||
|
||||
test("writeBooleanPreference ignores explicit null storage", () => {
|
||||
assert.doesNotThrow(() =>
|
||||
writeBooleanPreference(PIN_UPDATES_ON_TOP_STORAGE_KEY, true, null),
|
||||
);
|
||||
});
|
||||
|
||||
test("writeBooleanPreference ignores invalid storage overrides", () => {
|
||||
assert.doesNotThrow(() =>
|
||||
writeBooleanPreference(PIN_UPDATES_ON_TOP_STORAGE_KEY, true, {}),
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user