mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-06-25 15:43:49 +08:00
feat(update-center): 实现集中式软件更新中心功能
新增更新中心模块,支持管理 APM 和传统 deb 软件更新任务 - 添加更新任务队列管理、状态跟踪和日志记录功能 - 实现更新项忽略配置持久化存储 - 新增更新确认对话框和迁移提示 - 优化主窗口关闭时的任务保护机制 - 添加单元测试覆盖核心逻辑
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="show"
|
||||
class="absolute inset-0 flex items-center justify-center bg-slate-950/45 px-4"
|
||||
>
|
||||
<div
|
||||
class="w-full max-w-md rounded-3xl border border-amber-200 bg-white p-6 shadow-2xl dark:border-amber-500/30 dark:bg-slate-900"
|
||||
>
|
||||
<p class="text-lg font-semibold text-slate-900 dark:text-white">
|
||||
关闭确认
|
||||
</p>
|
||||
<p class="mt-2 text-sm text-slate-600 dark:text-slate-300">
|
||||
更新仍在进行中,确定关闭此窗口吗?
|
||||
</p>
|
||||
<div class="mt-4 flex justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-2xl border border-slate-200/70 px-4 py-2 text-sm font-semibold text-slate-600 dark:border-slate-700 dark:text-slate-200"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
继续查看
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-2xl bg-amber-500 px-4 py-2 text-sm font-semibold text-white"
|
||||
@click="$emit('confirm')"
|
||||
>
|
||||
确认关闭
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
show: boolean;
|
||||
}>();
|
||||
|
||||
defineEmits<{
|
||||
(e: "close"): void;
|
||||
(e: "confirm"): void;
|
||||
}>();
|
||||
</script>
|
||||
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<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"
|
||||
>
|
||||
<div class="flex items-start gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="mt-1 h-4 w-4 rounded border-slate-300 accent-brand focus:ring-brand"
|
||||
:checked="selected"
|
||||
:disabled="item.ignored === true"
|
||||
@change="$emit('toggle-selection')"
|
||||
/>
|
||||
<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
|
||||
v-if="item.ignored === true"
|
||||
class="mt-2 text-xs font-medium text-slate-500 dark:text-slate-400"
|
||||
>
|
||||
已忽略的更新不会加入本次任务。
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
v-if="task"
|
||||
class="text-right text-sm font-semibold text-slate-600 dark:text-slate-300"
|
||||
>
|
||||
<p>{{ statusLabel }}</p>
|
||||
<p v-if="showProgress" class="mt-1">{{ progressText }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showProgress" class="space-y-2">
|
||||
<div
|
||||
class="h-2 overflow-hidden rounded-full bg-slate-200 dark:bg-slate-800"
|
||||
>
|
||||
<div
|
||||
class="h-full rounded-full bg-gradient-to-r from-brand to-brand-dark"
|
||||
:style="progressStyle"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
|
||||
import type {
|
||||
UpdateCenterItem,
|
||||
UpdateCenterTaskState,
|
||||
} from "@/global/typedefinition";
|
||||
|
||||
const props = defineProps<{
|
||||
item: UpdateCenterItem;
|
||||
task?: UpdateCenterTaskState;
|
||||
selected: boolean;
|
||||
}>();
|
||||
|
||||
defineEmits<{
|
||||
(e: "toggle-selection"): void;
|
||||
}>();
|
||||
|
||||
const sourceLabel = computed(() => {
|
||||
return props.item.source === "apm" ? "APM" : "传统deb";
|
||||
});
|
||||
|
||||
const statusLabel = computed(() => {
|
||||
switch (props.task?.status) {
|
||||
case "downloading":
|
||||
return "下载中";
|
||||
case "installing":
|
||||
return "安装中";
|
||||
case "completed":
|
||||
return "已完成";
|
||||
case "failed":
|
||||
return "失败";
|
||||
case "cancelled":
|
||||
return "已取消";
|
||||
default:
|
||||
return "待处理";
|
||||
}
|
||||
});
|
||||
|
||||
const showProgress = computed(() => {
|
||||
return (
|
||||
props.task?.status === "downloading" || props.task?.status === "installing"
|
||||
);
|
||||
});
|
||||
|
||||
const progressText = computed(() => `${props.task?.progress ?? 0}%`);
|
||||
const progressStyle = computed(() => ({ width: progressText.value }));
|
||||
</script>
|
||||
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<div
|
||||
class="min-h-0 overflow-y-auto border-r border-slate-200/70 p-6 dark:border-slate-800/70"
|
||||
>
|
||||
<div
|
||||
v-if="items.length === 0"
|
||||
class="rounded-2xl border border-dashed border-slate-200/80 px-4 py-10 text-center text-slate-500 dark:border-slate-800/80 dark:text-slate-400"
|
||||
>
|
||||
暂无可展示的更新任务
|
||||
</div>
|
||||
<div v-else class="space-y-3">
|
||||
<UpdateCenterItem
|
||||
v-for="item in items"
|
||||
:key="item.taskKey"
|
||||
:item="item"
|
||||
:task="taskMap.get(item.taskKey)"
|
||||
:selected="selectedTaskKeys.has(item.taskKey)"
|
||||
@toggle-selection="$emit('toggle-selection', item.taskKey)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
|
||||
import type {
|
||||
UpdateCenterItem as UpdateCenterItemModel,
|
||||
UpdateCenterTaskState,
|
||||
} from "@/global/typedefinition";
|
||||
|
||||
import UpdateCenterItem from "./UpdateCenterItem.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
items: UpdateCenterItemModel[];
|
||||
tasks: UpdateCenterTaskState[];
|
||||
selectedTaskKeys: Set<string>;
|
||||
}>();
|
||||
|
||||
defineEmits<{
|
||||
(e: "toggle-selection", taskKey: string): void;
|
||||
}>();
|
||||
|
||||
const taskMap = computed(() => {
|
||||
return new Map(props.tasks.map((task) => [task.taskKey, task]));
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<aside
|
||||
class="hidden min-h-0 flex-col border-t border-slate-200/70 bg-slate-50/70 p-6 lg:flex lg:border-t-0 dark:border-slate-800/70 dark:bg-slate-950/50"
|
||||
>
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<p class="text-sm font-semibold text-slate-900 dark:text-white">
|
||||
任务日志
|
||||
</p>
|
||||
<span class="text-xs text-slate-500 dark:text-slate-400"
|
||||
>{{ tasks.length }} 项</span
|
||||
>
|
||||
</div>
|
||||
<div class="mt-4 min-h-0 flex-1 space-y-4 overflow-y-auto">
|
||||
<div
|
||||
v-if="tasks.length === 0"
|
||||
class="rounded-2xl border border-dashed border-slate-200/80 px-4 py-8 text-center text-sm text-slate-500 dark:border-slate-800/80 dark:text-slate-400"
|
||||
>
|
||||
暂无运行日志
|
||||
</div>
|
||||
<div
|
||||
v-for="task in tasks"
|
||||
:key="task.taskKey"
|
||||
class="rounded-2xl border border-slate-200/70 bg-white/80 p-4 dark:border-slate-800/70 dark:bg-slate-900/70"
|
||||
>
|
||||
<p class="text-sm font-semibold text-slate-900 dark:text-white">
|
||||
{{ task.packageName }}
|
||||
</p>
|
||||
<p class="mt-1 text-xs text-slate-500 dark:text-slate-400">
|
||||
{{ task.status }}
|
||||
</p>
|
||||
<p class="mt-3 text-xs leading-5 text-slate-600 dark:text-slate-300">
|
||||
{{ task.logs.at(-1)?.message || task.errorMessage || "等待日志输出" }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { UpdateCenterTaskState } from "@/global/typedefinition";
|
||||
|
||||
defineProps<{
|
||||
tasks: UpdateCenterTaskState[];
|
||||
}>();
|
||||
</script>
|
||||
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="show"
|
||||
class="absolute inset-0 flex items-center justify-center bg-slate-950/45 px-4"
|
||||
>
|
||||
<div
|
||||
class="w-full max-w-md rounded-3xl border border-slate-200/70 bg-white p-6 shadow-2xl dark:border-slate-700 dark:bg-slate-900"
|
||||
>
|
||||
<p class="text-lg font-semibold text-slate-900 dark:text-white">
|
||||
迁移确认
|
||||
</p>
|
||||
<p class="mt-2 text-sm text-slate-500 dark:text-slate-400">
|
||||
部分传统 deb 更新将迁移到 APM 管理。
|
||||
</p>
|
||||
<div class="mt-4 flex justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-2xl border border-slate-200/70 px-4 py-2 text-sm font-semibold text-slate-600 dark:border-slate-700 dark:text-slate-200"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-2xl bg-gradient-to-r from-brand to-brand-dark px-4 py-2 text-sm font-semibold text-white"
|
||||
@click="$emit('confirm')"
|
||||
>
|
||||
继续更新
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
show: boolean;
|
||||
}>();
|
||||
|
||||
defineEmits<{
|
||||
(e: "close"): void;
|
||||
(e: "confirm"): void;
|
||||
}>();
|
||||
</script>
|
||||
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col gap-4 border-b border-slate-200/70 px-6 py-5 dark:border-slate-800/70"
|
||||
>
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="flex-1">
|
||||
<p class="text-2xl font-semibold text-slate-900 dark:text-white">
|
||||
软件更新
|
||||
</p>
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400">
|
||||
集中管理 APM 与传统 deb 更新任务
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-2 rounded-2xl border border-slate-200/70 px-4 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"
|
||||
@click="$emit('refresh')"
|
||||
>
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
刷新
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-2 rounded-2xl bg-gradient-to-r from-brand to-brand-dark px-4 py-2 text-sm font-semibold text-white shadow-lg disabled:opacity-40"
|
||||
:disabled="selectedCount === 0"
|
||||
@click="$emit('start-selected')"
|
||||
>
|
||||
<i class="fas fa-play"></i>
|
||||
更新选中 ({{ selectedCount }})
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="关闭"
|
||||
class="inline-flex h-11 w-11 items-center justify-center rounded-full border border-slate-200/70 text-slate-500 transition hover:text-slate-900 dark:border-slate-700 dark:text-slate-300"
|
||||
@click="$emit('request-close')"
|
||||
>
|
||||
<i class="fas fa-xmark"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class="block">
|
||||
<span class="sr-only">搜索更新</span>
|
||||
<input
|
||||
:value="searchQuery"
|
||||
type="search"
|
||||
placeholder="搜索应用或包名"
|
||||
class="w-full rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-900 outline-none transition focus:border-brand/60 focus:bg-white dark:border-slate-700 dark:bg-slate-950 dark:text-slate-100 dark:focus:bg-slate-900"
|
||||
@input="handleInput"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
searchQuery: string;
|
||||
selectedCount: number;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "refresh"): void;
|
||||
(e: "start-selected"): void;
|
||||
(e: "request-close"): void;
|
||||
(e: "update:search-query", value: string): void;
|
||||
}>();
|
||||
|
||||
const handleInput = (event: Event): void => {
|
||||
const target = event.target as HTMLInputElement | null;
|
||||
emit("update:search-query", target?.value ?? props.searchQuery);
|
||||
};
|
||||
</script>
|
||||
Reference in New Issue
Block a user