云
17 小时以前 91bfcf61f44f353dfebc54a57b5b774721de3e9a
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
<script setup lang="ts">
import type { SystemUserProfileApi } from '#/api/system/user/profile';
import { onMounted, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { IconifyIcon } from '@vben/icons';
import { useUserStore } from '@vben/stores';
import { Skeleton, Tabs } from 'ant-design-vue';
import { getAuthPermissionInfoApi } from '#/api';
import { getUserProfile } from '#/api/system/user/profile';
import BaseInfo from './modules/base-info.vue';
import ProfileUser from './modules/profile-user.vue';
import ResetPwd from './modules/reset-pwd.vue';
import UserSocial from './modules/user-social.vue';
 
defineOptions({ name: 'Profile' });
const userStore = useUserStore();
const route = useRoute();
const router = useRouter();
const activeName = ref('basicInfo');
const profile = ref<SystemUserProfileApi.UserProfileRespVO>();
const loading = ref(true);
const failed = ref(false);
const refreshing = ref(false);
const sections = [
  { key: 'basicInfo', title: '基本资料', text: '完善个人联系方式,便于接收业务通知与日常联系。', icon: 'lucide:user-round-pen' },
  { key: 'resetPwd', title: '密码设置', text: '使用当前密码验证身份后,设置新的登录密码。', icon: 'lucide:shield-check' },
  { key: 'userSocial', title: '账号绑定', text: '管理第三方账号,便于使用已绑定的平台登录。', icon: 'lucide:link' },
];
watch(() => route.query.section, (value) => {
  activeName.value = ['basicInfo', 'resetPwd', 'userSocial'].includes(String(value)) ? String(value) : 'basicInfo';
}, { immediate: true });
function selectSection(value: string | number) {
  activeName.value = String(value);
}
async function loadProfile() {
  loading.value = true; failed.value = false;
  try { profile.value = await getUserProfile(); }
  catch { failed.value = true; }
  finally { loading.value = false; }
}
async function refreshProfile() {
  refreshing.value = true;
  try {
    profile.value = await getUserProfile();
    const info = await getAuthPermissionInfoApi();
    userStore.setUserInfo(info.user);
  } finally { refreshing.value = false; }
}
onMounted(loadProfile);
</script>
 
<template>
  <div class="portal-profile">
    <header class="profile-page-heading">
      <div><h1>个人中心</h1><span>管理个人资料、登录密码与第三方账号。</span></div>
      <button type="button" @click="router.push('/analytics')"><IconifyIcon icon="lucide:house" />返回首页</button>
    </header>
    <div v-if="loading" class="profile-loading" aria-live="polite"><Skeleton active avatar :paragraph="{ rows: 7 }" /><span>正在加载个人资料…</span></div>
    <div v-else-if="failed || !profile" class="profile-load-error" role="status"><IconifyIcon icon="lucide:cloud-off" /><h2>个人资料暂时无法加载</h2><p>请检查网络连接后重试。</p><button type="button" @click="loadProfile">重新加载</button></div>
    <div v-else class="profile-layout">
      <aside class="profile-identity"><ProfileUser :profile="profile" @success="refreshProfile" /></aside>
      <section class="profile-editor" aria-label="个人账号设置" :aria-busy="refreshing">
        <Tabs :active-key="activeName" @change="selectSection">
          <Tabs.TabPane v-for="section in sections" :key="section.key" force-render>
            <template #tab><span class="profile-tab"><IconifyIcon :icon="section.icon" />{{ section.title }}</span></template>
            <div class="profile-section-heading"><span class="profile-section-icon"><IconifyIcon :icon="section.icon" /></span><div><h2>{{ section.title }}</h2><p>{{ section.text }}</p></div></div>
            <BaseInfo v-if="section.key === 'basicInfo'" :profile="profile" @success="refreshProfile" />
            <ResetPwd v-else-if="section.key === 'resetPwd'" />
            <UserSocial v-else @update:active-name="selectSection" />
          </Tabs.TabPane>
        </Tabs>
      </section>
    </div>
  </div>
</template>
 
<style scoped>
.portal-profile { width: min(1240px, 100%); margin: 0 auto; padding: 10px 8px 32px; color: var(--portal-ink); }
.profile-page-heading { display: flex; align-items: center; justify-content: space-between; gap: 20px; padding: 12px 0 24px; }.profile-page-heading p { margin: 0 0 5px; color: var(--portal-muted); font-size: 12px; letter-spacing: .12em; }.profile-page-heading h1 { margin: 0 0 8px; font-size: 29px; font-weight: 700; }.profile-page-heading span { color: var(--portal-muted); font-size: 13px; }.profile-page-heading button { display: flex; min-height: 36px; align-items: center; gap: 7px; padding: 6px 12px; border: 1px solid #d8e5f0; border-radius: 5px; color: #315a80; background: var(--portal-surface); font-size: 12px; white-space: nowrap; }.profile-page-heading button svg { width: 15px; }
.profile-layout { display: grid; grid-template-columns: 282px minmax(0, 1fr); align-items: start; gap: 20px; }.profile-identity,.profile-editor { min-width: 0; overflow: hidden; border: 1px solid #dfebf4; border-radius: 7px; background: var(--portal-surface); box-shadow: 0 3px 10px rgb(24 66 106 / 3%); }.profile-editor { min-height: 540px; padding: 0 28px 32px; }.profile-editor :deep(.ant-tabs-nav) { margin-bottom: 24px; }.profile-editor :deep(.ant-tabs-tab) { padding: 19px 0 16px; }.profile-tab { display: inline-flex; align-items: center; gap: 7px; font-size: 14px; }.profile-tab svg { width: 16px; height: 16px; }.profile-section-heading { display: flex; align-items: center; gap: 12px; margin-bottom: 28px; }.profile-section-icon { display: grid; width: 42px; height: 42px; flex: none; place-items: center; border-radius: 9px; color: #2b6599; background: #edf5fc; }.profile-section-icon svg { width: 21px; }.profile-section-heading h2 { margin: 0 0 5px; font-size: 18px; font-weight: 650; }.profile-section-heading p { margin: 0; color: var(--portal-muted); font-size: 12px; line-height: 1.7; }.profile-loading,.profile-load-error { padding: 36px; border: 1px solid #dfeaf4; border-radius: 7px; background: var(--portal-surface); }.profile-loading > span { color: var(--portal-muted); font-size: 13px; }.profile-load-error { display: grid; min-height: 330px; justify-items: center; align-content: center; gap: 12px; text-align: center; }.profile-load-error > svg { width: 36px; height: 36px; color: #7695b0; }.profile-load-error h2 { margin: 0; font-size: 18px; }.profile-load-error p { margin: 0; color: var(--portal-muted); }.profile-load-error button { padding: 8px 18px; border-radius: 5px; color: #fff; background: #b7242d; }button { cursor: pointer; }button:focus-visible { outline: 2px solid #2c70b2; outline-offset: 3px; }
@media(max-width:1000px) { .profile-layout { grid-template-columns: 240px minmax(0,1fr); gap: 14px; }.profile-editor { padding: 0 20px 26px; } }
@media(max-width:767px) { .portal-profile { padding: 0 0 24px; }.profile-page-heading { align-items: flex-start; padding-top: 8px; }.profile-page-heading h1 { font-size: 25px; }.profile-page-heading span { display: block; max-width: 22em; line-height: 1.7; }.profile-page-heading button { padding: 6px 9px; }.profile-layout { grid-template-columns: 1fr; }.profile-editor { min-height: 0; padding: 0 18px 26px; }.profile-tab { gap: 5px; font-size: 13px; }.profile-editor :deep(.ant-tabs-tab + .ant-tabs-tab) { margin-left: 22px; }.profile-section-heading { margin-bottom: 24px; } }
</style>