<template>
  <div class="host-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 :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="warning" plain @click="openTokenDialog"><el-icon><Key /></el-icon>创建注册令牌</el-button> -->
          <el-button type="primary" plain @click="handleEdit">编辑宿主机</el-button>
          <el-button type="danger" plain @click="handleDelete">删除</el-button>
        </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.is_active ? 'dot-running' : 'dot-other'"></span>
            {{ detail.is_active ? '启用' : '禁用' }}
          </span>
        </div>
        <div class="status-item">
          <span class="status-label">IP 地址</span>
          <span class="status-value">{{ detail.ip || '-' }}</span>
        </div>
        <div class="status-item">
          <span class="status-label">CPU</span>
          <span class="status-value">{{ detail.max_cpu || 0 }}核</span>
        </div>
        <div class="status-item">
          <span class="status-label">内存</span>
          <span class="status-value">{{ formatMemKB(detail.max_memory) }}</span>
        </div>
        <div class="status-item">
          <span class="status-label">磁盘</span>
          <span class="status-value">{{ formatDiskGB(detail.max_disk) }}</span>
        </div>
      </div>
      <div class="status-bar" v-if="detail">
        <div class="status-item">
          <span class="status-label">带宽</span>
          <span class="status-value">↓{{ detail.rx_bandwidth || 0 }} / ↑{{ detail.tx_bandwidth || 0 }} Mbps</span>
        </div>
        <div class="status-item" v-if="loadAvg">
          <span class="status-label">负载</span>
          <span class="status-value">
            <span :style="{ color: loadColor(loadAvg['1min'], hostMetrics?.cpu?.cpu_count) }">{{ loadAvg['1min']?.toFixed(2) }}</span> /
            <span :style="{ color: loadColor(loadAvg['5min'], hostMetrics?.cpu?.cpu_count) }">{{ loadAvg['5min']?.toFixed(2) }}</span> /
            <span :style="{ color: loadColor(loadAvg['15min'], hostMetrics?.cpu?.cpu_count) }">{{ loadAvg['15min']?.toFixed(2) }}</span>
          </span>
        </div>
        <div class="status-item" v-if="hostMetrics?.internet_speed">
          <span class="status-label">实时网速</span>
          <span class="status-value">↓{{ formatSpeedAuto(hostMetrics.internet_speed.rx_bytes) }} / ↑{{ formatSpeedAuto(hostMetrics.internet_speed.tx_bytes) }}</span>
        </div>
        <div class="status-item">
          <span class="status-label">创建时间</span>
          <span class="status-value">{{ formatTimestamp(detail.created_at) }}</span>
        </div>
      </div>

      <!-- 标签页 -->
      <el-tabs v-model="activeTab" class="detail-tabs" v-if="detail">
        <el-tab-pane label="基本信息" name="info">
          <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 mono-text">{{ detail.base_url || '-' }}</span>
                </div>
                <div class="config-cell">
                  <span class="config-label">SSH 端口</span>
                  <span class="config-value">{{ detail.port || '-' }}</span>
                </div>
                <div class="config-cell">
                  <span class="config-label">SSH 用户</span>
                  <span class="config-value">{{ detail.user || '-' }}</span>
                </div>
              </div>
              <div class="config-row">
                <div class="config-cell">
                  <span class="config-label">认证Token</span>
                  <span class="config-value secret-cell">
                    <template v-if="detail.token">
                      <code>{{ showToken ? detail.token : '••••••••••••' }}</code>
                      <el-button link type="primary" size="small" @click="showToken = !showToken">{{ showToken ? '隐藏' : '显示' }}</el-button>
                      <el-button v-if="showToken" link type="primary" size="small" @click="copyText(detail.token)">复制</el-button>
                    </template>
                    <span v-else class="text-muted">未设置</span>
                  </span>
                </div>
                <div class="config-cell">
                  <span class="config-label">SSH 密码</span>
                  <span class="config-value secret-cell">
                    <template v-if="detail.password">
                      <code>{{ showPassword ? detail.password : '••••••••' }}</code>
                      <el-button link type="primary" size="small" @click="showPassword = !showPassword">{{ showPassword ? '隐藏' : '显示' }}</el-button>
                      <el-button v-if="showPassword" link type="primary" size="small" @click="copyText(detail.password)">复制</el-button>
                    </template>
                    <span v-else class="text-muted">未设置</span>
                  </span>
                </div>
                <div class="config-cell">
                  <span class="config-label">私钥</span>
                  <span class="config-value secret-cell">
                    <template v-if="detail.private_key">
                      <code>{{ showPrivateKey ? detail.private_key.substring(0, 40) + '...' : '••••••••••••' }}</code>
                      <el-button link type="primary" size="small" @click="showPrivateKey = !showPrivateKey">{{ showPrivateKey ? '隐藏' : '显示' }}</el-button>
                      <el-button v-if="showPrivateKey" link type="primary" size="small" @click="copyText(detail.private_key)">复制</el-button>
                    </template>
                    <span v-else class="text-muted">未设置</span>
                  </span>
                </div>
              </div>
              <div class="config-row">
                <div class="config-cell">
                  <span class="config-label">宿主机组</span>
                  <span class="config-value">{{ detail.host_group_id ? `#${detail.host_group_id}` : '-' }}</span>
                </div>
                <div class="config-cell">
                  <span class="config-label">介绍</span>
                  <span class="config-value">{{ detail.description || '-' }}</span>
                </div>
                <div class="config-cell">
                  <span class="config-label">更新时间</span>
                  <span class="config-value">{{ formatTimestamp(detail.updated_at) }}</span>
                </div>
              </div>
            </div>
          </div>
          <!-- 实时监控概览 -->
          <div class="section-block" v-loading="hostMetricsLoading">
            <div class="section-header">
              <h3 class="section-title"><svg class="sec-icon" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" fill="#67c23a"/></svg> 实时监控</h3>
              <el-button size="small" :icon="Refresh" @click="loadHostMetrics" :loading="hostMetricsLoading">刷新</el-button>
            </div>
            <div class="rt-grid" v-if="hostMetrics">
              <!-- CPU -->
              <div class="rt-card" v-if="hostMetrics.cpu">
                <div class="rt-card-icon cpu-icon">
                  <svg viewBox="0 0 24 24" width="22" height="22"><path d="M15 3H9v2H7v2H5v2H3v6h2v2h2v2h2v2h6v-2h2v-2h2v-2h2V9h-2V7h-2V5h-2V3zm0 2v2h2v2h2v6h-2v2h-2v2H9v-2H7v-2H5V9h2V7h2V5h6z" fill="currentColor"/><rect x="10" y="10" width="4" height="4" rx="0.5" fill="currentColor"/></svg>
                </div>
                <div class="rt-card-body">
                  <span class="rt-label">CPU</span>
                  <span class="rt-value" :style="{ color: quotaColor(hostMetrics.cpu.cpu_usage_percent ?? 0) }">{{ hostMetrics.cpu.cpu_usage_percent?.toFixed(1) }}<small>%</small></span>
                  <el-progress :percentage="Math.min(100, hostMetrics.cpu.cpu_usage_percent ?? 0)" :show-text="false" :stroke-width="6" :color="quotaColor(hostMetrics.cpu.cpu_usage_percent ?? 0)" />
                  <span class="rt-sub">{{ hostMetrics.cpu.cpu_count }} 逻辑核心</span>
                </div>
              </div>
              <!-- 内存 -->
              <div class="rt-card" v-if="hostMetrics.memory">
                <div class="rt-card-icon mem-icon">
                  <svg viewBox="0 0 24 24" width="22" height="22"><path d="M4 5h16a1 1 0 011 1v12a1 1 0 01-1 1H4a1 1 0 01-1-1V6a1 1 0 011-1zm1 2v10h14V7H5zm2 2h2v6H7V9zm4 0h2v6h-2V9zm4 2h2v4h-2v-4z" fill="currentColor"/></svg>
                </div>
                <div class="rt-card-body">
                  <span class="rt-label">内存</span>
                  <span class="rt-value" :style="{ color: quotaColor(hostMetrics.memory.percent ?? 0) }">{{ hostMetrics.memory.percent?.toFixed(1) }}<small>%</small></span>
                  <el-progress :percentage="Math.min(100, hostMetrics.memory.percent ?? 0)" :show-text="false" :stroke-width="6" :color="quotaColor(hostMetrics.memory.percent ?? 0)" />
                  <span class="rt-sub">{{ formatBytesAuto(hostMetrics.memory.used) }} / {{ formatBytesAuto(hostMetrics.memory.total) }}</span>
                </div>
              </div>
              <!-- 负载 -->
              <div class="rt-card" v-if="loadAvg">
                <div class="rt-card-icon load-icon">
                  <svg viewBox="0 0 24 24" width="22" height="22"><path d="M3 13h2v8H3v-8zm4-4h2v12H7V9zm4-4h2v16h-2V5zm4 6h2v10h-2V11zm4-2h2v12h-2V9z" fill="currentColor"/></svg>
                </div>
                <div class="rt-card-body">
                  <span class="rt-label">系统负载</span>
                  <div class="load-pills">
                    <span class="load-pill" :style="{ '--lc': loadColor(loadAvg['1min'], hostMetrics?.cpu?.cpu_count) }">
                      <em>1m</em>{{ loadAvg['1min']?.toFixed(2) }}
                    </span>
                    <span class="load-pill" :style="{ '--lc': loadColor(loadAvg['5min'], hostMetrics?.cpu?.cpu_count) }">
                      <em>5m</em>{{ loadAvg['5min']?.toFixed(2) }}
                    </span>
                    <span class="load-pill" :style="{ '--lc': loadColor(loadAvg['15min'], hostMetrics?.cpu?.cpu_count) }">
                      <em>15m</em>{{ loadAvg['15min']?.toFixed(2) }}
                    </span>
                  </div>
                  <span class="rt-sub">核心数 {{ hostMetrics?.cpu?.cpu_count ?? '-' }}，满载阈值 {{ hostMetrics?.cpu?.cpu_count ?? '-' }}.00</span>
                </div>
              </div>
              <!-- 网络 -->
              <div class="rt-card" v-if="hostMetrics.internet_speed">
                <div class="rt-card-icon net-icon">
                  <svg viewBox="0 0 24 24" width="22" height="22"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z" fill="currentColor"/></svg>
                </div>
                <div class="rt-card-body">
                  <span class="rt-label">网络带宽</span>
                  <div class="net-bw-row">
                    <span class="net-bw-item rx"><svg viewBox="0 0 12 12" width="12" height="12"><path d="M6 2v8M3 7l3 3 3-3" stroke="#67c23a" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>{{ formatSpeedAuto(hostMetrics.internet_speed.rx_bytes) }}</span>
                    <span class="net-bw-item tx"><svg viewBox="0 0 12 12" width="12" height="12"><path d="M6 10V2M3 5l3-3 3 3" stroke="#409eff" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>{{ formatSpeedAuto(hostMetrics.internet_speed.tx_bytes) }}</span>
                  </div>
                  <span class="rt-sub" v-if="hostMetrics.network">累计 ↓{{ formatBytesAuto(hostMetrics.network.rx_bytes) }} ↑{{ formatBytesAuto(hostMetrics.network.tx_bytes) }}</span>
                </div>
              </div>
            </div>
            <!-- 磁盘使用 -->
            <template v-if="metricsDisks.length">
              <h4 class="rt-subtitle"><svg viewBox="0 0 24 24" width="16" height="16" style="vertical-align:-2px"><path d="M4 4h16a2 2 0 012 2v12a2 2 0 01-2 2H4a2 2 0 01-2-2V6a2 2 0 012-2zm0 2v12h16V6H4zm2 8h2v2H6v-2zm4 0h8v2h-8v-2z" fill="#909399"/></svg> 磁盘挂载</h4>
              <div class="disk-list">
                <div class="disk-item" v-for="disk in metricsDisks" :key="disk.path">
                  <div class="disk-head">
                    <code class="disk-path">{{ disk.path }}</code>
                    <span class="disk-detail">{{ formatBytesAuto(disk.used) }} / {{ formatBytesAuto(disk.total) }}</span>
                    <span class="disk-pct" :style="{ color: quotaColor(disk.percent) }">{{ disk.percent.toFixed(1) }}%</span>
                  </div>
                  <el-progress :percentage="Math.min(100, disk.percent)" :show-text="false" :stroke-width="6" :color="quotaColor(disk.percent)" />
                </div>
              </div>
            </template>
            <el-empty v-if="!hostMetrics && !hostMetricsLoading" description="暂无实时监控数据" :image-size="60" />
          </div>

          <!-- 硬件与系统信息 -->
          <div class="section-block" v-if="hardwareInfo">
            <h3 class="section-title clickable" @click="showHardwareInfo = !showHardwareInfo">
              <svg class="sec-icon" viewBox="0 0 24 24"><path d="M20 8h-3V6c0-1.1-.9-2-2-2H9c-1.1 0-2 .9-2 2v2H4c-1.1 0-2 .9-2 2v10h20V10c0-1.1-.9-2-2-2zM9 6h6v2H9V6zm11 12H4v-6h16v6zm0-8H4v-0c0 0 0 0 0 0h16v0z" fill="#606266"/></svg>
              硬件与系统
              <el-icon class="section-arrow" :class="{ expanded: showHardwareInfo }"><ArrowRight /></el-icon>
            </h3>
            <div v-show="showHardwareInfo">
              <!-- 系统概览 -->
              <div class="hw-overview" v-if="hardwareInfo.system_info">
                <div class="hw-ov-item">
                  <svg viewBox="0 0 24 24" width="18" height="18"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-.22-13h-.06c-.4 0-.72.32-.72.72v4.72c0 .35.18.68.49.86l4.15 2.49c.34.2.78.1.98-.24.21-.34.1-.79-.25-.99l-3.87-2.3V7.72c0-.4-.32-.72-.72-.72z" fill="#409eff"/></svg>
                  <div><em>运行</em>{{ formatUptime(hardwareInfo.system_info.uptime_seconds) }}</div>
                </div>
                <div class="hw-ov-item">
                  <svg viewBox="0 0 24 24" width="18" height="18"><path d="M20 18c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2H4c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2H1v2h22v-2h-3zM4 6h16v10H4V6z" fill="#67c23a"/></svg>
                  <div><em>主机</em>{{ hardwareInfo.system_info.hostname }}</div>
                </div>
                <div class="hw-ov-item">
                  <svg viewBox="0 0 24 24" width="18" height="18"><path d="M12 7V3H2v18h20V7H12zM6 19H4v-2h2v2zm0-4H4v-2h2v2zm0-4H4V9h2v2zm0-4H4V5h2v2zm4 12H8v-2h2v2zm0-4H8v-2h2v2zm0-4H8V9h2v2zm0-4H8V5h2v2zm10 12h-8v-2h2v-2h-2v-2h2v-2h-2V9h8v10zm-2-8h-2v2h2v-2zm0 4h-2v2h2v-2z" fill="#e6a23c"/></svg>
                  <div><em>系统</em>{{ hardwareInfo.system_info.distro || hardwareInfo.system_info.os }}</div>
                </div>
                <div class="hw-ov-item">
                  <svg viewBox="0 0 24 24" width="18" height="18"><path d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z" fill="#909399"/></svg>
                  <div><em>内核</em><span class="mono-text" style="font-size:12px">{{ hardwareInfo.system_info.os }}</span></div>
                </div>
              </div>
              <!-- CPU / 内存 -->
              <div class="hw-spec-grid">
                <div class="hw-spec-card">
                  <div class="hw-spec-icon"><svg viewBox="0 0 24 24" width="20" height="20"><path d="M15 3H9v2H7v2H5v2H3v6h2v2h2v2h2v2h6v-2h2v-2h2v-2h2V9h-2V7h-2V5h-2V3zm0 2v2h2v2h2v6h-2v2h-2v2H9v-2H7v-2H5V9h2V7h2V5h6z" fill="#409eff"/><rect x="10" y="10" width="4" height="4" rx="0.5" fill="#409eff"/></svg></div>
                  <div class="hw-spec-body">
                    <span class="hw-spec-label">处理器</span>
                    <span class="hw-spec-main">{{ hardwareInfo.cpu_model }}</span>
                    <span class="hw-spec-detail">{{ hardwareInfo.cpu_physical_cores }}C / {{ hardwareInfo.cpu_logical_cores }}T · {{ formatCpuFreq(hardwareInfo.cpu_freq) }}</span>
                  </div>
                </div>
                <div class="hw-spec-card">
                  <div class="hw-spec-icon"><svg viewBox="0 0 24 24" width="20" height="20"><path d="M4 5h16a1 1 0 011 1v12a1 1 0 01-1 1H4a1 1 0 01-1-1V6a1 1 0 011-1zm1 2v10h14V7H5zm2 2h2v6H7V9zm4 0h2v6h-2V9zm4 2h2v4h-2v-4z" fill="#67c23a"/></svg></div>
                  <div class="hw-spec-body">
                    <span class="hw-spec-label">内存</span>
                    <span class="hw-spec-main">{{ formatBytesAuto(hardwareInfo.memory_total) }}</span>
                    <span class="hw-spec-detail">Swap {{ hardwareInfo.swap_total ? formatBytesAuto(hardwareInfo.swap_total) : '无' }} · {{ hardwareInfo.system_info?.arch || '-' }}</span>
                  </div>
                </div>
              </div>
              <!-- 磁盘设备 -->
              <template v-if="hardwareInfo.disk_devices && hardwareInfo.disk_devices.length">
                <h4 class="hw-subtitle"><svg viewBox="0 0 24 24" width="14" height="14" style="vertical-align:-1px"><path d="M4 4h16a2 2 0 012 2v12a2 2 0 01-2 2H4a2 2 0 01-2-2V6a2 2 0 012-2zm0 2v12h16V6H4zm2 8h2v2H6v-2zm4 0h8v2h-8v-2z" fill="#606266"/></svg> 存储设备</h4>
                <div class="hw-disk-cards">
                  <div class="hw-disk-card" v-for="dev in hardwareInfo.disk_devices" :key="dev.name">
                    <div class="hw-disk-icon">
                      <svg v-if="dev.type === 'ssd'" viewBox="0 0 24 24" width="28" height="28"><rect x="3" y="6" width="18" height="12" rx="2" stroke="#67c23a" stroke-width="1.5" fill="none"/><path d="M8 10l2 2-2 2M12 14h4" stroke="#67c23a" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
                      <svg v-else viewBox="0 0 24 24" width="28" height="28"><circle cx="12" cy="12" r="9" stroke="#909399" stroke-width="1.5" fill="none"/><circle cx="12" cy="12" r="3" stroke="#909399" stroke-width="1.5" fill="none"/><line x1="12" y1="3" x2="12" y2="6" stroke="#909399" stroke-width="1.5"/></svg>
                    </div>
                    <div class="hw-disk-info">
                      <span class="hw-disk-name">/dev/{{ dev.name }}</span>
                      <span class="hw-disk-model">{{ dev.model || '-' }}</span>
                    </div>
                    <div class="hw-disk-meta">
                      <el-tag :type="dev.type === 'ssd' ? 'success' : 'info'" size="small" effect="plain">{{ (dev.type || 'HDD').toUpperCase() }}</el-tag>
                      <span class="hw-disk-size">{{ formatBytesAuto(dev.size) }}</span>
                    </div>
                  </div>
                </div>
              </template>
              <!-- 网卡 -->
              <template v-if="filteredNics.length">
                <h4 class="hw-subtitle"><svg viewBox="0 0 24 24" width="14" height="14" style="vertical-align:-1px"><path d="M20 2H4a2 2 0 00-2 2v16a2 2 0 002 2h16a2 2 0 002-2V4a2 2 0 00-2-2zM8 18H6v-4h2v4zm4 0h-2V8h2v10zm4 0h-2v-6h2v6z" fill="#606266"/></svg> 网卡</h4>
                <div class="nic-cards">
                  <div class="nic-card" v-for="nic in filteredNics" :key="nic.name" :class="{ 'nic-down': !nic.is_up }">
                    <div class="nic-head">
                      <svg viewBox="0 0 24 24" width="16" height="16"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z" :fill="nic.is_up ? '#67c23a' : '#c0c4cc'"/></svg>
                      <span class="nic-name">{{ nic.name }}</span>
                      <el-tag :type="nic.is_up ? 'success' : 'info'" size="small" effect="plain" class="nic-tag">{{ nic.is_up ? 'UP' : 'DOWN' }}</el-tag>
                      <span class="nic-speed" v-if="nic.speed_mbps">{{ nic.speed_mbps >= 1000 ? (nic.speed_mbps/1000)+'G' : nic.speed_mbps+'M' }}</span>
                    </div>
                    <div class="nic-body">
                      <div class="nic-row" v-if="nic._ipv4List.length">
                        <span class="nic-k">IPv4</span>
                        <div class="nic-addr-list"><span class="nic-addr" v-for="(ip, i) in nic._ipv4List" :key="i">{{ ip }}</span></div>
                      </div>
                      <div class="nic-row" v-if="nic._ipv6List.length">
                        <span class="nic-k">IPv6</span>
                        <div class="nic-addr-list"><span class="nic-addr v6" v-for="(ip, i) in nic._ipv6List" :key="i">{{ ip }}</span></div>
                      </div>
                      <div class="nic-row" v-if="nic._mac">
                        <span class="nic-k">MAC</span>
                        <code class="nic-mac">{{ nic._mac }}</code>
                      </div>
                    </div>
                  </div>
                </div>
              </template>
            </div>
          </div>

          <div class="section-block">
            <h3 class="section-title clickable" @click="showDetailDiskIo = !showDetailDiskIo">
              硬盘 IO 限制
              <el-icon class="section-arrow" :class="{ expanded: showDetailDiskIo }"><ArrowRight /></el-icon>
            </h3>
            <div v-show="showDetailDiskIo" class="config-grid">
              <div class="config-row">
                <div class="config-cell" v-for="f in diskIoBwFields" :key="f.key">
                  <span class="config-label">{{ f.label }}带宽</span>
                  <span class="config-value mono-text">{{ formatDiskIoVal(detail[f.key], true) }}</span>
                </div>
              </div>
              <div class="config-row">
                <div class="config-cell" v-for="f in diskIoIopsFields" :key="f.key">
                  <span class="config-label">{{ f.label }} IOPS</span>
                  <span class="config-value mono-text">{{ formatDiskIoVal(detail[f.key], false) }}</span>
                </div>
              </div>
            </div>
          </div>
          <div class="section-block">
            <div class="section-header">
              <h3 class="section-title">KSM 内存去重</h3>
              <div style="display:flex;gap:8px">
                <el-button size="small" :icon="Refresh" @click="loadKsmStatus" :loading="ksmLoading">刷新</el-button>
              </div>
            </div>
            <template v-if="ksmStats">
              <el-descriptions :column="3" border size="small" style="margin-top:12px">
                <el-descriptions-item label="内核支持">
                  <el-tag :type="ksmStats.available ? 'success' : 'danger'" size="small">{{ ksmStats.available ? '支持' : '不支持' }}</el-tag>
                </el-descriptions-item>
                <el-descriptions-item label="启用状态">
                  <el-tag :type="ksmStats.enabled ? 'success' : 'info'" size="small">{{ ksmStats.enabled ? '已启用' : '未启用' }}</el-tag>
                </el-descriptions-item>
                <el-descriptions-item label="跨 NUMA 合并">
                  <el-tag :type="ksmStats.merge_across_nodes ? 'warning' : 'info'" size="small">{{ ksmStats.merge_across_nodes ? '是' : '否' }}</el-tag>
                </el-descriptions-item>
                <el-descriptions-item label="扫描页面数">{{ ksmStats.pages_to_scan ?? '-' }}</el-descriptions-item>
                <el-descriptions-item label="扫描间隔">{{ ksmStats.sleep_millisecs != null ? ksmStats.sleep_millisecs + ' ms' : '-' }}</el-descriptions-item>
                <el-descriptions-item label="完整扫描次数">{{ ksmStats.full_scans ?? '-' }}</el-descriptions-item>
                <el-descriptions-item label="已合并唯一页面">{{ ksmStats.pages_shared ?? '-' }}</el-descriptions-item>
                <el-descriptions-item label="共享总页面">{{ ksmStats.pages_sharing ?? '-' }}</el-descriptions-item>
                <el-descriptions-item label="未合并页面">{{ ksmStats.pages_unshared ?? '-' }}</el-descriptions-item>
                <el-descriptions-item label="节省内存">
                  <span style="font-weight:600;color:#67c23a">{{ formatMemKB(ksmStats.memory_saved_kb) }}</span>
                </el-descriptions-item>
              </el-descriptions>
              <div style="margin-top:12px;display:flex;gap:8px">
                <el-button v-if="!ksmStats.enabled" type="success" size="small" :loading="ksmActionLoading" @click="handleKsmAction('enable')">启用 KSM</el-button>
                <el-button v-if="ksmStats.enabled" type="danger" size="small" :loading="ksmActionLoading" @click="handleKsmAction('disable')">关闭 KSM</el-button>
                <el-button v-if="ksmStats.available" type="primary" size="small" @click="openKsmTuneDialog">调参</el-button>
              </div>
            </template>
            <el-empty v-else-if="!ksmLoading" description="点击刷新加载 KSM 状态" :image-size="60" />
          </div>
        </el-tab-pane>

        <el-tab-pane label="额度统计" name="quotaStats">
          <div class="section-block">
            <div class="section-header">
              <h3 class="section-title">资源额度统计</h3>
              <el-button size="small" :icon="Refresh" @click="loadQuotaStats" :loading="quotaStatsLoading">刷新</el-button>
            </div>
            <template v-if="quotaStats">
              <div class="quota-grid">
                <div class="quota-card quota-card-vm">
                  <div class="quota-card-icon"><el-icon :size="28" color="#409eff"><Monitor /></el-icon></div>
                  <div class="quota-card-body">
                    <div class="quota-card-label">虚拟机数量</div>
                    <div class="quota-card-number">{{ quotaStats.vm_count ?? 0 }}<span class="quota-card-unit">台</span></div>
                  </div>
                </div>
                <div class="quota-card" v-for="item in quotaAllocationItems" :key="item.label">
                  <el-progress type="circle" :percentage="Math.min(100, item.ratio)" :width="80" :stroke-width="7" :color="quotaColor(item.ratio)">
                    <span class="quota-ring-pct" :style="{ color: quotaColor(item.ratio) }">{{ item.ratio }}%</span>
                  </el-progress>
                  <div class="quota-card-body">
                    <div class="quota-card-label">{{ item.label }}</div>
                    <div class="quota-card-alloc">{{ item.allocatedText }} <span class="quota-card-sep">/</span> {{ item.plannedText }}</div>
                    <div class="quota-card-sub">已分配 / 规划</div>
                  </div>
                </div>
              </div>
              <div class="quota-actual-section">
                <h4 class="quota-subtitle">实时使用</h4>
                <div class="quota-actual-grid">
                  <div class="quota-actual-card">
                    <div class="quota-actual-header">
                      <span class="quota-actual-label">CPU 使用率</span>
                      <span class="quota-actual-value" :style="{ color: quotaColor(quotaStats.actual_cpu_percent ?? 0) }">{{ quotaStats.actual_cpu_percent != null ? quotaStats.actual_cpu_percent.toFixed(1) + '%' : '-' }}</span>
                    </div>
                    <el-progress :percentage="Math.min(100, quotaStats.actual_cpu_percent ?? 0)" :show-text="false" :stroke-width="10" :color="quotaColor(quotaStats.actual_cpu_percent ?? 0)" />
                  </div>
                  <div class="quota-actual-card">
                    <div class="quota-actual-header">
                      <span class="quota-actual-label">内存使用</span>
                      <span class="quota-actual-value" :style="{ color: quotaColor(actualMemPercent) }">{{ formatQuotaBytes(quotaStats.actual_memory_used) }} / {{ formatQuotaBytes(quotaStats.actual_memory_total) }}</span>
                    </div>
                    <el-progress :percentage="actualMemPercent" :show-text="false" :stroke-width="10" :color="quotaColor(actualMemPercent)" />
                  </div>
                </div>
              </div>
              <template v-if="quotaStatsDisk.length">
                <div class="quota-disk-section">
                  <h4 class="quota-subtitle">磁盘使用详情</h4>
                  <div class="quota-disk-list">
                    <div class="quota-disk-item" v-for="disk in quotaStatsDisk" :key="disk.path">
                      <div class="quota-disk-header">
                        <code class="quota-disk-path">{{ disk.path }}</code>
                        <span class="quota-disk-detail">{{ formatQuotaBytes(disk.used) }} / {{ formatQuotaBytes(disk.total) }}</span>
                        <span class="quota-disk-pct" :style="{ color: quotaColor(disk.percent ?? 0) }">{{ (disk.percent ?? 0).toFixed(1) }}%</span>
                      </div>
                      <el-progress :percentage="Math.min(100, disk.percent ?? 0)" :show-text="false" :stroke-width="8" :color="quotaColor(disk.percent ?? 0)" />
                    </div>
                  </div>
                </div>
              </template>
            </template>
            <el-empty v-else-if="!quotaStatsLoading" 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">{{ latestMetrics.cpu_count }} 核</div>
                </div>
                <div class="metric-summary-card">
                  <div class="metric-summary-label">内存使用率</div>
                  <div class="metric-summary-value">{{ latestMetrics.mem_percent?.toFixed(1) }}%</div>
                  <div class="metric-summary-sub">{{ formatBytesRaw(latestMetrics.mem_used) }} / {{ formatBytesRaw(latestMetrics.mem_total) }}</div>
                </div>
                <div class="metric-summary-card">
                  <div class="metric-summary-label">公网流量</div>
                  <div class="metric-summary-value">↓{{ formatNetLabel(latestMetrics.inet_rx) }}</div>
                  <div class="metric-summary-sub">↑{{ formatNetLabel(latestMetrics.inet_tx) }}</div>
                </div>
                <div class="metric-summary-card">
                  <div class="metric-summary-label">内网流量</div>
                  <div class="metric-summary-value">↓{{ formatBytesRaw(latestMetrics.net_rx) }}</div>
                  <div class="metric-summary-sub">↑{{ formatBytesRaw(latestMetrics.net_tx) }}</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><Monitor /></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><Coin /></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><Connection /></el-icon> 公网流量</span></template>
                    <div ref="inetChartRef" 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><Connection /></el-icon> 内网流量</span></template>
                    <div ref="netChartRef" 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="image">
          <ImageManage v-if="hostTabLoaded['image']" ref="imageManageRef" />
        </el-tab-pane>
        <el-tab-pane label="网络管理" name="network">
          <NetworkManage v-if="hostTabLoaded['network']" ref="networkManageRef" />
        </el-tab-pane>
        <el-tab-pane label="数据卷管理" name="volume">
          <VolumeManage v-if="hostTabLoaded['volume']" ref="volumeManageRef" />
        </el-tab-pane>
        <el-tab-pane label="虚拟机管理" name="vm">
          <VmManage v-if="hostTabLoaded['vm']" ref="vmManageRef" />
        </el-tab-pane>
        <el-tab-pane label="快照管理" name="snapshot">
          <SnapshotManage v-if="hostTabLoaded['snapshot']" ref="snapshotManageRef" />
        </el-tab-pane>
        <el-tab-pane label="备份管理" name="backup">
          <BackupManage v-if="hostTabLoaded['backup']" ref="backupManageRef" />
        </el-tab-pane>
        <el-tab-pane label="回收站" name="recycleBin">
          <RecycleBinManage v-if="hostTabLoaded['recycleBin']" ref="recycleBinManageRef" />
        </el-tab-pane>
        <el-tab-pane label="虚拟机监控" name="vmMonitor">
          <VmMonitor v-if="hostTabLoaded['vmMonitor']" ref="vmMonitorRef" />
        </el-tab-pane>

        <el-tab-pane label="组网管理" name="networking">
          <div class="section-block">
            <div class="section-header">
              <h3 class="section-title">用户组网列表</h3>
              <div style="display: flex; gap: 8px; align-items: center">
                <el-input v-model="nwFilterUserId" placeholder="按用户ID筛选" style="width: 140px" size="small" clearable @clear="loadNetworkingList" @keyup.enter="loadNetworkingList" />
                <el-input v-model="nwKeyword" placeholder="关键词搜索" style="width: 160px" size="small" clearable @clear="loadNetworkingList" @keyup.enter="loadNetworkingList" />
                <el-button size="small" :icon="Search" @click="loadNetworkingList">搜索</el-button>
                <el-button size="small" type="primary" @click="handleNwCreate">创建组网</el-button>
                <el-button size="small" :icon="Refresh" @click="loadNetworkingList" :loading="nwLoading">刷新</el-button>
              </div>
            </div>
            <el-table :data="nwList" v-loading="nwLoading" stripe size="small">
              <el-table-column prop="id" label="ID" width="60" />
              <el-table-column prop="name" label="名称" min-width="140" show-overflow-tooltip />
              <el-table-column prop="user_id" label="用户ID" width="80" />
              <el-table-column prop="host_id" label="宿主机ID" width="90" />
              <el-table-column label="网桥" width="120">
                <template #default="{ row }">{{ row.bridge_name || '-' }}</template>
              </el-table-column>
              <el-table-column label="网关" min-width="140">
                <template #default="{ row }"><span class="mono-text">{{ row.gateway || '-' }}</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="200" fixed="right">
                <template #default="{ row }">
                  <el-button link type="primary" size="small" @click="handleNwDetail(row)">详情</el-button>
                  <el-button link type="success" size="small" @click="handleNwAssign(row)">分配IP</el-button>
                  <el-button link type="danger" size="small" @click="handleNwDelete(row)">删除</el-button>
                </template>
              </el-table-column>
            </el-table>
            <el-empty v-if="!nwList.length && !nwLoading" description="暂无组网" :image-size="60" />
            <div class="pagination-wrapper" v-if="nwTotal > 0">
              <el-pagination v-model:current-page="nwPage" v-model:page-size="nwPageSize"
                :page-sizes="[10, 20, 50]" :total="nwTotal" layout="total, sizes, prev, pager, next" small
                @size-change="s => { nwPageSize = s; nwPage = 1; loadNetworkingList() }"
                @current-change="p => { nwPage = p; loadNetworkingList() }" />
            </div>
          </div>
        </el-tab-pane>
      </el-tabs>

      <!-- 组网详情弹窗 -->
      <el-dialog v-model="nwDetailVisible" title="组网详情" width="800px" destroy-on-close>
        <el-descriptions :column="2" border v-if="nwDetailData" style="margin-bottom: 16px">
          <el-descriptions-item label="ID">{{ nwDetailData.id }}</el-descriptions-item>
          <el-descriptions-item label="名称">{{ nwDetailData.name }}</el-descriptions-item>
          <el-descriptions-item label="用户ID">{{ nwDetailData.user_id }}</el-descriptions-item>
          <el-descriptions-item label="宿主机ID">{{ nwDetailData.host_id }}</el-descriptions-item>
          <el-descriptions-item label="网桥">{{ nwDetailData.bridge_name || '-' }}</el-descriptions-item>
          <el-descriptions-item label="网关">{{ nwDetailData.gateway || '-' }}</el-descriptions-item>
          <el-descriptions-item label="创建时间">{{ formatTimestamp(nwDetailData.created_at) }}</el-descriptions-item>
          <el-descriptions-item label="更新时间">{{ formatTimestamp(nwDetailData.updated_at) }}</el-descriptions-item>
        </el-descriptions>
        <h4 style="margin: 0 0 12px">组网下的网络 (已分配)</h4>
        <el-table :data="nwDetailNetworks" size="small" stripe>
          <el-table-column label="网络ID" width="70">
            <template #default="{ row }">{{ row.network?.id || '-' }}</template>
          </el-table-column>
          <el-table-column label="IP地址" min-width="150">
            <template #default="{ row }"><span class="mono-text">{{ row.network?.address || '-' }}</span></template>
          </el-table-column>
          <el-table-column label="MAC地址" min-width="160">
            <template #default="{ row }"><span class="mono-text">{{ row.network?.mac_address || '-' }}</span></template>
          </el-table-column>
          <el-table-column label="类型" width="80">
            <template #default="{ row }"><el-tag size="small">{{ row.network?.type || '-' }}</el-tag></template>
          </el-table-column>
          <el-table-column label="虚拟机ID" width="90">
            <template #default="{ row }">{{ row.network?.vm_id || '-' }}</template>
          </el-table-column>
          <el-table-column label="操作" width="80">
            <template #default="{ row }">
              <el-button link type="danger" size="small" @click="handleNwRemoveNet(row)">移除</el-button>
            </template>
          </el-table-column>
        </el-table>
        <el-empty v-if="!nwDetailNetworks.length" description="暂无分配的网络" :image-size="50" />
        <template #footer><el-button @click="nwDetailVisible = false">关闭</el-button></template>
      </el-dialog>

      <!-- 创建组网弹窗 -->
      <el-dialog v-model="nwCreateVisible" title="创建组网" width="480px" destroy-on-close class="tk-dialog">
        <el-form ref="nwCreateFormRef" :model="nwCreateForm" :rules="nwCreateRules" label-width="100px">
          <div class="tk-section">
            <div class="tk-section-title">组网信息</div>
            <el-form-item label="用户" prop="user_id">
              <div style="display: flex; gap: 8px; width: 100%">
                <el-input :model-value="nwCreateForm.user_id ? `${nwCreateUserName} (ID: ${nwCreateForm.user_id})` : '未选择'" disabled style="flex: 1" />
                <el-button type="primary" @click="showNwUserSelector = true">选择</el-button>
                <el-button v-if="nwCreateForm.user_id" @click="nwCreateForm.user_id = 0; nwCreateUserName = ''">清除</el-button>
              </div>
            </el-form-item>
            <el-form-item label="网桥名称">
              <el-input v-model="nwCreateForm.bridge_name" placeholder="可选" />
            </el-form-item>
            <el-form-item label="网关">
              <el-input v-model="nwCreateForm.gateway" placeholder="可选，如 10.0.0.1" />
            </el-form-item>
          </div>
        </el-form>
        <template #footer>
          <div class="tk-dialog-footer">
            <el-button @click="nwCreateVisible = false">取消</el-button>
            <el-button type="primary" :loading="nwSubmitLoading" @click="submitNwCreate">创建</el-button>
          </div>
        </template>
      </el-dialog>

      <!-- 分配IP弹窗 -->
      <el-dialog v-model="nwAssignVisible" title="为虚拟机分配组网IP" 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="组网">{{ nwAssignTarget?.name || '-' }} (ID: {{ nwAssignTarget?.id }})</el-form-item>
            <el-form-item label="虚拟机" required>
              <div style="display: flex; gap: 8px; width: 100%">
                <el-input :model-value="nwAssignVmId ? `${nwAssignVmName} (ID: ${nwAssignVmId})` : '未选择'" disabled style="flex: 1" />
                <el-button type="primary" @click="showNwVmSelector = true">选择</el-button>
              </div>
            </el-form-item>
            <el-form-item label="指定IP">
              <el-input v-model="nwAssignIp" placeholder="留空自动分配" />
            </el-form-item>
          </div>
        </el-form>
        <template #footer>
          <div class="tk-dialog-footer">
            <el-button @click="nwAssignVisible = false">取消</el-button>
            <el-button type="primary" :loading="nwSubmitLoading" @click="submitNwAssign" :disabled="!nwAssignVmId">分配</el-button>
          </div>
        </template>
      </el-dialog>

      <UserListSelector v-model="showNwUserSelector" @confirm="handleNwUserSelected" />
      <VmSelectorPopup v-model="showNwVmSelector" :service-id="serviceId" :host-id="hostId" @confirm="handleNwVmSelected" />
    </div>

    <!-- 编辑弹窗 -->
    <el-dialog v-model="editDialogVisible" title="编辑宿主机" width="890px" destroy-on-close class="tk-dialog">
      <el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px">
        <div class="tk-section">
          <div class="tk-section-title">基本信息</div>
          <el-form-item label="名称" prop="name"><el-input v-model="formData.name" /></el-form-item>
          <el-form-item label="服务地址" prop="base_url"><el-input v-model="formData.base_url" /></el-form-item>
          <el-form-item label="IP 地址" prop="ip"><el-input v-model="formData.ip" /></el-form-item>
          <el-form-item label="认证Token"><el-input v-model="formData.token" show-password /></el-form-item>
        </div>
        <div class="tk-section">
          <div class="tk-section-title">SSH 配置</div>
          <el-form-item label="端口"><el-input-number v-model="formData.port" :min="0" :max="65535" controls-position="right" style="width: 100%" /></el-form-item>
          <el-form-item label="用户名"><el-input v-model="formData.user" /></el-form-item>
          <el-form-item label="密码"><el-input v-model="formData.password" show-password /></el-form-item>
          <el-form-item label="私钥"><el-input v-model="formData.private_key" type="textarea" :rows="4" placeholder="SSH 私钥内容" /></el-form-item>
        </div>
        <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="formData.max_cpu" :min="0" controls-position="right" /><span class="tk-res-unit">核</span></el-form-item>
            <el-form-item label="内存">
              <el-input-number v-model="memoryDisplay" :min="0" controls-position="right" />
              <el-select v-model="memoryUnit" class="tk-unit-select">
                <el-option v-for="u in memoryUnitOptions" :key="u.label" :label="u.label" :value="u.label" />
              </el-select>
            </el-form-item>
            <el-form-item label="磁盘">
              <el-input-number v-model="diskDisplay" :min="0" controls-position="right" />
              <el-select v-model="diskUnit" class="tk-unit-select">
                <el-option v-for="u in diskUnitOptions" :key="u.label" :label="u.label" :value="u.label" />
              </el-select>
            </el-form-item>
            <el-form-item label="下行带宽"><el-input-number v-model="formData.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="formData.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 clickable" @click="showDiskIoSection = !showDiskIoSection">
            硬盘 IO 限制
            <el-icon class="section-arrow" :class="{ expanded: showDiskIoSection }"><ArrowRight /></el-icon>
            <span class="section-hint">可选，不展开则使用默认值</span>
          </div>
          <div v-show="showDiskIoSection">
            <div class="io-sub-title">
              带宽限制
              <el-select v-model="ioBwUnit" class="tk-unit-select" style="width: 90px; margin-left: 8px">
                <el-option v-for="u in ioBwUnitOptions" :key="u.label" :label="u.label" :value="u.label" />
              </el-select>
            </div>
            <div class="tk-resource-grid">
              <el-form-item v-for="f in diskIoBwFields" :key="f.key" :label="f.label">
                <el-input-number :model-value="+(formData[f.key] / getIoBwFactor()).toFixed(2)" @update:model-value="v => formData[f.key] = Math.round((v || 0) * getIoBwFactor())" :min="0" controls-position="right" />
              </el-form-item>
            </div>
            <div class="io-sub-title">IOPS 限制</div>
            <div class="tk-resource-grid">
              <el-form-item v-for="f in diskIoIopsFields" :key="f.key" :label="f.label">
                <el-input-number v-model="formData[f.key]" :min="0" controls-position="right" />
              </el-form-item>
            </div>
          </div>
        </div>
        <div class="tk-section">
          <div class="tk-section-title">其他配置</div>
          <el-form-item label="宿主机组">
            <div style="display: flex; gap: 8px; width: 100%">
              <el-input :model-value="formData.host_group_id ? `宿主机组 #${formData.host_group_id}` : '未选择'" disabled style="flex: 1" />
              <el-button type="primary" @click="showGroupSelector = true">选择</el-button>
              <el-button v-if="formData.host_group_id" @click="formData.host_group_id = 0">清除</el-button>
            </div>
          </el-form-item>
          <el-form-item label="介绍"><el-input v-model="formData.description" type="textarea" :rows="3" /></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="submitLoading" @click="handleSubmit">确定</el-button>
        </div>
      </template>
    </el-dialog>

    <HostGroupSelectorPopup v-model="showGroupSelector" :service-id="serviceId" :current-id="formData.host_group_id" @confirm="g => formData.host_group_id = g.id" />

    <!-- 创建注册令牌弹窗 -->
    <el-dialog v-model="tokenDialogVisible" title="创建宿主机注册令牌" width="700px" destroy-on-close class="token-dialog">
      <el-form ref="tokenFormRef" :model="tokenForm" :rules="tokenRules" label-width="120px">
        <div class="tk-section">
          <div class="tk-section-title">基本信息</div>
          <el-form-item label="宿主机名称" prop="name">
            <el-input v-model="tokenForm.name" placeholder="为该宿主机命名" />
          </el-form-item>
          <el-form-item label="所属宿主机组" prop="host_group_id">
            <div style="display: flex; gap: 8px; width: 100%">
              <el-input :model-value="tokenForm.host_group_id ? `宿主机组 #${tokenForm.host_group_id}` : ''" placeholder="请选择宿主机组" disabled style="flex: 1" />
              <el-button type="primary" @click="showTokenGroupSelector = true">选择</el-button>
              <el-button v-if="tokenForm.host_group_id" @click="tokenForm.host_group_id = 0">清除</el-button>
            </div>
          </el-form-item>
          <el-form-item label="宿主机描述">
            <el-input v-model="tokenForm.description" type="textarea" :rows="2" placeholder="宿主机描述（可选）" />
          </el-form-item>
        </div>
        <div class="tk-section">
          <div class="tk-section-title">资源配额</div>
          <div class="tk-resource-grid">
            <el-form-item label="CPU" prop="max_cpu" class="tk-res-item">
              <el-input-number v-model="tokenForm.max_cpu" :min="1" controls-position="right" /><span class="tk-res-unit">核</span>
            </el-form-item>
            <el-form-item label="内存" prop="max_memory" class="tk-res-item">
              <el-input-number v-model="tokenMemDisplay" :min="0" controls-position="right" />
              <el-select v-model="tokenMemUnit" class="tk-unit-select">
                <el-option v-for="u in memoryUnitOptions" :key="u.label" :label="u.label" :value="u.label" />
              </el-select>
            </el-form-item>
            <el-form-item label="磁盘" prop="max_disk" class="tk-res-item">
              <el-input-number v-model="tokenDiskDisplay" :min="0" controls-position="right" />
              <el-select v-model="tokenDiskUnit" class="tk-unit-select">
                <el-option v-for="u in diskUnitOptions" :key="u.label" :label="u.label" :value="u.label" />
              </el-select>
            </el-form-item>
            <el-form-item label="下行带宽" class="tk-res-item">
              <el-input-number v-model="tokenForm.rx_bandwidth" :min="0" controls-position="right" /><span class="tk-res-unit">Mbps</span>
            </el-form-item>
            <el-form-item label="上行带宽" class="tk-res-item">
              <el-input-number v-model="tokenForm.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 clickable" @click="showTokenDiskIo = !showTokenDiskIo">
            硬盘 IO 限制
            <el-icon class="section-arrow" :class="{ expanded: showTokenDiskIo }"><ArrowRight /></el-icon>
            <span class="section-hint">可选，不展开则使用默认值</span>
          </div>
          <div v-show="showTokenDiskIo">
            <div class="io-sub-title">
              带宽限制
              <el-select v-model="tokenIoBwUnit" class="tk-unit-select" style="width: 90px; margin-left: 8px">
                <el-option v-for="u in ioBwUnitOptions" :key="u.label" :label="u.label" :value="u.label" />
              </el-select>
            </div>
            <div class="tk-resource-grid">
              <el-form-item v-for="f in diskIoBwFields" :key="f.key" :label="f.label" class="tk-res-item">
                <el-input-number :model-value="+(tokenForm[f.key] / getTokenIoBwFactor()).toFixed(2)" @update:model-value="v => tokenForm[f.key] = Math.round((v || 0) * getTokenIoBwFactor())" :min="0" controls-position="right" />
              </el-form-item>
            </div>
            <div class="io-sub-title">IOPS 限制</div>
            <div class="tk-resource-grid">
              <el-form-item v-for="f in diskIoIopsFields" :key="f.key" :label="f.label" class="tk-res-item">
                <el-input-number v-model="tokenForm[f.key]" :min="0" controls-position="right" />
              </el-form-item>
            </div>
          </div>
        </div>
        <div class="tk-section">
          <div class="tk-section-title">令牌有效期</div>
          <el-form-item label="有效期" prop="expire_hours">
            <el-input-number v-model="tokenForm.expire_hours" :min="1" :max="8760" controls-position="right" style="width: 100%" />
            <div class="form-hint">单位：小时。默认 24 小时，最大 8760 小时（365天）</div>
          </el-form-item>
        </div>
      </el-form>
      <template #footer>
        <div class="tk-dialog-footer">
          <el-button @click="tokenDialogVisible = false">取消</el-button>
          <el-button type="primary" :loading="tokenSubmitLoading" @click="handleTokenSubmit">
            <el-icon><Key /></el-icon>创建令牌
          </el-button>
        </div>
      </template>
    </el-dialog>

    <!-- 令牌结果弹窗 -->
    <el-dialog v-model="tokenResultVisible" title="注册令牌已生成" width="560px" :close-on-click-modal="false" class="token-result-dialog">
      <div class="tk-result-wrapper">
        <div class="tk-result-header">
          <el-icon class="tk-result-icon"><Key /></el-icon>
          <div>
            <div class="tk-result-name">{{ tokenResultInfo.name }}</div>
            <div class="tk-result-meta">有效期 {{ tokenResultInfo.expire_hours }} 小时</div>
          </div>
        </div>
        <el-alert type="warning" :closable="false" show-icon style="margin-bottom: 16px">
          <template #title>请立即复制并保存此令牌，关闭后将无法再次查看</template>
        </el-alert>
        <div class="tk-token-block">
          <div class="tk-token-label">后端地址</div>
          <div class="tk-token-value">{{ baseUrl }}</div>
        </div>
        <div class="tk-token-block">
          <div class="tk-token-label">service_id（主控服务ID）</div>
          <div class="tk-token-value">{{ tokenResultInfo.service_id }}</div>
        </div>
        <div class="tk-token-block">
          <div class="tk-token-label">注册令牌</div>
          <div class="tk-token-value">{{ tokenResultInfo.token }}</div>
        </div>
        <el-button type="primary" class="tk-copy-btn" @click="copyToken">
          <el-icon><CopyDocument /></el-icon>复制令牌到剪贴板
        </el-button>
      </div>
      <template #footer>
        <el-button @click="tokenResultVisible = false">关闭</el-button>
      </template>
    </el-dialog>

    <!-- 令牌用宿主机组选择器 -->
    <HostGroupSelectorPopup v-model="showTokenGroupSelector" :service-id="serviceId" :current-id="tokenForm.host_group_id" @confirm="handleTokenGroupSelected" />

    <!-- KSM 调参弹窗 -->
    <el-dialog v-model="ksmTuneVisible" title="KSM 调参" width="480px" destroy-on-close>
      <el-form label-width="140px" style="padding: 8px 0">
        <el-form-item label="每次扫描页面数">
          <el-input-number v-model="ksmTuneForm.pages_to_scan" :min="0" :step="100" controls-position="right" style="width:200px" />
        </el-form-item>
        <el-form-item label="扫描间隔 (ms)">
          <el-input-number v-model="ksmTuneForm.sleep_millisecs" :min="0" :step="10" controls-position="right" style="width:200px" />
        </el-form-item>
        <el-form-item label="跨 NUMA 节点合并">
          <el-switch v-model="ksmTuneForm.merge_across_nodes" />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="ksmTuneVisible = false">取消</el-button>
        <el-button type="primary" :loading="ksmActionLoading" @click="submitKsmTune">确定</el-button>
      </template>
    </el-dialog>
  </div>
</template>

<script setup>
import { ref, reactive, computed, onMounted, onActivated, onDeactivated, onBeforeUnmount, watch, nextTick, provide } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ArrowLeft, ArrowRight, Refresh, Edit, Delete, Monitor, Coin, Connection, Search, Plus, Key, CopyDocument } from '@element-plus/icons-vue'
import {
  getRemoteHostDetail, getRemoteHostMetrics, updateRemoteHost, deleteRemoteHost,
  getUserNetworkingList, getUserNetworkingDetail, createUserNetworking, deleteUserNetworking,
  assignUserNetworking, removeUserNetworkingNetwork,
  createHostToken, getMetricsHistory, getHostQuotaStats,
  getHostKsmStatus, configureHostKsm
} from '@/api/admin/kvmService'
import { extractApiError } from '@/utils/kvmErrorUtil'
import { baseUrl } from '@/config/env'
import HostGroupSelectorPopup from '@/components/admin/HostGroupSelectorPopup.vue'
import ImageManage from '@/views/virtualization/ImageManage.vue'
import NetworkManage from '@/views/virtualization/NetworkManage.vue'
import VolumeManage from '@/views/virtualization/VolumeManage.vue'
import VmManage from '@/views/virtualization/VmManage.vue'
import SnapshotManage from '@/views/virtualization/SnapshotManage.vue'
import BackupManage from '@/views/virtualization/BackupManage.vue'
import RecycleBinManage from '@/views/virtualization/RecycleBinManage.vue'
import VmMonitor from '@/views/virtualization/VmMonitor.vue'
import { useTagsViewStore } from '@/store/tagsViewStore'
import UserListSelector from '@/components/admin/UserListSelector.vue'
import VmSelectorPopup from '@/components/admin/VmSelectorPopup.vue'
import * as echarts from 'echarts'

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 hostId = computed(() => parseInt(route.query.id) || 0)

const activeTab = ref('info')
const hostTabLoaded = reactive({ image: false, network: false, volume: false, vm: false, snapshot: false, backup: false, recycleBin: false, vmMonitor: false, networking: false })

const imageManageRef = ref(null)
const networkManageRef = ref(null)
const volumeManageRef = ref(null)
const vmManageRef = ref(null)
const snapshotManageRef = ref(null)
const backupManageRef = ref(null)
const recycleBinManageRef = ref(null)
const vmMonitorRef = ref(null)
const tabRefMap = { image: imageManageRef, network: networkManageRef, volume: volumeManageRef, vm: vmManageRef, snapshot: snapshotManageRef, backup: backupManageRef, recycleBin: recycleBinManageRef, vmMonitor: vmMonitorRef }

watch(activeTab, (tab) => {
  if (!['info', 'monitor', 'networking'].includes(tab)) {
    if (!hostTabLoaded[tab]) {
      hostTabLoaded[tab] = true
    } else {
      nextTick(() => { tabRefMap[tab]?.value?.loadList?.() })
    }
  }
  if (tab === 'monitor' && detail.value) {
    if (!historicalMetricsData.value) {
      loadHistoricalMetrics()
    }
  }
  if (tab === 'networking') loadNetworkingList()
  if (tab === 'quotaStats' && !quotaStats.value) loadQuotaStats()
})

const loading = ref(false)
const submitLoading = ref(false)
const detail = ref(null)

provide('embedded', true)
provide('serviceId', serviceId)
provide('serviceName', serviceName)
provide('hostId', hostId)
provide('hostDetail', detail)
const showToken = ref(false)
const showPassword = ref(false)
const showPrivateKey = ref(false)
const copyText = (text) => {
  if (!text) return
  if (navigator.clipboard?.writeText) {
    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'
  document.body.appendChild(ta)
  ta.select()
  try {
    document.execCommand('copy')
    ElMessage.success('已复制到剪贴板')
  } catch { ElMessage.error('复制失败') }
  document.body.removeChild(ta)
}
const historicalMetricsData = ref(null)
const historicalMetricsLoading = ref(false)
const editDialogVisible = ref(false)
const showGroupSelector = ref(false)
const formRef = ref(null)

const diskIoDefaults = {
  read_bytes_sec: 314572800, write_bytes_sec: 314572800,
  read_iops_sec: 1000, write_iops_sec: 1000,
  read_bytes_sec_max: 314572800, write_bytes_sec_max: 314572800,
  read_iops_sec_max: 1000, write_iops_sec_max: 1000
}
const diskIoBwFields = [
  { key: 'read_bytes_sec', label: '读取' },
  { key: 'write_bytes_sec', label: '写入' },
  { key: 'read_bytes_sec_max', label: '突发读取' },
  { key: 'write_bytes_sec_max', label: '突发写入' }
]
const diskIoIopsFields = [
  { key: 'read_iops_sec', label: '读取' },
  { key: 'write_iops_sec', label: '写入' },
  { key: 'read_iops_sec_max', label: '突发读取' },
  { key: 'write_iops_sec_max', label: '突发写入' }
]
const diskIoFields = [
  ...diskIoBwFields.map(f => ({ ...f, isBandwidth: true })),
  ...diskIoIopsFields.map(f => ({ ...f, isBandwidth: false }))
]
const formatDiskIoVal = (val, isBandwidth) => {
  if (!val && val !== 0) return '-'
  val = Number(val)
  if (!isBandwidth) return val.toLocaleString() + ' IOPS'
  if (val >= 1073741824) return (val / 1073741824).toFixed(1) + ' GB/s'
  if (val >= 1048576) return (val / 1048576).toFixed(0) + ' MB/s'
  if (val >= 1024) return (val / 1024).toFixed(0) + ' KB/s'
  return val + ' B/s'
}
const ioBwUnitOptions = [
  { label: 'B/s', factor: 1 },
  { label: 'KB/s', factor: 1024 },
  { label: 'MB/s', factor: 1048576 },
  { label: 'GB/s', factor: 1073741824 }
]
const ioBwUnit = ref('MB/s')
const tokenIoBwUnit = ref('MB/s')
const getIoBwFactor = () => ioBwUnitOptions.find(u => u.label === ioBwUnit.value)?.factor || 1048576
const getTokenIoBwFactor = () => ioBwUnitOptions.find(u => u.label === tokenIoBwUnit.value)?.factor || 1048576

const showDiskIoSection = ref(false)
const showTokenDiskIo = ref(false)
const showDetailDiskIo = ref(false)
const showHardwareInfo = ref(false)

const formData = reactive({
  name: '', base_url: '', ip: '', token: '', port: 22, user: '', password: '', private_key: '',
  max_cpu: 0, max_memory: 0, max_disk: 0, rx_bandwidth: 0, tx_bandwidth: 0, host_group_id: 0, description: '',
  ...diskIoDefaults
})
const formRules = {
  name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
  base_url: [{ required: true, message: '请输入服务地址', trigger: 'blur' }],
  ip: [{ required: true, message: '请输入IP', trigger: 'blur' }]
}

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 formatMemKB = (v) => {
  if (!v) return '-'; v = Number(v)
  if (v >= 1073741824) return (v / 1073741824).toFixed(1) + ' TB'
  if (v >= 1048576) return (v / 1048576).toFixed(1) + ' GB'
  if (v >= 1024) return (v / 1024).toFixed(1) + ' MB'
  return v + ' KB'
}
const formatDiskGB = (v) => {
  if (!v) return '-'; v = Number(v)
  if (v >= 1024) return (v / 1024).toFixed(1) + ' TB'
  return v.toFixed(1) + ' GB'
}

const memoryUnit = ref('GB')
const diskUnit = ref('GB')
const memoryUnitOptions = [
  { label: 'KB', factor: 1 },
  { label: 'MB', factor: 1024 },
  { label: 'GB', factor: 1048576 },
  { label: 'TB', factor: 1073741824 }
]
const diskUnitOptions = [
  { label: 'GB', factor: 1 },
  { label: 'TB', factor: 1024 }
]
const getMemFactor = () => memoryUnitOptions.find(u => u.label === memoryUnit.value)?.factor || 1048576
const getDiskFactor = () => diskUnitOptions.find(u => u.label === diskUnit.value)?.factor || 1

const memoryDisplay = computed({
  get: () => formData.max_memory ? +(formData.max_memory / getMemFactor()).toFixed(2) : 0,
  set: (v) => { formData.max_memory = Math.round((v || 0) * getMemFactor()) }
})
const diskDisplay = computed({
  get: () => formData.max_disk ? +(formData.max_disk / getDiskFactor()).toFixed(2) : 0,
  set: (v) => { formData.max_disk = Math.round((v || 0) * getDiskFactor()) }
})
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 loadDetail = async () => {
  if (!serviceId.value || !hostId.value) return
  loading.value = true
  try {
    const res = await getRemoteHostDetail({ service_id: serviceId.value, id: hostId.value })
    const body = res?.data
    if (body?.code === 200 && body?.data) {
      detail.value = body.data.host ?? body.data.data ?? body.data
    } else { ElMessage.error(extractApiError(body, '加载失败')) }
  } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '加载失败')) } finally { loading.value = false }
}

// ---- 实时监控指标 ----
const hostMetrics = ref(null)
const hostMetricsLoading = ref(false)
const hardwareInfo = ref(null)
const loadAvg = ref(null)

const loadHostMetrics = async () => {
  if (!hostId.value) return
  hostMetricsLoading.value = true
  try {
    const res = await getRemoteHostMetrics({ service_id: serviceId.value, host_id: hostId.value })
    const body = res?.data
    if (body?.code === 200 && body?.data) {
      const raw = body.data.data ?? body.data
      hostMetrics.value = raw
      if (raw.hardware_json) {
        try { hardwareInfo.value = JSON.parse(raw.hardware_json) } catch { hardwareInfo.value = null }
      }
      if (raw.load_avg_json) {
        try { loadAvg.value = JSON.parse(raw.load_avg_json) } catch { loadAvg.value = null }
      }
      if (raw.ksm && !ksmStats.value) {
        ksmStats.value = raw.ksm
      }
    }
  } catch (e) {
    console.warn('加载实时指标失败:', e)
  } finally {
    hostMetricsLoading.value = false
  }
}

const metricsDisks = computed(() => {
  if (!hostMetrics.value?.disk) return []
  return Object.entries(hostMetrics.value.disk).map(([path, info]) => {
    const pct = info.percent ?? (info.total ? ((info.used / info.total) * 100) : 0)
    return { path, ...info, percent: pct }
  })
})

const formatBytesAuto = (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 formatSpeedAuto = (val) => {
  if (!val && val !== 0) return '0 B/s'
  val = Number(val)
  if (val >= 1073741824) return (val / 1073741824).toFixed(1) + ' GB/s'
  if (val >= 1048576) return (val / 1048576).toFixed(1) + ' MB/s'
  if (val >= 1024) return (val / 1024).toFixed(1) + ' KB/s'
  return val + ' B/s'
}

const formatCpuFreq = (freq) => {
  if (!freq) return '-'
  if (typeof freq === 'object') {
    const cur = freq.current ? (freq.current >= 1000 ? (freq.current / 1000).toFixed(2) + ' GHz' : freq.current.toFixed(0) + ' MHz') : ''
    const max = freq.max ? (freq.max >= 1000 ? (freq.max / 1000).toFixed(1) + ' GHz' : freq.max + ' MHz') : ''
    if (cur && max) return `${cur}（最高 ${max}）`
    return cur || max || '-'
  }
  const v = Number(freq)
  return v >= 1000 ? (v / 1000).toFixed(2) + ' GHz' : v + ' MHz'
}

const formatUptime = (seconds) => {
  if (!seconds) return '-'
  const d = Math.floor(seconds / 86400)
  const h = Math.floor((seconds % 86400) / 3600)
  const m = Math.floor((seconds % 3600) / 60)
  const parts = []
  if (d > 0) parts.push(`${d} 天`)
  if (h > 0) parts.push(`${h} 小时`)
  if (m > 0 && d === 0) parts.push(`${m} 分钟`)
  return parts.join(' ') || '< 1 分钟'
}

const loadColor = (val, cores) => {
  if (!cores) return '#909399'
  const ratio = val / cores
  if (ratio >= 1) return '#f56c6c'
  if (ratio >= 0.7) return '#e6a23c'
  return '#67c23a'
}

const filteredNics = computed(() => {
  if (!hardwareInfo.value?.nic_info) return []
  const skipPrefixes = ['vnet', 'veth', 'ovs-', 'br-int', 'tap']
  return hardwareInfo.value.nic_info
    .filter(nic => !skipPrefixes.some(p => nic.name.startsWith(p)))
    .map(nic => {
      const addrs = nic.addresses || []
      const ipv4List = addrs.filter(a => a.family?.includes('AF_INET') && !a.family?.includes('AF_INET6')).map(a => a.address)
      const ipv6List = addrs.filter(a => a.family?.includes('AF_INET6')).map(a => a.address)
      const macEntry = addrs.find(a => a.family?.includes('AF_PACKET'))
      return { ...nic, _ipv4List: ipv4List, _ipv6List: ipv6List, _mac: macEntry?.address || '' }
    })
})

// ---- 额度统计 ----
const quotaStats = ref(null)
const quotaStatsLoading = ref(false)
const quotaStatsDisk = computed(() => {
  if (!quotaStats.value?.actual_disk_json) return []
  try {
    const parsed = JSON.parse(quotaStats.value.actual_disk_json)
    if (Array.isArray(parsed)) return parsed
    return Object.entries(parsed).map(([path, info]) => ({ path, ...info }))
  } catch { return [] }
})

const loadQuotaStats = async () => {
  if (!serviceId.value || !hostId.value) return
  quotaStatsLoading.value = true
  try {
    const res = await getHostQuotaStats({ service_id: serviceId.value, host_id: hostId.value })
    if (res?.data?.code === 200) quotaStats.value = res.data.data
    else quotaStats.value = null
  } catch { quotaStats.value = null } finally { quotaStatsLoading.value = false }
}

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

const formatQuotaDisk = (gb) => {
  if (!gb && gb !== 0) return '-'
  const v = Number(gb)
  if (v >= 1024) return (v / 1024).toFixed(1) + ' TB'
  return v + ' GB'
}

const formatBandwidth = (mbps) => {
  if (!mbps && mbps !== 0) return '-'
  const v = Number(mbps)
  if (v >= 1000) return (v / 1000).toFixed(1) + ' Gbps'
  return v + ' Mbps'
}

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

const quotaColor = (ratio) => {
  if (ratio >= 90) return '#f56c6c'
  if (ratio >= 70) return '#e6a23c'
  return '#409eff'
}

const quotaAllocationItems = computed(() => {
  const s = quotaStats.value
  if (!s) return []
  const ratio = (a, p) => p ? Math.round((a / p) * 100) : 0
  return [
    { label: 'CPU', ratio: ratio(s.allocated_cpu, s.planned_cpu), allocatedText: (s.allocated_cpu ?? 0) + ' 核', plannedText: (s.planned_cpu ?? 0) + ' 核' },
    { label: '内存', ratio: ratio(s.allocated_memory, s.planned_memory), allocatedText: formatQuotaKB(s.allocated_memory), plannedText: formatQuotaKB(s.planned_memory) },
    { label: '磁盘', ratio: ratio(s.allocated_disk, s.planned_disk), allocatedText: formatQuotaDisk(s.allocated_disk), plannedText: formatQuotaDisk(s.planned_disk) },
    { label: '下行带宽', ratio: ratio(s.allocated_rx_bandwidth, s.planned_rx_bandwidth), allocatedText: formatBandwidth(s.allocated_rx_bandwidth), plannedText: formatBandwidth(s.planned_rx_bandwidth) },
    { label: '上行带宽', ratio: ratio(s.allocated_tx_bandwidth, s.planned_tx_bandwidth), allocatedText: formatBandwidth(s.allocated_tx_bandwidth), plannedText: formatBandwidth(s.planned_tx_bandwidth) }
  ]
})

const actualMemPercent = computed(() => {
  const s = quotaStats.value
  if (!s?.actual_memory_total) return 0
  return Math.round((s.actual_memory_used / s.actual_memory_total) * 100)
})

// ---- KSM 内存去重 ----
const ksmStats = ref(null)
const ksmLoading = ref(false)
const ksmActionLoading = ref(false)
const ksmTuneVisible = ref(false)
const ksmTuneForm = reactive({ pages_to_scan: 300, sleep_millisecs: 100, merge_across_nodes: false })

const loadKsmStatus = async () => {
  if (!serviceId.value || !hostId.value) return
  ksmLoading.value = true
  try {
    const res = await getHostKsmStatus({ service_id: serviceId.value, host_id: hostId.value })
    if (res?.data?.code === 200) ksmStats.value = res.data.data?.stats || res.data.data
    else ElMessage.error(extractApiError(res?.data, '获取 KSM 状态失败'))
  } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '获取 KSM 状态失败')) } finally { ksmLoading.value = false }
}

const handleKsmAction = async (action) => {
  const labels = { enable: '启用', disable: '关闭' }
  try {
    await ElMessageBox.confirm(`确定要${labels[action]} KSM 吗？`, 'KSM 操作', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' })
  } catch { return }
  ksmActionLoading.value = true
  try {
    const fd = new FormData()
    fd.append('service_id', serviceId.value)
    fd.append('host_id', hostId.value)
    fd.append('action', action)
    const res = await configureHostKsm(fd)
    if (res?.data?.code === 200) {
      ElMessage.success(`KSM 已${labels[action]}`)
      ksmStats.value = res.data.data?.stats || res.data.data
    } else ElMessage.error(extractApiError(res?.data, '操作失败'))
  } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '操作失败')) } finally { ksmActionLoading.value = false }
}

const openKsmTuneDialog = () => {
  if (ksmStats.value) {
    ksmTuneForm.pages_to_scan = ksmStats.value.pages_to_scan ?? 300
    ksmTuneForm.sleep_millisecs = ksmStats.value.sleep_millisecs ?? 100
    ksmTuneForm.merge_across_nodes = !!ksmStats.value.merge_across_nodes
  }
  ksmTuneVisible.value = true
}

const submitKsmTune = async () => {
  ksmActionLoading.value = true
  try {
    const fd = new FormData()
    fd.append('service_id', serviceId.value)
    fd.append('host_id', hostId.value)
    fd.append('action', 'tune')
    fd.append('pages_to_scan', ksmTuneForm.pages_to_scan)
    fd.append('sleep_millisecs', ksmTuneForm.sleep_millisecs)
    fd.append('merge_across_nodes', ksmTuneForm.merge_across_nodes)
    const res = await configureHostKsm(fd)
    if (res?.data?.code === 200) {
      ElMessage.success('KSM 参数已更新')
      ksmStats.value = res.data.data?.stats || res.data.data
      ksmTuneVisible.value = false
    } else ElMessage.error(extractApiError(res?.data, '调参失败'))
  } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '调参失败')) } finally { ksmActionLoading.value = false }
}

const cpuChartRef = ref(null)
const memChartRef = ref(null)
const netChartRef = ref(null)
const inetChartRef = ref(null)
let cpuChart = null
let memChart = null
let netChart = null
let inetChart = null
let isPageActive = false

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

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 loadHistoricalMetrics = async () => {
  if (!serviceId.value || !hostId.value) 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: hostId.value,
      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 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 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 = spanMs >= 7 * 86400 * 1000 ? '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_percent ?? 0)
  const inetRxData = metrics.map(m => m.inet_rx ?? 0)
  const inetTxData = metrics.map(m => m.inet_tx ?? 0)

  const netRxRate = []
  const netTxRate = []
  for (let i = 0; i < metrics.length; i++) {
    if (i === 0) { netRxRate.push(0); netTxRate.push(0); continue }
    const dt = (new Date(metrics[i].bucket) - new Date(metrics[i - 1].bucket)) / 1000
    if (dt > 0) {
      netRxRate.push(Math.max(0, ((metrics[i].net_rx ?? 0) - (metrics[i - 1].net_rx ?? 0)) / dt))
      netTxRate.push(Math.max(0, ((metrics[i].net_tx ?? 0) - (metrics[i - 1].net_tx ?? 0)) / dt))
    } else {
      netRxRate.push(0); netTxRate.push(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} 内存: ${p[0].value.toFixed(1)}%` },
      grid: baseGrid, xAxis: makeXAxis(),
      yAxis: { type: 'value', min: 0, max: 100, axisLabel: { fontSize: 10, formatter: v => v + '%' } },
      series: [makeSeries('内存', memData, '#67c23a')]
    }, true)
  }

  if (inetChartRef.value) {
    if (!inetChart) inetChart = echarts.init(inetChartRef.value)
    inetChart.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: formatNetLabel } },
      series: [makeSeries('接收', inetRxData, '#409eff'), makeSeries('发送', inetTxData, '#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: formatNetLabel } },
      series: [makeSeries('接收', netRxRate, '#409eff'), makeSeries('发送', netTxRate, '#e6a23c')]
    }, true)
  }
}

const disposeCharts = () => {
  cpuChart?.dispose(); cpuChart = null
  memChart?.dispose(); memChart = null
  netChart?.dispose(); netChart = null
  inetChart?.dispose(); inetChart = null
}

const handleEdit = () => {
  if (!detail.value) return
  const d = detail.value
  Object.assign(formData, {
    name: d.name || '', base_url: d.base_url || '', ip: d.ip || '', token: d.token || '',
    port: d.port || 22, user: d.user || '', password: d.password || '', private_key: d.private_key || '',
    max_cpu: d.max_cpu || 0, max_memory: d.max_memory || 0, max_disk: d.max_disk || 0,
    rx_bandwidth: d.rx_bandwidth || 0, tx_bandwidth: d.tx_bandwidth || 0,
    host_group_id: d.host_group_id || 0, description: d.description || '',
    ...Object.fromEntries(diskIoFields.map(f => [f.key, d[f.key] ?? diskIoDefaults[f.key]]))
  })
  showDiskIoSection.value = diskIoFields.some(f => d[f.key] && d[f.key] !== diskIoDefaults[f.key])
  editDialogVisible.value = true
}

const handleSubmit = () => {
  formRef.value?.validate(async (valid) => {
    if (!valid) return
    submitLoading.value = true
    try {
      const payload = { ...formData, service_id: serviceId.value, id: hostId.value }
      if (!payload.token) delete payload.token
      if (!payload.password) delete payload.password
      if (!payload.private_key) delete payload.private_key
      if (!payload.description) delete payload.description
      if (!payload.host_group_id) delete payload.host_group_id
      const res = await updateRemoteHost(payload)
      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 { submitLoading.value = false }
  })
}

const handleDelete = () => {
  ElMessageBox.confirm(`确定要删除宿主机「${detail.value?.name}」吗？`, '删除确认', {
    confirmButtonText: '确定删除', cancelButtonText: '取消', type: 'warning'
  }).then(async () => {
    try {
      const res = await deleteRemoteHost({ service_id: serviceId.value, id: hostId.value })
      if (res?.data?.code === 200) { ElMessage.success('删除成功'); goBack() }
      else ElMessage.error(extractApiError(res?.data, '删除失败'))
    } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '删除失败')) }
  }).catch(() => {})
}

// ========== 创建注册令牌 ==========
const tokenDialogVisible = ref(false)
const tokenSubmitLoading = ref(false)
const tokenResultVisible = ref(false)
const showTokenGroupSelector = ref(false)
const tokenFormRef = ref(null)
const tokenMemUnit = ref('GB')
const tokenDiskUnit = ref('GB')

const tokenForm = reactive({
  name: '', host_group_id: 0, max_cpu: 4,
  max_memory: 4194304, max_disk: 100,
  rx_bandwidth: 100, tx_bandwidth: 100,
  description: '', expire_hours: 24,
  ...diskIoDefaults
})
const tokenResultInfo = reactive({ name: '', expire_hours: 24, token: '', service_id: 0 })
const tokenRules = {
  name: [{ required: true, message: '请输入宿主机名称', trigger: 'blur' }],
  host_group_id: [{ required: true, type: 'number', min: 1, message: '请选择宿主机组', trigger: 'change' }],
  max_cpu: [{ required: true, type: 'number', min: 1, message: '请设置最大CPU核数', trigger: 'change' }],
  max_memory: [{ required: true, type: 'number', min: 1, message: '请设置最大内存', trigger: 'change' }],
  max_disk: [{ required: true, type: 'number', min: 1, message: '请设置最大磁盘', trigger: 'change' }],
  expire_hours: [{ required: true, type: 'number', min: 1, message: '请设置有效期', trigger: 'change' }]
}

const getTokenMemFactor = () => memoryUnitOptions.find(u => u.label === tokenMemUnit.value)?.factor || 1048576
const getTokenDiskFactor = () => diskUnitOptions.find(u => u.label === tokenDiskUnit.value)?.factor || 1
const tokenMemDisplay = computed({
  get: () => tokenForm.max_memory ? +(tokenForm.max_memory / getTokenMemFactor()).toFixed(2) : 0,
  set: (v) => { tokenForm.max_memory = Math.round((v || 0) * getTokenMemFactor()) }
})
const tokenDiskDisplay = computed({
  get: () => tokenForm.max_disk ? +(tokenForm.max_disk / getTokenDiskFactor()).toFixed(2) : 0,
  set: (v) => { tokenForm.max_disk = Math.round((v || 0) * getTokenDiskFactor()) }
})

const openTokenDialog = () => {
  const d = detail.value
  Object.assign(tokenForm, {
    name: '', host_group_id: d?.host_group_id || 0,
    max_cpu: d?.max_cpu || 4,
    max_memory: d?.max_memory || 4194304,
    max_disk: d?.max_disk || 100,
    rx_bandwidth: d?.rx_bandwidth || 100,
    tx_bandwidth: d?.tx_bandwidth || 100,
    description: '', expire_hours: 24,
    ...Object.fromEntries(diskIoFields.map(f => [f.key, d?.[f.key] ?? diskIoDefaults[f.key]]))
  })
  tokenMemUnit.value = 'GB'
  tokenDiskUnit.value = 'GB'
  showTokenDiskIo.value = false
  tokenIoBwUnit.value = 'MB/s'
  tokenDialogVisible.value = true
}

const handleTokenGroupSelected = (group) => {
  tokenForm.host_group_id = group.id
}

const handleTokenSubmit = () => {
  tokenFormRef.value?.validate(async (valid) => {
    if (!valid) return
    tokenSubmitLoading.value = true
    try {
      const fd = new FormData()
      fd.append('service_id', serviceId.value)
      fd.append('name', tokenForm.name)
      fd.append('host_group_id', tokenForm.host_group_id)
      fd.append('max_cpu', tokenForm.max_cpu)
      fd.append('max_memory', tokenForm.max_memory)
      fd.append('max_disk', tokenForm.max_disk)
      fd.append('rx_bandwidth', tokenForm.rx_bandwidth)
      fd.append('tx_bandwidth', tokenForm.tx_bandwidth)
      diskIoFields.forEach(f => { if (tokenForm[f.key] !== undefined) fd.append(f.key, tokenForm[f.key]) })
      fd.append('description', tokenForm.description || '')
      fd.append('expire_hours', tokenForm.expire_hours)
      const res = await createHostToken(fd)
      const body = res?.data
      if (body?.code === 200 && body?.data) {
        tokenResultInfo.name = tokenForm.name
        tokenResultInfo.expire_hours = tokenForm.expire_hours
        tokenResultInfo.token = body.data.token || body.data.Token || JSON.stringify(body.data)
        tokenResultInfo.service_id = serviceId.value
        tokenDialogVisible.value = false
        tokenResultVisible.value = true
        ElMessage.success('注册令牌创建成功')
      } else {
        ElMessage.error(extractApiError(body, '创建令牌失败'))
      }
    } catch (e) {
      ElMessage.error(extractApiError(e?.response?.data, '创建令牌失败'))
    } finally {
      tokenSubmitLoading.value = false
    }
  })
}

const copyToken = async () => {
  const text = `后端地址：${baseUrl}\nservice_id：${tokenResultInfo.service_id}\n注册令牌：${tokenResultInfo.token}`
  try {
    await navigator.clipboard.writeText(text)
    ElMessage.success('令牌信息已复制到剪贴板')
  } catch {
    const ta = document.createElement('textarea')
    ta.value = text
    document.body.appendChild(ta)
    ta.select()
    document.execCommand('copy')
    document.body.removeChild(ta)
    ElMessage.success('令牌信息已复制到剪贴板')
  }
}

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

// ---- 组网管理 ----
const nwLoading = ref(false)
const nwSubmitLoading = ref(false)
const nwList = ref([])
const nwTotal = ref(0)
const nwPage = ref(1)
const nwPageSize = ref(10)
const nwFilterUserId = ref('')
const nwKeyword = ref('')

const nwDetailVisible = ref(false)
const nwDetailData = ref(null)
const nwDetailNetworks = ref([])
const nwDetailLoading = ref(false)

const nwCreateVisible = ref(false)
const nwCreateFormRef = ref(null)
const nwCreateForm = reactive({ user_id: 0, bridge_name: '', gateway: '' })
const nwCreateUserName = ref('')
const showNwUserSelector = ref(false)
const nwCreateRules = {
  user_id: [{ required: true, message: '请选择用户', trigger: 'change', type: 'number', min: 1 }]
}

const nwAssignVisible = ref(false)
const nwAssignTarget = ref(null)
const nwAssignVmId = ref(0)
const nwAssignVmName = ref('')
const nwAssignIp = ref('')
const showNwVmSelector = ref(false)

const loadNetworkingList = async () => {
  nwLoading.value = true
  try {
    const params = {
      service_id: serviceId.value,
      page: nwPage.value,
      count: nwPageSize.value,
      host_id: hostId.value
    }
    if (nwFilterUserId.value) params.user_id = parseInt(nwFilterUserId.value)
    if (nwKeyword.value) params.keyword = nwKeyword.value
    const res = await getUserNetworkingList(params)
    if (res?.data?.code === 200 && res?.data?.data) {
      const inner = res.data.data
      nwList.value = Array.isArray(inner) ? inner : (inner.data || [])
      nwTotal.value = inner.meta?.count ?? inner.total ?? nwList.value.length
    }
  } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '获取组网列表失败')) }
  finally { nwLoading.value = false }
}

const handleNwDetail = async (row) => {
  nwDetailVisible.value = true
  nwDetailData.value = row
  nwDetailNetworks.value = []
  nwDetailLoading.value = true
  try {
    const res = await getUserNetworkingDetail({ service_id: serviceId.value, networking_id: row.id })
    if (res?.data?.code === 200 && res?.data?.data) {
      const inner = res.data.data
      nwDetailData.value = inner.data ?? inner
      nwDetailNetworks.value = inner.networks || []
    }
  } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '获取详情失败')) }
  finally { nwDetailLoading.value = false }
}

const handleNwCreate = () => {
  Object.assign(nwCreateForm, { user_id: 0, bridge_name: '', gateway: '' })
  nwCreateUserName.value = ''
  nwCreateVisible.value = true
}

const handleNwUserSelected = (user) => {
  nwCreateForm.user_id = user.user_id || user.id
  nwCreateUserName.value = user.user_name || user.name || ''
}

const submitNwCreate = async () => {
  if (!nwCreateForm.user_id) { ElMessage.warning('请选择用户'); return }
  nwSubmitLoading.value = true
  try {
    const fd = new FormData()
    fd.append('service_id', serviceId.value)
    fd.append('host_id', hostId.value)
    fd.append('user_id', nwCreateForm.user_id)
    if (nwCreateForm.bridge_name) fd.append('bridge_name', nwCreateForm.bridge_name)
    if (nwCreateForm.gateway) fd.append('gateway', nwCreateForm.gateway)
    const res = await createUserNetworking(fd)
    if (res?.data?.code === 200) {
      ElMessage.success('创建成功')
      nwCreateVisible.value = false
      loadNetworkingList()
    } else ElMessage.error(extractApiError(res?.data, '创建失败'))
  } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '创建失败')) }
  finally { nwSubmitLoading.value = false }
}

const handleNwDelete = (row) => {
  ElMessageBox.confirm(`确定删除组网「${row.name || row.id}」？该操作不可撤销。`, '删除确认', { type: 'warning' })
    .then(async () => {
      try {
        const res = await deleteUserNetworking({ service_id: serviceId.value, networking_id: row.id })
        if (res?.data?.code === 200) { ElMessage.success('已删除'); loadNetworkingList() }
        else ElMessage.error(extractApiError(res?.data, '删除失败'))
      } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '删除失败')) }
    }).catch(() => {})
}

const handleNwAssign = (row) => {
  nwAssignTarget.value = row
  nwAssignVmId.value = 0
  nwAssignVmName.value = ''
  nwAssignIp.value = ''
  nwAssignVisible.value = true
}

const handleNwVmSelected = (vm) => {
  nwAssignVmId.value = vm.id
  nwAssignVmName.value = vm.name || ''
}

const submitNwAssign = async () => {
  if (!nwAssignVmId.value || !nwAssignTarget.value) { ElMessage.warning('请选择虚拟机'); return }
  nwSubmitLoading.value = true
  try {
    const fd = new FormData()
    fd.append('service_id', serviceId.value)
    fd.append('networking_id', nwAssignTarget.value.id)
    fd.append('vm_id', nwAssignVmId.value)
    if (nwAssignIp.value.trim()) fd.append('ip', nwAssignIp.value.trim())
    const res = await assignUserNetworking(fd)
    if (res?.data?.code === 200) {
      ElMessage.success('分配成功')
      nwAssignVisible.value = false
      if (nwDetailVisible.value && nwDetailData.value?.id === nwAssignTarget.value.id) {
        handleNwDetail(nwAssignTarget.value)
      }
    } else ElMessage.error(extractApiError(res?.data, '分配失败'))
  } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '分配失败')) }
  finally { nwSubmitLoading.value = false }
}

const handleNwRemoveNet = (netItem) => {
  const net = netItem.network || netItem
  if (!net.id || !nwDetailData.value?.id) { ElMessage.warning('缺少网络信息'); return }
  ElMessageBox.confirm(`确定移除网络 (ID: ${net.id}, VM: ${net.vm_id || '-'}) ？`, '移除确认', { type: 'warning' })
    .then(async () => {
      try {
        const fd = new FormData()
        fd.append('service_id', serviceId.value)
        fd.append('networking_id', nwDetailData.value.id)
        fd.append('network_id', net.id)
        fd.append('vm_id', net.vm_id || 0)
        const res = await removeUserNetworkingNetwork(fd)
        if (res?.data?.code === 200) {
          ElMessage.success('已移除')
          handleNwDetail(nwDetailData.value)
        } else ElMessage.error(extractApiError(res?.data, '移除失败'))
      } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '移除失败')) }
    }).catch(() => {})
}

let loadedHostId = null

const initPage = () => {
  if (!hostId.value || loadedHostId === hostId.value) return
  loadedHostId = hostId.value
  activeTab.value = 'info'
  Object.keys(hostTabLoaded).forEach(k => hostTabLoaded[k] = false)
  detail.value = null
  showToken.value = false
  showPassword.value = false
  showPrivateKey.value = false
  historicalMetricsData.value = null
  disposeCharts()
  loadDetail()
  loadHostMetrics()
  if (activeTab.value === 'monitor') loadHistoricalMetrics()
}

watch(hostId, () => { if (isPageActive) initPage() })
onActivated(() => {
  isPageActive = true
  if (loadedHostId !== hostId.value) initPage()
})
onMounted(() => { isPageActive = true; initPage() })
onDeactivated(() => { isPageActive = false })
onBeforeUnmount(() => { isPageActive = false; disposeCharts() })
</script>

<style scoped>
.host-detail-page { padding: 0; }
.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; }
.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 4px 0 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; 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; font-weight: 500; display: flex; align-items: center; gap: 6px; }
.status-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
.dot-running { background: #00b42a; }
.dot-other { background: #c9cdd4; }

.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: 20px; }
.section-title { font-size: 15px; font-weight: 600; color: #1d2129; margin: 0 0 16px; }
.section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
.section-header .section-title { margin-bottom: 0; }

.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: 12px 16px; border-right: 1px solid #e8e8e8; }
.config-cell:last-child { border-right: none; }
.config-label { display: block; font-size: 12px; color: #86909c; margin-bottom: 4px; }
.config-value { display: block; font-size: 14px; color: #1d2129; word-break: break-all; }
.secret-cell { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; }
.secret-cell code { max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mono-text { font-family: 'Consolas', 'Monaco', monospace; }

.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; }

.clickable { cursor: pointer; user-select: none; display: flex; align-items: center; gap: 6px; }
.clickable:hover { color: #409eff; }
.section-arrow { transition: transform 0.2s; font-size: 14px; }
.section-arrow.expanded { transform: rotate(90deg); }
.section-hint { font-size: 12px; color: #909399; font-weight: 400; }
.tk-section-title.clickable { cursor: pointer; user-select: none; display: flex; align-items: center; gap: 6px; }
.tk-section-title.clickable:hover { color: #409eff; }
.io-sub-title { font-size: 13px; font-weight: 500; color: #606266; margin: 12px 0 8px; display: flex; align-items: center; }

.quota-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); gap: 16px; margin-top: 12px; }
.quota-card { background: #f7f8fa; border: 1px solid #e8e8e8; border-radius: 8px; padding: 20px 16px; display: flex; flex-direction: column; align-items: center; gap: 12px; transition: box-shadow 0.2s; }
.quota-card:hover { box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06); }
.quota-card-vm { flex-direction: row; justify-content: center; gap: 16px; }
.quota-card-icon { display: flex; align-items: center; justify-content: center; width: 56px; height: 56px; background: #ecf5ff; border-radius: 12px; }
.quota-card-body { display: flex; flex-direction: column; align-items: center; gap: 2px; min-width: 0; }
.quota-card-vm .quota-card-body { align-items: flex-start; }
.quota-card-label { font-size: 12px; color: #86909c; white-space: nowrap; }
.quota-card-number { font-size: 32px; font-weight: 700; color: #1d2129; line-height: 1.1; }
.quota-card-unit { font-size: 14px; font-weight: 400; color: #86909c; margin-left: 4px; }
.quota-ring-pct { font-size: 16px; font-weight: 700; line-height: 1; }
.quota-card-alloc { font-size: 13px; color: #1d2129; font-weight: 500; white-space: nowrap; text-align: center; }
.quota-card-sep { color: #c0c4cc; margin: 0 2px; }
.quota-card-sub { font-size: 11px; color: #a8abb2; }

.quota-actual-section { margin-top: 24px; }
.quota-subtitle { font-size: 14px; font-weight: 600; color: #1d2129; margin: 0 0 14px; padding-left: 8px; border-left: 3px solid #409eff; }
.quota-actual-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 16px; }
.quota-actual-card { background: #f7f8fa; border: 1px solid #e8e8e8; border-radius: 8px; padding: 16px 20px; }
.quota-actual-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
.quota-actual-label { font-size: 13px; color: #606266; font-weight: 500; }
.quota-actual-value { font-size: 14px; font-weight: 600; }

.quota-disk-section { margin-top: 24px; }
.quota-disk-list { display: flex; flex-direction: column; gap: 12px; }
.quota-disk-item { background: #f7f8fa; border: 1px solid #e8e8e8; border-radius: 8px; padding: 12px 20px; }
.quota-disk-header { display: flex; align-items: center; gap: 12px; margin-bottom: 8px; flex-wrap: wrap; }
.quota-disk-path { font-size: 13px; font-family: 'Consolas', 'Monaco', monospace; color: #1d2129; background: #e8eaed; padding: 2px 8px; border-radius: 4px; }
.quota-disk-detail { font-size: 12px; color: #86909c; flex: 1; text-align: right; }
.quota-disk-pct { font-size: 13px; font-weight: 600; min-width: 48px; text-align: right; }

/* 实时监控 */
.sec-icon { width: 18px; height: 18px; vertical-align: -3px; margin-right: 4px; }
.rt-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 14px; }
.rt-card { display: flex; gap: 14px; background: #fff; border: 1px solid #ebeef5; border-radius: 10px; padding: 18px 20px; transition: box-shadow .2s, border-color .2s; }
.rt-card:hover { box-shadow: 0 4px 16px rgba(0,0,0,.06); border-color: #d9ecff; }
.rt-card-icon { flex-shrink: 0; width: 44px; height: 44px; border-radius: 10px; display: flex; align-items: center; justify-content: center; color: #fff; }
.cpu-icon { background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%); }
.mem-icon { background: linear-gradient(135deg, #67c23a 0%, #85ce61 100%); }
.load-icon { background: linear-gradient(135deg, #e6a23c 0%, #f0c78a 100%); }
.net-icon { background: linear-gradient(135deg, #909399 0%, #b1b3b8 100%); }
.rt-card-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
.rt-label { font-size: 12px; color: #86909c; font-weight: 500; }
.rt-value { font-size: 26px; font-weight: 700; line-height: 1.1; }
.rt-value small { font-size: 14px; font-weight: 500; margin-left: 1px; }
.rt-sub { font-size: 11px; color: #a8abb2; margin-top: 2px; }

.load-pills { display: flex; gap: 8px; margin: 4px 0 2px; }
.load-pill { display: inline-flex; align-items: center; gap: 4px; background: #f7f8fa; border: 1px solid #ebeef5; border-radius: 6px; padding: 4px 10px; font-size: 15px; font-weight: 700; color: var(--lc, #1d2129); }
.load-pill em { font-style: normal; font-size: 10px; font-weight: 500; color: #a8abb2; text-transform: uppercase; }

.net-bw-row { display: flex; gap: 16px; margin: 6px 0 2px; }
.net-bw-item { display: inline-flex; align-items: center; gap: 5px; font-size: 16px; font-weight: 700; color: #1d2129; }

.rt-subtitle { font-size: 13px; font-weight: 600; color: #606266; margin: 20px 0 10px; display: flex; align-items: center; gap: 6px; }
.disk-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 10px; }
.disk-item { background: #fff; border: 1px solid #ebeef5; border-radius: 8px; padding: 12px 16px; transition: box-shadow .2s; }
.disk-item:hover { box-shadow: 0 2px 8px rgba(0,0,0,.04); }
.disk-head { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
.disk-path { font-size: 13px; font-family: 'Consolas','Monaco',monospace; color: #1d2129; background: #f0f2f5; padding: 2px 8px; border-radius: 4px; }
.disk-detail { font-size: 12px; color: #86909c; flex: 1; text-align: right; }
.disk-pct { font-size: 13px; font-weight: 700; min-width: 44px; text-align: right; }

/* 硬件信息 */
.hw-overview { display: flex; flex-wrap: wrap; gap: 0; background: #f7f8fa; border-radius: 8px; border: 1px solid #ebeef5; margin-bottom: 16px; overflow: hidden; }
.hw-ov-item { flex: 1; min-width: 180px; display: flex; align-items: center; gap: 10px; padding: 12px 16px; border-right: 1px solid #ebeef5; }
.hw-ov-item:last-child { border-right: none; }
.hw-ov-item div { display: flex; flex-direction: column; min-width: 0; }
.hw-ov-item em { font-style: normal; font-size: 11px; color: #a8abb2; line-height: 1; margin-bottom: 2px; }
.hw-ov-item div { font-size: 13px; color: #1d2129; font-weight: 500; word-break: break-all; }

.hw-spec-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); gap: 12px; margin-bottom: 16px; }
.hw-spec-card { display: flex; gap: 14px; align-items: flex-start; background: #fff; border: 1px solid #ebeef5; border-radius: 10px; padding: 16px 20px; }
.hw-spec-icon { flex-shrink: 0; width: 40px; height: 40px; background: #f0f7ff; border-radius: 8px; display: flex; align-items: center; justify-content: center; }
.hw-spec-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.hw-spec-label { font-size: 11px; color: #a8abb2; }
.hw-spec-main { font-size: 14px; font-weight: 600; color: #1d2129; word-break: break-word; }
.hw-spec-detail { font-size: 12px; color: #86909c; }

.hw-subtitle { font-size: 13px; font-weight: 600; color: #606266; margin: 16px 0 10px; display: flex; align-items: center; gap: 6px; }

.hw-disk-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 10px; }
.hw-disk-card { display: flex; align-items: center; gap: 14px; background: #fff; border: 1px solid #ebeef5; border-radius: 10px; padding: 14px 18px; transition: box-shadow .2s; }
.hw-disk-card:hover { box-shadow: 0 2px 12px rgba(0,0,0,.05); }
.hw-disk-icon { flex-shrink: 0; width: 44px; height: 44px; background: #f0f7ff; border-radius: 10px; display: flex; align-items: center; justify-content: center; }
.hw-disk-info { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.hw-disk-name { font-size: 14px; font-weight: 600; color: #1d2129; font-family: 'Consolas','Monaco',monospace; }
.hw-disk-model { font-size: 12px; color: #86909c; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.hw-disk-meta { display: flex; flex-direction: column; align-items: flex-end; gap: 4px; flex-shrink: 0; }
.hw-disk-size { font-size: 15px; font-weight: 700; color: #1d2129; }

.nic-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 10px; }
.nic-card { background: #fff; border: 1px solid #ebeef5; border-radius: 10px; padding: 14px 18px; transition: box-shadow .2s; }
.nic-card:hover { box-shadow: 0 2px 12px rgba(0,0,0,.05); }
.nic-card.nic-down { opacity: .55; }
.nic-head { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.nic-name { font-size: 14px; font-weight: 600; color: #1d2129; }
.nic-tag { margin-left: auto; }
.nic-speed { font-size: 12px; color: #86909c; font-weight: 500; }
.nic-body { display: flex; flex-direction: column; gap: 4px; }
.nic-row { display: flex; align-items: center; gap: 8px; }
.nic-k { font-size: 11px; color: #a8abb2; min-width: 32px; font-weight: 500; }
.nic-addr-list { display: flex; flex-wrap: wrap; gap: 4px; }
.nic-addr { display: inline-block; background: #f0f7ff; border: 1px solid #d9ecff; border-radius: 4px; padding: 1px 8px; font-size: 12px; font-family: 'Consolas','Monaco',monospace; color: #409eff; }
.nic-addr.v6 { background: #fdf6ec; border-color: #faecd8; color: #e6a23c; font-size: 11px; }
.nic-mac { font-size: 12px; color: #606266; }

</style>
