PageSourceSearch

https://spacewaves2.net/template/default/js/comments_ajax.js?v=2

js spacewaves2.net collected 2026-09-26 03:39:30 UTC 23,163 bytes, 512 lines download raw bytes

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.