| | |
| | | <script setup lang="ts"> |
| | | import type { SystemUserProfileApi } from '#/api/system/user/profile'; |
| | | |
| | | import { onMounted, ref } from 'vue'; |
| | | |
| | | import { Page } from '@vben/common-ui'; |
| | | import { onMounted, ref, watch } from 'vue'; |
| | | import { useRoute, useRouter } from 'vue-router'; |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | import { useUserStore } from '@vben/stores'; |
| | | |
| | | import { Card, Tabs } from 'ant-design-vue'; |
| | | |
| | | 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() { |
| | | profile.value = await getUserProfile(); |
| | | loading.value = true; failed.value = false; |
| | | try { profile.value = await getUserProfile(); } |
| | | catch { failed.value = true; } |
| | | finally { loading.value = false; } |
| | | } |
| | | |
| | | /** 刷新个人信息 */ |
| | | async function refreshProfile() { |
| | | // 加载个人信息 |
| | | await loadProfile(); |
| | | |
| | | // 更新 store |
| | | const authPermissionInfo = await getAuthPermissionInfoApi(); |
| | | userStore.setUserInfo(authPermissionInfo.user); |
| | | refreshing.value = true; |
| | | try { |
| | | profile.value = await getUserProfile(); |
| | | const info = await getAuthPermissionInfoApi(); |
| | | userStore.setUserInfo(info.user); |
| | | } finally { refreshing.value = false; } |
| | | } |
| | | |
| | | /** 初始化 */ |
| | | onMounted(loadProfile); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <div class="flex"> |
| | | <!-- 左侧 个人信息 --> |
| | | <Card class="w-2/5" title="个人信息"> |
| | | <ProfileUser :profile="profile" @success="refreshProfile" /> |
| | | </Card> |
| | | |
| | | <!-- 右侧 标签页 --> |
| | | <Card class="ml-3 w-3/5"> |
| | | <Tabs v-model:active-key="activeName" class="-mt-4"> |
| | | <Tabs.TabPane key="basicInfo" tab="基本设置"> |
| | | <BaseInfo :profile="profile" @success="refreshProfile" /> |
| | | <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.TabPane key="resetPwd" tab="密码设置"> |
| | | <ResetPwd /> |
| | | </Tabs.TabPane> |
| | | <Tabs.TabPane key="userSocial" tab="社交绑定" force-render> |
| | | <UserSocial @update:active-name="activeName = $event" /> |
| | | </Tabs.TabPane> |
| | | <!-- TODO @芋艿:在线设备 --> |
| | | </Tabs> |
| | | </Card> |
| | | </section> |
| | | </div> |
| | | </Page> |
| | | </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> |
| | | |