<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
<title></title>
<style type="text/css">* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', sans-serif;
line-height: 1.6;
background: #f5f5f5;
color: #333;
}
.container {
max-width: 1200px;
margin: 0 auto;
display: flex;
gap: 20px;
padding: 20px;
}
.article-section {
flex: 2;
background: white;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
padding: 40px;
}
.comments-section {
flex: 1;
background: white;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
padding: 20px;
height: fit-content;
position: sticky;
top: 20px;
}
.article-title {
font-size: 24px;
font-weight: bold;
margin-bottom: 20px;
color: #1a1a1a;
}
.article-content {
font-size: 16px;
line-height: 1.8;
color: #333;
}
.article-content p {
margin-bottom: 20px;
position: relative;
}
/* 選中文字的樣式 */
.selected-text {
background: linear-gradient(120deg, #a8e6cf 0%, #dcedc1 100%);
padding: 2px 4px;
border-radius: 4px;
position: relative;
cursor: pointer;
}
/* 評論數量標記 */
.comment-count {
display: inline-block;
background: #ff6b6b;
color: white;
font-size: 12px;
padding: 2px 6px;
border-radius: 10px;
margin-left: 5px;
font-weight: bold;
cursor: pointer;
animation: pulse 0.3s ease-in-out;
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.1); }
100% { transform: scale(1); }
}
/* 評論彈窗 */
.comment-popup {
position: fixed;
background: white;
border: 1px solid #e0e0e0;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
padding: 15px;
width: 400px;
z-index: 1000;
display: none;
}
.comment-popup textarea {
width: 100%;
height: 100px;
border: 1px solid #e0e0e0;
border-radius: 6px;
padding: 10px;
font-size: 14px;
resize: vertical;
}
.comment-popup-buttons {
display: flex;
gap: 10px;
margin-top: 10px;
justify-content: flex-end;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
.btn-primary {
background: #4CAF50;
color: white;
}
.btn-primary:hover {
background: #45a049;
}
.btn-secondary {
background: #f5f5f5;
color: #666;
}
.btn-secondary:hover {
background: #e0e0e0;
}
/* 評論區域 */
.comments-header {
font-size: 16px;
font-weight: bold;
margin-bottom: 20px;
color: #1a1a1a;
border-bottom: 2px solid #f0f0f0;
padding-bottom: 10px;
}
.comment-item {
margin-bottom: 20px;
padding: 15px;
background: #f9f9f9;
border-radius: 8px;
border-left: 4px solid #4CAF50;
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.comment-user {
font-weight: bold;
color: #2c3e50;
margin-bottom: 5px;
}
.comment-quote {
background: white;
padding: 8px 12px;
border-radius: 6px;
font-style: italic;
color: #666;
border-left: 3px solid #ddd;
margin-bottom: 8px;
}
.comment-text {
color: #333;
line-height: 1.5;
}
.comment-time {
font-size: 12px;
color: #999;
margin-top: 8px;
}
/* 使用者登錄區域 */
.user-login {
margin-bottom: 20px;
padding: 15px;
background: #e8f5e8;
border-radius: 8px;
}
.user-login input {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
}
/* 選擇提示 */
.selection-hint {
position: fixed;
background: #333;
color: white;
padding: 8px 12px;
border-radius: 6px;
font-size: 12px;
z-index: 999;
display: none;
pointer-events: none;
}
/* 回應式設計 */
@media (max-width: 768px) {
.container {
flex-direction: column;
padding: 10px;
}
.article-section {
padding: 20px;
}
.comments-section {
position: static;
}
}
</style>
<div class="container">
<div class="article-section">
<h1 class="article-title">這是文章標題</h1>
<div class="article-content" id="articleContent">
<p>這是一段可以留言的文章內容。你可以選擇其中的任何句子來留言。。</p>
<p>這是另一段文章內容,同樣可以被選取與留言。</p>
<p>這是另一段文章內容,同樣可以被選取與留言。</p>
<p>這是另一段文章內容,同樣可以被選取與留言。</p>
<p>這是另一段文章內容,同樣可以被選取與留言。</p>
<p>這是另一段文章內容,同樣可以被選取與留言。</p>
<p>這是另一段文章內容,同樣可以被選取與留言。</p>
<p>這是另一段文章內容,同樣可以被選取與留言。</p>
<p>這是另一段文章內容,同樣可以被選取與留言。</p>
<p>這是另一段文章內容,同樣可以被選取與留言。</p>
<p>這是另一段文章內容,同樣可以被選取與留言。</p>
<p>這是另一段文章內容,同樣可以被選取與留言。</p>
<p>這是另一段文章內容,同樣可以被選取與留言。</p>
<p>這是另一段文章內容,同樣可以被選取與留言。</p>
<p>這是另一段文章內容,同樣可以被選取與留言。</p>
<p>這是另一段文章內容,同樣可以被選取與留言。</p>
<p>這是另一段文章內容,同樣可以被選取與留言。</p>
<p>這是另一段文章內容,同樣可以被選取與留言。</p>
<p>這是另一段文章內容,同樣可以被選取與留言。</p>
<p>這是另一段文章內容,同樣可以被選取與留言。</p>
<p>這是另一段文章內容,同樣可以被選取與留言。</p>
<p>這是另一段文章內容,同樣可以被選取與留言。</p>
<p>這是另一段文章內容,同樣可以被選取與留言。</p>
<p>這是另一段文章內容,同樣可以被選取與留言。</p>
<p>這是另一段文章內容,同樣可以被選取與留言。</p>
<p>這是另一段文章內容,同樣可以被選取與留言。</p>
<p>這是另一段文章內容,同樣可以被選取與留言。</p>
<p>這是另一段文章內容,同樣可以被選取與留言。</p>
<p>這是另一段文章內容,同樣可以被選取與留言。</p>
<p>這是另一段文章內容,同樣可以被選取與留言。</p>
<p>這是另一段文章內容,同樣可以被選取與留言。</p>
<p>這是另一段文章內容,同樣可以被選取與留言。</p>
<p>這是另一段文章內容,同樣可以被選取與留言。</p>
<p>這是另一段文章內容,同樣可以被選取與留言。</p>
<p> </p>
</div>
</div>
<div class="comments-section">
<div class="user-login"><input id="username" placeholder="請輸入您的用戶名" type="text" value="訪客用戶" /></div>
<div class="comments-header">評論區 (<span id="commentCount">0</span>)</div>
<div id="commentsList"><!-- 評論將在這裡顯示 --></div>
</div>
</div>
<!-- 評論彈窗 -->
<div class="comment-popup" id="commentPopup"><textarea id="commentText" placeholder="寫下你的想法..."></textarea>
<div class="comment-popup-buttons"><button class="btn btn-secondary">取消</button><button class="btn btn-primary">發表</button></div>
</div>
<!-- 選擇提示 -->
<div class="selection-hint" id="selectionHint">選擇文字後點擊添加評論</div>
<script>
let comments = [];
let currentSelection = null;
let commentIdCounter = 1;
// 初始化
window.addEventListener('DOMContentLoaded', () => {
initializeParagraphIds();
initializeTextSelection();
updateCommentCount();
});
// 為每段文字加上唯一 ID
function initializeParagraphIds() {
const paragraphs = document.querySelectorAll('#articleContent p');
paragraphs.forEach((p, index) => {
p.setAttribute('data-para-id', index);
});
}
// 選取文字時處理
function initializeTextSelection() {
const articleContent = document.getElementById('articleContent');
articleContent.addEventListener('mouseup', handleTextSelection);
articleContent.addEventListener('touchend', handleTextSelection);
document.addEventListener('click', function(e) {
if (!e.target.closest('.comment-popup') && !e.target.closest('#articleContent')) {
clearSelection();
}
});
}
function handleTextSelection(e) {
const selection = window.getSelection();
const selectedText = selection.toString().trim();
if (!selectedText) return;
const range = selection.getRangeAt(0);
let para = range.startContainer;
while (para && para.tagName !== 'P') {
para = para.parentNode;
}
if (!para) return;
const paraId = para.dataset.paraId;
currentSelection = { text: selectedText, paraId: paraId };
// 改成智慧浮動定位
showCommentPopupFromSelection();
}
function showCommentPopupFromSelection() {
const popup = document.getElementById('commentPopup');
const textarea = document.getElementById('commentText');
const width = 600;
const height = 300;
const selection = window.getSelection();
if (selection.rangeCount === 0) return;
const range = selection.getRangeAt(0);
const rect = range.getBoundingClientRect();
if (!rect) return;
// 計算理想彈窗位置
let left = rect.left ;
let top = rect.bottom ;
// 避免超出畫面邊界(右邊與下方)
if (left + width > window.innerWidth + window.scrollX) {
left = window.innerWidth + window.scrollX - width - 20;
}
if (top + height > window.innerHeight + window.scrollY) {
top = rect.top + window.scrollY - height - 10;
}
// 保底:避免超出上方與左邊
if (left < 0) left = 10;
if (top < 0) top = 10;
// 顯示彈窗
popup.style.left = left + 'px';
popup.style.top = top + 'px';
popup.style.display = 'block';
textarea.focus();
}
function closeCommentPopup() {
document.getElementById('commentPopup').style.display = 'none';
document.getElementById('commentText').value = '';
clearSelection();
}
function clearSelection() {
window.getSelection().removeAllRanges();
currentSelection = null;
}
function submitComment() {
const text = document.getElementById('commentText').value.trim();
const user = document.getElementById('username').value.trim() || '匿名使用者';
const { text: quote, paraId } = currentSelection;
if (!text || !quote || paraId == null) return;
const key = `${paraId}::${quote}`;
const comment = {
id: commentIdCounter++,
user: user,
quote: quote,
text: text,
time: new Date().toLocaleString('zh-CN'),
paraId: paraId,
key: key
};
comments.push(comment);
highlightTextInParagraph(paraId, quote, key);
updateCommentsDisplay();
updateCommentCount();
closeCommentPopup();
}
function highlightTextInParagraph(paraId, quoteText, key) {
const p = document.querySelector(`p[data-para-id="${paraId}"]`);
if (!p) return;
const html = p.innerHTML;
if (html.includes(`data-highlight="${key}"`)) {
const countSpan = p.querySelector(`.selected-text[data-highlight="${key}"] .comment-count`);
if (countSpan) {
const count = parseInt(countSpan.textContent);
countSpan.textContent = count + 1;
}
return;
}
const safeQuote = quoteText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const pattern = new RegExp(`(${safeQuote})`);
const replaced = html.replace(pattern, `<span class="selected-text" data-highlight="${key}">$1 <span class="comment-count">1</span></span>`);
p.innerHTML = replaced;
const span = p.querySelector(`.selected-text[data-highlight="${key}"]`);
if (span) {
span.onclick = () => scrollToComment(commentIdCounter - 1);
}
}
function updateCommentsDisplay() {
const list = document.getElementById('commentsList');
list.innerHTML = '';
comments.slice().reverse().forEach(comment => {
const item = document.createElement('div');
item.className = 'comment-item';
item.setAttribute('data-comment-id', comment.id);
item.innerHTML = `
<div class="comment-user">${comment.user}</div>
<div class="comment-quote">"${comment.quote}"</div>
<div class="comment-text">${comment.text}</div>
<div class="comment-time">${comment.time}</div>
`;
list.appendChild(item);
});
}
function scrollToComment(commentId) {
const el = document.querySelector(`.comment-item[data-comment-id="${commentId}"]`);
if (el) {
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
el.style.animation = 'pulse 0.6s ease-in-out';
setTimeout(() => el.style.animation = '', 600);
}
}
function updateCommentCount() {
document.getElementById('commentCount').textContent = comments.length;
}
// 快捷鍵支援
document.querySelector('.btn-primary').addEventListener('click', submitComment);
document.querySelector('.btn-secondary').addEventListener('click', closeCommentPopup);
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') closeCommentPopup();
if (e.ctrlKey && e.key === 'Enter' && document.getElementById('commentPopup').style.display === 'block') submitComment();
});
</script>
|