替换组件
贡献者无法表达所需 UI 时,可以替换整个页面。公共 key 保持稳定,例如 Identity.UsersComponent。
包装已有用户页面
创建 src/components/UsersPageWithNotice.vue:
<template>
<section>
<p class="alert alert-info" role="status">{{ localization.t('BookStore::UsersNotice') }}</p>
<UsersPage />
</section>
</template>
<script setup lang="ts">
import { useLocalization } from '@lsw-abpvue/core';
import { UsersPage } from '@lsw-abpvue/identity';
const localization = useLocalization();
</script>组件直接导入原始 UsersPage,增加提示,同时保留内置查询、CRUD、命令 token、贡献者与模态框行为。直接渲染导入的原组件,不会再次经过替换注册表而递归。
在本地化资源中添加 BookStore::UsersNotice,再注册包装组件:
import { inject, provideAppInitializer, ReplaceableComponentsService } from '@lsw-abpvue/core';
import { IdentityComponents } from '@lsw-abpvue/identity';
import UsersPageWithNotice from './components/UsersPageWithNotice.vue';
export const replaceUsers = provideAppInitializer(() => {
inject(ReplaceableComponentsService).add({
key: IdentityComponents.Users,
component: UsersPageWithNotice,
});
});将 replaceUsers 加入已有启动提供者,排在模块注册之后。保留 Identity 路由,AbpReplaceableRouteContainer 选择注册组件;未注册时使用模块默认组件。
完整重写页面
替换页面仍位于模块路由注入器下,守卫、路由元数据与组装后的扩展注册表仍然可用。原页面的局部状态和命令不会自动创建。
使用 AbpExtensibleTable 或 AbpPageToolbar 时,默认操作可能解析 USERS_PAGE。通过 provideAbp 提供自己的 add、edit、remove、managePermissions 命令;同时负责刷新、验证、并发与对话框状态。也可以显式定义业务列与操作,不渲染模块扩展。
运行时更换注册组件可能重新挂载并丢失局部表单状态。通常在启动时注册;确需运行时切换时,应明确协调状态。
替换主题控件
import { provideThemeComponents } from '@lsw-abpvue/theme-shared';
import MyDatePicker from './components/MyDatePicker.vue';
const datePickerOverride = provideThemeComponents({ AbpDatePicker: MyDatePicker });覆盖项放在选定主题提供者之后。十二个基础控件使用主题契约注册表,页面/布局组件使用 ReplaceableComponentsService。实现文档列出的模型、属性、事件和插槽,并运行主题契约测试。
检查效果
打开用户页面,确认提示和原有列;创建、编辑、取消记录,并检查没有路由权限的用户。确认宿主贡献者仍生效。完整重写页面还应检查默认操作引用的每一个命令。
保留模块扩展的完整替换
根据下面的示例创建 src/components/UsersReplacement.vue:
<template>
<AbpPage title="AbpIdentity::Users">
<template #toolbar><AbpPageToolbar :data="items" /></template>
<p class="alert alert-info">This directory uses the module's contributed columns and fields.</p>
<p v-if="preparing" role="status">Preparing the selected user…</p>
<AbpExtensibleTable :data="items" :list="list" record-key="id" searchable />
<AbpRecordModal
:editor="editor"
label="AbpIdentity::Users"
create-title="AbpIdentity::NewUser"
:save="save"
>
<AbpExtensibleForm
v-if="editor.form.value"
:form="editor.form.value"
:record="editor.editing.value"
/>
<fieldset :disabled="editor.busy.value">
<legend class="h6">Roles</legend>
<AbpToggle
v-for="role in roles"
:key="role.id ?? role.name ?? ''"
:label="role.name"
:model-value="chosenRoles.includes(role.name ?? '')"
@update:model-value="toggleRole(role.name ?? '', $event === true)"
/>
</fieldset>
</AbpRecordModal>
<AbpPermissionManagement
v-if="permissionUser"
v-model:visible="permissionsVisible"
provider-name="U"
:provider-key="permissionUser.id ?? ''"
:entity-display-name="permissionUser.userName ?? ''"
/>
</AbpPage>
</template>
<script setup lang="ts">
import { onBeforeUnmount, ref, shallowRef } from 'vue';
import { inject, useListService } from '@lsw-abpvue/core';
import {
AbpExtensibleForm,
AbpExtensibleTable,
AbpPage,
AbpPageToolbar,
AbpRecordModal,
useRecordEditor,
} from '@lsw-abpvue/components';
import { IdentityComponents, USERS_PAGE } from '@lsw-abpvue/identity';
import {
IdentityUserService,
type IdentityRoleDto,
type IdentityUserCreateDto,
type IdentityUserDto,
type IdentityUserUpdateDto,
} from '@lsw-abpvue/identity/proxy';
import { AbpPermissionManagement } from '@lsw-abpvue/permission-management';
import { AbpToggle } from '@lsw-abpvue/theme-shared';
const users = inject(IdentityUserService);
const list = useListService({ persistKey: IdentityComponents.Users });
const { items } = list.hookToQuery((query, signal) => users.getList(query, { signal }));
const preparing = ref(false);
const roles = shallowRef<IdentityRoleDto[]>([]);
const chosenRoles = ref<string[]>([]);
const permissionUser = shallowRef<IdentityUserDto>();
const permissionsVisible = ref(false);
const controller = new AbortController();
onBeforeUnmount(() => controller.abort());
const editor = useRecordEditor<IdentityUserDto>({
identifier: IdentityComponents.Users,
reload: () => list.get(),
create: body =>
users.create(body as unknown as IdentityUserCreateDto, { signal: controller.signal }),
update: (id, body) =>
users.update(id, body as unknown as IdentityUserUpdateDto, { signal: controller.signal }),
delete: id => users.delete(id, { signal: controller.signal }),
idOf: record => record.id,
nameOf: record => record.userName ?? '',
stampOf: record => record.concurrencyStamp,
deletionMessage: 'AbpIdentity::UserDeletionConfirmationMessage',
providers: [
{
provide: USERS_PAGE,
useValue: {
add: () => void prepare(),
edit: (record: IdentityUserDto) => prepare(record),
remove,
managePermissions,
},
},
],
});
async function prepare(record?: IdentityUserDto): Promise<void> {
if (preparing.value || editor.busy.value) return;
preparing.value = true;
try {
const available = await users.getAssignableRoles({ signal: controller.signal });
roles.value = available.items;
if (record?.id) {
const [detail, assigned] = await Promise.all([
users.get(record.id, { signal: controller.signal }),
users.getRoles(record.id, { signal: controller.signal }),
]);
chosenRoles.value = assigned.items.map(role => role.name ?? '');
editor.show(detail);
} else {
chosenRoles.value = available.items
.filter(role => role.isDefault)
.map(role => role.name ?? '');
editor.show();
}
} catch {
/* Keep the directory usable; framework handlers report the failure. */
} finally {
preparing.value = false;
}
}
async function remove(record: IdentityUserDto): Promise<void> {
if (preparing.value || editor.busy.value) return;
preparing.value = true;
try {
await editor.remove(record);
} finally {
preparing.value = false;
}
}
function managePermissions(record: IdentityUserDto): void {
permissionUser.value = record;
permissionsVisible.value = true;
}
function toggleRole(name: string, checked: boolean): void {
chosenRoles.value = checked
? [...new Set([...chosenRoles.value, name])]
: chosenRoles.value.filter(value => value !== name);
}
function save(): void {
if (!editor.busy.value) void editor.save({ roleNames: [...chosenRoles.value] });
}
</script>保留路由解析后的贡献器注册表,并提供默认 Users 操作所需的全部命令。页面管理列表,编辑前读取完整详情与角色,保存带上 roleNames 和 concurrencyStamp,以 U 和用户 id 打开包内权限管理。扩展 DTO 边界的类型转换表示运行时组装字段,不提供客户端请求体校验。
使用上面的替换初始化器,把导入改为 UsersReplacement。保留 Identity 配置与懒加载路由,并按生成模板安装、注册权限管理。示例不清空或重新注册默认项,宿主列、字段与操作贡献仍有效。
选择定制层次
| 需求 | 使用 |
|---|---|
| 增加提示或外围内容 | 直接导入原页面的包装 |
| 增加或调整列、字段、命令 | 贡献器 |
| 更改全部交互与布局并保留模块扩展契约 | 提供命令的完整替换 |
| 全局改变主题输入控件 | 主题组件覆盖 |
路由组件替换不改变后端端点与权限要求。只读替换也可以显式定义列,但应说明主动移除了哪些默认操作。
缺失命令排查
贡献操作出现 USERS_PAGE 的 NullInjectorError 时,检查替换是否在工具栏、表格子组件挂载前创建页面注入器。只有路由无法提供任意页面的局部函数。包装递归时,应直接导入原页面,不再使用同 key 的可替换容器。