1window.clearCommentsListCache = function () { 2 try { 3 var keys = []; 4 for (var i = 0; i < sessionStorage.length; i++) { 5 var k = sessionStorage.key(i); 6 if (k && (k.indexOf('cmt:') === 0 || k.indexOf('cmt2:') === 0)) keys.push(k); 7 } 8 keys.forEach(function (k) { sessionStorage.removeItem(k); }); 9 } catch (e) {} 10}; 11 12class CommentSystem { 13 constructor(options = {}) { 14 this.config = { 15 container: '.comments__list', 16 typeid: 0, 17 type: 'news', 18 source_title: document.title || 'Space Waves', 19 source_uri: window.location.pathname || '/', 20 loginNeeded: 1, 21 ...options 22 }; 23 this.init(); 24 } 25 26 init() { 27 this.container = document.querySelector(this.config.container); 28 this.paginationContainer = document.querySelector(this.config.pagination); 29 if (!this.container) return; 30 31 this.loadComments(); 32 this.setupEventListeners(); 33 } 34 35 commentsCacheKey(page) { 36 return 'cmt2:' + this.config.typeid + ':' + this.config.type + ':' + page; 37 } 38 39 applyCommentsPayload(data) { 40 this.renderComments(data.data); 41 this.renderPagination(data.page, data.per_page, data.total || data.data.length); 42 this.config.currentPage = data.page; 43 } 44 45 async loadComments(page = this.config.currentPage) { 46 const pageNum = page || 1; 47 const cacheKey = this.commentsCacheKey(pageNum); 48 49 if (this._inflightKey === cacheKey && this._inflight) { 50 return this._inflight; 51 } 52 53 try { 54 const raw = sessionStorage.getItem(cacheKey); 55 if (raw) { 56 const parsed = JSON.parse(raw); 57 if (parsed && parsed.exp > Date.now() && parsed.payload && parsed.payload.success) { 58 this.applyCommentsPayload(parsed.payload); 59 return; 60 } 61 } 62 } catch (e) {} 63 64 const params = new URLSearchParams({ 65 typeid: this.config.typeid, 66 type: this.config.type, 67 page: String(pageNum), 68 cv: '2' 69 }); 70 71 this._inflightKey = cacheKey; 72 this._inflight = (async () => { 73 try { 74 const response = await fetch('/api/comments?' + params.toString(), { 75 credentials: 'same-origin' 76 }); 77 const data = await response.json(); 78 79 if (data.success) { 80 this.applyCommentsPayload(data); 81 try { 82 sessionStorage.setItem(cacheKey, JSON.stringify({ 83 exp: Date.now() + 180000, 84 payload: data 85 })); 86 } catch (e) {} 87 } else { 88 console.error('No comments:', data.message); 89 } 90 } catch (error) { 91 console.error('Comments loading error:', error); 92 } finally { 93 this._inflight = null; 94 this._inflightKey = null; 95 } 96 })(); 97 98 return this._inflight; 99 } 100 101 renderComments(comments) { 102 let html = ''; 103 comments.forEach(comment => { 104 html += this.renderComment(comment); 105 }); 106 this.container.innerHTML = html || ''; 107 } 108 109 renderPagination(currentPage, perPage, totalItems) { 110 if (!this.paginationContainer) return; 111 112 const totalPages = Math.ceil(totalItems / perPage); 113 if (totalPages <= 1) { 114 this.paginationContainer.innerHTML = ''; 115 return; 116 } 117 118 let html = '<div class="comments__pagination">'; 119 120 const startPage = Math.max(1, currentPage - 2); 121 const endPage = Math.min(totalPages, currentPage + 2); 122 123 if (startPage > 1) { 124 html += `<button class="comments__page-btn" data-page="1">1</button>`; 125 if (startPage > 2) { 126 html += '<span class="pagination__ellipsis">...</span>'; 127 } 128 } 129 130 for (let i = startPage; i <= endPage; i++) { 131 html += `<button class="comments__page-btn ${i === currentPage ? 'comments__page-btn--active' : ''}" data-page="${i}">${i}</button>`; 132 } 133 134 if (endPage < totalPages) { 135 if (endPage < totalPages - 1) { 136 html += '<span class="pagination__ellipsis">...</span>'; 137 } 138 html += `<button class="comments__page-btn" data-page="${totalPages}">${totalPages}</button>`; 139 } 140 141 142 html += '</div>'; 143 this.paginationContainer.innerHTML = html; 144 } 145 146 renderComment(comment) { 147 const avatar = comment.face 148 ? `<img src="${comment.face}" alt="User" width="40" height="40" loading="lazy" decoding="async">` 149 : comment.cname.charAt(0); 150 151 const time = new Date(comment.pubtime * 1000).toLocaleStr
151ing(); 152 153 let repliesHtml = ''; 154 if (comment.reply && comment.reply.length) { 155 comment.reply.forEach(reply => { 156 repliesHtml += this.renderReply(reply); 157 }); 158 } 159 160 return ` 161 <div class="comments__item" data-comment-id="${comment.id}"> 162 <div class="comments__avatar">${avatar}</div> 163 <div class="comments__content"> 164 <div class="comments__header"> 165 <span class="comments__author">${comment.cname}</span> 166 <span class="comments__date">${time}</span> 167 </div> 168 <p class="comments__text">${comment.content}</p> 169 170 <div class="comments__actions"> 171 <button class="comments__vote-btn" data-action="upvote" data-comment-id="${comment.id}"> 172 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="comments__vote-icon comments__vote-icon--up"> 173 <path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-1.91l-.01-.01L23 10z"/> 174 </svg> 175 <span class="comments__vote-count">${comment.up || 0}</span> 176 </button> 177 <button class="comments__vote-btn" data-action="downvote" data-comment-id="${comment.id}"> 178 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="comments__vote-icon comments__vote-icon--down"> 179 <path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v1.91l.01.01L1 14c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.58-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/> 180 </svg> 181 <span class="comments__vote-count">${comment.down || 0}</span> 182 </button> 183 <button class="comments__reply-btn" data-comment-id="${comment.id}"> 184 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="comments__reply-icon"> 185 <path d="M10 9V5l-7 7 7 7v-4.1c5 0 8.5 1.6 11 5.1-1-5-4-10-11-11z"/> 186 </svg> 187 ${_LANG.reply} 188 </button> 189 </div> 190 191 <div class="comments__reply-list"> 192 <div class="comments__reply-form" style="display:none;"> 193 <form class="comments__form"> 194 <input type="hidden" name="parentid" value="${comment.id}"> 195 <input type="hidden" name="typeid" value="${this.config.typeid}"> 196 <input type="hidden" name="type" value="${this.config.type}"> 197 <input type="hidden" name="source_title" value="${this.config.source_title}"> 198 <input type="hidden" name="source_uri" value="${this.config.source_uri}"> 199 <div class="comments__form-group"> 200 <input placeholder="${_LANG.yourname}" type="text" name="comment_name" class="comments__form-input" required> 201 </div> 202 <div class="comments__form-group"> 203 <textarea name="comment_message" class="comments__form-input" placeholder="${_LANG.yourcomment}..." required></textarea> 204 </div> 205 <div class="comments__form-actions"> 206 <button type="submit" class="comments__form-submit">${_LANG.post}</button> 207 <button type="button" class="comments__form-cancel">${_LANG.cancel}</button> 208 </div> 209 </form> 210 </div> 211 ${repliesHtml} 212 </div> 213 </div> 214 </div>`; 215 } 216 217 renderReply(reply, isNew = false) { 218 const avatar = reply.face 219 ? `<img src="${reply.face}" alt="User" width="40" height="40" loading="lazy" decoding="async">` 220 : reply.cname.charAt(0); 221
222 const time = new Date(reply.pubtime * 1000).toLocaleString(); 223 224 return ` 225 <div class="comments__reply-item" data-reply-id="${reply.id}"> 226 <div class="comments__admin-reply"> 227 <div class="comments__admin-header"> 228 <span class="comments__admin-name"> 229 <div class="comments__reply-avatar">${avatar}</div> 230 ${reply.cname} 231 ${isNew ? '<span class="comments__under-review">'+_LANG.under_review+'</span>' : ''} 232 </span> 233 <span class="comments__reply-date">${time}</span> 234 </div> 235 <p class="comments__admin-text">${reply.content}</p> 236 <div class="comments__actions"> 237 <button class="comments__vote-btn" data-action="upvote" data-comment-id="${reply.id}"> 238 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="comments__vote-icon comments__vote-icon--up"> 239 <path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-1.91l-.01-.01L23 10z"/> 240 </svg> 241 <span class="comments__vote-count">${reply.up || 0}</span> 242 </button> 243 <button class="comments__vote-btn" data-action="downvote" data-comment-id="${reply.id}"> 244 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="comments__vote-icon comments__vote-icon--down"> 245 <path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v1.91l.01.01L1 14c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.58-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/> 246 </svg> 247 <span class="comments__vote-count">${reply.down || 0}</span> 248 </button> 249 </div> 250 </div> 251 </div>`; 252 } 253 254 setupEventListeners() { 255 // ä¼åäºä»¶å¤ç - 使ç¨é²æå requestIdleCallback 256 const debounce = (func, wait) => { 257 let timeout; 258 return function executedFunction(...args) { 259 const later = () => { 260 clearTimeout(timeout); 261 func(...args); 262 }; 263 clearTimeout(timeout); 264 timeout = setTimeout(later, wait); 265 }; 266 }; 267 268 document.addEventListener('click', async (e) => { 269 // å页æé®å¤ç 270 const pageBtn = e.target.closest('.comments__page-btn'); 271 if (pageBtn) { 272 e.preventDefault(); 273 const page = parseInt(pageBtn.dataset.page); 274 if (!isNaN(page)) { 275 // ä½¿ç¨ requestIdleCallback ä¼åæ»å¨ 276 requestIdleCallback(() => { 277 this.loadComments(page); 278 requestAnimationFrame(() => { 279 this.container.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'nearest' }); 280 setTimeout(() => window.scrollBy(0, -100), 100); 281 }); 282 }); 283 } 284 return; 285 } 286 287 // æç¥¨æé®å¤ç 288 const voteBtn = e.target.closest('.comments__vote-btn'); 289 if (voteBtn) { 290 const action = voteBtn.dataset.action; 291 const commentId = voteBtn.dataset.commentId; 292 const countEl = voteBtn.querySelector('.comments__vote-count'); 293 294 // 鲿¢éå¤ç¹å» 295 if (voteBtn.disabled) return; 296 voteBtn.disabled = true; 297 298 // ç«å³æ´æ°UI 299 const oldCount = parseInt(countEl.textContent); 300 countEl.textContent = oldCount + 1; 301 302 try { 303 const response = await fetch('/api/submitComment', { 304 method: 'POST', 305 headers: { 306 'Content-Type': 'application/x-www-form-urlencoded', 307 }, 308 body: `vote_action=${action}&comment_id=${commentId}` 309 }); 310 311 const result = await response.json(); 312 if (!result.success) { 313 countEl.textContent = oldCount; // Rollback 314 console.error('Failed:', result.message); 315 showToast(_LANG.try_again_later+': ' + result.message, false); 316 } else { 317 const scope = countEl.closest('.comments__item, .comments__reply-item') || countEl.parentElement; 318 if (scope && typeof result.up === 'number') { 319 const upCount = scope.querySelector('[data-action="upvote"] .comments__vote-count'); 320 if (upCount) upCount.textContent = result.up; 321 } 322 if (scope && typeof result.down === 'number') { 323 const downCount = scope.querySelector('[data-action="downvote"] .comments__vote-count'); 324 if (downCount) downCount.textContent = result.down; 325 } 326 if (typeof window.clearCommentsListCache === 'function') { 327 window.clearCommentsListCache(); 328 } 329 showToast(_LANG.thankyou, true); 330 } 331 } catch (error) { 332 countEl.textContent = oldCount; // Rollback 333 console.error('Error:', error); 334 showToast(_LANG.try_again_later+': ' + error.message, false); 335 } 336 } 337 338 // Reply button 339 if (e.target.closest('.comments__reply-btn')) { 340 const btn = e.target.closest('.comments__reply-btn'); 341 const commentId = btn.dataset.commentId; 342 const commentItem = btn.closest('.comments__item'); 343 const replyList = commentItem.querySelector('.comments__reply-list'); 344 345 // Hide other forms 346 document.querySelectorAll('.comments__reply-form').forEach(f => { 347 if (f !== replyList.querySelector('.comments__reply-form')) { 348 f.style.display = 'none'; 349 } 350 }); 351 352 // Show current form 353 var isLogin = (typeof window.iflogin !== 'undefined' ? window.iflogin : 0); 354 if(isLogin==1 || this.config.loginNeeded !=1){ 355 let form = replyList.querySelector('.comments__reply-form'); 356 if (!form) { 357 form = document.createElement('div'); 358 form.className = 'comments__reply-form'; 359 form.innerHTML = ` 360 <form class="comments__form"> 361 <input type="hidden" name="parentid" value="${commentId}"> 362 <input type="hidden" name="typeid" value="${this.config.typeid}"> 363 <input type="hidden" name="type" value="${this.config.type}"> 364 <div class="comments__form-group"> 365 <input placeholder="Your Name" type="text" name="comment_name" class="comments__form-input" required> 366 </div> 367 <div class="comments__form-group"> 368 <textarea name="comment_message" class="comments__form-input" placeholder="Write your reply..." required></textarea> 369 </div> 370 <div class="comments__form-actions"> 371 <button type="submit" class="comments__form-submit">${_LANG.post}</button> 372 <button type="button" class="comments__form-cancel">${_LANG.cancel}</button> 373 </div> 374 </form> 375 `; 376 replyList.prepend(form); 377 // removed recaptcha render for reply form per request 378 } 379 form.style.display = 'block'; 380 form.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); 381 }else{ 382 if (typeof showConfirm === 'function') { 383 showConfirm(_LANG.continue_by_sign||'continue by sign'); 384 } 385 } 386 387 } 388 389 // Cancel reply 390 if (e.target.closest('.comments__form-cancel')) { 391 e.target.closest('.comments__reply-form').style.display = 'none'; 392 } 393 394 // Submit reply 395 if (e.target.closest('.comments__form-submit')) { 396 e.preventDefault(); 397 const form = e.target.closest('form'); 398 const formContainer = form.closest('.comments__reply-form'); // è·å表åå®¹å¨ 399 const submitBtn = e.target.closest('button'); 400 const commentId = form.querySelector('[name="parentid"]').value; 401 const commentItem = document.querySelector(`[data-comment-id="${commentId}"]`); 402 const replyList = commentItem.querySelector('.comments__reply-list'); 403 404 // Get form values 405 const nameInput = form.querySelector('[name="comment_name"]'); 406 const contentInput = form.querySelector('[name="comment_message"]'); 407 const name = nameInput.value.trim(); 408 const content = contentInput.value.trim(); 409 410 // éªè¯é»è¾ 411 let validationPassed = true; 412 let validationMessage = ''; 413 414 // 妿æå¤é¨éªè¯å½æ° 415 if (typeof validateCommentForm === 'function') { 416 const validationResult = validateCommentForm(name, content); 417 if (!validationResult) { 418 validationMessage = validationResult.message || _LANG.try_again_later; 419 validationPassed = false;
420 } 421 } 422 // å¤ç¨éªè¯ 423 else { 424 if (!name) { 425 validationMessage = 'Please enter your name'; 426 validationPassed = false; 427 } 428 else if (!content) { 429 validationMessage = 'Please enter your comment'; 430 validationPassed = false; 431 } 432 } 433 434 // 妿éªè¯å¤±è´¥ 435 if (!validationPassed) { 436 showToast(validationMessage, false); 437 if (!name) nameInput.focus(); 438 else if (!content) contentInput.focus(); 439 return false; 440 } 441 442 submitBtn.disabled = true; 443 submitBtn.innerHTML = '<span class="spinner"></span> '+_LANG.submitting; 444 445 try { 446 // ç«å³æ¾ç¤ºåå¤(带"å®¡æ ¸ä¸"æ ç¾) 447 replyList.insertAdjacentHTML('afterbegin', this.renderReply({ 448 id: 'new-' + Date.now(), 449 cname: name, 450 content: content, 451 pubtime: Math.floor(Date.now() / 1000), 452 up: 0, 453 down: 0, 454 face: '' 455 }, true)); 456 457 // åå¤æäº¤æ°æ® - 强å¶ä½¿ç¨ URLSearchParams æ ¼å¼ 458 const formData = new FormData(form); 459 const urlParams = new URLSearchParams(); 460 461 // å° FormData 转æ¢ä¸º URLSearchParams 462 for (const [key, value] of formData.entries()) { 463 urlParams.append(key, value); 464 } 465 466 console.log('Sending comment data:', Object.fromEntries(urlParams.entries())); 467 468 // æäº¤å°æå¡å¨ - æç¡®æå®å 容类å 469 const response = await fetch('/api/submitComment', { 470 method: 'POST', 471 headers: { 472 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 473 }, 474 body: urlParams.toString() 475 }); 476 477 const result = await response.json(); 478 479 if (!result.success) { 480 throw new Error(result.message || _LANG.try_again_later); 481 } 482 483 if (typeof window.clearCommentsListCache === 'function') { 484 window.clearCommentsListCache(); 485 } 486 487 // æåæäº¤åé置表å 488 form.reset(); 489 formContainer.style.display = 'none'; 490 showToast(_LANG.submitted, true); 491 492 } catch (error) { 493 console.error('Error:', error); 494 showToast(_LANG.try_again_later, false); 495 496 // å¯ä»¥å¨è¿éæ·»å åæ»UIç代ç 497 // ä¾å¦ç§»é¤åæ·»å çåå¤ 498 const newReply = replyList.querySelector('[data-reply-id^="new-"]'); 499 if (newReply) { 500 newReply.remove(); 501 } 502 503 } finally { 504 submitBtn.disabled = false; 505 submitBtn.textContent = _LANG.post; 506 } 507 } 508 }); 509 } 510} 511 512window.CommentSystem = CommentSystem;
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.