Files
gao-customer-h5/src/pages/user/UserProfile.vue
T
2026-09-12 23:51:38 +08:00

267 lines
6.3 KiB
Vue

<template>
<div class="profile-page">
<t-form class="form" label-align="left" v-entrance-transition>
<t-form-item label="头像">
<photo-field
v-model="formModel.avatar"
add-text="更换"
alt-text="用户头像"
size="compact"
:loading="isAvatarUploading"
:preview="false"
:show-remove="false"
@add="changeAvatar"
/>
</t-form-item>
<t-form-item label="用户名">
<t-input v-model.trim="formModel.name" placeholder="用户名" borderless />
</t-form-item>
<t-form-item label="昵称">
<t-input v-model.trim="formModel.nickname" placeholder="昵称" borderless />
</t-form-item>
<t-form-item label="性别">
<t-radio-group class="sex" v-model="formModel.gender" variant="default-filled">
<t-radio :block="false" borderless value="MALE">男</t-radio>
<t-radio :block="false" borderless value="FEMALE">女</t-radio>
<t-radio :block="false" borderless value="">未知</t-radio>
</t-radio-group>
</t-form-item>
<t-form-item label="手机号">
<div class="mobile-value">
<div v-if="formModel.mobile" v-text="formModel.mobile" />
<span v-else v-text="'未填写'" />
</div>
</t-form-item>
</t-form>
<t-cell-group theme="card" v-entrance-transition="70">
<t-cell title="修改密码" arrow hover @click="toPassword" />
</t-cell-group>
<t-button
class="red bg-white cell-button"
variant="text"
v-entrance-transition="140"
@click="confirmLogout"
>退出登录</t-button>
<input
ref="avatarInputRef"
class="avatar-input"
type="file"
accept="image/*"
@change="handleAvatarSelected"
>
</div>
</template>
<script setup lang="ts">
import { DialogPlugin } from "tdesign-mobile-vue";
import { AttachmentAPI, type Gender, UserAPI, useUserStore } from "timi-web-api";
import PhotoField from "@/components/PhotoField.vue";
import { useNavBarStore } from "@/store/navBarStore";
import { buildFallbackAvatar } from "@/utils/avatar";
import { compressUploadImage } from "@/utils/imageCompression";
import { Message } from "@/utils/message";
type UserAvatarSource = NonNullable<Parameters<typeof UserAPI.getAvatarURL>[0]> & {
name?: string;
nick?: string;
};
type ProfileGender = Gender | "";
defineOptions({
name: "UserProfile"
});
const router = useRouter();
const navBarStore = useNavBarStore();
const userStore = useUserStore();
// ---------- 头像 ----------
const avatarInputRef = ref<HTMLInputElement>();
const avatarTempFileId = ref("");
const isAvatarUploading = ref(false);
// ---------- 表单 ----------
const isSaving = ref(false);
const formModel = reactive({
id: "",
name: "",
avatar: "",
nickname: "",
role: "",
gender: "" as ProfileGender,
mobile: "",
password: "",
office: "",
scope: "",
status: "",
lastLoginAt: ""
});
// ---------- 页面数据 ----------
function fillForm(): void {
const user = userStore.loginUser.user;
Object.assign(formModel, {
id: user?.id || "",
name: user?.name || "",
avatar: buildUserAvatar(user),
nickname: user?.nick || "",
role: formatRoleCode(userStore.getRoleList()[0]),
gender: user?.gender || "",
mobile: user?.phoneNo || "",
password: "",
office: "",
scope: "",
status: "",
lastLoginAt: user?.lastLoginAt ? new Date(user.lastLoginAt).toLocaleString() : ""
});
}
// ---------- 头像 ----------
function changeAvatar(): void {
avatarInputRef.value?.click();
}
async function handleAvatarSelected(event: Event): Promise<void> {
const input = event.target as HTMLInputElement;
const file = input.files?.[0];
if (!file) {
return;
}
isAvatarUploading.value = true;
try {
const compressedFile = await compressUploadImage(file);
const [tempFile] = await AttachmentAPI.upload(compressedFile);
if (!tempFile?.id) {
Message.error("头像上传失败");
return;
}
avatarTempFileId.value = tempFile.id;
formModel.avatar = AttachmentAPI.getTempReadURL(tempFile.id);
} finally {
isAvatarUploading.value = false;
input.value = "";
}
}
// ---------- 导航 ----------
async function toPassword(): Promise<void> {
await router.push("/management/profile/password");
}
// ---------- 保存 ----------
async function saveProfile(): Promise<void> {
if (!formModel.name) {
Message.warning("请填写用户名");
return;
}
isSaving.value = true;
try {
const result = await UserAPI.updateCurrent({
id: formModel.id,
name: formModel.name,
nick: formModel.nickname,
gender: formModel.gender,
tempFileIdList: avatarTempFileId.value ? [avatarTempFileId.value] : undefined
});
await userStore.updateLoginUser(result);
Message.success("资料已保存");
await router.back();
} finally {
isSaving.value = false;
}
}
// ---------- 退出登录 ----------
async function confirmLogout(): Promise<void> {
const dialog = DialogPlugin.confirm({
title: "退出登录",
content: "确定要退出当前账号吗?",
cancelBtn: "取消",
confirmBtn: "退出",
onCancel() {
dialog.destroy();
},
async onConfirm() {
dialog.destroy();
await logout();
}
});
}
async function logout(): Promise<void> {
await userStore.logout();
await router.replace("/login");
}
// ---------- 格式化 ----------
function formatRoleCode(roleCode?: string): string {
if (!roleCode) {
return "";
}
const codePartList = roleCode.split(":");
return codePartList[codePartList.length - 1] || roleCode;
}
function buildUserAvatar(user?: UserAvatarSource): string {
const name = user?.nick || user?.name || "用户";
return UserAPI.getAvatarURL(user) || buildFallbackAvatar(name, "#0f766e");
}
onMounted(() => {
navBarStore.setRightText("保存", "var(--td-brand-color)", saveProfile);
fillForm();
});
onUnmounted(() => {
navBarStore.clearRight();
});
</script>
<style scoped lang="less">
.profile-page {
gap: 1rem;
padding: calc(var(--app-nav-height) + 1rem) 0 calc(1rem + env(safe-area-inset-bottom, 0)) 0;
display: flex;
flex-direction: column;
.form {
padding: .5rem 0;
background: rgba(255, 255, 255, .92);
box-shadow: var(--td-shadow-1);
border-radius: 1.4rem;
.sex {
gap: .5rem;
display: flex;
}
}
.avatar-input {
display: none;
}
.mobile-value {
color: var(--td-text-color-primary);
line-height: 1.5rem;
}
}
:global(html.theme-dark) {
.profile-page {
.form {
background: rgba(15, 23, 42, .88);
box-shadow: 0 .75rem 2rem rgba(2, 6, 23, .32);
}
}
}
</style>