零件:GlobalSearcher.js

来自俄罗斯方块中文维基
// 可以用es6,且此时必定已有jquery
console.log(`globalSearcher.js is loaded.`)

function loadGlobalSearcher() {
    // 目前用的是warframe的entry。
    const dbEntry = `https://${mw.config.get('wgHuijiPrefix')}.huijiwiki.com/api/rest_v1/namespace/data`
    $('#globalSearcher').children('.gs-formcontainer').html('')
    $('#globalSearcher').children('.gs-formcontainer').append($('<form>').attr('id', 'gs-form'));
    const formbox = $('#globalSearcher').children('.gs-formcontainer').children('#gs-form')
    formbox.submit(function (event) {
        //阻止form因为回车而提交
        event.preventDefault()
    })
    const logbox = $('#globalSearcher').children('.gs-infocontainer')[0]
    const resultbox = $('#globalSearcher').children('.gs-resultcontainer')[0]
    const paginatorbox = $('#globalSearcher').children('.gs-paginator')
    const validCompTypes = [
        "text",
        "dropbox",
        "checkbox",
        "radio",
        "hidden"
    ]
    const config = JSON.parse($('#globalSearcher').find('.gs-config').attr('data-gsconfig'))
    // const config = {
    //     forms: [
    //         { type: 'hidden', model: 'data_type', value: 'item' },
    //         { type: 'text', model: 'item_name', placeholder: '输入名称', title: '物品名称', bindSubmit: true, caseSensitive: false },
    //         {
    //             type: 'dropbox',
    //             model: 'item_type',
    //             options: [
    //                 { value: '', text: '全部' },
    //                 { value: '0', text: '杂物', default: true },
    //                 { value: '1', text: '装备' },
    //                 { value: '2', text: '消耗品' },
    //                 { value: '3', text: '商品' },
    //                 { value: '8', text: '战斗道具' },
    //                 { value: '9', text: '补给品' },
    //                 { value: '11', text: '宝石' },
    //             ],
    //             title: '类型'
    //         },
    //         { type: 'checkbox', model: 'fixable', text: '可修理', },
    //         {
    //             type: 'radio',
    //             model: 'rarity',
    //             options: [
    //                 { value: '', text: '全部' },
    //                 { value: '0', text: '白色', default: true },
    //                 { value: '1', text: '绿色' },
    //                 { value: '2', text: '蓝色' },
    //                 { value: '3', text: '橙色' },
    //             ],
    //             title: '品质'
    //         }
    //     ],
    //     resultTemplate: 'ItemListEntry',
    //     class: 'itemlist',
    // }
    $('#globalSearcher').addClass(config.class)
    for (let compCfg of config.forms) {
        if (!validCompTypes.includes(compCfg.type)) {
            console.error(`不支持的组件类型: ${compCfg.type}`)
            continue
        }
        let comp = null
        switch (compCfg.type) {
            // 根据comCfg输出元素。
            // 表单元素必须有name属性,且值为model属性。
            case 'text': // 文本框
                comp = $('<input>')
                    .attr('type', 'text')
                    .attr('placeholder', compCfg.placeholder)
                    .attr('model', compCfg.model)
                    .attr('name', compCfg.model)
                    .attr('data-gs-bindsubmit', compCfg.bindSubmit)
                break;
            case 'hidden': // 隐藏域
                comp = $('<input>')
                    .attr('type', 'hidden')
                    .attr('model', compCfg.model)
                    .attr('name', compCfg.model)
                    .val(compCfg.value)
                break;
            case 'dropbox': // 下拉框
                comp = $('<select>')
                    .attr('model', compCfg.model)
                    .attr('name', compCfg.model);
                for (let option of compCfg.options) {
                    let opt = $('<option>')
                        .attr('value', option.value)
                        .text(option.text)
                    if (option.default) {
                        opt.attr('selected', 'selected')
                    }
                    comp.append(opt)
                }
                break;
            case 'checkbox': // 复选框
                comp = $('<div>').addClass('checkbox-group')
                let checkbox = $('<input>')
                    .attr('type', 'checkbox')
                    .attr('model', compCfg.model)
                    .attr('reverse', compCfg.reverse)
                    .attr('name', compCfg.model)
                if (compCfg.checked) {
                    checkbox.attr('checked', 'checked')
                }
                let label = $('<label>').append(checkbox).append(compCfg.text)
                comp.append(label)
                break;
            case 'radio': // 单选框
                comp = $('<div>').addClass('radio-group')
                for (let option of compCfg.options) {
                    let opt = $('<input>')
                        .attr('type', 'radio')
                        .attr('model', compCfg.model)
                        .attr('value', option.value)
                        .attr('name', compCfg.model)
                    if (option.default) {
                        opt.attr('checked', 'checked')
                    }
                    let label = $('<label>').text(option.text)
                    comp.append(opt).append(label)
                }
                break;
        }
        if (!compCfg.title) {
            compCfg.title = ''
        }
        comp.attr('data-gs-comptitle', compCfg.title)
            .attr('data-gs-comptype', compCfg.type)
        if (comp.attr('data-gs-bindsubmit') == 'true' && compCfg.type == 'text') {
            comp.keypress(function (event) {
                //        console.log(event.keyCode)
                let keycode = (event.keyCode ? event.keyCode : event.which)
                if (keycode == '13') {
                    sendQuery(1)
                }
            })
        }
        //  console.log(comp)
        comp.appendTo(formbox)
    }
    // 增加控件组标题
    formbox.children().each(function () {
        if ($(this).attr('type') == 'hidden') { return }
        $(this).wrap('<div class="gs-formgroup"></div>')
        $(this).wrap('<div class="gs-formgroup-content"></div>')
        $(this).parent().before($('<div class="gs-formgroup-title">' + $(this).attr('data-gs-comptitle') + '</div>'))
    })
    // 按钮
    let submitBtn = $('<button>').addClass('gs-submit btn btn-primary').text('提交')
    let currentQuery = parseQuery(formbox.serializeArray())
    // 查询事件
    function sendQuery(p = 1) {
        if (!($.cookie('gs-timer'))) {
            // 首次使用,初始化定时器
            $.cookie('gs-timer', new Date().getTime())
        } else {
            // 此前使用过
            let timer = $.cookie('gs-timer')
            let now = new Date().getTime()
            let timeThreshold = 1000
            if (now - timer < timeThreshold) {
                // 过于频繁时
                console.warn('too frequent')
                $(logbox).html(`查询过于频繁,请稍后再试。(查询间隔小于${timeThreshold}ms)`)
                return
            } else {
                // 不频繁时,更新定时器
                $.cookie('gs-timer', new Date().getTime())
            }
        }
        $(paginatorbox).html('')
        $(resultbox).html('')
        console.log('query sent')
        $(logbox).html(`正在查询……`)
        currentQuery = parseQuery(formbox.serializeArray())
        currentQuery = Object.assign(currentQuery, { page: p })
        //  console.log('currentQuery:')
        //  console.log(currentQuery)
        let result = $.ajax(dbEntry, {
            data: currentQuery,
        })
        result.done((data) => {
            // console.log('query finished.')
            // (data)
            $(logbox).html(`找到<span class="text-primary"> ${data._size}</span>个结果,当前显示第<span class="text-primary"> ${(currentQuery.page - 1) * config.pagesize + 1}-${(currentQuery.page - 1) * config.pagesize + data._returned}</span>个结果。`)
            if (data._size > 0) {
                renderData(data, resultbox, currentQuery)
            }
            if (data._size > data._returned) {
                console.log('rendering paginator...')
                renderPaginator({
                    currentPage: currentQuery.page,
                    totalPage: data._total_pages,
                    action: sendQuery
                })
            }
        })
        result.fail((req, status, err) => {
            $(logbox).html(`查询失败,错误编号:${status};信息:${err}`)
        })

    }
    submitBtn.click(function () {
        sendQuery(1)
    })
    submitBtn.appendTo(formbox)
    // let resetBtn = $('<input>').attr('type', 'reset')
    // resetBtn.appendTo(formbox)
    //  console.log({ formbox, resultbox, config })

    // 当searchOnLoad为True时,自动发起一次查询。
    if (config.searchOnLoad) {
        sendQuery(1)
    }

    function parseQuery(options) {
        // 将表单数据转换为查询参数
        let compCfg = {}
        for (let cfg of config.forms) {
            // 设法合并控件定义和表单数据。compCfg每个节点为一个comp的原始定义。
            if (cfg.model) {
                compCfg[cfg.model] = cfg
            }
        }
        let param = {
            filter: {},

            count: true,
            pagesize: config.pagesize,
        }
        if (config.sortkeys) { param.sort_by = config.sortkeys }
        for (let option of options) {
            if (option.value !== '') {
                let optionVal = null
                let flag = compCfg[option.name].flag

                if (flag) {
                    switch (flag) {
                        case 'i':
                            optionVal = {
                                $regex: option.value,
                                $options: 'i'
                            }
                            break;
                        case 'number':
                            optionVal = parseInt(option.value)
                            break;
                        case 'boolean':
                            if (['on', 'true'].includes(option.value)) { // 输入值由sealizeArray()方法决定,目前出现了true和on两种值。
                                optionVal = { $in: [true, 1, "true", "ture", "是"] } // 输出查询语句
                            }
                            break;
                        case 'e':
                        	optionVal = {$exists:true}
                        	break;
                        case 'ne':
                        	optionVal = {$exists:false}
                        	break;
                    }

                } else { optionVal = option.value }

                param.filter[option.name] = optionVal
            } else { continue }
        }
        // 将参数处理成一层
        let parsedParam = {}
        for (let key of Object.keys(param)) {
            parsedParam[key] = JSON.stringify(param[key])
        }
        return parsedParam
    }

    function renderData(data, container, queryParams) {

        $(container).html('已获得结果,正在渲染……')
        let output = ''
        let mwApi = new mw.Api();
        let paramStr = JSON.stringify(queryParams)
        if (data._embedded.length == 0) {
            output = '没有找到结果'
        } else {
            // 有一个或多个结果时,扁平化处理结果
            let flatResult = []
            for (let item of data._embedded) {
                let singleEntry = {}
                for (let key of Object.keys(item)) {
                    if (typeof item[key] == 'object' || typeof item[key] == 'array') {

                        continue
                    } else {
                        singleEntry[key] = item[key]
                        if (!(typeof singleEntry[key] == 'string')) {
                            singleEntry[key] = singleEntry[key].toString()
                        }
                    }
                }

                flatResult.push(singleEntry)
            }
            // console.log(flatResult)
            let renderStr = ''
            for (let item of flatResult) {
                let singleResultStr = ``
                singleResultStr = `{{${config.resultTemplate}`
                for (let key of Object.keys(item)) {
                    let value = item[key]
                    value = value.replace(/\n/g, '<br />')
                        .replaceAll('|', '{{!}}')
                    singleResultStr += `|${key}=${value}`
                }
                singleResultStr += '}}'
                renderStr += singleResultStr
            }
            // 将renderStr传递给html模板
            let cache = localStorage.getItem(paramStr)
            if (cache && cache !== '') {
                let cacheData = JSON.parse(cache)
                let now = new Date().getTime()
                if (now - cacheData.updateTime <= 60000) {
                    // 如果小于6000则直接使用缓存
                    console.log('renderData(): using render cache at ' + now)
                    $(container).html(cacheData.data.parse.text['*'])
                } else {
                    // 如果已经超过60000则重新请求
                    console.log('renderData(): render cache expired. sending request...')
                    sendPost()
                }

            } else {
                // 无效的cache
                console.log('renderData(): no cache or cache invalid. sending request...')
                sendPost()
            }

            function sendPost() {
                mwApi.post({
                    action: 'parse',
                    text: renderStr,
                    format: 'json',
                    utf8: 1,
                    prop: 'text',
                    contentmodel: 'wikitext',
                    maxage: 60,
                    smaxage: 60
                }).done(function (data) {
                    //      console.log(data)
                    $(container).html(data.parse.text['*'])
                    localStorage.setItem(paramStr, JSON.stringify({
                        updateTime: new Date().getTime(),
                        data: data
                    }))

                })
            }

        }
    }

    function renderPaginator(option) {
        // option.totalPage: 总页数
        // option.currentPage: 当前页
        // option.action: 分页按钮点击事件
        $(paginatorbox).html('')
        //  console.log('renderPaginator options')
        //   console.log(option)
        for (let i = 1; i <= option.totalPage; i++) {
            if (i >= option.currentPage + 5 || i <= option.currentPage - 5) {
                // 避免渲染过多的paginator
                continue
            }
            let pageBtn = $('<button class="btn btn-secondary gs-paginator-page">').text(i)
            if (i == option.currentPage) {
                pageBtn.addClass('active').attr('disabled', 'disabled')
            } else {
                pageBtn.click(function () {
                    option.action(i)
                })
            }
            pageBtn.appendTo(paginatorbox)
        }
        // 输出第一页和最后一页按钮
        let firstPageBtn = $('<button class="btn btn-secondary gs-paginator-first">').text('首页')
        if (option.currentPage == 1) {
            firstPageBtn.attr('disabled', 'disabled')
        }
        let lastPageBtn = $('<button class="btn btn-secondary gs-paginator-last">').text('尾页')
        if (option.currentPage == option.totalPage) {
            lastPageBtn.attr('disabled', 'disabled')
        }
        firstPageBtn.click(function () {
            option.action(1)
        })
        lastPageBtn.click(function () {
            option.action(option.totalPage)
        })
        firstPageBtn.prependTo(paginatorbox)
        lastPageBtn.appendTo(paginatorbox)
        let paginatorInfo = $('<div class="gs-paginator-info">').text(`当前第${option.currentPage}页,共${option.totalPage}页`)
        paginatorInfo.appendTo(paginatorbox)



    }
}
$(document).ready(() => {
    loadGlobalSearcher()
})