同位素过滤、排序、搜索和 Url 哈希

Isotope Filter, Sort, Search and Url Hash

我正在使用 Isotope 对产品页面进行排序、过滤和搜索。到目前为止,一切都很好。 我被困在几个方面。我无法在网上找到包含我需要的功能的确切组合的示例,因此需要一些帮助。

简而言之,我有多个 select 下拉菜单过滤产品、select 价格排序顺序和一个快速搜索输入字段。所有的工作,但我有一些需要修改。

待办事项:

过滤后搜索不起作用。我需要它来使用过滤器。

在价格 select 下拉列表中添加按 'Sale' 和 'New In' 排序。

添加 URL 哈希侦听器以创建 links 用于过滤,即 link 到 New In 首先排序。

Isotope website

Select Example

URL Hash example

Quick search example

我目前的过滤排序JS:

        $(document).ready(function(){

// quick search regex
var qsRegex;
var filterValue;
// init Isotope
var $grid = $(".grid").isotope({
  itemSelector: ".grid-item",
  layoutMode: "fitRows",
  getSortData: {
    price: '.t-price parseInt',
    category: '[data-category]',
  },
  filter: function() {
    var $this = $(this);
    var searchResult = qsRegex ? $this.text().match(qsRegex) : true;
    var selectResult = filterValue ? $this.is(filterValue) : true;
    return searchResult  && selectResult;
  }
});

      // bind filter on select change
//$(".filter-select").on("change", function() {
       // get filter value from option value
 // filterValue = $(this).val();
  //console.log(filterValue);
  //$grid.isotope();
//});


// store filter for each group
var filters = {};

$('.filter-select').on( 'change', function( event ) {
  var $select = $( event.target );
  // get group key
  var filterGroup = $select.attr('value-group');
  // set filter for group
  filters[ filterGroup ] = event.target.value;
  // combine filters
  var filterValue = concatValues( filters );
  // set filter for Isotope
  $grid.isotope({ filter: filterValue });
});

// flatten object by concatting values
function concatValues( obj ) {
  var value = '';
  for ( var prop in obj ) {
    value += obj[ prop ];
  }
  return value;
}



$('#price-sort').on( 'change', function() {
          var type = $(this).find(':selected').attr('data-sorttype');
          console.log(type);
    var sortValue = this.value;
      if(type=='ass'){$grid.isotope({ sortBy: sortValue , sortAscending: false});}
          else{$grid.isotope({ sortBy: sortValue , sortAscending: true});}
   $grid.isotope({ sortBy: sortValue });
  });


  // change is-checked class on buttons
  $('#price-sort').on( 'change', function() {
    var sortByValue = this.value;
      console.log(sortByValue);
    $grid.isotope({ sortBy: sortByValue});
  });


// use value of search field to filter
var $quicksearch = $(".quicksearch").keyup(
  debounce(function() {
    qsRegex = new RegExp($quicksearch.val(), "gi");
    $grid.isotope();
  })
);
// debounce so filtering doesn't happen every millisecond
function debounce(fn, threshold) {
  var timeout;
  return function debounced() {
    if (timeout) {
      clearTimeout(timeout);
    }
    function delayed() {
      fn();
      timeout = null;
    }
    setTimeout(delayed, threshold || 100);
  };
}


  });

HTML:

    <div id="sort-filter">
        <div id="sort">
                        <select id="price-sort" class="select-css form-control long">
                <option selected disabled class="s-title"> Sort </option>
                <option data-sorttype="dec" value="price">£ Low To High</option>
                <option data-sorttype="ass" value="price">£ High To Low</option>

            </select>

        </div>
        <div class="filters">
                    <select class="filter-select select-css short" value-group="sizes" id="sizes">
                    <option selected disabled class="s-title"> Size </option>
                      <option value="*">All</option>
                      <option value=".XS">XS</option>
                      <option value=".S">S</option>
                      <option value=".M">M</option>
                      <option value=".L">L</option>
                      <option value=".XL">XL</option>
                      <option value=".XXL">XXL</option>
                    </select>
    </div>
    </div>

<div class="container">

    <ul class="grid cs-style-3">
        <div class="grid-sizer"></div>

            <li class="grid-item XS Male Beige Bags Mint">
                <a href="link" class="animsition-link" data-animsition-out-class="fade-out-left-lg">
                    <figure style="background-image: URL(image.jpg);">
                        <img src="/image2.jpg" alt="hat sale item">
                </figure>
                <div id="pro-deets">
                <h3>hat sale item</h3>
                        <span id="price" class="holderpage">
                            £<span class="price t-price">3</span>

                        </span>
                </div></a>
            </li>

            <li class="grid-item L Female Brown Tops Worn">
                <a href="link" class="animsition-link" data-animsition-out-class="fade-out-left-lg">
                    <figure style="background-image: URL(image.jpg);">
                        <img src="/image2.jpg" alt="product no sale no new">
                </figure>
                <div id="pro-deets">
                <h3>product no sale no new</h3>
                        <span id="price" class="holderpage">
                            £<span class="price t-price">40</span>

                        </span>
                </div></a>
            </li>

            <li class="grid-item L Female Brown Tops Worn New" data-category="New">
                <a href="link" class="animsition-link" data-animsition-out-class="fade-out-left-lg">
                    <figure style="background-image: URL(image.jpg);">
                        <img src="/image2.jpg" alt="Skirt">
                </figure>
                <div id="pro-deets">
                <h3>Skirt</h3>
                        <span id="price" class="holderpage">
                            £<span class="price t-price">10</span>

                        </span>
                </div></a>
            </li>

            <li class="grid-item XS Male Beige Bags Mint Sale" data-category="Sale">
                <a href="link" class="animsition-link" data-animsition-out-class="fade-out-left-lg">
                    <figure style="background-image: URL(image.jpg);">
                        <img src="/image2.jpg" alt="Jacket">
                </figure>
                <div id="pro-deets">
                <h3>Jacket</h3>
                        <span id="price" class="holderpage">
                            £<span class="price sale">30</span>
                            <span class="price">£<span class="t-price">20</span></span>
                        </span>
                </div></a>
            </li>

        </ul>
        </div>  

请注意,这是一个分为两部分的答案。从不同的答案中检查第 2 部分。

答案:第 1 部分

当我阅读你的代码时,你走在了正确的轨道上。由于搜索和过滤在您的代码中不能一起工作的原因,问题在于,当您初始化 $grid 时,您为 $grid 定义了一个过滤函数。但是,当 select 过滤器组发生变化时,您可以通过调用 $grid.isotope({ filter: filterValue }) 重新定义该过滤器。当您使用任何可配置值调用 $grid.isotope() 时,$grid 将采用这些新配置。

因此,你问题的答案就是有两个变量。一个用于存储过滤值,一个用于存储搜索值。每当调用 $grid.isotope() 时,它只会使用这两个值进行过滤。

您的代码还有其他一些问题。您不需要进行两次价格排序。那只需要做一个。当涉及到 HTML、类 和 id 时,一个 id 应该只在一个页面中出现一个。这意味着,您不能有两个具有相同 ID 的部门。如果它是不重要的,它可能不会破坏你的页面。但是,在以编程方式操作页面时,这可能会破坏您的页面。除此之外,您使用 filter-select 的方式是为了从两个按钮组中获取值。但它可能适用于您的情况,我想您将来可能会需要它,因为除了尺寸之外,可能还会有颜色等……此外,在比较 JS 中的字符串以进行价格排序时。最好用 === 比较字符串是否相等。 JS中比较字符串可以参考这个Which equals operator (== vs ===) should be used in JavaScript comparisons?.

对于代码设计,你可以这样做。我认为将所有内容放入 document.ready() 的方式将使代码 运行 更快。

对于答案代码,套路很简单。当文档准备就绪时,所有与搜索字段和 select 字段关联的事件都会被初始化。之后, filterWithHash() 函数绑定到 onhashchange 事件。然后执行该函数以初始化网格,同时检查 URL 是否有任何关联哈希。

对于 URL 中的哈希,尝试 "filter=" 和 "search="。它们可以毫无问题地一起使用。您还可以将该函数转换为不仅可以采用散列,还可以采用 get 参数。

代码中还有一些注释可能会对您有所帮助。

<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<script src="https://unpkg.com/isotope-layout@3/dist/isotope.pkgd.js"></script>

<script>
$(document).ready(function(){
// Quick Search Regex
var qsRegex;
// Filter Value
var filterValue;
// Grid not initialize yet.
var $grid; 

// Since there is only one filter group and that is sizes. 
// It isn't necessary to be done like this. 
// Just grab the selected value everytime the sizes select is changed.
// However, this was still left like this.
$('.filter-select').on( 'change', function( event ) {
  // Hold the filter values here.
  var filters = {};
  var $select = $( event.target );
  // Get group key
  var filterGroup = $select.attr('value-group');
  // Set filter for group
  filters[ filterGroup ] = event.target.value;
  // Assign the filter value to the global filterValue
  filterValue = concatValues( filters );
  // Execute $grid.isotope() to update with current global filter value.
  $grid.isotope();
});

// flatten object by concatting values
function concatValues( obj ) {
  var value = '';
  for ( var prop in obj ) {
    value += obj[ prop ];
  }
  return value;
}

// change is-checked class on buttons
// Only need one price-sort not two
$('#price-sort').on( 'change', function() {
    var type = $(this).find(':selected').attr('data-sorttype');
    // REMEMBER TO TAKE THE CONSOLE LOG OUT IN PRODUCTION
    console.log(type);
    var sortValue = this.value;
    if( type === 'asc' ) $grid.isotope({ sortBy: sortValue , sortAscending: false});
    else $grid.isotope({ sortBy: sortValue , sortAscending: true});
    $grid.isotope({ sortBy: sortValue });
});

// use value of search field to filter
var $quicksearch = $("#quicksearch").keyup(
  debounce(function() {
    qsRegex = new RegExp($quicksearch.val(), "gi");
    // Every time qsRegex is update do $grid.isotope() to update
    // The filter with global filterValue and qsRegex
    $grid.isotope();
  })
);
// debounce so filtering doesn't happen every millisecond
function debounce(fn, threshold) {
  var timeout;
  return function debounced() {
    if (timeout) {
      clearTimeout(timeout);
    }
    function delayed() {
      fn();
      timeout = null;
    }
    setTimeout(delayed, threshold || 100);
  };
}

function getHashFilter() {
  // get filter=filterName
  var matches = location.hash.match( /filter=([^&]+)/i );
  var hashFilter = matches && matches[1];
  return hashFilter && decodeURIComponent( hashFilter );
}

function getSearchFilter() {
  // get search=filterName
  var matches = location.hash.match( /search=([^&]+)/i );
  var searchFilter = matches && matches[1];
  return searchFilter && decodeURIComponent( searchFilter );
}

/*
 * This function below can be customize to utilize not just only hashes
 * but also "Get Requests"
 */
function filterWithHash() {
  var hashFilter = getHashFilter();
  var searchFilter = getSearchFilter();
  if ( !searchFilter && !hashFilter && $grid ) {
    return;
  }
  
  // If hashFilter is there, utilize it.
  if ( hashFilter ) {
      var selectValue = $('select[id="sizes"]').find('option[value="'+ hashFilter +'"]');
      // Only search for a value if it is found within the select fields, else disregard it.
      if ( selectValue.length > 0 ){
          selectValue.prop('selected', 'selected');
          filterValue = hashFilter;
      }
  }
  // If searhFilter is there, utilize it.
  if ( searchFilter) {
    $('#quicksearch').val(searchFilter);
    qsRegex = new RegExp(searchFilter, "gi");
  }
  
  /* If $grid is not initialize, it will get initialize. 
   * This will only happen on first run.
   * One grid is initilized, everytime grid.isotope() is run
   * without any value, grid will be updated to what initilized below.
   * Thus, each later run of isotope(), the filter will look at both,
   * the searchResult and the qsRegex if they are available.
  */
  if ( !$grid ) {
    $grid = $(".grid").isotope({
              itemSelector: ".grid-item",
              layoutMode: "fitRows",
              getSortData: {
                price: '.t-price parseInt',
                category: '[data-category]',
              },
              filter: function() {
                var $this = $(this);
                var searchResult = qsRegex ? $this.text().match(qsRegex) : true;
                var selectResult = filterValue ? $this.is(filterValue) : true;
                return searchResult && selectResult;
              }
            });
  } else $grid.isotope();
}
/* 
 * Trigger filter with hash to initialize grid
 * and also to check the url for hash.
 */
filterWithHash();

// Bind the filterWithHash function to the hashchange event.
$(window).on( 'hashchange', filterWithHash );

});
</script>
</head>
<body>
<p><input type="text" id="quicksearch" placeholder="Search" /></p>

<div id="sort-filter">
  <!-- Short Div -->
  <div id="sort">
    <select id="price-sort" class="select-css form-control long">
      <option selected disabled class="s-title"> Sort </option>
      <option data-sorttype="des" value="price">£ Low To High</option>
      <option data-sorttype="asc" value="price">£ High To Low</option>
    </select>
  </div>

  <!-- Filter Div -->
  <div class="filters">
    <select class="filter-select select-css short" value-group="sizes" id="sizes">
      <option selected disabled class="s-title"> Size </option>
      <option value="*">All</option>
      <option value=".XS">XS</option>
      <option value=".S">S</option>
      <option value=".M">M</option>
      <option value=".L">L</option>
      <option value=".XL">XL</option>
      <option value=".XXL">XXL</option>
    </select>
  </div>
</div>

<div class="container">
  <ul class="grid cs-style-3">
    <div class="grid-sizer"></div>
    
      <li class="grid-item XS Male Beige Bags Mint">
        <a href="link" class="animsition-link" data-animsition-out-class="fade-out-left-lg">
          <figure style="background-image: URL(image.jpg);">
            <img src="/image2.jpg" alt="hat sale item">
          </figure>
          <div id="pro-deets"> <!-- This should not be id -->
            <h3>hat sale item</h3>
            <span id="price" class="holderpage"> <!-- This should not be id -->
              £<span class="price t-price">3</span>
            </span>
          </div>
        </a>
      </li>

      <li class="grid-item L Female Brown Tops Worn">
        <a href="link" class="animsition-link" data-animsition-out-class="fade-out-left-lg">
          <figure style="background-image: URL(image.jpg);">
            <img src="/image2.jpg" alt="product no sale no new">
          </figure>
          <div id="pro-deets">
            <h3>product no sale no new</h3>
            <span id="price" class="holderpage">
              £<span class="price t-price">40</span>
            </span>
          </div>
        </a>
      </li>

      <li class="grid-item L Female Brown Tops Worn New" data-category="New">
        <a href="link" class="animsition-link" data-animsition-out-class="fade-out-left-lg">
          <figure style="background-image: URL(image.jpg);">
            <img src="/image2.jpg" alt="Skirt">
          </figure>
          <div id="pro-deets">
            <h3>Skirt</h3>
            <span id="price" class="holderpage">
              £<span class="price t-price">10</span>
            </span>
          </div>
        </a>
      </li>

      <li class="grid-item XS Male Beige Bags Mint Sale" data-category="Sale">
        <a href="link" class="animsition-link" data-animsition-out-class="fade-out-left-lg">
          <figure style="background-image: URL(image.jpg);">
            <img src="/image2.jpg" alt="Jacket">
          </figure>
          <div id="pro-deets">
            <h3>Jacket</h3>
            <span id="price" class="holderpage">
              £<span class="price sale">30</span>
              <span class="price">£<span class="t-price">20</span></span>
            </span>
          </div>
        </a>
      </li>

  </ul>
</div>
</body>
</html>

第二个例子

我限制在 30000 个字符以内。因此,我从示例 2 中删除了 HTML 部分。只需将示例 1 的 JS 替换为示例 2 中的 JS 到 运行 示例 2.

第二个例子,它的套路和第一个例子差不多。对于第二个示例,每当用户 select 关联到网格过滤操作的任何字段时,selected 值将应用于网格。之后,这些值将应用于 location.hash。防止 filterWithHash() 到 运行 并解释刚刚创建的哈希。 setHash() 函数将名为 gridAlreadyUpdated 的变量设置为 true 以告诉 filterWithHash() 不需要更新任何内容。

setHash() 函数将只解释与过滤操作关联的散列参数。忽略其他哈希值。

我在代码中还写了一些其他注释,可能会对您有所帮助。

"use strict";
$(document).ready(function(){
// Quick Search Regex
var qsRegex;
// Filter Value
var filterValue;
// sortValue & whether to sortAscending
var sortValue;
var sortAscending;
// Grid not initialize yet.
var $grid; 
// Last state of all the filters
var lastState = {};

/*
 * Parameter name for quicksearch, filter, and sort
 * Have this here so everything can easily be changed in one place.
 *
 */
var quicksearchParamName = "search";
var filterParamName = "filter";
var sortValueParamName = "sort";
var sortTypeParamName = "sorttype";


/*
 * Regexes for grabbing values from hash parameter.
 *
 */
var quicksearchRegex = RegExp(quicksearchParamName + '=([^&]+)', 'i');
var filterRegex = RegExp(filterParamName + '=([^&]+)' , 'i');
var sortValueRegex = RegExp(sortValueParamName + '=([^&]+)' , 'i');
var sortTypeRegex = RegExp(sortTypeParamName + '=([^&]+)' , 'i');

/* 
 * This variable is for the setHash() function to communicate with
 * the filterWithHash() function. 
 *
 * There isn't a need to build a hash string, update everything, and then
 * reinterprete that same hash string right after.
 * 
 * Thus, there isn't a need to run setHash() and then let filterWithHash()
 * run on hash update.
 */
var gridAlreadyUpdated = false;

// use value of search field to filter
var $quicksearch = $("#quicksearch").keyup(
  debounce(function() {
    setHash(1);
  })
);
// debounce so filtering doesn't happen every millisecond
function debounce(fn, threshold) {
  var timeout;
  return function debounced() {
    if (timeout) {
      clearTimeout(timeout);
    }
    function delayed() {
      fn();
      timeout = null;
    }
    setTimeout(delayed, threshold || 100);
  };
}

/*
 * Since there is only one filter group and that is sizes. 
 * It isn't necessary to be done like this. 
 * Just grab the selected value everytime the sizes select is changed.
 * However, this was still left like this.
 *
 */
$('.filter-select').on( 'change', function( event ) {
  // Hold the filter values here.
  var filters = {};
  var $select = $( event.target );
  // Get group key
  var filterGroup = $select.attr('value-group');
  // Set filter for group
  filters[ filterGroup ] = event.target.value;
  // Assign the filter value to the global filterValue
  filterValue = concatValues( filters );
  // Execute $grid.isotope() to update with current global filter value.
  setHash(2);
});

// flatten object by concatting values
function concatValues( obj ) {
  var value = '';
  for ( var prop in obj ) {
    value += obj[ prop ];
  }
  return value;
}

/*
 * change is-checked class on buttons
 * Only need one price-sort not two
 *
 */
$('#price-sort').on( 'change', function() {
    setHash(3, this);
});

function getHashFilter() {
  // get filter=filterName
  var matches = location.hash.match( filterRegex );
  var hashFilter = matches && matches[1];
  return hashFilter && decodeURIComponent( hashFilter );
}

function getSearchFilter() {
  // get search=filterName
  var matches = location.hash.match( quicksearchRegex );
  var searchFilter = matches && matches[1];
  return searchFilter && decodeURIComponent( searchFilter );
}

/*
 * Get the sort param. This function will always return an array with
 * 2 indexes. If both sortValue and sortType is found then it will return
 * the values for both. Value is index 1, and type is index 2. 
 * 
 * For everything else, this function will return [null, null].
 */ 
function getSortParam() {
  var valueMatches = location.hash.match( sortValueRegex );
  var typeMatches = location.hash.match( sortTypeRegex );
  var v = valueMatches && valueMatches[1];
  var t = typeMatches && typeMatches[1];
  
  if ( v && t ) return [decodeURIComponent( v ), decodeURIComponent( t )];
  return [ null, null ];
}

/*
 * This function will set the hash when one of the filtering field is 
 * changed. 
 *
 * Parameter whocall is utilize to know who is the caller. There can only
 * be one caller at a time. Whocall is utilize as int because comparing 
 * int is much faster than comparing string.
 *
 * whocall(1) = quicksearch
 * whocall(2) = filter
 * whocall(3) = sorting
 * 
 * In a secure environment any other whocall besides the 3 above should 
 * generate an error.
 */
function setHash( whocall, obj ){
  var hashes = {};
  var hashStr = "";
  /* 
   * Regex can also be utilized here to change the hash string, but for
   * example, I thought this method look more clear.
   * 
   * For performance, I haven't tested regEx vs this, so I don't know.
   * Other method are available like URLSearchParams etc... but those method
   * might not be supported by all browser.
   *
   */
  if ( location.hash ){
      /* 
       * forEach can be uitlized here, but this provide better cross platform
       * compatibitliy.
       *
       */
    let temp = location.hash.substr(1).split("&");
    for ( let i = 0; i < temp.length; i++ ){
      let param = temp[i].split("=");
      // if param[0] is 0 length that is an invalid look something like &=abc.
      if ( param[0].length === 0 ) continue;
      /*
       * if more than > 2 that is also invalid but just grab the first one anyway.
       * if exactly 1 that is something like &filter=&somethingelse. So that is an 
       * empty param.
       *
       */
        let value = param.length > 1? param[1] : '';
        // This does not check if a url receive the same parameter multiple times.
        hashes[param[0]] = value;
      }
    }
    
  /*
   * If there is a quicksearch value assign that to the hashes object.
   * If not delete quicksearch name from the hashes object if there is.
   * With this way, if there was a value for quicksearch in the hash 
   * object, it will just get overwritten. If not that index will be create.
   * The delete statement is just for cosmetic. This we turn the url back
   * to without hashes if there isn't a value. 
   * However, for faster code, this can simply be done as 
   *
   *   hashes[quicksearchParamName] = $("#quicksearch").val()
   *
   * If do like the above, whether if there is a value or not, the hash
   * parameter for quicksearch will always be built.
   *
   */
  if ( whocall === 1 ){
    // 1 : quicksearch
    if ( $("#quicksearch").val() ) hashes[quicksearchParamName] = encodeURIComponent($("#quicksearch").val());
    else delete hashes[quicksearchParamName];
    qsRegex = new RegExp($("#quicksearch").val(), "gi");
    /*
     * For lastState, if setup correctly, val will give an empty string
     * or something here.
     *
     */  
    lastState["searchFilter"] = $("#quicksearch").val();
  } else if ( whocall === 2 ){
    // 2 : filter
    /*
     * If done correctly there will always be a filter value when the user
     * choose an option
     *
     */
    hashes[filterParamName] = encodeURIComponent(filterValue);
    lastState["filterValue"] = filterValue;
  } else {
    // 3 : price sort
    /*
     * If from user selecting, without an option for resetting. If done 
     * correctly, there will always be a sortValue and sortType.
     *
     */
    lastState["sortValue"] = sortValue = obj.value;
    lastState["sortType"] = obj.selectedOptions[0].getAttribute('data-sorttype');
    hashes[sortValueParamName] = encodeURIComponent(obj.value);
    hashes[sortTypeParamName] = obj.selectedOptions[0].getAttribute('data-sorttype');;
    sortAscending = hashes[sortTypeParamName] === "asc"? true : false;
  }

  // Update the grid.
  $grid.isotope({ sortBy: sortValue , sortAscending: sortAscending});  
  /*
   * Update the hash without making filterWithHash() update the grid.
   * Join everything in hashes together into a string. Always append &
   * after a key. But when assign to "location.hash", remove the last
   * character(extra &) from the string.
   *
   */
  for ( const k in hashes ) hashStr += k + "=" + hashes[k] + "&";
  gridAlreadyUpdated = true;
  location.hash = hashStr.substr(0, hashStr.length - 1);
}

/*
 * This function below can be customize to utilize not just only hashes
 * but also "Get Requests"
 *
 */
function filterWithHash() {
  // If the grid is already updated, there is nothing to do.
  if ( gridAlreadyUpdated ) {
      gridAlreadyUpdated = false;
      return;
  }
  var hashFilter = getHashFilter();
  var searchFilter = getSearchFilter();
  var sortParam = getSortParam();
  /*
   * If the last time we access the value for the filters and it
   * is the same at this time. There isn't a point to re-execute the code
   */
  if ( $grid && lastState["searchFilter"] === searchFilter 
             && lastState["filterValue"] === hashFilter
             && lastState["sortValue"] === sortParam[0] 
             && lastState["sortType"] === sortParam[1] ) {
      return;
  }

  lastState["sortValue"] = sortParam[0];
  lastState["sortType"] = sortParam[1];
  lastState["searchFilter"] = searchFilter;
  lastState["filterValue"] = hashFilter;

  /*
   * If searhFilter is there, utilize it.
   * Else, qsRegex is reset. That is because the user could input a value into the
   * search field and then later delete that value then press enter. If that happen 
   * and we don't reset the field, the result will not be reset.
   *
   * The same goes for hashFilter below, it is just easier to use this as an example.
   */  
  if ( searchFilter ) { 
      $('#quicksearch').val(searchFilter);
      qsRegex = new RegExp(searchFilter, "gi");
  } else {
      // searchhash could be null and that is not fine with RegExp. 
      // Hence, we give it an empty string.
      $('#quicksearch').val("");
      qsRegex = new RegExp("", "gi");
  }

  /* 
   * Refer to comment of searchFilter right above 
   *
   * Also, this is for one filter group. If you need to work out to multiple 
   * filter group, you would have to split them by the . go through each
   * index, and see if they are valid values. 
   *
   * If some are valid and some are not, disregard the invalid and use the valid.
   * If none are valid, disregard all.
   * 
   */
  if ( hashFilter ) {
    var selectValue = $('select[id="sizes"]').find('option[value="'+ hashFilter +'"]');
    // Only search for a value if it is found within the select fields, else disregard it.
    if ( selectValue.length > 0 ){
      selectValue.prop('selected', 'selected');
      filterValue = hashFilter;
    }
  } else {
    // filterValue will become null or empty whichever. But that is fine.
    filterValue = hashFilter;
    $('select[id="sizes"]').prop('selectedIndex',0);
  }

  /*
   * getSortParam will always return two index. It just whether if they both have
   * values or both null.
   *
   * If sortParam is [null, null] or its values are invalid. Turn sortValue and
   * sortAscending to null.
   *
   * If sortParam is [null, null] prop the default select for select group 
   * with the id price-sort.
   */
  if ( sortParam[0] ){
    // search price sort select group to see if the hash is valid.
    var sortObj = $('#price-sort').find('option[value="'+ sortParam[0] +'"][data-sorttype="'+ sortParam[1] +'"]');
    // If hash is valid prob the field
    // Else reset the field
    if ( sortObj.length > 0 ){
      sortObj.prop('selected', true);
      sortValue = sortParam[0];
      sortAscending = sortParam[1] === "asc"? true : false;
    } else {
      sortValue = null;
      sortAscending = null;
      $('select[id="price-sort"]').prop('selectedIndex', 0);
    }
  } else {
    sortValue = null;
    sortAscending = null;
    $('select[id="price-sort"]').prop('selectedIndex', 0);
  }

  /*
   * If $grid is not initialize, it will get initialize. 
   * This will only happen on first run.
   * One grid is initilized, everytime grid.isotope() is run
   * without any value, grid will be updated to what initilized below.
   * Thus, each later run of isotope(), the filter will look at both,
   * the searchResult and the qsRegex if they are available.
   *
   */
  if ( !$grid ) {
    $grid = $(".grid").isotope({
              itemSelector: ".grid-item",
              layoutMode: "fitRows",
              getSortData: {
                price: '.t-price parseInt',
                category: '[data-category]',
              },
              sortBy: sortValue ,
              sortAscending: sortAscending,
              filter: function() {
                var $this = $(this);
                var searchResult = qsRegex ? $this.text().match(qsRegex) : true;
                var selectResult = filterValue ? $this.is(filterValue) : true;
                return searchResult && selectResult;
              }              
            });
  /*
   * When grid.isotope() is execute with sortValue, if that sortValue === null 
   * then grid.isotope() will not execute the sort parameter. That is for the 
   * isotope version of when this was first written. The code may need to 
   * be updated for future version if the behaviour of the isotope() function
   * change.
   *
   */
  } else $grid.isotope({ sortBy: sortValue , sortAscending: sortAscending});
}

/* 
 * Trigger filter with hash to initialize grid
 * and also to check the url for hash.
 */
filterWithHash();

// Bind the filterWithHash function to the hashchange event.
$(window).on( 'hashchange', filterWithHash );

});

答案:第 2 部分

对于这个答案,它是示例 3。我很可能无法对此代码提供评论,因为此代码已达到字符数限制。必须删除代码中的一些注释。对于那些缺少的注释,更喜欢示例 1 和示例 2。

<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<script src="https://unpkg.com/imagesloaded@4/imagesloaded.pkgd.min.js"></script>
<script src="https://unpkg.com/isotope-layout@3/dist/isotope.pkgd.js"></script>

<script>
"use strict";
$(document).ready(function(){
// Quick Search Regex
var qsRegex;
// Filter Value
var filterValue;
// sortValue & whether to sortAscending
var sortValue;
var sortAscending;
// Grid not initialize yet.
var $grid;
// Last state of all the filters
var lastState = {};

/*
 * Parameter name for quicksearch, filter, and sort
 * Have this here so everything can easily be changed in one place.
 *
 */
var quicksearchParamName = "search";
var filterParamName = "filter";
var sortValueParamName = "sort";
var sortTypeParamName = "sorttype";


/*
 * Regexes for grabbing values from hash parameter.
 *
 */
var quicksearchRegex = RegExp(quicksearchParamName + '=([^&]+)', 'i');
var filterRegex = RegExp(filterParamName + '=([^&]+)' , 'i');
var sortValueRegex = RegExp(sortValueParamName + '=([^&]+)' , 'i');
var sortTypeRegex = RegExp(sortTypeParamName + '=([^&]+)' , 'i');

/*
 * This variable is for the setHash() function to communicate with
 * the filterWithHash() function.
 *
 * There isn't a need to build a hash string, update everything, and then
 * reinterprete that same hash string right after.
 *
 * Thus, there isn't a need to run setHash() and then let filterWithHash()
 * run on hash update.
 */
var gridAlreadyUpdated = false;

/*
 * Assiging filterElements here for easy modification.
 *
 * Currently the hash is something like this "filter=.XS.Jackets.Black"
 * It can always be changed to "filter=size:.XS+type:Jackets+color:black"
 * with some modification to the code.
 */
var $filterElements = $('.filter-select');
/*
 * The preceding $ above mean that $filterElements is a jquery object.
 * The below filterElements is the array of all the elements
 * of $filterElements converted to jquery object.
 *
 * This was done like this so each time a select group is needed it doesn't
 * have to be converted to an object of jquery.
 */
var filterElements = [];
for ( let i = 0; i < $filterElements.length; i++ ){
  filterElements[i] = ($($filterElements[i]));
}

/*
 * Reset filter button, remove if not need.
 *
 */
$("#btn-reset-filter").on('click', function( event ){
  $filterElements.prop('selectedIndex',0);
  // only trigger the event for one element.
  filterElements[0].trigger("change");
});

// use value of search field to filter
var $quicksearch = $("#quicksearch").keyup(
  debounce(function() {
    setHash(1);
  })
);
// debounce so filtering doesn't happen every millisecond
function debounce(fn, threshold) {
  var timeout;
  return function debounced() {
    if (timeout) {
      clearTimeout(timeout);
    }
    function delayed() {
      fn();
      timeout = null;
    }
    setTimeout(delayed, threshold || 100);
  };
}

/*
 * When any of the select field with class filter is change
 * we cycle through all the fields and grab all the values for each field
 * and concat them into the filterValue.
 *
 * Over here filterElements that is being used is the JQuery object.
 */
$filterElements.on( 'change', function( event ) {
  filterValue = "";
  for ( let i = 0; i < $filterElements.length; i++ ){
    if ( $filterElements[i].selectedIndex > 0 ) {
      filterValue += $filterElements[i].value;
    }
  }

  // Use set hash to update the $grid and set the hash.
  setHash(2);
});

/*
 * change is-checked class on buttons
 * Only need one price-sort not two
 *
 */
$('#price-sort').on( 'change', function() {
    setHash(3, this);
});

function removeDisabled() {
  $filterElements.prop('disabled', false);
  $('#quicksearch').prop('disabled', false);
  $('#price-sort').prop('disabled', false);
}


function getHashFilter() {
  // get filter=filterName
  var matches = location.hash.match( filterRegex );
  var hashFilter = matches && matches[1];
  return hashFilter && decodeURIComponent( hashFilter );
}

function getSearchFilter() {
  // get search=filterName
  var matches = location.hash.match( quicksearchRegex );
  var searchFilter = matches && matches[1];
  return searchFilter && decodeURIComponent( searchFilter );
}

/*
 * Get the sort param. This function will always return an array with
 * 2 indexes. If both sortValue and sortType is found then it will return
 * the values for both. Value is index 1, and type is index 2.
 *
 * For everything else, this function will return [null, null].
 */
function getSortParam() {
  var valueMatches = location.hash.match( sortValueRegex );
  var typeMatches = location.hash.match( sortTypeRegex );
  var v = valueMatches && valueMatches[1];
  var t = typeMatches && typeMatches[1];

  if ( v && t ) return [decodeURIComponent( v ), decodeURIComponent( t )];
  return [ null, null ];
}

/*
 * This function will set the hash when one of the filtering field is
 * changed.
 *
 * Parameter whocall is utilize to know who is the caller. There can only
 * be one caller at a time. Whocall is utilize as int because comparing
 * int is much faster than comparing string.
 *
 * whocall(1) = quicksearch
 * whocall(2) = filter
 * whocall(3) = sorting
 *
 * In a secure environment any other whocall besides the 3 above should
 * generate an error.
 */
function setHash ( whocall, obj ){
  var hashes = {};
  var hashStr = "";
  if ( location.hash ){
      /*
       * forEach can be uitlized here, but this provide better cross platform
       * compatibitliy.
       *
       */
    let temp = location.hash.substr(1).split("&");
    for ( let i = 0; i < temp.length; i++ ){
      let param = temp[i].split("=");
      // if param[0] is 0 length that is an invalid look something like &=abc.
      if ( param[0].length === 0 ) continue;
      /*
       * if more than > 2 that is also invalid but just grab the first one anyway.
       * if exactly 1 that is something like &filter=&somethingelse. So that is an
       * empty param.
       *
       */
        let value = param.length > 1? param[1] : '';
        // This does not check if a url receive the same parameter multiple times.
        hashes[param[0]] = value;
      }
    }

  /*
   * If there is a quicksearch value assign that to the hashes object.
   * If not delete quicksearch name from the hashes object if there is.
   * With this way, if there was a value for quicksearch in the hash
   * object, it will just get overwritten. If not that index will be create.
   * The delete statement is just for cosmetic. This we turn the url back
   * to without hashes if there isn't a value.
   * However, for faster code, this can simply be done as
   *
   *   hashes[quicksearchParamName] = $("#quicksearch").val()
   *
   * If do like the above, whether if there is a value or not, the hash
   * parameter for quicksearch will always be built.
   *
   */
  if ( whocall === 1 ){
    // 1 : quicksearch
    if ( $("#quicksearch").val() ) hashes[quicksearchParamName] = encodeURIComponent($("#quicksearch").val());
    else delete hashes[quicksearchParamName];
    qsRegex = new RegExp($("#quicksearch").val(), "gi");
    /*
     * For lastState, if setup correctly, val will give an empty string
     * or something here.
     *
     */
    lastState["searchFilter"] = $("#quicksearch").val();
  } else if ( whocall === 2 ){
    // 2 : filter
    /*
     * If done correctly there will always be a filter value when the user
     * choose an option
     *
     */
    hashes[filterParamName] = encodeURIComponent(filterValue);
    lastState["filterValue"] = filterValue;
  } else {
    // 3 : price sort
    /*
     * If from user selecting, without an option for resetting. If done
     * correctly, there will always be a sortValue and sortType.
     *
     */
    lastState["sortValue"] = sortValue = obj.value;
    lastState["sortType"] = obj.selectedOptions[0].getAttribute('data-sorttype');
    hashes[sortValueParamName] = encodeURIComponent(obj.value);
    hashes[sortTypeParamName] = obj.selectedOptions[0].getAttribute('data-sorttype');
    sortAscending = hashes[sortTypeParamName] === "asc"? true : false;
  }

  /*
   * If the fields are not disabled then update the grid if grid already intialized.
   * Otherwise, the initializer will do that. For that use the code in this comment
   * instead.
   *
   * if ( $grid ) $grid.isotope({ sortBy: sortValue , sortAscending: sortAscending});
   */
  $grid.isotope({ sortBy: sortValue , sortAscending: sortAscending});
  /*
   * Update the hash without making filterWithHash() update the grid.
   * Join everything in hashes together into a string. Always append &
   * after a key. But when assign to "location.hash", remove the last
   * character(extra &) from the string.
   *
   */
  for ( const k in hashes ) hashStr += k + "=" + hashes[k] + "&";
  gridAlreadyUpdated = true;
  location.hash = hashStr.substr(0, hashStr.length - 1);
}

/*
 * This function below can be customize to utilize not just only hashes
 * but also "Get Requests"
 *
 */
function filterWithHash() {
  // If the grid is already updated, there is nothing to do.
  if ( gridAlreadyUpdated ) {
      gridAlreadyUpdated = false;
      return;
  }
  var hashFilter = getHashFilter();
  var searchFilter = getSearchFilter();
  var sortParam = getSortParam();
  /*
   * If the last time we access the value for the filters and it
   * is the same at this time. There isn't a point to re-execute the code
   */
  if ( $grid && lastState["searchFilter"] === searchFilter
             && lastState["filterValue"] === hashFilter
             && lastState["sortValue"] === sortParam[0]
             && lastState["sortType"] === sortParam[1] ) {
      return;
  }

  lastState["sortValue"] = sortParam[0];
  lastState["sortType"] = sortParam[1];
  lastState["searchFilter"] = searchFilter;
  /*
   * Note that the lastState["filterValue"] for here is
   * the string value of hash filter and not the actual
   * processed filterValue.
   *
   */
  lastState["filterValue"] = hashFilter;

  /*
   * If searhFilter is there, utilize it.
   * Else, qsRegex is reset. That is because the user could input a value into the
   * search field and then later delete that value then press enter. If that happen
   * and we don't reset the field, the result will not be reset.
   *
   * The same goes for hashFilter below, it is just easier to use this as an example.
   */
  if ( searchFilter ) {
      $('#quicksearch').val(searchFilter);
      qsRegex = new RegExp(searchFilter, "gi");
  } else {
      // searchhash could be null and that is not fine with RegExp.
      // Hence, we give it an empty string.
      $('#quicksearch').val("");
      qsRegex = new RegExp("", "gi");
  }

  /*
   * Refer to comment of searchFilter right above
   *
   * This will split the hash string by the "." and then check for their values.
   * If they are valid, it will set the filter value by the valid one. It will
   * disregard all the invalid.
   *
   * This search is based on the filterElements. For each filterElements,
   * the code will search for a match in the split array. If that is found,
   * that value is added to the filterValue string.
   *
   * Therefore, any duplicate will not be taken into consideration.
   * Duplicates only can become a problem if values between each filterElements
   * are not unique in nature.
   */
  if ( hashFilter ) {
    hashFilter = hashFilter.split(/(?=\.)/);
    filterValue = "";
    for ( let i = 0; i < filterElements.length; i++ ){
      let foundedValue = false;
      /*
       * This will search through the entire splitted hash array. But can
       * be done to search at maximum the same amount of filterElements
       * that are available.
       *
       * Note that, this does not check or prop the All select field
       * as that field now does not contain any values.
       *
       * Also how many index turned to null can be keep tracked of. That
       * is because if there are 2 indexes and both turned to null but there
       * are 20 filter elements then There is no point executing the code
       * for the other 18 elements.
       *
       * However, for a smaller amount of filter elements, keeping track
       * of how many elements turned to null may create an overhead cost.
       */
      for ( let j = 0; j < hashFilter.length; j++ ){
        if ( hashFilter[j] === null ) continue;
        let selectValue = filterElements[i].find('option[value="'+ hashFilter[j] +'"]');
        /*
         * If selectValue is found then select the value in the element
         * and add that value to the filterValue string.
         *
         * One a value is founded and used, that value will be turned
         * to null and will not be reused.
         *
         * Make sure the value of each select group is unique.
         */
        if ( selectValue.length > 0 ){
          selectValue.prop('selected', 'selected');
          filterValue += hashFilter[j];
          hashFilter[j] = null;
          foundedValue = true;
          break;
        }
      }
      /*
       * If the hash string did not contain a value for this element,
       * prop selected index 0(reset the select field).
       *
       */
      if ( foundedValue === false ) filterElements[i].prop('selectedIndex',0);
    }

    /*
     * If after cycle through the hash above and filterValue's length is still 0
     * then a default value will be prop for everything.
     *
     * Notice the changes here, where $filterElements that is being used
     * is preceding with the $.
     */
    if ( filterValue.length === 0 ) $filterElements.prop('selectedIndex',0);
  } else {
    // filterValue will become null or empty whichever. But that is fine.
    filterValue = hashFilter;
    $filterElements.prop('selectedIndex',0);
  }

  /*
   * getSortParam will always return two index. It just whether if they both have
   * values or both null.
   *
   * If sortParam is [null, null] or its values are invalid. Turn sortValue and
   * sortAscending to null.
   *
   * If sortParam is [null, null] prop the default select for select group
   * with the id price-sort.
   */
  if ( sortParam[0] ){
    // search price sort select group to see if the hash is valid.
    var sortObj = $('#price-sort').find('option[value="'+ sortParam[0] +'"][data-sorttype="'+ sortParam[1] +'"]');
    // If hash is valid prob the field
    // Else reset the field
    if ( sortObj.length > 0 ){
      sortObj.prop('selected', true);
      sortValue = sortParam[0];
      sortAscending = sortParam[1] === "asc"? true : false;
    } else {
      sortValue = null;
      sortAscending = null;
      $('select[id="price-sort"]').prop('selectedIndex', 0);
    }
  } else {
    sortValue = null;
    sortAscending = null;
    $('select[id="price-sort"]').prop('selectedIndex', 0);
  }

  if ( !$grid ) {
    $grid = $(".grid").isotope({
              itemSelector: ".grid-item",
              layoutMode: "fitRows",
              getSortData: {
                price: '.t-price parseInt',
                category: '[data-category]',
                dateAdded: function( itemElem ){
                  // Sort by date: return the timestamp of dateAdded.
                  return new Date($(itemElem).find('.dateAdded').text()).getTime();
                },
                  /*
                   * Sort by item with class New.
                   * If the item contain class New, it will be assigned with a value of 0.
                   * Else, it will be assigned with a value of 1.
                   *
                   * There shouldn't be a sort descending for New. A field like that
                   * wasn't configured. Therefore, hash wouldn't be able to sort
                   * New in a descending way.
                   */
                New: function( itemElem ){
                  return $(itemElem).hasClass('New')? 0 : 1;
                }
              },
              sortBy: sortValue ,
              sortAscending: sortAscending,
              filter: function() {
                /* Note that current filtering using class name. */
                var $this = $(this);
                var searchResult = qsRegex ? $this.text().match(qsRegex) : true;
                var selectResult = filterValue ? $this.is(filterValue) : true;
                return searchResult && selectResult;
              }
            });
  } else $grid.isotope({ sortBy: sortValue , sortAscending: sortAscending});
}

$('.grid').imagesLoaded( function() {
  filterWithHash();
  $(window).on( 'hashchange', filterWithHash );
  removeDisabled();
});

});
</script>
</head>
<body>

<div id="sort-filter">
  <!-- Short Div -->
  <div id="sort">
    <select id="price-sort" class="select-css form-control long" disabled>
      <option selected disabled class="s-title"> Sort </option>
      <option data-sorttype="des" value="price">£ Low To High</option>
      <option data-sorttype="asc" value="price">£ High To Low</option>
      <option data-sorttype="des" value="dateAdded">Newest</option>
      <option data-sorttype="asc" value="dateAdded">Oldest</option>
      <option data-sorttype="asc" value="New">New</option>
      <option data-sorttype="asc" value="original-order">Original Order</option>
    </select>
  </div>

  <!-- Filter Div -->
  <div class="filters">
    <select class="filter-select select-css short" value-group="sizes" id="sizes" disabled>
      <option selected disabled class="s-title"> Size </option>
      <option value="">All</option>
      <option value=".XS">XS</option>
      <option value=".S">S</option>
      <option value=".M">M</option>
      <option value=".L">L</option>
      <option value=".XL">XL</option>
      <option value=".XXL">XXL</option>
    </select>

    <select class="filter-select select-css long" value-group="gender" id="gender" disabled>
      <option selected disabled> Gender </option>
      <option value="">All</option>
      <option value=".Male">Male</option>
      <option value=".Female">Female</option>
      <option value=".Genderless">Genderless</option>
    </select>

    <select class="filter-select select-css short" value-group="colour" id="colour" disabled>
      <option selected disabled> Colour </option>
      <option value="">All</option>
      <option value=".Black">Black</option>
      <option value=".Blue">Blue</option>
      <option value=".Brown">Brown</option>
      <option value=".Cream">Cream</option>
      <option value=".Other">Other</option>
    </select>

    <select class="filter-select select-css long" value-group="type" id="type" disabled>
      <option selected disabled> Type </option>
      <option value="">All</option>
      <option value=".Bags">Bags</option>
      <option value=".Bottoms">Bottoms</option>
      <option value=".Co-ords">Co-ords</option>
      <option value=".Jackets">Jackets</option>
    </select>

    <select class="filter-select select-css short" value-group="brand" id="brand" disabled>
      <option selected disabled> Brand </option>
      <option value="">All</option>
      <option value=".Adidas">Adidas</option>
      <option value=".American Sports Teams">American Sports Teams</option>
    </select>

    <button type="button" id="btn-reset-filter">Reset Filter</button>
    <input type="text" id="quicksearch" class="quicksearch" placeholder="Search" / disabled>

  </div>
</div>

<div class="container">
  <ul class="grid cs-style-3">
    <div class="grid-sizer"></div>

      <li class="grid-item XS Male Beige Bags Mint">
        <a href="link" class="animsition-link" data-animsition-out-class="fade-out-left-lg">
          <figure style="background-image: URL(image.jpg);">
            <img src="https://live.staticflickr.com/65535/48297243402_ea74392016_w_d.jpg" alt="hat sale item">
          </figure>
          <div id="pro-deets"> <!-- This should not be id -->
            <h3>hat sale item</h3>
            <span id="price" class="holderpage"> <!-- This should not be id -->
              £<span class="price t-price">3</span>
            </span>
          </div>
          <div class="dateAdded">1986-10-21</div>
        </a>
      </li>

      <li class="grid-item L Female Brown Tops Worn">
        <a href="link" class="animsition-link" data-animsition-out-class="fade-out-left-lg">
          <figure style="background-image: URL(image.jpg);">
            <img src="https://live.staticflickr.com/748/20445410340_c1a0fe6a6a_w_d.jpg" alt="product no sale no new">
          </figure>
          <div id="pro-deets">
            <h3>product no sale no new</h3>
            <span id="price" class="holderpage">
              £<span class="price t-price">40</span>
            </span>
          </div>
          <div class="dateAdded">2008-9-30</div>
        </a>
      </li>

      <li class="grid-item L Female Brown Tops Worn New" data-category="New">
        <a href="link" class="animsition-link" data-animsition-out-class="fade-out-left-lg">
          <figure style="background-image: URL(image.jpg);">
            <img src="https://live.staticflickr.com/724/20624833532_4b08d803b7_w_d.jpg" alt="Skirt">
          </figure>
          <div id="pro-deets">
            <h3>Skirt</h3>
            <span id="price" class="holderpage">
              £<span class="price t-price">10</span>
            </span>
          </div>
          <div class="dateAdded">1986-10-22</div>
        </a>
      </li>

      <li class="grid-item XS Male Beige Bags Mint Sale Jackets" data-category="Sale">
        <a href="link" class="animsition-link" data-animsition-out-class="fade-out-left-lg">
          <figure style="background-image: URL(image.jpg);">
            <img src="https://live.staticflickr.com/590/20447259789_9114edf13a_w_d.jpg" alt="Jacket">
          </figure>
          <div id="pro-deets">
            <h3>Jacket</h3>
            <span id="price" class="holderpage">
              £<span class="price sale">30</span>
              <span class="price">£<span class="t-price">20</span></span>
            </span>
          </div>
          <div class="dateAdded">2018-7-30</div>
        </a>
      </li>

      <li class="grid-item XXL Male Beige Bags Mint Sale" data-category="Sale">
        <a href="link" class="animsition-link" data-animsition-out-class="fade-out-left-lg">
          <figure style="background-image: URL(image.jpg);">
            <img src="https://live.staticflickr.com/4148/4973470198_b3af07ca8c_w_d.jpg" alt="Jacket">
          </figure>
          <div id="pro-deets">
            <h3>Wallet</h3>
            <span id="price" class="holderpage">
              £<span class="price sale">30</span>
              <span class="price">£<span class="t-price">50</span></span>
            </span>
          </div>
          <div class="dateAdded">2017-5-20</div>
        </a>
      </li>

  </ul>
</div>
</body>
</html>

照片来自 Flickr,链接:

Photo 1 - (400x267)

Photo 2 - (400x225)

Photo 3 - (400x225)

Photo 4 - (400x225)

Photo 5 - (400x250)