找回密码
 会员注册
查看: 114|回复: 0

ASP.NET Core 和 EF Core 系列教程——排序、筛选、分页和分组

[复制链接]

250

主题

1

回帖

819

积分

管理员

积分
819
发表于 2024-2-29 08:26:41 | 显示全部楼层 |阅读模式

ASP.NET Core 和 EF Core 系列教程——排序、筛选、分页和分组(3 / 10)

作者:Tom Dykstra和Rick Anderson

Contoso 大学示例 web 应用程序演示如何使用 Entity Framework Core 和 Visual Studio 创建 ASP.NET Core MVC web 应用程序。 想要获取有关系列教程的信息,请参阅第一个教程。

在前面的教程,你可以实现一组的用于学生实体的基本 CRUD 操作网页。 在本教程将向学生索引页添加排序、 筛选和分页功能。 你还将创建具有简单分组功能的页面。

下图显示你完成本教程后相关页面的样子。 列标题时一个链接,用户可以单击它使数据按该列排序。 反复单击列标题在升序排列和降序排列之间切换。

学生索引页

将列排序链接添加到学生索引页

为了添加排序学生索引页,你将更改学生控制器中的Index方法并向学生索引视图添加相关的代码。

向 Index 方法添加排序功能

StudentsController.cs,用以下代码替换Index方法:

  1. public async Task<IActionResult> Index(string sortOrder)
  2. {
  3. ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
  4. ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
  5. var students = from s in _context.Students
  6. select s;
  7. switch (sortOrder)
  8. {
  9. case "name_desc":
  10. students = students.OrderByDescending(s => s.LastName);
  11. break;
  12. case "Date":
  13. students = students.OrderBy(s => s.EnrollmentDate);
  14. break;
  15. case "date_desc":
  16. students = students.OrderByDescending(s => s.EnrollmentDate);
  17. break;
  18. default:
  19. students = students.OrderBy(s => s.LastName);
  20. break;
  21. }
  22. return View(await students.AsNoTracking().ToListAsync());
  23. }
复制代码

此代码从 URL 中的查询字符串中接收sortOrder参数。 ASP.NET Core MVC 提供的查询字符串作为参数传递给的操作方法。 “Name”或”Date”,后面可以选择性跟用于指定降序顺序的下划线和”desc”构成参数字符串。 默认排序顺序为升序。

第一次请求索引页时,没有任何查询字符串。 学生按姓氏升序显示也就是switch语句中的缺省值中的排序方式。 当用户单击列标题的超链接,将向Index方法提供相应的sortOrder查询字符串。

视图使用ViewData元素中两个元素 (NameSortParm 和 DateSortParm) 对应的查询字符串值配置列标题超链接。

  1. public async Task<IActionResult> Index(string sortOrder)
  2. {
  3. ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
  4. ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
  5. var students = from s in _context.Students
  6. select s;
  7. switch (sortOrder)
  8. {
  9. case "name_desc":
  10. students = students.OrderByDescending(s => s.LastName);
  11. break;
  12. case "Date":
  13. students = students.OrderBy(s => s.EnrollmentDate);
  14. break;
  15. case "date_desc":
  16. students = students.OrderByDescending(s => s.EnrollmentDate);
  17. break;
  18. default:
  19. students = students.OrderBy(s => s.LastName);
  20. break;
  21. }
  22. return View(await students.AsNoTracking().ToListAsync());
  23. }
复制代码

这两个语句都使用了三目运算符。 第一个语句指如果sortOrder参数为 null 或为空则 NameSortParm 应设置为”name_desc”; 否则,它应设置为一个空字符串。 这两个语句使试图能够如下所示设置列标题的超链接:

当前的排序顺序Last Name 超链接Date 超链接
Last Name 升序排列descendingascending
Last Name 降序排列ascendingascending
Date 升序排列ascendingdescending
Date 降序排列ascendingascending

该方法使用 LINQ to Entities 指定要作为排序依据的列。 代码在switch 语句之前创建了IQueryable变量然后在 switch 语句中对其进行修改,并在switch语句之后调用ToListAsync方法。 当你创建和修改IQueryable变量时数据库不会接收到任何查询。 在您调用如ToListAsync等方法将IQueryable转换为集合对象之前不会执行查询。 因此,在return View语句之前此代码只会执行一个查询。

此代码会获得具有大量列的冗长信息。 本系列最后一个教程将演示如何编写代码,使你可以使用字符串将需要OrderBy的行的名称作为参数传递给方法。

向学生索引视图的列标题添加超链接

用以下代码替换Views/Students/Index.cshtml,以添加列标题超链接。 高亮代码为已更改的行。

  1. @model IEnumerable<ContosoUniversity.Models.Student>
  2. @{
  3. ViewData["Title"] = "Index";
  4. }
  5. <h2>Index</h2>
  6. <p>
  7. <a asp-action="Create">Create New</a>
  8. </p>
  9. <table class="table">
  10. <thead>
  11. <tr>
  12. <th>
  13. <a asp-action="Index" asp-route-sortOrder="@ViewData["NameSortParm"]">@Html.DisplayNameFor(model => model.LastName)</a>
  14. </th>
  15. <th>
  16. @Html.DisplayNameFor(model => model.FirstMidName)
  17. </th>
  18. <th>
  19. <a asp-action="Index" asp-route-sortOrder="@ViewData["DateSortParm"]">@Html.DisplayNameFor(model => model.EnrollmentDate)</a>
  20. </th>
  21. <th></th>
  22. </tr>
  23. </thead>
  24. <tbody>
  25. @foreach (var item in Model) {
  26. <tr>
  27. <td>
  28. @Html.DisplayFor(modelItem => item.LastName)
  29. </td>
  30. <td>
  31. @Html.DisplayFor(modelItem => item.FirstMidName)
  32. </td>
  33. <td>
  34. @Html.DisplayFor(modelItem => item.EnrollmentDate)
  35. </td>
  36. <td>
  37. <a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
  38. <a asp-action="Details" asp-route-id="@item.ID">Details</a> |
  39. <a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
  40. </td>
  41. </tr>
  42. }
  43. </tbody>
  44. </table>
复制代码

代码中使用了ViewData元素中的信息来以相应的查询字符串值设置超链接。

运行应用程序中,选择Students卡,然后单击Last NameEnrollment Date列标题,以验证该排序成功。

名称顺序中的学生索引页

向学生索引页添加搜索框

向视图添加一个文本框和提交按钮来向索引页添加搜索框,并在Index方法中做相应更改。 你可以在文本框中输入字符串搜索名字和姓氏字段中的内容。

向索引方法添加筛选功能

StudentsController.cs,将Index方法替换为以下代码 (突出显示所做的更改)。

  1. public async Task<IActionResult> Index(string sortOrder, string searchString)
  2. {
  3. ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
  4. ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
  5. ViewData["CurrentFilter"] = searchString;
  6. var students = from s in _context.Students
  7. select s;
  8. if (!String.IsNullOrEmpty(searchString))
  9. {
  10. students = students.Where(s => s.LastName.Contains(searchString)
  11. || s.FirstMidName.Contains(searchString));
  12. }
  13. switch (sortOrder)
  14. {
  15. case "name_desc":
  16. students = students.OrderByDescending(s => s.LastName);
  17. break;
  18. case "Date":
  19. students = students.OrderBy(s => s.EnrollmentDate);
  20. break;
  21. case "date_desc":
  22. students = students.OrderByDescending(s => s.EnrollmentDate);
  23. break;
  24. default:
  25. students = students.OrderBy(s => s.LastName);
  26. break;
  27. }
  28. return View(await students.AsNoTracking().ToListAsync());
  29. }
复制代码

在Index方法中添加searchString参数。 搜索字符串值来自之后会添加到索引视图中的文本框。 你还向 LINQ 语句 添加了where 子句来选择仅名字或姓氏包含搜索字符串的学生。 添加 where 子句的语句只有在要搜索值的时候才执行。

此处对IQueryable对象调用Where方法,筛选器将在服务器上处理。 在某些场景下你可能会对内存中集合调用作为扩展方法的Where。 (例如,假设你使用_context.Students引用,不同于 EFDbSet,它返回IEnumerable集合的存储库方法的引用。)结果通常将相同,但在某些情况下可能会不同。

例如,默认情况下 .NET Framework 实现的Contains方法是对大小写敏感的,但 SQL Server 中由 SQL Server 的排序规则确定。 SQL Server 默认不区分大小写。 您可以调用ToUpper方法使得其大小写敏感:Where (s = > s.LastName.ToUpper()。Contains(searchString.ToUpper())。 这样做能确保如果将来使用返回IEnumerable集合的存储库方法而不是IQueryable对象来修改相关代码,结果还能保持相同。 (当你对IEnumerable集合调用Contains方法,你将获取.NET Framework 的实现; 当对IQueryable对象调用它,则会得到数据库驱动的实现。)但是,此解决方案会对性能产生负面影响。 ToUpper将函数加入到 TSQL SELECT 语句的 WHERE 子句中。 这样做会是的索引优化失去效果。 假设 SQL 大多是是大小写不敏感,在你将数据迁移到大小写敏感的数据存储库之前最好避免ToUpper代码。

向学生索引视图添加一个搜索框

打开Views/Student/Index.cshtml,在table标签之前添加高亮代码以在页面中创建标题,Search文本框,按钮。

  1. <p>
  2. <a asp-action="Create">Create New</a>
  3. </p>
  4. <form asp-action="Index" method="get">
  5. <div class="form-actions no-color">
  6. <p>
  7. Find by name: <input type="text" name="SearchString" value="@ViewData["currentFilter"]" />
  8. <input type="submit" value="Search" class="btn btn-default" /> |
  9. <a asp-action="Index">Back to Full List</a>
  10. </p>
  11. </div>
  12. </form>
  13. <table class="table">
复制代码

此代码通过使用

标记帮助器添加搜索文本框和按钮。 默认情况下,标记帮助器默认使用 post 方法提交数据,这意味着,参数在 HTTP 消息正文中传输表单数据,而不是在 URL 查询字符串上显示并传输。指定使用 HTTP GET 时,表单数据是通过 URL 查询字符串传输,这使得用户能够使用该 URL 来创建书签。 W3C 指南建议当操作未导致更新时使用 GET 方法。

运行应用程序,选择Students选项卡,输入搜索字符串,然后单击搜索以验证筛选是否正常工作。

使用筛选的学生索引页

请注意该 URL 包含搜索字符串。

  1. http://localhost:5813/Students?SearchString=an
复制代码

如果将此页加入书签,使用书签时你将获得筛选后的列表。 添加method="get"到form标签中是导致生成查询字符串的原因。

在此阶段,如果您单击列标题的排序链接你将丢失url上的查询字符串值。 下一部分将修复此问题。

向学生索引页添加分页功能

为了向学生索引页添加分页功能,你需要创建PaginatedList类,该类使用Skip和Take语句来对服务器上的数据进行筛选而不是始终检索所有的表行。 接下来你将对Index方法做更多的修改并将分页按钮添加到Index视图中。 如下图所示添加了分页按钮的学生索引页。

学生索引页,带有分页链接

在项目文件夹中,创建PaginatedList.cs,然后将模板代码替换为下面的代码。

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Threading.Tasks;
  5. using Microsoft.EntityFrameworkCore;
  6. namespace ContosoUniversity
  7. {
  8. public class PaginatedList<T> : List<T>
  9. {
  10. public int PageIndex { get; private set; }
  11. public int TotalPages { get; private set; }
  12. public PaginatedList(List<T> items, int count, int pageIndex, int pageSize)
  13. {
  14. PageIndex = pageIndex;
  15. TotalPages = (int)Math.Ceiling(count / (double)pageSize);
  16. this.AddRange(items);
  17. }
  18. public bool HasPreviousPage
  19. {
  20. get
  21. {
  22. return (PageIndex > 1);
  23. }
  24. }
  25. public bool HasNextPage
  26. {
  27. get
  28. {
  29. return (PageIndex < TotalPages);
  30. }
  31. }
  32. public static async Task<PaginatedList<T>> CreateAsync(IQueryable<T> source, int pageIndex, int pageSize)
  33. {
  34. var count = await source.CountAsync();
  35. var items = await source.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync();
  36. return new PaginatedList<T>(items, count, pageIndex, pageSize);
  37. }
  38. }
  39. }
复制代码

代码中的CreateAsync方法获得页面数和当前页码,并对IQueryable执行 相应的Skip和Take语句。 当IQueryable调用ToListAsync时,该方法将返回只包含在请求页里的学生列表。 属性HasPreviousPage和HasNextPage可用来启用或禁用PreviousNext分页按钮。

由于构造函数里不能运行异步代码,CreateAsync方法被用作构造函数而只用于创建一个PaginatedList对象。

向索引方法添加分页功能

StudentsController.cs中,用以下代码替换Index方法。

  1. public async Task<IActionResult> Index(
  2. string sortOrder,
  3. string currentFilter,
  4. string searchString,
  5. int? page)
  6. {
  7. ViewData["CurrentSort"] = sortOrder;
  8. ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
  9. ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
  10. if (searchString != null)
  11. {
  12. page = 1;
  13. }
  14. else
  15. {
  16. searchString = currentFilter;
  17. }
  18. ViewData["CurrentFilter"] = searchString;
  19. var students = from s in _context.Students
  20. select s;
  21. if (!String.IsNullOrEmpty(searchString))
  22. {
  23. students = students.Where(s => s.LastName.Contains(searchString)
  24. || s.FirstMidName.Contains(searchString));
  25. }
  26. switch (sortOrder)
  27. {
  28. case "name_desc":
  29. students = students.OrderByDescending(s => s.LastName);
  30. break;
  31. case "Date":
  32. students = students.OrderBy(s => s.EnrollmentDate);
  33. break;
  34. case "date_desc":
  35. students = students.OrderByDescending(s => s.EnrollmentDate);
  36. break;
  37. default:
  38. students = students.OrderBy(s => s.LastName);
  39. break;
  40. }
  41. int pageSize = 3;
  42. return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(), page ?? 1, pageSize));
  43. }
复制代码

代码中将总页数参数、 当前的排序顺序参数和当前的筛选器参数添加到方法签名中。

  1. public async Task<IActionResult> Index(
  2. string sortOrder,
  3. string currentFilter,
  4. string searchString,
  5. int? page)
复制代码

第一次显示页面,或如果用户未单击分页或排序链接,则所有参数都为null。 如果单击分页链接,页面变量将包含要显示的页码。

名为 CurrentSort 的ViewData元素提供了当前已排序的试图,因为这必须包含在分页链接中以保持排序顺序在分页时相同。

名为 CurrentFilter 的ViewData元素提供了当前已筛选的视图。为了在分页过程中维护筛选规则以及在页面重新显示的时候把筛选值恢复到文本框中,该值一定要被包含进分页链接里

如果分页期间更改搜索字符串,显示的页会被重置为 1,因为新的筛选器可能会导致显示不同的数据。 在文本框中输入了值以及按下提交按钮搜索字符串就会改变。 在这种情况下,searchString参数不为 null。

  1. if (searchString != null)
  2. {
  3. page = 1;
  4. }
  5. else
  6. {
  7. searchString = currentFilter;
  8. }
复制代码

在Index方法的结尾,PaginatedList.CreateAsync方法将学生查询转换为支持分页的集合类型,集合中包含了刚好能放进单页的学生实体。 然后将这个单页大小的学生集合 传递给视图。

  1. return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(), page ?? 1, pageSize));
复制代码

PaginatedList.CreateAsync方法从参数中获取页号。 两个问号表示 null 合并运算符。 Null 合并运算符可以为 null 的类型定义一个默认值; 表达式(page ?? 1)意味着返回的值如果page参数为 null 则返回 1,如果指定了一个值则返回指定的值。

向学生索引视图添加分页链接

Views/Students/Index.cshtml中,用以下代码替换现有代码。 高亮代码为更改的代码。

  1. @model PaginatedList<ContosoUniversity.Models.Student>
  2. @{
  3. ViewData["Title"] = "Index";
  4. }
  5. <h2>Index</h2>
  6. <p>
  7. <a asp-action="Create">Create New</a>
  8. </p>
  9. <form asp-action="Index" method="get">
  10. <div class="form-actions no-color">
  11. <p>
  12. Find by name: <input type="text" name="SearchString" value="@ViewData["currentFilter"]" />
  13. <input type="submit" value="Search" class="btn btn-default" /> |
  14. <a asp-action="Index">Back to Full List</a>
  15. </p>
  16. </div>
  17. </form>
  18. <table class="table">
  19. <thead>
  20. <tr>
  21. <th>
  22. <a asp-action="Index" asp-route-sortOrder="@ViewData["NameSortParm"]" asp-route-currentFilter="@ViewData["CurrentFilter"]">Last Name</a>
  23. </th>
  24. <th>
  25. First Name
  26. </th>
  27. <th>
  28. <a asp-action="Index" asp-route-sortOrder="@ViewData["DateSortParm"]" asp-route-currentFilter="@ViewData["CurrentFilter"]">Enrollment Date</a>
  29. </th>
  30. <th></th>
  31. </tr>
  32. </thead>
  33. <tbody>
  34. @foreach (var item in Model)
  35. {
  36. <tr>
  37. <td>
  38. @Html.DisplayFor(modelItem => item.LastName)
  39. </td>
  40. <td>
  41. @Html.DisplayFor(modelItem => item.FirstMidName)
  42. </td>
  43. <td>
  44. @Html.DisplayFor(modelItem => item.EnrollmentDate)
  45. </td>
  46. <td>
  47. <a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
  48. <a asp-action="Details" asp-route-id="@item.ID">Details</a> |
  49. <a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
  50. </td>
  51. </tr>
  52. }
  53. </tbody>
  54. </table>
  55. @{
  56. var prevDisabled = !Model.HasPreviousPage ? "disabled" : "";
  57. var nextDisabled = !Model.HasNextPage ? "disabled" : "";
  58. }
  59. <a asp-action="Index"
  60. asp-route-sortOrder="@ViewData["CurrentSort"]"
  61. asp-route-page="@(Model.PageIndex - 1)"
  62. asp-route-currentFilter="@ViewData["CurrentFilter"]"
  63. class="btn btn-default @prevDisabled">
  64. Previous
  65. </a>
  66. <a asp-action="Index"
  67. asp-route-sortOrder="@ViewData["CurrentSort"]"
  68. asp-route-page="@(Model.PageIndex + 1)"
  69. asp-route-currentFilter="@ViewData["CurrentFilter"]"
  70. class="btn btn-default @nextDisabled">
  71. Next
  72. </a>
复制代码

在页面顶部@model的语句表示视图现在获取的是PaginatedList对象而不是List对象。

列标题链接使用查询字符串将当前的搜索字符串传递到控制器,以便用户可以在筛选结果中进行排序:

  1. <a asp-action="Index" asp-route-sortOrder="@ViewData["DateSortParm"]" asp-route-currentFilter ="@ViewData["CurrentFilter"]">Enrollment Date</a>
复制代码

通过标记帮助程序显示分页按钮:

  1. <a asp-action="Index"
  2. asp-route-sortOrder="@ViewData["CurrentSort"]"
  3. asp-route-page="@(Model.PageIndex - 1)"
  4. asp-route-currentFilter="@ViewData["CurrentFilter"]"
  5. class="btn btn-default @prevDisabled">
  6. Previous
  7. </a>
复制代码

运行应用并转到学生页。

学生索引页,带有分页链接

单击以确保分页工作原理的不同的排序顺序中的分页链接。 然后输入搜索字符串,然后重试以验证分页还适用正确使用排序和筛选的分页。

创建关于页面来显示学生统计信息

在 Contoso 大学网站About页上,将显示每个课程有多少学生修读。 这要求在分组上再进行分组和简单计算。 要完成此操作,需要执行以下操作:

  • 创建一个视图模型类,该视图类是需要传递到该视图的数据的抽象。

  • 修改对 Home 控制器中的 About 方法。

  • 修改关于视图。

创建视图模型

Model文件夹中创建SchoolViewModels文件夹。

在新的文件夹中,添加EnrollmentDateGroup.cs类文件并将模板代码替换为以下代码:

  1. using System;
  2. using System.ComponentModel.DataAnnotations;
  3. namespace ContosoUniversity.Models.SchoolViewModels
  4. {
  5. public class EnrollmentDateGroup
  6. {
  7. [DataType(DataType.Date)]
  8. public DateTime? EnrollmentDate { get; set; }
  9. public int StudentCount { get; set; }
  10. }
  11. }
复制代码

修改 Home 控制器

HomeController.cs,在该文件的顶部添加以下 using 语句:

  1. using Microsoft.EntityFrameworkCore;
  2. using ContosoUniversity.Data;
  3. using ContosoUniversity.Models.SchoolViewModels;
复制代码

在类中,左左大括号后添加的数据库上下文类型的变量,并通过 ASP.NET Core 依赖注入获取上下文的实例:

  1. public class HomeController : Controller
  2. {
  3. private readonly SchoolContext _context;
  4. public HomeController(SchoolContext context)
  5. {
  6. _context = context;
  7. }
复制代码

将 About 方法的代码替换为以下代码:

  1. public async Task<ActionResult> About()
  2. {
  3. IQueryable<EnrollmentDateGroup> data =
  4. from student in _context.Students
  5. group student by student.EnrollmentDate into dateGroup
  6. select new EnrollmentDateGroup()
  7. {
  8. EnrollmentDate = dateGroup.Key,
  9. StudentCount = dateGroup.Count()
  10. };
  11. return View(await data.AsNoTracking().ToListAsync());
  12. }
复制代码

LINQ 语句将学生实体按修读日期分组,计算每个组中的实体数并将结果存储在EnrollmentDateGroup视图模型对象的集合中。

在 Entity Framework Core 1.0 版本中,整个结果集都会返回到客户端,并在客户端上进行分组。 在某些场景下,这会导致性能问题。 请务必使用用符合生产规模的数据来测试性能,如有必要使用原始 SQL 语句在服务器上进行分组。 有关如何使用原始的 SQL ,请参阅本系列最后一个教程。

修改关于视图

Views/Home/About.cshtml文件替换为以下代码:

  1. @model IEnumerable<ContosoUniversity.Models.SchoolViewModels.EnrollmentDateGroup>
  2. @{
  3. ViewData["Title"] = "Student Body Statistics";
  4. }
  5. <h2>Student Body Statistics</h2>
  6. <table>
  7. <tr>
  8. <th>
  9. Enrollment Date
  10. </th>
  11. <th>
  12. Students
  13. </th>
  14. </tr>
  15. @foreach (var item in Model)
  16. {
  17. <tr>
  18. <td>
  19. @Html.DisplayFor(modelItem => item.EnrollmentDate)
  20. </td>
  21. <td>
  22. @item.StudentCount
  23. </td>
  24. </tr>
  25. }
  26. </table>
复制代码

运行应用并转到关于页面。 表格中显示了每个修读日期的学生计数。

有关页面

摘要

在本教程中,你已了解如何执行排序、 筛选、 分页和分组。 在下一个的教程中,你将了解如何使用迁移来处理数据模型更改。


来源:https://blog.csdn.net/qq_17004327/article/details/78906596
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?会员注册

×
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2024-12-27 01:39 , Processed in 0.649318 second(s), 27 queries .

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表