Skip to content

Commit fb16e12

Browse files
BlueteemoTest Userzouyonghe
authored
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>
1 parent 76ee4f2 commit fb16e12

File tree

7 files changed

+229
-35
lines changed

7 files changed

+229
-35
lines changed

dashboard/src/i18n/locales/en-US/features/extension.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,8 @@
144144
"updated": "Last Updated",
145145
"updateStatus": "Update Status",
146146
"ascending": "Ascending",
147-
"descending": "Descending"
147+
"descending": "Descending",
148+
"pinUpdatesOnTop": "Pin Updates on Top"
148149
},
149150
"tags": {
150151
"danger": "Danger"

dashboard/src/i18n/locales/ru-RU/features/extension.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
{
1+
{
22
"title": "Плагины",
33
"subtitle": "Управление и настройка расширений системы",
44
"tabs": {
@@ -143,7 +143,8 @@
143143
"updated": "Дате обновления",
144144
"updateStatus": "Статусу обновления",
145145
"ascending": "По возрастанию",
146-
"descending": "По убыванию"
146+
"descending": "По убыванию",
147+
"pinUpdatesOnTop": "Обновления сверху"
147148
},
148149
"tags": {
149150
"danger": "Опасно"

dashboard/src/i18n/locales/zh-CN/features/extension.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,8 @@
147147
"updated": "更新时间",
148148
"updateStatus": "更新状态",
149149
"ascending": "升序",
150-
"descending": "降序"
150+
"descending": "降序",
151+
"pinUpdatesOnTop": "有更新置顶"
151152
},
152153
"tags": {
153154
"danger": "危险"

dashboard/src/views/extension/InstalledPluginsTab.vue

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ const {
5555
installedStatusFilter,
5656
installedSortBy,
5757
installedSortOrder,
58+
pinUpdatesOnTop,
5859
loading_,
5960
currentPage,
6061
dangerConfirmDialog,
@@ -354,6 +355,15 @@ const pinnedPlugins = computed(() => {
354355
:show-order="installedSortUsesOrder"
355356
@update:order="installedSortOrder = $event"
356357
/>
358+
<v-switch
359+
v-model="pinUpdatesOnTop"
360+
:label="tm('sort.pinUpdatesOnTop')"
361+
color="primary"
362+
density="compact"
363+
hide-details
364+
class="ml-4"
365+
style="max-width: 200px"
366+
/>
357367
</div>
358368
</div>
359369
</v-col>
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
export const SHOW_RESERVED_PLUGINS_STORAGE_KEY = "showReservedPlugins";
2+
export const PLUGIN_LIST_VIEW_MODE_STORAGE_KEY = "pluginListViewMode";
3+
export const PIN_UPDATES_ON_TOP_STORAGE_KEY = "pinUpdatesOnTop";
4+
5+
/**
6+
* Resolve the storage backend for reading preferences.
7+
* Pass `null` to explicitly disable storage access in callers/tests.
8+
*/
9+
const getStorageForRead = (storageOverride) => {
10+
if (storageOverride === null) {
11+
return null;
12+
}
13+
if (storageOverride !== undefined) {
14+
return typeof storageOverride?.getItem === "function"
15+
? storageOverride
16+
: null;
17+
}
18+
if (typeof window === "undefined") {
19+
return null;
20+
}
21+
try {
22+
const localStorage = window.localStorage ?? null;
23+
return typeof localStorage?.getItem === "function" ? localStorage : null;
24+
} catch {
25+
return null;
26+
}
27+
};
28+
29+
/**
30+
* Resolve the storage backend for writing preferences.
31+
* Pass `null` to explicitly disable storage access in callers/tests.
32+
*/
33+
const getStorageForWrite = (storageOverride) => {
34+
if (storageOverride === null) {
35+
return null;
36+
}
37+
if (storageOverride !== undefined) {
38+
return typeof storageOverride?.setItem === "function"
39+
? storageOverride
40+
: null;
41+
}
42+
if (typeof window === "undefined") {
43+
return null;
44+
}
45+
try {
46+
const localStorage = window.localStorage ?? null;
47+
return typeof localStorage?.setItem === "function" ? localStorage : null;
48+
} catch {
49+
return null;
50+
}
51+
};
52+
53+
export const readBooleanPreference = (key, fallback, storage) => {
54+
const targetStorage = getStorageForRead(storage);
55+
if (!targetStorage) {
56+
return fallback;
57+
}
58+
59+
try {
60+
const saved = targetStorage.getItem(key);
61+
if (saved === "true") {
62+
return true;
63+
}
64+
if (saved === "false") {
65+
return false;
66+
}
67+
return fallback;
68+
} catch {
69+
return fallback;
70+
}
71+
};
72+
73+
export const writeBooleanPreference = (key, value, storage) => {
74+
const targetStorage = getStorageForWrite(storage);
75+
if (!targetStorage) {
76+
return;
77+
}
78+
79+
try {
80+
targetStorage.setItem(key, String(value));
81+
} catch {
82+
// Ignore restricted storage environments.
83+
}
84+
};

dashboard/src/views/extension/useExtensionPage.js

Lines changed: 53 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@ import {
1414
getValidHashTab,
1515
replaceTabRoute,
1616
} from "@/utils/hashRouteTabs.mjs";
17+
import {
18+
PIN_UPDATES_ON_TOP_STORAGE_KEY,
19+
PLUGIN_LIST_VIEW_MODE_STORAGE_KEY,
20+
SHOW_RESERVED_PLUGINS_STORAGE_KEY,
21+
readBooleanPreference,
22+
writeBooleanPreference,
23+
} from "./extensionPreferenceStorage.mjs";
1724
import { ref, computed, onMounted, onUnmounted, reactive, watch } from "vue";
1825
import { useRoute, useRouter } from "vue-router";
1926
import { useDisplay } from "vuetify";
@@ -124,11 +131,7 @@ export const useExtensionPage = () => {
124131

125132
// 从 localStorage 恢复显示系统插件的状态,默认为 false(隐藏)
126133
const getInitialShowReserved = () => {
127-
if (typeof window !== "undefined" && window.localStorage) {
128-
const saved = localStorage.getItem("showReservedPlugins");
129-
return saved === "true";
130-
}
131-
return false;
134+
return readBooleanPreference(SHOW_RESERVED_PLUGINS_STORAGE_KEY, false);
132135
};
133136
const showReserved = ref(getInitialShowReserved());
134137
const snack_message = ref("");
@@ -178,16 +181,20 @@ export const useExtensionPage = () => {
178181
// 新增变量支持列表视图
179182
// 从 localStorage 恢复显示模式,默认为 false(卡片视图)
180183
const getInitialListViewMode = () => {
181-
if (typeof window !== "undefined" && window.localStorage) {
182-
return localStorage.getItem("pluginListViewMode") === "true";
183-
}
184-
return false;
184+
return readBooleanPreference(PLUGIN_LIST_VIEW_MODE_STORAGE_KEY, false);
185185
};
186186
const isListView = ref(getInitialListViewMode());
187187
const pluginSearch = ref("");
188188
const installedStatusFilter = ref("all");
189189
const installedSortBy = ref("default");
190190
const installedSortOrder = ref("desc");
191+
const getInitialPinUpdatesOnTop = () => {
192+
return readBooleanPreference(PIN_UPDATES_ON_TOP_STORAGE_KEY, true);
193+
};
194+
const pinUpdatesOnTop = ref(getInitialPinUpdatesOnTop());
195+
watch(pinUpdatesOnTop, (val) => {
196+
writeBooleanPreference(PIN_UPDATES_ON_TOP_STORAGE_KEY, val);
197+
});
191198
const loading_ = ref(false);
192199

193200
// 分页相关
@@ -426,6 +433,17 @@ export const useExtensionPage = () => {
426433
return Number.isFinite(parsed) ? parsed : null;
427434
};
428435

436+
const compareInstalledFallback = (left, right) => {
437+
const nameCompare = compareInstalledPluginNames(left.plugin, right.plugin);
438+
return nameCompare !== 0 ? nameCompare : left.index - right.index;
439+
};
440+
441+
const compareInstalledUpdatePinning = (left, right) => {
442+
const leftHasUpdate = left.plugin?.has_update ? 1 : 0;
443+
const rightHasUpdate = right.plugin?.has_update ? 1 : 0;
444+
return rightHasUpdate - leftHasUpdate;
445+
};
446+
429447
const sortInstalledPlugins = (plugins) => {
430448
return plugins
431449
.map((plugin, index) => ({
@@ -434,19 +452,24 @@ export const useExtensionPage = () => {
434452
installedAtTimestamp: getInstalledAtTimestamp(plugin),
435453
}))
436454
.sort((left, right) => {
437-
const fallbackNameCompare = compareInstalledPluginNames(
438-
left.plugin,
439-
right.plugin,
440-
);
441-
const fallbackResult =
442-
fallbackNameCompare !== 0 ? fallbackNameCompare : left.index - right.index;
455+
if (
456+
pinUpdatesOnTop.value &&
457+
installedSortBy.value !== "update_status"
458+
) {
459+
// Pinning updates is a primary grouping; the selected sort order still
460+
// applies within the "has update" and "no update" groups below.
461+
const pinCompare = compareInstalledUpdatePinning(left, right);
462+
if (pinCompare !== 0) {
463+
return pinCompare;
464+
}
465+
}
443466

444467
if (installedSortBy.value === "install_time") {
445468
const leftTimestamp = left.installedAtTimestamp;
446469
const rightTimestamp = right.installedAtTimestamp;
447470

448471
if (leftTimestamp == null && rightTimestamp == null) {
449-
return fallbackResult;
472+
return compareInstalledFallback(left, right);
450473
}
451474
if (leftTimestamp == null) {
452475
return 1;
@@ -459,7 +482,9 @@ export const useExtensionPage = () => {
459482
installedSortOrder.value === "desc"
460483
? rightTimestamp - leftTimestamp
461484
: leftTimestamp - rightTimestamp;
462-
return timeDiff !== 0 ? timeDiff : fallbackResult;
485+
return timeDiff !== 0
486+
? timeDiff
487+
: compareInstalledFallback(left, right);
463488
}
464489

465490
if (installedSortBy.value === "name") {
@@ -469,7 +494,7 @@ export const useExtensionPage = () => {
469494
? -nameCompare
470495
: nameCompare;
471496
}
472-
return left.index - right.index;
497+
return compareInstalledFallback(left, right);
473498
}
474499

475500
if (installedSortBy.value === "author") {
@@ -482,20 +507,20 @@ export const useExtensionPage = () => {
482507
? -authorCompare
483508
: authorCompare;
484509
}
485-
return fallbackResult;
510+
return compareInstalledFallback(left, right);
486511
}
487512

488513
if (installedSortBy.value === "update_status") {
489-
const leftHasUpdate = left.plugin?.has_update ? 1 : 0;
490-
const rightHasUpdate = right.plugin?.has_update ? 1 : 0;
491514
const updateDiff =
492515
installedSortOrder.value === "desc"
493-
? rightHasUpdate - leftHasUpdate
494-
: leftHasUpdate - rightHasUpdate;
495-
return updateDiff !== 0 ? updateDiff : fallbackResult;
516+
? compareInstalledUpdatePinning(left, right)
517+
: compareInstalledUpdatePinning(right, left);
518+
return updateDiff !== 0
519+
? updateDiff
520+
: compareInstalledFallback(left, right);
496521
}
497522

498-
return fallbackResult;
523+
return compareInstalledFallback(left, right);
499524
})
500525
.map((item) => item.plugin);
501526
};
@@ -636,9 +661,7 @@ export const useExtensionPage = () => {
636661
const toggleShowReserved = () => {
637662
showReserved.value = !showReserved.value;
638663
// 保存到 localStorage
639-
if (typeof window !== "undefined" && window.localStorage) {
640-
localStorage.setItem("showReservedPlugins", showReserved.value.toString());
641-
}
664+
writeBooleanPreference(SHOW_RESERVED_PLUGINS_STORAGE_KEY, showReserved.value);
642665
};
643666

644667
const toast = (message, success) => {
@@ -1603,9 +1626,7 @@ export const useExtensionPage = () => {
16031626

16041627
// 监听显示模式变化并保存到 localStorage
16051628
watch(isListView, (newVal) => {
1606-
if (typeof window !== "undefined" && window.localStorage) {
1607-
localStorage.setItem("pluginListViewMode", String(newVal));
1608-
}
1629+
writeBooleanPreference(PLUGIN_LIST_VIEW_MODE_STORAGE_KEY, newVal);
16091630
});
16101631

16111632
watch(
@@ -1695,6 +1716,7 @@ export const useExtensionPage = () => {
16951716
installedStatusFilter,
16961717
installedSortBy,
16971718
installedSortOrder,
1719+
pinUpdatesOnTop,
16981720
loading_,
16991721
currentPage,
17001722
marketCategoryFilter,

0 commit comments

Comments
 (0)