代码: 全选
這份程式碼中總共有 **9 處** 使用了 `jQuery.post` 來處理與 WordPress 後端的非同步請求(AJAX)。
為了用最優雅、改動最少且不容易出錯的方式將它們全部改為原生 JavaScript,我們可以使用現代瀏覽器內建的 `fetch()` API。
這裡有一個完美的解決策略:我們會在 `<script>` 的開頭封裝一個名為 `ejPost` 的原生 JavaScript 工具函式(功能完全對標 `jQuery.post`,且完美支援陣列參數轉換)。定義好後,**只要把原本的 `jQuery.post` 直接替換成 `ejPost` 即可,其餘內部的資料欄位和回呼函式(Callback)完全不需要修改!**
---
## 🛠️ 明確修改步驟說明
### 1. 新增原生 `ejPost` 工具函式
請在 `<script>` 標籤內的最上方(`const ejConfig = ...` 的下方),新增以下這段原生 JS 傳輸函式:
```javascript
// 用於取代 jQuery.post 的原生 fetch 封裝工具
function ejPost(url, data, callback) {
const params = new URLSearchParams();
for (const key in data) {
if (Array.isArray(data[key])) {
// 針對多選刪除等陣列資料,自動轉換為 WordPress 後端能識別的 key[] 格式
data[key].forEach(val => params.append(`${key}[]`, val));
} else {
params.append(key, data[key]);
}
}
fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: params
})
.then(response => response.json())
.then(res => callback(res))
.catch(err => console.error('AJAX 請求失敗:', err));
}
```
### 2. 需要替換 `jQuery.post` 的 9 個地方
請在程式碼中搜尋並將 `jQuery.post` 全數修改為 `ejPost`,分佈於以下函式中:
1. **`ejAdminSaveTextOrder()`**:儲存文本排序。
2. **`ejAdminToggleDeleteSelectedMode()`**:執行選中刪除文本。
3. **`ejAdminDeleteAllTexts()`**:刪除全部文本與排行榜。
4. **`ejRegisterUser()`**:註冊/保存參賽選手資料。
5. **`ejAdminAddText()`**:管理員新增比賽文本。
6. **`ejAdminDeleteText(textId)`**:管理員個別刪除文本。
7. **`ejStartGame()`**:點擊進入遊戲時載入文本內容。
8. **`window.addEventListener('message', ...)`**:遊戲結束時回傳並儲存成績。
9. **`ejLoadLeaderboard(type, textId = '')`**:載入排行榜 HTML。
---
## 📋 替換後的完整 `<script>` 區塊代碼
為了方便您直接複製使用,以下是已經將**所有 jQuery 移除並完整改為原生 JavaScript** 的 `<script>` 區塊:
```javascript
<script>
const ejConfig = {
ajaxurl: '<?php echo admin_url('admin-ajax.php'); ?>',
page_id: <?php echo $page_id; ?>,
nonce: '<?php echo wp_create_nonce('ejtype_nonce'); ?>'
};
// 用於取代 jQuery.post 的原生 fetch 封裝工具
function ejPost(url, data, callback) {
const params = new URLSearchParams();
for (const key in data) {
if (Array.isArray(data[key])) {
data[key].forEach(val => params.append(`${key}[]`, val));
} else {
params.append(key, data[key]);
}
}
fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: params
})
.then(response => response.json())
.then(res => callback(res))
.catch(err => console.error('AJAX 請求失敗:', err));
}
// 狀態變數定義
let isDeleteSelectedMode = false;
let selectedOrderTid = null;
let checkedDeleteTids = [];
// 設備偵測
document.addEventListener('DOMContentLoaded', () => {
const isMobile = window.innerWidth <= 768;
const deviceSelect = document.getElementById('ejSelectDevice');
if (deviceSelect) deviceSelect.value = isMobile ? 'mobile' : 'pc';
ejLoadLeaderboard('global');
});
// 管理員欄目顯示/隱藏切換
function ejToggleAdminPanel() {
const wrapper = document.getElementById('ejAdminWrapperBox');
const showBtn = document.getElementById('ejAdminToggleShowBtn');
if (!wrapper) return;
if (wrapper.classList.contains('ej-hidden')) {
wrapper.classList.remove('ej-hidden');
showBtn.classList.add('ej-hidden');
} else {
wrapper.classList.add('ej-hidden');
showBtn.classList.remove('ej-hidden');
}
}
// 處理媒體庫 ID 點擊事件 (包含排序與多選刪除核心邏輯)
function ejHandleTextIdClick(element, tid) {
if (isDeleteSelectedMode) {
// 多選刪除模式
const index = checkedDeleteTids.indexOf(tid);
if (index > -1) {
checkedDeleteTids.splice(index, 1);
element.style.background = '';
} else {
checkedDeleteTids.push(tid);
element.style.background = '#ffc107'; // 黃色代表選中
}
} else {
// 排序移動模式
const allIdCells = Array.from(document.querySelectorAll('.ej-text-id-cell'));
if (selectedOrderTid === null) {
selectedOrderTid = tid;
element.style.background = '#b3d7ff'; // 藍色代表首選高亮
} else {
if (selectedOrderTid === tid) {
// 重複點擊同一個,取消選取
selectedOrderTid = null;
element.style.background = '';
return;
}
// 獲取 A 與 B 的元素與所在 Tr
const cellA = allIdCells.find(c => parseInt(c.dataset.id) === selectedOrderTid);
const cellB = element;
if (!cellA || !cellB) return;
const rowA = cellA.closest('tr');
const rowB = cellB.closest('tr');
const indexA = allIdCells.indexOf(cellA);
const indexB = allIdCells.indexOf(cellB);
// 依照先後順序插入
if (indexA < indexB) {
rowB.parentNode.insertBefore(rowA, rowB.nextSibling); // A 在前,移到 B 後面
} else {
rowB.parentNode.insertBefore(rowA, rowB); // A 在後,移到 B 前面
}
// 清理樣式與暫存狀態
cellA.style.background = '';
selectedOrderTid = null;
// 非同步儲存新順序至後端
ejAdminSaveTextOrder();
}
}
}
// 發送排序數據至後端(已改為原生 JS)
function ejAdminSaveTextOrder() {
const newOrder = Array.from(document.querySelectorAll('.ej-text-id-cell')).map(c => parseInt(c.dataset.id));
ejPost(ejConfig.ajaxurl, {
action: 'ej_admin_reorder_texts',
nonce: ejConfig.nonce,
page_id: ejConfig.page_id,
order: newOrder
}, function(res) {
if (!res.success) alert('排序保存失敗: ' + (res.data || '未知錯誤'));
});
}
// 切換至多選刪除模式或執行刪除(已改為原生 JS)
function ejAdminToggleDeleteSelectedMode() {
const btn = document.getElementById('ejBtnDeleteSelected');
const cancelBtn = document.getElementById('ejBtnCancelDelete');
const promptText = document.getElementById('ejDeletePromptText');
if (!isDeleteSelectedMode) {
// 進入多選刪除模式
isDeleteSelectedMode = true;
// 清除可能殘留的排序高亮
if (selectedOrderTid !== null) {
const prevCell = document.querySelector(`.ej-text-id-cell[data-id="${selectedOrderTid}"]`);
if (prevCell) prevCell.style.background = '';
selectedOrderTid = null;
}
cancelBtn.classList.remove('ej-hidden');
promptText.classList.remove('ej-hidden');
btn.innerText = '🔥 執行刪除';
btn.classList.add('ej-btn-danger');
} else {
// 執行多選刪除
if (checkedDeleteTids.length === 0) {
alert('請先點擊文本媒體庫 ID 進行選擇!');
return;
}
if (!confirm(`確定要刪除已選中的 ${checkedDeleteTids.length} 項文本及其關聯成績嗎?此動作無法撤銷!`)) return;
ejPost(ejConfig.ajaxurl, {
action: 'ej_admin_delete_multiple_texts',
nonce: ejConfig.nonce,
page_id: ejConfig.page_id,
text_ids: checkedDeleteTids
}, function(res) {
if (res.success) {
alert('選中刪除成功!');
location.reload();
} else {
alert(res.data || '刪除失敗');
}
});
}
}
// 取消多選刪除模式
function ejAdminCancelDeleteMode() {
isDeleteSelectedMode = false;
checkedDeleteTids = [];
document.querySelectorAll('.ej-text-id-cell').forEach(c => c.style.background = '');
document.getElementById('ejBtnCancelDelete').classList.add('ej-hidden');
document.getElementById('ejDeletePromptText').classList.add('ej-hidden');
const btn = document.getElementById('ejBtnDeleteSelected');
btn.innerText = '✏️ 選中刪除';
btn.classList.remove('ej-btn-danger');
}
// 全部刪除文本與數據(已改為原生 JS)
function ejAdminDeleteAllTexts() {
if (!confirm('⚠️ 警告:這將會清空「所有」參賽文本以及整個打字系統的排行榜數據!此操作不可逆,確定要繼續嗎?')) return;
ejPost(ejConfig.ajaxurl, {
action: 'ej_admin_delete_all_texts',
nonce: ejConfig.nonce,
page_id: ejConfig.page_id
}, function(res) {
if (res.success) {
alert('所有文本與排行榜數據已成功清空!');
location.reload();
} else {
alert(res.data || '清空失敗');
}
});
}
// 註冊用戶(已改為原生 JS)
function ejRegisterUser() {
const name = document.getElementById('ejRegName').value.trim();
const ime = document.getElementById('ejRegIme').value.trim();
if (!name || !ime) return alert('請填寫完整資訊');
ejPost(ejConfig.ajaxurl, {
action: 'ej_save_user',
nonce: ejConfig.nonce,
page_id: ejConfig.page_id,
name: name,
ime: ime
}, function(res) {
if (res.success) location.reload();
else alert(res.data);
});
}
// 管理員新增文本(已改為原生 JS)
function ejAdminAddText() {
const mediaId = document.getElementById('ejAdminMediaId').value;
const name = document.getElementById('ejAdminTextName').value;
if (!mediaId) return alert('請輸入媒體庫ID');
ejPost(ejConfig.ajaxurl, {
action: 'ej_admin_text',
nonce: ejConfig.nonce,
page_id: ejConfig.page_id,
media_id: mediaId,
name: name
}, function(res) {
if (res.success) {
alert('新增成功');
location.reload();
} else {
alert(res.data);
}
});
}
// 管理員個別刪除文本(已改為原生 JS)
function ejAdminDeleteText(textId) {
if (!confirm('確定要刪除此文本嗎?這將會從比賽清單中移除該文本並清除其對應的排行榜成績(不會影響媒體庫原始檔案)。')) return;
ejPost(ejConfig.ajaxurl, {
action: 'ej_admin_delete_text',
nonce: ejConfig.nonce,
page_id: ejConfig.page_id,
text_id: textId
}, function(res) {
if (res.success) {
alert('刪除成功!');
location.reload();
} else {
alert(res.data || '刪除失敗');
}
});
}
// UI 切換
function ejShowPreGame() {
const summaryDiv = document.getElementById('ejGameResultSummary');
if (summaryDiv) summaryDiv.classList.add('ej-hidden');
document.getElementById('ejSectionMain').classList.add('ej-hidden');
document.getElementById('ejSectionPreGame').classList.remove('ej-hidden');
}
function ejCancelPreGame() {
document.getElementById('ejSectionPreGame').classList.add('ej-hidden');
document.getElementById('ejSectionMain').classList.remove('ej-hidden');
}
let currentTextId = '';
let currentDevice = '';
// 開始遊戲(已改為原生 JS)
function ejStartGame() {
currentTextId = document.getElementById('ejSelectText').value;
currentDevice = document.getElementById('ejSelectDevice').value;
if (!currentTextId) return alert('請選擇文本');
ejPost(ejConfig.ajaxurl, {
action: 'ej_get_text_content',
nonce: ejConfig.nonce,
page_id: ejConfig.page_id,
text_id: currentTextId
}, function(res) {
if (res.success) {
document.getElementById('ejSectionPreGame').classList.add('ej-hidden');
document.getElementById('ejSectionGame').style.display = 'block';
const iframe = document.getElementById('ejGameFrame');
iframe.contentWindow.postMessage({
action: 'load_wp_text',
title: res.data.name,
content: res.data.content
}, '*');
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
const rowHeightInput = iframeDoc.getElementById('typegamerowheight');
if (rowHeightInput) {
// 手機端為 2,電腦端為 5
rowHeightInput.value = (currentDevice === 'mobile') ? '2' : '5';
// 觸發事件確保內部遊戲框架能同步更新
rowHeightInput.dispatchEvent(new Event('input', {
bubbles: true
}));
rowHeightInput.dispatchEvent(new Event('change', {
bubbles: true
}));
}
} else {
alert('讀取文本失敗');
}
});
}
// 接收 iframe 成績(已改為原生 JS)
window.addEventListener('message', (event) => {
if (event.data && event.data.action === 'ejtype_game_over') {
const timeUsed = event.data.time;
document.getElementById('ejSectionGame').style.display = 'none';
if (document.fullscreenElement) document.exitFullscreen();
document.getElementById('ejSectionGame').classList.remove('web-fullscreen');
ejPost(ejConfig.ajaxurl, {
action: 'ej_save_score',
nonce: ejConfig.nonce,
page_id: ejConfig.page_id,
text_id: currentTextId,
device: currentDevice,
time: timeUsed
}, function(res) {
if (res.success) {
const data = res.data;
let summaryHtml = `<h4 style="margin: 0 0 10px 0; font-size: 18px;">🎯 挑戰完成回報</h4>`;
summaryHtml += `<p style="margin: 4px 0;">⏱️ <strong>本次用時:</strong>${data.time.toFixed(1)} 秒 | ⚡ <strong>本次速度:</strong>${data.speed} 字/分</p>`;
summaryHtml += `<p style="margin: 4px 0;">🏆 <strong>您的歷史最佳:</strong>${data.best_time.toFixed(1)} 秒 (${data.best_speed} 字/分)${data.is_new_best ? ' <span style="color: #dc3545; font-weight: bold; -webkit-animation: flash 1s infinite alternate; animation: flash 1s infinite alternate;">🔥 刷新個人紀錄!</span>' : ''}</p>`;
const summaryDiv = document.getElementById('ejGameResultSummary');
if (summaryDiv) {
summaryDiv.innerHTML = summaryHtml;
summaryDiv.classList.remove('ej-hidden');
}
}
document.getElementById('ejSectionMain').classList.remove('ej-hidden');
document.querySelectorAll('.ej-tab').forEach(t => t.classList.remove('active'));
const tabText = document.getElementById('ejTabText');
if (tabText) tabText.classList.add('active');
ejLoadLeaderboard('text', currentTextId);
});
}
});
// 排行榜載入(已改為原生 JS)
function ejLoadLeaderboard(type, textId = '') {
document.getElementById('ejLeaderboardArea').innerHTML = '載入中...';
ejPost(ejConfig.ajaxurl, {
action: 'ej_get_leaderboard',
nonce: ejConfig.nonce,
page_id: ejConfig.page_id,
type: type,
text_id: textId
}, function(res) {
if (res.success) document.getElementById('ejLeaderboardArea').innerHTML = res.data;
});
}
function ejSwitchBoard(type) {
const summaryDiv = document.getElementById('ejGameResultSummary');
if (summaryDiv) summaryDiv.classList.add('ej-hidden');
document.querySelectorAll('.ej-tab').forEach(t => t.classList.remove('active'));
if (window.event && window.event.target) {
window.event.target.classList.add('active');
} else {
const targetId = type === 'global' ? 'ejTabGlobal' : 'ejTabText';
const targetEl = document.getElementById(targetId);
if (targetEl) targetEl.classList.add('active');
}
let tid = type === 'text' ? (currentTextId || document.getElementById('ejSelectText').value) : '';
ejLoadLeaderboard(type, tid);
}
// 切換手機版「電腦/手機」設備排行榜標籤
function ejSwitchDeviceTab(device, element) {
const parent = element.parentElement;
parent.querySelectorAll('.ej-device-tab').forEach(t => t.classList.remove('active'));
element.classList.add('active');
const wrapper = parent.nextElementSibling;
if (device === 'pc') {
wrapper.querySelector('.ej-board-pc').classList.remove('ej-hidden-mobile');
wrapper.querySelector('.ej-board-mobile').classList.add('ej-hidden-mobile');
} else {
wrapper.querySelector('.ej-board-pc').classList.add('ej-hidden-mobile');
wrapper.querySelector('.ej-board-mobile').classList.remove('ej-hidden-mobile');
}
}
// 全螢幕控制
function ejToggleWebFS() {
document.getElementById('ejSectionGame').classList.toggle('web-fullscreen');
}
function ejToggleDeviceFS() {
const elem = document.getElementById('ejSectionGame');
if (!document.fullscreenElement) {
elem.requestFullscreen().catch(err => alert('全螢幕請求被拒絕'));
} else {
document.exitFullscreen();
}
}
</script>
```