PageSourceSearch

https://91hl01.com/usr/plugins/Search/search.js

js 91hl01.com collected 2026-09-28 09:06:41 UTC 22,208 bytes, 581 lines download raw bytes

1(async function (doc, win, hotRankList, keyword) {
2    // 获取 layer 模块(兼容 layui 和直接引入的 layer)
3    const layer = win.layer || (win.layui && win.layui.layer) || {
4        msg: function(msg) { console.log(msg); },
5        load: function() { return 0; },
6        closeAll: function() {}
7    };
8    
9    // 站内搜索入口 - 使用当前域名进行搜索
10    const buildSearchUrl = (q) => {
11        // 使用当前域名的根路径进行搜索,不使用后台配置的地址
12        const base = window.location.origin + '/';
13        return `${base}?s=${encodeURIComponent(q)}`;
14    };
15
16    // 列表(搜索结果)
17    class SortList {
18        constructor(selector, url) {
19            const _this = this;
20            this.$archive = () => $(selector);
21            this.keyword = keyword;
22            this.url = url;
23            this.sort = '';
24            this.page = 1;
25
26            // 单选
27            $(document).on('change', '.xqbj-search-sort input[name="searchsort"][type="radio"]', function() {
28                _this.sort = $(this).val();
29                _this.page = 1;
30                _this.load();
31            });
32        }
33
34        // 入口方法
35        load() {
36            this.showLoading();
37            this.fetchData()
38                .done((html) => {
39                    this.renderList(html);
40                    this.bindEvents();
41                })
42                .fail(() => {
43                    layer.msg('数据请求失败');
44                })
45                .always(() => {
46                    this.goTop();
47                    setTimeout(() => {
48                        layer.closeAll('loading')
49                    }, 500);
50                });
51        }
52
53        // 请求
54        fetchData() {
55            const data = {
56                q: this.keyword,
57                sort: this.sort,
58                page: this.page,
59            };
60
61            console.log("@data", data);
62
63            return $.ajax({
64                url: this.url,
65                method: 'GET',
66                data: { s: data.q, sort: data.sort, page: data.page }
67            });
68        }
69
70        // 渲染[后端处理]
71        renderList(html) {
72            this.$archive().fadeOut('fast', () => {
73                this.$archive().replaceWith(html).fadeIn(300);
74            });
75        }
76
77        // 事件
78        bindEvents() {
79            const _this = this;
80            $(doc).off('click.pageNav')
81
82            // 上一页
83            .on('click.pageNav', '.page-nav-ajax .prev a', function (e) {
84                e.preventDefault();
85                _this.page = $(this)?.data('page') || 1;
86                _this.load();
87            })
88
89            // 下一页
90            .on('click.pageNav', '.page-nav-ajax .next a', function (e) {
91                e.preventDefault();
92                _this.page = $(this)?.data('page') || 1;
93                console.log('next clicked');
94                _this.load();
95            })
96
97            // 跳转按钮
98            .on('click.pageNav', '.page-nav-ajax #submitBtn', function (e) {
99                e.preventDefault();
100                const input = $(this).closest('.page-nav-ajax').find('input').val();
101                console.log('goto clicked', input);
102
103                _this.page = (1 * input || 1);
104                _this.load();
105            });
106        }
107
108        // 显示加载动画
109        showLoading() {
110            layer.load(0);
111        }
112
113        // 显示加载动画
114        goTop() {
115            window.scrollTo({
116                top: 0,
117                behavior: 'smooth'
118            });
119        }
120    }
121
122    // 导航(头部导航)
123    class NavSearch {
124        constructor(hoturl, url, throttleInterval, keyLength) {
125            const _this = this;
126            this.hoturl = hoturl;
127            this.url = url;
128            this.throttleInterval = throttleInterval;
129            this.keyLength = keyLength;
130            this.domparser = new DOMParser();
131
132            // 模板定义(历史)
133            this.hotTemplate = (c = []) => {
134                const list = Array.isArray(c) ? c.slice(0, 10) : [];
135                const hasData = list.length > 0;
136                return `
137                    <div class="container-search-context">
138                    <div class="search-container">
139                        <div class="search-title">
140                            <span>历史记录</span>
141                            <div class="r-text on-history-clear">
142                                <div class="xqbj-icon-delete icon"></div>
143                                删除记录
144                            </div>
145                        </div>
146                        <div class="container-search-tags">
147                            <div class="list history-record">
148                                <div class="no-data">暂无数据</div>
149                            </div>
150                        </div>
151                        <div class="search-title search-border">
152                            <span>热门搜索</span>
153                        </div>
154                        <div class="container-search-hots">
155                        ${
156                            hasData
157                            ? `
158                                <div class="rank-list">
159                                    ${list.map((e, index) => `
160                                        <a href="${e.permalink}" class="rank-card">
161                                            ${index <= 2 ? `<div class="xqbj-icon-rank-${index + 1}"></div>` : `<div class="rank-card-icon">${index + 1}</div>`}
162                                            <div class="rank-card-title text-line-ellipsis-1">${e.title}</div>
163                                            <div class="mobile-rank-hot"><span>${e.hot_formatted} 热度</span></div>
164                                        </a>
165                                    `).join("")}
166                                </div>
167                            `
168                            : `<div class="no-data">暂无数据</div>`
169                        }
170                        </div>
171                    </div>
172                    </div>
173                `;
174            };
175
176            // 模板定义(搜索建议)
177            // 最多显示 20 条搜索建议,关键词高亮使用 <span class="keyword"> 包裹
178            // Requirements: 6.2
179            this.seaTemplate = (c) => {
180                const list = Array.isArray(c?.list) ? c.list.slice(0, 20) : [];
181                const hasData = list.length > 0;
182                return hasData ? `
183                    <div class="container-search-context">
184                        <div class="search-container">
185                            <div class="container-search-hots">
186                                <div class="rank-list">
187                                    ${list.map((e) => `
188                                        <a href="${e.url}" class="rank-card">
189                                            <div class="xqbj-icon-search1"></div>
190                                            <div class="rank-card-title text-line-ellipsis-1">${e.title}</div>
191                                        </a>
192                                    `).join("")}
193                                </div>
194                            </div>
195                        </div>
196                    </div>
197                ` : ``;
198            };
199
200            this.renderHotList = (list = []) => {
201                const tpl = _this.hotTemplate(list);
202                // 同时渲染桌面与移动端的搜索容器
203                $('.nav-search-container .container-hot-search').each(function () {
204                    $(this).html(tpl);
205                    $(this).addClass('show');
206                });
207            };
208
209            // 初始(mobile) - 跳转到搜索页面
210            $(doc).on('click', 'form#search-form', function (e) {
211                e.preventDefault();
212                top.location.href = '/usr/plugins/Search/search-page.php';
213            });
214
215            // 输入(mobile)
216            $(doc).on('input', '#input-mobile-control-key', _this.debounce(function() {
217                const navSearchContainer = $(this).closest('.nav-search-container');
218                const hotSearchContainer = navSearchContainer.find('.container-hot-search').get(0);
219                const listSearchContainer = navSearchContainer.find('.container-list-search').get(0);
220                const keyword = $(this).val().trim();
221
222                if (keyword) {
223                    _this.fetchSearchData(keyword)
224                        .done((d) => {
225                            const data = {
226                                list: d.map(e => ({
227                                    title: e,
228                                    url: buildSearchUrl(e)
229                                }))
230                            };
231
232                            // 渲色处理
233                            data.list = data.list.map(item => {
234                                const safeKeyword = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
235                                const regex = new RegExp(safeKeyword, 'g');
236
237                                return {
238                                    ...item,
239                                    title: item.title.replace(regex, `<span class="keyword">${keyword}</span>`)
240                                };
241                            });
242
243                            // 模板构建
244                            const tpl = _this.seaTemplate(data);
245
246                            // 模板渲染
247                            $(listSearchContainer).html(tpl);
248                        })
249                        .fail(() => {
250                            layer.msg('数据请求失败');
251                            console.log('数据请求失败');
252                        })
253                        .always(() => {
254                            $(listSearchContainer).addClass('show');
255                            $(hotSearchContainer).removeClass('show');
256                        });
257                } else {
258                    $(listSearchContainer).removeClass('show');
259                    $(hotSearchContainer).removeClass('show');
260                }
261            }, _this.throttleInterval));
262
263            // 搜索(mobile)
264            $(doc).on('click', '.input-mobile-search-btn', function () {
265                const navSearchContainer = $(this).closest('.van-nav-bar');
266                const keywordval = navSearchContainer.find('#input-mobile-control-key').val().trim();
267                const keywordhtml = _this.domparser.parseFromString(keywordval, 'text/html');
268                const keyword = keywordhtml.body.textContent;
269                try {
270                    top.location.href = buildSearchUrl(keyword);
271                } finally {
272                    $HistoryRecord.add(keyword);
273                }
274            });
275
276            // 搜索-键盘(mobile)
277            $('#input-mobile-control-key').on('keydown', function (event) {
278                if (event.key === 'Enter') {
279                    event.preventDefault();
280                    const navSearchContainer = $(this).closest('.van-nav-bar');
281                    const keywordval = navSearchContainer.find('#input-mobile-control-key').val().trim();
282                    const keywordhtml = _this.domparser.parseFromString(keywordval, 'text/html');
283                    const keyword = keywordhtml.body.textContent;
284                    try {
285                        top.location.href = buildSearchUrl(keyword);
286                    } finally {
287                        $HistoryRecord.add(keyword);
288                    }
289                }
290            });
291
292            // ###########################
293
294            // 初始(desktop)
295            $(doc).on('click', '.nav-search-container', function () {
296                $(this).addClass('expand').find('#input-desktop-control-key').focus();
297            });
298
299            // 输入(desktop)
300            $(doc).on('input', '#input-desktop-control-key', _this.debounce(function() {
301                const navSearchContainer = $(this).closest('.nav-search-container');
302                const hotSearchContainer = navSearchContainer.find('.container-hot-search').get(0);
303                const listSearchContainer = navSearchContainer.find('.container-list-search').get(0);
304                const keywordhtml = _this.domparser.parseFromString($(this).val(), 'text/html');
305                const keyword = keywordhtml.body.textContent;
306
307                if (keyword) {
308                    _this.fetchSearchData(keyword)
309                    .done((d) => {
310                        const data = {
311                            list: d.map(e => ({
312                                title: e,
313                                url: buildSearchUrl(e)
314                            }))
315                        };
316
317                        // 渲色处理
318                        data.list = data.list.map(item => {
319                            const safeKeyword = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
320                            const regex = new RegExp(safeKeyword, 'g');
321
322                            return {
323                                ...item,
324                                title: item.title.replace(regex, `<sp
324an class="keyword">${keyword}</span>`)
325                            };
326                        });
327
328                        // 模板构建
329                        const tpl = _this.seaTemplate(data);
330
331                        // 模板渲染
332                        $(listSearchContainer).html(tpl);
333                    })
334                    .fail(() => {
335                        layer.msg('数据请求失败');
336                        console.log('数据请求失败');
337                    })
338                    .always(() => {
339                        $(listSearchContainer).addClass('show');
340                        $(hotSearchContainer).removeClass('show');
341                    });
342                } else {
343                    $(listSearchContainer).removeClass('show');
344                    $(hotSearchContainer).addClass('show');
345                }
346            }, _this.throttleInterval));
347
348            // 区域失焦(desktop)
349            $(doc).on('mouseleave', '#navbarCollapse', function () {
350                const navSearchContainer = $(this).find('.nav-search-container');
351                const navSearchinput = $(this).find('#input-desktop-control-key');
352
353                if(navSearchinput.val() === '') {
354                    $(navSearchContainer).removeClass('expand');
355                    $(navSearchinput).val('').blur();
356                }
357            });
358
359            // 点击失焦(desktop)
360            $(doc).on('click', function (event) {
361                const navSearchContainer = $('#navbarCollapse .nav-search-container');
362                if($(event.target).closest('#navbarCollapse').length) {
363                    // 点击在导航栏内,不处理
364                } else if($(event.target).closest('.on-history-delete').length) {
365                    // 点击删除按钮,不处理
366                } else {
367                    $(navSearchContainer).removeClass('expand');
368                    $(this).val('');
369                }
370            });
371
372            // 确认(desktop)
373            $(doc).on('keydown', '#input-desktop-control-key', function (e) {
374                if (e.key === 'Enter') {
375                    const keywordhtml = _this.domparser.parseFromString($(this).val().trim(), 'text/html');
376                    const keyword = keywordhtml.body.textContent;
377                    if (keyword) {
378                        try {
379                            top.location.href = buildSearchUrl(keyword);
380                        } finally {
381                            $HistoryRecord.add(keyword);
382                        }
383                    }
384                }
385            });
386
387            // ###########################
388
389            // 返回
390            $(doc).on('click', '.backbtn', function () {
391                if (window.location.protocol === 'file:' || (!document.referrer && document.referrer.startsWith('file://'))) {
392                    top.location.href = `../index.html`;
393                } else if (!document.referrer) {
394                    top.location.href = `./`;
395                } else {
396                    history.back();
397                }
398            });
399
400            // 搜索历史收集(mobile and desktop)
401            $(doc).on('click', '.container-search-tags a, .container-search-lists a, .xqbj-search-tags-list a, .xqbj-search-none a', function (event) {
402                const keywordhtml = _this.domparser.parseFromString($(event.target)?.text()?.replace(/(^\s*)|(\s*$)/g, ""), 'text/html');
403                const keyword = keywordhtml.body.textContent;
404                try {
405                    $HistoryRecord.add(keyword);
406                } catch (error) {
407                    console.log("历史信息收集失败", error);
408                }
409            });
410
411            //
412            this.init();
413        }
414
415        // 初始化
416        init() {
417            const _this = this;
418            console.log('Search Plugin Init - hotRankList:', hotRankList);
419            _this.renderHotList(hotRankList);
420
421            // 若配置了接口,则再拉取最新数据覆盖
422            _this.fetchHotData()
423                .done((list = []) => {
424                    if (Array.isArray(list)) {
425                        _this.renderHotList(list);
426                    }
427                })
428                .fail(() => {
429                    console.log('热门搜索加载失败');
430                });
431
432            //
433            win?.$HistoryRecord && win?.$HistoryRecord.init();
434        }
435
436        // 搜索请求 - 关键词长度必须大于2字符
437        fetchSearchData(keyword) {
438            const _this = this;
439            if(keyword.length <= _this.keyLength) throw new Error('关键词长度必须大于2');
440
441            const data = {
442                q: keyword
443            };
444
445            console.log("@data", data, this.url);
446
447            return $.ajax({
448                url: `${this.url}`,
449                method: 'GET',
450                data
451            });
452        }
453
454        // 热门请求
455        fetchHotData() {
456            const _this = this;
457            if (!_this.hoturl || typeof _this.hoturl !== 'string') {
458                return $.Deferred().reject();
459            }
460            return $.ajax({
461                url: _this.hoturl,
462                method: 'POST'
463            });
464        }
465
466        // 节流
467        debounce(func, delay) {
468            let timer = null;
469            return function(...args) {
470                if (timer) clearTimeout(timer);
471                timer = setTimeout(() => {
472                    func.apply(this, args);
473                    timer = null;
474                }, delay);
475            };
476        }
477    }
478
479    // 历史(搜索历史)
480    class HistoryRecord {
481        constructor() {
482            this.key = '__GLOBAL_SEARCH_HISTORY__';      // localStorage 的 key
483            this.max = 8;                                // 最多保存 8 条记录
484            this.listcontainer = 'div.history-record';   // 列表容器元素
485            this.init();                                 // 初始加载记录
486        }
487
488        // 加载
489        init() {
490            const _this = this;
491            const history = JSON.parse(localStorage.getItem(this.key)) || [];
492            this.show(history);
493
494            // 事件
495            _this.event();
496        }
497
498        // 事件
499        event() {
500            const _this = this;
501            $(doc).on('click', '.on-history-clear', function () {
502                _this.clear();
503            });
504
505            $(doc).on('click', '.on-history-delete', function (e) {
506                e.preventDefault();
507                const keyword = $(this).closest('a').text();
508                _this.del(keyword);
509            });
510        }
511
512        // 显示(操作)
513        show(historys) {
514            const $container = $(this.listcontainer);
515            $container && $container.each((index, c) => {
516                const hasData = Array.isArray(historys) && historys.length > 0;
517                if (hasData) {
518                    c.innerHTML = historys.map(item => `<a href="${buildSearchUrl(item)}"><span class="text-line-ellipsis-1">${item}</span></a>`).join('');
519                } else {
520                    c.innerHTML = '<div class="no-data">暂无数据</div>';
521                }
522            });
523        }
524
525        // 添加
526        add(keyword) {
527            if (!keyword) return;
528
529            let history = JSON.parse(localStorage.getItem(this.key)) || [];
530
531            // 去重 + 添加到开头 + 限制数量
532            history = [keyword, ...history.filter(item => item !== keyword)].slice(0, this.max);
533
534            localStorage.setItem(this.key, JSON.stringify(history));
535            this.show(history);
536        }
537
538        // 删除
539        del(keyword) {
540            if (!keyword) return;
541
542            let history = JSON.parse(localStorage.getItem(this.key)) || [];
543
544            // 删除
545            history = history.filter(k => k !== keyword);
546
547            localStorage.setItem(this.key, JSON.stringify(history));
548            this.show(history);
549        }
550
551        // 清空
552        clear() {
553            localStorage.removeItem(this.key);
554            this.show([]);
555        }
556    }
557
558    // 初始化
559    document.addEventListener('DOMContentLoaded', function () {
560        // 使用当前域名作为搜索基础URL
561        const currentBaseUrl = window.location.origin + '/';
562        
563        // 导航
564        win['$NavSearch'] = new NavSearch(
565            '',                  // 无单独热门接口,保持空
566            currentBaseUrl,      // 使用当前域名进行搜索
567            300,
568            2   // 关键词长度下限(必须大于2字符)
569        );
570
571        // 搜索
572        win['$SortList'] = new SortList(
573            "div#archive",
574            currentBaseUrl
575        );
576
577        // 历史
578        win['$HistoryRecord'] = new HistoryRecord();
579    }, false);
580
581})(document, window, window.hotRankList || [], window.keyword || '');

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.