PageSourceSearch

https://daohang-c5k.pages.dev/assets/js/component_header.js?v=1789212342

js daohang-c5k.pages.dev collected 2026-10-03 06:39:41 UTC 52,991 bytes, 1,334 lines download raw bytes

1// 网站问候与统计
2document.addEventListener("DOMContentLoaded", function () {
3    // 网站问候
4    const weekday = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"];
5    const today = new Date();
6    const hrs = today.getHours();
7    const date = `${today.getFullYear()}年${(today.getMonth() + 1).toString().padStart(2, '0')}月${today.getDate().toString().padStart(2, '0')}日`;
8    const siteName = window.CONFIG && window.CONFIG.siteName ? window.CONFIG.siteName : "NOISE导航";
9    const greet = hrs < 5 ? `凌晨好,欢迎访问 ${siteName}` :
10        hrs < 9 ? `早上好,欢迎访问 ${siteName}` :
11            hrs < 11 ? `上午好,欢迎访问 ${siteName}` :
12                hrs < 13 ? `中午好,欢迎访问 ${siteName}` :
13                    hrs < 17 ? `下午好,欢迎访问 ${siteName}` :
14                        hrs < 19 ? `傍晚好,欢迎访问 ${siteName}` :
15                            hrs < 22 ? `晚上好,欢迎访问 ${siteName}` : `${siteName}提醒您,夜深了,早点休息哦😯`;
16    const lbl = document.getElementById('lbl');
17    if (lbl) {
18        lbl.textContent = '';
19        lbl.appendChild(document.createTextNode(greet));
20        const dateDiv = document.createElement('div');
21        dateDiv.id = 'date';
22        dateDiv.textContent = '今天是:' + date;
23        lbl.appendChild(dateDiv);
24    }
25
26    // 网站统计API软编码
27    const statisticsApi = window.CONFIG && window.CONFIG.statisticsApi ? window.CONFIG.statisticsApi : "";
28    const statisticsMode = window.CONFIG && window.CONFIG.statisticsMode ? window.CONFIG.statisticsMode : "";
29    const localCountRaw = window.CONFIG && window.CONFIG.statisticsLocalUrlCount !== undefined ? window.CONFIG.statisticsLocalUrlCount : null;
30    const localCount = typeof localCountRaw === 'number' ? localCountRaw : Number(localCountRaw);
31    if (statisticsMode === 'local') {
32        const dateDiv = document.getElementById('date');
33        if (Number.isFinite(localCount) && localCount > 0) {
34            if (dateDiv && !dateDiv.innerHTML.includes('本站已收录')) {
35                dateDiv.innerHTML += ` 本站已收录:${localCount}个网站`;
36            }
37        } else {
38            resolveLocalStatisticsCount()
39                .then(function (count) {
40                    if (dateDiv && Number.isFinite(count) && count > 0 && !dateDiv.innerHTML.includes('本站已收录')) {
41                        dateDiv.innerHTML += ` 本站已收录:${count}个网站`;
42                    }
43                })
44                .catch(function () {
45                    // Ignore local count fetch errors and keep header usable.
46                });
47        }
48    } else if (statisticsApi) {
49        fetch(statisticsApi)
50            .then(response => response.json())
51            .then(data => {
52                const urlCount = data.urlCount;
53                const dateDiv = document.getElementById('date');
54                if (dateDiv) {
55                    if (!dateDiv.innerHTML.includes('本站已收录')) {
56                        dateDiv.innerHTML += ` 本站已收录:${urlCount}个网站`;
57                    }
58                }
59            })
60            .catch(error => {
61                const dateDiv = document.getElementById('date');
62                if (dateDiv) dateDiv.innerHTML += ` 无法获取网站数量`;
63            });
64    }
65
66    // 最近收录网站功能
67    fetchRecentSites();
68
69    // 搜索模块事件绑定
70    const searchInput = document.getElementById('search-input');
71    const searchButton = document.getElementById('search-button');
72    const searchResults = document.getElementById('search-results');
73    const overlay = document.getElementById('overlay');
74    let resultsHeader = null;
75    if (searchResults) {
76        resultsHeader = searchResults.querySelector('.results-header');
77    }
78
79    if (searchButton && searchInput) {
80        searchButton.addEventListener('click', performSearch);
81        searchInput.addEventListener('keydown', function (e) {
82            if (e.key === 'Enter') {
83                performSearch();
84            }
85        });
86    }
87    if (overlay && searchResults) {
88        overlay.addEventListener('click', function () {
89            searchResults.style.display = 'none';
90            overlay.style.display = 'none';
91        });
92    }
93
94    // 近期收录弹窗
95    fetchAndShowLatestNotification();
96    setInterval(function () {
97        if (document.visibilityState === 'visible') {
98            fetchAndShowLatestNotification();
99        }
100    }, 60000);
101});
102
103// --- 最近收录网站相关 ---
104function buildNoCacheUrl(rawUrl) {
105    try {
106        var url = new URL(rawUrl, window.location.href);
107        url.searchParams.set('_t', Date.now().toString());
108        return url.toString();
109    } catch (e) {
110        var sep = rawUrl.indexOf('?') >= 0 ? '&' : '?';
111        return rawUrl + sep + '_t=' + Date.now();
112    }
113}
114
115function escapeHtml(value) {
116    const div = document.createElement('div');
117    div.textContent = value == null ? '' : String(value);
118    return div.innerHTML;
119}
120
121function safeExternalHref(rawUrl) {
122    const value = String(rawUrl == null ? '' : rawUrl).trim();
123    if (!value) return '#';
124    if (value.startsWith('#')) return value;
125    try {
126        const u = new URL(value, window.location.href);
127        const protocol = (u.protocol || '').toLowerCase();
128        if (protocol === 'http:' || protocol === 'https:') return u.toString();
129        return '#';
130    } catch (e) {
131        if (value.startsWith('/')) return value;
132        return '#';
133    }
134}
135
136async function fetchWithTimeout(url, options, timeoutMs) {
137    const controller = new AbortController();
138    const ms = Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : 1500;
139    const timer = setTimeout(function () {
140        try { controller.abort(); } catch (e) {}
141    }, ms);
142    try {
143        const opts = Object.assign({}, options || {}, { signal: controller.signal });
144        return await fetch(url, opts);
145    } finally {
146        clearTimeout(timer);
147    }
148}
149
150function normalizeSiteUrl(url) {
151    return String(url == null ? '' : url)
152        .trim()
153        .toLowerCase()
154        .replace(/^https?:\/\//, '')
155        .replace(/\/+$/, '');
156}
157
158async function resolveLocalStatisticsCount() {
159    const items = await ensureLocalSearchIndex();
160    if (!Array.isArray(items) || !items.length) {
161        return null;
162    }
163
164    const uniqueUrls = new Set();
165    for (let j = 0; j < items.length; j++) {
166        const item = items[j] || {};
167        if (String(item.taxonomy || '') === '友情链接') continue;
168        const normalizedUrl = normalizeSiteUrl(item.url);
169        if (normalizedUrl) uniqueUrls.add(normalizedUrl);
170    }
171
172    return uniqueUrls.size || null;
173}
174
175async function fetchRecentSites() {
176    const siteListElement = document.getElementById('site-list');
177    const titleElement = document.getElementById('recent-sites-title');
178    const recentSitesElement = document.getElementById('recent-sites');
179    if (!siteListElement || !titleElement || !recentSitesElement) return;
180
181    const cachedData = localStorage.getItem('recentSites');
182    const cachedTime = localStorage.getItem('recentSitesTimestamp');
183    let hasRenderedCache = false;
184    if (cachedData && cachedTime) {
185        try {
186            displaySites(JSON.parse(cachedData));
187            hasRenderedCache = true;
188        } catch (e) {
189            hasRenderedCache = false;
190        }
191    }
192
193    try {
194        const controller = new AbortController();
195        const timeoutId = setTimeout(() => controller.abort(), 5000);
196        const recentSitesApi = window.CONFIG && window.CONFIG.recentSitesApi ? window.CONFIG.recentSitesApi : 'https://extension.noisework.cn/api/notifications';
197        const response = await fetch(buildNoCacheUrl(recentSitesApi), {
198            signal: controller.signal,
199            cache: 'no-store'
200        });
201        clearTimeout(timeoutId);
202
203        if (!response.ok) {
204            throw new Error('网络响应不正常: ' + response.status);
205        }
206
207        const data = await response.json();
208        const nextCache = JSON.stringify(data);
209        if (!cachedData || cachedData !== nextCache) {
210            displaySites(data);
211        }
212        localStorage.setItem('recentSites', nextCache);
213        localStorage.setItem('recentSitesTimestamp', Date.now());
214    } catch (error) {
215        if (!hasRenderedCache) {
216            if (titleElement) titleElement.style.display = 'none';
217            if (recentSitesElement) recentSitesElement.style.display = 'none';
218        }
219    }
220}
221
222function displaySites(data) {
223    const siteListElement = document.getElementById('site-list');
224    const titleElement = document.getElementById('recent-sites-title');
225    const recentSitesElement = document.getElementById('recent-sites');
226    if (!siteListElement || !titleElement || !recentSitesElement) return;
227    siteListElement.innerHTML = '';
228    siteListElement.dataset.scrolling = '';
229    siteListElement.scrollTop = 0;
230
231    if (data.message) {
232        const messageItem = document.createElement('li');
233        messageItem.textContent = data.message;
234        siteListElement.appendChild(messageItem);
235        titleElement.style.display = '';
236        recentSitesElement.style.display = '';
237        return;
238    }
239
240    data.forEach(site => {
241        const listItem = document.createElement('li');
242        const title = document.createElement('strong');
243        title.className = 'title';
244        title.textContent = String((site && site.title) ? site.title : '') + ':';
245
246        const link = document.createElement('a');
247        link.className = 'url';
248        link.href = safeExternalHref(site && site.url);
249        link.target = '_blank';
250        link.rel = 'noopener noreferrer';
251        link.textContent = String((site && site.url) ? site.url : '');
252
253        const sep = document.createTextNode(':');
254
255        const desc = document.createElement('span');
256        desc.className = 'description';
257        desc.textContent = String((site && site.description) ? site.description : '无描述');
258
259        listItem.appendChild(title);
260        listItem.appendChild(link);
261        listItem.appendChild(sep);
262        listItem.appendChild(desc);
263        siteListElement.appendChild(listItem);
264    });
265
266    titleElement.style.display = '';
267    recentSitesElement.style.display = '';
268    startScrolling(siteListElement);
269}
270
271function startScrolling(element) {
272    if (!element || element.dataset.scrolling === 'true') return;
273    const scrollHeight = element.scrollHeight;
274    const visibleHeight = element.clientHeight;
275    const maxScrollTop = Math.max(0, scrollHeight - visibleHeight);
276    let scrollTop = 0;
277    const reduceMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
278    if (reduceMotion || scrollHeight <= visibleHeight + 4) return;
279    element.dataset.scrolling = 'true';
280    let lastFrameTime = 0;
281    function scroll(now) {
282        if (document.visibilityState !== 'visible') {
283            requestAnimationFrame(scroll);
284            return;
285        }
286        if (now - lastFrameTime < 42) {
287            requestAnimationFrame(scroll);
288            return;
289        }
290        lastFrameTime = now;
291        scrollTop += 0.3;
292        if (scrollTop >= maxScrollTop) {
293            scrollTop = 0;
294        }
295        element.scrollTop = scrollTop;
296        requestAnimationFrame(scroll);
297    }
298    requestAnimationFrame(scroll);
299}
300
301// --- 本地搜索相关 ---
302let _localSearchIndex = null;
303let _localSearchIndexLoaded = false;
304let _localSearchIndexLoadPromise = null;
305let _localSearchIndexScriptPromise = null;
306
307function coerceSearchIndexPayload(payload) {
308    if (Array.isArray(payload)) return payload;
309    if (payload && Array.isArray(payload.results)) return payload.results;
310    if (payload && Array.isArray(payload.data)) return payload.data;
311    if (typeof payload === 'string') {
312        try {
313            return coerceSearchIndexPayload(JSON.parse(payload));
314        } catch (e) {
315            return null;
316        }
317    }
318    return null;
319}
320
321function readLocalSearchIndexFromDom() {
322    if (typeof window !== 'undefined' && window.__HEADER_SEARCH_INDEX__) {
323        const fromWindow = coerceSearchIndexPayload(window.__HEADER_SEARCH_INDEX__);
324        if (Array.isArray(fromWindow)) return fromWindow;
325    }
326    try {
327        var el = document.getElementById('header-search-index');
328        var raw = el ? (el.textContent || el.innerText || '') : '';
329        return coerceSearchIndexPayload(raw ? JSON.parse(raw) : []);
330    } catch (e) {
331        return null;
332    }
333}
334
335async function fetchLocalSearchIndexFromFiles() {
336    if (window.location.protocol === 'file:') {
337        return [];
338    }
339    const configSearchIndexUrl = window.CONFIG && window.CONFIG.searchIndexUrl ? String(window.CONFIG.searchIndexUrl) : '';
340    const origin = window.location.origin || '';
341    const pathname = window.location.pathname || '/';
342    const baseDir = pathname.replace(/\/[^/]*$/, '/') || '/';
343    const candidates = [
344        configSearchIndexUrl,
345        origin + '/search-index.json',
346        origin + '/index.searchindex.json',
347        '/search-index.json',
348        '/index.searchindex.json',
349        'search-index.json',
350        '../search-index.json',
351        '../../search-index.json',
352        baseDir + 'search-index.json',
353        baseDir + 'index.searchindex.json'
354    ];
355    const seen = new Set();
356
357    for (let i = 0; i < candidates.length; i++) {
358        const url = candidates[i];
359        if (!url || seen.has(url)) continue;
360        seen.add(url);
361        try {
362            const resp = await fetch(url, { cache: 'no-store' });
363            if (!resp.ok) continue;
364            const parsed = coerceSearchIndexPayload(await resp.json());
365            if (Array.isArray(parsed) && parsed.length) {
366                return parsed;
367            }
368        } catch (e) {
369            // Try the next generated Hugo index path.
370        }
371    }
372
373    return [];
374}
375
376function ensureLocalSearchIndexScript() {
377    const existingIndex = readLocalSearchIndexFromDom();
378    if (Array.isArray(existingIndex) && existingIndex.length) {
379        return Promise.resolve(existingIndex);
380    }
381
382    if (_localSearchIndexScriptPromise) {
383        return _localSearchIndexScriptPromise;
384    }
385
386    _localSearchIndexScriptPromise = new Promise(function (resolve) {
387        const scriptUrl = window.CONFIG && window.CONFIG.searchIndexScriptUrl ? String(window.CONFIG.searchIndexScriptUrl) : 'search-index.js';
388        const existingScript = document.getElementById('header-search-index-js');
389
390        function finish() {
391            const loadedIndex = readLocalSearchIndexFromDom();
392            resolve(Array.isArray(loadedIndex) ? loadedIndex : []);
393        }
394
395        if (existingScript) {
396            if (Array.isArray(window.__HEADER_SEARCH_INDEX__) && window.__HEADER_SEARCH_INDEX__.length) {
397                finish();
398                return;
399            }
400            existingScript.addEventListener('load', finish, { once: true });
401            existingScript.addEventListener('error', function () {
402                resolve([]);
403            }, { once: true });
404            return;
405        }
406
407        const script = document.createElement('script');
408        script.id = 'header-search-index-js';
409        script.src = scriptUrl;
410        script.async = false;
411        script.onload = finish;
412        script.onerror = function () {
413            resolve([]);
414        };
415        document.head.appendChild(script);
416    }).finally(function () {
417        _localSearchIndexScriptPromise = null;
418    });
419
420    return _localSearchIndexScriptPromise;
421}
422
423async function ensureLocalSearchIndex() {
424    if (_localSearchIndexLoaded) return _localSearchIndex || [];
425    if (_localSearchIndexLoadPromise) return _localSearchIndexLoadPromise;
426    _localSearchIndexLoadPromise = (async function () {
427        var index = readLocalSearchIndexFromDom();
428        if (!Array.isArray(index) || !index.length) {
429            index = await ensureLocalSearchIndexScript();
430        }
431        if (!Array.isArray(index) || !index.length) {
432            index = await fetchLocalSearchIndexFromFiles();
433        }
434
435        _localSearchIndex = Array.isArray(index) ? index : [];
436        _localSearchIndexLoaded = true;
437        _localSearchIndexLoadPromise = null;
438        return _localSearchIndex;
439    })();
440    return _localSearchIndexLoadPromise;
441}
442
443function escapeHeaderHtml(value) {
444    const div = document.createElement('div');
445    div.textContent = value == null ? '' : String(value);
446    return div.innerHTML;
447}
448
449function normalizeSearchField(value) {
450    return value == null ? '' : String(value);
451}
452
453function normalizeSearchQuery(keyword) {
454    let query = String(keyword || '')
455        .toLowerCase()
456        .replace(/[,,。.;;、/|]+/g, ' ')
457        .replace(/\s+/g, ' ')
458        .trim();
459    if (!query) return '';
460    query = query
461        .replace(/^(有没有|有什么|有哪些|请|帮我|推荐|搜索|搜一下|找一下|找找|我想|请问|能不能|可以|怎么样|如何|怎么)\s*/g, '')
462        .replace(/\s*(呢|吗|吧|啊|呀|嘛|么)$/g, '')
463        .trim();
464    return query;
465}
466
467function extractSearchTerms(keyword) {
468    const normalized = normalizeSearchQuery(keyword);
469    if (!normalized) return [];
470
471    const terms = new Set();
472    normalized.split(/\s+/).filter(Boolean).forEach(function (part) {
473        terms.add(part);
474
475        const cjkSegments = part.match(/[\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af]+/g) || [];
476        cjkSegments.forEach(function (seg) {
477            if (seg.length === 1) {
478                terms.add(seg);
479                return;
480            }
481            if (seg.length <= 4) terms.add(seg);
482            for (let i = 0; i < seg.length - 1; i++) {
483                terms.add(seg.slice(i, i + 2));
484            }
485        });
486
487        const latinWords = part.match(/[a-z0-9]+/g) || [];
488        latinWords.forEach(function (word) {
489            if (word) terms.add(word);
490        });
491    });
492
493    return Array.from(terms).filter(Boolean);
494}
495
496function doLocalSearch(keyword) {
497    if (!_localSearchIndex || !_localSearchIndex.length) return [];
498
499    const terms = extractSearchTerms(keyword);
500    if (!terms.length) return [];
501
502    const results = [];
503
504    for (let i = 0; i < _localSearchIndex.length; i++) {
505        const item = _localSearchIndex[i];
506        const titleValue = normalizeSearchField(item.title);
507        const descValue = normalizeSearchField(item.description);
508        const taxonomyValue = normalizeSearchField(item.taxonomy);
509        const termValue = normalizeSearchField(item.term);
510        const urlValue = normalizeSearchField(item.url);
511        const title = titleValue.toLowerCase();
512        const desc = descValue.toLowerCase();
513        const taxonomy = taxonomyValue.toLowerCase();
514        const term = termValue.toLowerCase();
515        const url = urlValue.toLowerCase();
516        
517        let score = 0;
518        for (let t = 0; t < terms.length; t++) {
519            const q = terms[t];
520            if (!q) continue;
521            let matched = false;
522            if (title.includes(q)) {
523                score += title === q ? 18 : 10;
524                matched = true;
525            }
526            if (term.includes(q)) {
527                score += term === q ? 10 : 6;
528                matched = true;
529            }
530            if (taxonomy.includes(q)) {
531                score += taxonomy === q ? 9 : 5;
532                matched = true;
533            }
534            if (desc.includes(q)) {
535                score += 3;
536                matched = true;
537            }
538            if (url.includes(q)) {
539                score += 1;
540                matched = true;
541            }
542            if (!matched) score -= 1;
543        }
544
545        if (score > 0) {
546            results.push({
547                _score: score,
548                title: titleValue,
549                url: urlValue,
550                description: descValue,
551                taxonomy: taxonomyValue,
552                term: termValue
553            });
554        }
555    }
556
557    results.sort((a, b) => b._score - a._score);
558    return results;
559}
560
561// --- 搜索相关 ---
562async function performSearch() {
563    const searchInput = document.getElementById('search-input');
564    const searchResults = document.getElementById('search-results');
565    const overlay = document.getElementById('overlay');
566    const resultsHeader = searchResults ? searchResults.querySelector('.results-header') : null;
567    if (!searchInput || !searchResults) return;
568    
569    await ensureLocalSearchIndex();
570    
571    const keyword = searchInput.value.trim();
572    if (keyword) {
573        // 使用本地搜索
574        const results = doLocalSearch(keyword);
575        displaySearchResults(results, searchResults, overlay, resultsHeader);
576    } else {
577        displaySearchResults([], searchResults, overlay, resultsHeader);
578    }
579}
580
581if (typeof window !== 'undefined') {
582    window.performSearch = performSearch;
583}
584
585function displaySearchResults(results, searchResults, overlay, resultsHeader) {
586    if (!searchResults) return;
587    searchResults.innerHTML = '';
588    if (resultsHeader) {
589        if (Array.isArray(results)) {
590            resultsHeader.textContent = results.length ? ('当前搜索结果如下(' + results.length + ')') : '当前搜索结果如下:';
591        }
592        resultsHeader.style.display = "block";
593        searchResults.appendChild(resultsHeader);
594    }
595    if (Array.isArray(results) && results.length > 0) {
596        results.forEach(result => {
597            const div = document.createElement('div');
598            div.classList.add('result-item');
599            const meta = [result.taxonomy, result.term].filter(Boolean).join(' / ');
600            div.innerHTML = `
601                <div class="result-title">${escapeHeaderHtml(result.title || '未命名')}${meta ? `<span style="font-size:11px;opacity:0.65;margin-left:6px;">${escapeHeaderHtml(meta)}</span>` : ''}</div>
602                <div class="result-url"><a href="${escapeHeaderHtml(safeExternalHref(result.url || '#'))}" target="_blank" rel="noopener noreferrer">${escapeHeaderHtml(result.url || '#')}</a></div>
603                <div class="result-description">${escapeHeaderHtml(result.description || '无描述')}</div>
604            `;
605            searchResults.appendChild(div);
606        });
607    } else {
608        const div = document.createElement('div');
609        div.textContent = "未找到相关结果";
610        searchResults.appendChild(div);
611    }
612    searchResults.style.display = "flex";
613    if (overlay) overlay.style.display = "block";
614}
615
616// --- 近期收录弹窗(只保留右上角iziToast)---
617let shownNotifications = [];
618try {
619    shownNotifications = JSON.parse(localStorage.getItem('shownNotifications')) || [];
620} catch (e) {
621    shownNotifications = [];
622}
623
624async function fetchAndShowLatestNotification() {
625    try {
626        const recentSitesApi = window.CONFIG && window.CONFIG.recentSitesApi ? window.CONFIG.recentSitesApi : 'https://extension.noisework.cn/api/notifications';
627        const response = await fetchWithTimeout(buildNoCacheUrl(recentSitesApi), { cache: 'no-store' }, 1500);
628        if (!response || !response.ok) return;
629        const notifications = await response.json();
630
631        const bellIcon = document.getElementById('noisenotification-icon');
632        let hasNewNotification = false;
633
634        if (Array.isArray(notifications) && notifications.length > 0) {
635            const latestNotification = notifications.reduce((latest, current) => {
636                const currentTime = new Date(current.timestamp || current.date || 0);
637                const latestTime = new Date(latest.timestamp || latest.date || 0);
638                return currentTime > latestTime ? current : latest;
639            });
640
641            const safeTitle = escapeHtml(latestNotification && latestNotification.title);
642            const safeDesc = escapeHtml((latestNotification && latestNotification.description) ? latestNotification.description : (latestNotification && latestNotification.url));
643            const safeHref = safeExternalHref(latestNotification && latestNotification.url);
644            const notificationContent = '🎉站点收录更新通知:<strong>' + safeTitle + '</strong><br><a href="' + escapeHtml(safeHref) + '" target="_blank" rel="noopener noreferrer" style="color:#ff9800;text-decoration:underline;">' + safeDesc + '</a>';
645
646            // 首次访问 shownNotifications 为空,必弹
647            if (!shownNotifications.includes(notificationContent)) {
648                shownNotifications.push(notificationContent);
649                // 限制缓存长度,防止无限增长
650                if (shownNotifications.length > 20) shownNotifications.shift();
651                localStorage.setItem('shownNotifications', JSON.stringify(shownNotifications));
652                if (window.iziToast) {
653                    iziToast.show({
654                        title: false,
655                        message: notificationContent,
656                        position: 'topRight',
657                        timeout: 5000,
658                        messageColor: '#222',
659                        backgroundColor: '#fff',
660                        icon: 'icon-star'
661                    });
662                }
663                hasNewNotification = true;
664            }
665        }
666        // 控制铃铛图标颜色
667        if (bellIcon) {
668            if (hasNewNotification) {
669                bellIcon.classList.add('active');
670                // 标记有新通知
671                localStorage.setItem('hasNewNotification', '1');
672            } else if (localStorage.getItem('hasNewNotification') === '1') {
673                // 只要本地有未读通知,也保持变色
674                bellIcon.classList.add('active');
675            } else {
676                bellIcon.classList.remove('active');
677            }
678        }
679        // 可选:监听用户点击铃铛,清除未读标记
680        if (bellIcon) {
681            bellIcon.onclick = function() {
682                bellIcon.classList.remove('active');
683                localStorage.removeItem('hasNewNotification');
684            };
685        }
686    } catch (error) {
687        console.error('获取通知失败:', error);
688    }
689}
690
691function syncHeaderPageMode(activeKey) {
692    var headerTop = document.querySelector('.header-top');
693    if (!headerTop) return;
694    var isDefault = !activeKey || activeKey === 'default';
695    headerTop.classList.toggle('is-default-page', isDefault);
696    headerTop.classList.toggle('is-page-active', !isDefault);
697    queueSyncHeaderColumnHeights();
698}
699
700var headerHeightResizeObserver = null;
701var headerHeightMutationObserver = null;
702var cachedDefaultSideHeight = 0;
703var cachedDefaultHeaderTopHeight = 0;
704var headerHeightSyncRafId = 0;
705var headerHeightSyncTimerId = 0;
706var archiveScrollLockUntil = 0;
707var archiveScrollProgrammaticUntil = 0;
708var headerHeightMode = '';
709
710function clearHeaderPageHeightStyles(side) {
711    if (!side) return;
712    var region = side.querySelector('.header-page-region');
713    if (region) {
714        region.style.height = '';
715        region.style.maxHeight = '';
716    }
717    side.querySelectorAll('.page, .page-shell, .archive-scroll, .hot-page-shell, .hot-page-panels, .hot-page-panel, .hot-page-panel-body').forEach(function (node) {
718        node.style.height = '';
719        node.style.maxHeight = '';
720    });
721}
722
723function syncHeaderColumnHeights() {
724    var headerTop = document.querySelector('.header-top');
725    if (!headerTop) return;
726    var main = headerTop.querySelector('.header-top-main');
727    var side = headerTop.querySelector('.header-top-side');
728    if (!main || !side) return;
729
730    if (window.innerWidth <= 1024) {
731        if (headerHeightMode !== 'mobile') {
732            headerHeightMode = 'mobile';
733            headerTop.style.height = '';
734            headerTop.style.minHeight = '';
735            headerTop.style.maxHeight = '';
736            main.style.minHeight = '';
737            side.style.minHeight = '';
738            main.style.height = '';
739            side.style.height = '';
740            clearHeaderPageHeightStyles(side);
741        }
742        return;
743    }
744
745    headerHeightMode = 'desktop';
746
747    var activePageBefore = side.querySelector('.page.active');
748    var activePageIdBefore = activePageBefore && activePageBefore.id ? activePageBefore.id : '';
749    var archiveScrollBefore = null;
750    var archiveScrollTopBefore = 0;
751    if (activePageIdBefore === 'guidang' && activePageBefore) {
752        archiveScrollBefore = activePageBefore.querySelector('.archive-scroll');
753        if (archiveScrollBefore) {
754            archiveScrollTopBefore = archiveScrollBefore.scrollTop || 0;
755        }
756    }
757
758    if (Date.now() < archiveScrollLockUntil) {
759        if (activePageIdBefore === 'guidang') {
760            return;
761        }
762    }
763
764    // 重置所有内联样式以获取自然高度
765    headerTop.style.height = '';
766    headerTop.style.minHeight = '';
767    headerTop.style.maxHeight = '';
768    main.style.minHeight = '';
769    side.style.minHeight = '';
770    main.style.height = '';
771    side.style.height = '';
772    clearHeaderPageHeightStyles(side);
773
774    void headerTop.offsetHeight;
775
776    // 获取当前状态
777    var isPageActive = headerTop.classList.contains('is-page-active');
778    
779    // 获取左侧栏的自然高度(作为基准)
780    var mainRect = main.getBoundingClientRect();
781    var mainHeight = mainRect.height;
782
783    // 获取/更新默认右侧内容的自然高度
784    var defaultSide = side.querySelector('.header-side-default');
785    if (defaultSide) {
786        if (!isPageActive) {
787            cachedDefaultSideHeight = defaultSide.getBoundingClientRect().height;
788            cachedDefaultHeaderTopHeight = headerTop.getBoundingClientRect().height;
789        } else if (cachedDefaultSideHeight === 0) {
790            // 如果初始加载就是分页状态,尝试获取一次
791            var originalDisplay = defaultSide.style.display;
792            defaultSide.style.display = 'flex';
793            cachedDefaultSideHeight = defaultSide.getBoundingClientRect().height;
794            defaultSide.style.display = originalDisplay;
795        }
796    }
797
798    // 计算目标高度
799    // 分页激活时,锁定为默认状态下的整体高度,避免热榜/iframe 内容变化导致整体高度被“撑爆”
800    // 默认状态时,以左右栏更高的一侧作为基准
801    var targetHeight = isPageActive && cachedDefaultHeaderTopHeight > 0
802        ? cachedDefaultHeaderTopHeight
803        : Math.max(mainHeight, cachedDefaultSideHeight);
804
805    if (targetHeight > 0) {
806        var headerStyles = window.getComputedStyle(headerTop);
807        var headerPaddingTop = parseFloat(headerStyles.paddingTop || '0') || 0;
808        var headerPaddingBottom = parseFloat(headerStyles.paddingBottom || '0') || 0;
809        var innerColumnHeight = Math.max(0, targetHeight - headerPaddingTop - headerPaddingBottom);
810
811        // 应用高度锁定,确保左右栏等高且不因内容加载而闪烁
812        headerTop.style.height = targetHeight + 'px';
813        headerTop.style.minHeight = targetHeight + 'px';
814        headerTop.style.maxHeight = targetHeight + 'px';
815        main.style.height = innerColumnHeight + 'px';
816        side.style.height = innerColumnHeight + 'px';
817        // 同时设置 min-height 增强稳定性
818        main.style.minHeight = innerColumnHeight + 'px';
819        side.style.minHeight = innerColumnHeight + 'px';
820
821        // 如果分页处于激活状态,需要精确计算内部滚动区域的高度
822        if (isPageActive) {
823            var region = side.querySelector('.header-page-region');
824            var activePageNode = side.querySelector('.page.active');
825            
826            if (region && activePageNode) {
827                // 计算分页区域在右侧栏中的可用空间
828                var sideRect = side.getBoundingClientRect();
829                var regionRect = region.getBoundingClientRect();
830                var regionTopOffset = regionRect.top - sideRect.top;
831                var availableHeight = Math.max(0, innerColumnHeight - regionTopOffset);
832
833                region.style.height = availableHeight + 'px';
834                region.style.maxHeight = availableHeight + 'px';
835                var regionStyles = window.getComputedStyle(region);
836                var regionPaddingTop = parseFloat(regionStyles.paddingTop || '0') || 0;
837                var regionPaddingBottom = parseFloat(regionStyles.paddingBottom || '0') || 0;
838                var regionBorderTop = parseFloat(regionStyles.borderTopWidth || '0') || 0;
839                var regionBorderBottom = parseFloat(regionStyles.borderBottomWidth || '0') || 0;
840                var innerAvailableHeight = Math.max(0, availableHeight - regionPaddingTop 
840- regionPaddingBottom - regionBorderTop - regionBorderBottom);
841
842                activePageNode.style.height = innerAvailableHeight + 'px';
843                activePageNode.style.maxHeight = innerAvailableHeight + 'px';
844
845                var shell = activePageNode.querySelector('.page-shell');
846                if (shell) {
847                    shell.style.height = innerAvailableHeight + 'px';
848                    shell.style.maxHeight = innerAvailableHeight + 'px';
849                }
850
851                if (activePageNode.id === 'guidang' && shell) {
852                    var archiveShell = shell.classList && shell.classList.contains('archive-page-shell') ? shell : shell.querySelector('.archive-page-shell');
853                    if (archiveShell) {
854                        var archiveHeader = archiveShell.querySelector('.archive-header');
855                        var archiveScroll = archiveShell.querySelector('.archive-scroll');
856                        if (archiveHeader && archiveScroll) {
857                            var headerHeight = archiveHeader.getBoundingClientRect().height;
858                            var scrollHeight = Math.max(0, archiveShell.clientHeight - headerHeight);
859                            archiveScroll.style.height = scrollHeight + 'px';
860                            archiveScroll.style.maxHeight = scrollHeight + 'px';
861                        }
862                    }
863                }
864
865                // 针对热榜页面的特殊精确计算
866                var hotShell = activePageNode.querySelector('.hot-page-shell');
867                if (hotShell) {
868                    hotShell.style.height = innerAvailableHeight + 'px';
869                    hotShell.style.maxHeight = innerAvailableHeight + 'px';
870                    
871                    var intro = hotShell.querySelector('.hot-page-intro');
872                    var tabs = hotShell.querySelector('.hot-page-tabs');
873                    var panels = hotShell.querySelector('.hot-page-panels');
874                    
875                    if (panels) {
876                        var introHeight = intro ? intro.getBoundingClientRect().height : 0;
877                        var tabsHeight = tabs ? tabs.getBoundingClientRect().height : 0;
878                        var hotShellStyles = window.getComputedStyle(hotShell);
879                        var rowGap = parseFloat(hotShellStyles.rowGap || hotShellStyles.gap || '0') || 0;
880                        
881                        // 计算面板容器高度:总高度 - 说明区 - 切换标签 - 间距
882                        var panelsHeight = Math.max(0, innerAvailableHeight - introHeight - tabsHeight - (rowGap * 2));
883                        panels.style.height = panelsHeight + 'px';
884                        panels.style.maxHeight = panelsHeight + 'px';
885                        
886                        panels.querySelectorAll('.hot-page-panel').forEach(function (panel) {
887                            panel.style.height = panelsHeight + 'px';
888                            panel.style.maxHeight = panelsHeight + 'px';
889                            
890                            var head = panel.querySelector('.hot-page-panel-head');
891                            var updateTime = panel.querySelector('.update-time');
892                            var body = panel.querySelector('.hot-page-panel-body');
893                            
894                            if (body) {
895                                var panelStyles = window.getComputedStyle(panel);
896                                var panelGap = parseFloat(panelStyles.rowGap || panelStyles.gap || '0') || 0;
897                                var panelPadding = parseFloat(panelStyles.paddingTop || '0') + parseFloat(panelStyles.paddingBottom || '0');
898                                
899                                var headHeight = head ? head.getBoundingClientRect().height : 0;
900                                var timeHeight = updateTime ? updateTime.getBoundingClientRect().height : 0;
901                                
902                                // 计算列表主体高度
903                                var bodyHeight = Math.max(0, panelsHeight - headHeight - timeHeight - (panelGap * 2) - panelPadding);
904                                body.style.height = bodyHeight + 'px';
905                                body.style.maxHeight = bodyHeight + 'px';
906                                var list = body.querySelector('ol');
907                                if (list) {
908                                    list.style.height = bodyHeight + 'px';
909                                    list.style.maxHeight = bodyHeight + 'px';
910                                    list.style.overflowY = 'auto';
911                                    list.style.overflowX = 'hidden';
912                                }
913                            }
914                        });
915                    }
916                }
917            }
918        }
919    }
920
921    if (archiveScrollBefore && archiveScrollTopBefore > 0) {
922        requestAnimationFrame(function () {
923            try {
924                var nowTop = archiveScrollBefore.scrollTop || 0;
925                if (Math.abs(nowTop - archiveScrollTopBefore) <= 1) return;
926                archiveScrollProgrammaticUntil = Date.now() + 220;
927                archiveScrollBefore.scrollTop = archiveScrollTopBefore;
928                requestAnimationFrame(function () {
929                    var afterTop = archiveScrollBefore.scrollTop || 0;
930                    if (Math.abs(afterTop - archiveScrollTopBefore) <= 1) return;
931                    archiveScrollProgrammaticUntil = Date.now() + 220;
932                    archiveScrollBefore.scrollTop = archiveScrollTopBefore;
933                });
934            } catch (e) {
935            }
936        });
937    }
938}
939
940function queueSyncHeaderColumnHeights() {
941    if (Date.now() < archiveScrollLockUntil) {
942        var headerTop = document.querySelector('.header-top');
943        if (headerTop && headerTop.classList.contains('is-page-active')) {
944            var activePage = headerTop.querySelector('.header-top-side .page.active');
945            if (activePage && activePage.id === 'guidang') {
946                return;
947            }
948        }
949    }
950    if (!headerHeightSyncRafId) {
951        headerHeightSyncRafId = requestAnimationFrame(function () {
952            headerHeightSyncRafId = 0;
953            syncHeaderColumnHeights();
954        });
955    }
956    clearTimeout(headerHeightSyncTimerId);
957    headerHeightSyncTimerId = setTimeout(syncHeaderColumnHeights, 180);
958}
959
960function observeHeaderColumnChanges() {
961    var headerTop = document.querySelector('.header-top');
962    if (headerHeightResizeObserver) {
963        headerHeightResizeObserver.disconnect();
964        headerHeightResizeObserver = null;
965    }
966    if (headerHeightMutationObserver) {
967        headerHeightMutationObserver.disconnect();
968        headerHeightMutationObserver = null;
969    }
970    if (!headerTop || window.innerWidth <= 1024) return;
971    var main = headerTop.querySelector('.header-top-main');
972    var side = headerTop.querySelector('.header-top-side');
973    if (!main || !side) return;
974
975    if ('ResizeObserver' in window) {
976        headerHeightResizeObserver = new ResizeObserver(function () {
977            try {
978                if (headerTop && headerTop.classList && headerTop.classList.contains('is-page-active')) return;
979            } catch (e) {
980            }
981            queueSyncHeaderColumnHeights();
982        });
983        headerHeightResizeObserver.observe(main);
984        headerHeightResizeObserver.observe(side);
985        var activePage = side.querySelector('.page.active');
986        var defaultSide = side.querySelector('.header-side-default');
987        if (activePage) {
988            headerHeightResizeObserver.observe(activePage);
989        }
990        if (defaultSide) {
991            headerHeightResizeObserver.observe(defaultSide);
992        }
993    }
994
995    headerHeightMutationObserver = new MutationObserver(function () {
996        try {
997            if (headerTop && headerTop.classList && headerTop.classList.contains('is-page-active')) return;
998        } catch (e) {
999        }
1000        queueSyncHeaderColumnHeights();
1001    });
1002    headerHeightMutationObserver.observe(side, {
1003        childList: true,
1004        subtree: true
1005    });
1006}
1007
1008function switchHotPanel(target) {
1009    var hotShell = document.querySelector('.hot-page-shell');
1010    if (!hotShell) return;
1011
1012    hotShell.querySelectorAll('.hot-page-tab').forEach(function (tab) {
1013        var isActive = tab.getAttribute('data-hot-target') === target;
1014        tab.classList.toggle('active', isActive);
1015        tab.setAttribute('aria-selected', isActive ? 'true' : 'false');
1016    });
1017
1018    hotShell.querySelectorAll('.hot-page-panel').forEach(function (panel) {
1019        var isActive = panel.getAttribute('data-hot-panel') === target;
1020        panel.classList.toggle('active', isActive);
1021    });
1022
1023    queueSyncHeaderColumnHeights();
1024}
1025
1026function ensureHotPanelSelection() {
1027    var hotShell = document.querySelector('.hot-page-shell');
1028    if (!hotShell) return;
1029    var activeTab = hotShell.querySelector('.hot-page-tab.active');
1030    if (activeTab) {
1031        switchHotPanel(activeTab.getAttribute('data-hot-target'));
1032        return;
1033    }
1034    var firstTab = hotShell.querySelector('.hot-page-tab');
1035    if (firstTab) {
1036        switchHotPanel(firstTab.getAttribute('data-hot-target'));
1037    }
1038}
1039
1040function enhanceArchiveArticleRoot(root) {
1041    if (!root) return;
1042    try {
1043        root.querySelectorAll('a[href]').forEach(function (a) {
1044            var href = a.getAttribute('href') || '';
1045            if (!href || href.startsWith('#')) return;
1046            a.setAttribute('target', '_blank');
1047            var rel = (a.getAttribute('rel') || '').toLowerCase();
1048            var relParts = rel.split(/\s+/).filter(Boolean);
1049            if (relParts.indexOf('noopener') === -1) relParts.push('noopener');
1050            if (relParts.indexOf('noreferrer') === -1) relParts.push('noreferrer');
1051            a.setAttribute('rel', relParts.join(' '));
1052        });
1053    } catch (e) {
1054    }
1055
1056    try {
1057        root.querySelectorAll('pre > code').forEach(function (code) {
1058            var pre = code && code.parentElement ? code.parentElement : null;
1059            if (!pre) return;
1060            if (pre.getAttribute('data-ndh-copy-ready') === '1') return;
1061            pre.setAttribute('data-ndh-copy-ready', '1');
1062            var btn = document.createElement('button');
1063            btn.type = 'button';
1064            btn.className = 'ndh-code-copy';
1065            btn.setAttribute('aria-label', '复制代码');
1066            btn.innerHTML = '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M16 1H6a2 2 0 0 0-2 2v12h2V3h10V1zm3 4H10a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h9a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2zm0 16h-9V7h9v14z"></path></svg>';
1067            btn.addEventListener('click', function (ev) {
1068                try {
1069                    ev.preventDefault();
1070                    ev.stopPropagation();
1071                } catch (err) {
1072                }
1073                var text = '';
1074                try {
1075                    text = code.textContent || '';
1076                } catch (err) {
1077                    text = '';
1078                }
1079                function markCopied(ok) {
1080                    try {
1081                        btn.classList.toggle('copied', !!ok);
1082                        clearTimeout(btn._ndhCopyTimer);
1083                        btn._ndhCopyTimer = setTimeout(function () {
1084                            try { btn.classList.remove('copied'); } catch (e2) {}
1085                        }, 1100);
1086                    } catch (e2) {
1087                    }
1088                }
1089                try {
1090                    if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function' && window.isSecureContext) {
1091                        navigator.clipboard.writeText(text).then(function () {
1092                            markCopied(true);
1093                        }).catch(function () {
1094                            markCopied(false);
1095                        });
1096                        return;
1097                    }
1098                } catch (err) {
1099                }
1100                try {
1101                    var ta = document.createElement('textarea');
1102                    ta.value = text;
1103                    ta.setAttribute('readonly', 'readonly');
1104                    ta.style.position = 'fixed';
1105                    ta.style.opacity = '0';
1106                    ta.style.left = '-9999px';
1107                    document.body.appendChild(ta);
1108                    ta.select();
1109                    var ok = false;
1110                    try {
1111                        ok = document.execCommand('copy');
1112                    } catch (err) {
1113                        ok = false;
1114                    }
1115                    document.body.removeChild(ta);
1116                    markCopied(ok);
1117                } catch (err) {
1118                    markCopied(false);
1119                }
1120            }, { passive: false });
1121            pre.appendChild(btn);
1122        });
1123    } catch (e) {
1124    }
1125}
1126
1127function initArchivePanel() {
1128    var page = document.getElementById('guidang');
1129    if (!page) return;
1130    var shell = page.querySelector('.archive-page-shell');
1131    if (!shell) return;
1132    var indexEl = page.querySelector('[data-archive-index]');
1133    var articleView = page.querySelector('[data-archive-article-view]');
1134    var titleEl = page.querySelector('[data-archive-article-title]');
1135    var backBtn = page.querySelector('.archive-back');
1136    var scrollEl = page.querySelector('.archive-scroll');
1137    if (!indexEl || !articleView || !scrollEl) return;
1138    var suppressClickUntil = 0;
1139
1140    function markArchiveScrollActive() {
1141        archiveScrollLockUntil = Date.now() + 1400;
1142    }
1143
1144    var touchStartX = 0;
1145    var touchStartY = 0;
1146    var touchTracking = false;
1147    function onTouchStart(e) {
1148        try {
1149            if (!e || !e.touches || e.touches.length !== 1) return;
1150            var t = e.touches[0];
1151            touchStartX = t.clientX;
1152            touchStartY = t.clientY;
1153            touchTracking = true;
1154        } catch (err) {
1155        }
1156    }
1157    function onTouchEnd() {
1158        touchTracking = false;
1159    }
1160    function onTouchMove(e) {
1161        try {
1162            if (!touchTracking || !e || !e.touches || e.touches.length !== 1) return;
1163            var t = e.touches[0];
1164            var dx = t.clientX - touchStartX;
1165            var dy = t.clientY - touchStartY;
1166            if (Math.abs(dx) < 6 && Math.abs(dy) < 6) return;
1167            if (Math.abs(dy) >= Math.abs(dx) + 3) {
1168                markArchiveScrollActive();
1169            }
1170        } catch (err) {
1171        }
1172    }
1173
1174    scrollEl.addEventListener('touchstart', onTouchStart, { passive: true });
1175    scrollEl.addEventListener('touchmove', onTouchMove, { passive: true });
1176    scrollEl.addEventListener('touchend', onTouchEnd, { passive: true });
1177    scrollEl.addEventListener('touchcancel', onTouchEnd, { passive: true });
1178    scrollEl.addEventListener('wheel', markArchiveScrollActive, { passive: true });
1179
1180    scrollEl.addEventListener('scroll', function () {
1181        if (Date.now() < archiveScrollProgrammaticUntil) return;
1182        suppressClickUntil = Date.now() + 260;
1183        markArchiveScrollActive();
1184    }, { passive: true });
1185
1186    function setView(mode) {
1187        shell.setAttribute('data-archive-view', mode);
1188    }
1189
1190    function clearArticle() {
1191        while (articleView.firstChild) {
1192            articleView.removeChild(articleView.firstChild);
1193        }
1194        if (titleEl) titleEl.textContent = '';
1195    }
1196
1197    function normalizeRel(href) {
1198        try {
1199            return new URL(href, window.location.href).pathname.replace(/\/+$/, '/') || '/';
1200        } catch (e) {
1201            return '';
1202        }
1203    }
1204
1205    var templateById = new Map();
1206    var templateByPath = new Map();
1207    page.querySelectorAll('template[data-archive-id]').forEach(function (tpl) {
1208        var id = tpl.getAttribute('data-archive-id') || '';
1209        if (id) templateById.set(id, tpl);
1210        var url = tpl.getAttribute('data-archive-url') || '';
1211        if (url) {
1212            var path = normalizeRel(url);
1213            if (path) templateByPath.set(path, tpl);
1214        }
1215    });
1216
1217    function renderTemplate(tpl) {
1218        if (!tpl) return false;
1219        clearArticle();
1220        try {
1221            var frag = tpl.content ? tpl.content.cloneNode(true) : null;
1222            if (!frag) return false;
1223            articleView.appendChild(frag);
1224            enhanceArchiveArticleRoot(articleView);
1225            var articleTitle = articleView.querySelector('.archive-article-title');
1226            if (titleEl && articleTitle) {
1227                titleEl.textContent = articleTitle.textContent || '';
1228            }
1229            setView('article');
1230            scrollEl.scrollTop = 0;
1231            return true;
1232        } catch (e) {
1233            return false;
1234        }
1235    }
1236
1237    function openById(id) {
1238        return renderTemplate(templateById.get(id));
1239    }
1240
1241    function openByHref(href) {
1242        var path = normalizeRel(href);
1243        if (!path) return false;
1244        return renderTemplate(templateByPath.get(path));
1245    }
1246
1247    page.addEventListener('click', function (e) {
1248        if (Date.now() < suppressClickUntil) {
1249            e.preventDefault();
1250            return;
1251        }
1252        var link = e.target && e.target.closest ? e.target.closest('a.archive-item[data-archive-id]') : null;
1253        if (!link || !page.contains(link)) return;
1254        var id = link.getAttribute('data-archive-id') || '';
1255        var href = link.getAttribute('href') || '';
1256        e.preventDefault();
1257        if (id && openById(id)) return;
1258        if (href) openByHref(href);
1259    });
1260
1261    if (backBtn) {
1262        backBtn.addEventListener('click', function () {
1263            clearArticle();
1264            setView('index');
1265            scrollEl.scrollTop = 0;
1266        });
1267    }
1268
1269    articleView.addEventListener('click', function (e) {
1270        var a = e.target && e.target.closest ? e.target.closest('a[href]') : null;
1271        if (!a || !articleView.contains(a)) return;
1272        var href = a.getAttribute('href') || '';
1273        if (!href || href.startsWith('#')) return;
1274        var target = (a.getAttribute('target') || '').toLowerCase();
1275        if (target === '_blank') return;
1276        a.setAttribute('target', '_blank');
1277    });
1278}
1279
1280document.addEventListener("DOMContentLoaded", function () {
1281    var currentPage = document.querySelector('.page.active');
1282    syncHeaderPageMode(currentPage ? currentPage.id : 'default');
1283    ensureHotPanelSelection();
1284    initArchivePanel();
1285    window.addEventListener('resize', queueSyncHeaderColumnHeights);
1286    window.addEventListener('load', queueSyncHeaderColumnHeights);
1287    window.addEventListener('resize', observeHeaderColumnChanges);
1288    document.addEventListener('hotlist:updated', function () {
1289        queueSyncHeaderColumnHeights();
1290        observeHeaderColumnChanges();
1291    });
1292    observeHeaderColumnChanges();
1293});
1294
1295function broadcastEmbeddedTheme() {
1296    try {
1297        var isNight = document.body && document.body.classList && document.body.classList.contains('io-black-mode');
1298        document.querySelectorAll('iframe.embedded-page-frame').forEach(function (frame) {
1299            if (!frame || !frame.contentWindow) return;
1300            frame.contentWindow.postMessage({ type: 'ndh:theme', night: isNight ? '1' : '0' }, window.location.origin);
1301        });
1302    } catch (e) {
1303    }
1304}
1305
1306// Tab 切换功能
1307function showPage(key) {
1308    // 隐藏所有页面
1309    document.querySelectorAll('.page').forEach(function(page) {
1310        page.classList.remove('active');
1311    });
1312    // 显示当前页面
1313    var current = document.getElementById(key);
1314    if (current) {
1315        current.classList.add('active');
1316    }
1317    // 切换按钮高亮
1318    document.querySelectorAll('.nav-button').forEach(function(btn) {
1319        btn.classList.remove('active');
1320    });
1321    // 高亮当前按钮
1322    var btns = document.querySelectorAll('.nav-button');
1323    btns.forEach(function(btn) {
1324        if (btn.getAttribute('onclick') && btn.getAttribute('onclick').includes(key)) {
1325            btn.classList.add('active');
1326        }
1327    });
1328    syncHeaderPageMode(key);
1329    if (key === 'waline') {
1330        ensureHotPanelSelection();
1331    }
1332    broadcastEmbeddedTheme();
1333    observeHeaderColumnChanges();
1334}

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.