功能需求
- 使用者編輯文章時,定期自動將標題、內容儲存至資料庫(草稿表)
- 支援手動觸發、倒數計時顯示、啟用/停用計時器
- 頁面載入時檢查是否有草稿,提示使用者載入
- 發文成功後清除草稿
資料庫設計
CREATE TABLE `drafts` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`user_id` INT UNSIGNED NOT NULL,
`title` VARCHAR(255) NOT NULL DEFAULT '',
`content` LONGTEXT NOT NULL,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX `idx_user` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
後端 API (ajax.php)
<?php
// ajax.php
require_once 'db.php'; // PDO 連線
session_start();
header('Content-Type: application/json; charset=utf-8');
$action = $_GET['act'] ?? '';
$userId = $_SESSION['user_id'] ?? 0;
if (!$userId) {
echo json_encode(['status' => 'error', 'msg' => '未登入']);
exit;
}
$pdo = getPDO();
switch ($action) {
case 'autosave':
$title = trim($_POST['title'] ?? '');
$content = $_POST['content'] ?? '';
$postTime = $_POST['postTime'] ?? date('Y-m-d H:i:s');
if ($title === '' && $content === '') {
echo json_encode(['status' => 'error', 'msg' => '內容為空,無需儲存']);
break;
}
$stmt = $pdo->prepare(
"INSERT INTO drafts (user_id, title, content, created_at) VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE title=VALUES(title), content=VALUES(content), updated_at=NOW()"
);
// 假設每位使用者僅保留一份草稿,可用 user_id 做唯一鍵或另建 unique index
$stmt->execute([$userId, $title, $content, $postTime]);
echo json_encode(['status' => 'ok', 'msg' => '草稿已自動儲存']);
break;
case 'loaddraft':
$stmt = $pdo->prepare("SELECT title, content, updated_at FROM drafts WHERE user_id = ? ORDER BY updated_at DESC LIMIT 1");
$stmt->execute([$userId]);
$draft = $stmt->fetch(PDO::FETCH_ASSOC);
if ($draft) {
echo json_encode(['status' => 'ok', 'data' => $draft]);
} else {
echo json_encode(['status' => 'empty', 'msg' => '無草稿']);
}
break;
case 'cleardraft':
$pdo->prepare("DELETE FROM drafts WHERE user_id = ?")->execute([$userId]);
echo json_encode(['status' => 'ok', 'msg' => '草稿已清除']);
break;
default:
echo json_encode(['status' => 'error', 'msg' => '未知操作']);
}
前端 JavaScript (editor.js)
// editor.js
const AUTOSAVE_INTERVAL = 60; // 秒
let autosaveTimer = null;
let countdown = AUTOSAVE_INTERVAL;
const $title = document.getElementById('title');
const $content = document.getElementById('content'); // 或編輯器實例
const $countdownEl = document.getElementById('autosave-countdown');
const $statusEl = document.getElementById('autosave-status');
const $toggleBtn = document.getElementById('autosave-toggle');
function updateCountdownUI() {
$countdownEl.textContent = `${countdown} 秒`;
}
function startCountdown() {
clearInterval(autosaveTimer);
countdown = AUTOSAVE_INTERVAL;
updateCountdownUI();
$toggleBtn.textContent = '停止計時';
$toggleBtn.onclick = stopCountdown;
autosaveTimer = setInterval(() => {
countdown--;
updateCountdownUI();
if (countdown <= 0) {
clearInterval(autosaveTimer);
doAutosave();
}
}, 1000);
}
function stopCountdown() {
clearInterval(autosaveTimer);
$toggleBtn.textContent = '開始計時';
$toggleBtn.onclick = startCountdown;
$countdownEl.textContent = '已暫停';
}
function doAutosave() {
if (!$title.value.trim() && !$content.value.trim()) {
$statusEl.textContent = '內容為空,無需儲存';
$statusEl.className = 'warning';
return;
}
$statusEl.textContent = '正在儲存草稿...';
$statusEl.className = 'saving';
const formData = new FormData();
formData.append('title', $title.value);
formData.append('content', $content.value);
formData.append('postTime', new Date().toISOString().slice(0, 19).replace('T', ' '));
fetch('ajax.php?act=autosave', { method: 'POST', body: formData })
.then(r => r.json())
.then(res => {
if (res.status === 'ok') {
$statusEl.textContent = '草稿已自動儲存,可在意外發生時載入';
$statusEl.className = 'success';
checkDraft(); // 更新草稿列表
startCountdown(); // 重新開始倒數
} else {
$statusEl.textContent = '儲存失敗:' + res.msg;
$statusEl.className = 'error';
}
})
.catch(() => {
$statusEl.textContent = '網路錯誤,儲存失敗';
$statusEl.className = 'error';
});
}
function checkDraft() {
fetch('ajax.php?act=loaddraft')
.then(r => r.json())
.then(res => {
if (res.status === 'ok' && res.data) {
$statusEl.innerHTML = `發現草稿(更新於 ${res.data.updated_at}):
`;
}
});
}
function loadDraft() {
fetch('ajax.php?act=loaddraft')
.then(r => r.json())
.then(res => {
if (res.status === 'ok' && res.data) {
$title.value = res.data.title;
$content.value = res.data.content;
$statusEl.textContent = '草稿已載入';
$statusEl.className = 'success';
startCountdown();
}
});
}
function clearDraft() {
fetch('ajax.php?act=cleardraft', { method: 'POST' })
.then(r => r.json())
.then(res => {
if (res.status === 'ok') {
$statusEl.textContent = '草稿已捨棄';
$statusEl.className = 'info';
}
});
}
// 頁面載入時檢查草稿
document.addEventListener('DOMContentLoaded', () => {
checkDraft();
startCountdown();
});
// 發文成功時呼叫 clearDraft()
// 範例:表單送出前
document.getElementById('post-form')?.addEventListener('submit', () => {
clearDraft();
});
HTML 片段
<div id="autosave-bar">
<span id="autosave-countdown">60 秒</span>
<button id="autosave-toggle">停止計時</button>
<span id="autosave-status">準備中...</span>
</div>
<form id="post-form" method="post" action="save_post.php">
<input type="text" id="title" name="title" placeholder="文章標題" required>
<textarea id="content" name="content" placeholder="文章內容" rows="20"></textarea>
<button type="submit">發布文章</button>
</form>
關鍵優化點
- 防抖動:僅在倒數歸零且內容有變更時送出,避免頻繁寫入資料庫
- 內容編碼:使用
FormData 自動處理 multipart/form-data,無需手動 encodeURIComponent
- 編輯器相容:若使用 TinyMCE/CKEditor,取內容方式改為
tinymce.get('content').getContent()
- 多標籤頁同步:可結合
localStorage 或 BroadcastChannel API 同步草稿狀態