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" />

View File

@@ -86,6 +86,16 @@ export function BatchPush(paths) {
}));
}
/**
* CheckGitInstalled 检查 Git 是否已安装
* @returns {$CancellablePromise<$models.GitStatus>}
*/
export function CheckGitInstalled() {
return $Call.ByID(464620426).then(/** @type {($result: any) => any} */(($result) => {
return $$createType2($result);
}));
}
/**
* CheckoutRemoteBranch 检出远程分支到本地
* @param {string} path
@@ -196,7 +206,7 @@ export function FetchProject(path) {
*/
export function GetAllProjectOverview() {
return $Call.ByID(2453130151).then(/** @type {($result: any) => any} */(($result) => {
return $$createType3($result);
return $$createType4($result);
}));
}
@@ -206,7 +216,7 @@ export function GetAllProjectOverview() {
*/
export function GetAppSettings() {
return $Call.ByID(428589026).then(/** @type {($result: any) => any} */(($result) => {
return $$createType5($result);
return $$createType6($result);
}));
}
@@ -217,7 +227,7 @@ export function GetAppSettings() {
*/
export function GetBranches(path) {
return $Call.ByID(1686190192, path).then(/** @type {($result: any) => any} */(($result) => {
return $$createType7($result);
return $$createType8($result);
}));
}
@@ -250,7 +260,7 @@ export function GetCommitFileDiff(path, hash, filePath) {
*/
export function GetCommitFiles(path, hash) {
return $Call.ByID(2420707876, path, hash).then(/** @type {($result: any) => any} */(($result) => {
return $$createType9($result);
return $$createType10($result);
}));
}
@@ -262,7 +272,7 @@ export function GetCommitFiles(path, hash) {
*/
export function GetCommitLog(path, count) {
return $Call.ByID(1281870789, path, count).then(/** @type {($result: any) => any} */(($result) => {
return $$createType11($result);
return $$createType12($result);
}));
}
@@ -283,7 +293,7 @@ export function GetConflictFileContent(projectPath, filePath) {
*/
export function GetConflictFiles(path) {
return $Call.ByID(2446806137, path).then(/** @type {($result: any) => any} */(($result) => {
return $$createType13($result);
return $$createType14($result);
}));
}
@@ -323,7 +333,7 @@ export function GetFileDiffStaged(projectPath, filePath) {
*/
export function GetGitGlobalConfig() {
return $Call.ByID(154811497).then(/** @type {($result: any) => any} */(($result) => {
return $$createType15($result);
return $$createType16($result);
}));
}
@@ -334,7 +344,7 @@ export function GetGitGlobalConfig() {
*/
export function GetPlatformInfo(name) {
return $Call.ByID(2668095547, name).then(/** @type {($result: any) => any} */(($result) => {
return $$createType17($result);
return $$createType18($result);
}));
}
@@ -345,7 +355,7 @@ export function GetPlatformInfo(name) {
*/
export function GetProjectChangedFiles(path) {
return $Call.ByID(2302591462, path).then(/** @type {($result: any) => any} */(($result) => {
return $$createType19($result);
return $$createType20($result);
}));
}
@@ -356,7 +366,7 @@ export function GetProjectChangedFiles(path) {
*/
export function GetProjectStatus(path) {
return $Call.ByID(3451089027, path).then(/** @type {($result: any) => any} */(($result) => {
return $$createType21($result);
return $$createType22($result);
}));
}
@@ -366,7 +376,7 @@ export function GetProjectStatus(path) {
*/
export function GetProjectTree() {
return $Call.ByID(651189689).then(/** @type {($result: any) => any} */(($result) => {
return $$createType23($result);
return $$createType24($result);
}));
}
@@ -377,7 +387,7 @@ export function GetProjectTree() {
*/
export function GetRemoteBranches(path) {
return $Call.ByID(994796370, path).then(/** @type {($result: any) => any} */(($result) => {
return $$createType7($result);
return $$createType8($result);
}));
}
@@ -388,7 +398,7 @@ export function GetRemoteBranches(path) {
*/
export function GetStashList(path) {
return $Call.ByID(1948257945, path).then(/** @type {($result: any) => any} */(($result) => {
return $$createType25($result);
return $$createType26($result);
}));
}
@@ -399,7 +409,7 @@ export function GetStashList(path) {
*/
export function GetTags(path) {
return $Call.ByID(3979169621, path).then(/** @type {($result: any) => any} */(($result) => {
return $$createType27($result);
return $$createType28($result);
}));
}
@@ -411,10 +421,20 @@ export function GetTags(path) {
*/
export function GetUserInfo(platform, username) {
return $Call.ByID(2674655707, platform, username).then(/** @type {($result: any) => any} */(($result) => {
return $$createType29($result);
return $$createType30($result);
}));
}
/**
* InstallGit 下载并安装 Git
* installDir: 用户选择的安装目录Windows 有效)
* @param {string} installDir
* @returns {$CancellablePromise<void>}
*/
export function InstallGit(installDir) {
return $Call.ByID(1052641569, installDir);
}
/**
* IsMerging 检查是否处于合并状态
* @param {string} path
@@ -545,7 +565,7 @@ export function SaveConflictFile(projectPath, filePath, content) {
*/
export function SearchCommitLog(path, keyword, author, maxCount) {
return $Call.ByID(2874450917, path, keyword, author, maxCount).then(/** @type {($result: any) => any} */(($result) => {
return $$createType11($result);
return $$createType12($result);
}));
}
@@ -557,6 +577,14 @@ export function SelectDirectory() {
return $Call.ByID(2318416763);
}
/**
* SelectGitInstallDir 打开文件夹选择器让用户选择 Git 安装路径
* @returns {$CancellablePromise<string>}
*/
export function SelectGitInstallDir() {
return $Call.ByID(2941074074);
}
/**
* @param {application$0.App | null} app
* @returns {$CancellablePromise<void>}
@@ -697,31 +725,32 @@ export function UpdateUser(platform, oldUsername, newUsername, token) {
// Private type creation functions
const $$createType0 = $models.BatchPullResult.createFrom;
const $$createType1 = $Create.Array($$createType0);
const $$createType2 = $models.ProjectOverview.createFrom;
const $$createType3 = $Create.Array($$createType2);
const $$createType4 = config$0.Settings.createFrom;
const $$createType5 = $Create.Nullable($$createType4);
const $$createType6 = $models.BranchInfo.createFrom;
const $$createType7 = $Create.Array($$createType6);
const $$createType8 = $models.CommitFileInfo.createFrom;
const $$createType9 = $Create.Array($$createType8);
const $$createType10 = $models.CommitLog.createFrom;
const $$createType11 = $Create.Array($$createType10);
const $$createType12 = $models.ConflictFileInfo.createFrom;
const $$createType13 = $Create.Array($$createType12);
const $$createType14 = $models.GitConfig.createFrom;
const $$createType15 = $Create.Nullable($$createType14);
const $$createType16 = $models.PlatformInfo.createFrom;
const $$createType17 = $Create.Nullable($$createType16);
const $$createType18 = $models.FileInfo.createFrom;
const $$createType19 = $Create.Array($$createType18);
const $$createType20 = $models.ProjectStatus.createFrom;
const $$createType21 = $Create.Nullable($$createType20);
const $$createType22 = $models.TreeNode.createFrom;
const $$createType23 = $Create.Array($$createType22);
const $$createType24 = $models.StashInfo.createFrom;
const $$createType25 = $Create.Array($$createType24);
const $$createType26 = $models.TagInfo.createFrom;
const $$createType27 = $Create.Array($$createType26);
const $$createType28 = $models.UserInfo.createFrom;
const $$createType29 = $Create.Nullable($$createType28);
const $$createType2 = $models.GitStatus.createFrom;
const $$createType3 = $models.ProjectOverview.createFrom;
const $$createType4 = $Create.Array($$createType3);
const $$createType5 = config$0.Settings.createFrom;
const $$createType6 = $Create.Nullable($$createType5);
const $$createType7 = $models.BranchInfo.createFrom;
const $$createType8 = $Create.Array($$createType7);
const $$createType9 = $models.CommitFileInfo.createFrom;
const $$createType10 = $Create.Array($$createType9);
const $$createType11 = $models.CommitLog.createFrom;
const $$createType12 = $Create.Array($$createType11);
const $$createType13 = $models.ConflictFileInfo.createFrom;
const $$createType14 = $Create.Array($$createType13);
const $$createType15 = $models.GitConfig.createFrom;
const $$createType16 = $Create.Nullable($$createType15);
const $$createType17 = $models.PlatformInfo.createFrom;
const $$createType18 = $Create.Nullable($$createType17);
const $$createType19 = $models.FileInfo.createFrom;
const $$createType20 = $Create.Array($$createType19);
const $$createType21 = $models.ProjectStatus.createFrom;
const $$createType22 = $Create.Nullable($$createType21);
const $$createType23 = $models.TreeNode.createFrom;
const $$createType24 = $Create.Array($$createType23);
const $$createType25 = $models.StashInfo.createFrom;
const $$createType26 = $Create.Array($$createType25);
const $$createType27 = $models.TagInfo.createFrom;
const $$createType28 = $Create.Array($$createType27);
const $$createType29 = $models.UserInfo.createFrom;
const $$createType30 = $Create.Nullable($$createType29);

View File

@@ -15,6 +15,7 @@ export {
ConflictFileInfo,
FileInfo,
GitConfig,
GitStatus,
PlatformInfo,
ProjectOverview,
ProjectStatus,

View File

@@ -328,6 +328,51 @@ export class GitConfig {
}
}
/**
* GitStatus Git 安装状态
*/
export class GitStatus {
/**
* Creates a new GitStatus instance.
* @param {Partial<GitStatus>} [$$source = {}] - The source object to create the GitStatus.
*/
constructor($$source = {}) {
if (!("installed" in $$source)) {
/**
* @member
* @type {boolean}
*/
this["installed"] = false;
}
if (!("version" in $$source)) {
/**
* @member
* @type {string}
*/
this["version"] = "";
}
if (!("path" in $$source)) {
/**
* @member
* @type {string}
*/
this["path"] = "";
}
Object.assign(this, $$source);
}
/**
* Creates a new GitStatus instance from a string or object.
* @param {any} [$$source = {}]
* @returns {GitStatus}
*/
static createFrom($$source = {}) {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new GitStatus(/** @type {Partial<GitStatus>} */($$parsedSource));
}
}
/**
* PlatformInfo 平台信息
*/