用wordpress搞個打字比賽

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

Re: 用wordpress搞個打字比賽

帖子 ejsoon »

改進:
當打完字後,都執行哪些函式?

如果選手是用手機打的,能否在打完字時,排行榜切換至手機排行?

明確給出所有要修改的地方和所要替換的代碼部分,不需給出全部代碼。

代码: 全选

以下是針對您提出的兩個問題所做的詳細解答與程式碼修改指引:

## 一、 當打完字時,都執行了哪些函式?

當使用者在 Iframe 遊戲內打完字並觸發結束時,程式會依序執行以下前端(JavaScript)**與**後端(PHP)的函式:

### 1. 前端監聽與回報階段

* **`window.addEventListener('message', ...)`**:監聽到 Iframe 傳出的 `ejtype_game_over` 事件,開始執行後續動作。
* **`ejPost()`**:封裝好的原生存取工具,用來發送第一波 AJAX 請求,將比賽用時(`timeUsed`)傳送到後端儲存。

### 2. 後端儲存階段

* **`ej_ajax_save_score()`**(PHP):響應後端的 `ej_save_score` 動作。負責驗證資料、比對歷史最佳紀錄、計算打字速度,最後將結果儲存至 WordPress 的 Post Meta 中。

### 3. 前端更新與載入階段

* `ej_ajax_save_score` 執行成功回傳後,進入 JS 的 Callback 回呼函式。
* **`ejLoadLeaderboard('text', currentTextId)`**:前端呼叫此函式,準備載入單篇文本的排行榜。
* **`ejPost()`**:再次被呼叫,發送第二波 AJAX 請求,向後端索取最新的排行榜 HTML 結構。

### 4. 後端產生排行階段

* **`ej_ajax_get_leaderboard()`**(PHP):響應後端的 `ej_get_leaderboard` 動作。根據請求的類型(此處為 `text`),撈取所有玩家成績並進行排序,組裝成表格 HTML 輸出給前端。

---

## 二、 讓手機打字結束時自動切換至手機排行的修改方案

要實作此功能,最理想的做法是在前端 **`ejLoadLeaderboard`** 收到後端排行榜 HTML 並渲染到網頁後,檢查全域變數 `currentDevice` 是否為 `'mobile'`。如果是,就自動模擬點擊切換到手機版的 Tab。

請依照下方說明修改 JavaScript 區塊:

### 🔍 尋找目標代碼

在檔案中段的 `<script>` 標籤內,找到大約第 600 行左右的 `ejLoadLeaderboard` 函式:

```javascript
    // 排行榜載入
    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;
      });
    }

```

### 🔄 替換為以下新代碼

將上述區塊**完整替換**為以下代碼:

```javascript
    // 排行榜載入
    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;
          
          // 【修改點】若當前參賽設備為手機,自動切換至手機排行榜標籤
          if (currentDevice === 'mobile') {
            const mobileTab = document.querySelector('#ejLeaderboardArea .ej-device-tabs .ej-device-tab:nth-child(2)');
            if (mobileTab) {
              ejSwitchDeviceTab('mobile', mobileTab);
            }
          }
        }
      });
    }

```

### 💡 修改邏輯說明

1. 程式會在排行榜 HTML(`res.data`)成功寫入 `#ejLeaderboardArea` 後才執行。
2. 判斷 `currentDevice === 'mobile'`(此變數在進入遊戲點擊 `ejStartGame()` 時就已被正確賦值)。
3. 使用 `querySelector` 抓取剛生成出來的第二個頁籤(即「手機排名/手機總排名」)。
4. 呼叫原本就寫好的 `ejSwitchDeviceTab('mobile', mobileTab)` 函式,完美連動 CSS 樣式與欄位顯示。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5764
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 179 次
被圈友点赞: 207 次
联系:

Re: 用wordpress搞個打字比賽

帖子 ejsoon »

如果我希望點擊總排行榜時,也首先看到的是手機的排行榜呢?

代码: 全选

若您希望在使用者點擊「總排行榜」時(或是網頁剛載入預設顯示總排行榜時),在手機等行動裝置上**首要看到的也是手機總排行榜**,最乾淨且有效率的做法是直接修改**後端 PHP 產生總排行榜 HTML 的預設樣式**。

由於程式碼中原本就配有 CSS 媒體查詢(Media Query),在桌機寬螢幕下會自動並列顯示兩欄,只有在手機窄螢幕下才會切換為頁籤模式。因此,我們只需要將後端總排行榜的「預設選中狀態」與「隱藏樣式」對調即可,完全不會影響到桌機版的並列顯示。

以下為具體的修改步驟與替換代碼:

### 🔍 尋找目標代碼

在 `new3641.php 后半段` 的 `ej_ajax_get_leaderboard()` 函式中,找到處理總排行榜(即 `else` 區塊,大約在檔案第 740 ~ 755 行左右)的以下代碼:

```php
    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}'>";

```

### 🔄 替換為以下新代碼

將上述區塊**完整替換**為以下代碼(主要將 `active` 移至 mobile,並將 `ej-hidden-mobile` 改掛在 pc 上):

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

    echo '<div class="ej-boards-wrapper">';
    foreach (['pc' => '💻 電腦鍵盤總排名', 'mobile' => '📱 手機觸屏總排名'] as $dev => $title) {
      // 將預設隱藏類別改為當設備為 pc 時掛載,讓手機版窄螢幕預設隱藏電腦欄位
      $hide_class = ($dev === 'pc') ? ' ej-hidden-mobile' : '';
      echo "<div class='ej-board-column ej-board-{$dev}{$hide_class}'>";

```

### 💡 修改邏輯說明

1. **行動裝置(手機)環境**:當網頁寬度小於 767px 時,CSS 的 `.ej-hidden-mobile { display: none !important; }` 會生效。新代碼將此樣式改為掛載在 `pc` 欄位上,因此手機開網頁或點擊總排行時,電腦排行會被隱藏,直接秀出手機排行;同時上方對應的「📱 手機總排名」頁籤也會因為擁有 `active` 類別而呈現藍底高亮。
2. **桌面電腦環境**:當在寬螢幕觀看時,CSS 媒體查詢中的 `.ej-hidden-mobile` 根本不會觸發,且頁籤區塊 `.ej-device-tabs` 本身就是隱藏的(`display: none;`)。因此,桌機畫面上依然會保持原本的設計,將「電腦總排名」與「手機總排名」左右完美並列呈現,不受任何影響。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5764
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 179 次
被圈友点赞: 207 次
联系:

Re: 用wordpress搞個打字比賽

帖子 ejsoon »

new3643.php最新代碼:

代码: 全选

<?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);

  // 取得當前使用者資料
  $wp_user = wp_get_current_user();
  $user_info = ($is_logged_in && isset($users_data[$current_user_id])) ? $users_data[$current_user_id] : [
    'name' => $wp_user->display_name,
    'ime_table' => '',
    'ime_software' => ''
  ];

  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-user-info-readonly h4,
    .ej-user-info-readonly .ej-info-warning {
      display: none !important;
    }

    .ej-user-info-readonly .ej-form-group label {
      font-size: 13px;
      color: #666;
      font-weight: normal;
    }

    .ej-user-info-readonly .ej-form-group {
      margin-bottom: 0;
    }

    .ej-user-info-readonly {
      padding: 10px 15px !important;
      font-size: 14px;
    }

    .ej-user-info-readonly .ej-input-field {
      display: none;
    }

    .ej-user-info-readonly .ej-span-field {
      display: inline-block;
      font-weight: bold;
      color: #0056b3;
      padding-top: 5px;
    }

    .ej-user-info-edit .ej-span-field {
      display: none;
    }

    .ej-user-info-edit .ej-input-field {
      display: block;
    }

    .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) : ?>
          <?php echo esc_html($wp_user->user_login) ?>,歡迎回來!
        <?php else : ?>
          <span style="color: #dc3545; font-weight: bold;">請先 <a href="<?php echo wp_login_url(get_permalink()); ?>">登入</a> 以參加打字比賽。</span>
        <?php endif; ?>
      </div>
    </div>

    <div id="ejSectionMain">
      <?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; ?>

          <hr style="border: 0; border-top: 1px solid #ccc; margin: 20px 0;">
          <h4>👥 參賽人員管理</h4>
          <?php if (!empty($users_data)) : ?>
            <div style="overflow-x: auto;">
              <table class="ej-table" style="background: #fff; border-radius: 6px; box-shadow: 0 2px 5px rgba(0,0,0,0.05); white-space: nowrap;">
                <thead>
                  <tr>
                    <th>帳號名稱</th>
                    <th>選手名稱</th>
                    <th>碼表</th>
                    <th>軟體</th>
                    <th>操作</th>
                  </tr>
                </thead>
                <tbody>
                  <?php foreach ($users_data as $uid => $u) :
                    $wp_u = get_userdata($uid);
                    $wp_login = $wp_u ? $wp_u->user_login : '未知(' . $uid . ')';
                  ?>
                    <tr>
                      <td><?php echo esc_html($wp_login); ?></td>
                      <td><?php echo esc_html($u['name']); ?></td>
                      <td><?php echo esc_html($u['ime_table'] ?? ''); ?></td>
                      <td><?php echo esc_html($u['ime_software'] ?? ''); ?></td>
                      <td>
                        <button class="ej-btn ej-btn-danger" style="padding: 5px 12px; font-size: 13px;" onclick="ejAdminDeleteUser(<?php echo $uid; ?>)">❌ 刪除成績與資料</button>
                      </td>
                    </tr>
                  <?php endforeach; ?>
                </tbody>
              </table>
            </div>
          <?php else : ?>
            <p style="color: #666; margin: 0; font-style: italic;">目前尚無參賽者資料。</p>
          <?php endif; ?>
        </div>
      <?php endif; ?>

      <?php if ($is_logged_in) :
        // 判斷三個值是否皆不為空
        $has_full_info = !empty($user_info['name']) && !empty($user_info['ime_table']) && !empty($user_info['ime_software']);
      ?>
        <div id="ejUserInfoWrapper" class="<?php echo $has_full_info ? 'ej-user-info-readonly' : 'ej-user-info-edit'; ?>" style="background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 20px; border: 1px solid #ddd; transition: padding 0.3s;">
          <h4 style="margin-top: 0; margin-bottom: 15px;">📝 參賽資訊 </h4>
          <div style="display: flex; gap: 10px; flex-wrap: wrap; align-items: flex-end;">
            <div class="ej-form-group" style="margin-bottom: 0; flex: 1; min-width: 140px;">
              <label>選手名稱:</label>
              <span class="ej-span-field" id="ejSpanPlayerName"><?php echo esc_html($user_info['name'] ?? ''); ?></span>
              <input class="ej-input-field" type="text" id="ejPlayerName" value="<?php echo esc_attr($user_info['name'] ?? ''); ?>" placeholder="例:打字高手">
            </div>
            <div class="ej-form-group" style="margin-bottom: 0; flex: 1; min-width: 140px;">
              <label>輸入法碼表:</label>
              <span class="ej-span-field" id="ejSpanImeTable"><?php echo esc_html($user_info['ime_table'] ?? ''); ?></span>
              <input class="ej-input-field" type="text" id="ejImeTable" value="<?php echo esc_attr($user_info['ime_table'] ?? ''); ?>" placeholder="例:倉頡、鄭碼">
            </div>
            <div class="ej-form-group" style="margin-bottom: 0; flex: 1; min-width: 140px;">
              <label>輸入法軟體:</label>
              <span class="ej-span-field" id="ejSpanImeSoftware"><?php echo esc_html($user_info['ime_software'] ?? ''); ?></span>
              <input class="ej-input-field" type="text" id="ejImeSoftware" value="<?php echo esc_attr($user_info['ime_software'] ?? ''); ?>" placeholder="例:gcin, rime">
            </div>
            <div style="flex: 0 0 auto; display: flex; gap: 5px;">
              <button class="ej-btn" id="ejEditInfoBtn" style="height: 42px; padding: 10px; background: #6c757d;" onclick="ejToggleEditInfo()" title="切換編輯狀態">
                <svg id="ejIconPencil" class="<?php echo $has_full_info ? '' : 'ej-hidden'; ?>" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                  <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
                  <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
                </svg>
                <svg id="ejIconCheck" class="<?php echo $has_full_info ? 'ej-hidden' : ''; ?>" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                  <polyline points="20 6 9 17 4 12"></polyline>
                </svg>
              </button>
              <button class="ej-btn" style="height: 42px;" onclick="ejValidateAndShowPreGame()">🚀 開始打字</button>
            </div>
          </div>
          <p class="ej-info-warning" style="margin: 10px 0 0 0; font-size: 13px; color: #666;">⚠️ 字數限制:最長 7 個漢字或 12 個英文字母。不可包含特殊符號 (\, ", ')。</p>
        </div>
      <?php endif; ?>

      <div style="display:flex; justify-content:space-between; align-items:center;">
        <h3>排行榜</h3>
      </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 || '清空失敗');
        }
      });
    }

    // 計算字串長度權重 (判斷 7 個漢字或 12 個英文的線性組合)
    function isValidFieldLength(str) {
      let chCount = 0;
      let enCount = 0;
      for (let i = 0; i < str.length; i++) {
        if (str.charCodeAt(i) > 255) chCount++;
        else enCount++;
      }
      return (chCount / 7) + (enCount / 12) <= 1;
    }

    // 切換參賽資訊的編輯/唯讀狀態
    function ejToggleEditInfo() {
      const wrapper = document.getElementById('ejUserInfoWrapper');
      const iconPencil = document.getElementById('ejIconPencil');
      const iconCheck = document.getElementById('ejIconCheck');

      if (wrapper.classList.contains('ej-user-info-readonly')) {
        // 切換為編輯模式 (展開輸入框、顯示標題警告、顯示完成按鈕)
        wrapper.classList.remove('ej-user-info-readonly');
        wrapper.classList.add('ej-user-info-edit');
        iconPencil.classList.add('ej-hidden');
        iconCheck.classList.remove('ej-hidden');
      } else {
        // 切換回唯讀模式 (同步 Input 資料至 Span)
        document.getElementById('ejSpanPlayerName').innerText = document.getElementById('ejPlayerName').value;
        document.getElementById('ejSpanImeTable').innerText = document.getElementById('ejImeTable').value;
        document.getElementById('ejSpanImeSoftware').innerText = document.getElementById('ejImeSoftware').value;

        wrapper.classList.remove('ej-user-info-edit');
        wrapper.classList.add('ej-user-info-readonly');
        iconCheck.classList.add('ej-hidden');
        iconPencil.classList.remove('ej-hidden');
      }
    }

    // 點擊開始打字前進行資料驗證並儲存
    function ejValidateAndShowPreGame() {
      const name = document.getElementById('ejPlayerName').value.trim();
      const imeTable = document.getElementById('ejImeTable').value.trim();
      const imeSoftware = document.getElementById('ejImeSoftware').value.trim();

      if (!name || !imeTable || !imeSoftware) {
        return alert('參賽資訊不可為空!請確實填寫選手名稱、輸入法碼表與輸入法軟體。');
      }

      const illegalRegex = /[\\'"]/;
      if (illegalRegex.test(name) || illegalRegex.test(imeTable) || illegalRegex.test(imeSoftware)) {
        return alert('輸入內容不可包含特殊符號 (如反斜線 \\、單引號 \'、雙引號 ")!');
      }

      if (!isValidFieldLength(name) || !isValidFieldLength(imeTable) || !isValidFieldLength(imeSoftware)) {
        return alert('內容長度過長!每個欄位最長限制為 7 個漢字或 12 個英文字母。');
      }

      // 驗證通過,儲存至後端
      ejPost(ejConfig.ajaxurl, {
        action: 'ej_save_user',
        nonce: ejConfig.nonce,
        page_id: ejConfig.page_id,
        name: name,
        ime_table: imeTable,
        ime_software: imeSoftware
      }, function(res) {
        if (res.success) {
          ejShowPreGame();
        } else {
          alert(res.data || '保存用戶資料失敗');
        }
      });
    }

    // 管理員刪除用戶及成績
    function ejAdminDeleteUser(uid) {
      if (!confirm('⚠️ 確定要刪除此參賽者嗎?這將會清除他/她的「所有打字成績」!此動作不可復原!')) return;

      ejPost(ejConfig.ajaxurl, {
        action: 'ej_admin_delete_user',
        nonce: ejConfig.nonce,
        page_id: ejConfig.page_id,
        user_id: uid
      }, function(res) {
        if (res.success) {
          alert('參賽者及成績已成功刪除!');
          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
            }));
          }
          const rowLengthInput = iframeDoc.getElementById('typegamerowlength');
          if (rowLengthInput) {
            // 手機端為 2,電腦端為 5
            rowLengthInput.value = (currentDevice === 'mobile') ? '16' : '24';
            // 觸發事件確保內部遊戲框架能同步更新
            rowLengthInput.dispatchEvent(new Event('input', {
              bubbles: true
            }));
            rowLengthInput.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;

          // 【修改點】若當前參賽設備為手機,自動切換至手機排行榜標籤
          if (currentDevice === 'mobile') {
            const mobileTab = document.querySelector('#ejLeaderboardArea .ej-device-tabs .ej-device-tab:nth-child(2)');
            if (mobileTab) {
              ejSwitchDeviceTab('mobile', mobileTab);
            }
          }
        }
      });
    }

    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_table' => ejtype_clean_input($_POST['ime_table']),
    'ime_software' => ejtype_clean_input($_POST['ime_software'])
  ];
  update_post_meta($page_id, '_ejtype_users', $users);
  wp_send_json_success();
}

// 管理員刪除用戶與成績
add_action('wp_ajax_ej_admin_delete_user', 'ej_ajax_admin_delete_user');
function ej_ajax_admin_delete_user()
{
  check_ajax_referer('ejtype_nonce', 'nonce');
  if (!current_user_can('manage_options')) wp_send_json_error('權限不足');

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

  // 刪除用戶基本資料
  $users = get_post_meta($page_id, '_ejtype_users', true) ?: [];
  if (isset($users[$user_id])) {
    unset($users[$user_id]);
    update_post_meta($page_id, '_ejtype_users', $users);
  }

  // 刪除該用戶的所有成績
  $results = get_post_meta($page_id, '_ejtype_results', true) ?: [];
  $updated = false;
  foreach ($results as $tid => &$dev_data) {
    foreach (['pc', 'mobile'] as $dev) {
      if (isset($dev_data[$dev][$user_id])) {
        unset($dev_data[$dev][$user_id]);
        $updated = true;
      }
    }
  }

  if ($updated) {
    update_post_meta($page_id, '_ejtype_results', $results);
  }

  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_entry = isset($results[$text_id][$device][$uid]) ? $results[$text_id][$device][$uid] : null;
  // 向下相容檢查:判斷歷史數據是新版陣列還是舊版純數字
  $history_best = is_array($history_best_entry) ? $history_best_entry['time'] : $history_best_entry;
  $is_new_best = false;

  if ($history_best === null || $time < $history_best) {
    // 獲取使用者當前填寫的參賽資訊
    $users = get_post_meta($page_id, '_ejtype_users', true) ?: [];
    $u_info = isset($users[$uid]) ? $users[$uid] : [
      'name' => wp_get_current_user()->display_name,
      'ime_table' => '',
      'ime_software' => ''
    ];

    // 將成績與當下的選手資訊封裝成陣列存入
    $results[$text_id][$device][$uid] = [
      'time'         => $time,
      'name'         => $u_info['name'],
      'ime_table'    => $u_info['ime_table'],
      'ime_software' => $u_info['ime_software']
    ];
    $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" onclick="ejSwitchDeviceTab(\'pc\', this)">💻 電腦總排名</div>
            <div class="ej-device-tab active" onclick="ejSwitchDeviceTab(\'mobile\', this)">📱 手機總排名</div>
          </div>';

    echo '<div class="ej-boards-wrapper">';
    foreach (['pc' => '💻 電腦鍵盤總排名', 'mobile' => '📱 手機觸屏總排名'] as $dev => $title) {
      // 將預設隱藏類別改為當設備為 pc 時掛載,讓手機版窄螢幕預設隱藏電腦欄位
      $hide_class = ($dev === 'pc') ? ' 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] : [];
      // 靈活排序:相容新版陣列與舊版純數字排序
      uasort($scores, function ($a, $b) {
        $time_a = is_array($a) ? $a['time'] : $a;
        $time_b = is_array($b) ? $b['time'] : $b;
        return $time_a <=> $time_b;
      });

      echo "<table class='ej-table'><tr><th>排名</th><th>選手 (碼表/軟體)</th><th>用時 (秒)</th><th>速度 (字/分)</th></tr>";
      $rank = 1;
      foreach ($scores as $uid => $score_data) {
        // 解析新舊數據結構
        if (is_array($score_data)) {
          $time = $score_data['time'];
          $p_name = $score_data['name'];
          $p_table = $score_data['ime_table'];
          $p_software = $score_data['ime_software'];
        } else {
          $time = $score_data;
          if (!isset($users[$uid])) continue; // 舊數據若無該用戶資訊則跳過
          $u = $users[$uid];
          $p_name = $u['name'];
          $p_table = $u['ime_table'] ?? '';
          $p_software = $u['ime_software'] ?? '';
        }

        $speed = round($t_info['length'] / ($time / 60), 1);
        $ime_display = esc_html($p_table . ' / ' . $p_software);
        echo "<tr><td>{$rank}</td><td>" . esc_html($p_name) . " ({$ime_display})</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 => $score_data) {
          // 解析新舊數據結構
          if (is_array($score_data)) {
            $time = $score_data['time'];
            $p_name = $score_data['name'];
            $p_table = $score_data['ime_table'];
            $p_software = $score_data['ime_software'];
          } else {
            $time = $score_data;
            if (!isset($users[$uid])) continue;
            $u = $users[$uid];
            $p_name = $u['name'];
            $p_table = $u['ime_table'] ?? '';
            $p_software = $u['ime_software'] ?? '';
          }
          $speed = round($len / ($time / 60), 1);
          $all_scores[$dev][] = [
            'uid'          => $uid,
            'tid'          => $tid,
            'speed'        => $speed,
            'name'         => $p_name,
            'ime_table'    => $p_table,
            'ime_software' => $p_software
          ];
        }
      }
    }

    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) {
        $t = $texts[$s['tid']];
        $ime_display = esc_html($s['ime_table'] . ' / ' . $s['ime_software']);
        echo "<tr><td>{$rank}</td><td>" . esc_html($s['name']) . " ({$ime_display})</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/
弈趣極光:享受思維樂趣
回复
  • 相似主题
    回复总数
    阅读次数
    最新帖子

在线用户

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