代码: 全选
<?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('找不到該文本數據');
}