feat: 实现各端个人信息功能
- 新增 ProfileView 共享个人信息页面 - 扩展 auth API UserProfile 支持各角色 - 为 admin/school/teacher/parent 添加 profile 路由 - 各端 Layout 用户菜单增加个人信息入口及跳转 - 家长端移动版抽屉菜单增加个人信息入口 Made-with: Cursor
This commit is contained in:
parent
23eab43590
commit
f25664cf9a
@ -21,13 +21,15 @@ export interface LoginResponse {
|
|||||||
|
|
||||||
export interface UserProfile {
|
export interface UserProfile {
|
||||||
id: number;
|
id: number;
|
||||||
|
username?: string;
|
||||||
name: string;
|
name: string;
|
||||||
role: 'admin' | 'school' | 'teacher';
|
role: 'admin' | 'school' | 'teacher' | 'parent';
|
||||||
tenantId?: number;
|
tenantId?: number;
|
||||||
tenantName?: string;
|
tenantName?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
phone?: string;
|
phone?: string;
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
|
avatarUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 登录
|
// 登录
|
||||||
|
|||||||
@ -11,6 +11,7 @@ declare module 'vue' {
|
|||||||
AAvatar: typeof import('ant-design-vue/es')['Avatar']
|
AAvatar: typeof import('ant-design-vue/es')['Avatar']
|
||||||
ABadge: typeof import('ant-design-vue/es')['Badge']
|
ABadge: typeof import('ant-design-vue/es')['Badge']
|
||||||
AButton: typeof import('ant-design-vue/es')['Button']
|
AButton: typeof import('ant-design-vue/es')['Button']
|
||||||
|
AButtonGroup: typeof import('ant-design-vue/es')['ButtonGroup']
|
||||||
ACard: typeof import('ant-design-vue/es')['Card']
|
ACard: typeof import('ant-design-vue/es')['Card']
|
||||||
ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
|
ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
|
||||||
ACheckboxGroup: typeof import('ant-design-vue/es')['CheckboxGroup']
|
ACheckboxGroup: typeof import('ant-design-vue/es')['CheckboxGroup']
|
||||||
@ -49,7 +50,6 @@ declare module 'vue' {
|
|||||||
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
|
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
|
||||||
ARangePicker: typeof import('ant-design-vue/es')['RangePicker']
|
ARangePicker: typeof import('ant-design-vue/es')['RangePicker']
|
||||||
ARate: typeof import('ant-design-vue/es')['Rate']
|
ARate: typeof import('ant-design-vue/es')['Rate']
|
||||||
AResult: typeof import('ant-design-vue/es')['Result']
|
|
||||||
ARow: typeof import('ant-design-vue/es')['Row']
|
ARow: typeof import('ant-design-vue/es')['Row']
|
||||||
ASelect: typeof import('ant-design-vue/es')['Select']
|
ASelect: typeof import('ant-design-vue/es')['Select']
|
||||||
ASelectOptGroup: typeof import('ant-design-vue/es')['SelectOptGroup']
|
ASelectOptGroup: typeof import('ant-design-vue/es')['SelectOptGroup']
|
||||||
|
|||||||
@ -118,6 +118,12 @@ const routes: RouteRecordRaw[] = [
|
|||||||
component: () => import('@/views/admin/SettingsView.vue'),
|
component: () => import('@/views/admin/SettingsView.vue'),
|
||||||
meta: { title: '系统设置' },
|
meta: { title: '系统设置' },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'profile',
|
||||||
|
name: 'AdminProfile',
|
||||||
|
component: () => import('@/views/profile/ProfileView.vue'),
|
||||||
|
meta: { title: '个人信息' },
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -262,6 +268,12 @@ const routes: RouteRecordRaw[] = [
|
|||||||
component: () => import('@/views/school/settings/SettingsView.vue'),
|
component: () => import('@/views/school/settings/SettingsView.vue'),
|
||||||
meta: { title: '系统设置' },
|
meta: { title: '系统设置' },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'profile',
|
||||||
|
name: 'SchoolProfile',
|
||||||
|
component: () => import('@/views/profile/ProfileView.vue'),
|
||||||
|
meta: { title: '个人信息' },
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -382,6 +394,12 @@ const routes: RouteRecordRaw[] = [
|
|||||||
component: () => import('@/views/teacher/growth/GrowthRecordView.vue'),
|
component: () => import('@/views/teacher/growth/GrowthRecordView.vue'),
|
||||||
meta: { title: '成长档案' },
|
meta: { title: '成长档案' },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'profile',
|
||||||
|
name: 'TeacherProfile',
|
||||||
|
component: () => import('@/views/profile/ProfileView.vue'),
|
||||||
|
meta: { title: '个人信息' },
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -429,6 +447,12 @@ const routes: RouteRecordRaw[] = [
|
|||||||
component: () => import('@/views/parent/growth/GrowthRecordView.vue'),
|
component: () => import('@/views/parent/growth/GrowthRecordView.vue'),
|
||||||
meta: { title: '成长档案' },
|
meta: { title: '成长档案' },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'profile',
|
||||||
|
name: 'ParentProfile',
|
||||||
|
component: () => import('@/views/profile/ProfileView.vue'),
|
||||||
|
meta: { title: '个人信息' },
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -174,6 +174,8 @@ watch(
|
|||||||
selectedKeys.value = ['resources'];
|
selectedKeys.value = ['resources'];
|
||||||
} else if (path.startsWith('/admin/settings')) {
|
} else if (path.startsWith('/admin/settings')) {
|
||||||
selectedKeys.value = ['settings'];
|
selectedKeys.value = ['settings'];
|
||||||
|
} else if (path.startsWith('/admin/profile')) {
|
||||||
|
selectedKeys.value = [];
|
||||||
} else {
|
} else {
|
||||||
selectedKeys.value = ['dashboard'];
|
selectedKeys.value = ['dashboard'];
|
||||||
}
|
}
|
||||||
@ -203,7 +205,7 @@ const handleUserMenuClick = ({ key }: { key: string | number }) => {
|
|||||||
if (keyStr === 'logout') {
|
if (keyStr === 'logout') {
|
||||||
userStore.logout();
|
userStore.logout();
|
||||||
} else if (keyStr === 'profile') {
|
} else if (keyStr === 'profile') {
|
||||||
// 跳转到个人信息页面
|
router.push('/admin/profile');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -95,6 +95,10 @@
|
|||||||
<template #icon><FileImageOutlined /></template>
|
<template #icon><FileImageOutlined /></template>
|
||||||
<span>成长档案</span>
|
<span>成长档案</span>
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
|
<a-menu-item key="profile">
|
||||||
|
<template #icon><UserOutlined /></template>
|
||||||
|
<span>个人信息</span>
|
||||||
|
</a-menu-item>
|
||||||
</a-menu>
|
</a-menu>
|
||||||
|
|
||||||
<div class="drawer-footer">
|
<div class="drawer-footer">
|
||||||
@ -136,6 +140,11 @@
|
|||||||
</a-space>
|
</a-space>
|
||||||
<template #overlay>
|
<template #overlay>
|
||||||
<a-menu @click="handleUserMenuClick">
|
<a-menu @click="handleUserMenuClick">
|
||||||
|
<a-menu-item key="profile">
|
||||||
|
<UserOutlined />
|
||||||
|
个人信息
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-divider />
|
||||||
<a-menu-item key="logout">
|
<a-menu-item key="logout">
|
||||||
<LogoutOutlined />
|
<LogoutOutlined />
|
||||||
退出登录
|
退出登录
|
||||||
@ -217,6 +226,7 @@ const pageTitleMap: Record<string, string> = {
|
|||||||
lessons: '阅读记录',
|
lessons: '阅读记录',
|
||||||
tasks: '阅读任务',
|
tasks: '阅读任务',
|
||||||
growth: '成长档案',
|
growth: '成长档案',
|
||||||
|
profile: '个人信息',
|
||||||
};
|
};
|
||||||
|
|
||||||
const pageTitle = computed(() => {
|
const pageTitle = computed(() => {
|
||||||
@ -253,6 +263,8 @@ watch(
|
|||||||
selectedKeys.value = ['tasks'];
|
selectedKeys.value = ['tasks'];
|
||||||
} else if (path.startsWith('/parent/growth')) {
|
} else if (path.startsWith('/parent/growth')) {
|
||||||
selectedKeys.value = ['growth'];
|
selectedKeys.value = ['growth'];
|
||||||
|
} else if (path.startsWith('/parent/profile')) {
|
||||||
|
selectedKeys.value = ['profile'];
|
||||||
} else {
|
} else {
|
||||||
selectedKeys.value = ['dashboard'];
|
selectedKeys.value = ['dashboard'];
|
||||||
}
|
}
|
||||||
@ -265,7 +277,8 @@ const handleMenuClick = ({ key }: { key: string | number }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleMobileMenuClick = ({ key }: { key: string | number }) => {
|
const handleMobileMenuClick = ({ key }: { key: string | number }) => {
|
||||||
router.push(`/parent/${key}`);
|
const keyStr = String(key);
|
||||||
|
router.push(`/parent/${keyStr}`);
|
||||||
drawerVisible.value = false;
|
drawerVisible.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -278,6 +291,8 @@ const handleUserMenuClick = ({ key }: { key: string | number }) => {
|
|||||||
const keyStr = String(key);
|
const keyStr = String(key);
|
||||||
if (keyStr === 'logout') {
|
if (keyStr === 'logout') {
|
||||||
handleLogout();
|
handleLogout();
|
||||||
|
} else if (keyStr === 'profile') {
|
||||||
|
router.push('/parent/profile');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
140
reading-platform-frontend/src/views/profile/ProfileView.vue
Normal file
140
reading-platform-frontend/src/views/profile/ProfileView.vue
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
<template>
|
||||||
|
<div class="profile-view">
|
||||||
|
<div class="page-header">
|
||||||
|
<h1><UserOutlined /> 个人信息</h1>
|
||||||
|
<p>查看和修改您的账户信息</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a-spin :spinning="loading">
|
||||||
|
<div class="profile-content" v-if="profile">
|
||||||
|
<div class="profile-card">
|
||||||
|
<div class="avatar-section">
|
||||||
|
<a-avatar :size="96" class="profile-avatar">
|
||||||
|
<img v-if="avatarUrl" :src="avatarUrl" alt="头像" />
|
||||||
|
<UserOutlined v-else />
|
||||||
|
</a-avatar>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-section">
|
||||||
|
<a-descriptions :column="1" bordered size="small">
|
||||||
|
<a-descriptions-item label="姓名">
|
||||||
|
{{ profile.name || '-' }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="账号">
|
||||||
|
{{ profile.username || '-' }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="角色">
|
||||||
|
{{ roleLabel }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="手机号">
|
||||||
|
{{ profile.phone || '-' }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="邮箱">
|
||||||
|
{{ profile.email || '-' }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="所属机构" v-if="profile.tenantId">
|
||||||
|
{{ profile.tenantName || `租户ID: ${profile.tenantId}` }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
</a-descriptions>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a-empty v-else-if="!loading" description="加载失败,请刷新重试" />
|
||||||
|
</a-spin>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted } from 'vue';
|
||||||
|
import { UserOutlined } from '@ant-design/icons-vue';
|
||||||
|
import { getProfile, type UserProfile } from '@/api/auth';
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const profile = ref<UserProfile | null>(null);
|
||||||
|
|
||||||
|
const avatarUrl = computed(() => {
|
||||||
|
const p = profile.value;
|
||||||
|
if (!p) return '';
|
||||||
|
return (p.avatarUrl || p.avatar || '') as string;
|
||||||
|
});
|
||||||
|
|
||||||
|
const roleLabel = computed(() => {
|
||||||
|
const role = profile.value?.role;
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
admin: '超管',
|
||||||
|
school: '学校管理员',
|
||||||
|
teacher: '教师',
|
||||||
|
parent: '家长',
|
||||||
|
};
|
||||||
|
return role ? map[role] || role : '-';
|
||||||
|
});
|
||||||
|
|
||||||
|
const loadProfile = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const data = await getProfile();
|
||||||
|
profile.value = data as UserProfile;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('获取个人信息失败', error);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadProfile();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.profile-view {
|
||||||
|
padding: 24px;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(180deg, #FFF8F0 0%, #FFFFFF 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header p {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 32px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
|
||||||
|
max-width: 560px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-section {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-avatar {
|
||||||
|
background: linear-gradient(135deg, #FF8C42 0%, #FFB347 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-section :deep(.ant-descriptions-item-label) {
|
||||||
|
width: 100px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -249,6 +249,8 @@ watch(
|
|||||||
}
|
}
|
||||||
} else if (path === '/school' || path === '/school/dashboard') {
|
} else if (path === '/school' || path === '/school/dashboard') {
|
||||||
selectedKeys.value = ['dashboard'];
|
selectedKeys.value = ['dashboard'];
|
||||||
|
} else if (path.startsWith('/school/profile')) {
|
||||||
|
selectedKeys.value = [];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
@ -273,7 +275,7 @@ const handleUserMenuClick = ({ key }: { key: string | number }) => {
|
|||||||
message.success('退出成功');
|
message.success('退出成功');
|
||||||
router.push('/login');
|
router.push('/login');
|
||||||
} else if (keyStr === 'profile') {
|
} else if (keyStr === 'profile') {
|
||||||
// 跳转到个人信息页面
|
router.push('/school/profile');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -172,6 +172,8 @@ watch(
|
|||||||
selectedKeys.value = ['feedback'];
|
selectedKeys.value = ['feedback'];
|
||||||
} else if (path.startsWith('/teacher/growth')) {
|
} else if (path.startsWith('/teacher/growth')) {
|
||||||
selectedKeys.value = ['growth'];
|
selectedKeys.value = ['growth'];
|
||||||
|
} else if (path.startsWith('/teacher/profile')) {
|
||||||
|
selectedKeys.value = [];
|
||||||
} else {
|
} else {
|
||||||
selectedKeys.value = ['dashboard'];
|
selectedKeys.value = ['dashboard'];
|
||||||
}
|
}
|
||||||
@ -190,7 +192,7 @@ const handleUserMenuClick = ({ key }: { key: string | number }) => {
|
|||||||
message.success('退出成功');
|
message.success('退出成功');
|
||||||
router.push('/login');
|
router.push('/login');
|
||||||
} else if (keyStr === 'profile') {
|
} else if (keyStr === 'profile') {
|
||||||
// 跳转到个人信息页面
|
router.push('/teacher/profile');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
13
reading-platform-frontend/typed-router.d.ts
vendored
13
reading-platform-frontend/typed-router.d.ts
vendored
@ -240,6 +240,13 @@ declare module 'vue-router/auto-routes' {
|
|||||||
Record<never, never>,
|
Record<never, never>,
|
||||||
| never
|
| never
|
||||||
>,
|
>,
|
||||||
|
'/profile/ProfileView': RouteRecordInfo<
|
||||||
|
'/profile/ProfileView',
|
||||||
|
'/profile/ProfileView',
|
||||||
|
Record<never, never>,
|
||||||
|
Record<never, never>,
|
||||||
|
| never
|
||||||
|
>,
|
||||||
'/school/classes/ClassListView': RouteRecordInfo<
|
'/school/classes/ClassListView': RouteRecordInfo<
|
||||||
'/school/classes/ClassListView',
|
'/school/classes/ClassListView',
|
||||||
'/school/classes/ClassListView',
|
'/school/classes/ClassListView',
|
||||||
@ -880,6 +887,12 @@ declare module 'vue-router/auto-routes' {
|
|||||||
views:
|
views:
|
||||||
| never
|
| never
|
||||||
}
|
}
|
||||||
|
'src/views/profile/ProfileView.vue': {
|
||||||
|
routes:
|
||||||
|
| '/profile/ProfileView'
|
||||||
|
views:
|
||||||
|
| never
|
||||||
|
}
|
||||||
'src/views/school/classes/ClassListView.vue': {
|
'src/views/school/classes/ClassListView.vue': {
|
||||||
routes:
|
routes:
|
||||||
| '/school/classes/ClassListView'
|
| '/school/classes/ClassListView'
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user