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/system/views/menu.py b/backend/dvadmin/system/views/menu.py
index 28dce62..c0c6b65 100644
--- a/backend/dvadmin/system/views/menu.py
+++ b/backend/dvadmin/system/views/menu.py
@@ -120,11 +120,11 @@ class MenuViewSet(CustomModelViewSet):
"""用于前端获取当前角色的路由"""
user = request.user
if user.is_superuser:
- queryset = self.queryset.filter(status=1)
+ queryset = self.queryset.filter(status=1).order_by("id")
else:
role_list = user.role.values_list('id', flat=True)
menu_list = RoleMenuPermission.objects.filter(role__in=role_list).values_list('menu_id', flat=True)
- queryset = Menu.objects.filter(id__in=menu_list)
+ queryset = Menu.objects.filter(id__in=menu_list).order_by("id")
serializer = WebRouterSerializer(queryset, many=True, request=request)
data = serializer.data
return SuccessResponse(data=data, total=len(data), msg="获取成功")
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/router/index.ts b/web/src/router/index.ts
index 53aa3d9..f98dcf3 100644
--- a/web/src/router/index.ts
+++ b/web/src/router/index.ts
@@ -94,6 +94,8 @@ export function formatTwoStageRoutes(arr: any) {
return newArr;
}
+const frameOutRoutes = staticRoutes.map(item => item.path)
+
// 路由加载前
router.beforeEach(async (to, from, next) => {
// 检查浏览器本地版本与线上版本是否一致,判断是否需要刷新页面进行更新
@@ -112,8 +114,9 @@ router.beforeEach(async (to, from, next) => {
} else if (token && to.path === '/login') {
next('/home');
NProgress.done();
+ }else if(token && frameOutRoutes.includes(to.path) ){
+ next()
} else {
-
const storesRoutesList = useRoutesList(pinia);
const {routesList} = storeToRefs(storesRoutesList);
if (routesList.value.length === 0) {
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 @@
+
+
+
+
+ {{ scope.row.url }}
+
+
+
+
+
+