一,參加比賽需註冊和登入,在開始打字前應先登入。登入後記錄其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 體驗。