feat: add export
This commit is contained in:
@@ -21,6 +21,7 @@ from django.conf import settings
|
|||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('api/admin/system/', include('system.urls')),
|
path('api/admin/system/', include('system.urls')),
|
||||||
path('api/admin/ai/', include('ai.urls')),
|
path('api/admin/ai/', include('ai.urls')),
|
||||||
|
path('api-auth/', include('rest_framework.urls'))
|
||||||
]
|
]
|
||||||
|
|
||||||
# 演示环境下禁用 admin 路由
|
# 演示环境下禁用 admin 路由
|
||||||
|
|||||||
@@ -16,3 +16,4 @@ gunicorn==23.0.0
|
|||||||
django_redis==6.0.0
|
django_redis==6.0.0
|
||||||
django-ninja==1.4.3
|
django-ninja==1.4.3
|
||||||
ip2geotools==0.1.6
|
ip2geotools==0.1.6
|
||||||
|
pandas==2.2.3
|
||||||
@@ -30,7 +30,8 @@ class DictDataLabelValueSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class DictDataViewSet(CustomModelViewSet):
|
class DictDataViewSet(CustomModelViewSet):
|
||||||
queryset = DictData.objects.filter(is_deleted=False)
|
queryset = (DictData.objects.filter(is_deleted=False).select_related('dict_type')
|
||||||
|
.order_by('dict_type__id', 'sort'))
|
||||||
serializer_class = DictDataSerializer
|
serializer_class = DictDataSerializer
|
||||||
filterset_class = DictDataFilter
|
filterset_class = DictDataFilter
|
||||||
|
|
||||||
|
|||||||
@@ -182,6 +182,23 @@ class UserViewSet(CustomModelViewSet):
|
|||||||
search_fields = ['username', 'nickname', 'mobile'] # 支持模糊搜索
|
search_fields = ['username', 'nickname', 'mobile'] # 支持模糊搜索
|
||||||
ordering_fields = ['create_time', 'id']
|
ordering_fields = ['create_time', 'id']
|
||||||
ordering = ['-create_time']
|
ordering = ['-create_time']
|
||||||
|
export_fields = {
|
||||||
|
'id': 'ID',
|
||||||
|
'username': '用户名',
|
||||||
|
'nickname': '昵称',
|
||||||
|
'mobile': '手机号',
|
||||||
|
'email': '邮箱',
|
||||||
|
'status': {
|
||||||
|
'name': '状态',
|
||||||
|
'processor': lambda value, item: '启用' if value == 1 else '禁用'
|
||||||
|
},
|
||||||
|
'login_ip': '登录IP',
|
||||||
|
'last_login': '最后登录时间',
|
||||||
|
'create_time': '创建时间',
|
||||||
|
'update_time': '更新时间',
|
||||||
|
}
|
||||||
|
|
||||||
|
export_filename = '用户数据'
|
||||||
|
|
||||||
|
|
||||||
class Logout(APIView):
|
class Logout(APIView):
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
from rest_framework import viewsets, status
|
from rest_framework import viewsets, status
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
from utils.export_mixin import ExportMixin
|
||||||
|
|
||||||
class CustomModelViewSet(viewsets.ModelViewSet):
|
|
||||||
|
class CustomModelViewSet(viewsets.ModelViewSet, ExportMixin):
|
||||||
"""
|
"""
|
||||||
自定义ModelViewSet,提供以下增强功能:
|
自定义ModelViewSet,提供以下增强功能:
|
||||||
- 基于动作的序列化器选择
|
- 基于动作的序列化器选择
|
||||||
|
|||||||
101
backend/utils/export_mixin.py
Normal file
101
backend/utils/export_mixin.py
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
from django.http import HttpResponse
|
||||||
|
|
||||||
|
class ExportMixin:
|
||||||
|
"""
|
||||||
|
导出功能 Mixin,提供数据导出为 Excel 或 CSV 的功能
|
||||||
|
"""
|
||||||
|
# 导出配置
|
||||||
|
export_fields = {} # 字段映射配置
|
||||||
|
export_filename = None # 导出文件名
|
||||||
|
|
||||||
|
@action(detail=False, methods=['get'], url_path='export')
|
||||||
|
def export_data(self, request):
|
||||||
|
"""
|
||||||
|
导出数据功能
|
||||||
|
支持通过参数控制导出字段:
|
||||||
|
- fields: 指定要导出的字段,多个字段用逗号分隔
|
||||||
|
- format: 导出格式 (excel/csv),默认excel
|
||||||
|
"""
|
||||||
|
# 获取查询集
|
||||||
|
queryset = self.filter_queryset(self.get_queryset())
|
||||||
|
|
||||||
|
# 获取导出字段配置
|
||||||
|
export_config = self.get_export_fields(request)
|
||||||
|
|
||||||
|
# 序列化数据
|
||||||
|
serializer = self.get_serializer(queryset, many=True)
|
||||||
|
data = serializer.data
|
||||||
|
|
||||||
|
# 处理数据
|
||||||
|
processed_data = self.process_export_data(data, export_config)
|
||||||
|
|
||||||
|
# 生成文件名
|
||||||
|
filename = self.export_filename or f"{self.__class__.__name__}_export"
|
||||||
|
|
||||||
|
# 根据格式返回响应
|
||||||
|
export_format = request.query_params.get('format', 'excel').lower()
|
||||||
|
|
||||||
|
if export_format == 'csv':
|
||||||
|
return self.generate_csv_response(processed_data, filename)
|
||||||
|
else:
|
||||||
|
return self.generate_excel_response(processed_data, filename)
|
||||||
|
|
||||||
|
def get_export_fields(self, request):
|
||||||
|
"""
|
||||||
|
获取导出字段配置
|
||||||
|
支持通过URL参数指定字段: ?fields=id,name,email
|
||||||
|
"""
|
||||||
|
# 优先使用请求参数中的字段
|
||||||
|
fields_param = request.query_params.get('fields')
|
||||||
|
if fields_param:
|
||||||
|
field_names = [f.strip() for f in fields_param.split(',')]
|
||||||
|
# 只返回指定字段的配置
|
||||||
|
return {field: self.export_fields.get(field, field) for field in field_names}
|
||||||
|
# 默认返回所有配置字段
|
||||||
|
return self.export_fields or {}
|
||||||
|
|
||||||
|
def process_export_data(self, data, export_config):
|
||||||
|
"""
|
||||||
|
处理导出数据,根据配置映射字段名和处理数据
|
||||||
|
"""
|
||||||
|
if not export_config:
|
||||||
|
return data
|
||||||
|
|
||||||
|
processed_data = []
|
||||||
|
for item in data:
|
||||||
|
processed_item = {}
|
||||||
|
for field_key, field_config in export_config.items():
|
||||||
|
# field_config 可以是字符串(列名)或字典(包含列名和处理函数)
|
||||||
|
if isinstance(field_config, dict):
|
||||||
|
column_name = field_config.get('name', field_key)
|
||||||
|
processor = field_config.get('processor')
|
||||||
|
value = item.get(field_key)
|
||||||
|
if processor and callable(processor):
|
||||||
|
value = processor(value, item)
|
||||||
|
processed_item[column_name] = value
|
||||||
|
else:
|
||||||
|
# 简单映射
|
||||||
|
processed_item[field_config] = item.get(field_key, '')
|
||||||
|
processed_data.append(processed_item)
|
||||||
|
return processed_data
|
||||||
|
|
||||||
|
def generate_excel_response(self, data, filename):
|
||||||
|
"""生成Excel格式响应"""
|
||||||
|
df = pd.DataFrame(data)
|
||||||
|
response = HttpResponse(
|
||||||
|
content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||||
|
)
|
||||||
|
response['Content-Disposition'] = f'attachment; filename="{filename}.xlsx"'
|
||||||
|
df.to_excel(response, index=False)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def generate_csv_response(self, data, filename):
|
||||||
|
"""生成CSV格式响应"""
|
||||||
|
df = pd.DataFrame(data)
|
||||||
|
response = HttpResponse(content_type='text/csv')
|
||||||
|
response['Content-Disposition'] = f'attachment; filename="{filename}.csv"'
|
||||||
|
df.to_csv(response, index=False)
|
||||||
|
return response
|
||||||
Reference in New Issue
Block a user