通过 underscore.js 中的另一个数组过滤 json 数据 [英] Filter a json data by another array in underscore.js

查看:28
本文介绍了通过 underscore.js 中的另一个数组过滤 json 数据的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个搜索字段,我想使用 underscore.js 添加一些复杂的功能.

I have a search field and I want to add some complex functionality using underscore.js.

有时用户搜索整个句子"比如Samsung Galaxy A20s ultra".我想使用搜索字符串中的任何单词过滤 JSON 数据,并按包含更多单词的结果进行排序.

Sometimes users search for a whole "sentence" like "Samsung galaxy A20s ultra". I want to filter JSON data using any of the words in the search string and sort by results that contain more of the words.

示例数据:

var phones = [
{name: "Samsung A10s", id: 845},
{name: "Samsung galaxy", id: 839},
{name: "Nokia 7", id: 814},
{name: "Samsung S20s ultra", id: 514},
{name: "Apple iphone ultra", id: 159},
{name: "LG S20", id: 854}];

用下划线表示的最佳方法是什么?

What is the best way to do it in underscore?

推荐答案

在这个答案中,我将构建一个函数 searchByRelevance,它接受两个参数:

In this answer, I'll be building a function searchByRelevance that takes two arguments:

  1. 具有 nameid 属性的手机的 JSON 数组,以及
  2. 一个搜索字符串,
  1. a JSON array of phones with name and id properties, and
  2. a search string,

并返回一个新的 JSON 数组,其中只有 name 与搜索字符串至少有一个共同词的音素,排序使得最常见的音素排在最前面.

and which returns a new JSON array, with only the phones of which the name has at least one word in common with the search string, sorted such that the phones with the most common words come first.

让我们首先确定所有子任务以及如何使用 Underscore 实现它们.完成后,我们可以将它们组合到 searchByRelevance 函数中.最后,我还将花一些时间讨论我们如何确定什么是最好的".

Let's first identify all the subtasks and how you could implement them with Underscore. Once we've done that, we can compose them into the searchByRelevance function. In the end, I'll also spend some words on how we might determine what is "best".

为此您不需要下划线.字符串有一个 内置 split 方法:

You don't need Underscore for this. Strings have a builtin split method:

"Samsung galaxy A20s ultra".split(' ')
// [ 'Samsung', 'galaxy', 'A20s', 'ultra' ]

然而,如果你有一个完整的字符串数组并且你想把它们全部拆分,所以你得到一个数组数组,你可以使用 _.invoke:

However, if you have a whole array of strings and you want to split them all, so you get an array of arrays, you can do so using _.invoke:

_.invoke([
    'Samsung A10s',
    'Samsung galaxy',
    'Nokia 7',
    'Samsung S20s ultra',
    'Apple iphone ultra',
    'LG S20'
], 'split', ' ')
// [ [ 'Samsung', 'A10s' ],
//   [ 'Samsung', 'galaxy' ],
//   [ 'Nokia', '7' ],
//   [ 'Samsung', 'S20s', 'ultra' ],
//   [ 'Apple', 'iphone', 'ultra' ],
//   [ 'LG', 'S20' ] ]

找出两个数组的共同点

如果你有两个单词数组,

Find the words that two arrays have in common

If you have two arrays of words,

var words1 = [ 'Samsung', 'galaxy', 'A20s', 'ultra' ],
    words2 = [ 'Apple', 'iphone', 'ultra' ];

然后您可以使用 _.intersection:

then you can get a new array with just the words they have in common using _.intersection:

_.intersection(words1, words2) // [ 'ultra' ]

计算数组中的单词数

这又是你不需要下划线的东西:

Count the number of words in an array

This is again something you don't need Underscore for:

[ 'Samsung', 'A10s' ].length // 2

但是如果您有多个单词数组,则可以使用 _ 获取所有单词的字数统计.map:

But if you have multiple arrays of words, you can get the word counts for all of them using _.map:

_.map([
    [ 'Samsung', 'A10s' ],
    [ 'Samsung', 'galaxy' ],
    [ 'Nokia', '7' ],
    [ 'Samsung', 'S20s', 'ultra' ],
    [ 'Apple', 'iphone', 'ultra' ],
    [ 'LG', 'S20' ]
], 'length')
// [ 2, 2, 2, 3, 3, 2 ]

按某种标准对数组进行排序

_.sortBy 就是这样做的.例如,phones id 数据:

_.sortBy(phones, 'id')
// [ { name: 'Apple iphone ultra', id: 159 },
//   { name: 'Samsung S20s ultra', id: 514 },
//   { name: 'Nokia 7', id: 814 },
//   { name: 'Samsung galaxy', id: 839 },
//   { name: 'Samsung A10s', id: 845 },
//   { name: 'LG S20', id: 854 } ]

要降序排序而不是升序,您可以先升序排序,然后使用 内置reverse方法:

To sort descending instead of ascending, you can first sort ascending and then reverse the result using the builtin reverse method:

_.sortBy(phones, 'id').reverse()
// [ { name: 'LG S20', id: 854 },
//   { name: 'Samsung A10s', id: 845 },
//   { name: 'Samsung galaxy', id: 839 },
//   { name: 'Nokia 7', id: 814 },
//   { name: 'Samsung S20s ultra', id: 514 },
//   { name: 'Apple iphone ultra', id: 159 } ]

您还可以传递标准函数.该函数接收当前项目,它可以做任何事情,只要它返回一个字符串或数字作为当前项目的排名.例如,这按名称的最后一个字母对手机进行排序(使用 _.last):

You can also pass a criterion function. The function receives the current item and it can do anything, as long as it returns a string or number to use as the rank of the current item. For example, this sorts the phones by the last letter of the name (using _.last):

_.sortBy(phones, function(phone) { return _.last(phone.name); })
// [ { name: 'LG S20', id: 854 },
//   { name: 'Nokia 7', id: 814 },
//   { name: 'Samsung S20s ultra', id: 514 },
//   { name: 'Apple iphone ultra', id: 159 },
//   { name: 'Samsung A10s', id: 845 },
//   { name: 'Samsung galaxy', id: 839 } ]

按某种标准对数组的元素进行分组

不是直接排序,我们也可能首先只按一个标准分组项目.这里使用 _.groupBy<按名称的第一个字母对 phones 进行分组/code>_.first:

Group the elements of an array by some criterion

Instead of sorting directly, we might also first only group the items by a criterion. Here's grouping the phones by the first letter of the name, using _.groupBy and _.first:

_.groupBy(phones, function(phone) { return _.first(phone.name); })
// { S: [ { name: 'Samsung A10s', id: 845 },
//        { name: 'Samsung galaxy', id: 839 },
//        { name: 'Samsung S20s ultra', id: 514 } ],
//   N: [ { name: 'Nokia 7', id: 814 } ],
//   A: [ { name: 'Apple iphone ultra', id: 159 } ],
//   L: [ { name: 'LG S20', id: 854 } ] }

我们已经看到,我们可以传递键来排序或分组,或者传递一个返回值作为标准的函数.我们可以在这里使用第三个选项来代替上面的函数:

We have seen that we can pass keys to sort or group by, or a function that returns something to use as a criterion. There is a third option which we can use here instead of the function above:

_.groupBy(phones, ['name', 0])
// { S: [ { name: 'Samsung A10s', id: 845 },
//        { name: 'Samsung galaxy', id: 839 },
//        { name: 'Samsung S20s ultra', id: 514 } ],
//   N: [ { name: 'Nokia 7', id: 814 } ],
//   A: [ { name: 'Apple iphone ultra', id: 159 } ],
//   L: [ { name: 'LG S20', id: 854 } ] }

获取对象的键

这就是 _.keys 的用途:

_.keys({name: "Samsung A10s", id: 845}) // [ 'name', 'id' ]

您也可以使用标准的Object.keys._.keysObject.keys 不适用的旧环境中工作.否则,它们可以互换.

You can also do this with the standard Object.keys. _.keys works in old environments where Object.keys doesn't. Otherwise, they are interchangeable.

我们之前已经看到使用 _.map 来获取多个单词数组的长度.通常,它需要一个数组或对象以及您希望对该数组或对象的每个元素执行的操作,并且它将返回一个包含结果的数组:

We have previously seen the use of _.map to get the lengths of multiple arrays of words. In general, it takes an array or object and something that you want to be done with each element of that array or object, and it will return an array with the results:

_.map(phones, 'id')
// [ 845, 839, 814, 514, 159, 854 ]
_.map(phones, ['name', 0])
// [ 'S', 'S', 'N', 'S', 'A', 'L' ]
_.map(phones, function(phone) { return _.last(phone.name); })
// [ 's', 'y', '7', 'a', 'a', '0' ]

注意与 _.sortBy_.groupBy 的相似性.这是 Underscore 的一般模式:你有一个东西的集合,你想对每个元素做一些事情,以获得某种结果.您想要对每个元素进行的操作称为迭代器".Underscore 有一个函数,可确保您可以在与 iteratee 一起使用的所有函数中使用相同的 iteratee 速记:_.迭代对象.

Note the similarity with _.sortBy and _.groupBy. This is a general pattern in Underscore: you have a collection of something and you want to do something with each element, in order to arrive at some sort of result. The thing you want to do with each element is called the "iteratee". Underscore has a function that ensures you can use the same iteratee shorthands in all functions that work with an iteratee: _.iteratee.

有时您可能想对集合的每个元素做一些事情,并以不同于 _.map_.sortBy 和其他 Underscore 函数已经做到了.在这种情况下,您可以使用_.reduce,这是最通用的功能商场.例如,我们可以通过以下方式创建电话名称的混合,方法是取第一个电话名称的第一个字母,第二个电话名称的第二个字母,依此类推:

Sometimes you may want to do something with each element of a collection and combine the results in a way that is different from what _.map, _.sortBy and the other Underscore functions already do. In this case, you can use _.reduce, the most general function of them all. For example, here's how we can create a mixture of the names of the phones, by taking the first letter of the name of the first phone, the second letter of the name of the second phone, and so forth:

_.reduce(phones, function(memo, phone, index) {
    return memo + phone.name[index];
}, '')
// 'Sakse0'

我们传递给 _.reduce 的函数会为每部手机调用.memo 是我们迄今为止构建的结果.该函数的结果用作我们处理的下一部手机的新 memo.通过这种方式,我们一次构建一个电话._.reduce 的最后一个参数,在这种情况下,'' 设置了 memo 的初始值,所以我们有一些开始.

The function that we pass to _.reduce is invoked for each phone. memo is the result that we've built so far. The result of the function is used as the new memo for the next phone that we process. In this way, we build our string one phone at a time. The last argument to _.reduce, '' in this case, sets the initial value of memo so we have something to start with.

为此,我们有 _.flatten:

For this we have _.flatten:

_.flatten([
    [ 'Samsung', 'A10s' ],
    [ 'Samsung', 'galaxy' ],
    [ 'Nokia', '7' ],
    [ 'Samsung', 'S20s', 'ultra' ],
    [ 'Apple', 'iphone', 'ultra' ],
    [ 'LG', 'S20' ]
])
// [ 'Samsung', 'A10s', 'Samsung', 'galaxy', 'Nokia', '7',
//   'Samsung', 'S20s', 'ultra', 'Apple', 'iphone', 'ultra',
//   'LG', 'S20' ]

把它们放在一起

我们有一个音素数组和一个搜索字符串,我们想以某种方式将这些音素中的每一个与搜索字符串进行比较,最后我们想要组合结果,以便通过相关性获得音素.让我们从中间部分开始.

Putting it all together

We have an array of phones and a search string, we want to somehow compare each of those phones to the search string, and finally we want to combine the results of that so we get the phones by relevance. Let's start with the middle part.

这些电话中的每一个"都可以吗?按门铃?我们正在创建一个迭代器!我们希望它以一个电话作为参数,并且我们希望它返回它的 name 与搜索字符串共有的单词数.这个函数会这样做:

Does "each of those phones" ring a bell? We are creating an iteratee! We want it to take a phone as its argument, and we want it to return the number of words that its name has in common with the search string. This function will do that:

function relevance(phone) {
    return _.intersection(phone.name.split(' '), searchTerms).length;
}

这假设在 relevance 函数之外定义了一个 searchTerms 变量.它必须是一个包含搜索字符串中单词的数组.我们稍后会处理这个问题;让我们首先解决如何组合我们的结果.

This assumes that there is a searchTerms variable defined outside of the relevance function. It has to be an array with the words in the search string. We'll deal with this in a moment; let's address how to combine our results first.

虽然有很多可能的方法,但我认为下面的方法非常优雅.我首先按相关性对电话进行分组,

While there are many ways possible, I think the following is quite elegant. I start with grouping the phones by relevance,

_.groupBy(phones, relevance)

但我想省略与搜索字符串共有零个单词的电话组:

but I want to omit the group of phones that have zero words in common with the search string:

var groups = _.omit(_.groupBy(phones, relevance), '0');

请注意,我省略了 string'0',而不是 number0,因为_.groupBy的结果是一个对象,而对象的key总是字符串.

Note that I'm omitting the string key '0', not the number key 0, because the result of _.groupBy is an object, and the keys of an object are always strings.

现在我们需要按照匹配单词的数量对剩余的groups进行排序.我们通过获取我们的groups,

Now we need to order the remaining groups by the number of matching words. We know the number of matching words for each group by taking the keys of our groups,

_.keys(groups)

我们可以先对这些升序排序,但我们必须注意将它们转换回数字,以便我们将2排在10(数字比较)之前'2'之前的'10'(字典序比较):

and we can sort these ascending first, but we must take care to cast them back to numbers, so that we will sort 2 before 10 (numerical comparison) instead of '10' before '2' (lexicographical comparison):

_.sortBy(_.keys(groups), Number)

然后我们可以将其颠倒以达到我们小组的最终顺序.

then we can reverse this in order to arrive at the final order of our groups.

var tiers = _.sortBy(_.keys(groups), Number).reverse();

现在我们只需要将这个排序的键数组转换成一个包含实际电话组的数组.为此,我们可以使用 _.map_.propertyOf:

Now we just need to transform this sorted array of keys into an array with the actual groups of phones. To do this, we can use _.map and _.propertyOf:

_.map(tiers, _.propertyOf(groups))

最后,我们只需要将其展平为一个大数组,以便按相关性获得我们的搜索结果.

Finally, we only need to flatten this into one big array, in order to have our search results by relevance.

_.flatten(_.map(tiers, _.propertyOf(groups)))

让我们将所有这些都包装到我们的 searchByRelevance 函数中.请记住,我们仍然需要在 relevance 迭代对象之外定义 searchTerms:

Let's wrap all of this up into our searchByRelevance function. Remember that we still needed to define searchTerms outside of our relevance iteratee:

function searchByRelevance(phones, searchString) {
    var searchTerms = searchString.split(' ');
    function relevance(phone) {
        return _.intersection(phone.name.split(' '), searchTerms).length;
    }
    var groups = _.omit(_.groupBy(phones, relevance), '0');
    var tiers = _.sortBy(_.keys(groups), Number).reverse();
    return _.flatten(_.map(tiers, _.propertyOf(groups)));
}

现在开始测试!

searchByRelevance(phones, 'Samsung galaxy A20s ultra')
// [ { name: 'Samsung galaxy', id: 839 },
//   { name: 'Samsung S20s ultra', id: 514 },
//   { name: 'Samsung A10s', id: 845 },
//   { name: 'Apple iphone ultra', id: 159 } ]

什么是最好的"?

如果你衡量善"按代码行数,代码越少越好.我们只用了八行代码就实现了上面的 searchByRelevance,所以看起来还不错.

然而,它有点密集.如果我们使用链接可读性会有所提高>:

It is, however, a bit dense. The number of lines increases, but the readability improves a bit, if we use chaining:

function searchByRelevance(phones, searchString) {
    var searchTerms = searchString.split(' ');
    function relevance(phone) {
        return _.intersection(phone.name.split(' '), searchTerms).length;
    }
    var groups = _.chain(phones)
        .groupBy(relevance)
        .omit('0');
    return groups.keys()
        .sortBy(Number)
        .reverse()
        .map(_.propertyOf(groups.value()))
        .flatten()
        .value();
}

善良"的另一个维度是性能.searchByRelevance 可以更快吗?为了理解这一点,我们通常采用最小和最频繁的操作,并计算对于给定大小的输入执行该操作的频率.

Yet another dimension of "goodness" is performance. Could searchByRelevance be faster? To get a sense of this, we usually take the smallest and most frequent operation, and we calculate how often we'll be executing that operation for a given size of input.

我们将在 searchByRelevance 中做的主要事情是比较单词.这不是最小的操作,因为比较单词由比较字母组成,但是因为英语中的单词往往很短,我们现在可以假设比较两个单词是我们最小且执行次数最多的操作.这使计算更容易一些.

The main thing we'll be doing a lot in searchByRelevance, is comparing words. This is not the smallest operation, because comparing words consists of comparing letters, but because words in English tend to be short, we can pretend for now that comparing two words is our smallest and most executed operation. This makes the calculations a bit easier.

对于每部手机,我们都会将其名称中的每个词与搜索字符串中的每个词进行比较.如果我们有 100 个电话,平均电话名称有 3 个词,搜索字符串有 5 个词,那么我们将进行 100 * 3 * 5 = 1500 个词的比较.

For each phone, we will be comparing each word in its name with each word in our search string. If we have 100 phones, and the average phone name has 3 words, and the search string has 5 words, then we will be making 100 * 3 * 5 = 1500 word comparisons.

计算机速度很快,所以 1500 不算什么.通常,如果您执行最小步骤的次数保持在 100000 (100k) 以下,您可能甚至不会注意到延迟,除非最小步骤非常昂贵.

Computers are fast, so 1500 is nothing. Generally, if the number of times you execute your smallest step remains under 100000 (100k), you probably won't even notice a delay unless that smallest step is very expensive.

然而,随着输入量的增加,单词比较的数量将呈爆炸性增长.如果我们有 20000 (20k) 个电话,平均名称中有 5 个单词,搜索字符串为 10 个单词,那么我们已经在进行一百万个单词的比较.这可能意味着在结果出来之前盯着你的屏幕几秒钟.

However, the number of word comparisons will grow quite explosively with larger inputs. If we have 20000 (20k) phones, 5 words in the average name and a search string of 10 words, we are already making a million word comparisons. That could mean staring at your screen for a few seconds before the results come in.

我们是否可以编写一个 searchByRelevance 的变体,它可以在眨眼间搜索 2 万部长名称手机?是的,事实上我们也可以做一百万甚至更多!我不会逐行详细介绍,但我们可以通过使用适当的查找结构获得更快的速度:

Can we write a variant of searchByRelevance that can search 20k phones with long names in an eyeblink? Yes, and in fact we can probably also do a million or more! I won't go into the details line by line, but we can get much better speed by using appropriate lookup structures:

// lookup table by word in the name
function createIndex(phones) {
    return _.reduce(phones, function(lookup, phone) {
        _.each(phone.name.split(' '), function(word) {
            var matchingPhones = (lookup[word] || []);
            matchingPhones.push(phone.id);
            lookup[word] = matchingPhones;
        });
        return lookup;
    }, {});
}

// search using lookup tables
function searchByRelevance(phonesById, idsByWord, searchString) {
    var groups = _.chain(searchString.split(' '))
        .map(_.propertyOf(idsByWord))
        .compact()
        .flatten()
        .countBy()
        .pairs()
        .groupBy('1');
    return groups.keys()
        .sortBy(Number)
        .reverse()
        .map(_.propertyOf(groups.value()))
        .flatten(true) // only one level of flattening
        .map('0')
        .map(_.propertyOf(phonesById))
        .value();
}

为了使用它,我们创建了一次查找表,然后在每次搜索时重复使用它们.仅当手机的 JSON 数据发生变化时,我们才需要重新创建查找表.

To use this, we create the lookup tables once, then reuse them for each search. We need to recreate the lookup tables only if the JSON data of phones change.

var phonesById = _.indexBy(phones);
var idsByWord = createIndex(phones);

searchByRelevance(phonesById, idsByWord, 'Samsung galaxy A20s ultra')
// [ { name: 'Samsung galaxy', id: 839 },
//   { name: 'Samsung S20s ultra', id: 514 },
//   { name: 'Samsung A10s', id: 845 },
//   { name: 'Apple iphone ultra', id: 159 } ]
searchByRelevance(phonesById, idsByWord, 'Apple')
// [ { name: 'Apple iphone ultra', id: 159 } ]

要了解这有多快,让我们再次计算最小的操作.在createIndex 中,最小最频繁的操作是存储单词和手机id 之间的关联.我们为每部电话、其名称中的每个单词执行一次此操作.在 searchByRelevance 中,最小最频繁的操作是在 countBy 步骤中增加给定电话的相关性.我们对搜索字符串中的每个单词、与该单词匹配的每个电话都执行一次此操作.

To appreciate how much faster this is, let's count the smallest operations again. In createIndex, the smallest most frequent operation is storing an association between a word and the id of a phone. We do this once for each phone, for each word in its name. In searchByRelevance, the smallest most frequent operation is incrementing the relevance of a given phone in the countBy step. We do this once for each word in the search string, for each phone that matches that word.

如果我们做出一些合理的假设,我们可以估计给定搜索字符串的匹配音素数.手机名称中出现频率最高的词可能是品牌,例如Samsung";和苹果".由于至少有十个品牌,我们可以假设与给定搜索词匹配的手机数量通常不到手机总数的 10%.因此,执行一次搜索所需的时间是搜索字符串中的单词数乘以电话数乘以 10%(即除以 10).

We can estimate the number of matching phones for a given search string if we make some reasonable assumptions. The most frequent words in the phone names are probably the brands, such as "Samsung" and "Apple". Since there are at least ten brands, we can assume that the number of phones that match a given search term is generally less than 10% of the total number of phones. So the time it takes to execute one search is the number of words in the search string, times the number of phones, times 10% (i.e., divided by 10).

因此,如果我们有 100 个电话,名称中平均包含 3 个单词,那么索引需要 100 * 3 = 300 次将关联存储在 idsByWord 查找表中.使用搜索字符串中的 5 个单词执行搜索只需要 5 * 100 * 10% = 50 个相关性增量.这已经比我们在没有查找表的情况下需要的 1500 个单词的比较快得多,尽管在这种情况下计算机背后的人不会注意到差异.

So if we have 100 phones with on average 3 words in the name, then indexing takes 100 * 3 = 300 times storing an association in the idsByWord lookup table. Performing a search with 5 words in the search string takes only 5 * 100 * 10% = 50 relevance increments. This is already much faster than the 1500 word comparisons we needed without lookup tables, although the human behind the computer will not notice the difference in this case.

具有查找表的方法的速度优势随着输入量的增加而进一步增加:

The speed advantage of the approach with the lookup table further increases with larger inputs:

┌───────────────────┬───────┬────────┬───────┐
│ Problem size      │ Small │ Medium │ Large │
├───────────────────┼───────┼────────┼───────┤
│ phones            │   100 │    20k │    1M │
│ words per name    │     3 │      5 │     8 │
│ search terms      │     5 │     10 │    15 │
├───────────────────┼───────┼────────┼───────┤
│ w/o lookup tables │       │        │       │
│ word comparisons  │  1500 │     1M │  120M │
├───────────────────┼───────┼────────┼───────┤
│ w/ lookup tables  │       │        │       │
│ associations      │   300 │   100k │    8M │
│ increments        │    50 │    20k │  1.5M │
└───────────────────┴───────┴────────┴───────┘

事实上,这仍然低估了速度优势,因为随着手机数量的增加,与给定搜索词匹配的手机百分比可能会下降.

This is, in fact, still underestimating the speed advantage, since the percentage of phones that match a given search term is likely to drop as the number of phones increases.

查找表使搜索速度更快.但它是更好吗?正如我之前所说,对于小问题,速度差异不会很明显.查找表的一个缺点是它需要更多的代码,这使得它更难理解,并且需要花费更多的精力来维护.它还需要一个与关联数量一样大的查找表,这意味着我们将使用比以前更多的额外内存.

Lookup tables make searching much faster. But is it better? As I said before, for small problem sizes, the speed difference will not be noticable. A disadvantage of the lookup tables is that this requires more code, which makes it a bit harder understand, as well as taking more effort to maintain. It also requires a lookup table as large as the number of associations, which means we will be using much more additional memory than before.

总而言之,什么是最好的"?始终取决于不同约束之间的权衡,例如代码大小、速度和内存使用情况.由您决定如何权衡这些约束相互之间的关系.

To conclude, what is "best" always depends on a tradeoff between different constraints, such as code size, speed and memory usage. It is up to you to decide how you want to weigh these constraints relative to each other.

这篇关于通过 underscore.js 中的另一个数组过滤 json 数据的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆