perf: 添加用户资料编辑功能

- 新增用户资料编辑对话框组件
- 添加后端更新用户资料接口
- 在用户信息页面添加编辑按钮
- 新增中英文翻译字段
- 实现头像上传和昵称修改功能
pull/453/head
xiaojunnuo 2025-07-01 16:30:07 +08:00
parent c1bccb970f
commit 7c0f43c8a3
9 changed files with 161 additions and 4 deletions

View File

@ -81,4 +81,5 @@ export default {
nickName: "Nickname",
phoneNumber: "Phone Number",
changePassword: "Change Password",
updateProfile: "Update Profile",
};

View File

@ -19,4 +19,5 @@ export default {
create: "Create",
yes: "Yes",
no: "No",
handle: "Handle",
};

View File

@ -82,4 +82,5 @@ export default {
nickName: "昵称",
phoneNumber: "手机号",
changePassword: "修改密码",
updateProfile: "修改个人信息",
};

View File

@ -19,4 +19,5 @@ export default {
create: "新增",
yes: "是",
no: "否",
handle: "操作",
};

View File

@ -14,3 +14,11 @@ export async function changePassword(form: any) {
data: form,
});
}
export async function UpdateProfile(form: any) {
return await request({
url: "/mine/updateProfile",
method: "POST",
data: form,
});
}

View File

@ -0,0 +1,110 @@
// useUserProfile, 获取 openEditProfileDialog ,参考 useTemplate方法
import { useFormWrapper } from "@fast-crud/fast-crud";
import { ref } from "vue";
import { cloneDeep, merge } from "lodash-es";
// 假设的 API 导入
import * as userProfileApi from "./api";
import { useUserStore } from "/@/store/user";
import { useI18n } from "/src/locales";
/**
*
* @returns {{openEditProfileDialog: openEditProfileDialog}}
*/
export function useUserProfile() {
const { openCrudFormDialog } = useFormWrapper();
const wrapperRef = ref();
async function openEditProfileDialog(req: { onUpdated?: (ctx: any) => void }) {
const detail = await userProfileApi.getMineInfo();
if (!detail) {
throw new Error("用户资料不存在");
}
const { t } = useI18n();
const userStore = useUserStore();
const userProfileFormRef = ref();
async function doSubmit(opts: { form: any }) {
const form = opts.form;
const { id } = await userProfileApi.UpdateProfile(form);
if (req.onUpdated) {
req.onUpdated({ id });
}
}
const crudOptions: any = {
form: {
doSubmit,
wrapper: {
title: `编辑用户资料`,
width: 1100,
onOpened(opts: { form: any }) {
merge(opts.form, detail);
},
},
},
columns: {
nickName: {
title: t("certd.nickName"),
type: "text",
form: {
component: {
placeholder: t("certd.nickName"),
},
rules: [{ required: true, message: t("certd.nickName") }],
},
},
avatar: {
title: t("certd.avatar"),
type: "cropper-uploader",
column: {
width: 70,
component: {
style: {
height: "30px",
width: "auto",
},
buildUrl(key: string) {
return `api/basic/file/download?&key=` + key;
},
},
},
form: {
component: {
vModel: "modelValue",
valueType: "key",
cropper: {
aspectRatio: 1,
autoCropArea: 1,
viewMode: 0,
},
onReady: null,
uploader: {
type: "form",
action: "/basic/file/upload",
name: "file",
headers: {
Authorization: "Bearer " + userStore.getToken,
},
successHandle(res: any) {
return res;
},
},
buildUrl(key: string) {
return `api/basic/file/download?&key=` + key;
},
},
},
},
},
};
const wrapper = await openCrudFormDialog({ crudOptions });
wrapperRef.value = wrapper;
}
return {
openEditProfileDialog,
};
}

View File

@ -4,19 +4,21 @@
<div class="title">{{ t("certd.myInfo") }}</div>
</template>
<div class="p-10">
<a-descriptions title="" bordered :column="1">
<a-descriptions title="" bordered :column="2">
<a-descriptions-item :label="t('authentication.username')">{{ userInfo.username }}</a-descriptions-item>
<a-descriptions-item :label="t('authentication.nickName')">{{ userInfo.nickName }}</a-descriptions-item>
<a-descriptions-item :label="t('authentication.avatar')">
<a-avatar v-if="userInfo.avatar" size="large" :src="'api/basic/file/download?&key=' + userInfo.avatar" style="background-color: #eee"> </a-avatar>
<a-avatar v-else size="large" style="background-color: #00b4f5">
{{ userInfo.username }}
</a-avatar>
</a-descriptions-item>
<a-descriptions-item :label="t('authentication.nickName')">{{ userInfo.nickName }}</a-descriptions-item>
<a-descriptions-item :label="t('authentication.email')">{{ userInfo.email }}</a-descriptions-item>
<a-descriptions-item :label="t('authentication.phoneNumber')">{{ userInfo.phoneCode }}{{ userInfo.mobile }}</a-descriptions-item>
<a-descriptions-item :label="t('authentication.changePassword')">
<change-password-button :show-button="true"> </change-password-button>
<a-descriptions-item></a-descriptions-item>
<a-descriptions-item :label="t('common.handle')">
<a-button type="primary" @click="doUpdate">{{ t("authentication.updateProfile") }}</a-button>
<change-password-button class="ml-10" :show-button="true"> </change-password-button>
</a-descriptions-item>
</a-descriptions>
</div>
@ -28,6 +30,7 @@ import * as api from "./api";
import { Ref, ref } from "vue";
import ChangePasswordButton from "/@/views/certd/mine/change-password-button.vue";
import { useI18n } from "vue-i18n";
import { useUserProfile } from "./use";
const { t } = useI18n();
@ -41,4 +44,14 @@ const getUserInfo = async () => {
userInfo.value = await api.getMineInfo();
};
getUserInfo();
const { openEditProfileDialog } = useUserProfile();
function doUpdate() {
openEditProfileDialog({
onUpdated: async () => {
await getUserInfo();
},
});
}
</script>

View File

@ -32,4 +32,15 @@ export class MineController extends BaseController {
await this.userService.changePassword(userId, body);
return this.ok({});
}
@Post('/updateProfile', { summary: Constants.per.authOnly })
public async updateProfile(@Body(ALL) body: any) {
const userId = this.getUserId();
await this.userService.updateProfile(userId, {
avatar: body.avatar,
nickName: body.nickName,
});
return this.ok({});
}
}

View File

@ -23,6 +23,7 @@ export const AdminRoleId = 1
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class UserService extends BaseService<UserEntity> {
@InjectEntityModel(UserEntity)
repository: Repository<UserEntity>;
@Inject()
@ -335,4 +336,14 @@ export class UserService extends BaseService<UserEntity> {
},
})
}
async updateProfile(userId: any, body: any) {
await this.update({
id: userId,
...body,
})
}
}