Skip to content

AbpTypeahead ​

使用 AbpTypeahead 选择按需搜索的作者、用户或其他关联对象。

值与显示文字 ​

vue
<template>
  <AbpFormField v-slot="{ id }" label="Author" hint="Type at least two characters, such as Au">
    <AbpTypeahead
      :id="id"
      v-model="authorId"
      v-model:display-value="authorName"
      :search="search"
      :min-length="2"
      :debounce="300"
      clearable
    >
      <template #item="{ item }"
        ><strong>{{ item.label }}</strong></template
      >
      <template #empty>No matching author</template>
    </AbpTypeahead>
  </AbpFormField>
  <output>Value sent to the API: {{ authorId ?? 'none' }}; display: {{ authorName }}</output>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import { AbpFormField, AbpTypeahead, type AbpTypeaheadItem } from '@lsw-abpvue/theme-shared';
const authorId = ref<string | null>('austen');
const authorName = ref('Jane Austen');
const authors: AbpTypeaheadItem[] = [
  { value: 'austen', label: 'Jane Austen' },
  { value: 'orwell', label: 'George Orwell' },
];
async function search(term: string, signal: AbortSignal): Promise<readonly AbpTypeaheadItem[]> {
  signal.throwIfAborted();
  // For a backend lookup, forward signal to RestService's request configuration.
  return authors.filter(item => item.label.toLowerCase().includes(term.toLowerCase()));
}
</script>

模型保存作者 id,displayValue 保存名称。详情已返回名称时,编辑表单可以直接设置两者,不需要只为显示文字再查一次。

连接 Identity 用户 API ​

按下面示例创建 src/pages/UserAssignmentPage.vue,保留 Core、OAuth、路由、Basic Theme 提供者,安装 Identity 以使用其 /proxy 入口。后端需要暴露 Identity 用户 API,并授予调用者 AbpIdentity.Users。无需假定自定义作者端点。

vue
<template>
  <AbpPage title="AbpIdentity::Users">
    <AbpFormField v-slot="{ id, describedBy }" label="Assigned user" hint="Search by username">
      <AbpTypeahead
        :id="id"
        v-model="userId"
        v-model:display-value="userName"
        :search="search"
        :disabled="loadingRecord"
        :aria-describedby="describedBy"
        :min-length="2"
        clearable
      >
        <template #item="{ item }"
          ><strong>{{ item.label }}</strong></template
        >
        <template #empty>{{
          searchFailed ? 'Search failed. Edit the query to retry.' : 'No matching user'
        }}</template>
      </AbpTypeahead>
    </AbpFormField>
    <p v-if="recordFailed" role="alert">
      The selected user could not be loaded.
      <AbpButton variant="secondary" size="sm" @click="loadSelectedUser">Retry</AbpButton>
    </p>
    <output>Assigned user id: {{ userId ?? 'none' }}</output>
  </AbpPage>
</template>

<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from 'vue';
import { inject, useCurrentUser } from '@lsw-abpvue/core';
import { AbpPage } from '@lsw-abpvue/components';
import { IdentityUserService } from '@lsw-abpvue/identity/proxy';
import {
  AbpButton,
  AbpFormField,
  AbpTypeahead,
  type AbpTypeaheadItem,
} from '@lsw-abpvue/theme-shared';

const users = inject(IdentityUserService);
const initialUserId = useCurrentUser().user.value.id;
const userId = ref<string | null>(initialUserId ?? null);
const userName = ref('');
const loadingRecord = ref(false);
const recordFailed = ref(false);
const searchFailed = ref(false);
const recordRequest = new AbortController();
onBeforeUnmount(() => recordRequest.abort());
onMounted(loadSelectedUser);

async function loadSelectedUser(): Promise<void> {
  if (!initialUserId || loadingRecord.value) return;
  loadingRecord.value = true;
  recordFailed.value = false;
  try {
    const user = await users.get(initialUserId, { signal: recordRequest.signal });
    userId.value = user.id ?? null;
    userName.value = user.userName ?? '';
  } catch {
    if (!recordRequest.signal.aborted) recordFailed.value = true;
  } finally {
    loadingRecord.value = false;
  }
}

async function search(term: string, signal: AbortSignal): Promise<readonly AbpTypeaheadItem[]> {
  searchFailed.value = false;
  try {
    const result = await users.getList(
      { filter: term, sorting: 'userName', skipCount: 0, maxResultCount: 10 },
      { signal },
    );
    return result.items.flatMap(user =>
      user.id ? [{ value: user.id, label: user.userName ?? user.id }] : [],
    );
  } catch {
    // The control expects a resolved list; RestService has already reported a real failure.
    if (!signal.aborted) searchFailed.value = true;
    return [];
  }
}
</script>

在已有路由数组注册 /user-assignment,设置组件导入与 requiresAuthentication: true。示例预选当前用户来演示编辑;业务编辑器将 initialUserId 替换为详情 DTO 中的 id。DTO 已包含标签时直接设置两个模型,跳过详情查询。

search 回调通过 IdentityUserService 发送 filter、skipCount、maxResultCount,传递控件信号,将 IdentityUserDto 映射成选项。保存 DTO 只包含 id,displayValue 是显示文字。

选中用户加载失败显示 Retry;empty 插槽区分搜索失败和成功但无结果。RestService 报告失败后,回调按控件契约返回已完成的列表,忽略取消。失败不会变成成功分配。

请求生命周期 ​

minLength 默认 1,debounce 默认 300 毫秒。输入过短不搜索,新词或卸载取消过时请求。请求须遵循信号以及时释放网络操作。失败不等于成功的空结果,应通过请求/错误层报告。

定制结果 ​

item 插槽接收 item、active,empty 插槽解释成功的空结果。保留键盘选择,避免嵌套链接或按钮。select 事件传选项,清空时为 null,页面可以更新关联状态。

清空必填选择需要表单验证,不能把显示标签作为后端 id。参见请求生命周期与表单。

属性、事件与插槽 ​

源码

类型来自公开契约,默认表达式来自当前实现。短横线表示没有显式默认值;省略的可选布尔属性通常为 false。

Props ​

名称类型必填默认值
modelValueAbpOptionValue | undefined否—
displayValuestring | undefined否''
search(term: string, signal: AbortSignal) => Promise<readonly AbpTypeaheadItem[]>是—
debouncenumber | undefined否300
minLengthnumber | undefined否1
placeholderstring | undefined否—
disabledboolean | undefined否—
readonlyboolean | undefined否—
invalidboolean | undefined否—
clearableboolean | undefined否—
idstring | undefined否—
namestring | undefined否—
ariaDescribedbystring | undefined否—
ariaLabelstring | undefined否—

Events ​

名称参数
update:modelValue[value: AbpOptionValue]
update:displayValue[value: string]
select[item: AbpTypeaheadItem | null]

Slots ​

名称上下文
item(context: { item: AbpTypeaheadItem; active: boolean }) => unknown
empty() => unknown

非官方社区项目,采用 MIT 许可证,与 Volosoft 无关。