Skip to content

实体操作扩展 ​

行操作贡献者修改内置菜单。多个可见操作显示为下拉菜单,单个操作显示为按钮,没有可见操作时不显示控件。

替换一个操作 ​

本例替换编辑操作,仅对启用的用户显示。这是示例业务规则,不是 Identity 默认编辑策略。

ts
import { EntityAction } from '@lsw-abpvue/components';
import { IdentityComponents, USERS_PAGE, type IdentityConfigOptions } from '@lsw-abpvue/identity';
import type { IdentityUserDto } from '@lsw-abpvue/identity/proxy';

export const userActions = {
  entityActionContributors: {
    [IdentityComponents.Users]: [
      actions => {
        actions.dropByValue(action => action.text === 'AbpUi::Edit');
        actions.addHead(
          EntityAction.create<IdentityUserDto>({
            text: 'AbpUi::Edit',
            icon: 'bi bi-pencil',
            permission: 'AbpIdentity.Users.Update',
            visible: data => data?.record.isActive === true,
            action: data => data.getInjected(USERS_PAGE).edit(data.record),
          }),
        );
      },
    ],
  },
} satisfies IdentityConfigOptions;

注册到模块路由 ​

将示例保存为 src/identity-options.ts,在 src/routes.ts 使用导出的选项:

ts
import { lazyRoutes } from '@lsw-abpvue/core/router';
import { userActions } from './identity-options';

const identityRoute = lazyRoutes('/identity', () =>
  import('@lsw-abpvue/identity').then(module => module.createIdentityRoutes(userActions)),
);

将 identityRoute 放入应用路由数组,并保留启动时的 provideIdentityConfig()。替换已有 identity 路由,不要为同一前缀注册两份路由。模块配置改变后重启开发服务,再打开 /identity/users。

理解回调 ​

EntityAction<IdentityUserDto> 接收 PropData<IdentityUserDto>,data.record 是当前行。通过 data.getInjected(USERS_PAGE) 获取内置用户页面提供的命令,再调用 edit,保留原来的忙碌状态、DTO 读取和对话框流程。

默认编辑标签是 AbpUi::Edit。删除时匹配 key,不匹配翻译后的文字;不删除原操作会同时出现两个编辑操作。

permission 和 visible 必须同时满足。visible 可能在没有行记录时被调用,应保护可选的 data。icon 使用应用的图标类;showOnlyIcon 仍需要可访问的本地化标签。

执行自定义请求 ​

在回调中通过 data.getInjected 获取服务。例如获取生成服务,使用 ConfirmationService 确认,再等待请求完成。端点、权限和 DTO 必须由自己的后端提供。

回调可以返回 Promise,但表格不会自动为所有自定义操作管理忙碌状态。重复点击保护应放在页面拥有的命令或状态中,修改成功后明确刷新列表查询。

替换页面中的操作 ​

完整替换的页面如果渲染默认操作贡献者,需要自行提供 USERS_PAGE 命令。路由注入器不会为任意替换页面实现这些命令。包装原页面可以保留原来的命令提供者。

检查效果 ​

分别检查启用用户、停用用户和缺少 AbpIdentity.Users.Update 的登录用户。确认编辑只出现一次,打开原编辑对话框,服务端仍然执行授权。

相关内容见扩展行为与默认值。

打开自定义详情对话框 ​

创建 src/user-details.ts 与 src/components/UsersPageWithDetails.vue,并调整后者的服务导入。服务先读取完整 Identity 用户,再打开只读对话框,请求期间阻止重复点击。

ts
import { defineService, inject, onServiceDestroy } from '@lsw-abpvue/core';
import { IdentityUserService, type IdentityUserDto } from '@lsw-abpvue/identity/proxy';
import { EntityAction } from '@lsw-abpvue/components';
import { IdentityComponents, type IdentityConfigOptions } from '@lsw-abpvue/identity';
import { ref, shallowRef } from 'vue';

export const UserDetailsService = defineService('UserDetailsService', () => {
  const users = inject(IdentityUserService);
  const visible = ref(false);
  const busy = ref(false);
  const record = shallowRef<IdentityUserDto>();
  let request: AbortController | undefined;
  let generation = 0;
  function close(): void {
    generation++;
    request?.abort();
    request = undefined;
    visible.value = false;
    record.value = undefined;
    busy.value = false;
  }
  onServiceDestroy(close);
  return {
    visible,
    busy,
    record,
    close,
    async show(id: string): Promise<void> {
      if (busy.value) return;
      busy.value = true;
      const current = generation;
      request = new AbortController();
      try {
        const detail = await users.get(id, { signal: request.signal });
        if (current !== generation) return;
        record.value = detail;
        visible.value = true;
      } catch {
        /* Framework error handlers report the failed detail request. */
      } finally {
        if (current === generation) {
          busy.value = false;
          request = undefined;
        }
      }
    },
  };
});
export const userDetails = {
  entityActionContributors: {
    [IdentityComponents.Users]: [
      actions =>
        actions.addTail(
          EntityAction.create<IdentityUserDto>({
            text: 'BookStore::Details',
            icon: 'bi bi-info-circle',
            permission: 'AbpIdentity.Users',
            action: data => {
              if (data.record.id) return data.getInjected(UserDetailsService).show(data.record.id);
            },
          }),
        ),
    ],
  },
} satisfies IdentityConfigOptions;
vue
<template>
  <UsersPage />
  <AbpModal v-model:visible="visible" aria-label="User details">
    <template #header><h2 class="h5 mb-0">User details</h2></template>
    <dl v-if="record">
      <dt>User name</dt>
      <dd>{{ record.userName }}</dd>
      <dt>Email</dt>
      <dd>{{ record.email }}</dd>
    </dl>
    <template #footer="{ close }"
      ><AbpButton variant="secondary" @click="close">Close</AbpButton></template
    >
  </AbpModal>
</template>

<script setup lang="ts">
import { onBeforeUnmount } from 'vue';
import { inject } from '@lsw-abpvue/core';
import { UsersPage } from '@lsw-abpvue/identity';
import { AbpButton, AbpModal } from '@lsw-abpvue/theme-shared';
import { UserDetailsService } from './user-details';
const service = inject(UserDetailsService);
const { visible, record } = service;
onBeforeUnmount(service.close);
</script>

将 userDetails 传给已有 createIdentityRoutes(userDetails) 懒加载路由,再在模块启动后注册包装替换:

ts
import { inject, provideAppInitializer, ReplaceableComponentsService } from '@lsw-abpvue/core';
import { IdentityComponents } from '@lsw-abpvue/identity';
import UsersPageWithDetails from './components/UsersPageWithDetails.vue';

export const detailsPage = provideAppInitializer(() => {
  inject(ReplaceableComponentsService).add({ key: IdentityComponents.Users, component: UsersPageWithDetails });
});

启动 providers 加入 detailsPage,资源增加 BookStore::Details。贡献器与包装解析同一个根服务。直接导入的 UsersPage 保留默认命令与扩展渲染。示例使用包内 Identity 详情代理,没有假定新的业务端点。

添加、删除与顺序 ​

addHead/addTail 放在首尾,addBefore/addAfter 用谓词定位相对位置。删除匹配稳定 text key,不匹配翻译文字。替换可能重复的项时先 dropByValueAll。锚点不存在时相对插入可能失败,模块默认项不同的场景应准备后备位置。

不能用 clearContributors 删除一个按钮,它会清空整个贡献器容器。保留模块默认值,再删除最终列表中要替换的项。分别在两条记录上检查,避免操作错误地沿用之前选择。

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