From 1c4587e9451c7aa55e2f2539ecf66017c1cad5da Mon Sep 17 00:00:00 2001 From: Julian Freeman Date: Sat, 14 Mar 2026 21:47:04 -0400 Subject: [PATCH] fix updating ui --- src/components/SoftwareCard.vue | 80 ++++++++++++++++++++++----------- src/store/software.ts | 20 ++++++--- src/views/AllSoftware.vue | 10 ++++- src/views/Essentials.vue | 22 +++++---- src/views/Updates.vue | 22 +++++---- 5 files changed, 103 insertions(+), 51 deletions(-) diff --git a/src/components/SoftwareCard.vue b/src/components/SoftwareCard.vue index 76baaa1..18ec9ef 100644 --- a/src/components/SoftwareCard.vue +++ b/src/components/SoftwareCard.vue @@ -3,7 +3,8 @@ class="software-card" :class="{ 'installed-mode': software.status === 'installed', - 'is-selected': isSelected && software.status !== 'installed' + 'is-selected': isSelected && software.status !== 'installed', + 'is-busy': software.status === 'pending' || software.status === 'installing' }" @click="handleCardClick" > @@ -13,7 +14,7 @@ class="checkbox" :class="{ 'checked': isSelected, - 'disabled': software.status === 'installed' + 'disabled': software.status === 'installed' || software.status === 'pending' || software.status === 'installing' }" > @@ -60,15 +61,16 @@ 已安装 -
-
- - - - -
+ +
+ + 等待中 +
+ + +
+
+ 正在安装
@@ -114,6 +116,9 @@ const placeholderColor = computed(() => { }); const handleCardClick = () => { + // 安装或等待中禁止修改勾选 + if (props.software.status === 'pending' || props.software.status === 'installing') return; + if (props.selectable && props.software.status !== 'installed') { emit('toggleSelect', props.software.id); } @@ -135,7 +140,7 @@ const handleCardClick = () => { cursor: default; } -.software-card:not(.installed-mode):hover { +.software-card:not(.installed-mode):not(.is-busy):hover { border-color: rgba(0, 122, 255, 0.3); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.06); } @@ -145,6 +150,10 @@ const handleCardClick = () => { border-color: rgba(0, 122, 255, 0.4); } +.software-card.is-busy { + opacity: 0.8; +} + /* 勾选框样式 */ .selection-area { margin-right: 16px; @@ -302,31 +311,46 @@ const handleCardClick = () => { cursor: not-allowed; } +.status-pending { + display: flex; + align-items: center; + gap: 6px; + width: 90px; + justify-content: center; + color: #AEAEB2; +} + +.wait-icon { + font-size: 14px; +} + +.wait-text { + font-size: 12px; + font-weight: 600; +} + .progress-status { display: flex; align-items: center; - gap: 10px; + flex-direction: column; + gap: 4px; width: 90px; justify-content: center; } -.progress-ring { +.spinning-loader { width: 18px; height: 18px; + border: 2px solid rgba(0, 122, 255, 0.1); + border-top-color: var(--primary-color); + border-radius: 50%; + animation: spin 1s linear infinite; } -.progress-ring svg { - transform: rotate(-90deg); -} - -.progress-ring .bg { - stroke: var(--border-color); -} - -.progress-ring .fg { - stroke: var(--primary-color); - stroke-linecap: round; - transition: stroke-dashoffset 0.3s ease; +.loading-text { + font-size: 10px; + font-weight: 700; + color: var(--primary-color); } .status-success, .status-error { @@ -343,4 +367,8 @@ const handleCardClick = () => { .status-error { color: #FF3B30; } + +@keyframes spin { + to { transform: rotate(360deg); } +} diff --git a/src/store/software.ts b/src/store/software.ts index 3121190..94fd0bf 100644 --- a/src/store/software.ts +++ b/src/store/software.ts @@ -46,24 +46,27 @@ export const useSoftwareStore = defineStore('software', { }); }, sortedUpdates: (state) => [...state.updates].sort(sortByName), - sortedAllSoftware: (state) => [...state.allSoftware].sort(sortByName) + sortedAllSoftware: (state) => [...state.allSoftware].sort(sortByName), + // 全局繁忙状态:只要有任何软件在等待或安装中,就锁定关键操作 + isBusy: (state) => { + const allItems = [...state.essentials, ...state.updates, ...state.allSoftware]; + return allItems.some(item => item.status === 'pending' || item.status === 'installing'); + } }, actions: { async initializeApp() { if (this.isInitialized) return; - this.initStatus = '正在同步 Winget 模块...'; try { await invoke('initialize_app'); this.isInitialized = true; } catch (err) { this.initStatus = '环境配置失败,请检查运行日志'; - console.error('Init failed:', err); - // 即使失败也允许进入,让用户看日志 setTimeout(() => { this.isInitialized = true; }, 2000); } }, toggleSelection(id: string, type: 'essential' | 'update') { + if (this.isBusy) return; // 繁忙时禁止修改勾选 const list = type === 'essential' ? this.selectedEssentialIds : this.selectedUpdateIds; const index = list.indexOf(id); if (index === -1) list.push(id); @@ -95,6 +98,7 @@ export const useSoftwareStore = defineStore('software', { this.essentials = await invoke('get_essentials') }, async fetchUpdates() { + if (this.isBusy) return; this.loading = true try { const res = await invoke('get_updates') @@ -105,6 +109,7 @@ export const useSoftwareStore = defineStore('software', { } }, async fetchAll() { + if (this.isBusy) return; this.loading = true try { const res = await invoke('get_all_software') @@ -114,6 +119,7 @@ export const useSoftwareStore = defineStore('software', { } }, async syncDataIfNeeded(force = false) { + if (this.isBusy) return; const now = Date.now(); const CACHE_TIMEOUT = 5 * 60 * 1000; if (!force && this.allSoftware.length > 0 && (now - this.lastFetched < CACHE_TIMEOUT)) { @@ -140,8 +146,12 @@ export const useSoftwareStore = defineStore('software', { } }, async install(id: string) { + // 这里的 logic 同时负责单个安装和批量安装的任务入队 const software = this.findSoftware(id) - if (software) software.status = 'pending' + if (software) { + // 进入队列前统一标记为等待中 + software.status = 'pending'; + } await invoke('install_software', { id }) }, findSoftware(id: string) { diff --git a/src/views/AllSoftware.vue b/src/views/AllSoftware.vue index c81db05..ebb3501 100644 --- a/src/views/AllSoftware.vue +++ b/src/views/AllSoftware.vue @@ -9,7 +9,7 @@
@@ -118,7 +119,7 @@ const filteredSoftware = computed(() => { white-space: nowrap; } -.action-btn:hover { +.action-btn:hover:not(:disabled) { background-color: white; border-color: var(--primary-color); color: var(--primary-color); @@ -154,6 +155,11 @@ const filteredSoftware = computed(() => { box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.1); } +.search-input:disabled { + background-color: #F2F2F7; + cursor: not-allowed; +} + .software-list { display: flex; flex-direction: column; diff --git a/src/views/Essentials.vue b/src/views/Essentials.vue index 85ec484..34fb577 100644 --- a/src/views/Essentials.vue +++ b/src/views/Essentials.vue @@ -8,7 +8,7 @@ +
- +
- + @@ -150,10 +150,15 @@ onMounted(() => { transition: all 0.2s ease; } -.text-btn:hover { +.text-btn:hover:not(:disabled) { background-color: rgba(0, 122, 255, 0.08); } +.text-btn:disabled { + color: #AEAEB2; + cursor: not-allowed; +} + .divider { width: 1px; height: 12px; @@ -180,7 +185,7 @@ onMounted(() => { box-shadow: var(--btn-shadow); } -.primary-btn:hover { +.primary-btn:hover:not(:disabled) { background-color: var(--primary-hover); } @@ -190,7 +195,7 @@ onMounted(() => { border: 1px solid var(--border-color); } -.secondary-btn:hover { +.secondary-btn:hover:not(:disabled) { background-color: white; border-color: var(--primary-color); color: var(--primary-color); @@ -199,7 +204,6 @@ onMounted(() => { .action-btn:disabled { opacity: 0.5; cursor: not-allowed; - transform: none !important; box-shadow: none; } diff --git a/src/views/Updates.vue b/src/views/Updates.vue index b426a1a..2c8250a 100644 --- a/src/views/Updates.vue +++ b/src/views/Updates.vue @@ -8,7 +8,7 @@ +
- +
- + @@ -151,10 +151,15 @@ onMounted(() => { transition: all 0.2s ease; } -.text-btn:hover { +.text-btn:hover:not(:disabled) { background-color: rgba(0, 122, 255, 0.08); } +.text-btn:disabled { + color: #AEAEB2; + cursor: not-allowed; +} + .divider { width: 1px; height: 12px; @@ -181,7 +186,7 @@ onMounted(() => { box-shadow: var(--btn-shadow); } -.primary-btn:hover { +.primary-btn:hover:not(:disabled) { background-color: var(--primary-hover); } @@ -191,7 +196,7 @@ onMounted(() => { border: 1px solid var(--border-color); } -.secondary-btn:hover { +.secondary-btn:hover:not(:disabled) { background-color: white; border-color: var(--primary-color); color: var(--primary-color); @@ -200,7 +205,6 @@ onMounted(() => { .action-btn:disabled { opacity: 0.5; cursor: not-allowed; - transform: none !important; } .icon {