import { defineStore } from 'pinia' import { call, on, onTasksChanged } from './api' export const useAppStore = defineStore('app', { state: () => ({ bootstrap: { state: 'loading' }, projects: [], projectGroups: [], selectedProjectGroupId: Number(localStorage.getItem('cc-project-group-id') || 0), dashboard: { projects: 0, totalLines: 0, commits: 0 }, settings: { theme: 'dark', locale: 'zh-CN', glassOpacity: 55, gitScope: 'current', autoRefresh: true, loadingStyle: 'fullscreen-orbit', imageMode: 'base64' }, tasks: {}, batchTaskIds: [], batchSummary: null, pendingAction: '', favorites: [], unreadMessages: 0, // 左侧导航徽标(消息未读/待办/工单/今日到期/日历红点/团队指派) badges: { unread: 0, todosOpen: 0, ticketsActive: 0, todayDue: 0, calendarDot: false, teamAssigned: 0 }, toast: null, toastTimer: null, toastLeaveTimer: null, loading: false, syncStatus: { configured: false, loggedIn: false, userId: 0, username: '', online: false, syncing: false }, avatarSrc: '', loginOpen: false, paletteOpen: false, // 日历页"每日心语"入口:设为 'YYYY-MM-DD' 时 DailyCard 打开对应日期的卡片 dailyCardDate: '', // 节日名 → { mode: 'photo'|'art', image: dataURL }(管理员上传,随同步分发) festivalImages: {}, // 探测到加载失败的节日图(key → true),日历格自动回退动态插画 festBroken: {} }), actions: { async boot() { this.bootstrap = await call('GetBootstrapStatus') if (this.bootstrap.state === 'ready') { this.settings = await call('GetSettings') this.applyAppearance(this.settings) try { this.syncStatus = await call('GetSyncStatus') } catch {} this.loadFestivalImages() await this.refresh() } }, async loadFestivalImages() { try { this.festivalImages = (await call('ListFestivalImages')) || {} } catch { return } // 预探测 photo 模式的图片:解码失败的标记为 broken,展示时回退动态插画 for (const [key, it] of Object.entries(this.festivalImages)) { if (it.mode !== 'photo' || !it.image) continue const probe = new Image() probe.onerror = () => { this.festBroken = { ...this.festBroken, [key]: true } } probe.onload = () => { if (this.festBroken[key]) { const n = { ...this.festBroken }; delete n[key]; this.festBroken = n } } probe.src = it.image } }, applyAppearance(settings) { this.settings = { ...this.settings, ...settings } let theme = this.settings.theme || 'dark' if (theme === 'system') theme = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' document.documentElement.dataset.theme = theme document.documentElement.style.setProperty('--glass-user-opacity', String((this.settings.glassOpacity || 55) / 100)) localStorage.setItem('cc-settings', JSON.stringify(this.settings)) this.resolveAvatar() }, // resolveAvatar 把设置中的头像解析为可显示的 源:path 模式需经后端读文件转 dataURL。 async resolveAvatar() { const { avatarMode, avatarValue } = this.settings if (!avatarMode || !avatarValue) { this.avatarSrc = ''; return } if (avatarMode === 'path') { try { this.avatarSrc = await call('ReadImageAsDataURL', avatarValue) } catch { this.avatarSrc = '' } return } this.avatarSrc = avatarValue }, async refreshSyncStatus() { try { this.syncStatus = await call('GetSyncStatus') } catch {} }, // openAccount 是所有登录入口的统一行为:未登录弹全屏登录层,已登录进个人主页。 openAccount(router) { if (this.syncStatus.loggedIn) router?.push('/profile') else this.loginOpen = true }, async saveSettings(patch) { const next = { ...this.settings, ...patch } await call('SaveSettings', next) this.applyAppearance(next) return next }, showToast(payload) { clearTimeout(this.toastTimer) clearTimeout(this.toastLeaveTimer) this.toast = { ...payload, leaving: false, muted: false, id: Date.now() } this.toastTimer = setTimeout(() => { if (this.toast) this.toast.muted = true }, 3000) this.toastLeaveTimer = setTimeout(() => { if (this.toast) this.toast.leaving = true setTimeout(() => { this.toast = null }, 360) }, 5000) }, closeToast() { clearTimeout(this.toastTimer) clearTimeout(this.toastLeaveTimer) if (this.toast) this.toast.leaving = true setTimeout(() => { this.toast = null }, 220) }, async refresh() { this.loading = true this.refreshSyncStatus() try { this.projectGroups = await call('ListProjectGroups') if (this.selectedProjectGroupId && !this.projectGroups.some(g => g.id === this.selectedProjectGroupId)) { this.setProjectGroup(0) } const groupId = this.selectedProjectGroupId || 0 ;[this.projects, this.dashboard, this.favorites, this.unreadMessages] = await Promise.all([ groupId ? call('ListProjectsByGroup', groupId) : call('ListProjects'), groupId ? call('GetDashboardByGroup', groupId) : call('GetDashboard'), call('ListFavorites').catch(() => []), call('UnreadMessageCount').catch(() => 0) ]) } finally { this.loading = false } }, async toggleFavorite(projectId) { const fav = await call('ToggleFavorite', projectId) this.favorites = fav ? [projectId, ...this.favorites.filter(x => x !== projectId)] : this.favorites.filter(x => x !== projectId) return fav }, async refreshUnread() { try { this.unreadMessages = await call('UnreadMessageCount') } catch {} this.refreshBadges() }, async refreshBadges() { try { this.badges = await call('GetNavBadges') } catch {} }, async changeProjectGroup(groupId) { this.setProjectGroup(groupId) await this.refresh() }, setProjectGroup(groupId, persist = true) { this.selectedProjectGroupId = Number(groupId) || 0 if (persist) localStorage.setItem('cc-project-group-id', String(this.selectedProjectGroupId)) }, listen() { const offProgress = on('analysis:progress', e => { delete this.tasks.__batch_pending__ this.tasks[e.taskId] = e if (['completed', 'error', 'cancelled'].includes(e.stage)) { if (this.batchTaskIds.includes(e.taskId)) { this.batchTaskIds = this.batchTaskIds.filter(id => id !== e.taskId) if (this.batchTaskIds.length) { this.tasks.__batch_pending__ = { taskId: '__batch_pending__', projectId: 0, stage: 'queued', progress: 100, messageKey: e.messageKey, params: e.params } } } this.showToast({ type: e.stage === 'completed' ? 'success' : 'error', key: e.messageKey, params: e.params }) this.refresh() } }) const offBatch = on('batch:done', s => { this.batchSummary = s this.batchTaskIds = [] delete this.tasks.__batch_pending__ this.showToast({ type: s.failed ? 'error' : 'success', key: 'batchDoneToast', params: { completed: s.completed, total: s.total } }) this.refresh() }) const offMessage = on('message:new', () => { this.unreadMessages += 1; this.refreshBadges() }) const offSync = on('sync:done', async st => { this.syncStatus = st || this.syncStatus this.refreshBadges() // 拉取可能更新了设置(头像 / API Key),刷新本地副本 try { this.applyAppearance(await call('GetSettings')) } catch {} // 拉取可能带来新项目/分组/收藏,刷新项目列表 if (st?.pulled > 0) { this.refresh().catch(() => {}) this.loadFestivalImages() } }) // 徽标:启动即取,任务变更事件 + 60s 轮询兜底 this.refreshBadges() const offTasks = onTasksChanged(() => this.refreshBadges()) const badgeTimer = setInterval(() => this.refreshBadges(), 60000) return () => { offProgress?.(); offBatch?.(); offMessage?.(); offSync?.(); offTasks?.(); clearInterval(badgeTimer) } }, async analyze(id, kind = 'all') { const task = await call('StartAnalysis', id, kind) this.tasks[task] = { taskId: task, projectId: id, stage: 'start', progress: 1, messageKey: 'task.start' } return task }, async batchAnalyze(groupId = 0) { const ids = groupId ? await call('StartBatchAnalysisByGroup', groupId) : await call('StartBatchAnalysis') this.batchTaskIds = ids || [] if (this.batchTaskIds.length && !Object.values(this.tasks).some(x => !['completed', 'error', 'cancelled'].includes(x.stage))) { this.tasks.__batch_pending__ = { taskId: '__batch_pending__', projectId: 0, stage: 'queued', progress: 1, messageKey: 'task.start' } } return this.batchTaskIds } } })