提交最新版管理员登录

leijiabao_branch
bao 4 weeks ago
parent b07a320624
commit 8469cd6ff3

@ -1 +0,0 @@
// 管理员获取通知,发布通知的后端接口文件

@ -7,7 +7,78 @@ const dbModule = require('../db');
const db = dbModule.promisePool;
const { authenticateToken } = require('../middleware/auth');
const verificationService = require('../services/verificationService');
// 注册接口
//管理员登录接口
router.post('/admin/login', async (req, res) => {
try {
const { adminAccount, password } = req.body;
// 验证输入
if (!adminAccount || !password) {
return res.status(400).json({
success: false,
message: '请填写管理员账号和密码'
});
}
// 从数据库查找管理员用户role='admin'
const [users] = await db.query(
'SELECT * FROM users WHERE (username = ?) AND role = "admin"',
[adminAccount]
);
if (users.length === 0) {
return res.status(400).json({
success: false,
message: '管理员账号不存在或权限不足'
});
}
const user = users[0];
// 验证密码
const isValidPassword = await bcrypt.compare(password, user.password);
if (!isValidPassword) {
return res.status(400).json({
success: false,
message: '密码错误'
});
}
// 生成JWT token可以设置不同的密钥或更长的有效期
const token = jwt.sign(
{
userId: user.user_id,
email: user.email,
role: user.role, // 添加角色信息
isAdmin: true // 添加管理员标识
},
process.env.JWT_SECRET || 'your-secret-key',
{ expiresIn: '7d' } // 管理员token有效期更长
);
res.json({
success: true,
message: '管理员登录成功',
user: {
user_id: user.user_id,
username: user.username,
email: user.email,
role: user.role
},
token
});
} catch (error) {
console.error('管理员登录错误:', error);
res.status(500).json({
success: false,
message: '服务器错误'
});
}
});
// 用户注册接口
router.post('/register', async (req, res) => {
try {
const { username, email, password, verificationCode } = req.body;
@ -89,7 +160,7 @@ router.post('/register', async (req, res) => {
}
});
// 登录接口
// 用户登录接口
router.post('/login', async (req, res) => {
try {
const { email, password } = req.body;

@ -189,6 +189,72 @@ router.get('/:id', authenticateToken, checkAdmin, async (req, res) => {
}
});
// 回复反馈 - 需要认证和管理员权限
router.put('/:id/reply', authenticateToken, checkAdmin, async (req, res) => {
try {
const feedbackId = parseInt(req.params.id);
const { answer } = req.body;
// 验证参数
if (!answer || answer.trim() === '') {
return res.status(400).json({
success: false,
message: '回复内容不能为空'
});
}
if (answer.length > 500) {
return res.status(400).json({
success: false,
message: '回复内容不能超过500字'
});
}
// 先检查反馈是否存在
const checkQuery = `SELECT feedback_id FROM feedback WHERE feedback_id = ?`;
const [checkRows] = await db.execute(checkQuery, [feedbackId]);
if (checkRows.length === 0) {
return res.status(404).json({
success: false,
message: '反馈不存在'
});
}
// 更新反馈回复
const updateQuery = `
UPDATE feedback
SET answer = ?, answer_time = CURRENT_TIMESTAMP
WHERE feedback_id = ?
`;
await db.execute(updateQuery, [answer.trim(), feedbackId]);
// 获取更新后的反馈信息
const getQuery = `
SELECT f.*, u.username
FROM feedback f
JOIN users u ON f.user_id = u.user_id
WHERE f.feedback_id = ?
`;
const [updatedRows] = await db.execute(getQuery, [feedbackId]);
res.json({
success: true,
data: updatedRows[0],
message: '回复成功'
});
} catch (error) {
console.error('回复反馈错误:', error);
res.status(500).json({
success: false,
message: '服务器错误: ' + error.message
});
}
});
// 根据ID删除反馈 - 需要认证和管理员权限
router.delete('/:id', authenticateToken, checkAdmin, async (req, res) => {
try {

@ -1,129 +1,30 @@
// backend/routes/mynotice.js
const express = require('express');
const router = express.Router();
// 临时模拟数据
let mockNotices = [
{
id: 1,
type: 'feedback',
title: '关于图表导出功能的建议',
content: '建议增加导出为PDF格式的功能这样更方便分享和打印。',
feedbackType: 'suggestion',
time: new Date(Date.now() - 3600000 * 2),
isRead: false,
reply: '感谢您的建议我们已经在开发计划中预计下个版本会增加PDF导出功能。',
replyTime: new Date(Date.now() - 3600000 * 1)
},
{
id: 2,
type: 'feedback',
title: '界面颜色太刺眼',
content: '数据可视化页面的背景色太亮,长时间使用眼睛容易疲劳。',
feedbackType: 'ui',
time: new Date(Date.now() - 86400000 * 2),
isRead: true,
reply: '我们已收到您的反馈,会在下个版本中增加深色模式选项。',
replyTime: new Date(Date.now() - 86400000 * 1)
},
{
id: 3,
type: 'system',
title: '系统维护通知',
content: '为了提升系统性能我们将于本周六凌晨2:00-4:00进行系统维护期间服务将不可用。',
time: new Date(Date.now() - 86400000 * 3),
isRead: false
},
{
id: 4,
type: 'system',
title: '新功能上线',
content: '图表智能推荐功能已上线,系统会根据您的数据自动推荐最合适的图表类型。',
time: new Date(Date.now() - 86400000 * 5),
isRead: true
},
{
id: 5,
type: 'feedback',
title: '数据导入失败问题',
content: '导入超过10MB的CSV文件时系统会报错提示内存不足。',
feedbackType: 'bug',
time: new Date(Date.now() - 86400000 * 7),
isRead: false,
reply: null
}
];
// 模拟用户数据
const mockUsers = [
{ id: 1, username: '张三', email: 'zhangsan@example.com' },
{ id: 2, username: '李四', email: 'lisi@example.com' },
{ id: 3, username: '王五', email: 'wangwu@example.com' }
];
// 模拟notice表数据
let mockSystemNotices = [
{
id: 1,
title: '系统维护通知',
content: '为了提升系统性能我们将于本周六凌晨2:00-4:00进行系统维护期间服务将不可用。',
admin_id: 1,
is_read: false,
created_time: new Date(Date.now() - 86400000 * 3)
},
{
id: 2,
title: '新功能上线',
content: '图表智能推荐功能已上线,系统会根据您的数据自动推荐最合适的图表类型。',
admin_id: 1,
is_read: true,
created_time: new Date(Date.now() - 86400000 * 5)
}
];
// 模拟feedback表数据
let mockFeedbackList = [
{
feedback_id: 1,
user_id: 1,
type: 'suggestion',
content: '建议增加导出为PDF格式的功能这样更方便分享和打印。',
feedback_time: new Date(Date.now() - 3600000 * 2),
answer: '感谢您的建议我们已经在开发计划中预计下个版本会增加PDF导出功能。',
answer_time: new Date(Date.now() - 3600000 * 1)
},
{
feedback_id: 2,
user_id: 1,
type: 'ui',
content: '数据可视化页面的背景色太亮,长时间使用眼睛容易疲劳。',
feedback_time: new Date(Date.now() - 86400000 * 2),
answer: '我们已收到您的反馈,会在下个版本中增加深色模式选项。',
answer_time: new Date(Date.now() - 86400000 * 1)
},
{
feedback_id: 3,
user_id: 1,
type: 'bug',
content: '导入超过10MB的CSV文件时系统会报错提示内存不足。',
feedback_time: new Date(Date.now() - 86400000 * 7),
answer: null,
answer_time: null
}
];
const dbModule = require('../db');
const db = dbModule.promisePool;
// 获取用户通知列表
router.get('/api/mynotice', (req, res) => {
router.get('/', async (req, res) => {
try {
// 模拟从数据库获取数据
// 这里应该根据当前登录用户ID获取数据
const userId = 1; // 假设当前用户ID为1
// 从请求中获取用户ID假设通过认证中间件设置
const userId = req.user?.id || 1; // 默认为1用于测试
// 1. 获取用户的反馈信息
const userFeedback = mockFeedbackList.filter(feedback => feedback.user_id === userId);
const [userFeedback] = await db.execute(
`SELECT f.feedback_id, f.type, f.content, f.feedback_time, f.answer, f.answer_time
FROM feedback f
WHERE f.user_id = ?
ORDER BY f.feedback_time DESC`,
[userId]
);
// 2. 获取系统通知
const systemNotices = mockSystemNotices;
const [systemNotices] = await db.execute(
`SELECT n.notice_id, n.title, n.content, n.admin_id, n.is_read, n.created_time
FROM notice n
ORDER BY n.created_time DESC`
);
// 3. 合并数据并格式化
const formattedNotices = [];
@ -137,7 +38,7 @@ router.get('/api/mynotice', (req, res) => {
content: feedback.content,
feedbackType: feedback.type,
time: feedback.feedback_time,
isRead: false, // 这里应该从数据库读取已读状态
isRead: feedback.answer !== null, // 有回复视为已读
reply: feedback.answer,
replyTime: feedback.answer_time
};
@ -147,7 +48,7 @@ router.get('/api/mynotice', (req, res) => {
// 添加系统通知
systemNotices.forEach(notice => {
const formattedNotice = {
id: `system_${notice.id}`,
id: `system_${notice.notice_id}`,
type: 'system',
title: notice.title,
content: notice.content,
@ -183,15 +84,23 @@ router.get('/api/mynotice', (req, res) => {
});
// 标记单条通知为已读
router.post('/api/mynotice/read/:id', (req, res) => {
router.post('/read/:id', async (req, res) => {
try {
const { id } = req.params;
// 在实际应用中,这里应该更新数据库中的已读状态
// 更新mock数据
const notice = mockNotices.find(n => n.id == parseInt(id) || n.id == id);
if (notice) {
notice.isRead = true;
// 判断通知类型并更新对应的数据库记录
if (id.startsWith('feedback_')) {
// 反馈通知 - 不需要标记已读,因为反馈的已读状态由是否有回复决定
console.log(`用户查看了反馈 ${id}`);
} else if (id.startsWith('system_')) {
// 系统通知 - 更新notice表的is_read字段
const noticeId = id.replace('system_', '');
// 更新系统通知为已读
await db.execute(
'UPDATE notice SET is_read = TRUE WHERE notice_id = ?',
[noticeId]
);
}
res.json({
@ -210,18 +119,14 @@ router.post('/api/mynotice/read/:id', (req, res) => {
});
// 标记所有通知为已读
router.post('/api/mynotice/read-all', (req, res) => {
router.post('/read-all', async (req, res) => {
try {
// 在实际应用中,这里应该批量更新数据库中的已读状态
// 更新mock数据
mockNotices.forEach(notice => {
notice.isRead = true;
});
// 1. 将所有系统通知标记为已读
await db.execute(
'UPDATE notice SET is_read = TRUE WHERE is_read = FALSE'
);
// 模拟更新系统通知已读状态
mockSystemNotices.forEach(notice => {
notice.is_read = true;
});
// 注意:反馈通知的已读状态由是否有回复决定,无法批量标记
res.json({
code: 200,
@ -239,27 +144,30 @@ router.post('/api/mynotice/read-all', (req, res) => {
});
// 获取未读通知数量
router.get('/api/mynotice/unread-count', (req, res) => {
router.get('/unread-count', async (req, res) => {
try {
const userId = 1; // 假设当前用户ID为1
const userId = req.user?.id || 1; // 默认为1用于测试
// 获取用户的未读反馈
const unreadFeedback = mockFeedbackList.filter(feedback =>
feedback.user_id === userId && !feedback.is_read
).length;
// 1. 获取未读反馈数量(没有回复的反馈)
const [unreadFeedback] = await db.execute(
'SELECT COUNT(*) as count FROM feedback WHERE user_id = ? AND answer IS NULL',
[userId]
);
// 获取未读系统通知
const unreadSystemNotices = mockSystemNotices.filter(notice => !notice.is_read).length;
// 2. 获取未读系统通知数量
const [unreadSystemNotices] = await db.execute(
'SELECT COUNT(*) as count FROM notice WHERE is_read = FALSE'
);
const totalUnread = unreadFeedback + unreadSystemNotices;
const totalUnread = parseInt(unreadFeedback[0].count) + parseInt(unreadSystemNotices[0].count);
res.json({
code: 200,
message: '获取未读数量成功',
data: {
unreadCount: totalUnread,
feedbackUnread: unreadFeedback,
systemUnread: unreadSystemNotices
feedbackUnread: parseInt(unreadFeedback[0].count),
systemUnread: parseInt(unreadSystemNotices[0].count)
}
});
} catch (error) {

@ -0,0 +1,515 @@
<!-- src/Admin.vue -->
<!-- 作为管理员的主界面 -->
<template>
<div class="admin-welcome">
<!-- 欢迎横幅 -->
<div class="welcome-banner">
<div class="banner-content">
<h1 class="welcome-title">欢迎回来{{ admin.username }}</h1>
<p class="welcome-subtitle">PGFPlotsGenerator 管理员控制台</p>
<div class="admin-stats">
<div class="stat-card">
<div class="stat-icon">👤</div>
<div class="stat-info">
<div class="stat-number">{{ stats.users }}</div>
<div class="stat-label">注册用户</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">📂</div>
<div class="stat-info">
<div class="stat-number">{{ stats.files }}</div>
<div class="stat-label">数据文件</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">📜</div>
<div class="stat-info">
<div class="stat-number">{{ stats.generations }}</div>
<div class="stat-label">生成记录</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">💬</div>
<div class="stat-info">
<div class="stat-number">{{ stats.feedback }}</div>
<div class="stat-label">用户反馈</div>
</div>
</div>
</div>
</div>
</div>
<!-- 快速操作 - 修改为四宫格布局 -->
<div class="quick-actions">
<h2 class="section-title">快速操作</h2>
<div class="four-grid-container">
<!-- 第一行 -->
<div class="grid-row">
<div class="grid-item" @click="goToUserManagement">
<div class="grid-icon" style="background: #4caf50;">
<span class="icon-text">👤</span>
</div>
<div class="grid-content">
<h3>用户管理</h3>
<p>查看和管理所有注册用户</p>
</div>
</div>
<div class="grid-item" @click="goToFeedbackManagement">
<div class="grid-icon" style="background: #ff9800;">
<span class="icon-text">💬</span>
</div>
<div class="grid-content">
<h3>反馈管理</h3>
<p>处理用户反馈和建议</p>
</div>
</div>
</div>
<!-- 第二行 -->
<div class="grid-row">
<div class="grid-item" @click="goToSystemLogs">
<div class="grid-icon" style="background: #607d8b;">
<span class="icon-text">📋</span>
</div>
<div class="grid-content">
<h3>系统日志</h3>
<p>查看系统运行状态</p>
</div>
</div>
<div class="grid-item" @click="goToNoticeManagement">
<div class="grid-icon" style="background: #e91e63;">
<span class="icon-text">📢</span>
</div>
<div class="grid-content">
<h3>通知管理</h3>
<p>发布系统通知</p>
</div>
</div>
</div>
</div>
</div>
<!-- 管理员信息 -->
<div class="admin-info-section">
<h2 class="section-title">管理员信息</h2>
<div class="info-card">
<div class="info-row">
<span class="info-label">管理员ID</span>
<span class="info-value">{{ admin.user_id }}</span>
</div>
<div class="info-row">
<span class="info-label">用户名</span>
<span class="info-value">{{ admin.username }}</span>
</div>
<div class="info-row">
<span class="info-label">邮箱</span>
<span class="info-value">{{ admin.email }}</span>
</div>
<div class="info-row">
<span class="info-label">角色</span>
<span class="info-value admin-role">{{ admin.role }}</span>
</div>
</div>
</div>
</div>
</template>
<script>
import { useRouter } from 'vue-router'
export default {
name: 'AdminWelcome',
setup() {
const router = useRouter()
return { router }
},
data() {
return {
admin: {},
stats: {
users: 0,
files: 0,
generations: 0,
feedback: 0
},
currentTime: ''
}
},
created() {
this.loadAdminData()
this.updateCurrentTime()
this.loadSystemStats()
// 10
setInterval(this.loadSystemStats, 10 * 60 * 1000)
//
setInterval(this.updateCurrentTime, 1000)
},
methods: {
// Admin.vue loadAdminData
loadAdminData() {
const adminUser = localStorage.getItem('adminUser')
if (adminUser) {
try {
this.admin = JSON.parse(adminUser)
} catch (e) {
console.error('解析管理员信息失败:', e)
// App.vue
if (typeof this.$parent.handleAdminLogout === 'function') {
this.$parent.handleAdminLogout()
}
}
} else {
// App.vue
if (typeof this.$parent.handleAdminLogout === 'function') {
this.$parent.handleAdminLogout()
}
}
},
updateCurrentTime() {
const now = new Date()
this.currentTime = now.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
})
},
async loadSystemStats() {
try {
// API
// 使
this.stats = {
users: 156,
files: 342,
generations: 289,
feedback: 23
}
} catch (error) {
console.error('获取系统统计失败:', error)
}
},
goToUserManagement() {
alert('跳转到用户管理')
// 使
// this.$router.push('/admin/users')
},
goToFeedbackManagement() {
//
// URL
const currentOrigin = window.location.origin
const adminFeedbackUrl = `${currentOrigin}/admin/feedback`
console.log('在新标签页打开:', adminFeedbackUrl)
//
window.open(adminFeedbackUrl, '_blank')
},
goToSystemLogs() {
alert('跳转到系统日志')
// this.$router.push('/admin/logs')
},
goToNoticeManagement() {
alert('跳转到通知管理')
// this.$router.push('/admin/notices')
}
}
}
</script>
<style scoped>
.admin-welcome {
min-height: calc(100vh - 60px);
background: #f8f9fa;
padding: 0;
}
/* 欢迎横幅 */
.welcome-banner {
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
color: white;
padding: 40px 20px;
margin-bottom: 30px;
}
.banner-content {
max-width: 1200px;
margin: 0 auto;
}
.welcome-title {
font-size: 32px;
margin-bottom: 10px;
font-weight: 600;
}
.welcome-subtitle {
font-size: 16px;
opacity: 0.9;
margin-bottom: 30px;
}
.admin-stats {
display: flex;
gap: 20px;
flex-wrap: wrap;
}
.stat-card {
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
padding: 20px;
display: flex;
align-items: center;
gap: 15px;
min-width: 200px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.stat-icon {
font-size: 30px;
}
.stat-info {
display: flex;
flex-direction: column;
}
.stat-number {
font-size: 28px;
font-weight: 700;
line-height: 1;
}
.stat-label {
font-size: 14px;
opacity: 0.9;
margin-top: 5px;
}
/* 快速操作 - 四宫格布局 */
.quick-actions {
max-width: 900px; /* 调整宽度适应四宫格 */
margin: 0 auto 30px;
padding: 0 20px;
}
.section-title {
font-size: 24px;
color: #333;
margin-bottom: 30px; /* 增加底部间距 */
font-weight: 600;
text-align: center; /* 标题居中 */
}
/* 四宫格容器 */
.four-grid-container {
display: flex;
flex-direction: column;
gap: 20px;
}
/* 网格行 */
.grid-row {
display: flex;
gap: 20px;
}
/* 网格项 */
.grid-item {
flex: 1;
background: white;
border-radius: 12px;
overflow: hidden;
cursor: pointer;
transition: all 0.3s;
border: 1px solid #eaeaea;
display: flex;
flex-direction: column;
}
.grid-item:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
border-color: #dc3545;
}
/* 图标区域 */
.grid-icon {
height: 120px;
display: flex;
align-items: center;
justify-content: center;
}
.icon-text {
font-size: 48px;
}
/* 内容区域 */
.grid-content {
padding: 20px;
text-align: center;
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
.grid-content h3 {
font-size: 18px;
color: #333;
margin-bottom: 10px;
font-weight: 600;
}
.grid-content p {
color: #666;
font-size: 14px;
line-height: 1.5;
}
/* 管理员信息 */
.admin-info-section {
max-width: 1200px;
margin: 0 auto 30px;
padding: 0 20px;
}
.info-card {
background: white;
border-radius: 12px;
padding: 25px;
border: 1px solid #eaeaea;
}
.info-row {
display: flex;
margin-bottom: 15px;
padding-bottom: 15px;
border-bottom: 1px solid #f0f0f0;
}
.info-row:last-child {
margin-bottom: 0;
padding-bottom: 0;
border-bottom: none;
}
.info-label {
font-weight: 600;
color: #333;
min-width: 100px;
}
.info-value {
color: #666;
}
.admin-role {
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
color: white;
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
display: inline-block;
}
/* 系统状态 */
.system-status {
max-width: 1200px;
margin: 0 auto 30px;
padding: 0 20px;
}
.status-cards {
display: flex;
gap: 20px;
flex-wrap: wrap;
}
.status-card {
background: white;
border-radius: 12px;
padding: 25px;
display: flex;
align-items: center;
gap: 15px;
min-width: 250px;
border: 1px solid #eaeaea;
}
.status-icon {
font-size: 30px;
}
.status-content h3 {
font-size: 18px;
color: #333;
margin-bottom: 5px;
}
.status-content p {
color: #666;
font-size: 14px;
}
.status-normal {
border-left: 4px solid #4caf50;
}
.status-warning {
border-left: 4px solid #ff9800;
}
/* 响应式设计 */
@media (max-width: 768px) {
.admin-stats {
flex-direction: column;
}
.stat-card {
width: 100%;
min-width: auto;
}
.actions-grid {
grid-template-columns: 1fr;
}
.status-cards {
flex-direction: column;
}
.status-card {
width: 100%;
min-width: auto;
}
/* 在移动设备上将四宫格变为垂直布局 */
.grid-row {
flex-direction: column;
}
.grid-item {
margin-bottom: 20px;
}
.grid-item:last-child {
margin-bottom: 0;
}
}
</style>

@ -1,25 +1,35 @@
<!-- src/App.vue -->
<template>
<div id="app">
<!-- 使用顶部导航栏组件 -->
<!-- 根据认证状态显示不同的顶部导航栏 -->
<!-- 普通用户登录后显示普通导航栏 -->
<CommonNavbar
v-if="isAuthenticated && !isAdminAuthenticated"
:show-navbar="isAuthenticated"
:user="currentUser"
:unread-count="unreadCount"
@logout="handleLogout"
@user-click="goToChangeInformation"
/>
<!-- 管理员登录后显示管理员导航栏 -->
<AdminNavbar
v-else-if="isAdminAuthenticated"
:admin="currentAdmin"
@logout="handleAdminLogout"
/>
<!-- 主要内容 -->
<main>
<!-- 未登录显示认证页面 -->
<Auth
v-if="!isAuthenticated"
@auth-success="handleAuthSuccess"
v-if="!isAuthenticated && !isAdminAuthenticated"
@auth-success="handleAuthSuccess"
@admin-success="handleAdminSuccess"
/>
<!-- 登录后显示主应用 -->
<div v-else class="main-app">
<!-- 普通用户登录后显示主应用 -->
<div v-else-if="isAuthenticated" class="main-app">
<div class="app-layout">
<!-- 侧边栏组件 -->
<CommonSidebar
@ -33,6 +43,12 @@
</div>
</div>
</div>
<!-- 管理员登录后显示路由视图容器 -->
<div v-else-if="isAdminAuthenticated" class="admin-main">
<!-- 这里添加路由视图容器 -->
<router-view></router-view>
</div>
</main>
</div>
</template>
@ -40,18 +56,23 @@
<script>
import Auth from './components/TheAuth.vue';
import CommonSidebar from './components/CommonSidebar.vue';
import CommonNavbar from './components/CommonNavbar.vue'; //
import CommonNavbar from './components/CommonNavbar.vue';
import AdminNavbar from './components/AdminNavbar.vue';
// Admin
export default {
name: 'App',
components: {
Auth,
CommonSidebar,
CommonNavbar //
CommonNavbar,
AdminNavbar
},
data() {
return {
isSidebarCollapsed: false
isSidebarCollapsed: false,
isAdminAuthenticated: false,
currentAdmin: null
}
},
computed: {
@ -68,6 +89,7 @@ export default {
methods: {
checkAuthStatus() {
//
const token = localStorage.getItem('token') || sessionStorage.getItem('token')
const user = localStorage.getItem('user') || sessionStorage.getItem('user')
@ -75,6 +97,23 @@ export default {
this.$store.commit('SET_USER', JSON.parse(user))
this.validateToken(token)
}
//
const adminToken = localStorage.getItem('adminToken')
const adminUser = localStorage.getItem('adminUser')
if (adminToken && adminUser) {
try {
this.currentAdmin = JSON.parse(adminUser)
this.isAdminAuthenticated = true
// /admin
// URL
console.log('管理员已认证,当前路径:', window.location.pathname)
} catch (e) {
console.error('解析管理员信息失败:', e)
this.handleAdminLogout()
}
}
},
async validateToken(token) {
@ -98,16 +137,40 @@ export default {
this.$store.commit('SET_USER', user)
},
handleAdminSuccess(adminData) {
console.log('管理员登录成功:', adminData)
this.currentAdmin = adminData
this.isAdminAuthenticated = true
// localStorage
localStorage.setItem('adminUser', JSON.stringify(adminData))
localStorage.setItem('adminToken', adminData.token)
// Admin.vue
this.$router.push('/admin')
},
handleLogout() {
localStorage.removeItem('token')
localStorage.removeItem('user')
sessionStorage.removeItem('token')
sessionStorage.removeItem('user')
//
localStorage.removeItem('chart_chat_messages')
localStorage.removeItem('chat_session_id')
this.$store.commit('LOGOUT')
//
this.$router.push('/')
},
handleAdminLogout() {
localStorage.removeItem('adminToken')
localStorage.removeItem('adminUser')
this.isAdminAuthenticated = false
this.currentAdmin = null
//
this.$router.push('/')
},
handleSidebarToggle(isCollapsed) {
@ -122,7 +185,6 @@ export default {
</script>
<style>
/* 移除与导航栏相关的样式,因为它们已经移动到组件中 */
* {
margin: 0;
padding: 0;
@ -144,6 +206,11 @@ body {
margin: 0 auto;
}
.admin-main {
min-height: calc(100vh - 60px); /* 减去导航栏高度 */
background: #f8f9fa;
}
.app-layout {
display: flex;
min-height: calc(100vh - 100px);

@ -0,0 +1,226 @@
<!-- src/components/AdminLogin.vue -->
<template>
<form @submit.prevent="handleAdminLogin" class="admin-login-form">
<div class="form-group">
<label for="adminAccount">管理员账号</label>
<div class="input-wrapper">
<input
id="adminAccount"
v-model="form.adminAccount"
type="text"
placeholder="请输入管理员账号"
required
:class="{ error: errors.adminAccount }"
>
<span v-if="errors.adminAccount" class="error-text">{{ errors.adminAccount }}</span>
</div>
</div>
<div class="form-group">
<label for="password">密码</label>
<div class="input-wrapper">
<input
id="password"
v-model="form.password"
:type="showPassword ? 'text' : 'password'"
placeholder="请输入密码"
required
:class="{ error: errors.password }"
>
<button
type="button"
class="toggle-password"
@click="showPassword = !showPassword"
>
{{ showPassword ? '🙈' : '👁️' }}
</button>
</div>
<div v-if="errors.password" class="error-text">{{ errors.password }}</div>
</div>
<button
type="submit"
class="submit-button admin-submit"
:disabled="loading"
>
<span v-if="loading">...</span>
<span v-else></span>
</button>
<div v-if="error" class="error-message">
{{ error }}
</div>
</form>
</template>
<script>
export default {
name: 'AdminLogin',
data() {
return {
form: {
adminAccount: '',
password: ''
},
showPassword: false,
errors: {},
loading: false,
error: ''
}
},
methods: {
async handleAdminLogin() {
//
if (!this.validateForm()) return
this.loading = true
this.error = ''
try {
// API
const response = await fetch('http://localhost:3000/api/auth/admin/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
adminAccount: this.form.adminAccount,
password: this.form.password
})
})
const data = await response.json()
if (data.success) {
// token
localStorage.setItem('adminToken', data.token)
localStorage.setItem('adminUser', JSON.stringify(data.user))
//
this.$emit('success', {
...data.user,
token: data.token, // token
isAdmin: true
})
} else {
this.error = data.message || '管理员登录失败'
}
} catch (err) {
this.error = '网络错误,请检查后端服务'
console.error('管理员登录错误:', err)
} finally {
this.loading = false
}
},
validateForm() {
this.errors = {}
//
if (!this.form.adminAccount) {
this.errors.adminAccount = '管理员账号不能为空'
}
//
if (!this.form.password) {
this.errors.password = '密码不能为空'
} else if (this.form.password.length < 6) {
this.errors.password = '密码至少6位'
}
return Object.keys(this.errors).length === 0
}
}
}
</script>
<style scoped>
.admin-login-form {
display: flex;
flex-direction: column;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
color: #333;
font-weight: 500;
}
.input-wrapper {
position: relative;
display: flex;
align-items: center;
}
.input-wrapper input {
width: 100%;
padding: 12px 45px 12px 15px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 16px;
transition: border-color 0.3s;
box-sizing: border-box;
}
.input-wrapper input:focus {
outline: none;
border-color: #dc3545; /* 使用红色作为管理员主题色 */
}
.toggle-password {
position: absolute;
right: 12px;
background: none;
border: none;
cursor: pointer;
font-size: 16px;
padding: 4px;
}
input.error {
border-color: #e74c3c;
}
.error-text {
color: #e74c3c;
font-size: 14px;
margin-top: 5px;
display: block;
}
.admin-submit {
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
color: white;
border: none;
padding: 14px;
border-radius: 6px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: opacity 0.3s;
}
.admin-submit:hover:not(:disabled) {
opacity: 0.9;
}
.admin-submit:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.error-message {
background: #ffeaea;
color: #e74c3c;
padding: 12px;
border-radius: 6px;
margin-top: 15px;
font-size: 14px;
text-align: center;
}
</style>

@ -0,0 +1,131 @@
<!-- src/components/AdminNavbar.vue -->
<template>
<nav class="admin-navbar">
<div class="navbar-container">
<div class="navbar-left">
<div class="logo">
<span class="logo-icon"></span>
<span class="logo-text">PGFPlots Admin</span>
</div>
</div>
<div class="navbar-right">
<div class="admin-info">
<span class="admin-name">{{ admin.username }}</span>
<span class="admin-badge">管理员</span>
</div>
<button class="logout-btn" @click="logout">
<span>退出</span>
<span class="logout-icon">🚪</span>
</button>
</div>
</div>
</nav>
</template>
<script>
export default {
name: 'AdminNavbar',
props: {
admin: {
type: Object,
required: true
}
},
methods: {
logout() {
this.$emit('logout')
}
}
}
</script>
<style scoped>
.admin-navbar {
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
color: white;
padding: 0 20px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
height: 60px;
display: flex;
align-items: center;
}
.navbar-container {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
max-width: 1200px;
margin: 0 auto;
}
.navbar-left {
display: flex;
align-items: center;
}
.logo {
display: flex;
align-items: center;
gap: 10px;
}
.logo-icon {
font-size: 24px;
}
.logo-text {
font-size: 20px;
font-weight: 600;
letter-spacing: 0.5px;
}
.navbar-right {
display: flex;
align-items: center;
gap: 20px;
}
.admin-info {
display: flex;
align-items: center;
gap: 10px;
background: rgba(255, 255, 255, 0.1);
padding: 8px 15px;
border-radius: 20px;
}
.admin-name {
font-weight: 500;
}
.admin-badge {
background: rgba(255, 255, 255, 0.2);
padding: 2px 8px;
border-radius: 10px;
font-size: 12px;
font-weight: 600;
}
.logout-btn {
background: rgba(255, 255, 255, 0.15);
color: white;
border: 1px solid rgba(255, 255, 255, 0.3);
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.3s;
}
.logout-btn:hover {
background: rgba(255, 255, 255, 0.25);
}
.logout-icon {
font-size: 14px;
}
</style>

@ -1,4 +1,4 @@
<!-- src/components/Auth.vue -->
<!-- src/components/TheAuth.vue -->
<template>
<div class="auth-container">
<div class="auth-card">
@ -10,46 +10,63 @@
<!-- 登录/注册切换 -->
<div class="auth-tabs">
<button
:class="['tab-button', { active: isLogin }]"
@click="isLogin = true"
:class="['tab-button', { active: isLogin && !isAdminLogin }]"
@click="isLogin = true; isAdminLogin = false"
>
登录
</button>
<button
:class="['tab-button', { active: !isLogin }]"
@click="isLogin = false"
:class="['tab-button', { active: !isLogin && !isAdminLogin }]"
@click="isLogin = false; isAdminLogin = false"
>
注册
</button>
<button
:class="['tab-button admin-tab', { active: isAdminLogin }]"
@click="activateAdminLogin"
>
管理员登录
</button>
</div>
<!-- 动态显示登录或注册表单 -->
<LoginForm v-if="isLogin" @success="handleAuthSuccess" />
<RegisterForm v-else @success="handleAuthSuccess" />
<!-- 动态显示登录注册或管理员登录表单 -->
<LoginForm v-if="isLogin && !isAdminLogin" @success="handleAuthSuccess" />
<RegisterForm v-else-if="!isLogin && !isAdminLogin" @success="handleAuthSuccess" />
<AdminLogin v-else @success="handleAdminSuccess" />
</div>
</div>
</template>
<!-- 这里引用注册登录表单 -->
<script>
import LoginForm from './LoginForm.vue'
import RegisterForm from './RegisterForm.vue'
import AdminLogin from './AdminLogin.vue'
export default {
name: 'TheAuth',
components: {
LoginForm,
RegisterForm
RegisterForm,
AdminLogin
},
data() {
return {
isLogin: true //
isLogin: true, //
isAdminLogin: false //
}
},
methods: {
handleAuthSuccess(userData) {
//
this.$emit('auth-success', userData)
},
handleAdminSuccess(adminData) {
//
this.$emit('admin-success', adminData)
},
activateAdminLogin() {
this.isAdminLogin = true
this.isLogin = true // isLogintrue便header
}
}
}
@ -114,7 +131,16 @@ export default {
font-weight: 600;
}
.tab-button.admin-tab.active {
color: #dc3545;
border-bottom-color: #dc3545;
}
.tab-button:hover {
color: #667eea;
}
.tab-button.admin-tab:hover {
color: #dc3545;
}
</style>

@ -6,7 +6,7 @@ import { createRouter, createWebHistory } from 'vue-router'
const routerHistory = createWebHistory()
//配置路由组件
//配置路由组件, 确保组件名与路由名称不同
const router = createRouter({
history: routerHistory,
routes: [
@ -14,6 +14,18 @@ const router = createRouter({
path: '/',
redirect: '/chart-generator'
},
{
path: '/admin',
name: 'Admin',
component: () => import('../Admin.vue'),
meta: { requiresAdmin: true } // 添加路由元信息
},
{
path: '/admin/feedback',
name: 'AdminFeedback',
component: () => import('../views/AdminFeedback.vue'),
meta: { requiresAdmin: true }
},
{
path: '/login',
name: 'Login',
@ -55,8 +67,8 @@ const router = createRouter({
name: 'MyFeedback',
component: () => import('../views/MyFeedback.vue')
},
]
})
export default router
export default router

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save