PageSourceSearch

http://vortaro.cn/js/app.js?v=20260805

js vortaro.cn collected 2026-10-06 13:15:15 UTC 68,185 bytes, 1,694 lines download raw bytes

1/**
2 * 主应用逻辑
3 * 文件: js/app.js
4 * 功能:词典查询、pais高亮滚动、音频播放、复制保护
5 */
6(function() {
7    'use strict';
8    if (window._appLoaded) return;
9    window._appLoaded = true;
10
11    // ===== 全局变量 =====
12    var lastRequestId = 0, lastSearchTerm = '', currentDocumentPath = '', tocData = [];
13    var suggestionCache = new Map(), searchDebounceTimer = null;
14    var isScrolling = false;
15
16    // ===== 隐藏音频播放器(用于静默播放)=====
17    var hiddenAudioPlayer = null;
18
19// ===== 在 window.onload 函数中添加 =====
20window.onload = function() {
21    console.log('页面加载完成');
22
23    // 首次加载立即更新统计
24    updateStats();
25    // 启动递归定时器(替代 setInterval)
26    setTimeout(scheduleStatsUpdate, 30000);
27
28    var s = document.getElementById('searchInput');
29    if (s) {
30        s.addEventListener('input', function(e) {
31            window.changeChar(e);
32            clearTimeout(searchDebounceTimer);
33            searchDebounceTimer = setTimeout(window.getSuggestions, 200);
34        });
35        s.addEventListener('keydown', function(e) {
36            if (e.key === 'Enter') { e.preventDefault(); window.exactSearch(); }
37        });
38    }
39
40    var titleEl = document.getElementById('title');
41    if (titleEl) {
42        titleEl.addEventListener('click', function() {
43            window.loadFooterContent('后记.html');
44        });
45    }
46
47    if (s) s.value = '';
48    window.readHtml('前言.html');
49    window.initAudioPlayer();
50    setTimeout(function() { if (typeof window.addAiSpeakButtons === 'function') window.addAiSpeakButtons(); }, 500);
51    
52// ===== PIV2020 弹窗拖拽初始化 =====
53    (function initPivPopupDrag() {
54        var popup = document.getElementById('piv2020Popup');
55        var header = document.getElementById('piv2020Header');
56        if (!popup || !header) return;
57        initDrag(popup, header);
58    })();
59
60    // ===== 留言板弹窗拖拽初始化 =====
61    (function initGuestbookDrag() {
62        var popup = document.getElementById('guestbookPopup');
63        var header = document.getElementById('guestbookHeader');
64        if (!popup || !header) return;
65        initDrag(popup, header);
66    })();
67
68    // ===== 管理员后台弹窗拖拽初始化 =====
69    (function initAdminDrag() {
70        var popup = document.getElementById('adminPopup');
71        var header = document.getElementById('adminHeader');
72        if (!popup || !header) return;
73        initDrag(popup, header);
74    })();
75
76    // ===== 窗口大小变化时调整弹窗位置 =====
77    var resizeTimer = null;
78    window.addEventListener('resize', function() {
79        clearTimeout(resizeTimer);
80        resizeTimer = setTimeout(function() {
81            // 如果是手机端,弹窗已经用 CSS 固定了位置,不需要额外调整
82            if (window.innerWidth > 768) {
83                // 桌面端:如果弹窗超出可视区域,调整位置
84                ['guestbookPopup', 'adminPopup', 'piv2020Popup'].forEach(function(id) {
85                    var popup = document.getElementById(id);
86                    if (!popup || popup.style.display !== 'block') return;
87                    var rect = popup.getBoundingClientRect();
88                    var maxX = window.innerWidth - 50;
89                    var maxY = window.innerHeight - 50;
90                    if (rect.right > maxX) {
91                        var left = parseInt(popup.style.left) || 0;
92                        popup.style.left = Math.max(0, left - (rect.right - maxX)) + 'px';
93                    }
94                    if (rect.bottom > maxY) {
95                        var top = parseInt(popup.style.top) || 0;
96                        popup.style.top = Math.max(0, top - (rect.bottom - maxY)) + 'px';
97                    }
98                });
99            }
100        }, 300);
101    });
102    
103        // ===== 新增:URL Hash 直接查询支持 =====
104    function handleHashQuery() {
105        var hash = window.location.hash;
106        if (!hash || hash.length <= 1) return;
107        
108        var query = decodeURIComponent(hash.substring(1));
109        // 支持 PIV 风格的 _kd 后缀
110        if (query.endsWith('_kd')) {
111            query = query.substring(0, query.length - 3);
112        }
113        // 将下划线转为空格(PIV 风格)
114        query = query.replace(/_/g, ' ');
115        
116        var searchInput = document.getElementById('searchInput');
117        if (!searchInput) return;
118        
119        searchInput.value = query;
120        // 触发 input 事件以显示建议
121        var event = new Event('input', { bubbles: true });
122        searchInput.dispatchEvent(event);
123        
124        // 延迟执行精确搜索
125        setTimeout(function() {
126            window.exactSearch();
127        }, 350);
128    }
129
130    // 页面加载后执行
131    setTimeout(handleHashQuery, 400);
132
133    // 监听 hash 变化(用户手动修改)
134    window.addEventListener('hashchange', function() {
135        var listBox = document.getElementById('listBox');
136        if (listBox) listBox.innerHTML = '';
137        setTimeout(handleHashQuery, 100);
138    });
139};
140
141// ===== 通用拖拽函数(支持触摸) =====
142function initDrag(popup, header) {
143    if (!popup || !header) return;
144
145    var isDragging = false;
146    var startX, startY, startLeft, startTop;
147
148    // 如果是手机端(宽度 < 768px),禁用拖拽(因为已经固定位置了)
149    var isMobile = window.innerWidth < 768;
150
151    function dragStart(clientX, clientY) {
152        if (isMobile) return; // 手机端不拖动
153        var rect = popup.getBoundingClientRect();
154        startLeft = rect.left;
155        startTop = rect.top;
156        startX = clientX;
157        startY = clientY;
158        isDragging = true;
159
160        // 拖动时取消 transform,改用 left/top 定位
161        popup.style.transform = 'none';
162        popup.style.left = rect.left + 'px';
163        popup.style.top = rect.top + 'px';
164    }
165
166    function dragMove(clientX, clientY) {
167        if (!isDragging) return;
168        var newLeft = startLeft + clientX - startX;
169        var newTop = startTop + clientY - startY;
170        var maxLeft = window.innerWidth - 80;
171        var maxTop = window.innerHeight - 80;
172        popup.style.left = Math.max(-popup.offsetWidth + 60, Math.min(newLeft, maxLeft)) + 'px';
173        popup.style.top = Math.max(0, Math.min(newTop, maxTop)) + 'px';
174    }
175
176    function dragEnd() {
177        isDragging = false;
178    }
179
180    // --- 鼠标事件 ---
181    header.addEventListener('mousedown', function(e) {
182        if (e.target.tagName === 'SPAN' && e.target.getAttribute('onclick')) return;
183        dragStart(e.clientX, e.clientY);
184        e.preventDefault();
185    });
186
187    document.addEventListener('mousemove', function(e) {
188        dragMove(e.clientX, e.clientY);
189    });
190
191    document.addEventListener('mouseup', dragEnd);
192
193    // --- 触摸事件 ---
194    header.addEventListener('touchstart', function(e) {
195        if (e.target.tagName === 'SPAN' && e.target.getAttribute('onclick')) return;
196        var t = e.touches[0];
197        dragStart(t.clientX, t.clientY);
198    }, { passive: true });
199
200    document.addEventListener('touchmove', function(e) {
201        if (!isDragging) return;
202        var t = e.touches[0];
203        dragMove(t.clientX, t.clientY);
204        e.preventDefault();
205    }, { passive: false });
206
207    document.addEventListener('touchend', dragEnd);
208    document.addEventListener('touchcancel', dragEnd);
209}
210
211
212// ===== 统计更新(完全隔离错误,不影响页面) =====
213function updateStats() {
214    try {
215        var controller = new AbortController();
216        var timeoutId = setTimeout(function() { controller.abort(); }, 5000);
217
218        fetch("http://" + window.location.host + "/count?_=" + Date.now(), {
219            signal: controller.signal
220        })
221        .then(function(r) {
222            clearTimeout(timeoutId);
223            if (!r.ok) return null;
224            return r.json();
225        })
226                .then(function(d) {
227            var logEl = document.getElementById('log');
228            if (logEl) {
229                var line1 = '网页访问统计:今日' + (d.today || 0) + 
230                    ', 昨日' + (d.yestoday || 0) +
231                    ', 本周' + (d.week || 0) + 
232                    ', 本月' + (d.month || 0) +
233                    ', 本年' + (d.year || 0) + 
234                    ', 全部' + (d.all || 0);
235
236                var line2 = '真实访问统计:实时在线' + (d.online || 0) + 
237                    ', 今日访问' + (d.todayuv || 0) + 
238                    ', 真实访问合计(2026-08-05开始)<span id="alltimeBtn" style="color:#006400;text-decoration:underline;cursor:pointer;font-weight:bold;" title="点击查看地区分布">' + (d.alltime || 0) + '</span>';
239
240                logEl.innerHTML = line1 + '<br>' + line2;
241
242                // 绑定点击事件
243                setTimeout(function() {
244                    var btn = document.getElementById('alltimeBtn');
245                    if (btn) btn.onclick = showProvincePopup;
246                }, 0);
247            }
248        })
249        .catch(function() {
250            // 完全静默:不 console.error,不抛异常,避免触发全局错误监听
251        });
252    } catch (e) {
253        // 完全静默
254    }
255}
256
257// 使用 setTimeout 递归,只在页面可见时更新,后台标签页暂停
258function scheduleStatsUpdate() {
259    if (document.visibilityState === 'visible') {
260        updateStats();
261    }
262    setTimeout(scheduleStatsUpdate, 30000);
263}
264
265    // ==================== 等待元素出现的辅助函数 ====================
266    function waitForElement(selector, timeout, callback) {
267        var startTime = Date.now();
268        var interval = setInterval(function() {
269            var element = document.querySelector(selector);
270            if (element) {
271                clearInterval(interval);
272                callback(element);
273            } else if (Date.now() - startTime > timeout) {
274                clearInterval(interval);
275                console.log('等待元素超时:', selector);
276                callback(null);
277            }
278        }, 50);
279    }
280
281    function waitForElementInWebBrowser(selector, timeout, callback) {
282        var webBrowser = document.getElementById('webBrowser');
283        if (!webBrowser) {
284            callback(null);
285            return;
286        }
287        var startTime = Date.now();
288        var interval = setInterval(function() {
289            var element = webBrowser.querySelector(selector);
290            if (element) {
291                clearInterval(interval);
292                callback(element);
293            } else if (Date.now() - startTime > timeout) {
294                clearInterval(interval);
295                console.log('等待元素超时:', selector);
296                callback(null);
297            }
298        }, 50);
299    }
300
301    // ==================== pais高亮滚动函数 ====================
302
303    function getPaisElements() {
304        var webBrowser = document.getElementById('webBrowser');
305        if (!webBrowser) return [];
306        var elements = webBrowser.querySelectorAll('strong.pais, .pais, [class*="pais"]');
307        return elements;
308    }
309
310    window.scrollToSpecificPais = function(paisContent, searchTerm) {
311        console.log('scrollToSpecificPais 调用, paisContent:', paisContent, 'searchTerm:', searchTerm);
312
313        waitForElementInWebBrowser('strong.pais, .pais, [class*="pais"]', 3000, function(paisElementsList) {
314            if (!paisElementsList) return;
315
316            var webBrowser = document.getElementById('webBrowser');
317            if (!webBrowser) return;
318
319            var paisElements = webBrowser.querySelectorAll('strong.pais, .pais, [class*="pais"]');
320            if (!paisElements.length) return;
321
322            var targetElement = null;
323            var normalizedContent = paisContent.trim();
324
325            for (var i = 0; i < paisElements.length; i++) {
326                var text = paisElements[i].textContent.trim();
327                if (text === normalizedContent) {
328                    targetElement = paisElements[i];
329                    break;
330                }
331            }
332
333            if (!targetElement) {
334                var cleanContent = normalizedContent.replace(/^\*/, '').trim();
335                for (var i = 0; i < paisElements.length; i++) {
336                    var text = paisElements[i].textContent.trim();
337                    if (text === cleanContent || text.indexOf(cleanContent) !== -1) {
338                        targetElement = paisElements[i];
339                        break;
340                    }
341                }
342            }
343
344            if (!targetElement && searchTerm && searchTerm.length > 1) {
345                for (var i = 0; i < paisElements.length; i++) {
346                    var text = paisElements[i].textContent;
347                    if (text.toLowerCase().indexOf(searchTerm.toLowerCase()) !== -1) {
348                        targetElement = paisElements[i];
349                        break;
350                    }
351                }
352            }
353
354            if (targetElement) {
355                for (var i = 0; i < paisElements.length; i++) {
356                    var html = paisElements[i].innerHTML;
357                    if (html.indexOf('#FF00FF') !== -1) {
358                        paisElements[i].innerHTML = paisElements[i].textContent;
359                    }
360                }
361
362                if (searchTerm && searchTerm.length > 1) {
363                    var content = targetElement.textContent;
364                    var termIndex = content.toLowerCase().indexOf(searchTerm.toLowerCase());
365                    if (termIndex >= 0) {
366                        var before = content.substring(0, termIndex);
367                        var match = content.substring(termIndex, termIndex + searchTerm.length);
368                        var after = content.substring(termIndex + searchTerm.length);
369                        targetElement.innerHTML = before + '<span style="color:#FF00FF;background-color:rgba(255,0,255,0.2);">' + match + '</span>' + after;
370                    }
371                }
372
373                targetElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
374            }
375        });
376    };
377
378    window.scrollToFirstPais = function() {
379        console.log('scrollToFirstPais 调用');
380
381        waitForElementInWebBrowser('strong.pais, .pais, [class*="pais"]', 3000, function() {
382            var webBrowser = document.getElementById('webBrowser');
383            if (!webBrowser) return;
384
385            var paisElements = webBrowser.querySelectorAll('strong.pais, .pais, [class*="pais"]');
386            if (!paisElements.length) return;
387
388            var target = null;
389            for (var i = 0; i < paisElements.length; i++) {
390                if (paisElements[i].querySelector('span[style*="#FF00FF"]')) {
391                    target = paisElements[i];
392                    break;
393                }
394            }
395
396            var scrollTarget = target || paisElements[0];
397            scrollTarget.scrollIntoView({ behavior: 'smooth', block: 'start' });
398        });
399    };
400
401    window.ensureScrollToPais = function() {
402        if (isScrolling) return;
403        isScrolling = true;
404        window.scrollToFirstPais();
405        setTimeout(function() { isScrolling = false; }, 800);
406    };
407
408    window.scrollToKapvorto = function() {
409        console.log('scrollToKapvorto 调用');
410
411        waitForElementInWebBrowser('.kapvorto, [class*="kapvorto"]', 3000, function() {
412            var webBrowser = document.getElementById('webBrowser');
413            if (!webBrowser) return;
414
415            var kapvortoElement = webBrowser.querySelector('.kapvorto, [class*="kapvorto"]');
416            if (kapvortoElement) {
417                kapvortoElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
418                return;
419            }
420
421            var firstElement = webBrowser.querySelector('p, div, .entry-body');
422            if (firstElement) {
423                firstElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
424            } else {
425                webBrowser.scrollIntoView({ behavior: 'smooth', block: 'start' });
426            }
427        });
428    };
429
430    // ==================== 显示定义函数 ====================
431
432    window.displayDefinition = function(item) {
433        var w = document.getElementById('webBrowser');
434        if (!w) return;
435
436        var cleanWord = '';
437        if (item.word) {
438            cleanWord = item.word.replace(/[¹²³⁴⁵⁶⁷⁸⁹⁰]/g, '').replace(/↳/g, '').trim();
439        }
440
441        var html = '';
442        html += '<p>' + (item.definition || '') + '</p>';
443        w.innerHTML = html;
444
445        // 在 kapvorto 和 pais 节点后添加 PIV2020 链接
446        addPivLinksToDefinition();
447
448        setTimeout(function() {
449            if (typeof window.addParagraphIdentifiers === 'function') {
450                window.addParagraphIdentifiers();
451            }
452            if (typeof window.processDefinitionAudioLinks === 'function') {
453                window.processDefinitionAudioLinks();
454            }
455            if (typeof window.addAiSpeakButtons === 'function') {
456                window.addAiSpeakButtons();
457            }
458
459            var searchInput = document.getElementById('searchInput');
460            if (searchInput) {
461                var searchTerm = searchInput.value.trim().toLowerCase();
462                var wordLower = (item.word || '').toLowerCase();
463
464                if (item.word && item.word.indexOf('↳') !== -1) {
465                    var paisContent = item.word.replace('↳', '').trim();
466                    window.scrollToSpecificPais(paisContent, searchTerm);
467                } 
468                else if (wordLower === searchTerm || (searchTerm && wordLower.indexOf(searchTerm) === 0)) {
469                    window.scrollToKapvorto();
470                }
471                else {
472                    window.scrollToFirstPais();
473                }
474            }
475        }, 200);
476    };;
477
478    // ==================== 显示建议函数 ====================
479
480    function displaySuggestionsOptimized(suggestions, searchTerm) {
481        var currentInput = document.getElementById('searchInput');
482        if (!currentInput || currentInput.value.trim() !== searchTerm) return;
483
484        var lb = document.getElementById('listBox');
485        if (!lb) return;
486        var fragment = document.createDocumentFragment();
487
488        var wordMatches = suggestions.filter(function(item) {
489            return item.word && item.word.toLowerCase().indexOf(searchTerm.toLowerCase()) !== -1;
490        });
491
492        var itemsToShow = wordMatches.length > 0 ? wordMatches : suggestions;
493        var displayItems = itemsToShow.slice(0, 100);
494
495        displayItems.forEach(function(item, idx) {
496            var div = document.createElement('div');
497            div.style.cssText = 'margin-bottom:1px;cursor:pointer;padding:1px;white-space:nowrap;';
498            var wordDiv = document.createElement('div');
499            wordDiv.textContent = item.word;
500            wordDiv.style.cssText = 'white-space:pre;font-family:monospace;';
501            div.appendChild(wordDiv);
502
503            var isPaisEntry = item.word && item.word.indexOf('↳') !== -1;
504
505            div.addEventListener('click', function() {
506                var items = lb.getElementsByClassName('highlight');
507                while(items.length > 0) items[0].classList.remove('highlight');
508                div.classList.add('highlight');
509                window.displayDefinition(item);
510
511                if (isPaisEntry) {
512                    var paisContent = item.word.replace('↳', '').trim();
513                    window.scrollToSpecificPais(paisContent, searchTerm);
514                }
515            });
516            fragment.appendChild(div);
517        });
518
519        lb.innerHTML = '';
520        lb.appendChild(fragment);
521
522        if (displayItems.length > 0 && lb.children[0]) {
523            lb.children[0].classList.add('highlight');
524            setTimeout(function() {
525                window.displayDefinition(displayItems[0]);
526            }, 0);
527        }
528    }
529
530    // ==================== 音频播放核心函数(完全按照原始代码恢复)====================
531
532    /**
533     * 初始化音频播放器
534     */
535    window.initAudioPlayer = function() {
536        // 创建隐藏音频播放器
537        if (!document.getElementById('hiddenAudioPlayer')) {
538            var ha = document.createElement('audio');
539            ha.id = 'hiddenAudioPlayer';
540            ha.style.display = 'none';
541            document.body.appendChild(ha);
542        }
543        hiddenAudioPlayer = document.getElementById('hiddenAudioPlayer');
544
545        // 创建可见音频播放器容器
546        if (!document.getElementById('audioPlayerContainer')) {
547            var container = document.createElement('div');
548            container.id = 'audioPlayerContainer';
549            container.className = 'audio-player-container hidden';
550            container.innerHTML = '<div class="audio-player"><audio id="hmrbPlayer" controls></audio><button class="close-audio-btn" onclick="window.closeAudioPlayer()">\u00D7</button></div>';
551            document.body.appendChild(container);
552        }
553
554        // 全局点击事件:处理MP3链接
555        document.addEventListener('click', function(e) {
556            var target = e.target;
557            if (target.tagName === 'A') {
558                var href = target.getAttribute('href');
559                if (href && href.toLowerCase().endsWith('.mp3')) {
560                    e.preventDefault();
561                    var wb = document.getElementById('webBrowser');
562                    if (wb && wb.contains(target)) {
563                        window.playAudioWithControls(href);
564                    } else {
565                        window.playAudioWithoutControls(href);
566                    }
567                }
568            }
569        });
570
571        console.log('音频播放器初始化完成');
572    };
573
574    /**
575     * 播放音频(显示播放控件)
576     */
577    window.playAudioWithControls = function(audioUrl) {
578        var player = document.getElementById('hmrbPlayer');
579        var container = document.getElementById('audioPlayerContainer');
580        if (!player || !container) return;
581
582        // 处理相对路径
583        var url = audioUrl;
584        if (!url.includes('/') && !url.startsWith('http')) {
585            url = 'Sonoj/' + url;
586        }
587
588        player.src = url;
589        container.classList.remove('hidden');
590        player.controls = true;
591        player.play().catch(function() {});
592    };
593
594    /**
595     * 播放音频(隐藏控件)
596     */
597    window.playAudioWithoutControls = function(audioUrl) {
598        if (!hiddenAudioPlayer) {
599            var ha = document.createElement('audio');
600            ha.id = 'hiddenAudioPlayer';
601            ha.style.display = 'none';
602            document.body.appendChild(ha);
603            hiddenAudioPlayer = ha;
604        }
605
606        // 处理相对路径
607        var url = audioUrl;
608        if (!url.includes('/') && !url.startsWith('http')) {
609            url = 'Sonoj/' + url;
610        }
611
612        hiddenAudioPlayer.src = url;
613        hiddenAudioPlayer.play().catch(function() {});
614    };
615
616    /**
617     * 关闭音频播放器
618     */
619    window.closeAudioPlayer = function() {
620        var player = document.getElementById('hmrbPlayer');
621        var container = document.getElementById('audioPlayerContainer');
622        if (player) {
623            player.pause();
624            player.src = '';
625        }
626        if (container) {
627            container.classList.add('hidden');
628        }
629    };
630
631    /**
632     * 创建播放图标
633     */
634    window.createAudioPlayIcon = function() {
635        var img = document.createElement('img');
636        img.src = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PGNpcmNsZSBjeD0iMTIiIGN5PSIxMiIgcj0iMTAiIGZpbGw9IiNmZmZmZmYiIHN0cm9rZT0iI2Q0MGEwMCIgc3Ryb2tlLXdpZHRoPSIxLjUiLz48cGF0aCBkPSJNMTAgOHY4bDYuNzYtNHoiIGZpbGw9InVybCgjZ3JhZGllbnQpIi8+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkaWVudCIgeDE9IjAiIHkxPSIwIiB4Mj0iMTAwJSIgeTI9IjEwMCUiPjxzdG9wIG9mZnNldD0iMCUiIHN0b3AtY29sb3I9IiNlZDM1MzUiLz48c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiNmZmNiMDciLz48L2xpbmVhckdyYWRpZW50PjwvZGVmcz48L3N2Zz4=';
637        img.width = 18;
638        img.height = 18;
639        img.style.verticalAlign = 'middle';
640        img.alt = '播放';
641        return img;
642    };
643
644    /**
645     * 处理HTML文档中的MP3链接(LoadEsperantoFile加载的内容)
646     */
647    window.processHtmlAudioLinks = function() {
648        var w = document.getElementById('webBrowser');
649        if (!w) return;
650
651        var audioLinks = w.querySelectorAll('a[href$=".mp3"], a[href$=".MP3"]');
652        console.log('HTML文档中找到音频链接:', audioLinks.length);
653
654        audioLinks.forEach(function(link) {
655            // 如果链接内已有子元素,只添加点击事件
656            if (link.children.length > 0) {
657                link.style.cursor = 'pointer';
658                if (!link.title) link.title = '点击播放本地音频';
659                // 移除原有事件,重新绑定
660                var oldClick = link.onclick;
661                link.onclick = null;
662                link.addEventListener('click', function(e) {
663                    e.preventDefault();
664                    var href = this.getAttribute('href');
665                    if (href) {
666                        window.playAudioWithControls(href);
667                    }
668                });
669                return;
670            }
671
672            // 空链接:添加图标和样式
673            link.title = '点击播放本地音频';
674            link.style.cursor = 'pointer';
675            link.style.color = '#006400';
676            link.style.textDecoration = 'none';
677            link.style.borderBottom = '1px dotted #006400';
678            link.style.margin = '0 2px';
679            link.style.display = 'inline-flex';
680            link.style.alignItems = 'center';
681
682            var textContent = link.textContent.trim();
683            if (!textContent || textContent === link.getAttribute('href') || textContent === '') {
684                link.innerHTML = '';
685                var icon = window.createAudioPlayIcon();
686                link.appendChild(icon);
687            }
688
689            link.addEventListener('mouseover', function() {
690                this.style.color = '#ff6600';
691                this.style.borderBottomColor = '#ff6600';
692            });
693
694            link.addEventListener('mouseout', function() {
695                this.style.color = '#006400';
696                this.style.borderBottomColor = '#006400';
697            });
698
699            link.addEventListener('click', function(e) {
700                e.preventDefault();
701                var href = this.getAttribute('href');
702                if (href) {
703                    window.playAudioWithControls(href);
704                }
705            });
706        });
707    };
708
709    /**
710     * 处理词典查询结果中的MP3链接
711     */
712    window.processDefinitionAudioLinks = function() {
713        var w = document.getElementById('webBrowser');
714        if (!w) return;
715
716        w.querySelectorAll('a[href$=".mp3"]').forEach(function(link) {
717            // 如果已有子元素,跳过
718            if (link.children.length > 0) return;
719
720            var audioHref = link.getAttribute('href');
721            if (!audioHref.startsWith('http') && !audioHref.startsWith('/')) {
722                if (!audioHref.startsWith('Sonoj/')) audioHref = 'Sonoj/' + audioHref;
723            }
724
725            link.innerHTML = '';
726            link.style.cssText = 'cursor:pointer;text-decoration:none;display:inline-block;vertical-align:middle;margin:0 3px;';
727            link.title = '点击播放本地音频';
728
729            var icon = window.createAudioPlayIcon();
730            link.appendChild(icon);
731
732            link.onclick = function(e) {
733                e.preventDefault();
734                window.playAudioWithoutControls(audioHref);
735            };
736        });
737    };
738
739    // ==================== 显示内容函数 ====================
740
741    window.displayInWebBrowser = function(content) {
742        var w = document.getElementById('webBrowser');
743        if (!w) return;
744
745        w.querySelectorAll('.ai-speak-btn').forEach(function(e) { e.remove(); });
746        if (typeof window.resetAiButton === 'function') window.resetAiButton();
747        w.innerHTML = content;
748
749        setTimeout(function() {
750            if (typeof window.addParagraphIdentifiers === 'function') {
751                window.addParagraphIdentifiers();
752            }
753            window.processHtmlAudioLinks();
754            if (typeof window.addAiSpeakButtons === 'function') {
755                window.addAiSpeakButtons();
756            }
757        }, 50);
758
759        setTimeout(function() {
760            if (typeof window.addAiSpeakButtons === 'function') {
761                window.addAiSpeakButtons();
762            }
763            window.onDocumentLoaded();
764        }, 300);
765    };
766
767    // ==================== 目录相关函数 ====================
768
769    window.LoadEsperantoFile = function(fileName) {
770        window.clearPreviousResults();
771        currentDocumentPath = fileName;
772        fetch(fileName).then(function(r) {
773            if (!r.ok) { window.showMessage('文件不存在!'); return; }
774            return r.text();
775        }).then(function(c) {
776            if (c) { window.displayInWebBrowser(c); window.generateHtmlTOC(c); }
777        }).catch(function(e) { window.showMessage('加载文件时出错: ' + e.message); });
778    };
779
780    window.loadFooterContent = function(fileName) { window.clearInputs(); window.readHtml(fileName); };
781    window.vortoremoru = function(fileName) { window.clearInputs(); window.readHtml(fileName); };
782
783    window.readHtml = function(fileName) {
784        var w = document.getElementById('webBrowser');
785        fetch(fileName).then(function(r) {
786            if (!r.ok) throw new Error('网络响应不可用');
787            return r.text();
788        }).then(function(html) {
789            w.innerHTML = '';
790            var t = document.createElement('div');
791            t.innerHTML = html;
792            var scripts = t.getElementsByTagName('script');
793            for (var i = 0; i < t.childNodes.length; i++) {
794                if (t.childNodes[i].nodeName !== 'SCRIPT') w.appendChild(t.childNodes[i].cloneNode(true));
795            }
796            for (var j = 0; j < scripts.length; j++) {
797                if (scripts[j].src) {
798                    var ns = document.createElement('script');
799                    ns.src = scripts[j].src;
800                    document.head.appendChild(ns);
801                } else {
802                    try { (new Function(scripts[j].innerHTML)).call(window); } catch(e) {}
803                }
804            }
805            setTimeout(function() {
806                window.processHtmlAudioLinks();
807                if (typeof window.addAiSpeakButtons === 'function') window.addAiSpeakButtons();
808            }, 100);
809        }).catch(function() { w.innerHTML = '<p class="error">加载内容失败</p>'; });
810    };
811
812    window.readText = function(fileName) {
813        fetch('http://' + window.location.host + '/readText/' + fileName)
814            .then(function(r) { return r.text(); })
815            .then(function(t) {
816                document.getElementById('webBrowser').innerHTML = t;
817                setTimeout(function() {
818                    window.processHtmlAudioLinks();
819                    if (typeof window.addAiSpeakButtons === 'function') window.addAiSpeakButtons();
820                }, 100);
821            }).catch(function() {});
822    };
823
824    window.clearInputs = function() {
825        var s = document.getElementById('searchInput'), l = document.getElementById('listBox');
826        if (s) s.value = ''; if (l) l.innerHTML = '';
827    };
828
829    window.clearPreviousResults = function() {
830        var w = document.getElementById('webBrowser'), l = document.getElementById('listBox');
831        if (w) w.innerHTML = ''; if (l) l.innerHTML = '';
832        tocData = [];
833    };
834
835    window.generateHtmlTOC = function(htmlContent) {
836        var t = document.createElement('div');
837        t.innerHTML = htmlContent;
838        var lb = document.getElementById('listBox');
839        lb.innerHTML = '';
840        tocData = [];
841
842        // 用于记录每种标题类型(tagName + text)的出现次数
843        var headerTypeCounters = {};
844
845        t.querySelectorAll('h1,h2,h3,h4,h5,h6').forEach(function(h, i) {
846            var ht = window.decodeHTMLEntities(h.textContent.trim());
847            if (!ht) return;
848            var lv = parseInt(h.tagName.substring(1));
849
850            // 使用标题类型+文本作为键来计数
851            var headerKey = h.tagName.toLowerCase() + ':' + ht;
852            var headerIndex = headerTypeCounters[headerKey] || 0;
853            headerTypeCounters[headerKey] = headerIndex + 1;
854
855            var li = document.createElement('li');
856            li.style.paddingLeft = ((lv - 1) * 20) + 'px';
857            li.textContent = ht;
858            li.dataset.index = i;
859            li.dataset.level = lv;
860
861            // 保存完整的定位信息
862            tocData.push({
863                text: ht,
864                element: h.cloneNode(true),
865                level: lv,
866                index: i,
867                tagName: h.tagName.toLowerCase(),
868                headerIndex: headerIndex
869            });
870
871            // 点击时传整个tocData项
872            li.addEventListener('click', function() {
873                window.showContent(tocData[i]);
874                window.highlightTOCItem(i);
875            });
876
877            lb.appendChild(li);
878        });
879
880        if (!lb.querySelector('ul') && lb.children.length > 0) {
881            var ul = document.createElement('ul');
882            while (lb.firstChild) ul.appendChild(lb.firstChild);
883            lb.appendChild(ul);
884        }
885        if (lb.querySelector('li')) window.highlightTOCItem(0);
886    };
887
888    window.highlightTOCItem = function(index) {
889        var items = document.querySelectorAll('#listBox li');
890        items.forEach(function(i) { i.classList.remove('highlight'); });
891        if (items[index]) items[index].classList.add('highlight');
892    };
893
894    window.showContentByText = function(headerText, tagName, headerIndex) {
895        var w = document.getElementById('webBrowser');
896        if (!w) return;
897
898        // 如果没有提供tagName和headerIndex,退化为原来的行为
899        if (!tagName) {
900            var headers = w.querySelectorAll('h1,h2,h3,h4,h5,h6');
901            var target = null;
902            for (var i = 0; i < headers.length; i++) {
903                if (headers[i].textContent.trim() === headerText) {
904                    target = headers[i];
905                    break;
906                }
907            }
908            if (target) {
909                target.scrollIntoView({ behavior: 'smooth', block: 'start' });
910                var bg = target.style.backgroundColor;
911                target.style.backgroundColor = 'rgba(255,255,0,0.3)';
912                setTimeout(function() { target.style.backgroundColor = bg; }, 2000);
913            }
914            return;
915        }
916
917        // 通过tagName筛选 + headerIndex顺序匹配,精确定位
918        var headers = w.querySelectorAll(tagName);
919        var target = null;
920        var matchIndex = 0;
921
922        for (var i = 0; i < headers.length; i++) {
923            if (headers[i].textContent.trim() === headerText) {
924                if (matchIndex === headerIndex) {
925                    target = headers[i];
926                    break;
927                }
928                matchIndex++;
929            }
930        }
931
932        if (target) {
933            target.scrollIntoView({ behavior: 'smooth', block: 'start' });
934            var bg = target.style.backgroundColor;
935            target.style.backgroundColor = 'rgba(255,255,0,0.3)';
936            setTimeout(function() { target.style.backgroundColor = bg; }, 2000);
937        }
938    };
939
940    window.showContent = function(tocItem) {
941        if (!tocItem) return;
942        window.showContentByText(tocItem.text, tocItem.tagName, tocItem.headerIndex);
943    };
944
945    window.onDocumentLoaded = function() {
946        if (tocData.length > 0 && tocData[0]) {
947            setTimeout(function() { 
948                window.showContent(tocData[0]); 
949            }, 100);
950        }
951    };
952
953    window.decodeHTMLEntities = function(text) {
954        var t = document.createElement('textarea');
955        t.innerHTML = text;
956        return t.value;
957    };
958
959    // ==================== 复制保护段落标识 ====================
960
961    window.addParagraphIdentifiers = function() {
962        var w = document.getElementById('webBrowser');
963        if (!w) return 0;
964
965        var existingCount = w.querySelectorAll('[data-para-id]').length;
966        var newCount = 0;
967
968        var elements = w.querySelectorAll('p, div, h1, h2, h3, h4, h5, h6, li, td, .ckz, .pais, .kapvorto, .sekciokapo-subgrupo');
969
970        for (var i = 0; i < elements.length; i++) {
971            var el = elements[i];
972            var text = el.textContent || el.innerText;
973            if (text && text.trim().length > 0 && !el.hasAttribute('data-para-id')) {
974                el.setAttribute('data-para-id', 'para_' + (existingCount + newCount++));
975            }
976        }
977
978        return existingCount + newCount;
979    };
980
981    // ==================== 工具函数 ====================
982
983    window.changeChar = function(event) {
984        var input = event.target.value, result = input;
985        var map = { cx:'ĉ', gx:'ĝ', hx:'ĥ', jx:'ĵ', sx:'ŝ', ux:'ŭ', cw:'Ĉ', gw:'Ĝ', hw:'Ĥ', jw:'Ĵ', sw:'Ŝ', uw:'Ŭ' };
986        for (var k in map) {
987            if (input.indexOf(k) >= 0) result = result.split(k).join(map[k]);
988        }
989        if (result !== input) {
990            document.getElementById('searchInput').value = result;
991            event.target.setSelectionRange(result.length, result.length);
992        }
993    };
994
995    window.getKeyWord = function() {
996        var s = document.getElementById('searchInput');
997        return s ? s.value.toLowerCase() : '';
998    };
999
1000    window.showMessage = function(content) {
1001        var m = document.getElementById('message'), c = document.querySelector('.message');
1002        if (!m || !c) return;
1003        m.textContent = content;
1004        c.classList.add('show');
1005        setTimeout(function() { c.classList.remove('show'); }, 3000);
1006    };
1007
1008    window.closeMessage = function() {
1009        var c = document.querySelector('.message');
1010        if (c) c.classList.remove('show');
1011    };
1012
1013    window.setWebBrowser = function(str) {
1014        var w = document.getElementById('webBrowser');
1015        if (w) w.innerHTML = str;
1016    };
1017
1018    window.showPopup = function() {
1019        var p = document.getElementById('itemPopup');
1020        if (p) p.style.display = 'block';
1021    };
1022
1023    window.closePopup = function() {
1024        var p = document.getElementById('itemPopup');
1025        if (p) p.style.display = 'none';
1026    };
1027
1028    // ==================== 搜索相关函数 ====================
1029
1030    window.getSuggestions = function() {
1031        var input = document.getElementById('searchInput');
1032        if (!input) return;
1033        var val = input.value.trim();
1034        lastSearchTerm = val;
1035        if (!val) {
1036            var lb = document.getElementById('listBox');
1037            if (lb) lb.innerHTML = '';
1038            return;
1039        }
1040        clearTimeout(searchDebounceTimer);
1041        searchDebounceTimer = setTimeout(function() { getSuggestionsInternal(val); }, 200);
1042    };
1043
1044    function getSuggestionsInternal(input) {
1045        var currentInput = document.getElementById('searchInput');
1046        if (!currentInput || currentInput.value.trim() !== input) return;
1047
1048        var cacheKey = input.toLowerCase();
1049        if (suggestionCache.has(cacheKey)) {
1050            displaySuggestionsOptimized(suggestionCache.get(cacheKey), input);
1051            return;
1052        }
1053
1054        var rid = ++lastRequestId;
1055        fetchSuggestionsWithTimeout(input, rid).then(function(s) {
1056            if (rid !== lastRequestId) return;
1057            if (!currentInput || currentInput.value.trim() !== input) return;
1058
1059            if (!Array.isArray(s) || s.length === 0) {
1060                var pi = processInputQuery(input);
1061                if (pi !== input.toLowerCase()) {
1062                    fetchSuggestionsWithTimeout(pi, rid).then(function(s2) {
1063                        if (rid !== lastRequestId) return;
1064                        if (Array.isArray(s2) && s2.length > 0) {
1065                            suggestionCache.set(cacheKey, s2);
1066                            displaySuggestionsOptimized(s2, input);
1067                        } else {
1068                            var lb = document.getElementById('listBox');
1069                            if (lb) lb.innerHTML = '';
1070                        }
1071                    });
1072                    return;
1073                }
1074            }
1075
1076            if (Array.isArray(s) && s.length > 0) {
1077                suggestionCache.set(cacheKey, s);
1078                if (suggestionCache.size > 100) suggestionCache.delete(suggestionCache.keys().next().value);
1079                displaySuggestionsOptimized(s, input);
1080            } else {
1081                var lb = document.getElementById('listBox');
1082                if (lb) lb.innerHTML = '';
1083            }
1084        });
1085    }
1086
1087    function fetchSuggestionsWithTimeout(term, requestId) {
1088        return new Promise(function(resolve) {
1089            var controller = new AbortController();
1090            var timeoutId = setTimeout(function() { controller.abort(); resolve([]); }, 3000);
1091            fetch('http://' + window.location.host + '/getSuggestions/' + encodeURIComponent(term), { signal: controller.signal })
1092                .then(function(r) {
1093                    clearTimeout(timeoutId);
1094                    if (requestId === lastRequestId) return r.json();
1095                    throw new Error('expired');
1096                })
1097                .then(function(d) { resolve(d); })
1098                .catch(function() { clearTimeout(timeoutId); resolve([]); });
1099        });
1100    }
1101
1102    function processInputQuery(q) {
1103        var ignoreSuffixes = ['jn', 'j', 'n'];
1104        var replaceSuffixes = ['anta','ante','anto','inta','inte','into','ata','ate','ato','ita','ite','ito','ota','ote','oto','as','is','os','us','u'];
1105        function removeSuffix(word, suffixes) {
1106            for (var i = 0; i < suffixes.length; i++) {
1107                if (word.endsWith(suffixes[i])) return word.slice(0, -suffixes[i].length);
1108            }
1109            return word;
1110        }
1111        var w = removeSuffix(q.toLowerCase(), ignoreSuffixes);
1112        if (replaceSuffixes.indexOf(w) === -1) {
1113            for (var i = 0; i < replaceSuffixes.length; i++) {
1114                if (w.endsWith(replaceSuffixes[i])) {
1115                    w = w.slice(0, -replaceSuffixes[i].length) + 'i';
1116                    break;
1117                }
1118            }
1119        }
1120        return w;
1121    }
1122
1123    window.exactSearch = function() {
1124        var term = window.getKeyWord().trim();
1125        if (!term) return;
1126        var regex = /[a-zA-ZĉĈĝĜĥĤĵĴŝŜŭŬ\u4e00-\u9fa5]/;
1127        if (!regex.test(term)) {
1128            window.showMessage('请输入世界语单词或中文词汇!');
1129            return;
1130        }
1131        tryExactSearch(term).then(function(r) {
1132            if (r) return;
1133            var pt = processInputTerm(term);
1134            if (pt !== term.toLowerCase()) {
1135                tryExactSearch(pt).then(function(r2) {
1136                    if (!r2) window.setWebBrowser(term + ' 词库中暂无');
1137                });
1138            } else {
1139                window.setWebBrowser(term + ' 词库中暂无');
1140            }
1141        });
1142    };
1143
1144    function tryExactSearch(term) {
1145        return window.callApi('exactSearch/' + encodeURIComponent(term)).then(function(r) { return r !== '2'; });
1146    }
1147
1148    function processInputTerm(term) {
1149        var ignoreSuffixes = ['jn', 'j', 'n'];
1150        var replaceSuffixes = ['anta','ante','anto','inta','inte','into','onta','onte','onto','ata','ate','ato','ita','
1150ite','ito','ota','ote','oto','as','is','os','us','u'];
1151        var w = term.toLowerCase();
1152        for (var i = 0; i < ignoreSuffixes.length; i++) {
1153            if (w.endsWith(ignoreSuffixes[i])) {
1154                w = w.slice(0, -ignoreSuffixes[i].length);
1155                break;
1156            }
1157        }
1158        if (w.length >= 3 && replaceSuffixes.indexOf(w) === -1) {
1159            for (var j = 0; j < replaceSuffixes.length; j++) {
1160                if (w.endsWith(replaceSuffixes[j])) {
1161                    w = w.slice(0, -replaceSuffixes[j].length) + 'i';
1162                    break;
1163                }
1164            }
1165        }
1166        return w;
1167    }
1168
1169    window.exampleSearch = function() {
1170        var term = window.getKeyWord();
1171        if (!term) return;
1172        if (term.length < 2) {
1173            window.showMessage('请输入至少两个字符!');
1174            return;
1175        }
1176        window.callApi('exampleSearch/' + encodeURIComponent(term)).then(function(r) {
1177            if (r === '2') window.setWebBrowser(term + ' 词库中暂无');
1178            else if (r === '3') window.showMessage('服务报错');
1179        });
1180    };
1181
1182    window.callApi = function(endpoint) {
1183        return fetch('http://' + window.location.host + '/' + endpoint)
1184            .then(function(r) { return r.json(); })
1185            .then(function(d) { window.displayResults(d); return d.length ? '1' : '2'; })
1186            .catch(function() { return '3'; });
1187    };
1188
1189    window.displayResults = function(data) {
1190        var lb = document.getElementById('listBox');
1191        var wb = document.getElementById('webBrowser');
1192        if (!lb || !wb) return;
1193        lb.innerHTML = '';
1194        wb.innerHTML = '';
1195        data.forEach(function(item) {
1196            var btn = document.createElement('button');
1197            btn.classList.add('list-item');
1198            btn.textContent = item.word;
1199            btn.addEventListener('click', function() { window.displayDefinition(item); });
1200            lb.appendChild(btn);
1201        });
1202        if (data.length > 0) window.displayDefinition(data[0]);
1203    };
1204
1205    // ==================== 省份分布弹窗 ====================
1206
1207function showProvincePopup() {
1208    var popup = document.getElementById('provincePopup');
1209    if (!popup) {
1210        popup = document.createElement('div');
1211        popup.id = 'provincePopup';
1212        popup.style.cssText = [
1213            'display:none',
1214            'position:fixed',
1215            'z-index:99999',
1216            'top:50%',
1217            'left:50%',
1218            'transform:translate(-50%,-50%)',
1219            'background:#fff',
1220            'border:2px solid #006400',
1221            'border-radius:8px',
1222            'box-shadow:0 4px 20px rgba(0,0,0,0.3)',
1223            'width:360px',
1224            'max-height:500px',
1225            'overflow:auto',
1226            'font-family:monospace'
1227        ].join(';');
1228
1229        popup.innerHTML = [
1230            '<div style="background:#006400;color:#fff;padding:10px 15px;font-weight:bold;font-size:16px;',
1231            'display:flex;justify-content:space-between;align-items:center;',
1232            'border-radius:6px 6px 0 0;position:sticky;top:0;z-index:1;">',
1233            '<span>访问地区分布</span>',
1234            '<span onclick="closeProvincePopup()" style="cursor:pointer;padding:2px 6px;border-radius:4px;">&#10006;</span>',
1235            '</div>',
1236            '<div id="provincePopupContent" style="padding:15px;"></div>'
1237        ].join('');
1238
1239        document.body.appendChild(popup);
1240
1241        // 点击外部关闭
1242        document.addEventListener('mousedown', function(e) {
1243            if (popup.style.display === 'block' && !popup.contains(e.target)) {
1244                closeProvincePopup();
1245            }
1246        });
1247        // ESC关闭
1248        document.addEventListener('keydown', function(e) {
1249            if (e.key === 'Escape') closeProvincePopup();
1250        });
1251    }
1252
1253    document.getElementById('provincePopupContent').innerHTML =
1254        '<div style="text-align:center;color:#999;padding:20px;">加载中...</div>';
1255    popup.style.display = 'block';
1256
1257    fetch('http://' + window.location.host + '/provinceStats?_=' + Date.now())
1258        .then(function(r) { return r.json(); })
1259        .then(function(d) {
1260            var html = '<table style="width:100%;border-collapse:collapse;font-size:14px;">';
1261            html += '<tr style="background:#f0f0f0;">' +
1262                '<th style="text-align:left;padding:8px;border-bottom:1px solid #ddd;">地区</th>' +
1263                '<th style="text-align:right;padding:8px;border-bottom:1px solid #ddd;">人数</th></tr>';
1264
1265            var total = 0;
1266            for (var province in d.provinces) {
1267                var count = d.provinces[province];
1268                total += count;
1269                html += '<tr>' +
1270                    '<td style="padding:8px;border-bottom:1px solid #eee;">' + escapeHtml(province) + '</td>' +
1271                    '<td style="text-align:right;padding:8px;border-bottom:1px solid #eee;">' + count + '</td></tr>';
1272            }
1273
1274            html += '<tr style="font-weight:bold;color:#006400;background:#f9f9f9;">' +
1275                '<td style="padding:8px;">合计</td>' +
1276                '<td style="text-align:right;padding:8px;">' + total + '</td></tr>';
1277            html += '</table>';
1278
1279            document.getElementById('provincePopupContent').innerHTML = html;
1280        })
1281        .catch(function() {
1282            document.getElementById('provincePopupContent').innerHTML =
1283                '<div style="color:#c00;text-align:center;padding:20px;">加载失败</div>';
1284        });
1285}
1286
1287window.closeProvincePopup = function() {
1288    var p = document.getElementById('provincePopup');
1289    if (p) p.style.display = 'none';
1290};
1291
1292// 简单的HTML转义(弹窗内使用)
1293function escapeHtml(text) {
1294    if (!text) return '';
1295    var div = document.createElement('div');
1296    div.textContent = text;
1297    return div.innerHTML;
1298}
1299
1300
1301    // ==================== 在词条定义中添加 PIV2020 链接 ====================
1302
1303    function addPivLinksToDefinition() {
1304        var w = document.getElementById('webBrowser');
1305        if (!w) return;
1306
1307        // 处理 kapvorto 节点:在音频链接或无音频时直接在 kapvorto 后添加
1308        var kapvortos = w.querySelectorAll('strong.kapvorto');
1309        kapvortos.forEach(function(kw) {
1310            // 清理词条名:去掉前导*、|、上标数字等干扰字符
1311            var wordText = kw.textContent.replace(/^\*+/, '').replace(/\|/g, '').replace(/[¹²³⁴⁵⁶⁷⁸⁹⁰]/g, '').trim();
1312            if (!wordText) return;
1313
1314            // 检查是否已添加过 PIV 链接
1315            var parent = kw.parentNode;
1316            if (kw.nextElementSibling && kw.nextElementSibling.classList && kw.nextElementSibling.classList.contains('piv-link'))
1317                return;
1318
1319            var pivLink = document.createElement('span');
1320            pivLink.className = 'piv-link';
1321            pivLink.style.cssText = 'cursor:pointer;color:#006400;font-weight:bold;font-size:12px;margin-left:6px;vertical-align:middle;';
1322            pivLink.textContent = '[PIV2020]';
1323            pivLink.onclick = function(e) {
1324                e.stopPropagation();
1325                showPiv2020Popup(wordText);
1326            };
1327
1328            // 如果有音频链接,插在音频链接后面;否则插在 kapvorto 后面
1329            var nextEl = kw.nextElementSibling;
1330            if (nextEl && nextEl.tagName === 'A' && nextEl.getAttribute('href') && nextEl.getAttribute('href').indexOf('.mp3') > 0) {
1331                nextEl.parentNode.insertBefore(pivLink, nextEl.nextSibling);
1332            } else {
1333                kw.parentNode.insertBefore(pivLink, kw.nextSibling);
1334            }
1335        });
1336
1337        // 处理 pais 节点:在 </strong> 后面添加
1338        var paisNodes = w.querySelectorAll('strong.pais');
1339        paisNodes.forEach(function(pn) {
1340            // 清理词条名:去掉前导*、|、上标数字等干扰字符
1341            var wordText = pn.textContent.replace(/^\*+/, '').replace(/\|/g, '').replace(/[¹²³⁴⁵⁶⁷⁸⁹⁰]/g, '').trim();
1342            if (!wordText) return;
1343
1344            // 检查是否已添加过
1345            if (pn.nextElementSibling && pn.nextElementSibling.classList && pn.nextElementSibling.classList.contains('piv-link'))
1346                return;
1347
1348            var pivLink = document.createElement('span');
1349            pivLink.className = 'piv-link';
1350            pivLink.style.cssText = 'cursor:pointer;color:#006400;font-weight:bold;font-size:11px;margin-left:5px;vertical-align:middle;';
1351            pivLink.textContent = '[PIV2020]';
1352            pivLink.onclick = function(e) {
1353                e.stopPropagation();
1354                showPiv2020Popup(wordText);
1355            };
1356
1357            pn.parentNode.insertBefore(pivLink, pn.nextSibling);
1358        });
1359    }
1360
1361    // ==================== PIV2020 弹窗(改为 iframe 直接加载,避免 API 匹配偏差)====================
1362
1363           window.showPiv2020Popup = function(term) {
1364        var popup = document.getElementById('piv2020Popup');
1365        var title = document.getElementById('piv2020Title');
1366        var content = document.getElementById('piv2020Content');
1367        if (!popup || !title || !content) return;
1368
1369        var cleanTerm = term.replace(/^\*+/, '').replace(/\|/g, '').replace(/[¹²³⁴⁵⁶⁷⁸⁹⁰]/g, '').trim();
1370        title.textContent = 'PIV2020: ' + cleanTerm;
1371
1372        // 移动端自动调整尺寸和位置
1373        if (window.innerWidth < 768) {
1374            popup.style.width = '94vw';
1375            popup.style.height = '70vh';
1376            popup.style.left = '3vw';
1377            popup.style.top = '10vh';
1378            popup.style.resize = 'none';
1379        } else {
1380            // 桌面端:如果之前被手机尺寸覆盖过,恢复默认值
1381            if (popup.style.width === '94vw') {
1382                popup.style.width = '700px';
1383                popup.style.height = '500px';
1384                popup.style.left = '100px';
1385                popup.style.top = '100px';
1386                popup.style.resize = 'both';
1387            }
1388        }
1389
1390        var pivUrl = 'https://vortaro.net/#' + encodeURIComponent(cleanTerm) + '_kd';
1391        content.innerHTML = '<iframe src="' + pivUrl + '" style="width:100%;height:100%;border:none;" title="PIV2020"></iframe>';
1392
1393        popup.style.display = 'block';
1394    };
1395
1396    window.closePiv2020Popup = function() {
1397        var popup = document.getElementById('piv2020Popup');
1398        if (popup) popup.style.display = 'none';
1399    };
1400
1401    document.addEventListener('mousedown', function(e) {
1402        var popup = document.getElementById('piv2020Popup');
1403        if (popup && popup.style.display === 'block' && !popup.contains(e.target)) {
1404            closePiv2020Popup();
1405        }
1406    });
1407    document.addEventListener('keydown', function(e) {
1408        if (e.key === 'Escape') closePiv2020Popup();
1409    });
1410    
1411    // ==================== 留言板功能 ====================
1412
1413/**
1414 * 显示留言板弹窗
1415 */
1416window.showGuestbookPopup = function() {
1417    var popup = document.getElementById('guestbookPopup');
1418    if (!popup) return;
1419    
1420    // 手机端重置位置到顶部
1421    if (window.innerWidth < 768) {
1422        popup.style.transform = 'none';
1423        popup.style.top = '4vh';
1424        popup.style.left = '3vw';
1425    } else {
1426        // 桌面端居中
1427        popup.style.transform = 'translate(-50%, -50%)';
1428        popup.style.top = '50%';
1429        popup.style.left = '50%';
1430    }
1431    
1432    popup.style.display = 'block';
1433    loadGuestbookList();
1434    // 清空表单
1435    document.getElementById('gbSubject').value = '';
1436    document.getElementById('gbContent').value = '';
1437    document.getElementById('gbImage').value = '';
1438    document.getElementById('gbMessage').textContent = '';
1439};
1440
1441    /**
1442     * 关闭留言板弹窗
1443     */
1444    window.closeGuestbookPopup = function() {
1445        var popup = document.getElementById('guestbookPopup');
1446        if (popup) popup.style.display = 'none';
1447    };
1448
1449/**
1450 * 提交留言
1451 */
1452window.submitGuestbook = function(event) {
1453    event.preventDefault();
1454    var subjectEl = document.getElementById('gbSubject');
1455    var contentEl = document.getElementById('gbContent');
1456    var imageEl = document.getElementById('gbImage');
1457    var msgEl = document.getElementById('gbMessage');
1458    
1459    var subject = subjectEl ? subjectEl.value.trim() : '';
1460    var content = contentEl ? contentEl.value.trim() : '';
1461    var imageFile = imageEl ? imageEl.files[0] : null;
1462
1463    if (!subject || !content) {
1464        if (msgEl) {
1465            msgEl.style.color = '#c00';
1466            msgEl.textContent = '请填写主题和详细内容!';
1467        }
1468        return;
1469    }
1470
1471    var formData = new FormData();
1472    formData.append('subject', subject);
1473    formData.append('content', content);
1474    if (imageFile) {
1475        if (imageFile.size > 5 * 1024 * 1024) {
1476            if (msgEl) {
1477                msgEl.style.color = '#c00';
1478                msgEl.textContent = '图片不能超过 5MB!';
1479            }
1480            return;
1481        }
1482        formData.append('image', imageFile);
1483    }
1484
1485    if (msgEl) {
1486        msgEl.style.color = '#006400';
1487        msgEl.textContent = '提交中...';
1488    }
1489
1490    // 打印 FormData 内容(调试)
1491    console.log('=== 提交留言 ===');
1492    console.log('主题:', subject);
1493    console.log('内容:', content);
1494    console.log('图片:', imageFile ? imageFile.name + ' (' + imageFile.size + ' bytes)' : '无');
1495    console.log('图片类型:', imageFile ? imageFile.type : '无');
1496
1497    fetch('/api/guestbook/submit', {
1498        method: 'POST',
1499        body: formData
1500    })
1501    .then(function(response) {
1502        console.log('响应状态:', response.status, response.statusText);
1503        // 先读取响应文本
1504        return response.text().then(function(text) {
1505            console.log('响应内容:', text);
1506            try {
1507                return { status: response.status, data: JSON.parse(text) };
1508            } catch(e) {
1509                console.error('JSON解析失败:', e);
1510                return { status: response.status, data: { error: text || '服务器返回非JSON格式' } };
1511            }
1512        });
1513    })
1514    .then(function(result) {
1515        console.log('解析结果:', result);
1516        if (msgEl) {
1517            if (result.data && result.data.success) {
1518                msgEl.style.color = '#006400';
1519                msgEl.textContent = '✅ 留言提交成功!';
1520                if (subjectEl) subjectEl.value = '';
1521                if (contentEl) contentEl.value = '';
1522                if (imageEl) imageEl.value = '';
1523                loadGuestbookList();
1524            } else {
1525                var errorMsg = (result.data && result.data.error) ? result.data.error : '提交失败,请重试';
1526                msgEl.style.color = '#c00';
1527                msgEl.textContent = '❌ ' + errorMsg;
1528                console.error('服务器返回错误:', result);
1529            }
1530        }
1531    })
1532    .catch(function(err) {
1533        console.error('网络请求失败:', err);
1534        if (msgEl) {
1535            msgEl.style.color = '#c00';
1536            msgEl.textContent = '❌ 网络错误: ' + (err.message || '请重试');
1537        }
1538    });
1539};
1540
1541    /**
1542     * 加载留言列表
1543     */
1544    window.loadGuestbookList = function() {
1545        var listEl = document.getElementById('guestbookList');
1546        if (!listEl) return;
1547        listEl.innerHTML = '<div style="text-align:center;color:#999;padding:10px;">加载中...</div>';
1548
1549        fetch('http://' + window.location.host + '/api/guestbook/list')
1550            .then(function(r) { return r.json(); })
1551            .then(function(data) {
1552                if (!data.success || !data.messages || data.messages.length === 0) {
1553                    listEl.innerHTML = '<div style="text-align:center;color:#999;padding:15px;">📭 暂无留言,快来写下第一条吧!</div>';
1554                    return;
1555                }
1556                var html = '';
1557                data.messages.forEach(function(msg) {
1558                    var time = new Date(msg.time * 1000).toLocaleString('zh-CN');
1559                    var imageHtml = msg.image ? '<div style="margin-top:8px;"><img src="' + msg.image + '" style="max-width:100%;max-height:200px;border-radius:4px;border:1px solid #eee;"></div>' : '';
1560                    html += '<div style="border-bottom:1px solid #f0f0f0;padding:12px 0;">';
1561                    html += '<div style="font-weight:bold;font-size:15px;color:#006400;">' + escapeHtml(msg.subject) + '</div>';
1562                    html += '<div style="font-size:13px;color:#666;margin:4px 0;">' + escapeHtml(msg.content) + '</div>';
1563                    html += imageHtml;
1564                    html += '<div style="font-size:11px;color:#999;margin-top:6px;">' + time + ' · IP: ' + escapeHtml(msg.ip) + '</div>';
1565                    html += '</div>';
1566                });
1567                listEl.innerHTML = html;
1568            })
1569            .catch(function() {
1570                listEl.innerHTML = '<div style="text-align:center;color:#c00;padding:10px;">加载留言失败</div>';
1571            });
1572    };
1573
1574    /**
1575     * 显示管理员后台
1576     */
1577    window.showAdminPopup = function() {
1578        var popup = document.getElementById('adminPopup');
1579        if (!popup) return;
1580        // 手机端重置位置到顶部
1581    if (window.innerWidth < 768) {
1582        popup.style.transform = 'none';
1583        popup.style.top = '4vh';
1584        popup.style.left = '3vw';
1585    } else {
1586        // 桌面端居中
1587        popup.style.transform = 'translate(-50%, -50%)';
1588        popup.style.top = '50%';
1589        popup.style.left = '50%';
1590    }
1591        popup.style.display = 'block';
1592        var listEl = document.getElementById('adminList');
1593        if (listEl) listEl.innerHTML = '<div style="text-align:center;color:#999;padding:20px;font-size:14px;">请输入密码登录</div>';
1594        var pwdEl = document.getElementById('adminPassword');
1595        if (pwdEl) pwdEl.value = '';
1596    };
1597
1598    window.closeAdminPopup = function() {
1599        var popup = document.getElementById('adminPopup');
1600        if (popup) popup.style.display = 'none';
1601    };
1602 
1603
1604    /**
1605     * 加载管理员面板
1606     */
1607    window.loadAdminPanel = function() {
1608        var pwdEl = document.getElementById('adminPassword');
1609        var listEl = document.getElementById('adminList');
1610        var password = pwdEl ? pwdEl.value.trim() : '';
1611        
1612        if (!password) {
1613            if (listEl) listEl.innerHTML = '<div style="color:#c00;text-align:center;padding:10px;">请输入密码</div>';
1614            return;
1615        }
1616
1617        if (listEl) listEl.innerHTML = '<div style="text-align:center;color:#999;padding:20px;">加载中...</div>';
1618
1619        fetch('http://' + window.location.host + '/api/guestbook/admin?password=' + encodeURIComponent(password))
1620            .then(function(r) { return r.json(); })
1621            .then(function(data) {
1622                if (!listEl) return;
1623                if (!data.success) {
1624                    listEl.innerHTML = '<div style="color:#c00;text-align:center;padding:10px;">❌ ' + (data.error || '密码错误') + '</div>';
1625                    return;
1626                }
1627                var msgs = data.messages || [];
1628                if (msgs.length === 0) {
1629                    listEl.innerHTML = '<div style="text-align:center;color:#999;padding:20px;">暂无留言</div>';
1630                    return;
1631                }
1632                var html = '<div style="font-size:13px;color:#666;margin-bottom:10px;">共 ' + msgs.length + ' 条留言</div>';
1633                msgs.forEach(function(msg, idx) {
1634                    var time = new Date(msg.time * 1000).toLocaleString('zh-CN');
1635                    var imageHtml = msg.image ? '<a href="' + msg.image + '" target="_blank" style="color:#006400;">查看图片</a>' : '无';
1636                    html += '<div style="border-bottom:1px solid #eee;padding:10px 0;font-size:14px;">';
1637                    html += '<div style="display:flex;justify-content:space-between;flex-wrap:wrap;">';
1638                    html += '<span style="font-weight:bold;">#' + (idx + 1) + ' ' + escapeHtml(msg.subject) + '</span>';
1639                    html += '<span style="color:#999;font-size:12px;">' + time + '</span>';
1640                    html += '</div>';
1641                    html += '<div style="margin:4px 0;color:#333;">' + escapeHtml(msg.content) + '</div>';
1642                    html += '<div style="font-size:12px;color:#999;">IP: ' + escapeHtml(msg.ip) + ' | 图片: ' + imageHtml;
1643                    html += ' <button onclick="window.deleteGuestbook(' + msg.id + ')" style="color:#c00;border:none;background:none;cursor:pointer;font-size:12px;">删除</button>';
1644                    html += '</div>';
1645                    html += '</div>';
1646                });
1647                listEl.innerHTML = html;
1648            })
1649            .catch(function() {
1650                if (listEl) listEl.innerHTML = '<div style="color:#c00;text-align:center;padding:10px;">加载失败</div>';
1651            });
1652    };
1653
1654    /**
1655     * 删除留言(管理员)
1656     */
1657    window.deleteGuestbook = function(id) {
1658        if (!confirm('确定要删除这条留言吗?')) return;
1659        var pwdEl = document.getElementById('adminPassword');
1660        var password = pwdEl ? pwdEl.value.trim() : '';
1661
1662        fetch('http://' + window.location.host + '/api/guestbook/delete', {
1663            method: 'POST',
1664            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
1665            body: 'id=' + id + '&password=' + encodeURIComponent(password)
1666        })
1667        .then(function(r) { return r.json(); })
1668        .then(function(data) {
1669            if (data.success) {
1670                loadAdminPanel();
1671                loadGuestbookList();
1672            } else {
1673                alert('删除失败: ' + (data.error || '未知错误'));
1674            }
1675        })
1676        .catch(function() {
1677            alert('网络错误');
1678        });
1679    };
1680
1681    // 点击弹窗外关闭
1682    document.addEventListener('mousedown', function(e) {
1683        var popup = document.getElementById('guestbookPopup');
1684        if (popup && popup.style.display === 'block' && !popup.contains(e.target)) {
1685            // 不自动关闭,让用户点击 × 或取消按钮
1686        }
1687        var adminPopup = document.getElementById('adminPopup');
1688        if (adminPopup && adminPopup.style.display === 'block' && !adminPopup.contains(e.target)) {
1689            // 不自动关闭
1690        }
1691    });
1692
1693})();
1694         

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.