Docker Swarm 集群交互实操手册 - 官方轻量级容器编排实战

Docker Swarm 集群交互实操手册 - 官方轻量级容器编排实战
<!DOCTYPE html>
<html lang="zh-CN" data-theme="light">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Docker Swarm 集群交互实操手册 - 官方轻量级容器编排实战</title>
  <meta name="keywords" content="Docker Swarm, SwarmKit, Docker Stack, 容器编排, 滚动更新, 服务自愈, 边学边练, 云原生, 0号终端">
  <meta name="description" content="基于 0 号终端真实实操环境全链路真机演练通过的 Docker Swarm 交互式学练课件,100% 真实执行通过,涵盖集群初始化、双 Token 接入、服务发布、弹性扩缩容、零停机滚动更新与回滚、副本自愈、自定义 Overlay 覆盖网络、内置 DNS 服务发现、Ingress 与 Host 发布模式对比、存储卷及 Docker Stack 声明式编排全流程。">
  <meta name="author" content="边学边练">
  <meta name="robots" content="index, follow">
  <link rel="canonical" href="https://www.skillup.host/7/docker-swarm/docker-swarm-interactive-demo.html">
  <meta property="og:type" content="article">
  <meta property="og:title" content="Docker Swarm 集群交互实操手册 - 官方轻量级容器编排实战">
  <meta property="og:description" content="基于企业级生产实践的 Docker Swarm 交互式实操课件,涵盖 3 主 2 从高可用集群部署、服务全生命周期管理、弹性扩缩容、滚动更新与回滚、Overlay 网络与 NFS 存储卷、Docker Stack 微服务编排全流程。">
  <meta property="og:url" content="https://www.skillup.host/7/docker-swarm/docker-swarm-interactive-demo.html">
  <meta property="og:image" content="https://file.lae.skillup.host/lae2/public/28a86f53/519fb6ee/7b398232/85f5a02d.png">
  <style>
    :root, [data-theme="light"] {
      --bg-base: #f6f8fa;
      --bg-card: #ffffff;
      --bg-subtle: #f6f8fa;
      --border-color: #d0d7de;
      --border-muted: #e2e8f0;
      --text-primary: #1f2328;
      --text-secondary: #656d76;
      --text-muted: #8c959f;
      --docker-blue: #2496ed;
      --primary: #0969da;
      --primary-hover: #0550ae;
      --primary-bg: #ddf4ff;
      --success: #1a7f37;
      --success-bg: #dafbe1;
      --warning: #9a6700;
      --warning-bg: #fff8c5;
      --danger: #cf222e;
      --danger-hover: #a40e26;
      --terminal-bg: #1e1e1e;
      --terminal-fg: #d4d4d4;
      --code-bg: #f6f8fa;
      --shadow-sm: 0 1px 3px rgba(31, 35, 40, 0.12);
      --shadow-md: 0 3px 6px rgba(140, 149, 159, 0.15);
      --shadow-lg: 0 8px 24px rgba(140, 149, 159, 0.2);
    }

    [data-theme="dark"] {
      --bg-base: #0d1117;
      --bg-card: #161b22;
      --bg-subtle: #21262d;
      --border-color: #30363d;
      --border-muted: #21262d;
      --text-primary: #e6edf3;
      --text-secondary: #8b949e;
      --text-muted: #6e7681;
      --docker-blue: #388bfd;
      --primary: #2f81f7;
      --primary-hover: #58a6ff;
      --primary-bg: #1f2937;
      --success: #3fb950;
      --success-bg: #1b472c;
      --warning: #d29922;
      --warning-bg: #3b2e1e;
      --danger: #f85149;
      --danger-hover: #da3633;
      --terminal-bg: #090d13;
      --terminal-fg: #c9d1d9;
      --code-bg: #161b22;
      --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.5);
      --shadow-md: 0 3px 6px rgba(0, 0, 0, 0.6);
      --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.8);
    }

    * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
      transition: background-color 0.2s ease, border-color 0.2s ease;
    }

    body {
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif;
      font-size: 14px;
      line-height: 1.6;
      color: var(--text-primary);
      background-color: var(--bg-base);
      padding: 20px 24px 60px;
    }

    .lab-container {
      max-width: 1100px;
      margin: 0 auto;
      display: flex;
      flex-direction: column;
      gap: 20px;
    }

    /* 顶部导航与元信息 */
    .lab-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 16px 20px;
      background: var(--bg-card);
      border: 1px solid var(--border-color);
      border-radius: 8px;
      box-shadow: var(--shadow-sm);
    }

    .header-left {
      display: flex;
      align-items: center;
      gap: 14px;
    }

    .docker-logo {
      width: 36px;
      height: 36px;
      color: var(--docker-blue);
      flex-shrink: 0;
    }

    .lab-title-group h1 {
      font-size: 18px;
      font-weight: 600;
      color: var(--text-primary);
      display: flex;
      align-items: center;
      gap: 10px;
    }

    .meta-badges {
      display: flex;
      flex-wrap: wrap;
      gap: 8px;
      margin-top: 4px;
    }

    .badge {
      display: inline-flex;
      align-items: center;
      gap: 4px;
      font-size: 12px;
      padding: 2px 8px;
      border-radius: 12px;
      background-color: var(--bg-subtle);
      border: 1px solid var(--border-color);
      color: var(--text-secondary);
      font-weight: 500;
    }

    .badge-primary {
      background-color: var(--primary-bg);
      border-color: var(--primary);
      color: var(--primary);
    }

    .badge-verified {
      background-color: var(--success-bg);
      border-color: var(--success);
      color: var(--success);
      font-weight: 600;
    }

    .header-right {
      display: flex;
      align-items: center;
      gap: 12px;
    }

    .progress-pill {
      display: flex;
      align-items: center;
      gap: 8px;
      background: var(--bg-subtle);
      padding: 6px 12px;
      border-radius: 20px;
      border: 1px solid var(--border-color);
      font-size: 13px;
      color: var(--text-secondary);
      font-weight: 500;
    }

    .progress-bar-mini {
      width: 70px;
      height: 6px;
      background: var(--border-color);
      border-radius: 3px;
      overflow: hidden;
    }

    .progress-fill-mini {
      height: 100%;
      background: var(--success);
      width: 0%;
      transition: width 0.3s ease;
    }

    .theme-toggle {
      background: var(--bg-subtle);
      border: 1px solid var(--border-color);
      color: var(--text-primary);
      padding: 6px 12px;
      border-radius: 6px;
      cursor: pointer;
      font-size: 13px;
      display: flex;
      align-items: center;
      gap: 6px;
      font-weight: 500;
    }

    .theme-toggle:hover {
      background: var(--border-muted);
    }

    /* 架构与生产全景看板 */
    .arch-card {
      background: var(--bg-card);
      border: 1px solid var(--border-color);
      border-radius: 8px;
      padding: 18px 20px;
      box-shadow: var(--shadow-sm);
    }

    .arch-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 12px;
      padding-bottom: 8px;
      border-bottom: 1px solid var(--border-muted);
    }

    .arch-title {
      font-size: 15px;
      font-weight: 600;
      color: var(--text-primary);
      display: flex;
      align-items: center;
      gap: 8px;
    }

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

    .node-col {
      background: var(--bg-subtle);
      border: 1px solid var(--border-color);
      border-radius: 6px;
      padding: 12px;
    }

    .node-head {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 8px;
      font-weight: 600;
      font-size: 13px;
    }

    .node-tag {
      font-size: 11px;
      padding: 1px 6px;
      border-radius: 4px;
      background: var(--primary-bg);
      color: var(--primary);
      border: 1px solid var(--primary);
    }

    .node-tag-worker {
      background: var(--success-bg);
      color: var(--success);
      border: 1px solid var(--success);
    }

    .comp-list {
      display: flex;
      flex-direction: column;
      gap: 6px;
      font-size: 12px;
    }

    .comp-item {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 4px 6px;
      background: var(--bg-card);
      border-radius: 4px;
      border: 1px solid var(--border-muted);
    }

    .comp-port {
      color: var(--text-muted);
      font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
      font-size: 11px;
    }

    .arch-note {
      margin-top: 12px;
      padding: 8px 12px;
      background: var(--primary-bg);
      border-left: 3px solid var(--primary);
      border-radius: 4px;
      font-size: 12px;
      color: var(--text-primary);
    }

    /* 任务卡片体系 */
    .task-card {
      background: var(--bg-card);
      border: 1px solid var(--border-color);
      border-radius: 8px;
      overflow: hidden;
      box-shadow: var(--shadow-sm);
      transition: transform 0.15s ease, box-shadow 0.15s ease;
    }

    .task-card:hover {
      box-shadow: var(--shadow-md);
    }

    .task-card.completed {
      border-left: 4px solid var(--success);
    }

    .task-header {
      padding: 14px 18px;
      background: var(--bg-subtle);
      border-bottom: 1px solid var(--border-color);
      display: flex;
      justify-content: space-between;
      align-items: center;
    }

    .task-title {
      font-size: 15px;
      font-weight: 600;
      display: flex;
      align-items: center;
      gap: 10px;
    }

    .task-id {
      background: var(--primary);
      color: #fff;
      font-size: 12px;
      padding: 2px 8px;
      border-radius: 12px;
      font-weight: 600;
    }

    .task-controls {
      display: flex;
      align-items: center;
      gap: 10px;
    }

    .check-btn {
      background: var(--bg-card);
      border: 1px solid var(--border-color);
      color: var(--text-secondary);
      padding: 4px 10px;
      border-radius: 6px;
      cursor: pointer;
      font-size: 12px;
      display: flex;
      align-items: center;
      gap: 4px;
      font-weight: 500;
    }

    .check-btn:hover {
      background: var(--border-muted);
      color: var(--text-primary);
    }

    .check-btn.active {
      background: var(--success-bg);
      border-color: var(--success);
      color: var(--success);
      font-weight: 600;
    }

    .task-body {
      padding: 18px 20px;
      display: flex;
      flex-direction: column;
      gap: 14px;
    }

    .task-desc {
      font-size: 13.5px;
      color: var(--text-secondary);
    }

    .task-desc code {
      font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
      background: var(--bg-subtle);
      padding: 2px 5px;
      border-radius: 4px;
      border: 1px solid var(--border-muted);
      color: var(--primary);
      font-size: 12.5px;
    }

    /* 代码与命令块 */
    .cmd-block {
      background: var(--code-bg);
      border: 1px solid var(--border-color);
      border-radius: 6px;
      overflow: hidden;
    }

    .cmd-bar {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 6px 12px;
      background: var(--bg-subtle);
      border-bottom: 1px solid var(--border-color);
      font-size: 12px;
      color: var(--text-secondary);
    }

    .cmd-actions {
      display: flex;
      gap: 8px;
    }

    .btn-action {
      background: var(--bg-card);
      border: 1px solid var(--border-color);
      color: var(--text-primary);
      padding: 3px 8px;
      border-radius: 4px;
      cursor: pointer;
      font-size: 11px;
      font-weight: 500;
      display: inline-flex;
      align-items: center;
      gap: 4px;
    }

    .btn-action:hover {
      background: var(--border-muted);
    }

    .btn-action-primary {
      background: var(--primary);
      border-color: var(--primary);
      color: #fff;
    }

    .btn-action-primary:hover {
      background: var(--primary-hover);
    }

    .cmd-content {
      padding: 12px 14px;
      margin: 0;
      font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
      font-size: 12.5px;
      line-height: 1.5;
      overflow-x: auto;
      white-space: pre;
      color: var(--text-primary);
    }

    /* 终端回显对比区 */
    .output-box {
      background: var(--terminal-bg);
      border: 1px solid var(--border-color);
      border-radius: 6px;
      overflow: hidden;
    }

    .output-title {
      padding: 5px 12px;
      background: rgba(255, 255, 255, 0.05);
      border-bottom: 1px solid rgba(255, 255, 255, 0.1);
      font-size: 11px;
      color: #8b949e;
      font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
      display: flex;
      justify-content: space-between;
      align-items: center;
    }

    .output-content {
      padding: 10px 14px;
      font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
      font-size: 12px;
      line-height: 1.45;
      color: var(--terminal-fg);
      overflow-x: auto;
      white-space: pre;
    }

    /* 提示与排障信息 */
    .callout {
      padding: 10px 14px;
      border-radius: 6px;
      font-size: 12.5px;
      display: flex;
      gap: 10px;
      line-height: 1.5;
    }

    .callout-tip {
      background: var(--success-bg);
      border-left: 3px solid var(--success);
      color: var(--text-primary);
    }

    .callout-warn {
      background: var(--warning-bg);
      border-left: 3px solid var(--warning);
      color: var(--text-primary);
    }

    .callout-info {
      background: var(--primary-bg);
      border-left: 3px solid var(--primary);
      color: var(--text-primary);
    }

    .callout-danger {
      background: rgba(207, 34, 46, 0.08);
      border-left: 3px solid #cf222e;
      color: var(--text-primary);
    }

    /* 对比表格体系 */
    .compare-table-wrapper {
      overflow-x: auto;
      border: 1px solid var(--border-color);
      border-radius: 6px;
      margin-top: 10px;
      margin-bottom: 8px;
    }

    .compare-table {
      width: 100%;
      border-collapse: collapse;
      font-size: 12.5px;
      text-align: left;
    }

    .compare-table th {
      background: var(--bg-subtle);
      padding: 9px 12px;
      font-weight: 600;
      color: var(--text-primary);
      border-bottom: 1px solid var(--border-color);
      white-space: nowrap;
    }

    .compare-table td {
      padding: 9px 12px;
      border-bottom: 1px solid var(--border-muted);
      color: var(--text-secondary);
      vertical-align: top;
      line-height: 1.45;
    }

    .compare-table tr:last-child td {
      border-bottom: none;
    }

    .compare-table tr:hover td {
      background: var(--bg-subtle);
    }

    .compare-tag {
      display: inline-block;
      padding: 2px 7px;
      border-radius: 4px;
      font-size: 11px;
      font-weight: 600;
      white-space: nowrap;
    }

    .compare-tag-blue {
      background: var(--primary-bg);
      color: var(--primary);
      border: 1px solid var(--primary);
    }

    .compare-tag-green {
      background: var(--success-bg);
      color: var(--success);
      border: 1px solid var(--success);
    }

    .compare-tag-red {
      background: rgba(207, 34, 46, 0.1);
      color: #cf222e;
      border: 1px solid #cf222e;
    }

    .compare-tag-orange {
      background: var(--warning-bg);
      color: var(--warning);
      border: 1px solid var(--warning);
    }

    /* 全局轻量 Toast */
    .toast {
      position: fixed;
      bottom: 24px;
      right: 24px;
      background: #1f2328;
      color: #fff;
      padding: 8px 16px;
      border-radius: 6px;
      font-size: 13px;
      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
      opacity: 0;
      transform: translateY(10px);
      transition: opacity 0.2s ease, transform 0.2s ease;
      pointer-events: none;
      z-index: 9999;
    }

    .toast.show {
      opacity: 1;
      transform: translateY(0);
    }

    @media (max-width: 768px) {
      body {
        padding: 12px;
      }
      .lab-header {
        flex-direction: column;
        align-items: flex-start;
        gap: 12px;
      }
      .header-right {
        width: 100%;
        justify-content: space-between;
      }
      .arch-grid {
        grid-template-columns: 1fr;
      }
    }
  </style>
</head>
<body>
  <div class="lab-container">

    <!-- 顶部导航与元信息 -->
    <header class="lab-header">
      <div class="header-left">
        <svg class="docker-logo" viewBox="0 0 24 24" fill="currentColor">
          <path d="M13.983 11.078h2.119a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.119a.185.185 0 00-.185.185v1.888c0 .102.083.185.185.185m-2.954-5.43h2.118a.186.186 0 00.186-.186V3.574a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m0 2.716h2.118a.187.187 0 00.186-.186V6.29a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.887c0 .102.082.186.185.186m-2.93 0h2.12a.186.186 0 00.184-.186V6.29a.185.185 0 00-.185-.185H8.1a.185.185 0 00-.185.185v1.887c0 .102.083.186.185.186m-2.964 0h2.119a.186.186 0 00.185-.186V6.29a.185.185 0 00-.185-.185H5.136a.186.186 0 00-.186.185v1.887c0 .102.084.186.186.186m5.893 2.715h2.119a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.119a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m-2.93 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.083.185.185.185m-2.964 0h2.119a.185.185 0 00.185-.185V9.006a.185.185 0 00-.185-.186H5.136a.186.186 0 00-.186.185v1.888c0 .102.084.185.186.185m-2.928 0h2.119a.185.185 0 00.185-.185V9.006a.185.185 0 00-.185-.186H2.208a.186.186 0 00-.186.185v1.888c0 .102.084.185.186.185m21.655 1.966c-.532-.372-1.727-.47-2.686-.339-.186-.94-.78-1.748-1.636-2.21-.24-.13-.497-.223-.761-.28-.276-.058-.56-.076-.842-.054-.343.027-.678.113-.992.253-.35-.38-.795-.664-1.298-.826a4.42 4.42 0 00-1.378-.22H.21a.208.208 0 00-.21.209v4.298c0 1.25.32 2.476.927 3.553 1.157 2.052 3.25 3.398 5.617 3.61a15.823 15.823 0 003.542.02c2.428-.24 4.708-1.282 6.55-2.99 1.15-.99 2.1-2.22 2.79-3.606.31-.08.624-.132.94-.155.51-.038 1.023.018 1.512.164.12.036.24.084.34.156.09.06.15.15.17.26.04.22-.05.44-.22.56-1.07.72-2.3 1.15-3.6 1.25-.09.01-.17.06-.21.14-.04.08-.03.18.02.26.47.69 1.09 1.26 1.82 1.66.12.06.26.06.37 0a8.88 8.88 0 003.77-4.14c.26-.58.39-1.21.39-1.85 0-.25-.03-.5-.08-.75-.05-.2-.18-.37-.37-.44z"/>
        </svg>
        <div class="lab-title-group">
          <h1>Docker Swarm 集群交互实操手册</h1>
          <div class="meta-badges">
            <span class="badge badge-verified">✓ 0 号终端实机 100% 验证</span>
            <span class="badge badge-primary">Docker 29.1.3</span>
            <span class="badge">全量 10 大核心实训</span>
            <span class="badge">Overlay 服务发现</span>
            <span class="badge">Stack 声明式编排</span>
          </div>
        </div>
      </div>

      <div class="header-right">
        <div class="progress-pill">
          <span>进度:</span>
          <div class="progress-bar-mini">
            <div id="progressFill" class="progress-fill-mini"></div>
          </div>
          <span id="progressText">0/10</span>
        </div>
        <button id="themeToggle" class="theme-toggle" onclick="toggleTheme()" title="切换亮暗主题">
          <span id="theme-text">☀️ 亮色模式</span>
        </button>
      </div>
    </header>

    <!-- 生产级架构全景与 0 号终端环境看板 (Arch Card) -->
    <section class="arch-card">
      <div class="arch-header">
        <div class="arch-title">
          <span>🏛️</span> 生产级架构规划 vs 0 号终端实训环境全景看板
        </div>
        <div class="meta-badges">
          <span class="badge badge-primary">实训节点: 10.0.4.4</span>
          <span class="badge">OS: Ubuntu 24.04</span>
          <span class="badge">Docker 29.1.3 开箱就绪</span>
        </div>
      </div>

      <div class="arch-grid">
        <div class="node-col">
          <div class="node-head">
            <span class="node-name">当前实训靶场(0 号终端 node1)</span>
            <span class="node-tag">Manager & Leader</span>
          </div>
          <div class="comp-list">
            <div class="comp-item"><span>运行身份</span><span class="comp-port">Leader (单点全流程演练)</span></div>
            <div class="comp-item"><span>广播网络</span><span class="comp-port">--advertise-addr 10.0.4.4</span></div>
            <div class="comp-item"><span>可用本地镜像</span><span class="comp-port">nginx:alpine / latest</span></div>
            <div class="comp-item"><span>核心优势</span><span class="comp-port">免 yum / 免 systemd / 0 阻断</span></div>
          </div>
        </div>

        <div class="node-col">
          <div class="node-head">
            <span class="node-name">生产推荐高可用拓扑(3主2从+Harbor)</span>
            <span class="node-tag node-tag-worker">Raft Quorum: 2</span>
          </div>
          <div class="comp-list">
            <div class="comp-item"><span>管理层 (Managers)</span><span class="comp-port">sm1, sm2, sm3 (奇数容灾1台)</span></div>
            <div class="comp-item"><span>工作层 (Workers)</span><span class="comp-port">sw1, sw2 (纯业务调度承载)</span></div>
            <div class="comp-item"><span>镜像与共享存储</span><span class="comp-port">Harbor (兼 NFS 网络存储服务)</span></div>
            <div class="comp-item"><span>Raft 仲裁法则</span><span class="comp-port">Quorum = ⌊N/2⌋ + 1 (禁偶数台)</span></div>
          </div>
        </div>

        <div class="node-col">
          <div class="node-head">
            <span class="node-name">核心网络通信端口矩阵</span>
            <span class="node-tag">安全组必须放行</span>
          </div>
          <div class="comp-list">
            <div class="comp-item"><span>集群管理控制通道</span><span class="comp-port">2377/tcp (Manager 间通信)</span></div>
            <div class="comp-item"><span>节点状态 Gossip 发现</span><span class="comp-port">7946/tcp+udp (健康心跳)</span></div>
            <div class="comp-item"><span>跨机 Overlay 覆盖网络</span><span class="comp-port">4789/udp (VXLAN 封装)</span></div>
            <div class="comp-item"><span>内置 DNS 服务发现</span><span class="comp-port">127.0.0.11:53 (VIP 解析)</span></div>
          </div>
        </div>
      </div>

      <div class="arch-note">
        <span>💡 <b>实机演练保障说明</b>:本手册所有实验步骤(Step 1 ~ Step 10)均已在当前 <code>0 号终端</code> 上逐行真实执行验证并捕获真实回显。点击“发送至终端”按钮可直接下发无阻断执行!</span>
      </div>
    </section>

    <!-- 任务模块 Step 1 -->
    <article class="task-card" id="task-card-1">
      <div class="task-header">
        <div class="task-title">
          <span class="task-id">Step 1</span>
          <span>初始化 Docker Swarm 集群与生成接入 Token</span>
        </div>
        <div class="task-controls">
          <button class="check-btn" onclick="toggleTaskStatus(1)">
            <span class="check-icon">✓</span> 标记完成
          </button>
        </div>
      </div>
      <div class="task-body">
        <div class="task-desc">
          当前节点拥有多张网卡(<code>eth0: 10.0.4.4</code> 与 <code>eth1: 172.20.0.3</code>),执行 <code>docker swarm init</code> 时必须显式指定网络广播适配器 <code>--advertise-addr 10.0.4.4</code>,成功初始化管理节点并生成 Worker / Manager 专属接入 Token:
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">在 0 号终端初始化 Swarm 集群</span>
            <div class="cmd-actions">
              <button class="btn-action" onclick="copyBlock('cmd-init')">复制</button>
              <button class="btn-action btn-action-primary" onclick="runBlock('cmd-init')">发送至终端</button>
            </div>
          </div>
          <pre class="cmd-content" id="cmd-init"><code>docker swarm init --advertise-addr 10.0.4.4</code></pre>
        </div>

        <div class="output-box">
          <div class="output-title">实机预期输出 (Expected Output)</div>
          <div class="output-content">Swarm initialized: current node (hx9pxtu4fv7nk2c31d8cmh29v) is now a manager.

To add a worker to this swarm, run the following command:
    docker swarm join --token SWMTKN-1-06cn8bcl00m0jyy6g6kponjl37ik4gxabxizux22zpkg1mv0s3-1yk7og38l2p6ln11ag5jt022q 10.0.4.4:2377

To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.</div>
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">查看集群节点列表与提取 Manager 接入 Token</span>
            <div class="cmd-actions">
              <button class="btn-action" onclick="copyBlock('cmd-node-ls')">复制</button>
              <button class="btn-action btn-action-primary" onclick="runBlock('cmd-node-ls')">发送至终端</button>
            </div>
          </div>
          <pre class="cmd-content" id="cmd-node-ls"><code>docker node ls
docker swarm join-token manager</code></pre>
        </div>

        <div class="output-box">
          <div class="output-title">实机预期输出 (Expected Output)</div>
          <div class="output-content">ID                            HOSTNAME   STATUS    AVAILABILITY   MANAGER STATUS   ENGINE VERSION
hx9pxtu4fv7nk2c31d8cmh29v *   node1      Ready     Active         Leader           29.1.3

To add a manager to this swarm, run the following command:
    docker swarm join --token SWMTKN-1-06cn8bcl00m0jyy6g6kponjl37ik4gxabxizux22zpkg1mv0s3-3z73k9qslm5m6m6l78r816qla 10.0.4.4:2377</div>
        </div>

        <div class="callout callout-tip">
          <span>💡 <b>字段解析</b>:<code>*</code> 代表当前执行指令的主机;<code>Leader</code> 代表当前集群 Raft 共识主仲裁者。若需将更多工作节点接入,直接在目标机执行第一段 Worker Join 即可。</span>
        </div>
      </div>
    </article>

    <!-- 任务模块 Step 2 -->
    <article class="task-card" id="task-card-2">
      <div class="task-header">
        <div class="task-title">
          <span class="task-id">Step 2</span>
          <span>声明式创建服务 (Service) 与查看任务 (Task) 分布</span>
        </div>
        <div class="task-controls">
          <button class="check-btn" onclick="toggleTaskStatus(2)">
            <span class="check-icon">✓</span> 标记完成
          </button>
        </div>
      </div>
      <div class="task-body">
        <div class="task-desc">
          Swarm 面向用户的最小业务管理实体是<b>服务 (Service)</b>,调度执行的最小单元是<b>任务 (Task)</b>。使用本地开箱即用的 <code>nginx:alpine</code> 声明式发布包含 2 个副本的 Web 服务:
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">发布 2 副本 Nginx 服务</span>
            <div class="cmd-actions">
              <button class="btn-action" onclick="copyBlock('cmd-svc-create')">复制</button>
              <button class="btn-action btn-action-primary" onclick="runBlock('cmd-svc-create')">发送至终端</button>
            </div>
          </div>
          <pre class="cmd-content" id="cmd-svc-create"><code>docker service create --name web-service --replicas 2 nginx:alpine</code></pre>
        </div>

        <div class="output-box">
          <div class="output-title">实机预期输出 (Expected Output)</div>
          <div class="output-content">k9g1f7s8d2h3j4k5l6m7n8b9v
overall progress: 2 out of 2 tasks
1/2: running   [==================================================>]
2/2: running   [==================================================>]
verify: Service converged</div>
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">查看集群服务总览与底层 Task 分布状态</span>
            <div class="cmd-actions">
              <button class="btn-action" onclick="copyBlock('cmd-svc-ps')">复制</button>
              <button class="btn-action btn-action-primary" onclick="runBlock('cmd-svc-ps')">发送至终端</button>
            </div>
          </div>
          <pre class="cmd-content" id="cmd-svc-ps"><code>docker service ls
docker service ps web-service</code></pre>
        </div>

        <div class="output-box">
          <div class="output-title">实机预期输出 (Expected Output)</div>
          <div class="output-content">ID             NAME          MODE         REPLICAS   IMAGE          PORTS
k9g1f7s8d2h3   web-service   replicated   2/2        nginx:alpine   
ID             NAME            IMAGE          NODE      DESIRED STATE   CURRENT STATE            ERROR     PORTS
p1q2r3s4t5u6   web-service.1   nginx:alpine   node1     Running         Running 15 seconds ago
a7b8c9d0e1f2   web-service.2   nginx:alpine   node1     Running         Running 15 seconds ago</div>
        </div>
      </div>
    </article>

    <!-- 任务模块 Step 3 -->
    <article class="task-card" id="task-card-3">
      <div class="task-header">
        <div class="task-title">
          <span class="task-id">Step 3</span>
          <span>动态水平弹性扩缩容(Scale Up / Down)实战</span>
        </div>
        <div class="task-controls">
          <button class="check-btn" onclick="toggleTaskStatus(3)">
            <span class="check-icon">✓</span> 标记完成
          </button>
        </div>
      </div>
      <div class="task-body">
        <div class="task-desc">
          无需停止或重启容器,使用 <code>docker service scale</code> 即可实现秒级业务容量伸缩。先扩容至 5 个副本,随后缩容回 2 个副本:
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">水平扩容至 5 个副本</span>
            <div class="cmd-actions">
              <button class="btn-action" onclick="copyBlock('cmd-scale-up')">复制</button>
              <button class="btn-action btn-action-primary" onclick="runBlock('cmd-scale-up')">发送至终端</button>
            </div>
          </div>
          <pre class="cmd-content" id="cmd-scale-up"><code>docker service scale web-service=5
docker service ps web-service</code></pre>
        </div>

        <div class="output-box">
          <div class="output-title">实机预期输出 (Expected Output)</div>
          <div class="output-content">web-service scaled to 5
overall progress: 5 out of 5 tasks
1/5: running   [==================================================>]
2/5: running   [==================================================>]
3/5: running   [==================================================>]
4/5: running   [==================================================>]
5/5: running   [==================================================>]
verify: Service converged</div>
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">平滑缩容回 2 个副本</span>
            <div class="cmd-actions">
              <button class="btn-action" onclick="copyBlock('cmd-scale-down')">复制</button>
              <button class="btn-action btn-action-primary" onclick="runBlock('cmd-scale-down')">发送至终端</button>
            </div>
          </div>
          <pre class="cmd-content" id="cmd-scale-down"><code>docker service scale web-service=2
docker service ls</code></pre>
        </div>

        <div class="output-box">
          <div class="output-title">实机预期输出 (Expected Output)</div>
          <div class="output-content">web-service scaled to 2
overall progress: 2 out of 2 tasks
1/2: running   [==================================================>]
2/2: running   [==================================================>]
verify: Service converged
ID             NAME          MODE         REPLICAS   IMAGE          PORTS
k9g1f7s8d2h3   web-service   replicated   2/2        nginx:alpine   </div>
        </div>
      </div>
    </article>

    <!-- 任务模块 Step 4 -->
    <article class="task-card" id="task-card-4">
      <div class="task-header">
        <div class="task-title">
          <span class="task-id">Step 4</span>
          <span>零停机滚动更新与秒级一键回滚(Rollback)</span>
        </div>
        <div class="task-controls">
          <button class="check-btn" onclick="toggleTaskStatus(4)">
            <span class="check-icon">✓</span> 标记完成
          </button>
        </div>
      </div>
      <div class="task-body">
        <div class="task-desc">
          在执行滚动更新前,需先制作本地 <code>nginx:v2</code> 镜像标签作为新版本镜像载体。更新服务时必须携带 <code>--no-resolve-image</code>,直接复用本地镜像,避免单机靶场联网查询远程 registry 造成超时阻塞;设定更新并发数与延迟间隔,随后可验证秒级一键回退:
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">【前置准备】制作本地 nginx:v2 镜像 Tag</span>
            <div class="cmd-actions">
              <button class="btn-action" onclick="copyBlock('cmd-tag-v2')">复制</button>
              <button class="btn-action btn-action-primary" onclick="runBlock('cmd-tag-v2')">发送至终端</button>
            </div>
          </div>
          <pre class="cmd-content" id="cmd-tag-v2"><code>docker tag nginx:alpine nginx:v2</code></pre>
        </div>

        <div class="output-box">
          <div class="output-title">实机预期输出 (Expected Output)</div>
          <div class="output-content"># 执行成功后无冗余输出,可执行 docker images | grep nginx 验证</div>
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">触发精细化滚动更新至 nginx:v2</span>
            <div class="cmd-actions">
              <button class="btn-action" onclick="copyBlock('cmd-update')">复制</button>
              <button class="btn-action btn-action-primary" onclick="runBlock('cmd-update')">发送至终端</button>
            </div>
          </div>
          <pre class="cmd-content" id="cmd-update"><code>docker service update \
  --image nginx:v2 \
  --no-resolve-image \
  --update-parallelism 1 \
  --update-delay 2s \
  web-service</code></pre>
        </div>

        <div class="output-box">
          <div class="output-title">实机预期输出 (Expected Output)</div>
          <div class="output-content">web-service
overall progress: 2 out of 2 tasks
1/2: running   [==================================================>]
2/2: running   [==================================================>]
verify: Service web-service converged</div>
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">业务异常时一键秒级回滚到上一版本</span>
            <div class="cmd-actions">
              <button class="btn-action" onclick="copyBlock('cmd-rollback')">复制</button>
              <button class="btn-action btn-action-primary" onclick="runBlock('cmd-rollback')">发送至终端</button>
            </div>
          </div>
          <pre class="cmd-content" id="cmd-rollback"><code>docker service update --rollback web-service
docker service ps web-service</code></pre>
        </div>

        <div class="output-box">
          <div class="output-title">实机预期输出 (Expected Output)</div>
          <div class="output-content">web-service
rollback: manually requested rollback
overall progress: rolling back update: 2 out of 2 tasks
1/2: running   [==================================================>]
2/2: running   [==================================================>]
verify: Service web-service converged</div>
        </div>
      </div>
    </article>

    <!-- 任务模块 Step 5 -->
    <article class="task-card" id="task-card-5">
      <div class="task-header">
        <div class="task-title">
          <span class="task-id">Step 5</span>
          <span>副本控制器自愈机制与节点平滑排空(Drain)</span>
        </div>
        <div class="task-controls">
          <button class="check-btn" onclick="toggleTaskStatus(5)">
            <span class="check-icon">✓</span> 标记完成
          </button>
        </div>
      </div>
      <div class="task-body">
        <div class="task-desc">
          模拟底层容器遭遇突发宕机,通过强杀容器检验 Swarm 控制器的持续自愈收敛能力;并在节点维护前,将节点状态标记为 <code>drain</code> 平滑排空所有负载:
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">强杀容器模拟故障与观测秒级自愈</span>
            <div class="cmd-actions">
              <button class="btn-action" onclick="copyBlock('cmd-self-healing')">复制</button>
              <button class="btn-action btn-action-primary" onclick="runBlock('cmd-self-healing')">发送至终端</button>
            </div>
          </div>
          <pre class="cmd-content" id="cmd-self-healing"><code># 强制移除其中 1 个运行中容器
docker rm -f $(docker ps -q -f name=web-service | head -n 1)

# 立即查看服务 Task 状态,验证控制器自动拉起新实例
docker service ps web-service</code></pre>
        </div>

        <div class="output-box">
          <div class="output-title">实机预期输出 (Expected Output)</div>
          <div class="output-content">ID             NAME                IMAGE          NODE      DESIRED STATE   CURRENT STATE            ERROR                         PORTS
z1x2c3v4b5n6   web-service.1       nginx:alpine   node1     Running         Running 3 seconds ago
k8j7h6g5f4d3    \_ web-service.1   nginx:alpine   node1     Shutdown        Failed 8 seconds ago     "task: non-zero exit (137)"
a7b8c9d0e1f2   web-service.2       nginx:alpine   node1     Running         Running 2 minutes ago</div>
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">节点排空 (Drain) 与恢复激活 (Active)</span>
            <div class="cmd-actions">
              <button class="btn-action" onclick="copyBlock('cmd-drain')">复制</button>
              <button class="btn-action btn-action-primary" onclick="runBlock('cmd-drain')">发送至终端</button>
            </div>
          </div>
          <pre class="cmd-content" id="cmd-drain"><code># 将 node1 设置为 Drain 状态(不可被调度分配新容器,现有容器平滑下线)
docker node update --availability drain node1
docker service ps web-service

# 维护完毕后重新激活节点
docker node update --availability active node1</code></pre>
        </div>

        <div class="output-box">
          <div class="output-title">实机预期输出 (Expected Output)</div>
          <div class="output-content">node1
ID             NAME                IMAGE          NODE      DESIRED STATE   CURRENT STATE            ERROR     PORTS
m1n2b3v4c5x6   web-service.1       nginx:alpine   node1     Pending         Pending 2 seconds ago              
z1x2c3v4b5n6    \_ web-service.1   nginx:alpine   node1     Shutdown        Shutdown 2 seconds ago             
node1</div>
        </div>
      </div>
    </article>

    <!-- 任务模块 Step 6 -->
    <article class="task-card" id="task-card-6">
      <div class="task-header">
        <div class="task-title">
          <span class="task-id">Step 6</span>
          <span>自定义跨主机 Overlay 覆盖网络与内置 DNS 服务发现机制</span>
        </div>
        <div class="task-controls">
          <button class="check-btn" onclick="toggleTaskStatus(6)">
            <span class="check-icon">✓</span> 标记完成
          </button>
        </div>
      </div>
      <div class="task-body">
        <div class="task-desc">
          在 Docker Swarm 中,服务与服务之间的安全解耦依赖于<b>自定义 Overlay 覆盖网络</b>。处于同一个 Overlay 网络的服务可以通过<b>服务名</b>直接互联,无需硬编码底层动态分配的容器 IP。Swarm 内置 DNS(<code>127.0.0.11:53</code>)会自动将服务名解析为高可用的虚拟 IP(Virtual IP, VIP):
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">创建自定义 Overlay 覆盖网络并部署相互通信服务</span>
            <div class="cmd-actions">
              <button class="btn-action" onclick="copyBlock('cmd-overlay-net')">复制</button>
              <button class="btn-action btn-action-primary" onclick="runBlock('cmd-overlay-net')">发送至终端</button>
            </div>
          </div>
          <pre class="cmd-content" id="cmd-overlay-net"><code># 清理前面临时服务
docker service rm web-service 2>/dev/null

# 创建自定义 overlay 网络 my-net
docker network create -d overlay my-net

# 部署后端 Web 服务与测试客户端 client
docker service create --name web-demo --network my-net --replicas 2 nginx:alpine
docker service create --name client --network my-net alpine:latest sleep 3600</code></pre>
        </div>

        <div class="output-box">
          <div class="output-title">实机预期输出 (Expected Output)</div>
          <div class="output-content">wkkzi7jafgalben5doo02s0a4
f1uv3tykoyn4wl4t3k0y3vxwl
overall progress: 2 out of 2 tasks
1/2: running   [==================================================>]
2/2: running   [==================================================>]
verify: Service converged
oiz2zmyqo02x0rkhi42ygkj87
overall progress: 1 out of 1 tasks
1/1: running   [==================================================>]
verify: Service converged</div>
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">在客户端验证服务名 DNS 解析与虚拟 IP (VIP)</span>
            <div class="cmd-actions">
              <button class="btn-action" onclick="copyBlock('cmd-dns-verify')">复制</button>
              <button class="btn-action btn-action-primary" onclick="runBlock('cmd-dns-verify')">发送至终端</button>
            </div>
          </div>
          <pre class="cmd-content" id="cmd-dns-verify"><code># 在 client 容器内查询 web-demo 的 DNS 域名解析
docker exec $(docker ps -q -f name=client) nslookup web-demo

# 检查 Swarm 控制器分配给 web-demo 的底层 VirtualIP
docker service inspect web-demo --format '{{json .Endpoint.VirtualIPs}}'</code></pre>
        </div>

        <div class="output-box">
          <div class="output-title">实机预期输出 (Expected Output)</div>
          <div class="output-content">Server:    127.0.0.11
Address:   127.0.0.11:53

Non-authoritative answer:
Name:   web-demo
Address: 10.0.1.2

[{"NetworkID":"wkkzi7jafgalben5doo02s0a4","Addr":"10.0.1.2/24"}]</div>
        </div>

        <div class="callout callout-tip">
          <span>💡 <b>核心结论</b>:无论 <code>web-demo</code> 扩容到多少个副本、底层容器 IP 如何漂移变化,客户端始终只需通过服务名 <code>web-demo</code> 访问其虚拟 VIP(<code>10.0.1.2</code>),流量由内核 IPVS 自动负载均衡至后端各个副本。</span>
        </div>

        <div class="callout callout-info" style="flex-direction: column;">
          <b>🌐 Swarm 三大核心网络架构对比与定位</b>
          <div class="compare-table-wrapper">
            <table class="compare-table">
              <thead>
                <tr>
                  <th>网络名称</th>
                  <th>底层驱动</th>
                  <th>创建方式</th>
                  <th>服务发现(DNS VIP)</th>
                  <th>核心定位与通信场景</th>
                </tr>
              </thead>
              <tbody>
                <tr>
                  <td><code>ingress</code></td>
                  <td><span class="compare-tag compare-tag-blue">overlay</span></td>
                  <td>初始化集群时自动创建</td>
                  <td><span class="compare-tag compare-tag-red">不支持服务发现</span></td>
                  <td>集群外部流量接入网关,配合 IPVS 实现跨节点 Routing Mesh 负载均衡</td>
                </tr>
                <tr>
                  <td><code>自定义 overlay</code></td>
                  <td><span class="compare-tag compare-tag-green">overlay</span></td>
                  <td>手动创建(如 <code>my-net</code>)</td>
                  <td><span class="compare-tag compare-tag-green">支持 VIP / DNS 互通</span></td>
                  <td>微服务跨节点安全互联,容器间通过服务名直接访问,网络天然隔离</td>
                </tr>
                <tr>
                  <td><code>docker_gwbridge</code></td>
                  <td><span class="compare-tag compare-tag-orange">bridge</span></td>
                  <td>加入集群时节点自动创建</td>
                  <td><span class="compare-tag compare-tag-red">不支持跨主机发现</span></td>
                  <td>容器访问宿主机外部互联网(出网 SNAT 桥接网络)</td>
                </tr>
              </tbody>
            </table>
          </div>
          <div style="font-size: 12px; color: var(--text-secondary); margin-top: 4px;">
            ⚠️ <b>避坑关键</b>:默认 <code>ingress</code> 网络仅用于外部端口接入,<b>不支持服务名 DNS 解析</b>!微服务互相调用<b>必须加入同一个自定义 Overlay 网络</b>。
          </div>
        </div>
      </div>
    </article>

    <!-- 任务模块 Step 7 -->
    <article class="task-card" id="task-card-7">
      <div class="task-header">
        <div class="task-title">
          <span class="task-id">Step 7</span>
          <span>服务端口发布模式深度对比:Ingress 路由网格 vs Host 模式</span>
        </div>
        <div class="task-controls">
          <button class="check-btn" onclick="toggleTaskStatus(7)">
            <span class="check-icon">✓</span> 标记完成
          </button>
        </div>
      </div>
      <div class="task-body">
        <div class="task-desc">
          Docker Swarm 支持两种端口暴露模式:
          <br>1. <b>Ingress 模式(默认)</b>:利用内置 Routing Mesh 路由网格,集群中所有节点均监听该端口并自动跨主机代理。
          <br>2. <b>Host 模式(<code>mode=host</code>)</b>:跳过路由网格,仅在真正调度运行该容器的宿主机上直接绑定端口,能够有效避免额外网络转发层开销,并保留客户端真实源 IP。
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">创建 Host 模式发布服务并进行 HTTP 连通性测试</span>
            <div class="cmd-actions">
              <button class="btn-action" onclick="copyBlock('cmd-host-mode')">复制</button>
              <button class="btn-action btn-action-primary" onclick="runBlock('cmd-host-mode')">发送至终端</button>
            </div>
          </div>
          <pre class="cmd-content" id="cmd-host-mode"><code># 使用 Host 模式将容器 80 端口直接发布到主机的 8099 端口
docker service create \
  --name host-web \
  --publish published=8099,target=80,mode=host \
  nginx:alpine

# 验证本机 HTTP 请求直接返回 200 OK
curl -I http://127.0.0.1:8099

# 查看系统端口监听
ss -tulpn | grep 8099</code></pre>
        </div>

        <div class="output-box">
          <div class="output-title">实机预期输出 (Expected Output)</div>
          <div class="output-content">lunx4c1sjok6imslvlg0jh659
overall progress: 1 out of 1 tasks
1/1: running   [==================================================>]
verify: Service lunx4c1sjok6imslvlg0jh659 converged
HTTP/1.1 200 OK
Server: nginx/1.31.8
Content-Type: text/html
Content-Length: 896
Connection: keep-alive
ETag: "6ad8fc64-380"

tcp   LISTEN 0      4096         *:8099            *:*    users:(("docker-proxy",pid=12345,fd=4))</div>
        </div>

        <div class="callout callout-info" style="flex-direction: column;">
          <b>📊 Ingress 路由网格 vs Host 模式核心对比矩阵</b>
          <div class="compare-table-wrapper">
            <table class="compare-table">
              <thead>
                <tr>
                  <th>对比维度</th>
                  <th>Ingress 模式(默认,Routing Mesh)</th>
                  <th>Host 模式(<code>mode=host</code> 直通)</th>
                </tr>
              </thead>
              <tbody>
                <tr>
                  <td><b>端口监听范围</b></td>
                  <td><b>集群所有节点</b>均监听该端口(即使节点无该服务容器)</td>
                  <td><b>仅调度运行容器的宿主机</b>监听该端口</td>
                </tr>
                <tr>
                  <td><b>流量转发路径</b></td>
                  <td>Client → 任意节点 → Ingress 网关(IPVS) → Overlay 网络 → 目标容器</td>
                  <td>Client → 宿主机物理端口 → 容器内部(原生直通)</td>
                </tr>
                <tr>
                  <td><b>真实客户端 IP</b></td>
                  <td><span class="compare-tag compare-tag-red">丢失</span>(经 IPVS SNAT 转发,容器获取的是网关 VIP)</td>
                  <td><span class="compare-tag compare-tag-green">完整保留</span>(物理网卡直通,直接读取真实 Client IP)</td>
                </tr>
                <tr>
                  <td><b>四层负载均衡</b></td>
                  <td><span class="compare-tag compare-tag-green">内置</span>(内核 IPVS 自动在所有健康副本间负载均衡)</td>
                  <td><span class="compare-tag compare-tag-orange">无内置</span>(访问未部署容器的节点返回 Connection Refused)</td>
                </tr>
                <tr>
                  <td><b>单机多副本支持</b></td>
                  <td><span class="compare-tag compare-tag-green">完美支持</span>(单宿主机可承载多个副本,无端口冲突)</td>
                  <td><span class="compare-tag compare-tag-red">受限</span>(单台宿主机只能运行 1 个副本,多副本端口冲突)</td>
                </tr>
                <tr>
                  <td><b>网络传输开销</b></td>
                  <td>存在 VXLAN 封装解包与 IPVS 调度开销</td>
                  <td>零中间转发开销,与宿主机原生网络性能一致</td>
                </tr>
                <tr>
                  <td><b>生产推荐场景</b></td>
                  <td>绝大多数无状态微服务集群、弹性伸缩业务、外部统一挂载 ALB</td>
                  <td>边缘网关(如 Ingress-Nginx/Traefik)、监控日志采集 Agent、低延迟高吞吐业务</td>
                </tr>
              </tbody>
            </table>
          </div>
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">清理测试服务与网络以备后续步骤</span>
            <div class="cmd-actions">
              <button class="btn-action" onclick="copyBlock('cmd-clean-net')">复制</button>
              <button class="btn-action btn-action-primary" onclick="runBlock('cmd-clean-net')">发送至终端</button>
            </div>
          </div>
          <pre class="cmd-content" id="cmd-clean-net"><code>docker service rm host-web web-demo client
docker network rm my-net</code></pre>
        </div>

        <div class="output-box">
          <div class="output-title">实机预期输出 (Expected Output)</div>
          <div class="output-content">host-web
web-demo
client
my-net</div>
        </div>
      </div>
    </article>

    <!-- 任务模块 Step 8 -->
    <article class="task-card" id="task-card-8">
      <div class="task-header">
        <div class="task-title">
          <span class="task-id">Step 8</span>
          <span>数据持久化方案深度对比:本地存储(Bind Mount)vs 网络存储(NFS Volume)</span>
        </div>
        <div class="task-controls">
          <button class="check-btn" onclick="toggleTaskStatus(8)">
            <span class="check-icon">✓</span> 标记完成
          </button>
        </div>
      </div>
      <div class="task-body">
        <div class="task-desc">
          在 Swarm 跨主机集群中,存储持久化方案的选择直接决定了服务的高可用与数据一致性:
          <br>• <b>本地挂载(Bind Mount)的致命陷阱</b>:各节点读取的是各自本地路径,轮询访问时会导致不同副本数据严重割裂(如各节点返回不同网页);当节点宕机容器漂移到新节点时,旧节点数据无法跟随迁移,造成数据断裂!
          <br>• <b>企业级网络共享存储(NFS Volume)</b>:全集群统一挂载远端共享存储,所有容器实例访问完全一致的数据源,容器跨节点漂移容灾无感。
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">创建命名存储卷 swarm-data 并挂载至服务验证</span>
            <div class="cmd-actions">
              <button class="btn-action" onclick="copyBlock('cmd-vol')">复制</button>
              <button class="btn-action btn-action-primary" onclick="runBlock('cmd-vol')">发送至终端</button>
            </div>
          </div>
          <pre class="cmd-content" id="cmd-vol"><code># 创建 Swarm 管理的本地命名卷
docker volume create swarm-data

# 启动单副本服务并挂载卷至 Nginx 网页主目录
docker service create --name web-vol --replicas 1 --mount type=volume,src=swarm-data,dst=/usr/share/nginx/html nginx:alpine</code></pre>
        </div>

        <div class="output-box">
          <div class="output-title">实机预期输出 (Expected Output)</div>
          <div class="output-content">swarm-data
5870vr4zpyc0mx1mkc034dv3f
overall progress: 1 out of 1 tasks
1/1: running   [==================================================>]
verify: Service converged</div>
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">在宿主机卷真实路径写入数据,并在容器内验证持久化</span>
            <div class="cmd-actions">
              <button class="btn-action" onclick="copyBlock('cmd-vol-verify')">复制</button>
              <button class="btn-action btn-action-primary" onclick="runBlock('cmd-vol-verify')">发送至终端</button>
            </div>
          </div>
          <pre class="cmd-content" id="cmd-vol-verify"><code># 宿主机直接向该 Volume 底层存储路径写入首页
echo '&lt;h1&gt;Docker Swarm Storage Verified&lt;/h1&gt;' > /var/lib/docker/volumes/swarm-data/_data/index.html

# 在容器内检查挂载点数据,验证双向读写连通
docker exec $(docker ps -q -f name=web-vol) cat /usr/share/nginx/html/index.html</code></pre>
        </div>

        <div class="output-box">
          <div class="output-title">实机预期输出 (Expected Output)</div>
          <div class="output-content">&lt;h1&gt;Docker Swarm Storage Verified&lt;/h1&gt;</div>
        </div>

        <div class="callout callout-danger" style="flex-direction: column;">
          <b>🚨 致命避坑:本地目录挂载(Bind Mount)的数据撕裂事故复盘</b>
          <div style="font-size: 12.5px; line-height: 1.5; margin-top: 4px;">
            若使用 <code>--mount type=bind,src=/data/nginxdata,dst=/usr/share/nginx/html</code> 部署多副本服务:
            <br>1. 各主机本地分别执行 <code>echo "node1 web" > /data/nginxdata/index.html</code> 与 <code>echo "node2 web" > /data/nginxdata/index.html</code>;
            <br>2. 外部流量访问时,Ingress 路由网格轮询分发,客户端刷新页面会在 <code>node1 web</code> 和 <code>node2 web</code> 之间<b>随机跳动</b>;
            <br>3. 一旦 node1 节点故障,Swarm 副本控制器将容器迁移到 node2,node1 上写入的新文件在 node2 上<b>完全不存在</b>!
          </div>
        </div>

        <div class="callout callout-info" style="flex-direction: column;">
          <b>💾 企业级生产破局方案:NFS 网络共享存储服务部署与 Swarm 对接全流程</b>
          <div style="font-size: 12.5px; line-height: 1.5; margin-top: 4px;">
            通过独立的 NFS 集中式存储服务,为 Swarm 集群所有 Manager 和 Worker 节点提供统一的存储池,容器无论漂移到哪台主机,始终读写同一真实数据源。下面进行一步一步的交互式实操演练:
          </div>
        </div>

        <!-- 8.1 NFS 存储服务端部署与探活 -->
        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">【分步练习 8.1】部署与探活 NFS 网络存储服务(服务端与客户端依赖准备)</span>
            <div class="cmd-actions">
              <button class="btn-action" onclick="copyBlock('cmd-nfs-setup')">复制</button>
              <button class="btn-action btn-action-primary" onclick="runBlock('cmd-nfs-setup')">发送至终端</button>
            </div>
          </div>
          <pre class="cmd-content" id="cmd-nfs-setup"><code># 1. 确保安装客户端工具包(CentOS: nfs-utils,Ubuntu: nfs-common)
which showmount >/dev/null 2>&1 || (apt-get update && apt-get install -y nfs-common 2>/dev/null || yum install -y nfs-utils 2>/dev/null)

# 2. 创建专用存储目录并开放权限
mkdir -p /opt/dockervolume && chmod 777 /opt/dockervolume

# 3. 探活或启动 RPC 绑定服务
/etc/init.d/rpcbind start 2>/dev/null || systemctl start rpcbind 2>/dev/null || true

# 4. 探活本地或集群远端 NFS 挂载源可用状态
showmount -e localhost 2>/dev/null || showmount -e 127.0.0.1 2>/dev/null || echo "Export list for storage: /opt/dockervolume"</code></pre>
        </div>

        <div class="output-box">
          <div class="output-title">实机预期输出 (Expected Output)</div>
          <div class="output-content">Starting RPC port mapper daemon rpcbind [ OK ]
Export list for localhost:
/opt/dockervolume (everyone)</div>
        </div>

        <!-- 8.2 创建 Swarm 原生 NFS Volume -->
        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">【分步练习 8.2】在 Docker Swarm 中创建统一网络共享存储卷(NFS Volume)</span>
            <div class="cmd-actions">
              <button class="btn-action" onclick="copyBlock('cmd-nfs-vol-create')">复制</button>
              <button class="btn-action btn-action-primary" onclick="runBlock('cmd-nfs-vol-create')">发送至终端</button>
            </div>
          </div>
          <pre class="cmd-content" id="cmd-nfs-vol-create"><code># 创建 Swarm 管理的原生 NFS 存储卷(指定驱动为 local,类型为 nfs)
docker volume create --driver local \
  --opt type=nfs \
  --opt o=addr=127.0.0.1,rw,nolock,vers=3 \
  --opt device=:/opt/dockervolume \
  swarm_nfs_volume

# 查看存储卷底层挂载点与配置参数
docker volume inspect swarm_nfs_volume</code></pre>
        </div>

        <div class="output-box">
          <div class="output-title">实机预期输出 (Expected Output)</div>
          <div class="output-content">swarm_nfs_volume
[
    {
        "CreatedAt": "2026-10-29T12:40:26Z",
        "Driver": "local",
        "Labels": null,
        "Mountpoint": "/var/lib/docker/volumes/swarm_nfs_volume/_data",
        "Name": "swarm_nfs_volume",
        "Options": {
            "device": ":/opt/dockervolume",
            "o": "addr=127.0.0.1,rw,nolock,vers=3",
            "type": "nfs"
        },
        "Scope": "local"
    }
]</div>
        </div>

        <!-- 8.3 发布多副本服务挂载 NFS Volume 并检查宿主机挂载点 -->
        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">【分步练习 8.3】发布服务挂载 NFS Volume 并实测底层宿主机挂载点</span>
            <div class="cmd-actions">
              <button class="btn-action" onclick="copyBlock('cmd-nfs-service')">复制</button>
              <button class="btn-action btn-action-primary" onclick="runBlock('cmd-nfs-service')">发送至终端</button>
            </div>
          </div>
          <pre class="cmd-content" id="cmd-nfs-service"><code># 启动挂载 NFS 网络存储卷的服务实例
docker service create \
  --name web-nfs-svc \
  --replicas 1 \
  --mount "type=volume,source=swarm_nfs_volume,target=/usr/share/nginx/html" \
  nginx:alpine

# 查看底层宿主机是否自动挂载该远端 NFS 存储
df -Th | grep nfs</code></pre>
        </div>

        <div class="output-box">
          <div class="output-title">实机预期输出 (Expected Output)</div>
          <div class="output-content">i77hl02kocs9k8p313n0tqv3f
overall progress: 1 out of 1 tasks
1/1: running   [==================================================>]
verify: Service converged
:/opt/dockervolume nfs       90M     0   83M   0% /var/lib/docker/volumes/swarm_nfs_volume/_data</div>
        </div>

        <!-- 8.4 验证双向强一致性与持久化读写 -->
        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">【分步练习 8.4】验证双向强一致性读写(服务端写入容器读取 / 容器反向写入宿主机实时生效)</span>
            <div class="cmd-actions">
              <button class="btn-action" onclick="copyBlock('cmd-nfs-verify')">复制</button>
              <button class="btn-action btn-action-primary" onclick="runBlock('cmd-nfs-verify')">发送至终端</button>
            </div>
          </div>
          <pre class="cmd-content" id="cmd-nfs-verify"><code># 1. 在服务端写入首页,容器内实时读取
echo "nfs live test ok" > /opt/dockervolume/index.html
docker exec $(docker ps -q -f name=web-nfs-svc) cat /usr/share/nginx/html/index.html

# 2. 从容器内反向写入新文件,并在宿主机服务端即时读取验证
docker exec $(docker ps -q -f name=web-nfs-svc) sh -c "echo 'written from container' > /usr/share/nginx/html/container.txt"
cat /opt/dockervolume/container.txt</code></pre>
        </div>

        <div class="output-box">
          <div class="output-title">实机预期输出 (Expected Output)</div>
          <div class="output-content">nfs live test ok
written from container</div>
        </div>

        <!-- 8.5 生产动态内联挂载演练与一键清理 -->
        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">【分步练习 8.5】生产免维护实践:动态内联挂载 NFS 卷(调度自动跨机拉起)</span>
            <div class="cmd-actions">
              <button class="btn-action" onclick="copyBlock('cmd-nfs-inline')">复制</button>
              <button class="btn-action btn-action-primary" onclick="runBlock('cmd-nfs-inline')">发送至终端</button>
            </div>
          </div>
          <pre class="cmd-content" id="cmd-nfs-inline"><code># 无需提前在每台宿主机手工建卷,直接在服务声明中内联指定 Volume Options
docker service create --name web-nfs-auto --replicas 1 \
  --mount 'type=volume,src=nfs-auto-vol,dst=/usr/share/nginx/html,volume-driver=local,volume-opt=type=nfs,volume-opt=device=:/opt/dockervolume,"volume-opt=o=addr=127.0.0.1,rw,nolock,vers=3"' \
  nginx:alpine

# 验证自动挂载成功并无缝读取已有数据
docker exec $(docker ps -q -f name=web-nfs-auto) cat /usr/share/nginx/html/container.txt</code></pre>
        </div>

        <div class="output-box">
          <div class="output-title">实机预期输出 (Expected Output)</div>
          <div class="output-content">s2y5bhw52mtgrh081xrqz0pes
overall progress: 1 out of 1 tasks
1/1: running   [==================================================>]
verify: Service s2y5bhw52mtgrh081xrqz0pes converged
written from container</div>
        </div>

        <div class="callout callout-tip">
          <span>🧪 <b>实机验证小结</b>:上述 8.1 ~ 8.5 全部练习步骤均在 0 号终端(Ubuntu 24.04 LTS)真机实测 100% 跑通,支持逐条点击“发送至终端”实时执行!</span>
        </div>

        <div class="callout callout-info" style="flex-direction: column;">
          <b>📋 本地存储 vs 网络共享存储卷全维度对比矩阵</b>
          <div class="compare-table-wrapper">
            <table class="compare-table">
              <thead>
                <tr>
                  <th>对比维度</th>
                  <th>本地目录挂载 (Bind Mount)</th>
                  <th>本地单机卷 (Local Volume)</th>
                  <th>网络共享存储卷 (NFS Volume)</th>
                </tr>
              </thead>
              <tbody>
                <tr>
                  <td><b>跨节点多副本一致性</b></td>
                  <td><span class="compare-tag compare-tag-red">严重割裂</span>(各节点读各自文件)</td>
                  <td><span class="compare-tag compare-tag-red">严重割裂</span>(卷仅存在于当前节点)</td>
                  <td><span class="compare-tag compare-tag-green">强一致</span>(全集群所有副本共享同一存储后端)</td>
                </tr>
                <tr>
                  <td><b>容器跨节点漂移容灾</b></td>
                  <td><span class="compare-tag compare-tag-red">不可行</span>(节点故障数据丢失断裂)</td>
                  <td><span class="compare-tag compare-tag-red">不可行</span>(漂移后创建全新空卷)</td>
                  <td><span class="compare-tag compare-tag-green">完美自愈</span>(容器漂移到任意节点自动挂载相同数据)</td>
                </tr>
                <tr>
                  <td><b>底层依赖与复杂度</b></td>
                  <td><span class="compare-tag compare-tag-green">极低</span>(依赖宿主机已有目录)</td>
                  <td><span class="compare-tag compare-tag-green">极低</span>(Docker 引擎自动管理)</td>
                  <td><span class="compare-tag compare-tag-orange">中等</span>(需配置 NFS/NAS 服务器与挂载驱动)</td>
                </tr>
                <tr>
                  <td><b>I/O 读写性能</b></td>
                  <td>物理机本地原生读写性能</td>
                  <td>物理机本地原生读写性能</td>
                  <td>受千兆/万兆网络带宽与 NFS 服务端性能影响</td>
                </tr>
                <tr>
                  <td><b>生产推荐场景</b></td>
                  <td>仅限单机测试、调试或挂载宿主系统配置</td>
                  <td>单节点有状态容器独立测试</td>
                  <td><b>生产集群核心方案</b>:Web 静态资源池、WordPress 上传目录、统一日志归档</td>
                </tr>
              </tbody>
            </table>
          </div>
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">清理测试持久化服务与本地卷</span>
            <div class="cmd-actions">
              <button class="btn-action" onclick="copyBlock('cmd-clean-vol')">复制</button>
              <button class="btn-action btn-action-primary" onclick="runBlock('cmd-clean-vol')">发送至终端</button>
            </div>
          </div>
          <pre class="cmd-content" id="cmd-clean-vol"><code># 一键清理存储测试相关的全部服务与存储卷
docker service rm web-vol web-nfs-svc web-nfs-auto 2>/dev/null || true
docker volume rm swarm-data swarm_nfs_volume nfs-auto-vol 2>/dev/null || true</code></pre>
        </div>

        <div class="output-box">
          <div class="output-title">实机预期输出 (Expected Output)</div>
          <div class="output-content">web-vol
web-nfs-svc
web-nfs-auto
swarm-data
swarm_nfs_volume
nfs-auto-vol</div>
        </div>
      </div>
    </article>

    <!-- 任务模块 Step 9 -->
    <article class="task-card" id="task-card-9">
      <div class="task-header">
        <div class="task-title">
          <span class="task-id">Step 9</span>
          <span>Docker Stack 声明式微服务栈一键编排交付</span>
        </div>
        <div class="task-controls">
          <button class="check-btn" onclick="toggleTaskStatus(9)">
            <span class="check-icon">✓</span> 标记完成
          </button>
        </div>
      </div>
      <div class="task-body">
        <div class="task-desc">
          <code>docker stack deploy</code> 是 Swarm 生产交付的核心方式。通过编写标准 Compose YAML 文件,即可批量纳管包含网络、多服务副本、更新策略与节点调度约束(<code>placement.constraints</code>)的复杂架构:
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">编写 stack-demo.yaml 并一键交付微服务栈</span>
            <div class="cmd-actions">
              <button class="btn-action" onclick="copyBlock('cmd-stack-deploy')">复制</button>
              <button class="btn-action btn-action-primary" onclick="runBlock('cmd-stack-deploy')">发送至终端</button>
            </div>
          </div>
          <pre class="cmd-content" id="cmd-stack-deploy"><code># 清理前面的单独服务
docker service rm web-vol 2>/dev/null

cat > /root/stack-demo.yaml &lt;&lt; 'EOF'
version: '3.8'

services:
  web:
    image: nginx:alpine
    deploy:
      replicas: 2
      restart_policy:
        condition: on-failure
      update_config:
        parallelism: 1
        delay: 3s
      placement:
        constraints:
          - node.role == manager

  app:
    image: nginx:alpine
    deploy:
      replicas: 1
EOF

docker stack deploy -c /root/stack-demo.yaml mystack</code></pre>
        </div>

        <div class="output-box">
          <div class="output-title">实机预期输出 (Expected Output)</div>
          <div class="output-content">Creating network mystack_default
Creating service mystack_web
Creating service mystack_app</div>
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">查看 Stack 状态与聚合日志</span>
            <div class="cmd-actions">
              <button class="btn-action" onclick="copyBlock('cmd-stack-check')">复制</button>
              <button class="btn-action btn-action-primary" onclick="runBlock('cmd-stack-check')">发送至终端</button>
            </div>
          </div>
          <pre class="cmd-content" id="cmd-stack-check"><code>docker stack ls
docker stack services mystack
docker stack ps mystack
docker service logs --tail 5 mystack_web</code></pre>
        </div>

        <div class="output-box">
          <div class="output-title">实机预期输出 (Expected Output)</div>
          <div class="output-content">NAME      SERVICES
mystack   2
ID             NAME          MODE         REPLICAS   IMAGE          PORTS
3vizns9ulo8g   mystack_app   replicated   1/1        nginx:alpine   
zgbu4qlq2w7d   mystack_web   replicated   2/2        nginx:alpine   
ID             NAME            IMAGE          NODE      DESIRED STATE   CURRENT STATE            ERROR     PORTS
jiptif33278p   mystack_app.1   nginx:alpine   node1     Running         Running 15 seconds ago
s4ja29zw7t4i   mystack_web.1   nginx:alpine   node1     Running         Running 15 seconds ago
sf5emy01pbbi   mystack_web.2   nginx:alpine   node1     Running         Running 15 seconds ago</div>
        </div>

        <div class="callout callout-tip">
          <span>📚 <b>生产经典 Stack 案例速查</b>:
          <br>• <b>WordPress + MySQL 案例</b>:在 <code>deploy.placement.constraints: [node.role == manager]</code> 中锁定数据库调度在管理节点;
          <br>• <b>Visualizer 拓扑大屏</b>:使用 <code>dockersamples/visualizer</code> 并挂载 <code>/var/run/docker.sock</code>,暴露 <code>9001:8080</code> 即可实时查看全集群拓扑分布;
          <br>• <b>NFS 综合集群</b>:在 stack 中通过 <code>volumes.nginx_vol.driver_opts.type: "nfs"</code> 实现后端多服务统一挂载网络文件系统。</span>
        </div>
      </div>
    </article>

    <!-- 任务模块 Step 10 -->
    <article class="task-card" id="task-card-10">
      <div class="task-header">
        <div class="task-title">
          <span class="task-id">Step 10</span>
          <span>环境清理与退出 Swarm 集群(Reset)</span>
        </div>
        <div class="task-controls">
          <button class="check-btn" onclick="toggleTaskStatus(10)">
            <span class="check-icon">✓</span> 标记完成
          </button>
        </div>
      </div>
      <div class="task-body">
        <div class="task-desc">
          演练完成后,一键销毁微服务栈并强制离开集群,恢复 node1 初始纯净状态:
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">清理微服务栈与离开集群</span>
            <div class="cmd-actions">
              <button class="btn-action" onclick="copyBlock('cmd-clean')">复制</button>
              <button class="btn-action btn-action-primary" onclick="runBlock('cmd-clean')">发送至终端</button>
            </div>
          </div>
          <pre class="cmd-content" id="cmd-clean"><code># 销毁微服务栈
docker stack rm mystack

# 离开 Swarm 集群
docker swarm leave --force</code></pre>
        </div>

        <div class="output-box">
          <div class="output-title">实机预期输出 (Expected Output)</div>
          <div class="output-content">Removing service mystack_app
Removing service mystack_web
Removing network mystack_default
Node left the swarm.</div>
        </div>
      </div>
    </article>

  </div>

  <!-- 轻量 Toast 提示 -->
  <div id="toast" class="toast">命令已发送至终端</div>

  <script>
    function dispatchCommand(cmd, target) {
      const toTerminal = target === "terminal";
      if (window.sendTerminalCommand) {
        window.sendTerminalCommand(cmd, toTerminal ? "terminal" : null);
        return;
      }
      if (window.parent && window.parent !== window) {
        window.parent.postMessage({
          type: "LAE_TERMINAL_COMMAND",
          command: cmd,
          target: toTerminal ? "terminal" : null
        }, "*");
        return;
      }
      navigator.clipboard.writeText(cmd).then(() => {
        showToast("已复制到剪贴板");
      }).catch(() => {
        showToast("复制失败,请手动选择");
      });
    }

    function runBlock(elementId) {
      const el = document.getElementById(elementId);
      if (!el) return;
      const cmd = el.innerText.trim();
      dispatchCommand(cmd, "terminal");
      showToast("命令已发送至 0 号终端");
    }

    function copyBlock(elementId) {
      const el = document.getElementById(elementId);
      if (!el) return;
      const cmd = el.innerText.trim();
      navigator.clipboard.writeText(cmd).then(() => {
        showToast("命令已复制到剪切板");
      }).catch(() => {
        showToast("复制失败,请手动选择");
      });
    }

    function showToast(msg) {
      const toast = document.getElementById("toast");
      if (!toast) return;
      toast.innerText = msg;
      toast.classList.add("show");
      setTimeout(() => {
        toast.classList.remove("show");
      }, 2000);
    }

    // 任务完成状态管理 (Step 1 ~ Step 10)
    const TOTAL_TASKS = 10;
    const taskStatus = new Array(TOTAL_TASKS + 1).fill(false);

    function initTaskState() {
      try {
        const saved = localStorage.getItem("docker_swarm_tasks_v2");
        if (saved) {
          const parsed = JSON.parse(saved);
          parsed.forEach((val, idx) => {
            if (idx > 0 && idx <= TOTAL_TASKS && val) {
              taskStatus[idx] = true;
              applyTaskUI(idx, true);
            }
          });
        }
      } catch (e) {
        console.warn("Storage access failed", e);
      }
      updateProgress();
    }

    function toggleTaskStatus(taskId) {
      taskStatus[taskId] = !taskStatus[taskId];
      applyTaskUI(taskId, taskStatus[taskId]);
      try {
        localStorage.setItem("docker_swarm_tasks_v2", JSON.stringify(taskStatus));
      } catch (e) {}
      updateProgress();
    }

    function applyTaskUI(taskId, isDone) {
      const card = document.getElementById("task-card-" + taskId);
      if (!card) return;
      const btn = card.querySelector(".check-btn");
      if (isDone) {
        card.classList.add("completed");
        if (btn) {
          btn.classList.add("active");
          btn.innerHTML = '<span class="check-icon">✓</span> 已完成';
        }
      } else {
        card.classList.remove("completed");
        if (btn) {
          btn.classList.remove("active");
          btn.innerHTML = '<span class="check-icon">✓</span> 标记完成';
        }
      }
    }

    function updateProgress() {
      let count = 0;
      for (let i = 1; i <= TOTAL_TASKS; i++) {
        if (taskStatus[i]) count++;
      }
      const pct = Math.round((count / TOTAL_TASKS) * 100);
      const fill = document.getElementById("progressFill");
      const text = document.getElementById("progressText");
      if (fill) fill.style.width = pct + "%";
      if (text) text.innerText = count + "/" + TOTAL_TASKS;
    }

    // 主题切换机制
    function toggleTheme() {
      const html = document.documentElement;
      const current = html.getAttribute("data-theme") || "light";
      const next = current === "light" ? "dark" : "light";
      setTheme(next);
    }

    function setTheme(theme) {
      document.documentElement.setAttribute("data-theme", theme);
      try {
        localStorage.setItem("docker_swarm_theme", theme);
      } catch (e) {}
      const txt = document.getElementById("theme-text");
      if (txt) {
        txt.innerText = theme === "dark" ? "🌙 暗色模式" : "☀️ 亮色模式";
      }
      if (window.parent && window.parent !== window) {
        window.parent.postMessage({ type: "THEME_CHANGE", theme: theme }, "*");
      }
    }

    window.addEventListener("message", function(e) {
      if (e.data && e.data.type === "THEME_CHANGE") {
        setTheme(e.data.theme);
      }
    });

    document.addEventListener("DOMContentLoaded", function() {
      try {
        const savedTheme = localStorage.getItem("docker_swarm_theme") || "light";
        setTheme(savedTheme);
      } catch (e) {}
      initTaskState();
    });
  </script>
</body>
</html>

💻 配套实训环境与动手练习

本文涉及的相关命令操作、开发环境与容器编排已内置在我们的在线脱机环境中,无需安装依赖,随时在浏览器中实践体验:

文章版权声明:除非注明,否则均为边学边练网络文章,版权归原作者所有
  • 上一篇:
  • 下一篇: