Merge remote-tracking branch 'origin/develop' into develop

This commit is contained in:
李强
2024-10-28 21:13:07 +08:00
15 changed files with 521 additions and 6 deletions

1
backend/.gitignore vendored
View File

@@ -98,4 +98,5 @@ media/
__pypackages__/ __pypackages__/
package-lock.json package-lock.json
gunicorn.pid gunicorn.pid
plugins/*
!plugins/__init__.py !plugins/__init__.py

View File

@@ -404,7 +404,7 @@ PLUGINS_URL_PATTERNS = []
# ********** 一键导入插件配置开始 ********** # ********** 一键导入插件配置开始 **********
# 例如: # 例如:
# from dvadmin_upgrade_center.settings import * # 升级中心 # 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_third.settings import * # 第三方用户管理
# from dvadmin_ak_sk.settings import * # 秘钥管理管理 # from dvadmin_ak_sk.settings import * # 秘钥管理管理
# from dvadmin_tenants.settings import * # 租户管理 # from dvadmin_tenants.settings import * # 租户管理

View File

@@ -672,6 +672,53 @@
"model": "ApiWhiteList" "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": [], "menu_button": [],

View File

@@ -1,5 +1,7 @@
import hashlib import hashlib
import os import os
from time import time
from pathlib import PurePosixPath
from django.contrib.auth.models import AbstractUser, UserManager from django.contrib.auth.models import AbstractUser, UserManager
from django.db import models from django.db import models
@@ -595,3 +597,41 @@ class MessageCenterTargetUser(CoreModel):
db_table = table_prefix + "message_center_target_user" db_table = table_prefix + "message_center_target_user"
verbose_name = "消息中心目标用户表" verbose_name = "消息中心目标用户表"
verbose_name_plural = 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",)

View File

@@ -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()

View File

@@ -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.system_config import SystemConfigViewSet
from dvadmin.system.views.user import UserViewSet from dvadmin.system.views.user import UserViewSet
from dvadmin.system.views.menu_field import MenuFieldViewSet from dvadmin.system.views.menu_field import MenuFieldViewSet
from dvadmin.system.views.download_center import DownloadCenterViewSet
system_url = routers.SimpleRouter() system_url = routers.SimpleRouter()
system_url.register(r'menu', MenuViewSet) 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'role_menu_permission', RoleMenuPermissionViewSet)
system_url.register(r'column', MenuFieldViewSet) system_url.register(r'column', MenuFieldViewSet)
system_url.register(r'login_log', LoginLogViewSet) system_url.register(r'login_log', LoginLogViewSet)
system_url.register(r'download_center', DownloadCenterViewSet)
urlpatterns = [ urlpatterns = [

View File

@@ -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)

View File

@@ -120,11 +120,11 @@ class MenuViewSet(CustomModelViewSet):
"""用于前端获取当前角色的路由""" """用于前端获取当前角色的路由"""
user = request.user user = request.user
if user.is_superuser: if user.is_superuser:
queryset = self.queryset.filter(status=1) queryset = self.queryset.filter(status=1).order_by("id")
else: else:
role_list = user.role.values_list('id', flat=True) role_list = user.role.values_list('id', flat=True)
menu_list = RoleMenuPermission.objects.filter(role__in=role_list).values_list('menu_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) serializer = WebRouterSerializer(queryset, many=True, request=request)
data = serializer.data data = serializer.data
return SuccessResponse(data=data, total=len(data), msg="获取成功") return SuccessResponse(data=data, total=len(data), msg="获取成功")

View File

@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import datetime
from urllib.parse import quote from urllib.parse import quote
from django.db import transaction from django.db import transaction
@@ -11,8 +12,10 @@ from rest_framework.decorators import action
from rest_framework.request import Request from rest_framework.request import Request
from dvadmin.utils.import_export import import_to_data 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.utils.request_util import get_verbose_name
from dvadmin.system.tasks import async_export_data
from dvadmin.system.models import DownloadCenter
class ImportSerializerMixin: class ImportSerializerMixin:
@@ -301,6 +304,17 @@ class ExportSerializerMixin:
assert self.export_field_label, "'%s' 请配置对应的导出模板字段。" % self.__class__.__name__ assert self.export_field_label, "'%s' 请配置对应的导出模板字段。" % self.__class__.__name__
assert self.export_serializer_class, "'%s' 请配置对应的导出序列化器。" % self.__class__.__name__ assert self.export_serializer_class, "'%s' 请配置对应的导出序列化器。" % self.__class__.__name__
data = self.export_serializer_class(queryset, many=True, request=request).data 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 表 # 导出excel 表
response = HttpResponse(content_type="application/msexcel") response = HttpResponse(content_type="application/msexcel")
response["Access-Control-Expose-Headers"] = f"Content-Disposition" response["Access-Control-Expose-Headers"] = f"Content-Disposition"

View File

@@ -28,5 +28,4 @@ uvicorn==0.30.3
gunicorn==22.0.0 gunicorn==22.0.0
gevent==24.2.1 gevent==24.2.1
Pillow==10.4.0 Pillow==10.4.0
dvadmin-celery==1.0.5
pyinstaller==6.9.0 pyinstaller==6.9.0

View File

@@ -94,6 +94,8 @@ export function formatTwoStageRoutes(arr: any) {
return newArr; return newArr;
} }
const frameOutRoutes = staticRoutes.map(item => item.path)
// 路由加载前 // 路由加载前
router.beforeEach(async (to, from, next) => { router.beforeEach(async (to, from, next) => {
// 检查浏览器本地版本与线上版本是否一致,判断是否需要刷新页面进行更新 // 检查浏览器本地版本与线上版本是否一致,判断是否需要刷新页面进行更新
@@ -112,8 +114,9 @@ router.beforeEach(async (to, from, next) => {
} else if (token && to.path === '/login') { } else if (token && to.path === '/login') {
next('/home'); next('/home');
NProgress.done(); NProgress.done();
}else if(token && frameOutRoutes.includes(to.path) ){
next()
} else { } else {
const storesRoutesList = useRoutesList(pinia); const storesRoutesList = useRoutesList(pinia);
const {routesList} = storeToRefs(storesRoutesList); const {routesList} = storeToRefs(storesRoutesList);
if (routesList.value.length === 0) { if (routesList.value.length === 0) {

View File

@@ -10,6 +10,7 @@ import { errorLog, errorCreate } from './tools.ts';
import { Local, Session } from '/@/utils/storage'; import { Local, Session } from '/@/utils/storage';
import qs from 'qs'; import qs from 'qs';
import { getBaseURL } from './baseUrl'; import { getBaseURL } from './baseUrl';
import { successMessage } from './message.js';
/** /**
* @description 创建请求实例 * @description 创建请求实例
*/ */
@@ -204,6 +205,8 @@ export const requestForMock = createRequestFunction(serviceForMock);
* @param filename * @param filename
*/ */
export const downloadFile = function ({ url, params, method, filename = '文件导出' }: any) { export const downloadFile = function ({ url, params, method, filename = '文件导出' }: any) {
// return request({ url: url, method: method, params: params })
// .then((res: any) => successMessage(res.msg));
request({ request({
url: url, url: url,
method: method, method: method,
@@ -211,6 +214,8 @@ export const downloadFile = function ({ url, params, method, filename = '文件
responseType: 'blob' responseType: 'blob'
// headers: {Accept: 'application/vnd.openxmlformats-officedocument'} // headers: {Accept: 'application/vnd.openxmlformats-officedocument'}
}).then((res: any) => { }).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 xlsxName = window.decodeURI(res.headers['content-disposition'].split('=')[1])
const fileName = xlsxName || `${filename}.xlsx` const fileName = xlsxName || `${filename}.xlsx`
if (res) { if (res) {

View File

@@ -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 },
});
}

View File

@@ -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
}
}
},
},
};
};

View File

@@ -0,0 +1,42 @@
<template>
<fs-page>
<fs-crud ref="crudRef" v-bind="crudBinding">
<template #cell_url="scope">
<el-tag size="small">{{ scope.row.url }}</el-tag>
</template>
</fs-crud>
</fs-page>
</template>
<script lang="ts" setup name="downloadCenter">
import { ref, onMounted, inject, onBeforeUpdate } from 'vue';
import { GetPermission } from './api';
import { useExpose, useCrud } from '@fast-crud/fast-crud';
import { createCrudOptions } from './crud';
import PermissionComNew from './components/PermissionComNew/index.vue';
import _ from "lodash-es";
import { handleColumnPermission } from "/@/utils/columnPermission";
// crud组件的ref
const crudRef = ref();
// crud 配置的ref
const crudBinding = ref();
const { crudExpose } = useExpose({ crudRef, crudBinding });
// 你的crud配置
const { crudOptions } = createCrudOptions({ crudExpose });
// 初始化crud配置
const { resetCrudOptions } = useCrud({
crudExpose,
crudOptions,
context: {},
});
// 页面打开后获取列表数据
onMounted(async () => {
crudExpose.doRefresh();
});
</script>