You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

JHipster+Angular下拉框滚动加载更多用户数据求助

问题描述

在JHipster 7.9.3 + Angular 14.2.1项目中,User与UserProfile为一对一关联,创建/编辑UserProfile时需绑定对应用户。当前打开创建/编辑页面时,/api/users接口默认仅返回前20条数据,当用户数量超过20时,第21位及以后的用户无法在下拉选择框中显示。需保留下拉框滚动形式,实现滚动加载更多用户数据。

解决方案

前端修改(Angular)

原生<select>无法监听滚动事件实现加载更多,因此改用自定义下拉容器结合滚动加载逻辑:

1. 修改组件模板(user-profile-update-component.html)

替换原生select为自定义下拉结构,添加滚动监听和加载状态提示:

<div class="row mb-3">
  <label class="form-label" for="field_user">User</label>
  <!-- 自定义下拉触发容器 -->
  <div class="form-control position-relative" (click)="toggleDropdown()" id="field_user" data-cy="user">
    <span *ngIf="editForm.get('user')!.value">{{ editForm.get('user')!.value.login }}</span>
    <span *ngIf="!editForm.get('user')!.value" class="text-muted">请选择用户</span>
    <span class="position-absolute end-0 top-50 translate-middle-y me-2">▼</span>
    <!-- 下拉选项列表(带滚动) -->
    <div *ngIf="dropdownOpen" class="position-absolute top-100 start-0 end-0 bg-white border shadow-sm mt-1 max-h-40 overflow-auto" (scroll)="onDropdownScroll($event)">
      <div *ngIf="loading" class="text-center p-2">加载中...</div>
      <div 
        class="p-2 cursor-pointer" 
        *ngFor="let userOption of usersSharedCollection" 
        (click)="selectUser(userOption)"
        [class.bg-primary-subtle]="editForm.get('user')!.value?.id === userOption.id"
      >
        {{ userOption.login }}
      </div>
      <div *ngIf="hasMore && !loading" class="text-center p-2 text-muted">滚动加载更多</div>
    </div>
  </div>
  <input type="hidden" formControlName="user">
</div>
<div *ngIf="editForm.get(['user'])!.invalid && (editForm.get(['user'])!.dirty || editForm.get(['user'])!.touched)">
  <small class="form-text text-danger" *ngIf="editForm.get(['user'])?.errors?.required"> 此字段为必填项。 </small>
</div>

2. 修改组件逻辑(user-profile-update-component.ts)

添加分页变量、滚动加载及下拉控制逻辑:

import { Component, OnInit } from '@angular/core';
import { HttpResponse } from '@angular/common/http';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs';
import { finalize, map } from 'rxjs/operators';

import { UserProfileFormService, UserProfileFormGroup } from './user-profile-form.service';
import { IUserProfile } from '../user-profile.model';
import { UserProfileService } from '../service/user-profile.service';
import { IUser } from 'app/entities/user/user.model';
import { UserService } from 'app/entities/user/user.service';
import { Pagination } from 'app/core/request/request.model';

@Component({
  selector: 'jhi-user-profile-update',
  templateUrl: './user-profile-update.component.html',
  styles: [`
    .cursor-pointer { cursor: pointer; }
    .max-h-40 { max-height: 10rem; }
  `]
})
export class UserProfileUpdateComponent implements OnInit {
  isSaving = false;
  userProfile: IUserProfile | null = null;

  usersSharedCollection: IUser[] = [];
  dropdownOpen = false;
  loading = false;
  currentPage = 0;
  pageSize = 20;
  hasMore = true;

  editForm: UserProfileFormGroup = this.userProfileFormService.createUserProfileFormGroup();

  constructor(
    protected userProfileService: UserProfileService,
    protected userProfileFormService: UserProfileFormService,
    protected userService: UserService,
    protected activatedRoute: ActivatedRoute
  ) {}

  compareUser = (o1: IUser | null, o2: IUser | null): boolean => this.userService.compareUser(o1, o2);

  ngOnInit(): void {
    this.activatedRoute.data.subscribe(({ userProfile }) => {
      this.userProfile = userProfile;
      if (userProfile) {
        this.updateForm(userProfile);
      }
      // 加载第一页用户数据
      this.loadUsersPage(0);
    });
  }

  toggleDropdown(): void {
    this.dropdownOpen = !this.dropdownOpen;
  }

  selectUser(user: IUser): void {
    this.editForm.patchValue({ user });
    this.dropdownOpen = false;
  }

  onDropdownScroll(event: Event): void {
    const target = event.target as HTMLElement;
    // 滚动到底部附近时加载下一页
    if (target.scrollTop + target.clientHeight >= target.scrollHeight - 10 && this.hasMore && !this.loading) {
      this.currentPage++;
      this.loadUsersPage(this.currentPage);
    }
  }

  loadUsersPage(page: number): void {
    this.loading = true;
    const pagination: Pagination = { page, size: this.pageSize };
    this.userService
      .query(pagination)
      .pipe(map((res: HttpResponse<IUser[]>) => res.body ?? []))
      .subscribe({
        next: (users) => {
          this.usersSharedCollection = this.userService.addUserToCollectionIfMissing<IUser>(this.usersSharedCollection, ...users);
          this.hasMore = users.length === this.pageSize;
          this.loading = false;
        },
        error: () => this.loading = false
      });
  }

  previousState(): void {
    window.history.back();
  }

  save(): void {
    this.isSaving = true;
    const userProfile = this.userProfileFormService.getUserProfile(this.editForm);
    if (userProfile.id !== null) {
      this.subscribeToSaveResponse(this.userProfileService.update(userProfile));
    } else {
      this.subscribeToSaveResponse(this.userProfileService.create(userProfile));
    }
  }

  protected subscribeToSaveResponse(result: Observable<HttpResponse<IUserProfile>>): void {
    result.pipe(finalize(() => this.onSaveFinalize())).subscribe({
      next: () => this.onSaveSuccess(),
      error: () => this.onSaveError(),
    });
  }

  protected onSaveSuccess(): void {
    this.previousState();
  }

  protected onSaveError(): void {}

  protected onSaveFinalize(): void {
    this.isSaving = false;
  }

  protected updateForm(userProfile: IUserProfile): void {
    this.userProfile = userProfile;
    this.userProfileFormService.resetForm(this.editForm, userProfile);
    if (userProfile.user) {
      this.usersSharedCollection = this.userService.addUserToCollectionIfMissing<IUser>(this.usersSharedCollection, userProfile.user);
    }
  }
}

后端修改(Java)

后端接口已支持分页,需确保分页参数能正常传递:

检查PublicUserResource中的参数校验方法

确保onlyContainsAllowedProperties方法允许分页排序的合法字段,示例如下:

private boolean onlyContainsAllowedProperties(Pageable pageable) {
    Set<String> allowedProperties = Set.of("id", "login", "firstName", "lastName");
    return pageable.getSort().stream()
        .allMatch(order -> allowedProperties.contains(order.getProperty()));
}

该方法用于限制排序字段的范围,避免非法排序请求,需根据业务需求调整允许的字段列表。

内容的提问来源于stack exchange,提问作者MiMoy

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 13:02:06