feat: 启动时检测 Git 安装状态,支持自动下载安装\n\n- 启动时自动检测 Git 是否已安装\n- 未安装时显示全屏提示界面\n- 支持用户选择安装路径\n- 自动从 GitHub 下载 Git for Windows 并静默安装\n- 实时显示下载进度和安装状态\n- 安装完成后自动重新初始化 Git 客户端"

This commit is contained in:
zyj
2026-03-11 10:18:26 +08:00
parent 5154bb5564
commit beb40688ce
8 changed files with 768 additions and 45 deletions

View File

@@ -0,0 +1,376 @@
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import {
WarningOutlined,
DownloadOutlined,
FolderOpenOutlined,
CheckCircleOutlined,
LoadingOutlined,
CloseCircleOutlined,
} from '@ant-design/icons-vue'
import { AppService } from '../../bindings/github.com/zhuy1228/GitPilot/internal/app'
import { On } from '@wailsio/runtime'
const emit = defineEmits(['ready'])
const checking = ref(true)
const gitInstalled = ref(false)
const gitVersion = ref('')
const gitPath = ref('')
// 安装相关
const installDir = ref('C:\\Program Files\\Git')
const installing = ref(false)
const installPhase = ref('') // downloading | installing | done | error
const installPercent = ref(0)
const installMessage = ref('')
let unsubscribe = null
onMounted(async () => {
// 监听安装进度事件
unsubscribe = On('git-install-progress', (event) => {
const data = event.data?.[0] || event.data
if (data) {
installPhase.value = data.phase
installPercent.value = data.percent
installMessage.value = data.message
if (data.phase === 'done') {
installing.value = false
gitInstalled.value = true
setTimeout(() => emit('ready'), 1500)
} else if (data.phase === 'error') {
installing.value = false
}
}
})
// 检查 Git
try {
const status = await AppService.CheckGitInstalled()
gitInstalled.value = status.installed
gitVersion.value = status.version || ''
gitPath.value = status.path || ''
if (status.installed) {
emit('ready')
}
} catch (e) {
console.error('检查 Git 失败:', e)
gitInstalled.value = false
} finally {
checking.value = false
}
})
onBeforeUnmount(() => {
if (unsubscribe) unsubscribe()
})
async function selectInstallDir() {
try {
const dir = await AppService.SelectGitInstallDir()
if (dir) installDir.value = dir
} catch (e) {
console.error('选择目录失败:', e)
}
}
async function startInstall() {
installing.value = true
installPhase.value = 'downloading'
installPercent.value = 0
installMessage.value = '准备下载...'
try {
await AppService.InstallGit(installDir.value)
} catch (e) {
console.error('安装 Git 失败:', e)
installPhase.value = 'error'
installMessage.value = '安装失败: ' + String(e)
installing.value = false
}
}
</script>
<template>
<!-- 检查中 -->
<div v-if="checking" class="git-check-overlay">
<div class="git-check-card">
<LoadingOutlined :style="{ fontSize: '32px', color: 'var(--accent, #89b4fa)' }" spin />
<div class="git-check-title">正在检查 Git 环境...</div>
</div>
</div>
<!-- Git 未安装 -->
<div v-else-if="!gitInstalled" class="git-check-overlay">
<div class="git-check-card install-card">
<div class="git-check-icon">
<WarningOutlined :style="{ fontSize: '48px', color: '#fab387' }" />
</div>
<div class="git-check-title">未检测到 Git</div>
<div class="git-check-desc">GitPilot 需要 Git 才能正常工作请安装 Git 后使用</div>
<!-- 安装进度 -->
<template v-if="installing || installPhase === 'done'">
<div class="install-progress">
<div v-if="installPhase === 'downloading'" class="progress-section">
<div class="progress-bar-bg">
<div class="progress-bar-fill" :style="{ width: installPercent + '%' }"></div>
</div>
<div class="progress-text">
<DownloadOutlined /> {{ installMessage }}
</div>
</div>
<div v-else-if="installPhase === 'installing'" class="progress-section">
<div class="progress-bar-bg">
<div class="progress-bar-fill installing-anim" style="width: 100%"></div>
</div>
<div class="progress-text">
<LoadingOutlined spin /> {{ installMessage }}
</div>
</div>
<div v-else-if="installPhase === 'done'" class="progress-section done">
<CheckCircleOutlined :style="{ fontSize: '24px', color: '#a6e3a1' }" />
<div class="progress-text success">{{ installMessage }}</div>
</div>
<div v-else-if="installPhase === 'error'" class="progress-section error">
<CloseCircleOutlined :style="{ fontSize: '24px', color: '#f38ba8' }" />
<div class="progress-text error-text">{{ installMessage }}</div>
</div>
</div>
</template>
<!-- 安装表单 -->
<template v-else>
<div class="install-form">
<div class="install-field">
<label>安装路径</label>
<div class="install-dir-input">
<input
v-model="installDir"
class="dir-input"
placeholder="C:\Program Files\Git"
/>
<button class="dir-btn" @click="selectInstallDir" title="选择目录">
<FolderOpenOutlined />
</button>
</div>
</div>
</div>
<div class="install-actions">
<button class="install-btn primary" @click="startInstall">
<DownloadOutlined /> 自动下载并安装 Git
</button>
</div>
<div class="install-hint">
将从 GitHub 下载 Git for Windows 安装包 65MB需保持网络连接
</div>
</template>
</div>
</div>
</template>
<style scoped>
.git-check-overlay {
position: fixed;
inset: 0;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
background: var(--bg-primary, #1e1e2e);
}
.git-check-card {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
padding: 40px;
max-width: 480px;
width: 100%;
}
.install-card {
background: var(--bg-secondary, #181825);
border: 1px solid var(--border-color, #313244);
border-radius: 12px;
padding: 40px 36px;
}
.git-check-icon {
margin-bottom: 4px;
}
.git-check-title {
font-size: 20px;
font-weight: 700;
color: var(--text-primary, #cdd6f4);
}
.git-check-desc {
font-size: 14px;
color: var(--text-muted, #6c7086);
text-align: center;
line-height: 1.6;
}
.install-form {
width: 100%;
margin-top: 8px;
}
.install-field {
display: flex;
flex-direction: column;
gap: 6px;
}
.install-field label {
font-size: 13px;
font-weight: 600;
color: var(--text-secondary, #a6adc8);
}
.install-dir-input {
display: flex;
gap: 6px;
}
.dir-input {
flex: 1;
padding: 8px 12px;
background: var(--bg-primary, #1e1e2e);
border: 1px solid var(--border-color, #313244);
border-radius: 6px;
color: var(--text-primary, #cdd6f4);
font-size: 13px;
font-family: 'Consolas', 'Courier New', monospace;
outline: none;
transition: border-color 0.15s;
}
.dir-input:focus {
border-color: var(--accent, #89b4fa);
}
.dir-btn {
padding: 8px 12px;
background: var(--bg-primary, #1e1e2e);
border: 1px solid var(--border-color, #313244);
border-radius: 6px;
color: var(--accent, #89b4fa);
cursor: pointer;
font-size: 15px;
display: flex;
align-items: center;
transition: all 0.15s;
}
.dir-btn:hover {
background: var(--bg-hover, #313244);
}
.install-actions {
width: 100%;
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 4px;
}
.install-btn {
padding: 10px 20px;
border-radius: 8px;
border: 1px solid var(--border-color, #313244);
font-size: 14px;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: all 0.15s;
}
.install-btn.primary {
background: var(--accent, #89b4fa);
color: #1e1e2e;
border-color: var(--accent, #89b4fa);
}
.install-btn.primary:hover {
opacity: 0.9;
transform: translateY(-1px);
}
.install-hint {
font-size: 12px;
color: var(--text-muted, #6c7086);
text-align: center;
line-height: 1.5;
}
/* 安装进度 */
.install-progress {
width: 100%;
margin-top: 8px;
}
.progress-section {
display: flex;
flex-direction: column;
gap: 10px;
align-items: center;
}
.progress-section.done,
.progress-section.error {
gap: 12px;
}
.progress-bar-bg {
width: 100%;
height: 6px;
background: var(--bg-primary, #1e1e2e);
border-radius: 3px;
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
background: var(--accent, #89b4fa);
border-radius: 3px;
transition: width 0.3s ease;
}
.progress-bar-fill.installing-anim {
background: linear-gradient(90deg, var(--accent, #89b4fa) 0%, #b4befe 50%, var(--accent, #89b4fa) 100%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
.progress-text {
font-size: 13px;
color: var(--text-secondary, #a6adc8);
display: flex;
align-items: center;
gap: 6px;
}
.progress-text.success {
color: #a6e3a1;
font-weight: 600;
}
.progress-text.error-text {
color: #f38ba8;
font-size: 12px;
text-align: center;
word-break: break-all;
}
</style>

View File

@@ -2,6 +2,11 @@
import { ref, onBeforeUnmount } from 'vue'
const selectedProject = ref(null)
const gitReady = ref(false)
function onGitReady() {
gitReady.value = true
}
function onSelectProject(project) {
selectedProject.value = project
@@ -43,7 +48,11 @@ onBeforeUnmount(() => {
</script>
<template>
<div class="main-layout">
<!-- Git 环境检查 -->
<GitCheck v-if="!gitReady" @ready="onGitReady" />
<!-- 主界面 -->
<div v-else class="main-layout">
<Sidebar :selected-project="selectedProject" :style="{ width: sidebarWidth + 'px' }" @select-project="onSelectProject" />
<div class="resize-handle" @mousedown="startSidebarResize"></div>
<ContentArea :project="selectedProject" />