文章、評論、產品常需「讚/踩」互動。本文提供完整前端實作 (HTML 結構、CSS 樣式、jQuery AJAX 互動),後端只需依規範回傳 success/done/error 即可串接,支援防重複投票、狀態鎖定、用戶提示。
<div id="vote" data-id="<?php echo $articleId; ?>">
<span id="dig" class="vote-btn">
<span class="vote-num"><?php echo $digCount; ?></span>
</span>
<span id="bury" class="vote-btn">
<span class="vote-num"><?php echo $buryCount; ?></span>
</span>
</div>
data-id 為文章唯一識別碼,.vote-num 顯示當前計數。
#vote {
text-align: center;
margin: 20px 0;
}
.vote-btn {
display: inline-block;
width: 60px;
height: 54px;
margin: 0 20px;
cursor: pointer;
background-repeat: no-repeat;
background-position: center top;
transition: opacity 0.2s;
}
.vote-btn:hover { opacity: 0.8; }
.vote-btn.disabled { cursor: not-allowed; opacity: 0.5; }
#dig { background-image: url("/static/images/dig.png"); }
#bury { background-image: url("/static/images/bury.png"); }
.vote-num {
display: block;
margin-top: 36px;
font-size: 14px;
color: #fff;
text-shadow: 0 1px 1px rgba(0,0,0,0.3);
}
建議將 dig/bury 圖標合併為 CSS Sprite,減少 HTTP 請求。
$(function () {
$('#vote .vote-btn').on('click', function () {
var $btn = $(this);
var $container = $btn.parent();
var articleId = $container.data('id');
var type = $btn.attr('id'); // 'dig' 或 'bury'
// 防重複點擊 (雙重保險:前端鎖定 + 後端驗證)
if ($btn.hasClass('disabled')) return;
$container.find('.vote-btn').addClass('disabled').css('cursor', 'not-allowed');
$.post('/api/vote', { id: articleId, type: type }, function (resp) {
resp = $.trim(resp);
if (resp === 'success') {
// 計數 +1
var $num = $btn.find('.vote-num');
$num.text(parseInt($num.text(), 10) + 1);
// 永久鎖定
$container.find('.vote-btn').off('click').addClass('disabled');
// 踩時額外引導留言
if (type === 'bury') {
alert('感謝您的反饋,歡迎在評論區留言具體建議!');
}
} else if (resp === 'done') {
alert('您已經投過票了,無法重複投票。');
$container.find('.vote-btn').off('click').addClass('disabled');
} else {
alert('投票失敗,請稍後再試。');
// 解除鎖定,允許重試
$container.find('.vote-btn').removeClass('disabled').css('cursor', 'pointer');
}
}).fail(function () {
alert('網路錯誤,請檢查連線後重試。');
$container.find('.vote-btn').removeClass('disabled').css('cursor', 'pointer');
});
});
});
| 情況 | HTTP 狀態 | 回傳內容 | 說明 |
|---|---|---|---|
| 投票成功 | 200 | success | 前端計數 +1,永久鎖定 |
| 已投票 | 200 | done | 前端提示並鎖定 |
| 參數錯誤/未登入 | 400/401 | error 或 JSON 錯誤物件 | 前端解除鎖定允許重試 |
| 伺服器錯誤 | 500 | error | 前端解除鎖定允許重試 |
<?php
// API 端點:/api/vote
session_start();
$articleId = (int)($_POST['id'] ?? 0);
$type = in_array($_POST['type'] ?? '', ['dig', 'bury']) ? $_POST['type'] : '';
if (!$articleId || !$type) { http_response_code(400); exit('error'); }
// 識別用戶:登入用戶用 user_id,未登入用 IP + User-Agent 雜湊
$voterKey = $_SESSION['user_id'] ?? 'anon_' . hash('sha256', $_SERVER['REMOTE_ADDR'] . $_SERVER['HTTP_USER_AGENT']);
// 檢查是否已投票 (Redis Set 或資料庫唯一索引)
$redisKey = "vote:{$articleId}:{$type}";
if ($redis->sIsMember($redisKey, $voterKey)) {
exit('done');
}
// 寫入投票
$redis->multi()
->sAdd($redisKey, $voterKey)
->hIncrBy("vote:counts:{$articleId}", $type, 1)
->exec();
exit('success');
transform: scale(1.2) 彈跳動畫。aria-pressed、role="button"、鍵盤可聚焦操作。此前端代碼結構清晰、邏輯完整,配合簡單的後端 API 即可上線,適合快速整合至任何 PHP 專案 (Laravel、ThinkPHP、CodeIgniter、原生)。