【问题标题】:Angular Material 2 table server-side paginationAngular Material 2 表服务器端分页
【发布时间】:2022-05-06 00:53:18
【问题描述】:

我正在尝试实现 Angular Material 2,MatPaginator 服务器端分页。我怎样才能做到这一点?

下面是代码示例:

  <div class="example-container mat-elevation-z8">
  <mat-table #table [dataSource]="dataSource">

    <!-- Position Column -->
    <ng-container matColumnDef="position">
      <mat-header-cell *matHeaderCellDef> No. </mat-header-cell>
      <mat-cell *matCellDef="let element"> {{element.position}} </mat-cell>
    </ng-container>

    <!-- Name Column -->
    <ng-container matColumnDef="name">
      <mat-header-cell *matHeaderCellDef> Name </mat-header-cell>
      <mat-cell *matCellDef="let element"> {{element.name}} </mat-cell>
    </ng-container>

    <!-- Weight Column -->
    <ng-container matColumnDef="weight">
      <mat-header-cell *matHeaderCellDef> Weight </mat-header-cell>
      <mat-cell *matCellDef="let element"> {{element.weight}} </mat-cell>
    </ng-container>

    <!-- Symbol Column -->
    <ng-container matColumnDef="symbol">
      <mat-header-cell *matHeaderCellDef> Symbol </mat-header-cell>
      <mat-cell *matCellDef="let element"> {{element.symbol}} </mat-cell>
    </ng-container>

    <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
    <mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
  </mat-table>

  <mat-paginator #paginator
                 [pageSize]="10"
                 [pageSizeOptions]="[5, 10, 20]">
  </mat-paginator>
</div>

分页组件:

import {Component, ViewChild} from '@angular/core';
import {MatPaginator, MatTableDataSource} from '@angular/material';

/**
 * @title Table with pagination
 */
@Component({
  selector: 'table-pagination-example',
  styleUrls: ['table-pagination-example.css'],
  templateUrl: 'table-pagination-example.html',
})
export class TablePaginationExample {
  displayedColumns = ['position', 'name', 'weight', 'symbol'];
  dataSource = new MatTableDataSource<Element>(ELEMENT_DATA);

  @ViewChild(MatPaginator) paginator: MatPaginator;

  /**
   * Set the paginator after the view init since this component will
   * be able to query its view for the initialized paginator.
   */
  ngAfterViewInit() {
    this.dataSource.paginator = this.paginator;
  }
}

export interface Element {
  name: string;
  position: number;
  weight: number;
  symbol: string;
}

const ELEMENT_DATA: Element[] = [
  {position: 1, name: 'Hydrogen', weight: 1.0079, symbol: 'H'},
  {position: 2, name: 'Helium', weight: 4.0026, symbol: 'He'},
  {position: 3, name: 'Lithium', weight: 6.941, symbol: 'Li'},
  {position: 4, name: 'Beryllium', weight: 9.0122, symbol: 'Be'},
  {position: 5, name: 'Boron', weight: 10.811, symbol: 'B'},
  {position: 6, name: 'Carbon', weight: 12.0107, symbol: 'C'},
  {position: 7, name: 'Nitrogen', weight: 14.0067, symbol: 'N'},
  {position: 8, name: 'Oxygen', weight: 15.9994, symbol: 'O'},
  {position: 9, name: 'Fluorine', weight: 18.9984, symbol: 'F'},
  {position: 10, name: 'Neon', weight: 20.1797, symbol: 'Ne'},
  {position: 11, name: 'Sodium', weight: 22.9897, symbol: 'Na'},
  {position: 12, name: 'Magnesium', weight: 24.305, symbol: 'Mg'},
  {position: 13, name: 'Aluminum', weight: 26.9815, symbol: 'Al'},
  {position: 14, name: 'Silicon', weight: 28.0855, symbol: 'Si'},
  {position: 15, name: 'Phosphorus', weight: 30.9738, symbol: 'P'},
  {position: 16, name: 'Sulfur', weight: 32.065, symbol: 'S'},
  {position: 17, name: 'Chlorine', weight: 35.453, symbol: 'Cl'},
  {position: 18, name: 'Argon', weight: 39.948, symbol: 'Ar'},
  {position: 19, name: 'Potassium', weight: 39.0983, symbol: 'K'},
  {position: 20, name: 'Calcium', weight: 40.078, symbol: 'Ca'},
];

如何实现服务器端分页,这将触发下一页点击的更改事件或页面大小更改以获取下一组记录。

https://stackblitz.com/angular/qxxpqbqolyb?file=app%2Ftable-pagination-example.ts

【问题讨论】:

    标签: angular typescript angular-material2


    【解决方案1】:

    根据 Wilfredo 的回答 (https://stackoverflow.com/a/47994113/986160),我编译了一个完整的工作示例,因为问题中也缺少一些部分。这是使用 Angular 5 和 Material Design 进行服务器端分页和排序的更一般的案例(仍然需要插入过滤) - 希望它对某人有所帮助:

    分页组件:

    import { ViewChild, Component, Inject, OnInit, AfterViewInit } from '@angular/core';
    import { EntityJson } from './entity.json';
    import { EntityService } from './entity.service';
    import { MatPaginator, MatSort, MatTableDataSource } from '@angular/material';
    import { Observable } from 'rxjs/Observable';
    import { merge } from 'rxjs/observable/merge';
    import { of as observableOf } from 'rxjs/observable/of';
    import { catchError } from 'rxjs/operators/catchError';
    import { map } from 'rxjs/operators/map';
    import { startWith } from 'rxjs/operators/startWith';
    import { switchMap } from 'rxjs/operators/switchMap';
    
    @Component({
        selector: 'entity-latest-page',
        providers: [EntityService],
        styles: [`
            :host mat-table {
               display: flex;
               flex-direction: column;
               min-width: 100px;
               max-width: 800px;
               margin: 0 auto;
            }
        `],
        template:
        `<mat-card>
            <mat-card-title>Entity List 
            <button mat-button [routerLink]="['/create/entity']">
                CREATE
            </button>
            </mat-card-title>
            <mat-card-content>
                <mat-table #table matSort [dataSource]="entitiesDataSource" matSort class="mat-elevation-z2">
                    <ng-container matColumnDef="id">
                        <mat-header-cell *matHeaderCellDef mat-sort-header> Id </mat-header-cell>
                        <mat-cell *matCellDef="let element"> {{element.id}} </mat-cell>
                    </ng-container>
                    <ng-container matColumnDef="name">
                        <mat-header-cell *matHeaderCellDef  mat-sort-header> Name </mat-header-cell>
                        <mat-cell *matCellDef="let element"> {{element.name}} </mat-cell>
                    </ng-container>
                    <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
                    <mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
                </mat-table>
            </mat-card-content>
            <mat-card-content>
                <mat-paginator #paginator [length]="resultsLength"
                    [pageSize]="5"
                    [pageSizeOptions]="[5, 10, 20]">
                </mat-paginator>
            </mat-card-content>
        </mat-card>
        `
    })
    export class EntityLatestPageComponent implements AfterViewInit {
    
        private entities: EntityJson[];
        private entitiesDataSource: MatTableDataSource<EntityJson> = new MatTableDataSource();
        private displayedColumns = ['id', 'name'];
    
        resultsLength = 0;
        isLoadingResults = false;
        isRateLimitReached = false;
    
        @ViewChild(MatPaginator) paginator: MatPaginator;
        @ViewChild(MatSort) sort: MatSort;
    
        public constructor( @Inject(EntityService) private entityService: EntityService) {
        }
    
        public ngAfterViewInit() {
    
            // If the user changes the sort order, reset back to the first page.
            this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0);
            merge(this.sort.sortChange, this.paginator.page)
            .pipe(
                startWith({}),
                switchMap(() => {
                this.isLoadingResults = true;
                return this.entityService.fetchLatest(this.sort.active, this.sort.direction, 
                      this.paginator.pageIndex + 1, this.paginator.pageSize, 
                      (total) =>  this.resultsLength = total);
                }),
                map(data => {
                this.isLoadingResults = false;
                this.isRateLimitReached = false;
                //alternatively to response headers;
                //this.resultsLength = data.total;
                return data;
                }),
                catchError(() => {
                this.isLoadingResults = false;
                this.isRateLimitReached = true;
                return observableOf([]);
                })
            ).subscribe(data => this.entitiesDataSource.data = data);
        } 
    }
    

    服务:

    import { EntityJson } from './entity.json';
    import { ApiHelper } from '../common/api.helper';
    import { Http, Headers, Response, RequestOptions } from '@angular/http';
    import { Inject, Injectable } from '@angular/core';
    import { Observable } from 'rxjs/Observable';
    import { AuthenticationService } from '../auth/authentication.service';
    import { stringify } from 'query-string';
    
    @Injectable()
    export class EntityService {
    
        private options: RequestOptions;
        private apiPrefix: string;
        private apiEndpoint: string;
    
        constructor(
            @Inject(Http) private http: Http,
            @Inject(AuthenticationService) private authService: AuthenticationService) {
    
            this.options = authService.prepareRequestHeaders();
            this.apiPrefix = 'http://localhost:4200/api/v1/';
            this.apiEndpoint = this.apiPrefix + 'entities';
        }
    
        public fetchLatest(sort: string = '', order: string = '', page: number = 1, perPage: number = 5, initTotal: Function = () => {}): Observable<EntityJson[]> {
            return this.http.get(this.apiEndpoint +'?' + EntityService.createUrlQuery({sort: {field: sort, order: order}, pagination: { page, perPage }}), this.options)
                .map((res) => {
                    const total = res.headers.get('x-total-count').split('/').pop();
                    initTotal(total);
                    return JSON.parse(res.text()).content
                });
        }
    
        //should be put in a util
        static createUrlQuery(params: any) {
            if (!params) {
                return "";
            }
    
            let page;
            let perPage;
            let field;
            let order;
            let query: any = {};
            if (params.pagination) {
                 page = params.pagination.page;
                 perPage =  params.pagination.perPage;
                 query.range = JSON.stringify([
                    page,
                    perPage,
                ]);
            }
            if (params.sort) {
                field = params.sort.field;
                order = params.sort.order;
                if (field && order) {
                    query.sort = JSON.stringify([field, order]);
                }
                else {
                    query.sort = JSON.stringify(['id', 'ASC']);
                }
            }
            if (!params.filter) {
                params.filter = {};
            }
            if (Array.isArray(params.ids)) {
                params.filter.id = params.ids;
            }
    
            if (params.filter) {
                query.filter = JSON.stringify(params.filter)
            }
            console.log(query, stringify(query));
            return stringify(query);
        }
    }
    

    Spring Boot Rest 控制器端点

    @GetMapping("entities")
    public Iterable<Entity> filterBy(
            @RequestParam(required = false, name = "filter") String filterStr,
            @RequestParam(required = false, name = "range") String rangeStr, @RequestParam(required = false, name="sort") String sortStr) {
        //my own helpers - for source: https://github.com/zifnab87/react-admin-java-rest
        //FilterWrapper wrapper = filterService.extractFilterWrapper(filterStr, rangeStr, sortStr);
        //return filterService.filterBy(wrapper, repo);
    }
    

    一些注意事项:

    1. 确保导入模块:MatTableModuleMatPaginatorModuleMatSortModule 以及 Material Design 的其他模块。
    2. 我决定从我通过 Spring Boot @ControllerAdvice 填充的 Response-Header x-total-count 填充 resultsLength(总计)。或者,您可以从 EntityService 返回的对象中获取此信息(例如,Page 用于 Spring Boot)尽管这意味着您需要使用 any 作为返回类型或为项目中的所有实体声明包装类对象如果您想“类型安全”。

    【讨论】:

    • 嗨。您如何在示例中链接分页器和数据源?
    • 嗨! resultsLength 包含元素的总数。对于我的情况,我使用 ControllerAdvice 发送此信息,并添加一个标头,其中包含与 JPA 存储库查询匹配的元素总数。然后根据pageSize分页器计算需要的页数。检查我在 fetchLatest() 中传递的箭头函数。希望这会有所帮助。
    • Sergey,我认为关键是它们根本没有链接。只是间接地通过提供更新数据源的事件并读取总的结果长度。
    • @Michali,你能解释一下关于从后端获取数据的 EntityLatestPageComponent 类逻辑吗?这个类的ngAfterViewInit方法里面的逻辑很难理解
    • @Atul 你可以在这里查看我的 github 存储库:github.com/zifnab87/react-admin-java-rest/tree/master/src/main/… 这是一个支持 admin-on-rest/react-admin 默认数据提供者的 REST 端点约定的实现:@ 987654323@ - 至于 ngAfterViewInit 我只是从答案扩展它:stackoverflow.com/a/47994113/986160
    【解决方案2】:

    我在 angular material docs 中的 Table retrieving data through HTTP 之后发现了这个问题。

    例子说的是,使用ngAfterViewInit()加上observables来处理表格上的一切,分页,排序和其他你需要的东西,代码:

    import {Component, AfterViewInit, ViewChild} from '@angular/core';
    import {HttpClient} from '@angular/common/http';
    import {MatPaginator, MatSort, MatTableDataSource} from '@angular/material';
    import {Observable} from 'rxjs/Observable';
    import {merge} from 'rxjs/observable/merge';
    import {of as observableOf} from 'rxjs/observable/of';
    import {catchError} from 'rxjs/operators/catchError';
    import {map} from 'rxjs/operators/map';
    import {startWith} from 'rxjs/operators/startWith';
    import {switchMap} from 'rxjs/operators/switchMap';
    
    /**
     * @title Table retrieving data through HTTP
     */
    @Component({
      selector: 'table-http-example',
      styleUrls: ['table-http-example.css'],
      templateUrl: 'table-http-example.html',
    })
    export class TableHttpExample implements AfterViewInit {
      displayedColumns = ['created', 'state', 'number', 'title'];
      exampleDatabase: ExampleHttpDao | null;
      dataSource = new MatTableDataSource();
    
      resultsLength = 0;
      isLoadingResults = false;
      isRateLimitReached = false;
    
      @ViewChild(MatPaginator) paginator: MatPaginator;
      @ViewChild(MatSort) sort: MatSort;
    
      constructor(private http: HttpClient) {}
    
      ngAfterViewInit() {
        this.exampleDatabase = new ExampleHttpDao(this.http);
    
        // If the user changes the sort order, reset back to the first page.
        this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0);
    
        merge(this.sort.sortChange, this.paginator.page)
          .pipe(
            startWith({}),
            switchMap(() => {
              this.isLoadingResults = true;
              return this.exampleDatabase!.getRepoIssues(
                this.sort.active, this.sort.direction, this.paginator.pageIndex);
            }),
            map(data => {
              // Flip flag to show that loading has finished.
              this.isLoadingResults = false;
              this.isRateLimitReached = false;
              this.resultsLength = data.total_count;
    
              return data.items;
            }),
            catchError(() => {
              this.isLoadingResults = false;
              // Catch if the GitHub API has reached its rate limit. Return empty data.
              this.isRateLimitReached = true;
              return observableOf([]);
            })
          ).subscribe(data => this.dataSource.data = data);
      }
    }
    
    export interface GithubApi {
      items: GithubIssue[];
      total_count: number;
    }
    
    export interface GithubIssue {
      created_at: string;
      number: string;
      state: string;
      title: string;
    }
    
    /** An example database that the data source uses to retrieve data for the table. */
    export class ExampleHttpDao {
      constructor(private http: HttpClient) {}
    
      getRepoIssues(sort: string, order: string, page: number): Observable<GithubApi> {
        const href = 'https://api.github.com/search/issues';
        const requestUrl =
            `${href}?q=repo:angular/material2&sort=${sort}&order=${order}&page=${page + 1}`;
    
        return this.http.get<GithubApi>(requestUrl);
      }
    }
    

    由于 observables,一切都在 ngAfterViewInit 内部处理。 this.resultsLength = data.total_count; 行期望您的服务返回具有总寄存器计数的数据,在我的情况下,我使用的是 springboot,它返回了我需要的所有内容。

    如果您需要更多说明,请写下任何评论,我会更新答案,但检查文档中的示例您会明白的。

    【讨论】:

    • 我也在尝试将分页器与服务器端分页一起使用,您如何管理下一页和上一页事件?谢谢
    • @bre 您如何将事件订阅到控件,它将检测何时触发任何事件并刷新数据。
    • 嗨,感谢快速重播,但我结束了使用 ngxdatatable :-)
    • @Wilfredo:你能分享一下 Rest Controller 的实现吗?我在将它与 Rest Controller 集成时遇到了问题。
    • @Wilfredo:我发现并与 Rest Controller 集成。谢谢
    【解决方案3】:

    这是Michail Michailidis's answerofficial table pagination example 的组合,被压缩到一个文件中,并使用一个返回 Observable 并模拟延迟的模拟“网络”服务类。

    如果您有一个 Material 2 + Angular 5 项目启动并运行,您应该能够将其放入一个新的组件文件中,将其添加到您的模块列表中,然后开始修改。至少它应该是一个较低的入门门槛。

    import { ViewChild, Component, Inject, OnInit, AfterViewInit } from '@angular/core';
    import { MatPaginator, MatSort, MatTableDataSource } from '@angular/material';
    import { Observable } from 'rxjs/Observable';
    import { merge } from 'rxjs/observable/merge';
    import { of as observableOf } from 'rxjs/observable/of';
    import { catchError } from 'rxjs/operators/catchError';
    import { map } from 'rxjs/operators/map';
    import { startWith } from 'rxjs/operators/startWith';
    import { switchMap } from 'rxjs/operators/switchMap';
    import { BehaviorSubject } from 'rxjs/BehaviorSubject';
    
    @Component({
      selector: 'app-element-table',
      styles: [`
        :host mat-table {
          display: flex;
          flex-direction: column;
          min-width: 100px;
          max-width: 800px;
          margin: 0 auto;
        }
      `],
      template: `
      <mat-card>
        <mat-card-title>Element List</mat-card-title>
        <mat-card-content>
          <mat-table #table matSort [dataSource]="elementDataSource" class="mat-elevation-z2">
    
            <!-- Position Column -->
            <ng-container matColumnDef="position">
             <mat-header-cell *matHeaderCellDef mat-sort-header> No. </mat-header-cell>
             <mat-cell *matCellDef="let element"> {{element.position}} </mat-cell>
            </ng-container>
    
            <!-- Name Column -->
            <ng-container matColumnDef="name">
             <mat-header-cell *matHeaderCellDef mat-sort-header> Name </mat-header-cell>
             <mat-cell *matCellDef="let element"> {{element.name}} </mat-cell>
            </ng-container>
    
            <!-- Weight Column -->
            <ng-container matColumnDef="weight">
             <mat-header-cell *matHeaderCellDef mat-sort-header> Weight </mat-header-cell>
             <mat-cell *matCellDef="let element"> {{element.weight}} </mat-cell>
            </ng-container>
    
            <!-- Symbol Column -->
            <ng-container matColumnDef="symbol">
             <mat-header-cell *matHeaderCellDef mat-sort-header> Symbol </mat-header-cell>
             <mat-cell *matCellDef="let element"> {{element.symbol}} </mat-cell>
            </ng-container>
    
            <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
            <mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
          </mat-table>
        </mat-card-content>
        <mat-card-content>
          <mat-paginator #paginator [length]="resultsLength"
            [pageSize]="5"
            [pageSizeOptions]="[5, 10, 20]"
            showFirstLastButtons>
          </mat-paginator>
        </mat-card-content>
      </mat-card>
      `
    })
    export class ElementTableComponent implements AfterViewInit {
      public elementDataSource = new MatTableDataSource<PeriodicElement>();
      public displayedColumns = ['position', 'name', 'weight', 'symbol'];
    
      private entities: PeriodicElement[];
      private elementService = new ElementService();
    
      resultsLength = 0;
      isLoadingResults = false;
      isRateLimitReached = false;
    
      @ViewChild(MatPaginator) paginator: MatPaginator;
      @ViewChild(MatSort) sort: MatSort;
    
      public constructor() {
      }
    
      public ngAfterViewInit() {
        this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0);
        merge(this.sort.sortChange, this.paginator.page)
          .pipe(
            startWith({ data: [], resultsLength: 0 } as ElementResult),
            switchMap(() => {
              this.isLoadingResults = true;
              return this.elementService.fetchLatest(
                this.sort.active, this.sort.direction,
                this.paginator.pageIndex + 1, this.paginator.pageSize);
            }),
            map(result => {
              this.isLoadingResults = false;
              this.isRateLimitReached = false;
              this.resultsLength = result.resultsLength;
              return result.data;
            }),
            catchError(() => {
              this.isLoadingResults = false;
              this.isRateLimitReached = true;
              return observableOf([]);
            })
          ).subscribe(data => this.elementDataSource.data = data);
      }
    }
    
    // Simulates server-side rendering
    class ElementService {
      constructor() { }
    
      fetchLatest(active: string, direction: string, pageIndex: number, pageSize: number): Observable<ElementResult> {
    
        active = active || 'position';
        const cmp = (a, b) => (a[active] < b[active] ? -1 : 1);
        const rev = (a, b) => cmp(b, a);
        const [l, r] = [(pageIndex - 1) * pageSize, pageIndex * pageSize];
    
        const data = [...ELEMENT_DATA]
          .sort(direction === 'desc' ? rev : cmp)
          .filter((_, i) => l <= i && i < r);
    
        // 1 second delay to simulate network request delay
        return new BehaviorSubject({ resultsLength: ELEMENT_DATA.length, data }).debounceTime(1000);
      }
    }
    
    interface ElementResult {
      resultsLength: number;
      data: PeriodicElement[];
    }
    
    export interface PeriodicElement {
      name: string;
      position: number;
      weight: number;
      symbol: string;
    }
    
    const ELEMENT_DATA: PeriodicElement[] = [
      { position: 1, name: 'Hydrogen', weight: 1.0079, symbol: 'H' },
      { position: 2, name: 'Helium', weight: 4.0026, symbol: 'He' },
      { position: 3, name: 'Lithium', weight: 6.941, symbol: 'Li' },
      { position: 4, name: 'Beryllium', weight: 9.0122, symbol: 'Be' },
      { position: 5, name: 'Boron', weight: 10.811, symbol: 'B' },
      { position: 6, name: 'Carbon', weight: 12.0107, symbol: 'C' },
      { position: 7, name: 'Nitrogen', weight: 14.0067, symbol: 'N' },
      { position: 8, name: 'Oxygen', weight: 15.9994, symbol: 'O' },
      { position: 9, name: 'Fluorine', weight: 18.9984, symbol: 'F' },
      { position: 10, name: 'Neon', weight: 20.1797, symbol: 'Ne' },
      { position: 11, name: 'Sodium', weight: 22.9897, symbol: 'Na' },
      { position: 12, name: 'Magnesium', weight: 24.305, symbol: 'Mg' },
      { position: 13, name: 'Aluminum', weight: 26.9815, symbol: 'Al' },
      { position: 14, name: 'Silicon', weight: 28.0855, symbol: 'Si' },
      { position: 15, name: 'Phosphorus', weight: 30.9738, symbol: 'P' },
      { position: 16, name: 'Sulfur', weight: 32.065, symbol: 'S' },
      { position: 17, name: 'Chlorine', weight: 35.453, symbol: 'Cl' },
      { position: 18, name: 'Argon', weight: 39.948, symbol: 'Ar' },
      { position: 19, name: 'Potassium', weight: 39.0983, symbol: 'K' },
      { position: 20, name: 'Calcium', weight: 40.078, symbol: 'Ca' },
    ];
    

    顺便说一句,如果您希望自己实现过滤,this issue on material2 about filtering 可能会很有用。

    【讨论】:

    • 我收到错误:键入'{} | PeriodicElement[]' 不可分配给类型 'PeriodicElement[]'。类型“{}”不可分配给类型“PeriodicElement[]”。我 linie 102: ).subscribe(data => this.elementDataSource.data = data);有什么想法吗?
    • 我唯一能想到的就是类型统一规则变了,可能是'return observableOf([]);' catchError 内部不再与 PeriodicElement[] 完全统一。尝试:'return observableOf([] as PeriodicElement[]);'
    猜你喜欢
    • 2018-05-17
    • 2013-11-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-08-13
    • 1970-01-01
    • 2015-07-15
    • 2016-01-05
    相关资源
    最近更新 更多