feat: 提交详情能看 diff,也能丢给 AI 审这一次改动。文件检查可单独排除路径,TODO 只认大写,避免把 todo 表和模块当成待办标记。

热力图按容器宽度铺满一年,不再横向滚动。托盘右键换成可换皮肤的弹层;更新下载显示进度,退出后再由脚本拉起安装器,避免还占着 exe。
This commit is contained in:
李琦
2026-08-19 15:46:03 +08:00
parent 1694397245
commit 7c4109f687
38 changed files with 2618 additions and 129 deletions

View File

@@ -19,6 +19,7 @@ import TeamSwitcher from './components/TeamSwitcher.vue'
import TitleBar from './components/TitleBar.vue'
import CloudClaimModal from './components/CloudClaimModal.vue'
import { call, isNative, on } from './api'
import TrayPopup from './views/TrayPopup.vue'
const route = useRoute()
const router = useRouter()
@@ -30,7 +31,25 @@ const aboutOpen = ref(false)
const exitOpen = ref(false)
const updateInfo = ref(null)
const updateBusy = ref(false)
const updateProgress = ref({ received: 0, total: 0 })
const appVersion = ref('1.0.0')
const updatePercent = computed(() => {
const total = Number(updateProgress.value.total) || 0
const received = Number(updateProgress.value.received) || 0
if (total <= 0) return 0
return Math.min(100, Math.round(received / total * 100))
})
function fmtSize(n) {
n = Number(n) || 0
if (n < 1024) return `${n} B`
if (n < 1048576) return `${(n / 1024).toFixed(1)} KB`
return `${(n / 1048576).toFixed(1)} MB`
}
function errText(e) {
const code = String(e).split(':')[0].trim()
const key = 'errors.' + code
return t(key) !== key ? t(key) : String(e)
}
const displayedTask = ref(null)
let off
let offMenus = []
@@ -41,6 +60,7 @@ const visibleTask = computed(() => activeTask.value || displayedTask.value)
const activeTaskProject = computed(() => visibleTask.value?.params?.project || store.projects.find(p => p.id === visibleTask.value?.projectId)?.name || '')
const loadingStyle = computed(() => store.settings.loadingStyle === 'fullscreen' ? 'fullscreen-orbit' : (store.settings.loadingStyle || 'fullscreen-orbit'))
const useFullscreenLoading = computed(() => loadingStyle.value !== 'bar')
const isTrayWindow = document.documentElement.classList.contains('tray-window')
async function updateSetting(key, value) {
const next = await store.saveSettings({ [key]: value })
@@ -220,6 +240,14 @@ function onGlobalKey(e) {
}
}
onMounted(async () => {
if (isTrayWindow) {
document.documentElement.classList.add('tray-window')
try {
store.applyAppearance(await call('GetSettings'))
locale.value = store.settings.locale || 'zh-CN'
} catch { /* 托盘弹层只需要外观,失败时沿用本地缓存 */ }
return
}
addEventListener('keydown', onGlobalKey)
addEventListener('click', onFlyoutAway)
if (!native) return
@@ -241,6 +269,9 @@ onMounted(async () => {
on('menu:set', p => p?.key && updateSetting(p.key, p.value)),
on('app:update-available', p => {
if (p && !p.upToDate && !p.skipped) updateInfo.value = p
}),
on('app:update-progress', p => {
if (p) updateProgress.value = { received: Number(p.received) || 0, total: Number(p.total) || 0 }
})
]
await store.boot()
@@ -248,15 +279,18 @@ onMounted(async () => {
try { appVersion.value = await call('GetAppVersion') } catch { /* keep fallback */ }
})
async function installUpdate() {
if (updateBusy.value) return
updateBusy.value = true
updateProgress.value = { received: 0, total: Number(updateInfo.value?.sizeBytes) || 0 }
try {
await call('DownloadAndInstallUpdate')
} catch (e) {
store.showToast({ type: 'error', text: String(e) })
store.showToast({ type: 'error', text: errText(e) })
updateBusy.value = false
}
}
async function skipUpdate() {
if (updateBusy.value) return
try { await call('SkipAppUpdate', updateInfo.value?.latest || '') } catch {}
updateInfo.value = null
}
@@ -285,7 +319,8 @@ watch(activeTask, task => {
</script>
<template>
<BrowserBlocked v-if="!native" />
<TrayPopup v-if="isTrayWindow" />
<BrowserBlocked v-else-if="!native" />
<template v-else>
<TitleBar :app-version="appVersion" :shortcuts="pinnedShortcuts" @about="aboutOpen = true" @manage-shortcuts="shortcutPicker = true" />
<DatabaseSetup v-if="store.bootstrap.state !== 'ready' && store.bootstrap.state !== 'loading'" :status="store.bootstrap" class="with-titlebar" />
@@ -382,10 +417,15 @@ watch(activeTask, task => {
<section class="modal exit-modal" @click.stop>
<header class="modal-head">
<h2>{{ t('appUpdateTitle') }}</h2>
<button type="button" class="nm-close" :title="t('close')" @click="skipUpdate"><X /></button>
<button type="button" class="nm-close" :title="t('close')" :disabled="updateBusy" @click="skipUpdate"><X /></button>
</header>
<p class="exit-hint">{{ t('appUpdateBody', { current: updateInfo.current, latest: updateInfo.latest }) }}</p>
<pre v-if="updateInfo.changelog" style="white-space:pre-wrap;font-size:.85rem;opacity:.8;max-height:160px;overflow:auto">{{ updateInfo.changelog }}</pre>
<p class="exit-hint">{{ t('appUpdateHint') }}</p>
<template v-if="updateBusy">
<p class="exit-hint">{{ updateProgress.total > 0 ? t('appUpdateProgress', { received: fmtSize(updateProgress.received), total: fmtSize(updateProgress.total), percent: updatePercent }) : t('appUpdateDownloading') }}</p>
<div class="update-dl-bar" role="progressbar" :aria-valuenow="updatePercent" aria-valuemin="0" aria-valuemax="100"><i :style="{ width: updatePercent + '%' }"/></div>
</template>
<div class="exit-actions">
<button type="button" class="btn secondary" :disabled="updateBusy" @click="skipUpdate">{{ t('appUpdateSkip') }}</button>
<button type="button" class="btn primary" :disabled="updateBusy" @click="installUpdate">{{ updateBusy ? t('appUpdateDownloading') : t('appUpdateInstall') }}</button>