BatchInstallView.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. <script setup lang="ts">
  2. import { computed, onMounted } from 'vue'
  3. import { useI18n } from 'vue-i18n'
  4. import { useVersionsStore } from '@/stores/versions'
  5. import { useInstallStore } from '@/stores/install'
  6. import { useSystemStore } from '@/stores/system'
  7. import SoftwareIcon from '@/components/common/SoftwareIcon.vue'
  8. const { t } = useI18n()
  9. const versionsStore = useVersionsStore()
  10. const installStore = useInstallStore()
  11. const systemStore = useSystemStore()
  12. // 是否为 Windows 平台(只有 Windows 支持自定义安装路径)
  13. const isWindows = computed(() => systemStore.platform === 'win32')
  14. const status = computed(() => installStore.getStatus('all'))
  15. // Claude Code VS Code 扩展状态(使用 store 中的共享数据)
  16. const claudeCodeExtInstalled = computed(() => installStore.isClaudeCodeExtInstalled())
  17. const claudeCodeExtVersion = computed(() => installStore.getClaudeCodeExtVersion() || '')
  18. onMounted(() => {
  19. // 检查扩展安装状态
  20. installStore.checkClaudeCodeExtInstalled()
  21. })
  22. // 用户想要安装的软件(未安装且用户勾选了)
  23. const wantInstallNodejs = computed(() => !installStore.isInstalled('nodejs') && installStore.installOptions.all.installNodejs)
  24. const wantInstallPnpm = computed(() => !installStore.isInstalled('pnpm') && installStore.installOptions.all.installPnpm)
  25. const wantInstallVscode = computed(() => !installStore.isInstalled('vscode') && installStore.installOptions.all.installVscode)
  26. const wantInstallGit = computed(() => !installStore.isInstalled('git') && installStore.installOptions.all.installGit)
  27. const wantInstallClaudeCode = computed(() => !installStore.isInstalled('claudeCode') && installStore.installOptions.all.installClaudeCode)
  28. const wantInstallClaudeCodeExt = computed(() => !claudeCodeExtInstalled.value && installStore.installOptions.all.installClaudeCodeExt)
  29. // 是否有用户选择要安装的软件
  30. const hasAnythingToInstall = computed(() =>
  31. wantInstallNodejs.value || wantInstallPnpm.value || wantInstallVscode.value || wantInstallGit.value || wantInstallClaudeCode.value || wantInstallClaudeCodeExt.value
  32. )
  33. // 是否所有软件都已安装
  34. const isAllInstalled = computed(() =>
  35. installStore.isInstalled('nodejs') &&
  36. installStore.isInstalled('pnpm') &&
  37. installStore.isInstalled('vscode') &&
  38. installStore.isInstalled('git') &&
  39. installStore.isInstalled('claudeCode') &&
  40. claudeCodeExtInstalled.value
  41. )
  42. const isDisabled = computed(() =>
  43. status.value.installing ||
  44. versionsStore.isLoadingVersions ||
  45. versionsStore.hasVersionError ||
  46. !hasAnythingToInstall.value
  47. )
  48. const buttonText = computed(() => {
  49. if (status.value.installing) return t('install.installing')
  50. if (status.value.success) return t('install.complete')
  51. if (status.value.error) return t('install.reinstall')
  52. if (isAllInstalled.value) return t('install.allInstalled')
  53. if (!hasAnythingToInstall.value) return t('install.noSelection')
  54. return t('install.startAll')
  55. })
  56. const statusText = computed(() => {
  57. if (status.value.installing) return status.value.message
  58. if (status.value.success) return status.value.message
  59. if (status.value.error) return status.value.message
  60. if (versionsStore.isLoadingVersions) return t('common.loading')
  61. if (versionsStore.hasVersionError) return t('common.loadFailed')
  62. if (isAllInstalled.value) return t('install.allInstalledHint')
  63. if (!hasAnythingToInstall.value) return t('install.noSelectionHint')
  64. return t('common.ready')
  65. })
  66. async function handleInstall() {
  67. if (isDisabled.value) return
  68. const options = {
  69. installNodejs: wantInstallNodejs.value,
  70. nodejsVersion: versionsStore.selectedVersions.nodejs,
  71. nodejsPath: installStore.installOptions.all.nodejsPath,
  72. installPnpm: wantInstallPnpm.value,
  73. installVscode: wantInstallVscode.value,
  74. vscodeVersion: versionsStore.selectedVersions.vscode,
  75. vscodePath: installStore.installOptions.all.vscodePath,
  76. installGit: wantInstallGit.value,
  77. gitVersion: versionsStore.selectedVersions.git,
  78. gitPath: installStore.installOptions.all.gitPath,
  79. installClaudeCode: wantInstallClaudeCode.value,
  80. installClaudeCodeExt: wantInstallClaudeCodeExt.value
  81. }
  82. await installStore.doInstall('all', options)
  83. }
  84. async function handleCancel() {
  85. await installStore.cancelInstall()
  86. }
  87. async function selectDirectory(software: 'nodejs' | 'vscode' | 'git') {
  88. const currentPath = software === 'nodejs'
  89. ? installStore.installOptions.all.nodejsPath
  90. : software === 'vscode'
  91. ? installStore.installOptions.all.vscodePath
  92. : installStore.installOptions.all.gitPath
  93. const result = await window.electronAPI.selectDirectory(currentPath || undefined)
  94. if (!result.canceled && result.path) {
  95. if (software === 'nodejs') {
  96. installStore.installOptions.all.nodejsPath = result.path
  97. } else if (software === 'vscode') {
  98. installStore.installOptions.all.vscodePath = result.path
  99. } else {
  100. installStore.installOptions.all.gitPath = result.path
  101. }
  102. }
  103. }
  104. </script>
  105. <template>
  106. <div class="batch-install">
  107. <div class="software-info">
  108. <h2>{{ t('software.all.name') }}</h2>
  109. <p>{{ t('software.all.description') }}</p>
  110. </div>
  111. <div class="all-options">
  112. <!-- Node.js -->
  113. <div class="option-group">
  114. <div class="option-row">
  115. <el-checkbox
  116. :model-value="!installStore.isInstalled('nodejs') && installStore.installOptions.all.installNodejs"
  117. :disabled="installStore.isInstalled('nodejs')"
  118. @update:model-value="installStore.installOptions.all.installNodejs = $event as boolean"
  119. >
  120. <span class="software-label">
  121. <SoftwareIcon software="nodejs" :size="18" />
  122. <span>Node.js</span>
  123. </span>
  124. <el-tag v-if="installStore.isInstalled('nodejs')" type="success" size="small" style="margin-left: 8px">
  125. {{ t('common.installed') }} v{{ installStore.getInstalledVersion('nodejs') }}
  126. </el-tag>
  127. </el-checkbox>
  128. <div v-if="wantInstallNodejs" class="option-details">
  129. <el-select v-model="versionsStore.selectedVersions.nodejs" size="small" style="width: 150px">
  130. <el-option
  131. v-for="(v, index) in versionsStore.getVersionList('nodejs')"
  132. :key="v.value || `separator-${index}`"
  133. :value="v.value"
  134. :label="v.label"
  135. :disabled="v.disabled || v.separator"
  136. />
  137. </el-select>
  138. </div>
  139. </div>
  140. <div v-if="wantInstallNodejs && isWindows" class="path-row">
  141. <span class="path-label">{{ t('install.customPath') }}</span>
  142. <el-input
  143. v-model="installStore.installOptions.all.nodejsPath"
  144. size="small"
  145. :placeholder="t('install.customPathPlaceholder')"
  146. clearable
  147. />
  148. <el-button size="small" @click="selectDirectory('nodejs')">{{ t('install.browse') }}</el-button>
  149. </div>
  150. </div>
  151. <!-- pnpm -->
  152. <div class="option-group">
  153. <div class="option-row">
  154. <el-checkbox
  155. :model-value="!installStore.isInstalled('pnpm') && installStore.installOptions.all.installPnpm"
  156. :disabled="installStore.isInstalled('pnpm')"
  157. @update:model-value="installStore.installOptions.all.installPnpm = $event as boolean"
  158. >
  159. <span class="software-label">
  160. <SoftwareIcon software="nodejs" :size="18" />
  161. <span>pnpm</span>
  162. </span>
  163. <el-tag v-if="installStore.isInstalled('pnpm')" type="success" size="small" style="margin-left: 8px">
  164. {{ t('common.installed') }} v{{ installStore.getInstalledVersion('pnpm') }}
  165. </el-tag>
  166. </el-checkbox>
  167. <span v-if="wantInstallPnpm && !installStore.isInstalled('nodejs') && !wantInstallNodejs" class="option-hint">
  168. {{ t('software.nodejs.pnpmRequiresNodejs') }}
  169. </span>
  170. </div>
  171. </div>
  172. <!-- Git (Claude Code 运行时需要) -->
  173. <div class="option-group">
  174. <div class="option-row">
  175. <el-checkbox
  176. :model-value="!installStore.isInstalled('git') && installStore.installOptions.all.installGit"
  177. :disabled="installStore.isInstalled('git')"
  178. @update:model-value="installStore.installOptions.all.installGit = $event as boolean"
  179. >
  180. <span class="software-label">
  181. <SoftwareIcon software="git" :size="18" />
  182. <span>Git</span>
  183. </span>
  184. <el-tag v-if="installStore.isInstalled('git')" type="success" size="small" style="margin-left: 8px">
  185. {{ t('common.installed') }} v{{ installStore.getInstalledVersion('git') }}
  186. </el-tag>
  187. </el-checkbox>
  188. <div v-if="wantInstallGit" class="option-details">
  189. <el-select v-model="versionsStore.selectedVersions.git" size="small" style="width: 150px">
  190. <el-option
  191. v-for="(v, index) in versionsStore.getVersionList('git')"
  192. :key="v.value || `separator-${index}`"
  193. :value="v.value"
  194. :label="v.label"
  195. :disabled="v.disabled || v.separator"
  196. />
  197. </el-select>
  198. </div>
  199. </div>
  200. <div v-if="wantInstallGit && isWindows" class="path-row">
  201. <span class="path-label">{{ t('install.customPath') }}</span>
  202. <el-input
  203. v-model="installStore.installOptions.all.gitPath"
  204. size="small"
  205. :placeholder="t('install.customPathPlaceholder')"
  206. clearable
  207. />
  208. <el-button size="small" @click="selectDirectory('git')">{{ t('install.browse') }}</el-button>
  209. </div>
  210. </div>
  211. <!-- Claude Code (需要 Node.js 和 Git) -->
  212. <div class="option-group">
  213. <div class="option-row">
  214. <el-checkbox
  215. :model-value="!installStore.isInstalled('claudeCode') && installStore.installOptions.all.installClaudeCode"
  216. :disabled="installStore.isInstalled('claudeCode')"
  217. @update:model-value="installStore.installOptions.all.installClaudeCode = $event as boolean"
  218. >
  219. <span class="software-label">
  220. <SoftwareIcon software="claudeCode" :size="18" />
  221. <span>Claude Code</span>
  222. </span>
  223. <el-tag v-if="installStore.isInstalled('claudeCode')" type="success" size="small" style="margin-left: 8px">
  224. {{ t('common.installed') }} v{{ installStore.getInstalledVersion('claudeCode') }}
  225. </el-tag>
  226. </el-checkbox>
  227. <span v-if="wantInstallClaudeCode" class="option-hint">
  228. {{ t('claudeCode.requiresNodejsGit') }}
  229. </span>
  230. </div>
  231. </div>
  232. <!-- VS Code -->
  233. <div class="option-group">
  234. <div class="option-row">
  235. <el-checkbox
  236. :model-value="!installStore.isInstalled('vscode') && installStore.installOptions.all.installVscode"
  237. :disabled="installStore.isInstalled('vscode')"
  238. @update:model-value="installStore.installOptions.all.installVscode = $event as boolean"
  239. >
  240. <span class="software-label">
  241. <SoftwareIcon software="vscode" :size="18" />
  242. <span>VS Code</span>
  243. </span>
  244. <el-tag v-if="installStore.isInstalled('vscode')" type="success" size="small" style="margin-left: 8px">
  245. {{ t('common.installed') }} v{{ installStore.getInstalledVersion('vscode') }}
  246. </el-tag>
  247. </el-checkbox>
  248. <div v-if="wantInstallVscode" class="option-details">
  249. <el-select v-model="versionsStore.selectedVersions.vscode" size="small" style="width: 150px">
  250. <el-option
  251. v-for="(v, index) in versionsStore.getVersionList('vscode')"
  252. :key="v.value || `separator-${index}`"
  253. :value="v.value"
  254. :label="v.label"
  255. :disabled="v.disabled || v.separator"
  256. />
  257. </el-select>
  258. </div>
  259. </div>
  260. <div v-if="wantInstallVscode && isWindows" class="path-row">
  261. <span class="path-label">{{ t('install.customPath') }}</span>
  262. <el-input
  263. v-model="installStore.installOptions.all.vscodePath"
  264. size="small"
  265. :placeholder="t('install.customPathPlaceholder')"
  266. clearable
  267. />
  268. <el-button size="small" @click="selectDirectory('vscode')">{{ t('install.browse') }}</el-button>
  269. </div>
  270. </div>
  271. <!-- Claude Code for VS Code 扩展 (需要 VS Code 和 Claude Code) -->
  272. <div class="option-group">
  273. <div class="option-row">
  274. <el-checkbox
  275. :model-value="!claudeCodeExtInstalled && installStore.installOptions.all.installClaudeCodeExt"
  276. :disabled="claudeCodeExtInstalled"
  277. @update:model-value="installStore.installOptions.all.installClaudeCodeExt = $event as boolean"
  278. >
  279. <span class="software-label">
  280. <SoftwareIcon software="vscode" :size="18" />
  281. <span>{{ t('software.vscode.claudeCodeExt') }}</span>
  282. </span>
  283. <el-tag v-if="claudeCodeExtInstalled" type="success" size="small" style="margin-left: 8px">
  284. {{ t('common.installed') }}{{ claudeCodeExtVersion ? ` v${claudeCodeExtVersion}` : '' }}
  285. </el-tag>
  286. </el-checkbox>
  287. <span v-if="wantInstallClaudeCodeExt && !installStore.isInstalled('vscode') && !wantInstallVscode" class="option-hint">
  288. {{ t('software.vscode.installVscodeFirst') }}
  289. </span>
  290. </div>
  291. </div>
  292. </div>
  293. <div class="button-group">
  294. <el-button type="primary" :disabled="isDisabled" :loading="status.installing" @click="handleInstall">
  295. {{ buttonText }}
  296. </el-button>
  297. <el-button v-if="status.installing" @click="handleCancel">{{ t('common.cancel') }}</el-button>
  298. </div>
  299. <div class="status-container">
  300. <p :class="['status-text', { success: status.success, error: status.error }]">{{ statusText }}</p>
  301. <el-progress v-if="status.installing" :percentage="status.progress" :stroke-width="6" :show-text="false" />
  302. </div>
  303. </div>
  304. </template>
  305. <style scoped lang="scss">
  306. .batch-install {
  307. .all-options {
  308. margin-bottom: var(--spacing-lg);
  309. .option-group {
  310. padding: var(--spacing-sm) var(--spacing-md);
  311. background-color: var(--card-bg-color);
  312. border: 1px solid var(--border-color-lighter);
  313. border-radius: var(--border-radius);
  314. margin-bottom: var(--spacing-sm);
  315. .option-row {
  316. display: flex;
  317. align-items: center;
  318. flex-wrap: wrap;
  319. gap: var(--spacing-md);
  320. }
  321. .option-details {
  322. display: flex;
  323. align-items: center;
  324. gap: var(--spacing-md);
  325. }
  326. .option-hint {
  327. color: var(--text-color-secondary);
  328. font-size: 12px;
  329. }
  330. .path-row {
  331. display: flex;
  332. align-items: center;
  333. gap: var(--spacing-sm);
  334. margin-top: var(--spacing-sm);
  335. padding-left: 24px;
  336. .path-label {
  337. color: var(--text-color-secondary);
  338. font-size: 12px;
  339. white-space: nowrap;
  340. }
  341. .el-input {
  342. flex: 1;
  343. max-width: 400px;
  344. }
  345. }
  346. }
  347. .software-label {
  348. display: inline-flex;
  349. align-items: center;
  350. gap: var(--spacing-xs);
  351. }
  352. }
  353. }
  354. </style>