Files
win-softmgr/src/views/Essentials.vue
2026-03-14 19:04:06 -04:00

244 lines
5.5 KiB
Vue

<template>
<main class="content">
<header class="content-header">
<div class="header-left">
<h1>装机必备</h1>
</div>
<div class="header-actions">
<button
@click="store.syncDataIfNeeded(true)"
class="secondary-btn action-btn"
:disabled="store.loading"
>
<span class="icon" :class="{ 'spinning': store.loading }">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 2v6h-6"></path>
<path d="M3 12a9 9 0 0 1 15-6.7L21 8"></path>
<path d="M3 22v-6h6"></path>
<path d="M21 12a9 9 0 0 1-15 6.7L3 16"></path>
</svg>
</span>
{{ store.loading ? '正在同步...' : '同步状态' }}
</button>
<button
@click="installSelected"
class="primary-btn action-btn"
:disabled="store.loading || store.selectedEssentialIds.length === 0"
>
安装/更新所选 ({{ store.selectedEssentialIds.length }})
</button>
</div>
</header>
<!-- 批量选择控制栏 -->
<div class="selection-toolbar" v-if="selectableItems.length > 0">
<div class="toolbar-left">
<span class="selection-count">已选 {{ store.selectedEssentialIds.length }} / {{ selectableItems.length }} </span>
</div>
<div class="toolbar-actions">
<button @click="store.selectAll('essential')" class="text-btn">全选</button>
<div class="divider"></div>
<button @click="store.deselectAll('essential')" class="text-btn">取消</button>
<div class="divider"></div>
<button @click="store.invertSelection('essential')" class="text-btn">反选</button>
</div>
</div>
<div v-if="store.loading && store.mergedEssentials.length === 0" class="loading-state">
<div class="spinner"></div>
<p>正在读取必备软件列表...</p>
</div>
<div v-else class="software-list">
<SoftwareCard
v-for="item in store.mergedEssentials"
:key="item.id"
:software="item"
:action-label="item.actionLabel"
:selectable="true"
:is-selected="store.selectedEssentialIds.includes(item.id)"
@install="store.install"
@toggle-select="id => store.toggleSelection(id, 'essential')"
/>
</div>
</main>
</template>
<script setup lang="ts">
import SoftwareCard from '../components/SoftwareCard.vue';
import { useSoftwareStore } from '../store/software';
import { onMounted, computed } from 'vue';
const store = useSoftwareStore();
const selectableItems = computed(() => {
return store.mergedEssentials.filter(s => s.status !== 'installed');
});
const installSelected = () => {
store.selectedEssentialIds.forEach(id => {
store.install(id);
});
};
onMounted(() => {
store.syncDataIfNeeded();
store.initListener();
});
</script>
<style scoped>
.content {
flex: 1;
padding: 40px 60px;
overflow-y: auto;
}
.content-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.content-header h1 {
font-size: 32px;
font-weight: 700;
letter-spacing: -0.5px;
color: var(--text-main);
}
.header-actions {
display: flex;
gap: 12px;
align-items: center;
}
/* 批量选择工具栏 */
.selection-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 20px;
background-color: rgba(0, 0, 0, 0.02);
border-radius: 12px;
margin-bottom: 20px;
border: 1px solid rgba(0, 0, 0, 0.03);
}
.selection-count {
font-size: 13px;
color: var(--text-sec);
font-weight: 500;
}
.toolbar-actions {
display: flex;
align-items: center;
gap: 8px;
}
.text-btn {
background: none;
border: none;
font-size: 13px;
font-weight: 600;
color: var(--primary-color);
cursor: pointer;
padding: 4px 8px;
border-radius: 6px;
transition: all 0.2s ease;
}
.text-btn:hover {
background-color: rgba(0, 122, 255, 0.08);
}
.divider {
width: 1px;
height: 12px;
background-color: var(--border-color);
}
.action-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
border-radius: 12px;
font-weight: 600;
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
border: none;
white-space: nowrap;
}
.primary-btn {
background-color: var(--primary-color);
color: white;
box-shadow: var(--btn-shadow);
}
.primary-btn:hover {
background-color: var(--primary-hover);
transform: translateY(-1px);
}
.secondary-btn {
background-color: var(--bg-light);
color: var(--text-main);
border: 1px solid var(--border-color);
}
.secondary-btn:hover {
background-color: white;
border-color: var(--primary-color);
color: var(--primary-color);
}
.action-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none !important;
box-shadow: none;
}
.icon {
display: flex;
align-items: center;
}
.icon.spinning {
animation: spin 1s linear infinite;
}
.software-list {
display: flex;
flex-direction: column;
}
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 300px;
color: var(--text-sec);
}
.spinner {
width: 32px;
height: 32px;
border: 3px solid rgba(0, 122, 255, 0.1);
border-top-color: var(--primary-color);
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 16px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>