feat: add email notice and settings page but still have some issue
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from .models import NotificationEmail, NotificationSettings
|
||||||
|
|
||||||
|
@admin.register(NotificationEmail)
|
||||||
|
class NotificationEmailAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('email', 'is_enabled', 'description', 'created_at')
|
||||||
|
list_filter = ('is_enabled', 'created_at')
|
||||||
|
search_fields = ('email', 'description')
|
||||||
|
list_editable = ('is_enabled',)
|
||||||
|
ordering = ('-created_at',)
|
||||||
|
|
||||||
|
@admin.register(NotificationSettings)
|
||||||
|
class NotificationSettingsAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('email_notification_enabled', 'updated_at')
|
||||||
|
|
||||||
|
def has_add_permission(self, request):
|
||||||
|
# 只允许存在一个设置实例
|
||||||
|
return not NotificationSettings.objects.exists()
|
||||||
|
|
||||||
|
def has_delete_permission(self, request, obj=None):
|
||||||
|
# 不允许删除设置
|
||||||
|
return False
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class EmailConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'email_notice'
|
||||||
@@ -0,0 +1,297 @@
|
|||||||
|
from django.utils.deprecation import MiddlewareMixin
|
||||||
|
from django.http import JsonResponse
|
||||||
|
from .services import EmailNotificationService
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import threading
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class EmailNotificationMiddleware(MiddlewareMixin):
|
||||||
|
"""邮件通知中间件"""
|
||||||
|
|
||||||
|
def __init__(self, get_response):
|
||||||
|
self.get_response = get_response
|
||||||
|
super().__init__(get_response)
|
||||||
|
# 用于存储删除前的数据
|
||||||
|
self._delete_data_cache = {}
|
||||||
|
|
||||||
|
def process_request(self, request):
|
||||||
|
"""在处理请求前,如果是删除操作,先获取要删除的数据"""
|
||||||
|
try:
|
||||||
|
# 只处理DELETE操作
|
||||||
|
if request.method != 'DELETE':
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 只处理API请求
|
||||||
|
if not request.path.startswith('/api/'):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 从URL路径中提取ID
|
||||||
|
path_parts = request.path.split('/')
|
||||||
|
record_id = None
|
||||||
|
for part in path_parts:
|
||||||
|
if part.isdigit():
|
||||||
|
record_id = part
|
||||||
|
break
|
||||||
|
|
||||||
|
if not record_id:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 根据路径类型获取对应的数据
|
||||||
|
if '/api/items/' in request.path:
|
||||||
|
data = self._get_item_data_before_delete(record_id)
|
||||||
|
if data:
|
||||||
|
self._delete_data_cache[f"item_{record_id}"] = data
|
||||||
|
elif '/api/records/' in request.path or '/api/finance/' in request.path:
|
||||||
|
data = self._get_finance_data_before_delete(record_id)
|
||||||
|
if data:
|
||||||
|
self._delete_data_cache[f"finance_{record_id}"] = data
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取删除前数据失败: {e}")
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _get_item_data_before_delete(self, item_id):
|
||||||
|
"""获取物品删除前的数据"""
|
||||||
|
try:
|
||||||
|
from items.models import Item
|
||||||
|
item = Item.objects.get(id=item_id)
|
||||||
|
return {
|
||||||
|
'id': item.id,
|
||||||
|
'name': item.name,
|
||||||
|
'serial_number': item.serial_number,
|
||||||
|
'status': item.status,
|
||||||
|
'category': item.category,
|
||||||
|
'owner': item.owner,
|
||||||
|
'purchase_date': str(item.purchase_date) if item.purchase_date else '',
|
||||||
|
'purchase_price': str(item.purchase_price) if item.purchase_price else '',
|
||||||
|
'description': item.description,
|
||||||
|
'updated_at': str(item.updated_at) if hasattr(item, 'updated_at') else '',
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取物品数据失败: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _get_finance_data_before_delete(self, record_id):
|
||||||
|
"""获取财务记录删除前的数据"""
|
||||||
|
try:
|
||||||
|
from finance.models import FinanceRecord
|
||||||
|
record = FinanceRecord.objects.get(id=record_id)
|
||||||
|
return {
|
||||||
|
'id': record.id,
|
||||||
|
'amount': str(record.amount),
|
||||||
|
'transaction_type': record.transaction_type,
|
||||||
|
'description': record.description,
|
||||||
|
'department': record.department.name if record.department else '',
|
||||||
|
'category': record.category.name if record.category else '',
|
||||||
|
'approver': record.approver,
|
||||||
|
'transaction_date': str(record.transaction_date),
|
||||||
|
'proof_images': [str(img.image) for img in record.proof_images.all()] if hasattr(record, 'proof_images') else []
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取财务记录数据失败: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def process_response(self, request, response):
|
||||||
|
"""处理响应,在数据修改操作成功后异步发送邮件通知"""
|
||||||
|
try:
|
||||||
|
# 只处理API请求
|
||||||
|
if not request.path.startswith('/api/'):
|
||||||
|
return response
|
||||||
|
|
||||||
|
# 只处理数据修改操作
|
||||||
|
if request.method not in ['POST', 'PUT', 'PATCH', 'DELETE']:
|
||||||
|
return response
|
||||||
|
|
||||||
|
# 只处理成功的响应
|
||||||
|
if not (200 <= response.status_code < 300):
|
||||||
|
return response
|
||||||
|
|
||||||
|
# 获取用户信息
|
||||||
|
user_info = self._get_user_info(request)
|
||||||
|
|
||||||
|
# 异步处理邮件通知,避免阻塞API响应
|
||||||
|
if '/api/items/' in request.path:
|
||||||
|
self._handle_item_operation_async(request, response, user_info)
|
||||||
|
elif '/api/records/' in request.path or '/api/finance/' in request.path:
|
||||||
|
self._handle_finance_operation_async(request, response, user_info)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"邮件通知中间件处理失败: {e}")
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
def _get_user_info(self, request):
|
||||||
|
"""获取用户信息"""
|
||||||
|
try:
|
||||||
|
if hasattr(request, 'user') and request.user.is_authenticated:
|
||||||
|
return f"{request.user.username} ({request.user.email})"
|
||||||
|
else:
|
||||||
|
return f"匿名用户 (IP: {self._get_client_ip(request)})"
|
||||||
|
except:
|
||||||
|
return "未知用户"
|
||||||
|
|
||||||
|
def _get_client_ip(self, request):
|
||||||
|
"""获取客户端IP"""
|
||||||
|
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
|
||||||
|
if x_forwarded_for:
|
||||||
|
ip = x_forwarded_for.split(',')[0]
|
||||||
|
else:
|
||||||
|
ip = request.META.get('REMOTE_ADDR')
|
||||||
|
return ip
|
||||||
|
|
||||||
|
def _handle_item_operation_async(self, request, response, user_info):
|
||||||
|
"""异步处理物品操作"""
|
||||||
|
def send_notification():
|
||||||
|
try:
|
||||||
|
operation_type = self._get_operation_type(request.method, request.path)
|
||||||
|
|
||||||
|
# 对于DELETE操作,使用预先获取的数据
|
||||||
|
if request.method == 'DELETE':
|
||||||
|
# 从URL路径中提取ID
|
||||||
|
path_parts = request.path.split('/')
|
||||||
|
item_id = None
|
||||||
|
for part in path_parts:
|
||||||
|
if part.isdigit():
|
||||||
|
item_id = part
|
||||||
|
break
|
||||||
|
|
||||||
|
# 尝试从缓存中获取删除前的数据
|
||||||
|
cached_data = self._delete_data_cache.get(f"item_{item_id}")
|
||||||
|
if cached_data:
|
||||||
|
notification_data = {
|
||||||
|
'id': cached_data.get('id', item_id),
|
||||||
|
'name': f"[已删除] {cached_data.get('name', '未知物品')}",
|
||||||
|
'serial_number': cached_data.get('serial_number', ''),
|
||||||
|
'status': f"[删除前状态: {cached_data.get('status', '未知')}]",
|
||||||
|
'category': cached_data.get('category', ''),
|
||||||
|
'owner': cached_data.get('owner', ''),
|
||||||
|
'purchase_date': cached_data.get('purchase_date', ''),
|
||||||
|
'purchase_price': cached_data.get('purchase_price', ''),
|
||||||
|
'description': f"删除前描述: {cached_data.get('description', '')}",
|
||||||
|
'timestamp': cached_data.get('updated_at', ''),
|
||||||
|
'operation_path': request.path,
|
||||||
|
'operation_method': request.method
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
# 尝试从响应中获取数据
|
||||||
|
if hasattr(response, 'data'):
|
||||||
|
item_data = response.data
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
content = response.content.decode('utf-8')
|
||||||
|
item_data = json.loads(content) if content else {}
|
||||||
|
except:
|
||||||
|
item_data = {}
|
||||||
|
|
||||||
|
# 确保item_data不为None
|
||||||
|
if item_data is None:
|
||||||
|
item_data = {}
|
||||||
|
|
||||||
|
# 构建通知数据
|
||||||
|
notification_data = {
|
||||||
|
'id': item_data.get('id', ''),
|
||||||
|
'name': item_data.get('name', ''),
|
||||||
|
'serial_number': item_data.get('serial_number', ''),
|
||||||
|
'status': item_data.get('status', ''),
|
||||||
|
'category': item_data.get('category', ''),
|
||||||
|
'owner': item_data.get('owner', ''),
|
||||||
|
'timestamp': item_data.get('updated_at', ''),
|
||||||
|
'operation_path': request.path,
|
||||||
|
'operation_method': request.method
|
||||||
|
}
|
||||||
|
|
||||||
|
EmailNotificationService.send_operation_notification(
|
||||||
|
operation_type, '物品', notification_data, user_info
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"异步处理物品操作通知失败: {e} 如果需要查看删除前的数据,请检查以前的邮件。")
|
||||||
|
|
||||||
|
# 创建并启动后台线程
|
||||||
|
thread = threading.Thread(target=send_notification)
|
||||||
|
thread.daemon = True # 设置为守护线程
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
def _handle_finance_operation_async(self, request, response, user_info):
|
||||||
|
"""异步处理财务操作"""
|
||||||
|
def send_notification():
|
||||||
|
try:
|
||||||
|
operation_type = self._get_operation_type(request.method, request.path)
|
||||||
|
|
||||||
|
# 对于DELETE操作,响应通常不包含数据,需要从URL路径中提取ID
|
||||||
|
if request.method == 'DELETE':
|
||||||
|
# 从URL路径中提取ID
|
||||||
|
path_parts = request.path.split('/')
|
||||||
|
record_id = None
|
||||||
|
for part in path_parts:
|
||||||
|
if part.isdigit():
|
||||||
|
record_id = part
|
||||||
|
break
|
||||||
|
|
||||||
|
notification_data = {
|
||||||
|
'id': record_id or '未知',
|
||||||
|
'amount': '已删除记录',
|
||||||
|
'transaction_type': '已删除',
|
||||||
|
'description': '财务记录已被删除',
|
||||||
|
'department': '未知',
|
||||||
|
'category': '未知',
|
||||||
|
'approver': '未知',
|
||||||
|
'timestamp': '删除时间未知',
|
||||||
|
'operation_path': request.path,
|
||||||
|
'operation_method': request.method
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
# 尝试从响应中获取数据
|
||||||
|
if hasattr(response, 'data'):
|
||||||
|
finance_data = response.data
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
content = response.content.decode('utf-8')
|
||||||
|
finance_data = json.loads(content) if content else {}
|
||||||
|
except:
|
||||||
|
finance_data = {}
|
||||||
|
|
||||||
|
# 确保finance_data不为None
|
||||||
|
if finance_data is None:
|
||||||
|
finance_data = {}
|
||||||
|
|
||||||
|
# 构建通知数据
|
||||||
|
notification_data = {
|
||||||
|
'id': finance_data.get('id', ''),
|
||||||
|
'amount': finance_data.get('amount', ''),
|
||||||
|
'transaction_type': finance_data.get('transaction_type', ''),
|
||||||
|
'description': finance_data.get('description', ''),
|
||||||
|
'department': finance_data.get('department', ''),
|
||||||
|
'category': finance_data.get('category', ''),
|
||||||
|
'approver': finance_data.get('approver', ''),
|
||||||
|
'timestamp': finance_data.get('transaction_date', ''),
|
||||||
|
'operation_path': request.path,
|
||||||
|
'operation_method': request.method
|
||||||
|
}
|
||||||
|
|
||||||
|
EmailNotificationService.send_operation_notification(
|
||||||
|
operation_type, '财务记录', notification_data, user_info
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"异步处理财务操作通知失败: {e} 如果需要查看删除前的数据,请检查以前的邮件。")
|
||||||
|
|
||||||
|
# 创建并启动后台线程
|
||||||
|
thread = threading.Thread(target=send_notification)
|
||||||
|
thread.daemon = True # 设置为守护线程
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
def _get_operation_type(self, method, path):
|
||||||
|
"""根据HTTP方法和路径判断操作类型"""
|
||||||
|
if method == 'POST':
|
||||||
|
return 'CREATE'
|
||||||
|
elif method in ['PUT', 'PATCH']:
|
||||||
|
return 'UPDATE'
|
||||||
|
elif method == 'DELETE':
|
||||||
|
return 'DELETE'
|
||||||
|
else:
|
||||||
|
return 'UNKNOWN'
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
from django.db import models
|
||||||
|
from django.core.validators import EmailValidator
|
||||||
|
|
||||||
|
class NotificationEmail(models.Model):
|
||||||
|
"""通知邮箱模型"""
|
||||||
|
email = models.EmailField(
|
||||||
|
unique=True,
|
||||||
|
validators=[EmailValidator()],
|
||||||
|
verbose_name='邮箱地址'
|
||||||
|
)
|
||||||
|
is_enabled = models.BooleanField(
|
||||||
|
default=True,
|
||||||
|
verbose_name='是否启用'
|
||||||
|
)
|
||||||
|
description = models.CharField(
|
||||||
|
max_length=200,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
verbose_name='描述'
|
||||||
|
)
|
||||||
|
created_at = models.DateTimeField(
|
||||||
|
auto_now_add=True,
|
||||||
|
verbose_name='创建时间'
|
||||||
|
)
|
||||||
|
updated_at = models.DateTimeField(
|
||||||
|
auto_now=True,
|
||||||
|
verbose_name='更新时间'
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = '通知邮箱'
|
||||||
|
verbose_name_plural = '通知邮箱'
|
||||||
|
ordering = ['-created_at']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
status = "启用" if self.is_enabled else "禁用"
|
||||||
|
return f"{self.email} ({status})"
|
||||||
|
|
||||||
|
class NotificationSettings(models.Model):
|
||||||
|
"""通知设置模型"""
|
||||||
|
email_notification_enabled = models.BooleanField(
|
||||||
|
default=True,
|
||||||
|
verbose_name='邮件通知总开关'
|
||||||
|
)
|
||||||
|
created_at = models.DateTimeField(
|
||||||
|
auto_now_add=True,
|
||||||
|
verbose_name='创建时间'
|
||||||
|
)
|
||||||
|
updated_at = models.DateTimeField(
|
||||||
|
auto_now=True,
|
||||||
|
verbose_name='更新时间'
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = '通知设置'
|
||||||
|
verbose_name_plural = '通知设置'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
status = "开启" if self.email_notification_enabled else "关闭"
|
||||||
|
return f"邮件通知: {status}"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_settings(cls):
|
||||||
|
"""获取通知设置(单例模式)"""
|
||||||
|
settings, created = cls.objects.get_or_create(pk=1)
|
||||||
|
return settings
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
from django.core.mail import send_mail
|
||||||
|
from django.conf import settings
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class EmailNotificationService:
|
||||||
|
"""邮件通知服务"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_notification_settings():
|
||||||
|
"""获取通知设置"""
|
||||||
|
try:
|
||||||
|
from .models import NotificationEmail, NotificationSettings
|
||||||
|
|
||||||
|
# 获取全局设置
|
||||||
|
global_settings = NotificationSettings.get_settings()
|
||||||
|
|
||||||
|
# 获取启用的邮箱列表
|
||||||
|
enabled_emails = NotificationEmail.objects.filter(is_enabled=True).values_list('email', flat=True)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'email_enabled': global_settings.email_notification_enabled,
|
||||||
|
'notification_emails': list(enabled_emails)
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取通知设置失败: {e}")
|
||||||
|
return {'email_enabled': False, 'notification_emails': []}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def send_operation_notification(operation_type, model_name, instance_data, user_info=None):
|
||||||
|
"""
|
||||||
|
发送操作通知邮件到多个邮箱
|
||||||
|
|
||||||
|
Args:
|
||||||
|
operation_type: 操作类型 ('CREATE', 'UPDATE', 'DELETE')
|
||||||
|
model_name: 模型名称 (如 'Item', 'FinanceRecord')
|
||||||
|
instance_data: 实例数据
|
||||||
|
user_info: 操作用户信息
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
settings_data = EmailNotificationService.get_notification_settings()
|
||||||
|
|
||||||
|
if not settings_data.get('email_enabled') or not settings_data.get('notification_emails'):
|
||||||
|
return
|
||||||
|
|
||||||
|
notification_emails = settings_data.get('notification_emails', [])
|
||||||
|
if not notification_emails:
|
||||||
|
logger.warning("没有配置启用的通知邮箱")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 构建邮件内容
|
||||||
|
subject = f"[爱特工作室管理系统] {model_name}数据变更通知"
|
||||||
|
|
||||||
|
operation_map = {
|
||||||
|
'CREATE': '创建',
|
||||||
|
'UPDATE': '更新',
|
||||||
|
'DELETE': '删除'
|
||||||
|
}
|
||||||
|
|
||||||
|
operation_text = operation_map.get(operation_type, operation_type)
|
||||||
|
|
||||||
|
message = f"""
|
||||||
|
系统数据变更通知
|
||||||
|
|
||||||
|
操作类型: {operation_text}
|
||||||
|
数据类型: {model_name}
|
||||||
|
操作时间: {instance_data.get('timestamp', '未知')}
|
||||||
|
操作用户: {user_info or '系统'}
|
||||||
|
|
||||||
|
变更详情:
|
||||||
|
{json.dumps(instance_data, ensure_ascii=False, indent=2)}
|
||||||
|
|
||||||
|
---
|
||||||
|
此邮件由爱特工作室物品管理及财务管理系统自动发送
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 发送邮件到所有启用的通知邮箱
|
||||||
|
for email in notification_emails:
|
||||||
|
if email.strip(): # 确保邮箱不为空
|
||||||
|
try:
|
||||||
|
send_mail(
|
||||||
|
subject=subject,
|
||||||
|
message=message,
|
||||||
|
from_email=settings.EMAIL_HOST_USER,
|
||||||
|
recipient_list=[email.strip()],
|
||||||
|
fail_silently=False,
|
||||||
|
)
|
||||||
|
logger.info(f"邮件通知发送成功到 {email}: {operation_type} {model_name}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"发送邮件到 {email} 失败: {e}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"发送邮件通知失败: {e}")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def send_item_operation_notification(operation_type, item_instance, user_info=None):
|
||||||
|
"""发送物品操作通知"""
|
||||||
|
try:
|
||||||
|
instance_data = {
|
||||||
|
'id': getattr(item_instance, 'id', None),
|
||||||
|
'name': getattr(item_instance, 'name', ''),
|
||||||
|
'serial_number': getattr(item_instance, 'serial_number', ''),
|
||||||
|
'status': getattr(item_instance, 'status', ''),
|
||||||
|
'category': getattr(item_instance, 'category', ''),
|
||||||
|
'owner': getattr(item_instance, 'owner', ''),
|
||||||
|
'timestamp': str(getattr(item_instance, 'updated_at', '')),
|
||||||
|
}
|
||||||
|
|
||||||
|
EmailNotificationService.send_operation_notification(
|
||||||
|
operation_type, '物品', instance_data, user_info
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"发送物品操作通知失败: {e}")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def send_finance_operation_notification(operation_type, finance_instance, user_info=None):
|
||||||
|
"""发送财务记录操作通知"""
|
||||||
|
try:
|
||||||
|
instance_data = {
|
||||||
|
'id': getattr(finance_instance, 'id', None),
|
||||||
|
'amount': str(getattr(finance_instance, 'amount', '')),
|
||||||
|
'transaction_type': getattr(finance_instance, 'transaction_type', ''),
|
||||||
|
'description': getattr(finance_instance, 'description', ''),
|
||||||
|
'department': str(getattr(finance_instance, 'department', '')),
|
||||||
|
'category': str(getattr(finance_instance, 'category', '')),
|
||||||
|
'approver': getattr(finance_instance, 'approver', ''),
|
||||||
|
'timestamp': str(getattr(finance_instance, 'transaction_date', '')),
|
||||||
|
}
|
||||||
|
|
||||||
|
EmailNotificationService.send_operation_notification(
|
||||||
|
operation_type, '财务记录', instance_data, user_info
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"发送财务记录操作通知失败: {e}")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def update_notification_emails(emails_data):
|
||||||
|
"""
|
||||||
|
更新通知邮箱列表(使用数据库存储)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
emails_data: 邮箱数据列表,格式:[{'email': 'xxx@xxx.com', 'is_enabled': True, 'description': '描述'}]
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from .models import NotificationEmail
|
||||||
|
|
||||||
|
# 清空现有邮箱
|
||||||
|
NotificationEmail.objects.all().delete()
|
||||||
|
|
||||||
|
# 添加新的邮箱
|
||||||
|
for email_info in emails_data:
|
||||||
|
if isinstance(email_info, str):
|
||||||
|
# 兼容旧格式(纯邮箱字符串)
|
||||||
|
NotificationEmail.objects.create(
|
||||||
|
email=email_info.strip(),
|
||||||
|
is_enabled=True
|
||||||
|
)
|
||||||
|
elif isinstance(email_info, dict):
|
||||||
|
# 新格式(包含详细信息)
|
||||||
|
NotificationEmail.objects.create(
|
||||||
|
email=email_info.get('email', '').strip(),
|
||||||
|
is_enabled=email_info.get('is_enabled', True),
|
||||||
|
description=email_info.get('description', '')
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"通知邮箱配置已更新: {len(emails_data)} 个邮箱")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"更新通知邮箱配置失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def update_global_notification_setting(enabled):
|
||||||
|
"""
|
||||||
|
更新全局邮件通知开关
|
||||||
|
|
||||||
|
Args:
|
||||||
|
enabled: 是否启用邮件通知
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from .models import NotificationSettings
|
||||||
|
|
||||||
|
settings = NotificationSettings.get_settings()
|
||||||
|
settings.email_notification_enabled = enabled
|
||||||
|
settings.save()
|
||||||
|
|
||||||
|
logger.info(f"全局邮件通知设置已更新: {enabled}")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"更新全局邮件通知设置失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_all_notification_emails():
|
||||||
|
"""获取所有通知邮箱(包括禁用的)"""
|
||||||
|
try:
|
||||||
|
from .models import NotificationEmail
|
||||||
|
|
||||||
|
emails = NotificationEmail.objects.all().values(
|
||||||
|
'id', 'email', 'is_enabled', 'description', 'created_at', 'updated_at'
|
||||||
|
)
|
||||||
|
return list(emails)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取通知邮箱列表失败: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def toggle_email_status(email_id, is_enabled):
|
||||||
|
"""
|
||||||
|
切换指定邮箱的启用状态
|
||||||
|
|
||||||
|
Args:
|
||||||
|
email_id: 邮箱ID
|
||||||
|
is_enabled: 是否启用
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from .models import NotificationEmail
|
||||||
|
|
||||||
|
email_obj = NotificationEmail.objects.get(id=email_id)
|
||||||
|
email_obj.is_enabled = is_enabled
|
||||||
|
email_obj.save()
|
||||||
|
|
||||||
|
logger.info(f"邮箱 {email_obj.email} 状态已更新: {is_enabled}")
|
||||||
|
return True
|
||||||
|
except NotificationEmail.DoesNotExist:
|
||||||
|
logger.error(f"邮箱ID {email_id} 不存在")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"更新邮箱状态失败: {e}")
|
||||||
|
return False
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
from django.core import mail
|
||||||
|
from .models import NotificationEmail, NotificationSettings
|
||||||
|
from .services import EmailNotificationService
|
||||||
|
|
||||||
|
class EmailNotificationTestCase(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
"""设置测试数据"""
|
||||||
|
self.email1 = NotificationEmail.objects.create(
|
||||||
|
email="test1@example.com",
|
||||||
|
is_enabled=True,
|
||||||
|
description="测试邮箱1"
|
||||||
|
)
|
||||||
|
self.email2 = NotificationEmail.objects.create(
|
||||||
|
email="test2@example.com",
|
||||||
|
is_enabled=False,
|
||||||
|
description="测试邮箱2"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_get_notification_settings(self):
|
||||||
|
"""测试获取通知设置"""
|
||||||
|
settings = EmailNotificationService.get_notification_settings()
|
||||||
|
self.assertTrue(settings['email_enabled'])
|
||||||
|
self.assertIn("test1@example.com", settings['notification_emails'])
|
||||||
|
self.assertNotIn("test2@example.com", settings['notification_emails'])
|
||||||
|
|
||||||
|
def test_toggle_email_status(self):
|
||||||
|
"""测试切换邮箱状态"""
|
||||||
|
result = EmailNotificationService.toggle_email_status(self.email2.id, True)
|
||||||
|
self.assertTrue(result)
|
||||||
|
|
||||||
|
self.email2.refresh_from_db()
|
||||||
|
self.assertTrue(self.email2.is_enabled)
|
||||||
|
|
||||||
|
def test_update_notification_emails(self):
|
||||||
|
"""测试更新通知邮箱"""
|
||||||
|
emails_data = [
|
||||||
|
{'email': 'new1@example.com', 'is_enabled': True, 'description': '新邮箱1'},
|
||||||
|
{'email': 'new2@example.com', 'is_enabled': False, 'description': '新邮箱2'}
|
||||||
|
]
|
||||||
|
|
||||||
|
result = EmailNotificationService.update_notification_emails(emails_data)
|
||||||
|
self.assertTrue(result)
|
||||||
|
|
||||||
|
# 检查数据库中的邮箱数量
|
||||||
|
self.assertEqual(NotificationEmail.objects.count(), 2)
|
||||||
|
self.assertTrue(NotificationEmail.objects.filter(email='new1@example.com').exists())
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
app_name = 'email_notice'
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('api/notification-settings/', views.notification_settings, name='notification_settings'),
|
||||||
|
path('api/toggle-email-status/', views.toggle_email_status, name='toggle_email_status'),
|
||||||
|
]
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
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
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
@require_http_methods(["POST", "GET"])
|
||||||
|
def notification_settings(request):
|
||||||
|
"""通知设置API"""
|
||||||
|
|
||||||
|
if request.method == 'GET':
|
||||||
|
try:
|
||||||
|
settings_data = EmailNotificationService.get_notification_settings()
|
||||||
|
all_emails = EmailNotificationService.get_all_notification_emails()
|
||||||
|
return JsonResponse({
|
||||||
|
'success': True,
|
||||||
|
'data': {
|
||||||
|
'email_enabled': settings_data.get('email_enabled', False),
|
||||||
|
'notification_emails': settings_data.get('notification_emails', []),
|
||||||
|
'all_emails': all_emails
|
||||||
|
}
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
return JsonResponse({'success': False, 'error': str(e)}, status=500)
|
||||||
|
|
||||||
|
elif request.method == 'POST':
|
||||||
|
try:
|
||||||
|
data = json.loads(request.body)
|
||||||
|
emails_to_update = data.get('notification_emails')
|
||||||
|
email_enabled_status = data.get('email_enabled')
|
||||||
|
|
||||||
|
# 标志位,用于判断是否执行了任何更新操作
|
||||||
|
was_updated = False
|
||||||
|
|
||||||
|
# 1. 如果请求中包含 'notification_emails' 键,则处理邮箱列表
|
||||||
|
if emails_to_update is not None: # 允许 emails_to_update 为空列表 []
|
||||||
|
email_regex = re.compile(r'^[^\s@]+@[^\s@]+\.[^\s@]+$')
|
||||||
|
valid_emails_data = []
|
||||||
|
|
||||||
|
for email_info in emails_to_update:
|
||||||
|
# 兼容前端可能发送的字符串或对象格式
|
||||||
|
if isinstance(email_info, str):
|
||||||
|
if email_info.strip() and email_regex.match(email_info.strip()):
|
||||||
|
valid_emails_data.append({
|
||||||
|
'email': email_info.strip(),
|
||||||
|
'is_enabled': True,
|
||||||
|
'description': ''
|
||||||
|
})
|
||||||
|
elif isinstance(email_info, dict):
|
||||||
|
email = email_info.get('email', '').strip()
|
||||||
|
if email and email_regex.match(email):
|
||||||
|
valid_emails_data.append({
|
||||||
|
'email': email,
|
||||||
|
'is_enabled': email_info.get('is_enabled', True),
|
||||||
|
'description': email_info.get('description', '')
|
||||||
|
})
|
||||||
|
|
||||||
|
# 调用服务层更新邮箱,服务层需要能处理空列表
|
||||||
|
EmailNotificationService.update_notification_emails(valid_emails_data)
|
||||||
|
was_updated = True
|
||||||
|
|
||||||
|
if email_enabled_status is not None:
|
||||||
|
EmailNotificationService.update_global_notification_setting(email_enabled_status)
|
||||||
|
was_updated = True
|
||||||
|
|
||||||
|
# 执行更新成功,返回成功
|
||||||
|
if was_updated:
|
||||||
|
return JsonResponse({
|
||||||
|
'success': True,
|
||||||
|
'message': '通知设置已更新',
|
||||||
|
# 返回最新的数据状态
|
||||||
|
'data': {
|
||||||
|
'email_enabled': EmailNotificationService.get_notification_settings().get('email_enabled', False),
|
||||||
|
'all_emails': EmailNotificationService.get_all_notification_emails()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
# 如果请求体为空或不包含任何有效键,则返回错误
|
||||||
|
return JsonResponse({'success': False, 'error': '未提供任何有效的更新数据'}, status=400)
|
||||||
|
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return JsonResponse({'success': False, 'error': '无效的JSON数据'}, status=400)
|
||||||
|
except Exception as e:
|
||||||
|
return JsonResponse({'success': False, 'error': str(e)}, status=500)
|
||||||
|
|
||||||
|
return JsonResponse({'success': False, 'error': '不支持的请求方法'}, status=405)
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
@require_http_methods(["POST"])
|
||||||
|
def toggle_email_status(request):
|
||||||
|
"""切换邮箱启用状态API"""
|
||||||
|
try:
|
||||||
|
data = json.loads(request.body)
|
||||||
|
email_id = data.get('email_id')
|
||||||
|
is_enabled = data.get('is_enabled', True)
|
||||||
|
|
||||||
|
if not email_id:
|
||||||
|
return JsonResponse({'success': False, 'error': '缺少邮箱ID'}, status=400)
|
||||||
|
|
||||||
|
success = EmailNotificationService.toggle_email_status(email_id, is_enabled)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
return JsonResponse({
|
||||||
|
'success': True,
|
||||||
|
'message': f'邮箱状态已更新为{"启用" if is_enabled else "禁用"}',
|
||||||
|
'data': {
|
||||||
|
'all_emails': EmailNotificationService.get_all_notification_emails()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
return JsonResponse({'success': False, 'error': '更新邮箱状态失败'}, status=500)
|
||||||
|
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return JsonResponse({'success': False, 'error': '无效的JSON数据'}, status=400)
|
||||||
|
except Exception as e:
|
||||||
|
return JsonResponse({'success': False, 'error': str(e)}, status=500)
|
||||||
@@ -46,6 +46,7 @@ INSTALLED_APPS = [
|
|||||||
"corsheaders",
|
"corsheaders",
|
||||||
"items",
|
"items",
|
||||||
"finance",
|
"finance",
|
||||||
|
"email_notice",
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
@@ -57,6 +58,7 @@ MIDDLEWARE = [
|
|||||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||||
"django.contrib.messages.middleware.MessageMiddleware",
|
"django.contrib.messages.middleware.MessageMiddleware",
|
||||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||||
|
"email_notice.middleware.EmailNotificationMiddleware",
|
||||||
]
|
]
|
||||||
|
|
||||||
ROOT_URLCONF = "item_manager.urls"
|
ROOT_URLCONF = "item_manager.urls"
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ urlpatterns = [
|
|||||||
path("admin/", admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
path("", include("items.urls")),
|
path("", include("items.urls")),
|
||||||
path("", include("finance.urls")),
|
path("", include("finance.urls")),
|
||||||
|
path("", include("email_notice.urls")),
|
||||||
path("api-auth/", include("rest_framework.urls")),
|
path("api-auth/", include("rest_framework.urls")),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -29,12 +29,21 @@
|
|||||||
财务记录
|
财务记录
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
</el-menu>
|
</el-menu>
|
||||||
|
<div class="header-actions">
|
||||||
|
<el-button
|
||||||
|
type="text"
|
||||||
|
@click="goToSettings"
|
||||||
|
class="settings-btn"
|
||||||
|
>
|
||||||
|
<el-icon size="20"><Setting /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-header>
|
</el-header>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { House, Box, Document, Money, Tickets } from '@element-plus/icons-vue'
|
import { House, Box, Document, Money, Tickets, Setting } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'AppHeader',
|
name: 'AppHeader',
|
||||||
@@ -43,7 +52,13 @@ export default {
|
|||||||
Box,
|
Box,
|
||||||
Document,
|
Document,
|
||||||
Money,
|
Money,
|
||||||
Tickets
|
Tickets,
|
||||||
|
Setting
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
goToSettings() {
|
||||||
|
this.$router.push('/settings')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -94,4 +109,20 @@ export default {
|
|||||||
height: 60px;
|
height: 60px;
|
||||||
line-height: 60px;
|
line-height: 60px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-btn {
|
||||||
|
color: #606266;
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-btn:hover {
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import Dashboard from '../views/Dashboard.vue'
|
|||||||
import FinanceDashboard from '../views/FinanceDashboard.vue'
|
import FinanceDashboard from '../views/FinanceDashboard.vue'
|
||||||
import FinanceRecordList from '../views/FinanceRecordList.vue'
|
import FinanceRecordList from '../views/FinanceRecordList.vue'
|
||||||
import FinanceRecordDetail from '../views/FinanceRecordDetail.vue'
|
import FinanceRecordDetail from '../views/FinanceRecordDetail.vue'
|
||||||
|
import Settings from '../views/Settings.vue'
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
@@ -50,6 +51,11 @@ const routes = [
|
|||||||
name: 'FinanceRecordDetail',
|
name: 'FinanceRecordDetail',
|
||||||
component: FinanceRecordDetail,
|
component: FinanceRecordDetail,
|
||||||
props: true
|
props: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/settings',
|
||||||
|
name: 'Settings',
|
||||||
|
component: Settings
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -141,7 +141,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { itemService } from '../services/api'
|
import { itemService } from '@/services/api'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import AppHeader from '../components/AppHeader.vue'
|
import AppHeader from '../components/AppHeader.vue'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
|
|||||||
@@ -0,0 +1,753 @@
|
|||||||
|
<template>
|
||||||
|
<div class="settings-page">
|
||||||
|
<AppHeader />
|
||||||
|
<div class="settings-container">
|
||||||
|
<div class="settings-header">
|
||||||
|
<h2>网站设置</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-content">
|
||||||
|
<!-- 主题切换 -->
|
||||||
|
<el-card class="setting-card" shadow="never">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<el-icon><Sunny /></el-icon>
|
||||||
|
<span>主题设置</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="setting-item">
|
||||||
|
<div class="setting-label">
|
||||||
|
<span>网站主题</span>
|
||||||
|
<p class="setting-desc">切换白天/黑夜模式</p>
|
||||||
|
</div>
|
||||||
|
<div class="setting-control">
|
||||||
|
<el-switch
|
||||||
|
v-model="isDarkMode"
|
||||||
|
@change="toggleTheme"
|
||||||
|
active-text="黑夜模式"
|
||||||
|
inactive-text="白天模式"
|
||||||
|
active-color="#409eff"
|
||||||
|
inactive-color="#dcdfe6"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 邮箱通知设置 -->
|
||||||
|
<el-card class="setting-card" shadow="never">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<el-icon><Message /></el-icon>
|
||||||
|
<span>通知设置</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 全局邮件通知开关 -->
|
||||||
|
<div class="setting-item">
|
||||||
|
<div class="setting-label">
|
||||||
|
<span>邮件通知</span>
|
||||||
|
<p class="setting-desc">开启后,所有数据修改操作将发送邮件通知</p>
|
||||||
|
</div>
|
||||||
|
<div class="setting-control">
|
||||||
|
<el-switch
|
||||||
|
v-model="emailNotificationEnabled"
|
||||||
|
@change="saveEmailNotificationSetting"
|
||||||
|
active-text="开启"
|
||||||
|
inactive-text="关闭"
|
||||||
|
active-color="#67c23a"
|
||||||
|
inactive-color="#dcdfe6"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 邮箱管理 -->
|
||||||
|
<div class="setting-item" style="flex-direction: column; align-items: flex-start;">
|
||||||
|
<div class="setting-label" style="margin-bottom: 15px;">
|
||||||
|
<span>通知邮箱管理</span>
|
||||||
|
<p class="setting-desc">管理接收通知的邮箱列表,可以单独启用/禁用每个邮箱</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 现有邮箱列表 -->
|
||||||
|
<div v-if="allEmails.length > 0" class="email-management-section">
|
||||||
|
<h4>现有邮箱</h4>
|
||||||
|
<div class="existing-emails">
|
||||||
|
<div
|
||||||
|
v-for="emailItem in allEmails"
|
||||||
|
:key="emailItem.id"
|
||||||
|
class="email-management-item"
|
||||||
|
>
|
||||||
|
<div class="email-info">
|
||||||
|
<span class="email-address">{{ emailItem.email }}</span>
|
||||||
|
<span v-if="emailItem.description" class="email-description">{{ emailItem.description }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="email-actions">
|
||||||
|
<el-switch
|
||||||
|
:model-value="emailItem.is_enabled"
|
||||||
|
@change="(value) => toggleEmailStatus(emailItem.id, value)"
|
||||||
|
size="small"
|
||||||
|
active-color="#67c23a"
|
||||||
|
inactive-color="#f56c6c"
|
||||||
|
/>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
:icon="Delete"
|
||||||
|
circle
|
||||||
|
size="small"
|
||||||
|
@click="removeExistingEmail(emailItem.id)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 添加新邮箱 -->
|
||||||
|
<div class="email-management-section">
|
||||||
|
<h4>添加新邮箱</h4>
|
||||||
|
<div class="new-email-form">
|
||||||
|
<div class="form-row">
|
||||||
|
<el-input
|
||||||
|
v-model="newEmail.email"
|
||||||
|
placeholder="请输入邮箱地址"
|
||||||
|
style="flex: 1; margin-right: 10px;"
|
||||||
|
/>
|
||||||
|
<el-input
|
||||||
|
v-model="newEmail.description"
|
||||||
|
placeholder="描述(可选)"
|
||||||
|
style="flex: 1; margin-right: 10px;"
|
||||||
|
/>
|
||||||
|
<el-switch
|
||||||
|
v-model="newEmail.is_enabled"
|
||||||
|
active-text="启用"
|
||||||
|
inactive-text="禁用"
|
||||||
|
size="small"
|
||||||
|
style="margin-right: 10px;"
|
||||||
|
/>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:icon="Plus"
|
||||||
|
@click="addNewEmail"
|
||||||
|
>
|
||||||
|
添加
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 网站说明 -->
|
||||||
|
<el-card class="setting-card" shadow="never">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<el-icon><InfoFilled /></el-icon>
|
||||||
|
<span>网站说明</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="content-section">
|
||||||
|
<h3>系统介绍</h3>
|
||||||
|
<p>爱特工作室物品管理及财务管理系统是为爱特工作室量身定制的综合管理平台,主要功能包括:</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>物品管理:</strong>管理工作室的各类物品,包括电子设备、办公用品等</li>
|
||||||
|
<li><strong>借用记录:</strong>跟踪物品的借用情况,确保物品流转透明化</li>
|
||||||
|
<li><strong>财务管理:</strong>记录工作室的收支情况,支持多部门资金管理</li>
|
||||||
|
<li><strong>数据统计:</strong>提供直观的数据可视化界面</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>使用说明</h3>
|
||||||
|
<ol>
|
||||||
|
<li>物品管理:可以添加、编辑、删除物品信息,查看物品状态</li>
|
||||||
|
<li>借用流程:选择物品 → 填写借用信息 → 确认借用 → 归还确认</li>
|
||||||
|
<li>财务记录:添加收支记录,支持上传凭证图片</li>
|
||||||
|
<li>权限管理:不同用户具有不同的操作权限</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h3>免责声明</h3>
|
||||||
|
<div class="disclaimer">
|
||||||
|
<p><strong>重要提醒:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>本系统仅供爱特工作室内部使用,请勿泄露系统访问信息</li>
|
||||||
|
<li>用户需对自己的操作行为负责,误操作造成的数据丢失由操作者承担责任</li>
|
||||||
|
<li>系统会记录所有用户操作日志,请规范使用</li>
|
||||||
|
<li>如发现系统异常或安全问题,请及时联系管理员</li>
|
||||||
|
<li>系统数据会定期备份,但建议重要数据另行保存</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>联系方式</h3>
|
||||||
|
<p>如有问题或建议,请联系系统管理员:</p>
|
||||||
|
<ul>
|
||||||
|
<li>邮箱:admin@itstudio.com</li>
|
||||||
|
<li>QQ群:123456789</li>
|
||||||
|
<li>技术支持:Web部、站长</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>版本信息</h3>
|
||||||
|
<p>
|
||||||
|
当前版本:v1.0.0<br>
|
||||||
|
更新时间:2024年9月<br>
|
||||||
|
开发团队:爱特工作室
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { Sunny, Message, InfoFilled, Plus, Delete } from '@element-plus/icons-vue'
|
||||||
|
import AppHeader from '@/components/AppHeader.vue'
|
||||||
|
import { API_BASE_URL_WITHOUT_API } from '@/services/api'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Settings',
|
||||||
|
components: {
|
||||||
|
AppHeader,
|
||||||
|
Sunny,
|
||||||
|
Message,
|
||||||
|
InfoFilled,
|
||||||
|
Plus,
|
||||||
|
Delete
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
const isDarkMode = ref(false)
|
||||||
|
// notificationEmails 不再需要,因为我们现在使用 allEmails 来管理所有邮箱
|
||||||
|
const emailNotificationEnabled = ref(false)
|
||||||
|
const allEmails = ref([])
|
||||||
|
const newEmail = ref({
|
||||||
|
email: '',
|
||||||
|
description: '',
|
||||||
|
is_enabled: true
|
||||||
|
})
|
||||||
|
|
||||||
|
// 加载设置
|
||||||
|
const loadSettings = () => {
|
||||||
|
// 从localStorage加载主题设置
|
||||||
|
const savedTheme = localStorage.getItem('theme')
|
||||||
|
isDarkMode.value = savedTheme === 'dark'
|
||||||
|
applyTheme()
|
||||||
|
|
||||||
|
// 从localStorage加载邮件通知设置
|
||||||
|
const emailEnabled = localStorage.getItem('emailNotificationEnabled')
|
||||||
|
emailNotificationEnabled.value = emailEnabled === 'true'
|
||||||
|
|
||||||
|
// 从后端加载最新设置
|
||||||
|
loadSettingsFromServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从服务器加载设置
|
||||||
|
const loadSettingsFromServer = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL_WITHOUT_API}/api/notification-settings/`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const result = await response.json()
|
||||||
|
if (result.success && result.data) {
|
||||||
|
if (result.data.email_enabled !== undefined) {
|
||||||
|
emailNotificationEnabled.value = result.data.email_enabled
|
||||||
|
localStorage.setItem('emailNotificationEnabled', result.data.email_enabled.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载所有邮箱设置
|
||||||
|
if (result.data.all_emails && Array.isArray(result.data.all_emails)) {
|
||||||
|
allEmails.value = result.data.all_emails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载服务器设置失败:', error)
|
||||||
|
ElMessage.error('从服务器加载设置失败,请刷新页面重试')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用主题
|
||||||
|
const applyTheme = () => {
|
||||||
|
const htmlElement = document.documentElement
|
||||||
|
if (isDarkMode.value) {
|
||||||
|
htmlElement.classList.add('dark')
|
||||||
|
htmlElement.style.setProperty('--el-bg-color', '#1a1a1a')
|
||||||
|
htmlElement.style.setProperty('--el-text-color-primary', '#e5e7eb')
|
||||||
|
htmlElement.style.setProperty('--el-text-color-regular', '#d1d5db')
|
||||||
|
} else {
|
||||||
|
htmlElement.classList.remove('dark')
|
||||||
|
htmlElement.style.removeProperty('--el-bg-color')
|
||||||
|
htmlElement.style.removeProperty('--el-text-color-primary')
|
||||||
|
htmlElement.style.removeProperty('--el-text-color-regular')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换主题
|
||||||
|
const toggleTheme = () => {
|
||||||
|
localStorage.setItem('theme', isDarkMode.value ? 'dark' : 'light')
|
||||||
|
applyTheme()
|
||||||
|
ElMessage.success(`已切换到${isDarkMode.value ? '黑夜' : '白天'}模式`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存邮件通知设置 (这个函数现在只用来切换总开关)
|
||||||
|
const saveEmailNotificationSetting = async () => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem('emailNotificationEnabled', emailNotificationEnabled.value.toString())
|
||||||
|
|
||||||
|
// 后端接口期望收到完整的邮箱列表,所以我们把现有列表一起发过去
|
||||||
|
const emailsToUpdate = allEmails.value.map(item => ({
|
||||||
|
email: item.email,
|
||||||
|
description: item.description,
|
||||||
|
is_enabled: item.is_enabled
|
||||||
|
}))
|
||||||
|
|
||||||
|
const response = await fetch(`${API_BASE_URL_WITHOUT_API}/api/notification-settings/`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
notification_emails: emailsToUpdate,
|
||||||
|
email_enabled: emailNotificationEnabled.value
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const result = await response.json()
|
||||||
|
if (result.success) {
|
||||||
|
ElMessage.success(`邮件通知已${emailNotificationEnabled.value ? '开启' : '关闭'}并同步到服务器`)
|
||||||
|
// 重新从服务器加载,确保状态一致
|
||||||
|
loadSettingsFromServer()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(`保存失败: ${result.error}`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('服务器响应错误')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存通知设置失败:', error)
|
||||||
|
ElMessage.warning('设置已保存到本地,但同步到服务器失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// [已修改] 切换单个邮箱启用状态
|
||||||
|
const toggleEmailStatus = async (emailId, isEnabled) => {
|
||||||
|
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/`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
email_id: emailId,
|
||||||
|
is_enabled: isEnabled
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const result = await response.json()
|
||||||
|
if (result.success) {
|
||||||
|
ElMessage.success(`邮箱已${isEnabled ? '启用' : '禁用'}`)
|
||||||
|
// 操作成功后,从服务器重新加载所有邮箱,以确保数据同步
|
||||||
|
loadSettingsFromServer()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(`操作失败: ${result.error}`)
|
||||||
|
// 操作失败时,也重新加载,以恢复到服务器上的真实状态
|
||||||
|
loadSettingsFromServer()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('服务器响应错误')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新邮箱状态失败:', error)
|
||||||
|
ElMessage.error('同步服务器失败,请刷新页面')
|
||||||
|
loadSettingsFromServer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// [已修改] 添加新邮箱
|
||||||
|
const addNewEmail = async () => {
|
||||||
|
if (!newEmail.value.email) {
|
||||||
|
ElMessage.error('邮箱地址不能为空')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||||
|
if (!emailRegex.test(newEmail.value.email.trim())) {
|
||||||
|
ElMessage.error('请输入有效的邮箱地址')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// [关键修改] 构造一个包含所有现有邮箱和新邮箱的完整列表
|
||||||
|
const updatedEmailList = allEmails.value.map(e => ({
|
||||||
|
email: e.email,
|
||||||
|
description: e.description,
|
||||||
|
is_enabled: e.is_enabled
|
||||||
|
}))
|
||||||
|
|
||||||
|
updatedEmailList.push({
|
||||||
|
email: newEmail.value.email.trim(),
|
||||||
|
description: newEmail.value.description,
|
||||||
|
is_enabled: newEmail.value.is_enabled
|
||||||
|
})
|
||||||
|
|
||||||
|
// [关键修改] 将这个完整的列表发送给后端
|
||||||
|
const response = await fetch(`${API_BASE_URL_WITHOUT_API}/api/notification-settings/`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
notification_emails: updatedEmailList,
|
||||||
|
email_enabled: emailNotificationEnabled.value // 别忘了带上总开关的状态
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const result = await response.json()
|
||||||
|
if (result.success) {
|
||||||
|
ElMessage.success('新邮箱已添加')
|
||||||
|
// 清空输入框
|
||||||
|
newEmail.value.email = ''
|
||||||
|
newEmail.value.description = ''
|
||||||
|
newEmail.value.is_enabled = true
|
||||||
|
// [关键修改] 从服务器重新加载所有设置,以获取包含新邮箱 ID 的完整列表
|
||||||
|
loadSettingsFromServer()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(`添加失败: ${result.error}`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('服务器响应错误')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('添加新邮箱失败:', error)
|
||||||
|
ElMessage.error('添加新邮箱失败,请稍后重试')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// [已修改] 删除现有邮箱
|
||||||
|
const removeExistingEmail = async (emailId) => {
|
||||||
|
try {
|
||||||
|
// [关键修改] 构造一个不包含要删除邮箱的新列表
|
||||||
|
const updatedEmailList = allEmails.value
|
||||||
|
.filter(item => item.id !== emailId)
|
||||||
|
.map(e => ({
|
||||||
|
email: e.email,
|
||||||
|
description: e.description,
|
||||||
|
is_enabled: e.is_enabled
|
||||||
|
}))
|
||||||
|
|
||||||
|
// [关键修改] 将这个新列表发送给后端进行批量更新
|
||||||
|
const response = await fetch(`${API_BASE_URL_WITHOUT_API}/api/notification-settings/`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
notification_emails: updatedEmailList,
|
||||||
|
email_enabled: emailNotificationEnabled.value // 别忘了带上总开关的状态
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const result = await response.json()
|
||||||
|
if (result.success) {
|
||||||
|
ElMessage.success('邮箱已删除')
|
||||||
|
// [关键修改] 从服务器重新加载,确保列表同步
|
||||||
|
loadSettingsFromServer()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(`删除失败: ${result.error}`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('服务器响应错误')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除邮箱失败:', error)
|
||||||
|
ElMessage.error('删除邮箱失败,请稍后重试')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadSettings()
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
isDarkMode,
|
||||||
|
emailNotificationEnabled,
|
||||||
|
allEmails,
|
||||||
|
newEmail,
|
||||||
|
toggleTheme,
|
||||||
|
saveEmailNotificationSetting,
|
||||||
|
toggleEmailStatus,
|
||||||
|
removeExistingEmail,
|
||||||
|
addNewEmail,
|
||||||
|
// 删除不再需要的旧方法
|
||||||
|
// saveNotificationEmails,
|
||||||
|
// addEmail,
|
||||||
|
// removeEmail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.settings-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-header {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-header h2 {
|
||||||
|
color: #303133;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-card {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px 0;
|
||||||
|
border-bottom: 1px solid #f0f2f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-label {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-label span {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-desc {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #909399;
|
||||||
|
margin: 4px 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-control {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-section {
|
||||||
|
padding: 0;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-section h3 {
|
||||||
|
color: #409eff;
|
||||||
|
font-size: 18px;
|
||||||
|
margin: 20px 0 10px 0;
|
||||||
|
border-left: 4px solid #409eff;
|
||||||
|
padding-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-section p {
|
||||||
|
margin: 10px 0;
|
||||||
|
color: #606266;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-section ul,
|
||||||
|
.content-section ol {
|
||||||
|
margin: 10px 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-section li {
|
||||||
|
margin: 5px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disclaimer {
|
||||||
|
background-color: #fef0f0;
|
||||||
|
border: 1px solid #fbc4c4;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 15px;
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disclaimer p {
|
||||||
|
color: #f56c6c;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disclaimer ul {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disclaimer li {
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 邮箱管理样式 */
|
||||||
|
.email-management-section {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-management-section h4 {
|
||||||
|
color: #409eff;
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.existing-emails {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-management-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-address {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #303133;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-description {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-email-form {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暗色主题下的邮箱管理样式 */
|
||||||
|
.dark .email-management-section h4 {
|
||||||
|
color: #60a5fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .email-management-item {
|
||||||
|
background-color: #2d2d2d;
|
||||||
|
border-color: #434343;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .email-address {
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .email-description {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暗色主题 */
|
||||||
|
.dark .settings-page {
|
||||||
|
background-color: #141414;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .settings-header h2 {
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .setting-card {
|
||||||
|
background-color: #1f1f1f;
|
||||||
|
border-color: #434343;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .card-header {
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .setting-label span {
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .setting-desc {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .content-section h3 {
|
||||||
|
color: #60a5fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .content-section p,
|
||||||
|
.dark .content-section li {
|
||||||
|
color: #d1d5db;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user