feat: add JWT auth
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
# ItemManagerWebsite
|
# ItemManagerWebsite
|
||||||
爱特工作室物品管理及财务管理系统
|
爱特工作室管理系统
|
||||||
|
|
||||||
爱特工作室物品管理及财务管理系统是中国海洋大学爱特工作室开发的一个用于管理物品和财务的系统。该系统旨在帮助工作室更高效地管理物品库存和财务记录。遵循**MIT协议**开源,允许自由、合法地使用。
|
爱特工作室管理系统是中国海洋大学爱特工作室开发的一个用于管理物品和财务的系统。该系统旨在帮助工作室更高效地管理物品库存和财务记录。遵循**MIT协议**开源,允许自由、合法地使用。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ class EmailNotificationService:
|
|||||||
])
|
])
|
||||||
for it in data_items:
|
for it in data_items:
|
||||||
plain_lines.append(f"- {it['label']}: {it['value']}")
|
plain_lines.append(f"- {it['label']}: {it['value']}")
|
||||||
plain_lines.append("\n此邮件由爱特工作室物品管理及财务管理系统自动发送")
|
plain_lines.append("\n此邮件由爱特工作室管理系统自动发送")
|
||||||
plain_message = "\n".join(plain_lines)
|
plain_message = "\n".join(plain_lines)
|
||||||
|
|
||||||
# 发送邮件到所有启用的通知邮箱
|
# 发送邮件到所有启用的通知邮箱
|
||||||
|
|||||||
@@ -53,7 +53,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
此邮件由「爱特工作室物品管理及财务管理系统」自动发送,请勿直接回复。
|
此邮件由「爱特工作室管理系统」自动发送,请勿直接回复。
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -1,23 +1,24 @@
|
|||||||
|
from rest_framework.decorators import api_view, authentication_classes, permission_classes
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework_simplejwt.authentication import JWTAuthentication
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from django.http import JsonResponse
|
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
|
||||||
from django.views.decorators.http import require_http_methods
|
|
||||||
|
|
||||||
from .services import EmailNotificationService
|
from .services import EmailNotificationService
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
@api_view(["GET", "POST"])
|
||||||
@require_http_methods(["POST", "GET"])
|
@authentication_classes([JWTAuthentication])
|
||||||
|
@permission_classes([IsAuthenticated])
|
||||||
def notification_settings(request):
|
def notification_settings(request):
|
||||||
"""通知设置API"""
|
"""通知设置API(需要JWT)"""
|
||||||
|
|
||||||
if request.method == 'GET':
|
if request.method == 'GET':
|
||||||
try:
|
try:
|
||||||
settings_data = EmailNotificationService.get_notification_settings()
|
settings_data = EmailNotificationService.get_notification_settings()
|
||||||
all_emails = EmailNotificationService.get_all_notification_emails()
|
all_emails = EmailNotificationService.get_all_notification_emails()
|
||||||
return JsonResponse({
|
return Response({
|
||||||
'success': True,
|
'success': True,
|
||||||
'data': {
|
'data': {
|
||||||
'email_enabled': settings_data.get('email_enabled', False),
|
'email_enabled': settings_data.get('email_enabled', False),
|
||||||
@@ -26,11 +27,11 @@ def notification_settings(request):
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return JsonResponse({'success': False, 'error': str(e)}, status=500)
|
return Response({'success': False, 'error': str(e)}, status=500)
|
||||||
|
|
||||||
elif request.method == 'POST':
|
elif request.method == 'POST':
|
||||||
try:
|
try:
|
||||||
data = json.loads(request.body)
|
data = request.data if hasattr(request, 'data') else json.loads(request.body or '{}')
|
||||||
emails_to_update = data.get('notification_emails')
|
emails_to_update = data.get('notification_emails')
|
||||||
email_enabled_status = data.get('email_enabled')
|
email_enabled_status = data.get('email_enabled')
|
||||||
|
|
||||||
@@ -38,7 +39,7 @@ def notification_settings(request):
|
|||||||
was_updated = False
|
was_updated = False
|
||||||
|
|
||||||
# 1. 如果请求中包含 'notification_emails' 键,则处理邮箱列表
|
# 1. 如果请求中包含 'notification_emails' 键,则处理邮箱列表
|
||||||
if emails_to_update is not None: # 允许 emails_to_update 为空列表 []
|
if emails_to_update is not None: # 允许空列表
|
||||||
email_regex = re.compile(r'^[^\s@]+@[^\s@]+\.[^\s@]+$')
|
email_regex = re.compile(r'^[^\s@]+@[^\s@]+\.[^\s@]+$')
|
||||||
valid_emails_data = []
|
valid_emails_data = []
|
||||||
|
|
||||||
@@ -70,7 +71,7 @@ def notification_settings(request):
|
|||||||
|
|
||||||
# 执行更新成功,返回成功
|
# 执行更新成功,返回成功
|
||||||
if was_updated:
|
if was_updated:
|
||||||
return JsonResponse({
|
return Response({
|
||||||
'success': True,
|
'success': True,
|
||||||
'message': '通知设置已更新',
|
'message': '通知设置已更新',
|
||||||
# 返回最新的数据状态
|
# 返回最新的数据状态
|
||||||
@@ -81,31 +82,31 @@ def notification_settings(request):
|
|||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
# 如果请求体为空或不包含任何有效键,则返回错误
|
# 如果请求体为空或不包含任何有效键,则返回错误
|
||||||
return JsonResponse({'success': False, 'error': '未提供任何有效的更新数据'}, status=400)
|
return Response({'success': False, 'error': '未提供任何有效的更新数据'}, status=400)
|
||||||
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
return JsonResponse({'success': False, 'error': '无效的JSON数据'}, status=400)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return JsonResponse({'success': False, 'error': str(e)}, status=500)
|
return Response({'success': False, 'error': str(e)}, status=500)
|
||||||
|
|
||||||
return JsonResponse({'success': False, 'error': '不支持的请求方法'}, status=405)
|
return Response({'success': False, 'error': '不支持的请求方法'}, status=405)
|
||||||
|
|
||||||
@csrf_exempt
|
|
||||||
@require_http_methods(["POST"])
|
@api_view(["POST"])
|
||||||
|
@authentication_classes([JWTAuthentication])
|
||||||
|
@permission_classes([IsAuthenticated])
|
||||||
def toggle_email_status(request):
|
def toggle_email_status(request):
|
||||||
"""切换邮箱启用状态API"""
|
"""切换邮箱启用状态API(需要JWT)"""
|
||||||
try:
|
try:
|
||||||
data = json.loads(request.body)
|
data = request.data if hasattr(request, 'data') else json.loads(request.body or '{}')
|
||||||
email_id = data.get('email_id')
|
email_id = data.get('email_id')
|
||||||
is_enabled = data.get('is_enabled', True)
|
is_enabled = data.get('is_enabled', True)
|
||||||
|
|
||||||
if not email_id:
|
if not email_id:
|
||||||
return JsonResponse({'success': False, 'error': '缺少邮箱ID'}, status=400)
|
return Response({'success': False, 'error': '缺少邮箱ID'}, status=400)
|
||||||
|
|
||||||
success = EmailNotificationService.toggle_email_status(email_id, is_enabled)
|
success = EmailNotificationService.toggle_email_status(email_id, is_enabled)
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
return JsonResponse({
|
return Response({
|
||||||
'success': True,
|
'success': True,
|
||||||
'message': f'邮箱状态已更新为{"启用" if is_enabled else "禁用"}',
|
'message': f'邮箱状态已更新为{"启用" if is_enabled else "禁用"}',
|
||||||
'data': {
|
'data': {
|
||||||
@@ -113,9 +114,7 @@ def toggle_email_status(request):
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
return JsonResponse({'success': False, 'error': '更新邮箱状态失败'}, status=500)
|
return Response({'success': False, 'error': '更新邮箱状态失败'}, status=500)
|
||||||
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
return JsonResponse({'success': False, 'error': '无效的JSON数据'}, status=400)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return JsonResponse({'success': False, 'error': str(e)}, status=500)
|
return Response({'success': False, 'error': str(e)}, status=500)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from django.utils import timezone
|
|||||||
from rest_framework import viewsets, status
|
from rest_framework import viewsets, status
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
from rest_framework_simplejwt.authentication import JWTAuthentication
|
||||||
|
|
||||||
from .models import FinancialRecord, Department, Category, ProofImage
|
from .models import FinancialRecord, Department, Category, ProofImage
|
||||||
from .serializers import (
|
from .serializers import (
|
||||||
@@ -20,6 +21,7 @@ class FinancialRecordViewSet(viewsets.ModelViewSet):
|
|||||||
"""
|
"""
|
||||||
获取财务记录
|
获取财务记录
|
||||||
"""
|
"""
|
||||||
|
authentication_classes = [JWTAuthentication]
|
||||||
queryset = FinancialRecord.objects.all()
|
queryset = FinancialRecord.objects.all()
|
||||||
|
|
||||||
def get_serializer_class(self):
|
def get_serializer_class(self):
|
||||||
@@ -185,6 +187,7 @@ class ProofImageViewSet(viewsets.ModelViewSet):
|
|||||||
"""
|
"""
|
||||||
凭证API
|
凭证API
|
||||||
"""
|
"""
|
||||||
|
authentication_classes = [JWTAuthentication]
|
||||||
queryset = ProofImage.objects.all()
|
queryset = ProofImage.objects.all()
|
||||||
serializer_class = ProofImageSerializer
|
serializer_class = ProofImageSerializer
|
||||||
|
|
||||||
@@ -283,6 +286,7 @@ class DepartmentViewSet(viewsets.ModelViewSet):
|
|||||||
"""
|
"""
|
||||||
获取部门
|
获取部门
|
||||||
"""
|
"""
|
||||||
|
authentication_classes = [JWTAuthentication]
|
||||||
queryset = Department.objects.all()
|
queryset = Department.objects.all()
|
||||||
serializer_class = DepartmentSerializer
|
serializer_class = DepartmentSerializer
|
||||||
|
|
||||||
@@ -346,5 +350,6 @@ class CategoryViewSet(viewsets.ModelViewSet):
|
|||||||
"""
|
"""
|
||||||
获取分类
|
获取分类
|
||||||
"""
|
"""
|
||||||
|
authentication_classes = [JWTAuthentication]
|
||||||
queryset = Category.objects.all()
|
queryset = Category.objects.all()
|
||||||
serializer_class = CategorySerializer
|
serializer_class = CategorySerializer
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ https://docs.djangoproject.com/en/5.2/ref/settings/
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
from datetime import timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# SECURE 文件用来存储敏感信息,如 SECRET_KEY,SMTP信息 等
|
# SECURE 文件用来存储敏感信息,如 SECRET_KEY,SMTP信息 等
|
||||||
@@ -134,13 +135,24 @@ AUTH_PASSWORD_VALIDATORS = [
|
|||||||
# REST Framework configuration
|
# REST Framework configuration
|
||||||
REST_FRAMEWORK = {
|
REST_FRAMEWORK = {
|
||||||
"DEFAULT_PERMISSION_CLASSES": [
|
"DEFAULT_PERMISSION_CLASSES": [
|
||||||
"rest_framework.permissions.AllowAny",
|
"rest_framework.permissions.IsAuthenticated",
|
||||||
],
|
],
|
||||||
"DEFAULT_AUTHENTICATION_CLASSES": [
|
"DEFAULT_AUTHENTICATION_CLASSES": [
|
||||||
"rest_framework.authentication.SessionAuthentication",
|
"rest_framework_simplejwt.authentication.JWTAuthentication",
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# JWT 配置(可根据需要调整过期时间)
|
||||||
|
SIMPLE_JWT = {
|
||||||
|
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=60),
|
||||||
|
"REFRESH_TOKEN_LIFETIME": timedelta(days=7),
|
||||||
|
"ROTATE_REFRESH_TOKENS": False,
|
||||||
|
"BLACKLIST_AFTER_ROTATION": False,
|
||||||
|
"ALGORITHM": "HS256",
|
||||||
|
"SIGNING_KEY": SECRET_KEY,
|
||||||
|
"AUTH_HEADER_TYPES": ("Bearer",),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# CORS settings
|
# CORS settings
|
||||||
CORS_ALLOWED_ORIGINS = [
|
CORS_ALLOWED_ORIGINS = [
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ from django.conf import settings
|
|||||||
from django.conf.urls.static import static
|
from django.conf.urls.static import static
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import path, include
|
from django.urls import path, include
|
||||||
|
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("admin/", admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
@@ -27,6 +28,8 @@ urlpatterns = [
|
|||||||
path("", include("email_notice.urls")),
|
path("", include("email_notice.urls")),
|
||||||
path("", include("personnel.urls")),
|
path("", include("personnel.urls")),
|
||||||
path("api-auth/", include("rest_framework.urls")),
|
path("api-auth/", include("rest_framework.urls")),
|
||||||
|
path("api/token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
|
||||||
|
path("api/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
|
||||||
]
|
]
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from django.utils import timezone
|
|||||||
from rest_framework import viewsets, status
|
from rest_framework import viewsets, status
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
from rest_framework_simplejwt.authentication import JWTAuthentication
|
||||||
|
|
||||||
from .models import Item, ItemUsage, Category
|
from .models import Item, ItemUsage, Category
|
||||||
from .serializers import (
|
from .serializers import (
|
||||||
@@ -13,6 +14,7 @@ from .serializers import (
|
|||||||
|
|
||||||
class ItemViewSet(viewsets.ModelViewSet):
|
class ItemViewSet(viewsets.ModelViewSet):
|
||||||
"""物品管理API"""
|
"""物品管理API"""
|
||||||
|
authentication_classes = [JWTAuthentication]
|
||||||
queryset = Item.objects.all()
|
queryset = Item.objects.all()
|
||||||
serializer_class = ItemSerializer
|
serializer_class = ItemSerializer
|
||||||
|
|
||||||
@@ -107,6 +109,7 @@ class ItemViewSet(viewsets.ModelViewSet):
|
|||||||
|
|
||||||
class ItemUsageViewSet(viewsets.ModelViewSet):
|
class ItemUsageViewSet(viewsets.ModelViewSet):
|
||||||
"""使用记录管理API"""
|
"""使用记录管理API"""
|
||||||
|
authentication_classes = [JWTAuthentication]
|
||||||
queryset = ItemUsage.objects.all()
|
queryset = ItemUsage.objects.all()
|
||||||
serializer_class = ItemUsageSerializer
|
serializer_class = ItemUsageSerializer
|
||||||
|
|
||||||
@@ -130,11 +133,13 @@ class ItemUsageViewSet(viewsets.ModelViewSet):
|
|||||||
|
|
||||||
class CategoryViewSet(viewsets.ModelViewSet):
|
class CategoryViewSet(viewsets.ModelViewSet):
|
||||||
"""物品类别管理API"""
|
"""物品类别管理API"""
|
||||||
|
authentication_classes = [JWTAuthentication]
|
||||||
queryset = Category.objects.all()
|
queryset = Category.objects.all()
|
||||||
serializer_class = CategorySerializer
|
serializer_class = CategorySerializer
|
||||||
|
|
||||||
|
|
||||||
class UserViewSet(viewsets.ReadOnlyModelViewSet):
|
class UserViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
"""用户管理API(只读)"""
|
"""用户管理API(只读)"""
|
||||||
|
authentication_classes = [JWTAuthentication]
|
||||||
queryset = User.objects.all()
|
queryset = User.objects.all()
|
||||||
serializer_class = UserSerializer
|
serializer_class = UserSerializer
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from rest_framework import viewsets
|
|||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.filters import SearchFilter, OrderingFilter
|
from rest_framework.filters import SearchFilter, OrderingFilter
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
from rest_framework_simplejwt.authentication import JWTAuthentication
|
||||||
|
|
||||||
from .filters import PersonnelFilter
|
from .filters import PersonnelFilter
|
||||||
from .models import Personnel, ProjectGroup
|
from .models import Personnel, ProjectGroup
|
||||||
@@ -20,6 +21,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
class PersonnelViewSet(viewsets.ModelViewSet):
|
class PersonnelViewSet(viewsets.ModelViewSet):
|
||||||
"""人员信息视图集"""
|
"""人员信息视图集"""
|
||||||
|
authentication_classes = [JWTAuthentication]
|
||||||
queryset = Personnel.objects.all()
|
queryset = Personnel.objects.all()
|
||||||
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
||||||
filterset_class = PersonnelFilter
|
filterset_class = PersonnelFilter
|
||||||
@@ -191,6 +193,7 @@ class PersonnelViewSet(viewsets.ModelViewSet):
|
|||||||
|
|
||||||
class ProjectGroupViewSet(viewsets.ModelViewSet):
|
class ProjectGroupViewSet(viewsets.ModelViewSet):
|
||||||
"""项目组视图集"""
|
"""项目组视图集"""
|
||||||
|
authentication_classes = [JWTAuthentication]
|
||||||
queryset = ProjectGroup.objects.all()
|
queryset = ProjectGroup.objects.all()
|
||||||
serializer_class = ProjectGroupSerializer
|
serializer_class = ProjectGroupSerializer
|
||||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||||
|
|||||||
Binary file not shown.
@@ -5,7 +5,7 @@
|
|||||||
<el-button type="text" class="menu-toggle mobile-only" @click="drawerVisible = true" aria-label="打开导航">
|
<el-button type="text" class="menu-toggle mobile-only" @click="drawerVisible = true" aria-label="打开导航">
|
||||||
<el-icon><Menu /></el-icon>
|
<el-icon><Menu /></el-icon>
|
||||||
</el-button>
|
</el-button>
|
||||||
<h1 class="logo">爱特工作室物品管理及财务管理系统</h1>
|
<h1 class="logo">爱特工作室管理系统</h1>
|
||||||
</div>
|
</div>
|
||||||
<el-menu
|
<el-menu
|
||||||
mode="horizontal"
|
mode="horizontal"
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
<template>
|
||||||
|
<el-card class="logout-card" shadow="never">
|
||||||
|
<div class="logout-row">
|
||||||
|
<div class="logout-info">
|
||||||
|
<el-icon class="logout-icon"><User /></el-icon>
|
||||||
|
<div class="logout-text">
|
||||||
|
<div class="title">账户</div>
|
||||||
|
<div class="desc">当前为已登录状态,如需切换账号请先退出登录</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<el-button type="danger" @click="logout" :loading="loading">
|
||||||
|
退出登录
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { User } from '@element-plus/icons-vue'
|
||||||
|
import { authService } from '@/services/api'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'LogoutBar',
|
||||||
|
components: { User },
|
||||||
|
data() {
|
||||||
|
return { loading: false }
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async logout() {
|
||||||
|
try {
|
||||||
|
this.loading = true
|
||||||
|
// 清除本地JWT
|
||||||
|
authService.clearTokens()
|
||||||
|
// 可选:清空本地与设置相关的缓存
|
||||||
|
localStorage.removeItem('emailNotificationEnabled')
|
||||||
|
// 跳转到登录页
|
||||||
|
const redirect = this.$route.fullPath || '/'
|
||||||
|
await this.$router.replace({ path: '/login', query: { redirect } })
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.logout-card {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-icon {
|
||||||
|
font-size: 22px;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-text .title {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-text .desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.logout-row {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.actions {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.actions .el-button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@@ -10,6 +10,7 @@ import FinanceRecordDetail from '../views/FinanceRecordDetail.vue'
|
|||||||
import PersonnelList from '../views/PersonnelList.vue'
|
import PersonnelList from '../views/PersonnelList.vue'
|
||||||
import DepartmentProjectGroupManagement from '../views/DepartmentProjectGroupManagement.vue'
|
import DepartmentProjectGroupManagement from '../views/DepartmentProjectGroupManagement.vue'
|
||||||
import Settings from '../views/Settings.vue'
|
import Settings from '../views/Settings.vue'
|
||||||
|
import { authService } from '@/services/api'
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
@@ -76,4 +77,23 @@ const router = createRouter({
|
|||||||
routes
|
routes
|
||||||
})
|
})
|
||||||
|
|
||||||
|
router.beforeEach((to, from, next) => {
|
||||||
|
// 允许直接访问登录页
|
||||||
|
if (to.path === '/login') {
|
||||||
|
if (authService.isAuthenticated()) {
|
||||||
|
// 已登录,跳转到首页或原定路径
|
||||||
|
const redirect = to.query.redirect || '/'
|
||||||
|
return next(redirect)
|
||||||
|
}
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其他页面需要登录
|
||||||
|
if (!authService.isAuthenticated()) {
|
||||||
|
return next({ path: '/login', query: { redirect: to.fullPath } })
|
||||||
|
}
|
||||||
|
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|||||||
@@ -1,38 +1,106 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
export const API_BASE_URL = 'http://localhost:8000/api'
|
export const API_BASE_URL = 'http://localhost:8000/api'
|
||||||
|
|
||||||
export const API_BASE_URL_WITHOUT_API = 'http://localhost:8000'
|
export const API_BASE_URL_WITHOUT_API = 'http://localhost:8000'
|
||||||
|
|
||||||
|
const ACCESS_TOKEN_KEY = 'access_token'
|
||||||
|
const REFRESH_TOKEN_KEY = 'refresh_token'
|
||||||
|
|
||||||
|
export const authService = {
|
||||||
|
getAccessToken() {
|
||||||
|
return localStorage.getItem(ACCESS_TOKEN_KEY)
|
||||||
|
},
|
||||||
|
getRefreshToken() {
|
||||||
|
return localStorage.getItem(REFRESH_TOKEN_KEY)
|
||||||
|
},
|
||||||
|
setTokens({ access, refresh }) {
|
||||||
|
if (access) localStorage.setItem(ACCESS_TOKEN_KEY, access)
|
||||||
|
if (refresh) localStorage.setItem(REFRESH_TOKEN_KEY, refresh)
|
||||||
|
},
|
||||||
|
clearTokens() {
|
||||||
|
localStorage.removeItem(ACCESS_TOKEN_KEY)
|
||||||
|
localStorage.removeItem(REFRESH_TOKEN_KEY)
|
||||||
|
},
|
||||||
|
isAuthenticated() {
|
||||||
|
return !!this.getAccessToken()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const apiClient = axios.create({
|
const apiClient = axios.create({
|
||||||
baseURL: API_BASE_URL,
|
baseURL: API_BASE_URL,
|
||||||
headers: {
|
headers: { 'Content-Type': 'application/json' }
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 请求拦截器
|
// 请求拦截器:自动附加JWT
|
||||||
apiClient.interceptors.request.use(
|
apiClient.interceptors.request.use(
|
||||||
config => {
|
config => {
|
||||||
console.log('API请求:', config.method?.toUpperCase(), config.url)
|
const token = authService.getAccessToken()
|
||||||
|
if (token) {
|
||||||
|
config.headers = config.headers || {}
|
||||||
|
config.headers['Authorization'] = `Bearer ${token}`
|
||||||
|
}
|
||||||
return config
|
return config
|
||||||
},
|
},
|
||||||
error => {
|
error => Promise.reject(error)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 响应拦截器:401时尝试刷新token并重试
|
||||||
|
let isRefreshing = false
|
||||||
|
let pendingQueue = []
|
||||||
|
|
||||||
|
function processQueue(error, token = null) {
|
||||||
|
pendingQueue.forEach(({ resolve, reject, config }) => {
|
||||||
|
if (token) {
|
||||||
|
config.headers['Authorization'] = `Bearer ${token}`
|
||||||
|
resolve(apiClient(config))
|
||||||
|
} else {
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
pendingQueue = []
|
||||||
|
}
|
||||||
|
|
||||||
|
apiClient.interceptors.response.use(
|
||||||
|
response => response,
|
||||||
|
async error => {
|
||||||
|
const originalRequest = error.config || {}
|
||||||
|
const status = error.response?.status
|
||||||
|
|
||||||
|
if (status === 401 && !originalRequest._retry) {
|
||||||
|
if (isRefreshing) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
pendingQueue.push({ resolve, reject, config: originalRequest })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
originalRequest._retry = true
|
||||||
|
isRefreshing = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const refresh = authService.getRefreshToken()
|
||||||
|
if (!refresh) throw new Error('No refresh token')
|
||||||
|
const resp = await axios.post(`${API_BASE_URL_WITHOUT_API}/api/token/refresh/`, { refresh })
|
||||||
|
const newAccess = resp.data?.access
|
||||||
|
if (!newAccess) throw new Error('No access token in refresh response')
|
||||||
|
authService.setTokens({ access: newAccess })
|
||||||
|
processQueue(null, newAccess)
|
||||||
|
return apiClient(originalRequest)
|
||||||
|
} catch (refreshErr) {
|
||||||
|
processQueue(refreshErr, null)
|
||||||
|
authService.clearTokens()
|
||||||
|
// 跳转到登录页
|
||||||
|
try { window?.location && (window.location.href = '/login') } catch (_) {}
|
||||||
|
return Promise.reject(refreshErr)
|
||||||
|
} finally {
|
||||||
|
isRefreshing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return Promise.reject(error)
|
return Promise.reject(error)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// 响应拦截器
|
export { apiClient }
|
||||||
apiClient.interceptors.response.use(
|
|
||||||
response => {
|
|
||||||
console.log('API响应:', response.status, response.config.url)
|
|
||||||
return response
|
|
||||||
},
|
|
||||||
error => {
|
|
||||||
console.error('API错误:', error.response?.status, error.response?.data)
|
|
||||||
return Promise.reject(error)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
export const itemService = {
|
export const itemService = {
|
||||||
// 获取所有物品
|
// 获取所有物品
|
||||||
@@ -314,4 +382,3 @@ export const projectGroupService = {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="login-header">
|
<div class="login-header">
|
||||||
<div class="header-content">
|
<div class="header-content">
|
||||||
<h1 class="system-title">爱特工作室物品管理及财务管理系统</h1>
|
<h1 class="system-title">爱特工作室管理系统</h1>
|
||||||
<div class="favicon-container">
|
<div class="favicon-container">
|
||||||
<img src="../../public/favicon.svg" alt="网站图标" class="favicon">
|
<img src="../../public/favicon.svg" alt="网站图标" class="favicon">
|
||||||
</div>
|
</div>
|
||||||
@@ -95,6 +95,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {ElMessage} from 'element-plus'
|
import {ElMessage} from 'element-plus'
|
||||||
|
import { API_BASE_URL_WITHOUT_API, authService } from '@/services/api'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Login',
|
name: 'Login',
|
||||||
@@ -150,37 +151,43 @@ export default {
|
|||||||
|
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
|
|
||||||
// 模拟登录API调用
|
// 调用JWT登录
|
||||||
await this.performLogin()
|
const resp = await fetch(`${API_BASE_URL_WITHOUT_API}/api/token/`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
username: this.loginForm.username,
|
||||||
|
password: this.loginForm.password,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!resp.ok) {
|
||||||
|
throw new Error('用户名或密码错误')
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await resp.json()
|
||||||
|
const access = data.access
|
||||||
|
const refresh = data.refresh
|
||||||
|
if (!access || !refresh) {
|
||||||
|
throw new Error('登录响应缺少令牌')
|
||||||
|
}
|
||||||
|
|
||||||
|
authService.setTokens({ access, refresh })
|
||||||
ElMessage.success('登录成功')
|
ElMessage.success('登录成功')
|
||||||
|
|
||||||
// 跳转到主页
|
// 跳转到原目标或首页
|
||||||
await this.$router.push('/')
|
const redirect = this.$route.query.redirect || '/'
|
||||||
|
await this.$router.replace(redirect)
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('登录失败:', error)
|
console.error('登录失败:', error)
|
||||||
if (error !== 'validation failed') {
|
if (error !== 'validation failed') {
|
||||||
ElMessage.error('登录失败,请检查账户和密码')
|
ElMessage.error(error.message || '登录失败,请检查账户和密码')
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async performLogin() {
|
|
||||||
// 模拟API调用延迟
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
// 简单的模拟验证
|
|
||||||
if (this.loginForm.username && this.loginForm.password) {
|
|
||||||
resolve()
|
|
||||||
} else {
|
|
||||||
reject(new Error('账户或密码错误'))
|
|
||||||
}
|
|
||||||
}, 1500)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
<div class="settings-page">
|
<div class="settings-page">
|
||||||
<AppHeader />
|
<AppHeader />
|
||||||
<div class="settings-container">
|
<div class="settings-container">
|
||||||
|
<LogoutBar />
|
||||||
<div class="settings-header">
|
<div class="settings-header">
|
||||||
<h2>网站设置及说明</h2>
|
<h2>网站设置及说明</h2>
|
||||||
</div>
|
</div>
|
||||||
@@ -121,7 +122,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<div class="content-section">
|
<div class="content-section">
|
||||||
<h3>系统介绍</h3>
|
<h3>系统介绍</h3>
|
||||||
<p>爱特工作室物品管理及财务管理系统是为爱特工作室量身定制的综合管理平台,主要功能包括:</p>
|
<p>爱特工作室管理系统是为爱特工作室量身定制的综合管理平台,主要功能包括:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li><strong>物品管理:</strong>管理工作室的各类物品,包括电子设备、办公用品等</li>
|
<li><strong>物品管理:</strong>管理工作室的各类物品,包括电子设备、办公用品等</li>
|
||||||
<li><strong>借用记录:</strong>跟踪物品的借用情况,确保物品流转透明化</li>
|
<li><strong>借用记录:</strong>跟踪物品的借用情况,确保物品流转透明化</li>
|
||||||
@@ -175,12 +176,14 @@ import {onMounted, ref} from 'vue'
|
|||||||
import {ElMessage} from 'element-plus'
|
import {ElMessage} from 'element-plus'
|
||||||
import {Delete, InfoFilled, Message, Plus} from '@element-plus/icons-vue'
|
import {Delete, InfoFilled, Message, Plus} from '@element-plus/icons-vue'
|
||||||
import AppHeader from '@/components/AppHeader.vue'
|
import AppHeader from '@/components/AppHeader.vue'
|
||||||
import {API_BASE_URL_WITHOUT_API} from '@/services/api'
|
import LogoutBar from '@/components/LogoutBar.vue'
|
||||||
|
import {API_BASE_URL_WITHOUT_API, authService} from '@/services/api'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Settings',
|
name: 'Settings',
|
||||||
components: {
|
components: {
|
||||||
AppHeader,
|
AppHeader,
|
||||||
|
LogoutBar,
|
||||||
Message,
|
Message,
|
||||||
InfoFilled,
|
InfoFilled,
|
||||||
Plus,
|
Plus,
|
||||||
@@ -195,6 +198,14 @@ export default {
|
|||||||
is_enabled: true
|
is_enabled: true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const authHeaders = () => {
|
||||||
|
const token = authService.getAccessToken()
|
||||||
|
return {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(token ? { 'Authorization': `Bearer ${token}` } : {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 加载设置
|
// 加载设置
|
||||||
const loadSettings = () => {
|
const loadSettings = () => {
|
||||||
// 从localStorage加载邮件通知设置
|
// 从localStorage加载邮件通知设置
|
||||||
@@ -210,9 +221,7 @@ export default {
|
|||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE_URL_WITHOUT_API}/api/notification-settings/`, {
|
const response = await fetch(`${API_BASE_URL_WITHOUT_API}/api/notification-settings/`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: authHeaders()
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
@@ -228,6 +237,8 @@ export default {
|
|||||||
allEmails.value = result.data.all_emails
|
allEmails.value = result.data.all_emails
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (response.status === 401) {
|
||||||
|
ElMessage.error('未授权,请重新登录')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载服务器设置失败:', error)
|
console.error('加载服务器设置失败:', error)
|
||||||
@@ -249,9 +260,7 @@ export default {
|
|||||||
|
|
||||||
const response = await fetch(`${API_BASE_URL_WITHOUT_API}/api/notification-settings/`, {
|
const response = await fetch(`${API_BASE_URL_WITHOUT_API}/api/notification-settings/`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: authHeaders(),
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
notification_emails: emailsToUpdate,
|
notification_emails: emailsToUpdate,
|
||||||
email_enabled: emailNotificationEnabled.value
|
email_enabled: emailNotificationEnabled.value
|
||||||
@@ -279,13 +288,9 @@ export default {
|
|||||||
// 切换单个邮箱启用状态
|
// 切换单个邮箱启用状态
|
||||||
const toggleEmailStatus = async (emailId, isEnabled) => {
|
const toggleEmailStatus = async (emailId, isEnabled) => {
|
||||||
try {
|
try {
|
||||||
// [关键修改] 请求发送到后端的 toggle_email_status 视图对应的 URL
|
|
||||||
// 你需要在 urls.py 中为 toggle_email_status 视图配置一个 URL,例如 'api/toggle-email-status/'
|
|
||||||
const response = await fetch(`${API_BASE_URL_WITHOUT_API}/api/toggle-email-status/`, {
|
const response = await fetch(`${API_BASE_URL_WITHOUT_API}/api/toggle-email-status/`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: authHeaders(),
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
email_id: emailId,
|
email_id: emailId,
|
||||||
is_enabled: isEnabled
|
is_enabled: isEnabled
|
||||||
@@ -342,9 +347,7 @@ export default {
|
|||||||
// [关键修改] 将这个完整的列表发送给后端
|
// [关键修改] 将这个完整的列表发送给后端
|
||||||
const response = await fetch(`${API_BASE_URL_WITHOUT_API}/api/notification-settings/`, {
|
const response = await fetch(`${API_BASE_URL_WITHOUT_API}/api/notification-settings/`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: authHeaders(),
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
notification_emails: updatedEmailList,
|
notification_emails: updatedEmailList,
|
||||||
email_enabled: emailNotificationEnabled.value // 别忘了带上总开关的状态
|
email_enabled: emailNotificationEnabled.value // 别忘了带上总开关的状态
|
||||||
@@ -388,9 +391,7 @@ export default {
|
|||||||
// [关键修改] 将这个新列表发送给后端进行批量更新
|
// [关键修改] 将这个新列表发送给后端进行批量更新
|
||||||
const response = await fetch(`${API_BASE_URL_WITHOUT_API}/api/notification-settings/`, {
|
const response = await fetch(`${API_BASE_URL_WITHOUT_API}/api/notification-settings/`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: authHeaders(),
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
notification_emails: updatedEmailList,
|
notification_emails: updatedEmailList,
|
||||||
email_enabled: emailNotificationEnabled.value // 别忘了带上总开关的状态
|
email_enabled: emailNotificationEnabled.value // 别忘了带上总开关的状态
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ module.exports = defineConfig({
|
|||||||
entry: 'src/main.js',
|
entry: 'src/main.js',
|
||||||
template: 'public/index.html',
|
template: 'public/index.html',
|
||||||
filename: 'index.html',
|
filename: 'index.html',
|
||||||
title: '爱特工作室物品管理及财务管理系统',
|
title: '爱特工作室管理系统',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user