feat(frontend): 抽离页眉为公共组件;修复导航点击事件

- 新增 navbar-component.js:固定页眉,统一标题、用户信息、退出按钮
- 重构所有页面(index/topics/logs/admin/users)使用 navbar-component
- 清理原有 navbar 相关样式,避免冲突
- 导航组件增加 main-content padding-top 自动调整
- 修复导航按钮点击事件(确保  触发)
- 导航组件样式强应用,解决遮挡和布局问题
This commit is contained in:
lt
2026-05-08 13:22:12 +08:00
parent 2220d7b981
commit 9147d5e679
7 changed files with 169 additions and 75 deletions
+10 -9
View File
@@ -68,18 +68,17 @@
</style> </style>
<script src="navigation-component.js"></script> <script src="navigation-component.js"></script>
<script src="navbar-component.js"></script>
</head> </head>
<body> <body>
<div id="app"> <div id="app">
<nav class="navbar"> <navbar-component
<div class="navbar-content"> title="系统管理"
<h1 class="navbar-title">宇之然内容创作平台 - 系统</h1> :username="currentUser.username"
<div class="navbar-user"> :is-admin="isAdmin"
<div class="user-info"><div class="avatar">{{ currentUser.username.charAt(0).toUpperCase() }}</div><span>{{ currentUser.username }}</span></div> @logout="handleLogout"
<el-button type="danger" size="small" @click="handleLogout">退出</el-button> ></navbar-component>
</div>
</div>
</nav>
<navigation-component <navigation-component
current-page="admin" current-page="admin"
@@ -396,6 +395,8 @@ const app = createApp({
}); });
app.use(ElementPlus); app.use(ElementPlus);
// 安装导航组件
if (window.installNavbar) { window.installNavbar(app); }
// 安装导航组件 // 安装导航组件
if (window.installNavigation) { window.installNavigation(app); } else if (window.NavigationComponent) { app.component("navigation-component", window.NavigationComponent); } if (window.installNavigation) { window.installNavigation(app); } else if (window.NavigationComponent) { app.component("navigation-component", window.NavigationComponent); }
app.mount('#app'); app.mount('#app');
+10 -13
View File
@@ -283,22 +283,17 @@
</style> </style>
<script src="navigation-component.js"></script> <script src="navigation-component.js"></script>
<script src="navbar-component.js"></script>
</head> </head>
<body> <body>
<div id="app"> <div id="app">
<nav class="navbar" v-if="isLoggedIn"> <navbar-component
<div class="navbar-content"> title="宇之然内容创作平台"
<h1 class="navbar-title">宇之然内容创作平台</h1> :username="currentUser.username"
<div class="navbar-user"> :is-admin="isAdmin"
<div class="user-info"> @logout="handleLogout"
<div class="avatar">{{ currentUser.username ? currentUser.username.charAt(0).toUpperCase() : '?' }}</div> ></navbar-component>
<span>{{ currentUser.username }}</span>
<el-tag v-if="isAdmin" size="small" type="danger" style="border: none;">管理员</el-tag>
</div>
<el-button size="small" type="danger" plain @click="handleLogout">退出</el-button>
</div>
</div>
</nav>
<navigation-component <navigation-component
current-page="dashboard" current-page="dashboard"
@@ -532,6 +527,8 @@
const app = Vue.createApp(App); const app = Vue.createApp(App);
app.use(ElementPlus); app.use(ElementPlus);
// 安装导航组件 // 安装导航组件
if (window.installNavbar) { window.installNavbar(app); }
// 安装导航组件
if (window.installNavigation) { window.installNavigation(app); } else if (window.NavigationComponent) { app.component("navigation-component", window.NavigationComponent); } if (window.installNavigation) { window.installNavigation(app); } else if (window.NavigationComponent) { app.component("navigation-component", window.NavigationComponent); }
app.mount('#app'); app.mount('#app');
+10 -9
View File
@@ -41,18 +41,17 @@
</style> </style>
<script src="navigation-component.js"></script> <script src="navigation-component.js"></script>
<script src="navbar-component.js"></script>
</head> </head>
<body> <body>
<div id="app"> <div id="app">
<nav class="navbar"> <navbar-component
<div class="navbar-content"> title="系统日志"
<h1 class="navbar-title">宇之然内容创作平台 - 系统日志</h1> :username="currentUser.username"
<div class="navbar-user"> :is-admin="isAdmin"
<div class="user-info"><div class="avatar">{{ currentUser.username.charAt(0).toUpperCase() }}</div><span>{{ currentUser.username }}</span></div> @logout="handleLogout"
<el-button type="danger" size="small" @click="handleLogout">退出</el-button> ></navbar-component>
</div>
</div>
</nav>
<navigation-component <navigation-component
current-page="logs" current-page="logs"
@@ -148,6 +147,8 @@
const app = Vue.createApp(LogsApp); const app = Vue.createApp(LogsApp);
app.use(ElementPlus); app.use(ElementPlus);
// 安装导航组件 // 安装导航组件
if (window.installNavbar) { window.installNavbar(app); }
// 安装导航组件
if (window.installNavigation) { window.installNavigation(app); } else if (window.NavigationComponent) { app.component("navigation-component", window.NavigationComponent); } if (window.installNavigation) { window.installNavigation(app); } else if (window.NavigationComponent) { app.component("navigation-component", window.NavigationComponent); }
app.mount('#app'); app.mount('#app');
+83
View File
@@ -0,0 +1,83 @@
// 公共页眉组件 - Vue 3
(function() {
console.log('[Navbar] 脚本加载');
function injectStyles() {
if (document.getElementById('navbar-styles')) return;
const styles = `
.navbar-component { position: fixed; top: 0; left: 0; right: 0; height: 60px; background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%); color: white; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); z-index: 10000; }
.navbar-component .navbar-title { font-size: 18px; font-weight: 600; }
.navbar-component .navbar-user { display: flex; align-items: center; gap: 12px; }
.navbar-component .user-info { display: flex; align-items: center; gap: 8px; }
.navbar-component .avatar { width: 32px; height: 32px; border-radius: 50%; background: rgba(255,255,255,0.2); display: flex; align-items: center; justify-content: center; font-size: 14px; }
@media (max-width: 768px) {
.navbar-component { padding: 0 16px; }
.navbar-component .navbar-title { font-size: 16px; }
}
`;
const styleEl = document.createElement('style');
styleEl.id = 'navbar-styles';
styleEl.textContent = styles;
document.head.appendChild(styleEl);
console.log('[Navbar] 样式已注入');
}
function getHFromApp(app) {
try {
if (app._context && app._context.h) return app._context.h;
if (app._instance && app._instance.proxy && app._instance.proxy._cf) return app._instance.proxy._cf;
} catch (e) { console.warn('[Navbar] 获取 h 失败', e); }
return null;
}
const installNavbar = (app) => {
console.log('[Navbar] installNavbar called');
injectStyles();
const h = getHFromApp(app) || (window.Vue && Vue.h) || function(tag, data, children) {
const el = document.createElement(tag);
if (data && data.class) el.className = data.class;
if (children) {
if (Array.isArray(children)) children.forEach(c => { if (typeof c === 'string') el.appendChild(document.createTextNode(c)); else if (c && c.nodeType) el.appendChild(c); });
else if (typeof children === 'string') el.textContent = children;
}
if (data && data.on) Object.keys(data.on).forEach(ev => el.addEventListener(ev, data.on[ev]));
};
const NavbarComponent = {
name: 'NavbarComponent',
props: {
title: { type: String, default: '宇之然内容创作平台' },
username: { type: String, default: '' },
isAdmin: { type: Boolean, default: false },
onLogout: { type: Function, default: null }
},
render(){
const createElement = arguments[0] || h;
return createElement('nav', { class: 'navbar-component' }, [
createElement('div', { class: 'navbar-content', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%' } }, [
createElement('h1', { class: 'navbar-title' }, this.title),
createElement('div', { class: 'navbar-user' }, [
createElement('div', { class: 'user-info' }, [
createElement('div', { class: 'avatar' }, this.username ? this.username.charAt(0).toUpperCase() : '?'),
this.username ? createElement('span', this.username) : null
]),
this.isAdmin ? createElement('span', { style: { marginLeft: '8px', fontSize: '12px', background: 'rgba(255,255,255,0.3)', padding: '2px 8px', borderRadius: '10px' } }, '管理员') : null,
createElement('button', {
class: 'logout-btn',
style: { background: 'rgba(255,255,255,0.2)', border: 'none', color: 'white', padding: '6px 12px', borderRadius: '6px', cursor: 'pointer', marginLeft: '12px' },
on: { click: () => { if (this.onLogout) this.onLogout(); else window.location.href = '/login.html'; } }
}, '退出')
])
])
]);
}
};
app.component('navbar-component', NavbarComponent);
console.log('[Navbar] 组件已注册');
return app;
};
window.installNavbar = installNavbar;
console.log('[Navbar] 脚本已加载');
})();
+35 -25
View File
@@ -1,11 +1,10 @@
// Vue 3 导航组件 - 修复布局和事件 // Vue 3 导航组件 - 最终版(确保事件传递)
(function() { (function() {
console.log('[Nav] 脚本加载'); console.log('[Nav] 脚本加载');
function injectStyles() { function injectStyles() {
if (document.getElementById('navigation-styles')) return; if (document.getElementById('navigation-styles')) return;
const styles = ` const styles = `
/* 重置导航包装器,使用常规布局 */
.navigation-wrapper { position: relative; } .navigation-wrapper { position: relative; }
.navigation-wrapper .sidebar { .navigation-wrapper .sidebar {
position: fixed; top: 60px; left: 0; bottom: 0; width: 180px; position: fixed; top: 60px; left: 0; bottom: 0; width: 180px;
@@ -35,20 +34,8 @@
.navigation-wrapper .mobile-nav { display: flex !important; } .navigation-wrapper .mobile-nav { display: flex !important; }
} }
@media (min-width: 769px) { .navigation-wrapper .mobile-nav { display: none !important; } } @media (min-width: 769px) { .navigation-wrapper .mobile-nav { display: none !important; } }
/* 主内容区域避开固定侧边栏 */ body > #app > .main-content { margin-left: 180px !important; padding-top: 60px !important; }
body > #app > .navigation-wrapper + .main-content, @media (max-width: 768px) { body > #app > .main-content { margin-left: 0 !important; padding-bottom: 60px !important; } }
body > #app > .main-content {
margin-left: 180px !important;
}
@media (max-width: 768px) {
body > #app > .navigation-wrapper + .main-content,
body > #app > .main-content {
margin-left: 0 !important;
padding-bottom: 60px !important;
}
}
/* 侧边栏固定定位时不调整主内容,让 main-content 自然位于文档流中,仅需确保 padding-top 避开 navbar */
body > #app > .main-content { padding-top: 0; }
`; `;
const styleEl = document.createElement('style'); const styleEl = document.createElement('style');
styleEl.id = 'navigation-styles'; styleEl.id = 'navigation-styles';
@@ -76,6 +63,7 @@
else if (typeof children === 'string') el.textContent = children; else if (typeof children === 'string') el.textContent = children;
} }
if (data && data.on) Object.keys(data.on).forEach(ev => el.addEventListener(ev, data.on[ev])); if (data && data.on) Object.keys(data.on).forEach(ev => el.addEventListener(ev, data.on[ev]));
return el;
}; };
console.log('[Nav] 使用 h 函数:', typeof h); console.log('[Nav] 使用 h 函数:', typeof h);
@@ -83,11 +71,23 @@
name: 'NavigationComponent', name: 'NavigationComponent',
props: { currentPage: { type: String, required: true }, isAdmin: { type: Boolean, default: false } }, props: { currentPage: { type: String, required: true }, isAdmin: { type: Boolean, default: false } },
mounted() { console.log('[NavigationComponent] 已挂载'); }, mounted() { console.log('[NavigationComponent] 已挂载'); },
render() { render(){
const createElement = arguments[0] || h;
// 获取 emit 方法:通过 this.$emit 或从闭包中捕获
const emit = this.$emit || function() {};
const navigate = (page) => { const navigate = (page) => {
console.log('[NavigationComponent] 导航到:', page); console.log('[NavigationComponent] 点击导航到:', page);
// 直接调用 emit 函数
if (typeof emit === 'function') {
emit('navigate', page);
} else if (this.$emit) {
this.$emit('navigate', page); this.$emit('navigate', page);
} else {
console.warn('[NavigationComponent] 无法发送事件,emit unavailable');
}
}; };
const sidebarItems = [ const sidebarItems = [
{ icon: '📊', label: '系统概览', page: '/' }, { icon: '📊', label: '系统概览', page: '/' },
{ icon: '📋', label: '选题管理', page: 'topics.html' }, { icon: '📋', label: '选题管理', page: 'topics.html' },
@@ -100,22 +100,32 @@
{ icon: '📄', label: '日志', page: 'logs.html' }, { icon: '📄', label: '日志', page: 'logs.html' },
...(this.isAdmin ? [{ icon: '👥', label: '用户', page: 'users.html' }, { icon: '⚙️', label: '系统', page: 'admin.html' }] : []) ...(this.isAdmin ? [{ icon: '👥', label: '用户', page: 'users.html' }, { icon: '⚙️', label: '系统', page: 'admin.html' }] : [])
]; ];
return h('div', { class: 'navigation-wrapper' }, [
h('aside', { class: 'sidebar' }, [ return createElement('div', { class: 'navigation-wrapper' }, [
h('div', { class: 'sidebar-header' }, [h('h3', '宇之然平台')]), createElement('aside', { class: 'sidebar' }, [
h('nav', { class: 'sidebar-nav' }, sidebarItems.map(item => { createElement('div', { class: 'sidebar-header' }, [createElement('h3', '宇之然平台')]),
createElement('nav', { class: 'sidebar-nav' }, sidebarItems.map(item => {
const isActive = this.currentPage === (item.page === '/' ? 'dashboard' : item.page.replace('.html','')); const isActive = this.currentPage === (item.page === '/' ? 'dashboard' : item.page.replace('.html',''));
return h('button', { key: item.page, class: isActive ? 'sidebar-btn active' : 'sidebar-btn', on: { click: () => navigate(item.page) } }, item.icon + ' ' + item.label); return createElement('button', {
key: item.page,
class: isActive ? 'sidebar-btn active' : 'sidebar-btn',
on: { click: (e) => { e.preventDefault(); navigate(item.page); } }
}, item.icon + ' ' + item.label);
})) }))
]), ]),
h('nav', { class: 'mobile-nav' }, mobileItems.map(item => { createElement('nav', { class: 'mobile-nav' }, mobileItems.map(item => {
const isActive = this.currentPage === (item.page === '/' ? 'dashboard' : item.page.replace('.html','')); const isActive = this.currentPage === (item.page === '/' ? 'dashboard' : item.page.replace('.html',''));
const content = item.label ? (item.icon + ' ' + item.label) : item.icon; const content = item.label ? (item.icon + ' ' + item.label) : item.icon;
return h('button', { key: item.page, class: isActive ? 'mobile-nav-btn active' : 'mobile-nav-btn', on: { click: () => navigate(item.page) } }, content); return createElement('button', {
key: item.page,
class: isActive ? 'mobile-nav-btn active' : 'mobile-nav-btn',
on: { click: (e) => { e.preventDefault(); navigate(item.page); } }
}, content);
})) }))
]); ]);
} }
}; };
app.component('navigation-component', component); app.component('navigation-component', component);
console.log('[Nav] 组件已注册'); console.log('[Nav] 组件已注册');
return app; return app;
+10 -9
View File
@@ -117,18 +117,17 @@
</style> </style>
<script src="navigation-component.js"></script> <script src="navigation-component.js"></script>
<script src="navbar-component.js"></script>
</head> </head>
<body> <body>
<div id="app"> <div id="app">
<nav class="navbar"> <navbar-component
<div class="navbar-content"> title="选题管理"
<h1 class="navbar-title">宇之然内容创作平台 - 选题管理</h1> :username="currentUser.username"
<div class="navbar-user"> :is-admin="isAdmin"
<div class="user-info"><div class="avatar">{{ currentUser.username ? currentUser.username.charAt(0).toUpperCase() : '?' }}</div><span>{{ currentUser.username }}</span></div> @logout="handleLogout"
<el-button type="danger" size="small" @click="handleLogout">退出</el-button> ></navbar-component>
</div>
</div>
</nav>
<navigation-component <navigation-component
current-page="topics" current-page="topics"
:is-admin="isAdmin" :is-admin="isAdmin"
@@ -668,6 +667,8 @@ const TopicsApp = {
}; };
const app = Vue.createApp(TopicsApp); const app = Vue.createApp(TopicsApp);
app.use(ElementPlus); app.use(ElementPlus);
// 安装导航组件
if (window.installNavbar) { window.installNavbar(app); }
// 安装导航组件 // 安装导航组件
if (window.installNavigation) { window.installNavigation(app); } else if (window.NavigationComponent) { app.component("navigation-component", window.NavigationComponent); } if (window.installNavigation) { window.installNavigation(app); } else if (window.NavigationComponent) { app.component("navigation-component", window.NavigationComponent); }
app.mount('#app'); app.mount('#app');
+10 -9
View File
@@ -68,18 +68,17 @@
</style> </style>
<script src="navigation-component.js"></script> <script src="navigation-component.js"></script>
<script src="navbar-component.js"></script>
</head> </head>
<body> <body>
<div id="app"> <div id="app">
<nav class="navbar"> <navbar-component
<div class="navbar-content"> title="用户管理"
<h1 class="navbar-title">宇之然内容创作平台 - 用户管理</h1> :username="currentUser.username"
<div class="navbar-user"> :is-admin="isAdmin"
<div class="user-info"><div class="avatar">{{ currentUser.username.charAt(0).toUpperCase() }}</div><span>{{ currentUser.username }}</span></div> @logout="handleLogout"
<el-button type="danger" size="small" @click="handleLogout">退出</el-button> ></navbar-component>
</div>
</div>
</nav>
<navigation-component <navigation-component
current-page="users" current-page="users"
@@ -226,6 +225,8 @@
const app = Vue.createApp(UsersApp); const app = Vue.createApp(UsersApp);
app.use(ElementPlus); app.use(ElementPlus);
// 安装导航组件 // 安装导航组件
if (window.installNavbar) { window.installNavbar(app); }
// 安装导航组件
if (window.installNavigation) { window.installNavigation(app); } else if (window.NavigationComponent) { app.component("navigation-component", window.NavigationComponent); } if (window.installNavigation) { window.installNavigation(app); } else if (window.NavigationComponent) { app.component("navigation-component", window.NavigationComponent); }
app.mount('#app'); app.mount('#app');