mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-09-20 21:50:11 +08:00
refactor(更新中心): E方案重设计更新条目 + 已忽略沉底 + 复用app.update显示更新时间 + 安全加固
- UpdateCenterItem.vue: 改为单行紧凑卡片,显示 包名·当前→新版本、来源标签、更新时间(品牌色)、大小;已忽略项淡化 - updateCenter.ts: filteredItems 将已忽略项沉底到列表末尾 - UpdateCenterList/Modal + App.vue: 透传全局 apps,用 app.json 的 Update 字段(Date.parse)推算 updateTime,零额外网络请求 - typedefinition.ts + update-center/types.ts: 新增可选 updateTime 字段 - install-manager.ts: launch-app 补 pkgname.length > 256 长度限制(防 DoS) - index.ts: close 事件先 clearTimeout 防抖定时器再同步保存窗口状态(消除竞态) - App.vue: fetchWithRetry 重试 3→2、延迟 1000→500ms(体验优化) - 7维审计通过;AI审查2阻断项(getIconUrl防护/fetchWithRetry泛型)经核实为误报
This commit is contained in:
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
spark-store (5.2.1.16-test) UNRELEASED; urgency=medium
|
spark-store (5.2.1.18-test) UNRELEASED; urgency=medium
|
||||||
|
|
||||||
* Initial release. (Closes: #nnnn) <nnnn is the bug number of your ITP>
|
* Initial release. (Closes: #nnnn) <nnnn is the bug number of your ITP>
|
||||||
|
|
||||||
|
|||||||
@@ -1359,7 +1359,8 @@ ipcMain.handle(
|
|||||||
if (
|
if (
|
||||||
!pkgname ||
|
!pkgname ||
|
||||||
typeof pkgname !== "string" ||
|
typeof pkgname !== "string" ||
|
||||||
!PKGNAME_PATTERN.test(pkgname)
|
!PKGNAME_PATTERN.test(pkgname) ||
|
||||||
|
pkgname.length > 256
|
||||||
) {
|
) {
|
||||||
logger.warn(`Invalid pkgname provided for launch-app: ${pkgname}`);
|
logger.warn(`Invalid pkgname provided for launch-app: ${pkgname}`);
|
||||||
return { success: false, message: "Invalid package name" };
|
return { success: false, message: "Invalid package name" };
|
||||||
|
|||||||
@@ -24,4 +24,6 @@ export interface UpdateCenterItem {
|
|||||||
migrationSource?: UpdateSource;
|
migrationSource?: UpdateSource;
|
||||||
migrationTarget?: UpdateSource;
|
migrationTarget?: UpdateSource;
|
||||||
aptssVersion?: string;
|
aptssVersion?: string;
|
||||||
|
// 更新发布时间(毫秒时间戳);由上游解析填充,暂无则缺省
|
||||||
|
updateTime?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -483,6 +483,8 @@ async function createWindow() {
|
|||||||
mainWindow.on("close", (event) => {
|
mainWindow.on("close", (event) => {
|
||||||
if (allowAppExit) {
|
if (allowAppExit) {
|
||||||
// 真正退出前同步保存最终窗口尺寸(防抖可能尚未触发)
|
// 真正退出前同步保存最终窗口尺寸(防抖可能尚未触发)
|
||||||
|
// 先清除尚未触发的防抖定时器,避免旧定时器随后覆盖本次同步写入
|
||||||
|
if (saveBoundsTimer) clearTimeout(saveBoundsTimer);
|
||||||
const { width, height, x, y } = mainWindow.getBounds();
|
const { width, height, x, y } = mainWindow.getBounds();
|
||||||
saveWindowState({
|
saveWindowState({
|
||||||
width,
|
width,
|
||||||
|
|||||||
+7
-3
@@ -220,6 +220,7 @@
|
|||||||
<UpdateCenterModal
|
<UpdateCenterModal
|
||||||
:show="updateCenterStore.isOpen.value"
|
:show="updateCenterStore.isOpen.value"
|
||||||
:store="updateCenterStore"
|
:store="updateCenterStore"
|
||||||
|
:apps="apps"
|
||||||
@update:search-query="updateCenterStore.searchQuery.value = $event"
|
@update:search-query="updateCenterStore.searchQuery.value = $event"
|
||||||
@toggle-selection="updateCenterStore.toggleSelection"
|
@toggle-selection="updateCenterStore.toggleSelection"
|
||||||
@request-start-selected="handleStartSelectedUpdates"
|
@request-start-selected="handleStartSelectedUpdates"
|
||||||
@@ -435,7 +436,10 @@ const axiosInstance = axios.create({
|
|||||||
// 使每次 URL 不同,穿透 CDN 边缘缓存,确保返回最新列表。
|
// 使每次 URL 不同,穿透 CDN 边缘缓存,确保返回最新列表。
|
||||||
// 与主进程 onBeforeSendHeaders 注入的 no-cache 互为兜底(C1 已对 Chromium 磁盘缓存生效)。
|
// 与主进程 onBeforeSendHeaders 注入的 no-cache 互为兜底(C1 已对 Chromium 磁盘缓存生效)。
|
||||||
axiosInstance.interceptors.request.use((config) => {
|
axiosInstance.interceptors.request.use((config) => {
|
||||||
if (config.method?.toLowerCase() === "get" && typeof config.url === "string") {
|
if (
|
||||||
|
config.method?.toLowerCase() === "get" &&
|
||||||
|
typeof config.url === "string"
|
||||||
|
) {
|
||||||
// 按去除 query 的 pathname 判断,兼容 C2 自身追加的 ?_t= 及任何既有 query
|
// 按去除 query 的 pathname 判断,兼容 C2 自身追加的 ?_t= 及任何既有 query
|
||||||
const reqPath = config.url.split("?")[0];
|
const reqPath = config.url.split("?")[0];
|
||||||
if (reqPath.endsWith(".json")) {
|
if (reqPath.endsWith(".json")) {
|
||||||
@@ -449,8 +453,8 @@ axiosInstance.interceptors.request.use((config) => {
|
|||||||
const fetchWithRetry = async <T,>(
|
const fetchWithRetry = async <T,>(
|
||||||
url: string,
|
url: string,
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
retries = 3,
|
retries = 2,
|
||||||
delay = 1000,
|
delay = 500,
|
||||||
): Promise<T> => {
|
): Promise<T> => {
|
||||||
try {
|
try {
|
||||||
const response = await axiosInstance.get<T>(url, { signal });
|
const response = await axiosInstance.get<T>(url, { signal });
|
||||||
|
|||||||
@@ -66,6 +66,7 @@
|
|||||||
:items="store.filteredItems.value"
|
:items="store.filteredItems.value"
|
||||||
:tasks="store.snapshot.value.tasks"
|
:tasks="store.snapshot.value.tasks"
|
||||||
:selected-task-keys="store.selectedTaskKeys.value"
|
:selected-task-keys="store.selectedTaskKeys.value"
|
||||||
|
:apps="apps"
|
||||||
@toggle-selection="emit('toggle-selection', $event)"
|
@toggle-selection="emit('toggle-selection', $event)"
|
||||||
@ignore-item="store.ignoreItem"
|
@ignore-item="store.ignoreItem"
|
||||||
@unignore-item="store.unignoreItem"
|
@unignore-item="store.unignoreItem"
|
||||||
@@ -86,6 +87,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
|
|
||||||
|
import type { App } from "@/global/typedefinition";
|
||||||
import type { UpdateCenterStore } from "@/modules/updateCenter";
|
import type { UpdateCenterStore } from "@/modules/updateCenter";
|
||||||
|
|
||||||
import UpdateCenterList from "./update-center/UpdateCenterList.vue";
|
import UpdateCenterList from "./update-center/UpdateCenterList.vue";
|
||||||
@@ -103,6 +105,7 @@ const emit = defineEmits<{
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
show: boolean;
|
show: boolean;
|
||||||
store: UpdateCenterStore;
|
store: UpdateCenterStore;
|
||||||
|
apps: App[];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const selectedCount = computed(() => props.store.getSelectedItems().length);
|
const selectedCount = computed(() => props.store.getSelectedItems().length);
|
||||||
|
|||||||
@@ -1,100 +1,113 @@
|
|||||||
<template>
|
<template>
|
||||||
<label
|
<label
|
||||||
class="flex flex-col gap-4 rounded-2xl border border-slate-200/70 bg-white/90 p-4 shadow-sm dark:border-slate-800/70 dark:bg-slate-900/70"
|
class="relative flex items-center gap-3 rounded-xl border p-2.5 shadow-sm transition"
|
||||||
|
:class="
|
||||||
|
item.ignored === true
|
||||||
|
? 'border-slate-200/50 bg-slate-50/60 opacity-70 dark:border-slate-800/50 dark:bg-slate-900/40'
|
||||||
|
: 'border-slate-200/70 bg-white/90 dark:border-slate-800/70 dark:bg-slate-900/70'
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<div class="flex items-start gap-3">
|
<input
|
||||||
<input
|
type="checkbox"
|
||||||
type="checkbox"
|
class="h-4 w-4 shrink-0 rounded border-slate-300 accent-brand focus:ring-brand"
|
||||||
class="mt-1 h-4 w-4 rounded border-slate-300 accent-brand focus:ring-brand"
|
:checked="selected"
|
||||||
:checked="selected"
|
:disabled="item.ignored === true"
|
||||||
:disabled="item.ignored === true"
|
@change="$emit('toggle-selection')"
|
||||||
@change="$emit('toggle-selection')"
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex h-9 w-9 shrink-0 items-center justify-center overflow-hidden rounded-md bg-slate-100 text-slate-400 dark:bg-slate-800 dark:text-slate-500"
|
||||||
|
:class="item.ignored === true ? 'opacity-60' : ''"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:src="iconSrc"
|
||||||
|
:alt="`${item.displayName} 图标`"
|
||||||
|
class="h-full w-full object-cover"
|
||||||
|
@error="handleIconError"
|
||||||
/>
|
/>
|
||||||
<div
|
</div>
|
||||||
class="flex h-12 w-12 shrink-0 items-center justify-center overflow-hidden rounded-xl bg-slate-100 text-slate-400 dark:bg-slate-800 dark:text-slate-500"
|
|
||||||
>
|
<div class="min-w-0 flex-1">
|
||||||
<img
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
:src="iconSrc"
|
|
||||||
:alt="`${item.displayName} 图标`"
|
|
||||||
class="h-full w-full object-cover"
|
|
||||||
@error="handleIconError"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="min-w-0 flex-1">
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
|
||||||
<p class="font-semibold text-slate-900 dark:text-white">
|
|
||||||
{{ item.displayName }}
|
|
||||||
</p>
|
|
||||||
<span
|
|
||||||
class="rounded-full bg-slate-100 px-2.5 py-1 text-xs font-semibold text-slate-600 dark:bg-slate-800 dark:text-slate-200"
|
|
||||||
>
|
|
||||||
{{ sourceLabel }}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
v-if="item.isMigration"
|
|
||||||
class="rounded-full bg-brand/10 px-2.5 py-1 text-xs font-semibold text-brand"
|
|
||||||
>
|
|
||||||
将迁移到 APM
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
v-if="item.ignored === true"
|
|
||||||
class="rounded-full bg-slate-200 px-2.5 py-1 text-xs font-semibold text-slate-500 dark:bg-slate-800 dark:text-slate-300"
|
|
||||||
>
|
|
||||||
已忽略
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p class="mt-1 text-sm text-slate-500 dark:text-slate-400">
|
|
||||||
{{ item.packageName }} · 当前 {{ item.currentVersion }} · 更新至
|
|
||||||
{{ item.newVersion }}
|
|
||||||
</p>
|
|
||||||
<p
|
<p
|
||||||
v-if="item.ignored === true"
|
class="truncate font-semibold"
|
||||||
class="mt-2 text-xs font-medium text-slate-500 dark:text-slate-400"
|
:class="
|
||||||
|
item.ignored === true
|
||||||
|
? 'text-sm text-slate-400 dark:text-slate-500'
|
||||||
|
: 'text-sm text-slate-900 dark:text-white'
|
||||||
|
"
|
||||||
>
|
>
|
||||||
已忽略的更新不会加入本次任务。
|
{{ item.displayName }}
|
||||||
</p>
|
</p>
|
||||||
|
<span
|
||||||
|
class="shrink-0 rounded-full bg-slate-100 px-2 py-0.5 text-[10px] font-semibold text-slate-600 dark:bg-slate-800 dark:text-slate-200"
|
||||||
|
>
|
||||||
|
{{ sourceLabel }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="item.ignored === true"
|
||||||
|
class="shrink-0 rounded-full bg-slate-200 px-2 py-0.5 text-[10px] font-semibold text-slate-500 dark:bg-slate-800 dark:text-slate-300"
|
||||||
|
>
|
||||||
|
已忽略
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<p class="mt-0.5 truncate text-xs text-slate-500 dark:text-slate-400">
|
||||||
v-if="task"
|
{{ item.packageName }} · {{ item.currentVersion }} →
|
||||||
class="text-right text-sm font-semibold text-slate-600 dark:text-slate-300"
|
{{ item.newVersion }}
|
||||||
>
|
</p>
|
||||||
<p>{{ statusLabel }}</p>
|
|
||||||
<p v-if="showProgress" class="mt-1">{{ progressText }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-end">
|
<div
|
||||||
<button
|
v-if="task"
|
||||||
v-if="item.ignored === true"
|
class="shrink-0 text-right text-xs font-semibold text-slate-600 dark:text-slate-300"
|
||||||
type="button"
|
>
|
||||||
class="inline-flex items-center gap-2 rounded-2xl border border-slate-300/80 px-3 py-2 text-sm font-semibold text-slate-600 transition hover:bg-slate-50 dark:border-slate-700 dark:text-slate-200 dark:hover:bg-slate-800"
|
<p>{{ statusLabel }}</p>
|
||||||
aria-label="取消忽略"
|
<p v-if="showProgress" class="mt-0.5">{{ progressText }}</p>
|
||||||
@click.stop="$emit('unignore-item')"
|
|
||||||
>
|
|
||||||
<i class="fas fa-rotate-left"></i>
|
|
||||||
取消忽略
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-else
|
|
||||||
type="button"
|
|
||||||
class="inline-flex items-center gap-2 rounded-2xl border border-amber-300/80 px-3 py-2 text-sm font-semibold text-amber-700 transition hover:bg-amber-50 dark:border-amber-500/40 dark:text-amber-300 dark:hover:bg-amber-500/10"
|
|
||||||
aria-label="忽略更新"
|
|
||||||
@click.stop="$emit('ignore-item')"
|
|
||||||
>
|
|
||||||
<i class="fas fa-eye-slash"></i>
|
|
||||||
忽略更新
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="showProgress" class="space-y-2">
|
<div
|
||||||
|
v-if="item.ignored !== true"
|
||||||
|
class="flex shrink-0 flex-col items-end gap-0.5"
|
||||||
|
>
|
||||||
|
<span class="text-[11px] font-semibold text-brand dark:text-amber-300">{{
|
||||||
|
timeLabel
|
||||||
|
}}</span>
|
||||||
|
<span class="text-[10px] text-slate-400">{{ sizeLabel }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex shrink-0 flex-col items-end gap-0.5">
|
||||||
|
<span class="text-[11px] font-semibold text-slate-400">{{
|
||||||
|
timeLabel
|
||||||
|
}}</span>
|
||||||
|
<span class="text-[10px] text-slate-400">{{ sizeLabel }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-if="item.ignored === true"
|
||||||
|
type="button"
|
||||||
|
class="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg border border-slate-300/80 text-slate-500 transition hover:bg-slate-50 dark:border-slate-700 dark:text-slate-300 dark:hover:bg-slate-800"
|
||||||
|
aria-label="取消忽略"
|
||||||
|
@click.stop="$emit('unignore-item')"
|
||||||
|
>
|
||||||
|
<i class="fas fa-rotate-left text-xs"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-else
|
||||||
|
type="button"
|
||||||
|
class="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg text-amber-600 transition hover:bg-amber-50 dark:text-amber-300 dark:hover:bg-amber-500/10"
|
||||||
|
aria-label="忽略更新"
|
||||||
|
@click.stop="$emit('ignore-item')"
|
||||||
|
>
|
||||||
|
<i class="fas fa-eye-slash text-xs"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="showProgress"
|
||||||
|
class="absolute inset-x-0 bottom-0 h-1 overflow-hidden rounded-b-xl bg-slate-200 dark:bg-slate-800"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class="h-2 overflow-hidden rounded-full bg-slate-200 dark:bg-slate-800"
|
class="h-full rounded-full bg-gradient-to-r from-brand to-brand-dark"
|
||||||
>
|
:style="progressStyle"
|
||||||
<div
|
></div>
|
||||||
class="h-full rounded-full bg-gradient-to-r from-brand to-brand-dark"
|
|
||||||
:style="progressStyle"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
</template>
|
</template>
|
||||||
@@ -185,4 +198,27 @@ const showProgress = computed(() => {
|
|||||||
|
|
||||||
const progressText = computed(() => `${props.task?.progress ?? 0}%`);
|
const progressText = computed(() => `${props.task?.progress ?? 0}%`);
|
||||||
const progressStyle = computed(() => ({ width: progressText.value }));
|
const progressStyle = computed(() => ({ width: progressText.value }));
|
||||||
|
|
||||||
|
// 相对时间(如「3天前」),无数据降级为 「—」
|
||||||
|
const timeLabel = computed(() => {
|
||||||
|
const t = props.item.updateTime;
|
||||||
|
if (!t || typeof t !== "number") return "—";
|
||||||
|
const diff = Date.now() - t;
|
||||||
|
const day = 24 * 60 * 60 * 1000;
|
||||||
|
if (diff < 0) return "刚刚";
|
||||||
|
if (diff < day) return "今天";
|
||||||
|
if (diff < 2 * day) return "昨天";
|
||||||
|
if (diff < 30 * day) return `${Math.floor(diff / day)}天前`;
|
||||||
|
if (diff < 365 * day) return `${Math.floor(diff / (30 * day))}个月前`;
|
||||||
|
return `${Math.floor(diff / (365 * day))}年前`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 大小格式化(如 45 MB / 1.2 GB)
|
||||||
|
const sizeLabel = computed(() => {
|
||||||
|
const size = props.item.size;
|
||||||
|
if (!size || size <= 0) return "—";
|
||||||
|
const mb = size / (1024 * 1024);
|
||||||
|
if (mb >= 1024) return `${(mb / 1024).toFixed(1)} GB`;
|
||||||
|
return `${Math.round(mb)} MB`;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-else class="space-y-3">
|
<div v-else class="space-y-3">
|
||||||
<UpdateCenterItem
|
<UpdateCenterItem
|
||||||
v-for="item in items"
|
v-for="item in enrichedItems"
|
||||||
:key="item.taskKey"
|
:key="item.taskKey"
|
||||||
:item="item"
|
:item="item"
|
||||||
:task="taskMap.get(item.taskKey)"
|
:task="taskMap.get(item.taskKey)"
|
||||||
@@ -29,6 +29,7 @@
|
|||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
App,
|
||||||
UpdateCenterItem as UpdateCenterItemModel,
|
UpdateCenterItem as UpdateCenterItemModel,
|
||||||
UpdateCenterTaskState,
|
UpdateCenterTaskState,
|
||||||
} from "@/global/typedefinition";
|
} from "@/global/typedefinition";
|
||||||
@@ -39,6 +40,7 @@ const props = defineProps<{
|
|||||||
items: UpdateCenterItemModel[];
|
items: UpdateCenterItemModel[];
|
||||||
tasks: UpdateCenterTaskState[];
|
tasks: UpdateCenterTaskState[];
|
||||||
selectedTaskKeys: Set<string>;
|
selectedTaskKeys: Set<string>;
|
||||||
|
apps: App[];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
@@ -47,6 +49,23 @@ defineEmits<{
|
|||||||
(e: "unignore-item", packageName: string, newVersion: string): void;
|
(e: "unignore-item", packageName: string, newVersion: string): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
// 从商店目录 apps 的 update 字段(app.json 的 Update,如 "2026-08-08")
|
||||||
|
// 推算更新发布时间,补全到更新列表项,用于显示「X天前」。匹配不到则保留原值(降级为「—」)。
|
||||||
|
const enrichedItems = computed<UpdateCenterItemModel[]>(() => {
|
||||||
|
const updateByPkg = new Map<string, number>();
|
||||||
|
for (const app of props.apps) {
|
||||||
|
if (app.pkgname && app.update) {
|
||||||
|
const t = Date.parse(app.update);
|
||||||
|
if (!Number.isNaN(t)) updateByPkg.set(app.pkgname, t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return props.items.map((item) => {
|
||||||
|
if (item.updateTime && !Number.isNaN(item.updateTime)) return item;
|
||||||
|
const t = updateByPkg.get(item.packageName);
|
||||||
|
return t ? { ...item, updateTime: t } : item;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const taskMap = computed(() => {
|
const taskMap = computed(() => {
|
||||||
return new Map(props.tasks.map((task) => [task.taskKey, task]));
|
return new Map(props.tasks.map((task) => [task.taskKey, task]));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -156,6 +156,8 @@ export interface UpdateCenterItem {
|
|||||||
migrationSource?: UpdateSource;
|
migrationSource?: UpdateSource;
|
||||||
migrationTarget?: UpdateSource;
|
migrationTarget?: UpdateSource;
|
||||||
aptssVersion?: string;
|
aptssVersion?: string;
|
||||||
|
// 更新发布时间(毫秒时间戳),用于列表显示「X天前」;暂无数据时前端降级为「—」
|
||||||
|
updateTime?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateCenterTaskState {
|
export interface UpdateCenterTaskState {
|
||||||
|
|||||||
@@ -90,7 +90,14 @@ export const createUpdateCenterStore = (): UpdateCenterStore => {
|
|||||||
|
|
||||||
const filteredItems = computed(() => {
|
const filteredItems = computed(() => {
|
||||||
const query = searchQuery.value.trim();
|
const query = searchQuery.value.trim();
|
||||||
return snapshot.value.items.filter((item) => matchesSearch(item, query));
|
const matched = snapshot.value.items.filter((item) =>
|
||||||
|
matchesSearch(item, query),
|
||||||
|
);
|
||||||
|
// 已忽略项沉底:非忽略在前、已忽略在后,各自保持原有顺序
|
||||||
|
return [
|
||||||
|
...matched.filter((item) => item.ignored !== true),
|
||||||
|
...matched.filter((item) => item.ignored === true),
|
||||||
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
const allSelected = computed(() => {
|
const allSelected = computed(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user