黄中银 пре 1 месец
родитељ
комит
6b8679d785

+ 68 - 34
ApqInstaller/electron/modules/installer.ts

@@ -130,11 +130,19 @@ function sudoExec(command: string): Promise<{ stdout: string; stderr: string }>
   return new Promise((resolve, reject) => {
     const options = { name: APP_NAME }
     sudo.exec(command, options, (error, stdout, stderr) => {
+      // 过滤乱码字符的辅助函数(Windows 命令行输出可能是 GBK 编码)
+      const cleanOutput = (str: string | Buffer | undefined): string => {
+        if (!str) return ''
+        const raw = str.toString()
+        // 过滤掉非 ASCII 可打印字符和非中文字符,保留换行符
+        return raw.replace(/[^\x20-\x7E\u4e00-\u9fa5\n\r]/g, '').trim()
+      }
+
       if (error) {
         // 包含 stderr 和 stdout 信息以便调试
         const exitCode = (error as Error & { code?: number }).code
-        const stderrStr = stderr?.toString() || ''
-        const stdoutStr = stdout?.toString() || ''
+        const stderrStr = cleanOutput(stderr)
+        const stdoutStr = cleanOutput(stdout)
         let errorMessage = error.message
         if (exitCode !== undefined) {
           errorMessage += `\n退出码: ${exitCode}`
@@ -149,8 +157,8 @@ function sudoExec(command: string): Promise<{ stdout: string; stderr: string }>
         reject(enhancedError)
       } else {
         resolve({
-          stdout: stdout?.toString() || '',
-          stderr: stderr?.toString() || ''
+          stdout: cleanOutput(stdout),
+          stderr: cleanOutput(stderr)
         })
       }
     })
@@ -436,7 +444,7 @@ function getUninstallArgs(software: UninstallableSoftwareType): CommandResult {
 
 // ==================== 安装流程 ====================
 
-export type StatusCallback = (software: SoftwareTypeWithAll, message: string, progress: number) => void
+export type StatusCallback = (software: SoftwareTypeWithAll, message: string, progress: number, skipLog?: boolean) => void
 export type CompleteCallback = (software: SoftwareTypeWithAll, message: string) => void
 export type ErrorCallback = (software: SoftwareTypeWithAll, message: string) => void
 
@@ -495,16 +503,17 @@ export async function installNodejs(
     logger.installInfo(`开始下载 Node.js: ${downloadUrl}`)
 
     try {
-      let lastReportedPercent = 0
+      let lastLoggedPercent = 0
       await downloadFile(downloadUrl, installerPath, (downloaded, total, percent) => {
-        // 每 5% 报告一次进度,减少日志量
-        if (percent - lastReportedPercent >= 5 || percent >= 100) {
-          lastReportedPercent = Math.floor(percent / 5) * 5
-          const progress = 10 + Math.round(percent * 0.4)
-          const downloadedMB = (downloaded / 1024 / 1024).toFixed(1)
-          const totalMB = (total / 1024 / 1024).toFixed(1)
-          onStatus('nodejs', `正在下载 Node.js ${targetVersion}... (${downloadedMB}MB / ${totalMB}MB)`, progress)
+        const progress = 10 + Math.round(percent * 0.4)
+        const downloadedMB = (downloaded / 1024 / 1024).toFixed(1)
+        const totalMB = (total / 1024 / 1024).toFixed(1)
+        // 每次都更新进度条,但只在每 5% 时记录日志
+        const shouldLog = percent - lastLoggedPercent >= 5 || percent >= 100
+        if (shouldLog) {
+          lastLoggedPercent = Math.floor(percent / 5) * 5
         }
+        onStatus('nodejs', `正在下载 Node.js ${targetVersion}... (${downloadedMB}MB / ${totalMB}MB)`, progress, !shouldLog)
       })
     } catch (error) {
       logger.installError('下载 Node.js 失败', error)
@@ -662,7 +671,7 @@ export async function installPnpm(onStatus: StatusCallback): Promise<void> {
 
   // 刷新系统环境变量,确保能找到刚安装的 Node.js
   const { getRefreshedPath } = await import('./utils')
-  const refreshedPath = await getRefreshedPath()
+  await getRefreshedPath()
 
   // 使用完整路径,避免 PATH 未生效的问题
   const npmCmd = getNpmPath()
@@ -674,10 +683,33 @@ export async function installPnpm(onStatus: StatusCallback): Promise<void> {
 
   checkCancelled()
 
-  // 安装 pnpm,使用刷新后的 PATH
+  // 安装 pnpm,需要管理员权限进行全局安装
   onStatus('pnpm', `${STATUS_MESSAGES.INSTALLING} pnpm...`, 40)
-  const installEnv = { ...process.env, PATH: refreshedPath }
-  await execa(npmCmd, ['install', '-g', 'pnpm'], { env: installEnv, shell: platform === 'win32' })
+  try {
+    await executeCommand(npmCmd, ['install', '-g', 'pnpm'], true)
+  } catch (error) {
+    // 提取更有意义的错误信息,过滤乱码
+    const execaError = error as { message?: string; stderr?: string; stdout?: string; shortMessage?: string; exitCode?: number }
+    let errorMessage = `npm install -g pnpm 失败`
+    if (execaError.exitCode !== undefined) {
+      errorMessage += ` (退出码: ${execaError.exitCode})`
+    }
+    // 过滤乱码字符,只保留可读字符
+    if (execaError.stderr) {
+      const stderrClean = execaError.stderr.replace(/[^\x20-\x7E\u4e00-\u9fa5\n\r]/g, '').trim()
+      if (stderrClean) {
+        errorMessage += `\n${stderrClean}`
+      }
+    }
+    if (execaError.stdout) {
+      const stdoutClean = execaError.stdout.replace(/[^\x20-\x7E\u4e00-\u9fa5\n\r]/g, '').trim()
+      if (stdoutClean) {
+        errorMessage += `\n${stdoutClean}`
+      }
+    }
+    logger.installError('安装 pnpm 失败', error)
+    throw new Error(errorMessage)
+  }
 
   checkCancelled()
 
@@ -744,16 +776,17 @@ export async function installVscode(version = 'stable', onStatus: StatusCallback
     logger.installInfo(`开始下载 VS Code: ${downloadUrl}`)
 
     try {
-      let lastReportedPercent = 0
+      let lastLoggedPercent = 0
       await downloadFile(downloadUrl, installerPath, (downloaded, total, percent) => {
-        // 每 5% 报告一次进度,减少日志量
-        if (percent - lastReportedPercent >= 5 || percent >= 100) {
-          lastReportedPercent = Math.floor(percent / 5) * 5
-          const progress = 10 + Math.round(percent * 0.5)
-          const downloadedMB = (downloaded / 1024 / 1024).toFixed(1)
-          const totalMB = (total / 1024 / 1024).toFixed(1)
-          onStatus('vscode', `正在下载 VS Code ${targetVersion}... (${downloadedMB}MB / ${totalMB}MB)`, progress)
+        const progress = 10 + Math.round(percent * 0.5)
+        const downloadedMB = (downloaded / 1024 / 1024).toFixed(1)
+        const totalMB = (total / 1024 / 1024).toFixed(1)
+        // 每次都更新进度条,但只在每 5% 时记录日志
+        const shouldLog = percent - lastLoggedPercent >= 5 || percent >= 100
+        if (shouldLog) {
+          lastLoggedPercent = Math.floor(percent / 5) * 5
         }
+        onStatus('vscode', `正在下载 VS Code ${targetVersion}... (${downloadedMB}MB / ${totalMB}MB)`, progress, !shouldLog)
       })
     } catch (error) {
       logger.installError('下载 VS Code 失败', error)
@@ -851,17 +884,18 @@ export async function installGit(version = 'stable', onStatus: StatusCallback, c
     logger.installInfo(`开始下载 Git: ${downloadUrl}`)
 
     try {
-      let lastReportedPercent = 0
+      let lastLoggedPercent = 0
       await downloadFile(downloadUrl, installerPath, (downloaded, total, percent) => {
-        // 每 5% 报告一次进度,减少日志量
-        if (percent - lastReportedPercent >= 5 || percent >= 100) {
-          lastReportedPercent = Math.floor(percent / 5) * 5
-          // 下载进度占 10% - 60%
-          const progress = 10 + Math.round(percent * 0.5)
-          const downloadedMB = (downloaded / 1024 / 1024).toFixed(1)
-          const totalMB = (total / 1024 / 1024).toFixed(1)
-          onStatus('git', `正在下载 Git ${targetVersion}... (${downloadedMB}MB / ${totalMB}MB)`, progress)
+        // 下载进度占 10% - 60%
+        const progress = 10 + Math.round(percent * 0.5)
+        const downloadedMB = (downloaded / 1024 / 1024).toFixed(1)
+        const totalMB = (total / 1024 / 1024).toFixed(1)
+        // 每次都更新进度条,但只在每 5% 时记录日志
+        const shouldLog = percent - lastLoggedPercent >= 5 || percent >= 100
+        if (shouldLog) {
+          lastLoggedPercent = Math.floor(percent / 5) * 5
         }
+        onStatus('git', `正在下载 Git ${targetVersion}... (${downloadedMB}MB / ${totalMB}MB)`, progress, !shouldLog)
       })
     } catch (error) {
       logger.installError('下载 Git 失败', error)

+ 5 - 3
ApqInstaller/electron/modules/ipc-handlers.ts

@@ -811,9 +811,11 @@ export function registerHandlers(): void {
     const startTime = Date.now()
 
     // 状态回调
-    const onStatus = (sw: SoftwareTypeWithAll, message: string, progress: number): void => {
-      sendToRenderer('install-status', { software: sw, message, progress })
-      logger.installInfo(`[${sw}] ${message} (${progress}%)`)
+    const onStatus = (sw: SoftwareTypeWithAll, message: string, progress: number, skipLog?: boolean): void => {
+      sendToRenderer('install-status', { software: sw, message, progress, skipLog })
+      if (!skipLog) {
+        logger.installInfo(`[${sw}] ${message} (${progress}%)`)
+      }
     }
 
     try {

+ 1 - 0
ApqInstaller/shared/types.ts

@@ -52,6 +52,7 @@ export interface InstallStatus {
   progress: number
   i18nKey?: string
   i18nParams?: Record<string, string>
+  skipLog?: boolean
 }
 
 export interface InstallResult {

+ 66 - 57
ApqInstaller/src/App.vue

@@ -79,7 +79,10 @@ function bindInstallListeners() {
   window.electronAPI.onInstallStatus((data) => {
     const translatedMessage = getTranslatedMessage(data.message, data.i18nKey, data.i18nParams)
     installStore.updateStatus(data.software, translatedMessage, data.progress)
-    installStore.addLog(translatedMessage)
+    // 只在 skipLog 不为 true 时记录日志
+    if (!data.skipLog) {
+      installStore.addLog(translatedMessage)
+    }
   })
 
   window.electronAPI.onInstallComplete((data) => {
@@ -147,68 +150,74 @@ onUnmounted(() => {
     <div class="app-container">
       <!-- 主面板 -->
       <div class="main-panel">
-        <!-- 顶部菜单栏 -->
-        <div class="menu-bar">
-          <div class="menu-left">
-            <h1>{{ t('app.title') }}</h1>
+        <!-- 固定头部区域 -->
+        <div class="header-fixed">
+          <!-- 顶部菜单栏 -->
+          <div class="menu-bar">
+            <div class="menu-left">
+              <h1>{{ t('app.title') }}</h1>
+            </div>
+            <div class="menu-right">
+              <SettingsPanel />
+            </div>
           </div>
-          <div class="menu-right">
-            <SettingsPanel />
-          </div>
-        </div>
 
-        <!-- 下载源选择栏 -->
-        <MirrorSelector />
-
-        <!-- 系统状态提示 -->
-        <el-alert
-          v-if="systemStore.systemStatus.visible"
-          :type="systemStore.systemStatus.type"
-          :title="t(systemStore.systemStatus.messageKey, systemStore.systemStatus.messageParams)"
-          :closable="true"
-          show-icon
-          @close="systemStore.hideSystemStatus"
-        >
-          <template v-if="systemStore.systemStatus.actionTextKey" #default>
-            <el-button size="small" type="primary" @click="systemStore.systemStatus.actionHandler?.()">
-              {{ t(systemStore.systemStatus.actionTextKey, systemStore.systemStatus.actionTextParams) }}
-            </el-button>
-          </template>
-        </el-alert>
-
-        <!-- 标签页 -->
-        <div class="tabs">
-          <button
-            v-for="tab in tabs"
-            :key="tab.id"
-            :class="['tab-btn', { active: systemStore.activeTab === tab.id }]"
-            @click="systemStore.setActiveTab(tab.id)"
+          <!-- 下载源选择栏 -->
+          <MirrorSelector />
+
+          <!-- 系统状态提示 -->
+          <el-alert
+            v-if="systemStore.systemStatus.visible"
+            :type="systemStore.systemStatus.type"
+            :title="t(systemStore.systemStatus.messageKey, systemStore.systemStatus.messageParams)"
+            :closable="true"
+            show-icon
+            @close="systemStore.hideSystemStatus"
           >
-            <SoftwareIcon :software="tab.id" :size="18" />
-            <span class="tab-label">{{ t(tab.label) }}</span>
-            <span
-              v-if="tab.showInstalledDot && tab.softwareType && installStore.isInstalled(tab.softwareType)"
-              class="installed-dot"
-            ></span>
-          </button>
+            <template v-if="systemStore.systemStatus.actionTextKey" #default>
+              <el-button size="small" type="primary" @click="systemStore.systemStatus.actionHandler?.()">
+                {{ t(systemStore.systemStatus.actionTextKey, systemStore.systemStatus.actionTextParams) }}
+              </el-button>
+            </template>
+          </el-alert>
+
+          <!-- 标签页 -->
+          <div class="tabs">
+            <button
+              v-for="tab in tabs"
+              :key="tab.id"
+              :class="['tab-btn', { active: systemStore.activeTab === tab.id }]"
+              @click="systemStore.setActiveTab(tab.id)"
+            >
+              <SoftwareIcon :software="tab.id" :size="18" />
+              <span class="tab-label">{{ t(tab.label) }}</span>
+              <span
+                v-if="tab.showInstalledDot && tab.softwareType && installStore.isInstalled(tab.softwareType)"
+                class="installed-dot"
+              ></span>
+            </button>
+          </div>
         </div>
 
-        <!-- 标签页内容 - 使用动态组件实现懒加载 -->
-        <div class="tab-content">
-          <ErrorBoundary>
-            <KeepAlive>
-              <component
-                :is="currentComponent"
-                :key="currentTab.id"
-                @install="handleInstall"
-                @cancel="handleCancel"
-              />
-            </KeepAlive>
-          </ErrorBoundary>
-        </div>
+        <!-- 可滚动内容区域 -->
+        <div class="content-scrollable">
+          <!-- 标签页内容 - 使用动态组件实现懒加载 -->
+          <div class="tab-content">
+            <ErrorBoundary>
+              <KeepAlive>
+                <component
+                  :is="currentComponent"
+                  :key="currentTab.id"
+                  @install="handleInstall"
+                  @cancel="handleCancel"
+                />
+              </KeepAlive>
+            </ErrorBoundary>
+          </div>
 
-        <!-- 安装日志 -->
-        <InstallLog v-if="installStore.isAnyInstalling || installStore.installLogs.length > 0" />
+          <!-- 安装日志 -->
+          <InstallLog v-if="installStore.isAnyInstalling || installStore.installLogs.length > 0" />
+        </div>
       </div>
     </div>
   </div>

+ 13 - 1
ApqInstaller/src/styles/main.scss

@@ -43,6 +43,7 @@ body {
   box-shadow: var(--box-shadow-light);
   padding: var(--spacing-lg);
   overflow: hidden;
+  min-height: 0; // 允许 flex 子元素收缩
 
   h1 {
     font-size: 24px;
@@ -52,6 +53,18 @@ body {
   }
 }
 
+// 固定头部区域(不滚动)
+.header-fixed {
+  flex-shrink: 0;
+}
+
+// 可滚动内容区域
+.content-scrollable {
+  flex: 1;
+  overflow-y: auto;
+  min-height: 0; // 允许 flex 子元素收缩
+}
+
 
 // 标签页
 .tabs {
@@ -105,7 +118,6 @@ body {
 // 标签页内容
 .tab-content {
   flex: 1;
-  overflow-y: auto;
 }
 
 .tab-pane {

+ 1 - 1
ApqInstaller/src/views/IntroView.vue

@@ -8,9 +8,9 @@ const systemStore = useSystemStore()
 
 const features = [
   { key: 'nodejs', tab: 'nodejs' },
+  { key: 'claudeCode', tab: 'claudeCode' },
   { key: 'vscode', tab: 'vscode' },
   { key: 'git', tab: 'git' },
-  { key: 'claudeCode', tab: 'claudeCode' },
   { key: 'oneClick', tab: 'all' }
 ]