用wordpress搞個打字比賽

分享和討論倉頡輸入法相關的資訊和内容
版面规则
公正客觀講理,杜絶廢話連篇
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5749
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 179 次
被圈友点赞: 207 次
联系:

Re: 用wordpress搞個打字比賽

帖子 ejsoon »

把所有的jquery都改成原生的js,明確告知要修改的地方和要替換的代碼。

代码: 全选

這份程式碼中總共有 **9 處** 使用了 `jQuery.post` 來處理與 WordPress 後端的非同步請求(AJAX)。

為了用最優雅、改動最少且不容易出錯的方式將它們全部改為原生 JavaScript,我們可以使用現代瀏覽器內建的 `fetch()` API。

這裡有一個完美的解決策略:我們會在 `<script>` 的開頭封裝一個名為 `ejPost` 的原生 JavaScript 工具函式(功能完全對標 `jQuery.post`,且完美支援陣列參數轉換)。定義好後,**只要把原本的 `jQuery.post` 直接替換成 `ejPost` 即可,其餘內部的資料欄位和回呼函式(Callback)完全不需要修改!**

---

## 🛠️ 明確修改步驟說明

### 1. 新增原生 `ejPost` 工具函式

請在 `<script>` 標籤內的最上方(`const ejConfig = ...` 的下方),新增以下這段原生 JS 傳輸函式:

```javascript
// 用於取代 jQuery.post 的原生 fetch 封裝工具
function ejPost(url, data, callback) {
  const params = new URLSearchParams();
  for (const key in data) {
    if (Array.isArray(data[key])) {
      // 針對多選刪除等陣列資料,自動轉換為 WordPress 後端能識別的 key[] 格式
      data[key].forEach(val => params.append(`${key}[]`, val));
    } else {
      params.append(key, data[key]);
    }
  }

  fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: params
  })
  .then(response => response.json())
  .then(res => callback(res))
  .catch(err => console.error('AJAX 請求失敗:', err));
}

```

### 2. 需要替換 `jQuery.post` 的 9 個地方

請在程式碼中搜尋並將 `jQuery.post` 全數修改為 `ejPost`,分佈於以下函式中:

1. **`ejAdminSaveTextOrder()`**:儲存文本排序。
2. **`ejAdminToggleDeleteSelectedMode()`**:執行選中刪除文本。
3. **`ejAdminDeleteAllTexts()`**:刪除全部文本與排行榜。
4. **`ejRegisterUser()`**:註冊/保存參賽選手資料。
5. **`ejAdminAddText()`**:管理員新增比賽文本。
6. **`ejAdminDeleteText(textId)`**:管理員個別刪除文本。
7. **`ejStartGame()`**:點擊進入遊戲時載入文本內容。
8. **`window.addEventListener('message', ...)`**:遊戲結束時回傳並儲存成績。
9. **`ejLoadLeaderboard(type, textId = '')`**:載入排行榜 HTML。

---

## 📋 替換後的完整 `<script>` 區塊代碼

為了方便您直接複製使用,以下是已經將**所有 jQuery 移除並完整改為原生 JavaScript** 的 `<script>` 區塊:

```javascript
  <script>
    const ejConfig = {
      ajaxurl: '<?php echo admin_url('admin-ajax.php'); ?>',
      page_id: <?php echo $page_id; ?>,
      nonce: '<?php echo wp_create_nonce('ejtype_nonce'); ?>'
    };

    // 用於取代 jQuery.post 的原生 fetch 封裝工具
    function ejPost(url, data, callback) {
      const params = new URLSearchParams();
      for (const key in data) {
        if (Array.isArray(data[key])) {
          data[key].forEach(val => params.append(`${key}[]`, val));
        } else {
          params.append(key, data[key]);
        }
      }

      fetch(url, {
        method: 'POST',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        body: params
      })
      .then(response => response.json())
      .then(res => callback(res))
      .catch(err => console.error('AJAX 請求失敗:', err));
    }

    // 狀態變數定義
    let isDeleteSelectedMode = false;
    let selectedOrderTid = null;
    let checkedDeleteTids = [];

    // 設備偵測
    document.addEventListener('DOMContentLoaded', () => {
      const isMobile = window.innerWidth <= 768;
      const deviceSelect = document.getElementById('ejSelectDevice');
      if (deviceSelect) deviceSelect.value = isMobile ? 'mobile' : 'pc';
      ejLoadLeaderboard('global');
    });

    // 管理員欄目顯示/隱藏切換
    function ejToggleAdminPanel() {
      const wrapper = document.getElementById('ejAdminWrapperBox');
      const showBtn = document.getElementById('ejAdminToggleShowBtn');
      if (!wrapper) return;

      if (wrapper.classList.contains('ej-hidden')) {
        wrapper.classList.remove('ej-hidden');
        showBtn.classList.add('ej-hidden');
      } else {
        wrapper.classList.add('ej-hidden');
        showBtn.classList.remove('ej-hidden');
      }
    }

    // 處理媒體庫 ID 點擊事件 (包含排序與多選刪除核心邏輯)
    function ejHandleTextIdClick(element, tid) {
      if (isDeleteSelectedMode) {
        // 多選刪除模式
        const index = checkedDeleteTids.indexOf(tid);
        if (index > -1) {
          checkedDeleteTids.splice(index, 1);
          element.style.background = '';
        } else {
          checkedDeleteTids.push(tid);
          element.style.background = '#ffc107'; // 黃色代表選中
        }
      } else {
        // 排序移動模式
        const allIdCells = Array.from(document.querySelectorAll('.ej-text-id-cell'));

        if (selectedOrderTid === null) {
          selectedOrderTid = tid;
          element.style.background = '#b3d7ff'; // 藍色代表首選高亮
        } else {
          if (selectedOrderTid === tid) {
            // 重複點擊同一個,取消選取
            selectedOrderTid = null;
            element.style.background = '';
            return;
          }

          // 獲取 A 與 B 的元素與所在 Tr
          const cellA = allIdCells.find(c => parseInt(c.dataset.id) === selectedOrderTid);
          const cellB = element;
          if (!cellA || !cellB) return;

          const rowA = cellA.closest('tr');
          const rowB = cellB.closest('tr');

          const indexA = allIdCells.indexOf(cellA);
          const indexB = allIdCells.indexOf(cellB);

          // 依照先後順序插入
          if (indexA < indexB) {
            rowB.parentNode.insertBefore(rowA, rowB.nextSibling); // A 在前,移到 B 後面
          } else {
            rowB.parentNode.insertBefore(rowA, rowB); // A 在後,移到 B 前面
          }

          // 清理樣式與暫存狀態
          cellA.style.background = '';
          selectedOrderTid = null;

          // 非同步儲存新順序至後端
          ejAdminSaveTextOrder();
        }
      }
    }

    // 發送排序數據至後端(已改為原生 JS)
    function ejAdminSaveTextOrder() {
      const newOrder = Array.from(document.querySelectorAll('.ej-text-id-cell')).map(c => parseInt(c.dataset.id));

      ejPost(ejConfig.ajaxurl, {
        action: 'ej_admin_reorder_texts',
        nonce: ejConfig.nonce,
        page_id: ejConfig.page_id,
        order: newOrder
      }, function(res) {
        if (!res.success) alert('排序保存失敗: ' + (res.data || '未知錯誤'));
      });
    }

    // 切換至多選刪除模式或執行刪除(已改為原生 JS)
    function ejAdminToggleDeleteSelectedMode() {
      const btn = document.getElementById('ejBtnDeleteSelected');
      const cancelBtn = document.getElementById('ejBtnCancelDelete');
      const promptText = document.getElementById('ejDeletePromptText');

      if (!isDeleteSelectedMode) {
        // 進入多選刪除模式
        isDeleteSelectedMode = true;

        // 清除可能殘留的排序高亮
        if (selectedOrderTid !== null) {
          const prevCell = document.querySelector(`.ej-text-id-cell[data-id="${selectedOrderTid}"]`);
          if (prevCell) prevCell.style.background = '';
          selectedOrderTid = null;
        }

        cancelBtn.classList.remove('ej-hidden');
        promptText.classList.remove('ej-hidden');
        btn.innerText = '🔥 執行刪除';
        btn.classList.add('ej-btn-danger');
      } else {
        // 執行多選刪除
        if (checkedDeleteTids.length === 0) {
          alert('請先點擊文本媒體庫 ID 進行選擇!');
          return;
        }

        if (!confirm(`確定要刪除已選中的 ${checkedDeleteTids.length} 項文本及其關聯成績嗎?此動作無法撤銷!`)) return;

        ejPost(ejConfig.ajaxurl, {
          action: 'ej_admin_delete_multiple_texts',
          nonce: ejConfig.nonce,
          page_id: ejConfig.page_id,
          text_ids: checkedDeleteTids
        }, function(res) {
          if (res.success) {
            alert('選中刪除成功!');
            location.reload();
          } else {
            alert(res.data || '刪除失敗');
          }
        });
      }
    }

    // 取消多選刪除模式
    function ejAdminCancelDeleteMode() {
      isDeleteSelectedMode = false;
      checkedDeleteTids = [];

      document.querySelectorAll('.ej-text-id-cell').forEach(c => c.style.background = '');
      document.getElementById('ejBtnCancelDelete').classList.add('ej-hidden');
      document.getElementById('ejDeletePromptText').classList.add('ej-hidden');

      const btn = document.getElementById('ejBtnDeleteSelected');
      btn.innerText = '✏️ 選中刪除';
      btn.classList.remove('ej-btn-danger');
    }

    // 全部刪除文本與數據(已改為原生 JS)
    function ejAdminDeleteAllTexts() {
      if (!confirm('⚠️ 警告:這將會清空「所有」參賽文本以及整個打字系統的排行榜數據!此操作不可逆,確定要繼續嗎?')) return;

      ejPost(ejConfig.ajaxurl, {
        action: 'ej_admin_delete_all_texts',
        nonce: ejConfig.nonce,
        page_id: ejConfig.page_id
      }, function(res) {
        if (res.success) {
          alert('所有文本與排行榜數據已成功清空!');
          location.reload();
        } else {
          alert(res.data || '清空失敗');
        }
      });
    }

    // 註冊用戶(已改為原生 JS)
    function ejRegisterUser() {
      const name = document.getElementById('ejRegName').value.trim();
      const ime = document.getElementById('ejRegIme').value.trim();
      if (!name || !ime) return alert('請填寫完整資訊');

      ejPost(ejConfig.ajaxurl, {
        action: 'ej_save_user',
        nonce: ejConfig.nonce,
        page_id: ejConfig.page_id,
        name: name,
        ime: ime
      }, function(res) {
        if (res.success) location.reload();
        else alert(res.data);
      });
    }

    // 管理員新增文本(已改為原生 JS)
    function ejAdminAddText() {
      const mediaId = document.getElementById('ejAdminMediaId').value;
      const name = document.getElementById('ejAdminTextName').value;
      if (!mediaId) return alert('請輸入媒體庫ID');

      ejPost(ejConfig.ajaxurl, {
        action: 'ej_admin_text',
        nonce: ejConfig.nonce,
        page_id: ejConfig.page_id,
        media_id: mediaId,
        name: name
      }, function(res) {
        if (res.success) {
          alert('新增成功');
          location.reload();
        } else {
          alert(res.data);
        }
      });
    }

    // 管理員個別刪除文本(已改為原生 JS)
    function ejAdminDeleteText(textId) {
      if (!confirm('確定要刪除此文本嗎?這將會從比賽清單中移除該文本並清除其對應的排行榜成績(不會影響媒體庫原始檔案)。')) return;

      ejPost(ejConfig.ajaxurl, {
        action: 'ej_admin_delete_text',
        nonce: ejConfig.nonce,
        page_id: ejConfig.page_id,
        text_id: textId
      }, function(res) {
        if (res.success) {
          alert('刪除成功!');
          location.reload();
        } else {
          alert(res.data || '刪除失敗');
        }
      });
    }

    // UI 切換
    function ejShowPreGame() {
      const summaryDiv = document.getElementById('ejGameResultSummary');
      if (summaryDiv) summaryDiv.classList.add('ej-hidden');

      document.getElementById('ejSectionMain').classList.add('ej-hidden');
      document.getElementById('ejSectionPreGame').classList.remove('ej-hidden');
    }

    function ejCancelPreGame() {
      document.getElementById('ejSectionPreGame').classList.add('ej-hidden');
      document.getElementById('ejSectionMain').classList.remove('ej-hidden');
    }

    let currentTextId = '';
    let currentDevice = '';

    // 開始遊戲(已改為原生 JS)
    function ejStartGame() {
      currentTextId = document.getElementById('ejSelectText').value;
      currentDevice = document.getElementById('ejSelectDevice').value;

      if (!currentTextId) return alert('請選擇文本');

      ejPost(ejConfig.ajaxurl, {
        action: 'ej_get_text_content',
        nonce: ejConfig.nonce,
        page_id: ejConfig.page_id,
        text_id: currentTextId
      }, function(res) {
        if (res.success) {
          document.getElementById('ejSectionPreGame').classList.add('ej-hidden');
          document.getElementById('ejSectionGame').style.display = 'block';

          const iframe = document.getElementById('ejGameFrame');
          iframe.contentWindow.postMessage({
            action: 'load_wp_text',
            title: res.data.name,
            content: res.data.content
          }, '*');
          const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
          const rowHeightInput = iframeDoc.getElementById('typegamerowheight');
          if (rowHeightInput) {
            // 手機端為 2,電腦端為 5
            rowHeightInput.value = (currentDevice === 'mobile') ? '2' : '5';
            // 觸發事件確保內部遊戲框架能同步更新
            rowHeightInput.dispatchEvent(new Event('input', {
              bubbles: true
            }));
            rowHeightInput.dispatchEvent(new Event('change', {
              bubbles: true
            }));
          }
        } else {
          alert('讀取文本失敗');
        }
      });
    }

    // 接收 iframe 成績(已改為原生 JS)
    window.addEventListener('message', (event) => {
      if (event.data && event.data.action === 'ejtype_game_over') {
        const timeUsed = event.data.time;

        document.getElementById('ejSectionGame').style.display = 'none';
        if (document.fullscreenElement) document.exitFullscreen();
        document.getElementById('ejSectionGame').classList.remove('web-fullscreen');

        ejPost(ejConfig.ajaxurl, {
          action: 'ej_save_score',
          nonce: ejConfig.nonce,
          page_id: ejConfig.page_id,
          text_id: currentTextId,
          device: currentDevice,
          time: timeUsed
        }, function(res) {
          if (res.success) {
            const data = res.data;
            let summaryHtml = `<h4 style="margin: 0 0 10px 0; font-size: 18px;">🎯 挑戰完成回報</h4>`;
            summaryHtml += `<p style="margin: 4px 0;">⏱️ <strong>本次用時:</strong>${data.time.toFixed(1)} 秒 | ⚡ <strong>本次速度:</strong>${data.speed} 字/分</p>`;
            summaryHtml += `<p style="margin: 4px 0;">🏆 <strong>您的歷史最佳:</strong>${data.best_time.toFixed(1)} 秒 (${data.best_speed} 字/分)${data.is_new_best ? ' <span style="color: #dc3545; font-weight: bold; -webkit-animation: flash 1s infinite alternate; animation: flash 1s infinite alternate;">🔥 刷新個人紀錄!</span>' : ''}</p>`;

            const summaryDiv = document.getElementById('ejGameResultSummary');
            if (summaryDiv) {
              summaryDiv.innerHTML = summaryHtml;
              summaryDiv.classList.remove('ej-hidden');
            }
          }

          document.getElementById('ejSectionMain').classList.remove('ej-hidden');

          document.querySelectorAll('.ej-tab').forEach(t => t.classList.remove('active'));
          const tabText = document.getElementById('ejTabText');
          if (tabText) tabText.classList.add('active');

          ejLoadLeaderboard('text', currentTextId);
        });
      }
    });

    // 排行榜載入(已改為原生 JS)
    function ejLoadLeaderboard(type, textId = '') {
      document.getElementById('ejLeaderboardArea').innerHTML = '載入中...';
      ejPost(ejConfig.ajaxurl, {
        action: 'ej_get_leaderboard',
        nonce: ejConfig.nonce,
        page_id: ejConfig.page_id,
        type: type,
        text_id: textId
      }, function(res) {
        if (res.success) document.getElementById('ejLeaderboardArea').innerHTML = res.data;
      });
    }

    function ejSwitchBoard(type) {
      const summaryDiv = document.getElementById('ejGameResultSummary');
      if (summaryDiv) summaryDiv.classList.add('ej-hidden');

      document.querySelectorAll('.ej-tab').forEach(t => t.classList.remove('active'));
      if (window.event && window.event.target) {
        window.event.target.classList.add('active');
      } else {
        const targetId = type === 'global' ? 'ejTabGlobal' : 'ejTabText';
        const targetEl = document.getElementById(targetId);
        if (targetEl) targetEl.classList.add('active');
      }

      let tid = type === 'text' ? (currentTextId || document.getElementById('ejSelectText').value) : '';
      ejLoadLeaderboard(type, tid);
    }

    // 切換手機版「電腦/手機」設備排行榜標籤
    function ejSwitchDeviceTab(device, element) {
      const parent = element.parentElement;
      parent.querySelectorAll('.ej-device-tab').forEach(t => t.classList.remove('active'));
      element.classList.add('active');

      const wrapper = parent.nextElementSibling;
      if (device === 'pc') {
        wrapper.querySelector('.ej-board-pc').classList.remove('ej-hidden-mobile');
        wrapper.querySelector('.ej-board-mobile').classList.add('ej-hidden-mobile');
      } else {
        wrapper.querySelector('.ej-board-pc').classList.add('ej-hidden-mobile');
        wrapper.querySelector('.ej-board-mobile').classList.remove('ej-hidden-mobile');
      }
    }

    // 全螢幕控制
    function ejToggleWebFS() {
      document.getElementById('ejSectionGame').classList.toggle('web-fullscreen');
    }

    function ejToggleDeviceFS() {
      const elem = document.getElementById('ejSectionGame');
      if (!document.fullscreenElement) {
        elem.requestFullscreen().catch(err => alert('全螢幕請求被拒絕'));
      } else {
        document.exitFullscreen();
      }
    }
  </script>

```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5749
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 179 次
被圈友点赞: 207 次
联系:

Re: 用wordpress搞個打字比賽

帖子 ejsoon »

去掉jquery:

代码: 全选

<?php
// 註冊短代碼 [ejtype_competition]
add_shortcode('ejtype_competition', 'ejtype_competition_render');

function ejtype_competition_render()
{
  $is_logged_in = is_user_logged_in(); // 檢查登入狀態
  $page_id = get_the_ID();
  $current_user_id = get_current_user_id();
  $is_admin = $is_logged_in && current_user_can('manage_options');

  // 取得 Page Meta 數據
  $users_data = get_post_meta($page_id, '_ejtype_users', true) ?: [];
  $texts_data = get_post_meta($page_id, '_ejtype_texts', true) ?: [];

  // 取得遊戲主程式媒體庫 ID 及其網址
  $game_url = wp_get_attachment_url(19258);

  // 必須在登入情況下才去檢查是否註冊過
  $user_registered = $is_logged_in && isset($users_data[$current_user_id]);
  $user_info = $user_registered ? $users_data[$current_user_id] : null;

  ob_start();
?>
  <style>
    /* --- 現代化 CSS 樣式 --- */
    .ej-container {
      font-family: '微軟正黑體', sans-serif;
      margin: 0 auto;
      background: #fff;
      border-radius: 12px;
      box-shadow: 0 8px 20px rgba(0, 0, 0, 0.08);
      padding: 25px;
      color: #333;
    }

    .ej-header {
      border-bottom: 2px solid #f0f0f0;
      padding-bottom: 15px;
      margin-bottom: 20px;
      display: flex;
      justify-content: space-between;
      align-items: center;
    }

    .ej-header h2 {
      margin: 0;
      color: #0056b3;
      font-size: 24px;
      display: flex;
      align-items: center;
    }

    .ej-btn {
      background: #0056b3;
      color: #fff;
      border: none;
      padding: 10px 20px;
      border-radius: 6px;
      cursor: pointer;
      transition: 0.3s;
      font-size: 15px;
    }

    .ej-btn:hover {
      background: #004494;
    }

    .ej-btn-danger {
      background: #dc3545;
    }

    .ej-btn-danger:hover {
      background: #bd2130;
    }

    .ej-form-group {
      margin-bottom: 15px;
    }

    .ej-form-group label {
      display: block;
      margin-bottom: 5px;
      font-weight: bold;
    }

    .ej-form-group input,
    .ej-form-group select {
      width: 100%;
      padding: 10px;
      border: 1px solid #ddd;
      border-radius: 6px;
    }

    .ej-table {
      width: 100%;
      border-collapse: collapse;
      margin-top: 15px;
    }

    .ej-table th,
    .ej-table td {
      padding: 12px;
      border-bottom: 1px solid #eee;
      text-align: left;
    }

    .ej-table th {
      background: #f8f9fa;
      font-weight: bold;
    }

    /* 遊戲區域 & 全螢幕控制 */
    .ej-game-area {
      display: none;
      position: relative;
      border-radius: 8px;
      overflow: hidden;
      background: #f4f4f4;
      border: 2px solid #ddd;
      height: 80vh;
    }

    .ej-game-area iframe {
      width: 100%;
      height: 100%;
      border: none;
    }

    .ej-fullscreen-controls {
      position: absolute;
      top: 10px;
      right: 10px;
      display: flex;
      gap: 10px;
      z-index: 100;
    }

    .ej-fs-btn {
      background: rgba(0, 0, 0, 0.5);
      border: none;
      border-radius: 4px;
      padding: 5px;
      cursor: pointer;
      display: flex;
      fill: white;
      transition: 0.2s;
    }

    .ej-fs-btn:hover {
      background: rgba(0, 0, 0, 0.8);
    }

    .ej-game-area.web-fullscreen {
      position: fixed;
      top: 0;
      left: 0;
      width: 100vw;
      height: 100vh;
      z-index: 9999;
      border: none;
      border-radius: 0;
    }

    /* 隱藏區塊 */
    .ej-hidden {
      display: none !important;
    }

    .ej-tabs {
      display: flex;
      gap: 10px;
      margin-bottom: 15px;
    }

    .ej-tab {
      padding: 8px 15px;
      background: #eee;
      border-radius: 6px;
      cursor: pointer;
    }

    .ej-tab.active {
      background: #0056b3;
      color: white;
    }

    /* --- 設備排行榜並列與手機標籤切換樣式 --- */
    .ej-boards-wrapper {
      display: flex;
      gap: 20px;
      margin-top: 15px;
    }

    .ej-board-column {
      flex: 1;
      min-width: 0;
      /* 防止表格撐破寬度 */
    }

    .ej-device-tabs {
      display: none;
      /* 預設不顯示(寬螢幕) */
      gap: 10px;
      margin-bottom: 15px;
      margin-top: 15px;
    }

    .ej-device-tab {
      flex: 1;
      text-align: center;
      padding: 8px 15px;
      background: #eee;
      border-radius: 6px;
      cursor: pointer;
      font-size: 14px;
    }

    .ej-device-tab.active {
      background: #0056b3;
      color: white;
    }

    /* 當螢幕小於 768px 時切換為 Tabs 模式 */
    @media (max-width: 767px) {
      .ej-boards-wrapper {
        display: block;
      }

      .ej-device-tabs {
        display: flex;
      }

      .ej-hidden-mobile {
        display: none !important;
      }
    }
  </style>

  <div class="ej-container" id="ejApp">
    <div class="ej-header">
      <h2>
        🏆 打字比賽︱Typerace
        <?php if ($is_admin) : ?>
          <button id="ejAdminToggleShowBtn" class="ej-fs-btn ej-hidden" onclick="ejToggleAdminPanel()" style="margin-left: 10px; padding: 4px;" title="展開管理員面板">
            <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
              <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
              <line x1="9" y1="3" x2="9" y2="21"></line>
            </svg>
          </button>
        <?php endif; ?>
      </h2>
      <div id="ejUserStatus">
        <?php if ($is_logged_in && $user_registered) : ?>
          歡迎, <span id="ejCurrentName"><?php echo esc_html($user_info['name']); ?></span>
          (<?php echo esc_html($user_info['ime']); ?>)
        <?php elseif (!$is_logged_in) : ?>
          <span style="color: #dc3545; font-weight: bold;">請先 <a href="<?php echo wp_login_url(get_permalink()); ?>">登入</a> 以參加打字比賽。</span>
        <?php endif; ?>
      </div>
    </div>

    <div id="ejSectionRegister" class="<?php echo ($is_logged_in && !$user_registered) ? '' : 'ej-hidden'; ?>">
      <h3>請先填寫參賽資料</h3>
      <div class="ej-form-group">
        <label>顯示名稱:</label>
        <input type="text" id="ejRegName" placeholder="不可包含特殊符號">
      </div>
      <div class="ej-form-group">
        <label>輸入法名稱:</label>
        <input type="text" id="ejRegIme" placeholder="例如:注音、倉頡">
      </div>
      <button class="ej-btn" onclick="ejRegisterUser()">保存資料</button>
    </div>

    <div id="ejSectionMain" class="<?php echo (!$is_logged_in || $user_registered) ? '' : 'ej-hidden'; ?>">

      <?php if ($is_admin) : ?>
        <div id="ejAdminWrapperBox" style="background: #e9ecef; padding: 15px; border-radius: 8px; margin-bottom: 20px;">
          <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; border-bottom: 1px solid #ccc; padding-bottom: 5px;">
            <h4 style="margin: 0;">⚙️ 管理員:新增比賽文本</h4>
            <button class="ej-fs-btn" onclick="ejToggleAdminPanel()" style="padding: 4px;" title="最小化面板">
              <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                <line x1="5" y1="12" x2="19" y2="12"></line>
              </svg>
            </button>
          </div>

          <div style="display:flex; gap:10px; margin-bottom: 15px;">
            <input type="number" id="ejAdminMediaId" placeholder="媒體庫 TXT 檔案 ID">
            <input type="text" id="ejAdminTextName" placeholder="文本自訂名稱 (選填)">
            <button class="ej-btn" onclick="ejAdminAddText()">新增/更新</button>
          </div>

          <h4>📋 已加入的文本管理 (點擊媒體庫ID可調整排序)</h4>
          <?php if (!empty($texts_data)) : ?>
            <table class="ej-table" style="background: #fff; border-radius: 6px; box-shadow: 0 2px 5px rgba(0,0,0,0.05);">
              <thead>
                <tr>
                  <th>媒體庫 ID</th>
                  <th>文本名稱</th>
                  <th>字數</th>
                  <th>操作</th>
                </tr>
              </thead>
              <tbody id="ejAdminTextTableBody">
                <?php foreach ($texts_data as $tid => $t) : ?>
                  <tr data-row-id="<?php echo $tid; ?>">
                    <td class="ej-text-id-cell" data-id="<?php echo $tid; ?>" style="cursor: pointer; user-select: none; font-weight: bold; transition: background 0.2s;" onclick="ejHandleTextIdClick(this, <?php echo $tid; ?>)">
                      <code><?php echo esc_html($tid); ?></code>
                    </td>
                    <td style="font-weight: bold;"><?php echo esc_html($t['name']); ?></td>
                    <td><?php echo esc_html($t['length']); ?> 字</td>
                    <td>
                      <button class="ej-btn ej-btn-danger" style="padding: 5px 12px; font-size: 13px;" onclick="ejAdminDeleteText(<?php echo $tid; ?>)">❌ 刪除文本</button>
                    </td>
                  </tr>
                <?php endforeach; ?>
              </tbody>
            </table>

            <div style="margin-top: 15px; display: flex; gap: 10px; align-items: center; flex-wrap: wrap;">
              <button class="ej-btn ej-btn-danger" style="padding: 6px 14px; font-size: 14px;" onclick="ejAdminDeleteAllTexts()">🗑️ 全部刪除</button>
              <button id="ejBtnDeleteSelected" class="ej-btn" style="padding: 6px 14px; font-size: 14px; background: #ffc107; color: #212529;" onclick="ejAdminToggleDeleteSelectedMode()">✏️ 選中刪除</button>
              <button id="ejBtnCancelDelete" class="ej-btn ej-hidden" style="padding: 6px 14px; font-size: 14px; background: #6c757d;" onclick="ejAdminCancelDeleteMode()">取消刪除</button>
              <span id="ejDeletePromptText" class="ej-hidden" style="color: #dc3545; font-weight: bold; font-size: 14px; -webkit-animation: flash 1s infinite alternate; animation: flash 1s infinite alternate;">💡 請選擇要刪除的文本...</span>
            </div>
          <?php else : ?>
            <p style="color: #666; margin: 0; font-style: italic;">目前尚無加入任何文本。</p>
          <?php endif; ?>
        </div>
      <?php endif; ?>

      <div style="display:flex; justify-content:space-between; align-items:center;">
        <h3>排行榜</h3>
        <?php if ($is_logged_in) : ?>
          <button class="ej-btn" onclick="ejShowPreGame()">🚀 開始打字</button>
        <?php endif; ?>
      </div>

      <div id="ejGameResultSummary" class="ej-hidden" style="background: #e6f4ea; color: #137333; padding: 15px; border-radius: 8px; margin-bottom: 20px; border: 1px solid #ceead6;"></div>

      <div class="ej-tabs">
        <div id="ejTabGlobal" class="ej-tab active" onclick="ejSwitchBoard('global')">總排行榜</div>
        <div id="ejTabText" class="ej-tab" onclick="ejSwitchBoard('text')">單篇排行榜</div>
      </div>

      <div id="ejLeaderboardArea">
        <p>載入中...</p>
      </div>
    </div>

    <div id="ejSectionPreGame" class="ej-hidden">
      <h3>選擇比賽項目</h3>
      <div class="ej-form-group">
        <label>選擇文本:</label>
        <select id="ejSelectText">
          <option value="">-- 請選擇 --</option>
          <?php foreach ($texts_data as $tid => $t) : ?>
            <option value="<?php echo esc_attr($tid); ?>"><?php echo esc_html($t['name'] . ' (' . $t['length'] . '字)'); ?></option>
          <?php endforeach; ?>
        </select>
      </div>
      <div class="ej-form-group">
        <label>您的設備 (系統已自動偵測):</label>
        <select id="ejSelectDevice">
          <option value="pc">電腦 (鍵盤)</option>
          <option value="mobile">手機 (觸控)</option>
        </select>
      </div>
      <button class="ej-btn" onclick="ejCancelPreGame()" style="background:#6c757d;">取消</button>
      <button class="ej-btn" onclick="ejStartGame()">進入打字介面</button>
    </div>

    <div id="ejSectionGame" class="ej-game-area">
      <div class="ej-fullscreen-controls">
        <button class="ej-fs-btn" onclick="ejToggleWebFS()" title="網頁內全屏">
          <svg width="36" height="36" viewBox="0 0 24 24">
            <path d="M5 5h5v2H7v3H5V5zm9 0h5v5h-2V7h-3V5zm5 9h-2v3h-3v2h5v-5zm-14 3v-3h2v3h3v2H5v-5z" />
          </svg>
        </button>
        <button class="ej-fs-btn" onclick="ejToggleDeviceFS()" title="設備全屏">
          <svg width="36" height="36" viewBox="0 0 24 24">
            <path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" />
          </svg>
        </button>
      </div>
      <iframe id="ejGameFrame" src="<?php echo $game_url; ?>"></iframe>
    </div>
  </div>

  <script>
    const ejConfig = {
      ajaxurl: '<?php echo admin_url('admin-ajax.php'); ?>',
      page_id: <?php echo $page_id; ?>,
      nonce: '<?php echo wp_create_nonce('ejtype_nonce'); ?>'
    };

    // 用於取代 jQuery.post 的原生 fetch 封裝工具
    function ejPost(url, data, callback) {
      const params = new URLSearchParams();
      for (const key in data) {
        if (Array.isArray(data[key])) {
          // 針對多選刪除等陣列資料,自動轉換為 WordPress 後端能識別的 key[] 格式
          data[key].forEach(val => params.append(`${key}[]`, val));
        } else {
          params.append(key, data[key]);
        }
      }

      fetch(url, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/x-www-form-urlencoded'
          },
          body: params
        })
        .then(response => response.json())
        .then(res => callback(res))
        .catch(err => console.error('AJAX 請求失敗:', err));
    }

    // 狀態變數定義
    let isDeleteSelectedMode = false;
    let selectedOrderTid = null;
    let checkedDeleteTids = [];

    // 設備偵測
    document.addEventListener('DOMContentLoaded', () => {
      const isMobile = window.innerWidth <= 768;
      const deviceSelect = document.getElementById('ejSelectDevice');
      if (deviceSelect) deviceSelect.value = isMobile ? 'mobile' : 'pc';
      ejLoadLeaderboard('global');
    });

    // 管理員欄目顯示/隱藏切換
    function ejToggleAdminPanel() {
      const wrapper = document.getElementById('ejAdminWrapperBox');
      const showBtn = document.getElementById('ejAdminToggleShowBtn');
      if (!wrapper) return;

      if (wrapper.classList.contains('ej-hidden')) {
        wrapper.classList.remove('ej-hidden');
        showBtn.classList.add('ej-hidden');
      } else {
        wrapper.classList.add('ej-hidden');
        showBtn.classList.remove('ej-hidden');
      }
    }

    // 處理媒體庫 ID 點擊事件 (包含排序與多選刪除核心邏輯)
    function ejHandleTextIdClick(element, tid) {
      if (isDeleteSelectedMode) {
        // 多選刪除模式
        const index = checkedDeleteTids.indexOf(tid);
        if (index > -1) {
          checkedDeleteTids.splice(index, 1);
          element.style.background = '';
        } else {
          checkedDeleteTids.push(tid);
          element.style.background = '#ffc107'; // 黃色代表選中
        }
      } else {
        // 排序移動模式
        const allIdCells = Array.from(document.querySelectorAll('.ej-text-id-cell'));

        if (selectedOrderTid === null) {
          selectedOrderTid = tid;
          element.style.background = '#b3d7ff'; // 藍色代表首選高亮
        } else {
          if (selectedOrderTid === tid) {
            // 重複點擊同一個,取消選取
            selectedOrderTid = null;
            element.style.background = '';
            return;
          }

          // 獲取 A 與 B 的元素與所在 Tr
          const cellA = allIdCells.find(c => parseInt(c.dataset.id) === selectedOrderTid);
          const cellB = element;
          if (!cellA || !cellB) return;

          const rowA = cellA.closest('tr');
          const rowB = cellB.closest('tr');

          const indexA = allIdCells.indexOf(cellA);
          const indexB = allIdCells.indexOf(cellB);

          // 依照先後順序插入
          if (indexA < indexB) {
            rowB.parentNode.insertBefore(rowA, rowB.nextSibling); // A 在前,移到 B 後面
          } else {
            rowB.parentNode.insertBefore(rowA, rowB); // A 在後,移到 B 前面
          }

          // 清理樣式與暫存狀態
          cellA.style.background = '';
          selectedOrderTid = null;

          // 非同步儲存新順序至後端
          ejAdminSaveTextOrder();
        }
      }
    }

    // 發送排序數據至後端
    function ejAdminSaveTextOrder() {
      const newOrder = Array.from(document.querySelectorAll('.ej-text-id-cell')).map(c => parseInt(c.dataset.id));

      ejPost(ejConfig.ajaxurl, {
        action: 'ej_admin_reorder_texts',
        nonce: ejConfig.nonce,
        page_id: ejConfig.page_id,
        order: newOrder
      }, function(res) {
        if (!res.success) alert('排序保存失敗: ' + (res.data || '未知錯誤'));
      });
    }

    // 切換至多選刪除模式或執行刪除
    function ejAdminToggleDeleteSelectedMode() {
      const btn = document.getElementById('ejBtnDeleteSelected');
      const cancelBtn = document.getElementById('ejBtnCancelDelete');
      const promptText = document.getElementById('ejDeletePromptText');

      if (!isDeleteSelectedMode) {
        // 進入多選刪除模式
        isDeleteSelectedMode = true;

        // 清除可能殘留的排序高亮
        if (selectedOrderTid !== null) {
          const prevCell = document.querySelector(`.ej-text-id-cell[data-id="${selectedOrderTid}"]`);
          if (prevCell) prevCell.style.background = '';
          selectedOrderTid = null;
        }

        cancelBtn.classList.remove('ej-hidden');
        promptText.classList.remove('ej-hidden');
        btn.innerText = '🔥 執行刪除';
        btn.classList.add('ej-btn-danger');
      } else {
        // 執行多選刪除
        if (checkedDeleteTids.length === 0) {
          alert('請先點擊文本媒體庫 ID 進行選擇!');
          return;
        }

        if (!confirm(`確定要刪除已選中的 ${checkedDeleteTids.length} 項文本及其關聯成績嗎?此動作無法撤銷!`)) return;

        ejPost(ejConfig.ajaxurl, {
          action: 'ej_admin_delete_multiple_texts',
          nonce: ejConfig.nonce,
          page_id: ejConfig.page_id,
          text_ids: checkedDeleteTids
        }, function(res) {
          if (res.success) {
            alert('選中刪除成功!');
            location.reload();
          } else {
            alert(res.data || '刪除失敗');
          }
        });
      }
    }

    // 取消多選刪除模式
    function ejAdminCancelDeleteMode() {
      isDeleteSelectedMode = false;
      checkedDeleteTids = [];

      document.querySelectorAll('.ej-text-id-cell').forEach(c => c.style.background = '');
      document.getElementById('ejBtnCancelDelete').classList.add('ej-hidden');
      document.getElementById('ejDeletePromptText').classList.add('ej-hidden');

      const btn = document.getElementById('ejBtnDeleteSelected');
      btn.innerText = '✏️ 選中刪除';
      btn.classList.remove('ej-btn-danger');
    }

    // 全部刪除文本與數據
    function ejAdminDeleteAllTexts() {
      if (!confirm('⚠️ 警告:這將會清空「所有」參賽文本以及整個打字系統的排行榜數據!此操作不可逆,確定要繼續嗎?')) return;

      ejPost(ejConfig.ajaxurl, {
        action: 'ej_admin_delete_all_texts',
        nonce: ejConfig.nonce,
        page_id: ejConfig.page_id
      }, function(res) {
        if (res.success) {
          alert('所有文本與排行榜數據已成功清空!');
          location.reload();
        } else {
          alert(res.data || '清空失敗');
        }
      });
    }

    // 註冊用戶
    function ejRegisterUser() {
      const name = document.getElementById('ejRegName').value.trim();
      const ime = document.getElementById('ejRegIme').value.trim();
      if (!name || !ime) return alert('請填寫完整資訊');

      ejPost(ejConfig.ajaxurl, {
        action: 'ej_save_user',
        nonce: ejConfig.nonce,
        page_id: ejConfig.page_id,
        name: name,
        ime: ime
      }, function(res) {
        if (res.success) location.reload();
        else alert(res.data);
      });
    }

    // 管理員新增文本
    function ejAdminAddText() {
      const mediaId = document.getElementById('ejAdminMediaId').value;
      const name = document.getElementById('ejAdminTextName').value;
      if (!mediaId) return alert('請輸入媒體庫ID');

      ejPost(ejConfig.ajaxurl, {
        action: 'ej_admin_text',
        nonce: ejConfig.nonce,
        page_id: ejConfig.page_id,
        media_id: mediaId,
        name: name
      }, function(res) {
        if (res.success) {
          alert('新增成功');
          location.reload();
        } else {
          alert(res.data);
        }
      });
    }

    // 管理員個別刪除文本
    function ejAdminDeleteText(textId) {
      if (!confirm('確定要刪除此文本嗎?這將會從比賽清單中移除該文本並清除其對應的排行榜成績(不會影響媒體庫原始檔案)。')) return;

      ejPost(ejConfig.ajaxurl, {
        action: 'ej_admin_delete_text',
        nonce: ejConfig.nonce,
        page_id: ejConfig.page_id,
        text_id: textId
      }, function(res) {
        if (res.success) {
          alert('刪除成功!');
          location.reload();
        } else {
          alert(res.data || '刪除失敗');
        }
      });
    }

    // UI 切換
    function ejShowPreGame() {
      const summaryDiv = document.getElementById('ejGameResultSummary');
      if (summaryDiv) summaryDiv.classList.add('ej-hidden');

      document.getElementById('ejSectionMain').classList.add('ej-hidden');
      document.getElementById('ejSectionPreGame').classList.remove('ej-hidden');
    }

    function ejCancelPreGame() {
      document.getElementById('ejSectionPreGame').classList.add('ej-hidden');
      document.getElementById('ejSectionMain').classList.remove('ej-hidden');
    }

    let currentTextId = '';
    let currentDevice = '';

    // 開始遊戲
    function ejStartGame() {
      currentTextId = document.getElementById('ejSelectText').value;
      currentDevice = document.getElementById('ejSelectDevice').value;

      if (!currentTextId) return alert('請選擇文本');

      ejPost(ejConfig.ajaxurl, {
        action: 'ej_get_text_content',
        nonce: ejConfig.nonce,
        page_id: ejConfig.page_id,
        text_id: currentTextId
      }, function(res) {
        if (res.success) {
          document.getElementById('ejSectionPreGame').classList.add('ej-hidden');
          document.getElementById('ejSectionGame').style.display = 'block';

          const iframe = document.getElementById('ejGameFrame');
          iframe.contentWindow.postMessage({
            action: 'load_wp_text',
            title: res.data.name,
            content: res.data.content
          }, '*');
          const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
          const rowHeightInput = iframeDoc.getElementById('typegamerowheight');
          if (rowHeightInput) {
            // 手機端為 2,電腦端為 5
            rowHeightInput.value = (currentDevice === 'mobile') ? '2' : '5';
            // 觸發事件確保內部遊戲框架能同步更新
            rowHeightInput.dispatchEvent(new Event('input', {
              bubbles: true
            }));
            rowHeightInput.dispatchEvent(new Event('change', {
              bubbles: true
            }));
          }
        } else {
          alert('讀取文本失敗');
        }
      });
    }

    // 接收 iframe 成績
    window.addEventListener('message', (event) => {
      if (event.data && event.data.action === 'ejtype_game_over') {
        const timeUsed = event.data.time;

        document.getElementById('ejSectionGame').style.display = 'none';
        if (document.fullscreenElement) document.exitFullscreen();
        document.getElementById('ejSectionGame').classList.remove('web-fullscreen');

        ejPost(ejConfig.ajaxurl, {
          action: 'ej_save_score',
          nonce: ejConfig.nonce,
          page_id: ejConfig.page_id,
          text_id: currentTextId,
          device: currentDevice,
          time: timeUsed
        }, function(res) {
          if (res.success) {
            const data = res.data;
            let summaryHtml = `<h4 style="margin: 0 0 10px 0; font-size: 18px;">🎯 挑戰完成回報</h4>`;
            summaryHtml += `<p style="margin: 4px 0;">⏱️ <strong>本次用時:</strong>${data.time.toFixed(1)} 秒 | ⚡ <strong>本次速度:</strong>${data.speed} 字/分</p>`;
            summaryHtml += `<p style="margin: 4px 0;">🏆 <strong>您的歷史最佳:</strong>${data.best_time.toFixed(1)} 秒 (${data.best_speed} 字/分)${data.is_new_best ? ' <span style="color: #dc3545; font-weight: bold; -webkit-animation: flash 1s infinite alternate; animation: flash 1s infinite alternate;">🔥 刷新個人紀錄!</span>' : ''}</p>`;

            const summaryDiv = document.getElementById('ejGameResultSummary');
            if (summaryDiv) {
              summaryDiv.innerHTML = summaryHtml;
              summaryDiv.classList.remove('ej-hidden');
            }
          }

          document.getElementById('ejSectionMain').classList.remove('ej-hidden');

          document.querySelectorAll('.ej-tab').forEach(t => t.classList.remove('active'));
          const tabText = document.getElementById('ejTabText');
          if (tabText) tabText.classList.add('active');

          ejLoadLeaderboard('text', currentTextId);
        });
      }
    });

    // 排行榜載入
    function ejLoadLeaderboard(type, textId = '') {
      document.getElementById('ejLeaderboardArea').innerHTML = '載入中...';
      ejPost(ejConfig.ajaxurl, {
        action: 'ej_get_leaderboard',
        nonce: ejConfig.nonce,
        page_id: ejConfig.page_id,
        type: type,
        text_id: textId
      }, function(res) {
        if (res.success) document.getElementById('ejLeaderboardArea').innerHTML = res.data;
      });
    }

    function ejSwitchBoard(type) {
      const summaryDiv = document.getElementById('ejGameResultSummary');
      if (summaryDiv) summaryDiv.classList.add('ej-hidden');

      document.querySelectorAll('.ej-tab').forEach(t => t.classList.remove('active'));
      if (window.event && window.event.target) {
        window.event.target.classList.add('active');
      } else {
        const targetId = type === 'global' ? 'ejTabGlobal' : 'ejTabText';
        const targetEl = document.getElementById(targetId);
        if (targetEl) targetEl.classList.add('active');
      }

      let tid = type === 'text' ? (currentTextId || document.getElementById('ejSelectText').value) : '';
      ejLoadLeaderboard(type, tid);
    }

    // 切換手機版「電腦/手機」設備排行榜標籤
    function ejSwitchDeviceTab(device, element) {
      const parent = element.parentElement;
      parent.querySelectorAll('.ej-device-tab').forEach(t => t.classList.remove('active'));
      element.classList.add('active');

      const wrapper = parent.nextElementSibling;
      if (device === 'pc') {
        wrapper.querySelector('.ej-board-pc').classList.remove('ej-hidden-mobile');
        wrapper.querySelector('.ej-board-mobile').classList.add('ej-hidden-mobile');
      } else {
        wrapper.querySelector('.ej-board-pc').classList.add('ej-hidden-mobile');
        wrapper.querySelector('.ej-board-mobile').classList.remove('ej-hidden-mobile');
      }
    }

    // 全螢幕控制
    function ejToggleWebFS() {
      document.getElementById('ejSectionGame').classList.toggle('web-fullscreen');
    }

    function ejToggleDeviceFS() {
      const elem = document.getElementById('ejSectionGame');
      if (!document.fullscreenElement) {
        elem.requestFullscreen().catch(err => alert('全螢幕請求被拒絕'));
      } else {
        document.exitFullscreen();
      }
    }
  </script>
<?php
  return ob_get_clean();
}

// ---------------- 後端 AJAX 處理 ----------------

function ejtype_clean_input($val)
{
  return str_replace(['\\', '"', "'"], '', sanitize_text_field($val));
}

// 儲存用戶資料
add_action('wp_ajax_ej_save_user', 'ej_ajax_save_user');
function ej_ajax_save_user()
{
  check_ajax_referer('ejtype_nonce', 'nonce');
  $page_id = intval($_POST['page_id']);
  $uid = get_current_user_id();

  $users = get_post_meta($page_id, '_ejtype_users', true) ?: [];
  $users[$uid] = [
    'name' => ejtype_clean_input($_POST['name']),
    'ime' => ejtype_clean_input($_POST['ime'])
  ];
  update_post_meta($page_id, '_ejtype_users', $users);
  wp_send_json_success();
}

// 管理員新增文本
add_action('wp_ajax_ej_admin_text', 'ej_ajax_admin_text');
function ej_ajax_admin_text()
{
  check_ajax_referer('ejtype_nonce', 'nonce');
  if (!current_user_can('manage_options')) wp_send_json_error('權限不足');

  $page_id = intval($_POST['page_id']);
  $media_id = intval($_POST['media_id']);

  $file_path = get_attached_file($media_id);
  if (!$file_path || !file_exists($file_path)) wp_send_json_error('找不到該媒體檔案');

  $content = file_get_contents($file_path);
  $clean_content = preg_replace('/\s+/u', '', $content);
  $length = mb_strlen($clean_content, 'UTF-8');

  $name = !empty($_POST['name']) ? ejtype_clean_input($_POST['name']) : get_the_title($media_id);

  $texts = get_post_meta($page_id, '_ejtype_texts', true) ?: [];
  $texts[$media_id] = [
    'name' => $name,
    'length' => $length
  ];
  update_post_meta($page_id, '_ejtype_texts', $texts);
  wp_send_json_success();
}

// 【新增後端處理】保存變更後的文本排序
add_action('wp_ajax_ej_admin_reorder_texts', 'ej_ajax_admin_reorder_texts');
function ej_ajax_admin_reorder_texts()
{
  check_ajax_referer('ejtype_nonce', 'nonce');
  if (!current_user_can('manage_options')) wp_send_json_error('權限不足');

  $page_id = intval($_POST['page_id']);
  $new_order = isset($_POST['order']) ? array_map('intval', $_POST['order']) : [];

  $texts = get_post_meta($page_id, '_ejtype_texts', true) ?: [];
  $reordered_texts = [];

  // 根據前端傳過來的 ID 順序重新構建關聯陣列
  foreach ($new_order as $tid) {
    if (isset($texts[$tid])) {
      $reordered_texts[$tid] = $texts[$tid];
    }
  }

  // 安全檢查:若有漏網之魚則補回陣列尾端
  foreach ($texts as $tid => $t) {
    if (!isset($reordered_texts[$tid])) {
      $reordered_texts[$tid] = $t;
    }
  }

  update_post_meta($page_id, '_ejtype_texts', $reordered_texts);
  wp_send_json_success();
}

// 【新增後端處理】多選選中刪除
add_action('wp_ajax_ej_admin_delete_multiple_texts', 'ej_ajax_admin_delete_multiple_texts');
function ej_ajax_admin_delete_multiple_texts()
{
  check_ajax_referer('ejtype_nonce', 'nonce');
  if (!current_user_can('manage_options')) wp_send_json_error('權限不足');

  $page_id = intval($_POST['page_id']);
  $text_ids = isset($_POST['text_ids']) ? array_map('intval', $_POST['text_ids']) : [];

  $texts = get_post_meta($page_id, '_ejtype_texts', true) ?: [];
  $results = get_post_meta($page_id, '_ejtype_results', true) ?: [];

  foreach ($text_ids as $tid) {
    if (isset($texts[$tid])) unset($texts[$tid]);
    if (isset($results[$tid])) unset($results[$tid]);
  }

  update_post_meta($page_id, '_ejtype_texts', $texts);
  update_post_meta($page_id, '_ejtype_results', $results);
  wp_send_json_success();
}

// 【新增後端處理】全部刪除
add_action('wp_ajax_ej_admin_delete_all_texts', 'ej_ajax_admin_delete_all_texts');
function ej_ajax_admin_delete_all_texts()
{
  check_ajax_referer('ejtype_nonce', 'nonce');
  if (!current_user_can('manage_options')) wp_send_json_error('權限不足');

  $page_id = intval($_POST['page_id']);

  update_post_meta($page_id, '_ejtype_texts', []);
  update_post_meta($page_id, '_ejtype_results', []);
  wp_send_json_success();
}

// 取得文本內容給前端
add_action('wp_ajax_ej_get_text_content', 'ej_ajax_get_text_content');
function ej_ajax_get_text_content()
{
  $page_id = intval($_POST['page_id']);
  $text_id = intval($_POST['text_id']);
  $texts = get_post_meta($page_id, '_ejtype_texts', true) ?: [];

  $file_path = get_attached_file($text_id);
  if ($file_path && file_exists($file_path)) {
    wp_send_json_success([
      'name' => $texts[$text_id]['name'],
      'content' => file_get_contents($file_path)
    ]);
  }
  wp_send_json_error();
}

// 儲存成績
add_action('wp_ajax_ej_save_score', 'ej_ajax_save_score');
function ej_ajax_save_score()
{
  $page_id = intval($_POST['page_id']);
  $uid = get_current_user_id();
  $text_id = intval($_POST['text_id']);
  $device = sanitize_text_field($_POST['device']);
  $time = floatval($_POST['time']);

  $results = get_post_meta($page_id, '_ejtype_results', true) ?: [];

  if (!isset($results[$text_id])) $results[$text_id] = ['pc' => [], 'mobile' => []];

  $history_best = isset($results[$text_id][$device][$uid]) ? $results[$text_id][$device][$uid] : null;
  $is_new_best = false;

  if ($history_best === null || $time < $history_best) {
    $results[$text_id][$device][$uid] = $time;
    $history_best = $time;
    $is_new_best = true;
  }

  update_post_meta($page_id, '_ejtype_results', $results);

  $texts = get_post_meta($page_id, '_ejtype_texts', true) ?: [];
  $length = isset($texts[$text_id]['length']) ? $texts[$text_id]['length'] : 0;

  $current_speed = $length > 0 ? round($length / ($time / 60), 1) : 0;
  $best_speed = $length > 0 ? round($length / ($history_best / 60), 1) : 0;

  wp_send_json_success([
    'time'        => $time,
    'speed'       => $current_speed,
    'best_time'   => $history_best,
    'best_speed'  => $best_speed,
    'is_new_best' => $is_new_best
  ]);
}

// 獲取排行榜 HTML
add_action('wp_ajax_ej_get_leaderboard', 'ej_ajax_get_leaderboard');
add_action('wp_ajax_nopriv_ej_get_leaderboard', 'ej_ajax_get_leaderboard');
function ej_ajax_get_leaderboard()
{
  $page_id = intval($_POST['page_id']);
  $type = sanitize_text_field($_POST['type']);
  $text_id = intval($_POST['text_id']);

  $users = get_post_meta($page_id, '_ejtype_users', true) ?: [];
  $texts = get_post_meta($page_id, '_ejtype_texts', true) ?: [];
  $results = get_post_meta($page_id, '_ejtype_results', true) ?: [];

  ob_start();

  if ($type === 'text') {
    echo '<div style="margin-bottom: 20px; background: #f8f9fa; padding: 12px; border-radius: 6px; border: 1px solid #eee;">';
    echo '<label style="font-weight: bold; margin-right: 10px;">切換檢視文本:</label>';
    echo '<select id="ejLeaderboardTextSelect" style="padding: 6px 12px; border-radius: 4px; border: 1px solid #ccc; max-width: 100%;" onchange="ejLoadLeaderboard(\'text\', this.value)">';
    echo '<option value="">-- 請選擇比賽文本 --</option>';
    foreach ($texts as $tid => $t) {
      $selected = ($tid == $text_id) ? 'selected' : '';
      echo "<option value='" . esc_attr($tid) . "' {$selected}>" . esc_html($t['name']) . " (" . esc_html($t['length']) . "字)</option>";
    }
    echo '</select>';
    echo '</div>';

    if (!$text_id || !isset($texts[$text_id])) {
      echo '<p style="color: #666; font-style: italic; text-align: center; padding: 30px 0;">💡 請先點擊上方選單切換文本,或點擊右上方「🚀 開始打字」完成一場參賽項目。</p>';
      wp_send_json_success(ob_get_clean());
      return;
    }

    $t_info = $texts[$text_id];
    echo "<div style='margin-bottom:10px;'><strong>當前文本:</strong> {$t_info['name']} ({$t_info['length']} 字)</div>";

    echo '<div class="ej-device-tabs">
            <div class="ej-device-tab active" onclick="ejSwitchDeviceTab(\'pc\', this)">💻 電腦排名</div>
            <div class="ej-device-tab" onclick="ejSwitchDeviceTab(\'mobile\', this)">📱 手機排名</div>
          </div>';

    echo '<div class="ej-boards-wrapper">';
    foreach (['pc' => '💻 電腦鍵盤排名', 'mobile' => '📱 手機觸屏排名'] as $dev => $title) {
      $hide_class = ($dev === 'mobile') ? ' ej-hidden-mobile' : '';
      echo "<div class='ej-board-column ej-board-{$dev}{$hide_class}'>";
      echo "<h4>{$title}</h4>";
      $scores = isset($results[$text_id][$dev]) ? $results[$text_id][$dev] : [];
      asort($scores);

      echo "<table class='ej-table'><tr><th>排名</th><th>選手 (輸入法)</th><th>用時 (秒)</th><th>速度 (字/分)</th></tr>";
      $rank = 1;
      foreach ($scores as $uid => $time) {
        if (!isset($users[$uid])) continue;
        $speed = round($t_info['length'] / ($time / 60), 1);
        $u = $users[$uid];
        echo "<tr><td>{$rank}</td><td>{$u['name']} ({$u['ime']})</td><td>" . number_format($time, 1) . "</td><td>{$speed}</td></tr>";
        $rank++;
      }
      if (empty($scores)) echo "<tr><td colspan='4'>暫無成績</td></tr>";
      echo "</table>";
      echo "</div>";
    }
    echo '</div>';
  } else {
    $all_scores = ['pc' => [], 'mobile' => []];
    foreach ($results as $tid => $dev_data) {
      if (!isset($texts[$tid])) continue;
      $len = $texts[$tid]['length'];
      foreach ($dev_data as $dev => $user_times) {
        foreach ($user_times as $uid => $time) {
          $speed = round($len / ($time / 60), 1);
          $all_scores[$dev][] = ['uid' => $uid, 'tid' => $tid, 'speed' => $speed];
        }
      }
    }

    echo '<div class="ej-device-tabs">
            <div class="ej-device-tab active" onclick="ejSwitchDeviceTab(\'pc\', this)">💻 電腦總排名</div>
            <div class="ej-device-tab" onclick="ejSwitchDeviceTab(\'mobile\', this)">📱 手機總排名</div>
          </div>';

    echo '<div class="ej-boards-wrapper">';
    foreach (['pc' => '💻 電腦鍵盤總排名', 'mobile' => '📱 手機觸屏總排名'] as $dev => $title) {
      $hide_class = ($dev === 'mobile') ? ' ej-hidden-mobile' : '';
      echo "<div class='ej-board-column ej-board-{$dev}{$hide_class}'>";
      echo "<h4>{$title}</h4>";
      $dev_scores = $all_scores[$dev];
      usort($dev_scores, function ($a, $b) {
        return $b['speed'] <=> $a['speed'];
      });

      echo "<table class='ej-table'><tr><th>排名</th><th>選手 (輸入法)</th><th>文本名稱</th><th>字數</th><th>速度 (字/分)</th></tr>";
      $rank = 1;
      foreach ($dev_scores as $s) {
        if (!isset($users[$s['uid']])) continue;
        $u = $users[$s['uid']];
        $t = $texts[$s['tid']];
        echo "<tr><td>{$rank}</td><td>{$u['name']} ({$u['ime']})</td><td>{$t['name']}</td><td>{$t['length']}</td><td>{$s['speed']}</td></tr>";
        $rank++;
      }
      if (empty($dev_scores)) echo "<tr><td colspan='5'>暫無成績</td></tr>";
      echo "</table>";
      echo "</div>";
    }
    echo '</div>';
  }

  wp_send_json_success(ob_get_clean());
}

// 管理員刪除單一文本與相關成績
add_action('wp_ajax_ej_admin_delete_text', 'ej_ajax_admin_delete_text');
function ej_ajax_admin_delete_text()
{
  check_ajax_referer('ejtype_nonce', 'nonce');
  if (!current_user_can('manage_options')) wp_send_json_error('權限不足');

  $page_id = intval($_POST['page_id']);
  $text_id = intval($_POST['text_id']);

  $texts = get_post_meta($page_id, '_ejtype_texts', true) ?: [];
  if (isset($texts[$text_id])) {
    unset($texts[$text_id]);
    update_post_meta($page_id, '_ejtype_texts', $texts);

    $results = get_post_meta($page_id, '_ejtype_results', true) ?: [];
    if (isset($results[$text_id])) {
      unset($results[$text_id]);
      update_post_meta($page_id, '_ejtype_results', $results);
    }

    wp_send_json_success();
  }

  wp_send_json_error('找不到該文本數據');
}
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5749
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 179 次
被圈友点赞: 207 次
联系:

Re: 用wordpress搞個打字比賽

帖子 ejsoon »

尹卂搞的打字比賽

https://ejsoon.vip/ejtyperace/

代码: 全选

// 註冊短代碼 [ejtype_competition]
add_shortcode('ejtype_competition', 'ejtype_competition_render');

function ejtype_competition_render()
{
  $is_logged_in = is_user_logged_in(); // 檢查登入狀態
  $page_id = get_the_ID();
  $current_user_id = get_current_user_id();
  $is_admin = $is_logged_in && current_user_can('manage_options');

  // 取得 Page Meta 數據
  $users_data = get_post_meta($page_id, '_ejtype_users', true) ?: [];
  $texts_data = get_post_meta($page_id, '_ejtype_texts', true) ?: [];

  // 取得遊戲主程式媒體庫 ID 及其網址
  $game_url = wp_get_attachment_url(19258);

  // 必須在登入情況下才去檢查是否註冊過
  $user_registered = $is_logged_in && isset($users_data[$current_user_id]);
  $user_info = $user_registered ? $users_data[$current_user_id] : null;

  ob_start();
?>
  <style>
    /* --- 現代化 CSS 樣式 --- */
    .ej-container {
      font-family: '微軟正黑體', sans-serif;
      margin: 0 auto;
      background: #fff;
      border-radius: 12px;
      box-shadow: 0 8px 20px rgba(0, 0, 0, 0.08);
      padding: 25px;
      color: #333;
    }

    .ej-header {
      border-bottom: 2px solid #f0f0f0;
      padding-bottom: 15px;
      margin-bottom: 20px;
      display: flex;
      justify-content: space-between;
      align-items: center;
    }

    .ej-header h2 {
      margin: 0;
      color: #0056b3;
      font-size: 24px;
      display: flex;
      align-items: center;
    }

    .ej-btn {
      background: #0056b3;
      color: #fff;
      border: none;
      padding: 10px 20px;
      border-radius: 6px;
      cursor: pointer;
      transition: 0.3s;
      font-size: 15px;
    }

    .ej-btn:hover {
      background: #004494;
    }

    .ej-btn-danger {
      background: #dc3545;
    }

    .ej-btn-danger:hover {
      background: #bd2130;
    }

    .ej-form-group {
      margin-bottom: 15px;
    }

    .ej-form-group label {
      display: block;
      margin-bottom: 5px;
      font-weight: bold;
    }

    .ej-form-group input,
    .ej-form-group select {
      width: 100%;
      padding: 10px;
      border: 1px solid #ddd;
      border-radius: 6px;
    }

    .ej-table {
      width: 100%;
      border-collapse: collapse;
      margin-top: 15px;
    }

    .ej-table th,
    .ej-table td {
      padding: 12px;
      border-bottom: 1px solid #eee;
      text-align: left;
    }

    .ej-table th {
      background: #f8f9fa;
      font-weight: bold;
    }

    /* 遊戲區域 & 全螢幕控制 */
    .ej-game-area {
      display: none;
      position: relative;
      border-radius: 8px;
      overflow: hidden;
      background: #f4f4f4;
      border: 2px solid #ddd;
      height: 80vh;
    }

    .ej-game-area iframe {
      width: 100%;
      height: 100%;
      border: none;
    }

    .ej-fullscreen-controls {
      position: absolute;
      top: 10px;
      right: 10px;
      display: flex;
      gap: 10px;
      z-index: 100;
    }

    .ej-fs-btn {
      background: rgba(0, 0, 0, 0.5);
      border: none;
      border-radius: 4px;
      padding: 5px;
      cursor: pointer;
      display: flex;
      fill: white;
      transition: 0.2s;
    }

    .ej-fs-btn:hover {
      background: rgba(0, 0, 0, 0.8);
    }

    .ej-game-area.web-fullscreen {
      position: fixed;
      top: 0;
      left: 0;
      width: 100vw;
      height: 100vh;
      z-index: 9999999;
      border: none;
      border-radius: 0;
    }

    /* 隱藏區塊 */
    .ej-hidden {
      display: none !important;
    }

    .ej-tabs {
      display: flex;
      gap: 10px;
      margin-bottom: 15px;
    }

    .ej-tab {
      padding: 8px 15px;
      background: #eee;
      border-radius: 6px;
      cursor: pointer;
    }

    .ej-tab.active {
      background: #0056b3;
      color: white;
    }

    /* --- 設備排行榜並列與手機標籤切換樣式 --- */
    .ej-boards-wrapper {
      display: flex;
      gap: 20px;
      margin-top: 15px;
    }

    .ej-board-column {
      flex: 1;
      min-width: 0;
      /* 防止表格撐破寬度 */
    }

    .ej-device-tabs {
      display: none;
      /* 預設不顯示(寬螢幕) */
      gap: 10px;
      margin-bottom: 15px;
      margin-top: 15px;
    }

    .ej-device-tab {
      flex: 1;
      text-align: center;
      padding: 8px 15px;
      background: #eee;
      border-radius: 6px;
      cursor: pointer;
      font-size: 14px;
    }

    .ej-device-tab.active {
      background: #0056b3;
      color: white;
    }

    /* 當螢幕小於 768px 時切換為 Tabs 模式 */
    @media (max-width: 767px) {
      .ej-boards-wrapper {
        display: block;
      }

      .ej-device-tabs {
        display: flex;
      }

      .ej-hidden-mobile {
        display: none !important;
      }
    }
  </style>

  <div class="ej-container" id="ejApp">
    <div class="ej-header">
      <h2>
        🏆 尹卂搞的打字比賽︱Ejtyperace
        <?php if ($is_admin) : ?>
          <button id="ejAdminToggleShowBtn" class="ej-fs-btn ej-hidden" onclick="ejToggleAdminPanel()" style="margin-left: 10px; padding: 4px;" title="展開管理員面板">
            <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
              <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
              <line x1="9" y1="3" x2="9" y2="21"></line>
            </svg>
          </button>
        <?php endif; ?>
      </h2>
      <div id="ejUserStatus">
        <?php if ($is_logged_in && $user_registered) : ?>
          歡迎, <span id="ejCurrentName"><?php echo esc_html($user_info['name']); ?></span>
          (<?php echo esc_html($user_info['ime']); ?>)
        <?php elseif (!$is_logged_in) : ?>
          <span style="color: #dc3545; font-weight: bold;">請先 <a href="<?php echo wp_login_url(get_permalink()); ?>">登入</a> 以參加打字比賽。</span>
        <?php endif; ?>
      </div>
    </div>

    <div id="ejSectionRegister" class="<?php echo ($is_logged_in && !$user_registered) ? '' : 'ej-hidden'; ?>">
      <h3>請先填寫參賽資料</h3>
      <div class="ej-form-group">
        <label>顯示名稱:</label>
        <input type="text" id="ejRegName" placeholder="不可包含特殊符號">
      </div>
      <div class="ej-form-group">
        <label>輸入法名稱:</label>
        <input type="text" id="ejRegIme" placeholder="例如:注音、倉頡">
      </div>
      <button class="ej-btn" onclick="ejRegisterUser()">保存資料</button>
    </div>

    <div id="ejSectionMain" class="<?php echo (!$is_logged_in || $user_registered) ? '' : 'ej-hidden'; ?>">

      <?php if ($is_admin) : ?>
        <div id="ejAdminWrapperBox" style="background: #e9ecef; padding: 15px; border-radius: 8px; margin-bottom: 20px;">
          <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; border-bottom: 1px solid #ccc; padding-bottom: 5px;">
            <h4 style="margin: 0;">⚙️ 管理員:新增比賽文本</h4>
            <button class="ej-fs-btn" onclick="ejToggleAdminPanel()" style="padding: 4px;" title="最小化面板">
              <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                <line x1="5" y1="12" x2="19" y2="12"></line>
              </svg>
            </button>
          </div>

          <div style="display:flex; gap:10px; margin-bottom: 15px;">
            <input type="number" id="ejAdminMediaId" placeholder="媒體庫 TXT 檔案 ID">
            <input type="text" id="ejAdminTextName" placeholder="文本自訂名稱 (選填)">
            <button class="ej-btn" onclick="ejAdminAddText()">新增/更新</button>
          </div>

          <h4>📋 已加入的文本管理 (點擊媒體庫ID可調整排序)</h4>
          <?php if (!empty($texts_data)) : ?>
            <table class="ej-table" style="background: #fff; border-radius: 6px; box-shadow: 0 2px 5px rgba(0,0,0,0.05);">
              <thead>
                <tr>
                  <th>媒體庫 ID</th>
                  <th>文本名稱</th>
                  <th>字數</th>
                  <th>操作</th>
                </tr>
              </thead>
              <tbody id="ejAdminTextTableBody">
                <?php foreach ($texts_data as $tid => $t) : ?>
                  <tr data-row-id="<?php echo $tid; ?>">
                    <td class="ej-text-id-cell" data-id="<?php echo $tid; ?>" style="cursor: pointer; user-select: none; font-weight: bold; transition: background 0.2s;" onclick="ejHandleTextIdClick(this, <?php echo $tid; ?>)">
                      <code><?php echo esc_html($tid); ?></code>
                    </td>
                    <td style="font-weight: bold;"><?php echo esc_html($t['name']); ?></td>
                    <td><?php echo esc_html($t['length']); ?> 字</td>
                    <td>
                      <button class="ej-btn ej-btn-danger" style="padding: 5px 12px; font-size: 13px;" onclick="ejAdminDeleteText(<?php echo $tid; ?>)">❌ 刪除文本</button>
                    </td>
                  </tr>
                <?php endforeach; ?>
              </tbody>
            </table>

            <div style="margin-top: 15px; display: flex; gap: 10px; align-items: center; flex-wrap: wrap;">
              <button class="ej-btn ej-btn-danger" style="padding: 6px 14px; font-size: 14px;" onclick="ejAdminDeleteAllTexts()">🗑️ 全部刪除</button>
              <button id="ejBtnDeleteSelected" class="ej-btn" style="padding: 6px 14px; font-size: 14px; background: #ffc107; color: #212529;" onclick="ejAdminToggleDeleteSelectedMode()">✏️ 選中刪除</button>
              <button id="ejBtnCancelDelete" class="ej-btn ej-hidden" style="padding: 6px 14px; font-size: 14px; background: #6c757d;" onclick="ejAdminCancelDeleteMode()">取消刪除</button>
              <span id="ejDeletePromptText" class="ej-hidden" style="color: #dc3545; font-weight: bold; font-size: 14px; -webkit-animation: flash 1s infinite alternate; animation: flash 1s infinite alternate;">💡 請選擇要刪除的文本...</span>
            </div>
          <?php else : ?>
            <p style="color: #666; margin: 0; font-style: italic;">目前尚無加入任何文本。</p>
          <?php endif; ?>
        </div>
      <?php endif; ?>

      <div style="display:flex; justify-content:space-between; align-items:center;">
        <h3>排行榜</h3>
        <?php if ($is_logged_in) : ?>
          <button class="ej-btn" onclick="ejShowPreGame()">🚀 開始打字</button>
        <?php endif; ?>
      </div>

      <div id="ejGameResultSummary" class="ej-hidden" style="background: #e6f4ea; color: #137333; padding: 15px; border-radius: 8px; margin-bottom: 20px; border: 1px solid #ceead6;"></div>

      <div class="ej-tabs">
        <div id="ejTabGlobal" class="ej-tab active" onclick="ejSwitchBoard('global')">總排行榜</div>
        <div id="ejTabText" class="ej-tab" onclick="ejSwitchBoard('text')">單篇排行榜</div>
      </div>

      <div id="ejLeaderboardArea">
        <p>載入中...</p>
      </div>
    </div>

    <div id="ejSectionPreGame" class="ej-hidden">
      <h3>選擇比賽項目</h3>
      <div class="ej-form-group">
        <label>選擇文本:</label>
        <select id="ejSelectText">
          <option value="">-- 請選擇 --</option>
          <?php foreach ($texts_data as $tid => $t) : ?>
            <option value="<?php echo esc_attr($tid); ?>"><?php echo esc_html($t['name'] . ' (' . $t['length'] . '字)'); ?></option>
          <?php endforeach; ?>
        </select>
      </div>
      <div class="ej-form-group">
        <label>您的設備 (系統已自動偵測):</label>
        <select id="ejSelectDevice">
          <option value="pc">電腦 (鍵盤)</option>
          <option value="mobile">手機 (觸控)</option>
        </select>
      </div>
      <button class="ej-btn" onclick="ejCancelPreGame()" style="background:#6c757d;">取消</button>
      <button class="ej-btn" onclick="ejStartGame()">進入打字介面</button>
    </div>

    <div id="ejSectionGame" class="ej-game-area">
      <div class="ej-fullscreen-controls">
        <button class="ej-fs-btn" onclick="ejToggleWebFS()" title="網頁內全屏">
          <svg width="36" height="36" viewBox="0 0 24 24">
            <path d="M5 5h5v2H7v3H5V5zm9 0h5v5h-2V7h-3V5zm5 9h-2v3h-3v2h5v-5zm-14 3v-3h2v3h3v2H5v-5z" />
          </svg>
        </button>
        <button class="ej-fs-btn" onclick="ejToggleDeviceFS()" title="設備全屏">
          <svg width="36" height="36" viewBox="0 0 24 24">
            <path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" />
          </svg>
        </button>
      </div>
      <iframe id="ejGameFrame" src="<?php echo $game_url; ?>"></iframe>
    </div>
  </div>

  <script>
    const ejConfig = {
      ajaxurl: '<?php echo admin_url('admin-ajax.php'); ?>',
      page_id: <?php echo $page_id; ?>,
      nonce: '<?php echo wp_create_nonce('ejtype_nonce'); ?>'
    };

    // 用於取代 jQuery.post 的原生 fetch 封裝工具
    function ejPost(url, data, callback) {
      const params = new URLSearchParams();
      for (const key in data) {
        if (Array.isArray(data[key])) {
          // 針對多選刪除等陣列資料,自動轉換為 WordPress 後端能識別的 key[] 格式
          data[key].forEach(val => params.append(`${key}[]`, val));
        } else {
          params.append(key, data[key]);
        }
      }

      fetch(url, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/x-www-form-urlencoded'
          },
          body: params
        })
        .then(response => response.json())
        .then(res => callback(res))
        .catch(err => console.error('AJAX 請求失敗:', err));
    }

    // 狀態變數定義
    let isDeleteSelectedMode = false;
    let selectedOrderTid = null;
    let checkedDeleteTids = [];

    // 設備偵測
    document.addEventListener('DOMContentLoaded', () => {
      const isMobile = window.innerWidth <= 768;
      const deviceSelect = document.getElementById('ejSelectDevice');
      if (deviceSelect) deviceSelect.value = isMobile ? 'mobile' : 'pc';
      ejLoadLeaderboard('global');
    });

    // 管理員欄目顯示/隱藏切換
    function ejToggleAdminPanel() {
      const wrapper = document.getElementById('ejAdminWrapperBox');
      const showBtn = document.getElementById('ejAdminToggleShowBtn');
      if (!wrapper) return;

      if (wrapper.classList.contains('ej-hidden')) {
        wrapper.classList.remove('ej-hidden');
        showBtn.classList.add('ej-hidden');
      } else {
        wrapper.classList.add('ej-hidden');
        showBtn.classList.remove('ej-hidden');
      }
    }

    // 處理媒體庫 ID 點擊事件 (包含排序與多選刪除核心邏輯)
    function ejHandleTextIdClick(element, tid) {
      if (isDeleteSelectedMode) {
        // 多選刪除模式
        const index = checkedDeleteTids.indexOf(tid);
        if (index > -1) {
          checkedDeleteTids.splice(index, 1);
          element.style.background = '';
        } else {
          checkedDeleteTids.push(tid);
          element.style.background = '#ffc107'; // 黃色代表選中
        }
      } else {
        // 排序移動模式
        const allIdCells = Array.from(document.querySelectorAll('.ej-text-id-cell'));

        if (selectedOrderTid === null) {
          selectedOrderTid = tid;
          element.style.background = '#b3d7ff'; // 藍色代表首選高亮
        } else {
          if (selectedOrderTid === tid) {
            // 重複點擊同一個,取消選取
            selectedOrderTid = null;
            element.style.background = '';
            return;
          }

          // 獲取 A 與 B 的元素與所在 Tr
          const cellA = allIdCells.find(c => parseInt(c.dataset.id) === selectedOrderTid);
          const cellB = element;
          if (!cellA || !cellB) return;

          const rowA = cellA.closest('tr');
          const rowB = cellB.closest('tr');

          const indexA = allIdCells.indexOf(cellA);
          const indexB = allIdCells.indexOf(cellB);

          // 依照先後順序插入
          if (indexA < indexB) {
            rowB.parentNode.insertBefore(rowA, rowB.nextSibling); // A 在前,移到 B 後面
          } else {
            rowB.parentNode.insertBefore(rowA, rowB); // A 在後,移到 B 前面
          }

          // 清理樣式與暫存狀態
          cellA.style.background = '';
          selectedOrderTid = null;

          // 非同步儲存新順序至後端
          ejAdminSaveTextOrder();
        }
      }
    }

    // 發送排序數據至後端
    function ejAdminSaveTextOrder() {
      const newOrder = Array.from(document.querySelectorAll('.ej-text-id-cell')).map(c => parseInt(c.dataset.id));

      ejPost(ejConfig.ajaxurl, {
        action: 'ej_admin_reorder_texts',
        nonce: ejConfig.nonce,
        page_id: ejConfig.page_id,
        order: newOrder
      }, function(res) {
        if (!res.success) alert('排序保存失敗: ' + (res.data || '未知錯誤'));
      });
    }

    // 切換至多選刪除模式或執行刪除
    function ejAdminToggleDeleteSelectedMode() {
      const btn = document.getElementById('ejBtnDeleteSelected');
      const cancelBtn = document.getElementById('ejBtnCancelDelete');
      const promptText = document.getElementById('ejDeletePromptText');

      if (!isDeleteSelectedMode) {
        // 進入多選刪除模式
        isDeleteSelectedMode = true;

        // 清除可能殘留的排序高亮
        if (selectedOrderTid !== null) {
          const prevCell = document.querySelector(`.ej-text-id-cell[data-id="${selectedOrderTid}"]`);
          if (prevCell) prevCell.style.background = '';
          selectedOrderTid = null;
        }

        cancelBtn.classList.remove('ej-hidden');
        promptText.classList.remove('ej-hidden');
        btn.innerText = '🔥 執行刪除';
        btn.classList.add('ej-btn-danger');
      } else {
        // 執行多選刪除
        if (checkedDeleteTids.length === 0) {
          alert('請先點擊文本媒體庫 ID 進行選擇!');
          return;
        }

        if (!confirm(`確定要刪除已選中的 ${checkedDeleteTids.length} 項文本及其關聯成績嗎?此動作無法撤銷!`)) return;

        ejPost(ejConfig.ajaxurl, {
          action: 'ej_admin_delete_multiple_texts',
          nonce: ejConfig.nonce,
          page_id: ejConfig.page_id,
          text_ids: checkedDeleteTids
        }, function(res) {
          if (res.success) {
            alert('選中刪除成功!');
            location.reload();
          } else {
            alert(res.data || '刪除失敗');
          }
        });
      }
    }

    // 取消多選刪除模式
    function ejAdminCancelDeleteMode() {
      isDeleteSelectedMode = false;
      checkedDeleteTids = [];

      document.querySelectorAll('.ej-text-id-cell').forEach(c => c.style.background = '');
      document.getElementById('ejBtnCancelDelete').classList.add('ej-hidden');
      document.getElementById('ejDeletePromptText').classList.add('ej-hidden');

      const btn = document.getElementById('ejBtnDeleteSelected');
      btn.innerText = '✏️ 選中刪除';
      btn.classList.remove('ej-btn-danger');
    }

    // 全部刪除文本與數據
    function ejAdminDeleteAllTexts() {
      if (!confirm('⚠️ 警告:這將會清空「所有」參賽文本以及整個打字系統的排行榜數據!此操作不可逆,確定要繼續嗎?')) return;

      ejPost(ejConfig.ajaxurl, {
        action: 'ej_admin_delete_all_texts',
        nonce: ejConfig.nonce,
        page_id: ejConfig.page_id
      }, function(res) {
        if (res.success) {
          alert('所有文本與排行榜數據已成功清空!');
          location.reload();
        } else {
          alert(res.data || '清空失敗');
        }
      });
    }

    // 註冊用戶
    function ejRegisterUser() {
      const name = document.getElementById('ejRegName').value.trim();
      const ime = document.getElementById('ejRegIme').value.trim();
      if (!name || !ime) return alert('請填寫完整資訊');

      ejPost(ejConfig.ajaxurl, {
        action: 'ej_save_user',
        nonce: ejConfig.nonce,
        page_id: ejConfig.page_id,
        name: name,
        ime: ime
      }, function(res) {
        if (res.success) location.reload();
        else alert(res.data);
      });
    }

    // 管理員新增文本
    function ejAdminAddText() {
      const mediaId = document.getElementById('ejAdminMediaId').value;
      const name = document.getElementById('ejAdminTextName').value;
      if (!mediaId) return alert('請輸入媒體庫ID');

      ejPost(ejConfig.ajaxurl, {
        action: 'ej_admin_text',
        nonce: ejConfig.nonce,
        page_id: ejConfig.page_id,
        media_id: mediaId,
        name: name
      }, function(res) {
        if (res.success) {
          alert('新增成功');
          location.reload();
        } else {
          alert(res.data);
        }
      });
    }

    // 管理員個別刪除文本
    function ejAdminDeleteText(textId) {
      if (!confirm('確定要刪除此文本嗎?這將會從比賽清單中移除該文本並清除其對應的排行榜成績(不會影響媒體庫原始檔案)。')) return;

      ejPost(ejConfig.ajaxurl, {
        action: 'ej_admin_delete_text',
        nonce: ejConfig.nonce,
        page_id: ejConfig.page_id,
        text_id: textId
      }, function(res) {
        if (res.success) {
          alert('刪除成功!');
          location.reload();
        } else {
          alert(res.data || '刪除失敗');
        }
      });
    }

    // UI 切換
    function ejShowPreGame() {
      const summaryDiv = document.getElementById('ejGameResultSummary');
      if (summaryDiv) summaryDiv.classList.add('ej-hidden');

      document.getElementById('ejSectionMain').classList.add('ej-hidden');
      document.getElementById('ejSectionPreGame').classList.remove('ej-hidden');
    }

    function ejCancelPreGame() {
      document.getElementById('ejSectionPreGame').classList.add('ej-hidden');
      document.getElementById('ejSectionMain').classList.remove('ej-hidden');
    }

    let currentTextId = '';
    let currentDevice = '';

    // 開始遊戲
    function ejStartGame() {
      currentTextId = document.getElementById('ejSelectText').value;
      currentDevice = document.getElementById('ejSelectDevice').value;

      if (!currentTextId) return alert('請選擇文本');

      ejPost(ejConfig.ajaxurl, {
        action: 'ej_get_text_content',
        nonce: ejConfig.nonce,
        page_id: ejConfig.page_id,
        text_id: currentTextId
      }, function(res) {
        if (res.success) {
          document.getElementById('ejSectionPreGame').classList.add('ej-hidden');
          document.getElementById('ejSectionGame').style.display = 'block';

          const iframe = document.getElementById('ejGameFrame');
          iframe.contentWindow.postMessage({
            action: 'load_wp_text',
            title: res.data.name,
            content: res.data.content
          }, '*');
          const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
          const rowHeightInput = iframeDoc.getElementById('typegamerowheight');
          if (rowHeightInput) {
            // 手機端為 2,電腦端為 5
            rowHeightInput.value = (currentDevice === 'mobile') ? '2' : '5';
            // 觸發事件確保內部遊戲框架能同步更新
            rowHeightInput.dispatchEvent(new Event('input', {
              bubbles: true
            }));
            rowHeightInput.dispatchEvent(new Event('change', {
              bubbles: true
            }));
          }
        } else {
          alert('讀取文本失敗');
        }
      });
    }

    // 接收 iframe 成績
    window.addEventListener('message', (event) => {
      if (event.data && event.data.action === 'ejtype_game_over') {
        const timeUsed = event.data.time;

        document.getElementById('ejSectionGame').style.display = 'none';
        if (document.fullscreenElement) document.exitFullscreen();
        document.getElementById('ejSectionGame').classList.remove('web-fullscreen');

        ejPost(ejConfig.ajaxurl, {
          action: 'ej_save_score',
          nonce: ejConfig.nonce,
          page_id: ejConfig.page_id,
          text_id: currentTextId,
          device: currentDevice,
          time: timeUsed
        }, function(res) {
          if (res.success) {
            const data = res.data;
            let summaryHtml = `<h4 style="margin: 0 0 10px 0; font-size: 18px;">🎯 挑戰完成回報</h4>`;
            summaryHtml += `<p style="margin: 4px 0;">⏱️ <strong>本次用時:</strong>${data.time.toFixed(1)} 秒 | ⚡ <strong>本次速度:</strong>${data.speed} 字/分</p>`;
            summaryHtml += `<p style="margin: 4px 0;">🏆 <strong>您的歷史最佳:</strong>${data.best_time.toFixed(1)} 秒 (${data.best_speed} 字/分)${data.is_new_best ? ' <span style="color: #dc3545; font-weight: bold; -webkit-animation: flash 1s infinite alternate; animation: flash 1s infinite alternate;">🔥 刷新個人紀錄!</span>' : ''}</p>`;

            const summaryDiv = document.getElementById('ejGameResultSummary');
            if (summaryDiv) {
              summaryDiv.innerHTML = summaryHtml;
              summaryDiv.classList.remove('ej-hidden');
            }
          }

          document.getElementById('ejSectionMain').classList.remove('ej-hidden');

          document.querySelectorAll('.ej-tab').forEach(t => t.classList.remove('active'));
          const tabText = document.getElementById('ejTabText');
          if (tabText) tabText.classList.add('active');

          ejLoadLeaderboard('text', currentTextId);
        });
      }
    });

    // 排行榜載入
    function ejLoadLeaderboard(type, textId = '') {
      document.getElementById('ejLeaderboardArea').innerHTML = '載入中...';
      ejPost(ejConfig.ajaxurl, {
        action: 'ej_get_leaderboard',
        nonce: ejConfig.nonce,
        page_id: ejConfig.page_id,
        type: type,
        text_id: textId
      }, function(res) {
        if (res.success) document.getElementById('ejLeaderboardArea').innerHTML = res.data;
      });
    }

    function ejSwitchBoard(type) {
      const summaryDiv = document.getElementById('ejGameResultSummary');
      if (summaryDiv) summaryDiv.classList.add('ej-hidden');

      document.querySelectorAll('.ej-tab').forEach(t => t.classList.remove('active'));
      if (window.event && window.event.target) {
        window.event.target.classList.add('active');
      } else {
        const targetId = type === 'global' ? 'ejTabGlobal' : 'ejTabText';
        const targetEl = document.getElementById(targetId);
        if (targetEl) targetEl.classList.add('active');
      }

      let tid = type === 'text' ? (currentTextId || document.getElementById('ejSelectText').value) : '';
      ejLoadLeaderboard(type, tid);
    }

    // 切換手機版「電腦/手機」設備排行榜標籤
    function ejSwitchDeviceTab(device, element) {
      const parent = element.parentElement;
      parent.querySelectorAll('.ej-device-tab').forEach(t => t.classList.remove('active'));
      element.classList.add('active');

      const wrapper = parent.nextElementSibling;
      if (device === 'pc') {
        wrapper.querySelector('.ej-board-pc').classList.remove('ej-hidden-mobile');
        wrapper.querySelector('.ej-board-mobile').classList.add('ej-hidden-mobile');
      } else {
        wrapper.querySelector('.ej-board-pc').classList.add('ej-hidden-mobile');
        wrapper.querySelector('.ej-board-mobile').classList.remove('ej-hidden-mobile');
      }
    }

    // 全螢幕控制
    function ejToggleWebFS() {
      document.getElementById('ejSectionGame').classList.toggle('web-fullscreen');
    }

    function ejToggleDeviceFS() {
      const elem = document.getElementById('ejSectionGame');
      if (!document.fullscreenElement) {
        elem.requestFullscreen().catch(err => alert('全螢幕請求被拒絕'));
      } else {
        document.exitFullscreen();
      }
    }
  </script>
<?php
  return ob_get_clean();
}

// ---------------- 後端 AJAX 處理 ----------------

function ejtype_clean_input($val)
{
  return str_replace(['\\', '"', "'"], '', sanitize_text_field($val));
}

// 儲存用戶資料
add_action('wp_ajax_ej_save_user', 'ej_ajax_save_user');
function ej_ajax_save_user()
{
  check_ajax_referer('ejtype_nonce', 'nonce');
  $page_id = intval($_POST['page_id']);
  $uid = get_current_user_id();

  $users = get_post_meta($page_id, '_ejtype_users', true) ?: [];
  $users[$uid] = [
    'name' => ejtype_clean_input($_POST['name']),
    'ime' => ejtype_clean_input($_POST['ime'])
  ];
  update_post_meta($page_id, '_ejtype_users', $users);
  wp_send_json_success();
}

// 管理員新增文本
add_action('wp_ajax_ej_admin_text', 'ej_ajax_admin_text');
function ej_ajax_admin_text()
{
  check_ajax_referer('ejtype_nonce', 'nonce');
  if (!current_user_can('manage_options')) wp_send_json_error('權限不足');

  $page_id = intval($_POST['page_id']);
  $media_id = intval($_POST['media_id']);

  $file_path = get_attached_file($media_id);
  if (!$file_path || !file_exists($file_path)) wp_send_json_error('找不到該媒體檔案');

  $content = file_get_contents($file_path);
  $clean_content = preg_replace('/\s+/u', '', $content);
  $length = mb_strlen($clean_content, 'UTF-8');

  $name = !empty($_POST['name']) ? ejtype_clean_input($_POST['name']) : get_the_title($media_id);

  $texts = get_post_meta($page_id, '_ejtype_texts', true) ?: [];
  $texts[$media_id] = [
    'name' => $name,
    'length' => $length
  ];
  update_post_meta($page_id, '_ejtype_texts', $texts);
  wp_send_json_success();
}

// 【新增後端處理】保存變更後的文本排序
add_action('wp_ajax_ej_admin_reorder_texts', 'ej_ajax_admin_reorder_texts');
function ej_ajax_admin_reorder_texts()
{
  check_ajax_referer('ejtype_nonce', 'nonce');
  if (!current_user_can('manage_options')) wp_send_json_error('權限不足');

  $page_id = intval($_POST['page_id']);
  $new_order = isset($_POST['order']) ? array_map('intval', $_POST['order']) : [];

  $texts = get_post_meta($page_id, '_ejtype_texts', true) ?: [];
  $reordered_texts = [];

  // 根據前端傳過來的 ID 順序重新構建關聯陣列
  foreach ($new_order as $tid) {
    if (isset($texts[$tid])) {
      $reordered_texts[$tid] = $texts[$tid];
    }
  }

  // 安全檢查:若有漏網之魚則補回陣列尾端
  foreach ($texts as $tid => $t) {
    if (!isset($reordered_texts[$tid])) {
      $reordered_texts[$tid] = $t;
    }
  }

  update_post_meta($page_id, '_ejtype_texts', $reordered_texts);
  wp_send_json_success();
}

// 【新增後端處理】多選選中刪除
add_action('wp_ajax_ej_admin_delete_multiple_texts', 'ej_ajax_admin_delete_multiple_texts');
function ej_ajax_admin_delete_multiple_texts()
{
  check_ajax_referer('ejtype_nonce', 'nonce');
  if (!current_user_can('manage_options')) wp_send_json_error('權限不足');

  $page_id = intval($_POST['page_id']);
  $text_ids = isset($_POST['text_ids']) ? array_map('intval', $_POST['text_ids']) : [];

  $texts = get_post_meta($page_id, '_ejtype_texts', true) ?: [];
  $results = get_post_meta($page_id, '_ejtype_results', true) ?: [];

  foreach ($text_ids as $tid) {
    if (isset($texts[$tid])) unset($texts[$tid]);
    if (isset($results[$tid])) unset($results[$tid]);
  }

  update_post_meta($page_id, '_ejtype_texts', $texts);
  update_post_meta($page_id, '_ejtype_results', $results);
  wp_send_json_success();
}

// 【新增後端處理】全部刪除
add_action('wp_ajax_ej_admin_delete_all_texts', 'ej_ajax_admin_delete_all_texts');
function ej_ajax_admin_delete_all_texts()
{
  check_ajax_referer('ejtype_nonce', 'nonce');
  if (!current_user_can('manage_options')) wp_send_json_error('權限不足');

  $page_id = intval($_POST['page_id']);

  update_post_meta($page_id, '_ejtype_texts', []);
  update_post_meta($page_id, '_ejtype_results', []);
  wp_send_json_success();
}

// 取得文本內容給前端
add_action('wp_ajax_ej_get_text_content', 'ej_ajax_get_text_content');
function ej_ajax_get_text_content()
{
  $page_id = intval($_POST['page_id']);
  $text_id = intval($_POST['text_id']);
  $texts = get_post_meta($page_id, '_ejtype_texts', true) ?: [];

  $file_path = get_attached_file($text_id);
  if ($file_path && file_exists($file_path)) {
    wp_send_json_success([
      'name' => $texts[$text_id]['name'],
      'content' => file_get_contents($file_path)
    ]);
  }
  wp_send_json_error();
}

// 儲存成績
add_action('wp_ajax_ej_save_score', 'ej_ajax_save_score');
function ej_ajax_save_score()
{
  $page_id = intval($_POST['page_id']);
  $uid = get_current_user_id();
  $text_id = intval($_POST['text_id']);
  $device = sanitize_text_field($_POST['device']);
  $time = floatval($_POST['time']);

  $results = get_post_meta($page_id, '_ejtype_results', true) ?: [];

  if (!isset($results[$text_id])) $results[$text_id] = ['pc' => [], 'mobile' => []];

  $history_best = isset($results[$text_id][$device][$uid]) ? $results[$text_id][$device][$uid] : null;
  $is_new_best = false;

  if ($history_best === null || $time < $history_best) {
    $results[$text_id][$device][$uid] = $time;
    $history_best = $time;
    $is_new_best = true;
  }

  update_post_meta($page_id, '_ejtype_results', $results);

  $texts = get_post_meta($page_id, '_ejtype_texts', true) ?: [];
  $length = isset($texts[$text_id]['length']) ? $texts[$text_id]['length'] : 0;

  $current_speed = $length > 0 ? round($length / ($time / 60), 1) : 0;
  $best_speed = $length > 0 ? round($length / ($history_best / 60), 1) : 0;

  wp_send_json_success([
    'time'        => $time,
    'speed'       => $current_speed,
    'best_time'   => $history_best,
    'best_speed'  => $best_speed,
    'is_new_best' => $is_new_best
  ]);
}

// 獲取排行榜 HTML
add_action('wp_ajax_ej_get_leaderboard', 'ej_ajax_get_leaderboard');
add_action('wp_ajax_nopriv_ej_get_leaderboard', 'ej_ajax_get_leaderboard');
function ej_ajax_get_leaderboard()
{
  $page_id = intval($_POST['page_id']);
  $type = sanitize_text_field($_POST['type']);
  $text_id = intval($_POST['text_id']);

  $users = get_post_meta($page_id, '_ejtype_users', true) ?: [];
  $texts = get_post_meta($page_id, '_ejtype_texts', true) ?: [];
  $results = get_post_meta($page_id, '_ejtype_results', true) ?: [];

  ob_start();

  if ($type === 'text') {
    echo '<div style="margin-bottom: 20px; background: #f8f9fa; padding: 12px; border-radius: 6px; border: 1px solid #eee;">';
    echo '<label style="font-weight: bold; margin-right: 10px;">切換檢視文本:</label>';
    echo '<select id="ejLeaderboardTextSelect" style="padding: 6px 12px; border-radius: 4px; border: 1px solid #ccc; max-width: 100%;" onchange="ejLoadLeaderboard(\'text\', this.value)">';
    echo '<option value="">-- 請選擇比賽文本 --</option>';
    foreach ($texts as $tid => $t) {
      $selected = ($tid == $text_id) ? 'selected' : '';
      echo "<option value='" . esc_attr($tid) . "' {$selected}>" . esc_html($t['name']) . " (" . esc_html($t['length']) . "字)</option>";
    }
    echo '</select>';
    echo '</div>';

    if (!$text_id || !isset($texts[$text_id])) {
      echo '<p style="color: #666; font-style: italic; text-align: center; padding: 30px 0;">💡 請先點擊上方選單切換文本,或點擊右上方「🚀 開始打字」完成一場參賽項目。</p>';
      wp_send_json_success(ob_get_clean());
      return;
    }

    $t_info = $texts[$text_id];
    echo "<div style='margin-bottom:10px;'><strong>當前文本:</strong> {$t_info['name']} ({$t_info['length']} 字)</div>";

    echo '<div class="ej-device-tabs">
            <div class="ej-device-tab active" onclick="ejSwitchDeviceTab(\'pc\', this)">💻 電腦排名</div>
            <div class="ej-device-tab" onclick="ejSwitchDeviceTab(\'mobile\', this)">📱 手機排名</div>
          </div>';

    echo '<div class="ej-boards-wrapper">';
    foreach (['pc' => '💻 電腦鍵盤排名', 'mobile' => '📱 手機觸屏排名'] as $dev => $title) {
      $hide_class = ($dev === 'mobile') ? ' ej-hidden-mobile' : '';
      echo "<div class='ej-board-column ej-board-{$dev}{$hide_class}'>";
      echo "<h4>{$title}</h4>";
      $scores = isset($results[$text_id][$dev]) ? $results[$text_id][$dev] : [];
      asort($scores);

      echo "<table class='ej-table'><tr><th>排名</th><th>選手 (輸入法)</th><th>用時 (秒)</th><th>速度 (字/分)</th></tr>";
      $rank = 1;
      foreach ($scores as $uid => $time) {
        if (!isset($users[$uid])) continue;
        $speed = round($t_info['length'] / ($time / 60), 1);
        $u = $users[$uid];
        echo "<tr><td>{$rank}</td><td>{$u['name']} ({$u['ime']})</td><td>" . number_format($time, 1) . "</td><td>{$speed}</td></tr>";
        $rank++;
      }
      if (empty($scores)) echo "<tr><td colspan='4'>暫無成績</td></tr>";
      echo "</table>";
      echo "</div>";
    }
    echo '</div>';
  } else {
    $all_scores = ['pc' => [], 'mobile' => []];
    foreach ($results as $tid => $dev_data) {
      if (!isset($texts[$tid])) continue;
      $len = $texts[$tid]['length'];
      foreach ($dev_data as $dev => $user_times) {
        foreach ($user_times as $uid => $time) {
          $speed = round($len / ($time / 60), 1);
          $all_scores[$dev][] = ['uid' => $uid, 'tid' => $tid, 'speed' => $speed];
        }
      }
    }

    echo '<div class="ej-device-tabs">
            <div class="ej-device-tab active" onclick="ejSwitchDeviceTab(\'pc\', this)">💻 電腦總排名</div>
            <div class="ej-device-tab" onclick="ejSwitchDeviceTab(\'mobile\', this)">📱 手機總排名</div>
          </div>';

    echo '<div class="ej-boards-wrapper">';
    foreach (['pc' => '💻 電腦鍵盤總排名', 'mobile' => '📱 手機觸屏總排名'] as $dev => $title) {
      $hide_class = ($dev === 'mobile') ? ' ej-hidden-mobile' : '';
      echo "<div class='ej-board-column ej-board-{$dev}{$hide_class}'>";
      echo "<h4>{$title}</h4>";
      $dev_scores = $all_scores[$dev];
      usort($dev_scores, function ($a, $b) {
        return $b['speed'] <=> $a['speed'];
      });

      echo "<table class='ej-table'><tr><th>排名</th><th>選手 (輸入法)</th><th>文本名稱</th><th>字數</th><th>速度 (字/分)</th></tr>";
      $rank = 1;
      foreach ($dev_scores as $s) {
        if (!isset($users[$s['uid']])) continue;
        $u = $users[$s['uid']];
        $t = $texts[$s['tid']];
        echo "<tr><td>{$rank}</td><td>{$u['name']} ({$u['ime']})</td><td>{$t['name']}</td><td>{$t['length']}</td><td>{$s['speed']}</td></tr>";
        $rank++;
      }
      if (empty($dev_scores)) echo "<tr><td colspan='5'>暫無成績</td></tr>";
      echo "</table>";
      echo "</div>";
    }
    echo '</div>';
  }

  wp_send_json_success(ob_get_clean());
}

// 管理員刪除單一文本與相關成績
add_action('wp_ajax_ej_admin_delete_text', 'ej_ajax_admin_delete_text');
function ej_ajax_admin_delete_text()
{
  check_ajax_referer('ejtype_nonce', 'nonce');
  if (!current_user_can('manage_options')) wp_send_json_error('權限不足');

  $page_id = intval($_POST['page_id']);
  $text_id = intval($_POST['text_id']);

  $texts = get_post_meta($page_id, '_ejtype_texts', true) ?: [];
  if (isset($texts[$text_id])) {
    unset($texts[$text_id]);
    update_post_meta($page_id, '_ejtype_texts', $texts);

    $results = get_post_meta($page_id, '_ejtype_results', true) ?: [];
    if (isset($results[$text_id])) {
      unset($results[$text_id]);
      update_post_meta($page_id, '_ejtype_results', $results);
    }

    wp_send_json_success();
  }

  wp_send_json_error('找不到該文本數據');
}
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5749
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 179 次
被圈友点赞: 207 次
联系:

Re: 用wordpress搞個打字比賽

帖子 ejsoon »

如果是觸屏打完應能自動切換至觸屏排行。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5749
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 179 次
被圈友点赞: 207 次
联系:

Re: 用wordpress搞個打字比賽

帖子 ejsoon »

@鶴飛四季:要不要來玩玩?我不知道你現在用的是什麼輸入法,可以參加比賽來曬一曬你現在的輸入法。

https://ejsoon.vip/ejtyperace/
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5749
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 179 次
被圈友点赞: 207 次
联系:

Re: 用wordpress搞個打字比賽

帖子 ejsoon »

要隨時可以改自己的輸入法,碼表,及軟體。

代码: 全选

<?php
// 註冊短代碼 [ejtype_competition]
add_shortcode('ejtype_competition', 'ejtype_competition_render');

function ejtype_competition_render()
{
  $is_logged_in = is_user_logged_in(); // 檢查登入狀態
  $page_id = get_the_ID();
  $current_user_id = get_current_user_id();
  $is_admin = $is_logged_in && current_user_can('manage_options');

  // 取得 Page Meta 數據
  $users_data = get_post_meta($page_id, '_ejtype_users', true) ?: [];
  $texts_data = get_post_meta($page_id, '_ejtype_texts', true) ?: [];

  // 取得遊戲主程式媒體庫 ID 及其網址
  $game_url = wp_get_attachment_url(19258);

  // 必須在登入情況下才去檢查是否註冊過
  $user_registered = $is_logged_in && isset($users_data[$current_user_id]);
  $user_info = $user_registered ? $users_data[$current_user_id] : null;

  ob_start();
?>
  <style>
    /* --- 現代化 CSS 樣式 --- */
    .ej-container {
      font-family: '微軟正黑體', sans-serif;
      margin: 0 auto;
      background: #fff;
      border-radius: 12px;
      box-shadow: 0 8px 20px rgba(0, 0, 0, 0.08);
      padding: 25px;
      color: #333;
    }

    .ej-header {
      border-bottom: 2px solid #f0f0f0;
      padding-bottom: 15px;
      margin-bottom: 20px;
      display: flex;
      justify-content: space-between;
      align-items: center;
    }

    .ej-header h2 {
      margin: 0;
      color: #0056b3;
      font-size: 24px;
      display: flex;
      align-items: center;
    }

    .ej-btn {
      background: #0056b3;
      color: #fff;
      border: none;
      padding: 10px 20px;
      border-radius: 6px;
      cursor: pointer;
      transition: 0.3s;
      font-size: 15px;
    }

    .ej-btn:hover {
      background: #004494;
    }

    .ej-btn-danger {
      background: #dc3545;
    }

    .ej-btn-danger:hover {
      background: #bd2130;
    }

    .ej-form-group {
      margin-bottom: 15px;
    }

    .ej-form-group label {
      display: block;
      margin-bottom: 5px;
      font-weight: bold;
    }

    .ej-form-group input,
    .ej-form-group select {
      width: 100%;
      padding: 10px;
      border: 1px solid #ddd;
      border-radius: 6px;
    }

    .ej-table {
      width: 100%;
      border-collapse: collapse;
      margin-top: 15px;
    }

    .ej-table th,
    .ej-table td {
      padding: 12px;
      border-bottom: 1px solid #eee;
      text-align: left;
    }

    .ej-table th {
      background: #f8f9fa;
      font-weight: bold;
    }

    /* 遊戲區域 & 全螢幕控制 */
    .ej-game-area {
      display: none;
      position: relative;
      border-radius: 8px;
      overflow: hidden;
      background: #f4f4f4;
      border: 2px solid #ddd;
      height: 80vh;
    }

    .ej-game-area iframe {
      width: 100%;
      height: 100%;
      border: none;
    }

    .ej-fullscreen-controls {
      position: absolute;
      top: 10px;
      right: 10px;
      display: flex;
      gap: 10px;
      z-index: 100;
    }

    .ej-fs-btn {
      background: rgba(0, 0, 0, 0.5);
      border: none;
      border-radius: 4px;
      padding: 5px;
      cursor: pointer;
      display: flex;
      fill: white;
      transition: 0.2s;
    }

    .ej-fs-btn:hover {
      background: rgba(0, 0, 0, 0.8);
    }

    .ej-game-area.web-fullscreen {
      position: fixed;
      top: 0;
      left: 0;
      width: 100vw;
      height: 100vh;
      z-index: 9999999;
      border: none;
      border-radius: 0;
    }

    /* 隱藏區塊 */
    .ej-hidden {
      display: none !important;
    }

    .ej-tabs {
      display: flex;
      gap: 10px;
      margin-bottom: 15px;
    }

    .ej-tab {
      padding: 8px 15px;
      background: #eee;
      border-radius: 6px;
      cursor: pointer;
    }

    .ej-tab.active {
      background: #0056b3;
      color: white;
    }

    /* --- 設備排行榜並列與手機標籤切換樣式 --- */
    .ej-boards-wrapper {
      display: flex;
      gap: 20px;
      margin-top: 15px;
    }

    .ej-board-column {
      flex: 1;
      min-width: 0;
      /* 防止表格撐破寬度 */
    }

    .ej-device-tabs {
      display: none;
      /* 預設不顯示(寬螢幕) */
      gap: 10px;
      margin-bottom: 15px;
      margin-top: 15px;
    }

    .ej-device-tab {
      flex: 1;
      text-align: center;
      padding: 8px 15px;
      background: #eee;
      border-radius: 6px;
      cursor: pointer;
      font-size: 14px;
    }

    .ej-device-tab.active {
      background: #0056b3;
      color: white;
    }

    /* 當螢幕小於 768px 時切換為 Tabs 模式 */
    @media (max-width: 767px) {
      .ej-header {
        flex-direction: column;
        align-items: flex-start;
        justify-content: flex-start;
      }

      .ej-header>h2 {
        width: 100%;
      }

      .ej-header>#ejUserStatus {
        font-size: 12px;
        align-self: flex-end;
        margin-top: 4px;
      }

      .ej-boards-wrapper {
        display: block;
      }

      .ej-device-tabs {
        display: flex;
      }

      .ej-hidden-mobile {
        display: none !important;
      }
    }
  </style>

  <div class="ej-container" id="ejApp">
    <div class="ej-header">
      <h2>
        🏆 Ejtyperace
        <?php if ($is_admin) : ?>
          <button id="ejAdminToggleShowBtn" class="ej-fs-btn ej-hidden" onclick="ejToggleAdminPanel()" style="margin-left: 10px; padding: 4px;" title="展開管理員面板">
            <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
              <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
              <line x1="9" y1="3" x2="9" y2="21"></line>
            </svg>
          </button>
        <?php endif; ?>
      </h2>
      <div id="ejUserStatus">
        <?php if ($is_logged_in && $user_registered) : ?>
          歡迎, <span id="ejCurrentName"><?php echo esc_html($user_info['name']); ?></span>
          (<?php echo esc_html($user_info['ime']); ?>)
        <?php elseif (!$is_logged_in) : ?>
          <span style="color: #dc3545; font-weight: bold;">請先 <a href="<?php echo wp_login_url(get_permalink()); ?>">登入</a> 以參加打字比賽。</span>
        <?php endif; ?>
      </div>
    </div>

    <div id="ejSectionRegister" class="<?php echo ($is_logged_in && !$user_registered) ? '' : 'ej-hidden'; ?>">
      <h3>請先填寫參賽資料</h3>
      <div class="ej-form-group">
        <label>顯示名稱:</label>
        <input type="text" id="ejRegName" placeholder="不可包含特殊符號">
      </div>
      <div class="ej-form-group">
        <label>輸入法名稱:</label>
        <input type="text" id="ejRegIme" placeholder="例如:注音、倉頡">
      </div>
      <button class="ej-btn" onclick="ejRegisterUser()">保存資料</button>
    </div>

    <div id="ejSectionMain" class="<?php echo (!$is_logged_in || $user_registered) ? '' : 'ej-hidden'; ?>">

      <?php if ($is_admin) : ?>
        <div id="ejAdminWrapperBox" style="background: #e9ecef; padding: 15px; border-radius: 8px; margin-bottom: 20px;">
          <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; border-bottom: 1px solid #ccc; padding-bottom: 5px;">
            <h4 style="margin: 0;">⚙️ 管理員:新增比賽文本</h4>
            <button class="ej-fs-btn" onclick="ejToggleAdminPanel()" style="padding: 4px;" title="最小化面板">
              <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                <line x1="5" y1="12" x2="19" y2="12"></line>
              </svg>
            </button>
          </div>

          <div style="display:flex; gap:10px; margin-bottom: 15px;">
            <input type="number" id="ejAdminMediaId" placeholder="媒體庫 TXT 檔案 ID" style="min-width: 0;">
            <input type="text" id="ejAdminTextName" placeholder="文本自訂名稱 (選填)" style="min-width: 0;">
            <button class="ej-btn" onclick="ejAdminAddText()">新增/更新</button>
          </div>

          <h4>📋 已加入的文本管理 (點擊媒體庫ID可調整排序)</h4>
          <?php if (!empty($texts_data)) : ?>
            <table class="ej-table" style="background: #fff; border-radius: 6px; box-shadow: 0 2px 5px rgba(0,0,0,0.05);">
              <thead>
                <tr>
                  <th>媒體庫 ID</th>
                  <th>文本名稱</th>
                  <th>字數</th>
                  <th>操作</th>
                </tr>
              </thead>
              <tbody id="ejAdminTextTableBody">
                <?php foreach ($texts_data as $tid => $t) : ?>
                  <tr data-row-id="<?php echo $tid; ?>">
                    <td class="ej-text-id-cell" data-id="<?php echo $tid; ?>" style="cursor: pointer; user-select: none; font-weight: bold; transition: background 0.2s;" onclick="ejHandleTextIdClick(this, <?php echo $tid; ?>)">
                      <code><?php echo esc_html($tid); ?></code>
                    </td>
                    <td style="font-weight: bold;"><?php echo esc_html($t['name']); ?></td>
                    <td><?php echo esc_html($t['length']); ?> 字</td>
                    <td>
                      <button class="ej-btn ej-btn-danger" style="padding: 5px 12px; font-size: 13px;" onclick="ejAdminDeleteText(<?php echo $tid; ?>)">❌ 刪除文本</button>
                    </td>
                  </tr>
                <?php endforeach; ?>
              </tbody>
            </table>

            <div style="margin-top: 15px; display: flex; gap: 10px; align-items: center; flex-wrap: wrap;">
              <button class="ej-btn ej-btn-danger" style="padding: 6px 14px; font-size: 14px;" onclick="ejAdminDeleteAllTexts()">🗑️ 全部刪除</button>
              <button id="ejBtnDeleteSelected" class="ej-btn" style="padding: 6px 14px; font-size: 14px; background: #ffc107; color: #212529;" onclick="ejAdminToggleDeleteSelectedMode()">✏️ 選中刪除</button>
              <button id="ejBtnCancelDelete" class="ej-btn ej-hidden" style="padding: 6px 14px; font-size: 14px; background: #6c757d;" onclick="ejAdminCancelDeleteMode()">取消刪除</button>
              <span id="ejDeletePromptText" class="ej-hidden" style="color: #dc3545; font-weight: bold; font-size: 14px; -webkit-animation: flash 1s infinite alternate; animation: flash 1s infinite alternate;">💡 請選擇要刪除的文本...</span>
            </div>
          <?php else : ?>
            <p style="color: #666; margin: 0; font-style: italic;">目前尚無加入任何文本。</p>
          <?php endif; ?>
        </div>
      <?php endif; ?>

      <div style="display:flex; justify-content:space-between; align-items:center;">
        <h3>排行榜</h3>
        <?php if ($is_logged_in) : ?>
          <button class="ej-btn" onclick="ejShowPreGame()">🚀 開始打字</button>
        <?php endif; ?>
      </div>

      <div id="ejGameResultSummary" class="ej-hidden" style="background: #e6f4ea; color: #137333; padding: 15px; border-radius: 8px; margin-bottom: 20px; border: 1px solid #ceead6;"></div>

      <div class="ej-tabs">
        <div id="ejTabGlobal" class="ej-tab active" onclick="ejSwitchBoard('global')">總排行榜</div>
        <div id="ejTabText" class="ej-tab" onclick="ejSwitchBoard('text')">單篇排行榜</div>
      </div>

      <div id="ejLeaderboardArea">
        <p>載入中...</p>
      </div>
    </div>

    <div id="ejSectionPreGame" class="ej-hidden">
      <h3>選擇比賽項目</h3>
      <div class="ej-form-group">
        <label>選擇文本:</label>
        <select id="ejSelectText">
          <option value="">-- 請選擇 --</option>
          <?php foreach ($texts_data as $tid => $t) : ?>
            <option value="<?php echo esc_attr($tid); ?>"><?php echo esc_html($t['name'] . ' (' . $t['length'] . '字)'); ?></option>
          <?php endforeach; ?>
        </select>
      </div>
      <div class="ej-form-group">
        <label>您的設備 (系統已自動偵測):</label>
        <select id="ejSelectDevice">
          <option value="pc">電腦 (鍵盤)</option>
          <option value="mobile">手機 (觸控)</option>
        </select>
      </div>
      <button class="ej-btn" onclick="ejCancelPreGame()" style="background:#6c757d;">取消</button>
      <button class="ej-btn" onclick="ejStartGame()">進入打字介面</button>
    </div>

    <div id="ejSectionGame" class="ej-game-area">
      <div class="ej-fullscreen-controls">
        <button class="ej-fs-btn" onclick="ejToggleWebFS()" title="網頁內全屏">
          <svg width="36" height="36" viewBox="0 0 24 24">
            <path d="M5 5h5v2H7v3H5V5zm9 0h5v5h-2V7h-3V5zm5 9h-2v3h-3v2h5v-5zm-14 3v-3h2v3h3v2H5v-5z" />
          </svg>
        </button>
        <button class="ej-fs-btn" onclick="ejToggleDeviceFS()" title="設備全屏">
          <svg width="36" height="36" viewBox="0 0 24 24">
            <path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" />
          </svg>
        </button>
      </div>
      <iframe id="ejGameFrame" src="<?php echo $game_url; ?>"></iframe>
    </div>
  </div>

  <script>
    const ejConfig = {
      ajaxurl: '<?php echo admin_url('admin-ajax.php'); ?>',
      page_id: <?php echo $page_id; ?>,
      nonce: '<?php echo wp_create_nonce('ejtype_nonce'); ?>'
    };

    // 用於取代 jQuery.post 的原生 fetch 封裝工具
    function ejPost(url, data, callback) {
      const params = new URLSearchParams();
      for (const key in data) {
        if (Array.isArray(data[key])) {
          // 針對多選刪除等陣列資料,自動轉換為 WordPress 後端能識別的 key[] 格式
          data[key].forEach(val => params.append(`${key}[]`, val));
        } else {
          params.append(key, data[key]);
        }
      }

      fetch(url, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/x-www-form-urlencoded'
          },
          body: params
        })
        .then(response => response.json())
        .then(res => callback(res))
        .catch(err => console.error('AJAX 請求失敗:', err));
    }

    // 狀態變數定義
    let isDeleteSelectedMode = false;
    let selectedOrderTid = null;
    let checkedDeleteTids = [];

    // 設備偵測
    document.addEventListener('DOMContentLoaded', () => {
      const isMobile = window.innerWidth <= 768;
      const deviceSelect = document.getElementById('ejSelectDevice');
      if (deviceSelect) deviceSelect.value = isMobile ? 'mobile' : 'pc';
      ejLoadLeaderboard('global');
    });

    // 管理員欄目顯示/隱藏切換
    function ejToggleAdminPanel() {
      const wrapper = document.getElementById('ejAdminWrapperBox');
      const showBtn = document.getElementById('ejAdminToggleShowBtn');
      if (!wrapper) return;

      if (wrapper.classList.contains('ej-hidden')) {
        wrapper.classList.remove('ej-hidden');
        showBtn.classList.add('ej-hidden');
      } else {
        wrapper.classList.add('ej-hidden');
        showBtn.classList.remove('ej-hidden');
      }
    }

    // 處理媒體庫 ID 點擊事件 (包含排序與多選刪除核心邏輯)
    function ejHandleTextIdClick(element, tid) {
      if (isDeleteSelectedMode) {
        // 多選刪除模式
        const index = checkedDeleteTids.indexOf(tid);
        if (index > -1) {
          checkedDeleteTids.splice(index, 1);
          element.style.background = '';
        } else {
          checkedDeleteTids.push(tid);
          element.style.background = '#ffc107'; // 黃色代表選中
        }
      } else {
        // 排序移動模式
        const allIdCells = Array.from(document.querySelectorAll('.ej-text-id-cell'));

        if (selectedOrderTid === null) {
          selectedOrderTid = tid;
          element.style.background = '#b3d7ff'; // 藍色代表首選高亮
        } else {
          if (selectedOrderTid === tid) {
            // 重複點擊同一個,取消選取
            selectedOrderTid = null;
            element.style.background = '';
            return;
          }

          // 獲取 A 與 B 的元素與所在 Tr
          const cellA = allIdCells.find(c => parseInt(c.dataset.id) === selectedOrderTid);
          const cellB = element;
          if (!cellA || !cellB) return;

          const rowA = cellA.closest('tr');
          const rowB = cellB.closest('tr');

          const indexA = allIdCells.indexOf(cellA);
          const indexB = allIdCells.indexOf(cellB);

          // 依照先後順序插入
          if (indexA < indexB) {
            rowB.parentNode.insertBefore(rowA, rowB.nextSibling); // A 在前,移到 B 後面
          } else {
            rowB.parentNode.insertBefore(rowA, rowB); // A 在後,移到 B 前面
          }

          // 清理樣式與暫存狀態
          cellA.style.background = '';
          selectedOrderTid = null;

          // 非同步儲存新順序至後端
          ejAdminSaveTextOrder();
        }
      }
    }

    // 發送排序數據至後端
    function ejAdminSaveTextOrder() {
      const newOrder = Array.from(document.querySelectorAll('.ej-text-id-cell')).map(c => parseInt(c.dataset.id));

      ejPost(ejConfig.ajaxurl, {
        action: 'ej_admin_reorder_texts',
        nonce: ejConfig.nonce,
        page_id: ejConfig.page_id,
        order: newOrder
      }, function(res) {
        if (!res.success) alert('排序保存失敗: ' + (res.data || '未知錯誤'));
      });
    }

    // 切換至多選刪除模式或執行刪除
    function ejAdminToggleDeleteSelectedMode() {
      const btn = document.getElementById('ejBtnDeleteSelected');
      const cancelBtn = document.getElementById('ejBtnCancelDelete');
      const promptText = document.getElementById('ejDeletePromptText');

      if (!isDeleteSelectedMode) {
        // 進入多選刪除模式
        isDeleteSelectedMode = true;

        // 清除可能殘留的排序高亮
        if (selectedOrderTid !== null) {
          const prevCell = document.querySelector(`.ej-text-id-cell[data-id="${selectedOrderTid}"]`);
          if (prevCell) prevCell.style.background = '';
          selectedOrderTid = null;
        }

        cancelBtn.classList.remove('ej-hidden');
        promptText.classList.remove('ej-hidden');
        btn.innerText = '🔥 執行刪除';
        btn.classList.add('ej-btn-danger');
      } else {
        // 執行多選刪除
        if (checkedDeleteTids.length === 0) {
          alert('請先點擊文本媒體庫 ID 進行選擇!');
          return;
        }

        if (!confirm(`確定要刪除已選中的 ${checkedDeleteTids.length} 項文本及其關聯成績嗎?此動作無法撤銷!`)) return;

        ejPost(ejConfig.ajaxurl, {
          action: 'ej_admin_delete_multiple_texts',
          nonce: ejConfig.nonce,
          page_id: ejConfig.page_id,
          text_ids: checkedDeleteTids
        }, function(res) {
          if (res.success) {
            alert('選中刪除成功!');
            location.reload();
          } else {
            alert(res.data || '刪除失敗');
          }
        });
      }
    }

    // 取消多選刪除模式
    function ejAdminCancelDeleteMode() {
      isDeleteSelectedMode = false;
      checkedDeleteTids = [];

      document.querySelectorAll('.ej-text-id-cell').forEach(c => c.style.background = '');
      document.getElementById('ejBtnCancelDelete').classList.add('ej-hidden');
      document.getElementById('ejDeletePromptText').classList.add('ej-hidden');

      const btn = document.getElementById('ejBtnDeleteSelected');
      btn.innerText = '✏️ 選中刪除';
      btn.classList.remove('ej-btn-danger');
    }

    // 全部刪除文本與數據
    function ejAdminDeleteAllTexts() {
      if (!confirm('⚠️ 警告:這將會清空「所有」參賽文本以及整個打字系統的排行榜數據!此操作不可逆,確定要繼續嗎?')) return;

      ejPost(ejConfig.ajaxurl, {
        action: 'ej_admin_delete_all_texts',
        nonce: ejConfig.nonce,
        page_id: ejConfig.page_id
      }, function(res) {
        if (res.success) {
          alert('所有文本與排行榜數據已成功清空!');
          location.reload();
        } else {
          alert(res.data || '清空失敗');
        }
      });
    }

    // 註冊用戶
    function ejRegisterUser() {
      const name = document.getElementById('ejRegName').value.trim();
      const ime = document.getElementById('ejRegIme').value.trim();
      if (!name || !ime) return alert('請填寫完整資訊');

      ejPost(ejConfig.ajaxurl, {
        action: 'ej_save_user',
        nonce: ejConfig.nonce,
        page_id: ejConfig.page_id,
        name: name,
        ime: ime
      }, function(res) {
        if (res.success) location.reload();
        else alert(res.data);
      });
    }

    // 管理員新增文本
    function ejAdminAddText() {
      const mediaId = document.getElementById('ejAdminMediaId').value;
      const name = document.getElementById('ejAdminTextName').value;
      if (!mediaId) return alert('請輸入媒體庫ID');

      ejPost(ejConfig.ajaxurl, {
        action: 'ej_admin_text',
        nonce: ejConfig.nonce,
        page_id: ejConfig.page_id,
        media_id: mediaId,
        name: name
      }, function(res) {
        if (res.success) {
          alert('新增成功');
          location.reload();
        } else {
          alert(res.data);
        }
      });
    }

    // 管理員個別刪除文本
    function ejAdminDeleteText(textId) {
      if (!confirm('確定要刪除此文本嗎?這將會從比賽清單中移除該文本並清除其對應的排行榜成績(不會影響媒體庫原始檔案)。')) return;

      ejPost(ejConfig.ajaxurl, {
        action: 'ej_admin_delete_text',
        nonce: ejConfig.nonce,
        page_id: ejConfig.page_id,
        text_id: textId
      }, function(res) {
        if (res.success) {
          alert('刪除成功!');
          location.reload();
        } else {
          alert(res.data || '刪除失敗');
        }
      });
    }

    // UI 切換
    function ejShowPreGame() {
      const summaryDiv = document.getElementById('ejGameResultSummary');
      if (summaryDiv) summaryDiv.classList.add('ej-hidden');

      document.getElementById('ejSectionMain').classList.add('ej-hidden');
      document.getElementById('ejSectionPreGame').classList.remove('ej-hidden');
    }

    function ejCancelPreGame() {
      document.getElementById('ejSectionPreGame').classList.add('ej-hidden');
      document.getElementById('ejSectionMain').classList.remove('ej-hidden');
    }

    let currentTextId = '';
    let currentDevice = '';

    // 開始遊戲
    function ejStartGame() {
      currentTextId = document.getElementById('ejSelectText').value;
      currentDevice = document.getElementById('ejSelectDevice').value;

      if (!currentTextId) return alert('請選擇文本');

      ejPost(ejConfig.ajaxurl, {
        action: 'ej_get_text_content',
        nonce: ejConfig.nonce,
        page_id: ejConfig.page_id,
        text_id: currentTextId
      }, function(res) {
        if (res.success) {
          document.getElementById('ejSectionPreGame').classList.add('ej-hidden');
          document.getElementById('ejSectionGame').style.display = 'block';

          const iframe = document.getElementById('ejGameFrame');
          iframe.contentWindow.postMessage({
            action: 'load_wp_text',
            title: res.data.name,
            content: res.data.content
          }, '*');
          const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
          const rowHeightInput = iframeDoc.getElementById('typegamerowheight');
          if (rowHeightInput) {
            // 手機端為 2,電腦端為 5
            rowHeightInput.value = (currentDevice === 'mobile') ? '2' : '5';
            // 觸發事件確保內部遊戲框架能同步更新
            rowHeightInput.dispatchEvent(new Event('input', {
              bubbles: true
            }));
            rowHeightInput.dispatchEvent(new Event('change', {
              bubbles: true
            }));
          }
        } else {
          alert('讀取文本失敗');
        }
      });
    }

    // 接收 iframe 成績
    window.addEventListener('message', (event) => {
      if (event.data && event.data.action === 'ejtype_game_over') {
        const timeUsed = event.data.time;

        document.getElementById('ejSectionGame').style.display = 'none';
        if (document.fullscreenElement) document.exitFullscreen();
        document.getElementById('ejSectionGame').classList.remove('web-fullscreen');

        ejPost(ejConfig.ajaxurl, {
          action: 'ej_save_score',
          nonce: ejConfig.nonce,
          page_id: ejConfig.page_id,
          text_id: currentTextId,
          device: currentDevice,
          time: timeUsed
        }, function(res) {
          if (res.success) {
            const data = res.data;
            let summaryHtml = `<h4 style="margin: 0 0 10px 0; font-size: 18px;">🎯 挑戰完成回報</h4>`;
            summaryHtml += `<p style="margin: 4px 0;">⏱️ <strong>本次用時:</strong>${data.time.toFixed(1)} 秒 | ⚡ <strong>本次速度:</strong>${data.speed} 字/分</p>`;
            summaryHtml += `<p style="margin: 4px 0;">🏆 <strong>您的歷史最佳:</strong>${data.best_time.toFixed(1)} 秒 (${data.best_speed} 字/分)${data.is_new_best ? ' <span style="color: #dc3545; font-weight: bold; -webkit-animation: flash 1s infinite alternate; animation: flash 1s infinite alternate;">🔥 刷新個人紀錄!</span>' : ''}</p>`;

            const summaryDiv = document.getElementById('ejGameResultSummary');
            if (summaryDiv) {
              summaryDiv.innerHTML = summaryHtml;
              summaryDiv.classList.remove('ej-hidden');
            }
          }

          document.getElementById('ejSectionMain').classList.remove('ej-hidden');

          document.querySelectorAll('.ej-tab').forEach(t => t.classList.remove('active'));
          const tabText = document.getElementById('ejTabText');
          if (tabText) tabText.classList.add('active');

          ejLoadLeaderboard('text', currentTextId);
        });
      }
    });

    // 排行榜載入
    function ejLoadLeaderboard(type, textId = '') {
      document.getElementById('ejLeaderboardArea').innerHTML = '載入中...';
      ejPost(ejConfig.ajaxurl, {
        action: 'ej_get_leaderboard',
        nonce: ejConfig.nonce,
        page_id: ejConfig.page_id,
        type: type,
        text_id: textId
      }, function(res) {
        if (res.success) document.getElementById('ejLeaderboardArea').innerHTML = res.data;
      });
    }

    function ejSwitchBoard(type) {
      const summaryDiv = document.getElementById('ejGameResultSummary');
      if (summaryDiv) summaryDiv.classList.add('ej-hidden');

      document.querySelectorAll('.ej-tab').forEach(t => t.classList.remove('active'));
      if (window.event && window.event.target) {
        window.event.target.classList.add('active');
      } else {
        const targetId = type === 'global' ? 'ejTabGlobal' : 'ejTabText';
        const targetEl = document.getElementById(targetId);
        if (targetEl) targetEl.classList.add('active');
      }

      let tid = type === 'text' ? (currentTextId || document.getElementById('ejSelectText').value) : '';
      ejLoadLeaderboard(type, tid);
    }

    // 切換手機版「電腦/手機」設備排行榜標籤
    function ejSwitchDeviceTab(device, element) {
      const parent = element.parentElement;
      parent.querySelectorAll('.ej-device-tab').forEach(t => t.classList.remove('active'));
      element.classList.add('active');

      const wrapper = parent.nextElementSibling;
      if (device === 'pc') {
        wrapper.querySelector('.ej-board-pc').classList.remove('ej-hidden-mobile');
        wrapper.querySelector('.ej-board-mobile').classList.add('ej-hidden-mobile');
      } else {
        wrapper.querySelector('.ej-board-pc').classList.add('ej-hidden-mobile');
        wrapper.querySelector('.ej-board-mobile').classList.remove('ej-hidden-mobile');
      }
    }

    // 全螢幕控制
    function ejToggleWebFS() {
      document.getElementById('ejSectionGame').classList.toggle('web-fullscreen');
    }

    function ejToggleDeviceFS() {
      const elem = document.getElementById('ejSectionGame');
      if (!document.fullscreenElement) {
        elem.requestFullscreen().catch(err => alert('全螢幕請求被拒絕'));
      } else {
        document.exitFullscreen();
      }
    }
  </script>
<?php
  return ob_get_clean();
}

// ---------------- 後端 AJAX 處理 ----------------

function ejtype_clean_input($val)
{
  return str_replace(['\\', '"', "'"], '', sanitize_text_field($val));
}

// 儲存用戶資料
add_action('wp_ajax_ej_save_user', 'ej_ajax_save_user');
function ej_ajax_save_user()
{
  check_ajax_referer('ejtype_nonce', 'nonce');
  $page_id = intval($_POST['page_id']);
  $uid = get_current_user_id();

  $users = get_post_meta($page_id, '_ejtype_users', true) ?: [];
  $users[$uid] = [
    'name' => ejtype_clean_input($_POST['name']),
    'ime' => ejtype_clean_input($_POST['ime'])
  ];
  update_post_meta($page_id, '_ejtype_users', $users);
  wp_send_json_success();
}

// 管理員新增文本
add_action('wp_ajax_ej_admin_text', 'ej_ajax_admin_text');
function ej_ajax_admin_text()
{
  check_ajax_referer('ejtype_nonce', 'nonce');
  if (!current_user_can('manage_options')) wp_send_json_error('權限不足');

  $page_id = intval($_POST['page_id']);
  $media_id = intval($_POST['media_id']);

  $file_path = get_attached_file($media_id);
  if (!$file_path || !file_exists($file_path)) wp_send_json_error('找不到該媒體檔案');

  $content = file_get_contents($file_path);
  $clean_content = preg_replace('/\s+/u', '', $content);
  $length = mb_strlen($clean_content, 'UTF-8');

  $name = !empty($_POST['name']) ? ejtype_clean_input($_POST['name']) : get_the_title($media_id);

  $texts = get_post_meta($page_id, '_ejtype_texts', true) ?: [];
  $texts[$media_id] = [
    'name' => $name,
    'length' => $length
  ];
  update_post_meta($page_id, '_ejtype_texts', $texts);
  wp_send_json_success();
}

// 【新增後端處理】保存變更後的文本排序
add_action('wp_ajax_ej_admin_reorder_texts', 'ej_ajax_admin_reorder_texts');
function ej_ajax_admin_reorder_texts()
{
  check_ajax_referer('ejtype_nonce', 'nonce');
  if (!current_user_can('manage_options')) wp_send_json_error('權限不足');

  $page_id = intval($_POST['page_id']);
  $new_order = isset($_POST['order']) ? array_map('intval', $_POST['order']) : [];

  $texts = get_post_meta($page_id, '_ejtype_texts', true) ?: [];
  $reordered_texts = [];

  // 根據前端傳過來的 ID 順序重新構建關聯陣列
  foreach ($new_order as $tid) {
    if (isset($texts[$tid])) {
      $reordered_texts[$tid] = $texts[$tid];
    }
  }

  // 安全檢查:若有漏網之魚則補回陣列尾端
  foreach ($texts as $tid => $t) {
    if (!isset($reordered_texts[$tid])) {
      $reordered_texts[$tid] = $t;
    }
  }

  update_post_meta($page_id, '_ejtype_texts', $reordered_texts);
  wp_send_json_success();
}

// 【新增後端處理】多選選中刪除
add_action('wp_ajax_ej_admin_delete_multiple_texts', 'ej_ajax_admin_delete_multiple_texts');
function ej_ajax_admin_delete_multiple_texts()
{
  check_ajax_referer('ejtype_nonce', 'nonce');
  if (!current_user_can('manage_options')) wp_send_json_error('權限不足');

  $page_id = intval($_POST['page_id']);
  $text_ids = isset($_POST['text_ids']) ? array_map('intval', $_POST['text_ids']) : [];

  $texts = get_post_meta($page_id, '_ejtype_texts', true) ?: [];
  $results = get_post_meta($page_id, '_ejtype_results', true) ?: [];

  foreach ($text_ids as $tid) {
    if (isset($texts[$tid])) unset($texts[$tid]);
    if (isset($results[$tid])) unset($results[$tid]);
  }

  update_post_meta($page_id, '_ejtype_texts', $texts);
  update_post_meta($page_id, '_ejtype_results', $results);
  wp_send_json_success();
}

// 【新增後端處理】全部刪除
add_action('wp_ajax_ej_admin_delete_all_texts', 'ej_ajax_admin_delete_all_texts');
function ej_ajax_admin_delete_all_texts()
{
  check_ajax_referer('ejtype_nonce', 'nonce');
  if (!current_user_can('manage_options')) wp_send_json_error('權限不足');

  $page_id = intval($_POST['page_id']);

  update_post_meta($page_id, '_ejtype_texts', []);
  update_post_meta($page_id, '_ejtype_results', []);
  wp_send_json_success();
}

// 取得文本內容給前端
add_action('wp_ajax_ej_get_text_content', 'ej_ajax_get_text_content');
function ej_ajax_get_text_content()
{
  $page_id = intval($_POST['page_id']);
  $text_id = intval($_POST['text_id']);
  $texts = get_post_meta($page_id, '_ejtype_texts', true) ?: [];

  $file_path = get_attached_file($text_id);
  if ($file_path && file_exists($file_path)) {
    wp_send_json_success([
      'name' => $texts[$text_id]['name'],
      'content' => file_get_contents($file_path)
    ]);
  }
  wp_send_json_error();
}

// 儲存成績
add_action('wp_ajax_ej_save_score', 'ej_ajax_save_score');
function ej_ajax_save_score()
{
  $page_id = intval($_POST['page_id']);
  $uid = get_current_user_id();
  $text_id = intval($_POST['text_id']);
  $device = sanitize_text_field($_POST['device']);
  $time = floatval($_POST['time']);

  $results = get_post_meta($page_id, '_ejtype_results', true) ?: [];

  if (!isset($results[$text_id])) $results[$text_id] = ['pc' => [], 'mobile' => []];

  $history_best = isset($results[$text_id][$device][$uid]) ? $results[$text_id][$device][$uid] : null;
  $is_new_best = false;

  if ($history_best === null || $time < $history_best) {
    $results[$text_id][$device][$uid] = $time;
    $history_best = $time;
    $is_new_best = true;
  }

  update_post_meta($page_id, '_ejtype_results', $results);

  $texts = get_post_meta($page_id, '_ejtype_texts', true) ?: [];
  $length = isset($texts[$text_id]['length']) ? $texts[$text_id]['length'] : 0;

  $current_speed = $length > 0 ? round($length / ($time / 60), 1) : 0;
  $best_speed = $length > 0 ? round($length / ($history_best / 60), 1) : 0;

  wp_send_json_success([
    'time'        => $time,
    'speed'       => $current_speed,
    'best_time'   => $history_best,
    'best_speed'  => $best_speed,
    'is_new_best' => $is_new_best
  ]);
}

// 獲取排行榜 HTML
add_action('wp_ajax_ej_get_leaderboard', 'ej_ajax_get_leaderboard');
add_action('wp_ajax_nopriv_ej_get_leaderboard', 'ej_ajax_get_leaderboard');
function ej_ajax_get_leaderboard()
{
  $page_id = intval($_POST['page_id']);
  $type = sanitize_text_field($_POST['type']);
  $text_id = intval($_POST['text_id']);

  $users = get_post_meta($page_id, '_ejtype_users', true) ?: [];
  $texts = get_post_meta($page_id, '_ejtype_texts', true) ?: [];
  $results = get_post_meta($page_id, '_ejtype_results', true) ?: [];

  ob_start();

  if ($type === 'text') {
    echo '<div style="margin-bottom: 20px; background: #f8f9fa; padding: 12px; border-radius: 6px; border: 1px solid #eee;">';
    echo '<label style="font-weight: bold; margin-right: 10px;">切換檢視文本:</label>';
    echo '<select id="ejLeaderboardTextSelect" style="padding: 6px 12px; border-radius: 4px; border: 1px solid #ccc; max-width: 100%;" onchange="ejLoadLeaderboard(\'text\', this.value)">';
    echo '<option value="">-- 請選擇比賽文本 --</option>';
    foreach ($texts as $tid => $t) {
      $selected = ($tid == $text_id) ? 'selected' : '';
      echo "<option value='" . esc_attr($tid) . "' {$selected}>" . esc_html($t['name']) . " (" . esc_html($t['length']) . "字)</option>";
    }
    echo '</select>';
    echo '</div>';

    if (!$text_id || !isset($texts[$text_id])) {
      echo '<p style="color: #666; font-style: italic; text-align: center; padding: 30px 0;">💡 請先點擊上方選單切換文本,或點擊右上方「🚀 開始打字」完成一場參賽項目。</p>';
      wp_send_json_success(ob_get_clean());
      return;
    }

    $t_info = $texts[$text_id];
    echo "<div style='margin-bottom:10px;'><strong>當前文本:</strong> {$t_info['name']} ({$t_info['length']} 字)</div>";

    echo '<div class="ej-device-tabs">
            <div class="ej-device-tab active" onclick="ejSwitchDeviceTab(\'pc\', this)">💻 電腦排名</div>
            <div class="ej-device-tab" onclick="ejSwitchDeviceTab(\'mobile\', this)">📱 手機排名</div>
          </div>';

    echo '<div class="ej-boards-wrapper">';
    foreach (['pc' => '💻 電腦鍵盤排名', 'mobile' => '📱 手機觸屏排名'] as $dev => $title) {
      $hide_class = ($dev === 'mobile') ? ' ej-hidden-mobile' : '';
      echo "<div class='ej-board-column ej-board-{$dev}{$hide_class}'>";
      echo "<h4>{$title}</h4>";
      $scores = isset($results[$text_id][$dev]) ? $results[$text_id][$dev] : [];
      asort($scores);

      echo "<table class='ej-table'><tr><th>排名</th><th>選手 (輸入法)</th><th>用時 (秒)</th><th>速度 (字/分)</th></tr>";
      $rank = 1;
      foreach ($scores as $uid => $time) {
        if (!isset($users[$uid])) continue;
        $speed = round($t_info['length'] / ($time / 60), 1);
        $u = $users[$uid];
        echo "<tr><td>{$rank}</td><td>{$u['name']} ({$u['ime']})</td><td>" . number_format($time, 1) . "</td><td>{$speed}</td></tr>";
        $rank++;
      }
      if (empty($scores)) echo "<tr><td colspan='4'>暫無成績</td></tr>";
      echo "</table>";
      echo "</div>";
    }
    echo '</div>';
  } else {
    $all_scores = ['pc' => [], 'mobile' => []];
    foreach ($results as $tid => $dev_data) {
      if (!isset($texts[$tid])) continue;
      $len = $texts[$tid]['length'];
      foreach ($dev_data as $dev => $user_times) {
        foreach ($user_times as $uid => $time) {
          $speed = round($len / ($time / 60), 1);
          $all_scores[$dev][] = ['uid' => $uid, 'tid' => $tid, 'speed' => $speed];
        }
      }
    }

    echo '<div class="ej-device-tabs">
            <div class="ej-device-tab active" onclick="ejSwitchDeviceTab(\'pc\', this)">💻 電腦總排名</div>
            <div class="ej-device-tab" onclick="ejSwitchDeviceTab(\'mobile\', this)">📱 手機總排名</div>
          </div>';

    echo '<div class="ej-boards-wrapper">';
    foreach (['pc' => '💻 電腦鍵盤總排名', 'mobile' => '📱 手機觸屏總排名'] as $dev => $title) {
      $hide_class = ($dev === 'mobile') ? ' ej-hidden-mobile' : '';
      echo "<div class='ej-board-column ej-board-{$dev}{$hide_class}'>";
      echo "<h4>{$title}</h4>";
      $dev_scores = $all_scores[$dev];
      usort($dev_scores, function ($a, $b) {
        return $b['speed'] <=> $a['speed'];
      });

      echo "<table class='ej-table'><tr><th>排名</th><th>選手 (輸入法)</th><th>文本名稱</th><th>字數</th><th>速度 (字/分)</th></tr>";
      $rank = 1;
      foreach ($dev_scores as $s) {
        if (!isset($users[$s['uid']])) continue;
        $u = $users[$s['uid']];
        $t = $texts[$s['tid']];
        echo "<tr><td>{$rank}</td><td>{$u['name']} ({$u['ime']})</td><td>{$t['name']}</td><td>{$t['length']}</td><td>{$s['speed']}</td></tr>";
        $rank++;
      }
      if (empty($dev_scores)) echo "<tr><td colspan='5'>暫無成績</td></tr>";
      echo "</table>";
      echo "</div>";
    }
    echo '</div>';
  }

  wp_send_json_success(ob_get_clean());
}

// 管理員刪除單一文本與相關成績
add_action('wp_ajax_ej_admin_delete_text', 'ej_ajax_admin_delete_text');
function ej_ajax_admin_delete_text()
{
  check_ajax_referer('ejtype_nonce', 'nonce');
  if (!current_user_can('manage_options')) wp_send_json_error('權限不足');

  $page_id = intval($_POST['page_id']);
  $text_id = intval($_POST['text_id']);

  $texts = get_post_meta($page_id, '_ejtype_texts', true) ?: [];
  if (isset($texts[$text_id])) {
    unset($texts[$text_id]);
    update_post_meta($page_id, '_ejtype_texts', $texts);

    $results = get_post_meta($page_id, '_ejtype_results', true) ?: [];
    if (isset($results[$text_id])) {
      unset($results[$text_id]);
      update_post_meta($page_id, '_ejtype_results', $results);
    }

    wp_send_json_success();
  }

  wp_send_json_error('找不到該文本數據');
}
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5749
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 179 次
被圈友点赞: 207 次
联系:

Re: 用wordpress搞個打字比賽

帖子 ejsoon »

改進:
之前當第一次登入時,會首先進入一個需要填寫名稱和輸入法名的頁面。現在改為去掉這個頁面,直接進入到有標題——歡迎、排行榜的頁面。

標題下方(如果是管理員則在文本管理的下方)出現一行label+input,有三個欄目可輸入:選手名稱(直接填入註冊用戶名),輸入法碼表(如倉頡、鄭碼),輸入法軟體(如gcin,lime,rime)。

在點擊「開始打字」時,如果這些輸入框存在空的,或者存在非法字符(如"\),或者字數過長(最長七個漢字或十二個英文),則提醒並不能繼續。如果都符合,則記錄到用戶信息json中,並在打字結束時記錄到結果中。

用戶每次在開始打字前,都可以更改這三個輸入框的值。

管理員在文本管理下方(同屬一個div)增加一個參賽人員管理,可以顯示所有參賽者的信息(用戶名、參賽名稱、輸入法碼表、軟體),可以刪除用戶。當用戶被刪除時,他的所有成績都將會被清除。

明確給出所有要修改的地方和所要替換的代碼。

代码: 全选

改進:
之前當第一次登入時,會首先進入一個需要填寫名稱和輸入法名的頁面。現在改為去掉這個頁面,直接進入到有標題——歡迎、排行榜的頁面。

標題下方(如果是管理員則在文本管理的下方)出現一行label+input,有三個欄目可輸入:選手名稱(直接填入註冊用戶名),輸入法碼表(如倉頡、鄭碼),輸入法軟體(如gcin,lime,rime)。

在點擊「開始打字」時,如果這些輸入框存在空的,或者存在非法字符(如"\),或者字數過長(最長七個漢字或十二個英文),則提醒並不能繼續。如果都符合,則記錄到用戶信息json中,並在打字結束時記錄到結果中。

用戶每次在開始打字前,都可以更改這三個輸入框的值。

管理員在文本管理下方(同屬一個div)增加一個參賽人員管理,可以顯示所有參賽者的信息(用戶名、參賽名稱、輸入法碼表、軟體),可以刪除用戶。當用戶被刪除時,他的所有成績都將會被清除。

明確給出所有要修改的地方和所要替換的代碼。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
回复

在线用户

正浏览此版面之用户: 没有注册用户 和 6 访客