用wordpress搞個打字比賽

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

用wordpress搞個打字比賽

帖子 ejsoon »

所上傳的ejtype文檔是一個打字網頁程式,其被上傳至一個wordpress的媒體庫中,下面在wordpress的page中利用它的meta和這個文件,製作一個打字比賽php-js程式:
一,參加比賽需註冊和登入,在開始打字前應先登入。登入後記錄其id,並要求用戶填寫以下資料:顯示名、輸入法名。關聯至id後轉成json,保存至page的meta用戶信息中。

二,開始打字前,需要選擇打字文本。可選的文本的媒體庫id及名稱將由網站管理者填入和保存及刪除,保存格式為json,保存至page的meta比賽文本數據中,數據有:文本的媒體庫id、文本名稱(自動調用但可更改)、字數(程式自動統計)。只有管理員可以保存和刪除,其它非管理人員只能選擇使用哪個文本來進行打字。

三,當填有用戶信息,選好打字文本後,還有一個「設備」的選項,程式首先能根據其屏寬自動幫其選擇是「電腦、手機」,但用戶可以更改。之後用iframe引用的ejtype將在網頁內全屏或整個設備全屏。提供兩個全屏按鈕固定在屏幕右上角,這兩個按鈕都為36*36的svg圖標,全屏可隨時退出或進入。用戶將可以開始在ejtype中打字。

四,當ejtype打字結束,它的結果將保存在page的meta結果表中,格式為json,數據有:用戶id、文本id、用時、設備類別。同時顯示這個文本id的兩種設備類別的排行榜,同一用戶將只顯示他的最短時間,時間越少排名越靠前,用時相同者將並列。排行榜將自動用字數/時間來計算打字速度,保留一位小數。在單個文本的排行榜的項目有:用戶顯示名稱、輸入法名、用時、速度。排行榜上方將有文本的信息,包括文本名稱、字數。

五,剛進入這個頁面時,有總排行榜,將所有文本所有用戶的打字速度來進行排行,分為「電腦、手機」兩類。總排行榜的項目有:用戶顯示名、輸入法名、文本名稱,文本字數、打字速度。

六,代碼將用php和js編寫,由snippets擴展將代碼嵌入至一個page中。用戶在上傳信息時,要用到wp_ajax,meta的json要用php來讀取而不能曝露至前端。保存數據前檢查不能出現\和"等較難轉義的符號。如有必要可對所上傳的ejtype的代碼進行更改,以應用於這個打字比賽程式。

七,整個頁面要用css美化,不要使用原生的html樣式。

代码: 全选

所上傳的ejtype文檔是一個打字網頁程式,其被上傳至一個wordpress的媒體庫中,下面在wordpress的page中利用它的meta和這個文件,製作一個打字比賽php-js程式:
一,參加比賽需註冊和登入,在開始打字前應先登入。登入後記錄其id,並要求用戶填寫以下資料:顯示名、輸入法名。關聯至id後轉成json,保存至page的meta用戶信息中。

二,開始打字前,需要選擇打字文本。可選的文本的媒體庫id及名稱將由網站管理者填入和保存及刪除,保存格式為json,保存至page的meta比賽文本數據中,數據有:文本的媒體庫id、文本名稱(自動調用但可更改)、字數(程式自動統計)。只有管理員可以保存和刪除,其它非管理人員只能選擇使用哪個文本來進行打字。

三,當填有用戶信息,選好打字文本後,還有一個「設備」的選項,程式首先能根據其屏寬自動幫其選擇是「電腦、手機」,但用戶可以更改。之後用iframe引用的ejtype將在網頁內全屏或整個設備全屏。提供兩個全屏按鈕固定在屏幕右上角,這兩個按鈕都為36*36的svg圖標,全屏可隨時退出或進入。用戶將可以開始在ejtype中打字。

四,當ejtype打字結束,它的結果將保存在page的meta結果表中,格式為json,數據有:用戶id、文本id、用時、設備類別。同時顯示這個文本id的兩種設備類別的排行榜,同一用戶將只顯示他的最短時間,時間越少排名越靠前,用時相同者將並列。排行榜將自動用字數/時間來計算打字速度,保留一位小數。在單個文本的排行榜的項目有:用戶顯示名稱、輸入法名、用時、速度。排行榜上方將有文本的信息,包括文本名稱、字數。

五,剛進入這個頁面時,有總排行榜,將所有文本所有用戶的打字速度來進行排行,分為「電腦、手機」兩類。總排行榜的項目有:用戶顯示名、輸入法名、文本名稱,文本字數、打字速度。

六,代碼將用php和js編寫,由snippets擴展將代碼嵌入至一個page中。用戶在上傳信息時,要用到wp_ajax,meta的json要用php來讀取而不能曝露至前端。保存數據前檢查不能出現\和"等較難轉義的符號。如有必要可對所上傳的ejtype的代碼進行更改,以應用於這個打字比賽程式。

七,整個頁面要用css美化,不要使用原生的html樣式。

gemini pro:

代码: 全选

這是一個非常完整且具備挑戰性的整合開發需求。為了在 WordPress 中實現這個打字比賽系統,我們需要分成兩個部分來進行:

1. **修改原有的 `ejtype` HTML 代碼**:讓它能接受 WordPress 傳來的文本,並在打字結束時將成績傳回給 WordPress。
2. **編寫 WordPress Snippet (PHP + JS + CSS)**:處理用戶權限、資料儲存(Page Meta)、介面切換、全螢幕控制以及排行榜運算。

以下是為您量身打造的解決方案。

---

### 第一步:修改上傳的 `ejtype20250531.html`

為了讓 iframe 內的打字程式能與外部 WordPress 網頁溝通,請在 `ejtype20250531.html` 的 `<script>` 標籤中加入和修改以下代碼:

**1. 接收 WordPress 傳來的文本:**
在 `<script>` 區塊的最底部(`});` 之前)加入這段事件監聽:

```javascript
window.addEventListener('message', (event) => {
    if (event.data && event.data.action === 'load_wp_text') {
        const title = event.data.title;
        const content = event.data.content;
        pretxtContent = content;
        
        // 隱藏原有的下拉選單,因為現在由 WP 控制
        document.getElementById('typegameselect').style.display = 'none';
        
        // 載入文本
        selectArticle(title, content);
    }
});

```

**2. 打字結束時回傳成績:**
找到原代碼中的 `function endGame() { ... }`,將其修改為以下內容:

```javascript
function endGame() {
    clearInterval(timer);
    typegamestopBtn.dataset.state = 'stopped';
    typegamespeedSpan.dataset.highlight = 'true';
    gameStarted = false;
    
    // 計算精確時間並回傳給父層 (WordPress)
    const timeDiff = Date.now() - startTime;
    const timeInSeconds = timeDiff / 1000;
    
    window.parent.postMessage({
        action: 'ejtype_game_over',
        time: timeInSeconds,
        chars: typedCorrectChars
    }, '*');
}

```

*修改完成後,請將這個 HTML 檔案上傳至您的伺服器(或媒體庫),並記下它的絕對網址(例如:`https://yoursite.com/ejtype20250531.html`)。*

---

### 第二步:WordPress 整合代碼 (PHP Snippet)

請使用 WPCode 或 Code Snippets 外掛,新增一個 PHP 腳本。這段代碼包含了後端 AJAX 處理、前端介面、樣式 (CSS) 與互動邏輯 (JS)。

**請注意:** 代碼中的 `EJTYPE_HTML_URL` 必須替換為您剛剛上傳的 HTML 檔案的實際網址。

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

function ejtype_competition_render() {
    if (!is_user_logged_in()) {
        return '<div class="ej-notice">請先 <a href="' . wp_login_url(get_permalink()) . '">登入</a> 以參加打字比賽。</div>';
    }

    $page_id = get_the_ID();
    $current_user_id = get_current_user_id();
    $is_admin = 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) ?: [];
    
    $user_registered = 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; max-width: 900px; 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; }
        .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-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; }
    </style>

    <div class="ej-container" id="ejApp">
        <div class="ej-header">
            <h2>🏆 打字比賽系統</h2>
            <div id="ejUserStatus">
                <?php if ($user_registered): ?>
                    歡迎, <span id="ejCurrentName"><?php echo esc_html($user_info['name']); ?></span> 
                    (<?php echo esc_html($user_info['ime']); ?>)
                <?php endif; ?>
            </div>
        </div>

        <div id="ejSectionRegister" class="<?php echo $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 !$user_registered ? 'ej-hidden' : ''; ?>">
            
            <?php if ($is_admin): ?>
            <div style="background: #e9ecef; padding: 15px; border-radius: 8px; margin-bottom: 20px;">
                <h4>⚙️ 管理員:新增比賽文本</h4>
                <div style="display:flex; gap:10px;">
                    <input type="number" id="ejAdminMediaId" placeholder="媒體庫 TXT 檔案 ID">
                    <input type="text" id="ejAdminTextName" placeholder="文本自訂名稱 (選填)">
                    <button class="ej-btn" onclick="ejAdminAddText()">新增/更新</button>
                </div>
            </div>
            <?php endif; ?>

            <div style="display:flex; justify-content:space-between; align-items:center;">
                <h3>排行榜</h3>
                <button class="ej-btn" onclick="ejShowPreGame()">🚀 開始打字</button>
            </div>
            
            <div class="ej-tabs">
                <div class="ej-tab active" onclick="ejSwitchBoard('global')">總排行榜</div>
                <div 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="https://您的網址/ejtype20250531.html"></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'); ?>'
        };

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

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

            jQuery.post(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');

            jQuery.post(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); }
            });
        }

        // UI 切換
        function ejShowPreGame() {
            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('請選擇文本');

            // 獲取文本內容並傳給 iframe
            jQuery.post(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
                    }, '*');
                } else {
                    alert('讀取文本失敗');
                }
            });
        }

        // 接收 iframe 成績
        window.addEventListener('message', (event) => {
            if (event.data && event.data.action === 'ejtype_game_over') {
                const timeUsed = event.data.time;
                alert(`恭喜完成!用時:${timeUsed.toFixed(1)} 秒`);
                
                // 關閉遊戲區
                document.getElementById('ejSectionGame').style.display = 'none';
                if(document.fullscreenElement) document.exitFullscreen();
                document.getElementById('ejSectionGame').classList.remove('web-fullscreen');
                
                // 儲存成績
                jQuery.post(ejConfig.ajaxurl, {
                    action: 'ej_save_score',
                    nonce: ejConfig.nonce,
                    page_id: ejConfig.page_id,
                    text_id: currentTextId,
                    device: currentDevice,
                    time: timeUsed
                }, function(res) {
                    document.getElementById('ejSectionMain').classList.remove('ej-hidden');
                    ejLoadLeaderboard('text', currentTextId);
                });
            }
        });

        // 排行榜載入
        function ejLoadLeaderboard(type, textId = '') {
            document.getElementById('ejLeaderboardArea').innerHTML = '載入中...';
            jQuery.post(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) {
            document.querySelectorAll('.ej-tab').forEach(t => t.classList.remove('active'));
            event.target.classList.add('active');
            let tid = type === 'text' ? document.getElementById('ejSelectText').value : '';
            ejLoadLeaderboard(type, tid);
        }

        // 全螢幕控制
        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_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' => []];
    
    // 只保留最短時間
    if (!isset($results[$text_id][$device][$uid]) || $time < $results[$text_id][$device][$uid]) {
        $results[$text_id][$device][$uid] = $time;
    }
    
    update_post_meta($page_id, '_ejtype_results', $results);
    wp_send_json_success();
}

// 獲取排行榜 HTML
add_action('wp_ajax_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' && $text_id && isset($texts[$text_id])) {
        $t_info = $texts[$text_id];
        echo "<div style='margin-bottom:10px;'><strong>當前文本:</strong> {$t_info['name']} ({$t_info['length']} 字)</div>";
        
        foreach (['pc' => '💻 電腦設備排名', 'mobile' => '📱 手機設備排名'] as $dev => $title) {
            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>";
        }
    } 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];
                }
            }
        }
        
        foreach (['pc' => '💻 電腦設備總排', 'mobile' => '📱 手機設備總排'] as $dev => $title) {
            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>";
        }
    }
    
    wp_send_json_success(ob_get_clean());
}
?>

```

---

### 如何佈署這個系統?

1. **上傳修改後的 HTML**:
將上述**第一步**修改好的 `ejtype20250531.html` 上傳到您的伺服器或 WordPress 媒體庫。
2. **加入 PHP 腳本**:
使用 WordPress 外掛(例如 **WPCode** 或 **Code Snippets**),新增一個 PHP snippet,並將**第二步**的代碼完全貼上。
3. **替換 iframe 網址**:
在貼上的 PHP 代碼中,找到 `<iframe id="ejGameFrame" src="https://您的網址/ejtype20250531.html"></iframe>` 這行,將 `src` 換成你實際的檔案路徑。
4. **建立頁面**:
在 WordPress 中建立一個新頁面(Page),並在內容處放入短代碼 `[ejtype_competition]`。
5. **管理員新增文本**:
管理員登入後進入該頁面,會看到**管理員專區**。您需要先將 `.txt` 純文字檔上傳到 WordPress 媒體庫,點擊該檔案取得其 **ID**(在媒體庫網址列會看到 `item=123`,123就是ID),將該 ID 填入管理介面即可新增比賽文本。

此設計符合您的所有需求,包含 JSON Meta 儲存限制、安全防護(過濾特殊符號)、純後端讀寫 Meta 防曝露,以及自動化的排行榜與 UI 體驗。
附件
ejtype20250531.7z
(9.13 KiB) 已下载 2 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5735
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 179 次
被圈友点赞: 207 次
联系:

Re: 用wordpress搞個打字比賽

帖子 ejsoon »

改進:
一,未登入時也要顯示排行榜。

二,排行榜的手機端和電腦端應左右並列顯示。

三,當沒有選擇一個文本並打完時,不會出現「單篇排行榜」。

四,當選擇一個文本並打完時,「單篇排行榜」ej-tab應該激活高亮。增加顯示「本次時間、速度,你的歷史最佳(如有)」。

只需給出所要修改的地方。

代码: 全选

改進:
一,未登入時也要顯示排行榜。

二,排行榜的手機端和電腦端應左右並列顯示。

三,當沒有選擇一個文本並打完時,不會出現「單篇排行榜」。

四,當選擇一個文本並打完時,「單篇排行榜」ej-tab應該激活高亮。增加顯示「本次時間、速度,你的歷史最佳(如有)」。

只需給出所要修改的地方。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5735
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 179 次
被圈友点赞: 207 次
联系:

Re: 用wordpress搞個打字比賽

帖子 ejsoon »

改進:
當為管理員時,應該顯示所有已加入的文本,它們的名稱,字數,並提供刪除按鈕。

只需給出所要修改的地方。

代码: 全选

改進:
當為管理員時,應該顯示所有已加入的文本,它們的名稱,字數,並提供刪除按鈕。

只需給出所要修改的地方。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5735
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 179 次
被圈友点赞: 207 次
联系:

Re: 用wordpress搞個打字比賽

帖子 ejsoon »

gemini pro:

代码: 全选

這就為您提供只需修改與覆蓋的特定代碼片段,以實現您的四項改進需求。

### 1. 新增 CSS 樣式

在 `<style>` 區塊的最下方加入這幾行(用於排行榜並列、結算成績框):

```css
/* --- 排行榜並列與成績結算樣式 --- */
.ej-board-container { display: flex; gap: 20px; flex-wrap: wrap; }
.ej-board-col { flex: 1; min-width: 300px; }
.ej-stat-box { background: #e3f2fd; padding: 15px; border-radius: 8px; margin-bottom: 15px; border-left: 4px solid #0056b3; line-height: 1.6; }

```

---

### 2. 修改 PHP 前端渲染結構 (處理未登入狀態與單篇標籤)

找到 `ejtype_competition_render()` 函數的**最上方與 HTML 結構部分**,將其覆蓋為:

```php
function ejtype_competition_render() {
    // 移除原本的 is_user_logged_in() return 阻斷,改為記錄變數
    $is_logged_in = is_user_logged_in();
    $page_id = get_the_ID();
    $current_user_id = $is_logged_in ? get_current_user_id() : 0;
    $is_admin = current_user_can('manage_options');
    
    $users_data = get_post_meta($page_id, '_ejtype_users', true) ?: [];
    $texts_data = get_post_meta($page_id, '_ejtype_texts', true) ?: [];
    
    $user_registered = $is_logged_in && isset($users_data[$current_user_id]);
    $user_info = $user_registered ? $users_data[$current_user_id] : null;

    ob_start();
    ?>
    <div class="ej-container" id="ejApp">
        <div class="ej-header">
            <h2>🏆 打字比賽系統</h2>
            <div id="ejUserStatus">
                <?php if ($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): ?>
                    請先 <a href="<?php echo wp_login_url(get_permalink()); ?>" style="color:#0056b3; font-weight:bold;">登入</a> 以參加比賽
                <?php endif; ?>
            </div>
        </div>

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

        <div id="ejSectionMain" class="<?php echo ($is_logged_in && !$user_registered) ? 'ej-hidden' : ''; ?>">
            <div style="display:flex; justify-content:space-between; align-items:center;">
                <h3>排行榜</h3>
                <?php if ($user_registered): ?>
                    <button class="ej-btn" onclick="ejShowPreGame()">🚀 開始打字</button>
                <?php endif; ?>
            </div>
            
            <div class="ej-tabs">
                <div class="ej-tab active" onclick="ejSwitchBoard('global')">總排行榜</div>
                <div class="ej-tab ej-hidden" id="ejTabSingle" onclick="ejSwitchBoard('text')">單篇排行榜</div>
            </div>

            ```

---

### 3. 修改 JS (處理成績回傳、自動切換標籤與插入統計數據)
在 HTML 底部的 `<script>` 中,覆蓋 `ejSwitchBoard`、`window.addEventListener('message')` 和 `ejLoadLeaderboard` 這三個部分:

```javascript
        // 讓 ejSwitchBoard 支援直接高亮與切換特定文本
        function ejSwitchBoard(type, specificTextId = null) {
            document.querySelectorAll('.ej-tab').forEach(t => t.classList.remove('active'));
            if(type === 'global') {
                document.querySelector('.ej-tab[onclick*="global"]').classList.add('active');
            } else {
                document.getElementById('ejTabSingle').classList.add('active');
            }
            
            let tid = specificTextId || (type === 'text' ? document.getElementById('ejSelectText').value : '');
            ejLoadLeaderboard(type, tid);
        }

        // 接收 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');
                
                jQuery.post(ejConfig.ajaxurl, {
                    action: 'ej_save_score',
                    nonce: ejConfig.nonce,
                    page_id: ejConfig.page_id,
                    text_id: currentTextId,
                    device: currentDevice,
                    time: timeUsed
                }, function(res) {
                    document.getElementById('ejSectionMain').classList.remove('ej-hidden');
                    
                    if(res.success) {
                        const d = res.data;
                        // 儲存結算 HTML,準備在載入排行榜時插入
                        window.ejLatestStats = `
                        <div class="ej-stat-box">
                            <strong>🏁 本次結算:</strong> 用時 ${d.current_time.toFixed(1)} 秒,速度 ${d.current_speed} 字/分。
                            <br><strong>🏆 你的歷史最佳:</strong> 用時 ${d.best_time.toFixed(1)} 秒,速度 ${d.best_speed} 字/分。
                            ${d.is_new_best ? '<span style="color:#dc3545; font-weight:bold;"> (🎉 新紀錄!)</span>' : ''}
                        </div>`;
                    }
                    // 修改四:顯示單篇標籤、自動切換並高亮
                    document.getElementById('ejTabSingle').classList.remove('ej-hidden');
                    ejSwitchBoard('text', currentTextId); 
                });
            }
        });

        // 修改載入排行榜,支援插入個人成績統計
        function ejLoadLeaderboard(type, textId = '') {
            document.getElementById('ejLeaderboardArea').innerHTML = '載入中...';
            jQuery.post(ejConfig.ajaxurl, {
                action: 'ej_get_leaderboard',
                nonce: ejConfig.nonce,
                page_id: ejConfig.page_id,
                type: type,
                text_id: textId
            }, function(res) {
                if(res.success) {
                    let html = res.data;
                    // 如果有本次結算成績,則加在排行榜上方
                    if (type === 'text' && window.ejLatestStats) {
                        html = window.ejLatestStats + html;
                        window.ejLatestStats = ''; // 顯示過即清空
                    }
                    document.getElementById('ejLeaderboardArea').innerHTML = html;
                }
            });
        }

```

---

### 4. 修改 PHP AJAX 後端 (回傳結算數據 & 渲染並排結構)

最後,請將後端負責**儲存成績 (`ej_ajax_save_score`)** 與 **渲染排行榜 (`ej_ajax_get_leaderboard`)** 的兩個函數完全替換如下:

```php
// 修改四:讓後端計算並回傳本次成績與最佳成績
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']);
    
    $texts = get_post_meta($page_id, '_ejtype_texts', true) ?: [];
    $text_length = isset($texts[$text_id]) ? $texts[$text_id]['length'] : 0;
    
    $results = get_post_meta($page_id, '_ejtype_results', true) ?: [];
    if (!isset($results[$text_id])) $results[$text_id] = ['pc' => [], 'mobile' => []];
    
    $current_time = $time;
    $current_speed = $text_length > 0 ? round($text_length / ($current_time / 60), 1) : 0;
    
    $best_time = isset($results[$text_id][$device][$uid]) ? $results[$text_id][$device][$uid] : $current_time;
    $is_new_best = ($time <= $best_time || !isset($results[$text_id][$device][$uid]));
    
    if ($is_new_best) {
        $results[$text_id][$device][$uid] = $time;
        $best_time = $time;
    }
    $best_speed = $text_length > 0 ? round($text_length / ($best_time / 60), 1) : 0;
    
    update_post_meta($page_id, '_ejtype_results', $results);
    
    wp_send_json_success([
        'current_time' => $current_time,
        'current_speed' => $current_speed,
        'best_time' => $best_time,
        'best_speed' => $best_speed,
        'is_new_best' => $is_new_best
    ]);
}

// 修改二:利用 flex 排版將兩側排行榜並排輸出
add_action('wp_ajax_nopriv_ej_get_leaderboard', 'ej_ajax_get_leaderboard'); // 允許未登入請求
add_action('wp_ajax_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' && $text_id && isset($texts[$text_id])) {
        $t_info = $texts[$text_id];
        echo "<div style='margin-bottom:10px;'><strong>當前文本:</strong> {$t_info['name']} ({$t_info['length']} 字)</div>";
        echo "<div class='ej-board-container'>"; // ★ Flex 容器
        
        foreach (['pc' => '💻 電腦設備排名', 'mobile' => '📱 手機設備排名'] as $dev => $title) {
            echo "<div class='ej-board-col'><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></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-board-container'>"; // ★ Flex 容器
        foreach (['pc' => '💻 電腦設備總排', 'mobile' => '📱 手機設備總排'] as $dev => $title) {
            echo "<div class='ej-board-col'><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></div>";
        }
        echo "</div>";
    }
    wp_send_json_success(ob_get_clean());
}

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

Re: 用wordpress搞個打字比賽

帖子 ejsoon »

gemini pro要睡到12:52:

代码: 全选

這裡為您提供實現該改進需求所需修改的三個地方(管理員前端 UI、JavaScript 刪除函數、後端 AJAX 刪除處理):

### 一、 修改 PHP 前端渲染中的「管理員專區」

請在 `ejtype_competition_render()` 函數中,找到 `<?php if ($is_admin): ?>` 區塊,將其**完全覆蓋**為以下包含文本列表與刪除按鈕的代碼:

```php
            <?php if ($is_admin): ?>
            <div style="background: #e9ecef; padding: 15px; border-radius: 8px; margin-bottom: 20px;">
                <h4>⚙️ 管理員:新增比賽文本</h4>
                <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>📋 已加入的文本管理</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);">
                        <tr>
                            <th>媒體庫 ID</th>
                            <th>文本名稱</th>
                            <th>字數</th>
                            <th>操作</th>
                        </tr>
                        <?php foreach ($texts_data as $tid => $t): ?>
                            <tr>
                                <td><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; ?>
                    </table>
                <?php else: ?>
                    <p style="color: #666; margin: 0; font-style: italic;">目前尚無加入任何文本。</p>
                <?php endif; ?>
            </div>
            <?php endif; ?>

```

---

### 二、 新增 JavaScript 刪除函數

請在 HTML 底部的 `<script>` 區塊內(可以放在 `ejAdminAddText` 函數的下方),**新增**以下點擊刪除按鈕時執行的 JS 函數:

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

            jQuery.post(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 || '刪除失敗');
                }
            });
        }

```

---

### 三、 新增後端 PHP AJAX 刪除處理

請在整個 Snippet 的最底部(`// ---------------- 後端 AJAX 處理 ----------------` 區域內),**新增**以下用來移除 Page Meta 中文本與成績數據的 PHP 函數:

```php
// 管理員刪除文本與相關成績
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']);
    
    // 1. 從文本數據中刪除
    $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);
        
        // 2. 同步刪除該文本的所有比賽成績紀錄(避免排行榜殘留髒數據)
        $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
一枝独秀
一枝独秀
帖子: 5735
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 179 次
被圈友点赞: 207 次
联系:

Re: 用wordpress搞個打字比賽

帖子 ejsoon »

new3.php當前代碼:

代码: 全选

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

function ejtype_competition_render()
{
  if (!is_user_logged_in()) {
    return '<div class="ej-notice">請先 <a href="' . wp_login_url(get_permalink()) . '">登入</a> 以參加打字比賽。</div>';
  }

  $page_id = get_the_ID();
  $current_user_id = get_current_user_id();
  $is_admin = 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) ?: [];

  $user_registered = 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;
      max-width: 900px;
      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;
    }

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

  <div class="ej-container" id="ejApp">
    <div class="ej-header">
      <h2>🏆 打字比賽系統</h2>
      <div id="ejUserStatus">
        <?php if ($user_registered) : ?>
          歡迎, <span id="ejCurrentName"><?php echo esc_html($user_info['name']); ?></span>
          (<?php echo esc_html($user_info['ime']); ?>)
        <?php endif; ?>
      </div>
    </div>

    <div id="ejSectionRegister" class="<?php echo $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 !$user_registered ? 'ej-hidden' : ''; ?>">

      <?php if ($is_admin) : ?>
        <div style="background: #e9ecef; padding: 15px; border-radius: 8px; margin-bottom: 20px;">
          <h4>⚙️ 管理員:新增比賽文本</h4>
          <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>📋 已加入的文本管理</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);">
              <tr>
                <th>媒體庫 ID</th>
                <th>文本名稱</th>
                <th>字數</th>
                <th>操作</th>
              </tr>
              <?php foreach ($texts_data as $tid => $t) : ?>
                <tr>
                  <td><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; ?>
            </table>
          <?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>
        <button class="ej-btn" onclick="ejShowPreGame()">🚀 開始打字</button>
      </div>

      <div class="ej-tabs">
        <div class="ej-tab active" onclick="ejSwitchBoard('global')">總排行榜</div>
        <div 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="http://wp1.ejsoon.win/wp-content/uploads/2026/05/ejtype20260524.html"></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'); ?>'
    };

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

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

      jQuery.post(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');

      jQuery.post(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;

      jQuery.post(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() {
      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('請選擇文本');

      // 獲取文本內容並傳給 iframe
      jQuery.post(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
          }, '*');
        } else {
          alert('讀取文本失敗');
        }
      });
    }

    // 接收 iframe 成績
    window.addEventListener('message', (event) => {
      if (event.data && event.data.action === 'ejtype_game_over') {
        const timeUsed = event.data.time;
        alert(`恭喜完成!用時:${timeUsed.toFixed(1)} 秒`);

        // 關閉遊戲區
        document.getElementById('ejSectionGame').style.display = 'none';
        if (document.fullscreenElement) document.exitFullscreen();
        document.getElementById('ejSectionGame').classList.remove('web-fullscreen');

        // 儲存成績
        jQuery.post(ejConfig.ajaxurl, {
          action: 'ej_save_score',
          nonce: ejConfig.nonce,
          page_id: ejConfig.page_id,
          text_id: currentTextId,
          device: currentDevice,
          time: timeUsed
        }, function(res) {
          document.getElementById('ejSectionMain').classList.remove('ej-hidden');
          ejLoadLeaderboard('text', currentTextId);
        });
      }
    });

    // 排行榜載入
    function ejLoadLeaderboard(type, textId = '') {
      document.getElementById('ejLeaderboardArea').innerHTML = '載入中...';
      jQuery.post(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) {
      document.querySelectorAll('.ej-tab').forEach(t => t.classList.remove('active'));
      event.target.classList.add('active');
      let tid = type === 'text' ? document.getElementById('ejSelectText').value : '';
      ejLoadLeaderboard(type, tid);
    }

    // 全螢幕控制
    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_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' => []];

  // 只保留最短時間
  if (!isset($results[$text_id][$device][$uid]) || $time < $results[$text_id][$device][$uid]) {
    $results[$text_id][$device][$uid] = $time;
  }

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

// 獲取排行榜 HTML
add_action('wp_ajax_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' && $text_id && isset($texts[$text_id])) {
    $t_info = $texts[$text_id];
    echo "<div style='margin-bottom:10px;'><strong>當前文本:</strong> {$t_info['name']} ({$t_info['length']} 字)</div>";

    foreach (['pc' => '💻 電腦設備排名', 'mobile' => '📱 手機設備排名'] as $dev => $title) {
      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>";
    }
  } 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];
        }
      }
    }

    foreach (['pc' => '💻 電腦設備總排', 'mobile' => '📱 手機設備總排'] as $dev => $title) {
      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>";
    }
  }

  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']);
    
    // 1. 從文本數據中刪除
    $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);
        
        // 2. 同步刪除該文本的所有比賽成績紀錄(避免排行榜殘留髒數據)
        $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
一枝独秀
一枝独秀
帖子: 5735
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 179 次
被圈友点赞: 207 次
联系:

Re: 用wordpress搞個打字比賽

帖子 ejsoon »

在「單文本排行」中,應能選擇文本。

在「文本管理」界面,應能對其排序,同時可以多選,全選,並將它們刪除。
上次由 ejsoon 在 2026年 5月 24日 11:24,总共编辑 1 次。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5735
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 179 次
被圈友点赞: 207 次
联系:

Re: 用wordpress搞個打字比賽

帖子 ejsoon »

改進:
當沒有登入時,也顯示出排行榜。

排行榜的「電腦設備排行」和「手機設備排行」應左右並列顯示。當屏幕較窄時(如果小於768px),則改為用兩個標籤頁切換。

需要明確給出所要修改的地方。

代码: 全选

為實現您的需求(未登入時仍可查看排行榜、電腦/手機排行榜在寬螢幕並列顯示、窄螢幕改為標籤切換),我們需要針對程式碼進行 6 個地方的明確修改。

以下是具體的修改方案:

---

### 修改 1:移除非登入阻斷,調整登入與註冊狀態判斷

**修改位置:** 約在檔案最前方的 `ejtype_competition_render` 函式開頭。
**說明:** 刪除原本直接 `return` 提示的邏輯,改為定義狀態變數,讓未登入使用者可以繼續向下渲染結構。

**【將此段程式碼:】**

```php
function ejtype_competition_render()
{
  if (!is_user_logged_in()) {
    return '<div class="ej-notice">請先 <a href="' . wp_login_url(get_permalink()) . '">登入</a> 以參加打字比賽。</div>';
  }

  $page_id = get_the_ID();
  $current_user_id = get_current_user_id();
  $is_admin = 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) ?: [];

  $user_registered = isset($users_data[$current_user_id]);
  $user_info = $user_registered ? $users_data[$current_user_id] : null;

```

**【修改為:】**

```php
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) ?: [];

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

```

---

### 修改 2:新增並列與手機版切換標籤的 CSS 樣式

**修改位置:** 在 `<style>` 標籤內(例如在 `.ej-hidden` 樣式下方)。
**說明:** 新增 Flex 佈局以實現左右並列,並加入 Media Query 當螢幕小於 `768px` 時切換為標籤頁(Tabs)模式。

**【在 `<style>` 內新增以下程式碼:】**

```css
    /* --- 設備排行榜並列與手機標籤切換樣式 --- */
    .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;
      }
    }

```

---

### 修改 3:調整前端 HTML 區塊顯示邏輯與提示

**修改位置:** 網頁主體 HTML 區塊(包含 `#ejUserStatus`、`#ejSectionRegister` 與 `#ejSectionMain`)。
**說明:** 調整控制顯示與隱藏的 class,當未登入時,不顯示註冊區、顯示登入提示、不隱藏主排行榜區,並隱藏「開始打字」按鈕。

**【將此段 HTML 程式碼:】**

```html
    <div class="ej-header">
      <h2>🏆 打字比賽系統</h2>
      <div id="ejUserStatus">
        <?php if ($user_registered) : ?>
          歡迎, <span id="ejCurrentName"><?php echo esc_html($user_info['name']); ?></span>
          (<?php echo esc_html($user_info['ime']); ?>)
        <?php endif; ?>
      </div>
    </div>

    <div id="ejSectionRegister" class="<?php echo $user_registered ? 'ej-hidden' : ''; ?>">
      <h3>請先填寫參賽資料</h3>
      ...
    </div>

    <div id="ejSectionMain" class="<?php echo !$user_registered ? 'ej-hidden' : ''; ?>">
      ...
      <div style="display:flex; justify-content:space-between; align-items:center;">
        <h3>排行榜</h3>
        <button class="ej-btn" onclick="ejShowPreGame()">🚀 開始打字</button>
      </div>

```

**【修改為:】**

```html
    <div class="ej-header">
      <h2>🏆 打字比賽系統</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>

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

```

---

### 修改 4:新增 JavaScript 切換手機版排行榜標籤的函式

**修改位置:** 在 `<script>` 區塊內,任意一個 function 旁邊(例如在 `ejSwitchBoard` 下方)。
**說明:** 提供給手機版標籤點擊時,切換電腦/手機排行榜顯示狀態的 JS 腳本。

**【在 `<script>` 內新增以下程式碼:】**

```javascript
    // 切換手機版「電腦/手機」設備排行榜標籤
    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; // 找到緊鄰的 .ej-boards-wrapper
      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');
      }
    }

```

---

### 修改 5:允許未登入使用者發送 AJAX 排行榜請求

**修改位置:** 在後端 AJAX 處理區塊,約在 `function ej_ajax_get_leaderboard()` 定義的上方。
**說明:** 預設 WordPress 的 `wp_ajax_` 只允許登入用戶。必須註冊 `wp_ajax_nopriv_` 才能讓訪客也順利載入排行榜資料。

**【將此段程式碼:】**

```php
// 獲取排行榜 HTML
add_action('wp_ajax_ej_get_leaderboard', 'ej_ajax_get_leaderboard');
function ej_ajax_get_leaderboard()

```

**【修改為:】**

```php
// 獲取排行榜 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()

```

---

### 修改 6:重構後端排行榜 HTML 輸出結構

**修改位置:** 整體替換 `function ej_ajax_get_leaderboard()` 內部的 HTML 輸出邏輯。
**說明:** 在輸出的 HTML 中,加上我們在前面步驟設定的 `.ej-device-tabs`(手機版頁籤)以及 `.ej-boards-wrapper`(並列容器)等對應的標籤及 class。

**【請完整替換 `ej_ajax_get_leaderboard()` 函式內容如下:】**

```php
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' && $text_id && isset($texts[$text_id])) {
    $t_info = $texts[$text_id];
    echo "<div style='margin-bottom:10px;'><strong>當前文本:</strong> {$t_info['name']} ({$t_info['length']} 字)</div>";

    // 1. 輸出手機專用切換標籤 (預設電腦 active)
    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>';

    // 2. 進入並列容器
    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>"; // 結束 .ej-board-column
    }
    echo '</div>'; // 結束 .ej-boards-wrapper

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

    // 1. 輸出手機專用切換標籤 (預設電腦 active)
    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>';

    // 2. 進入並列容器
    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>"; // 結束 .ej-board-column
    }
    echo '</div>'; // 結束 .ej-boards-wrapper
  }

  wp_send_json_success(ob_get_clean());
}

```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
回复

在线用户

正浏览此版面之用户: ejsoon 和 4 访客