Browse Source

支持自定义安装路径

黄中银 1 month ago
parent
commit
97c8dea394

+ 61 - 20
ApqInstaller/electron/modules/installer.ts

@@ -226,8 +226,10 @@ export async function checkAllInstalled(): Promise<AllInstalledInfo> {
 
 /**
  * 获取 Node.js 安装命令
+ * @param version 版本号
+ * @param customPath 自定义安装路径 (仅 Windows 支持)
  */
-function getNodeInstallArgs(version = 'lts'): CommandResult {
+function getNodeInstallArgs(version = 'lts', customPath?: string): CommandResult {
   const platform = os.platform() as Platform
   const major = version.split('.')[0]
   const majorNum = parseInt(major)
@@ -255,6 +257,9 @@ function getNodeInstallArgs(version = 'lts'): CommandResult {
       if (version !== 'lts' && version.includes('.')) {
         args.push('--version', version)
       }
+      if (customPath) {
+        args.push('--location', customPath)
+      }
       return { command: 'winget', args }
     }
     case 'darwin':
@@ -268,17 +273,21 @@ function getNodeInstallArgs(version = 'lts'): CommandResult {
 
 /**
  * 获取 VS Code 安装命令
+ * @param version 版本号
+ * @param customPath 自定义安装路径 (仅 Windows 支持)
  */
-function getVSCodeInstallArgs(version = 'stable'): CommandResult {
+function getVSCodeInstallArgs(version = 'stable', customPath?: string): CommandResult {
   const platform = os.platform() as Platform
 
   if (version === 'insiders') {
     switch (platform) {
-      case 'win32':
-        return {
-          command: 'winget',
-          args: ['install', '-e', '--id', WINGET_PACKAGES.vscode.insiders, '--silent', '--accept-source-agreements', '--accept-package-agreements']
+      case 'win32': {
+        const args = ['install', '-e', '--id', WINGET_PACKAGES.vscode.insiders, '--silent', '--accept-source-agreements', '--accept-package-agreements']
+        if (customPath) {
+          args.push('--location', customPath)
         }
+        return { command: 'winget', args }
+      }
       case 'darwin':
         return {
           command: 'brew',
@@ -297,6 +306,9 @@ function getVSCodeInstallArgs(version = 'stable'): CommandResult {
       if (version !== 'stable' && /^\d+\.\d+\.\d+$/.test(version)) {
         args.push('--version', version)
       }
+      if (customPath) {
+        args.push('--location', customPath)
+      }
       return { command: 'winget', args }
     }
     case 'darwin':
@@ -313,14 +325,21 @@ function getVSCodeInstallArgs(version = 'stable'): CommandResult {
 
 /**
  * 获取 Git 安装命令
+ * @param version 版本号
+ * @param customPath 自定义安装路径 (仅 Windows 支持)
  */
-function getGitInstallArgs(version = 'stable'): CommandResult {
+function getGitInstallArgs(version = 'stable', customPath?: string): CommandResult {
   const platform = os.platform() as Platform
 
   if (version === 'mingit') {
     switch (platform) {
-      case 'win32':
-        return { command: 'winget', args: ['install', '-e', '--id', WINGET_PACKAGES.git.mingit, '--silent', '--accept-source-agreements', '--accept-package-agreements'] }
+      case 'win32': {
+        const args = ['install', '-e', '--id', WINGET_PACKAGES.git.mingit, '--silent', '--accept-source-agreements', '--accept-package-agreements']
+        if (customPath) {
+          args.push('--location', customPath)
+        }
+        return { command: 'winget', args }
+      }
       case 'darwin':
         return { command: 'brew', args: ['install', BREW_PACKAGES.git.stable] }
       case 'linux':
@@ -332,8 +351,13 @@ function getGitInstallArgs(version = 'stable'): CommandResult {
 
   if (version === 'lfs') {
     switch (platform) {
-      case 'win32':
-        return { command: 'winget', args: ['install', '-e', '--id', WINGET_PACKAGES.git.lfs, '--silent', '--accept-source-agreements', '--accept-package-agreements'] }
+      case 'win32': {
+        const args = ['install', '-e', '--id', WINGET_PACKAGES.git.lfs, '--silent', '--accept-source-agreements', '--accept-package-agreements']
+        if (customPath) {
+          args.push('--location', customPath)
+        }
+        return { command: 'winget', args }
+      }
       case 'darwin':
         return { command: 'brew', args: ['install', BREW_PACKAGES.git.lfs] }
       case 'linux':
@@ -349,6 +373,9 @@ function getGitInstallArgs(version = 'stable'): CommandResult {
       if (version !== 'stable' && /^\d+\.\d+\.\d+$/.test(version)) {
         args.push('--version', version)
       }
+      if (customPath) {
+        args.push('--location', customPath)
+      }
       return { command: 'winget', args }
     }
     case 'darwin':
@@ -434,16 +461,21 @@ async function aptUpdate(onStatus: StatusCallback, software: SoftwareTypeWithAll
 
 /**
  * 安装 Node.js
+ * @param version 版本号
+ * @param installPnpm 是否安装 pnpm
+ * @param onStatus 状态回调
+ * @param customPath 自定义安装路径 (仅 Windows 支持)
  */
 export async function installNodejs(
   version = 'lts',
   installPnpm = true,
-  onStatus: StatusCallback
+  onStatus: StatusCallback,
+  customPath?: string
 ): Promise<void> {
   resetCancelState()
 
   onStatus('nodejs', `${STATUS_MESSAGES.INSTALLING} Node.js...`, 20)
-  const args = getNodeInstallArgs(version)
+  const args = getNodeInstallArgs(version, customPath)
   await executeCommand(args.command, args.args, true)
 
   checkCancelled()
@@ -483,24 +515,30 @@ export async function installNodejs(
 
 /**
  * 安装 VS Code
+ * @param version 版本号
+ * @param onStatus 状态回调
+ * @param customPath 自定义安装路径 (仅 Windows 支持)
  */
-export async function installVscode(version = 'stable', onStatus: StatusCallback): Promise<void> {
+export async function installVscode(version = 'stable', onStatus: StatusCallback, customPath?: string): Promise<void> {
   resetCancelState()
 
   onStatus('vscode', `${STATUS_MESSAGES.INSTALLING} VS Code...`, 30)
-  const args = getVSCodeInstallArgs(version)
+  const args = getVSCodeInstallArgs(version, customPath)
   await executeCommand(args.command, args.args, true)
   onStatus('vscode', STATUS_MESSAGES.COMPLETE, 100)
 }
 
 /**
  * 安装 Git
+ * @param version 版本号
+ * @param onStatus 状态回调
+ * @param customPath 自定义安装路径 (仅 Windows 支持)
  */
-export async function installGit(version = 'stable', onStatus: StatusCallback): Promise<void> {
+export async function installGit(version = 'stable', onStatus: StatusCallback, customPath?: string): Promise<void> {
   resetCancelState()
 
   onStatus('git', `${STATUS_MESSAGES.INSTALLING} Git...`, 30)
-  const args = getGitInstallArgs(version)
+  const args = getGitInstallArgs(version, customPath)
   await executeCommand(args.command, args.args, true)
   onStatus('git', STATUS_MESSAGES.COMPLETE, 100)
 }
@@ -529,11 +567,14 @@ export async function installAll(options: InstallOptions, onStatus: StatusCallba
   const {
     installNodejs: doNodejs = true,
     nodejsVersion = 'lts',
+    nodejsPath,
     installPnpm: doPnpm = true,
     installVscode: doVscode = true,
     vscodeVersion = 'stable',
+    vscodePath,
     installGit: doGit = true,
     gitVersion = 'stable',
+    gitPath,
     installClaudeCode: doClaudeCode = false
   } = options
 
@@ -549,7 +590,7 @@ export async function installAll(options: InstallOptions, onStatus: StatusCallba
   if (doNodejs) {
     checkCancelled()
     onStatus('all', `${STATUS_MESSAGES.INSTALLING} Node.js...`, getProgress())
-    const nodeArgs = getNodeInstallArgs(nodejsVersion)
+    const nodeArgs = getNodeInstallArgs(nodejsVersion, nodejsPath)
     await executeCommand(nodeArgs.command, nodeArgs.args, true)
 
     // 使用完整路径,避免 PATH 未生效的问题
@@ -582,7 +623,7 @@ export async function installAll(options: InstallOptions, onStatus: StatusCallba
   if (doVscode) {
     checkCancelled()
     onStatus('all', `${STATUS_MESSAGES.INSTALLING} VS Code...`, getProgress())
-    const vscodeArgs = getVSCodeInstallArgs(vscodeVersion)
+    const vscodeArgs = getVSCodeInstallArgs(vscodeVersion, vscodePath)
     await executeCommand(vscodeArgs.command, vscodeArgs.args, true)
     currentStep++
   }
@@ -591,7 +632,7 @@ export async function installAll(options: InstallOptions, onStatus: StatusCallba
   if (doGit) {
     checkCancelled()
     onStatus('all', `${STATUS_MESSAGES.INSTALLING} Git...`, getProgress())
-    const gitArgs = getGitInstallArgs(gitVersion)
+    const gitArgs = getGitInstallArgs(gitVersion, gitPath)
     await executeCommand(gitArgs.command, gitArgs.args, true)
     currentStep++
   }

+ 4 - 4
ApqInstaller/electron/modules/ipc-handlers.ts

@@ -343,7 +343,7 @@ export function registerHandlers(): void {
 
   // 统一安装入口
   ipcMain.handle('install', async (_event, software: SoftwareTypeWithAll, options = {}) => {
-    const { version, installPnpm = true } = options
+    const { version, installPnpm = true, nodejsPath, vscodePath, gitPath } = options
     const startTime = Date.now()
 
     // 状态回调
@@ -374,17 +374,17 @@ export function registerHandlers(): void {
 
       switch (software) {
         case 'nodejs':
-          await installNodejs(version, installPnpm, onStatus)
+          await installNodejs(version, installPnpm, onStatus, nodejsPath)
           onComplete('nodejs', installPnpm ? '✅ Node.js + pnpm 安装完成!' : '✅ Node.js 安装完成!')
           break
 
         case 'vscode':
-          await installVscode(version, onStatus)
+          await installVscode(version, onStatus, vscodePath)
           onComplete('vscode', '✅ VS Code 安装完成!')
           break
 
         case 'git':
-          await installGit(version, onStatus)
+          await installGit(version, onStatus, gitPath)
           onComplete('git', '✅ Git 安装完成!')
           break
 

+ 3 - 0
ApqInstaller/electron/modules/types.ts

@@ -35,10 +35,13 @@ export interface InstallOptions {
   installPnpm?: boolean
   installNodejs?: boolean
   nodejsVersion?: string
+  nodejsPath?: string // Node.js 自定义安装路径 (仅 Windows)
   installVscode?: boolean
   vscodeVersion?: string
+  vscodePath?: string // VS Code 自定义安装路径 (仅 Windows)
   installGit?: boolean
   gitVersion?: string
+  gitPath?: string // Git 自定义安装路径 (仅 Windows)
   installClaudeCode?: boolean
   customPath?: string
 }

+ 3 - 1
ApqInstaller/src/i18n/en-US.ts

@@ -101,7 +101,9 @@ export default {
     configuring: 'Configuring',
     updatingSource: 'Updating package sources...',
     allInstalled: 'All Installed',
-    allInstalledHint: 'All software is already installed'
+    allInstalledHint: 'All software is already installed',
+    customPath: 'Install Path',
+    customPathPlaceholder: 'Leave empty for default path'
   },
   settings: {
     title: 'Settings',

+ 3 - 1
ApqInstaller/src/i18n/zh-CN.ts

@@ -101,7 +101,9 @@ export default {
     configuring: '正在配置',
     updatingSource: '正在更新软件源...',
     allInstalled: '全部已安装',
-    allInstalledHint: '所有软件均已安装'
+    allInstalledHint: '所有软件均已安装',
+    customPath: '安装路径',
+    customPathPlaceholder: '留空使用默认路径'
   },
   settings: {
     title: '设置',

+ 8 - 1
ApqInstaller/src/stores/install.ts

@@ -24,13 +24,18 @@ interface InstallStatusState {
 }
 
 interface InstallOptionsState {
-  nodejs: { installPnpm: boolean }
+  nodejs: { installPnpm: boolean; customPath?: string }
+  vscode: { customPath?: string }
+  git: { customPath?: string }
   all: {
     installNodejs: boolean
     installVscode: boolean
     installGit: boolean
     installPnpm: boolean
     installClaudeCode: boolean
+    nodejsPath?: string
+    vscodePath?: string
+    gitPath?: string
   }
 }
 
@@ -68,6 +73,8 @@ export const useInstallStore = defineStore('install', () => {
   // 安装选项
   const installOptions = ref<InstallOptionsState>({
     nodejs: { installPnpm: true },
+    vscode: {},
+    git: {},
     all: {
       installNodejs: true,
       installVscode: true,

+ 54 - 0
ApqInstaller/src/views/BatchInstallView.vue

@@ -3,11 +3,16 @@ import { computed } 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'))
 
@@ -54,11 +59,14 @@ async function handleInstall() {
   const options = {
     installNodejs: needInstallNodejs.value,
     nodejsVersion: versionsStore.selectedVersions.nodejs,
+    nodejsPath: installStore.installOptions.all.nodejsPath,
     installPnpm: needInstallNodejs.value && installStore.installOptions.all.installPnpm,
     installVscode: needInstallVscode.value,
     vscodeVersion: versionsStore.selectedVersions.vscode,
+    vscodePath: installStore.installOptions.all.vscodePath,
     installGit: needInstallGit.value,
     gitVersion: versionsStore.selectedVersions.git,
+    gitPath: installStore.installOptions.all.gitPath,
     installClaudeCode: needInstallClaudeCode.value
   }
 
@@ -103,6 +111,15 @@ async function handleCancel() {
             <el-checkbox v-model="installStore.installOptions.all.installPnpm" size="small">+ pnpm</el-checkbox>
           </div>
         </div>
+        <div v-if="needInstallNodejs && 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
+          />
+        </div>
       </div>
 
       <!-- VS Code -->
@@ -129,6 +146,15 @@ async function handleCancel() {
             </el-select>
           </div>
         </div>
+        <div v-if="needInstallVscode && 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
+          />
+        </div>
       </div>
 
       <!-- Git -->
@@ -155,6 +181,15 @@ async function handleCancel() {
             </el-select>
           </div>
         </div>
+        <div v-if="needInstallGit && 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
+          />
+        </div>
       </div>
 
       <!-- Claude Code -->
@@ -219,6 +254,25 @@ async function handleCancel() {
         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 {