feat(personal-center): 实现个人信息功能

This commit is contained in:
caozehui
2026-05-15 16:05:56 +08:00
parent 0c6ed249ee
commit 480714172e
8 changed files with 921 additions and 2 deletions

1
.gitignore vendored
View File

@@ -43,3 +43,4 @@ yarn.lock
# Temp
/codeTemp/*
SKILL.md

View File

@@ -19,6 +19,25 @@ interface BackendUserInfoDTO {
buttons?: string[] | null;
}
interface BackendMyProfileDetailDTO {
id?: string | number | null;
userId?: string | number | null;
username?: string | null;
userName?: string | null;
nickname?: string | null;
company?: string | null;
email?: string | null;
mobile?: string | null;
sex?: Api.SystemManage.UserGender | null;
avatar?: string | null;
loginIp?: string | null;
loginDate?: string | null;
createTime?: string | null;
roles?: Api.SystemManage.RoleSimple[] | null;
dept?: Api.SystemManage.DeptSimple | null;
position?: Api.SystemManage.PostSimple | null;
}
let userInfoPromise: Promise<ServiceRequestResult<BackendUserInfoDTO>> | null = null;
/** 将后端 token 结构转换成前端现有结构 */
@@ -39,6 +58,42 @@ function mapUserInfo(data: BackendUserInfoDTO): Api.Auth.UserInfo {
};
}
function safeStringId(value: string | number | null | undefined): string | null {
return value === null || value === undefined ? null : String(value);
}
// eslint-disable-next-line complexity
function mapMyProfileDetail(data: BackendMyProfileDetailDTO, fallbackUserId = ''): Api.Auth.MyProfileDetail {
const baseInfo = {
userId: String(data.id ?? data.userId ?? fallbackUserId ?? ''),
username: data.username ?? data.userName ?? '',
nickname: data.nickname ?? '',
deptId: safeStringId(data.dept?.id),
deptName: data.dept?.name ?? '',
positionId: safeStringId(data.position?.id),
positionName: data.position?.name ?? ''
};
const contactInfo = {
company: data.company ?? null,
email: data.email ?? '',
mobile: data.mobile ?? '',
sex: data.sex ?? 0,
avatar: data.avatar ?? ''
};
const extraInfo = {
roles: data.roles ?? [],
dept: data.dept ?? null,
position: data.position ?? null,
loginIp: data.loginIp ?? '',
loginDate: data.loginDate ?? null,
createTime: data.createTime ?? null
};
return { ...baseInfo, ...contactInfo, ...extraInfo };
}
export function clearUserInfoCache() {
userInfoPromise = null;
}
@@ -101,6 +156,45 @@ export async function fetchGetUserInfo(force = false): Promise<ServiceRequestRes
};
}
/** 获取当前登录人资料详情 */
export async function fetchGetMyProfileDetail(
options: {
userId?: string;
} = {}
): Promise<ServiceRequestResult<Api.Auth.MyProfileDetail>> {
const result = await request<BackendMyProfileDetailDTO>({
url: `${SYSTEM_SERVICE_PREFIX}/user/profile/get`,
method: 'get'
});
if (result.error || !result.data) {
return result as ServiceRequestResult<Api.Auth.MyProfileDetail>;
}
return {
...result,
data: mapMyProfileDetail(result.data, options.userId ?? '')
};
}
/** 更新当前登录人基础资料 */
export function fetchUpdateMyProfile(data: Api.Auth.UpdateMyProfileParams) {
return request<boolean>({
url: `${SYSTEM_SERVICE_PREFIX}/user/profile/update`,
method: 'put',
data
});
}
/** 修改当前登录人密码 */
export function fetchUpdateMyPassword(data: Api.Auth.UpdateMyPasswordParams) {
return request<boolean>({
url: `${SYSTEM_SERVICE_PREFIX}/user/profile/update-password`,
method: 'put',
data
});
}
/**
* 刷新 token
*

View File

@@ -172,6 +172,18 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
return false;
}
async function refreshUserInfo() {
const { data: info, error } = await fetchGetUserInfo(true);
if (!error) {
Object.assign(userInfo, info);
return true;
}
return false;
}
async function initUserInfo() {
const hasToken = getToken();
@@ -194,6 +206,7 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
loginLoading,
resetStore,
login,
initUserInfo
initUserInfo,
refreshUserInfo
};
});

View File

@@ -17,5 +17,39 @@ declare namespace Api {
roles: string[];
buttons: string[];
}
interface MyProfileDetail {
userId: string;
username: string;
nickname?: string | null;
deptId?: string | null;
deptName?: string | null;
positionId?: string | null;
positionName?: string | null;
company?: string | null;
email?: string | null;
mobile?: string | null;
sex?: Api.SystemManage.UserGender | null;
avatar?: string | null;
roles: Api.SystemManage.RoleSimple[];
dept?: Api.SystemManage.DeptSimple | null;
position?: Api.SystemManage.PostSimple | null;
loginIp?: string | null;
loginDate?: string | null;
createTime?: string | null;
}
interface UpdateMyProfileParams {
nickname?: string | null;
email?: string | null;
mobile?: string | null;
sex?: Api.SystemManage.UserGender | null;
avatar?: string | null;
}
interface UpdateMyPasswordParams {
oldPassword: string;
newPassword: string;
}
}
}

View File

@@ -1,3 +1,436 @@
<script setup lang="ts">
import { computed, onActivated, onMounted, ref } from 'vue';
import { userGenderRecord } from '@/constants/business';
import { fetchGetMyProfileDetail, fetchUpdateMyProfile } from '@/service/api';
import { buildFileProxyUrl, uploadFile } from '@/service/api/file';
import { useAuthStore } from '@/store/modules/auth';
import { useAppStore } from '@/store/modules/app';
import { $t } from '@/locales';
import ProfileInfoDialog from './modules/profile-info-dialog.vue';
import ProfilePasswordDialog from './modules/profile-password-dialog.vue';
import { buildProfileUpdatePayload, formatProfileDateTime, resolveProfileRoleLabels } from './modules/profile-model';
defineOptions({ name: 'MyProfile' });
const authStore = useAuthStore();
const appStore = useAppStore();
const loading = ref(false);
const avatarSubmitting = ref(false);
const profile = ref<Api.Auth.MyProfileDetail | null>(null);
const profileInfoVisible = ref(false);
const passwordVisible = ref(false);
const avatarInputRef = ref<HTMLInputElement | null>(null);
const descriptionColumns = computed(() => (appStore.isMobile ? 1 : 2));
const displayName = computed(() => profile.value?.nickname?.trim() || profile.value?.username || '--');
const displayUsername = computed(() => profile.value?.username?.trim() || '--');
const companyText = computed(() => profile.value?.company?.trim() || '--');
const deptText = computed(() => profile.value?.dept?.name?.trim() || profile.value?.deptName?.trim() || '--');
const positionText = computed(
() => profile.value?.position?.name?.trim() || profile.value?.positionName?.trim() || '--'
);
const mobileText = computed(() => profile.value?.mobile?.trim() || '--');
const emailText = computed(() => profile.value?.email?.trim() || '--');
const genderText = computed(() => {
const value = profile.value?.sex;
if (value === null || value === undefined) {
return '--';
}
return $t(userGenderRecord[value]);
});
const roleLabels = computed(() => {
const roles = profile.value?.roles ?? [];
if (roles.length === 0) {
return [];
}
return resolveProfileRoleLabels(roles);
});
function getAvatarText() {
const name = displayName.value;
return name === '--' ? 'CN' : name.slice(0, 1).toUpperCase();
}
function getEditableProfileValues(current: Api.Auth.MyProfileDetail): Api.Auth.UpdateMyProfileParams {
return {
nickname: current.nickname ?? '',
email: current.email ?? '',
mobile: current.mobile ?? '',
sex: current.sex ?? 1,
avatar: current.avatar ?? ''
};
}
async function loadProfile() {
const userId = authStore.userInfo.userId;
if (!userId) {
profile.value = null;
return;
}
const { data, error } = await fetchGetMyProfileDetail({
userId
});
if (!error) {
profile.value = data;
}
}
async function initPage() {
loading.value = true;
await authStore.initUserInfo();
await loadProfile();
loading.value = false;
}
function triggerAvatarSelect() {
if (!profile.value || avatarSubmitting.value) {
return;
}
avatarInputRef.value?.click();
}
async function handleAvatarChange(event: Event) {
const input = event.target as HTMLInputElement;
const file = input.files?.[0];
input.value = '';
if (!file || !profile.value) {
return;
}
if (!file.type.startsWith('image/')) {
window.$message?.error('请上传图片文件');
return;
}
avatarSubmitting.value = true;
const uploadResult = await uploadFile(file, 'avatar');
if (uploadResult.error) {
avatarSubmitting.value = false;
return;
}
const avatar = buildFileProxyUrl(uploadResult.data.configId, uploadResult.data.path);
const payload = buildProfileUpdatePayload({
...getEditableProfileValues(profile.value),
avatar
});
const updateResult = await fetchUpdateMyProfile(payload);
avatarSubmitting.value = false;
if (updateResult.error) {
return;
}
window.$message?.success('头像更新成功');
await Promise.all([loadProfile(), authStore.refreshUserInfo()]);
}
async function handleProfileSubmitted() {
await Promise.all([loadProfile(), authStore.refreshUserInfo()]);
}
onMounted(() => {
initPage();
});
onActivated(() => {
initPage();
});
</script>
<template>
<LookForward title="个人信息" subtitle="功能建设中,敬请期待" />
<div v-loading="loading" class="my-profile-page">
<template v-if="profile">
<ElCard class="my-profile-hero-card" shadow="never">
<div class="my-profile-hero">
<div class="my-profile-hero__identity">
<button
class="my-profile-hero__avatar-button"
type="button"
:disabled="avatarSubmitting"
@click="triggerAvatarSelect"
>
<ElAvatar v-if="profile.avatar" :src="profile.avatar" :size="88" class="my-profile-hero__avatar" />
<div v-else class="my-profile-hero__avatar-fallback">{{ getAvatarText() }}</div>
<div class="my-profile-hero__avatar-mask">
<span>{{ avatarSubmitting ? '上传中...' : '更换头像' }}</span>
</div>
</button>
<input
ref="avatarInputRef"
class="my-profile-hero__avatar-input"
type="file"
accept="image/*"
@change="handleAvatarChange"
/>
<div class="my-profile-hero__summary">
<div class="my-profile-hero__title-row">
<h1 class="my-profile-hero__title">{{ displayName }}</h1>
<ElTag type="info" effect="plain">个人中心</ElTag>
</div>
<p class="my-profile-hero__subtitle">@{{ displayUsername }}</p>
<div class="my-profile-hero__meta">
<ElTag effect="plain">{{ companyText }}</ElTag>
<ElTag effect="plain">{{ deptText }}</ElTag>
<ElTag effect="plain">{{ positionText }}</ElTag>
</div>
</div>
</div>
<div class="my-profile-hero__actions">
<ElButton type="primary" @click="profileInfoVisible = true">编辑基本信息</ElButton>
<ElButton @click="passwordVisible = true">修改密码</ElButton>
</div>
</div>
</ElCard>
<div class="my-profile-content">
<ElCard shadow="never">
<template #header>
<div class="my-profile-card__header">
<span class="my-profile-card__title">基本资料</span>
</div>
</template>
<ElDescriptions :column="descriptionColumns" border>
<ElDescriptionsItem label="用户名">{{ displayUsername }}</ElDescriptionsItem>
<ElDescriptionsItem label="昵称">{{ displayName }}</ElDescriptionsItem>
<ElDescriptionsItem label="手机号">{{ mobileText }}</ElDescriptionsItem>
<ElDescriptionsItem label="邮箱">{{ emailText }}</ElDescriptionsItem>
<ElDescriptionsItem label="性别">{{ genderText }}</ElDescriptionsItem>
<ElDescriptionsItem label="所属公司">{{ companyText }}</ElDescriptionsItem>
<ElDescriptionsItem label="所属部门">{{ deptText }}</ElDescriptionsItem>
<ElDescriptionsItem label="所属岗位">{{ positionText }}</ElDescriptionsItem>
<ElDescriptionsItem label="角色" :span="descriptionColumns">
<div v-if="roleLabels.length" class="my-profile-role-list">
<ElTag v-for="roleLabel in roleLabels" :key="roleLabel" type="primary" effect="plain">
{{ roleLabel }}
</ElTag>
</div>
<span v-else>--</span>
</ElDescriptionsItem>
</ElDescriptions>
</ElCard>
<ElCard shadow="never">
<template #header>
<div class="my-profile-card__header">
<span class="my-profile-card__title">登录信息</span>
</div>
</template>
<ElDescriptions :column="descriptionColumns" border>
<ElDescriptionsItem label="最近登录 IP">{{ profile.loginIp?.trim() || '--' }}</ElDescriptionsItem>
<ElDescriptionsItem label="最近登录时间">{{ formatProfileDateTime(profile.loginDate) }}</ElDescriptionsItem>
<ElDescriptionsItem label="账号创建时间" :span="descriptionColumns">
{{ formatProfileDateTime(profile.createTime) }}
</ElDescriptionsItem>
</ElDescriptions>
</ElCard>
</div>
</template>
<ElEmpty v-else description="未获取到个人信息" />
<ProfileInfoDialog v-model:visible="profileInfoVisible" :profile="profile" @submitted="handleProfileSubmitted" />
<ProfilePasswordDialog v-model:visible="passwordVisible" :username="profile?.username" />
</div>
</template>
<style scoped>
.my-profile-page {
display: flex;
flex-direction: column;
gap: 16px;
}
.my-profile-hero-card {
overflow: hidden;
border: 1px solid rgb(226 232 240 / 92%);
background:
radial-gradient(circle at top left, rgb(14 116 144 / 12%), transparent 32%),
radial-gradient(circle at bottom right, rgb(16 185 129 / 10%), transparent 26%),
linear-gradient(135deg, rgb(255 255 255 / 99%), rgb(248 250 252 / 98%));
}
.my-profile-hero {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 16px;
align-items: center;
}
.my-profile-hero__identity {
display: flex;
min-width: 0;
align-items: center;
gap: 18px;
}
.my-profile-hero__avatar-button {
position: relative;
display: inline-flex;
width: 88px;
height: 88px;
align-items: center;
justify-content: center;
border: none;
border-radius: 999px;
padding: 0;
overflow: hidden;
background: transparent;
cursor: pointer;
}
.my-profile-hero__avatar-button:disabled {
cursor: not-allowed;
}
.my-profile-hero__avatar,
.my-profile-hero__avatar-fallback {
width: 88px;
height: 88px;
}
.my-profile-hero__avatar-fallback {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 999px;
background: linear-gradient(135deg, rgb(14 116 144 / 92%), rgb(15 118 110 / 84%));
color: #fff;
font-size: 28px;
font-weight: 700;
}
.my-profile-hero__avatar-mask {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: rgb(15 23 42 / 52%);
color: #fff;
font-size: 12px;
font-weight: 600;
opacity: 0;
transition: opacity 0.2s ease;
}
.my-profile-hero__avatar-button:hover .my-profile-hero__avatar-mask,
.my-profile-hero__avatar-button:focus-visible .my-profile-hero__avatar-mask {
opacity: 1;
}
.my-profile-hero__avatar-input {
display: none;
}
.my-profile-hero__summary {
display: flex;
min-width: 0;
flex-direction: column;
gap: 10px;
}
.my-profile-hero__title-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px;
}
.my-profile-hero__title {
margin: 0;
color: rgb(15 23 42 / 98%);
font-size: 28px;
line-height: 1.15;
letter-spacing: -0.02em;
}
.my-profile-hero__subtitle {
margin: 0;
color: rgb(100 116 139 / 92%);
font-size: 14px;
}
.my-profile-hero__meta {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.my-profile-hero__actions {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 10px;
}
.my-profile-content {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
}
.my-profile-card__header {
display: flex;
align-items: center;
justify-content: space-between;
}
.my-profile-card__title {
color: rgb(15 23 42 / 98%);
font-size: 15px;
font-weight: 700;
}
.my-profile-role-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
@media (width <= 960px) {
.my-profile-hero {
grid-template-columns: 1fr;
}
.my-profile-hero__actions {
justify-content: flex-start;
}
.my-profile-content {
grid-template-columns: 1fr;
}
}
@media (width <= 640px) {
.my-profile-hero__identity {
flex-direction: column;
align-items: flex-start;
}
.my-profile-hero__title {
font-size: 24px;
}
}
</style>

View File

@@ -0,0 +1,136 @@
<script setup lang="ts">
import { computed, nextTick, ref, watch } from 'vue';
import { userGenderOptions } from '@/constants/business';
import { fetchUpdateMyProfile } from '@/service/api';
import { useForm, useFormRules } from '@/hooks/common/form';
import { translateOptions } from '@/utils/common';
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
import { buildProfileUpdatePayload } from './profile-model';
defineOptions({ name: 'ProfileInfoDialog' });
interface Props {
profile?: Api.Auth.MyProfileDetail | null;
}
const props = defineProps<Props>();
interface Emits {
(e: 'submitted'): void;
}
const emit = defineEmits<Emits>();
const visible = defineModel<boolean>('visible', {
default: false
});
const { formRef, validate } = useForm();
const { createRequiredRule, patternRules } = useFormRules();
const submitting = ref(false);
const genderOptions = computed(() =>
translateOptions(userGenderOptions).map(item => ({
...item,
value: Number(item.value) as Api.SystemManage.UserGender
}))
);
const model = ref<Api.Auth.UpdateMyProfileParams>({
nickname: '',
email: '',
mobile: '',
sex: 1,
avatar: ''
});
const rules = computed<Record<string, App.Global.FormRule[]>>(() => ({
nickname: [createRequiredRule('请输入昵称')],
mobile: model.value.mobile?.trim() ? [patternRules.phone] : [],
email: model.value.email?.trim() ? [patternRules.email] : []
}));
function initModel() {
model.value = {
nickname: props.profile?.nickname ?? '',
email: props.profile?.email ?? '',
mobile: props.profile?.mobile ?? '',
sex: props.profile?.sex ?? 1,
avatar: props.profile?.avatar ?? ''
};
}
function closeDialog() {
visible.value = false;
}
async function handleSubmit() {
if (!props.profile) {
return;
}
await validate();
submitting.value = true;
const { error } = await fetchUpdateMyProfile(buildProfileUpdatePayload(model.value));
submitting.value = false;
if (error) {
return;
}
window.$message?.success('个人信息更新成功');
closeDialog();
emit('submitted');
}
watch(visible, async value => {
if (!value) {
return;
}
initModel();
await nextTick();
formRef.value?.clearValidate();
});
</script>
<template>
<BusinessFormDialog
v-model="visible"
title="编辑基本信息"
preset="sm"
:confirm-loading="submitting"
@confirm="handleSubmit"
>
<ElForm ref="formRef" :model="model" :rules="rules" label-position="top" autocomplete="off">
<ElRow :gutter="16">
<ElCol :span="24">
<ElFormItem label="昵称" prop="nickname">
<ElInput v-model="model.nickname" maxlength="30" placeholder="请输入昵称" />
</ElFormItem>
</ElCol>
<ElCol :span="24">
<ElFormItem label="手机号" prop="mobile">
<ElInput v-model="model.mobile" maxlength="20" placeholder="请输入手机号" />
</ElFormItem>
</ElCol>
<ElCol :span="24">
<ElFormItem label="邮箱" prop="email">
<ElInput v-model="model.email" maxlength="100" placeholder="请输入邮箱" />
</ElFormItem>
</ElCol>
<ElCol :span="24">
<ElFormItem label="性别" prop="sex">
<ElSelect v-model="model.sex" placeholder="请选择性别">
<ElOption v-for="{ label, value } in genderOptions" :key="value" :label="label" :value="value" />
</ElSelect>
</ElFormItem>
</ElCol>
</ElRow>
</ElForm>
</BusinessFormDialog>
</template>

View File

@@ -0,0 +1,27 @@
import dayjs from 'dayjs';
function getNullableText(value?: string | null) {
return value?.trim() || null;
}
export function formatProfileDateTime(value?: string | number | null) {
if (!value) {
return '--';
}
return dayjs(value).format('YYYY-MM-DD HH:mm:ss');
}
export function resolveProfileRoleLabels(roles: Api.SystemManage.RoleSimple[]) {
return roles.map(role => role.name?.trim() || role.code || role.id);
}
export function buildProfileUpdatePayload(form: Api.Auth.UpdateMyProfileParams): Api.Auth.UpdateMyProfileParams {
return {
nickname: getNullableText(form.nickname),
email: getNullableText(form.email),
mobile: getNullableText(form.mobile),
sex: form.sex ?? null,
avatar: getNullableText(form.avatar)
};
}

View File

@@ -0,0 +1,181 @@
<script setup lang="ts">
import { computed, nextTick, ref, watch } from 'vue';
import { fetchUpdateMyPassword } from '@/service/api';
import { useAuthStore } from '@/store/modules/auth';
import { useForm, useFormRules } from '@/hooks/common/form';
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
defineOptions({ name: 'ProfilePasswordDialog' });
interface Props {
username?: string | null;
}
const props = defineProps<Props>();
interface Emits {
(e: 'submitted'): void;
}
const emit = defineEmits<Emits>();
const visible = defineModel<boolean>('visible', {
default: false
});
const authStore = useAuthStore();
const { formRef, validate } = useForm();
const { createRequiredRule, createConfirmPwdRule, patternRules } = useFormRules();
const submitting = ref(false);
const model = ref({
oldPassword: '',
newPassword: '',
confirmPassword: ''
});
const confirmDisabled = computed(() => {
return (
submitting.value ||
!model.value.oldPassword.trim() ||
!model.value.newPassword.trim() ||
!model.value.confirmPassword.trim()
);
});
const rules = computed<Record<string, App.Global.FormRule[]>>(() => ({
oldPassword: [createRequiredRule('请输入旧密码')],
newPassword: [
createRequiredRule('请输入新密码'),
patternRules.pwd,
{
asyncValidator: (_rule, value: string) => {
if (value.trim() !== '' && value === model.value.oldPassword) {
return Promise.reject(new Error('新密码不能与旧密码相同'));
}
return Promise.resolve();
},
trigger: 'change'
}
],
confirmPassword: createConfirmPwdRule(model.value.newPassword)
}));
const displayUsername = computed(() => props.username?.trim() || '--');
function initModel() {
model.value.oldPassword = '';
model.value.newPassword = '';
model.value.confirmPassword = '';
}
function closeDialog() {
visible.value = false;
}
async function handleSubmit() {
if (confirmDisabled.value) {
return;
}
await validate();
submitting.value = true;
const { error } = await fetchUpdateMyPassword({
oldPassword: model.value.oldPassword.trim(),
newPassword: model.value.newPassword.trim()
});
submitting.value = false;
if (error) {
return;
}
window.$message?.success('密码修改成功,请重新登录');
closeDialog();
emit('submitted');
await authStore.resetStore();
}
watch(visible, async value => {
if (!value) {
return;
}
initModel();
await nextTick();
formRef.value?.clearValidate();
});
</script>
<template>
<BusinessFormDialog
v-model="visible"
title="修改密码"
preset="sm"
:confirm-loading="submitting"
:confirm-disabled="confirmDisabled"
@confirm="handleSubmit"
>
<ElForm ref="formRef" :model="model" :rules="rules" label-position="top" autocomplete="off">
<input class="business-form-autofill-guard" type="text" name="fake-username" autocomplete="username" />
<input class="business-form-autofill-guard" type="password" name="fake-password" autocomplete="new-password" />
<ElRow :gutter="16">
<ElCol :span="24">
<ElFormItem label="用户名">
<ElInput :model-value="displayUsername" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="24">
<ElAlert title="密码修改后会退出当前登录态,请使用新密码重新登录。" type="info" :closable="false" show-icon />
</ElCol>
<ElCol :span="24">
<ElFormItem label="旧密码" prop="oldPassword">
<ElInput
v-model="model.oldPassword"
show-password
autocomplete="current-password"
placeholder="请输入旧密码"
/>
</ElFormItem>
</ElCol>
<ElCol :span="24">
<ElFormItem label="新密码" prop="newPassword">
<ElInput v-model="model.newPassword" show-password autocomplete="new-password" placeholder="请输入新密码" />
</ElFormItem>
</ElCol>
<ElCol :span="24">
<ElFormItem label="确认新密码" prop="confirmPassword">
<ElInput
v-model="model.confirmPassword"
show-password
autocomplete="new-password"
placeholder="请再次输入新密码"
/>
</ElFormItem>
</ElCol>
</ElRow>
</ElForm>
</BusinessFormDialog>
</template>
<style scoped>
.business-form-autofill-guard {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
opacity: 0;
pointer-events: none;
}
</style>