<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>
|