| | |
| | | <script setup lang="ts"> |
| | | import type { SystemUserProfileApi } from '#/api/system/user/profile'; |
| | | |
| | | import { computed } from 'vue'; |
| | | |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | import { preferences } from '@vben/preferences'; |
| | | import { formatDateTime } from '@vben/utils'; |
| | | |
| | | import { Descriptions, DescriptionsItem, Tooltip } from 'ant-design-vue'; |
| | | |
| | | import { updateUserProfile } from '#/api/system/user/profile'; |
| | | import { CropperAvatar } from '#/components/cropper'; |
| | | import { useUpload } from '#/components/upload/use-upload'; |
| | | |
| | | const props = defineProps<{ |
| | | profile?: SystemUserProfileApi.UserProfileRespVO; |
| | | }>(); |
| | | |
| | | const emit = defineEmits<{ |
| | | (e: 'success'): void; |
| | | }>(); |
| | | |
| | | const avatar = computed( |
| | | () => props.profile?.avatar || preferences.app.defaultAvatar, |
| | | ); |
| | | |
| | | async function handelUpload({ |
| | | file, |
| | | filename, |
| | | }: { |
| | | file: Blob; |
| | | filename: string; |
| | | }) { |
| | | // 1. 上传头像,获取 URL |
| | | const props = defineProps<{ profile: SystemUserProfileApi.UserProfileRespVO }>(); |
| | | const emit = defineEmits<{ success: [] }>(); |
| | | const avatar = computed(() => props.profile.avatar || preferences.app.defaultAvatar); |
| | | const details = computed(() => [ |
| | | { label: '用户账号', value: props.profile.username, icon: 'lucide:user-round' }, |
| | | { label: '所属部门', value: props.profile.dept?.name, icon: 'lucide:building-2' }, |
| | | { label: '所属岗位', value: props.profile.posts?.map(p => p.name).join('、'), icon: 'lucide:briefcase-business' }, |
| | | { label: '手机号码', value: props.profile.mobile, icon: 'lucide:smartphone' }, |
| | | { label: '电子邮箱', value: props.profile.email, icon: 'lucide:mail' }, |
| | | ]); |
| | | async function handleUpload({ file, filename }: { file: Blob; filename: string }) { |
| | | const { httpRequest } = useUpload(); |
| | | // 将 Blob 转换为 File |
| | | const fileObj = new File([file], filename, { type: file.type }); |
| | | const avatar = await httpRequest(fileObj); |
| | | // 2. 更新用户头像 |
| | | await updateUserProfile({ avatar }); |
| | | const result = await httpRequest(new File([file], filename, { type: file.type })); |
| | | if (!result.url) throw new Error('头像上传未返回图片地址,请重试'); |
| | | await updateUserProfile({ avatar: result.url }); |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <div v-if="profile"> |
| | | <div class="flex flex-col items-center"> |
| | | <Tooltip title="点击上传头像"> |
| | | <CropperAvatar |
| | | :show-btn="false" |
| | | :upload-api="handelUpload" |
| | | :value="avatar" |
| | | :width="120" |
| | | @change="emit('success')" |
| | | /> |
| | | </Tooltip> |
| | | </div> |
| | | <div class="mt-8"> |
| | | <Descriptions :column="2"> |
| | | <DescriptionsItem> |
| | | <template #label> |
| | | <div class="flex items-center"> |
| | | <IconifyIcon icon="ant-design:user-outlined" class="mr-1" /> |
| | | 用户账号 |
| | | <div class="profile-user"> |
| | | <div class="profile-user-banner" aria-hidden="true"></div> |
| | | <div class="profile-avatar"><CropperAvatar :value="avatar" :width="84" :upload-api="handleUpload" :show-btn="true" btn-text="更换头像" :btn-props="{ type: 'link', size: 'small' }" @change="emit('success')" /></div> |
| | | <div class="profile-user-name"><h2>{{ profile.nickname || profile.username }}</h2><p>{{ profile.dept?.name || '个人账号' }}</p><div v-if="profile.roles?.length" class="profile-roles"><span v-for="role in profile.roles" :key="role.id ?? role.name">{{ role.name }}</span></div></div> |
| | | <dl class="profile-facts"><div v-for="item in details" :key="item.label"><dt><IconifyIcon :icon="item.icon" />{{ item.label }}</dt><dd>{{ item.value || '未设置' }}</dd></div></dl> |
| | | <div class="profile-login"><span><IconifyIcon icon="lucide:clock-3" />最近登录</span><time>{{ profile.loginDate ? formatDateTime(profile.loginDate) : '暂无记录' }}</time><small v-if="profile.createTime">加入时间:{{ formatDateTime(profile.createTime)?.slice(0, 10) }}</small></div> |
| | | </div> |
| | | </template> |
| | | {{ profile.username }} |
| | | </DescriptionsItem> |
| | | <DescriptionsItem> |
| | | <template #label> |
| | | <div class="flex items-center"> |
| | | <IconifyIcon |
| | | icon="ant-design:user-switch-outlined" |
| | | class="mr-1" |
| | | /> |
| | | 所属角色 |
| | | </div> |
| | | </template> |
| | | {{ profile.roles.map((role) => role.name).join(',') }} |
| | | </DescriptionsItem> |
| | | <DescriptionsItem> |
| | | <template #label> |
| | | <div class="flex items-center"> |
| | | <IconifyIcon icon="ant-design:phone-outlined" class="mr-1" /> |
| | | 手机号码 |
| | | </div> |
| | | </template> |
| | | {{ profile.mobile }} |
| | | </DescriptionsItem> |
| | | <DescriptionsItem> |
| | | <template #label> |
| | | <div class="flex items-center"> |
| | | <IconifyIcon icon="ant-design:mail-outlined" class="mr-1" /> |
| | | 用户邮箱 |
| | | </div> |
| | | </template> |
| | | {{ profile.email }} |
| | | </DescriptionsItem> |
| | | <DescriptionsItem> |
| | | <template #label> |
| | | <div class="flex items-center"> |
| | | <IconifyIcon icon="ant-design:team-outlined" class="mr-1" /> |
| | | 所属部门 |
| | | </div> |
| | | </template> |
| | | {{ profile.dept?.name }} |
| | | </DescriptionsItem> |
| | | <DescriptionsItem> |
| | | <template #label> |
| | | <div class="flex items-center"> |
| | | <IconifyIcon |
| | | icon="ant-design:usergroup-add-outlined" |
| | | class="mr-1" |
| | | /> |
| | | 所属岗位 |
| | | </div> |
| | | </template> |
| | | {{ |
| | | profile.posts && profile.posts.length > 0 |
| | | ? profile.posts.map((post) => post.name).join(',') |
| | | : '-' |
| | | }} |
| | | </DescriptionsItem> |
| | | <DescriptionsItem> |
| | | <template #label> |
| | | <div class="flex items-center"> |
| | | <IconifyIcon |
| | | icon="ant-design:clock-circle-outlined" |
| | | class="mr-1" |
| | | /> |
| | | 创建时间 |
| | | </div> |
| | | </template> |
| | | {{ formatDateTime(profile.createTime) }} |
| | | </DescriptionsItem> |
| | | <DescriptionsItem> |
| | | <template #label> |
| | | <div class="flex items-center"> |
| | | <IconifyIcon icon="ant-design:login-outlined" class="mr-1" /> |
| | | 登录时间 |
| | | </div> |
| | | </template> |
| | | {{ formatDateTime(profile.loginDate) }} |
| | | </DescriptionsItem> |
| | | </Descriptions> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | <style scoped> |
| | | .profile-user-banner { height: 78px; background: linear-gradient(100deg, rgb(24 74 116 / 92%), rgb(43 105 154 / 38%)), url('/static/imgs/portal-hero-v2.png') center 60% / cover; }.profile-avatar { position: relative; display: flex; justify-content: center; margin-top: -42px; }.profile-avatar :deep(.group) { border: 4px solid #fff; box-shadow: 0 3px 8px rgb(24 66 106 / 9%); }.profile-avatar :deep(.ant-btn) { font-size: 11px; }.profile-user-name { padding: 5px 20px 20px; text-align: center; }.profile-user-name h2 { margin: 0 0 5px; font-size: 21px; font-weight: 650; overflow-wrap: anywhere; }.profile-user-name p { margin: 0; color: var(--portal-muted); font-size: 12px; }.profile-roles { display: flex; flex-wrap: wrap; justify-content: center; gap: 6px; margin-top: 12px; }.profile-roles span { padding: 3px 8px; border-radius: 4px; color: #2d618b; background: #edf5fc; font-size: 11px; }.profile-facts { margin: 0 20px; padding: 5px 0 10px; border-top: 1px solid #e7eff6; }.profile-facts > div { padding: 12px 0; border-bottom: 1px solid #edf2f7; }.profile-facts dt { display: flex; align-items: center; gap: 7px; margin-bottom: 6px; color: var(--portal-muted); font-size: 11px; }.profile-facts svg,.profile-login svg { width: 14px; height: 14px; }.profile-facts dd { margin: 0; padding-left: 21px; color: var(--portal-ink); font-size: 13px; line-height: 1.6; overflow-wrap: anywhere; }.profile-login { display: grid; gap: 6px; padding: 10px 20px 22px; color: var(--portal-muted); font-size: 11px; }.profile-login > span { display: flex; align-items: center; gap: 7px; }.profile-login time { padding-left: 21px; }.profile-login small { padding-left: 21px; font-size: 10px; } |
| | | @media(max-width:767px) { .profile-facts { display: grid; grid-template-columns: repeat(2,minmax(0,1fr)); column-gap: 16px; }.profile-login { grid-template-columns: auto 1fr; }.profile-login time { padding: 0; }.profile-login small { grid-column: 1 / -1; } } |
| | | </style> |
| | | |