feat(布局): 顶部通知与个人信息
- 新增统一通知 API (api/notification.ts),支持教师/园校/家长端通知列表、未读数、标记已读 - NotificationBell 支持多角色,各端布局顶部统一使用该组件 - 新增个人信息页 (profile/ProfileView.vue),展示姓名、角色、机构、邮箱、手机等 - 各端增加 /profile 路由,用户下拉菜单「个人信息」跳转至对应个人信息页 - auth 类型补充 parent 角色 Made-with: Cursor
This commit is contained in:
parent
709c9a9a57
commit
55ab98f361
@ -11,7 +11,7 @@ export interface LoginResponse {
|
|||||||
user: {
|
user: {
|
||||||
id: number;
|
id: number;
|
||||||
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;
|
||||||
@ -22,7 +22,7 @@ export interface LoginResponse {
|
|||||||
export interface UserProfile {
|
export interface UserProfile {
|
||||||
id: number;
|
id: number;
|
||||||
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;
|
||||||
|
|||||||
52
reading-platform-frontend/src/api/notification.ts
Normal file
52
reading-platform-frontend/src/api/notification.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { http } from './index';
|
||||||
|
|
||||||
|
export interface NotificationItem {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
notificationType: string;
|
||||||
|
isRead: boolean;
|
||||||
|
readAt?: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NotificationsResponse {
|
||||||
|
items: NotificationItem[];
|
||||||
|
total: number;
|
||||||
|
unreadCount: number;
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NotificationQueryParams {
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
isRead?: boolean;
|
||||||
|
notificationType?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 根据角色获取通知列表(teacher | school | parent) */
|
||||||
|
export function getNotifications(
|
||||||
|
role: 'teacher' | 'school' | 'parent',
|
||||||
|
params?: NotificationQueryParams
|
||||||
|
): Promise<NotificationsResponse> {
|
||||||
|
return http.get(`/${role}/notifications`, { params });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 根据角色获取未读数量 */
|
||||||
|
export function getUnreadCount(role: 'teacher' | 'school' | 'parent'): Promise<number> {
|
||||||
|
return http.get(`/${role}/notifications/unread-count`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 标记单条已读 */
|
||||||
|
export function markNotificationAsRead(
|
||||||
|
role: 'teacher' | 'school' | 'parent',
|
||||||
|
id: number
|
||||||
|
): Promise<any> {
|
||||||
|
return http.put(`/${role}/notifications/${id}/read`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 全部标记已读 */
|
||||||
|
export function markAllNotificationsAsRead(role: 'teacher' | 'school' | 'parent'): Promise<any> {
|
||||||
|
return http.put(`/${role}/notifications/read-all`);
|
||||||
|
}
|
||||||
@ -65,7 +65,12 @@ import {
|
|||||||
SoundOutlined,
|
SoundOutlined,
|
||||||
} from '@ant-design/icons-vue';
|
} from '@ant-design/icons-vue';
|
||||||
import { useUserStore } from '@/stores/user';
|
import { useUserStore } from '@/stores/user';
|
||||||
import { getNotifications, markNotificationAsRead, markAllNotificationsAsRead } from '@/api/parent';
|
import {
|
||||||
|
getNotifications,
|
||||||
|
getUnreadCount,
|
||||||
|
markNotificationAsRead,
|
||||||
|
markAllNotificationsAsRead,
|
||||||
|
} from '@/api/notification';
|
||||||
import type { Component } from 'vue';
|
import type { Component } from 'vue';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
@ -108,17 +113,16 @@ const handleBellClick = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const loadNotifications = async () => {
|
const loadNotifications = async () => {
|
||||||
|
const role = userStore.user?.role as 'teacher' | 'school' | 'parent' | undefined;
|
||||||
|
if (role !== 'teacher' && role !== 'school' && role !== 'parent') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const role = userStore.user?.role;
|
const data = await getNotifications(role, { pageSize: 5 });
|
||||||
// 根据角色调用不同的API
|
|
||||||
if (role === 'parent') {
|
|
||||||
const data = await getNotifications({ pageSize: 5 });
|
|
||||||
notifications.value = data.items;
|
notifications.value = data.items;
|
||||||
total.value = data.total;
|
total.value = data.total;
|
||||||
unreadCount.value = data.unreadCount;
|
unreadCount.value = data.unreadCount;
|
||||||
}
|
|
||||||
// 其他角色的通知可以在这里扩展
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Failed to load notifications:', error);
|
console.error('Failed to load notifications:', error);
|
||||||
} finally {
|
} finally {
|
||||||
@ -127,12 +131,10 @@ const loadNotifications = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleNotificationClick = async (notification: any) => {
|
const handleNotificationClick = async (notification: any) => {
|
||||||
if (!notification.isRead) {
|
const role = userStore.user?.role as 'teacher' | 'school' | 'parent' | undefined;
|
||||||
|
if (!notification.isRead && (role === 'teacher' || role === 'school' || role === 'parent')) {
|
||||||
try {
|
try {
|
||||||
const role = userStore.user?.role;
|
await markNotificationAsRead(role, notification.id);
|
||||||
if (role === 'parent') {
|
|
||||||
await markNotificationAsRead(notification.id);
|
|
||||||
}
|
|
||||||
notification.isRead = true;
|
notification.isRead = true;
|
||||||
unreadCount.value = Math.max(0, unreadCount.value - 1);
|
unreadCount.value = Math.max(0, unreadCount.value - 1);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -143,11 +145,10 @@ const handleNotificationClick = async (notification: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const markAllRead = async () => {
|
const markAllRead = async () => {
|
||||||
|
const role = userStore.user?.role as 'teacher' | 'school' | 'parent' | undefined;
|
||||||
|
if (role !== 'teacher' && role !== 'school' && role !== 'parent') return;
|
||||||
try {
|
try {
|
||||||
const role = userStore.user?.role;
|
await markAllNotificationsAsRead(role);
|
||||||
if (role === 'parent') {
|
|
||||||
await markAllNotificationsAsRead();
|
|
||||||
}
|
|
||||||
notifications.value.forEach((n) => (n.isRead = true));
|
notifications.value.forEach((n) => (n.isRead = true));
|
||||||
unreadCount.value = 0;
|
unreadCount.value = 0;
|
||||||
message.success('已全部标记为已读');
|
message.success('已全部标记为已读');
|
||||||
@ -162,7 +163,10 @@ const viewAll = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 可以定期检查未读数量
|
const role = userStore.user?.role as 'teacher' | 'school' | 'parent' | undefined;
|
||||||
|
if (role === 'teacher' || role === 'school' || role === 'parent') {
|
||||||
|
getUnreadCount(role).then((n) => (unreadCount.value = n)).catch(() => {});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -112,6 +112,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: '个人信息' },
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -256,6 +262,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: '个人信息' },
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -376,6 +388,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: '个人信息' },
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -423,6 +441,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: '个人信息' },
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -149,9 +149,7 @@
|
|||||||
|
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<a-space>
|
<a-space>
|
||||||
<a-badge count="5">
|
<NotificationBell />
|
||||||
<BellOutlined style="font-size: 18px; cursor: pointer; color: #666;" />
|
|
||||||
</a-badge>
|
|
||||||
|
|
||||||
<a-dropdown>
|
<a-dropdown>
|
||||||
<a-space class="user-info" style="cursor: pointer;">
|
<a-space class="user-info" style="cursor: pointer;">
|
||||||
@ -227,6 +225,7 @@ import {
|
|||||||
Settings,
|
Settings,
|
||||||
} from 'lucide-vue-next';
|
} from 'lucide-vue-next';
|
||||||
import { DatabaseOutlined, FormatPainterOutlined } from '@ant-design/icons-vue';
|
import { DatabaseOutlined, FormatPainterOutlined } from '@ant-design/icons-vue';
|
||||||
|
import NotificationBell from '@/components/NotificationBell.vue';
|
||||||
import { useUserStore } from '@/stores/user';
|
import { useUserStore } from '@/stores/user';
|
||||||
import { useBreakpoints } from '@/composables/useBreakpoints';
|
import { useBreakpoints } from '@/composables/useBreakpoints';
|
||||||
|
|
||||||
@ -293,7 +292,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>
|
||||||
|
|||||||
@ -122,9 +122,7 @@
|
|||||||
|
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<a-space>
|
<a-space>
|
||||||
<a-badge :count="notifications">
|
<NotificationBell />
|
||||||
<BellOutlined style="font-size: 18px; cursor: pointer; color: #666;" />
|
|
||||||
</a-badge>
|
|
||||||
|
|
||||||
<a-dropdown>
|
<a-dropdown>
|
||||||
<a-space class="user-info" style="cursor: pointer;">
|
<a-space class="user-info" style="cursor: pointer;">
|
||||||
@ -136,6 +134,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 />
|
||||||
退出登录
|
退出登录
|
||||||
@ -152,9 +155,7 @@
|
|||||||
<div class="mobile-header-content">
|
<div class="mobile-header-content">
|
||||||
<MenuOutlined class="menu-icon" @click="drawerVisible = true" />
|
<MenuOutlined class="menu-icon" @click="drawerVisible = true" />
|
||||||
<span class="page-title">{{ pageTitle }}</span>
|
<span class="page-title">{{ pageTitle }}</span>
|
||||||
<a-badge :count="notifications" class="notification-badge">
|
<NotificationBell icon-color="#333" />
|
||||||
<BellOutlined style="font-size: 20px; color: #333;" />
|
|
||||||
</a-badge>
|
|
||||||
</div>
|
</div>
|
||||||
</a-layout-header>
|
</a-layout-header>
|
||||||
|
|
||||||
@ -191,11 +192,11 @@ import {
|
|||||||
CheckSquareOutlined,
|
CheckSquareOutlined,
|
||||||
MenuUnfoldOutlined,
|
MenuUnfoldOutlined,
|
||||||
MenuFoldOutlined,
|
MenuFoldOutlined,
|
||||||
BellOutlined,
|
|
||||||
LogoutOutlined,
|
LogoutOutlined,
|
||||||
DownOutlined,
|
DownOutlined,
|
||||||
MenuOutlined,
|
MenuOutlined,
|
||||||
} from '@ant-design/icons-vue';
|
} from '@ant-design/icons-vue';
|
||||||
|
import NotificationBell from '@/components/NotificationBell.vue';
|
||||||
import { useUserStore } from '@/stores/user';
|
import { useUserStore } from '@/stores/user';
|
||||||
import { useBreakpoints } from '@/composables/useBreakpoints';
|
import { useBreakpoints } from '@/composables/useBreakpoints';
|
||||||
|
|
||||||
@ -206,7 +207,6 @@ const { isMobile } = useBreakpoints();
|
|||||||
|
|
||||||
const collapsed = ref(false);
|
const collapsed = ref(false);
|
||||||
const selectedKeys = ref(['dashboard']);
|
const selectedKeys = ref(['dashboard']);
|
||||||
const notifications = ref(0);
|
|
||||||
const drawerVisible = ref(false);
|
const drawerVisible = ref(false);
|
||||||
|
|
||||||
const userName = computed(() => userStore.user?.name || '家长');
|
const userName = computed(() => userStore.user?.name || '家长');
|
||||||
@ -275,6 +275,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');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
160
reading-platform-frontend/src/views/profile/ProfileView.vue
Normal file
160
reading-platform-frontend/src/views/profile/ProfileView.vue
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
<template>
|
||||||
|
<div class="profile-view">
|
||||||
|
<div class="profile-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2 class="card-title">个人信息</h2>
|
||||||
|
<a-button type="primary" ghost size="small" :loading="loading" @click="refresh">
|
||||||
|
刷新
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
<a-spin :spinning="loading">
|
||||||
|
<div v-if="user" class="profile-body">
|
||||||
|
<div class="avatar-section">
|
||||||
|
<a-avatar :size="80" class="profile-avatar">
|
||||||
|
<template #icon><UserOutlined /></template>
|
||||||
|
</a-avatar>
|
||||||
|
<div class="name-role">
|
||||||
|
<span class="display-name">{{ user.name }}</span>
|
||||||
|
<a-tag :color="roleTagColor">{{ roleLabel }}</a-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a-descriptions bordered size="small" :column="1" class="profile-desc">
|
||||||
|
<a-descriptions-item label="姓名">{{ user.name }}</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="角色">{{ roleLabel }}</a-descriptions-item>
|
||||||
|
<a-descriptions-item v-if="user.tenantName" label="所属机构">
|
||||||
|
{{ user.tenantName }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item v-if="user.email" label="邮箱">
|
||||||
|
{{ user.email || '—' }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item v-if="user.phone" label="手机号">
|
||||||
|
{{ user.phone || '—' }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
</a-descriptions>
|
||||||
|
</div>
|
||||||
|
<div v-else class="empty-tip">
|
||||||
|
<UserOutlined />
|
||||||
|
<p>暂无个人信息,请刷新重试</p>
|
||||||
|
</div>
|
||||||
|
</a-spin>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted } from 'vue';
|
||||||
|
import { UserOutlined } from '@ant-design/icons-vue';
|
||||||
|
import { useUserStore } from '@/stores/user';
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
const user = computed(() => userStore.user);
|
||||||
|
|
||||||
|
const roleLabel = computed(() => {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
admin: '超级管理员',
|
||||||
|
school: '园校管理员',
|
||||||
|
teacher: '教师',
|
||||||
|
parent: '家长',
|
||||||
|
};
|
||||||
|
return user.value?.role ? map[user.value.role] || user.value.role : '—';
|
||||||
|
});
|
||||||
|
|
||||||
|
const roleTagColor = computed(() => {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
admin: 'red',
|
||||||
|
school: 'blue',
|
||||||
|
teacher: 'green',
|
||||||
|
parent: 'orange',
|
||||||
|
};
|
||||||
|
return user.value?.role ? map[user.value.role] || 'default' : 'default';
|
||||||
|
});
|
||||||
|
|
||||||
|
async function refresh() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
await userStore.fetchUserInfo();
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!user.value) refresh();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.profile-view {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-body {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
padding-bottom: 24px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-avatar {
|
||||||
|
background: linear-gradient(135deg, #e67635 0%, #c45a1e 100%);
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-role {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-name {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-desc :deep(.ant-descriptions-item-label) {
|
||||||
|
width: 100px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-tip {
|
||||||
|
padding: 48px 24px;
|
||||||
|
text-align: center;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-tip .anticon {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -186,9 +186,7 @@
|
|||||||
|
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<a-space>
|
<a-space>
|
||||||
<a-badge :count="notifications">
|
<NotificationBell />
|
||||||
<BellOutlined style="font-size: 18px; cursor: pointer; color: #666;" />
|
|
||||||
</a-badge>
|
|
||||||
|
|
||||||
<a-dropdown>
|
<a-dropdown>
|
||||||
<a-space class="user-info" style="cursor: pointer;">
|
<a-space class="user-info" style="cursor: pointer;">
|
||||||
@ -274,6 +272,7 @@ import {
|
|||||||
IdcardOutlined,
|
IdcardOutlined,
|
||||||
FolderAddOutlined,
|
FolderAddOutlined,
|
||||||
} from '@ant-design/icons-vue';
|
} from '@ant-design/icons-vue';
|
||||||
|
import NotificationBell from '@/components/NotificationBell.vue';
|
||||||
import { useUserStore } from '@/stores/user';
|
import { useUserStore } from '@/stores/user';
|
||||||
import { useBreakpoints } from '@/composables/useBreakpoints';
|
import { useBreakpoints } from '@/composables/useBreakpoints';
|
||||||
|
|
||||||
@ -285,7 +284,6 @@ const { isMobile } = useBreakpoints();
|
|||||||
const collapsed = ref(false);
|
const collapsed = ref(false);
|
||||||
const selectedKeys = ref(['dashboard']);
|
const selectedKeys = ref(['dashboard']);
|
||||||
const openKeys = ref<string[]>(['staff', 'teaching', 'data', 'system']);
|
const openKeys = ref<string[]>(['staff', 'teaching', 'data', 'system']);
|
||||||
const notifications = ref(0);
|
|
||||||
const drawerVisible = ref(false);
|
const drawerVisible = ref(false);
|
||||||
|
|
||||||
const userName = computed(() => userStore.user?.name || '管理员');
|
const userName = computed(() => userStore.user?.name || '管理员');
|
||||||
@ -362,7 +360,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>
|
||||||
|
|||||||
@ -116,9 +116,7 @@
|
|||||||
|
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<a-space>
|
<a-space>
|
||||||
<a-badge :count="notifications">
|
<NotificationBell />
|
||||||
<BellOutlined style="font-size: 18px; cursor: pointer; color: #666;" />
|
|
||||||
</a-badge>
|
|
||||||
|
|
||||||
<a-dropdown>
|
<a-dropdown>
|
||||||
<a-space class="user-info" style="cursor: pointer;">
|
<a-space class="user-info" style="cursor: pointer;">
|
||||||
@ -194,6 +192,7 @@ import {
|
|||||||
CameraOutlined,
|
CameraOutlined,
|
||||||
FolderAddOutlined,
|
FolderAddOutlined,
|
||||||
} from '@ant-design/icons-vue';
|
} from '@ant-design/icons-vue';
|
||||||
|
import NotificationBell from '@/components/NotificationBell.vue';
|
||||||
import { useUserStore } from '@/stores/user';
|
import { useUserStore } from '@/stores/user';
|
||||||
import { useBreakpoints } from '@/composables/useBreakpoints';
|
import { useBreakpoints } from '@/composables/useBreakpoints';
|
||||||
|
|
||||||
@ -204,7 +203,6 @@ const { isMobile } = useBreakpoints();
|
|||||||
|
|
||||||
const collapsed = ref(false);
|
const collapsed = ref(false);
|
||||||
const selectedKeys = ref(['dashboard']);
|
const selectedKeys = ref(['dashboard']);
|
||||||
const notifications = ref(0);
|
|
||||||
const drawerVisible = ref(false);
|
const drawerVisible = ref(false);
|
||||||
|
|
||||||
const userName = computed(() => userStore.user?.name || '教师');
|
const userName = computed(() => userStore.user?.name || '教师');
|
||||||
@ -259,7 +257,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>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user