250 lines
8.2 KiB
TypeScript
250 lines
8.2 KiB
TypeScript
import { defineStore } from 'pinia'
|
||
import { invoke } from '@tauri-apps/api/core'
|
||
import { listen } from '@tauri-apps/api/event'
|
||
|
||
const sortByName = (a: any, b: any) => a.name.localeCompare(b.name, 'zh-CN', { sensitivity: 'accent' });
|
||
|
||
export interface LogEntry {
|
||
id: string; // 日志唯一标识
|
||
timestamp: string;
|
||
command: string;
|
||
output: string;
|
||
status: 'info' | 'success' | 'error';
|
||
}
|
||
|
||
export const useSoftwareStore = defineStore('software', {
|
||
state: () => ({
|
||
essentials: [] as any[],
|
||
essentialsVersion: '',
|
||
updates: [] as any[],
|
||
allSoftware: [] as any[],
|
||
selectedEssentialIds: [] as string[],
|
||
selectedUpdateIds: [] as string[],
|
||
logs: [] as LogEntry[],
|
||
settings: {
|
||
repo_url: 'https://karlblue.github.io/winget-repo'
|
||
},
|
||
loading: false,
|
||
isInitialized: false,
|
||
initStatus: '正在检查系统环境...',
|
||
lastFetched: 0
|
||
}),
|
||
getters: {
|
||
// ... (mergedEssentials, sortedUpdates, sortedAllSoftware, isBusy getters stay the same)
|
||
mergedEssentials: (state) => {
|
||
return state.essentials.map(item => {
|
||
const installedInfo = state.allSoftware.find(s => s.id.toLowerCase() === item.id.toLowerCase());
|
||
const isInstalled = !!installedInfo;
|
||
const hasUpdate = state.updates.some(s => s.id.toLowerCase() === item.id.toLowerCase());
|
||
|
||
let displayStatus = item.status;
|
||
let actionLabel = '安装';
|
||
|
||
// 统一字段:version 始终代表当前安装的版本,recommended_version 代表清单推荐的版本
|
||
const currentVersion = installedInfo ? installedInfo.version : null;
|
||
const recommendedVersion = item.version;
|
||
|
||
if (isInstalled) {
|
||
if (hasUpdate) {
|
||
actionLabel = '更新';
|
||
} else if (displayStatus === 'idle') {
|
||
displayStatus = 'installed';
|
||
actionLabel = '已安装';
|
||
}
|
||
}
|
||
|
||
return {
|
||
...item,
|
||
version: currentVersion,
|
||
recommended_version: recommendedVersion,
|
||
status: displayStatus,
|
||
actionLabel
|
||
};
|
||
});
|
||
},
|
||
sortedUpdates: (state) => [...state.updates].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 = '正在加载应用配置...';
|
||
try {
|
||
this.settings = await invoke('get_settings');
|
||
this.initStatus = '正在同步 Winget 模块...';
|
||
await invoke('initialize_app');
|
||
this.isInitialized = true;
|
||
} catch (err) {
|
||
this.initStatus = '环境配置失败,请检查运行日志';
|
||
setTimeout(() => { this.isInitialized = true; }, 2000);
|
||
}
|
||
},
|
||
|
||
async saveSettings(newSettings: any) {
|
||
await invoke('save_settings', { settings: newSettings });
|
||
this.settings = newSettings;
|
||
},
|
||
|
||
async syncEssentials() {
|
||
this.loading = true;
|
||
try {
|
||
await invoke('sync_essentials');
|
||
await this.fetchEssentials();
|
||
} finally {
|
||
this.loading = false;
|
||
}
|
||
},
|
||
|
||
// ... (Selection methods stay the same)
|
||
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);
|
||
else list.splice(index, 1);
|
||
},
|
||
selectAll(type: 'essential' | 'update') {
|
||
if (type === 'essential') {
|
||
const selectable = this.mergedEssentials.filter(s => s.status !== 'installed');
|
||
this.selectedEssentialIds = selectable.map(s => s.id);
|
||
} else {
|
||
this.selectedUpdateIds = this.updates.map(s => s.id);
|
||
}
|
||
},
|
||
deselectAll(type: 'essential' | 'update') {
|
||
if (type === 'essential') this.selectedEssentialIds = [];
|
||
else this.selectedUpdateIds = [];
|
||
},
|
||
invertSelection(type: 'essential' | 'update') {
|
||
if (type === 'essential') {
|
||
const selectable = this.mergedEssentials.filter(s => s.status !== 'installed').map(s => s.id);
|
||
this.selectedEssentialIds = selectable.filter(id => !this.selectedEssentialIds.includes(id));
|
||
} else {
|
||
const selectable = this.updates.map(s => s.id);
|
||
this.selectedUpdateIds = selectable.filter(id => !this.selectedUpdateIds.includes(id));
|
||
}
|
||
},
|
||
|
||
async fetchEssentials() {
|
||
let repo = await invoke('get_essentials') as any;
|
||
|
||
// 如果本地没有文件,则尝试联网获取一次
|
||
if (!repo) {
|
||
try {
|
||
await invoke('sync_essentials');
|
||
repo = await invoke('get_essentials') as any;
|
||
} catch (err) {
|
||
console.error('Initial sync failed:', err);
|
||
}
|
||
}
|
||
|
||
if (repo) {
|
||
this.essentials = repo.essentials;
|
||
this.essentialsVersion = repo.version;
|
||
} else {
|
||
this.essentials = [];
|
||
this.essentialsVersion = '';
|
||
}
|
||
},
|
||
async fetchUpdates() {
|
||
if (this.isBusy) return;
|
||
this.loading = true
|
||
try {
|
||
const res = await invoke('get_updates')
|
||
this.updates = res as any[]
|
||
if (this.selectedUpdateIds.length === 0) this.selectAll('update');
|
||
} finally {
|
||
this.loading = false
|
||
}
|
||
},
|
||
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)) {
|
||
if (this.essentials.length === 0) await this.fetchEssentials();
|
||
return;
|
||
}
|
||
await this.fetchAllData();
|
||
},
|
||
async fetchAllData() {
|
||
this.loading = true;
|
||
try {
|
||
// 先确保加载了必备清单(内部处理本地缺失逻辑)
|
||
await this.fetchEssentials();
|
||
|
||
// 然后同步本地软件安装/更新状态,不再强制联网下载 JSON
|
||
const [all, updates] = await Promise.all([
|
||
invoke('get_installed_software'),
|
||
invoke('get_updates')
|
||
]);
|
||
|
||
this.allSoftware = all as any[];
|
||
this.updates = updates as any[];
|
||
this.lastFetched = Date.now();
|
||
if (this.selectedEssentialIds.length === 0) this.selectAll('essential');
|
||
} finally {
|
||
this.loading = false;
|
||
}
|
||
},
|
||
async install(id: string) {
|
||
const software = this.findSoftware(id)
|
||
if (software) {
|
||
software.status = 'pending';
|
||
await invoke('install_software', {
|
||
id,
|
||
version: software.version,
|
||
use_manifest: software.use_manifest,
|
||
manifest_url: software.manifest_url
|
||
})
|
||
}
|
||
},
|
||
findSoftware(id: string) {
|
||
return this.essentials.find(s => s.id === id) ||
|
||
this.updates.find(s => s.id === id) ||
|
||
this.allSoftware.find(s => s.id === id)
|
||
},
|
||
initListener() {
|
||
if ((window as any).__tauri_listener_init) return;
|
||
(window as any).__tauri_listener_init = true;
|
||
|
||
listen('install-status', (event: any) => {
|
||
const { id, status, progress } = event.payload
|
||
const software = this.findSoftware(id)
|
||
if (software) {
|
||
software.status = status
|
||
software.progress = progress
|
||
}
|
||
if (status === 'success') {
|
||
this.lastFetched = 0;
|
||
this.selectedEssentialIds = this.selectedEssentialIds.filter(i => i !== id);
|
||
this.selectedUpdateIds = this.selectedUpdateIds.filter(i => i !== id);
|
||
}
|
||
})
|
||
|
||
// 日志监听:根据 ID 追加内容
|
||
listen('log-event', (event: any) => {
|
||
const payload = event.payload as LogEntry;
|
||
const existingLog = this.logs.find(l => l.id === payload.id);
|
||
|
||
if (existingLog) {
|
||
// 如果是增量更新
|
||
if (payload.output) {
|
||
existingLog.output += '\n' + payload.output;
|
||
}
|
||
if (payload.status !== 'info') {
|
||
existingLog.status = payload.status;
|
||
}
|
||
} else {
|
||
// 如果是新日志
|
||
this.logs.unshift(payload);
|
||
if (this.logs.length > 100) this.logs.pop();
|
||
}
|
||
})
|
||
}
|
||
}
|
||
})
|