feat(update-center): 添加加载状态处理及UI优化

为更新中心添加加载状态管理,包括:
- 在打开和刷新操作时显示加载状态
- 禁用刷新按钮防止重复操作
- 添加加载中的动画效果和提示文本
- 优化加载时的UI显示
This commit is contained in:
2026-04-16 14:00:33 +08:00
parent e72553d570
commit 42046caf2c
6 changed files with 122 additions and 19 deletions
@@ -63,6 +63,7 @@ const createStore = (
return {
isOpen: ref(true),
loading: ref(false),
showCloseConfirm: ref(true),
showMigrationConfirm: ref(false),
searchQuery: ref(""),
@@ -220,4 +221,52 @@ describe("UpdateCenterModal", () => {
expect(store.requestClose).toHaveBeenCalledTimes(1);
});
it("shows loading panel when loading with no items", () => {
const store = createStore({
items: [],
tasks: [],
warnings: [],
hasRunningTasks: false,
});
store.loading.value = true;
render(UpdateCenterModal, {
props: {
show: true,
store,
},
});
expect(screen.getByText("正在检查更新…")).toBeTruthy();
});
it("shows refresh hint while loading with existing items", () => {
const store = createStore({ hasRunningTasks: false });
store.loading.value = true;
render(UpdateCenterModal, {
props: {
show: true,
store,
},
});
expect(screen.getByText("Spark Weather")).toBeTruthy();
expect(screen.getByText("正在刷新更新列表…")).toBeTruthy();
});
it("disables refresh button while loading", () => {
const store = createStore({ hasRunningTasks: false });
store.loading.value = true;
render(UpdateCenterModal, {
props: {
show: true,
store,
},
});
expect(screen.getByRole("button", { name: /刷新/ })).toBeDisabled();
});
});
+15 -2
View File
@@ -61,10 +61,16 @@ describe("updateCenter store", () => {
open.mockResolvedValue(snapshot);
const store = createUpdateCenterStore();
await store.open("apm");
const openPromise = store.open("apm");
expect(store.isOpen.value).toBe(true);
expect(store.loading.value).toBe(true);
await openPromise;
expect(open).toHaveBeenCalledWith("apm");
expect(store.isOpen.value).toBe(true);
expect(store.loading.value).toBe(false);
expect(store.snapshot.value).toEqual(snapshot);
expect(store.filteredItems.value).toEqual(snapshot.items);
});
@@ -76,7 +82,12 @@ describe("updateCenter store", () => {
const store = createUpdateCenterStore();
await store.open("apm");
await store.refresh();
const refreshPromise = store.refresh();
expect(store.loading.value).toBe(true);
await refreshPromise;
expect(store.loading.value).toBe(false);
expect(refresh).toHaveBeenCalledWith("apm");
});
@@ -209,11 +220,13 @@ describe("updateCenter store", () => {
it("blocks close requests while the snapshot reports running tasks", () => {
const store = createUpdateCenterStore();
store.isOpen.value = true;
store.loading.value = true;
store.snapshot.value = createSnapshot({ hasRunningTasks: true });
store.requestClose();
expect(store.isOpen.value).toBe(false);
expect(store.loading.value).toBe(false);
expect(store.showCloseConfirm.value).toBe(false);
});
+27 -9
View File
@@ -22,6 +22,7 @@
:selected-count="selectedCount"
:all-selected="store.allSelected.value"
:some-selected="store.someSelected.value"
:loading="store.loading.value"
@refresh="store.refresh"
@start-selected="emit('request-start-selected')"
@request-close="store.requestClose"
@@ -42,16 +43,33 @@
</p>
</div>
<div class="flex min-h-0 flex-1">
<UpdateCenterList
:items="store.filteredItems.value"
:tasks="store.snapshot.value.tasks"
:selected-task-keys="store.selectedTaskKeys.value"
@toggle-selection="emit('toggle-selection', $event)"
@ignore-item="store.ignoreItem"
@unignore-item="store.unignoreItem"
/>
<div
v-if="store.loading.value && store.filteredItems.value.length === 0"
class="flex min-h-0 flex-1 items-center justify-center p-6"
>
<div class="flex flex-col items-center gap-3 text-slate-500 dark:text-slate-400">
<i class="fas fa-circle-notch fa-spin text-3xl"></i>
<p class="text-sm">正在检查更新</p>
</div>
</div>
<template v-else>
<div
v-if="store.loading.value && store.filteredItems.value.length > 0"
class="border-b border-slate-200/70 px-6 py-2 text-center text-xs text-slate-400 dark:border-slate-800/70 dark:text-slate-500"
>
正在刷新更新列表
</div>
<div class="flex min-h-0 flex-1">
<UpdateCenterList
:items="store.filteredItems.value"
:tasks="store.snapshot.value.tasks"
:selected-task-keys="store.selectedTaskKeys.value"
@toggle-selection="emit('toggle-selection', $event)"
@ignore-item="store.ignoreItem"
@unignore-item="store.unignoreItem"
/>
</div>
</template>
<UpdateCenterMigrationConfirm
:show="store.showMigrationConfirm.value"
@@ -14,11 +14,12 @@
<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"
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 disabled:opacity-40 dark:border-slate-700 dark:text-slate-200 dark:hover:bg-slate-800"
:disabled="loading"
@click="$emit('refresh')"
>
<i class="fas fa-sync-alt"></i>
刷新
<i class="fas fa-sync-alt" :class="{ 'animate-spin': loading }"></i>
{{ loading ? "刷新中" : "刷新" }}
</button>
<button
type="button"
@@ -88,6 +89,7 @@ const props = defineProps<{
selectedCount: number;
allSelected: boolean;
someSelected: boolean;
loading?: boolean;
}>();
const emit = defineEmits<{
+18 -4
View File
@@ -19,6 +19,7 @@ const EMPTY_SNAPSHOT: UpdateCenterSnapshot = {
export interface UpdateCenterStore {
isOpen: Ref<boolean>;
loading: Ref<boolean>;
showCloseConfirm: Ref<boolean>;
showMigrationConfirm: Ref<boolean>;
searchQuery: Ref<string>;
@@ -54,6 +55,7 @@ const matchesSearch = (item: UpdateCenterItem, query: string): boolean => {
export const createUpdateCenterStore = (): UpdateCenterStore => {
const isOpen = ref(false);
const loading = ref(false);
const showCloseConfirm = ref(false);
const showMigrationConfirm = ref(false);
const searchQuery = ref("");
@@ -134,17 +136,27 @@ export const createUpdateCenterStore = (): UpdateCenterStore => {
const open = async (storeFilter: StoreFilter = "both"): Promise<void> => {
lastStoreFilter = storeFilter;
resetSessionState();
const nextSnapshot = await window.updateCenter.open(storeFilter);
applySnapshot(nextSnapshot);
isOpen.value = true;
loading.value = true;
try {
const nextSnapshot = await window.updateCenter.open(storeFilter);
applySnapshot(nextSnapshot);
} finally {
loading.value = false;
}
};
const refresh = async (
storeFilter: StoreFilter = lastStoreFilter,
): Promise<void> => {
lastStoreFilter = storeFilter;
const nextSnapshot = await window.updateCenter.refresh(storeFilter);
applySnapshot(nextSnapshot);
loading.value = true;
try {
const nextSnapshot = await window.updateCenter.refresh(storeFilter);
applySnapshot(nextSnapshot);
} finally {
loading.value = false;
}
};
const ignoreItem = async (
@@ -197,6 +209,7 @@ export const createUpdateCenterStore = (): UpdateCenterStore => {
const closeNow = (): void => {
resetSessionState();
loading.value = false;
isOpen.value = false;
};
@@ -270,6 +283,7 @@ export const createUpdateCenterStore = (): UpdateCenterStore => {
return {
isOpen,
loading,
showCloseConfirm,
showMigrationConfirm,
searchQuery,