<template>
  <div class="token-import-page">
    <!-- 顶部导航 -->
    <AppNavbar />

    <div class="container">
      <!-- 页面头部 -->
      <div class="page-header">
        <div class="header-content">
          <h1>游戏Token管理</h1>
        </div>
      </div>

      <!-- Token导入区域 -->
      <div
        ref="importSectionRef"
        v-if="!tokenStore.hasTokens || showImportForm"
        class="import-section"
      >
        <div class="import-card">
          <div class="card-header">
            <h2>
              <n-icon><Add /></n-icon>
              添加游戏Token
            </h2>
            
            <!-- 导入方式选择 -->
            <n-radio-group
              v-model:value="importMethod"
              class="import-method-tabs"
              size="small"
            >
              <!-- 隐藏手动输入和URL获取 -->
              <!-- <n-radio-button value="manual">
                手动输入
              </n-radio-button>
              <n-radio-button value="url">
                URL获取
              </n-radio-button> -->
              <n-radio-button value="bin">
                <span class="btn-text-full">bin文件上传</span>
                <span class="btn-text-short">bin上传</span>
              </n-radio-button>
              <n-radio-button value="archive">
                <span class="btn-text-full">压缩包上传</span>
                <span class="btn-text-short">压缩包</span>
              </n-radio-button>
            </n-radio-group>
          </div>

          <!-- 手动输入表单 -->
          <n-form
            v-if="importMethod === 'manual'"
            ref="importFormRef"
            :model="importForm"
            :rules="importRules"
            :label-placement="'top'"
            :size="'large'"
            :show-label="true"
          >
            <n-form-item
              :label="'游戏角色名称'"
              :path="'name'"
              :show-label="true"
            >
              <n-input
                v-model:value="importForm.name"
                placeholder="悦耳大王到此一游"
                clearable
              />
            </n-form-item>

            <n-form-item
              :label="'Token字符串'"
              :path="'base64Token'"
              :show-label="true"
            >
              <n-input
                v-model:value="importForm.base64Token"
                type="textarea"
                :rows="3"
                placeholder="粘贴Token字符串..."
                clearable
              />
            </n-form-item>

            <!-- 角色详情 -->
            <n-collapse>
              <n-collapse-item
                title="角色详情 (可选)"
                name="optional"
              >
                <div class="optional-fields">
                  <n-form-item label="服务器">
                    <n-input
                      v-model:value="importForm.server"
                      placeholder="服务器名称"
                    />
                  </n-form-item>

                  <n-form-item label="自定义连接地址">
                    <n-input
                      v-model:value="importForm.wsUrl"
                      placeholder="留空使用默认连接"
                    />
                  </n-form-item>
                </div>
              </n-collapse-item>
            </n-collapse>


            <div class="form-actions">
              <n-button
                type="primary"
                size="large"
                block
                :loading="isImporting"
                @click="handleImport"
              >
                <template #icon>
                  <n-icon><CloudUpload /></n-icon>
                </template>
                添加Token
              </n-button>

              <n-button
                v-if="tokenStore.hasTokens"
                size="large"
                block
                @click="showImportForm = false"
              >
                取消
              </n-button>
            </div>
          </n-form>

          <!-- URL获取表单 -->
          <n-form
            v-if="importMethod === 'url'"
            ref="urlFormRef"
            :model="urlForm"
            :rules="urlRules"
            label-placement="top"
            size="large"
          >
            <n-form-item
              label="游戏角色名称"
              path="name"
            >
              <n-input
                v-model:value="urlForm.name"
                placeholder="悦耳大王到此一游"
                clearable
              />
            </n-form-item>

            <n-form-item
              label="Token获取地址"
              path="url"
            >
              <n-input
                v-model:value="urlForm.url"
                placeholder="输入API接口地址..."
                clearable
              />
              <template #feedback>
                <div class="form-tips">
                  <span class="form-tip">
                    接口应返回包含token字段的JSON数据
                  </span>
                  <span class="form-tip cors-tip">
                    注意：如果是跨域URL，服务器需要支持CORS，否则会被浏览器阻止
                  </span>
                </div>
              </template>
            </n-form-item>

            <!-- 角色详情 -->
            <n-collapse>
              <n-collapse-item
                title="角色详情 (可选)"
                name="optional"
              >
                <div class="optional-fields">
                  <n-form-item label="服务器">
                    <n-input
                      v-model:value="urlForm.server"
                      placeholder="服务器名称"
                    />
                  </n-form-item>

                  <n-form-item label="自定义连接地址">
                    <n-input
                      v-model:value="urlForm.wsUrl"
                      placeholder="留空使用默认连接"
                    />
                  </n-form-item>
                </div>
              </n-collapse-item>
            </n-collapse>

            <div class="form-actions">
              <n-button
                type="primary"
                size="large"
                block
                :loading="isImporting"
                @click="handleUrlImport"
              >
                <template #icon>
                  <n-icon><CloudUpload /></n-icon>
                </template>
                获取并添加Token
              </n-button>

              <n-button
                v-if="tokenStore.hasTokens"
                size="large"
                block
                @click="showImportForm = false"
              >
                取消
              </n-button>
            </div>
          </n-form>

          <!-- bin文件上传表单 -->
          <n-form
            v-if="importMethod === 'bin'"
            ref="binFormRef"
            :model="binForm"
            :rules="binRules"
            label-placement="top"
            size="large"
          >
            <n-form-item
              label="游戏角色名称"
              path="name"
            >
              <n-input
                v-model:value="binForm.name"
                placeholder="悦耳大王到此一游"
                clearable
              />
            </n-form-item>

            <n-form-item
              label="Bin文件上传"
              path="files"
            >
              <!-- 整体容器：确保按钮和文件列表垂直排列 -->
              <div class="bin-upload-wrapper">
                <!-- 上传按钮区域 -->
                <div class="upload-buttons-wrapper">
                  <!-- 使用原生文件输入和按钮组合，增强浏览器兼容性 -->
                  <div class="file-upload-container">
                    <n-button @click="() => fileInputRef?.click()">
                      <template #icon>
                        <n-icon><CloudUpload /></n-icon>
                      </template>
                      选择bin文件
                    </n-button>
                    <input
                      ref="fileInputRef"
                      type="file"
                      multiple
                      accept=".bin"
                      style="display: none"
                      @change="handleFileSelect"
                    />
                  </div>
                  
                  <!-- 文件夹批量上传区域 -->
                  <div class="folder-batch-upload">
                    <n-button @click="showFolderBatchUpload = true" type="info" dashed>
                      <template #icon>
                        <n-icon><FolderOpen /></n-icon>
                      </template>
                      {{ isMobileDevice ? '批量上传' : '文件夹批量上传' }}
                    </n-button>
                  </div>
                </div>
                
                <!-- 文件列表显示（放在按钮下方） -->
                <div v-if="binForm.files && binForm.files.length > 0" class="file-list-container">
                <div class="file-list-header">
                  <n-divider title-placement="left">
                    <span class="file-count">已选择 {{ binForm.files.length }} 个文件</span>
                  </n-divider>
                  <n-button 
                    text 
                    size="small" 
                    @click="showAllFiles = !showAllFiles"
                    v-if="binForm.files.length > 10"
                  >
                    {{ showAllFiles ? '收起' : '展开全部' }}
                    <template #icon>
                      <n-icon>
                        <component :is="showAllFiles ? ChevronUp : ChevronDown" />
                      </n-icon>
                    </template>
                  </n-button>
                </div>
                <div class="file-list" :class="{ 'collapsed': !showAllFiles && binForm.files.length > 10 }">
                  <n-tag 
                    v-for="(file, index) in binForm.files" 
                    :key="index" 
                    :closable="true" 
                    @close="removeFile(index)"
                    class="file-tag"
                  >
                    {{ file.name }}
                  </n-tag>
                </div>
              </div>
              </div><!-- 关闭 bin-upload-wrapper -->
              
              <template #feedback>
                <div class="form-tips">
                  <span class="form-tip">
                    支持批量上传多个.bin文件，文件名会作为游戏角色名称
                  </span>
                </div>
              </template>
            </n-form-item>

            <!-- 角色详情 -->
            <n-collapse>
              <n-collapse-item
                title="角色详情 (可选)"
                name="optional"
              >
                <div class="optional-fields">
                  <n-form-item label="服务器">
                    <n-input
                      v-model:value="binForm.server"
                      placeholder="服务器名称"
                    />
                  </n-form-item>

                  <n-form-item label="自定义连接地址">
                    <n-input
                      v-model:value="binForm.wsUrl"
                      placeholder="留空使用默认连接"
                    />
                  </n-form-item>
                </div>
              </n-collapse-item>
            </n-collapse>

            <!-- 本地存储的bin文件 -->
            <n-collapse>
              <n-collapse-item
                title="本地存储的bin文件"
                name="storedBinFiles"
              >
                <div class="stored-bin-files">
                  <div v-if="storedBinFiles.length === 0" class="empty-state">
                    <p>暂无本地存储的bin文件</p>
                  </div>
                  <div v-else class="bin-files-list">
                    <div v-for="binFile in storedBinFiles" :key="binFile.id" class="bin-file-item">
                      <div class="bin-file-info">
                        <h4>{{ binFile.fileName }}</h4>
                        <p>角色名: {{ binFile.roleName }}</p>
                        <p>上传时间: {{ formatTime(binFile.createdAt) }}</p>
                      </div>
                      <div class="bin-file-actions">
                        <n-button size="small" type="primary" @click="useStoredBinFile(binFile)">
                          使用此文件
                        </n-button>
                        <n-button size="small" type="error" @click="deleteStoredBinFile(binFile.id)">
                          删除
                        </n-button>
                      </div>
                    </div>
                  </div>
                </div>
              </n-collapse-item>
            </n-collapse>

            <div class="form-actions">
              <n-button
                type="primary"
                size="large"
                block
                :loading="isImporting"
                @click="handleBinImport"
              >
                <template #icon>
                  <n-icon><CloudUpload /></n-icon>
                </template>
                导入并添加Token
              </n-button>

              <n-button
                v-if="tokenStore.hasTokens"
                size="large"
                block
                @click="showImportForm = false"
              >
                取消
              </n-button>
            </div>
          </n-form>

          <!-- 压缩包上传表单 -->
          <n-form
            v-if="importMethod === 'archive'"
            ref="archiveFormRef"
            :model="archiveForm"
            :rules="archiveRules"
            label-placement="top"
            size="large"
          >
            <n-form-item
              label="压缩包上传"
              path="archiveFile"
            >
              <!-- 使用原生文件输入和按钮组合，增强浏览器兼容性 -->
              <div class="file-upload-container">
                <n-button @click="() => archiveFileInputRef?.click()">
                  <template #icon>
                    <n-icon><CloudUpload /></n-icon>
                  </template>
                  选择压缩包
                </n-button>
                <input
                  ref="archiveFileInputRef"
                  type="file"
                  accept=".zip,.rar,.7z,.tar,.gz"
                  style="display: none"
                  @change="handleArchiveFileSelect"
                />
              </div>
              
              <!-- 文件列表显示 -->
              <div v-if="archiveForm.archiveFile" class="file-list">
                <n-divider title="已选择文件" align-text="left" />
                <n-tag :closable="true" @close="removeArchiveFile">
                  {{ archiveForm.archiveFile.name }}
                </n-tag>
              </div>
              <template #feedback>
                <div class="form-tips">
                  <span class="form-tip">
                    支持上传.zip、.rar、.7z、.tar、.gz等格式的压缩包，系统将自动解压并导入其中的所有.bin文件
                  </span>
                </div>
              </template>
            </n-form-item>

            <!-- 角色详情 -->
            <n-collapse>
              <n-collapse-item
                title="角色详情 (可选)"
                name="optional"
              >
                <div class="optional-fields">
                  <n-form-item label="服务器">
                    <n-input
                      v-model:value="archiveForm.server"
                      placeholder="服务器名称"
                    />
                  </n-form-item>

                  <n-form-item label="自定义连接地址">
                    <n-input
                      v-model:value="archiveForm.wsUrl"
                      placeholder="留空使用默认连接"
                    />
                  </n-form-item>
                </div>
              </n-collapse-item>
            </n-collapse>

            <div class="form-actions">
              <n-button
                type="primary"
                size="large"
                block
                :loading="isImporting"
                @click="handleArchiveImport"
              >
                <template #icon>
                  <n-icon><CloudUpload /></n-icon>
                </template>
                解压并导入Token
              </n-button>

              <n-button
                v-if="tokenStore.hasTokens"
                size="large"
                block
                @click="showImportForm = false"
              >
                取消
              </n-button>
            </div>
          </n-form>
        </div>
      </div>

      <!-- 🔥 v3.14.1: 文件上传进度显示 -->
      <div
        v-if="uploadProgress.show"
        class="upload-progress-section"
      >
        <n-card
          title="文件上传进度"
          :bordered="false"
          class="upload-progress-card"
        >
          <template #header-extra>
            <n-tag
              :type="uploadProgress.failedCount > 0 ? 'warning' : 'success'"
              size="small"
            >
              成功 {{ uploadProgress.successCount }} / 失败 {{ uploadProgress.failedCount }}
            </n-tag>
          </template>
          
          <div class="progress-content">
            <div class="progress-info">
              <n-text>正在处理：{{ uploadProgress.currentFile }}</n-text>
              <n-text depth="3">{{ uploadProgress.current }} / {{ uploadProgress.total }}</n-text>
            </div>
            
            <n-progress
              type="line"
              :percentage="(uploadProgress.current / uploadProgress.total) * 100"
              :status="uploadProgress.failedCount > 0 ? 'warning' : 'success'"
              :show-indicator="true"
            />
            
            <n-text depth="3" style="font-size: 12px; margin-top: 8px;">
              {{ getUploadTypeText() }}
            </n-text>
          </div>
        </n-card>
      </div>

      <!-- 批量自动化任务面板 -->
      <div
        v-if="tokenStore.hasTokens"
        class="batch-task-section"
      >
        <BatchTaskPanel />
      </div>

      <!-- Token进度列表（批量任务执行时显示，完成后不自动消失） -->
      <div
        v-if="batchStore.showProgress && tokenStore.hasTokens"
        class="progress-section"
      >
        <div class="progress-header">
          <div class="progress-title">
            <h2>执行进度</h2>
            <!-- 🚀 v3.13.5.5: 添加进度卡片显示开关 -->
            <n-tooltip placement="top">
              <template #trigger>
                <n-button
                  :type="showProgressCards ? 'primary' : 'default'"
                  size="small"
                  @click="toggleProgressCards"
                  style="margin-left: 12px;"
                >
                  <template #icon>
                    <n-icon>
                      <component :is="showProgressCards ? Eye : EyeOff" />
                    </n-icon>
                  </template>
                  {{ showProgressCards ? '隐藏卡片' : '显示卡片' }}
                </n-button>
              </template>
              {{ showProgressCards ? '隐藏进度卡片以减少渲染压力' : '显示进度卡片查看详细状态' }}
            </n-tooltip>
          </div>
          <n-space :size="8">
            <n-switch
              v-model:value="autoFollowLatest"
              @update:value="handleAutoFollowToggle"
            >
              <template #checked>
                自动跟随
              </template>
              <template #unchecked>
                自动跟随
              </template>
            </n-switch>
            <n-button
              secondary
              @click="handleCloseProgress"
            >
              <template #icon>
                <n-icon><Close /></n-icon>
              </template>
              关闭
            </n-button>
          </n-space>
        </div>
        <!-- 🚀 v3.13.5.5: 使用v-show控制进度卡片显示，减少渲染压力 -->
        <VirtualScrollList
          v-show="showProgressCards"
          ref="virtualScrollRef"
          :items="tokenStore.gameTokens"
          :item-height="220"
          :buffer="5"
          class="progress-grid"
          @scroll="handleProgressScroll"
        >
          <template #default="{ visibleItems }">
            <TaskProgressCard
              v-for="token in visibleItems"
              :key="token.id"
              :token-id="token.id"
              :token-name="token.name"
              :progress="batchStore.taskProgress[token.id]"
            />
          </template>
        </VirtualScrollList>
        
        <!-- 🚀 隐藏卡片时显示提示信息（极致紧凑版） -->
        <div v-if="!showProgressCards" class="progress-summary">
          <n-alert type="info" :bordered="false" size="small">
            <span style="font-size: 13px; line-height: 1.4;">
              💡 <strong>进度卡片已隐藏</strong>，可通过上方统计查看或点击"显示卡片"
            </span>
          </n-alert>
        </div>
      </div>

      <!-- Token列表 -->
      <div
        v-if="tokenStore.hasTokens"
        class="tokens-section"
      >
        <div class="section-header">
          <div class="section-title">
            <h2>我的Token列表</h2>
            <span style="margin-left: 12px; color: var(--text-color-3); font-size: 14px;">
              {{ paginationInfo }}
            </span>
            <!-- 🚀 v3.13.5.5: 添加Token列表卡片显示开关 -->
            <n-tooltip placement="top">
              <template #trigger>
                <n-button
                  :type="showTokenCards ? 'primary' : 'default'"
                  size="small"
                  @click="toggleTokenCards"
                  style="margin-left: 12px;"
                >
                  <template #icon>
                    <n-icon>
                      <component :is="showTokenCards ? Eye : EyeOff" />
                    </n-icon>
                  </template>
                  {{ showTokenCards ? '隐藏卡片' : '显示卡片' }}
                </n-button>
              </template>
              {{ showTokenCards ? '隐藏Token卡片以减少渲染压力' : '显示Token卡片查看详细信息' }}
            </n-tooltip>
          </div>
          <div class="header-actions">
            <n-button
              v-if="tokenStore.selectedToken"
              type="success"
              @click="goToDashboard"
            >
              <template #icon>
                <n-icon><Home /></n-icon>
              </template>
              返回控制台
            </n-button>

            <n-button
              v-if="!showImportForm"
              type="primary"
              @click="handleAddTokenClick"
            >
              <template #icon>
                <n-icon><Add /></n-icon>
              </template>
              添加Token
            </n-button>

            <n-dropdown
              :options="bulkOptions"
              @select="handleBulkAction"
            >
              <n-button>
                <template #icon>
                  <n-icon><Menu /></n-icon>
                </template>
                批量操作
              </n-button>
            </n-dropdown>
          </div>
        </div>

        <!-- 🚀 v3.13.5.5: 使用v-show控制Token卡片显示 -->
        <div v-show="showTokenCards" class="tokens-grid">
          <div
            v-for="token in paginatedTokens"
            :key="token.id"
            class="token-card"
            :class="{
              active: token.id === tokenStore.selectedTokenId,
              connected: getConnectionStatus(token.id) === 'connected'
            }"
            @click="selectToken(token)"
          >
            <div class="card-header">
              <div class="token-info">
                <h3 class="token-name">
                  {{ token.name }}
                </h3>
                <div class="token-meta">
                  <span
                    v-if="token.server"
                    class="meta-item"
                  >{{ token.server }}</span>
                </div>
              </div>

              <div class="card-actions">
                <n-dropdown
                  :options="getTokenActions(token)"
                  @select="(key) => handleTokenAction(key, token)"
                >
                  <n-button text>
                    <template #icon>
                      <n-icon><EllipsisHorizontal /></n-icon>
                    </template>
                  </n-button>
                </n-dropdown>
              </div>
            </div>

            <div class="card-body">
              <div class="token-display">
                <span class="token-label">Token:</span>
                <code class="token-value">{{ maskToken(token.token) }}</code>
              </div>

              <div class="connection-status">
                <div class="status-indicator">
                  <span
                    class="status-dot"
                    :class="getConnectionStatus(token.id)"
                  />
                  <span class="status-text">
                    {{ getConnectionStatusText(token.id) }}
                  </span>
                </div>

                <div class="connection-actions">
                  <n-button
                    v-if="token.sourceUrl"
                    size="small"
                    type="default"
                    :loading="refreshingTokens.has(token.id)"
                    @click.stop="refreshToken(token)"
                  >
                    <template #icon>
                      <n-icon><Refresh /></n-icon>
                    </template>
                    刷新
                  </n-button>
                  
                  <n-button
                    size="small"
                    :type="getConnectionStatus(token.id) === 'connected' ? 'warning' : 'primary'"
                    @click.stop="toggleConnection(token)"
                  >
                    {{ getConnectionStatus(token.id) === 'connected' ? '断开' : '连接' }}
                  </n-button>
                </div>
              </div>

              <div class="token-timestamps">
                <div class="timestamp-item">
                  <span class="timestamp-label">创建：</span>
                  <span class="timestamp-value">{{ formatTime(token.createdAt) }}</span>
                </div>
                <div class="timestamp-item">
                  <span class="timestamp-label">使用：</span>
                  <span class="timestamp-value">{{ formatTime(token.lastUsed) }}</span>
                </div>
              </div>
            </div>

            <div
              v-if="token.id === tokenStore.selectedTokenId"
              class="card-footer"
            >
              <n-button
                type="primary"
                block
                @click="goToDashboard"
              >
                开始任务管理
              </n-button>
            </div>
          </div>
        </div>
        
        <!-- 🚀 分页控件（优化大量token渲染） -->
        <div v-show="showTokenCards && pagination.enabled && totalPages > 1" class="pagination-controls">
          <n-space align="center" justify="space-between" style="width: 100%;">
            <n-button-group>
              <n-button
                :disabled="pagination.currentPage === 1"
                @click="pagination.currentPage = 1"
              >
                <template #icon>
                  <n-icon><ChevronBack /></n-icon><n-icon><ChevronBack /></n-icon>
                </template>
                首页
              </n-button>
              <n-button
                :disabled="pagination.currentPage === 1"
                @click="pagination.currentPage--"
              >
                <template #icon>
                  <n-icon><ChevronBack /></n-icon>
                </template>
                上一页
              </n-button>
            </n-button-group>
            
            <n-space align="center">
              <span style="color: var(--text-color-3);">第</span>
              <n-input-number
                v-model:value="pagination.currentPage"
                :min="1"
                :max="totalPages"
                :show-button="false"
                size="small"
                style="width: 80px;"
              />
              <span style="color: var(--text-color-3);">/ {{ totalPages }} 页</span>
              <n-select
                v-model:value="pagination.pageSize"
                :options="[
                  { label: '25条/页', value: 25 },
                  { label: '50条/页', value: 50 },
                  { label: '100条/页', value: 100 },
                  { label: '200条/页', value: 200 }
                ]"
                size="small"
                style="width: 120px;"
                @update:value="pagination.currentPage = 1"
              />
            </n-space>
            
            <n-button-group>
              <n-button
                :disabled="pagination.currentPage === totalPages"
                @click="pagination.currentPage++"
              >
                下一页
                <template #icon>
                  <n-icon><ChevronForward /></n-icon>
                </template>
              </n-button>
              <n-button
                :disabled="pagination.currentPage === totalPages"
                @click="pagination.currentPage = totalPages"
              >
                末页
                <template #icon>
                  <n-icon><ChevronForward /></n-icon><n-icon><ChevronForward /></n-icon>
                </template>
              </n-button>
            </n-button-group>
          </n-space>
        </div>
        
        <!-- 🚀 v3.13.5.5: 隐藏Token卡片时显示提示信息（极致紧凑版） -->
        <div v-if="!showTokenCards" class="tokens-summary">
          <n-alert type="info" :bordered="false" size="small">
            <span style="font-size: 13px; line-height: 1.4;">
              💡 <strong>Token列表已隐藏</strong> (当前 <strong>{{ tokenStore.gameTokens.length }}</strong> 个)，点击"显示卡片"查看
            </span>
          </n-alert>
        </div>
      </div>

      <!-- 空状态 -->
      <div
        v-if="!tokenStore.hasTokens && !showImportForm"
        class="empty-state"
      >
        <n-empty
          size="large"
          description="还没有导入任何Token"
        >
          <template #icon>
            <n-icon size="64">
              <Key />
            </n-icon>
          </template>
        </n-empty>
      </div>
    </div>

    <!-- 编辑Token模态框 -->
    <n-modal
      v-model:show="showEditModal"
      preset="card"
      title="编辑Token"
      style="width: 500px"
    >
      <n-form
        ref="editFormRef"
        :model="editForm"
        :rules="editRules"
        label-placement="left"
        label-width="80px"
      >
        <n-form-item
          label="名称"
          path="name"
        >
          <n-input v-model:value="editForm.name" />
        </n-form-item>
        <n-form-item label="服务器">
          <n-input v-model:value="editForm.server" />
        </n-form-item>
        <n-form-item label="WebSocket地址">
          <n-input v-model:value="editForm.wsUrl" />
        </n-form-item>
      </n-form>

      <template #footer>
        <div class="modal-actions">
          <n-button @click="showEditModal = false">
            取消
          </n-button>
          <n-button
            type="primary"
            @click="saveEdit"
          >
            保存
          </n-button>
        </div>
      </template>
    </n-modal>

    <!-- 批量删除Token模态框 -->
    <n-modal
      v-model:show="showBatchDeleteModal"
      preset="card"
      title="批量删除Token"
      style="width: 90%; max-width: 600px; max-height: 80vh;"
    >
      <div class="batch-delete-content">
        <n-alert type="warning" :bordered="false" style="margin-bottom: 16px;">
          <template #icon>
            <n-icon><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M12 17q.425 0 .713-.288Q13 16.425 13 16t-.287-.713Q12.425 15 12 15t-.712.287Q11 15.575 11 16t.288.712Q11.575 17 12 17Zm-1-4h2V7h-2v6Zm1 9q-2.075 0-3.9-.788-1.825-.787-3.175-2.137-1.35-1.35-2.137-3.175Q2 14.075 2 12t.788-3.9q.787-1.825 2.137-3.175 1.35-1.35 3.175-2.138Q9.925 2 12 2t3.9.787q1.825.788 3.175 2.138 1.35 1.35 2.137 3.175Q22 9.925 22 12t-.788 3.9q-.787 1.825-2.137 3.175-1.35 1.35-3.175 2.137Q14.075 22 12 22Z"/></svg></n-icon>
          </template>
          请选择要删除的Token，删除后无法恢复
        </n-alert>

        <div class="select-all-row">
          <n-checkbox
            :checked="isAllSelected"
            :indeterminate="isIndeterminate"
            @update:checked="toggleSelectAll"
          >
            <span v-if="batchDeleteSearchQuery.trim()">
              当前结果全选 ({{ selectedInFilteredCount }} / {{ filteredTokensForDelete.length }})
            </span>
            <span v-else>
              全选 ({{ selectedTokensForDelete.length }} / {{ tokenStore.gameTokens.length }})
            </span>
          </n-checkbox>
          
          <div style="flex: 1; max-width: 300px;">
            <n-input
              v-model:value="batchDeleteSearchQuery"
              placeholder="搜索或范围 (如: 590-599)"
              clearable
              size="small"
            >
              <template #prefix>
                <n-icon>
                  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
                    <path fill="currentColor" d="M15.5 14h-.79l-.28-.27A6.471 6.471 0 0 0 16 9.5A6.5 6.5 0 1 0 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5S14 7.01 14 9.5S11.99 14 9.5 14z"/>
                  </svg>
                </n-icon>
              </template>
            </n-input>
            <div v-if="batchDeleteSearchQuery.trim() && batchDeleteSearchQuery.match(/^\d+(?:服)?[-~]\d+(?:服)?$/i)" class="search-hint">
              <n-icon size="12" style="margin-right: 2px;">
                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
                  <path fill="currentColor" d="M11 17h2v-6h-2v6zm1-8q.425 0 .713-.288Q13 8.425 13 8t-.287-.713Q12.425 7 12 7t-.712.287Q11 7.575 11 8t.288.712Q11.575 9 12 9zm0 13q-2.075 0-3.9-.788-1.825-.787-3.175-2.137-1.35-1.35-2.137-3.175Q2 14.075 2 12t.788-3.9q.787-1.825 2.137-3.175 1.35-1.35 3.175-2.138Q9.925 2 12 2t3.9.787q1.825.788 3.175 2.138 1.35 1.35 2.137 3.175Q22 9.925 22 12t-.788 3.9q-.787 1.825-2.137 3.175-1.35 1.35-3.175 2.137Q14.075 22 12 22Z"/>
                </svg>
              </n-icon>
              <span>范围搜索模式</span>
            </div>
          </div>
        </div>

        <n-scrollbar style="max-height: 450px;">
          <n-checkbox-group v-model:value="selectedTokensForDelete">
            <div class="token-delete-list">
              <n-empty 
                v-if="filteredTokensForDelete.length === 0"
                description="没有找到匹配的Token"
                size="small"
                style="padding: 40px 0;"
              />
              <div
                v-for="token in filteredTokensForDelete"
                :key="token.id"
                class="token-delete-item"
                :class="{ 
                  'selected': selectedTokensForDelete.includes(token.id),
                  'not-selected': !selectedTokensForDelete.includes(token.id) && selectedTokensForDelete.length > 0
                }"
              >
                <n-checkbox :value="token.id">
                  <div class="token-delete-info">
                    <div class="token-delete-name">
                      <n-icon size="16" style="margin-right: 6px;">
                        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
                          <path fill="currentColor" d="M12 4a4 4 0 0 1 4 4a4 4 0 0 1-4 4a4 4 0 0 1-4-4a4 4 0 0 1 4-4m0 10c4.42 0 8 1.79 8 4v2H4v-2c0-2.21 3.58-4 8-4Z"/>
                        </svg>
                      </n-icon>
                      <span>{{ token.name }}</span>
                    </div>
                    <div class="token-delete-meta">
                      <span v-if="token.server" class="meta-item">
                        <n-icon size="12"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M4 1h16a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1m0 8h16a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1m0 8h16a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1M9 5h1V3H9v2m0 8h1v-2H9v2m0 8h1v-2H9v2M5 3v2h2V3H5m0 8v2h2v-2H5m0 8v2h2v-2H5Z"/></svg></n-icon>
                        {{ token.server }}
                      </span>
                      <span v-if="token.createTime" class="meta-item">
                        <n-icon size="12"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M12 20a8 8 0 0 0 8-8a8 8 0 0 0-8-8a8 8 0 0 0-8 8a8 8 0 0 0 8 8m0-18a10 10 0 0 1 10 10a10 10 0 0 1-10 10C6.47 22 2 17.5 2 12A10 10 0 0 1 12 2m.5 5v5.25l4.5 2.67l-.75 1.23L11 13V7h1.5Z"/></svg></n-icon>
                        {{ new Date(token.createTime).toLocaleDateString() }}
                      </span>
                    </div>
                  </div>
                </n-checkbox>
              </div>
            </div>
          </n-checkbox-group>
        </n-scrollbar>
      </div>

      <template #footer>
        <div class="modal-actions">
          <n-button @click="showBatchDeleteModal = false">
            取消
          </n-button>
          <n-button
            type="error"
            :disabled="selectedTokensForDelete.length === 0"
            @click="handleBatchDelete"
          >
            <template #icon>
              <n-icon><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg></n-icon>
            </template>
            删除选中 ({{ selectedTokensForDelete.length }})
          </n-button>
        </div>
      </template>
    </n-modal>

    <!-- 原始Token数据查看模态框 -->
    <n-modal
      v-model:show="showRawDataModal"
      title="Token原始数据"
      width="80%"
      max-height="80vh"
      class="raw-data-modal"
    >
      <template #footer>
        <n-button @click="showRawDataModal = false">关闭</n-button>
        <n-button type="primary" @click="copyRawData">复制数据</n-button>
      </template>
      <div class="raw-data-content">
        <div class="token-info-header">
          <h3>Token信息</h3>
          <p>名称: {{ currentRawToken?.name }}</p>
          <p>导入方式: {{ currentRawToken?.importMethod || '手动' }}</p>
          <p>服务器: {{ currentRawToken?.server || '默认' }}</p>
          <p v-if="currentRawToken?.binFileContent">
            <span class="label">Bin文件内容:</span> 已存储
          </p>
        </div>
        <div class="raw-data-display">
          <pre>{{ formattedRawData }}</pre>
        </div>
      </div>
    </n-modal>

    <!-- 文件夹批量上传模态框 -->
    <n-modal
      v-model:show="showFolderBatchUpload"
      :title="isMobileDevice ? '批量上传' : '文件夹批量上传'"
      preset="card"
      style="width: 90%; max-width: 500px"
      class="folder-batch-upload-modal"
    >
      <div class="folder-upload-instructions">
        <n-alert type="info" :show-icon="true">
          <template #header>
            {{ isMobileDevice ? '批量上传说明' : '文件夹批量上传说明' }}
          </template>
          <div class="instruction-steps">
            <p v-if="isMobileDevice">
              1. 点击下方"选择文件"按钮，选择多个.bin文件
            </p>
            <p v-else>
              1. 点击下方"选择文件夹"按钮，选择包含.bin文件的文件夹
            </p>
            <p>2. 系统将自动处理所有选中的文件</p>
            <p>3. 使用文件名作为角色名，批量导入所有文件</p>
          </div>
        </n-alert>
      </div>

      <div class="folder-upload-form">
        <n-form
          ref="folderBinFormRef"
          :model="folderBinForm"
          :rules="folderBinRules"
          label-placement="top"
        >
          <n-form-item :label="isMobileDevice ? '选择文件' : '选择文件夹'" path="folderPath">
            <input
              ref="folderInputRef"
              :type="isMobileDevice ? 'file' : 'file'"
              :webkitdirectory="!isMobileDevice"
              :multiple="isMobileDevice"
              accept=".bin"
              style="display: none"
              @change="handleFolderSelect"
            >
            <n-button
              type="primary"
              block
              @click="triggerFolderInput"
            >
              <template #icon>
                <n-icon><FolderOpen /></n-icon>
              </template>
              {{ isMobileDevice ? '选择文件' : '选择文件夹' }}
            </n-button>
            <div v-if="folderBinForm.folderName" class="selected-folder-info">
              <p>已{{ isMobileDevice ? '选择' : '选择' }}: {{ folderBinForm.folderName }}</p>
            </div>
          </n-form-item>
          
          <n-form-item v-if="folderBatchFiles.length > 0">
            <n-button
              type="primary"
              block
              :loading="isImporting"
              @click="processFolderBatchUpload"
            >
              <template #icon>
                <n-icon><CloudUpload /></n-icon>
              </template>
              批量导入 ({{ folderBatchFiles.length }})
            </n-button>
          </n-form-item>
        </n-form>
      </div>

      <div class="folder-batch-files-list" v-if="folderBatchFiles.length > 0">
        <h3>待导入文件列表 ({{ folderBatchFiles.length }})</h3>
        <div class="batch-files-container">
          <div
            v-for="(fileInfo, index) in folderBatchFiles"
            :key="index"
            class="batch-file-item"
          >
            <div class="file-info">
              <span class="file-name">{{ fileInfo.fileName }}</span>
              <span class="role-name">{{ fileInfo.roleName }}</span>
            </div>
            <n-button
              text
              type="error"
              @click="removeFileFromFolderBatchList(index)"
            >
              <template #icon>
                <n-icon><TrashBin /></n-icon>
              </template>
            </n-button>
          </div>
        </div>
      </div>

      <template #footer>
        <div class="modal-actions">
          <n-button @click="closeFolderBatchUpload">
            取消
          </n-button>
        </div>
      </template>
    </n-modal>
  </div>
</template>

<script setup>
/** 日志配置检查 */
const shouldLog = (type) => {
  try {
    const config = JSON.parse(localStorage.getItem('batchTaskLogConfig') || '{}')
    return config[type] === true
  } catch {
    return false
  }
}

import { ref, reactive, onMounted, computed, h, nextTick, watch } from 'vue'
import { useRouter } from 'vue-router'
import { useMessage, useDialog, NIcon } from 'naive-ui'
import { useTokenStore } from '../stores/tokenStore'
import { useBatchTaskStore } from '../stores/batchTaskStore'
import { useLocalTokenStore } from '../stores/localTokenManager'
import ThemeToggle from '@/components/ThemeToggle.vue'
import AppNavbar from '@/components/AppNavbar.vue'
import BatchTaskPanel from '@/components/BatchTaskPanel.vue'
import TaskProgressCard from '@/components/TaskProgressCard.vue'
import VirtualScrollList from '@/components/VirtualScrollList.vue'
import {
  Add,
  CloudUpload,
  Menu,
  EllipsisHorizontal,
  Key,
  Refresh,
  Home,
  Create,
  Copy,
  SyncCircle,
  Link,
  TrashBin,
  Eye,
  EyeOff,
  FolderOpen,
  Close,
  ChevronUp,
  ChevronDown,
  ChevronBack,
  ChevronForward,
  CheckmarkCircle,
  CloseCircle,
  Remove,
  InformationCircle
} from '@vicons/ionicons5'

const router = useRouter()
const message = useMessage()
const dialog = useDialog()
const tokenStore = useTokenStore()
const batchStore = useBatchTaskStore()
const localTokenStore = useLocalTokenStore()

// 响应式数据
const showImportForm = ref(false)
const isImporting = ref(false)
const showEditModal = ref(false)
const showBatchDeleteModal = ref(false)
const selectedTokensForDelete = ref([])
const batchDeleteSearchQuery = ref('')
const importFormRef = ref(null)
const urlFormRef = ref(null)
const binFormRef = ref(null)
const archiveFormRef = ref(null)
const editFormRef = ref(null)
const fileInputRef = ref(null)
const archiveFileInputRef = ref(null)
const importSectionRef = ref(null)
const editingToken = ref(null)
const importMethod = ref('bin') // 默认显示bin文件上传
const refreshingTokens = ref(new Set())
const showMobileBatchUpload = ref(false)
const showFolderBatchUpload = ref(false)
const isMobileDevice = ref(false)
const showAllFiles = ref(false) // 控制文件列表展开/收起

// 自动跟随最新token相关
const virtualScrollRef = ref(null)
const autoFollowLatest = ref(true) // 默认开启
const isUserScrolling = ref(false) // 标记用户是否在手动滚动
const isAutoScrolling = ref(false) // 标记是否正在自动滚动（避免误判）
let scrollTimeout = null

// 🚀 v3.13.5.5: 进度卡片显示开关（减少渲染压力）
const showProgressCards = ref(
  localStorage.getItem('showProgressCards') !== 'false' // 默认显示，除非明确设置为false
)

// 🚀 v3.13.5.5: Token列表卡片显示开关（减少渲染压力）
const showTokenCards = ref(
  localStorage.getItem('showTokenCards') !== 'false' // 默认显示，除非明确设置为false
)

// 🚀 分页配置（优化大量token的渲染性能）
const pagination = reactive({
  currentPage: 1,
  pageSize: 50, // 每页显示50个token
  enabled: true  // 分页开关
})

// 分页后的tokens（减少DOM元素数量，提升性能）
const paginatedTokens = computed(() => {
  if (!pagination.enabled) {
    return tokenStore.gameTokens
  }
  
  const start = (pagination.currentPage - 1) * pagination.pageSize
  const end = start + pagination.pageSize
  return tokenStore.gameTokens.slice(start, end)
})

// 总页数
const totalPages = computed(() => {
  if (!pagination.enabled) {
    return 1
  }
  return Math.ceil(tokenStore.gameTokens.length / pagination.pageSize)
})

// 分页信息文本
const paginationInfo = computed(() => {
  if (!pagination.enabled) {
    return `共 ${tokenStore.gameTokens.length} 个Token`
  }
  
  const start = (pagination.currentPage - 1) * pagination.pageSize + 1
  const end = Math.min(pagination.currentPage * pagination.pageSize, tokenStore.gameTokens.length)
  return `显示 ${start}-${end} / 共 ${tokenStore.gameTokens.length} 个Token`
})

// 🔥 v3.14.1: 文件上传进度跟踪
const uploadProgress = reactive({
  show: false,
  current: 0,
  total: 0,
  currentFile: '',
  type: '', // 'bin', 'archive', 'mobile', 'folder'
  successCount: 0,
  failedCount: 0
})

// 🔥 v3.14.2: 并发上传配置
const uploadConfig = {
  concurrentLimit: 3  // 同时上传3个文件（平衡性能和稳定性）
}

// 获取上传类型文本
const getUploadTypeText = () => {
  const typeMap = {
    'bin': '📁 bin文件上传',
    'archive': '📦 压缩包上传',
    'mobile': '📤 手机端批量上传',
    'folder': '📤 文件夹批量上传'
  }
  return typeMap[uploadProgress.type] || '文件上传中...'
}

// 重置上传进度
const resetUploadProgress = () => {
  uploadProgress.show = false
  uploadProgress.current = 0
  uploadProgress.total = 0
  uploadProgress.currentFile = ''
  uploadProgress.type = ''
  uploadProgress.successCount = 0
  uploadProgress.failedCount = 0
}

// 更新上传进度
const updateUploadProgress = (current, total, fileName, success = false, failed = false) => {
  uploadProgress.current = current
  uploadProgress.total = total
  uploadProgress.currentFile = fileName
  if (success) uploadProgress.successCount++
  if (failed) uploadProgress.failedCount++
}

// 🔥 v3.14.2: 并发处理辅助函数
/**
 * 并发批量处理任务
 * @param {Array} items - 要处理的项目列表
 * @param {Function} processor - 处理单个项目的异步函数
 * @param {Number} concurrentLimit - 并发数量限制
 * @returns {Promise<Array>} 所有结果的数组
 */
const processConcurrently = async (items, processor, concurrentLimit = 3) => {
  const results = []
  
  // 分批处理
  for (let i = 0; i < items.length; i += concurrentLimit) {
    const batch = items.slice(i, i + concurrentLimit)
    
    // 并发处理当前批次
    const batchResults = await Promise.all(
      batch.map((item, index) => processor(item, i + index))
    )
    
    results.push(...batchResults)
  }
  
  return results
}

// 🔥 v3.14.2: 处理压缩包中的单个bin文件（用于并发）
/**
 * 处理从压缩包中提取的bin文件
 * @param {Object} fileInfo - 包含文件元数据的对象 {name, entry, type}
 * @param {Number} index - 文件索引
 * @param {Number} totalFiles - 总文件数
 * @param {String} namePrefix - 名称前缀
 * @returns {Promise<Object>} 处理结果
 */
const processSingleArchiveFile = async (fileInfo, index, totalFiles, namePrefix = '') => {
  const fileName = fileInfo.name
  const nameWithoutExt = fileName.substring(0, fileName.lastIndexOf('.'))
  const roleName = `${namePrefix || nameWithoutExt}_${index + 1}`
  
  try {
    // 更新UI进度
    updateUploadProgress(index + 1, totalFiles, roleName)
    console.log(`📦 [压缩包导入] 正在处理 ${index + 1}/${totalFiles}: ${roleName}`)
    
    // 读取bin文件内容（从ZIP中提取）
    let arrayBuffer
    if (fileInfo.type === 'zip') {
      arrayBuffer = await fileInfo.entry.async('arraybuffer')
    } else {
      throw new Error(`不支持的压缩格式类型: ${fileInfo.type}`)
    }
    
    if (shouldLog('websocket')) console.log(`[压缩包导入] 成功读取文件内容，字节长度: ${arrayBuffer.byteLength}`)
    
    // 上传bin文件到服务器并获取响应
    if (shouldLog('websocket')) console.log(`[压缩包导入] 正在上传文件到服务器...`)
    const response = await fetch('https://xxz-xyzw.hortorgames.com/login/authuser?_seq=1', {
      method: 'POST',
      headers: { 'Content-Type': 'application/octet-stream' },
      body: arrayBuffer
    })
    
    if (!response.ok) {
      throw new Error(`上传失败: ${response.status} ${response.statusText}`)
    }
    
    // 获取服务器响应的二进制数据
    const responseArrayBuffer = await response.arrayBuffer()
    if (shouldLog('websocket')) console.log(`[压缩包导入] 服务器响应字节长度: ${responseArrayBuffer.byteLength}`)
    
    // 从服务器响应中提取Token
    const roleToken = extractRoleToken(responseArrayBuffer)
    
    if (!roleToken) {
      throw new Error('无法从服务器响应中提取Token，可能是文件格式不正确')
    }
    
    if (shouldLog('websocket')) console.log(`[压缩包导入] 成功提取roleToken: ${roleToken.substring(0, 20)}...`)
    
    // 生成完整的Token数据和WSS连接参数
    const sessId = Date.now() * 1000 + Math.floor(Math.random() * 1000)
    const connId = Date.now()
    const tokenData = {
      roleToken,
      sessId,
      connId,
      isRestore: 0,
    }
    
    if (shouldLog('websocket')) console.log(`[压缩包导入] 生成的Token数据:`, tokenData)
    
    // 创建完整的Token格式
    const tokenStr = JSON.stringify(tokenData)
    const base64Token = btoa(unescape(encodeURIComponent(tokenStr)))
    if (shouldLog('websocket')) console.log(`[压缩包导入] Base64编码的Token: ${base64Token.substring(0, 20)}...`)
    
    // 生成WSS链接
    const p = encodeURIComponent(tokenStr)
    const e = 'x'
    const lang = 'chinese'
    const wssUrl = `wss://xxz-xyzw.hortorgames.com/agent?p=${p}&e=${e}&lang=${lang}`
    if (shouldLog('websocket')) console.log(`[压缩包导入] 生成的WSS链接: ${wssUrl.substring(0, 100)}...`)
    
    console.log(`✅ [压缩包导入] 成功 ${index + 1}/${totalFiles}: ${roleName}`)
    
    // 返回成功结果
    return {
      success: true,
      tokenData: {
        name: roleName,
        token: base64Token,
        wsUrl: wssUrl,
        rawToken: tokenData,
        arrayBuffer: arrayBuffer,
        fileName: fileName
      }
    }
    
  } catch (error) {
    console.error(`❌ [压缩包导入] 失败 ${index + 1}/${totalFiles}: ${roleName}`, error)
    if (shouldLog('websocket')) console.error(`[压缩包导入] 文件处理失败: ${fileName}`, error)
    
    // 返回失败结果
    return {
      success: false,
      error: error,
      fileName: fileName,
      roleName: roleName
    }
  }
}

// 🔥 v3.14.2: 处理单个bin文件上传（用于并发 - 通用版本）
/**
 * 通用的bin文件上传处理函数，支持不同的输入格式
 * @param {File|Object} fileInput - File对象或包含file的对象 {file, fileName, roleName}
 * @param {Number} index - 文件索引
 * @param {Number} totalFiles - 总文件数
 * @param {String} namePrefix - 名称前缀（可选）
 * @param {String} logPrefix - 日志前缀（默认'Bin导入'）
 * @returns {Promise<Object>} 处理结果
 */
const processSingleBinFile = async (fileInput, index, totalFiles, namePrefix = '', logPrefix = 'Bin导入') => {
  // 兼容不同的输入格式
  const file = fileInput.file || fileInput
  const fileName = fileInput.fileName || file.name
  const nameWithoutExt = fileName.substring(0, fileName.lastIndexOf('.'))
  const roleName = fileInput.roleName || 
                   (totalFiles === 1 ? (namePrefix || nameWithoutExt) : `${namePrefix || nameWithoutExt}_${index + 1}`)
  
  try {
    // 更新UI进度
    updateUploadProgress(index + 1, totalFiles, roleName)
    console.log(`📁 [${logPrefix}] 正在处理 ${index + 1}/${totalFiles}: ${roleName}`)
    
    // 读取bin文件内容
    const arrayBuffer = await readBinFile(file)
    if (shouldLog('websocket')) console.log(`[${logPrefix}] 成功读取文件内容，字节长度: ${arrayBuffer.byteLength}`)
    
    // 上传bin文件到服务器并获取响应
    if (shouldLog('websocket')) console.log(`[${logPrefix}] 正在上传文件到服务器...`)
    const response = await fetch('https://xxz-xyzw.hortorgames.com/login/authuser?_seq=1', {
      method: 'POST',
      headers: { 'Content-Type': 'application/octet-stream' },
      body: arrayBuffer
    })
    
    if (!response.ok) {
      throw new Error(`上传失败: ${response.status} ${response.statusText}`)
    }
    
    // 获取服务器响应的二进制数据
    const responseArrayBuffer = await response.arrayBuffer()
    if (shouldLog('websocket')) console.log(`[${logPrefix}] 服务器响应字节长度: ${responseArrayBuffer.byteLength}`)
    
    // 从服务器响应中提取Token
    const roleToken = extractRoleToken(responseArrayBuffer)
    
    if (!roleToken) {
      throw new Error('无法从服务器响应中提取Token，可能是文件格式不正确')
    }
    
    if (shouldLog('websocket')) console.log(`[${logPrefix}] 成功提取roleToken: ${roleToken.substring(0, 20)}...`)
    
    // 生成完整的Token数据和WSS连接参数
    const sessId = Date.now() * 1000 + Math.floor(Math.random() * 1000)
    const connId = Date.now()
    const tokenData = {
      roleToken,
      sessId,
      connId,
      isRestore: 0,
    }
    
    if (shouldLog('websocket')) console.log(`[${logPrefix}] 生成的Token数据:`, tokenData)
    
    // 创建完整的Token格式
    const tokenStr = JSON.stringify(tokenData)
    const base64Token = btoa(unescape(encodeURIComponent(tokenStr)))
    if (shouldLog('websocket')) console.log(`[${logPrefix}] Base64编码的Token: ${base64Token.substring(0, 20)}...`)
    
    // 生成WSS链接
    const p = encodeURIComponent(tokenStr)
    const e = 'x'
    const lang = 'chinese'
    const wssUrl = `wss://xxz-xyzw.hortorgames.com/agent?p=${p}&e=${e}&lang=${lang}`
    if (shouldLog('websocket')) console.log(`[${logPrefix}] 生成的WSS链接: ${wssUrl.substring(0, 100)}...`)
    
    console.log(`✅ [${logPrefix}] 成功 ${index + 1}/${totalFiles}: ${roleName}`)
    
    // 返回成功结果
    return {
      success: true,
      tokenData: {
        name: roleName,
        token: base64Token,
        wsUrl: wssUrl,
        rawToken: tokenData,
        arrayBuffer: arrayBuffer,
        fileName: fileName
      }
    }
    
  } catch (error) {
    console.error(`❌ [${logPrefix}] 失败 ${index + 1}/${totalFiles}: ${roleName}`, error)
    if (shouldLog('websocket')) console.error(`[${logPrefix}] 文件处理失败: ${fileName}`, error)
    
    // 返回失败结果
    return {
      success: false,
      error: error,
      fileName: fileName,
      roleName: roleName
    }
  }
}

// 导入表单
const importForm = reactive({
  name: '',
  base64Token: '',
  server: '',
  wsUrl: ''
})

// URL表单
const urlForm = reactive({
  name: '',
  url: '',
  server: '',
  wsUrl: ''
})

// Bin文件表单
const binForm = reactive({
  name: '',
  server: '',
  wsUrl: '',
  files: []
})

// 压缩包表单
const archiveForm = reactive({
  name: '',
  server: '',
  wsUrl: '',
  archiveFile: null
})

// 本地存储的bin文件
const storedBinFiles = ref([])

// 手机端批量上传相关
const mobileBinFormRef = ref(null)
const mobileFileInputRef = ref(null)
const mobileBinForm = reactive({
  currentFiles: [],
  currentFileName: ''
})
const mobileBatchFiles = ref([])

// 文件夹批量上传相关
const folderBinFormRef = ref(null)
const folderInputRef = ref(null)
const folderBinForm = reactive({
  folderPath: '',
  folderName: ''
})
const folderBatchFiles = ref([])

// 手机端批量上传表单验证规则
const mobileBinRules = {
  currentFiles: [
    { required: true, message: '请选择bin文件', trigger: 'change' }
  ]
}

// 文件夹批量上传表单验证规则
const folderBinRules = {
  folderPath: [
    { required: true, message: '请选择文件夹', trigger: 'change' }
  ]
}

// 触发手机端文件选择
const triggerMobileFileInput = () => {
  mobileFileInputRef.value.click()
}

// 触发文件夹选择
const triggerFolderInput = () => {
  folderInputRef.value.click()
}

// 处理手机端文件选择
const handleMobileFileSelect = (event) => {
  const files = event.target.files
  if (files.length === 0) {
    return
  }
  
  // 清空当前批量文件列表
  mobileBatchFiles.value = []
  
  // 处理每个选中的文件
  for (let i = 0; i < files.length; i++) {
    const file = files[i]
    if (file && file.name.endsWith('.bin')) {
      // 使用文件名作为角色名（去除扩展名）
      const fileName = file.name
      const roleName = fileName.substring(0, fileName.lastIndexOf('.'))
      
      // 添加到批量文件列表
      mobileBatchFiles.value.push({
        file: file,
        fileName: fileName,
        roleName: roleName
      })
    }
  }
  
  // 更新当前文件名显示
  if (mobileBatchFiles.value.length > 0) {
    if (mobileBatchFiles.value.length === 1) {
      mobileBinForm.currentFileName = mobileBatchFiles.value[0].fileName
    } else {
      mobileBinForm.currentFileName = `已选择 ${mobileBatchFiles.value.length} 个文件`
    }
    message.success(`已选择 ${mobileBatchFiles.value.length} 个.bin文件`)
  } else {
    message.error('请选择有效的.bin文件')
    mobileBinForm.currentFileName = ''
  }
  
  // 清空input，允许重复选择相同文件
  event.target.value = ''
}

// 处理文件夹选择
const handleFolderSelect = (event) => {
  const files = event.target.files
  if (files.length === 0) {
    return
  }
  
  // 清空当前批量文件列表
  folderBatchFiles.value = []
  
  // 如果是移动设备，直接处理选中的文件
  if (isMobileDevice.value) {
    folderBinForm.folderName = `已选择 ${files.length} 个文件`
    
    // 处理每个选中的文件
    for (let i = 0; i < files.length; i++) {
      const file = files[i]
      if (file && file.name.endsWith('.bin')) {
        // 使用文件名作为角色名（去除扩展名）
        const fileName = file.name
        const roleName = fileName.substring(0, fileName.lastIndexOf('.'))
        
        // 添加到批量文件列表
        folderBatchFiles.value.push({
          file: file,
          fileName: fileName,
          roleName: roleName
        })
      }
    }
    
    // 显示选择结果
    if (folderBatchFiles.value.length > 0) {
      message.success(`已选择 ${folderBatchFiles.value.length} 个.bin文件`)
    } else {
      message.warning('未找到有效的.bin文件')
    }
  } else {
    // 桌面设备，使用文件夹选择逻辑
    // 获取文件夹名称（使用第一个文件的路径提取）
    const firstFilePath = files[0].webkitRelativePath
    const folderName = firstFilePath.split('/')[0]
    folderBinForm.folderName = folderName
    
    // 处理文件夹中的每个文件
    for (let i = 0; i < files.length; i++) {
      const file = files[i]
      if (file && file.name.endsWith('.bin')) {
        // 使用文件名作为角色名（去除扩展名）
        const fileName = file.name
        const roleName = fileName.substring(0, fileName.lastIndexOf('.'))
        
        // 添加到批量文件列表
        folderBatchFiles.value.push({
          file: file,
          fileName: fileName,
          roleName: roleName
        })
      }
    }
    
    // 显示选择结果
    if (folderBatchFiles.value.length > 0) {
      message.success(`在文件夹"${folderName}"中找到 ${folderBatchFiles.value.length} 个.bin文件`)
    } else {
      message.warning(`在文件夹"${folderName}"中未找到.bin文件`)
    }
  }
  
  // 清空input，允许重复选择相同文件/文件夹
  event.target.value = ''
}

// 添加文件到批量列表 - 已移除，现在直接在handleMobileFileSelect中处理
// const addFileToBatchList = () => {
//   if (!mobileBinForm.currentFile) {
//     message.error('请先选择文件')
//     return
//   }

//   const fileName = mobileBinForm.currentFile.name
//   const nameWithoutExt = fileName.substring(0, fileName.lastIndexOf('.'))
//   const baseName = mobileBinForm.baseName || nameWithoutExt
//   const roleName = `${baseName}_${mobileBatchFiles.value.length + 1}`

//   mobileBatchFiles.value.push({
//     file: mobileBinForm.currentFile,
//     fileName: fileName,
//     roleName: roleName
//   })

//   // 重置当前文件
//   mobileBinForm.currentFile = null
//   mobileBinForm.currentFileName = ''

//   message.success(`已添加文件: ${fileName}，角色名: ${roleName}`)
// }

// 从批量列表中移除文件
const removeFileFromBatchList = (index) => {
  const removedFile = mobileBatchFiles.value[index]
  mobileBatchFiles.value.splice(index, 1)
  
  // 更新当前文件名显示
  if (mobileBatchFiles.value.length > 0) {
    if (mobileBatchFiles.value.length === 1) {
      mobileBinForm.currentFileName = mobileBatchFiles.value[0].fileName
    } else {
      mobileBinForm.currentFileName = `已选择 ${mobileBatchFiles.value.length} 个文件`
    }
  } else {
    mobileBinForm.currentFileName = ''
  }
  
  message.success(`已移除文件: ${removedFile.fileName}`)
}

// 从文件夹批量列表中移除文件
const removeFileFromFolderBatchList = (index) => {
  const removedFile = folderBatchFiles.value[index]
  folderBatchFiles.value.splice(index, 1)
  
  message.success(`已移除文件: ${removedFile.fileName}`)
}

// 关闭手机端批量上传模态框
const closeMobileBatchUpload = () => {
  showMobileBatchUpload.value = false
  // 重置表单和列表
  mobileBinForm.currentFiles = []
  mobileBinForm.currentFileName = ''
  mobileBatchFiles.value = []
}

// 关闭文件夹批量上传模态框
const closeFolderBatchUpload = () => {
  showFolderBatchUpload.value = false
  // 重置表单和列表
  folderBinForm.folderPath = ''
  folderBinForm.folderName = ''
  folderBatchFiles.value = []
}

// 处理手机端批量上传（🔥 v3.14.2: 支持并发上传）
const processMobileBatchUpload = async () => {
  if (mobileBatchFiles.value.length === 0) {
    message.error('请先选择文件')
    return
  }

  try {
    isImporting.value = true
    const files = mobileBatchFiles.value
    const totalFiles = files.length
    
    // 🔥 显示上传进度
    uploadProgress.show = true
    uploadProgress.type = 'mobile'
    uploadProgress.total = totalFiles
    
    console.log(`🚀 [批量上传] 开始并发处理 ${totalFiles} 个文件（并发数：${uploadConfig.concurrentLimit}）`)
    
    // 🔥 v3.14.2: 使用并发处理
    const results = await processConcurrently(
      files,
      (fileInfo, index) => processSingleBinFile(fileInfo, index, totalFiles, '', '批量上传'),
      uploadConfig.concurrentLimit
    )
    
    // 处理结果并保存Token
    let successCount = 0
    let failedCount = 0
    const binFilesToSave = [] // 收集需要保存的bin文件
    
    for (const result of results) {
      if (result.success) {
        const tokenData = result.tokenData
        
        // 准备bin文件本地存储数据（延后批量保存）
        try {
          const base64Content = arrayBufferToBase64(tokenData.arrayBuffer)
          const binFileId = `bin_${Date.now()}_${Math.floor(Math.random() * 1000)}`
          
          binFilesToSave.push({
            id: binFileId,
            name: tokenData.fileName,
            roleName: tokenData.name,
            content: base64Content,
            createdAt: new Date().toISOString(),
            lastUsed: new Date().toISOString()
          })
          
          // 创建Token对象
          const tokenInfo = {
            name: tokenData.name,
            token: tokenData.token,
            wsUrl: tokenData.wsUrl,
            server: '',
            rawData: tokenData.rawToken,
            binFileContent: tokenData.arrayBuffer,
            binFileId: binFileId,
            importMethod: 'bin',
            createdAt: Date.now(),
            lastUsed: null,
            lastRefreshed: Date.now()
          }
          
          // 🔥 立即保存Token（使用importBase64Token以正确保存binFileContent）
          const importResult = tokenStore.importBase64Token(
            tokenInfo.name,
            tokenInfo.token,
            {
              server: tokenInfo.server,
              wsUrl: tokenInfo.wsUrl,
              importMethod: 'bin',
              binFileContent: tokenInfo.binFileContent,
              binFileId: tokenInfo.binFileId,
              rawData: tokenInfo.rawData,
              lastRefreshed: tokenInfo.lastRefreshed
            }
          )
          
          if (!importResult.success) {
            throw new Error(importResult.error)
          }
          
          successCount++
          uploadProgress.successCount = successCount
          
        } catch (saveError) {
          console.error(`[批量上传] 保存Token失败: ${tokenData.name}`, saveError)
          failedCount++
          uploadProgress.failedCount = failedCount
        }
      } else {
        failedCount++
        uploadProgress.failedCount = failedCount
      }
    }
    
    // 🔥 v3.14.2.3: 分段存储 - 每段30个文件（3MB），彻底解决内存峰值
    if (binFilesToSave.length > 0) {
      setTimeout(async () => {
        try {
          await saveBinFilesBatch(binFilesToSave, 30, '批量上传')
        } catch (storageError) {
          console.error('[批量上传] 批量保存bin文件失败:', storageError)
        }
      }, 500)
    }
    
    // 🔥 隐藏上传进度（延迟2秒，让用户看到最终结果）
    setTimeout(() => {
      resetUploadProgress()
    }, 2000)

    // 🔥 显示最终结果
    if (successCount > 0) {
      message.success(`成功导入 ${successCount} 个Token${failedCount > 0 ? `，${failedCount} 个失败` : ''}`)
      
      // 关闭模态框并重置表单
      closeMobileBatchUpload()
      
      // 如果是第一次导入Token，自动跳转到仪表盘
      if (tokenStore.gameTokens.length === successCount) {
        router.push('/dashboard')
      }
    } else {
      message.error(`所有文件导入失败（共 ${totalFiles} 个）`)
    }
  } catch (error) {
    console.error('[批量上传] 错误:', error)
    message.error(error.message || '批量导入失败')
    resetUploadProgress()  // 出错时也要隐藏进度
  } finally {
    isImporting.value = false
  }
}

// 处理文件夹批量上传（🔥 v3.14.2: 支持并发上传）
const processFolderBatchUpload = async () => {
  if (folderBatchFiles.value.length === 0) {
    message.error('请先选择文件夹')
    return
  }

  try {
    isImporting.value = true
    const files = folderBatchFiles.value
    const totalFiles = files.length
    
    // 🔥 显示上传进度
    uploadProgress.show = true
    uploadProgress.type = 'folder'
    uploadProgress.total = totalFiles
    
    console.log(`🚀 [文件夹上传] 开始并发处理 ${totalFiles} 个文件（并发数：${uploadConfig.concurrentLimit}）`)
    
    // 🔥 v3.14.2: 使用并发处理
    const results = await processConcurrently(
      files,
      (fileInfo, index) => processSingleBinFile(fileInfo, index, totalFiles, '', '文件夹上传'),
      uploadConfig.concurrentLimit
    )
    
    // 处理结果并保存Token
    let successCount = 0
    let failedCount = 0
    const binFilesToSave = [] // 收集需要保存的bin文件
    
    for (const result of results) {
      if (result.success) {
        const tokenData = result.tokenData
        
        // 准备bin文件本地存储数据（延后批量保存）
        try {
          const base64Content = arrayBufferToBase64(tokenData.arrayBuffer)
          const binFileId = `bin_${Date.now()}_${Math.floor(Math.random() * 1000)}`
          
          binFilesToSave.push({
            id: binFileId,
            name: tokenData.fileName,
            roleName: tokenData.name,
            content: base64Content,
            createdAt: new Date().toISOString(),
            lastUsed: new Date().toISOString()
          })
          
          // 创建Token对象
          const tokenInfo = {
            name: tokenData.name,
            token: tokenData.token,
            wsUrl: tokenData.wsUrl,
            server: '',
            rawData: tokenData.rawToken,
            binFileContent: tokenData.arrayBuffer,
            binFileId: binFileId,
            importMethod: 'bin',
            createdAt: Date.now(),
            lastUsed: null,
            lastRefreshed: Date.now()
          }
          
          // 🔥 立即保存Token（使用importBase64Token以正确保存binFileContent）
          const importResult = tokenStore.importBase64Token(
            tokenInfo.name,
            tokenInfo.token,
            {
              server: tokenInfo.server,
              wsUrl: tokenInfo.wsUrl,
              importMethod: 'bin',
              binFileContent: tokenInfo.binFileContent,
              binFileId: tokenInfo.binFileId,
              rawData: tokenInfo.rawData,
              lastRefreshed: tokenInfo.lastRefreshed
            }
          )
          
          if (!importResult.success) {
            throw new Error(importResult.error)
          }
          
          successCount++
          uploadProgress.successCount = successCount
          
        } catch (saveError) {
          console.error(`[文件夹上传] 保存Token失败: ${tokenData.name}`, saveError)
          failedCount++
          uploadProgress.failedCount = failedCount
        }
      } else {
        failedCount++
        uploadProgress.failedCount = failedCount
      }
    }
    
    // 🔥 v3.14.2.3: 分段存储 - 每段30个文件（3MB），彻底解决内存峰值
    if (binFilesToSave.length > 0) {
      setTimeout(async () => {
        try {
          await saveBinFilesBatch(binFilesToSave, 30, '文件夹上传')
        } catch (storageError) {
          console.error('[文件夹上传] 批量保存bin文件失败:', storageError)
        }
      }, 500)
    }
    
    // 🔥 隐藏上传进度（延迟2秒，让用户看到最终结果）
    setTimeout(() => {
      resetUploadProgress()
    }, 2000)

    // 🔥 显示最终结果
    if (successCount > 0) {
      message.success(`成功导入 ${successCount} 个Token${failedCount > 0 ? `，${failedCount} 个失败` : ''}`)
      
      // 关闭模态框并重置表单
      closeFolderBatchUpload()
      
      // 如果是第一次导入Token，自动跳转到仪表盘
      if (tokenStore.gameTokens.length === successCount) {
        router.push('/dashboard')
      }
    } else {
      message.error(`所有文件导入失败（共 ${totalFiles} 个）`)
    }
  } catch (error) {
    console.error('[文件夹上传] 错误:', error)
    message.error(error.message || '批量导入失败')
    resetUploadProgress()  // 出错时也要隐藏进度
  } finally {
    isImporting.value = false
  }
}

// 加载本地存储的bin文件（兼容分段存储方式）
const loadStoredBinFiles = () => {
  try {
    // 优先尝试加载分段存储的文件（新方式）
    const segmentedFiles = getAllStoredBinFilesSegmented()
    if (segmentedFiles && segmentedFiles.length > 0) {
      storedBinFiles.value = segmentedFiles
      return
    }
    
    // 如果没有分段存储的文件，尝试加载旧方式存储的文件（向后兼容）
    const storedFiles = localStorage.getItem('storedBinFiles')
    if (storedFiles) {
      storedBinFiles.value = JSON.parse(storedFiles)
    } else {
      storedBinFiles.value = []
    }
  } catch (error) {
    if (shouldLog('websocket')) console.error('加载本地存储的bin文件失败:', error)
    storedBinFiles.value = []
  }
}

// 保存本地存储的bin文件
const saveStoredBinFiles = () => {
  try {
    localStorage.setItem('storedBinFiles', JSON.stringify(storedBinFiles.value))
  } catch (error) {
    if (shouldLog('websocket')) console.error('保存本地存储的bin文件失败:', error)
    message.error('保存bin文件失败')
  }
}

// 🆕 v3.15.0: IndexedDB存储方案 - 彻底解决localStorage配额限制
/**
 * 使用IndexedDB批量保存bin文件，突破localStorage 5-10MB限制
 * 
 * 优势：
 * - 突破localStorage限制，支持几百MB甚至GB的存储
 * - 异步操作不阻塞UI
 * - 性能更好，没有序列化内存峰值问题
 * - 支持915+个bin文件
 * 
 * 向后兼容：
 * - 少量数据（≤30个）同时保存到localStorage作为备份
 * - tokenStore会优先从IndexedDB读取
 * 
 * @param {Array} binFilesToSave - 需要保存的bin文件数组
 * @param {number} batchSize - 批次大小（仅用于显示进度）
 * @param {string} logPrefix - 日志前缀
 */
const saveBinFilesBatch = async (binFilesToSave, batchSize = 30, logPrefix = '批量保存') => {
  if (!binFilesToSave || binFilesToSave.length === 0) {
    return
  }
  
  const totalFiles = binFilesToSave.length
  
  console.log(`🚀 [${logPrefix}] IndexedDB存储方案：${totalFiles}个bin文件`)
  console.log(`💡 [${logPrefix}] 使用IndexedDB，突破localStorage 5-10MB限制`)
  
  try {
    // 🆕 v3.15.0: 动态导入IndexedDB工具
    const { default: indexedDBStorage } = await import('../utils/indexedDBStorage.js')
    
    // 主存储：IndexedDB
    console.log(`📦 [${logPrefix}] 保存到IndexedDB...`)
    await indexedDBStorage.saveBinFiles(binFilesToSave)
    console.log(`✅ [${logPrefix}] IndexedDB保存成功！`)
    
    // 🔧 向后兼容：少量数据同时保存到localStorage
    if (totalFiles <= 30) {
      console.log(`💾 [${logPrefix}] 数据量较小，同时保存到localStorage作为备份...`)
      try {
        const index = {}
        const segmentData = {}
        
        binFilesToSave.forEach(binFile => {
          segmentData[binFile.id] = binFile
          index[binFile.id] = 0
        })
        
        localStorage.setItem('storedBinFiles_0', JSON.stringify(segmentData))
        localStorage.setItem('storedBinFiles_index', JSON.stringify(index))
        console.log(`✅ [${logPrefix}] localStorage备份完成`)
      } catch (error) {
        console.warn(`⚠️ [${logPrefix}] localStorage备份失败（不影响IndexedDB）:`, error.message)
      }
    } else {
      console.log(`💡 [${logPrefix}] 数据量较大（${totalFiles}个），仅使用IndexedDB存储`)
    }
    
    console.log(`🎉 [${logPrefix}] 保存完成！`)
    
  } catch (error) {
    console.error(`❌ [${logPrefix}] IndexedDB保存失败:`, error)
    
    // 🔧 回退：如果IndexedDB失败且数据量小，尝试localStorage
    if (totalFiles <= 30) {
      console.warn(`⚠️ [${logPrefix}] IndexedDB失败，尝试使用localStorage回退...`)
      try {
        const index = {}
        const segmentData = {}
        
        binFilesToSave.forEach(binFile => {
          segmentData[binFile.id] = binFile
          index[binFile.id] = 0
        })
        
        localStorage.setItem('storedBinFiles_0', JSON.stringify(segmentData))
        localStorage.setItem('storedBinFiles_index', JSON.stringify(index))
        console.log(`✅ [${logPrefix}] localStorage回退成功`)
      } catch (fallbackError) {
        console.error(`❌ [${logPrefix}] localStorage回退也失败:`, fallbackError)
        throw new Error(`存储失败：IndexedDB和localStorage都无法保存数据`)
      }
    } else {
      // 大量数据无法使用localStorage，直接抛出错误
      throw new Error(`无法保存大量bin文件（${totalFiles}个）：IndexedDB失败且数据量超过localStorage限制`)
    }
  }
}

// 🆕 v3.14.2.3: 读取分段存储的bin文件
const loadStoredBinFileSegmented = (fileId) => {
  try {
    // 读取索引，找到文件在哪个段
    const index = JSON.parse(localStorage.getItem('storedBinFiles_index') || '{}')
    const segmentNum = index[fileId]
    
    if (segmentNum === undefined) {
      return null // 文件不存在
    }
    
    // 读取该段数据
    const segmentKey = `storedBinFiles_${segmentNum}`
    const segmentData = JSON.parse(localStorage.getItem(segmentKey) || '{}')
    
    return segmentData[fileId] || null
  } catch (error) {
    console.error('读取分段bin文件失败:', error)
    return null
  }
}

// 🆕 v3.14.2.3: 获取所有分段存储的bin文件列表
const getAllStoredBinFilesSegmented = () => {
  try {
    const index = JSON.parse(localStorage.getItem('storedBinFiles_index') || '{}')
    const fileIds = Object.keys(index)
    
    return fileIds.map(fileId => {
      const segmentNum = index[fileId]
      const segmentKey = `storedBinFiles_${segmentNum}`
      const segmentData = JSON.parse(localStorage.getItem(segmentKey) || '{}')
      return segmentData[fileId]
    }).filter(file => file !== undefined && file !== null)
  } catch (error) {
    console.error('读取所有分段bin文件失败:', error)
    return []
  }
}

// 🆕 v3.15.0: 删除bin文件（IndexedDB + localStorage）
const deleteSegmentedBinFile = async (fileId) => {
  try {
    // 🆕 v3.15.0: 优先从IndexedDB删除
    try {
      const { default: indexedDBStorage } = await import('../utils/indexedDBStorage.js')
      await indexedDBStorage.deleteBinFile(fileId)
      console.log('✅ 成功从IndexedDB删除bin文件:', fileId)
    } catch (e) {
      console.warn('⚠️ IndexedDB删除失败:', e.message)
    }
    
    // 同时从localStorage分段存储删除（兼容旧数据）
    const index = JSON.parse(localStorage.getItem('storedBinFiles_index') || '{}')
    const segmentNum = index[fileId]
    
    if (segmentNum === undefined) {
      console.warn('文件不在localStorage分段存储中:', fileId)
      return false
    }
    
    // 从该段数据中删除文件
    const segmentKey = `storedBinFiles_${segmentNum}`
    const segmentData = JSON.parse(localStorage.getItem(segmentKey) || '{}')
    delete segmentData[fileId]
    
    // 保存更新后的段数据
    localStorage.setItem(segmentKey, JSON.stringify(segmentData))
    
    // 从索引中删除文件记录
    delete index[fileId]
    localStorage.setItem('storedBinFiles_index', JSON.stringify(index))
    
    console.log('✅ 成功从localStorage分段存储删除bin文件:', fileId)
    return true
  } catch (error) {
    console.error('❌ 删除bin文件失败:', error)
    return false
  }
}

// 使用本地存储的bin文件
const useStoredBinFile = (binFile) => {
  try {
    // 将bin文件内容从Base64转换为ArrayBuffer
    const binContent = base64ToArrayBuffer(binFile.content)
    
    // 创建文件对象
    const file = new File([binContent], binFile.fileName, { type: 'application/octet-stream' })
    
    // 设置表单名称
    binForm.name = binFile.roleName
    
    // 清空现有文件列表并添加新文件
    binForm.files = [file]
    
    message.success(`已选择本地存储的bin文件: ${binFile.fileName}`)
  } catch (error) {
    if (shouldLog('websocket')) console.error('使用本地存储的bin文件失败:', error)
    message.error('使用bin文件失败')
  }
}

// 🆕 v3.15.0: 删除本地存储的bin文件（IndexedDB + localStorage）
const deleteStoredBinFile = (binFileId) => {
  dialog.warning({
    title: '确认删除',
    content: '确定要删除这个bin文件吗？此操作不可撤销。',
    positiveText: '确定',
    negativeText: '取消',
    onPositiveClick: async () => {
      try {
        // 🆕 v3.15.0: 从IndexedDB和localStorage分段存储中删除
        const deletedFromSegmented = await deleteSegmentedBinFile(binFileId)
        
        // 如果没有在分段存储中，则从旧方式存储中删除（向后兼容）
        if (!deletedFromSegmented) {
          storedBinFiles.value = storedBinFiles.value.filter(file => file.id !== binFileId)
          saveStoredBinFiles()
        } else {
          // 从显示列表中移除
          storedBinFiles.value = storedBinFiles.value.filter(file => file.id !== binFileId)
        }
        
        message.success('bin文件已删除')
      } catch (error) {
        if (shouldLog('websocket')) console.error('删除本地存储的bin文件失败:', error)
        message.error('删除bin文件失败')
      }
    }
  })
}

// 格式化时间
const formatTime = (timestamp) => {
  if (!timestamp) return '未知'
  return new Date(timestamp).toLocaleString()
}

// Bin文件表单验证规则
const binRules = {
  name: [
    { required: true, message: '请输入角色名称', trigger: 'blur' }
  ],
  files: [
    { required: true, message: '请上传bin文件', trigger: 'change' }
  ]
}

// 压缩包表单验证规则
const archiveRules = {
  archiveFile: [
    { required: true, message: '请上传压缩包文件', trigger: 'change' }
  ]
}

// 编辑表单
const editForm = reactive({
  name: '',
  server: '',
  wsUrl: ''
})

// 验证规则
const importRules = {
  name: [
    { required: true, message: '请输入Token名称', trigger: 'blur' }
  ],
  base64Token: [
    { required: true, message: '请输入Base64 Token', trigger: 'blur' }
  ]
}

const urlRules = {
  name: [
    { required: true, message: '请输入Token名称', trigger: 'blur' }
  ],
  url: [
    { required: true, message: '请输入Token获取地址', trigger: 'blur' },
    { type: 'url', message: '请输入有效的URL地址', trigger: 'blur' }
  ]
}

const editRules = {
  name: [
    { required: true, message: '请输入Token名称', trigger: 'blur' }
  ]
}

// 处理文件选择事件
const handleFileSelect = (event) => {
  const files = Array.from(event.target.files);
  if (files.length > 0) {
    binForm.files = files;
    // 如果没有手动输入名称且只上传了一个文件，自动填充名称
    if (!binForm.name && files.length === 1) {
      const fileName = files[0].name;
      const nameWithoutExt = fileName.substring(0, fileName.lastIndexOf('.'));
      binForm.name = nameWithoutExt;
    }
    // 清空input，允许重复选择同一个文件
    event.target.value = '';
  }
};

// 移除选中的文件
const removeFile = (index) => {
  if (binForm.files && binForm.files.length > 0) {
    binForm.files.splice(index, 1);
    // 如果文件列表为空，清空引用以触发UI更新
    if (binForm.files.length === 0) {
      binForm.files = null;
    }
  }
};

// 处理压缩包文件选择
const handleArchiveFileSelect = (event) => {
  const files = event.target.files;
  if (files.length > 0) {
    archiveForm.archiveFile = files[0];
    // 如果没有手动输入名称，自动填充名称（去除扩展名）
    if (!archiveForm.name) {
      const fileName = files[0].name;
      const nameWithoutExt = fileName.substring(0, fileName.lastIndexOf('.'));
      archiveForm.name = nameWithoutExt;
    }
    // 清空input，允许重复选择同一个文件
    event.target.value = '';
  }
};

// 移除选中的压缩包
const removeArchiveFile = () => {
  archiveForm.archiveFile = null;
};

// 将ArrayBuffer转换为Base64字符串
const arrayBufferToBase64 = (arrayBuffer) => {
  let binary = '';
  const bytes = new Uint8Array(arrayBuffer);
  const len = bytes.byteLength;
  for (let i = 0; i < len; i++) {
    binary += String.fromCharCode(bytes[i]);
  }
  return btoa(binary);
};

// 从Base64字符串转换回ArrayBuffer
const base64ToArrayBuffer = (base64) => {
  const binaryString = atob(base64);
  const bytes = new Uint8Array(binaryString.length);
  for (let i = 0; i < binaryString.length; i++) {
    bytes[i] = binaryString.charCodeAt(i);
  }
  return bytes.buffer;
};

// 读取bin文件内容
// 更适合处理二进制文件的读取方式
const readBinFile = (file) => {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = (e) => {
      // 对于二进制数据，使用readAsArrayBuffer更安全，确保原始格式不被破坏
      resolve(e.target.result);
    };
    reader.onerror = reject;
    // 使用readAsArrayBuffer而不是readAsText，以保留二进制文件的原始格式
    reader.readAsArrayBuffer(file);
  });
};

// 处理bin文件导入（🔥 v3.14.2: 支持并发上传）
const handleBinImport = async () => {
  if (!binForm.files || binForm.files.length === 0) {
    message.error('请先选择bin文件');
    return;
  }

  try {
    isImporting.value = true;
    const files = Array.from(binForm.files)
    const totalFiles = files.length
    
    // 🔥 显示上传进度
    uploadProgress.show = true
    uploadProgress.type = 'bin'
    uploadProgress.total = totalFiles
    
    console.log(`🚀 [Bin导入] 开始并发处理 ${totalFiles} 个文件（并发数：${uploadConfig.concurrentLimit}）`)
    
    // 🔥 v3.14.2: 使用并发处理
    const results = await processConcurrently(
      files,
      (file, index) => processSingleBinFile(file, index, totalFiles, binForm.name),
      uploadConfig.concurrentLimit
    )
    
    // 🔥 v3.14.2.5: 流式处理 - 平衡速度与内存
    // 策略：每处理100个文件保存一次（速度更快，内存控制在2000MB以内）
    let successCount = 0
    let failedCount = 0
    const STREAM_BATCH_SIZE = 100 // 每批处理100个文件
    let binFilesToSave = [] // 当前批次的bin文件
    
    console.log(`🌊 [Bin导入] 流式处理策略：每${STREAM_BATCH_SIZE}个文件保存一次（优化速度）`)
    
    for (let i = 0; i < results.length; i++) {
      const result = results[i]
      
      if (result.success) {
        const tokenData = result.tokenData
        
        try {
          const base64Content = arrayBufferToBase64(tokenData.arrayBuffer)
          const binFileId = `bin_${Date.now()}_${Math.floor(Math.random() * 1000)}`
          
          binFilesToSave.push({
            id: binFileId,
            name: tokenData.fileName,
            roleName: tokenData.name,
            content: base64Content,
            createdAt: new Date().toISOString(),
            lastUsed: new Date().toISOString()
          })
          
          // 构建完整的tokenInfo
          const tokenInfo = {
            name: tokenData.name,
            token: tokenData.token,
            server: binForm.server,
            wsUrl: binForm.wsUrl || tokenData.wsUrl,
            createdAt: Date.now(),
            updatedAt: Date.now(),
            rawToken: tokenData.rawToken,
            binFileContent: tokenData.arrayBuffer,
            binFileId: binFileId,
            lastRefreshed: Date.now(),
            importMethod: 'bin'
          }
          
          // 🔥 立即保存Token
          const importResult = tokenStore.importBase64Token(
            tokenInfo.name,
            tokenInfo.token,
            {
              server: tokenInfo.server,
              wsUrl: tokenInfo.wsUrl,
              importMethod: 'bin',
              binFileContent: tokenInfo.binFileContent,
              binFileId: tokenInfo.binFileId,
              rawData: tokenInfo.rawToken,
              lastRefreshed: tokenInfo.lastRefreshed
            }
          )
          
          if (!importResult.success) {
            throw new Error(importResult.error)
          }
          
          successCount++
          uploadProgress.successCount = successCount
          
        } catch (saveError) {
          console.error(`[Bin导入] 保存Token失败: ${tokenData.name}`, saveError)
          failedCount++
          uploadProgress.failedCount = failedCount
        }
      } else {
        failedCount++
        uploadProgress.failedCount = failedCount
      }
      
      // 🌊 流式保存：每处理STREAM_BATCH_SIZE个文件，或到达最后一个文件时，立即保存
      const shouldSave = binFilesToSave.length >= STREAM_BATCH_SIZE || i === results.length - 1
      
      if (shouldSave && binFilesToSave.length > 0) {
        console.log(`💾 [Bin导入] 流式保存第 ${Math.floor(i / STREAM_BATCH_SIZE) + 1} 批（${binFilesToSave.length}个文件）...`)
        
        try {
          // 立即保存当前批次（每段100个文件的分段存储，减少分段次数提速）
          await saveBinFilesBatch(binFilesToSave, 100, `Bin导入-批次${Math.floor(i / STREAM_BATCH_SIZE) + 1}`)
          
          // ✅ 关键：立即清空数组并触发GC
          binFilesToSave = []
          
          // 手动触发GC（如果可用）
          if (typeof window !== 'undefined' && window.gc) {
            try {
              window.gc()
            } catch (e) {}
          }
          
          // 给浏览器一点时间进行GC（减少延迟以提速）
          await new Promise(resolve => setTimeout(resolve, 50))
          
          console.log(`✅ [Bin导入] 第 ${Math.floor(i / STREAM_BATCH_SIZE) + 1} 批保存完成，内存已释放`)
          
        } catch (storageError) {
          console.error(`❌ [Bin导入] 第 ${Math.floor(i / STREAM_BATCH_SIZE) + 1} 批保存失败:`, storageError)
          binFilesToSave = [] // 即使失败也要清空，避免内存泄漏
        }
      }
      
      // 清除当前result的引用，帮助GC
      results[i] = null
    }
    
    console.log(`🎉 [Bin导入] 流式处理完成！成功${successCount}个，失败${failedCount}个`)
    
    // 🔥 隐藏上传进度（延迟2秒，让用户看到最终结果）
    setTimeout(() => {
      resetUploadProgress()
    }, 2000)

    // 🔥 显示最终结果
    if (successCount > 0) {
      // 重置表单
      resetBinForm();
      showImportForm.value = false;
      message.success(`成功导入 ${successCount} 个Token${failedCount > 0 ? `，${failedCount} 个失败` : ''}`);
    } else {
      message.error(`所有文件导入失败（共 ${totalFiles} 个）`)
    }
  } catch (error) {
    console.error('[Bin导入] 错误:', error);
    message.error(`导入失败: ${error.message}`);
    resetUploadProgress()  // 出错时也要隐藏进度
  } finally {
    isImporting.value = false;
  }
};

// 处理压缩包导入
// 🔥 v3.14.2: 支持并发上传
const handleArchiveImport = async () => {
  if (!archiveForm.archiveFile) {
    message.error('请先选择压缩包文件');
    return;
  }

  try {
    isImporting.value = true;
    const archiveFile = archiveForm.archiveFile;
    const archiveName = archiveFile.name;
    
    if (shouldLog('websocket')) console.log(`[压缩包导入] 开始处理压缩包: ${archiveName}`);

    // 检查文件类型
    const fileExtension = archiveName.split('.').pop().toLowerCase();
    let extractedFiles = [];

    // 根据文件类型选择不同的解压方法
    if (fileExtension === 'zip') {
      // 使用JSZip处理ZIP文件
      const JSZip = (await import('jszip')).default;
      const zip = new JSZip();
      
      // 读取ZIP文件
      const arrayBuffer = await readBinFile(archiveFile);
      const zipData = await zip.loadAsync(arrayBuffer);
      
      // 遍历ZIP中的所有文件
      zip.forEach((relativePath, zipEntry) => {
        if (zipEntry.name.endsWith('.bin')) {
          extractedFiles.push({
            name: zipEntry.name,
            entry: zipEntry,
            type: 'zip'
          });
        }
      });
      
      if (shouldLog('websocket')) console.log(`[压缩包导入] ZIP文件中找到 ${extractedFiles.length} 个.bin文件`);
    } else if (fileExtension === 'rar') {
      // 对于RAR文件，我们需要使用不同的方法
      // 由于RAR是专有格式，我们可能需要服务器端支持
      message.warning('RAR格式解压功能正在开发中，请使用ZIP格式');
      return;
    } else {
      message.error(`不支持的压缩格式: ${fileExtension}`);
      return;
    }

    if (extractedFiles.length === 0) {
      message.warning('压缩包中未找到.bin文件');
      return;
    }

    const totalFiles = extractedFiles.length
    
    // 🔥 显示上传进度
    uploadProgress.show = true
    uploadProgress.type = 'archive'
    uploadProgress.total = totalFiles
    
    console.log(`🚀 [压缩包导入] 开始并发处理 ${totalFiles} 个文件（并发数：${uploadConfig.concurrentLimit}）`)
    
    // 🔥 v3.14.2: 使用并发处理提取的bin文件
    const results = await processConcurrently(
      extractedFiles,
      (fileInfo, index) => processSingleArchiveFile(fileInfo, index, totalFiles, archiveForm.name),
      uploadConfig.concurrentLimit
    )
    
    // 处理结果并保存Token
    let successCount = 0
    let failedCount = 0
    const binFilesToSave = [] // 收集需要保存的bin文件
    
    for (const result of results) {
      if (result.success) {
        const tokenData = result.tokenData
        
        // 准备bin文件本地存储数据（延后批量保存）
        try {
          const base64Content = arrayBufferToBase64(tokenData.arrayBuffer)
          const binFileId = `bin_${Date.now()}_${Math.floor(Math.random() * 1000)}`
          
          binFilesToSave.push({
            id: binFileId,
            name: tokenData.fileName,
            roleName: tokenData.name,
            content: base64Content,
            createdAt: new Date().toISOString(),
            lastUsed: new Date().toISOString()
          })
          
          // 构建完整的tokenInfo
          const tokenInfo = {
            name: tokenData.name,
            token: tokenData.token,
            server: archiveForm.server,
            wsUrl: archiveForm.wsUrl || tokenData.wsUrl,
            createdAt: Date.now(),
            updatedAt: Date.now(),
            rawToken: tokenData.rawToken,
            binFileContent: tokenData.arrayBuffer,
            binFileId: binFileId,
            lastRefreshed: Date.now(),
            importMethod: 'archive'
          }
          
          // 🔥 立即保存Token
          const importResult = tokenStore.importBase64Token(
            tokenInfo.name,
            tokenInfo.token,
            {
              server: tokenInfo.server,
              wsUrl: tokenInfo.wsUrl,
              importMethod: 'archive',
              binFileContent: tokenInfo.binFileContent,
              binFileId: tokenInfo.binFileId,
              rawData: tokenInfo.rawToken,
              lastRefreshed: tokenInfo.lastRefreshed
            }
          )
          
          if (!importResult.success) {
            throw new Error(importResult.error)
          }
          
          successCount++
          uploadProgress.successCount = successCount
          
        } catch (saveError) {
          console.error(`[压缩包导入] 保存Token失败: ${tokenData.name}`, saveError)
          failedCount++
          uploadProgress.failedCount = failedCount
        }
      } else {
        failedCount++
        uploadProgress.failedCount = failedCount
      }
    }
    
    // 🔥 v3.14.2.3: 分段存储 - 每段30个文件（3MB），彻底解决内存峰值
    if (binFilesToSave.length > 0) {
      setTimeout(async () => {
        try {
          await saveBinFilesBatch(binFilesToSave, 30, '压缩包导入')
        } catch (storageError) {
          console.error('[压缩包导入] 批量保存bin文件失败:', storageError)
        }
      }, 500)
    }
    
    // 🔥 隐藏上传进度（延迟2秒，让用户看到最终结果）
    setTimeout(() => {
      resetUploadProgress()
    }, 2000)

    // 🔥 显示最终结果
    if (successCount > 0) {
      // 重置表单
      resetArchiveForm();
      showImportForm.value = false;
      message.success(`成功从压缩包导入 ${successCount} 个Token${failedCount > 0 ? `，${failedCount} 个失败` : ''}`);
    } else {
      message.error(`所有文件导入失败（共 ${totalFiles} 个）`)
    }
  } catch (error) {
    console.error('[压缩包导入] 错误:', error);
    message.error(`导入失败: ${error.message}`);
    resetUploadProgress()  // 出错时也要隐藏进度
  } finally {
    isImporting.value = false;
  }
};

// 重置压缩包表单
const resetArchiveForm = () => {
  archiveForm.name = '';
  archiveForm.server = '';
  archiveForm.wsUrl = '';
  archiveForm.archiveFile = null;
};

// 从二进制数据中提取Token
const extractRoleToken = (arrayBuffer) => {
  try {
    // 将ArrayBuffer转换为Uint8Array以便处理
    const bytes = new Uint8Array(arrayBuffer);

    // 转换为ASCII字符串以便搜索
    let asciiString = '';
    for (let i = 0; i < bytes.length; i++) {
      // 只转换可打印的ASCII字符（32-126）
      if (bytes[i] >= 32 && bytes[i] <= 126) {
        asciiString += String.fromCharCode(bytes[i]);
      } else {
        asciiString += '.'; // 用点号表示不可打印字符
      }
    }

    // 添加调试信息
    if (shouldLog('websocket')) console.log('[Bin导入] 转换后的ASCII字符串前200个字符:', asciiString.substring(0, 200));
    
    // 搜索Token的位置 - 只查找 "Token" 字符串（与原始工具保持一致）
    const tokenIndex = asciiString.indexOf('Token');

    if (tokenIndex !== -1) {
      if (shouldLog('websocket')) console.log(`[Bin导入] 找到Token标记在位置 ${tokenIndex}`);
      
      // 找到Token标记，提取Token值
      let tokenStart = tokenIndex + 5; // "Token"长度为5

      // 跳过可能的非Base64字符，直到找到Base64字符
      while (tokenStart < asciiString.length) {
        const char = asciiString[tokenStart];
        if (isBase64Char(char)) {
          break;
        }
        tokenStart++;
      }

      // 提取Base64 Token
      let tokenEnd = tokenStart;
      while (tokenEnd < asciiString.length && isBase64Char(asciiString[tokenEnd])) {
        tokenEnd++;
      }

      const tokenValue = asciiString.substring(tokenStart, tokenEnd);
      if (shouldLog('websocket')) console.log(`[Bin导入] 提取的Token值: ${tokenValue.substring(0, 50)}...`);
      if (shouldLog('websocket')) console.log(`[Bin导入] Token长度: ${tokenValue.length}`);

      if (tokenValue.length > 0) {
        return tokenValue;
      } else {
        if (shouldLog('websocket')) console.error('找到Token标记但未找到Token值');
        return null;
      }
    } else {
      if (shouldLog('websocket')) console.error('在响应中未找到Token标记');
      return null;
    }
  } catch (error) {
    if (shouldLog('websocket')) console.error('提取Token时发生错误:', error);
    return null;
  }
};

// 检查字符是否为Base64字符
const isBase64Char = (char) => {
  // Base64字符集: A-Z, a-z, 0-9, +, /, =
  return /[A-Za-z0-9+/=]/.test(char);
};

// 重置bin文件上传表单
const resetBinForm = () => {
  binForm.name = '';
  binForm.server = '';
  binForm.wsUrl = '';
  binForm.files = [];
  // 移除对不存在的resetValidation方法的调用
  // 直接重置表单数据即可清除验证状态
}


const bulkOptions = [
  { label: '导出所有Token', key: 'export' },
  { label: '导入Token文件', key: 'import' },
  { label: '指定删除Token', key: 'batch-delete' },
  { label: '清理过期Token', key: 'clean' },
  { label: '断开所有连接', key: 'disconnect' },
  { label: '清除所有Token', key: 'clear' }
]

// 方法
const handleImport = async () => {
  if (!importFormRef.value) return

  try {
    await importFormRef.value.validate()
    isImporting.value = true

    const result = tokenStore.importBase64Token(
      importForm.name,
      importForm.base64Token,
      {
        server: importForm.server,
        wsUrl: importForm.wsUrl,
        importMethod: 'manual',
        // 为手动导入的Token也添加binFileContent属性，以便支持刷新功能
        binFileContent: importForm.base64Token
      }
    )

    if (result.success) {
      message.success(result.message)
      // 显示token详情信息（如果有）
      if (result.details) {
        if (shouldLog('websocket')) console.log('Token导入详情:', result.details)
      }
      resetImportForm()
      showImportForm.value = false
    } else {
      const errorMsg = result.error || result.message || 'Token导入失败'
      message.error(errorMsg)
      if (shouldLog('websocket')) console.error('Token导入错误详情:', {
        error: result.error,
        message: result.message,
        originalToken: importForm.base64Token?.substring(0, 50) + '...'
      })
    }
  } catch (error) {
    // 表单验证失败
  } finally {
    isImporting.value = false
  }
}

// URL获取Token
const handleUrlImport = async () => {
  if (!urlFormRef.value) return

  try {
    await urlFormRef.value.validate()
    isImporting.value = true

    // 获取Token数据 - 处理跨域问题
    let response
    
    // 检查是否为本地或相同域名的URL
    const isLocalUrl = urlForm.url.startsWith(window.location.origin) || 
                      urlForm.url.startsWith('/') ||
                      urlForm.url.startsWith('http://localhost') ||
                      urlForm.url.startsWith('http://127.0.0.1')
    
    if (isLocalUrl) {
      // 本地URL直接请求
      response = await fetch(urlForm.url)
    } else {
      // 跨域URL - 尝试CORS请求
      try {
        response = await fetch(urlForm.url, {
          method: 'GET',
          headers: {
            'Accept': 'application/json',
          },
          mode: 'cors'
        })
      } catch (corsError) {
        throw new Error(`跨域请求被阻止。请确保目标服务器支持CORS，或使用浏览器扩展/代理服务器。错误详情: ${corsError.message}`)
      }
    }

    if (!response.ok) {
      throw new Error(`请求失败: ${response.status} ${response.statusText}`)
    }
    
    const data = await response.json()
    
    // 检查返回数据是否包含token
    if (!data.token) {
      throw new Error('返回数据中未找到token字段')
    }

    // 使用获取到的token创建新的token记录
    const result = tokenStore.importBase64Token(
      urlForm.name,
      data.token,
      {
        server: urlForm.server || data.server,
        wsUrl: urlForm.wsUrl,
        sourceUrl: urlForm.url, // 保存源URL用于刷新
        importMethod: 'url'
      }
    )

    if (result.success) {
      message.success(result.message)
      // 显示token详情信息（如果有）
      if (result.details) {
        if (shouldLog('websocket')) console.log('URL Token导入详情:', result.details)
      }
      resetUrlForm()
      showImportForm.value = false
    } else {
      const errorMsg = result.error || result.message || 'URL Token导入失败'
      message.error(errorMsg)
      if (shouldLog('websocket')) console.error('URL Token导入错误详情:', {
        error: result.error,
        message: result.message,
        sourceUrl: urlForm.url,
        receivedToken: data?.token?.substring(0, 50) + '...'
      })
    }
  } catch (error) {
    if (shouldLog('websocket')) console.error('URL获取Token失败:', error)
    message.error(error.message || 'URL获取Token失败')
  } finally {
    isImporting.value = false
  }
}

// 刷新Token
const refreshToken = async (token) => {
  if (!token.sourceUrl) {
    message.warning('该Token未配置刷新地址')
    return
  }

  refreshingTokens.value.add(token.id)
  
  try {
    // 使用与导入相同的跨域处理逻辑
    let response
    
    const isLocalUrl = token.sourceUrl.startsWith(window.location.origin) || 
                      token.sourceUrl.startsWith('/') ||
                      token.sourceUrl.startsWith('http://localhost') ||
                      token.sourceUrl.startsWith('http://127.0.0.1')
    
    if (isLocalUrl) {
      response = await fetch(token.sourceUrl)
    } else {
      try {
        response = await fetch(token.sourceUrl, {
          method: 'GET',
          headers: {
            'Accept': 'application/json',
          },
          mode: 'cors'
        })
      } catch (corsError) {
        throw new Error(`跨域请求被阻止。请确保目标服务器支持CORS。错误详情: ${corsError.message}`)
      }
    }

    if (!response.ok) {
      throw new Error(`请求失败: ${response.status} ${response.statusText}`)
    }
    
    const data = await response.json()
    
    if (!data.token) {
      throw new Error('返回数据中未找到token字段')
    }

    // 更新token信息
    tokenStore.updateToken(token.id, {
      token: data.token,
      server: data.server || token.server,
      lastRefreshed: Date.now()
    })

    // 如果当前token有连接，需要重新连接
    if (tokenStore.getWebSocketStatus(token.id) === 'connected') {
      tokenStore.closeWebSocketConnection(token.id)
      setTimeout(() => {
        tokenStore.createWebSocketConnection(token.id, data.token, token.wsUrl)
      }, 500)
    }

    message.success('Token刷新成功')
  } catch (error) {
    if (shouldLog('websocket')) console.error('刷新Token失败:', error)
    message.error(error.message || 'Token刷新失败')
  } finally {
    refreshingTokens.value.delete(token.id)
  }
}

// 从bin文件刷新Token
const refreshBinToken = async (token) => {
  if (!token.binFileContent) {
    message.warning('该Token没有Bin文件内容，无法刷新')
    return
  }

  refreshingTokens.value.add(token.id)
  
  try {
    // 显示加载状态
    const loadingMsg = message.loading('正在从Bin文件刷新Token...', { duration: 0 })
    
    let binArrayBuffer
    
    // 检查是否是手动导入的Token（Base64字符串）
    if (token.importMethod === 'manual') {
      // 对于手动导入的Token，我们需要先解析Base64字符串获取原始数据
      try {
        // 尝试解码Base64字符串
        const decodedString = atob(token.binFileContent)
        
        // 尝试解析为JSON
        let tokenData
        try {
          tokenData = JSON.parse(decodedString)
        } catch {
          // 不是JSON格式，作为纯token处理
          tokenData = {token: decodedString}
        }
        
        // 提取实际token
        const actualToken = tokenData.token || tokenData.gameToken || decodedString
        
        // 对于手动导入的Token，我们直接使用原始token创建新的token
        const newTokenData = {
          roleToken: actualToken,
          timestamp: Date.now()
        }
        
        // 生成新的token
        const newToken = btoa(JSON.stringify(newTokenData))
        
        // 创建WebSocket URL
        const wsUrl = `wss://xyyw.hortor.net/gs?token=${newToken}&xyyw-platform=test&xyyw-version=1.0.0`
        
        // 更新token信息
        tokenStore.updateToken(token.id, {
          token: newToken,
          wsUrl: wsUrl,
          lastRefreshed: Date.now()
        })
        
        // 注意：不再在这里重建连接，由调用者控制连接重建
        
        loadingMsg.destroy()
        message.success('Token刷新成功')
        return
      } catch (error) {
        if (shouldLog('websocket')) console.error('处理手动导入Token失败:', error)
        throw new Error('处理手动导入Token失败: ' + error.message)
      }
    } else {
      // 对于bin文件导入的Token，检查binFileContent是否为ArrayBuffer
      if (token.binFileContent instanceof ArrayBuffer) {
        // 如果是ArrayBuffer，直接使用
        binArrayBuffer = token.binFileContent
        if (shouldLog('websocket')) console.log(`[Bin刷新] 使用ArrayBuffer格式的bin文件内容，大小: ${binArrayBuffer.byteLength} 字节`)
      } else {
        // 如果是base64字符串，转换为ArrayBuffer
        try {
          const binaryString = atob(token.binFileContent)
          const bytes = new Uint8Array(binaryString.length)
          for (let i = 0; i < binaryString.length; i++) {
            bytes[i] = binaryString.charCodeAt(i)
          }
          binArrayBuffer = bytes.buffer
          if (shouldLog('websocket')) console.log(`[Bin刷新] 将base64字符串转换为ArrayBuffer，大小: ${binArrayBuffer.byteLength} 字节`)
        } catch (error) {
          if (shouldLog('websocket')) console.error('转换binFileContent失败:', error)
          throw new Error('bin文件内容格式错误')
        }
      }
    }
    
    // 发送请求到登录接口（仅对bin文件导入的Token）
    const loginUrl = 'https://xxz-xyzw.hortorgames.com/login/authuser?_seq=1';
    if (shouldLog('websocket')) console.log(`[Bin刷新] 发送登录请求到: ${loginUrl}`);
    
    const response = await fetch(loginUrl, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/octet-stream',
      },
      body: binArrayBuffer
    })
    
    if (shouldLog('websocket')) console.log(`[Bin刷新] 响应状态:`, response.status);
    
    if (!response.ok) {
      const errorText = await response.text();
      if (shouldLog('websocket')) console.error(`[Bin刷新] 登录请求失败，状态: ${response.status}, 响应:`, errorText);
      throw new Error(`请求失败: ${response.status} ${response.statusText}`);
    }
    
    // 获取服务器响应的二进制数据
    const responseArrayBuffer = await response.arrayBuffer();
    if (shouldLog('websocket')) console.log(`[Bin刷新] 服务器响应字节长度: ${responseArrayBuffer.byteLength}`);
    
    // 从服务器响应中提取Token
    const roleToken = extractRoleToken(responseArrayBuffer);
    
    if (!roleToken) {
      throw new Error('无法从服务器响应中提取Token');
    }
    
    if (shouldLog('websocket')) console.log(`[Bin刷新] ✅ 成功提取roleToken: ${roleToken.substring(0, 20)}...`);
    if (shouldLog('websocket')) console.log(`[TokenImport] 🔍 调试 - 完整roleToken数据:`, roleToken);
    if (shouldLog('websocket')) console.log(`[TokenImport] 🔍 调试 - roleToken长度: ${roleToken.length}`);
    
    // 生成完整的Token数据和WSS连接参数
    const sessId = Date.now() * 1000 + Math.floor(Math.random() * 1000);
    const connId = Date.now();
    const tokenData = {
      roleToken,
      sessId,
      connId,
      isRestore: 0,
    };
    
    if (shouldLog('websocket')) console.log(`[Bin刷新] 生成的Token数据:`, tokenData);
    
    // 创建完整的Token格式
    const tokenStr = JSON.stringify(tokenData);
    const newToken = btoa(unescape(encodeURIComponent(tokenStr)));
    if (shouldLog('websocket')) console.log(`[Bin刷新] Base64编码的Token: ${newToken.substring(0, 20)}...`);
    
    // 生成WSS链接
    const p = encodeURIComponent(tokenStr);
    const e = 'x';
    const lang = 'chinese';
    const wsUrl = `wss://xxz-xyzw.hortorgames.com/agent?p=${p}&e=${e}&lang=${lang}`;
    if (shouldLog('websocket')) console.log(`[Bin刷新] 生成的WSS链接: ${wsUrl.substring(0, 100)}...`);
    if (shouldLog('websocket')) console.log(`[TokenImport] 🔍 调试 - 完整WSS链接: ${wsUrl}`);
    if (shouldLog('websocket')) console.log(`[TokenImport] 🔍 调试 - WSS链接长度: ${wsUrl.length}`);
    
    // 更新token信息
    tokenStore.updateToken(token.id, {
      token: newToken,
      wsUrl: wsUrl,
      rawData: tokenData,
      binFileId: token.binFileId,
      lastRefreshed: Date.now()
    });
    
    // 注意：不再在这里重建连接，由调用者控制连接重建
    
    loadingMsg.destroy()
    message.success('Token刷新成功')
    
    // 强制刷新UI状态
    nextTick(() => {
      // 触发响应式更新
      refreshingTokens.value = new Set(refreshingTokens.value)
      if (shouldLog('websocket')) console.log(`[Bin刷新] UI状态已刷新`)
    })
  } catch (error) {
    if (shouldLog('websocket')) console.error('从bin文件刷新Token失败:', error)
    message.error(error.message || 'Token刷新失败')
  } finally {
    // 确保从刷新状态中移除
    refreshingTokens.value.delete(token.id)
    // 强制刷新UI状态
    nextTick(() => {
      // 触发响应式更新
      refreshingTokens.value = new Set(refreshingTokens.value)
      if (shouldLog('websocket')) console.log(`[Bin刷新] UI状态已刷新（finally块）`)
    })
  }
}

const resetImportForm = () => {
  Object.keys(importForm).forEach(key => {
    importForm[key] = ''
  })
}

const resetUrlForm = () => {
  Object.keys(urlForm).forEach(key => {
    urlForm[key] = ''
  })
}

const selectToken = (token) => {
  tokenStore.selectToken(token.id)
  message.success(`已选择：${token.name}`)
}

const getConnectionStatus = (tokenId) => {
  return tokenStore.getWebSocketStatus(tokenId)
}

const getConnectionStatusText = (tokenId) => {
  const status = getConnectionStatus(tokenId)
  switch (status) {
    case 'connected': return '已连接'
    case 'connecting': return '连接中'
    case 'error': return '连接错误'
    default: return '未连接'
  }
}

const toggleConnection = async (token) => {
  const status = getConnectionStatus(token.id)

  if (status === 'connected') {
    tokenStore.closeWebSocketConnection(token.id)
    message.info('WebSocket连接已断开')
  } else {
    // 检查是否有bin文件内容
    if (token.binFileContent && token.importMethod === 'bin') {
      try {
        // 显示加载状态
        const loadingMsg = message.loading('正在从Bin文件获取Token并建立连接...', { duration: 0 })
        
        // 从bin文件刷新token - 这会发送bin文件到服务器获取新的roleToken
        await refreshBinToken(token)
        
        // 使用刷新后的token建立连接 - 这会使用新的roleToken创建WebSocket连接
        await tokenStore.reconnectWebSocket(token.id)
        
        loadingMsg.destroy()
        message.success('已从Bin文件重新获取Token并建立连接')
      } catch (error) {
        if (shouldLog('websocket')) console.error('从Bin文件获取Token并建立连接失败:', error)
        message.error(`连接失败: ${error.message}`)
      }
    } else {
      // 如果没有bin文件内容，使用原有方式连接
      tokenStore.createWebSocketConnection(token.id, token.token, token.wsUrl)
      message.success('正在建立WebSocket连接...')
    }
  }
}

const getTokenActions = (token) => {
  const actions = [
    { 
      label: '编辑', 
      key: 'edit',
      icon: () => h(NIcon, null, { default: () => h(Create) })
    },
    { 
      label: '复制Token', 
      key: 'copy',
      icon: () => h(NIcon, null, { default: () => h(Copy) })
    },
    {
      label: '查看原始数据',
      key: 'view-raw',
      icon: () => h(NIcon, null, { default: () => h(Eye) })
    }
  ]

  // 根据Token类型添加不同的刷新选项
  if (token.importMethod === 'url' && token.sourceUrl) {
    actions.push({ 
      label: '从URL刷新', 
      key: 'refresh-url',
      icon: () => h(NIcon, null, { default: () => h(SyncCircle) })
    })
  } else {
    actions.push({ 
      label: '刷新Token', 
      key: 'refresh',
      icon: () => h(NIcon, null, { default: () => h(Refresh) })
    })
  }

  actions.push(
    { 
      label: '重新连接', 
      key: 'reconnect',
      icon: () => h(NIcon, null, { default: () => h(Link) })
    },
    { type: 'divider' },
    { 
      label: '删除', 
      key: 'delete',
      icon: () => h(NIcon, null, { default: () => h(TrashBin) }),
      props: { style: { color: '#e74c3c' } }
    }
  )

  return actions
}

const handleTokenAction = async (key, token) => {
  switch (key) {
    case 'edit':
      editToken(token)
      break
    case 'copy':
      copyToken(token)
      break
    case 'view-raw':
      viewRawToken(token)
      break
    case 'refresh':
      // 手动添加的Token的刷新逻辑
      if (token.binFileContent) {
        // 如果有bin文件内容，使用bin文件刷新
        refreshBinToken(token)
      } else if (token.sourceUrl) {
        // 如果有源URL，使用URL刷新
        refreshToken(token)
      } else {
        message.warning('该Token没有配置刷新源，无法刷新。请重新导入Token。')
      }
      break
    case 'refresh-url':
      // URL获取的Token刷新
      refreshToken(token)
      break
    case 'reconnect':
      reconnectToken(token)
      break
    case 'delete':
      deleteToken(token)
      break
  }
}

const editToken = (token) => {
  editingToken.value = token
  Object.assign(editForm, {
    name: token.name,
    server: token.server || '',
    wsUrl: token.wsUrl || ''
  })
  showEditModal.value = true
}

const saveEdit = async () => {
  if (!editFormRef.value || !editingToken.value) return

  try {
    await editFormRef.value.validate()

    tokenStore.updateToken(editingToken.value.id, {
      name: editForm.name,
      server: editForm.server,
      wsUrl: editForm.wsUrl
    })

    message.success('Token信息已更新')
    showEditModal.value = false
    editingToken.value = null
  } catch (error) {
    // 验证失败
  }
}

const copyToken = async (token) => {
  try {
    await navigator.clipboard.writeText(token.token)
    message.success('Token已复制到剪贴板')
  } catch (error) {
    message.error('复制失败')
  }
}

const reconnectToken = async (token) => {
  // 检查是否有bin文件内容
  if (token.binFileContent && token.importMethod === 'bin') {
    try {
      // 显示加载状态
      const loadingMsg = message.loading('正在从Bin文件获取Token并重新连接...', { duration: 0 })
      
      // 先关闭现有连接
      tokenStore.closeWebSocketConnection(token.id)
      
      // 从bin文件刷新token - 这会发送bin文件到服务器获取新的roleToken
      await refreshBinToken(token)
      
      // 使用刷新后的token建立连接 - 这会使用新的roleToken创建WebSocket连接
      await tokenStore.reconnectWebSocket(token.id)
      
      loadingMsg.destroy()
      message.success('已从Bin文件重新获取Token并建立连接')
    } catch (error) {
      if (shouldLog('websocket')) console.error('从Bin文件获取Token并重新连接失败:', error)
      message.error(`重新连接失败: ${error.message}`)
    }
  } else {
    // 如果没有bin文件内容，使用原有方式重新连接
    tokenStore.closeWebSocketConnection(token.id)
    setTimeout(() => {
      tokenStore.createWebSocketConnection(token.id, token.token, token.wsUrl)
      message.success('正在重新连接...')
    }, 500)
  }
}

const deleteToken = (token) => {
  dialog.warning({
    title: '删除Token',
    content: `确定要删除Token "${token.name}" 吗？此操作无法恢复。`,
    positiveText: '确定删除',
    negativeText: '取消',
    onPositiveClick: () => {
      tokenStore.removeToken(token.id)
      message.success('Token已删除')
    }
  })
}

const handleBulkAction = (key) => {
  switch (key) {
    case 'export':
      exportTokens()
      break
    case 'import':
      importTokenFile()
      break
    case 'batch-delete':
      showBatchDeleteModal.value = true
      selectedTokensForDelete.value = []
      batchDeleteSearchQuery.value = ''
      break
    case 'clean':
      cleanExpiredTokens()
      break
    case 'disconnect':
      disconnectAll()
      break
    case 'clear':
      clearAllTokens()
      break
  }
}

// 批量删除Token
const handleBatchDelete = () => {
  if (selectedTokensForDelete.value.length === 0) {
    message.warning('请至少选择一个Token')
    return
  }

  dialog.warning({
    title: '确认批量删除',
    content: `确定要删除选中的 ${selectedTokensForDelete.value.length} 个Token吗？此操作不可撤销。`,
    positiveText: '确定删除',
    negativeText: '取消',
    onPositiveClick: async () => {
      try {
        let successCount = 0
        let failCount = 0

        for (const tokenId of selectedTokensForDelete.value) {
          const result = await tokenStore.removeToken(tokenId)
          if (result.success) {
            successCount++
          } else {
            failCount++
          }
        }

        if (successCount > 0) {
          message.success(`成功删除 ${successCount} 个Token${failCount > 0 ? `，失败 ${failCount} 个` : ''}`)
        } else {
          message.error('删除失败')
        }

        // 关闭弹窗并清空选择
        showBatchDeleteModal.value = false
        selectedTokensForDelete.value = []
      } catch (error) {
        message.error('批量删除失败：' + error.message)
      }
    }
  })
}

// 过滤后的Token列表（用于批量删除）
const filteredTokensForDelete = computed(() => {
  if (!batchDeleteSearchQuery.value.trim()) {
    return tokenStore.gameTokens
  }
  
  const query = batchDeleteSearchQuery.value.trim()
  
  // 检测是否是范围搜索（格式：数字-数字，例如 "590-599" 或 "590服-599服"）
  const rangeMatch = query.match(/^(\d+)(?:服)?[-~](\d+)(?:服)?$/i)
  
  if (rangeMatch) {
    // 范围搜索
    const start = parseInt(rangeMatch[1])
    const end = parseInt(rangeMatch[2])
    const min = Math.min(start, end)
    const max = Math.max(start, end)
    
    return tokenStore.gameTokens.filter(token => {
      // 从名称或服务器中提取数字
      const serverMatch = (token.server || token.name).match(/(\d+)/)
      if (serverMatch) {
        const serverNum = parseInt(serverMatch[1])
        return serverNum >= min && serverNum <= max
      }
      return false
    })
  }
  
  // 普通模糊搜索
  const queryLower = query.toLowerCase()
  return tokenStore.gameTokens.filter(token => {
    return token.name.toLowerCase().includes(queryLower) ||
           (token.server && token.server.toLowerCase().includes(queryLower)) ||
           token.id.toLowerCase().includes(queryLower)
  })
})

// 全选/取消全选（针对当前搜索结果）
const toggleSelectAll = (checked) => {
  if (checked) {
    // 全选：选中当前搜索结果中的所有Token
    const filteredIds = filteredTokensForDelete.value.map(t => t.id)
    // 合并已选中的和当前搜索结果的
    const uniqueIds = new Set([...selectedTokensForDelete.value, ...filteredIds])
    selectedTokensForDelete.value = Array.from(uniqueIds)
  } else {
    // 取消全选：移除当前搜索结果中的所有Token
    const filteredIds = new Set(filteredTokensForDelete.value.map(t => t.id))
    selectedTokensForDelete.value = selectedTokensForDelete.value.filter(id => !filteredIds.has(id))
  }
}

// 计算是否全选（基于当前搜索结果）
const isAllSelected = computed(() => {
  if (filteredTokensForDelete.value.length === 0) return false
  return filteredTokensForDelete.value.every(token => selectedTokensForDelete.value.includes(token.id))
})

// 计算是否部分选中（基于当前搜索结果）
const isIndeterminate = computed(() => {
  if (filteredTokensForDelete.value.length === 0) return false
  const selectedCount = filteredTokensForDelete.value.filter(token => 
    selectedTokensForDelete.value.includes(token.id)
  ).length
  return selectedCount > 0 && selectedCount < filteredTokensForDelete.value.length
})

// 当前搜索结果中已选中的数量
const selectedInFilteredCount = computed(() => {
  return filteredTokensForDelete.value.filter(token => 
    selectedTokensForDelete.value.includes(token.id)
  ).length
})

const exportTokens = () => {
  try {
    const data = tokenStore.exportTokens()
    const dataStr = JSON.stringify(data, null, 2)
    const dataBlob = new Blob([dataStr], { type: 'application/json' })

    const link = document.createElement('a')
    link.href = URL.createObjectURL(dataBlob)
    link.download = `tokens_backup_${new Date().toISOString().split('T')[0]}.json`
    link.click()

    message.success('Token数据已导出')
  } catch (error) {
    message.error('导出失败')
  }
}

const importTokenFile = () => {
  const input = document.createElement('input')
  input.type = 'file'
  input.accept = '.json'
  input.onchange = (e) => {
    const file = e.target.files[0]
    if (file) {
      const reader = new FileReader()
      reader.onload = (e) => {
        try {
          const data = JSON.parse(e.target.result)
          const result = tokenStore.importTokens(data)
          if (result.success) {
            message.success(result.message)
          } else {
            message.error(result.message)
          }
        } catch (error) {
          message.error('文件格式错误')
        }
      }
      reader.readAsText(file)
    }
  }
  input.click()
}

const cleanExpiredTokens = () => {
  const count = tokenStore.cleanExpiredTokens()
  message.success(`已清理 ${count} 个过期Token`)
}

const disconnectAll = () => {
  tokenStore.gameTokens.forEach(token => {
    tokenStore.closeWebSocketConnection(token.id)
  })
  message.success('所有连接已断开')
}

const clearAllTokens = () => {
  dialog.error({
    title: '清除所有Token',
    content: '确定要清除所有Token吗？此操作无法恢复！',
    positiveText: '确定清除',
    negativeText: '取消',
    onPositiveClick: () => {
      tokenStore.clearAllTokens()
      message.success('所有Token已清除')
    }
  })
}

// 原始Token数据查看相关
const showRawDataModal = ref(false)
const currentRawToken = ref(null)
const formattedRawData = computed(() => {
  if (!currentRawToken.value) return ''
  try {
    // 尝试解析token为JSON
    const tokenData = JSON.parse(atob(currentRawToken.value.token))
    return JSON.stringify(tokenData, null, 2)
  } catch (error) {
    // 如果解析失败，返回原始token（可能不是base64编码的JSON）
    return currentRawToken.value.token
  }
})

const viewRawToken = (token) => {
  currentRawToken.value = token
  showRawDataModal.value = true
}

const copyRawData = async () => {
  if (!formattedRawData.value) return
  
  try {
    await navigator.clipboard.writeText(formattedRawData.value)
    message.success('原始数据已复制到剪贴板')
  } catch (error) {
    message.error('复制失败')
  }
}

const maskToken = (token) => {
  if (!token) return ''
  try {
    // 尝试解析Base64编码的token
    const decodedToken = atob(token)
    try {
      // 尝试解析为JSON
      const tokenData = JSON.parse(decodedToken)
      // 将JSON对象转换为字符串并遮蔽
      const tokenStr = JSON.stringify(tokenData)
      if (tokenStr.length <= 16) {
        return tokenStr
      }
      // 保留前8个字符和后8个字符，中间用***代替
      return tokenStr.substring(0, 8) + '***' + tokenStr.substring(tokenStr.length - 8)
    } catch (jsonError) {
      // 如果不是JSON格式，直接处理解码后的字符串
      if (decodedToken.length <= 16) {
        return decodedToken
      }
      return decodedToken.substring(0, 8) + '***' + decodedToken.substring(decodedToken.length - 8)
    }
  } catch (error) {
    // 如果不是Base64编码，直接处理原始字符串
    const len = token.length
    if (len <= 16) return token
    return token.substring(0, 8) + '***' + token.substring(len - 8)
  }
}

const goToDashboard = () => {
  router.push('/dashboard')
}

// 处理添加Token按钮点击
const handleAddTokenClick = async () => {
  showImportForm.value = true
  
  // 等待DOM更新后滚动到导入表单区域
  await nextTick()
  
  if (importSectionRef.value) {
    importSectionRef.value.scrollIntoView({
      behavior: 'smooth',
      block: 'start'
    })
  }
}

// 关闭进度显示
const handleCloseProgress = () => {
  batchStore.closeProgressDisplay()
}

// 🚀 v3.13.5.5: 切换进度卡片显示状态（减少渲染压力）
const toggleProgressCards = () => {
  showProgressCards.value = !showProgressCards.value
  // 保存到localStorage，实现记忆功能
  localStorage.setItem('showProgressCards', showProgressCards.value.toString())
  
  // 提示用户
  if (showProgressCards.value) {
    message.success('已显示进度卡片')
  } else {
    message.info('已隐藏进度卡片，减少渲染压力')
  }
}

// 🚀 v3.13.5.5: 切换Token列表卡片显示状态（减少渲染压力）
const toggleTokenCards = () => {
  showTokenCards.value = !showTokenCards.value
  // 保存到localStorage，实现记忆功能
  localStorage.setItem('showTokenCards', showTokenCards.value.toString())
  
  // 提示用户
  if (showTokenCards.value) {
    message.success('已显示Token列表卡片')
  } else {
    message.info('已隐藏Token列表卡片，减少渲染压力')
  }
}

// 🚀 监听token数量变化，自动调整页码（防止页码超出范围）
watch(() => tokenStore.gameTokens.length, (newLength) => {
  if (pagination.enabled && totalPages.value > 0) {
    // 如果当前页超出范围，跳转到最后一页
    if (pagination.currentPage > totalPages.value) {
      pagination.currentPage = totalPages.value
    }
  }
})

// 处理自动跟随开关切换
const handleAutoFollowToggle = (value) => {
  if (value) {
    // 开启时立即滚动到最新token
    scrollToLatestExecutingToken()
  }
}

// 处理用户手动滚动
const handleProgressScroll = () => {
  if (!autoFollowLatest.value) return
  
  // 如果是自动滚动触发的，忽略
  if (isAutoScrolling.value) return
  
  // 标记用户正在滚动
  isUserScrolling.value = true
  
  // 清除之前的定时器
  if (scrollTimeout) {
    clearTimeout(scrollTimeout)
  }
  
  // 500ms后认为用户停止了滚动，关闭自动跟随
  scrollTimeout = setTimeout(() => {
    if (isUserScrolling.value && autoFollowLatest.value) {
      autoFollowLatest.value = false
      message.info('已停止自动跟随', { duration: 2000 })
    }
    isUserScrolling.value = false
  }, 500)
}

// 滚动到最新正在执行的token
const scrollToLatestExecutingToken = () => {
  if (!autoFollowLatest.value || !virtualScrollRef.value) return
  
  // 找到最新的正在执行的token
  const executingTokens = tokenStore.gameTokens
    .map((token, index) => ({ token, index }))
    .filter(({ token }) => {
      const progress = batchStore.taskProgress[token.id]
      return progress && progress.status === 'executing'
    })
  
  if (executingTokens.length > 0) {
    // 标记为自动滚动，避免触发用户滚动逻辑
    isAutoScrolling.value = true
    
    // 滚动到最后一个正在执行的token
    const lastExecuting = executingTokens[executingTokens.length - 1]
    virtualScrollRef.value.scrollToIndex(lastExecuting.index, 'smooth')
    
    // 滚动完成后（1秒后）清除自动滚动标记
    // 使用较长的时间确保滚动动画完成
    setTimeout(() => {
      isAutoScrolling.value = false
    }, 1000)
  }
}

// 检测是否为移动设备
const detectMobileDevice = () => {
  const userAgent = navigator.userAgent || navigator.vendor || window.opera
  const mobileRegex = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i
  isMobileDevice.value = mobileRegex.test(userAgent)
}

// 生命周期
onMounted(() => {
  tokenStore.initTokenStore()
  
  // 检测设备类型
  detectMobileDevice()
  
  // 加载本地存储的bin文件
  loadStoredBinFiles()

  // 如果没有token，显示导入表单
  if (!tokenStore.hasTokens) {
    showImportForm.value = true
  }
})

// 监听任务执行状态，在开始执行时自动启用跟随
watch(() => batchStore.isExecuting, (newValue) => {
  if (newValue) {
    // 任务开始执行时，自动启用跟随
    autoFollowLatest.value = true
    isUserScrolling.value = false
  }
})

// 监听taskProgress变化，自动滚动到最新正在执行的token
watch(
  () => batchStore.taskProgress,
  () => {
    if (autoFollowLatest.value && !isUserScrolling.value) {
      // 使用nextTick确保DOM已更新
      nextTick(() => {
        scrollToLatestExecutingToken()
      })
    }
  },
  { deep: true }
)
</script>

<style scoped lang="scss">
.token-import-page {
  min-height: 100vh;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  padding: 0; /* 移除padding，让导航栏顶格 */
}

/* 深色主题下的页面背景 */
[data-theme="dark"] .token-import-page {
  background: linear-gradient(135deg, #0f172a 0%, #1f2937 100%);
}

.container {
  max-width: 100%; /* 移除宽度限制，充分利用屏幕宽度 */
  margin: 0 auto;
  padding: 16px; /* 恢复内边距，但不影响导航栏 */
}

.page-header {
  text-align: center;
  margin-bottom: var(--spacing-2xl);
}

.header-content {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--spacing-md);
  color: white;
  text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}

.header-top {
  display: flex;
  align-items: center;
  gap: var(--spacing-md);
  position: relative;
  width: 100%;
  justify-content: center;
}

.theme-toggle {
  position: absolute;
  right: 0;
  background: rgba(255, 255, 255, 0.2);
  backdrop-filter: blur(10px);
  border: 1px solid rgba(255, 255, 255, 0.3);
}

.brand-logo {
  width: 64px;
  height: 64px;
  border-radius: var(--border-radius-medium);
  filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2));
}

.header-content h1 {
  font-size: var(--font-size-3xl);
  font-weight: var(--font-weight-bold);
  margin: 0;
  color: #ffffff;
  text-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
}

.header-content p {
  font-size: var(--font-size-lg);
  margin: 0;
  color: rgba(255, 255, 255, 0.95);
  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
}

.import-section {
  margin-bottom: var(--spacing-2xl);
}

// 🔥 v3.14.1: 上传进度显示样式
.upload-progress-section {
  margin-bottom: var(--spacing-2xl);
  max-width: 600px;
  margin-left: auto;
  margin-right: auto;
}

.upload-progress-card {
  animation: fadeIn 0.3s ease-in-out;
  
  .progress-content {
    display: flex;
    flex-direction: column;
    gap: var(--spacing-md);
    
    .progress-info {
      display: flex;
      justify-content: space-between;
      align-items: center;
      gap: var(--spacing-md);
      
      .n-text:first-child {
        font-weight: var(--font-weight-medium);
        color: var(--text-primary);
      }
      
      .n-text:last-child {
        font-size: var(--font-size-sm);
        color: var(--text-secondary);
      }
    }
  }
}

@keyframes fadeIn {
  from {
    opacity: 0;
    transform: translateY(-10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.import-card {
  background: var(--bg-primary);
  border-radius: var(--border-radius-xl);
  padding: var(--spacing-2xl);
  box-shadow: var(--shadow-large);
  max-width: 600px;
  margin: 0 auto;
}

.card-header {
  text-align: center;
  margin-bottom: var(--spacing-xl);

  h2 {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--spacing-sm);
    color: var(--text-primary);
    font-size: var(--font-size-xl);
    margin-bottom: var(--spacing-sm);
  }

  p {
    color: var(--text-secondary);
    margin: 0 0 var(--spacing-md) 0;
  }

  .subtitle {
    font-size: var(--font-size-md);
    color: var(--text-tertiary);
    margin: 0;
    font-weight: var(--font-weight-normal);
  }

  .import-method-tabs {
    margin-top: var(--spacing-md);
    display: flex;
    justify-content: center;
    
    .btn-text-short {
      display: none;
    }
    
    .btn-text-full {
      display: inline;
    }
  }
}

.form-tips {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-xs);
}

.form-tip {
  color: var(--text-tertiary);
  font-size: var(--font-size-sm);
}

.cors-tip {
  color: var(--warning-color);
  font-weight: var(--font-weight-medium);
}

.connection-actions {
  display: flex;
  gap: var(--spacing-xs);
  align-items: center;
}

/* 深色主题强制覆盖（与全局 data-theme 保持一致） */
[data-theme="dark"] .n-form-item-label,
[data-theme="dark"] .n-form-item-label__text {
  color: #ffffff !important;
}

[data-theme="dark"] .n-input__input,
[data-theme="dark"] .n-input__textarea {
  color: #ffffff !important;
  background-color: rgba(255, 255, 255, 0.1) !important;
}

[data-theme="dark"] .n-input__placeholder {
  color: rgba(255, 255, 255, 0.5) !important;
}

[data-theme="dark"] .n-card {
  background-color: rgba(255, 255, 255, 0.1) !important;
  color: #ffffff !important;
}

[data-theme="dark"] .import-card {
  background: rgba(45, 55, 72, 0.9) !important;
  color: #ffffff !important;
}

[data-theme="dark"] .import-card h2 {
  color: #ffffff !important;
}

[data-theme="dark"] .import-card .subtitle {
  color: rgba(255, 255, 255, 0.7) !important;
}

[data-theme="dark"] .n-collapse-item__header {
  color: #ffffff !important;
}

[data-theme="dark"] .n-collapse-item__content-wrapper {
  background-color: transparent !important;
}

[data-theme="dark"] .n-radio-button {
  color: #ffffff !important;
}

[data-theme="dark"] .n-radio-button--checked {
  background-color: rgba(16, 185, 129, 0.8) !important;
  color: #ffffff !important;
}

[data-theme="dark"] .form-tip {
  color: rgba(255, 255, 255, 0.6) !important;
}

.optional-fields {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--spacing-md);
}

/* Bin上传整体容器（确保垂直布局） */
.bin-upload-wrapper {
  display: flex;
  flex-direction: column; /* 垂直排列：按钮区域在上，文件列表在下 */
  width: 100%;
  gap: 0; /* 使用子元素的margin控制间距 */
}

/* 上传按钮区域样式（左右排列） */
.upload-buttons-wrapper {
  display: flex;
  flex-direction: row; /* 左右排列 */
  gap: 12px;
  margin-bottom: 16px; /* 与文件列表保持间距 */
  flex-wrap: wrap; /* 小屏幕时换行 */
  width: 100%; /* 确保占满宽度 */
}

.file-upload-container,
.folder-batch-upload {
  flex: 1; /* 平均分配宽度 */
  min-width: 150px; /* 最小宽度，防止过窄 */
  
  .n-button {
    width: 100%; /* 按钮占满容器宽度 */
  }
}

/* 文件列表容器样式（在按钮下方独立显示） */
.file-list-container {
  margin-top: 0; /* 按钮已经有margin-bottom */
  width: 100%; /* 占满整行 */
  clear: both; /* 清除浮动 */
  display: block; /* 确保块级显示 */
}

.file-list-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 8px;
  
  .file-count {
    font-weight: 600;
    color: #667eea;
    font-size: 14px;
  }
}

.file-list {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  padding: 12px;
  background: rgba(102, 126, 234, 0.05);
  border-radius: 8px;
  border: 1px solid rgba(102, 126, 234, 0.2);
  max-height: 300px; /* 最大高度300px */
  overflow-y: auto; /* 超出显示滚动条 */
  transition: max-height 0.3s ease;
  
  /* 美化滚动条 */
  &::-webkit-scrollbar {
    width: 8px;
  }
  
  &::-webkit-scrollbar-track {
    background: rgba(0, 0, 0, 0.05);
    border-radius: 4px;
  }
  
  &::-webkit-scrollbar-thumb {
    background: rgba(102, 126, 234, 0.3);
    border-radius: 4px;
    
    &:hover {
      background: rgba(102, 126, 234, 0.5);
    }
  }
  
  /* 收起状态：只显示3行 */
  &.collapsed {
    max-height: 120px;
    overflow: hidden;
    position: relative;
    
    /* 渐变遮罩效果 */
    &::after {
      content: '';
      position: absolute;
      bottom: 0;
      left: 0;
      right: 0;
      height: 40px;
      background: linear-gradient(to bottom, transparent, rgba(102, 126, 234, 0.05));
      pointer-events: none;
    }
  }
}

.file-tag {
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* 深色主题适配 */
html.dark .file-list,
html[data-theme="dark"] .file-list {
  background: rgba(102, 126, 234, 0.1);
  border-color: rgba(102, 126, 234, 0.3);
  
  &::-webkit-scrollbar-track {
    background: rgba(255, 255, 255, 0.05);
  }
  
  &::-webkit-scrollbar-thumb {
    background: rgba(102, 126, 234, 0.4);
    
    &:hover {
      background: rgba(102, 126, 234, 0.6);
    }
  }
  
  &.collapsed::after {
    background: linear-gradient(to bottom, transparent, rgba(102, 126, 234, 0.1));
  }
}

html.dark .file-list-header .file-count,
html[data-theme="dark"] .file-list-header .file-count {
  color: #8ea3f5;
}

.form-actions {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-md);
  margin-top: var(--spacing-xl);
  position: sticky; /* 粘性定位 */
  bottom: 0;
  background: var(--bg-primary); /* 背景色，避免被内容遮挡 */
  padding: 16px 0 0 0;
  z-index: 10;
  
  /* 顶部渐变阴影，增强分离感 */
  &::before {
    content: '';
    position: absolute;
    top: -16px;
    left: -24px;
    right: -24px;
    height: 16px;
    background: linear-gradient(to bottom, transparent, var(--bg-primary));
    pointer-events: none;
  }
}

/* 深色主题下的按钮区域 */
html.dark .form-actions,
html[data-theme="dark"] .form-actions {
  background: rgba(30, 30, 30, 0.95);
  
  &::before {
    background: linear-gradient(to bottom, transparent, rgba(30, 30, 30, 0.95));
  }
}

.tokens-section {
  background: var(--bg-primary);
  border-radius: var(--border-radius-xl);
  padding: var(--spacing-xl);
  box-shadow: var(--shadow-medium);
}

/* 深色主题下的列表区域背景 */
[data-theme="dark"] .tokens-section {
  background: rgba(45, 55, 72, 0.9);
  color: #ffffff;
}

.section-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: var(--spacing-xl);
  
  /* 🚀 v3.13.5.5: 标题和按钮容器 */
  .section-title {
    display: flex;
    align-items: center;
    gap: 12px;
  }

  h2 {
    color: var(--text-primary);
    font-size: var(--font-size-xl);
    margin: 0;
  }
}

.header-actions {
  display: flex;
  gap: var(--spacing-md);
}

/* 🚀 v3.13.5.5: Token列表隐藏时的提示信息样式（极致紧凑版） */
.tokens-summary {
  padding: 8px 0;
}

/* 🚀 分页控件样式 */
.pagination-controls {
  padding: 16px 0;
  margin-bottom: 16px;
  border-bottom: 1px solid var(--border-color);
}

.tokens-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
  gap: var(--spacing-lg);
}

.token-card {
  border: 2px solid var(--border-light);
  border-radius: var(--border-radius-large);
  padding: var(--spacing-lg);
  cursor: pointer;
  transition: all var(--transition-normal);

  &:hover {
    box-shadow: var(--shadow-medium);
    transform: translateY(-2px);
  }

  &.active {
    border-color: var(--primary-color);
    box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);
  }

  &.connected {
    border-left: 4px solid var(--success-color);
  }
}

.card-header {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  margin-bottom: var(--spacing-md);
}

.token-info {
  flex: 1;
}

.token-name {
  font-size: var(--font-size-md);
  font-weight: var(--font-weight-semibold);
  color: var(--text-primary);
  margin: 0 0 var(--spacing-xs) 0;
}

.token-meta {
  display: flex;
  gap: var(--spacing-sm);
  flex-wrap: wrap;
}

.meta-item {
  background: var(--bg-tertiary);
  color: var(--text-secondary);
  font-size: var(--font-size-xs);
  padding: var(--spacing-xs) var(--spacing-sm);
  border-radius: var(--border-radius-small);
}

.card-body {
  margin-bottom: var(--spacing-md);
}

.token-display {
  display: flex;
  align-items: center;
  gap: var(--spacing-sm);
  margin-bottom: var(--spacing-md);
  padding: var(--spacing-sm);
  background: var(--bg-tertiary);
  border-radius: var(--border-radius-medium);
}

.token-label {
  color: var(--text-secondary);
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-medium);
}

.token-value {
  font-family: monospace;
  font-size: var(--font-size-sm);
  color: var(--text-primary);
  flex: 1;
}

.connection-status {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: var(--spacing-md);
}

.status-indicator {
  display: flex;
  align-items: center;
  gap: var(--spacing-sm);
}

.status-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--text-tertiary);

  &.connected {
    background: var(--success-color);
  }

  &.connecting {
    background: var(--warning-color);
  }

  &.error {
    background: var(--error-color);
  }
}

.status-text {
  font-size: var(--font-size-sm);
  color: var(--text-secondary);
}

.token-timestamps {
  display: flex;
  justify-content: space-between;
  gap: var(--spacing-sm);
}

.timestamp-item {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-xs);
}

.timestamp-label {
  font-size: var(--font-size-xs);
  color: var(--text-tertiary);
}

.timestamp-value {
  font-size: var(--font-size-xs);
  color: var(--text-secondary);
}

.card-footer {
  border-top: 1px solid var(--border-light);
  padding-top: var(--spacing-md);
}

.empty-state {
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 400px;
  background: var(--bg-primary);
  border-radius: var(--border-radius-xl);
  box-shadow: var(--shadow-medium);
}

// 批量删除弹窗样式
.batch-delete-content {
  .select-all-row {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 12px 0;
    border-bottom: 1px solid var(--border-light);
    margin-bottom: 12px;
    
    .search-hint {
      display: flex;
      align-items: center;
      margin-top: 4px;
      font-size: 11px;
      color: #18a058;
      font-weight: 500;
    }
  }
  
  .token-delete-list {
    display: flex;
    flex-direction: column;
    gap: 4px;
  }
  
  .token-delete-item {
    padding: 8px 10px;
    background: var(--bg-tertiary);
    border-radius: var(--border-radius-small);
    border: 2px solid transparent;
    transition: all 0.3s ease;
    
    &:hover {
      background: var(--bg-secondary);
      transform: translateX(2px);
    }
    
    // 被选中的项 - 淡红色主题
    &.selected {
      background: rgba(244, 67, 54, 0.08);
      border-color: rgba(244, 67, 54, 0.3);
      
      .token-delete-name {
        color: #d32f2f;
      }
      
      .token-delete-meta {
        color: rgba(211, 47, 47, 0.7);
      }
      
      :deep(.n-icon) {
        color: #d32f2f;
      }
      
      &:hover {
        background: rgba(244, 67, 54, 0.12);
        border-color: rgba(244, 67, 54, 0.4);
      }
    }
    
    // 未被选中的项（当有选中项时）
    &.not-selected {
      opacity: 0.9;
      
      &:hover {
        opacity: 0.95;
      }
    }
    
    :deep(.n-checkbox) {
      width: 100%;
      
      .n-checkbox__label {
        width: 100%;
      }
    }
  }
  
  .token-delete-info {
    display: flex;
    align-items: center;
    gap: 12px;
    width: 100%;
  }
  
  .token-delete-name {
    display: flex;
    align-items: center;
    font-size: 14px;
    font-weight: 500;
    color: var(--text-primary);
    min-width: 0;
    flex: 1;
  }
  
  .token-delete-meta {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    font-size: 11px;
    color: var(--text-secondary);
    
    .meta-item {
      display: flex;
      align-items: center;
      gap: 3px;
      white-space: nowrap;
    }
  }
}

.modal-actions {
  display: flex;
  gap: var(--spacing-md);
  justify-content: flex-end;
}

@media (max-width: 768px) {
  .container {
    padding: 0 12px; /* 减小移动端内边距 */
  }

  .tokens-grid {
    grid-template-columns: 1fr;
  }
  
  .token-import-page {
    padding: 0; /* 移动端也移除padding，让导航栏顶格 */
  }

  /* 移动端：导入方式按钮优化 */
  .import-method-tabs {
    flex-wrap: wrap;
    gap: 8px;
    
    /* 移动端显示简短文字 */
    .btn-text-full {
      display: none !important;
    }
    
    .btn-text-short {
      display: inline !important;
    }
    
    :deep(.n-radio-button) {
      font-size: 13px !important;
      
      .n-radio-button__label {
        padding: 4px 12px !important;
      }
    }
  }
  
  /* 移动端：批量删除搜索框 */
  .batch-delete-content {
    .select-all-row {
      flex-wrap: wrap;
      
      > div {
        max-width: 100% !important;
        order: 2;
        width: 100%;
      }
      
      .n-checkbox {
        order: 1;
      }
    }
  }

  /* 移动端：按钮垂直排列 */
  .upload-buttons-wrapper {
    flex-direction: column;
    gap: 8px;
    margin-bottom: 12px;
  }
  
  .file-upload-container,
  .folder-batch-upload {
    width: 100%;
    flex: none; /* 移除flex属性 */
    min-width: auto; /* 移除最小宽度限制 */
  }

  .optional-fields {
    grid-template-columns: 1fr;
  }

  .section-header {
    flex-direction: column;
    gap: var(--spacing-md);
    align-items: stretch;
  }

  .token-timestamps {
    flex-direction: column;
  }
}

/* 原始Token数据查看模态框样式 */
.raw-data-modal {
  .raw-data-content {
    max-height: 60vh;
    overflow: auto;
  }
  
  .token-info-header {
    background-color: var(--bg-secondary);
    padding: var(--spacing-md);
    border-radius: var(--border-radius-medium);
    margin-bottom: var(--spacing-lg);
    
    h3 {
      margin: 0 0 var(--spacing-md) 0;
      color: var(--text-primary);
    }
    
    p {
      margin: var(--spacing-xs) 0;
      color: var(--text-secondary);
    }
    
    .label {
      font-weight: var(--font-weight-medium);
      color: var(--text-primary);
    }
  }
  
  .raw-data-display {
    background-color: var(--bg-secondary);
    padding: var(--spacing-md);
    border-radius: var(--border-radius-medium);
    max-height: 40vh;
    overflow: auto;
    
    pre {
      margin: 0;
      font-family: var(--font-family-mono);
      font-size: var(--font-size-sm);
      line-height: 1.5;
      color: var(--text-primary);
    }
  }
}

/* 本地存储的bin文件管理样式 */
.stored-bin-files {
  .empty-state {
    text-align: center;
    padding: var(--spacing-lg);
    color: var(--text-tertiary);
  }
  
  .bin-files-list {
    display: flex;
    flex-direction: column;
    gap: var(--spacing-md);
  }
  
  .bin-file-item {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: var(--spacing-md);
    background-color: var(--bg-secondary);
    border-radius: var(--border-radius-medium);
    border: 1px solid var(--border-light);
    
    &:hover {
      border-color: var(--primary-color);
    }
  }
  
  .bin-file-info {
    flex: 1;
    
    h4 {
      margin: 0 0 var(--spacing-xs) 0;
      color: var(--text-primary);
      font-size: var(--font-size-md);
    }
    
    p {
      margin: var(--spacing-xs) 0;
      color: var(--text-secondary);
      font-size: var(--font-size-sm);
    }
  }
  
  .bin-file-actions {
    display: flex;
    gap: var(--spacing-sm);
  }
}

/* 手机端批量上传模态框样式 */
.mobile-batch-upload-modal {
  .mobile-upload-instructions {
    margin-bottom: var(--spacing-lg);
    
    .instruction-steps {
      margin-top: var(--spacing-md);
      
      p {
        margin: var(--spacing-xs) 0;
        font-size: var(--font-size-sm);
        color: var(--text-secondary);
      }
    }
  }
  
  .mobile-upload-form {
    margin-bottom: var(--spacing-lg);
    
    .selected-file-info {
      margin-top: var(--spacing-sm);
      padding: var(--spacing-sm);
      background-color: var(--bg-tertiary);
      border-radius: var(--border-radius-small);
      
      p {
        margin: 0;
        font-size: var(--font-size-sm);
        color: var(--text-secondary);
      }
    }
  }
  
  .mobile-batch-files-list {
    margin-bottom: var(--spacing-lg);
    
    h3 {
      margin: 0 0 var(--spacing-md) 0;
      color: var(--text-primary);
      font-size: var(--font-size-md);
    }
    
    .batch-files-container {
      max-height: 200px;
      overflow-y: auto;
      border: 1px solid var(--border-light);
      border-radius: var(--border-radius-medium);
      padding: var(--spacing-sm);
    }
    
    .batch-file-item {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: var(--spacing-sm);
      margin-bottom: var(--spacing-xs);
      background-color: var(--bg-secondary);
      border-radius: var(--border-radius-small);
      
      &:last-child {
        margin-bottom: 0;
      }
      
      .file-info {
        flex: 1;
        
        .file-name {
          display: block;
          font-size: var(--font-size-sm);
          color: var(--text-primary);
          margin-bottom: var(--spacing-xs);
        }
        
        .role-name {
          display: block;
          font-size: var(--font-size-xs);
          color: var(--text-secondary);
        }
      }
    }
  }
}

/* 文件夹批量上传模态框样式 */
.folder-batch-upload-modal {
  .folder-upload-instructions {
    margin-bottom: var(--spacing-lg);
    
    .instruction-steps {
      margin-top: var(--spacing-md);
      
      p {
        margin: var(--spacing-xs) 0;
        font-size: var(--font-size-sm);
        color: var(--text-secondary);
      }
    }
  }
  
  .folder-upload-form {
    margin-bottom: var(--spacing-lg);
    
    .selected-folder-info {
      margin-top: var(--spacing-sm);
      padding: var(--spacing-sm);
      background-color: var(--bg-tertiary);
      border-radius: var(--border-radius-small);
      
      p {
        margin: 0;
        font-size: var(--font-size-sm);
        color: var(--text-secondary);
      }
    }
  }
  
  .folder-batch-files-list {
    margin-bottom: var(--spacing-lg);
    
    h3 {
      margin: 0 0 var(--spacing-md) 0;
      color: var(--text-primary);
      font-size: var(--font-size-md);
    }
    
    .batch-files-container {
      max-height: 200px;
      overflow-y: auto;
      border: 1px solid var(--border-light);
      border-radius: var(--border-radius-medium);
      padding: var(--spacing-sm);
    }
    
    .batch-file-item {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: var(--spacing-sm);
      margin-bottom: var(--spacing-xs);
      background-color: var(--bg-secondary);
      border-radius: var(--border-radius-small);
      
      &:last-child {
        margin-bottom: 0;
      }
      
      .file-info {
        flex: 1;
        
        .file-name {
          display: block;
          font-size: var(--font-size-sm);
          color: var(--text-primary);
          margin-bottom: var(--spacing-xs);
        }
        
        .role-name {
          display: block;
          font-size: var(--font-size-xs);
          color: var(--text-secondary);
        }
      }
    }
  }
}

/* 批量任务面板 */
.batch-task-section {
  margin-bottom: 24px;
}

/* 进度列表 */
.progress-section {
  margin-bottom: 16px; /* 减小底部间距 */
  
  .progress-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 12px; /* 减小间距 */
    
    /* 🚀 v3.13.5.5: 标题和按钮容器 */
    .progress-title {
      display: flex;
      align-items: center;
      gap: 12px;
    }
    
    h2 {
      color: white;
      font-size: 20px;
      text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
      margin: 0;
    }
  }
  
  /* 🚀 v3.13.5.5: 隐藏卡片时的提示信息样式（极致紧凑版） */
  .progress-summary {
    padding: 8px 0;
  }
  
  // 100并发优化：虚拟滚动容器样式（支持网格布局）
  .progress-grid {
    height: calc(100vh - 300px); // 固定高度，用于虚拟滚动
    min-height: 400px;
    max-height: 800px;
    // 不设置overflow，让虚拟滚动组件自己处理
    
    // 内部卡片使用网格布局（恢复响应式多列）
    :deep(.virtual-scroll-content) {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
      gap: 12px;
      padding: 0 2px; // 防止阴影被裁剪
    }
  }
  
  /* 响应式布局：7个不同档次（虚拟滚动优化版本） */
  @media (min-width: 2400px) {
    .progress-grid :deep(.virtual-scroll-content) {
      grid-template-columns: repeat(7, 1fr); /* 超宽屏：7列 */
    }
  }
  
  @media (min-width: 2000px) and (max-width: 2399px) {
    .progress-grid :deep(.virtual-scroll-content) {
      grid-template-columns: repeat(6, 1fr); /* 宽屏：6列 */
    }
  }
  
  @media (min-width: 1600px) and (max-width: 1999px) {
    .progress-grid :deep(.virtual-scroll-content) {
      grid-template-columns: repeat(5, 1fr); /* 大屏：5列 */
    }
  }
  
  @media (min-width: 1280px) and (max-width: 1599px) {
    .progress-grid :deep(.virtual-scroll-content) {
      grid-template-columns: repeat(4, 1fr); /* 中屏：4列 */
    }
  }
  
  @media (min-width: 1024px) and (max-width: 1279px) {
    .progress-grid :deep(.virtual-scroll-content) {
      grid-template-columns: repeat(3, 1fr); /* 小屏：3列 */
    }
  }
  
  @media (min-width: 768px) and (max-width: 1023px) {
    .progress-grid :deep(.virtual-scroll-content) {
      grid-template-columns: repeat(2, 1fr); /* 平板：2列 */
    }
  }
  
  @media (max-width: 767px) {
    .progress-grid :deep(.virtual-scroll-content) {
      grid-template-columns: 1fr; /* 手机：1列 */
    }
  }
}

/* 针对更小屏幕（小于400px）的额外优化 */
@media (max-width: 400px) {
  .import-method-tabs {
    :deep(.n-radio-button) {
      font-size: 12px !important;
      
      .n-radio-button__label {
        padding: 3px 8px !important;
      }
    }
  }
  
  .card-header {
    h2 {
      font-size: 18px !important;
    }
  }
}

/* 深色主题下的批量删除样式 */
html.dark,
html[data-theme="dark"] {
  .batch-delete-content {
    .search-hint {
      color: #4ade80 !important;
    }
    
    .token-delete-item {
      &.selected {
        background: rgba(244, 67, 54, 0.15);
        border-color: rgba(244, 67, 54, 0.4);
        
        .token-delete-name {
          color: #ef5350;
        }
        
        .token-delete-meta {
          color: rgba(239, 83, 80, 0.8);
        }
        
        :deep(.n-icon) {
          color: #ef5350;
        }
        
        &:hover {
          background: rgba(244, 67, 54, 0.2);
          border-color: rgba(244, 67, 54, 0.5);
        }
      }
    }
  }
}
</style>
