云
13 小时以前 91bfcf61f44f353dfebc54a57b5b774721de3e9a
src/views/_core/profile/modules/profile-user.vue
@@ -1,148 +1,40 @@
<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>
          </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 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>
<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>