From 199a75293d52a7d6d8d6fc3532791cd11cea0e90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=BF=E8=BE=89?= Date: Thu, 24 Oct 2024 22:08:24 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=8B=E8=BD=BD=E4=B8=AD=E5=BF=83=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E5=92=8C=E5=BC=82=E6=AD=A5=E5=AF=BC=E5=87=BA=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/.gitignore | 1 + backend/application/settings.py | 2 +- .../dvadmin/system/fixtures/init_menu.json | 47 +++++ backend/dvadmin/system/models.py | 40 +++++ backend/dvadmin/system/tasks.py | 107 ++++++++++++ backend/dvadmin/system/urls.py | 2 + .../dvadmin/system/views/download_center.py | 46 +++++ backend/dvadmin/utils/import_export_mixin.py | 16 +- backend/requirements.txt | 1 - web/src/utils/service.ts | 5 + web/src/views/system/downloadCenter/api.ts | 49 ++++++ web/src/views/system/downloadCenter/crud.tsx | 160 ++++++++++++++++++ web/src/views/system/downloadCenter/index.vue | 42 +++++ 13 files changed, 515 insertions(+), 3 deletions(-) create mode 100644 backend/dvadmin/system/tasks.py create mode 100644 backend/dvadmin/system/views/download_center.py create mode 100644 web/src/views/system/downloadCenter/api.ts create mode 100644 web/src/views/system/downloadCenter/crud.tsx create mode 100644 web/src/views/system/downloadCenter/index.vue diff --git a/backend/.gitignore b/backend/.gitignore index 6c50cc9..047099f 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -98,4 +98,5 @@ media/ __pypackages__/ package-lock.json gunicorn.pid +plugins/* !plugins/__init__.py diff --git a/backend/application/settings.py b/backend/application/settings.py index 8b2c8c2..1d0adf7 100644 --- a/backend/application/settings.py +++ b/backend/application/settings.py @@ -404,7 +404,7 @@ PLUGINS_URL_PATTERNS = [] # ********** 一键导入插件配置开始 ********** # 例如: # from dvadmin_upgrade_center.settings import * # 升级中心 -# from dvadmin_celery.settings import * # celery 异步任务 +from dvadmin3_celery.settings import * # celery 异步任务 # from dvadmin_third.settings import * # 第三方用户管理 # from dvadmin_ak_sk.settings import * # 秘钥管理管理 # from dvadmin_tenants.settings import * # 租户管理 diff --git a/backend/dvadmin/system/fixtures/init_menu.json b/backend/dvadmin/system/fixtures/init_menu.json index b683f0a..835a41e 100644 --- a/backend/dvadmin/system/fixtures/init_menu.json +++ b/backend/dvadmin/system/fixtures/init_menu.json @@ -672,6 +672,53 @@ "model": "ApiWhiteList" } ] + }, + { + "name": "下载中心", + "icon": "ele-Download", + "sort": 9, + "is_link": false, + "is_catalog": false, + "web_path": "/downloadCenter", + "component": "system/downloadCenter/index", + "component_name": "downloadCenter", + "status": true, + "cache": false, + "visible": true, + "parent": 277, + "children": [], + "menu_button": [ + { + "name": "查询", + "value": "Search", + "api": "/api/system/downloadCenter/", + "method": 0 + }, + { + "name": "详情", + "value": "Retrieve", + "api": "/api/system/downloadCenter/{id}/", + "method": 0 + }, + { + "name": "新增", + "value": "Create", + "api": "/api/system/downloadCenter/", + "method": 1 + }, + { + "name": "编辑", + "value": "Update", + "api": "/api/system/downloadCenter/{id}/", + "method": 2 + }, + { + "name": "删除", + "value": "Delete", + "api": "/api/system/downloadCenter/{id}/", + "method": 3 + } + ] } ], "menu_button": [], diff --git a/backend/dvadmin/system/models.py b/backend/dvadmin/system/models.py index e0e1841..d9a4274 100644 --- a/backend/dvadmin/system/models.py +++ b/backend/dvadmin/system/models.py @@ -1,5 +1,7 @@ import hashlib import os +from time import time +from pathlib import PurePosixPath from django.contrib.auth.models import AbstractUser, UserManager from django.db import models @@ -595,3 +597,41 @@ class MessageCenterTargetUser(CoreModel): db_table = table_prefix + "message_center_target_user" verbose_name = "消息中心目标用户表" verbose_name_plural = verbose_name + + +def media_file_name_downloadcenter(instance:'DownloadCenter', filename): + h = instance.md5sum + basename, ext = os.path.splitext(filename) + return PurePosixPath("files", "dlct", h[:1], h[1:2], basename + '-' + str(time()).replace('.', '') + ext.lower()) + + +class DownloadCenter(CoreModel): + TASK_STATUS_CHOICES = [ + (0, '任务已创建'), + (1, '任务进行中'), + (2, '任务完成'), + (3, '任务失败'), + ] + task_name = models.CharField(max_length=255, verbose_name="任务名称", help_text="任务名称") + task_status = models.SmallIntegerField(default=0, choices=TASK_STATUS_CHOICES, verbose_name='是否可下载', help_text='是否可下载') + file_name = models.CharField(max_length=255, null=True, blank=True, verbose_name="文件名", help_text="文件名") + url = models.FileField(upload_to=media_file_name_downloadcenter, null=True, blank=True) + size = models.BigIntegerField(default=0, verbose_name="文件大小", help_text="文件大小") + md5sum = models.CharField(max_length=36, null=True, blank=True, verbose_name="文件md5", help_text="文件md5") + + def save(self, *args, **kwargs): + if self.url: + if not self.md5sum: # file is new + md5 = hashlib.md5() + for chunk in self.url.chunks(): + md5.update(chunk) + self.md5sum = md5.hexdigest() + if not self.size: + self.size = self.url.size + super(DownloadCenter, self).save(*args, **kwargs) + + class Meta: + db_table = table_prefix + "download_center" + verbose_name = "下载中心" + verbose_name_plural = verbose_name + ordering = ("-create_datetime",) diff --git a/backend/dvadmin/system/tasks.py b/backend/dvadmin/system/tasks.py new file mode 100644 index 0000000..0245e63 --- /dev/null +++ b/backend/dvadmin/system/tasks.py @@ -0,0 +1,107 @@ +from hashlib import md5 +from io import BytesIO +from datetime import datetime +from time import sleep + +from openpyxl import Workbook +from openpyxl.worksheet.table import Table, TableStyleInfo +from openpyxl.utils import get_column_letter +from django.core.files.base import ContentFile + +from application.celery import app +from dvadmin.system.models import DownloadCenter + +def is_number(num): + try: + float(num) + return True + except ValueError: + pass + + try: + import unicodedata + unicodedata.numeric(num) + return True + except (TypeError, ValueError): + pass + return False + +def get_string_len(string): + """ + 获取字符串最大长度 + :param string: + :return: + """ + length = 4 + if string is None: + return length + if is_number(string): + return length + for char in string: + length += 2.1 if ord(char) > 256 else 1 + return round(length, 1) if length <= 50 else 50 + +@app.task +def async_export_data(data: list, filename: str, dcid: int, export_field_label: dict): + instance = DownloadCenter.objects.get(pk=dcid) + instance.task_status = 1 + instance.save() + sleep(2) + try: + wb = Workbook() + ws = wb.active + header_data = ["序号", *export_field_label.values()] + hidden_header = ["#", *export_field_label.keys()] + df_len_max = [get_string_len(ele) for ele in header_data] + row = get_column_letter(len(export_field_label) + 1) + column = 1 + ws.append(header_data) + for index, results in enumerate(data): + results_list = [] + for h_index, h_item in enumerate(hidden_header): + for key, val in results.items(): + if key == h_item: + if val is None or val == "": + results_list.append("") + elif isinstance(val, datetime): + val = val.strftime("%Y-%m-%d %H:%M:%S") + results_list.append(val) + else: + results_list.append(val) + # 计算最大列宽度 + result_column_width = get_string_len(val) + if h_index != 0 and result_column_width > df_len_max[h_index]: + df_len_max[h_index] = result_column_width + ws.append([index + 1, *results_list]) + column += 1 + #  更新列宽 + for index, width in enumerate(df_len_max): + ws.column_dimensions[get_column_letter(index + 1)].width = width + tab = Table(displayName="Table", ref=f"A1:{row}{column}") # 名称管理器 + style = TableStyleInfo( + name="TableStyleLight11", + showFirstColumn=True, + showLastColumn=True, + showRowStripes=True, + showColumnStripes=True, + ) + tab.tableStyleInfo = style + ws.add_table(tab) + stream = BytesIO() + wb.save(stream) + stream.seek(0) + s = md5() + while True: + chunk = stream.read(1024) + if not chunk: + break + s.update(chunk) + stream.seek(0) + instance.md5sum = s.hexdigest() + instance.file_name = filename + instance.url.save(filename, ContentFile(stream.read())) + instance.task_status = 2 + except Exception as e: + instance.task_status = 3 + instance.description = str(e)[:250] + instance.save() diff --git a/backend/dvadmin/system/urls.py b/backend/dvadmin/system/urls.py index a41e048..c9c12e9 100644 --- a/backend/dvadmin/system/urls.py +++ b/backend/dvadmin/system/urls.py @@ -18,6 +18,7 @@ from dvadmin.system.views.role_menu_button_permission import RoleMenuButtonPermi from dvadmin.system.views.system_config import SystemConfigViewSet from dvadmin.system.views.user import UserViewSet from dvadmin.system.views.menu_field import MenuFieldViewSet +from dvadmin.system.views.download_center import DownloadCenterViewSet system_url = routers.SimpleRouter() system_url.register(r'menu', MenuViewSet) @@ -36,6 +37,7 @@ system_url.register(r'role_menu_button_permission', RoleMenuButtonPermissionView system_url.register(r'role_menu_permission', RoleMenuPermissionViewSet) system_url.register(r'column', MenuFieldViewSet) system_url.register(r'login_log', LoginLogViewSet) +system_url.register(r'download_center', DownloadCenterViewSet) urlpatterns = [ diff --git a/backend/dvadmin/system/views/download_center.py b/backend/dvadmin/system/views/download_center.py new file mode 100644 index 0000000..2587d74 --- /dev/null +++ b/backend/dvadmin/system/views/download_center.py @@ -0,0 +1,46 @@ +from rest_framework import serializers +from django.conf import settings +from django_filters.rest_framework import FilterSet, CharFilter + +from dvadmin.utils.serializers import CustomModelSerializer +from dvadmin.utils.viewset import CustomModelViewSet +from dvadmin.system.models import DownloadCenter + + +class DownloadCenterSerializer(CustomModelSerializer): + url = serializers.SerializerMethodField(read_only=True) + + def get_url(self, instance): + if self.request.query_params.get('prefix'): + if settings.ENVIRONMENT in ['local']: + prefix = 'http://127.0.0.1:8000' + elif settings.ENVIRONMENT in ['test']: + prefix = 'http://{host}/api'.format(host=self.request.get_host()) + else: + prefix = 'https://{host}/api'.format(host=self.request.get_host()) + return (f'{prefix}/media/{str(instance.url)}') + return f'media/{str(instance.url)}' + + class Meta: + model = DownloadCenter + fields = "__all__" + read_only_fields = ["id"] + + +class DownloadCenterFilterSet(FilterSet): + task_name = CharFilter(field_name='task_name', lookup_expr='icontains') + file_name = CharFilter(field_name='file_name', lookup_expr='icontains') + + class Meta: + model = DownloadCenter + fields = ['task_status', 'task_name', 'file_name'] + + +class DownloadCenterViewSet(CustomModelViewSet): + queryset = DownloadCenter.objects.all() + serializer_class = DownloadCenterSerializer + filter_class = DownloadCenterFilterSet + permission_classes = [] + + def get_queryset(self): + return super().get_queryset().filter(creator=self.request.user) diff --git a/backend/dvadmin/utils/import_export_mixin.py b/backend/dvadmin/utils/import_export_mixin.py index 44f51cc..74e85fd 100644 --- a/backend/dvadmin/utils/import_export_mixin.py +++ b/backend/dvadmin/utils/import_export_mixin.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import datetime from urllib.parse import quote from django.db import transaction @@ -11,8 +12,10 @@ from rest_framework.decorators import action from rest_framework.request import Request from dvadmin.utils.import_export import import_to_data -from dvadmin.utils.json_response import DetailResponse +from dvadmin.utils.json_response import DetailResponse, SuccessResponse from dvadmin.utils.request_util import get_verbose_name +from dvadmin.system.tasks import async_export_data +from dvadmin.system.models import DownloadCenter class ImportSerializerMixin: @@ -301,6 +304,17 @@ class ExportSerializerMixin: assert self.export_field_label, "'%s' 请配置对应的导出模板字段。" % self.__class__.__name__ assert self.export_serializer_class, "'%s' 请配置对应的导出序列化器。" % self.__class__.__name__ data = self.export_serializer_class(queryset, many=True, request=request).data + try: + from dvadmin3_celery import settings + async_export_data.delay( + data, + str(f"导出{get_verbose_name(queryset)}-{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}.xlsx"), + DownloadCenter.objects.create(creator=request.user, task_name=f'{get_verbose_name(queryset)}数据导出任务').pk, + self.export_field_label + ) + return SuccessResponse(msg="导入任务已创建,请前往‘下载中心’等待下载") + except: + pass # 导出excel 表 response = HttpResponse(content_type="application/msexcel") response["Access-Control-Expose-Headers"] = f"Content-Disposition" diff --git a/backend/requirements.txt b/backend/requirements.txt index 121a930..3395719 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -28,5 +28,4 @@ uvicorn==0.30.3 gunicorn==22.0.0 gevent==24.2.1 Pillow==10.4.0 -dvadmin-celery==1.0.5 pyinstaller==6.9.0 \ No newline at end of file diff --git a/web/src/utils/service.ts b/web/src/utils/service.ts index 3775e26..afb30cc 100644 --- a/web/src/utils/service.ts +++ b/web/src/utils/service.ts @@ -10,6 +10,7 @@ import { errorLog, errorCreate } from './tools.ts'; import { Local, Session } from '/@/utils/storage'; import qs from 'qs'; import { getBaseURL } from './baseUrl'; +import { successMessage } from './message.js'; /** * @description 创建请求实例 */ @@ -204,6 +205,8 @@ export const requestForMock = createRequestFunction(serviceForMock); * @param filename */ export const downloadFile = function ({ url, params, method, filename = '文件导出' }: any) { + // return request({ url: url, method: method, params: params }) + // .then((res: any) => successMessage(res.msg)); request({ url: url, method: method, @@ -211,6 +214,8 @@ export const downloadFile = function ({ url, params, method, filename = '文件 responseType: 'blob' // headers: {Accept: 'application/vnd.openxmlformats-officedocument'} }).then((res: any) => { + // console.log(res.headers['content-type']); // 根据content-type不同来判断是否异步下载 + if (res.headers['content-type'] === 'application/json') return successMessage('导入任务已创建,请前往‘下载中心’等待下载'); const xlsxName = window.decodeURI(res.headers['content-disposition'].split('=')[1]) const fileName = xlsxName || `${filename}.xlsx` if (res) { diff --git a/web/src/views/system/downloadCenter/api.ts b/web/src/views/system/downloadCenter/api.ts new file mode 100644 index 0000000..4baa884 --- /dev/null +++ b/web/src/views/system/downloadCenter/api.ts @@ -0,0 +1,49 @@ +import { request } from '/@/utils/service'; +import { PageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud'; + +export const apiPrefix = '/api/system/download_center/'; + +export function GetPermission() { + return request({ + url: apiPrefix + 'field_permission/', + method: 'get', + }); +} + +export function GetList(query: PageQuery) { + return request({ + url: apiPrefix, + method: 'get', + params: query, + }); +} +export function GetObj(id: InfoReq) { + return request({ + url: apiPrefix + id, + method: 'get', + }); +} + +export function AddObj(obj: AddReq) { + return request({ + url: apiPrefix, + method: 'post', + data: obj, + }); +} + +export function UpdateObj(obj: EditReq) { + return request({ + url: apiPrefix + obj.id + '/', + method: 'put', + data: obj, + }); +} + +export function DelObj(id: DelReq) { + return request({ + url: apiPrefix + id + '/', + method: 'delete', + data: { id }, + }); +} diff --git a/web/src/views/system/downloadCenter/crud.tsx b/web/src/views/system/downloadCenter/crud.tsx new file mode 100644 index 0000000..79639c0 --- /dev/null +++ b/web/src/views/system/downloadCenter/crud.tsx @@ -0,0 +1,160 @@ +import { CrudOptions, AddReq, DelReq, EditReq, dict, CrudExpose, compute } from '@fast-crud/fast-crud'; +import * as api from './api'; +import { dictionary } from '/@/utils/dictionary'; +import { successMessage } from '../../../utils/message'; +import { auth } from '/@/utils/authFunction' + +interface CreateCrudOptionsTypes { + output: any; + crudOptions: CrudOptions; +} + +//此处为crudOptions配置 +export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExpose; }): CreateCrudOptionsTypes { + const pageRequest = async (query: any) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }: EditReq) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }: DelReq) => { + return await api.DelObj(row.id); + }; + const addRequest = async ({ form }: AddReq) => { + return await api.AddObj(form); + }; + + //权限判定 + + // @ts-ignore + // @ts-ignore + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest, + }, + pagination: { + show: true + }, + actionbar: { + buttons: { + add: { + show: false + } + } + }, + toolbar: { + buttons: { + export: { + show: false + } + } + }, + rowHandle: { + //固定右侧 + fixed: 'right', + width: 120, + buttons: { + view: { + show: false + }, + edit: { + show: false + }, + remove: { + show: false + }, + download: { + show: compute(ctx => ctx.row.task_status === 2), + text: '下载文件', + type: 'warning', + click: (ctx) => window.open(ctx.row.url, '_blank') + } + }, + }, + form: { + col: { span: 24 }, + labelWidth: '100px', + wrapper: { + is: 'el-dialog', + width: '600px', + }, + }, + columns: { + _index: { + title: '序号', + form: { show: false }, + column: { + type: 'index', + align: 'center', + width: '70px', + columnSetDisabled: true, //禁止在列设置中选择 + }, + }, + task_name: { + title: '任务名', + type: 'text', + column: { + minWidth: 160, + align: 'left' + }, + search: { + show: true + } + }, + file_name: { + title: '文件名', + type: 'text', + column: { + minWidth: 160, + align: 'left' + }, + search: { + show: true + } + }, + size: { + title: '文件大小(b)', + type: 'number', + column: { + width: 100 + } + }, + task_status: { + title: '任务状态', + type: 'dict-select', + dict: dict({ + data: [ + { label: '任务已创建', value: 0 }, + { label: '任务进行中', value: 1 }, + { label: '任务完成', value: 2 }, + { label: '任务失败', value: 3 }, + ] + }), + column: { + width: 120 + }, + search: { + show: true + } + }, + create_datetime: { + title: '创建时间', + column: { + width: 160 + } + }, + update_datetime: { + title: '创建时间', + column: { + width: 160 + } + } + }, + }, + }; +}; diff --git a/web/src/views/system/downloadCenter/index.vue b/web/src/views/system/downloadCenter/index.vue new file mode 100644 index 0000000..2ea8a54 --- /dev/null +++ b/web/src/views/system/downloadCenter/index.vue @@ -0,0 +1,42 @@ + + +