<template>
  <div class="app" ref="appRef">
    <header v-if="!focusMode" class="app-header">
      <div class="brand">
        <button class="sidebar-toggle" type="button" @click="toggleSidebar">
          ☰
        </button>
        <h1>萌芽SSH</h1>
      </div>
      <div class="header-right">
        <nav class="app-nav">
          <button
            class="nav-btn"
            :class="{ active: activePanel === 'connect' }"
            @click="openPanel('connect')"
          >
            新建连接
          </button>
          <button
            class="nav-btn"
            :class="{ active: activePanel === 'ssh' }"
            @click="openPanel('ssh')"
          >
            SSH 配置
          </button>
          <button
            class="nav-btn"
            :class="{ active: activePanel === 'commands' }"
            @click="openPanel('commands')"
          >
            快捷命令
          </button>
          <button
            class="nav-btn"
            :class="{ active: activePanel === 'scripts' }"
            @click="openPanel('scripts')"
          >
            脚本管理
          </button>
        </nav>
      </div>
    </header>

    <div v-if="notice.text" class="global-notice" :class="notice.type">
      {{ notice.text }}
    </div>

    <!-- 全局专注模式小按钮，始终固定在角落 -->
    <button class="focus-floating" type="button" @click="toggleFocus">
      {{ focusMode ? "□" : "👁" }}
    </button>

    <main class="main-full" :class="{ 'focus-main': focusMode }">
      <aside v-if="!focusMode && showSidebar && sessions.length" class="session-sidebar">
        <h3 class="sidebar-title">会话</h3>
        <button
          v-for="session in sessions"
          :key="session.id"
          class="sidebar-tab"
          :class="{ active: session.id === activeId }"
          @click="setActive(session.id)"
        >
          <span class="dot" :class="session.status"></span>
          <span class="label">{{ session.title }}</span>
          <span class="close" @click.stop="closeSession(session.id)">×</span>
        </button>
      </aside>

      <section class="terminal-panel">
        <div class="terminal-container">
          <div v-if="sessions.length === 0" class="empty-terminal">
            <div class="empty-card">
              <h3>准备连接</h3>
              <p>点击上方“新建连接”，填写主机信息后打开终端。</p>
            </div>
          </div>
          <div
            v-for="session in sessions"
            :key="session.id"
            class="terminal-wrapper"
            v-show="session.id === activeId"
          >
            <div class="terminal-header">
              <span>{{ session.title }}</span>
              <span class="status-text">{{ statusLabel(session.status) }}</span>
            </div>
            <div class="terminal-body" :ref="(el) => setTerminalRef(session.id, el)"></div>
          </div>
        </div>

        <!-- 终端下方快捷按键条（移动端友好，尽量占用较少高度） -->
        <div v-if="sessions.length && !focusMode" class="quick-keys" @contextmenu.prevent>
          <button class="quick-key" @click="sendQuickKey('esc')" @touchend.prevent="sendQuickKey('esc')">ESC</button>
          <button class="quick-key" :class="{ active: shiftPending }" @click="sendQuickKey('shift')" @touchend.prevent="sendQuickKey('shift')">SHIFT</button>
          <button class="quick-key" :class="{ active: ctrlPending }" @click="sendQuickKey('ctrl')" @touchend.prevent="sendQuickKey('ctrl')">CTRL</button>
          <button class="quick-key" @click="sendQuickKey('tab')" @touchend.prevent="sendQuickKey('tab')">TAB</button>
          <button class="quick-key" @click="sendQuickKey('up')" @touchend.prevent="sendQuickKey('up')">↑</button>
          <button class="quick-key" @click="sendQuickKey('down')" @touchend.prevent="sendQuickKey('down')">↓</button>
          <button class="quick-key" @click="sendQuickKey('left')" @touchend.prevent="sendQuickKey('left')">←</button>
          <button class="quick-key" @click="sendQuickKey('right')" @touchend.prevent="sendQuickKey('right')">→</button>
          <button class="quick-key primary" @click="sendQuickKey('enter')" @touchend.prevent="sendQuickKey('enter')">↵</button>
          <button class="quick-key" @click="sendQuickKey('slash')" @touchend.prevent="sendQuickKey('slash')">/</button>
          <button class="quick-key" @click="sendQuickKey('minus')" @touchend.prevent="sendQuickKey('minus')">-</button>
          <button class="quick-key" @click="sendQuickKey('dot')" @touchend.prevent="sendQuickKey('dot')">.</button>
          <button class="quick-key" @click="sendQuickKey('hash')" @touchend.prevent="sendQuickKey('hash')">#</button>
          <button class="quick-key" @click="sendQuickKey('amp')" @touchend.prevent="sendQuickKey('amp')">&amp;</button>
        </div>
      </section>

      <!-- 顶部导航对应的弹出面板，覆盖在终端之上 -->
      <div v-if="!focusMode && activePanel" class="overlay" @click.self="closeOverlay">
        <div class="overlay-card">
          <header class="overlay-header">
            <h2 v-if="activePanel === 'connect'">新建连接</h2>
            <h2 v-else-if="activePanel === 'ssh'">SSH 配置</h2>
            <h2 v-else-if="activePanel === 'commands'">快捷命令</h2>
            <h2 v-else-if="activePanel === 'scripts'">脚本管理</h2>
            <button class="icon-btn" type="button" @click="closeOverlay">×</button>
          </header>

          <!-- 新建连接面板 -->
          <div v-if="activePanel === 'connect'" class="overlay-body">
            <div class="form-grid">
              <label>
                主机
                <input v-model.trim="form.host" placeholder="例如 192.168.1.10" />
              </label>
              <label>
                端口
                <input
                  v-model.number="form.port"
                  type="number"
                  min="1"
                  max="65535"
                />
              </label>
              <label>
                用户名
                <input v-model.trim="form.username" placeholder="root / ubuntu" />
              </label>
              <label>
                认证方式
                <select v-model="form.authType">
                  <option value="password">密码</option>
                  <option value="key">私钥</option>
                </select>
              </label>
              <label v-if="form.authType === 'password'">
                密码
                <input v-model="form.password" type="password" placeholder="仅用于本次连接" />
              </label>
              <label v-else>
                私钥
                <textarea
                  v-model="form.privateKey"
                  rows="4"
                  placeholder="粘贴 OpenSSH 私钥"
                ></textarea>
              </label>
              <label v-if="form.authType === 'key'">
                私钥口令
                <input v-model="form.passphrase" type="password" placeholder="可留空" />
              </label>
            </div>
            <div class="form-actions">
              <button class="primary" @click="createSession">连接</button>
              <button class="secondary" @click="resetForm">清空</button>
            </div>
            <p v-if="formError" class="error">{{ formError }}</p>
            <div class="tips">
              <span>WebSocket：{{ wsUrl }}</span>
              <span>支持多窗口并行连接</span>
            </div>
          </div>

          <!-- SSH 配置面板 -->
          <div v-else-if="activePanel === 'ssh'" class="overlay-body">
            <div class="sub-panel">
              <header class="sub-panel-header">
                <h3>SSH 配置 (使用 /api/ssh)</h3>
                <button class="secondary tiny" @click="loadSSH" :disabled="sshLoading">
                  刷新
                </button>
              </header>
              <p v-if="sshError" class="error small">{{ sshError }}</p>
              <div class="ssh-list" v-if="sshList.length">
                <div class="ssh-item" v-for="item in sshList" :key="item.name">
                  <div class="ssh-meta">
                    <div class="ssh-title">
                      <strong>{{ item.alias }}</strong>
                      <span class="muted">({{ item.name }})</span>
                    </div>
                    <div class="ssh-host">
                      {{ item.username }}@{{ item.host }}:{{ item.port || 22 }}
                    </div>
                  </div>
                  <div class="ssh-actions">
                    <button class="secondary tiny" @click="connectWithSSH(item)">
                      连接
                    </button>
                    <button class="secondary tiny" @click="editSSH(item)">
                      编辑
                    </button>
                    <button class="danger tiny" @click="deleteSSH(item)">
                      删除
                    </button>
                  </div>
                </div>
              </div>
              <p v-else class="empty small">暂无 SSH 配置，可在下方新建。</p>

              <form class="ssh-form" @submit.prevent="saveSSH">
                <h4>{{ sshEditingName ? "编辑 SSH 配置" : "新建 SSH 配置" }}</h4>
                <div class="form-grid">
                  <label>
                    标识名 (name)
                    <input
                      v-model.trim="sshForm.name"
                      :disabled="!!sshEditingName"
                      placeholder="文件名，不含 .json"
                    />
                  </label>
                  <label>
                    别名 (alias)
                    <input v-model.trim="sshForm.alias" placeholder="例如 生产环境" />
                  </label>
                  <label>
                    主机 (host)
                    <input v-model.trim="sshForm.host" placeholder="例如 192.168.1.10" />
                  </label>
                  <label>
                    端口
                    <input
                      v-model.number="sshForm.port"
                      type="number"
                      min="1"
                      max="65535"
                    />
                  </label>
                  <label>
                    用户名
                    <input v-model.trim="sshForm.username" placeholder="root / ubuntu" />
                  </label>
                  <label>
                    密码 (可选)
                    <input v-model="sshForm.password" type="password" />
                  </label>
                  <label>
                    私钥 (可选)
                    <textarea
                      v-model="sshForm.privateKey"
                      rows="4"
                      placeholder="粘贴 OpenSSH 私钥"
                    ></textarea>
                  </label>
                  <label>
                    私钥口令 (可选)
                    <input v-model="sshForm.passphrase" type="password" />
                  </label>
                </div>
                <div class="form-actions">
                  <button class="primary small" type="submit" :disabled="sshSaving">
                    {{ sshSaving ? "保存中..." : "保存" }}
                  </button>
                  <button class="secondary small" type="button" @click="resetSSHForm">
                    重置
                  </button>
                </div>
              </form>
            </div>
          </div>

          <!-- 快捷命令面板 -->
          <div v-else-if="activePanel === 'commands'" class="overlay-body">
            <div class="sub-panel">
              <header class="sub-panel-header">
                <h3>快捷命令 (使用 /api/commands)</h3>
                <button
                  class="secondary tiny"
                  @click="loadCommands"
                  :disabled="commandLoading"
                >
                  刷新
                </button>
              </header>
              <p v-if="commandError" class="error small">{{ commandError }}</p>
              <div class="command-list" v-if="commandList.length">
                <div
                  class="command-item"
                  v-for="(item, index) in commandList"
                  :key="index"
                >
                  <div class="command-meta">
                    <strong>{{ item.alias }}</strong>
                    <code class="command-text">{{ item.command }}</code>
                  </div>
                  <div class="command-actions">
                    <button class="secondary tiny" @click="applyCommandToTerminal(item)">
                      发送
                    </button>
                    <button class="secondary tiny" @click="editCommand(item, index)">
                      编辑
                    </button>
                    <button class="danger tiny" @click="deleteCommand(index)">
                      删除
                    </button>
                  </div>
                </div>
              </div>
              <p v-else class="empty small">暂无快捷命令，可在下方新建。</p>

              <form class="command-form" @submit.prevent="saveCommand">
                <h4>{{ commandEditingIndex >= 0 ? "编辑快捷命令" : "新建快捷命令" }}</h4>
                <div class="form-grid">
                  <label>
                    别名
                    <input v-model.trim="commandForm.alias" placeholder="例如 查看磁盘" />
                  </label>
                  <label>
                    命令
                    <input
                      v-model.trim="commandForm.command"
                      placeholder="例如 df -h"
                    />
                  </label>
                </div>
                <div class="form-actions">
                  <button class="primary small" type="submit" :disabled="commandSaving">
                    {{ commandSaving ? "保存中..." : "保存" }}
                  </button>
                  <button class="secondary small" type="button" @click="resetCommandForm">
                    重置
                  </button>
                </div>
              </form>
            </div>
          </div>

          <!-- 脚本管理面板 -->
          <div v-else-if="activePanel === 'scripts'" class="overlay-body">
            <div class="sub-panel">
              <header class="sub-panel-header">
                <h3>脚本管理 (使用 /api/scripts)</h3>
                <button class="secondary tiny" @click="loadScripts" :disabled="scriptLoading">
                  刷新
                </button>
              </header>
              <p v-if="scriptError" class="error small">{{ scriptError }}</p>
              <div class="script-layout">
                <div class="script-list" v-if="scriptList.length">
                  <button
                    v-for="item in scriptList"
                    :key="item.name"
                    class="script-item"
                    :class="{ active: item.name === scriptSelected }"
                    type="button"
                    @click="selectScript(item.name)"
                  >
                    {{ item.name }}
                  </button>
                </div>
                <p v-else class="empty small">暂无脚本，可直接在右侧新建。</p>

                <div class="script-editor">
                  <div class="form-grid">
                    <label>
                      名称
                      <input
                        v-model.trim="scriptForm.name"
                        placeholder="例如 docker-info.sh"
                        :disabled="!!scriptSelected"
                      />
                    </label>
                  </div>
                  <label class="script-content-label">
                    内容
                    <textarea
                      v-model="scriptForm.content"
                      rows="6"
                      placeholder="#!/bin/bash"
                    ></textarea>
                  </label>
                  <div class="form-actions">
                    <button
                      class="primary small"
                      type="button"
                      @click="saveScript"
                      :disabled="scriptSaving"
                    >
                      {{ scriptSaving ? "保存中..." : scriptSelected ? "更新" : "新建" }}
                    </button>
                    <button
                      v-if="scriptSelected"
                      class="danger small"
                      type="button"
                      @click="deleteScript"
                    >
                      删除
                    </button>
                    <button class="secondary small" type="button" @click="resetScriptForm">
                      重置
                    </button>
                    <button
                      class="secondary small"
                      type="button"
                      @click="applyScriptToTerminal"
                      :disabled="!scriptForm.content"
                    >
                      发送
                    </button>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </main>
  </div>
</template>

<script setup>
import {
  computed,
  markRaw,
  nextTick,
  onBeforeUnmount,
  onMounted,
  reactive,
  ref,
  watch,
} from "vue";
import { Terminal } from "@xterm/xterm";
import { FitAddon } from "@xterm/addon-fit";
import { apiRequest } from "./api";

const form = reactive({
  host: "",
  port: 22,
  username: "",
  authType: "password",
  password: "",
  privateKey: "",
  passphrase: "",
});

const sessions = ref([]);
const activeId = ref("");
const formError = ref("");
const terminalRefs = new Map();
const activePanel = ref("");
const showSidebar = ref(false);
const focusMode = ref(false);
const shiftPending = ref(false);
const ctrlPending = ref(false);

const wsUrl = computed(() => {
  const envUrl = import.meta.env.VITE_WS_URL;
  if (envUrl) return envUrl;
  if (typeof window === "undefined") return "ws://localhost:8080/api/ws/ssh";
  // 默认走同源 WebSocket（更适合反向代理 + HTTPS）
  const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
  return `${protocol}//${window.location.host}/api/ws/ssh`;
});

function setTerminalRef(id, el) {
  if (el) {
    terminalRefs.set(id, el);
  } else {
    terminalRefs.delete(id);
  }
}

function statusLabel(status) {
  switch (status) {
    case "ready":
      return "已连接";
    case "connecting":
      return "连接中";
    case "connected":
      return "WebSocket 已连";
    case "closing":
      return "关闭中";
    case "closed":
      return "已关闭";
    case "error":
      return "错误";
    default:
      return "准备中";
  }
}

// ─── SSH 配置 (/api/ssh) ─────────────────────────
const sshList = ref([]);
const sshLoading = ref(false);
const sshSaving = ref(false);
const sshError = ref("");
const sshEditingName = ref("");
const sshForm = reactive({
  name: "",
  alias: "",
  host: "",
  port: 22,
  username: "",
  password: "",
  privateKey: "",
  passphrase: "",
});

async function loadSSH() {
  sshError.value = "";
  sshLoading.value = true;
  try {
    const res = await apiRequest("/ssh", { method: "GET" });
    sshList.value = Array.isArray(res.data) ? res.data : [];
  } catch (err) {
    sshError.value = err.message || "加载 SSH 配置失败";
  } finally {
    sshLoading.value = false;
  }
}

function resetSSHForm() {
  sshEditingName.value = "";
  sshForm.name = "";
  sshForm.alias = "";
  sshForm.host = "";
  sshForm.port = 22;
  sshForm.username = "";
  sshForm.password = "";
  sshForm.privateKey = "";
  sshForm.passphrase = "";
}

function fillFromSSH(item) {
  form.host = item.host || "";
  form.port = item.port || 22;
  form.username = item.username || "";
  if (item.privateKey) {
    form.authType = "key";
    form.privateKey = item.privateKey;
    form.passphrase = item.passphrase || "";
    form.password = "";
  } else if (item.password) {
    form.authType = "password";
    form.password = item.password;
    form.privateKey = "";
    form.passphrase = "";
  }
}

function connectWithSSH(item) {
  fillFromSSH(item);
  createSession();
}

function editSSH(item) {
  sshEditingName.value = item.name || "";
  sshForm.name = item.name || "";
  sshForm.alias = item.alias || "";
  sshForm.host = item.host || "";
  sshForm.port = item.port || 22;
  sshForm.username = item.username || "";
  sshForm.password = item.password || "";
  sshForm.privateKey = item.privateKey || "";
  sshForm.passphrase = item.passphrase || "";
}

async function saveSSH() {
  sshError.value = "";
  if (!sshForm.alias || !sshForm.host || !sshForm.username) {
    sshError.value = "alias、host 和 username 为必填项";
    return;
  }
  if (!sshEditingName.value && !sshForm.name) {
    sshError.value = "新建配置时 name 为必填项";
    return;
  }
  sshSaving.value = true;
  try {
    const payload = {
      alias: sshForm.alias,
      host: sshForm.host,
      port: sshForm.port || 22,
      username: sshForm.username,
      password: sshForm.password,
      privateKey: sshForm.privateKey,
      passphrase: sshForm.passphrase,
    };
    if (sshEditingName.value) {
      await apiRequest(`/ssh/${encodeURIComponent(sshEditingName.value)}`, {
        method: "PUT",
        body: JSON.stringify(payload),
      });
    } else {
      payload.name = sshForm.name;
      await apiRequest("/ssh", {
        method: "POST",
        body: JSON.stringify(payload),
      });
    }
    await loadSSH();
    if (!sshEditingName.value) {
      resetSSHForm();
    }
  } catch (err) {
    sshError.value = err.message || "保存 SSH 配置失败";
  } finally {
    sshSaving.value = false;
  }
}

async function deleteSSH(item) {
  sshError.value = "";
  if (!item.name) return;
  try {
    await apiRequest(`/ssh/${encodeURIComponent(item.name)}`, {
      method: "DELETE",
    });
    if (sshEditingName.value === item.name) {
      resetSSHForm();
    }
    await loadSSH();
  } catch (err) {
    sshError.value = err.message || "删除 SSH 配置失败";
  }
}

// ─── 快捷命令 (/api/commands) ─────────────────────
const commandList = ref([]);
const commandLoading = ref(false);
const commandSaving = ref(false);
const commandError = ref("");
const commandEditingIndex = ref(-1);
const commandForm = reactive({
  alias: "",
  command: "",
});

async function loadCommands() {
  commandError.value = "";
  commandLoading.value = true;
  try {
    const res = await apiRequest("/commands", { method: "GET" });
    commandList.value = Array.isArray(res.data) ? res.data : [];
  } catch (err) {
    commandError.value = err.message || "加载快捷命令失败";
  } finally {
    commandLoading.value = false;
  }
}

function resetCommandForm() {
  commandEditingIndex.value = -1;
  commandForm.alias = "";
  commandForm.command = "";
}

function editCommand(item, index) {
  commandEditingIndex.value = index;
  commandForm.alias = item.alias || "";
  commandForm.command = item.command || "";
}

async function saveCommand() {
  commandError.value = "";
  if (!commandForm.alias || !commandForm.command) {
    commandError.value = "alias 和 command 为必填项";
    return;
  }
  commandSaving.value = true;
  try {
    const payload = {
      alias: commandForm.alias,
      command: commandForm.command,
    };
    if (commandEditingIndex.value >= 0) {
      await apiRequest(`/commands/${commandEditingIndex.value}`, {
        method: "PUT",
        body: JSON.stringify(payload),
      });
    } else {
      await apiRequest("/commands", {
        method: "POST",
        body: JSON.stringify(payload),
      });
    }
    await loadCommands();
    resetCommandForm();
  } catch (err) {
    commandError.value = err.message || "保存快捷命令失败";
  } finally {
    commandSaving.value = false;
  }
}

async function deleteCommand(index) {
  commandError.value = "";
  try {
    await apiRequest(`/commands/${index}`, { method: "DELETE" });
    if (commandEditingIndex.value === index) {
      resetCommandForm();
    }
    await loadCommands();
  } catch (err) {
    commandError.value = err.message || "删除快捷命令失败";
  }
}

function applyCommandToTerminal(item) {
  const session = sessions.value.find((s) => s.id === activeId.value);
  if (!session || !session.ws || session.ws.readyState !== WebSocket.OPEN) {
    commandError.value = "当前没有可用终端连接";
    return;
  }
  const cmd = item.command || "";
  if (!cmd) return;
  session.ws.send(
    JSON.stringify({
      type: "input",
      data: `${cmd}\n`,
    })
  );
  closeOverlay();
}

// ─── 脚本管理 (/api/scripts) ──────────────────────
const scriptList = ref([]);
const scriptLoading = ref(false);
const scriptSaving = ref(false);
const scriptError = ref("");
const scriptSelected = ref("");
const scriptForm = reactive({
  name: "",
  content: "",
});

async function loadScripts() {
  scriptError.value = "";
  scriptLoading.value = true;
  try {
    const res = await apiRequest("/scripts", { method: "GET" });
    scriptList.value = Array.isArray(res.data) ? res.data : [];
  } catch (err) {
    scriptError.value = err.message || "加载脚本列表失败";
  } finally {
    scriptLoading.value = false;
  }
}

async function selectScript(name) {
  scriptError.value = "";
  scriptSelected.value = name;
  try {
    const res = await apiRequest(`/scripts/${encodeURIComponent(name)}`, {
      method: "GET",
    });
    scriptForm.name = res.data?.name || name;
    scriptForm.content = res.data?.content || "";
  } catch (err) {
    scriptError.value = err.message || "加载脚本内容失败";
  }
}

function resetScriptForm() {
  scriptSelected.value = "";
  scriptForm.name = "";
  scriptForm.content = "";
}

async function saveScript() {
  scriptError.value = "";
  if (!scriptSelected.value && !scriptForm.name) {
    scriptError.value = "新建脚本时 name 为必填项";
    return;
  }
  scriptSaving.value = true;
  try {
    if (scriptSelected.value) {
      await apiRequest(`/scripts/${encodeURIComponent(scriptSelected.value)}`, {
        method: "PUT",
        body: JSON.stringify({ content: scriptForm.content || "" }),
      });
    } else {
      await apiRequest("/scripts", {
        method: "POST",
        body: JSON.stringify({
          name: scriptForm.name,
          content: scriptForm.content || "",
        }),
      });
      scriptSelected.value = scriptForm.name;
    }
    await loadScripts();
  } catch (err) {
    scriptError.value = err.message || "保存脚本失败";
  } finally {
    scriptSaving.value = false;
  }
}

async function deleteScript() {
  scriptError.value = "";
  if (!scriptSelected.value) return;
  try {
    await apiRequest(`/scripts/${encodeURIComponent(scriptSelected.value)}`, {
      method: "DELETE",
    });
    await loadScripts();
    resetScriptForm();
  } catch (err) {
    scriptError.value = err.message || "删除脚本失败";
  }
}

function applyScriptToTerminal() {
  const session = sessions.value.find((s) => s.id === activeId.value);
  if (!session || !session.ws || session.ws.readyState !== WebSocket.OPEN) {
    scriptError.value = "当前没有可用终端连接";
    return;
  }
  if (!scriptForm.content) return;
  session.ws.send(
    JSON.stringify({
      type: "input",
      data: `${scriptForm.content}\n`,
    })
  );
  closeOverlay();
}

function sendQuickKey(type) {
  const session = sessions.value.find((s) => s.id === activeId.value);
  if (!session || !session.ws || session.ws.readyState !== WebSocket.OPEN) {
    return;
  }

  // SHIFT / CTRL 单独发送通常无效果，做成“下一次快捷键生效”的修饰键
  if (type === "shift") {
    shiftPending.value = !shiftPending.value;
    return;
  }
  if (type === "ctrl") {
    ctrlPending.value = !ctrlPending.value;
    return;
  }

  let data = "";
  switch (type) {
    case "esc":
      data = "\x1b";
      break;
    case "tab":
      data = "\t";
      break;
    case "slash":
      data = "/";
      break;
    case "minus":
      data = "-";
      break;
    case "dot":
      data = ".";
      break;
    case "hash":
      data = "#";
      break;
    case "amp":
      data = "&";
      break;
    case "up":
      data = "\x1b[A";
      break;
    case "down":
      data = "\x1b[B";
      break;
    case "right":
      data = "\x1b[C";
      break;
    case "left":
      data = "\x1b[D";
      break;
    case "enter":
      data = "\r";
      break;
    default:
      return;
  }

  // SHIFT 一次性修饰
  if (shiftPending.value) {
    if (data === "\t") {
      data = "\x1b[Z"; // Shift+Tab
    } else if (data === "/") {
      data = "?";
    } else if (data === "-") {
      data = "_";
    } else if (data === ".") {
      data = ">";
    } else if (data === "\x1b[A") {
      data = "\x1b[1;2A";
    } else if (data === "\x1b[B") {
      data = "\x1b[1;2B";
    } else if (data === "\x1b[C") {
      data = "\x1b[1;2C";
    } else if (data === "\x1b[D") {
      data = "\x1b[1;2D";
    } else if (data.length === 1) {
      data = data.toUpperCase();
    }
  }

  // CTRL 一次性修饰
  if (ctrlPending.value) {
    if (data === "\x1b[A") {
      data = "\x1b[1;5A";
    } else if (data === "\x1b[B") {
      data = "\x1b[1;5B";
    } else if (data === "\x1b[C") {
      data = "\x1b[1;5C";
    } else if (data === "\x1b[D") {
      data = "\x1b[1;5D";
    } else if (data.length === 1) {
      const ch = data.toLowerCase();
      if (ch >= "a" && ch <= "z") {
        data = String.fromCharCode(ch.charCodeAt(0) - 96);
      } else if (ch === "/") {
        data = "\x1f"; // Ctrl+/
      }
    }
  }

  session.ws.send(
    JSON.stringify({
      type: "input",
      data,
    })
  );

  shiftPending.value = false;
  ctrlPending.value = false;
}

function resetForm() {
  form.host = "";
  form.port = 22;
  form.username = "";
  form.authType = "password";
  form.password = "";
  form.privateKey = "";
  form.passphrase = "";
  formError.value = "";
}

function createSession() {
  formError.value = "";
  if (!form.host || !form.username) {
    formError.value = "请填写主机与用户名。";
    return;
  }
  if (form.authType === "password" && !form.password) {
    formError.value = "请选择密码认证时请输入密码。";
    return;
  }
  if (form.authType === "key" && !form.privateKey) {
    formError.value = "请选择私钥认证时请输入私钥。";
    return;
  }

  const id = crypto.randomUUID();
  const title = `${form.username}@${form.host}:${form.port}`;
  const session = {
    id,
    title,
    status: "connecting",
    ws: null,
    term: null,
    fit: null,
  };

  sessions.value.push(session);
  activeId.value = id;

  const payload = {
    type: "connect",
    host: form.host,
    port: form.port,
    username: form.username,
    password: form.authType === "password" ? form.password : "",
    privateKey: form.authType === "key" ? form.privateKey : "",
    passphrase: form.authType === "key" ? form.passphrase : "",
  };

  nextTick(() => initSession(session, payload));
  closeOverlay();
}

function initSession(session, payload) {
  const container = terminalRefs.get(session.id);
  if (!container) return;

  const term = markRaw(
    new Terminal({
      fontFamily: `"JetBrains Mono", "Fira Code", "Cascadia Code", monospace`,
      fontSize: 14,
      lineHeight: 1.4,
      cursorBlink: true,
      scrollback: 8000,
      smoothScrollDuration: 140,
      scrollSensitivity: 1.15,
      fastScrollSensitivity: 4,
      theme: {
        background: "#0b0f17",
        foreground: "#e6edf3",
        selection: "#334155",
        cursor: "#7dd3fc",
        black: "#0b0f17",
        red: "#f87171",
        green: "#34d399",
        yellow: "#fbbf24",
        blue: "#60a5fa",
        magenta: "#c084fc",
        cyan: "#22d3ee",
        white: "#e2e8f0",
        brightBlack: "#1f2937",
        brightRed: "#fb7185",
        brightGreen: "#4ade80",
        brightYellow: "#facc15",
        brightBlue: "#93c5fd",
        brightMagenta: "#d8b4fe",
        brightCyan: "#67e8f9",
        brightWhite: "#f8fafc",
      },
    })
  );
  const fitAddon = markRaw(new FitAddon());
  term.loadAddon(fitAddon);
  term.open(container);
  fitAddon.fit();
  term.focus();
  term.writeln("\x1b[90m[正在建立连接...]\x1b[0m");

  const ws = markRaw(new WebSocket(wsUrl.value));
  session.ws = ws;
  session.term = term;
  session.fit = fitAddon;

  const sendResize = () => {
    if (ws.readyState !== WebSocket.OPEN) return;
    ws.send(
      JSON.stringify({
        type: "resize",
        cols: term.cols,
        rows: term.rows,
      })
    );
  };

  ws.onopen = () => {
    session.status = "connected";
    ws.send(
      JSON.stringify({
        ...payload,
        cols: term.cols,
        rows: term.rows,
      })
    );
  };

  ws.onmessage = (event) => {
    let msg;
    try {
      msg = JSON.parse(event.data);
    } catch (err) {
      term.write(event.data);
      return;
    }
    if (msg.type === "output" && msg.data) {
      term.write(msg.data);
    } else if (msg.type === "status") {
      session.status = msg.status || session.status;
      if (msg.message) {
        term.writeln(`\r\x1b[90m[${msg.message}]\x1b[0m`);
      }
    } else if (msg.type === "error") {
      session.status = "error";
      if (msg.message) {
        term.writeln(`\r\x1b[31m[${msg.message}]\x1b[0m`);
      }
    }
  };

  ws.onerror = () => {
    session.status = "error";
    term.writeln("\r\n\x1b[31m[连接错误]\x1b[0m");
  };

  ws.onclose = () => {
    if (session.status !== "closed") {
      session.status = "closed";
    }
    term.writeln("\r\n\x1b[90m[连接已关闭]\x1b[0m");
  };

  term.onData((data) => {
    if (ws.readyState === WebSocket.OPEN) {
      ws.send(JSON.stringify({ type: "input", data }));
    }
  });

  term.onResize(() => sendResize());

  focusSession(session);
}

function focusSession(session) {
  if (!session || !session.term || !session.fit) return;
  nextTick(() => {
    session.fit.fit();
    if (session.ws?.readyState === WebSocket.OPEN) {
      session.ws.send(
        JSON.stringify({
          type: "resize",
          cols: session.term.cols,
          rows: session.term.rows,
        })
      );
    }
    session.term.scrollToBottom();
    session.term.focus();
  });
}

function setActive(id) {
  activeId.value = id;
}

function closeSession(id) {
  const index = sessions.value.findIndex((item) => item.id === id);
  if (index === -1) return;
  const session = sessions.value[index];
  if (session.ws && session.ws.readyState === WebSocket.OPEN) {
    session.ws.send(JSON.stringify({ type: "close" }));
    session.ws.close();
  }
  session.term?.dispose();
  sessions.value.splice(index, 1);
  if (activeId.value === id) {
    activeId.value = sessions.value[0]?.id || "";
  }
}

function handleWindowResize() {
  const session = sessions.value.find((item) => item.id === activeId.value);
  if (!session) return;
  focusSession(session);
}

watch(activeId, (id) => {
  const session = sessions.value.find((item) => item.id === id);
  if (session) {
    focusSession(session);
  }
});

function openPanel(name) {
  activePanel.value = name;
}

function closeOverlay() {
  activePanel.value = "";
}

function toggleSidebar() {
  showSidebar.value = !showSidebar.value;
}

function toggleFocus() {
  focusMode.value = !focusMode.value;
}

const notice = reactive({
  text: "",
  type: "info",
});
let noticeTimer = null;

function showNotice(text, type = "info", duration = 3200) {
  notice.text = text;
  notice.type = type;
  if (noticeTimer) {
    clearTimeout(noticeTimer);
  }
  noticeTimer = setTimeout(() => {
    notice.text = "";
    noticeTimer = null;
  }, duration);
}

function isEditableTarget(target) {
  if (!target || typeof target !== "object") return false;
  const tag = target.tagName?.toLowerCase();
  if (tag === "input" || tag === "textarea") return true;
  return !!target.isContentEditable;
}

function bytesToBase64(buffer) {
  const bytes = new Uint8Array(buffer);
  const chunkSize = 0x8000;
  let binary = "";
  for (let i = 0; i < bytes.length; i += chunkSize) {
    binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize));
  }
  return btoa(binary);
}

function chunkBase64(text, size = 76) {
  const chunks = [];
  for (let i = 0; i < text.length; i += size) {
    chunks.push(text.slice(i, i + size));
  }
  return chunks.join("\n");
}

function safeFilename(name) {
  return name.replace(/[^a-zA-Z0-9._-]/g, "-");
}

function formatTimestamp(date = new Date()) {
  const pad = (v) => String(v).padStart(2, "0");
  return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(
    date.getDate()
  )}-${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`;
}

function sendImageToTerminal(file) {
  const session = sessions.value.find((item) => item.id === activeId.value);
  if (!session || !session.ws || session.ws.readyState !== WebSocket.OPEN) {
    showNotice("已识别剪贴板图片，但当前没有可用终端连接", "warning");
    return;
  }
  const maxBytes = 2 * 1024 * 1024;
  if (file.size > maxBytes) {
    showNotice("图片过大，已取消粘贴（限制 2MB）", "warning");
    return;
  }
  const ext = (file.type || "image/png").split("/")[1] || "png";
  const filename = safeFilename(`clipboard-${formatTimestamp()}.${ext}`);
  const reader = new FileReader();
  reader.onload = () => {
    try {
      const base64 = bytesToBase64(reader.result);
      const payload = chunkBase64(base64);
      const marker = `__MENGYA_CLIP_${Date.now()}__`;
      const script =
        `cat <<'${marker}' | base64 -d > ${filename}\n` +
        `${payload}\n` +
        `${marker}\n`;
      session.ws.send(JSON.stringify({ type: "input", data: script }));
      session.term?.writeln(
        `\r\n\x1b[90m[已接收剪贴板图片，保存为 ${filename}]\x1b[0m`
      );
      showNotice(`已发送图片到终端：${filename}`, "success");
    } catch (err) {
      console.error(err);
      showNotice("图片粘贴失败，请重试", "error");
    }
  };
  reader.onerror = () => {
    showNotice("读取剪贴板图片失败", "error");
  };
  reader.readAsArrayBuffer(file);
}

function handlePaste(event) {
  if (!event || !event.clipboardData) return;
  if (isEditableTarget(event.target)) return;
  const text = event.clipboardData.getData("text/plain");
  if (text) return;
  const items = Array.from(event.clipboardData.items || []);
  const imageItem = items.find((item) => item.type?.startsWith("image/"));
  if (!imageItem) return;
  const file = imageItem.getAsFile();
  if (!file) return;
  event.preventDefault();
  event.stopPropagation();
  sendImageToTerminal(file);
}

// 根元素引用，用于动态调整高度以适配移动端虚拟键盘
const appRef = ref(null);
let viewportFitTimer = null;

function updateAppHeight() {
  const vv = window.visualViewport;
  const h = vv ? vv.height : window.innerHeight;
  // 部分移动端浏览器键盘弹出时高度不缩，但会覆盖页面；用 padding-bottom 兜底
  const keyboard =
    vv && typeof vv.height === "number"
      ? Math.max(0, window.innerHeight - vv.height - (vv.offsetTop || 0))
      : 0;
  if (appRef.value) {
    appRef.value.style.height = h + "px";
    appRef.value.style.setProperty("--keyboard-inset", keyboard + "px");
  }
  // 触发布局变化后，让终端重新适配可视区域
  requestAnimationFrame(() => {
    const session = sessions.value.find((item) => item.id === activeId.value);
    if (session) focusSession(session);
  });
  if (viewportFitTimer) {
    clearTimeout(viewportFitTimer);
  }
  viewportFitTimer = setTimeout(() => {
    const session = sessions.value.find((item) => item.id === activeId.value);
    if (session) focusSession(session);
  }, 140);
}

function hideSplashScreen() {
  if (typeof window === "undefined") return;
  const hide = window.__hideSplash;
  if (typeof hide === "function") {
    requestAnimationFrame(() => hide());
  }
}

onMounted(() => {
  window.addEventListener("resize", handleWindowResize);
  document.addEventListener("paste", handlePaste);
  // 移动端虚拟键盘弹出时，visualViewport 会缩减，用它动态撑高度
  if (window.visualViewport) {
    window.visualViewport.addEventListener("resize", updateAppHeight);
    window.visualViewport.addEventListener("scroll", updateAppHeight);
  } else {
    window.addEventListener("resize", updateAppHeight);
  }
  updateAppHeight();
  // 尝试加载后端配置，如后端未启动仅在面板中提示错误
  Promise.allSettled([loadSSH(), loadCommands(), loadScripts()]).finally(() => {
    hideSplashScreen();
  });
});

onBeforeUnmount(() => {
  window.removeEventListener("resize", handleWindowResize);
  document.removeEventListener("paste", handlePaste);
  if (viewportFitTimer) {
    clearTimeout(viewportFitTimer);
    viewportFitTimer = null;
  }
  if (window.visualViewport) {
    window.visualViewport.removeEventListener("resize", updateAppHeight);
    window.visualViewport.removeEventListener("scroll", updateAppHeight);
  } else {
    window.removeEventListener("resize", updateAppHeight);
  }
  sessions.value.forEach((session) => {
    session.ws?.close();
    session.term?.dispose();
  });
});
</script>

<style scoped>
.app {
  /* 初始高度用 100svh（支持动态视口），JS 会在键盘弹出时动态覆盖为 visualViewport.height */
  height: 100svh;
  height: 100vh; /* 降级 */
  display: flex;
  flex-direction: column;
  overflow: hidden;
  overscroll-behavior: none;
  background: radial-gradient(circle at top, #1f2937 0, #020617 55%, #000 100%);
  color: #e5e7eb;
  font-family: system-ui, -apple-system, BlinkMacSystemFont, "SF Pro Text",
    "Segoe UI", sans-serif;
}

.global-notice {
  position: fixed;
  top: 64px;
  right: 20px;
  z-index: 50;
  padding: 10px 14px;
  border-radius: 10px;
  font-size: 13px;
  background: rgba(15, 23, 42, 0.92);
  color: #e2e8f0;
  border: 1px solid rgba(148, 163, 184, 0.3);
  box-shadow: 0 10px 30px rgba(15, 23, 42, 0.45);
  backdrop-filter: blur(8px);
}

.global-notice.success {
  border-color: rgba(34, 197, 94, 0.4);
  color: #bbf7d0;
}

.global-notice.warning {
  border-color: rgba(251, 191, 36, 0.4);
  color: #fde68a;
}

.global-notice.error {
  border-color: rgba(248, 113, 113, 0.45);
  color: #fecaca;
}

.app-header {
  height: 56px;
  padding: 0 20px;
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 16px;
  border-bottom: 1px solid rgba(148, 163, 184, 0.3);
  backdrop-filter: blur(18px);
  background: linear-gradient(
    to right,
    rgba(15, 23, 42, 0.9),
    rgba(15, 23, 42, 0.7)
  );
  position: sticky;
  top: 0;
  z-index: 10;
}

.brand {
  display: flex;
  align-items: center;
  gap: 8px;
}

.sidebar-toggle {
  width: 28px;
  height: 28px;
  border-radius: 999px;
  border: 1px solid rgba(75, 85, 99, 0.9);
  background: #020617;
  color: #e5e7eb;
  font-size: 14px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}

.sidebar-toggle:hover {
  border-color: rgba(148, 163, 184, 0.9);
}

.header-right {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 8px;
}

.focus-toggle {
  width: 32px;
  height: 32px;
  border-radius: 999px;
  border: 1px solid rgba(75, 85, 99, 0.9);
  background: #020617;
  color: #e5e7eb;
  font-size: 14px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}

.focus-toggle:hover {
  border-color: rgba(148, 163, 184, 0.9);
}

.focus-floating {
  position: fixed;
  top: 10px;
  right: 10px;
  width: 26px;
  height: 26px;
  border-radius: 999px;
  border: 1px solid rgba(75, 85, 99, 0.9);
  background: #020617;
  color: #e5e7eb;
  font-size: 13px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 50;
}

.focus-floating:hover {
  border-color: rgba(148, 163, 184, 0.9);
}

.brand h1 {
  font-size: 18px;
  font-weight: 600;
  letter-spacing: 0.04em;
  background: linear-gradient(120deg, #a5b4fc, #38bdf8, #22c55e);
  -webkit-background-clip: text;
  color: transparent;
  margin: 0;
}

.app-nav {
  display: flex;
  gap: 8px;
  flex-wrap: nowrap;
  overflow-x: auto;
  scrollbar-width: none;
  -ms-overflow-style: none;
}

.app-nav::-webkit-scrollbar {
  width: 0;
  height: 0;
}

.nav-btn {
  position: relative;
  border-radius: 12px;
  white-space: nowrap;
  flex-shrink: 0;
  border: 1px solid rgba(148, 163, 184, 0.5);
  background: radial-gradient(circle at top, #0f172a, #020617);
  color: #e5e7eb;
  padding: 6px 14px;
  font-size: 13px;
  cursor: pointer;
  outline: none;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  transition:
    border-color 0.15s ease,
    background 0.15s ease,
    transform 0.1s ease,
    box-shadow 0.1s ease;
  box-shadow: 0 0 0 1px rgba(15, 23, 42, 0.9), 0 10px 30px rgba(15, 23, 42, 0.6);
}

.app-nav {
  /* 移动端横滑时更顺手，避免被按钮点击完全吞掉 */
  -webkit-overflow-scrolling: touch;
  touch-action: pan-x;
}

.nav-btn::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: radial-gradient(circle, #4ade80, #22c55e);
  box-shadow: 0 0 0 1px rgba(34, 197, 94, 0.4);
}

.nav-btn:hover {
  border-color: rgba(248, 250, 252, 0.8);
  background: radial-gradient(circle at top, #111827, #020617);
  box-shadow: 0 10px 35px rgba(15, 23, 42, 0.9);
  transform: translateY(-0.5px);
}

.nav-btn.active {
  border-color: rgba(96, 165, 250, 0.9);
  background: radial-gradient(circle at top, #1d283a, #020617);
  box-shadow: 0 0 0 1px rgba(37, 99, 235, 0.7),
    0 18px 45px rgba(30, 64, 175, 0.9);
}

.main-full {
  flex: 1;
  display: flex;
  position: relative;
  padding: 12px 12px 16px;
  gap: 8px;
  min-height: 0;
}

.focus-main {
  padding: 0;
  gap: 0;
}

.session-sidebar {
  position: absolute;
  inset: 0 auto 0 0; /* 顶部、左侧贴边，覆盖在终端上 */
  width: 220px;
  padding: 8px 8px 10px;
  border-radius: 10px 0 0 10px;
  background: radial-gradient(circle at top left, #020617, #020617);
  box-shadow:
    0 0 0 1px rgba(15, 23, 42, 0.9),
    0 20px 50px rgba(15, 23, 42, 0.9);
  display: flex;
  flex-direction: column;
  gap: 4px;
  z-index: 30;
}

.sidebar-title {
  margin: 0 0 4px;
  font-size: 12px;
  color: #9ca3af;
}

.sidebar-tab {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 4px 8px;
  border-radius: 8px;
  border: 1px solid rgba(55, 65, 81, 0.9);
  background: #020617;
  color: #e5e7eb;
  font-size: 12px;
  cursor: pointer;
}

.sidebar-tab .label {
  flex: 1;
  white-space: nowrap;
  text-overflow: ellipsis;
  overflow: hidden;
}

.sidebar-tab .dot {
  width: 8px;
  height: 8px;
  border-radius: 999px;
  background: #4b5563;
}

.sidebar-tab .dot.ready {
  background: #22c55e;
}

.sidebar-tab .dot.connecting {
  background: #f97316;
}

.sidebar-tab .dot.error {
  background: #ef4444;
}

.sidebar-tab .dot.closed {
  background: #6b7280;
}

.sidebar-tab .close {
  font-size: 10px;
  opacity: 0.8;
}

.sidebar-tab.active {
  border-color: rgba(59, 130, 246, 0.9);
  background: radial-gradient(circle at top, #111827, #020617);
}

.terminal-panel {
  flex: 1;
  display: flex;
  flex-direction: column;
  border-radius: 12px;
  background: radial-gradient(circle at top left, #020617, #020617);
  box-shadow:
    0 0 0 1px rgba(15, 23, 42, 0.9),
    0 30px 80px rgba(15, 23, 42, 0.95),
    0 0 40px rgba(56, 189, 248, 0.12);
  overflow: hidden;
  /* 虚拟键盘覆盖时，给底部留白避免遮挡终端最后几行 */
  padding-bottom: var(--keyboard-inset, 0px);
  min-height: 0;
}

.tabs {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 8px 10px;
  background: radial-gradient(circle at top left, #020617, #020617);
  border-bottom: 1px solid rgba(30, 64, 175, 0.6);
}

.tabs .empty {
  font-size: 13px;
  color: #9ca3af;
}

.tab {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 11px;
  border-radius: 8px;
  border: 1px solid rgba(55, 65, 81, 0.9);
  background: radial-gradient(circle at top, #020617, #020617);
  color: #e5e7eb;
  font-size: 12px;
  cursor: pointer;
  outline: none;
  transition:
    background 0.15s ease,
    border-color 0.15s ease,
    transform 0.08s ease;
}

.tab .label {
  max-width: 200px;
  white-space: nowrap;
  text-overflow: ellipsis;
  overflow: hidden;
}

.tab .status {
  width: 8px;
  height: 8px;
  border-radius: 999px;
  background: #4b5563;
}

.tab .status.ready {
  background: #22c55e;
  box-shadow: 0 0 0 2px rgba(22, 163, 74, 0.5);
}

.tab .status.connecting {
  background: #f97316;
}

.tab .status.error {
  background: #ef4444;
}

.tab .status.closed {
  background: #6b7280;
}

.tab .close {
  font-size: 10px;
  opacity: 0.8;
}

.tab:hover {
  border-color: rgba(148, 163, 184, 0.9);
}

.tab.active {
  border-color: rgba(59, 130, 246, 0.95);
  background: radial-gradient(circle at top, #111827, #020617);
  transform: translateY(-0.5px);
}

.terminal-container {
  flex: 1;
  position: relative;
  background: #020617;
  /* 交给 xterm 内部滚动，外层不再出现滚动条 */
  overflow: hidden;
  min-height: 0;
}

.empty-terminal {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: radial-gradient(circle at top, #020617, #020617);
}

.empty-card {
  padding: 20px 24px;
  border-radius: 10px;
  border: 1px solid rgba(55, 65, 81, 0.9);
  background: radial-gradient(circle at top, #020617, #020617);
  box-shadow:
    0 0 0 1px rgba(15, 23, 42, 0.8),
    0 16px 40px rgba(15, 23, 42, 0.9);
  text-align: center;
}

.empty-card h3 {
  margin: 0 0 8px;
  font-size: 16px;
}

.empty-card p {
  margin: 0;
  font-size: 13px;
  color: #9ca3af;
}

.terminal-wrapper {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.terminal-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 6px 12px;
  font-size: 12px;
  background: linear-gradient(to right, #020617, #020617);
  border-bottom: 1px solid rgba(31, 41, 55, 0.9);
  color: #e5e7eb;
}

.terminal-header .status-text {
  color: #9ca3af;
}

.terminal-body {
  flex: 1;
  padding: 8px;
  min-height: 0;
  overflow: hidden;
  overscroll-behavior: contain;
}

.quick-keys {
  display: flex;
  flex-wrap: nowrap;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  touch-action: pan-x;
  gap: 4px;
  padding: 4px 8px 6px;
  background: #020617;
  border-top: 1px solid rgba(31, 41, 55, 0.9);
  scrollbar-width: none;
}
.quick-keys::-webkit-scrollbar {
  display: none;
}

.quick-key {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  min-width: 44px;
  height: 26px;
  border-radius: 8px;
  border: 1px solid rgba(55, 65, 81, 0.9);
  background: radial-gradient(circle at top, #020617, #020617);
  color: #e5e7eb;
  font-size: 11px;
  letter-spacing: 0.06em;
  white-space: nowrap;
  text-align: center;
  cursor: pointer;
  touch-action: manipulation;
  user-select: none;
  -webkit-user-select: none;
}

.quick-key.primary {
  border-color: rgba(59, 130, 246, 0.9);
  background: radial-gradient(circle at top, #1d4ed8, #1e293b);
}

.quick-key.active {
  border-color: rgba(59, 130, 246, 0.9);
  box-shadow: 0 0 0 1px rgba(59, 130, 246, 0.35) inset;
  background: radial-gradient(circle at top, #1d4ed8, #111827);
}

.quick-key:active {
  transform: translateY(1px);
  background: #030712;
}

.overlay {
  position: absolute;
  inset: 0;
  display: flex;
  justify-content: center;
  align-items: flex-start;
  padding: 20px;
  background: radial-gradient(circle at top, rgba(15, 23, 42, 0.8), rgba(15, 23, 42, 0.95));
  z-index: 20;
}

.overlay-card {
  width: min(960px, 100%);
  max-height: calc(100vh - 80px);
  background: radial-gradient(circle at top left, #020617, #020617);
  border-radius: 12px;
  border: 1px solid rgba(51, 65, 85, 0.9);
  box-shadow:
    0 0 0 1px rgba(15, 23, 42, 0.9),
    0 24px 80px rgba(15, 23, 42, 1),
    0 0 40px rgba(56, 189, 248, 0.18);
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.overlay-header {
  padding: 12px 16px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  border-bottom: 1px solid rgba(30, 64, 175, 0.7);
}

.overlay-header h2 {
  margin: 0;
  font-size: 16px;
  font-weight: 600;
}

.icon-btn {
  border: none;
  background: transparent;
  color: #9ca3af;
  font-size: 18px;
  cursor: pointer;
  padding: 0 4px;
}

.icon-btn:hover {
  color: #e5e7eb;
}

.overlay-body {
  padding: 14px 16px 16px;
  overflow: auto;
  /* 保留滚动行为但隐藏滚动条（表单区域） */
  scrollbar-width: none;
  -ms-overflow-style: none;
}

.overlay-body::-webkit-scrollbar {
  width: 0;
  height: 0;
}

.form-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 10px 12px;
  margin-bottom: 12px;
}

label {
  font-size: 12px;
  color: #9ca3af;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

input,
select,
textarea {
  border-radius: 6px;
  border: 1px solid rgba(55, 65, 81, 0.9);
  background: #020617;
  color: #e5e7eb;
  padding: 6px 8px;
  font-size: 13px;
  outline: none;
}

input:focus,
select:focus,
textarea:focus {
  border-color: rgba(96, 165, 250, 0.9);
  box-shadow: 0 0 0 1px rgba(37, 99, 235, 0.8);
}

textarea {
  resize: vertical;
}

.form-actions {
  display: flex;
  gap: 8px;
  margin-bottom: 8px;
}

.primary,
.secondary,
.danger {
  border-radius: 10px;
  border: 1px solid transparent;
  padding: 6px 12px;
  font-size: 13px;
  cursor: pointer;
  outline: none;
}

.primary {
  background: radial-gradient(circle at top, #3b82f6, #1d4ed8);
  border-color: rgba(59, 130, 246, 0.9);
  color: #f9fafb;
  box-shadow: 0 10px 35px rgba(37, 99, 235, 0.9);
}

.primary.small {
  padding: 5px 10px;
  font-size: 12px;
}

.secondary {
  background: #020617;
  border-color: rgba(75, 85, 99, 0.9);
  color: #e5e7eb;
}

.secondary.small,
.danger.small,
.tiny {
  padding: 4px 9px;
  font-size: 12px;
}

.danger {
  background: radial-gradient(circle at top, #ef4444, #b91c1c);
  border-color: rgba(248, 113, 113, 0.9);
  color: #fef2f2;
}

.tiny {
  border-radius: 8px;
}

.error {
  color: #fca5a5;
  font-size: 12px;
  margin: 2px 0 4px;
}

.error.small {
  font-size: 11px;
}

.tips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  font-size: 11px;
  color: #9ca3af;
}

.sub-panel-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 8px;
}

.sub-panel-header h3 {
  margin: 0;
  font-size: 14px;
}

.ssh-list,
.command-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-bottom: 10px;
}

.ssh-item,
.command-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 7px 9px;
  border-radius: 6px;
  border: 1px solid rgba(55, 65, 81, 0.85);
  background: #020617;
}

.ssh-title {
  display: flex;
  align-items: center;
  gap: 6px;
}

.muted {
  font-size: 11px;
  color: #6b7280;
}

.ssh-host {
  font-size: 12px;
  color: #9ca3af;
}

.ssh-actions,
.command-actions {
  display: flex;
  gap: 6px;
}

.command-text {
  font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Monaco,
    Consolas, "Liberation Mono", "Courier New", monospace;
  font-size: 11px;
  padding: 2px 6px;
  border-radius: 8px;
  background: rgba(15, 23, 42, 0.9);
  border: 1px solid rgba(55, 65, 81, 0.9);
}

.script-layout {
  display: grid;
  grid-template-columns: minmax(120px, 200px) minmax(0, 1fr);
  gap: 12px;
}

.script-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.script-item {
  border-radius: 10px;
  border: 1px solid rgba(55, 65, 81, 0.9);
  background: #020617;
  color: #e5e7eb;
  padding: 5px 10px;
  text-align: left;
  font-size: 12px;
  cursor: pointer;
}

.script-item.active {
  border-color: rgba(59, 130, 246, 0.9);
  background: radial-gradient(circle at top, #111827, #020617);
}

.script-editor {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.script-content-label textarea {
  min-height: 140px;
  font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Monaco,
    Consolas, "Liberation Mono", "Courier New", monospace;
}

.empty.small {
  font-size: 12px;
  color: #6b7280;
}

@media (max-width: 768px) {
  .app-header {
    flex-direction: column;
    height: auto;
    padding: 8px 12px;
    gap: 8px;
  }

  .app-nav {
    width: 100%;
    justify-content: flex-start;
    flex-wrap: nowrap !important;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    touch-action: pan-x;
    white-space: nowrap;
  }

   .brand {
    align-self: flex-start;
  }

  .main-full {
    padding: 4px 4px 8px;
    gap: 4px;
  }

  .overlay {
    padding: 10px;
  }
}
</style>

<style>
html,
body,
#app {
  width: 100%;
  height: 100%;
  overflow: hidden;
  overscroll-behavior: none;
}

html,
body {
  margin: 0;
  padding: 0;
  /* 禁止页面级滚动，由 .app 内部负责布局 */
  height: 100%;
  overflow: hidden;
  scrollbar-width: none;
  -ms-overflow-style: none;
}

html::-webkit-scrollbar,
body::-webkit-scrollbar {
  width: 0;
  height: 0;
}

/* xterm 终端滚动：移动端更顺滑 + 细窄主题滚动条 */
.xterm-viewport {
  overflow-y: auto !important;
  overflow-x: hidden !important;
  scrollbar-width: thin; /* Firefox */
  scrollbar-color: rgba(56, 189, 248, 0.55) rgba(2, 6, 23, 0.65);
  -ms-overflow-style: auto;
  overscroll-behavior: contain;
  touch-action: pan-y;
  -webkit-overflow-scrolling: touch;
}

.xterm-viewport::-webkit-scrollbar {
  width: 6px;
  height: 6px;
}

.xterm-viewport::-webkit-scrollbar-track {
  background: rgba(2, 6, 23, 0.55);
  border-left: 1px solid rgba(15, 23, 42, 0.75);
}

.xterm-viewport::-webkit-scrollbar-thumb {
  border-radius: 999px;
  background: linear-gradient(
    to bottom,
    rgba(56, 189, 248, 0.6),
    rgba(59, 130, 246, 0.5)
  );
}

.xterm-viewport::-webkit-scrollbar-thumb:hover {
  background: linear-gradient(
    to bottom,
    rgba(103, 232, 249, 0.75),
    rgba(96, 165, 250, 0.65)
  );
}
</style>
