<template>
  <div class="vm-detail-page">
    <!-- 顶部面包屑 + 返回 -->
    <div class="page-header">
      <div class="header-left">
        <el-button @click="goBack" link class="back-btn"><el-icon><ArrowLeft /></el-icon> 返回虚拟机列表</el-button>
      </div>
      <div class="header-right">
        <el-button plain @click="goMigrationRecords">迁移记录</el-button>
        <el-button plain :icon="Refresh" @click="loadDetail" :loading="loading">刷新</el-button>
      </div>
    </div>

    <div class="main-content" v-loading="loading">
      <!-- 实例概览栏 -->
      <div class="instance-overview" v-if="detail">
        <div class="overview-left">
          <h2 class="instance-name">{{ detail.name || '-' }} <span class="instance-id">{{ detail.id }}</span></h2>
        </div>
        <div class="overview-actions">
          <el-button type="primary" @click="handleGetVnc">远程连接</el-button>
          <el-button type="success" @click="handleSshConnect" :disabled="!publicIpList.length || isWindows">SSH连接</el-button>
          <el-button type="danger" @click="handlePower('stop')" :disabled="detail.status === 'stopped' || detail.status === 'stop' || isMigrating">关机</el-button>
          <el-dropdown trigger="click" @command="handleMoreCommand">
            <el-button>更多 <el-icon class="el-icon--right"><ArrowDown /></el-icon></el-button>
            <template #dropdown>
              <el-dropdown-menu>
                <el-dropdown-item command="start" :disabled="detail.status === 'running'">启动</el-dropdown-item>
                <el-dropdown-item command="reboot">重启</el-dropdown-item>
                <el-dropdown-item command="suspend">暂停</el-dropdown-item>
                <el-dropdown-item command="resume" v-if="detail.status === 'paused'">恢复</el-dropdown-item>
                <el-dropdown-item divided command="editVm" :disabled="isMigrating">修改虚拟机</el-dropdown-item>
                <el-dropdown-item command="refactorVm" :disabled="isMigrating">重构虚拟机</el-dropdown-item>
                <el-dropdown-item command="updateTraffic" :disabled="isMigrating">修改带宽</el-dropdown-item>
                <el-dropdown-item command="resetMac" :disabled="isMigrating">重置MAC地址</el-dropdown-item>
                <el-dropdown-item command="disconnectNetwork" :disabled="isMigrating || detail.network_disabled">断网</el-dropdown-item>
                <el-dropdown-item command="connectNetwork" :disabled="isMigrating || !detail.network_disabled">恢复网络</el-dropdown-item>
                <el-dropdown-item divided command="rebuild" :disabled="isMigrating">重装虚拟机</el-dropdown-item>
                <el-dropdown-item command="rescue">救援模式</el-dropdown-item>
                <el-dropdown-item command="exitRescue">退出救援</el-dropdown-item>
                <el-dropdown-item divided command="migrateVm" :disabled="isMigrating">迁移虚拟机</el-dropdown-item>
                <el-dropdown-item command="dataMigrateVm" :disabled="isMigrating">数据迁移</el-dropdown-item>
                <el-dropdown-item command="dataMigrateProgress" v-if="isMigrating">查看迁移进度</el-dropdown-item>
                <el-dropdown-item command="abortDataMigrate" v-if="isMigrating" style="color:#f56c6c">中断迁移</el-dropdown-item>
              </el-dropdown-menu>
            </template>
          </el-dropdown>
        </div>
      </div>

      <!-- 状态概览条 -->
      <div class="status-bar" v-if="detail">
        <div class="status-item">
          <span class="status-label">实例状态</span>
          <span class="status-value">
            <span class="status-dot" :class="detail.status === 'running' ? 'dot-running' : 'dot-other'"></span>
            {{ vmStatusLabel(detail.status) }}
            <el-tag v-if="detail.network_disabled" type="danger" size="small" effect="dark" style="margin-left:8px">已断网</el-tag>
            <el-tag v-if="isMigrating" type="warning" size="small" effect="dark" style="margin-left:8px">迁移中</el-tag>
          </span>
        </div>
        <div class="status-item">
          <span class="status-label">IP地址</span>
          <span class="status-value" v-if="publicIpList.length || privateIpList.length">
            <span class="ip-main">{{ (publicIpList[0] || privateIpList[0]) }}</span>
            <el-popover v-if="publicIpList.length + privateIpList.length > 1" trigger="hover" placement="bottom" :width="360" popper-class="ip-popover-panel">
              <template #reference>
                <el-tag size="small" type="info" class="ip-more-tag">+{{ publicIpList.length + privateIpList.length - 1 }}</el-tag>
              </template>
              <div v-if="publicIpList.length" class="ip-popover-header">
                <span>公网IP（{{ publicIpList.length }}）</span>
                <el-button link type="primary" size="small" @click="copyAllIps(publicIpList)">复制全部</el-button>
              </div>
              <div class="ip-popover-list">
                <div v-for="(ip, idx) in publicIpList" :key="'pub'+idx" class="ip-popover-item">
                  <span class="ip-text">{{ ip }}</span>
                  <el-button link type="primary" size="small" class="ip-copy-btn" @click="copyText(ip)">复制</el-button>
                </div>
              </div>
              <template v-if="privateIpList.length">
                <div class="ip-popover-header" style="margin-top:8px">
                  <span>内网IP（{{ privateIpList.length }}）</span>
                  <el-button link type="primary" size="small" @click="copyAllIps(privateIpList)">复制全部</el-button>
                </div>
                <div class="ip-popover-list">
                  <div v-for="(ip, idx) in privateIpList" :key="'pri'+idx" class="ip-popover-item">
                    <span class="ip-text">{{ ip }}</span>
                    <el-button link type="primary" size="small" class="ip-copy-btn" @click="copyText(ip)">复制</el-button>
                  </div>
                </div>
              </template>
            </el-popover>
          </span>
          <span class="status-value" v-else>{{ detail.ips || '-' }}</span>
        </div>
        <div class="status-item">
          <span class="status-label">创建时间</span>
          <span class="status-value">{{ formatTimestamp(detail.created_at) }}</span>
        </div>
      </div>

      <!-- 数据迁移进度横幅 -->
      <div class="migrate-banner" v-if="isMigrating">
        <div class="migrate-banner-info">
          <el-icon class="migrate-spin"><Loading /></el-icon>
          <span class="migrate-banner-title">数据迁移进行中</span>
          <template v-if="dataMigrateProgressData">
            <span class="migrate-divider"></span>
            <el-tag :type="migrateStageType(dataMigrateProgressData.stage)" size="small" effect="dark">{{ migrateStageLabel(dataMigrateProgressData.stage) }}</el-tag>
            <span v-if="dataMigrateProgressData.progress != null" class="migrate-progress-text">{{ dataMigrateProgressData.progress }}%</span>
            <span v-if="dataMigrateProgressData.speed" class="migrate-speed">{{ dataMigrateProgressData.speed }}</span>
            <span v-if="dataMigrateProgressData.message" class="migrate-msg">{{ dataMigrateProgressData.message }}</span>
          </template>
          <span v-else class="migrate-msg">正在获取进度信息...</span>
        </div>
        <div class="migrate-banner-actions">
          <el-button size="small" @click="loadDataMigrateProgress" :loading="dataMigrateProgressLoading">
            <el-icon v-if="!dataMigrateProgressLoading"><Refresh /></el-icon>刷新进度
          </el-button>
          <el-button size="small" type="danger" plain @click="handleAbortMigrate" :loading="abortLoading">中断迁移</el-button>
        </div>
      </div>

      <!-- 标签页 -->
      <el-tabs v-model="activeTab" class="detail-tabs" v-if="detail">
        <el-tab-pane label="实例详情" name="info">
          <el-alert
            v-if="detail.pause_info"
            :title="detail.pause_info.reason_text || pauseReasonLabel(detail.pause_info.reason_code)"
            :type="detail.pause_info.reason_code === 'resource_threshold' ? 'warning' : 'info'"
            :description="formatPauseViolations(detail.pause_info)"
            show-icon :closable="false" style="margin-bottom: 16px"
          />
          <!-- 配置信息 -->
          <div class="section-block">
            <h3 class="section-title">配置信息</h3>
            <div class="config-grid">
              <div class="config-row">
                <div class="config-cell">
                  <span class="config-label">实例规格</span>
                  <span class="config-value spec-value">{{ detail.vcpu || '-' }}核vCPU | {{ formatMemory(detail.memory) }} | {{ detail.rx_bandwidth || 0 }} Mbps</span>
                </div>
                <div class="config-cell">
                  <span class="config-label">操作系统</span>
                  <span class="config-value">{{ vmImage?.name || '-' }}</span>
                </div>
                <div class="config-cell">
                  <span class="config-label">镜像类型</span>
                  <span class="config-value">
                    <el-tag v-if="vmImage" :type="vmImage.os_type === 'linux' ? 'success' : 'primary'" size="small">{{ vmImage.os_type || '-' }}</el-tag>
                    <span v-else>-</span>
                  </span>
                </div>
                <div class="config-cell">
                  <span class="config-label">资源阈值</span>
                  <span class="config-value threshold-summary">
                    <template v-for="item in thresholdResources" :key="item.key">
                      <el-tag v-if="isThresholdEnabled(resourceThresholdPolicy, item.key)" size="small" type="warning">
                        {{ item.label }} {{ formatThreshold(resourceThresholdPolicy, item.key) }}
                      </el-tag>
                    </template>
                    <span v-if="!hasEnabledThreshold" style="color:#909399">全部关闭</span>
                    <el-button link type="primary" size="small" @click="openThresholdDialog" :disabled="isMigrating">配置</el-button>
                  </span>
                </div>
              </div>
              <div class="config-row">
                <div class="config-cell">
                  <span class="config-label">公网IP</span>
                  <span class="config-value ip-value" v-if="publicIpList.length">
                    <span class="ip-main" style="color:#165dff">{{ publicIpList[0] }}</span>
                    <el-popover v-if="publicIpList.length > 1" trigger="hover" placement="bottom" :width="360" popper-class="ip-popover-panel">
                      <template #reference>
                        <el-tag size="small" type="primary" class="ip-more-tag">+{{ publicIpList.length - 1 }}</el-tag>
                      </template>
                      <div class="ip-popover-header">
                        <span>全部公网IP（{{ publicIpList.length }}）</span>
                        <el-button link type="primary" size="small" @click="copyAllIps(publicIpList)">复制全部</el-button>
                      </div>
                      <div class="ip-popover-list">
                        <div v-for="(ip, idx) in publicIpList" :key="idx" class="ip-popover-item">
                          <span class="ip-text">{{ ip }}</span>
                          <el-button link type="primary" size="small" class="ip-copy-btn" @click="copyText(ip)">复制</el-button>
                        </div>
                      </div>
                    </el-popover>
                  </span>
                  <span class="config-value ip-value" v-else>暂无</span>
                </div>
                <div class="config-cell">
                  <span class="config-label">内网IP</span>
                  <span class="config-value ip-value" v-if="privateIpList.length">
                    <span class="ip-main" style="color:#67c23a">{{ privateIpList[0] }}</span>
                    <el-popover v-if="privateIpList.length > 1" trigger="hover" placement="bottom" :width="360" popper-class="ip-popover-panel">
                      <template #reference>
                        <el-tag size="small" type="success" class="ip-more-tag">+{{ privateIpList.length - 1 }}</el-tag>
                      </template>
                      <div class="ip-popover-header">
                        <span>全部内网IP（{{ privateIpList.length }}）</span>
                        <el-button link type="primary" size="small" @click="copyAllIps(privateIpList)">复制全部</el-button>
                      </div>
                      <div class="ip-popover-list">
                        <div v-for="(ip, idx) in privateIpList" :key="idx" class="ip-popover-item">
                          <span class="ip-text">{{ ip }}</span>
                          <el-button link type="primary" size="small" class="ip-copy-btn" @click="copyText(ip)">复制</el-button>
                        </div>
                      </div>
                    </el-popover>
                  </span>
                  <span class="config-value ip-value" v-else>暂无</span>
                </div>
                <div class="config-cell">
                  <span class="config-label">下行/上行带宽</span>
                  <span class="config-value">{{ detail.rx_bandwidth || 0 }} / {{ detail.tx_bandwidth || 0 }} Mbps</span>
                </div>
              </div>
              <div class="config-row">
                <div class="config-cell">
                  <span class="config-label">安全组</span>
                  <span class="config-value">
                    <template v-if="vmPortGroup || vmOutPortGroup">
                      <el-tag v-if="vmPortGroup" size="small" type="success" style="margin-right: 4px">入站: {{ vmPortGroup.name }}</el-tag>
                      <el-tag v-if="vmOutPortGroup" size="small" type="warning">出站: {{ vmOutPortGroup.name }}</el-tag>
                    </template>
                    <span v-else style="color: #909399">未绑定</span>
                  </span>
                </div>
              </div>
              <div class="config-row">
                <div class="config-cell">
                  <span class="config-label">用户名</span>
                  <span class="config-value" style="font-weight:500">{{ isWindows ? 'Administrator' : 'root' }}</span>
                </div>
                <div class="config-cell">
                  <span class="config-label">远程端口</span>
                  <span class="config-value">{{ isWindows ? (detail.ssh_port && detail.ssh_port !== 22 ? detail.ssh_port : 3389) : (detail.ssh_port || 22) }}</span>
                </div>
                <div class="config-cell">
                  <span class="config-label">密码</span>
                  <span class="config-value password-cell">
                    <code>{{ showPassword ? (detail.root_password || '-') : '••••••••' }}</code>
                    <el-button link type="primary" size="small" @click="showPassword = !showPassword">{{ showPassword ? '隐藏' : '显示' }}</el-button>
                    <el-button link type="primary" size="small" @click="copyText(detail.root_password)">复制</el-button>
                  </span>
                </div>
              </div>
              <div class="config-row">
                <div class="config-cell">
                  <span class="config-label">流量</span>
                  <span class="config-value traffic-cell">
                    <span class="traffic-main">{{ formatTrafficMb(detailTrafficUsedMb) }} / {{ formatTrafficMb(detailTrafficTotalMb) }}</span>
                    <span class="traffic-sub">
                      基础 {{ formatTrafficMb(detailTrafficBaseMb) }}
                      <template v-if="detailTrafficTempMb > 0">
                        · 临时 {{ formatTrafficMb(detailTrafficTempMb) }}
                        <span v-if="detailTrafficCycleStartText" class="traffic-cycle">（周期 {{ detailTrafficCycleStartText }}）</span>
                      </template>
                    </span>
                    <span class="traffic-actions">
                      <el-button link type="primary" size="small" class="cfg-edit-btn" @click="openVmTrafficPolicyDialog" :disabled="isMigrating">
                        <el-icon :size="14"><Edit /></el-icon>修改
                      </el-button>
                      <el-button link type="warning" size="small" class="cfg-edit-btn" @click="openVmAddTrafficDialog('temporary')" :disabled="isMigrating">加临时</el-button>
                    </span>
                  </span>
                </div>
                <div class="config-cell">
                  <span class="config-label">快照配额</span>
                  <span class="config-value">{{ detail.snapshot_num ?? '-' }}</span>
                </div>
                <div class="config-cell">
                  <span class="config-label">备份配额</span>
                  <span class="config-value">{{ detail.backup_num ?? '-' }}</span>
                </div>
              </div>
              <div class="config-row">
                <div class="config-cell" style="flex: 2">
                  <span class="config-label">UUID</span>
                  <span class="config-value mono-text">{{ detail.uuid || '-' }}</span>
                </div>
                <div class="config-cell">
                  <span class="config-label">更新时间</span>
                  <span class="config-value">{{ formatTimestamp(detail.updated_at) }}</span>
                </div>
              </div>
            </div>
          </div>

        </el-tab-pane>

        <el-tab-pane label="网络管理" name="network">
          <div class="section-block">
            <div class="section-header">
              <h3 class="section-title">网络列表</h3>
              <div style="display: flex; gap: 8px">
                <el-button size="small" type="primary" @click="showNetBindBridgeSelector = true">绑定外网</el-button>
                <el-button size="small" type="success" @click="showNetBindNatSelector = true">绑定内网</el-button>
                <el-button size="small" :icon="Refresh" @click="loadDetail">刷新</el-button>
              </div>
            </div>
            <el-table :data="pagedNetworks" size="small" stripe>
              <el-table-column prop="id" label="ID" width="60" />
              <el-table-column prop="name" label="名称" min-width="100" />
              <el-table-column prop="address" label="地址(CIDR)" min-width="140" show-overflow-tooltip />
              <el-table-column prop="gateway" label="网关" min-width="120" />
              <el-table-column prop="nameservers" label="DNS" min-width="140" show-overflow-tooltip />
              <el-table-column prop="mac_address" label="MAC地址" min-width="150" show-overflow-tooltip />
              <el-table-column label="主IP" width="70" align="center">
                <template #default="{ row }">
                  <el-tag v-if="row.is_primary" type="success" size="small" effect="dark">主</el-tag>
                </template>
              </el-table-column>
              <el-table-column label="类型" width="80">
                <template #default="{ row }">
                  <el-tag :type="row.type === 'bridge' ? 'success' : 'warning'" size="small">{{ row.type === 'bridge' ? '网桥' : 'NAT' }}</el-tag>
                </template>
              </el-table-column>
              <el-table-column prop="bridge_name" label="网桥" width="100" />
              <el-table-column prop="target_device" label="目标设备" width="100" show-overflow-tooltip />
              <el-table-column label="操作" width="240" fixed="right">
                <template #default="{ row }">
                  <el-button link type="primary" size="small" @click="handleNetDetail(row)">详情</el-button>
                  <el-button link type="primary" size="small" @click="handleNetEdit(row)">编辑</el-button>
                  <el-button v-if="!row.is_primary" link type="warning" size="small" @click="handleSetPrimary(row)">设为主IP</el-button>
                  <el-button link type="danger" size="small" @click="handleNetDelete(row)">解绑</el-button>
                </template>
              </el-table-column>
            </el-table>
            <el-empty v-if="!vmNetworks.length" description="暂无网络" :image-size="60" />
            <div class="pagination-wrapper" v-if="vmNetworks.length > 0">
              <el-pagination v-model:current-page="networkPage" v-model:page-size="networkPageSize"
                :page-sizes="[10, 20, 50]" :total="vmNetworks.length" layout="total, sizes, prev, pager, next" small
                @size-change="s => { networkPageSize = s; networkPage = 1 }"
                @current-change="p => { networkPage = p }" />
            </div>
          </div>
        </el-tab-pane>

        <el-tab-pane label="数据卷管理" name="volume">
          <div class="section-block">
            <div class="section-header">
              <h3 class="section-title">数据卷列表</h3>
              <div style="display: flex; gap: 8px">
                <el-button size="small" type="primary" @click="showVolSelector = true">绑定数据卷</el-button>
                <el-button size="small" :icon="Refresh" @click="loadDetail">刷新</el-button>
              </div>
            </div>
            <el-table :data="pagedVolumes" size="small" stripe>
              <el-table-column prop="id" label="ID" width="60" />
              <el-table-column prop="name" label="名称" min-width="120" />
              <el-table-column label="大小" width="80">
                <template #default="{ row }">{{ row.size ? row.size + ' GB' : '-' }}</template>
              </el-table-column>
              <el-table-column label="类型" width="80">
                <template #default="{ row }">
                  <el-tag :type="row.is_system ? 'danger' : ''" size="small">{{ row.is_system ? '系统盘' : '数据盘' }}</el-tag>
                </template>
              </el-table-column>
              <el-table-column label="状态" width="90">
                <template #default="{ row }">
                  <el-tag :type="volumeStatusType(row.status)" size="small">{{ volumeStatusLabel(row.status) }}</el-tag>
                </template>
              </el-table-column>
              <el-table-column label="挂载" width="90">
                <template #default="{ row }">
                  <el-tag :type="isVolumeMounted(row) ? 'success' : 'info'" size="small">{{ isVolumeMounted(row) ? '已挂载' : '未挂载' }}</el-tag>
                </template>
              </el-table-column>
              <el-table-column prop="path" label="路径" min-width="160" show-overflow-tooltip>
                <template #default="{ row }"><span class="mono-text">{{ row.path || '-' }}</span></template>
              </el-table-column>
              <el-table-column label="操作" width="300" fixed="right">
                <template #default="{ row }">
                  <el-button link type="primary" size="small" @click="handleVolDetail(row)">详情</el-button>
                  <el-button link type="primary" size="small" @click="handleVolResize(row)">调整大小</el-button>
                  <el-button link type="success" size="small" @click="handleVolMount(row)" v-if="!isVolumeMounted(row)">挂载</el-button>
                  <el-button link type="warning" size="small" @click="handleVolUnmount(row)" v-if="isVolumeMounted(row)">卸载</el-button>
                  <el-button link type="info" size="small" @click="handleVolTransfer(row)">迁移</el-button>
                  <el-button link type="danger" size="small" @click="handleVolDelete(row)">删除</el-button>
                </template>
              </el-table-column>
            </el-table>
            <el-empty v-if="!vmVolumes.length" description="暂无磁盘卷" :image-size="60" />
            <div class="pagination-wrapper" v-if="vmVolumes.length > 0">
              <el-pagination v-model:current-page="volumePage" v-model:page-size="volumePageSize"
                :page-sizes="[10, 20, 50]" :total="vmVolumes.length" layout="total, sizes, prev, pager, next" small
                @size-change="s => { volumePageSize = s; volumePage = 1 }"
                @current-change="p => { volumePage = p }" />
            </div>
          </div>
        </el-tab-pane>

        <el-tab-pane label="安全组管理" name="security">
          <div class="section-block">
            <div class="section-header">
              <h3 class="section-title">安全组管理</h3>
              <div style="display: flex; gap: 8px">
                <el-button size="small" type="primary" @click="handleSgCreate"><el-icon><Plus /></el-icon>创建安全组</el-button>
                <el-button size="small" @click="handleSgBind">绑定安全组</el-button>
                <el-button size="small" :icon="Refresh" @click="async () => { await loadDetail(); loadSgLockInfo() }">刷新</el-button>
              </div>
            </div>
            <el-table :data="pagedSecurityGroups" size="small" stripe>
              <el-table-column prop="id" label="ID" width="70" />
              <el-table-column prop="name" label="名称" min-width="140" show-overflow-tooltip />
              <el-table-column label="锁定" width="80">
                <template #default="{ row }">
                  <el-tag :type="row.lock ? 'danger' : 'success'" size="small">{{ row.lock ? '是' : '否' }}</el-tag>
                </template>
              </el-table-column>
              <el-table-column label="白名单" width="80">
                <template #default="{ row }">
                  <el-tag :type="row.drop_all ? 'warning' : 'info'" size="small">{{ row.drop_all ? '开启' : '关闭' }}</el-tag>
                </template>
              </el-table-column>
              <el-table-column label="方向" width="80">
                <template #default="{ row }">
                  <el-tag :type="row.direction === 'in' ? 'success' : 'warning'" size="small">{{ row.direction === 'in' ? '入站' : row.direction === 'out' ? '出站' : (row.direction || '-') }}</el-tag>
                </template>
              </el-table-column>
              <el-table-column label="操作" width="280" fixed="right">
                <template #default="{ row }">
                  <el-button link type="primary" size="small" @click="handleSgGoDetail(row)">编辑</el-button>
                  <el-button link type="success" size="small" @click="handleSgSync(row)">同步</el-button>
                  <el-button link type="warning" size="small" @click="handleSgApply(row)">应用</el-button>
                  <el-dropdown trigger="click" @command="cmd => handleSgRowMore(row, cmd)" style="margin-left: 4px; margin-top: 4.5px">
                    <el-button link type="info" size="small">更多<el-icon class="el-icon--right"><ArrowDown /></el-icon></el-button>
                    <template #dropdown>
                      <el-dropdown-menu>
                        <el-dropdown-item command="unbindCurrent" style="color: #e6a23c">解绑当前虚拟机</el-dropdown-item>
                        <el-dropdown-item command="bindVm" divided>绑定虚拟机</el-dropdown-item>
                        <el-dropdown-item command="unbindVm">解绑虚拟机</el-dropdown-item>
                        <el-dropdown-item command="whitelist" divided>{{ row.drop_all ? '关闭白名单' : '开启白名单' }}</el-dropdown-item>
                        <el-dropdown-item command="viewDetail">查看详情</el-dropdown-item>
                        <el-dropdown-item command="delete" divided style="color: #f56c6c">删除</el-dropdown-item>
                      </el-dropdown-menu>
                    </template>
                  </el-dropdown>
                </template>
              </el-table-column>
            </el-table>
            <el-empty v-if="!sgTableData.length" description="暂无绑定的安全组" :image-size="60" />

            <div class="pagination-wrapper" v-if="sgTableData.length > 0">
              <el-pagination
                v-model:current-page="sgPage"
                v-model:page-size="sgPageSize"
                :page-size="[10,20,50]"
                :total="sgTableData.length"
                layout="total,sizes,prev,pager,next"
                small
                @size-change="s => {sgPageSize = s; sgPage = 1}"
                @current-change="p => {sgPage = p}"
              >
              </el-pagination>
            </div>
          </div>
        </el-tab-pane>

        <el-tab-pane label="快照" name="snapshot">
          <div class="section-block">
            <div class="section-header">
              <h3 class="section-title">快照管理</h3>
              <div style="display: flex; align-items: center; gap: 8px">
                <el-tag v-if="snapshotQuota" size="small" effect="plain">{{ snapshotQuota.count }} / {{ snapshotQuota.limit }}</el-tag>
                <el-button size="small" @click="handleSetSnapshotLimit">设置上限</el-button>
                <el-button size="small" type="primary" @click="handleCreateSnapshot">创建快照</el-button>
                <el-button size="small" @click="loadSnapshots">刷新</el-button>
              </div>
            </div>
            <el-table :data="snapshotList" v-loading="snapshotLoading" stripe size="small" style="width: 100%">
              <el-table-column prop="id" label="ID" width="60" />
              <el-table-column prop="name" label="名称" min-width="140" show-overflow-tooltip />
              <el-table-column label="状态" width="90">
                <template #default="{ row }">
                  <el-tag :type="taskStatusType(row.status)" size="small">{{ snapshotStatusLabel(row.status) }}</el-tag>
                </template>
              </el-table-column>
              <el-table-column label="任务ID" min-width="160">
                <template #default="{ row }">
                  <span style="font-family: Consolas, monospace; font-size: 12px">{{ row.task_id || '-' }}</span>
                </template>
              </el-table-column>
              <el-table-column label="创建时间" width="170">
                <template #default="{ row }">{{ formatTimestamp(row.created_at) }}</template>
              </el-table-column>
              <el-table-column label="更新时间" width="170">
                <template #default="{ row }">{{ formatTimestamp(row.updated_at) }}</template>
              </el-table-column>
              <el-table-column label="操作" width="180" fixed="right">
                <template #default="{ row }">
                  <el-button link type="primary" size="small" @click="handleRestoreSnapshot(row)">恢复</el-button>
                  <el-button link type="info" size="small" @click="handleSnapshotProgress(row)" v-if="row.status === 'running' || row.status === 'pending'">进度</el-button>
                  <el-button link type="danger" size="small" @click="handleDeleteSnapshot(row)">删除</el-button>
                </template>
              </el-table-column>
            </el-table>
            <el-empty v-if="!snapshotList.length && !snapshotLoading" description="暂无快照" :image-size="60" />
            <div class="pagination-wrapper" v-if="snapshotTotal > 0">
              <el-pagination v-model:current-page="snapshotPage" v-model:page-size="snapshotPageSize"
                :page-sizes="[10, 20, 50]" :total="snapshotTotal" layout="total, sizes, prev, pager, next" small
                @size-change="s => { snapshotPageSize = s; snapshotPage = 1; loadSnapshots() }"
                @current-change="p => { snapshotPage = p; loadSnapshots() }" />
            </div>
          </div>
        </el-tab-pane>

        <el-tab-pane label="备份" name="backup">
          <div class="section-block">
            <div class="section-header">
              <h3 class="section-title">备份管理</h3>
              <div style="display: flex; align-items: center; gap: 8px">
                <el-tag v-if="backupQuota" size="small" effect="plain">{{ backupQuota.count }} / {{ backupQuota.limit }}</el-tag>
                <el-button size="small" @click="handleSetBackupLimit">设置上限</el-button>
                <el-button size="small" type="primary" @click="handleCreateBackup">创建备份</el-button>
                <el-button size="small" @click="loadBackups">刷新</el-button>
              </div>
            </div>
            <el-table :data="backupList" v-loading="backupLoading" stripe size="small" style="width: 100%">
              <el-table-column prop="id" label="ID" width="60" />
              <el-table-column prop="name" label="名称" min-width="140" show-overflow-tooltip />
              <el-table-column label="状态" width="90">
                <template #default="{ row }">
                  <el-tag :type="taskStatusType(row.status)" size="small">{{ snapshotStatusLabel(row.status) }}</el-tag>
                </template>
              </el-table-column>
              <el-table-column label="任务ID" min-width="160">
                <template #default="{ row }">
                  <span style="font-family: Consolas, monospace; font-size: 12px">{{ row.task_id || '-' }}</span>
                </template>
              </el-table-column>
              <el-table-column label="创建时间" width="170">
                <template #default="{ row }">{{ formatTimestamp(row.created_at) }}</template>
              </el-table-column>
              <el-table-column label="更新时间" width="170">
                <template #default="{ row }">{{ formatTimestamp(row.updated_at) }}</template>
              </el-table-column>
              <el-table-column label="操作" width="180" fixed="right">
                <template #default="{ row }">
                  <el-button link type="primary" size="small" @click="handleRestoreBackup(row)">恢复</el-button>
                  <el-button link type="info" size="small" @click="handleBackupProgress(row)" v-if="row.status === 'running' || row.status === 'pending'">进度</el-button>
                  <el-button link type="danger" size="small" @click="handleDeleteBackup(row)">删除</el-button>
                </template>
              </el-table-column>
            </el-table>
            <el-empty v-if="!backupList.length && !backupLoading" description="暂无备份" :image-size="60" />
            <div class="pagination-wrapper" v-if="backupTotal > 0">
              <el-pagination v-model:current-page="backupPage" v-model:page-size="backupPageSize"
                :page-sizes="[10, 20, 50]" :total="backupTotal" layout="total, sizes, prev, pager, next" small
                @size-change="s => { backupPageSize = s; backupPage = 1; loadBackups() }"
                @current-change="p => { backupPage = p; loadBackups() }" />
            </div>
          </div>
        </el-tab-pane>

        <el-tab-pane label="组网管理" name="userNetworking">
          <div class="section-block">
            <div class="section-header">
              <h3 class="section-title">用户组网</h3>
              <div style="display: flex; gap: 8px">
                <el-button size="small" type="primary" @click="handleVnCreate"><el-icon><Plus /></el-icon>创建组网</el-button>
                <el-button size="small" :icon="Refresh" @click="loadVmNetworkingList" :loading="vnLoading">刷新</el-button>
              </div>
            </div>
            <el-table :data="vnList" v-loading="vnLoading" stripe size="small">
              <el-table-column label="组网" min-width="130">
                <template #default="{ row }">{{ row.networking?.name || '-' }} <span style="color:#909399">(ID: {{ row.networking?.id }})</span></template>
              </el-table-column>
              <el-table-column label="状态" width="90">
                <template #default="{ row }">
                  <el-tag v-if="row.network" type="success" size="small">已加入</el-tag>
                  <el-tag v-else type="info" size="small">未加入</el-tag>
                </template>
              </el-table-column>
              <el-table-column label="网桥" width="130">
                <template #default="{ row }"><span class="mono-text">{{ row.network?.bridge_name || row.networking?.bridge_name || '-' }}</span></template>
              </el-table-column>
              <el-table-column label="IP地址" min-width="150">
                <template #default="{ row }">
                  <span v-if="row.network" class="mono-text">{{ row.network.address || '-' }}</span>
                  <span v-else style="color:#c0c4cc">-</span>
                </template>
              </el-table-column>
              <el-table-column label="网关" width="140">
                <template #default="{ row }">
                  <span v-if="row.network" class="mono-text">{{ row.network.gateway || '-' }}</span>
                  <span v-else class="mono-text" style="color:#c0c4cc">{{ row.networking?.gateway || '-' }}</span>
                </template>
              </el-table-column>
              <el-table-column label="MAC地址" min-width="160">
                <template #default="{ row }">
                  <span v-if="row.network" class="mono-text">{{ row.network.mac_address || '-' }}</span>
                  <span v-else style="color:#c0c4cc">-</span>
                </template>
              </el-table-column>
              <el-table-column label="类型" width="80">
                <template #default="{ row }">
                  <el-tag v-if="row.network" :type="row.network.type === 'bridge' ? 'success' : 'warning'" size="small">{{ row.network.type || '-' }}</el-tag>
                  <span v-else style="color:#c0c4cc">-</span>
                </template>
              </el-table-column>
              <el-table-column label="操作" width="120" fixed="right">
                <template #default="{ row }">
                  <el-button v-if="!row.network" link type="primary" size="small" @click="handleJoinNetworking(row)">加入</el-button>
                  <el-button v-else link type="danger" size="small" @click="handleLeaveNetworking(row)">移除</el-button>
                </template>
              </el-table-column>
            </el-table>
            <el-empty v-if="!vnList.length && !vnLoading" description="没有匹配的可用组网" :image-size="60" />
          </div>
        </el-tab-pane>

        <el-tab-pane label="监控" name="monitor">
          <div class="section-block">
            <div class="section-header">
              <h3 class="section-title">监控指标</h3>
              <div style="display: flex; align-items: center; gap: 8px; flex-wrap: wrap">
                <el-radio-group v-model="monitorTimeMode" size="small" @change="loadHistoricalMetrics">
                  <el-radio-button label="relative">最近</el-radio-button>
                  <el-radio-button label="fixed">自定义</el-radio-button>
                </el-radio-group>
                <el-select v-if="monitorTimeMode === 'relative'" v-model="monitorRelativeMinutes" size="small" style="width: 120px" @change="loadHistoricalMetrics">
                  <el-option label="10分钟" :value="10" />
                  <el-option label="30分钟" :value="30" />
                  <el-option label="1小时" :value="60" />
                  <el-option label="6小时" :value="360" />
                  <el-option label="12小时" :value="720" />
                  <el-option label="1天" :value="1440" />
                  <el-option label="7天" :value="10080" />
                </el-select>
                <el-date-picker
                  v-else
                  v-model="monitorDateRange"
                  type="datetimerange"
                  range-separator="至"
                  start-placeholder="开始"
                  end-placeholder="结束"
                  size="small"
                  style="width: 340px"
                  @change="loadHistoricalMetrics"
                />
                <span style="font-size:12px;color:#909399;white-space:nowrap">粒度: {{ currentIntervalLabel }}</span>
                <el-button size="small" :icon="Refresh" @click="loadHistoricalMetrics" :loading="historicalMetricsLoading">刷新</el-button>
              </div>
            </div>
            <template v-if="latestMetrics">
              <div class="metric-summary-row">
                <div class="metric-summary-card">
                  <div class="metric-summary-label">CPU 使用率</div>
                  <div class="metric-summary-value">{{ latestMetrics.cpu_usage?.toFixed(1) }}%</div>
                  <div class="metric-summary-sub">&nbsp;</div>
                </div>
                <div class="metric-summary-card">
                  <div class="metric-summary-label">内存</div>
                  <div class="metric-summary-value">{{ vmMemPercent(latestMetrics) }}%</div>
                  <div class="metric-summary-sub">{{ formatMemKB(latestMetrics.mem_used) }} / {{ formatMemKB(latestMetrics.mem_total) }}</div>
                </div>
                <div class="metric-summary-card">
                  <div class="metric-summary-label">磁盘 I/O</div>
                  <div class="metric-summary-value">读 {{ formatBytesRaw(latestMetrics.disk_read) }}</div>
                  <div class="metric-summary-sub">写 {{ formatBytesRaw(latestMetrics.disk_write) }}</div>
                </div>
                <div class="metric-summary-card">
                  <div class="metric-summary-label">网络流量</div>
                  <div class="metric-summary-value">↓{{ formatNetLabel(latestMetrics.net_rx) }}</div>
                  <div class="metric-summary-sub">↑{{ formatNetLabel(latestMetrics.net_tx) }}</div>
                </div>
                <div class="metric-summary-card">
                  <div class="metric-summary-label">累计流量</div>
                  <div class="metric-summary-value">{{ latestMetrics.traffic_used_mb != null ? latestMetrics.traffic_used_mb + ' MB' : '-' }}</div>
                  <div class="metric-summary-sub">&nbsp;</div>
                </div>
              </div>
            </template>
            <template v-if="historicalMetricsData">
              <el-row :gutter="16">
                <el-col :span="12">
                  <el-card shadow="hover" class="metrics-card">
                    <template #header><span class="metrics-title"><el-icon><Refresh /></el-icon> CPU 使用率</span></template>
                    <div ref="cpuChartRef" class="chart-container"></div>
                  </el-card>
                </el-col>
                <el-col :span="12">
                  <el-card shadow="hover" class="metrics-card">
                    <template #header><span class="metrics-title"><el-icon><Refresh /></el-icon> 内存使用</span></template>
                    <div ref="memChartRef" class="chart-container"></div>
                  </el-card>
                </el-col>
              </el-row>
              <el-row :gutter="16" style="margin-top: 16px">
                <el-col :span="12">
                  <el-card shadow="hover" class="metrics-card">
                    <template #header><span class="metrics-title"><el-icon><Refresh /></el-icon> 磁盘读写量</span></template>
                    <div ref="diskChartRef" class="chart-container"></div>
                  </el-card>
                </el-col>
                <el-col :span="12">
                  <el-card shadow="hover" class="metrics-card">
                    <template #header><span class="metrics-title"><el-icon><Refresh /></el-icon> 网络速率</span></template>
                    <div ref="netChartRef" class="chart-container"></div>
                  </el-card>
                </el-col>
              </el-row>
              <el-row :gutter="16" style="margin-top: 16px">
                <el-col :span="12">
                  <el-card shadow="hover" class="metrics-card">
                    <template #header><span class="metrics-title"><el-icon><Refresh /></el-icon> 磁盘IO速率</span></template>
                    <div ref="diskIopsChartRef" class="chart-container"></div>
                  </el-card>
                </el-col>
                <el-col :span="12">
                  <el-card shadow="hover" class="metrics-card">
                    <template #header><span class="metrics-title"><el-icon><Refresh /></el-icon> 流量使用趋势</span></template>
                    <div ref="trafficUsedChartRef" class="chart-container"></div>
                  </el-card>
                </el-col>
              </el-row>
            </template>
            <el-empty v-else-if="!historicalMetricsLoading" description="加载监控数据中..." :image-size="80" />
          </div>
        </el-tab-pane>

        <!-- 流量管理（合并流量策略 + 流量统计） -->
        <el-tab-pane label="流量管理" name="trafficManage">
          <div class="section-block">
            <div class="section-header">
              <h3 class="section-title">流量策略</h3>
              <div style="display:flex;gap:8px">
                <el-button size="small" type="primary" @click="openVmTrafficPolicyDialog">修改流量策略</el-button>
                <el-button size="small" type="success" @click="openVmAddTrafficDialog('fixed')">增加固定流量</el-button>
                <el-button size="small" type="warning" @click="openVmAddTrafficDialog('temporary')">增加临时流量</el-button>
                <el-button size="small" :icon="Refresh" @click="loadVmTrafficPolicy" :loading="vmTrafficPolicyLoading">刷新</el-button>
              </div>
            </div>
            <el-descriptions v-if="vmTrafficPolicy" :column="3" border size="small" style="margin-top:12px">
              <el-descriptions-item label="流量上限">{{ vmTrafficPolicy.traffic_max_mb != null ? (vmTrafficPolicy.traffic_max_mb === 0 ? '不限' : vmTrafficPolicy.traffic_max_mb + ' MB') : '-' }}</el-descriptions-item>
              <el-descriptions-item label="耗尽下行限速">{{ vmTrafficPolicy.exhausted_rx_mbps != null ? (vmTrafficPolicy.exhausted_rx_mbps === 0 ? '不限' : vmTrafficPolicy.exhausted_rx_mbps + ' Mbps') : '-' }}</el-descriptions-item>
              <el-descriptions-item label="耗尽上行限速">{{ vmTrafficPolicy.exhausted_tx_mbps != null ? (vmTrafficPolicy.exhausted_tx_mbps === 0 ? '不限' : vmTrafficPolicy.exhausted_tx_mbps + ' Mbps') : '-' }}</el-descriptions-item>
            </el-descriptions>
            <el-empty v-else-if="!vmTrafficPolicyLoading" description="暂无流量策略数据" :image-size="60" />
          </div>
          <div class="section-block">
            <div class="section-header">
              <h3 class="section-title">每小时流量</h3>
              <div style="display: flex; align-items: center; gap: 8px; flex-wrap: wrap">
                <el-radio-group v-model="trafficTimeMode" size="small" @change="loadTrafficHourly">
                  <el-radio-button label="relative">最近</el-radio-button>
                  <el-radio-button label="fixed">自定义</el-radio-button>
                </el-radio-group>
                <el-select v-if="trafficTimeMode === 'relative'" v-model="trafficRelativeMinutes" size="small" style="width: 120px" @change="loadTrafficHourly">
                  <el-option label="6小时" :value="360" />
                  <el-option label="12小时" :value="720" />
                  <el-option label="1天" :value="1440" />
                  <el-option label="3天" :value="4320" />
                  <el-option label="7天" :value="10080" />
                </el-select>
                <el-date-picker
                  v-else
                  v-model="trafficHourlyRange"
                  type="datetimerange"
                  range-separator="至"
                  start-placeholder="开始"
                  end-placeholder="结束"
                  size="small"
                  style="width: 340px"
                  @change="loadTrafficHourly"
                />
                <el-button size="small" :icon="Refresh" @click="loadTrafficHourly" :loading="trafficHourlyLoading">刷新</el-button>
              </div>
            </div>
            <el-card v-if="trafficHourlyData.length" shadow="hover" class="metrics-card" style="margin-top:12px">
              <template #header><span class="metrics-title"><el-icon><Refresh /></el-icon> 每小时流量（MB）</span></template>
              <div ref="trafficHourlyChartRef" class="chart-container"></div>
            </el-card>
            <el-empty v-else-if="!trafficHourlyLoading" description="暂无流量统计数据" :image-size="60" />
          </div>
        </el-tab-pane>

      </el-tabs>
    </div>

    <!-- 资源阈值策略弹窗 -->
    <el-dialog v-model="thresholdDialogVisible" title="资源阈值策略" width="680px" destroy-on-close class="tk-dialog">
      <div v-loading="thresholdLoading">
        <el-alert type="warning" :closable="false" show-icon style="margin-bottom: 16px">
          <template #title>某项百分比和持续时间均为 0 时关闭；启用时百分比范围为 (0, 100]，持续时间为 1～86400 秒。</template>
        </el-alert>
        <el-form ref="thresholdFormRef" :model="thresholdForm" :rules="thresholdRules" label-width="100px">
          <div class="threshold-policy-grid">
            <template v-for="item in thresholdResources" :key="item.key">
              <div class="threshold-policy-title">{{ item.label }}</div>
              <el-form-item label="使用率阈值" :prop="`${item.key}_threshold_percent`">
                <el-input-number v-model="thresholdForm[`${item.key}_threshold_percent`]" :min="0" :max="100" :precision="2" controls-position="right" />
                <span class="form-unit">%</span>
              </el-form-item>
              <el-form-item label="持续时间" :prop="`${item.key}_threshold_duration_seconds`">
                <el-input-number v-model="thresholdForm[`${item.key}_threshold_duration_seconds`]" :min="0" :max="86400" :precision="0" controls-position="right" />
                <span class="form-unit">秒</span>
              </el-form-item>
            </template>
          </div>
        </el-form>
      </div>
      <template #footer>
        <el-button @click="thresholdDialogVisible = false">取消</el-button>
        <el-button type="primary" :loading="thresholdSubmitLoading" @click="submitThresholdPolicy">保存策略</el-button>
      </template>
    </el-dialog>

    <!-- 电源操作确认弹窗 -->
    <el-dialog v-model="powerDialogVisible" :title="`${powerLabels[powerAction] || ''}虚拟机`" width="400px" destroy-on-close>
      <div style="display: flex; align-items: flex-start; gap: 12px; padding: 8px 0">
        <el-icon :size="22" :style="{ color: powerAction === 'stop' ? '#F56C6C' : powerAction === 'reboot' ? '#E6A23C' : '#409EFF', flexShrink: 0, marginTop: '-1px' }">
          <WarningFilled />
        </el-icon>
        <div>
          <div style="font-size: 15px; font-weight: 500; color: #303133; margin-bottom: 12px">
            确定要{{ powerLabels[powerAction] }}虚拟机「{{ detail?.name }}」吗？
          </div>
          <el-checkbox v-model="powerForce" style="margin-bottom: 4px">强制执行</el-checkbox>
          <div style="font-size: 12px; color: #909399; padding-left: 24px">勾选后将强制{{ powerLabels[powerAction] }}，可能导致数据丢失</div>
        </div>
      </div>
      <template #footer>
        <el-button @click="powerDialogVisible = false">取消</el-button>
        <el-button :type="powerAction === 'stop' ? 'danger' : 'primary'" @click="submitPower">确定{{ powerLabels[powerAction] }}</el-button>
      </template>
    </el-dialog>

    <!-- 重装弹窗 -->
    <el-dialog v-model="rebuildDialogVisible" title="重装虚拟机" width="480px" destroy-on-close class="tk-dialog">
      <el-form label-width="100px">
        <div class="tk-section">
          <div class="tk-section-title">重装信息</div>
          <el-alert title="重装会清除当前虚拟机数据并使用新镜像重新创建，请谨慎操作！" type="warning" :closable="false" show-icon style="margin-bottom: 16px" />
          <el-form-item label="虚拟机">{{ detail?.name || '-' }}</el-form-item>
          <el-form-item label="新镜像" required>
            <div style="display: flex; gap: 8px; width: 100%">
              <el-input :model-value="rebuildImageId ? `${rebuildImageName || ''} (ID: ${rebuildImageId})` : ''" readonly placeholder="请选择镜像" style="flex: 1" />
              <el-button type="primary" @click="showImageSelector = true">选择</el-button>
            </div>
          </el-form-item>
        </div>
      </el-form>
      <template #footer>
        <div class="tk-dialog-footer">
          <el-button @click="rebuildDialogVisible = false">取消</el-button>
          <el-button type="danger" :loading="actionLoading" @click="submitRebuild">确定重装</el-button>
        </div>
      </template>
    </el-dialog>

    <ImageSelectorPopup v-model="showImageSelector" :service-id="serviceId" :current-id="rebuildImageId" @confirm="img => { rebuildImageId = img.id; rebuildImageName = img.name }" />

    <!-- 创建快照弹窗 -->
    <el-dialog v-model="snapshotCreateVisible" title="创建快照" width="480px" destroy-on-close class="tk-dialog">
      <el-form :model="snapshotForm" label-width="100px">
        <div class="tk-section">
          <div class="tk-section-title">快照信息</div>
          <el-form-item label="虚拟机">{{ detail?.name || '-' }} (ID: {{ vmId }})</el-form-item>
          <el-form-item label="快照名称" required>
            <el-input v-model="snapshotForm.name" placeholder="请输入快照名称" />
          </el-form-item>
        </div>
      </el-form>
      <template #footer>
        <div class="tk-dialog-footer">
          <el-button @click="snapshotCreateVisible = false">取消</el-button>
          <el-button type="primary" :loading="actionLoading" @click="submitCreateSnapshot">创建</el-button>
        </div>
      </template>
    </el-dialog>

    <!-- 创建备份弹窗 -->
    <el-dialog v-model="backupCreateVisible" title="创建备份" width="480px" destroy-on-close class="tk-dialog">
      <el-form :model="backupForm" label-width="100px">
        <div class="tk-section">
          <div class="tk-section-title">备份信息</div>
          <el-form-item label="虚拟机">{{ detail?.name || '-' }} (ID: {{ vmId }})</el-form-item>
          <el-form-item label="备份名称" required>
            <el-input v-model="backupForm.name" placeholder="请输入备份名称" />
          </el-form-item>
        </div>
      </el-form>
      <template #footer>
        <div class="tk-dialog-footer">
          <el-button @click="backupCreateVisible = false">取消</el-button>
          <el-button type="primary" :loading="actionLoading" @click="submitCreateBackup">创建</el-button>
        </div>
      </template>
    </el-dialog>

    <!-- 快照/备份进度弹窗 -->
    <el-dialog v-model="taskProgressVisible" :title="taskProgressTitle" width="520px" destroy-on-close>
      <div v-loading="taskProgressLoading">
        <el-descriptions :column="1" border size="small" v-if="taskProgressData">
          <el-descriptions-item label="任务ID">
            <span class="mono-text">{{ taskProgressData.task_id || '-' }}</span>
          </el-descriptions-item>
          <el-descriptions-item label="状态">
            <el-tag :type="taskStatusType(taskProgressData.status)" size="small">{{ snapshotStatusLabel(taskProgressData.status) }}</el-tag>
          </el-descriptions-item>
          <template v-if="taskProgressMeta">
            <el-descriptions-item v-for="(val, key) in taskProgressMeta" :key="key" :label="taskMetaLabel(key)">
              <span :style="key.includes('path') ? 'font-family: Consolas, monospace; font-size: 13px; word-break: break-all' : ''">{{ val }}</span>
            </el-descriptions-item>
          </template>
        </el-descriptions>
        <el-empty v-else description="暂无进度信息" />
      </div>
      <template #footer>
        <el-button @click="taskProgressVisible = false">关闭</el-button>
      </template>
    </el-dialog>

    <!-- 编辑虚拟机弹窗（仅 update API 支持的参数） -->
    <el-dialog v-model="editDialogVisible" title="修改虚拟机" width="640px" destroy-on-close class="tk-dialog">
      <el-form ref="editFormRef" :model="editForm" label-width="100px" v-loading="dialogOptionsLoading">
        <div class="tk-section">
          <div class="tk-section-title">带宽与端口</div>
          <div class="tk-resource-grid">
            <el-form-item label="下行带宽">
              <el-input-number v-model="editForm.rx_bandwidth" :min="0" controls-position="right" /><span class="tk-res-unit">Mbps</span>
            </el-form-item>
            <el-form-item label="上行带宽">
              <el-input-number v-model="editForm.tx_bandwidth" :min="0" controls-position="right" /><span class="tk-res-unit">Mbps</span>
            </el-form-item>
          </div>
          <el-form-item label="额外数据卷">
            <div style="display:flex;align-items:center;gap:6px">
              <el-input-number v-model="editForm.data_volume_size" :min="0" controls-position="right" style="width:200px" />
              <span class="tk-res-unit">GB</span>
              <span style="font-size:12px;color:#909399;margin-left:8px">0 表示不创建</span>
            </div>
          </el-form-item>
          <el-form-item label="Root密码">
            <el-input v-model="editForm.root_password" placeholder="留空则不修改" show-password />
          </el-form-item>
          <el-form-item label="SSH端口">
            <el-input-number v-model="editForm.ssh_port" :min="1" :max="65535" controls-position="right" style="width: 200px" />
          </el-form-item>
        </div>
        <div class="tk-section">
          <div class="tk-section-title">网络配置</div>
          <el-form-item label="网络">
            <div style="display: flex; flex-wrap: wrap; align-items: center; gap: 6px; width: 100%">
              <el-tag v-for="net in editSelectedNetworks" :key="net.id" closable @close="removeEditNetwork(net.id)">
                {{ net.name }} (ID:{{ net.id }})
              </el-tag>
              <el-button size="small" @click="showEditNetworkSelector = true">添加网络</el-button>
            </div>
          </el-form-item>
          <el-form-item label="内网">
            <div style="display: flex; align-items: center; gap: 8px; width: 100%">
              <el-tag v-if="editSelectedInternalNetworks.length" closable @close="removeEditInternalNetwork(editSelectedInternalNetworks[0].id)">
                {{ editSelectedInternalNetworks[0].name }} (ID:{{ editSelectedInternalNetworks[0].id }})
              </el-tag>
              <el-button size="small" @click="showEditInternalNetworkSelector = true">{{ editSelectedInternalNetworks.length ? '更换内网' : '选择内网' }}</el-button>
            </div>
          </el-form-item>
          <el-form-item label="安全组">
            <el-select v-model="editForm.port_group_id" placeholder="选择安全组（可选）" filterable clearable style="width: 100%">
              <el-option v-for="g in sgOptions" :key="g.id" :label="`${g.name} (ID: ${g.id})`" :value="g.id" />
            </el-select>
          </el-form-item>
        </div>
      </el-form>
      <template #footer>
        <div class="tk-dialog-footer">
          <el-button @click="editDialogVisible = false">取消</el-button>
          <el-button type="primary" :loading="actionLoading" @click="submitEditVm">确定</el-button>
        </div>
      </template>
    </el-dialog>
    <!-- 编辑用网络选择器（外网 bridge） -->
    <NetworkSelectorPopup ref="editNetSelectorRef" v-model="showEditNetworkSelector" :service-id="serviceId" :host-id="vmHostId" filter-type="bridge" filter-used="false" @confirm="handleEditNetworkConfirm" @create="() => handleNetCreate('edit')" />
    <!-- 编辑用内网选择器（内网 nat） -->
    <NetworkSelectorPopup ref="editInternalNetSelectorRef" v-model="showEditInternalNetworkSelector" :service-id="serviceId" :host-id="vmHostId" filter-type="nat" filter-used="false" @confirm="handleEditInternalNetworkConfirm" @create="() => handleNetCreate('editInternal')" />

    <!-- 重构虚拟机弹窗 -->
    <el-dialog v-model="refactorDialogVisible" title="重构虚拟机" width="700px" destroy-on-close class="tk-dialog">
      <el-form ref="refactorFormRef" :model="refactorForm" label-width="100px" v-loading="dialogOptionsLoading">
        <el-alert title="重构会修改虚拟机的底层配置参数，请谨慎操作！" type="warning" :closable="false" show-icon style="margin-bottom: 16px" />
        <div class="tk-section">
          <div class="tk-section-title">资源配置</div>
          <div class="tk-resource-grid">
            <el-form-item label="CPU">
              <el-input-number v-model="refactorForm.vcpu" :min="0" controls-position="right" /><span class="tk-res-unit">核</span>
            </el-form-item>
            <el-form-item label="内存">
              <el-input-number v-model="refactorMemDisplay" :min="0" :step="refactorMemUnit === 1048576 ? 1 : (refactorMemUnit === 1024 ? 512 : 1024)" :precision="refactorMemUnit === 1048576 ? 2 : 0" controls-position="right" />
              <el-select v-model="refactorMemUnit" class="tk-unit-select" @change="onRefactorMemUnitChange">
                <el-option label="KB" :value="1" />
                <el-option label="MB" :value="1024" />
                <el-option label="GB" :value="1048576" />
              </el-select>
            </el-form-item>
            <el-form-item label="下行带宽">
              <el-input-number v-model="refactorForm.rx_bandwidth" :min="0" controls-position="right" /><span class="tk-res-unit">Mbps</span>
            </el-form-item>
            <el-form-item label="上行带宽">
              <el-input-number v-model="refactorForm.tx_bandwidth" :min="0" controls-position="right" /><span class="tk-res-unit">Mbps</span>
            </el-form-item>
          </div>
        </div>
        <div class="tk-section">
          <div class="tk-section-title">基本参数</div>
          <el-form-item label="Root密码">
            <el-input v-model="refactorForm.root_password" placeholder="不修改留空" show-password />
          </el-form-item>
          <el-form-item label="UUID">
            <el-input v-model="refactorForm.uuid" placeholder="虚拟机UUID" />
          </el-form-item>
          <el-form-item label="Mate Data ID">
            <el-input v-model="refactorForm.mate_data_id" placeholder="元数据ID" />
          </el-form-item>
          <el-form-item label="物理机名称">
            <el-input v-model="refactorForm.physical_name" placeholder="不修改留空" />
          </el-form-item>
          <el-form-item label="配置路径">
            <el-input v-model="refactorForm.config_path" placeholder="不修改留空" />
          </el-form-item>
        </div>
        <div class="tk-section">
          <div class="tk-section-title">端口与VNC</div>
          <div class="tk-resource-grid">
            <el-form-item label="SSH端口">
              <el-input-number v-model="refactorForm.ssh_port" :min="0" :max="65535" controls-position="right" />
            </el-form-item>
            <el-form-item label="VNC端口">
              <el-input-number v-model="refactorForm.vnc_port" :min="0" :max="65535" controls-position="right" />
            </el-form-item>
          </div>
          <el-form-item label="VNC密码">
            <el-input v-model="refactorForm.vnc_password" placeholder="不填随机" show-password />
          </el-form-item>
        </div>
        <div class="tk-section">
          <div class="tk-section-title">网络配置</div>
          <el-form-item label="网络">
            <div style="display: flex; flex-wrap: wrap; align-items: center; gap: 6px; width: 100%">
              <el-tag v-for="net in refactorSelectedNetworks" :key="net.id" closable @close="removeRefactorNetwork(net.id)">
                {{ net.name }} (ID:{{ net.id }})
              </el-tag>
              <el-button size="small" @click="showRefactorNetworkSelector = true">添加网络</el-button>
            </div>
          </el-form-item>
          <el-form-item label="内网">
            <div style="display: flex; align-items: center; gap: 8px; width: 100%">
              <el-tag v-if="refactorSelectedInternalNetworks.length" closable @close="removeRefactorInternalNetwork(refactorSelectedInternalNetworks[0].id)">
                {{ refactorSelectedInternalNetworks[0].name }} (ID:{{ refactorSelectedInternalNetworks[0].id }})
              </el-tag>
              <el-button size="small" @click="showRefactorInternalNetworkSelector = true">{{ refactorSelectedInternalNetworks.length ? '更换内网' : '选择内网' }}</el-button>
            </div>
          </el-form-item>
          <el-form-item label="安全组">
            <el-select v-model="refactorForm.port_group_id" placeholder="选择安全组（可选）" filterable clearable style="width: 100%">
              <el-option v-for="g in sgOptions" :key="g.id" :label="`${g.name} (ID: ${g.id})`" :value="g.id" />
            </el-select>
          </el-form-item>
        </div>
      </el-form>
      <template #footer>
        <div class="tk-dialog-footer">
          <el-button @click="refactorDialogVisible = false">取消</el-button>
          <el-button type="warning" :loading="actionLoading" @click="submitRefactorVm">确定重构</el-button>
        </div>
      </template>
    </el-dialog>

    <!-- 重构用网络选择器（外网 bridge） -->
    <NetworkSelectorPopup ref="refactorNetSelectorRef" v-model="showRefactorNetworkSelector" :service-id="serviceId" :host-id="vmHostId" filter-type="bridge" filter-used="false" @confirm="handleRefactorNetworkConfirm" @create="() => handleNetCreate('refactor')" />
    <!-- 重构用内网选择器（内网 nat） -->
    <NetworkSelectorPopup ref="refactorInternalNetSelectorRef" v-model="showRefactorInternalNetworkSelector" :service-id="serviceId" :host-id="vmHostId" filter-type="nat" filter-used="false" @confirm="handleRefactorInternalNetworkConfirm" @create="() => handleNetCreate('refactorInternal')" />

    <!-- VNC 连接弹窗 -->
    <el-dialog v-model="vncDialogVisible" title="获取 VNC 连接" width="560px" destroy-on-close class="tk-dialog">
      <el-form label-width="100px" v-loading="dialogOptionsLoading">
        <div class="tk-section">
          <div class="tk-section-title">VNC 配置</div>
          <el-form-item label="虚拟机">{{ detail?.name || '-' }} (ID: {{ vmId }})</el-form-item>
          <el-form-item label="VNC节点">
            <el-select v-model="vncNodeId" placeholder="选择VNC节点" filterable style="width: 100%">
              <el-option v-for="n in vncNodeOptions" :key="n.id" :label="`${n.name} (${n.ip}:${n.port})`" :value="n.id" />
            </el-select>
          </el-form-item>
        </div>
      </el-form>
      <div v-if="vncResult" class="vnc-result">
        <el-descriptions :column="1" border size="small">
          <el-descriptions-item label="VNC地址">
            <el-link type="primary" :href="vncResult.url" target="_blank">{{ vncResult.url }}</el-link>
          </el-descriptions-item>
          <el-descriptions-item label="过期时间">{{ formatTimestamp(vncResult.expire_at) }}</el-descriptions-item>
        </el-descriptions>
      </div>
      <template #footer>
        <div class="tk-dialog-footer">
          <el-button @click="vncDialogVisible = false">关闭</el-button>
          <el-button type="primary" :loading="vncLoading" @click="submitGetVnc">获取</el-button>
        </div>
      </template>
    </el-dialog>


    <!-- 修改带宽弹窗 -->
    <el-dialog v-model="trafficDialogVisible" title="修改虚拟机带宽" width="640px" destroy-on-close class="tk-dialog">
      <el-form :model="trafficForm" label-width="100px">
        <div class="tk-section">
          <div class="tk-section-title">带宽与流量</div>
          <div class="tk-resource-grid">
            <el-form-item label="下行带宽">
              <el-input-number v-model="trafficForm.rx_bandwidth" :min="0" controls-position="right" /><span class="tk-res-unit">Mbps</span>
            </el-form-item>
            <el-form-item label="上行带宽">
              <el-input-number v-model="trafficForm.tx_bandwidth" :min="0" controls-position="right" /><span class="tk-res-unit">Mbps</span>
            </el-form-item>
          </div>
          <el-form-item label="流量上限">
            <div class="tk-inline-unit">
              <el-input-number v-model="trafficForm._trafficGB" :min="0" :precision="2" controls-position="right" /><span class="tk-res-unit">GB</span>
            </div>
          </el-form-item>
          <el-form-item label="耗尽下行限速">
            <div class="tk-inline-unit">
              <el-input-number v-model="trafficForm.traffic_exhausted_rx_mbps" :min="0" :precision="2" controls-position="right" /><span class="tk-res-unit">Mbps（0 不限）</span>
            </div>
          </el-form-item>
          <el-form-item label="耗尽上行限速">
            <div class="tk-inline-unit">
              <el-input-number v-model="trafficForm.traffic_exhausted_tx_mbps" :min="0" :precision="2" controls-position="right" /><span class="tk-res-unit">Mbps（0 不限）</span>
            </div>
          </el-form-item>
        </div>
      </el-form>
      <template #footer>
        <div class="tk-dialog-footer">
          <el-button @click="trafficDialogVisible = false">取消</el-button>
          <el-button type="primary" :loading="actionLoading" @click="submitUpdateTraffic">确定</el-button>
        </div>
      </template>
    </el-dialog>

    <!-- 流量策略管理弹窗 -->
    <el-dialog v-model="vmTrafficPolicyVisible" title="修改流量策略" width="480px" destroy-on-close class="tk-dialog">
      <el-form :model="vmTrafficPolicyForm" label-width="120px">
        <el-form-item label="流量上限">
          <div class="tk-inline-unit">
            <el-input-number v-model="vmTrafficPolicyForm.traffic_max_mb" :min="0" controls-position="right" /><span class="tk-res-unit">MB（0 不限）</span>
          </div>
        </el-form-item>
        <el-form-item label="耗尽下行限速">
          <div class="tk-inline-unit">
            <el-input-number v-model="vmTrafficPolicyForm.exhausted_rx_mbps" :min="0" :precision="2" controls-position="right" /><span class="tk-res-unit">Mbps（0 不限）</span>
          </div>
        </el-form-item>
        <el-form-item label="耗尽上行限速">
          <div class="tk-inline-unit">
            <el-input-number v-model="vmTrafficPolicyForm.exhausted_tx_mbps" :min="0" :precision="2" controls-position="right" /><span class="tk-res-unit">Mbps（0 不限）</span>
          </div>
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="tk-dialog-footer">
          <el-button @click="vmTrafficPolicyVisible = false">取消</el-button>
          <el-button type="primary" :loading="vmTrafficPolicyLoading" @click="submitUpdateVmTrafficPolicy">确定</el-button>
        </div>
      </template>
    </el-dialog>

    <!-- 增加固定/临时流量弹窗 -->
    <el-dialog v-model="vmAddTrafficVisible" :title="vmAddTrafficType === 'fixed' ? '增加固定流量上限' : '增加一次性临时流量'" width="400px" destroy-on-close class="tk-dialog">
      <el-form :model="vmAddTrafficForm" label-width="110px">
        <el-form-item label="流量数量">
          <div class="tk-inline-unit">
            <el-input-number v-model="vmAddTrafficForm.traffic_mb" :min="1" controls-position="right" /><span class="tk-res-unit">MB</span>
          </div>
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="tk-dialog-footer">
          <el-button @click="vmAddTrafficVisible = false">取消</el-button>
          <el-button type="primary" :loading="vmTrafficPolicyLoading" @click="submitAddVmTraffic">确定</el-button>
        </div>
      </template>
    </el-dialog>

    <!-- 迁移虚拟机弹窗 -->
    <el-dialog v-model="migrateDialogVisible" title="迁移虚拟机（更换宿主机）" width="580px" destroy-on-close class="tk-dialog">
      <el-form label-width="100px" v-loading="migrateOptionsLoading">
        <el-alert type="warning" :closable="false" show-icon style="margin-bottom: 16px">将虚拟机迁移到其他宿主机或宿主机组，请谨慎操作！</el-alert>
        <div class="tk-section">
          <div class="tk-section-title">迁移配置</div>
          <!-- <el-form-item label="当前宿主机">
            <el-tag>{{ detail?.host_name || detail?.host_id || '-' }}</el-tag>
          </el-form-item> -->
          <el-form-item label="迁移方式">
            <el-radio-group v-model="migrateMode">
              <el-radio value="host">指定宿主机</el-radio>
              <el-radio value="group">指定宿主机组</el-radio>
            </el-radio-group>
          </el-form-item>
          <el-form-item label="目标宿主机" v-if="migrateMode === 'host'">
            <el-select v-model="migrateForm.target_host_id" placeholder="选择目标宿主机" filterable style="width: 100%" @change="clearMigrateNetworks">
              <el-option v-for="h in migrateHostOptions" :key="h.id" :label="`${h.name} (${h.ip || h.id})`" :value="h.id" :disabled="h.id === detail?.host_id" />
            </el-select>
          </el-form-item>
          <el-form-item label="目标宿主机组" v-if="migrateMode === 'group'">
            <el-select v-model="migrateForm.target_host_group_id" placeholder="选择目标宿主机组" filterable style="width: 100%">
              <el-option v-for="g in migrateGroupOptions" :key="g.id" :label="`${g.name} (ID: ${g.id})`" :value="g.id" />
            </el-select>
          </el-form-item>
          <div class="tk-resource-grid">
            <el-form-item label="IPv4数量">
              <el-input-number v-model="migrateForm.ipv4_num" :min="0" controls-position="right" /><span class="tk-res-unit">个</span>
            </el-form-item>
            <el-form-item label="IPv6数量">
              <el-input-number v-model="migrateForm.ipv6_num" :min="0" controls-position="right" /><span class="tk-res-unit">个</span>
            </el-form-item>
          </div>
          <el-form-item label="目标网络" v-if="migrateMode === 'host'">
            <div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;width:100%">
              <el-tag v-for="n in migrateSelectedNetworks" :key="n.id" closable @close="removeMigrateNetwork(n.id)">{{ n.name || n.id }} (ID:{{ n.id }})</el-tag>
              <el-button size="small" :disabled="!migrateForm.target_host_id" @click="showMigrateNetworkSelector = true">选择网络</el-button>
            </div>
            <span class="tk-form-tip">可选；指定目标宿主机上要分配的网络。</span>
          </el-form-item>
        </div>
      </el-form>
      <template #footer>
        <div class="tk-dialog-footer">
          <el-button @click="migrateDialogVisible = false">取消</el-button>
          <el-button type="warning" :loading="actionLoading" @click="submitMigrateVm">确定迁移</el-button>
        </div>
      </template>
    </el-dialog>
    <NetworkSelectorPopup v-model="showMigrateNetworkSelector" :service-id="serviceId" :host-id="migrateForm.target_host_id || 0" filter-type="bridge" filter-used="false" :multiple="true" @confirm="handleMigrateNetworkConfirm" />

    <!-- 数据迁移弹窗 -->
    <el-dialog v-model="dataMigrateVisible" title="发起虚拟机数据迁移" width="600px" destroy-on-close class="tk-dialog">
      <el-form :model="dataMigrateForm" label-width="100px" v-loading="dataMigrateLoading">
        <el-alert type="info" :closable="false" show-icon style="margin-bottom:16px">
          源服务的虚拟机数据将通过 rsync 传输到目标服务的宿主机上，完成后在目标宿主机上校验并恢复虚拟机。
        </el-alert>
        <div class="tk-section">
          <div class="tk-section-title">源信息</div>
          <el-form-item label="主控服务">
            <el-input :model-value="`${serviceName} (ID: ${serviceId})`" disabled />
          </el-form-item>
          <el-form-item label="虚拟机">
            <el-input :model-value="`${detail?.name || ''} (ID: ${vmId})`" disabled />
          </el-form-item>
        </div>
        <div class="tk-section">
          <div class="tk-section-title">目标信息</div>
          <el-form-item label="主控服务" required>
            <el-select v-model="dataMigrateForm.target_service_id" placeholder="选择目标主控服务" filterable style="width:100%"
              @change="dataMigrateForm.target_host_id = null; loadDataMigrateHosts()">
              <el-option v-for="s in dataMigrateServiceOptions" :key="s.id" :label="`${s.name} (ID:${s.id})`" :value="s.id" />
            </el-select>
          </el-form-item>
          <el-form-item label="宿主机" required>
            <el-select v-model="dataMigrateForm.target_host_id" placeholder="选择目标宿主机" filterable style="width:100%"
              :disabled="!dataMigrateForm.target_service_id" :loading="dataMigrateHostsLoading"
              @change="dataMigrateForm.network_ids = []; dataMigrateSelectedNetworks = []; loadDataMigrateNetworks()">
              <el-option v-for="h in dataMigrateHostOptions" :key="h.id" :label="`${h.name} (${h.ip || h.id})`" :value="h.id" />
            </el-select>
          </el-form-item>
          <div class="tk-resource-grid">
            <el-form-item label="IPv4数量">
              <el-input-number v-model="dataMigrateForm.ipv4_num" :min="0" controls-position="right" /><span class="tk-res-unit">个</span>
            </el-form-item>
            <el-form-item label="IPv6数量">
              <el-input-number v-model="dataMigrateForm.ipv6_num" :min="0" controls-position="right" /><span class="tk-res-unit">个</span>
            </el-form-item>
          </div>
          <el-form-item label="网络ID">
            <div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;width:100%">
              <el-tag v-for="n in dataMigrateSelectedNetworks" :key="n.id" closable @close="removeDataMigrateNetwork(n.id)">
                {{ n.name || n.id }} (ID:{{ n.id }})
              </el-tag>
              <el-button size="small" :disabled="!dataMigrateForm.target_host_id" @click="showDataMigrateNetworkSelector = true">选择网络</el-button>
            </div>
            <span class="tk-form-tip">在目标宿主机监听的网络ID列表（可选）</span>
          </el-form-item>
        </div>
      </el-form>
      <template #footer>
        <div class="tk-dialog-footer">
          <el-button @click="dataMigrateVisible = false">取消</el-button>
          <el-button type="primary" :loading="actionLoading" @click="submitDataMigrate">发起迁移</el-button>
        </div>
      </template>
    </el-dialog>
    <!-- 数据迁移用网络选择器 -->
    <NetworkSelectorPopup ref="dataMigrateNetSelectorRef" v-model="showDataMigrateNetworkSelector"
      :service-id="dataMigrateForm.target_service_id || 0"
      :host-id="dataMigrateForm.target_host_id || 0"
      filter-type="bridge" filter-used="false" :multiple="true"
      @confirm="handleDataMigrateNetworkConfirm"
      @create="() => handleNetCreate('dataMigrate')" />

    <!-- 数据迁移进度弹窗 -->
    <el-dialog v-model="dataMigrateProgressVisible" title="数据迁移进度" width="560px" destroy-on-close>
      <div v-loading="dataMigrateProgressLoading">
        <el-descriptions :column="2" border size="small" v-if="dataMigrateProgressData">
          <el-descriptions-item label="阶段" :span="2">
            <el-tag :type="migrateStageType(dataMigrateProgressData.stage)" size="small">{{ migrateStageLabel(dataMigrateProgressData.stage) }}</el-tag>
          </el-descriptions-item>
          <el-descriptions-item label="迁移记录" v-if="dataMigrateProgressData.migration_record_id">
            <el-link type="primary" @click="goMigrationRecords({ id: dataMigrateProgressData.migration_record_id })">#{{ dataMigrateProgressData.migration_record_id }}</el-link>
          </el-descriptions-item>
          <el-descriptions-item label="迁移链" v-if="dataMigrateProgressData.chain_id">
            <el-link type="primary" class="mono-text" @click="goMigrationRecords({ chain_id: dataMigrateProgressData.chain_id })">{{ dataMigrateProgressData.chain_id }}</el-link>
          </el-descriptions-item>
          <el-descriptions-item label="导出任务ID" :span="2" v-if="dataMigrateProgressData.export_task_id">
            <span style="font-family:monospace;font-size:12px">{{ dataMigrateProgressData.export_task_id }}</span>
          </el-descriptions-item>
          <el-descriptions-item label="导入任务ID" :span="2" v-if="dataMigrateProgressData.import_task_id">
            <span style="font-family:monospace;font-size:12px">{{ dataMigrateProgressData.import_task_id }}</span>
          </el-descriptions-item>
          <el-descriptions-item label="源主控服务">{{ dataMigrateProgressData.source_service_id ?? '-' }}</el-descriptions-item>
          <el-descriptions-item label="目标主控服务">{{ dataMigrateProgressData.target_service_id ?? '-' }}</el-descriptions-item>
          <el-descriptions-item label="源宿主机">{{ dataMigrateProgressData.source_host_id ?? '-' }}</el-descriptions-item>
          <el-descriptions-item label="目标宿主机">{{ dataMigrateProgressData.target_host_id ?? '-' }}</el-descriptions-item>
          <el-descriptions-item label="源虚拟机" v-if="dataMigrateProgressData.source_vm_id">
            <el-link type="primary" @click="goVmById(dataMigrateProgressData.source_service_id, dataMigrateProgressData.source_vm_id)">VM #{{ dataMigrateProgressData.source_vm_id }}</el-link>
          </el-descriptions-item>
          <el-descriptions-item label="导入虚拟机" v-if="dataMigrateProgressData.imported_vm_id">
            <el-link type="primary" @click="goVmById(dataMigrateProgressData.target_service_id, dataMigrateProgressData.imported_vm_id)">VM #{{ dataMigrateProgressData.imported_vm_id }}</el-link>
          </el-descriptions-item>
          <el-descriptions-item label="进度" :span="2" v-if="dataMigrateProgressData.progress != null">
            <el-progress :percentage="dataMigrateProgressData.progress" :status="migrateProgressBarStatus(dataMigrateProgressData.stage)" style="width:100%" />
          </el-descriptions-item>
          <el-descriptions-item label="速度" v-if="dataMigrateProgressData.speed">{{ dataMigrateProgressData.speed }}</el-descriptions-item>
          <el-descriptions-item label="信息" :span="dataMigrateProgressData.speed ? 1 : 2" v-if="dataMigrateProgressData.message">{{ dataMigrateProgressData.message }}</el-descriptions-item>
          <el-descriptions-item label="详情" :span="2" v-if="dataMigrateProgressData.detail">{{ dataMigrateProgressData.detail }}</el-descriptions-item>
          <el-descriptions-item label="错误" :span="2" v-if="dataMigrateProgressData.error">
            <span style="color:#f56c6c">{{ dataMigrateProgressData.error }}</span>
          </el-descriptions-item>
          <el-descriptions-item label="警告" :span="2" v-if="dataMigrateProgressData.warnings?.length">
            <div v-for="(warning, index) in dataMigrateProgressData.warnings" :key="index" style="color:#e6a23c">{{ warning }}</div>
          </el-descriptions-item>
        </el-descriptions>
        <el-empty v-else-if="!dataMigrateProgressLoading" description="暂无进度信息" :image-size="60" />
      </div>
      <template #footer>
        <el-button @click="dataMigrateProgressVisible = false">关闭</el-button>
        <el-button :icon="Refresh" @click="loadDataMigrateProgress" :loading="dataMigrateProgressLoading">刷新进度</el-button>
        <el-button type="danger" @click="handleAbortMigrate" :loading="abortLoading" v-if="isMigrating">中断迁移</el-button>
      </template>
    </el-dialog>

    <!-- 绑定外网选择器（bridge） -->
    <NetworkSelectorPopup ref="bindBridgeNetSelectorRef" v-model="showNetBindBridgeSelector" :service-id="serviceId" :host-id="vmHostId" filter-type="bridge" filter-used="false" @confirm="handleNetBindBridgeConfirm" @create="() => handleNetCreate('bindBridge')" />
    <!-- 绑定内网选择器（nat） -->
    <NetworkSelectorPopup ref="bindNatNetSelectorRef" v-model="showNetBindNatSelector" :service-id="serviceId" :host-id="vmHostId" filter-type="nat" filter-used="false" @confirm="handleNetBindNatConfirm" @create="() => handleNetCreate('bindNat')" />

    <!-- 创建/编辑网络弹窗 -->
    <el-dialog v-model="netDialogVisible" :title="netDialogType === 'add' ? '创建网络' : '编辑网络'" width="600px" destroy-on-close class="tk-dialog">
      <el-form ref="netFormRef" :model="netForm" :rules="netFormRules" label-width="100px">
        <div class="tk-section">
          <div class="tk-section-title">基本信息</div>
          <el-form-item label="名称" prop="name"><el-input v-model="netForm.name" placeholder="网络名称" /></el-form-item>
          <el-form-item label="网络类型" prop="type">
            <el-select v-model="netForm.type" style="width: 100%">
              <el-option label="网桥(Bridge/外网)" value="bridge" />
              <el-option label="内网(NAT)" value="nat" />
            </el-select>
          </el-form-item>
          <el-form-item label="IP" prop="address">
            <div class="tk-inline-unit">
              <el-input v-model="netForm.address" placeholder="例如 192.168.1.0/24" />
              <span class="tk-res-unit">CIDR</span>
            </div>
          </el-form-item>
          <el-form-item label="网关地址" prop="gateway"><el-input v-model="netForm.gateway" placeholder="例如 192.168.1.1" /></el-form-item>
          <el-form-item label="DNS 服务器"><el-input v-model="netForm.nameservers" placeholder="默认 114.114.114.114,8.8.8.8" /></el-form-item>
        </div>
        <div class="tk-section">
          <div class="tk-section-title">高级配置</div>
          <el-form-item label="MAC 地址"><el-input v-model="netForm.mac_address" placeholder="不填则随机" /></el-form-item>
          <el-form-item label="虚拟网桥名"><el-input v-model="netForm.bridge_name" placeholder="不填使用默认" /></el-form-item>
          <el-form-item label="逻辑网桥名"><el-input v-model="netForm.ls_bridge_name" placeholder="不填使用默认" /></el-form-item>
          <el-form-item label="逻辑端口名"><el-input v-model="netForm.ls_name" placeholder="不填使用默认" /></el-form-item>
        </div>
      </el-form>
      <template #footer>
        <div class="tk-dialog-footer">
          <el-button @click="handleNetDialogCancel">取消</el-button>
          <el-button type="primary" :loading="actionLoading" @click="submitNetForm">确定</el-button>
        </div>
      </template>
    </el-dialog>

    <!-- 网络详情弹窗 -->
    <el-dialog v-model="netDetailVisible" title="网络详情" width="600px" destroy-on-close>
      <el-descriptions :column="2" border v-if="netDetailData">
        <el-descriptions-item label="ID">{{ netDetailData.id }}</el-descriptions-item>
        <el-descriptions-item label="名称">{{ netDetailData.name }}</el-descriptions-item>
        <el-descriptions-item label="类型"><el-tag :type="netDetailData.type === 'bridge' ? 'success' : 'warning'" size="small">{{ netDetailData.type === 'bridge' ? '网桥' : 'NAT' }}</el-tag></el-descriptions-item>
        <el-descriptions-item label="地址">{{ netDetailData.address }}</el-descriptions-item>
        <el-descriptions-item label="网关">{{ netDetailData.gateway }}</el-descriptions-item>
        <el-descriptions-item label="DNS">{{ netDetailData.nameservers || '-' }}</el-descriptions-item>
        <el-descriptions-item label="MAC 地址">{{ netDetailData.mac_address || '-' }}</el-descriptions-item>
        <el-descriptions-item label="虚拟网桥">{{ netDetailData.bridge_name || '-' }}</el-descriptions-item>
        <el-descriptions-item label="逻辑网桥">{{ netDetailData.ls_bridge_name || '-' }}</el-descriptions-item>
        <el-descriptions-item label="逻辑端口">{{ netDetailData.ls_name || '-' }}</el-descriptions-item>
        <el-descriptions-item label="目标设备">{{ netDetailData.target_device || '-' }}</el-descriptions-item>
      </el-descriptions>
      <template #footer><el-button @click="netDetailVisible = false">关闭</el-button></template>
    </el-dialog>

    <!-- 数据卷选择器 -->
    <VolumeSelectorPopup v-model="showVolSelector" :service-id="serviceId" :host-id="vmHostId" @confirm="handleVolBindConfirm" @create="handleVolCreateFromSelector" />

    <!-- 创建数据卷弹窗 -->
    <el-dialog v-model="volCreateVisible" title="创建数据卷" width="560px" :close-on-click-modal="false" class="tk-dialog">
      <el-form ref="volCreateFormRef" :model="volCreateForm" :rules="volCreateRules" label-width="100px">
        <div class="tk-section">
          <div class="tk-section-title">卷信息</div>
          <el-form-item label="名称" prop="name"><el-input v-model="volCreateForm.name" placeholder="数据卷名称" /></el-form-item>
          <el-form-item label="大小" prop="size">
            <div class="tk-inline-unit">
              <el-input-number v-model="volCreateForm.size" :min="1" controls-position="right" /><span class="tk-res-unit">GB</span>
            </div>
          </el-form-item>
          <el-form-item label="宿主机">
            <el-input :model-value="vmHostId ? `宿主机 #${vmHostId}` : '-'" disabled style="width: 100%" />
          </el-form-item>
          <el-form-item label="系统卷"><el-switch v-model="volCreateForm.is_system" /></el-form-item>
          <el-form-item v-if="volCreateForm.is_system" label="镜像">
            <div class="bind-selector-row">
              <el-input :model-value="volCreateForm.image_id ? `镜像 #${volCreateForm.image_id}${volCreateForm._imageName ? ' - ' + volCreateForm._imageName : ''}` : '未选择'" disabled style="flex: 1" />
              <el-button type="primary" @click="volImageSelectorFromCreate = true; volCreateVisible = false; showVolImageSelector = true" style="margin-left: 8px">选择</el-button>
              <el-button v-if="volCreateForm.image_id" @click="volCreateForm.image_id = 0; volCreateForm._imageName = ''" style="margin-left: 4px">清除</el-button>
            </div>
          </el-form-item>
          <el-form-item label="虚拟机">
            <el-input :model-value="detail ? `${detail.name || ''} (ID: ${vmId})` : '未选择'" disabled style="width: 100%" />
          </el-form-item>
          <el-form-item label="目标设备名"><el-input v-model="volCreateForm.target_device" placeholder="不填自动生成" /></el-form-item>
        </div>
      </el-form>
      <template #footer>
        <div class="tk-dialog-footer">
          <el-button @click="volCreateVisible = false">取消</el-button>
          <el-button type="primary" :loading="actionLoading" @click="submitVolCreate">确定</el-button>
        </div>
      </template>
    </el-dialog>
    <ImageSelectorPopup v-model="showVolImageSelector" :service-id="serviceId" :current-id="volCreateForm.image_id" @confirm="handleVolImageSelected" />

    <!-- 调整卷大小弹窗 -->
    <el-dialog v-model="volResizeVisible" title="调整数据卷大小" width="480px" destroy-on-close class="tk-dialog">
      <el-form label-width="100px">
        <div class="tk-section">
          <div class="tk-section-title">调整信息</div>
          <el-form-item label="卷名称">{{ volResizeTarget?._name || '-' }}</el-form-item>
          <el-form-item label="当前大小">{{ volResizeTarget?._currentSize || 0 }} GB</el-form-item>
          <el-form-item label="新大小">
            <div class="tk-inline-unit">
              <el-input-number v-model="volNewSize" :min="volResizeTarget?._currentSize || 1" controls-position="right" /><span class="tk-res-unit">GB</span>
            </div>
          </el-form-item>
        </div>
      </el-form>
      <template #footer>
        <div class="tk-dialog-footer">
          <el-button @click="volResizeVisible = false">取消</el-button>
          <el-button type="primary" :loading="actionLoading" @click="submitVolResize">确定</el-button>
        </div>
      </template>
    </el-dialog>

    <!-- 迁移卷弹窗 -->
    <el-dialog v-model="volTransferVisible" title="迁移数据卷" width="480px" destroy-on-close class="tk-dialog">
      <el-form label-width="100px" v-loading="dialogOptionsLoading">
        <div class="tk-section">
          <div class="tk-section-title">迁移信息</div>
          <el-alert type="info" :closable="false" show-icon style="margin-bottom: 16px">将数据卷迁移到另一台虚拟机上</el-alert>
          <el-form-item label="卷名称">{{ volTransferTarget?._name || '-' }}</el-form-item>
          <el-form-item label="目标虚拟机" required>
            <el-select v-model="volTransferVmId" placeholder="选择目标虚拟机" filterable style="width: 100%">
              <el-option v-for="v in vmListOptions" :key="v.id" :label="`${v.name} (ID: ${v.id})`" :value="v.id" />
            </el-select>
          </el-form-item>
        </div>
      </el-form>
      <template #footer>
        <div class="tk-dialog-footer">
          <el-button @click="volTransferVisible = false">取消</el-button>
          <el-button type="primary" :loading="actionLoading" @click="submitVolTransfer">确定</el-button>
        </div>
      </template>
    </el-dialog>

    <!-- 数据卷详情弹窗 -->
    <el-dialog v-model="volDetailVisible" title="数据卷详情" width="600px" destroy-on-close>
      <el-descriptions :column="2" border v-if="volDetailData">
        <el-descriptions-item label="ID">{{ volDetailData.id }}</el-descriptions-item>
        <el-descriptions-item label="名称">{{ volDetailData.name }}</el-descriptions-item>
        <el-descriptions-item label="大小">{{ volDetailData.size }} GB</el-descriptions-item>
        <el-descriptions-item label="类型"><el-tag :type="volDetailData.is_system ? 'danger' : 'info'" size="small">{{ volDetailData.is_system ? '系统盘' : '数据盘' }}</el-tag></el-descriptions-item>
        <el-descriptions-item label="挂载状态"><el-tag :type="volDetailData.is_mount ? 'success' : 'info'" size="small">{{ volDetailData.is_mount ? '已挂载' : '未挂载' }}</el-tag></el-descriptions-item>
        <el-descriptions-item label="状态"><el-tag :type="volDetailData.status === 'ready' ? 'success' : 'info'" size="small">{{ volDetailData.status === 'ready' ? '就绪' : (volDetailData.status || '-') }}</el-tag></el-descriptions-item>
        <el-descriptions-item label="宿主机ID">{{ volDetailData.host_id || '-' }}</el-descriptions-item>
        <el-descriptions-item label="宿主机卷ID">{{ volDetailData.host_volume_id || '-' }}</el-descriptions-item>
        <el-descriptions-item label="路径" :span="2"><span class="mono-text">{{ volDetailData.path || '-' }}</span></el-descriptions-item>
        <el-descriptions-item label="目标设备" v-if="volDetailData.target_device">{{ volDetailData.target_device }}</el-descriptions-item>
        <el-descriptions-item label="创建时间">{{ volDetailData.created_at ? new Date(Number(volDetailData.created_at.seconds) * 1000).toLocaleString('zh-CN') : '-' }}</el-descriptions-item>
        <el-descriptions-item label="更新时间">{{ volDetailData.updated_at ? new Date(Number(volDetailData.updated_at.seconds) * 1000).toLocaleString('zh-CN') : '-' }}</el-descriptions-item>
      </el-descriptions>
      <template #footer><el-button @click="volDetailVisible = false">关闭</el-button></template>
    </el-dialog>

    <!-- 绑定安全组到当前虚拟机 -->
    <SecurityGroupSelectorPopup v-model="sgBindVisible" :service-id="serviceId" @confirm="handleSgBindConfirm" @create="handleSgCreate" />

    <!-- 创建安全组弹窗 -->
    <el-dialog v-model="sgCreateDialogVisible" title="创建安全组" width="520px" destroy-on-close class="tk-dialog">
      <el-form ref="sgCreateFormRef" :model="sgCreateForm" :rules="sgCreateRules" label-width="100px" v-loading="dialogOptionsLoading">
        <div class="tk-section">
          <div class="tk-section-title">安全组信息</div>
          <el-form-item label="名称" prop="name">
            <el-input v-model="sgCreateForm.name" placeholder="安全组名称" />
          </el-form-item>
          <el-form-item label="方向" prop="direction">
            <el-select v-model="sgCreateForm.direction" placeholder="选择规则方向" style="width: 100%">
              <el-option label="入站 (in)" value="in" />
              <el-option label="出站 (out)" value="out" />
            </el-select>
          </el-form-item>
          <el-form-item label="宿主机" prop="host_id">
            <el-select v-model="sgCreateForm.host_id" placeholder="选择宿主机" filterable clearable style="width: 100%">
              <el-option v-for="h in sgHostOptions" :key="h.id" :label="`${h.name} (${h.ip || h.id})`" :value="h.id" />
            </el-select>
          </el-form-item>
          <el-form-item label="锁定">
            <el-switch v-model="sgCreateForm.lock" active-text="用户不可修改" />
          </el-form-item>
          <el-form-item label="白名单模式">
            <el-switch v-model="sgCreateForm.drop_all" active-text="开启白名单" />
          </el-form-item>
        </div>
      </el-form>
      <template #footer>
        <div class="tk-dialog-footer">
          <el-button @click="sgCreateDialogVisible = false">取消</el-button>
          <el-button type="primary" :loading="sgSubmitLoading" @click="submitSgCreate">确定</el-button>
        </div>
      </template>
    </el-dialog>

    <!-- 同步安全组弹窗 -->
    <el-dialog v-model="sgSyncDialogVisible" title="同步安全组到宿主机" width="480px" destroy-on-close class="tk-dialog">
      <el-form label-width="100px" v-loading="dialogOptionsLoading">
        <div class="tk-section">
          <div class="tk-section-title">同步配置</div>
          <el-form-item label="安全组">{{ sgSyncTarget?.name || '-' }}</el-form-item>
          <el-form-item label="目标宿主机">
            <el-select v-model="sgSyncHostId" placeholder="选择宿主机" filterable style="width: 100%">
              <el-option v-for="h in sgHostOptions" :key="h.id" :label="`${h.name} (${h.ip || h.id})`" :value="h.id" />
            </el-select>
          </el-form-item>
        </div>
      </el-form>
      <template #footer>
        <div class="tk-dialog-footer">
          <el-button @click="sgSyncDialogVisible = false">取消</el-button>
          <el-button type="primary" :loading="sgSubmitLoading" @click="submitSgSync">同步</el-button>
        </div>
      </template>
    </el-dialog>

    <!-- 安全组绑定/解绑虚拟机弹窗 -->
    <el-dialog v-model="sgVmBindDialogVisible" :title="sgVmBindType === 'bind' ? '绑定安全组到虚拟机' : '解绑安全组'" width="420px" destroy-on-close>
      <el-form label-width="100px">
        <el-form-item label="安全组">{{ sgVmBindTarget?.name || '-' }}</el-form-item>
        <el-form-item label="虚拟机">
          <el-input :model-value="detail ? `${detail.name || ''} (ID: ${vmId})` : '-'" disabled style="width: 100%" />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="sgVmBindDialogVisible = false">取消</el-button>
        <el-button :type="sgVmBindType === 'bind' ? 'primary' : 'warning'" :loading="sgSubmitLoading" @click="submitSgVmBind">
          {{ sgVmBindType === 'bind' ? '绑定' : '解绑' }}
        </el-button>
      </template>
    </el-dialog>

    <!-- 安全组详情+规则弹窗 -->
    <el-dialog v-model="sgDetailVisible" title="安全组详情 & 规则" width="800px" destroy-on-close>
      <el-descriptions :column="2" border v-if="sgCurrentDetail" v-loading="sgDetailLoading" style="margin-bottom: 20px">
        <el-descriptions-item label="ID" width="100px">{{ sgCurrentDetail.id }}</el-descriptions-item>
        <el-descriptions-item label="名称" width="100px">{{ sgCurrentDetail.name }}</el-descriptions-item>
        <el-descriptions-item label="锁定" width="100px">
          <el-tag :type="sgCurrentDetail.lock ? 'danger' : 'success'" size="small">{{ sgCurrentDetail.lock ? '是' : '否' }}</el-tag>
        </el-descriptions-item>
        <el-descriptions-item label="白名单" width="100px">
          <el-tag :type="sgCurrentDetail.drop_all ? 'warning' : 'info'" size="small">{{ sgCurrentDetail.drop_all ? '开启' : '关闭' }}</el-tag>
        </el-descriptions-item>
        <el-descriptions-item label="方向">
          <el-tag :type="sgCurrentDetail.direction === 'in' ? 'success' : 'warning'" size="small">{{ sgCurrentDetail.direction === 'in' ? '入站' : sgCurrentDetail.direction === 'out' ? '出站' : (sgCurrentDetail.direction || '-') }}</el-tag>
        </el-descriptions-item>
      </el-descriptions>
      <div class="rules-section">
        <div class="rules-header">
          <h4>安全组规则</h4>
          <el-button type="primary" size="small" @click="handleSgAddRule">新增规则</el-button>
        </div>
        <el-table :data="sgCurrentDetail?.rules || []" stripe size="small" style="width: 100%">
          <el-table-column prop="id" label="ID" width="60" />
          <el-table-column prop="protocol" label="协议" width="80">
            <template #default="{ row }">
              <el-tag size="small">{{ (row.protocol || '-').toUpperCase() }}</el-tag>
            </template>
          </el-table-column>
          <el-table-column prop="action" label="动作" width="80">
            <template #default="{ row }">
              <el-tag :type="row.action === 'allow' ? 'success' : 'danger'" size="small">{{ row.action === 'allow' ? '允许' : '拒绝' }}</el-tag>
            </template>
          </el-table-column>
          <el-table-column prop="port_range" label="端口范围" min-width="120" />
          <el-table-column prop="ip_range" label="IP 范围" min-width="140" />
          <el-table-column prop="priority" label="优先级" width="80" />
          <el-table-column label="操作" width="130">
            <template #default="{ row }">
              <el-button link type="primary" size="small" @click="handleSgEditRule(row)">编辑</el-button>
              <el-button link type="danger" size="small" @click="handleSgDeleteRule(row)">删除</el-button>
            </template>
          </el-table-column>
        </el-table>
      </div>
      <template #footer><el-button @click="sgDetailVisible = false">关闭</el-button></template>
    </el-dialog>

    <!-- 新增/编辑安全组规则弹窗 -->
    <el-dialog v-model="sgRuleDialogVisible" :title="sgRuleDialogType === 'add' ? '新增安全组规则' : '编辑安全组规则'" width="520px" destroy-on-close class="tk-dialog">
      <el-form ref="sgRuleFormRef" :model="sgRuleForm" :rules="sgRuleRules" label-width="100px">
        <div class="tk-section">
          <div class="tk-section-title">规则配置</div>
          <el-form-item label="协议" prop="protocol">
            <el-select v-model="sgRuleForm.protocol" style="width: 100%">
              <el-option label="TCP" value="tcp" />
              <el-option label="UDP" value="udp" />
            </el-select>
          </el-form-item>
          <el-form-item label="动作" prop="action">
            <el-select v-model="sgRuleForm.action" style="width: 100%">
              <el-option label="允许 (Allow)" value="allow" />
              <el-option label="拒绝 (Deny)" value="deny" />
            </el-select>
          </el-form-item>
          <el-form-item label="端口范围">
            <el-input v-model="sgRuleForm.port_range" placeholder="如 80 或 80-90" />
          </el-form-item>
          <el-form-item label="IP 范围">
            <el-input v-model="sgRuleForm.ip_range" placeholder="如 0.0.0.0/0 或 192.168.1.0/24" />
          </el-form-item>
          <el-form-item label="优先级">
            <el-input-number v-model="sgRuleForm.priority" :min="0" :max="9999" style="width: 100%" />
          </el-form-item>
        </div>
      </el-form>
      <template #footer>
        <div class="tk-dialog-footer">
          <el-button @click="sgRuleDialogVisible = false">取消</el-button>
          <el-button type="primary" :loading="sgSubmitLoading" @click="submitSgRule">确定</el-button>
        </div>
      </template>
    </el-dialog>

    <!-- 加入组网弹窗 -->
    <el-dialog v-model="vnJoinVisible" title="加入组网" width="480px" destroy-on-close class="tk-dialog">
      <el-form label-width="100px">
        <div class="tk-section">
          <div class="tk-section-title">组网信息</div>
          <el-form-item label="组网">{{ vnJoinTarget?.name || '-' }} (ID: {{ vnJoinTarget?.id }})</el-form-item>
          <el-form-item label="网桥"><span class="mono-text">{{ vnJoinTarget?.bridge_name || '-' }}</span></el-form-item>
          <el-form-item label="网关"><span class="mono-text">{{ vnJoinTarget?.gateway || '-' }}</span></el-form-item>
          <el-form-item label="指定IP">
            <el-input v-model="vnJoinIp" placeholder="留空自动分配" />
          </el-form-item>
        </div>
      </el-form>
      <template #footer>
        <div class="tk-dialog-footer">
          <el-button @click="vnJoinVisible = false">取消</el-button>
          <el-button type="primary" :loading="vnSubmitLoading" @click="submitJoinNetworking">加入</el-button>
        </div>
      </template>
    </el-dialog>

    <!-- 创建组网弹窗 -->
    <el-dialog v-model="vnCreateVisible" title="创建组网" width="500px" destroy-on-close class="tk-dialog">
      <el-form ref="vnCreateFormRef" :model="vnCreateForm" :rules="vnCreateRules" label-width="100px">
        <div class="tk-section">
          <div class="tk-section-title">组网配置</div>
          <el-form-item label="名称" prop="name">
            <el-input v-model="vnCreateForm.name" placeholder="组网名称" />
          </el-form-item>
          <el-form-item label="网桥名称" prop="bridge_name">
            <el-input v-model="vnCreateForm.bridge_name" placeholder="网桥名称" />
          </el-form-item>
          <el-form-item label="网关">
            <el-input v-model="vnCreateForm.gateway" placeholder="可选，如 10.0.0.1/24" />
          </el-form-item>
          <el-form-item label="宿主机">
            <span style="color: #606266">{{ vnCreateHostName || '加载中...' }} <span style="color: #909399">(ID: {{ vmHostId }})</span></span>
          </el-form-item>
          <el-form-item label="用户">
            <span style="color: #606266">{{ vnCreateUserName || '加载中...' }} <span style="color: #909399">(ID: {{ detail?.user_id || '-' }})</span></span>
          </el-form-item>
        </div>
      </el-form>
      <template #footer>
        <div class="tk-dialog-footer">
          <el-button @click="vnCreateVisible = false">取消</el-button>
          <el-button type="primary" :loading="vnSubmitLoading" @click="submitVnCreate">创建</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>

<script setup>
import { ref, reactive, computed, onMounted, onActivated, onDeactivated, onBeforeUnmount, nextTick, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ArrowLeft, Refresh, ArrowDown, Plus, Search, WarningFilled, Loading, Edit } from '@element-plus/icons-vue'
import {
  getVmDetail, getVmStatus, getVmResourceThresholdPolicy, updateVmResourceThresholdPolicy,
  startVm, stopVm, rebootVm, suspendVm, resumeVm,
  rebuildVm, refactorVm, updateVm, updateVmTraffic, rescueVm, exitRescueVm,
  getVmVnc, getVncNodeList,
  getSecurityGroupList, getUserNetworkingList, createUserNetworking, assignUserNetworking, removeUserNetworkingNetwork,
  getRemoteHostList, getSecurityGroupDetail, createSecurityGroup,
  syncSecurityGroup, deleteSecurityGroup,
  enableSecurityGroupWhitelist, disableSecurityGroupWhitelist,
  applySecurityGroup, createSecurityGroupRule, updateSecurityGroupRule, deleteSecurityGroupRule,
  createNetwork, updateNetwork, deleteNetwork,
  createVolume, resizeVolume, mountVolume, unmountVolume, transferVolume, deleteVolume,
  getVmList, bindSecurityGroup, unbindSecurityGroup,
  getSnapshotList, createSnapshot, restoreSnapshot, deleteSnapshot, getSnapshotProgress, getSnapshotCount, setSnapshotLimit,
  getBackupList, createBackup, restoreBackup, deleteBackup, getBackupProgress, getBackupCount, setBackupLimit,
  migrateVm, getRemoteHostGroupList, getRemoteHostDetail,
  dataMigrateVm, getDataMigrateProgress, abortDataMigrate,
  getKvmServiceList, getMetricsHistory, getNetworkList,
  getVmTrafficPolicy, updateVmTrafficPolicy, addVmFixedTraffic, addVmTemporaryTraffic,
  setNetworkPrimary, resetVmMac,
  disconnectVmNetwork, connectVmNetwork, getVmTrafficHourly
} from '@/api/admin/kvmService'
import { getUserInfo } from '@/api/admin/user'
import { extractApiError } from '@/utils/kvmErrorUtil'
import { vmStatusLabel as vmStatusLabelUtil, vmStatusType as vmStatusTypeUtil, volumeStatusLabel, volumeStatusType } from '@/utils/tool'
import * as echarts from 'echarts'
import ImageSelectorPopup from '@/components/admin/ImageSelectorPopup.vue'
import NetworkSelectorPopup from '@/components/admin/NetworkSelectorPopup.vue'
import SecurityGroupSelectorPopup from '@/components/admin/SecurityGroupSelectorPopup.vue'
import VolumeSelectorPopup from '@/components/admin/VolumeSelectorPopup.vue'
import VmSelectorPopup from '@/components/admin/VmSelectorPopup.vue'
import { useTagsViewStore } from '@/store/tagsViewStore'

const route = useRoute()
const router = useRouter()
const tagsViewStore = useTagsViewStore()

const serviceId = computed(() => parseInt(route.query.service_id) || 0)
const serviceName = computed(() => route.query.service_name || '')
const vmId = computed(() => parseInt(route.query.vm_id) || 0)
const goMigrationRecords = (query = {}) => router.push({ path: '/virtualization/migration-record', query })
const goVmById = (targetServiceId, targetVmId) => router.push({ path: '/virtualization/vm-detail', query: { service_id: targetServiceId, vm_id: targetVmId } })

const loading = ref(false)
const actionLoading = ref(false)
const statusLoading = ref(false)
const dialogOptionsLoading = ref(false)
const detail = ref(null)
const vmHostId = ref(0)
const vmNetworks = ref([])
const vmVolumes = ref([])
const vmImage = ref(null)

const vmPortGroup = ref(null)
const vmOutPortGroup = ref(null)
const hostOptions = ref([])
const rebuildDialogVisible = ref(false)
const rebuildImageId = ref(0)
const rebuildImageName = ref('')
const showImageSelector = ref(false)
const activeTab = ref('info')
const showPassword = ref(false)

const thresholdResources = [
  { key: 'cpu', label: 'CPU' },
  { key: 'memory', label: '内存' },
  { key: 'bandwidth', label: '带宽' }
]
const emptyThresholdPolicy = () => Object.fromEntries(thresholdResources.flatMap(({ key }) => [
  [`${key}_threshold_percent`, 0], [`${key}_threshold_duration_seconds`, 0]
]))
const resourceThresholdPolicy = computed(() => detail.value?.resource_threshold_policy || emptyThresholdPolicy())
const isThresholdEnabled = (policy, key) => Number(policy?.[`${key}_threshold_percent`] || 0) > 0 && Number(policy?.[`${key}_threshold_duration_seconds`] || 0) > 0
const hasEnabledThreshold = computed(() => thresholdResources.some(({ key }) => isThresholdEnabled(resourceThresholdPolicy.value, key)))
const formatThreshold = (policy, key) => `${Number(policy?.[`${key}_threshold_percent`] || 0)}% / ${Number(policy?.[`${key}_threshold_duration_seconds`] || 0)}秒`
const pauseReasonLabel = (code) => ({ manual: '管理员手动暂停', resource_threshold: '资源阈值触发暂停', external: '外部原因暂停' }[code] || '虚拟机已暂停')
const formatPauseViolations = (pauseInfo) => {
  const labels = { cpu: 'CPU', memory: '内存', bandwidth: '带宽' }
  const directions = { rx: '下行', tx: '上行' }
  const violations = pauseInfo?.violations || []
  if (!violations.length) return pauseInfo?.paused_at ? `暂停时间：${formatTimestamp(pauseInfo.paused_at)}` : ''
  return violations.map(v => `${labels[v.resource] || v.resource}${directions[v.direction] ? `（${directions[v.direction]}）` : ''}：实际 ${v.observed_percent}% / 阈值 ${v.threshold_percent}%，持续 ${v.duration_seconds} 秒`).join('；')
}

const thresholdDialogVisible = ref(false)
const thresholdLoading = ref(false)
const thresholdSubmitLoading = ref(false)
const thresholdFormRef = ref(null)
const thresholdForm = reactive(emptyThresholdPolicy())
const validateThresholdPair = (rule, value, callback) => {
  const key = rule.field.replace(/_threshold_(percent|duration_seconds)$/, '')
  const percent = Number(thresholdForm[`${key}_threshold_percent`] || 0)
  const duration = Number(thresholdForm[`${key}_threshold_duration_seconds`] || 0)
  if ((percent === 0) !== (duration === 0)) callback(new Error('阈值与持续时间必须同时填写或同时为 0'))
  else callback()
}
const thresholdRules = {}
thresholdResources.forEach(({ key }) => {
  thresholdRules[`${key}_threshold_percent`] = [{ validator: validateThresholdPair, trigger: 'change' }]
  thresholdRules[`${key}_threshold_duration_seconds`] = [{ validator: validateThresholdPair, trigger: 'change' }]
})

const openThresholdDialog = async () => {
  thresholdDialogVisible.value = true
  thresholdLoading.value = true
  try {
    const res = await getVmResourceThresholdPolicy({ service_id: serviceId.value, vm_id: vmId.value })
    if (res?.data?.code !== 200) throw new Error(extractApiError(res?.data, '加载阈值策略失败'))
    Object.assign(thresholdForm, emptyThresholdPolicy(), res.data.data || {})
  } catch (e) {
    Object.assign(thresholdForm, emptyThresholdPolicy(), resourceThresholdPolicy.value)
    ElMessage.error(e?.message || extractApiError(e?.response?.data, '加载阈值策略失败'))
  } finally { thresholdLoading.value = false }
}

const submitThresholdPolicy = () => {
  thresholdFormRef.value?.validate(async valid => {
    if (!valid) return
    thresholdSubmitLoading.value = true
    try {
      const fd = new FormData()
      fd.append('service_id', serviceId.value)
      fd.append('vm_id', vmId.value)
      thresholdResources.forEach(({ key }) => {
        fd.append(`${key}_threshold_percent`, thresholdForm[`${key}_threshold_percent`])
        fd.append(`${key}_threshold_duration_seconds`, thresholdForm[`${key}_threshold_duration_seconds`])
      })
      const res = await updateVmResourceThresholdPolicy(fd)
      if (res?.data?.code !== 200) throw new Error(extractApiError(res?.data, '保存阈值策略失败'))
      if (detail.value) detail.value.resource_threshold_policy = res.data.data || { ...thresholdForm }
      ElMessage.success('资源阈值策略已更新')
      thresholdDialogVisible.value = false
    } catch (e) { ElMessage.error(e?.message || extractApiError(e?.response?.data, '保存阈值策略失败')) }
    finally { thresholdSubmitLoading.value = false }
  })
}

const isWindows = computed(() => vmImage.value?.os_type === 'windows')

const extractIp = (addr) => addr ? addr.split('/')[0] : ''
const publicIpList = computed(() => vmNetworks.value.filter(n => n.type === 'bridge').map(n => extractIp(n.address)).filter(Boolean))
const privateIpList = computed(() => vmNetworks.value.filter(n => n.type === 'nat').map(n => extractIp(n.address)).filter(Boolean))
const publicIps = computed(() => publicIpList.value.join(', '))
const privateIps = computed(() => privateIpList.value.join(', '))

const isVolumeMounted = (row) => {
  if (row.is_mount !== undefined) return !!row.is_mount
  return row.status === 'ready'
}

const formatTrafficMax = (val) => {
  if (val == null) return '-'
  const gb = val / 1024
  if (gb >= 1024) return `${(gb / 1024).toFixed(2)} TB`
  return `${gb.toFixed(2)} GB`
}

// 流量分段展示：以 MB 输入，自适应输出单位；与 formatTrafficMax 区别是允许 0 / 小数值精简显示
const formatTrafficMb = (mb) => {
  const n = Number(mb)
  if (!Number.isFinite(n) || n <= 0) return '0 MB'
  if (n >= 1024 * 1024) return `${(n / 1024 / 1024).toFixed(2)} TB`
  if (n >= 1024) return `${(n / 1024).toFixed(2)} GB`
  return `${n} MB`
}

// 概览流量段：优先使用 add.json 新字段 traffic_max_mb / temporary_traffic_mb，fallback 旧字段 traffic_max
// traffic_used 单位假设为 MB（与 traffic_max_mb 同维度），如后端实为字节需调整
const detailTrafficBaseMb = computed(() => Number(detail.value?.traffic_max_mb ?? detail.value?.traffic_max ?? 0) || 0)
const detailTrafficTempMb = computed(() => Number(detail.value?.temporary_traffic_mb || 0))
const detailTrafficTotalMb = computed(() => detailTrafficBaseMb.value + detailTrafficTempMb.value)
const detailTrafficUsedMb = computed(() => Number(detail.value?.traffic_used || 0))
const detailTrafficCycleStartText = computed(() => {
  const t = detail.value?.temporary_cycle_start
  const sec = typeof t === 'object' ? (t?.seconds ?? null) : t
  if (!sec) return ''
  const d = new Date(Number(sec) * 1000)
  if (isNaN(d.getTime())) return ''
  const yyyy = d.getFullYear()
  const mm = String(d.getMonth() + 1).padStart(2, '0')
  const dd = String(d.getDate()).padStart(2, '0')
  return `${yyyy}-${mm}-${dd}`
})

const copyAllIps = (ipList) => {
  if (!ipList?.length) return
  const text = ipList.join('\n')
  if (navigator.clipboard && window.isSecureContext) {
    navigator.clipboard.writeText(text).then(() => ElMessage.success(`已复制 ${ipList.length} 个IP`)).catch(() => fallbackCopy(text))
  } else {
    fallbackCopy(text)
  }
}

const networkPage = ref(1)
const networkPageSize = ref(10)
const pagedNetworks = computed(() => {
  const start = (networkPage.value - 1) * networkPageSize.value
  return vmNetworks.value.slice(start, start + networkPageSize.value)
})
const volumePage = ref(1)
const volumePageSize = ref(10)
const pagedVolumes = computed(() => {
  const start = (volumePage.value - 1) * volumePageSize.value
  return vmVolumes.value.slice(start, start + volumePageSize.value)
})

const copyText = (text) => {
  if (!text) { ElMessage.warning('无内容可复制'); return }
  if (navigator.clipboard && window.isSecureContext) {
    navigator.clipboard.writeText(text).then(() => ElMessage.success('已复制到剪贴板')).catch(() => fallbackCopy(text))
  } else {
    fallbackCopy(text)
  }
}
const fallbackCopy = (text) => {
  const ta = document.createElement('textarea')
  ta.value = text
  ta.style.cssText = 'position:fixed;left:-9999px;top:-9999px;opacity:0'
  document.body.appendChild(ta)
  ta.select()
  try {
    document.execCommand('copy')
    ElMessage.success('已复制到剪贴板')
  } catch { ElMessage.error('复制失败，请手动复制') }
  document.body.removeChild(ta)
}

const handleMoreCommand = (cmd) => {
  const powerActions = ['start', 'stop', 'reboot', 'suspend', 'resume']
  if (powerActions.includes(cmd)) { handlePower(cmd); return }
  const actionMap = {
    editVm: handleEditVm, refactorVm: handleRefactorVm, updateTraffic: handleUpdateTraffic,
    rebuild: handleRebuild, rescue: handleRescue, exitRescue: handleExitRescue,
    migrateVm: handleMigrateVm, resetMac: handleResetMac,
    disconnectNetwork: handleDisconnectNetwork, connectNetwork: handleConnectNetwork
  }
  if (actionMap[cmd]) actionMap[cmd]()
  if (cmd === 'dataMigrateVm') handleDataMigrateVm()
  if (cmd === 'dataMigrateProgress') { dataMigrateProgressVisible.value = true; loadDataMigrateProgress() }
  if (cmd === 'abortDataMigrate') handleAbortMigrate()
}

const vmStatusType = (s) => vmStatusTypeUtil(s)
const vmStatusLabel = (s) => vmStatusLabelUtil(s)
const imgStatusType = (s) => ({ ready: 'success', downloading: 'warning', pending: 'info', error: 'danger' }[s] || 'info')
const imgStatusLabel = (s) => ({ ready: '就绪', downloading: '下载中', pending: '等待中', error: '错误' }[s] || s || '-')

const formatMemory = (kb) => { if (!kb) return '-'; kb = Number(kb); if (kb >= 1073741824) return (kb / 1073741824).toFixed(1) + ' TB'; if (kb >= 1048576) return (kb / 1048576).toFixed(1) + ' GB'; if (kb >= 1024) return (kb / 1024).toFixed(0) + ' MB'; return kb + ' KB' }
const formatNetSpeed = (bytes) => { if (bytes == null) return '-'; const n = Number(bytes); if (n >= 1073741824) return (n / 1073741824).toFixed(2) + ' GB/s'; if (n >= 1048576) return (n / 1048576).toFixed(2) + ' MB/s'; if (n >= 1024) return (n / 1024).toFixed(2) + ' KB/s'; return n + ' B/s' }
const formatTimestamp = (ts) => {
  if (!ts) return '-'
  if (typeof ts === 'object' && ts.seconds) return new Date(Number(ts.seconds) * 1000).toLocaleString('zh-CN')
  if (typeof ts === 'string' || typeof ts === 'number') { const d = new Date(ts); return isNaN(d.getTime()) ? String(ts) : d.toLocaleString('zh-CN') }
  return '-'
}
const getHostLabel = (hid) => { const h = hostOptions.value.find(x => x.id === hid); return h ? h.name : (hid || '-') }

const loadHostOptions = async () => {
  try {
    const res = await getRemoteHostList({ service_id: serviceId.value, page: 1, page_size: 10 })
    if (res?.data?.code === 200 && res?.data?.data) {
      const inner = res.data.data
      hostOptions.value = Array.isArray(inner) ? inner : (inner.hosts || inner.list || inner.data || [])
    }
  } catch { /* */ }
}

const loadDetail = async () => {
  if (!vmId.value) return
  loading.value = true
  try {
    const res = await getVmDetail({ service_id: serviceId.value, vm_id: vmId.value })
    if (res?.data?.code === 200 && res?.data?.data) {
      const d = res.data.data
      detail.value = d.data ?? d.vm ?? d
      vmNetworks.value = d.networks || []
      vmVolumes.value = d.volumes || []
      if (d.image) {
        vmImage.value = d.image
      } else {
        const sysVol = (d.volumes || []).find(v => v.is_system)
        if (sysVol) {
          const name = sysVol.name || ''
          const osType = /windows/i.test(name) ? 'windows' : 'linux'
          vmImage.value = { id: 0, name, os_type: osType, status: sysVol.status }
        } else {
          vmImage.value = null
        }
      }
      vmPortGroup.value = d.in_port_group || null
      vmOutPortGroup.value = d.out_port_group || null
      vmHostId.value = detail.value?.host_id || vmVolumes.value[0]?.host_id || vmNetworks.value[0]?.host_id || 0
      handleDetailMigrateState()
    } else ElMessage.error(extractApiError(res?.data, '加载失败'))
  } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '加载失败')) } finally { loading.value = false }
}

const handleDetailMigrateState = () => {
  const vm = detail.value
  if (!vm) return
  if (vm.migrating && vm.migrate_task_id) {
    dataMigrateTaskId.value = vm.migrate_task_id
    if (!dataMigrationId.value) dataMigrationId.value = vm.migrate_task_id
    if (!migratePollingTimer) {
      loadDataMigrateProgress()
      startMigratePolling()
    }
    startDetailAutoRefresh()
  } else if (!vm.migrating) {
    stopDetailAutoRefresh()
    if (migratePollingTimer) {
      stopMigratePolling()
      dataMigrateProgressData.value = null
      dataMigrationId.value = ''
      dataMigrateTaskId.value = ''
    }
  }
}

const fetchVmStatus = async () => {
  if (!detail.value) return
  statusLoading.value = true
  try {
    const res = await getVmStatus({ service_id: serviceId.value, vm_id: vmId.value })
    if (res?.data?.code === 200 && res?.data?.data) {
      const outer = res.data.data
      const inner = outer.data ?? outer
      const state = inner.state ?? inner.status ?? inner
      const desc = inner.desc || ''
      detail.value = { ...detail.value, status: state }
      ElMessage.success(`状态已刷新: ${desc || vmStatusLabel(state)}`)
    }
  } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '获取状态失败')) } finally { statusLoading.value = false }
}

// ---- ECharts 监控 ----
const cpuChartRef = ref(null)
const memChartRef = ref(null)
const netChartRef = ref(null)
const diskChartRef = ref(null)
const diskIopsChartRef = ref(null)
const trafficUsedChartRef = ref(null)
let cpuChart = null
let memChart = null
let netChart = null
let diskChart = null
let diskIopsChart = null
let trafficUsedChart = null
let isPageActive = false

const historicalMetricsData = ref(null)
const historicalMetricsLoading = ref(false)

const monitorTimeMode = ref('relative')
const monitorRelativeMinutes = ref(10)
const monitorDateRange = ref(null)

const getMonitorTimeRange = () => {
  if (monitorTimeMode.value === 'relative') {
    const endTime = new Date()
    const startTime = new Date(endTime - monitorRelativeMinutes.value * 60 * 1000)
    return { startTime, endTime }
  } else {
    if (!monitorDateRange.value || monitorDateRange.value.length < 2) return null
    return { startTime: new Date(monitorDateRange.value[0]), endTime: new Date(monitorDateRange.value[1]) }
  }
}

function calcInterval(startTime, endTime) {
  const spanMin = (endTime.getTime() - startTime.getTime()) / 60000
  if (spanMin < 30) return '1m'
  if (spanMin < 60) return '3m'
  if (spanMin < 360) return '5m'
  if (spanMin < 720) return '15m'
  if (spanMin < 1440) return '30m'
  if (spanMin < 4320) return '1h'
  if (spanMin < 10080) return '2h'
  if (spanMin < 43200) return '6h'
  if (spanMin < 129600) return '12h'
  return '1d'
}

const intervalLabelMap = { '1m': '1分钟', '3m': '3分钟', '5m': '5分钟', '15m': '15分钟', '30m': '30分钟', '1h': '1小时', '2h': '2小时', '6h': '6小时', '12h': '12小时', '1d': '1天' }
const currentIntervalLabel = computed(() => {
  const range = getMonitorTimeRange()
  if (!range) return '-'
  const iv = calcInterval(range.startTime, range.endTime)
  return intervalLabelMap[iv] || iv
})

const latestMetrics = computed(() => {
  const arr = historicalMetricsData.value
  if (!Array.isArray(arr) || !arr.length) return null
  return arr[arr.length - 1]
})

const formatBytesRaw = (val) => {
  if (!val && val !== 0) return '-'; val = Number(val)
  if (val >= 1099511627776) return (val / 1099511627776).toFixed(2) + ' TB'
  if (val >= 1073741824) return (val / 1073741824).toFixed(2) + ' GB'
  if (val >= 1048576) return (val / 1048576).toFixed(2) + ' MB'
  if (val >= 1024) return (val / 1024).toFixed(1) + ' KB'
  return val + ' B'
}

const formatNetLabel = (v) => {
  if (!v) return '0 B/s'
  if (v >= 1073741824) return (v / 1073741824).toFixed(1) + ' GB/s'
  if (v >= 1048576) return (v / 1048576).toFixed(1) + ' MB/s'
  if (v >= 1024) return (v / 1024).toFixed(1) + ' KB/s'
  return v + ' B/s'
}

const formatMemKB = (kb) => {
  if (!kb && kb !== 0) return '-'
  kb = Number(kb)
  if (kb >= 1073741824) return (kb / 1073741824).toFixed(1) + ' TB'
  if (kb >= 1048576) return (kb / 1048576).toFixed(1) + ' GB'
  if (kb >= 1024) return (kb / 1024).toFixed(0) + ' MB'
  return kb + ' KB'
}

const vmMemPercent = (m) => {
  if (!m || !m.mem_total) return '0.0'
  return ((m.mem_used / m.mem_total) * 100).toFixed(1)
}

const loadHistoricalMetrics = async () => {
  if (!serviceId.value || !detail.value?.name) return
  const range = getMonitorTimeRange()
  if (!range) return
  historicalMetricsLoading.value = true
  try {
    const { startTime, endTime } = range
    const interval = calcInterval(startTime, endTime)
    const params = {
      service_id: serviceId.value,
      host_id: vmHostId.value,
      vm_name: detail.value.name,
      start: startTime.toISOString(),
      end_time: endTime.toISOString(),
      interval
    }
    const res = await getMetricsHistory(params)
    const body = res?.data
    if (body?.code === 200 && body?.data) {
      historicalMetricsData.value = Array.isArray(body.data) ? body.data : (body.data.data || [])
      await nextTick()
      renderHistoricalCharts()
    } else {
      ElMessage.error(extractApiError(body, '加载监控数据失败'))
    }
  } catch (e) {
    ElMessage.error(extractApiError(e?.response?.data, '加载监控数据失败'))
  } finally {
    historicalMetricsLoading.value = false
  }
}

const renderHistoricalCharts = () => {
  const metrics = historicalMetricsData.value
  if (!Array.isArray(metrics) || !metrics.length) return

  const range = getMonitorTimeRange()
  const spanMs = range ? (range.endTime.getTime() - range.startTime.getTime()) : 0
  const showDate = spanMs >= 12 * 3600 * 1000
  const symbolType = showDate ? 'circle' : 'none'
  const labelRotate = showDate ? 45 : 0

  const times = metrics.map(m => {
    const date = new Date(m.bucket)
    if (showDate) return date.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })
    return date.toLocaleTimeString('zh-CN', { hour12: false, hour: '2-digit', minute: '2-digit' })
  })

  const cpuData = metrics.map(m => m.cpu_usage ?? 0)
  const memData = metrics.map(m => m.mem_used ?? 0)
  const memTotal = Math.max(...metrics.map(m => m.mem_total ?? 0))
  const memAxisFmt = memTotal >= 1048576
    ? (v) => (v / 1048576).toFixed(1) + ' GB'
    : memTotal >= 1024
      ? (v) => (v / 1024).toFixed(0) + ' MB'
      : (v) => v + ' KB'

  const diskReadData = metrics.map(m => m.disk_read ?? 0)
  const diskWriteData = metrics.map(m => m.disk_write ?? 0)

  const diskReadRate = []
  const diskWriteRate = []
  for (let i = 0; i < metrics.length; i++) {
    if (i === 0) { diskReadRate.push(0); diskWriteRate.push(0); continue }
    const dt = (new Date(metrics[i].bucket) - new Date(metrics[i - 1].bucket)) / 1000
    if (dt > 0) {
      diskReadRate.push(+Math.max(0, ((metrics[i].disk_read ?? 0) - (metrics[i - 1].disk_read ?? 0)) / dt / 1024).toFixed(2))
      diskWriteRate.push(+Math.max(0, ((metrics[i].disk_write ?? 0) - (metrics[i - 1].disk_write ?? 0)) / dt / 1024).toFixed(2))
    } else { diskReadRate.push(0); diskWriteRate.push(0) }
  }

  const netRxData = metrics.map(m => m.net_rx ?? 0)
  const netTxData = metrics.map(m => m.net_tx ?? 0)

  const baseGrid = { top: 10, right: 16, bottom: 24, left: 50 }
  const makeXAxis = () => ({ type: 'category', data: times, boundaryGap: false, axisLabel: { fontSize: 10, rotate: labelRotate } })
  const makeSeries = (name, data, color) => ({ name, type: 'line', smooth: true, symbol: symbolType, areaStyle: { opacity: 0.15 }, lineStyle: { width: 2, color }, itemStyle: { color }, data })

  if (cpuChartRef.value) {
    if (!cpuChart) cpuChart = echarts.init(cpuChartRef.value)
    cpuChart.setOption({
      tooltip: { trigger: 'axis', formatter: (p) => `${p[0].axisValue}<br/>${p[0].marker} CPU: ${p[0].value.toFixed(1)}%` },
      grid: baseGrid, xAxis: makeXAxis(),
      yAxis: { type: 'value', min: 0, max: 100, axisLabel: { fontSize: 10, formatter: v => v + '%' } },
      series: [makeSeries('CPU', cpuData, '#409eff')]
    }, true)
  }

  if (memChartRef.value) {
    if (!memChart) memChart = echarts.init(memChartRef.value)
    memChart.setOption({
      tooltip: { trigger: 'axis', formatter: (p) => `${p[0].axisValue}<br/>${p[0].marker} 内存: ${formatMemKB(p[0].value)}` },
      grid: { ...baseGrid, left: 60 }, xAxis: makeXAxis(),
      yAxis: { type: 'value', min: 0, max: memTotal || undefined, axisLabel: { fontSize: 10, formatter: memAxisFmt } },
      series: [makeSeries('内存', memData, '#67c23a')]
    }, true)
  }

  if (diskChartRef.value) {
    if (!diskChart) diskChart = echarts.init(diskChartRef.value)
    diskChart.setOption({
      tooltip: { trigger: 'axis', formatter: (params) => {
        let s = params[0].axisValue
        params.forEach(p => { s += `<br/>${p.marker} ${p.seriesName}: ${formatBytesRaw(p.value)}` })
        return s
      }},
      grid: baseGrid, xAxis: makeXAxis(),
      yAxis: { type: 'value', min: 0, axisLabel: { fontSize: 10, formatter: v => formatBytesRaw(v) } },
      series: [makeSeries('读取', diskReadData, '#409eff'), makeSeries('写入', diskWriteData, '#e6a23c')]
    }, true)
  }

  if (diskIopsChartRef.value) {
    if (!diskIopsChart) diskIopsChart = echarts.init(diskIopsChartRef.value)
    diskIopsChart.setOption({
      tooltip: { trigger: 'axis', formatter: (params) => {
        let s = params[0].axisValue
        params.forEach(p => { s += `<br/>${p.marker} ${p.seriesName}: ${formatBytesRaw(p.value)}/s` })
        return s
      }},
      grid: baseGrid, xAxis: makeXAxis(),
      yAxis: { type: 'value', min: 0, axisLabel: { fontSize: 10, formatter: v => formatBytesRaw(v) + '/s' } },
      series: [makeSeries('读取', diskReadRate, '#409eff'), makeSeries('写入', diskWriteRate, '#e6a23c')]
    }, true)
  }

  if (netChartRef.value) {
    if (!netChart) netChart = echarts.init(netChartRef.value)
    netChart.setOption({
      tooltip: { trigger: 'axis', formatter: (params) => {
        let s = params[0].axisValue
        params.forEach(p => { s += `<br/>${p.marker} ${p.seriesName}: ${formatNetLabel(p.value)}` })
        return s
      }},
      grid: baseGrid, xAxis: makeXAxis(),
      yAxis: { type: 'value', min: 0, axisLabel: { fontSize: 10, formatter: v => formatNetLabel(v) } },
      series: [makeSeries('接收', netRxData, '#409eff'), makeSeries('发送', netTxData, '#e6a23c')]
    }, true)
  }

  if (trafficUsedChartRef.value) {
    if (!trafficUsedChart) trafficUsedChart = echarts.init(trafficUsedChartRef.value)
    const trafficDelta = []
    for (let i = 0; i < metrics.length; i++) {
      if (i === 0) { trafficDelta.push(0); continue }
      const delta = Math.max(0, (metrics[i].traffic_used_mb ?? 0) - (metrics[i - 1].traffic_used_mb ?? 0))
      trafficDelta.push(+delta.toFixed(2))
    }
    trafficUsedChart.setOption({
      tooltip: { trigger: 'axis', formatter: (p) => `${p[0].axisValue}<br/>${p[0].marker} 流量增量: ${p[0].value} MB` },
      grid: baseGrid, xAxis: makeXAxis(),
      yAxis: { type: 'value', min: 0, axisLabel: { fontSize: 10, formatter: v => v + ' MB' } },
      series: [makeSeries('流量增量', trafficDelta, '#722ed1')]
    }, true)
  }
}

const disposeCharts = () => {
  cpuChart?.dispose(); cpuChart = null
  memChart?.dispose(); memChart = null
  netChart?.dispose(); netChart = null
  diskChart?.dispose(); diskChart = null
  diskIopsChart?.dispose(); diskIopsChart = null
  trafficUsedChart?.dispose(); trafficUsedChart = null
  trafficHourlyChart?.dispose(); trafficHourlyChart = null
}

const powerDialogVisible = ref(false)
const powerAction = ref('')
const powerForce = ref(false)
const powerLabels = { start: '启动', stop: '停止', reboot: '重启', suspend: '暂停', resume: '恢复' }

const handlePower = (action) => {
  powerAction.value = action
  powerForce.value = false
  powerDialogVisible.value = true
}

const submitPower = async () => {
  const action = powerAction.value
  const label = powerLabels[action]
  powerDialogVisible.value = false
  try {
    const apis = { start: startVm, stop: stopVm, reboot: rebootVm, suspend: suspendVm, resume: resumeVm }
    const fd = new FormData()
    fd.append('service_id', serviceId.value)
    fd.append('vm_id', vmId.value)
    if (powerForce.value) fd.append('force', true)
    const res = await apis[action](fd)
    if (res?.data?.code === 200) { ElMessage.success(`${label}成功`); loadDetail() }
    else ElMessage.error(extractApiError(res?.data, `${label}失败`))
  } catch (e) { ElMessage.error(extractApiError(e?.response?.data, `${label}失败`)) }
}

const handleRebuild = () => {
  rebuildImageId.value = detail.value?.image_id || 0
  rebuildImageName.value = ''
  rebuildDialogVisible.value = true
}

const submitRebuild = async () => {
  if (!rebuildImageId.value) { ElMessage.warning('请选择镜像'); return }
  actionLoading.value = true
  try {
    const fd = new FormData()
    fd.append('service_id', serviceId.value)
    fd.append('vm_id', vmId.value)
    fd.append('image_id', rebuildImageId.value)
    const res = await rebuildVm(fd)
    if (res?.data?.code === 200) { ElMessage.success('重装成功'); rebuildDialogVisible.value = false; loadDetail() }
    else ElMessage.error(extractApiError(res?.data, '重装失败'))
  } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '重装失败')) } finally { actionLoading.value = false }
}

const handleRescue = () => {
  ElMessageBox.confirm(`确定让虚拟机「${detail.value?.name}」进入救援模式吗？`, '救援模式', {
    confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning'
  }).then(async () => {
    const fd = new FormData(); fd.append('service_id', serviceId.value); fd.append('vm_id', vmId.value)
    try {
      const res = await rescueVm(fd)
      if (res?.data?.code === 200) { ElMessage.success('已进入救援模式'); loadDetail() }
      else ElMessage.error(extractApiError(res?.data, '操作失败'))
    } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '操作失败')) }
  }).catch(() => {})
}

const handleExitRescue = () => {
  ElMessageBox.confirm(`确定让虚拟机「${detail.value?.name}」退出救援模式吗？`, '退出救援', {
    confirmButtonText: '确定', cancelButtonText: '取消', type: 'info'
  }).then(async () => {
    const fd = new FormData(); fd.append('service_id', serviceId.value); fd.append('vm_id', vmId.value)
    try {
      const res = await exitRescueVm(fd)
      if (res?.data?.code === 200) { ElMessage.success('已退出救援模式'); loadDetail() }
      else ElMessage.error(extractApiError(res?.data, '操作失败'))
    } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '操作失败')) }
  }).catch(() => {})
}

// ---- 编辑虚拟机（仅 update API 参数） ----
const editDialogVisible = ref(false)
const editFormRef = ref(null)
const editForm = reactive({
  rx_bandwidth: 0, tx_bandwidth: 0, data_volume_size: 0,
  root_password: '', ssh_port: 22,
  port_group_id: ''
})
const editSelectedNetworks = ref([])
const showEditNetworkSelector = ref(false)
const editSelectedInternalNetworks = ref([])
const showEditInternalNetworkSelector = ref(false)

const handleEditNetworkConfirm = (network) => {
  if (!editSelectedNetworks.value.some(n => n.id === network.id)) {
    editSelectedNetworks.value = [...editSelectedNetworks.value, { id: network.id, name: network.name }]
  }
}
const removeEditNetwork = (id) => {
  editSelectedNetworks.value = editSelectedNetworks.value.filter(n => n.id !== id)
}
const handleEditInternalNetworkConfirm = (network) => {
  editSelectedInternalNetworks.value = [{ id: network.id, name: network.name }]
}
const removeEditInternalNetwork = (id) => {
  editSelectedInternalNetworks.value = editSelectedInternalNetworks.value.filter(n => n.id !== id)
}

const handleEditVm = async () => {
  if (!detail.value) return
  const d = detail.value
  Object.assign(editForm, {
    rx_bandwidth: d.rx_bandwidth || 0,
    tx_bandwidth: d.tx_bandwidth || 0,
    data_volume_size: d.data_volume_size || 0,
    root_password: d.root_password || '',
    ssh_port: d.ssh_port || 22,
    port_group_id: vmPortGroup.value?.id || ''
  })
  const bridgeNets = vmNetworks.value.filter(n => n.type === 'bridge')
  const natNets = vmNetworks.value.filter(n => n.type === 'nat')
  editSelectedNetworks.value = bridgeNets.map(n => ({ id: n.id, name: n.name }))
  editSelectedInternalNetworks.value = natNets.length ? [{ id: natNets[0].id, name: natNets[0].name }] : []
  editDialogVisible.value = true
  dialogOptionsLoading.value = true
  try {
    await Promise.all([
      !sgOptions.value.length ? loadSgOptions() : Promise.resolve()
    ])
  } finally { dialogOptionsLoading.value = false }
}

const submitEditVm = async () => {
  actionLoading.value = true
  try {
    const fd = new FormData()
    fd.append('service_id', serviceId.value)
    fd.append('vm_id', vmId.value)
    fd.append('rx_bandwidth', editForm.rx_bandwidth)
    fd.append('tx_bandwidth', editForm.tx_bandwidth)
    if (editForm.data_volume_size > 0) fd.append('data_volume_size', editForm.data_volume_size)
    if (editForm.root_password) fd.append('root_password', editForm.root_password)
    fd.append('ssh_port', editForm.ssh_port)
    editSelectedNetworks.value.forEach(n => fd.append('network_ids', n.id))
    if (editSelectedInternalNetworks.value.length) fd.append('internet_network_id', editSelectedInternalNetworks.value[0].id)
    if (editForm.port_group_id) fd.append('port_group_id', editForm.port_group_id)
    const res = await updateVm(fd)
    if (res?.data?.code === 200) { ElMessage.success('修改成功'); editDialogVisible.value = false; loadDetail() }
    else ElMessage.error(extractApiError(res?.data, '修改失败'))
  } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '修改失败')) } finally { actionLoading.value = false }
}

// ---- 重构虚拟机 ----
const refactorDialogVisible = ref(false)
const refactorFormRef = ref(null)
const refactorForm = reactive({
  memory: 0, vcpu: 0, rx_bandwidth: 0, tx_bandwidth: 0,
  root_password: '', uuid: '', mate_data_id: '', physical_name: '', config_path: '',
  ssh_port: 0, vnc_port: 0, vnc_password: '',
  port_group_id: ''
})
const refactorMemUnit = ref(1048576)
const refactorMemDisplay = ref(0)
const refactorSelectedNetworks = ref([])
const showRefactorNetworkSelector = ref(false)
const refactorSelectedInternalNetworks = ref([])
const showRefactorInternalNetworkSelector = ref(false)

const onRefactorMemUnitChange = () => {
  refactorMemDisplay.value = refactorForm.memory ? Math.round(refactorForm.memory / refactorMemUnit.value * 100) / 100 : 0
}
watch(refactorMemDisplay, (v) => { refactorForm.memory = Math.round(v * refactorMemUnit.value) })

const handleRefactorNetworkConfirm = (network) => {
  if (!refactorSelectedNetworks.value.some(n => n.id === network.id)) {
    refactorSelectedNetworks.value = [...refactorSelectedNetworks.value, { id: network.id, name: network.name }]
  }
}
const removeRefactorNetwork = (id) => {
  refactorSelectedNetworks.value = refactorSelectedNetworks.value.filter(n => n.id !== id)
}
const handleRefactorInternalNetworkConfirm = (network) => {
  refactorSelectedInternalNetworks.value = [{ id: network.id, name: network.name }]
}
const removeRefactorInternalNetwork = (id) => {
  refactorSelectedInternalNetworks.value = refactorSelectedInternalNetworks.value.filter(n => n.id !== id)
}

const handleRefactorVm = async () => {
  if (!detail.value) return
  const d = detail.value
  Object.assign(refactorForm, {
    memory: d.memory || 0, vcpu: d.vcpu || 0,
    rx_bandwidth: d.rx_bandwidth || 0, tx_bandwidth: d.tx_bandwidth || 0,
    root_password: d.root_password || '',
    uuid: d.uuid || '', mate_data_id: d.mate_data_id || '',
    physical_name: d.physical_name || '', config_path: d.config_path || '',
    ssh_port: d.ssh_port || 0, vnc_port: 0, vnc_password: '',
    port_group_id: vmPortGroup.value?.id || ''
  })
  const bridgeNets = vmNetworks.value.filter(n => n.type === 'bridge')
  const natNets = vmNetworks.value.filter(n => n.type === 'nat')
  refactorSelectedNetworks.value = bridgeNets.map(n => ({ id: n.id, name: n.name }))
  refactorSelectedInternalNetworks.value = natNets.length ? [{ id: natNets[0].id, name: natNets[0].name }] : []
  const mem = d.memory || 0
  if (mem >= 1048576 && mem % 1048576 === 0) { refactorMemUnit.value = 1048576; refactorMemDisplay.value = mem / 1048576 }
  else if (mem >= 1024 && mem % 1024 === 0) { refactorMemUnit.value = 1024; refactorMemDisplay.value = mem / 1024 }
  else { refactorMemUnit.value = 1; refactorMemDisplay.value = mem }
  refactorDialogVisible.value = true
  dialogOptionsLoading.value = true
  try {
    await Promise.all([
      !sgOptions.value.length ? loadSgOptions() : Promise.resolve()
    ])
  } finally { dialogOptionsLoading.value = false }
}

const submitRefactorVm = async () => {
  actionLoading.value = true
  try {
    const fd = new FormData()
    fd.append('service_id', serviceId.value)
    fd.append('vm_id', vmId.value)
    if (refactorForm.memory) fd.append('memory', refactorForm.memory)
    if (refactorForm.vcpu) fd.append('vcpu', refactorForm.vcpu)
    fd.append('rx_bandwidth', refactorForm.rx_bandwidth)
    fd.append('tx_bandwidth', refactorForm.tx_bandwidth)
    if (refactorForm.root_password) fd.append('root_password', refactorForm.root_password)
    if (refactorForm.uuid) fd.append('uuid', refactorForm.uuid)
    if (refactorForm.mate_data_id) fd.append('mate_data_id', refactorForm.mate_data_id)
    if (refactorForm.physical_name) fd.append('physical_name', refactorForm.physical_name)
    if (refactorForm.config_path) fd.append('config_path', refactorForm.config_path)
    if (refactorForm.ssh_port) fd.append('ssh_port', refactorForm.ssh_port)
    if (refactorForm.vnc_port) fd.append('vnc_port', refactorForm.vnc_port)
    if (refactorForm.vnc_password) fd.append('vnc_password', refactorForm.vnc_password)
    refactorSelectedNetworks.value.forEach(n => fd.append('network_ids', n.id))
    if (refactorSelectedInternalNetworks.value.length) fd.append('internet_network_id', refactorSelectedInternalNetworks.value[0].id)
    if (refactorForm.port_group_id) fd.append('port_group_id', refactorForm.port_group_id)
    const res = await refactorVm(fd)
    if (res?.data?.code === 200) { ElMessage.success('重构成功'); refactorDialogVisible.value = false; loadDetail() }
    else ElMessage.error(extractApiError(res?.data, '重构失败'))
  } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '重构失败')) } finally { actionLoading.value = false }
}

// ---- 修改带宽 ----
const trafficDialogVisible = ref(false)
const trafficForm = reactive({ rx_bandwidth: 0, tx_bandwidth: 0, _trafficGB: 0, traffic_exhausted_rx_mbps: 0, traffic_exhausted_tx_mbps: 0 })

const handleUpdateTraffic = () => {
  if (!detail.value) return
  Object.assign(trafficForm, {
    rx_bandwidth: detail.value.rx_bandwidth || 0,
    tx_bandwidth: detail.value.tx_bandwidth || 0,
    _trafficGB: ((detail.value.traffic_max || 0) / 1024).toFixed(2) * 1,
    traffic_exhausted_rx_mbps: 0,
    traffic_exhausted_tx_mbps: 0
  })
  trafficDialogVisible.value = true
}

const submitUpdateTraffic = async () => {
  actionLoading.value = true
  try {
    const fd = new FormData()
    fd.append('service_id', serviceId.value)
    fd.append('vm_id', vmId.value)
    fd.append('rx_bandwidth', trafficForm.rx_bandwidth)
    fd.append('tx_bandwidth', trafficForm.tx_bandwidth)
    if (trafficForm._trafficGB) fd.append('traffic_max', Math.round(trafficForm._trafficGB * 1024))  // GB → Mb
    if (trafficForm.traffic_exhausted_rx_mbps > 0) fd.append('traffic_exhausted_rx_mbps', trafficForm.traffic_exhausted_rx_mbps)
    if (trafficForm.traffic_exhausted_tx_mbps > 0) fd.append('traffic_exhausted_tx_mbps', trafficForm.traffic_exhausted_tx_mbps)
    const res = await updateVmTraffic(fd)
    if (res?.data?.code === 200) { ElMessage.success('带宽修改成功'); trafficDialogVisible.value = false; loadDetail() }
    else ElMessage.error(extractApiError(res?.data, '修改失败'))
  } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '修改失败')) } finally { actionLoading.value = false }
}

// ---- 流量策略 ----
// 测试未通过（接口新增，待联调）
const vmTrafficPolicy = ref(null)
const vmTrafficPolicyLoading = ref(false)
const vmTrafficPolicyVisible = ref(false)
const vmAddTrafficVisible = ref(false)
const vmAddTrafficType = ref('fixed')
const vmTrafficPolicyForm = reactive({ traffic_max_mb: 0, exhausted_rx_mbps: 0, exhausted_tx_mbps: 0 })
const vmAddTrafficForm = reactive({ traffic_mb: 1 })

const loadVmTrafficPolicy = async () => {
  if (!serviceId.value || !vmId.value) return
  vmTrafficPolicyLoading.value = true
  try {
    const res = await getVmTrafficPolicy({ service_id: serviceId.value, vm_id: vmId.value })
    if (res?.data?.code === 200) vmTrafficPolicy.value = res.data.data
  } catch { /* ignore */ } finally { vmTrafficPolicyLoading.value = false }
}

const openVmTrafficPolicyDialog = () => {
  // 从概览触发时 vmTrafficPolicy 可能尚未加载（流量策略 tab 懒加载），
  // 用 detail.value 上 add.json 新字段作 fallback；同时异步加载策略
  Object.assign(vmTrafficPolicyForm, {
    traffic_max_mb: vmTrafficPolicy.value?.traffic_max_mb ?? detail.value?.traffic_max_mb ?? detail.value?.traffic_max ?? 0,
    exhausted_rx_mbps: vmTrafficPolicy.value?.exhausted_rx_mbps ?? detail.value?.traffic_exhausted_rx_mbps ?? 0,
    exhausted_tx_mbps: vmTrafficPolicy.value?.exhausted_tx_mbps ?? detail.value?.traffic_exhausted_tx_mbps ?? 0
  })
  vmTrafficPolicyVisible.value = true
  if (!vmTrafficPolicy.value) loadVmTrafficPolicy()
}

const submitUpdateVmTrafficPolicy = async () => {
  vmTrafficPolicyLoading.value = true
  try {
    const fd = new FormData()
    fd.append('service_id', serviceId.value)
    fd.append('vm_id', vmId.value)
    fd.append('traffic_max_mb', vmTrafficPolicyForm.traffic_max_mb)
    fd.append('exhausted_rx_mbps', vmTrafficPolicyForm.exhausted_rx_mbps)
    fd.append('exhausted_tx_mbps', vmTrafficPolicyForm.exhausted_tx_mbps)
    const res = await updateVmTrafficPolicy(fd)
    if (res?.data?.code === 200) { ElMessage.success('修改成功'); vmTrafficPolicyVisible.value = false; loadVmTrafficPolicy() }
    else ElMessage.error(extractApiError(res?.data, '修改失败'))
  } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '修改失败')) } finally { vmTrafficPolicyLoading.value = false }
}

const openVmAddTrafficDialog = (type) => {
  vmAddTrafficType.value = type
  vmAddTrafficForm.traffic_mb = 1
  vmAddTrafficVisible.value = true
}

const submitAddVmTraffic = async () => {
  if (!vmAddTrafficForm.traffic_mb || vmAddTrafficForm.traffic_mb < 1) { ElMessage.warning('请输入有效的流量数量'); return }
  vmTrafficPolicyLoading.value = true
  try {
    const fd = new FormData()
    fd.append('service_id', serviceId.value)
    fd.append('vm_id', vmId.value)
    fd.append('traffic_mb', vmAddTrafficForm.traffic_mb)
    const apiFn = vmAddTrafficType.value === 'fixed' ? addVmFixedTraffic : addVmTemporaryTraffic
    const res = await apiFn(fd)
    if (res?.data?.code === 200) { ElMessage.success('操作成功'); vmAddTrafficVisible.value = false; loadVmTrafficPolicy() }
    else ElMessage.error(extractApiError(res?.data, '操作失败'))
  } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '操作失败')) } finally { vmTrafficPolicyLoading.value = false }
}

// ---- 迁移虚拟机 ----
const migrateDialogVisible = ref(false)
const migrateOptionsLoading = ref(false)
const migrateMode = ref('host')
const migrateHostOptions = ref([])
const migrateGroupOptions = ref([])
const migrateForm = reactive({ target_host_id: null, target_host_group_id: null, ipv4_num: 0, ipv6_num: 0, network_ids: [] })
const showMigrateNetworkSelector = ref(false)
const migrateSelectedNetworks = ref([])
const clearMigrateNetworks = () => { migrateSelectedNetworks.value = []; migrateForm.network_ids = [] }
const handleMigrateNetworkConfirm = items => {
  const list = Array.isArray(items) ? items : [items]
  const ids = new Set(migrateSelectedNetworks.value.map(item => item.id))
  list.forEach(item => { if (!ids.has(item.id)) migrateSelectedNetworks.value.push(item) })
  migrateForm.network_ids = migrateSelectedNetworks.value.map(item => item.id)
}
const removeMigrateNetwork = id => { migrateSelectedNetworks.value = migrateSelectedNetworks.value.filter(item => item.id !== id); migrateForm.network_ids = migrateSelectedNetworks.value.map(item => item.id) }

const handleMigrateVm = async () => {
  Object.assign(migrateForm, { target_host_id: null, target_host_group_id: null, ipv4_num: 0, ipv6_num: 0, network_ids: [] })
  migrateSelectedNetworks.value = []
  migrateMode.value = 'host'
  migrateDialogVisible.value = true
  migrateOptionsLoading.value = true
  try {
    const [hostRes, groupRes] = await Promise.all([
      getRemoteHostList({ service_id: serviceId.value, page: 1, page_size: 10 }),
      getRemoteHostGroupList({ service_id: serviceId.value, page: 1, page_size: 10 })
    ])
    if (hostRes?.data?.code === 200 && hostRes?.data?.data) {
      const inner = hostRes.data.data
      migrateHostOptions.value = Array.isArray(inner) ? inner : (inner.hosts || inner.data || [])
    }
    if (groupRes?.data?.code === 200 && groupRes?.data?.data) {
      const inner = groupRes.data.data
      migrateGroupOptions.value = Array.isArray(inner) ? inner : (inner.host_groups || inner.data || [])
    }
  } catch { /* */ } finally { migrateOptionsLoading.value = false }
}

const submitMigrateVm = async () => {
  if (migrateMode.value === 'host' && !migrateForm.target_host_id) { ElMessage.warning('请选择目标宿主机'); return }
  if (migrateMode.value === 'group' && !migrateForm.target_host_group_id) { ElMessage.warning('请选择目标宿主机组'); return }
  actionLoading.value = true
  try {
    const fd = new FormData()
    fd.append('service_id', serviceId.value)
    fd.append('vm_id', vmId.value)
    if (migrateMode.value === 'host') fd.append('target_host_id', migrateForm.target_host_id)
    else fd.append('target_host_group_id', migrateForm.target_host_group_id)
    if (migrateForm.ipv4_num) fd.append('ipv4_num', migrateForm.ipv4_num)
    if (migrateForm.ipv6_num) fd.append('ipv6_num', migrateForm.ipv6_num)
    migrateForm.network_ids.forEach(id => fd.append('network_ids', id))
    const res = await migrateVm(fd)
    if (res?.data?.code === 200) {
      const migrationData = res.data.data || {}
      ElMessage.success(`迁移已提交${migrationData.migration_record_id ? `，记录 #${migrationData.migration_record_id}` : ''}`)
      migrateDialogVisible.value = false
      loadDetail()
    }
    else ElMessage.error(extractApiError(res?.data, '迁移失败'))
  } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '迁移失败')) } finally { actionLoading.value = false }
}

// ---- 数据迁移 ----
const dataMigrateVisible = ref(false)
const dataMigrateLoading = ref(false)
const dataMigrateHostsLoading = ref(false)
const dataMigrateServiceOptions = ref([])
const dataMigrateHostOptions = ref([])
const dataMigrateProgressVisible = ref(false)
const dataMigrateProgressLoading = ref(false)
const dataMigrateProgressData = ref(null)
const dataMigrationId = ref('')
const dataMigrateTaskId = ref('')
const dataMigrationRecordId = ref(null)
const dataMigrationChainId = ref('')
const dataMigrateForm = reactive({ target_service_id: null, target_host_id: null, ipv4_num: 0, ipv6_num: 0, network_ids: [] })
const dataMigrateNetworkOptions = ref([])
const showDataMigrateNetworkSelector = ref(false)
const dataMigrateNetSelectorRef = ref(null)
const editNetSelectorRef = ref(null)
const editInternalNetSelectorRef = ref(null)
const refactorNetSelectorRef = ref(null)
const refactorInternalNetSelectorRef = ref(null)
const bindBridgeNetSelectorRef = ref(null)
const bindNatNetSelectorRef = ref(null)
const dataMigrateSelectedNetworks = ref([])

const handleDataMigrateNetworkConfirm = (items) => {
  const arr = Array.isArray(items) ? items : [items]
  const existIds = new Set(dataMigrateSelectedNetworks.value.map(n => n.id))
  arr.forEach(n => { if (!existIds.has(n.id)) dataMigrateSelectedNetworks.value.push(n) })
  dataMigrateForm.network_ids = dataMigrateSelectedNetworks.value.map(n => n.id)
}
const removeDataMigrateNetwork = (id) => {
  dataMigrateSelectedNetworks.value = dataMigrateSelectedNetworks.value.filter(n => n.id !== id)
  dataMigrateForm.network_ids = dataMigrateSelectedNetworks.value.map(n => n.id)
}

const handleDataMigrateVm = async () => {
  Object.assign(dataMigrateForm, { target_service_id: null, target_host_id: null, ipv4_num: 0, ipv6_num: 0, network_ids: [] })
  dataMigrateSelectedNetworks.value = []
  dataMigrateNetworkOptions.value = []
  dataMigrateVisible.value = true
  dataMigrateLoading.value = true
  try {
    const res = await getKvmServiceList({ page: 1, count: 10 })
    if (res?.data?.code === 200 && res?.data?.data) {
      const inner = res.data.data
      const raw = inner.data || inner.list || (Array.isArray(inner) ? inner : [])
      dataMigrateServiceOptions.value = raw.map(s => ({ id: s.id ?? s.Id, name: s.name ?? s.Name }))
    }
  } catch { /* */ } finally { dataMigrateLoading.value = false }
}

const loadDataMigrateHosts = async () => {
  if (!dataMigrateForm.target_service_id) return
  dataMigrateHostsLoading.value = true
  dataMigrateNetworkOptions.value = []
  dataMigrateForm.network_ids = []
  dataMigrateSelectedNetworks.value = []
  try {
    const res = await getRemoteHostList({ service_id: dataMigrateForm.target_service_id, page: 1, page_size: 10 })
    if (res?.data?.code === 200 && res?.data?.data) {
      const inner = res.data.data
      dataMigrateHostOptions.value = Array.isArray(inner) ? inner : (inner.hosts || inner.data || [])
    }
  } catch { /* */ } finally { dataMigrateHostsLoading.value = false }
}

const loadDataMigrateNetworks = async () => {
  if (!dataMigrateForm.target_service_id || !dataMigrateForm.target_host_id) return
  try {
    const res = await getNetworkList({ service_id: dataMigrateForm.target_service_id, host_id: dataMigrateForm.target_host_id })
    if (res?.data?.code === 200 && res?.data?.data) {
      const inner = res.data.data
      dataMigrateNetworkOptions.value = Array.isArray(inner) ? inner : (inner.networks || inner.data || [])
    }
  } catch { dataMigrateNetworkOptions.value = [] }
}

const submitDataMigrate = async () => {
  if (!dataMigrateForm.target_service_id) { ElMessage.warning('请选择目标主控服务'); return }
  if (!dataMigrateForm.target_host_id) { ElMessage.warning('请选择目标宿主机'); return }
  actionLoading.value = true
  try {
    const fd = new FormData()
    fd.append('source_service_id', serviceId.value)
    fd.append('source_vm_id', vmId.value)
    fd.append('target_service_id', dataMigrateForm.target_service_id)
    fd.append('target_host_id', dataMigrateForm.target_host_id)
    if (dataMigrateForm.ipv4_num) fd.append('ipv4_num', dataMigrateForm.ipv4_num)
    if (dataMigrateForm.ipv6_num) fd.append('ipv6_num', dataMigrateForm.ipv6_num)
    if (dataMigrateForm.network_ids?.length) {
      dataMigrateForm.network_ids.forEach(id => fd.append('network_ids', id))
    }
    const res = await dataMigrateVm(fd)
    if (res?.data?.code === 200) {
      ElMessage.success('数据迁移已发起')
      const d = res.data.data
      dataMigrationId.value = d?.migration_id || ''
      dataMigrateTaskId.value = d?.export_task?.task_id || ''
      dataMigrationRecordId.value = d?.migration_record_id || null
      dataMigrationChainId.value = d?.chain_id || ''
      dataMigrateVisible.value = false
      dataMigrateProgressVisible.value = true
      loadDataMigrateProgress()
      startMigratePolling()
    } else ElMessage.error(extractApiError(res?.data, '发起迁移失败'))
  } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '发起迁移失败')) } finally { actionLoading.value = false }
}

const MIGRATE_DONE_STAGES = ['completed', 'done', 'success', 'warning', 'failed', 'aborted', 'cancelled', 'error']

const loadDataMigrateProgress = async () => {
  if (!dataMigrationId.value && !dataMigrateTaskId.value) return
  const params = { service_id: serviceId.value, host_id: vmHostId.value }
  if (dataMigrationId.value) params.migration_id = dataMigrationId.value
  if (dataMigrateTaskId.value) params.task_id = dataMigrateTaskId.value
  dataMigrateProgressLoading.value = true
  try {
    const res = await getDataMigrateProgress(params)
    if (res?.data?.code === 200 && res.data.data) {
      dataMigrateProgressData.value = res.data.data
      const d = res.data.data
      if (d.migration_record_id) dataMigrationRecordId.value = d.migration_record_id
      if (d.chain_id) dataMigrationChainId.value = d.chain_id
      if (!dataMigrationId.value && d.migration_id) dataMigrationId.value = d.migration_id
      if (!dataMigrateTaskId.value) {
        dataMigrateTaskId.value = d.export_task_id || d.import_task_id || ''
      }
      const stage = d.stage
      if (MIGRATE_DONE_STAGES.includes(stage)) {
        stopMigratePolling()
        if (stage === 'completed' || stage === 'done' || stage === 'success') ElMessage.success('数据迁移已完成')
        else if (stage === 'warning') ElMessage.warning('数据迁移已完成，但存在警告')
        else if (stage === 'failed' || stage === 'error') ElMessage.error(d.error || '数据迁移失败')
        loadDetail()
      }
    } else {
      dataMigrateProgressData.value = null
      stopMigratePolling()
    }
  } catch {
    dataMigrateProgressData.value = null
    stopMigratePolling()
  } finally { dataMigrateProgressLoading.value = false }
}

const isMigrating = computed(() => {
  if (detail.value?.migrating) return true
  const stage = dataMigrateProgressData.value?.stage
  return stage && !MIGRATE_DONE_STAGES.includes(stage)
})

const migrateStageLabel = (stage) => ({
  exporting: '导出中', importing: '导入中', transferring: '传输中',
  verifying: '校验中', completed: '已完成', done: '已完成', success: '成功', warning: '已完成（有警告）', running: '执行中',
  failed: '失败', error: '错误', aborted: '已中断', cancelled: '已取消',
  pending: '等待中', preparing: '准备中'
}[stage] || stage || '-')

const migrateStageType = (stage) => ({
  exporting: 'warning', importing: 'warning', transferring: '',
  verifying: '', completed: 'success', done: 'success', success: 'success', warning: 'warning', running: 'primary',
  failed: 'danger', error: 'danger', aborted: 'info', cancelled: 'info',
  pending: 'info', preparing: 'info'
}[stage] || 'info')

const migrateProgressBarStatus = (stage) => {
  if (stage === 'completed' || stage === 'done' || stage === 'success') return 'success'
  if (stage === 'failed' || stage === 'error') return 'exception'
  return ''
}

let migratePollingTimer = null
const startMigratePolling = () => {
  stopMigratePolling()
  migratePollingTimer = setInterval(loadDataMigrateProgress, 3000)
}
const stopMigratePolling = () => {
  if (migratePollingTimer) { clearInterval(migratePollingTimer); migratePollingTimer = null }
}

let detailAutoRefreshTimer = null
const startDetailAutoRefresh = () => {
  if (detailAutoRefreshTimer) return
  detailAutoRefreshTimer = setInterval(() => { loadDetail() }, 3000)
}
const stopDetailAutoRefresh = () => {
  if (detailAutoRefreshTimer) { clearInterval(detailAutoRefreshTimer); detailAutoRefreshTimer = null }
}

const abortLoading = ref(false)
const handleAbortMigrate = () => {
  ElMessageBox.confirm('确定要中断当前数据迁移吗？此操作不可恢复！', '中断迁移', {
    confirmButtonText: '确定中断', cancelButtonText: '取消', type: 'warning', confirmButtonClass: 'el-button--danger'
  }).then(async () => {
    abortLoading.value = true
    try {
      const fd = new FormData()
      fd.append('service_id', serviceId.value)
      fd.append('vm_id', vmId.value)
      const res = await abortDataMigrate(fd)
      if (res?.data?.code === 200) {
        ElMessage.success('迁移已中断')
        stopMigratePolling()
        dataMigrateProgressData.value = null
        dataMigrationId.value = ''
        dataMigrateTaskId.value = ''
        loadDetail()
      } else ElMessage.error(extractApiError(res?.data, '中断迁移失败'))
    } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '中断迁移失败')) } finally { abortLoading.value = false }
  }).catch(() => {})
}

// ---- VNC 连接 ----
const vncDialogVisible = ref(false)
const vncNodeId = ref(null)
const vncNodeOptions = ref([])
const vncLoading = ref(false)
const vncResult = ref(null)

const loadVncNodes = async () => {
  try {
    const res = await getVncNodeList({ service_id: serviceId.value, page: 1, page_size: 10 })
    if (res?.data?.code === 200 && res?.data?.data) {
      const inner = res.data.data
      vncNodeOptions.value = inner.items || inner.vnc_nodes || inner.nodes || inner.data || (Array.isArray(inner) ? inner : [])
    }
  } catch { /* */ }
}

const handleSshConnect = () => {
  const host = publicIpList.value[0]
  if (!host) return ElMessage.warning('无可用公网 IP')
  const username = isWindows.value ? 'Administrator' : 'root'
  const password = detail.value?.root_password || ''
  const encodedPwd = btoa(password)
  window.open(`https://webssh1.007yjs.com/?hostname=${encodeURIComponent(host)}&username=${encodeURIComponent(username)}&password=${encodedPwd}`, '_blank')
}

const handleGetVnc = async () => {
  vncNodeId.value = null
  vncResult.value = null
  vncDialogVisible.value = true
  if (!vncNodeOptions.value.length) {
    dialogOptionsLoading.value = true
    try { await loadVncNodes() } finally { dialogOptionsLoading.value = false }
  }
}

const submitGetVnc = async () => {
  vncLoading.value = true
  vncResult.value = null
  try {
    const res = await getVmVnc({ service_id: serviceId.value, id: vncNodeId.value, vm_id: vmId.value })
    if (res?.data?.code === 200 && res?.data?.data) {
      vncResult.value = res.data.data
    } else ElMessage.error(extractApiError(res?.data, '获取VNC连接失败'))
  } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '获取VNC连接失败')) } finally { vncLoading.value = false }
}

// ---- 安全组选项（用于编辑弹窗） ----
const sgOptions = ref([])

const loadSgOptions = async () => {
  try {
    const res = await getSecurityGroupList({ service_id: serviceId.value, page: 1, page_size: 10 })
    if (res?.data?.code === 200 && res?.data?.data) {
      const inner = res.data.data
      sgOptions.value = inner.groups || inner.post_groups || inner.data || (Array.isArray(inner) ? inner : [])
    }
  } catch { /* */ }
}

// ---- 绑定网络（通过 updateVm 接口） ----
const showNetBindBridgeSelector = ref(false)
const showNetBindNatSelector = ref(false)

const submitNetBind = async (paramName, newId, existingIds) => {
  actionLoading.value = true
  try {
    const fd = new FormData()
    fd.append('service_id', serviceId.value)
    fd.append('vm_id', vmId.value)
    const bridgeIds = vmNetworks.value.filter(n => n.type === 'bridge').map(n => n.id)
    const natNet = vmNetworks.value.find(n => n.type === 'nat')
    if (paramName === 'network_ids') {
      const allBridge = [...bridgeIds, newId]
      allBridge.forEach(id => fd.append('network_ids', id))
      if (natNet) fd.append('internet_network_id', natNet.id)
    } else {
      bridgeIds.forEach(id => fd.append('network_ids', id))
      fd.append('internet_network_id', newId)
    }
    if (detail.value?.rx_bandwidth) fd.append('rx_bandwidth', detail.value.rx_bandwidth)
    if (detail.value?.tx_bandwidth) fd.append('tx_bandwidth', detail.value.tx_bandwidth)
    if (detail.value?.ssh_port) fd.append('ssh_port', detail.value.ssh_port)
    if (vmPortGroup.value?.id) fd.append('port_group_id', vmPortGroup.value.id)
    const res = await updateVm(fd)
    if (res?.data?.code === 200) {
      ElMessage.success('绑定网络成功')
      loadDetail()
    } else {
      ElMessage.error(extractApiError(res?.data, '绑定网络失败'))
    }
  } catch (e) {
    ElMessage.error(extractApiError(e?.response?.data, '绑定网络失败'))
  } finally {
    actionLoading.value = false
  }
}

const handleNetBindBridgeConfirm = (selectedNetwork) => {
  const existingIds = vmNetworks.value.filter(n => n.type === 'bridge').map(n => n.id)
  if (existingIds.includes(selectedNetwork.id)) {
    ElMessage.warning('该网络已绑定')
    return
  }
  submitNetBind('network_ids', selectedNetwork.id)
}

const handleNetBindNatConfirm = (selectedNetwork) => {
  const existingNat = vmNetworks.value.find(n => n.type === 'nat')
  if (existingNat?.id === selectedNetwork.id) {
    ElMessage.warning('该内网已绑定')
    return
  }
  submitNetBind('internet_network_id', selectedNetwork.id)
}

// ---- 网络操作（创建/编辑/删除/详情） ----
const netDialogVisible = ref(false)
const netDialogType = ref('add')
const netDialogSource = ref('')
const netFormRef = ref(null)
const netDetailVisible = ref(false)
const netDetailData = ref(null)
const netForm = reactive({ id: 0, name: '', address: '', gateway: '', nameservers: '', type: 'bridge', mac_address: '', bridge_name: '', ls_bridge_name: '', ls_name: '', host_id: 0 })
const netFormRules = {
  name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
  address: [{ required: true, message: '请输入 CIDR 格式网段', trigger: 'blur' }],
  gateway: [{ required: true, message: '请输入网关', trigger: 'blur' }],
  type: [{ required: true, message: '请选择类型', trigger: 'change' }]
}

const handleNetCreate = (source = '') => {
  netDialogSource.value = source
  netDialogType.value = 'add'
  const hostId = source === 'dataMigrate' ? (dataMigrateForm.target_host_id || vmHostId.value) : vmHostId.value
  Object.assign(netForm, { id: 0, name: '', address: '', gateway: '', nameservers: '', type: 'bridge', mac_address: '', bridge_name: '', ls_bridge_name: '', ls_name: '', host_id: hostId })
  netDialogVisible.value = true
}
const handleNetDialogCancel = () => {
  netDialogVisible.value = false
  netDialogSource.value = ''
}

const handleNetEdit = (row) => {
  netDialogType.value = 'edit'
  Object.assign(netForm, { id: row.id, name: row.name || '', address: row.address || '', gateway: row.gateway || '', nameservers: row.nameservers || '', type: row.type || 'bridge', mac_address: row.mac_address || '', bridge_name: row.bridge_name || '', ls_bridge_name: row.ls_bridge_name || '', ls_name: row.ls_name || '', host_id: row.host_id || vmHostId.value })
  netDialogVisible.value = true
}
const submitNetForm = () => {
  netFormRef.value?.validate(async (valid) => {
    if (!valid) return
    actionLoading.value = true
    try {
      const fd = new FormData()
      const sid = netDialogSource.value === 'dataMigrate' && dataMigrateForm.target_service_id ? dataMigrateForm.target_service_id : serviceId.value
      fd.append('service_id', sid)
      fd.append('name', netForm.name)
      fd.append('address', netForm.address)
      fd.append('gateway', netForm.gateway)
      fd.append('type', netForm.type)
      fd.append('host_id', netForm.host_id)
      if (netForm.nameservers) fd.append('nameservers', netForm.nameservers)
      if (netForm.mac_address) fd.append('mac_address', netForm.mac_address)
      if (netForm.bridge_name) fd.append('bridge_name', netForm.bridge_name)
      if (netForm.ls_bridge_name) fd.append('ls_bridge_name', netForm.ls_bridge_name)
      if (netForm.ls_name) fd.append('ls_name', netForm.ls_name)
      let res
      if (netDialogType.value === 'add') { res = await createNetwork(fd) }
      else { fd.append('id', netForm.id); res = await updateNetwork(fd) }
      if (res?.data?.code === 200) {
        ElMessage.success(netDialogType.value === 'add' ? '创建成功' : '修改成功')
        netDialogVisible.value = false
        const src = netDialogSource.value
        netDialogSource.value = ''
        const refMap = {
          dataMigrate: dataMigrateNetSelectorRef,
          edit: editNetSelectorRef,
          editInternal: editInternalNetSelectorRef,
          refactor: refactorNetSelectorRef,
          refactorInternal: refactorInternalNetSelectorRef,
          bindBridge: bindBridgeNetSelectorRef,
          bindNat: bindNatNetSelectorRef,
        }
        const selectorRef = refMap[src]
        if (selectorRef) {
          nextTick(() => selectorRef.value?.loadList())
        }
        if (src !== 'dataMigrate') loadDetail()
      }
      else ElMessage.error(extractApiError(res?.data, '操作失败'))
    } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '操作失败')) } finally { actionLoading.value = false }
  })
}
const handleNetDetail = (row) => { netDetailData.value = row; netDetailVisible.value = true }
const handleNetDelete = (row) => {
  ElMessageBox.confirm(`确定从该虚拟机解绑网络「${row.name}」(ID: ${row.id}) 吗？`, '解绑网络', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' })
    .then(async () => {
      actionLoading.value = true
      try {
        const fd = new FormData()
        fd.append('service_id', serviceId.value)
        fd.append('vm_id', vmId.value)
        const remainingNetworks = vmNetworks.value.filter(n => n.id !== row.id)
        const bridgeIds = remainingNetworks.filter(n => n.type === 'bridge').map(n => n.id)
        const natNet = remainingNetworks.find(n => n.type === 'nat')
        bridgeIds.forEach(id => fd.append('network_ids', id))
        if (natNet) fd.append('internet_network_id', natNet.id)
        if (detail.value?.rx_bandwidth) fd.append('rx_bandwidth', detail.value.rx_bandwidth)
        if (detail.value?.tx_bandwidth) fd.append('tx_bandwidth', detail.value.tx_bandwidth)
        if (detail.value?.ssh_port) fd.append('ssh_port', detail.value.ssh_port)
        if (vmPortGroup.value?.id) fd.append('port_group_id', vmPortGroup.value.id)
        const res = await updateVm(fd)
        if (res?.data?.code === 200) { ElMessage.success('解绑网络成功'); loadDetail() }
        else ElMessage.error(extractApiError(res?.data, '解绑网络失败'))
      } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '解绑网络失败')) }
      finally { actionLoading.value = false }
    }).catch(() => {})
}

const handleSetPrimary = (row) => {
  ElMessageBox.confirm(
    `将「${row.address || row.name}」设为主IP？设置后将重启虚拟机。`,
    '设置主IP', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }
  ).then(async () => {
    try {
      const fd = new FormData()
      fd.append('service_id', serviceId.value)
      fd.append('network_id', row.id)
      fd.append('host_id', row.host_id || detail.value?.host_id)
      const res = await setNetworkPrimary(fd)
      if (res?.data?.code === 200) { ElMessage.success('设置主IP成功，虚拟机将重启'); loadDetail() }
      else ElMessage.error(extractApiError(res?.data, '设置主IP失败'))
    } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '设置主IP失败')) }
  }).catch(() => {})
}

const handleResetMac = () => {
  ElMessageBox.confirm(
    '重置MAC地址将重建 CloudInit 并重启虚拟机，确定继续？',
    '重置MAC地址', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }
  ).then(async () => {
    try {
      const fd = new FormData()
      fd.append('service_id', serviceId.value)
      fd.append('vm_id', detail.value?.id)
      const res = await resetVmMac(fd)
      if (res?.data?.code === 200) { ElMessage.success('MAC地址重置成功，虚拟机将重启'); loadDetail() }
      else ElMessage.error(extractApiError(res?.data, '重置MAC失败'))
    } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '重置MAC失败')) }
  }).catch(() => {})
}

const handleDisconnectNetwork = () => {
  ElMessageBox.confirm(
    '断开虚拟机外部网络连接？断网后虚拟机将无法访问外部网络。',
    '断网', { confirmButtonText: '确定断网', cancelButtonText: '取消', type: 'warning' }
  ).then(async () => {
    try {
      const fd = new FormData()
      fd.append('service_id', serviceId.value)
      fd.append('vm_id', detail.value?.id)
      const res = await disconnectVmNetwork(fd)
      if (res?.data?.code === 200) { ElMessage.success('已断网'); loadDetail() }
      else ElMessage.error(extractApiError(res?.data, '断网失败'))
    } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '断网失败')) }
  }).catch(() => {})
}

const handleConnectNetwork = () => {
  ElMessageBox.confirm(
    '恢复虚拟机外部网络连接？',
    '恢复网络', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'info' }
  ).then(async () => {
    try {
      const fd = new FormData()
      fd.append('service_id', serviceId.value)
      fd.append('vm_id', detail.value?.id)
      const res = await connectVmNetwork(fd)
      if (res?.data?.code === 200) { ElMessage.success('网络已恢复'); loadDetail() }
      else ElMessage.error(extractApiError(res?.data, '恢复网络失败'))
    } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '恢复网络失败')) }
  }).catch(() => {})
}

// ---- 每小时流量统计 ----
const trafficTimeMode = ref('relative')
const trafficRelativeMinutes = ref(1440)
const trafficHourlyRange = ref(null)
const trafficHourlyData = ref([])
const trafficHourlyLoading = ref(false)
const trafficHourlyChartRef = ref(null)
let trafficHourlyChart = null

const getTrafficTimeRange = () => {
  if (trafficTimeMode.value === 'relative') {
    const endTime = new Date()
    const startTime = new Date(endTime - trafficRelativeMinutes.value * 60 * 1000)
    return { startTime, endTime }
  } else {
    if (!trafficHourlyRange.value || trafficHourlyRange.value.length < 2) return null
    return { startTime: new Date(trafficHourlyRange.value[0]), endTime: new Date(trafficHourlyRange.value[1]) }
  }
}

const loadTrafficHourly = async () => {
  if (!serviceId.value || !detail.value?.host_id || !detail.value?.name) return
  const range = getTrafficTimeRange()
  if (!range) return
  trafficHourlyLoading.value = true
  try {
    const res = await getVmTrafficHourly({
      service_id: serviceId.value,
      host_id: detail.value.host_id,
      vm_name: detail.value.name,
      start: range.startTime.toISOString(),
      end_time: range.endTime.toISOString()
    })
    const raw = res?.data?.data?.data
    trafficHourlyData.value = typeof raw === 'string' ? JSON.parse(raw) : (Array.isArray(raw) ? raw : [])
    nextTick(renderTrafficHourlyChart)
  } catch { trafficHourlyData.value = [] } finally { trafficHourlyLoading.value = false }
}

const renderTrafficHourlyChart = () => {
  if (!trafficHourlyChartRef.value || !trafficHourlyData.value.length) return
  if (!trafficHourlyChart) trafficHourlyChart = echarts.init(trafficHourlyChartRef.value)
  const data = trafficHourlyData.value
  const times = data.map(d => {
    const date = new Date(d.bucket)
    return date.toLocaleTimeString('zh-CN', { hour12: false, hour: '2-digit', minute: '2-digit' })
  })
  const toMB = (b) => +(b / 1048576).toFixed(2)
  trafficHourlyChart.setOption({
    tooltip: { trigger: 'axis', formatter: (params) => params.map(p => `${p.marker}${p.seriesName}: ${p.value} MB`).join('<br/>') },
    legend: { data: ['下行', '上行', '合计'], bottom: 0 },
    grid: { top: 10, right: 16, bottom: 40, left: 50 },
    xAxis: { type: 'category', data: times, boundaryGap: false, axisLabel: { fontSize: 10 } },
    yAxis: { type: 'value', name: 'MB', axisLabel: { fontSize: 10 } },
    series: [
      { name: '下行', type: 'bar', stack: 'traffic', data: data.map(d => toMB(d.rx_bytes)), itemStyle: { color: '#409EFF' } },
      { name: '上行', type: 'bar', stack: 'traffic', data: data.map(d => toMB(d.tx_bytes)), itemStyle: { color: '#67C23A' } },
      { name: '合计', type: 'line', smooth: true, data: data.map(d => toMB(d.total_bytes)), itemStyle: { color: '#E6A23C' }, lineStyle: { width: 2 } }
    ]
  }, true)
}

// ---- 数据卷操作（绑定/创建/调整/挂载/卸载/迁移/删除/详情） ----
const showVolSelector = ref(false)

const handleVolBindConfirm = async (vol) => {
  if (!vol?.id) return
  actionLoading.value = true
  try {
    const fd = new FormData()
    fd.append('service_id', serviceId.value)
    fd.append('volume_id', vol.id)
    fd.append('vm_id', vmId.value)
    const res = await mountVolume(fd)
    if (res?.data?.code === 200) { ElMessage.success('绑定成功'); loadDetail() }
    else ElMessage.error(extractApiError(res?.data, '绑定失败'))
  } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '绑定失败')) } finally { actionLoading.value = false }
}

const handleVolCreateFromSelector = () => {
  Object.assign(volCreateForm, { name: '', size: 10, is_system: false, target_device: '', image_id: 0, _imageName: '' })
  volCreateVisible.value = true
}

const volCreateVisible = ref(false)
const volCreateFormRef = ref(null)
const volCreateForm = reactive({ name: '', size: 10, is_system: false, target_device: '', image_id: 0, _imageName: '' })
const volCreateRules = { name: [{ required: true, message: '请输入名称', trigger: 'blur' }], size: [{ required: true, message: '请输入大小', trigger: 'blur' }] }
const showVolImageSelector = ref(false)
const volImageSelectorFromCreate = ref(false)
const handleVolImageSelected = (img) => { volCreateForm.image_id = img.id; volCreateForm._imageName = img.name; volCreateVisible.value = true; volImageSelectorFromCreate.value = false }
watch(showVolImageSelector, (val) => { if (!val && volImageSelectorFromCreate.value) { volCreateVisible.value = true; volImageSelectorFromCreate.value = false } })
const volResizeVisible = ref(false)
const volResizeTarget = ref(null)
const volNewSize = ref(1)
const volTransferVisible = ref(false)
const volTransferTarget = ref(null)
const volTransferVmId = ref(null)
const vmListOptions = ref([])
const volDetailVisible = ref(false)
const volDetailData = ref(null)

const handleVolCreate = () => {
  Object.assign(volCreateForm, { name: '', size: 10, is_system: false, target_device: '', image_id: 0, _imageName: '' })
  volCreateVisible.value = true
}
const submitVolCreate = () => {
  volCreateFormRef.value?.validate(async (valid) => {
    if (!valid) return
    actionLoading.value = true
    try {
      const fd = new FormData()
      fd.append('service_id', serviceId.value)
      fd.append('name', volCreateForm.name)
      fd.append('size', volCreateForm.size)
      fd.append('is_system', volCreateForm.is_system)
      fd.append('vm_id', vmId.value)
      fd.append('host_id', vmHostId.value)
      if (volCreateForm.target_device) fd.append('target_device', volCreateForm.target_device)
      if (volCreateForm.image_id) fd.append('image_id', volCreateForm.image_id)
      const res = await createVolume(fd)
      if (res?.data?.code === 200) { ElMessage.success('创建成功'); volCreateVisible.value = false; loadDetail() }
      else ElMessage.error(extractApiError(res?.data, '创建失败'))
    } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '创建失败')) } finally { actionLoading.value = false }
  })
}
const handleVolResize = (row) => { volResizeTarget.value = { id: row.id, _name: row.name, _currentSize: row.size || 0 }; volNewSize.value = row.size || 1; volResizeVisible.value = true }
const submitVolResize = async () => {
  actionLoading.value = true
  try {
    const fd = new FormData(); fd.append('service_id', serviceId.value); fd.append('volume_id', volResizeTarget.value.id); fd.append('size', volNewSize.value)
    const res = await resizeVolume(fd)
    if (res?.data?.code === 200) { ElMessage.success('调整成功'); volResizeVisible.value = false; loadDetail() }
    else ElMessage.error(extractApiError(res?.data, '调整失败'))
  } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '调整失败')) } finally { actionLoading.value = false }
}
const handleVolMount = (row) => {
  ElMessageBox.confirm(`确定要将「${row.name}」挂载到当前虚拟机吗？`, '挂载', { type: 'info' }).then(async () => {
    try {
      const fd = new FormData(); fd.append('service_id', serviceId.value); fd.append('volume_id', row.id); fd.append('vm_id', vmId.value)
      const res = await mountVolume(fd)
      if (res?.data?.code === 200) { ElMessage.success('挂载成功'); loadDetail() }
      else ElMessage.error(extractApiError(res?.data, '挂载失败'))
    } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '挂载失败')) }
  }).catch(() => {})
}
const handleVolUnmount = (row) => {
  ElMessageBox.confirm(`确定要卸载「${row.name}」吗？`, '卸载', { type: 'warning' }).then(async () => {
    try {
      const fd = new FormData(); fd.append('service_id', serviceId.value); fd.append('volume_id', row.id)
      const res = await unmountVolume(fd)
      if (res?.data?.code === 200) { ElMessage.success('卸载成功'); loadDetail() }
      else ElMessage.error(extractApiError(res?.data, '卸载失败'))
    } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '卸载失败')) }
  }).catch(() => {})
}
const loadVmListOptions = async () => {
  try {
    const res = await getVmList({ service_id: serviceId.value, page: 1, count: 10 })
    if (res?.data?.code === 200 && res?.data?.data) {
      const inner = res.data.data
      vmListOptions.value = inner.vms || inner.data || (Array.isArray(inner) ? inner : [])
    }
  } catch { /* */ }
}
const handleVolTransfer = async (row) => {
  volTransferTarget.value = { id: row.id, _name: row.name }; volTransferVmId.value = vmId.value
  volTransferVisible.value = true
  if (!vmListOptions.value.length) {
    dialogOptionsLoading.value = true
    try { await loadVmListOptions() } finally { dialogOptionsLoading.value = false }
  }
}
const submitVolTransfer = async () => {
  if (!volTransferVmId.value) { ElMessage.warning('请选择目标虚拟机'); return }
  actionLoading.value = true
  try {
    const fd = new FormData(); fd.append('service_id', serviceId.value); fd.append('volume_id', volTransferTarget.value.id); fd.append('vm_id', volTransferVmId.value)
    const res = await transferVolume(fd)
    if (res?.data?.code === 200) { ElMessage.success('迁移成功'); volTransferVisible.value = false; loadDetail() }
    else ElMessage.error(extractApiError(res?.data, '迁移失败'))
  } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '迁移失败')) } finally { actionLoading.value = false }
}
const handleVolDelete = (row) => {
  ElMessageBox.confirm(`确定要删除「${row.name}」吗？此操作不可恢复！`, '删除', { type: 'error' }).then(async () => {
    try {
      const res = await deleteVolume({ service_id: serviceId.value, volume_id: row.id })
      if (res?.data?.code === 200) { ElMessage.success('删除成功'); loadDetail() }
      else ElMessage.error(extractApiError(res?.data, '删除失败'))
    } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '删除失败')) }
  }).catch(() => {})
}
const handleVolDetail = (row) => {
  router.push({ path: '/virtualization/volume-detail', query: { service_id: serviceId.value, volume_id: row.id } })
}

// ---- 安全组完整管理 ----
const sgTableData = computed(() => {
  const list = []
  if (vmPortGroup.value) list.push(vmPortGroup.value)
  if (vmOutPortGroup.value) list.push(vmOutPortGroup.value)
  return list
})
//安全组分页
const sgPage = ref(1)
const sgPageSize = ref(10)
const pagedSecurityGroups = computed(() =>{
   const start = (sgPage.value -1) * sgPageSize.value
    return sgTableData.value.slice(start,start + sgPageSize.value)
  })
const sgSubmitLoading = ref(false)
const sgDetailLoading = ref(false)
const sgHostOptions = ref([])

const loadSgHostOptions = async () => {
  try {
    const res = await getRemoteHostList({ service_id: serviceId.value, page: 1, page_size: 10 })
    const body = res?.data
    if (body?.code === 200 && body?.data) {
      const inner = body.data
      sgHostOptions.value = Array.isArray(inner) ? inner : (inner.hosts || inner.list || inner.data || [])
    }
  } catch (e) { console.error('加载宿主机列表失败:', e) }
}

// 绑定安全组到当前虚拟机（通过安全组选择器）
const sgBindVisible = ref(false)
const handleSgBind = () => { sgBindVisible.value = true }
const handleSgBindConfirm = async (sg) => {
  if (!sg?.id) return
  actionLoading.value = true
  try {
    const fd = new FormData(); fd.append('service_id', serviceId.value); fd.append('id', sg.id); fd.append('vm_id', vmId.value)
    const res = await bindSecurityGroup(fd)
    if (res?.data?.code === 200) { ElMessage.success('绑定成功'); loadDetail() }
    else ElMessage.error(extractApiError(res?.data, '绑定失败'))
  } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '绑定失败')) } finally { actionLoading.value = false }
}

// 解绑指定安全组与当前虚拟机
const handleSgUnbindFromVm = async (sg) => {
  if (!sg) return
  try { await ElMessageBox.confirm(`确定要解绑安全组「${sg.name}」（${sg.direction === 'in' ? '入站' : '出站'}）吗？`, '解绑安全组', { type: 'warning' }) } catch { return }
  actionLoading.value = true
  try {
    const fd = new FormData(); fd.append('service_id', serviceId.value); fd.append('id', sg.id); fd.append('vm_id', vmId.value)
    const res = await unbindSecurityGroup(fd)
    if (res?.data?.code === 200) { ElMessage.success('解绑成功'); loadDetail() }
    else ElMessage.error(extractApiError(res?.data, '解绑失败'))
  } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '解绑失败')) } finally { actionLoading.value = false }
}

// 创建安全组
const sgCreateDialogVisible = ref(false)
const sgCreateFormRef = ref(null)
const sgCreateForm = reactive({ name: '', host_id: null, direction: '', lock: false, drop_all: false })
const sgCreateRules = {
  name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
  direction: [{ required: true, message: '请选择方向', trigger: 'change' }],
  host_id: [{ required: true, message: '请选择宿主机', trigger: 'change' }]
}
const handleSgCreate = async () => {
  Object.assign(sgCreateForm, { name: '', host_id: vmHostId.value || null, direction: '', lock: false, drop_all: false })
  sgCreateDialogVisible.value = true
  if (!sgHostOptions.value.length) {
    dialogOptionsLoading.value = true
    try { await loadSgHostOptions() } finally { dialogOptionsLoading.value = false }
  }
}
const submitSgCreate = () => {
  sgCreateFormRef.value?.validate(async (valid) => {
    if (!valid) return
    sgSubmitLoading.value = true
    try {
      const fd = new FormData()
      fd.append('service_id', serviceId.value)
      fd.append('name', sgCreateForm.name)
      fd.append('host_id', sgCreateForm.host_id)
      fd.append('direction', sgCreateForm.direction)
      fd.append('lock', sgCreateForm.lock ? 'true' : 'false')
      fd.append('drop_all', sgCreateForm.drop_all ? 'true' : 'false')
      const res = await createSecurityGroup(fd)
      if (res?.data?.code === 200) {
        ElMessage.success('创建成功')
        sgCreateDialogVisible.value = false
        loadDetail()
      } else ElMessage.error(extractApiError(res?.data, '创建失败'))
    } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '创建失败')) } finally { sgSubmitLoading.value = false }
  })
}

// 同步安全组
const sgSyncDialogVisible = ref(false)
const sgSyncTarget = ref(null)
const sgSyncHostId = ref(null)
const handleSgSync = async (row) => {
  sgSyncTarget.value = row
  sgSyncHostId.value = row.host_id || vmHostId.value || null
  sgSyncDialogVisible.value = true
  if (!sgHostOptions.value.length) {
    dialogOptionsLoading.value = true
    try { await loadSgHostOptions() } finally { dialogOptionsLoading.value = false }
  }
}
const submitSgSync = async () => {
  if (!sgSyncHostId.value) { ElMessage.warning('请选择宿主机'); return }
  sgSubmitLoading.value = true
  try {
    const fd = new FormData(); fd.append('service_id', serviceId.value); fd.append('id', sgSyncTarget.value.id); fd.append('host_id', sgSyncHostId.value)
    const res = await syncSecurityGroup(fd)
    if (res?.data?.code === 200) {
      ElMessage.success('同步成功')
      sgSyncDialogVisible.value = false
      loadDetail()
    } else ElMessage.error(extractApiError(res?.data, '同步失败'))
  } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '同步失败')) } finally { sgSubmitLoading.value = false }
}

// 应用安全组
const handleSgApply = (row) => {
  ElMessageBox.confirm(`确定要应用安全组「${row.name}」的规则到所有已绑定虚拟机吗？`, '应用安全组', {
    confirmButtonText: '确定应用', cancelButtonText: '取消', type: 'info'
  }).then(async () => {
    try {
      const fd = new FormData(); fd.append('service_id', serviceId.value); fd.append('id', row.id)
      const res = await applySecurityGroup(fd)
      if (res?.data?.code === 200) ElMessage.success('应用成功')
      else ElMessage.error(extractApiError(res?.data, '应用失败'))
    } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '应用失败')) }
  }).catch(() => {})
}

// 编辑（跳转安全组详情页）
const handleSgGoDetail = (row) => {
  router.push({ path: '/virtualization/security-group-detail', query: { service_id: serviceId.value, service_name: serviceName.value, sg_id: row.id } })
}

// 白名单切换
const handleSgToggleWhitelist = (row) => {
  const action = row.drop_all ? '关闭' : '开启'
  ElMessageBox.confirm(`确定要${action}安全组「${row.name}」的白名单模式吗？`, `${action}白名单`, {
    confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning'
  }).then(async () => {
    try {
      const api = row.drop_all ? disableSecurityGroupWhitelist : enableSecurityGroupWhitelist
      const fd = new FormData(); fd.append('service_id', serviceId.value); fd.append('id', row.id)
      const res = await api(fd)
      if (res?.data?.code === 200) { ElMessage.success(`${action}成功`); loadDetail() }
      else ElMessage.error(extractApiError(res?.data, `${action}失败`))
    } catch (e) { ElMessage.error(extractApiError(e?.response?.data, `${action}失败`)) }
  }).catch(() => {})
}

// 删除安全组
const handleSgDelete = (row) => {
  ElMessageBox.confirm(`确定要删除安全组「${row.name}」吗？`, '删除确认', {
    confirmButtonText: '确定删除', cancelButtonText: '取消', type: 'warning'
  }).then(async () => {
    try {
      const res = await deleteSecurityGroup({ service_id: serviceId.value, id: row.id })
      if (res?.data?.code === 200) { ElMessage.success('删除成功'); loadDetail() }
      else ElMessage.error(extractApiError(res?.data, '删除失败'))
    } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '删除失败')) }
  }).catch(() => {})
}

// 绑定/解绑虚拟机（操作栏更多菜单，针对其他虚拟机）
const sgVmBindDialogVisible = ref(false)
const sgVmBindType = ref('bind')
const sgVmBindTarget = ref(null)
const handleSgBindVm = (row) => {
  sgVmBindType.value = 'bind'; sgVmBindTarget.value = row
  sgVmBindDialogVisible.value = true
}
const handleSgUnbindVm = (row) => {
  sgVmBindType.value = 'unbind'; sgVmBindTarget.value = row
  sgVmBindDialogVisible.value = true
}
const submitSgVmBind = async () => {
  sgSubmitLoading.value = true
  try {
    const api = sgVmBindType.value === 'bind' ? bindSecurityGroup : unbindSecurityGroup
    const fd = new FormData(); fd.append('service_id', serviceId.value); fd.append('id', sgVmBindTarget.value.id); fd.append('vm_id', vmId.value)
    const res = await api(fd)
    if (res?.data?.code === 200) {
      ElMessage.success(sgVmBindType.value === 'bind' ? '绑定成功' : '解绑成功')
      sgVmBindDialogVisible.value = false
      loadDetail()
    } else ElMessage.error(extractApiError(res?.data, '操作失败'))
  } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '操作失败')) } finally { sgSubmitLoading.value = false }
}

// 查看安全组详情（含规则）
const sgDetailVisible = ref(false)
const sgCurrentDetail = ref(null)
const handleSgViewDetail = async (row) => {
  sgDetailVisible.value = true
  sgDetailLoading.value = true
  sgCurrentDetail.value = row
  try {
    const res = await getSecurityGroupDetail({ service_id: serviceId.value, id: row.id })
    if (res?.data?.code === 200 && res?.data?.data) {
      const inner = res.data.data
      sgCurrentDetail.value = inner.group || inner.data || inner
    }
  } catch { /* fallback */ } finally { sgDetailLoading.value = false }
}

// 安全组规则操作
const sgRuleDialogVisible = ref(false)
const sgRuleDialogType = ref('add')
const sgRuleFormRef = ref(null)
const sgRuleForm = reactive({ id: undefined, group_id: 0, protocol: 'tcp', action: 'allow', port_range: '', ip_range: '', priority: 0, port_group_id: '' })
const sgRuleRules = {
  protocol: [{ required: true, message: '请选择协议', trigger: 'change' }],
  action: [{ required: true, message: '请选择动作', trigger: 'change' }]
}
const handleSgAddRule = () => {
  sgRuleDialogType.value = 'add'
  Object.assign(sgRuleForm, { id: undefined, group_id: sgCurrentDetail.value?.id || 0, protocol: 'tcp', action: 'allow', port_range: '', ip_range: '', priority: 0, port_group_id: '' })
  sgRuleDialogVisible.value = true
}
const handleSgEditRule = (rule) => {
  sgRuleDialogType.value = 'edit'
  Object.assign(sgRuleForm, {
    id: rule.id, group_id: sgCurrentDetail.value?.id || 0, port_group_id: sgCurrentDetail.value?.id || '',
    protocol: rule.protocol || 'tcp', action: rule.action || 'allow',
    port_range: rule.port_range || '', ip_range: rule.ip_range || '', priority: rule.priority || 0
  })
  sgRuleDialogVisible.value = true
}
const submitSgRule = () => {
  sgRuleFormRef.value?.validate(async (valid) => {
    if (!valid) return
    sgSubmitLoading.value = true
    try {
      const fd = new FormData()
      fd.append('service_id', serviceId.value)
      fd.append('group_id', sgRuleForm.group_id)
      if (sgRuleForm.id) fd.append('id', sgRuleForm.id)
      if (sgRuleForm.port_group_id) fd.append('port_group_id', sgRuleForm.port_group_id)
      fd.append('protocol', sgRuleForm.protocol)
      fd.append('action', sgRuleForm.action)
      if (sgRuleForm.port_range) fd.append('port_range', sgRuleForm.port_range)
      if (sgRuleForm.ip_range) fd.append('ip_range', sgRuleForm.ip_range)
      fd.append('priority', sgRuleForm.priority || 0)
      const res = sgRuleDialogType.value === 'add'
        ? await createSecurityGroupRule(fd)
        : await updateSecurityGroupRule(fd)
      if (res?.data?.code === 200) {
        ElMessage.success(sgRuleDialogType.value === 'add' ? '规则创建成功' : '规则修改成功')
        sgRuleDialogVisible.value = false
        handleSgViewDetail(sgCurrentDetail.value)
      } else ElMessage.error(extractApiError(res?.data, '操作失败'))
    } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '操作失败')) } finally { sgSubmitLoading.value = false }
  })
}
const handleSgDeleteRule = (rule) => {
  ElMessageBox.confirm('确定要删除该规则吗？', '删除确认', {
    confirmButtonText: '确定删除', cancelButtonText: '取消', type: 'warning'
  }).then(async () => {
    try {
      const res = await deleteSecurityGroupRule({ service_id: serviceId.value, id: rule.id })
      if (res?.data?.code === 200) {
        ElMessage.success('删除成功')
        handleSgViewDetail(sgCurrentDetail.value)
      } else ElMessage.error(extractApiError(res?.data, '删除失败'))
    } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '删除失败')) }
  }).catch(() => {})
}

// 更多菜单
const handleSgRowMore = (row, command) => {
  if (command === 'unbindCurrent') handleSgUnbindFromVm(row)
  else if (command === 'bindVm') handleSgBindVm(row)
  else if (command === 'unbindVm') handleSgUnbindVm(row)
  else if (command === 'whitelist') handleSgToggleWhitelist(row)
  else if (command === 'viewDetail') handleSgViewDetail(row)
  else if (command === 'delete') handleSgDelete(row)
}

// ---- 快照/备份管理 ----
const snapshotList = ref([])
const snapshotLoading = ref(false)
const snapshotQuota = ref(null)
const snapshotPage = ref(1)
const snapshotPageSize = ref(10)
const snapshotTotal = ref(0)
const backupList = ref([])
const backupLoading = ref(false)
const backupQuota = ref(null)
const backupPage = ref(1)
const backupPageSize = ref(10)
const backupTotal = ref(0)
const snapshotCreateVisible = ref(false)
const backupCreateVisible = ref(false)
const snapshotForm = reactive({ name: '' })
const backupForm = reactive({ name: '' })
const taskProgressVisible = ref(false)
const taskProgressLoading = ref(false)
const taskProgressData = ref(null)
const taskProgressTitle = ref('')

const taskStatusType = (s) => ({ running: 'primary', completed: 'success', ready: 'success', success: 'success', failed: 'danger', error: 'danger', pending: 'info' }[s] || 'info')
const snapshotStatusLabel = (s) => ({ completed: '完成', ready: '完成', success: '成功', pending: '等待', running: '运行中', failed: '失败', error: '错误' }[s] || s || '-')
const taskMetaLabel = (key) => ({ vm_name: '虚拟机名称', backup_path: '备份路径', snapshot_path: '快照路径', path: '路径', progress: '进度', message: '信息', error: '错误信息' }[key] || key)

const taskProgressMeta = computed(() => {
  if (!taskProgressData.value?.meta) return null
  const raw = taskProgressData.value.meta
  if (typeof raw === 'object') return raw
  if (typeof raw === 'string') {
    const trimmed = raw.trim()
    if (!trimmed || trimmed === '""' || trimmed === '{}') return null
    try { return JSON.parse(trimmed) } catch { return { 信息: raw } }
  }
  return null
})

const loadSnapshots = async () => {
  snapshotLoading.value = true
  try {
    const res = await getSnapshotList({ service_id: serviceId.value, vm_id: vmId.value, page: snapshotPage.value, page_size: snapshotPageSize.value })
    if (res?.data?.code === 200 && res?.data?.data) {
      const d = res.data.data
      snapshotList.value = d.data || d.list || (Array.isArray(d) ? d : [])
      snapshotTotal.value = d.meta?.count ?? d.total ?? snapshotList.value.length
    } else { snapshotList.value = []; snapshotTotal.value = 0 }
  } catch { snapshotList.value = []; snapshotTotal.value = 0 } finally { snapshotLoading.value = false }
}

const loadSnapshotQuota = async () => {
  try {
    const res = await getSnapshotCount({ service_id: serviceId.value, vm_id: vmId.value })
    if (res?.data?.code === 200 && res?.data?.data) {
      const d = res.data.data
      snapshotQuota.value = { count: d.count ?? 0, limit: d.limit ?? 0 }
    }
  } catch { /* ignore */ }
}

const handleSetSnapshotLimit = () => {
  ElMessageBox.prompt('请输入快照数量上限', '设置快照上限', {
    confirmButtonText: '确定', cancelButtonText: '取消',
    inputPattern: /^[1-9]\d*$/, inputErrorMessage: '请输入正整数',
    inputValue: String(snapshotQuota.value?.limit || 10)
  }).then(async ({ value }) => {
    try {
      const fd = new FormData()
      fd.append('service_id', serviceId.value)
      fd.append('vm_id', vmId.value)
      fd.append('limit', value)
      const res = await setSnapshotLimit(fd)
      if (res?.data?.code === 200) { ElMessage.success('快照上限设置成功'); loadSnapshotQuota() }
      else ElMessage.error(extractApiError(res?.data, '设置失败'))
    } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '设置失败')) }
  }).catch(() => {})
}

const loadBackups = async () => {
  backupLoading.value = true
  try {
    const res = await getBackupList({ service_id: serviceId.value, vm_id: vmId.value, page: backupPage.value, page_size: backupPageSize.value })
    if (res?.data?.code === 200 && res?.data?.data) {
      const d = res.data.data
      backupList.value = d.data || d.list || (Array.isArray(d) ? d : [])
      backupTotal.value = d.meta?.count ?? d.total ?? backupList.value.length
    } else { backupList.value = []; backupTotal.value = 0 }
  } catch { backupList.value = []; backupTotal.value = 0 } finally { backupLoading.value = false }
}

const loadBackupQuota = async () => {
  try {
    const res = await getBackupCount({ service_id: serviceId.value, vm_id: vmId.value })
    if (res?.data?.code === 200 && res?.data?.data) {
      const d = res.data.data
      backupQuota.value = { count: d.count ?? 0, limit: d.limit ?? 0 }
    }
  } catch { /* ignore */ }
}

const handleSetBackupLimit = () => {
  ElMessageBox.prompt('请输入备份数量上限', '设置备份上限', {
    confirmButtonText: '确定', cancelButtonText: '取消',
    inputPattern: /^[1-9]\d*$/, inputErrorMessage: '请输入正整数',
    inputValue: String(backupQuota.value?.limit || 10)
  }).then(async ({ value }) => {
    try {
      const fd = new FormData()
      fd.append('service_id', serviceId.value)
      fd.append('vm_id', vmId.value)
      fd.append('limit', value)
      const res = await setBackupLimit(fd)
      if (res?.data?.code === 200) { ElMessage.success('备份上限设置成功'); loadBackupQuota() }
      else ElMessage.error(extractApiError(res?.data, '设置失败'))
    } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '设置失败')) }
  }).catch(() => {})
}

const handleCreateSnapshot = () => {
  Object.assign(snapshotForm, { name: '' })
  snapshotCreateVisible.value = true
}
const submitCreateSnapshot = async () => {
  if (!snapshotForm.name) { ElMessage.warning('请输入快照名称'); return }
  actionLoading.value = true
  try {
    const fd = new FormData()
    fd.append('service_id', serviceId.value)
    fd.append('vm_id', vmId.value)
    fd.append('name', snapshotForm.name)
    const res = await createSnapshot(fd)
    if (res?.data?.code === 200) { ElMessage.success('快照创建成功'); snapshotCreateVisible.value = false; loadSnapshots(); loadSnapshotQuota() }
    else ElMessage.error(extractApiError(res?.data, '创建失败'))
  } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '创建失败')) } finally { actionLoading.value = false }
}

const handleRestoreSnapshot = (row) => {
  ElMessageBox.confirm(`确定要恢复快照「${row.name}」吗？`, '恢复确认', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' })
    .then(async () => {
      try {
        const fd = new FormData()
        fd.append('service_id', serviceId.value)
        fd.append('snapshot_id', row.id)
        fd.append('vm_id', vmId.value)
        const res = await restoreSnapshot(fd)
        if (res?.data?.code === 200) ElMessage.success('恢复操作已提交')
        else ElMessage.error(extractApiError(res?.data, '恢复失败'))
      } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '恢复失败')) }
    }).catch(() => {})
}

const handleDeleteSnapshot = (row) => {
  ElMessageBox.confirm(`确定要删除快照「${row.name}」吗？`, '删除确认', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' })
    .then(async () => {
      try {
        const fd = new FormData()
        fd.append('service_id', serviceId.value)
        fd.append('snapshot_id', row.id)
        fd.append('vm_id', row.vm_id)
        const res = await deleteSnapshot(fd)
        if (res?.data?.code === 200) { ElMessage.success('删除成功'); loadSnapshots(); loadSnapshotQuota() }
        else ElMessage.error(extractApiError(res?.data, '删除失败'))
      } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '删除失败')) }
    }).catch(() => {})
}

const handleSnapshotProgress = async (row) => {
  taskProgressTitle.value = '快照任务进度'
  taskProgressData.value = null
  taskProgressVisible.value = true
  taskProgressLoading.value = true
  try {
    const res = await getSnapshotProgress({ service_id: serviceId.value, task_id: String(row.task_id || row.id) })
    if (res?.data?.code === 200) taskProgressData.value = res.data.data?.data ?? res.data.data
    else ElMessage.warning('暂无进度信息')
  } catch { ElMessage.warning('获取进度失败') } finally { taskProgressLoading.value = false }
}

const handleCreateBackup = () => {
  Object.assign(backupForm, { name: '' })
  backupCreateVisible.value = true
}
const submitCreateBackup = async () => {
  if (!backupForm.name) { ElMessage.warning('请输入备份名称'); return }
  actionLoading.value = true
  try {
    const fd = new FormData()
    fd.append('service_id', serviceId.value)
    fd.append('vm_id', vmId.value)
    fd.append('name', backupForm.name)
    const res = await createBackup(fd)
    if (res?.data?.code === 200) { ElMessage.success('备份创建成功'); backupCreateVisible.value = false; loadBackups(); loadBackupQuota() }
    else ElMessage.error(extractApiError(res?.data, '创建失败'))
  } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '创建失败')) } finally { actionLoading.value = false }
}

const handleRestoreBackup = (row) => {
  ElMessageBox.confirm(`确定要恢复备份「${row.name}」吗？`, '恢复确认', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' })
    .then(async () => {
      try {
        const fd = new FormData()
        fd.append('service_id', serviceId.value)
        fd.append('backup_id', row.id)
        fd.append('vm_id', vmId.value)
        const res = await restoreBackup(fd)
        if (res?.data?.code === 200) ElMessage.success('恢复操作已提交')
        else ElMessage.error(extractApiError(res?.data, '恢复失败'))
      } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '恢复失败')) }
    }).catch(() => {})
}

const handleDeleteBackup = (row) => {
  ElMessageBox.confirm(`确定要删除备份「${row.name}」吗？`, '删除确认', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' })
    .then(async () => {
      try {
        const fd = new FormData()
        fd.append('service_id', serviceId.value)
        fd.append('backup_id', row.id)
        fd.append('vm_id', row.vm_id)
        const res = await deleteBackup(fd)
        if (res?.data?.code === 200) { ElMessage.success('删除成功'); loadBackups(); loadBackupQuota() }
        else ElMessage.error(extractApiError(res?.data, '删除失败'))
      } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '删除失败')) }
    }).catch(() => {})
}

const handleBackupProgress = async (row) => {
  taskProgressTitle.value = '备份任务进度'
  taskProgressData.value = null
  taskProgressVisible.value = true
  taskProgressLoading.value = true
  try {
    const res = await getBackupProgress({ service_id: serviceId.value, task_id: String(row.task_id || row.id) })
    if (res?.data?.code === 200) taskProgressData.value = res.data.data?.data ?? res.data.data
    else ElMessage.warning('暂无进度信息')
  } catch { ElMessage.warning('获取进度失败') } finally { taskProgressLoading.value = false }
}

const goBack = () => {
  tagsViewStore.delVisitedView(route)
  // router.push({ path: '/virtualization/kvm-service-detail', query: { service_id: serviceId.value, service_name: serviceName.value } })
  router.back()
}

// ---- 组网管理 ----
const vnLoading = ref(false)
const vnSubmitLoading = ref(false)
const vnList = ref([])
const vnJoinVisible = ref(false)
const vnJoinTarget = ref(null)
const vnJoinIp = ref('')

const vmBridgeNameMap = computed(() => {
  const map = {}
  for (const n of vmNetworks.value) {
    if (n.bridge_name) map[n.bridge_name] = n
  }
  return map
})

const loadVmNetworkingList = async () => {
  if (!detail.value) return
  vnLoading.value = true
  vnList.value = []
  try {
    const userId = detail.value.user_id
    const hostId = vmHostId.value
    if (!userId || !hostId) { vnLoading.value = false; return }
    const res = await getUserNetworkingList({
      service_id: serviceId.value, page: 1, count: 10,
      host_id: hostId, user_id: userId
    })
    if (res?.data?.code === 200 && res?.data?.data) {
      const inner = res.data.data
      const networkings = Array.isArray(inner) ? inner : (inner.data || [])
      const results = []
      for (const nw of networkings) {
        const matchedVmNet = nw.bridge_name ? vmBridgeNameMap.value[nw.bridge_name] : null
        results.push({ networking: nw, network: matchedVmNet || null })
      }
      vnList.value = results
    }
  } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '获取组网信息失败')) }
  finally { vnLoading.value = false }
}

// 创建组网
const vnCreateVisible = ref(false)
const vnCreateFormRef = ref(null)
const vnCreateForm = reactive({ name: '', bridge_name: '', gateway: '' })
const vnCreateHostName = ref('')
const vnCreateUserName = ref('')
const vnCreateRules = {
  bridge_name: [{ required: true, message: '请输入网桥名称', trigger: 'blur' }]
}

const handleVnCreate = async () => {
  Object.assign(vnCreateForm, { name: '', bridge_name: '', gateway: '' })
  vnCreateHostName.value = ''
  vnCreateUserName.value = ''
  vnCreateVisible.value = true
  const userId = detail.value?.user_id
  const hostId = vmHostId.value
  const requests = []
  if (userId) {
    requests.push(
      getUserInfo({ user_id: userId }).then(res => {
        if (res?.data?.code === 200 && res?.data?.data) {
          vnCreateUserName.value = res.data.data.UserName || ''
        }
      }).catch(() => {})
    )
  }
  if (hostId) {
    requests.push(
      getRemoteHostDetail({ service_id: serviceId.value, id: hostId }).then(res => {
        if (res?.data?.code === 200 && res?.data?.data) {
          const h = res.data.data.host ?? res.data.data
          vnCreateHostName.value = h.name || ''
        }
      }).catch(() => {})
    )
  }
  await Promise.all(requests)
}

const submitVnCreate = () => {
  vnCreateFormRef.value?.validate(async (valid) => {
    if (!valid) return
    vnSubmitLoading.value = true
    try {
      const fd = new FormData()
      fd.append('service_id', serviceId.value)
      fd.append('name', vnCreateForm.name)
      fd.append('bridge_name', vnCreateForm.bridge_name)
      fd.append('host_id', vmHostId.value)
      fd.append('user_id', detail.value?.user_id || 0)
      if (vnCreateForm.gateway) fd.append('gateway', vnCreateForm.gateway)
      const res = await createUserNetworking(fd)
      if (res?.data?.code === 200) {
        ElMessage.success('创建组网成功')
        vnCreateVisible.value = false
        loadVmNetworkingList()
      } else ElMessage.error(extractApiError(res?.data, '创建组网失败'))
    } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '创建组网失败')) }
    finally { vnSubmitLoading.value = false }
  })
}

const handleJoinNetworking = (row) => {
  vnJoinTarget.value = row.networking
  vnJoinIp.value = ''
  vnJoinVisible.value = true
}

const submitJoinNetworking = async () => {
  if (!vnJoinTarget.value) return
  vnSubmitLoading.value = true
  try {
    const fd = new FormData()
    fd.append('service_id', serviceId.value)
    fd.append('networking_id', vnJoinTarget.value.id)
    fd.append('vm_id', vmId.value)
    if (vnJoinIp.value.trim()) fd.append('ip', vnJoinIp.value.trim())
    const res = await assignUserNetworking(fd)
    if (res?.data?.code === 200) {
      ElMessage.success('加入组网成功')
      vnJoinVisible.value = false
      loadVmNetworkingList()
      loadDetail()
    } else ElMessage.error(extractApiError(res?.data, '加入组网失败'))
  } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '加入组网失败')) }
  finally { vnSubmitLoading.value = false }
}

const handleLeaveNetworking = (row) => {
  const netId = row.network?.id
  const networkingId = row.networking?.id
  if (!netId || !networkingId) { ElMessage.warning('缺少网络信息'); return }
  ElMessageBox.confirm(
    `确定要将该虚拟机从组网「${row.networking?.name || networkingId}」中移除？`,
    '移除确认', { type: 'warning' }
  ).then(async () => {
    try {
      const fd = new FormData()
      fd.append('service_id', serviceId.value)
      fd.append('networking_id', networkingId)
      fd.append('network_id', netId)
      fd.append('vm_id', vmId.value)
      const res = await removeUserNetworkingNetwork(fd)
      if (res?.data?.code === 200) {
        ElMessage.success('已移除')
        loadVmNetworkingList()
        loadDetail()
      } else ElMessage.error(extractApiError(res?.data, '移除失败'))
    } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '移除失败')) }
  }).catch(() => {})
}

let loadedVmId = null
const initPage = async () => {
  if (!vmId.value || loadedVmId === vmId.value) return
  loadedVmId = vmId.value
  historicalMetricsData.value = null
  dataMigrateProgressData.value = null
  dataMigrationId.value = ''
  dataMigrateTaskId.value = ''
  stopMigratePolling()
  disposeCharts()
  loadHostOptions()
  await loadDetail()
  triggerTabLoad(activeTab.value)
}

const triggerTabLoad = (tab) => {
  if (tab === 'monitor' && !historicalMetricsData.value) loadHistoricalMetrics()
  if (tab === 'snapshot') { loadSnapshots(); loadSnapshotQuota() }
  if (tab === 'backup') { loadBackups(); loadBackupQuota() }
  if (tab === 'userNetworking') loadVmNetworkingList()
  if (tab === 'security') loadSgLockInfo()
  if (tab === 'trafficManage') { loadVmTrafficPolicy(); loadTrafficHourly() }
}

// 请求安全组详情补充 lock 字段
const loadSgLockInfo = async () => {
  const groups = [vmPortGroup.value, vmOutPortGroup.value].filter(Boolean)
  for (const sg of groups) {
    try {
      const res = await getSecurityGroupDetail({ service_id: serviceId.value, id: sg.id })
      if (res?.data?.code === 200 && res?.data?.data) {
        const d = res.data.data.group || res.data.data.data || res.data.data
        if (vmPortGroup.value?.id === sg.id) vmPortGroup.value = { ...vmPortGroup.value, lock: d.lock ?? sg.lock }
        if (vmOutPortGroup.value?.id === sg.id) vmOutPortGroup.value = { ...vmOutPortGroup.value, lock: d.lock ?? sg.lock }
      }
    } catch { /* */ }
  }
}

const isCurrentRoute = () => route.name === 'VirtVmDetail'

watch(vmId, () => { if (isPageActive && isCurrentRoute()) initPage() })
watch(activeTab, (tab) => { if (detail.value) triggerTabLoad(tab) })
onActivated(async () => {
  isPageActive = true
  await nextTick()
  if (isCurrentRoute() && loadedVmId !== vmId.value) initPage()
})
onDeactivated(() => { isPageActive = false; stopMigratePolling() })
onBeforeUnmount(() => { isPageActive = false; disposeCharts(); stopMigratePolling(); stopDetailAutoRefresh() })
onMounted(() => { isPageActive = true; initPage() })
</script>

<style scoped>
.vm-detail-page { padding: 0; background: #f5f7fa; min-height: 100vh; }
.page-header { display: flex; justify-content: space-between; align-items: center; padding: 12px 24px; background: #fff; border-bottom: 1px solid #e8e8e8; }
.header-left { display: flex; align-items: center; gap: 0; }
.back-btn { font-size: 14px; color: #606266; }
.back-btn:hover { color: #409eff; }
.header-right { display: flex; gap: 8px; }
.main-content { padding: 20px 24px; }

/* 实例概览 */
.instance-overview { display: flex; justify-content: space-between; align-items: center; background: #fff; padding: 20px 24px; border-radius: 4px; margin-bottom: 0; border: 1px solid #e8e8e8; border-bottom: none; }
.instance-name { margin: 0; font-size: 18px; font-weight: 600; color: #1d2129; }
.instance-id { font-size: 14px; font-weight: 400; color: #86909c; margin-left: 8px; }
.overview-actions { display: flex; gap: 8px; }

/* 状态概览条 */
.status-bar { display: flex; gap: 0; background: #fff; padding: 16px 24px; border: 1px solid #e8e8e8; border-top: 1px solid #f0f0f0; border-radius: 0 0 4px 4px; margin-bottom: 16px; }
.status-item { flex: 1; display: flex; flex-direction: column; gap: 4px; }
.status-item + .status-item { border-left: 1px solid #e8e8e8; padding-left: 24px; }
.status-label { font-size: 12px; color: #86909c; }
.status-value { font-size: 14px; color: #1d2129; display: flex; align-items: center; gap: 6px; }
.status-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; flex-shrink: 0; }
.dot-running { background: #00b42a; }
.dot-other { background: #86909c; }

/* 标签页 */
.detail-tabs { background: #fff; border-radius: 4px; border: 1px solid #e8e8e8; padding: 0 24px; }
:deep(.detail-tabs > .el-tabs__header) { margin-bottom: 0; }
:deep(.detail-tabs > .el-tabs__content) { padding: 0 0 24px; }

/* 区块 */
.section-block { margin-top: 24px; }
.section-title { font-size: 15px; font-weight: 600; color: #1d2129; margin: 0 0 16px; padding-bottom: 8px; border-bottom: 1px solid #f0f0f0; }
.section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
.section-header .section-title { margin-bottom: 0; padding-bottom: 0; border-bottom: none; }

/* 配置信息网格 */
.config-grid { border: 1px solid #e8e8e8; border-radius: 4px; overflow: hidden; }
.config-row { display: flex; border-bottom: 1px solid #e8e8e8; }
.config-row:last-child { border-bottom: none; }
.config-cell { flex: 1; padding: 14px 16px; display: flex; flex-direction: column; gap: 6px; border-right: 1px solid #e8e8e8; }
.config-cell:last-child { border-right: none; }
.config-label { font-size: 12px; color: #86909c; line-height: 1; }
.config-value { font-size: 14px; color: #1d2129; line-height: 1.4; word-break: break-all; }
.cfg-edit-btn { margin-left: 8px; padding: 0 4px; height: 18px; vertical-align: middle; }
.cfg-edit-btn .el-icon { margin-right: 2px; vertical-align: -2px; }
.traffic-cell { display: flex; flex-direction: column; gap: 2px; }
.traffic-cell .traffic-main { font-size: 14px; color: #1d2129; }
.traffic-cell .traffic-sub { font-size: 12px; color: #86909c; line-height: 1.3; }
.traffic-cell .traffic-cycle { color: #c0c4cc; margin-left: 2px; }
.traffic-cell .traffic-actions { display: inline-flex; gap: 6px; margin-top: 2px; }
.traffic-cell .traffic-actions .cfg-edit-btn { margin-left: 0; }
.threshold-summary { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; }
.threshold-policy-grid { display: grid; grid-template-columns: 70px 1fr 1fr; gap: 8px 12px; align-items: start; }
.threshold-policy-title { padding-top: 9px; font-size: 14px; font-weight: 600; color: #303133; }
.threshold-policy-grid :deep(.el-form-item) { margin-bottom: 14px; }
.form-unit { margin-left: 8px; color: #86909c; }
.spec-value { font-size: 13px; color: #4e5969; }
.ip-value { color: #165dff; font-weight: 500; }
.password-cell { display: flex; align-items: center; gap: 8px; }
.password-cell code { font-family: 'Cascadia Code', Consolas, monospace; font-size: 13px; color: #1d2129; }
.mono-text { font-family: 'Cascadia Code', Consolas, monospace; font-size: 13px; }

.tk-form-tip { display: block; font-size: 12px; color: #909399; line-height: 1.5; margin-top: 4px; }

/* 迁移横幅 */
.migrate-banner { display: flex; justify-content: space-between; align-items: center; background: linear-gradient(135deg, #fffbf0 0%, #fff7e6 100%); border: 1px solid #faecd8; border-radius: 8px; padding: 14px 20px; margin-bottom: 16px; box-shadow: 0 1px 4px rgba(230,162,60,.1); }
.migrate-banner-info { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; min-width: 0; }
.migrate-banner-title { font-weight: 600; font-size: 14px; color: #e6a23c; white-space: nowrap; }
.migrate-progress-text { font-size: 16px; font-weight: 700; color: #e6a23c; letter-spacing: .5px; }
.migrate-divider { width: 1px; height: 16px; background: #e6a23c; opacity: .35; flex-shrink: 0; }
.migrate-speed { font-size: 14px; font-weight: 700; color: #409eff; letter-spacing: .3px; }
.migrate-msg { font-size: 13px; color: #8c8c8c; max-width: 260px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.migrate-banner-actions { display: flex; gap: 8px; flex-shrink: 0; margin-left: 16px; }
.migrate-spin { animation: migrate-rotate 1s linear infinite; color: #e6a23c; font-size: 18px; }
@keyframes migrate-rotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }

/* 监控指标 */
.metrics-card { margin-bottom: 0; }
.metrics-title { font-weight: 600; font-size: 13px; display: inline-flex; align-items: center; gap: 6px; }
.metrics-title .el-icon { font-size: 16px; color: #409eff; }
.chart-container { width: 100%; height: 220px; }

.metric-summary-row { display: flex; gap: 16px; margin-bottom: 16px; }
.metric-summary-card { flex: 1; min-width: 0; background: #f7f8fa; border-radius: 6px; padding: 14px 16px; border: 1px solid #e8e8e8; display: flex; flex-direction: column; }
.metric-summary-label { font-size: 12px; color: #86909c; margin-bottom: 8px; }
.metric-summary-value { font-size: 22px; font-weight: 600; color: #1d2129; line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.metric-summary-sub { font-size: 12px; color: #86909c; margin-top: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

.vnc-result { margin-top: 12px; }
.rules-section { margin-top: 8px; }
.rules-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
.rules-header h4 { margin: 0; font-size: 15px; font-weight: 600; color: #303133; }
.ip-main { font-weight: 500; font-family: 'SF Mono', Consolas, monospace; font-size: 13px; }
.ip-more-tag { cursor: pointer; vertical-align: middle; flex-shrink: 0; margin-left: 4px; }
.ip-popover-header { display: flex; justify-content: space-between; align-items: center; padding-bottom: 8px; border-bottom: 1px solid #ebeef5; margin-bottom: 4px; font-size: 13px; color: #606266; }
.ip-popover-list { max-height: 240px; overflow-y: auto; scrollbar-width: none; -ms-overflow-style: none; }
.ip-popover-list::-webkit-scrollbar { display: none; }
.ip-popover-item { display: flex; justify-content: space-between; align-items: center; padding: 6px 0; border-bottom: 1px solid #f2f3f5; }
.ip-popover-item:last-child { border-bottom: none; }
.ip-popover-item .ip-text { font-family: 'SF Mono', Consolas, monospace; font-size: 13px; color: #303133; word-break: break-all; }
.ip-popover-item .ip-copy-btn { flex-shrink: 0; margin-left: 8px; }
</style>
