| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390 |
- <script setup lang="ts">
- import { computed, onMounted } from 'vue'
- import { useI18n } from 'vue-i18n'
- import { useVersionsStore } from '@/stores/versions'
- import { useInstallStore } from '@/stores/install'
- import { useSystemStore } from '@/stores/system'
- import SoftwareIcon from '@/components/common/SoftwareIcon.vue'
- const { t } = useI18n()
- const versionsStore = useVersionsStore()
- const installStore = useInstallStore()
- const systemStore = useSystemStore()
- // 是否为 Windows 平台(只有 Windows 支持自定义安装路径)
- const isWindows = computed(() => systemStore.platform === 'win32')
- const status = computed(() => installStore.getStatus('all'))
- // Claude Code VS Code 扩展状态(使用 store 中的共享数据)
- const claudeCodeExtInstalled = computed(() => installStore.isClaudeCodeExtInstalled())
- const claudeCodeExtVersion = computed(() => installStore.getClaudeCodeExtVersion() || '')
- onMounted(() => {
- // 检查扩展安装状态
- installStore.checkClaudeCodeExtInstalled()
- })
- // 用户想要安装的软件(未安装且用户勾选了)
- const wantInstallNodejs = computed(() => !installStore.isInstalled('nodejs') && installStore.installOptions.all.installNodejs)
- const wantInstallPnpm = computed(() => !installStore.isInstalled('pnpm') && installStore.installOptions.all.installPnpm)
- const wantInstallVscode = computed(() => !installStore.isInstalled('vscode') && installStore.installOptions.all.installVscode)
- const wantInstallGit = computed(() => !installStore.isInstalled('git') && installStore.installOptions.all.installGit)
- const wantInstallClaudeCode = computed(() => !installStore.isInstalled('claudeCode') && installStore.installOptions.all.installClaudeCode)
- const wantInstallClaudeCodeExt = computed(() => !claudeCodeExtInstalled.value && installStore.installOptions.all.installClaudeCodeExt)
- // 是否有用户选择要安装的软件
- const hasAnythingToInstall = computed(() =>
- wantInstallNodejs.value || wantInstallPnpm.value || wantInstallVscode.value || wantInstallGit.value || wantInstallClaudeCode.value || wantInstallClaudeCodeExt.value
- )
- // 是否所有软件都已安装
- const isAllInstalled = computed(() =>
- installStore.isInstalled('nodejs') &&
- installStore.isInstalled('pnpm') &&
- installStore.isInstalled('vscode') &&
- installStore.isInstalled('git') &&
- installStore.isInstalled('claudeCode') &&
- claudeCodeExtInstalled.value
- )
- const isDisabled = computed(() =>
- status.value.installing ||
- versionsStore.isLoadingVersions ||
- versionsStore.hasVersionError ||
- !hasAnythingToInstall.value
- )
- const buttonText = computed(() => {
- if (status.value.installing) return t('install.installing')
- if (status.value.success) return t('install.complete')
- if (status.value.error) return t('install.reinstall')
- if (isAllInstalled.value) return t('install.allInstalled')
- if (!hasAnythingToInstall.value) return t('install.noSelection')
- return t('install.startAll')
- })
- const statusText = computed(() => {
- if (status.value.installing) return status.value.message
- if (status.value.success) return status.value.message
- if (status.value.error) return status.value.message
- if (versionsStore.isLoadingVersions) return t('common.loading')
- if (versionsStore.hasVersionError) return t('common.loadFailed')
- if (isAllInstalled.value) return t('install.allInstalledHint')
- if (!hasAnythingToInstall.value) return t('install.noSelectionHint')
- return t('common.ready')
- })
- async function handleInstall() {
- if (isDisabled.value) return
- const options = {
- installNodejs: wantInstallNodejs.value,
- nodejsVersion: versionsStore.selectedVersions.nodejs,
- nodejsPath: installStore.installOptions.all.nodejsPath,
- installPnpm: wantInstallPnpm.value,
- installVscode: wantInstallVscode.value,
- vscodeVersion: versionsStore.selectedVersions.vscode,
- vscodePath: installStore.installOptions.all.vscodePath,
- installGit: wantInstallGit.value,
- gitVersion: versionsStore.selectedVersions.git,
- gitPath: installStore.installOptions.all.gitPath,
- installClaudeCode: wantInstallClaudeCode.value,
- installClaudeCodeExt: wantInstallClaudeCodeExt.value
- }
- await installStore.doInstall('all', options)
- }
- async function handleCancel() {
- await installStore.cancelInstall()
- }
- async function selectDirectory(software: 'nodejs' | 'vscode' | 'git') {
- const currentPath = software === 'nodejs'
- ? installStore.installOptions.all.nodejsPath
- : software === 'vscode'
- ? installStore.installOptions.all.vscodePath
- : installStore.installOptions.all.gitPath
- const result = await window.electronAPI.selectDirectory(currentPath || undefined)
- if (!result.canceled && result.path) {
- if (software === 'nodejs') {
- installStore.installOptions.all.nodejsPath = result.path
- } else if (software === 'vscode') {
- installStore.installOptions.all.vscodePath = result.path
- } else {
- installStore.installOptions.all.gitPath = result.path
- }
- }
- }
- </script>
- <template>
- <div class="batch-install">
- <div class="software-info">
- <h2>{{ t('software.all.name') }}</h2>
- <p>{{ t('software.all.description') }}</p>
- </div>
- <div class="all-options">
- <!-- Node.js -->
- <div class="option-group">
- <div class="option-row">
- <el-checkbox
- :model-value="!installStore.isInstalled('nodejs') && installStore.installOptions.all.installNodejs"
- :disabled="installStore.isInstalled('nodejs')"
- @update:model-value="installStore.installOptions.all.installNodejs = $event as boolean"
- >
- <span class="software-label">
- <SoftwareIcon software="nodejs" :size="18" />
- <span>Node.js</span>
- </span>
- <el-tag v-if="installStore.isInstalled('nodejs')" type="success" size="small" style="margin-left: 8px">
- {{ t('common.installed') }} v{{ installStore.getInstalledVersion('nodejs') }}
- </el-tag>
- </el-checkbox>
- <div v-if="wantInstallNodejs" class="option-details">
- <el-select v-model="versionsStore.selectedVersions.nodejs" size="small" style="width: 150px">
- <el-option
- v-for="(v, index) in versionsStore.getVersionList('nodejs')"
- :key="v.value || `separator-${index}`"
- :value="v.value"
- :label="v.label"
- :disabled="v.disabled || v.separator"
- />
- </el-select>
- </div>
- </div>
- <div v-if="wantInstallNodejs && isWindows" class="path-row">
- <span class="path-label">{{ t('install.customPath') }}</span>
- <el-input
- v-model="installStore.installOptions.all.nodejsPath"
- size="small"
- :placeholder="t('install.customPathPlaceholder')"
- clearable
- />
- <el-button size="small" @click="selectDirectory('nodejs')">{{ t('install.browse') }}</el-button>
- </div>
- </div>
- <!-- pnpm -->
- <div class="option-group">
- <div class="option-row">
- <el-checkbox
- :model-value="!installStore.isInstalled('pnpm') && installStore.installOptions.all.installPnpm"
- :disabled="installStore.isInstalled('pnpm')"
- @update:model-value="installStore.installOptions.all.installPnpm = $event as boolean"
- >
- <span class="software-label">
- <SoftwareIcon software="nodejs" :size="18" />
- <span>pnpm</span>
- </span>
- <el-tag v-if="installStore.isInstalled('pnpm')" type="success" size="small" style="margin-left: 8px">
- {{ t('common.installed') }} v{{ installStore.getInstalledVersion('pnpm') }}
- </el-tag>
- </el-checkbox>
- <span v-if="wantInstallPnpm && !installStore.isInstalled('nodejs') && !wantInstallNodejs" class="option-hint">
- {{ t('software.nodejs.pnpmRequiresNodejs') }}
- </span>
- </div>
- </div>
- <!-- Git (Claude Code 运行时需要) -->
- <div class="option-group">
- <div class="option-row">
- <el-checkbox
- :model-value="!installStore.isInstalled('git') && installStore.installOptions.all.installGit"
- :disabled="installStore.isInstalled('git')"
- @update:model-value="installStore.installOptions.all.installGit = $event as boolean"
- >
- <span class="software-label">
- <SoftwareIcon software="git" :size="18" />
- <span>Git</span>
- </span>
- <el-tag v-if="installStore.isInstalled('git')" type="success" size="small" style="margin-left: 8px">
- {{ t('common.installed') }} v{{ installStore.getInstalledVersion('git') }}
- </el-tag>
- </el-checkbox>
- <div v-if="wantInstallGit" class="option-details">
- <el-select v-model="versionsStore.selectedVersions.git" size="small" style="width: 150px">
- <el-option
- v-for="(v, index) in versionsStore.getVersionList('git')"
- :key="v.value || `separator-${index}`"
- :value="v.value"
- :label="v.label"
- :disabled="v.disabled || v.separator"
- />
- </el-select>
- </div>
- </div>
- <div v-if="wantInstallGit && isWindows" class="path-row">
- <span class="path-label">{{ t('install.customPath') }}</span>
- <el-input
- v-model="installStore.installOptions.all.gitPath"
- size="small"
- :placeholder="t('install.customPathPlaceholder')"
- clearable
- />
- <el-button size="small" @click="selectDirectory('git')">{{ t('install.browse') }}</el-button>
- </div>
- </div>
- <!-- Claude Code (需要 Node.js 和 Git) -->
- <div class="option-group">
- <div class="option-row">
- <el-checkbox
- :model-value="!installStore.isInstalled('claudeCode') && installStore.installOptions.all.installClaudeCode"
- :disabled="installStore.isInstalled('claudeCode')"
- @update:model-value="installStore.installOptions.all.installClaudeCode = $event as boolean"
- >
- <span class="software-label">
- <SoftwareIcon software="claudeCode" :size="18" />
- <span>Claude Code</span>
- </span>
- <el-tag v-if="installStore.isInstalled('claudeCode')" type="success" size="small" style="margin-left: 8px">
- {{ t('common.installed') }} v{{ installStore.getInstalledVersion('claudeCode') }}
- </el-tag>
- </el-checkbox>
- <span v-if="wantInstallClaudeCode" class="option-hint">
- {{ t('claudeCode.requiresNodejsGit') }}
- </span>
- </div>
- </div>
- <!-- VS Code -->
- <div class="option-group">
- <div class="option-row">
- <el-checkbox
- :model-value="!installStore.isInstalled('vscode') && installStore.installOptions.all.installVscode"
- :disabled="installStore.isInstalled('vscode')"
- @update:model-value="installStore.installOptions.all.installVscode = $event as boolean"
- >
- <span class="software-label">
- <SoftwareIcon software="vscode" :size="18" />
- <span>VS Code</span>
- </span>
- <el-tag v-if="installStore.isInstalled('vscode')" type="success" size="small" style="margin-left: 8px">
- {{ t('common.installed') }} v{{ installStore.getInstalledVersion('vscode') }}
- </el-tag>
- </el-checkbox>
- <div v-if="wantInstallVscode" class="option-details">
- <el-select v-model="versionsStore.selectedVersions.vscode" size="small" style="width: 150px">
- <el-option
- v-for="(v, index) in versionsStore.getVersionList('vscode')"
- :key="v.value || `separator-${index}`"
- :value="v.value"
- :label="v.label"
- :disabled="v.disabled || v.separator"
- />
- </el-select>
- </div>
- </div>
- <div v-if="wantInstallVscode && isWindows" class="path-row">
- <span class="path-label">{{ t('install.customPath') }}</span>
- <el-input
- v-model="installStore.installOptions.all.vscodePath"
- size="small"
- :placeholder="t('install.customPathPlaceholder')"
- clearable
- />
- <el-button size="small" @click="selectDirectory('vscode')">{{ t('install.browse') }}</el-button>
- </div>
- </div>
- <!-- Claude Code for VS Code 扩展 (需要 VS Code 和 Claude Code) -->
- <div class="option-group">
- <div class="option-row">
- <el-checkbox
- :model-value="!claudeCodeExtInstalled && installStore.installOptions.all.installClaudeCodeExt"
- :disabled="claudeCodeExtInstalled"
- @update:model-value="installStore.installOptions.all.installClaudeCodeExt = $event as boolean"
- >
- <span class="software-label">
- <SoftwareIcon software="vscode" :size="18" />
- <span>{{ t('software.vscode.claudeCodeExt') }}</span>
- </span>
- <el-tag v-if="claudeCodeExtInstalled" type="success" size="small" style="margin-left: 8px">
- {{ t('common.installed') }}{{ claudeCodeExtVersion ? ` v${claudeCodeExtVersion}` : '' }}
- </el-tag>
- </el-checkbox>
- <span v-if="wantInstallClaudeCodeExt && !installStore.isInstalled('vscode') && !wantInstallVscode" class="option-hint">
- {{ t('software.vscode.installVscodeFirst') }}
- </span>
- </div>
- </div>
- </div>
- <div class="button-group">
- <el-button type="primary" :disabled="isDisabled" :loading="status.installing" @click="handleInstall">
- {{ buttonText }}
- </el-button>
- <el-button v-if="status.installing" @click="handleCancel">{{ t('common.cancel') }}</el-button>
- </div>
- <div class="status-container">
- <p :class="['status-text', { success: status.success, error: status.error }]">{{ statusText }}</p>
- <el-progress v-if="status.installing" :percentage="status.progress" :stroke-width="6" :show-text="false" />
- </div>
- </div>
- </template>
- <style scoped lang="scss">
- .batch-install {
- .all-options {
- margin-bottom: var(--spacing-lg);
- .option-group {
- padding: var(--spacing-sm) var(--spacing-md);
- background-color: var(--card-bg-color);
- border: 1px solid var(--border-color-lighter);
- border-radius: var(--border-radius);
- margin-bottom: var(--spacing-sm);
- .option-row {
- display: flex;
- align-items: center;
- flex-wrap: wrap;
- gap: var(--spacing-md);
- }
- .option-details {
- display: flex;
- align-items: center;
- gap: var(--spacing-md);
- }
- .option-hint {
- color: var(--text-color-secondary);
- font-size: 12px;
- }
- .path-row {
- display: flex;
- align-items: center;
- gap: var(--spacing-sm);
- margin-top: var(--spacing-sm);
- padding-left: 24px;
- .path-label {
- color: var(--text-color-secondary);
- font-size: 12px;
- white-space: nowrap;
- }
- .el-input {
- flex: 1;
- max-width: 400px;
- }
- }
- }
- .software-label {
- display: inline-flex;
- align-items: center;
- gap: var(--spacing-xs);
- }
- }
- }
- </style>
|