首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP + AJAX 實作文章自動儲存草稿功能

     編輯:PHP 相關知識

功能需求

  • 使用者編輯文章時,定期自動將標題、內容儲存至資料庫(草稿表)
  • 支援手動觸發、倒數計時顯示、啟用/停用計時器
  • 頁面載入時檢查是否有草稿,提示使用者載入
  • 發文成功後清除草稿

資料庫設計

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 同步草稿狀態
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved