Skip to content

个人资料与设置页签 ​

模块需要在本地个人资料或设置页增加面板时,可以贡献页签。每个面板是 Vue 组件,自行负责加载、验证和保存。

增加两个组件 ​

创建 src/components/ContactProfileTab.vue。下面通过实际 Account 资料代理编辑电话,保留其他资料字段、额外属性和并发戳,不把整个资料替换成只含电话的请求体。

vue
<template>
  <form @submit.prevent="save">
    <p v-if="refreshFailed" role="alert">
      Saved, but application configuration could not be refreshed.
      <AbpButton variant="secondary" size="sm" @click="refreshConfiguration"
        >Retry refresh</AbpButton
      >
    </p>
    <p v-if="loading" role="status">Loading profile…</p>
    <p v-if="failed" role="alert">
      Profile could not be loaded. <button type="button" @click="load">Retry</button>
    </p>
    <AbpFormField v-slot="{ id, describedBy, invalid }" label="Phone number" :errors="errors">
      <AbpInput
        :id="id"
        v-model="form.controls.phoneNumber.value"
        type="tel"
        :maxlength="32"
        :disabled="loading || failed || saving"
        :invalid="invalid"
        :aria-describedby="describedBy"
      />
    </AbpFormField>
    <AbpButton type="submit" :loading="saving" :disabled="loading || failed">Save</AbpButton>
  </form>
</template>

<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref, shallowRef } from 'vue';
import { ConfigStateService, inject } from '@lsw-abpvue/core';
import { ManageProfileStateService } from '@lsw-abpvue/account-core';
import { ProfileService, type ProfileDto } from '@lsw-abpvue/account-core/proxy';
import {
  AbpButton,
  AbpFormField,
  AbpInput,
  useAbpForm,
  useServerValidation,
  useToaster,
  useValidationMessages,
  Validators,
} from '@lsw-abpvue/theme-shared';
const service = inject(ProfileService);
const state = inject(ManageProfileStateService);
const config = inject(ConfigStateService);
const toaster = useToaster();
const original = shallowRef<ProfileDto>();
const form = useAbpForm({ phoneNumber: { value: '', validators: [Validators.maxLength(32)] } });
useServerValidation(form);
const messages = useValidationMessages();
const errors = computed(() =>
  form.controls.phoneNumber.touched ? messages(form.controls.phoneNumber.errors) : [],
);
const loading = ref(true);
const failed = ref(false);
const saving = ref(false);
const refreshFailed = ref(false);
const controller = new AbortController();
onBeforeUnmount(() => controller.abort());
onMounted(load);
async function load(): Promise<void> {
  loading.value = true;
  failed.value = false;
  try {
    original.value = await service.get({ signal: controller.signal });
    form.reset({ phoneNumber: original.value.phoneNumber ?? '' });
  } catch {
    if (!controller.signal.aborted) failed.value = true;
  } finally {
    loading.value = false;
  }
}
async function save(): Promise<void> {
  if (!original.value || saving.value || !form.validate()) return;
  saving.value = true;
  try {
    const result = await service.update(
      {
        userName: original.value.userName,
        email: original.value.email,
        name: original.value.name,
        surname: original.value.surname,
        concurrencyStamp: original.value.concurrencyStamp,
        extraProperties: original.value.extraProperties,
        phoneNumber: form.value.phoneNumber,
      },
      { signal: controller.signal },
    );
    original.value = result;
    state.set(result);
    form.reset({ phoneNumber: result.phoneNumber ?? '' });
    await refreshConfiguration();
    toaster.success('AbpUi::SavedSuccessfully');
  } catch {
    /* Request handlers report errors; keep the entered phone number. */
  } finally {
    saving.value = false;
  }
}
async function refreshConfiguration(): Promise<void> {
  refreshFailed.value = false;
  try {
    await config.refreshAppState();
  } catch {
    refreshFailed.value = true;
  }
}
</script>

创建 src/components/PrintingTab.vue。使用后端示例中的完整 PrintingSettingsAppService,实现 GET/PUT /api/app/printing-settings,接收与返回 { copies: number },通过持久化设置存储保存当前用户覆盖,本教程使用 AbpIdentity.Users 权限。这是应用端点,不是设置管理的内置端点。

vue
<template>
  <form @submit.prevent="save">
    <p v-if="refreshFailed" role="alert">
      Saved, but application configuration could not be refreshed.
      <AbpButton variant="secondary" size="sm" @click="refreshConfiguration"
        >Retry refresh</AbpButton
      >
    </p>
    <p v-if="loading" role="status">Loading printing settings…</p>
    <p v-if="failed" role="alert">
      Settings could not be loaded. <button type="button" @click="load">Retry</button>
    </p>
    <AbpFormField
      v-slot="{ id, describedBy, invalid }"
      label="Default copies"
      :errors="errors"
      required
    >
      <AbpInput
        :id="id"
        v-model="form.controls.copies.value"
        type="number"
        :min="1"
        :max="20"
        :disabled="loading || failed || saving"
        :invalid="invalid"
        :aria-describedby="describedBy"
      />
    </AbpFormField>
    <AbpButton type="submit" :loading="saving" :disabled="loading || failed">Save</AbpButton>
  </form>
</template>

<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
import { ConfigStateService, inject, RestService } from '@lsw-abpvue/core';
import {
  AbpButton,
  AbpFormField,
  AbpInput,
  useAbpForm,
  useServerValidation,
  useToaster,
  useValidationMessages,
  Validators,
} from '@lsw-abpvue/theme-shared';

interface PrintingSettings {
  copies: number;
}
const rest = inject(RestService);
const config = inject(ConfigStateService);
const toaster = useToaster();
const form = useAbpForm<{ copies: number | null }>({
  copies: { value: 1, validators: [Validators.required(), Validators.range(1, 20)] },
});
useServerValidation(form);
const messages = useValidationMessages();
const errors = computed(() =>
  form.controls.copies.touched ? messages(form.controls.copies.errors) : [],
);
const loading = ref(true);
const failed = ref(false);
const saving = ref(false);
const refreshFailed = ref(false);
const controller = new AbortController();
onBeforeUnmount(() => controller.abort());
onMounted(load);
async function load(): Promise<void> {
  loading.value = true;
  failed.value = false;
  try {
    const result = await rest.request<never, PrintingSettings>(
      { method: 'GET', url: '/api/app/printing-settings' },
      { signal: controller.signal },
    );
    form.reset(result);
  } catch {
    if (!controller.signal.aborted) failed.value = true;
  } finally {
    loading.value = false;
  }
}
async function save(): Promise<void> {
  if (saving.value || loading.value || failed.value || !form.validate()) return;
  saving.value = true;
  try {
    await rest.request(
      { method: 'PUT', url: '/api/app/printing-settings', body: form.value },
      { signal: controller.signal },
    );
    form.reset(form.value);
    await refreshConfiguration();
    toaster.success('AbpUi::SavedSuccessfully');
  } catch {
    /* Request handlers report errors; preserve the form. */
  } finally {
    saving.value = false;
  }
}
async function refreshConfiguration(): Promise<void> {
  refreshFailed.value = false;
  try {
    await config.refreshAppState();
  } catch {
    refreshFailed.value = true;
  }
}
</script>

加载失败禁用 Save 并提供 Retry,保存失败保留输入与字段错误,面板卸载时通过 AbortSignal 结束请求。

注册面板 ​

创建 src/custom-tabs.ts,把两个导入调整为 components 目录:

ts
import { inject, makeEnvironmentProviders, provideAppInitializer } from '@lsw-abpvue/core';
import { ManageProfileTabsService } from '@lsw-abpvue/account-core';
import { SettingTabsService } from '@lsw-abpvue/setting-management/config';
import ContactProfileTab from './ContactProfileTab.vue';
import PrintingTab from './PrintingTab.vue';

export const customTabs = makeEnvironmentProviders([
  provideAppInitializer(() => {
    inject(ManageProfileTabsService).add([
      {
        name: 'BookStore::Contact',
        text: 'BookStore::Contact',
        order: 3,
        component: ContactProfileTab,
      },
    ]);
    inject(SettingTabsService).add([
      {
        name: 'BookStore::Printing',
        order: 5,
        requiredPolicy: 'AbpIdentity.Users',
        component: PrintingTab,
      },
    ]);
  }),
]);

在已有启动 providers 中,将 customTabs 放到模块配置和默认页签注册之后。OAuth 后保留 provideAccountConfig(),让 My account 使用本地目的地;account 的 provideManageProfileTabs() 注册资料与密码默认页签;provideSettingManagementConfig() 注册设置。保留已有 account、setting-management 懒加载路由。注册页签不会创建路由或安装后端端点。

在应用资源中增加 BookStore::Contact、BookStore::Printing。打开 /account/manage 或 /setting-management,选择新增页签。

稳定标识、顺序与权限 ​

属性用途
name稳定标识,默认也作为本地化 key
text资料页签必填,设置页签可选的标签覆盖
component选中后挂载的面板
order排序,默认零
requiredPolicy可见所需权限
visible / invisible额外条件 / 明确隐藏
parentName分组时的父项标识

通过 tabs.patch(name, { order: 10 }) 调整,通过 tabs.remove([name]) 删除,删除也会移除子项。改显示文字时保持 name 稳定。隐藏不影响后端能力,端点仍要授权。

刷新与未保存状态 ​

资料示例把响应写回 ManageProfileStateService,让其他面板看到最新共享资料。有效设置或当前用户字段被其他界面读取时,保存后刷新应用配置。保存设置覆盖和读取有效值是两个请求;保存成功但刷新失败时,反馈应明确区分。

切换面板可能卸载组件。模态框 dirty 关闭保护不会自动保护页面或页签导航。需要保护的面板应自行增加导航策略,或在受保护对话框中编辑,不能假定页签列表自动保存。

范围与检查 ​

贡献只影响本地页面,不修改独立授权服务器的 /Account/Manage。需要检查实际 My account 提供者与目的地。

检查首次加载、失败重试、无效份数、保存失败、拒绝权限、保存后重进、其他页签读取新资料及租户有效设置。参见认证、表单和设置。

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