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

pull/98/head
猿小天 2023-05-27 22:41:02 +08:00
commit 815b8e956b
18 changed files with 730 additions and 21 deletions

View File

@ -366,14 +366,17 @@ CAPTCHA_CHALLENGE_FUNCT = "captcha.helpers.math_challenge" # 加减乘除验证
# ================================================= #
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
API_LOG_ENABLE = True
# 是否启动API日志记录
API_LOG_ENABLE = locals().get("API_LOG_ENABLE", True)
# API 日志记录的请求方式
API_LOG_METHODS = locals().get("API_LOG_METHODS", ["POST", "UPDATE", "DELETE", "PUT"])
# API_LOG_METHODS = 'ALL' # ['POST', 'DELETE']
API_LOG_METHODS = ["POST", "UPDATE", "DELETE", "PUT"] # ['POST', 'DELETE']
API_MODEL_MAP = {
# 在操作日志中详细记录的请求模块映射
API_MODEL_MAP = locals().get("API_MODEL_MAP", {
"/token/": "登录模块",
"/api/login/": "登录模块",
"/api/plugins_market/plugins/": "插件市场",
}
"/api/logout/": "登录模块",
})
DJANGO_CELERY_BEAT_TZ_AWARE = False
CELERY_TIMEZONE = "Asia/Shanghai" # celery 时区问题

View File

@ -39,6 +39,11 @@ DEBUG = True
ENABLE_LOGIN_ANALYSIS_LOG = True
# 登录接口 /api/token/ 是否需要验证码认证,用于测试,正式环境建议取消
LOGIN_NO_CAPTCHA_AUTH = True
# 是否启动API日志记录
API_LOG_ENABLE = locals().get("API_LOG_ENABLE", True)
# API 日志记录的请求方式
API_LOG_METHODS = locals().get("API_LOG_METHODS", ["POST", "UPDATE", "DELETE", "PUT"])
# API_LOG_METHODS = 'ALL' # ['POST', 'DELETE']
# ================================================= #
# ****************** 其他 配置 ******************* #
# ================================================= #

View File

@ -21,18 +21,21 @@ class FileSerializer(CustomModelSerializer):
fields = "__all__"
def create(self, validated_data):
file_engine = dispatch.get_system_config_values("fileStorageConfig.file_engine")
file_engine = dispatch.get_system_config_values("fileStorageConfig.file_engine") or 'local'
file_backup = dispatch.get_system_config_values("fileStorageConfig.file_backup")
file = self.initial_data.get('file')
file_size = file.size
validated_data['name'] = file.name
validated_data['size'] = file_size
validated_data['md5sum'] = hashlib.md5().hexdigest()
md5 = hashlib.md5()
for chunk in file.chunks():
md5.update(chunk)
validated_data['md5sum'] = md5.hexdigest()
validated_data['engine'] = file_engine
validated_data['mime_type'] = file.content_type
if file_backup:
validated_data['url'] = file
if file_engine =='oss':
if file_engine == 'oss':
from dvadmin_cloud_storage.views.aliyun import ali_oss_upload
file_path = ali_oss_upload(file)
if file_path:

View File

@ -21,9 +21,9 @@ def import_to_data(file_url, field_data, m2m_fields=None):
file_path_dir = os.path.join(settings.BASE_DIR, file_url)
workbook = openpyxl.load_workbook(file_path_dir)
table = workbook[workbook.sheetnames[0]]
theader = tuple(table.values)[0] #Excel的表头
is_update = '更新主键(勿改)' in theader #是否导入更新
if is_update is False: #不是更新时,删除id列
theader = tuple(table.values)[0] # Excel的表头
is_update = '更新主键(勿改)' in theader # 是否导入更新
if is_update is False: # 不是更新时,删除id列
field_data.pop('id')
# 获取参数映射
validation_data_dict = {}
@ -35,9 +35,10 @@ def import_to_data(file_url, field_data, m2m_fields=None):
for k, v in choices.get("data").items():
data_dict[k] = v
elif choices.get("queryset") and choices.get("values_name"):
data_list = choices.get("queryset").values(choices.get("values_name"), "id")
data_list = choices.get("queryset").values(choices.get("values_name"),
choices.get("values_value", "id"))
for ele in data_list:
data_dict[ele.get(choices.get("values_name"))] = ele.get("id")
data_dict[ele.get(choices.get("values_name"))] = ele.get(choices.get("values_value", "id"))
else:
continue
validation_data_dict[key] = data_dict
@ -53,12 +54,11 @@ def import_to_data(file_url, field_data, m2m_fields=None):
values = items[1]
value_type = 'str'
if isinstance(values, dict):
value_type = values.get('type','str')
value_type = values.get('type', 'str')
cell_value = table.cell(row=row + 1, column=index + 2).value
if cell_value is None or cell_value=='':
if cell_value is None or cell_value == '':
continue
elif value_type == 'date':
print(61, datetime.strptime(str(cell_value), '%Y-%m-%d %H:%M:%S').date())
try:
cell_value = datetime.strptime(str(cell_value), '%Y-%m-%d %H:%M:%S').date()
except:
@ -66,7 +66,7 @@ def import_to_data(file_url, field_data, m2m_fields=None):
elif value_type == 'datetime':
cell_value = datetime.strptime(str(cell_value), '%Y-%m-%d %H:%M:%S')
else:
# 由于excel导入数字类型后会出现数字加 .0 的,进行处理
# 由于excel导入数字类型后会出现数字加 .0 的,进行处理
if type(cell_value) is float and str(cell_value).split(".")[1] == "0":
cell_value = int(str(cell_value).split(".")[0])
elif type(cell_value) is str:

View File

@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
from types import FunctionType, MethodType
from urllib.parse import quote
from django.db import transaction
@ -68,6 +69,8 @@ class ImportSerializerMixin:
:return:
"""
assert self.import_field_dict, "'%s' 请配置对应的导出模板字段。" % self.__class__.__name__
if isinstance(self.import_field_dict, MethodType) or isinstance(self.import_field_dict, FunctionType):
self.import_field_dict = self.import_field_dict()
# 导出模板
if request.method == "GET":
# 示例数据
@ -160,6 +163,8 @@ class ImportSerializerMixin:
assert self.import_field_dict, "'%s' 请配置对应的导入模板字段。" % self.__class__.__name__
assert self.import_serializer_class, "'%s' 请配置对应的导入序列化器。" % self.__class__.__name__
data = self.import_serializer_class(queryset, many=True, request=request).data
if isinstance(self.import_field_dict, MethodType) or isinstance(self.import_field_dict, FunctionType):
self.import_field_dict = self.import_field_dict()
# 导出excel 表
response = HttpResponse(content_type="application/msexcel")
response["Access-Control-Expose-Headers"] = f"Content-Disposition"

View File

@ -44,6 +44,7 @@
"vue": "2.7.14",
"vue-echarts": "^6.5.4",
"vue-grid-layout": "^2.4.0",
"vue-html2pdf": "^1.8.0",
"vue-i18n": "^8.15.1",
"vue-infinite-scroll": "^2.0.2",
"vue-router": "^3.6.5",

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,176 @@
<template>
<div>
<div style="position: absolute;z-index: 10000">
<el-button
type="success"
round
size="small"
v-if="downloadButtonShow"
:style="downloadButtonStyle"
@click="generateReport"
:loading="downloadLoading"
>{{ downloadButtonTitle }}
</el-button>
<el-button
type="primary"
round
size="small"
v-if="previewButtonShow"
style="position: fixed; right: 50px; bottom: 70px"
@click="previewPdf"
:loading="previewLoading"
>{{ previewButtonTitle }}
</el-button>
</div>
<vue-html2pdf
:show-layout="true"
:float-layout="false"
:enable-download="false"
:preview-modal="preview"
:filename="filename"
:paginate-elements-by-height="1100"
:pdf-quality="2"
pdf-format="a4"
pdf-orientation="portrait"
pdf-content-width="100%"
:manual-pagination="true"
:html-to-pdf-options="{ margin: [5, 5, 0, 5] }"
@beforeDownload="beforeDownload($event)"
ref="html2Pdf"
>
<section slot="pdf-content">
<slot></slot>
</section>
</vue-html2pdf>
</div>
</template>
<script>
import SongtiSCBlack from '@/assets/fonts/SongtiSCBlack'
import VueHtml2pdf from 'vue-html2pdf'
export default {
name: 'dvaHtml2pdf',
components: {
VueHtml2pdf
},
props: {
filename: { // pdf
type: String,
require: true
},
company: { //
type: String,
default: 'xxx '
},
//
downloadButtonShow: {
type: Boolean,
default: true
},
//
downloadButtonStyle: {
type: Object,
default () {
return {
position: 'fixed', right: '50px', bottom: '30px'
}
}
},
//
downloadButtonTitle: {
type: String,
default: '下载报告'
},
//
previewButtonShow: {
type: Boolean,
default: true
},
//
previewButtonStyle: {
type: Object,
default () {
return {
position: 'fixed', right: '50px', bottom: '70px'
}
}
},
//
previewButtonTitle: {
type: String,
default: '预览报告'
}
},
data () {
return {
preview: false,
downloadLoading: false,
previewLoading: false
}
},
created () {
},
mounted () {
},
methods: {
async beforeDownload ({ html2pdf, options, pdfContent }) {
if (this.preview) return
await html2pdf()
.set(options)
.from(pdfContent)
.toPdf()
.get('pdf')
.then((pdf) => {
const totalPages = pdf.internal.getNumberOfPages()
for (let i = 1; i <= totalPages; i++) {
pdf.setPage(i)
pdf.addFileToVFS('MyFont.ttf', SongtiSCBlack)
pdf.addFont('MyFont.ttf', 'MyFont', 'normal')
pdf.setFont('MyFont')
pdf.setFontSize(10)
pdf.setTextColor(150)
pdf.text(
'第 ' + i + '页 共 ' + totalPages + '页',
pdf.internal.pageSize.getWidth() * 0.45,
pdf.internal.pageSize.getHeight() - 2
)
pdf.text(
this.company,
pdf.internal.pageSize.getWidth() * 0.79,
pdf.internal.pageSize.getHeight() - 2
)
}
})
.save(this.filename)
},
generateReport () {
this.preview = false
this.downloadLoading = true
this.$nextTick(() => {
this.$refs.html2Pdf.generatePdf()
const _this = this
setTimeout(function () {
_this.downloadLoading = false
}, 4000)
})
},
previewPdf () {
this.preview = true
this.previewLoading = true
this.$nextTick(() => {
this.$refs.html2Pdf.generatePdf()
const _this = this
setTimeout(function () {
_this.previewLoading = false
}, 4000)
})
}
}
}
</script>
<style scoped>
</style>

View File

@ -1,7 +1,7 @@
import Vue from 'vue'
import d2Container from './d2-container'
import tableProgress from './table-progress/lib/table-progress.vue'
// 注意 有些组件使用异步加载会有影响
Vue.component('d2-container', d2Container)
Vue.component('d2-icon', () => import('./d2-icon'))
@ -11,3 +11,5 @@ Vue.component('foreignKey', () => import('./foreign-key/index.vue'))
Vue.component('manyToMany', () => import('./many-to-many/index.vue'))
Vue.component('d2p-tree-selector', () => import('./tree-selector/lib/tree-selector.vue'))
Vue.component('dept-format', () => import('./dept-format/lib/dept-format.vue'))
Vue.component('dvaHtml2pdf', () => import('./dvaHtml2pdf/index.vue'))
Vue.component('table-progress', tableProgress)

View File

@ -0,0 +1,20 @@
/*
* @创建文件时间: 2021-08-02 23:55:30
* @Auther: 猿小天
* @最后修改人: 猿小天
* @最后修改时间: 2021-08-08 12:27:45
* 联系Qq:1638245306
* @文件介绍:
*/
export default {
// 字段类型配置注册之后即可在crud.js中使用了
'selector-table': {
// 表单组件配置
form: { component: { name: 'selector-table-input', props: { color: 'danger' } } },
// 行组件配置
component: { name: 'values-format', props: {} },
// 行展示时居中
align: 'center'
// 您还可以写更多默认配置
}
}

View File

@ -0,0 +1,14 @@
import { d2CrudPlus } from 'd2-crud-plus'
import group from './group'
function install (Vue, options) {
Vue.component('selector-table-input', () => import('./selector-table'))
if (d2CrudPlus != null) {
// 注册字段类型`demo-extend`
d2CrudPlus.util.columnResolve.addTypes(group)
}
}
// 导出install
export default {
install
}

View File

@ -0,0 +1,397 @@
<template>
<div ref="selectedTableRef">
<el-popover
placement="bottom"
width="400"
trigger="click"
@show="visibleChange">
<div class="option">
<el-input style="margin-bottom: 10px" v-model="search" clearable placeholder="请输入关键词" @change="getDict"
@clear="getDict">
<el-button style="width: 100px" slot="append" icon="el-icon-search"></el-button>
</el-input>
<el-table
ref="tableRef"
:data="tableData"
size="mini"
border
:row-key="dict.value"
style="width: 400px"
max-height="200"
height="200"
:highlight-current-row="!_elProps.tableConfig.multiple"
@selection-change="handleSelectionChange"
@row-click="handleCurrentChange"
>
<el-table-column v-if="_elProps.tableConfig.multiple" fixed type="selection" reserve-selection width="55"/>
<el-table-column fixed type="index" label="#" width="50"/>
<el-table-column :prop="item.prop" :label="item.label" :width="item.width"
v-for="(item,index) in _elProps.tableConfig.columns" :key="index"/>
</el-table>
<el-pagination style="margin-top: 10px;max-width: 200px" background
small
:current-page="pageConfig.page"
:page-size="pageConfig.limit"
layout="prev, pager, next"
:total="pageConfig.total"
@current-change="handlePageChange"
/>
</div>
<div slot="reference" ref="divRef" :style="{'pointerEvents': disabled?'none':''}">
<div v-if="currentValue" class="div-input el-input__inner" :class="disabled?'div-disabled':''">
<div>
<el-tag
style="margin-right: 5px"
v-for="(item,index) in currentValue"
:key="index"
:closable="disabled"
size="small"
:hit="false"
type="info"
@close="itemClosed(item,index)"
disable-transitions
>
<span>{{ item[dict.label] }}</span>
</el-tag>
</div>
</div>
<el-input v-else placeholder="请选择" slot:reference :disabled="disabled"></el-input>
</div>
</el-popover>
</div>
</template>
<script>
import { request } from '@/api/service'
import XEUtils from 'xe-utils'
import { d2CrudPlus } from 'd2-crud-plus'
export default {
name: 'selector-table-input',
model: {
prop: 'value',
event: ['change', 'input']
},
mixins: [d2CrudPlus.input, d2CrudPlus.inputDict],
props: {
//
value: {
type: [String, Number, Array],
required: false,
default: ''
},
//
dict: {
type: Object,
require: false
},
//
elProps: {
type: Object,
require: false,
default () {
return {
tableConfig: {
multiple: false,
columns: []
}
}
}
},
// component.props
color: {
required: false
},
styleName: {
type: [Object, String],
required: false,
default () {
return {}
}
},
disabled: {
type: Boolean,
default: false
}
},
data () {
return {
// valueprops
currentValue: [],
pageConfig: {
page: 1,
limit: 5,
total: 0
},
search: null,
tableData: [],
multipleSelection: [],
collapseTags: false
}
},
computed: {
// computedvaluewatch
_elProps () {
return this.elProps
}
},
watch: {
value: {
handler (value, oldVal) {
// inputv-modelvalue
// watchvaluechange
// changevalueform-data-change
this.$emit('change', value)
this.$emit('input', value)
// currentValue
if (Array.isArray(value) && value.length === 0) {
this.currentValue = null
this.multipleSelection = null
} else {
if (value && this.dispatch) {
this.dispatch('ElFormItem', 'el.form.blur')
}
}
},
deep: true,
immediate: true
},
multipleSelection: {
handler (newValue, oldVal) {
const { tableConfig } = this._elProps
//
if (!tableConfig.multiple) {
this.currentValue = [newValue]
} else {
this.currentValue = newValue
}
},
deep: true,
immediate: true
}
// currentValue (newValue, oldVal) {
// const { tableConfig } = this._elProps
// const { value } = this.dict
// if (newValue) {
// if (!tableConfig.multiple) {
// if (newValue[0]) {
// this.$emit('input', newValue[0][value])
// this.$emit('change', newValue[0][value])
// }
// } else {
// console.log(newValue)
// const result = newValue.map((item) => {
// return item[value]
// })
// this.$emit('input', result)
// this.$emit('change', result)
// }
// }
// }
},
mounted () {
// currentValue
this.setCurrentValue(this.value)
},
methods: {
//
setCurrentValue (val) {
if (val.toString().length > 0) {
// value
const { url, value, label } = this.dict
const params = {}
params[value] = val
return request({
url: url,
params: params,
method: 'get'
}).then(res => {
const { data } = res.data
if (data && data.length > 0) {
this.currentValue = data
} else {
this.currentValue = null
}
})
} else {
this.currentValue = null
}
},
//
getDict () {
const that = this
let url
if (typeof that.dict.url === 'function') {
const form = that.d2CrudContext.getForm()
url = that.dict.url(that.dict, { form })
} else {
url = that.dict.url
}
let dictParams = {}
if (that.dict.params) {
dictParams = { ...that.dict.params }
}
const params = {
page: that.pageConfig.page,
limit: that.pageConfig.limit
}
if (that.search) {
params.search = that.search
params.page = 1
}
if (that._elProps.tableConfig.data === undefined || that._elProps.tableConfig.data.length === 0) {
request({
url: url,
method: 'get',
params: { ...params, ...dictParams }
}).then(res => {
const { data, page, limit, total } = res.data
that.pageConfig.page = page
that.pageConfig.limit = limit
that.pageConfig.total = total
if (that._elProps.tableConfig.isTree) {
that.tableData = XEUtils.toArrayTree(data, { parentKey: 'parent', key: 'id', children: 'children' })
} else {
that.tableData = data
}
})
} else {
that.tableData = that._elProps.tableConfig.data
}
},
/**
* 下拉框展开/关闭
* @param bool
*/
visibleChange () {
const that = this
that.getDict()
const { tableConfig } = that._elProps
if (tableConfig.multiple) {
that.$refs.tableRef.clearSelection() // ,
that.currentValue ? that.currentValue.forEach(item => {
that.$refs.tableRef.toggleRowSelection(item, true)
}) : null
}
},
/**
* 分页
* @param page
*/
handlePageChange (page) {
this.pageConfig.page = page
this.getDict()
},
/**
* 表格多选
* @param val:Array
*/
handleSelectionChange (val) {
this.multipleSelection = val
this.$emit('checkChange', val)
const result = val.map((item) => {
return item[this.dict.value]
})
this.$emit('input', result)
this.$emit('change', result)
},
/**
* 表格单选
* @param val:Object
*/
handleCurrentChange (val) {
const { tableConfig } = this._elProps
if (!tableConfig.multiple) {
this.multipleSelection = val
this.$emit('radioChange', val)
this.$emit('input', val[this.dict.value])
this.$emit('change', val[this.dict.value])
}
},
/***
* 清空
*/
onClear () {
const { tableConfig } = this._elProps
if (!tableConfig.multiple) {
this.$emit('input', '')
this.$emit('change', '')
} else {
this.$emit('input', [])
this.$emit('change', [])
}
},
/**
* tag删除事件
* @param obj
*/
itemClosed (obj, index) {
const { tableConfig } = this._elProps
XEUtils.remove(this.multipleSelection, index)
XEUtils.remove(this.currentValue, index)
if (!tableConfig.multiple) {
this.$emit('input', '')
this.$emit('change', '')
} else {
this.$refs.tableRef?.toggleRowSelection(obj, false)
// const { value } = this.dict
// const result = this.currentValue.map((item) => {
// return item[value]
// })
// this.$emit('input', result)
// this.$emit('change', result)
}
}
}
}
</script>
<style scoped>
.option {
height: auto;
line-height: 1;
padding: 5px;
background-color: #fff;
}
</style>
<style lang="scss">
.popperClass {
height: 320px;
}
.el-select-dropdown__wrap {
max-height: 310px !important;
}
.tableSelector {
.el-icon, .el-tag__close {
display: none;
}
}
.div-input {
-webkit-appearance: none;
background-color: #FFF;
background-image: none;
border-radius: 4px;
border: 1px solid #DCDFE6;
-webkit-box-sizing: border-box;
box-sizing: border-box;
color: #606266;
display: inline-block;
min-height: 40px;
line-height: 40px;
outline: 0;
padding: 0 15px;
-webkit-transition: border-color .2s cubic-bezier(.645, .045, .355, 1);
transition: border-color .2s cubic-bezier(.645, .045, .355, 1);
min-width: 120px;
}
.div-disabled{
background-color: #F5F7FA;
border-color: #E4E7ED;
color: #C0C4CC;
cursor: not-allowed;
pointer-events: none;
}
</style>

View File

@ -0,0 +1,12 @@
export default {
// 字段类型配置注册之后即可在crud.js中使用了
'table-progress': {
// 表单组件配置
form: { component: { name: 'form-input', props: { color: 'danger' } } },
// 行组件配置
component: { name: 'table-progress', props: {} },
// 行展示时居中
align: 'center'
// 您还可以写更多默认配置
}
}

View File

@ -0,0 +1,13 @@
import { d2CrudPlus } from 'd2-crud-plus'
import group from './group'
function install (Vue) {
Vue.component('table-progress', () => import('./lib/table-progress'))
if (d2CrudPlus != null) {
// 注册字段类型`demo-extend`
d2CrudPlus.util.columnResolve.addTypes(group)
}
}
export default {
install
}

View File

@ -0,0 +1,55 @@
<template>
<div>
<el-progress :percentage="currentValue" :color="setColor" :format="format"></el-progress>
</div>
</template>
<script>
//
//
export default {
name: 'table-progress',
props: {
// row.xxx
value: {
type: String || Number,
required: false
}
},
data () {
return {
currentValue: ''
}
},
watch: {
value (value) {
// this.$emit('change', value)
if (this.currentValue === value) {
return
}
this.setValue(value)
}
},
created () {
this.setValue(this.value)
},
methods: {
setValue (value) {
// value
this.currentValue = Number(this.value)
// key
},
setColor () {
if (this.value <= 50) {
return '#F56C6C'
} else if (this.value > 50 && this.value < 80) {
return '#E6A23C'
} else {
return '#67C23A'
}
},
format (percentage) {
return `${percentage}%`
}
}
}
</script>

View File

@ -104,7 +104,7 @@ export default {
},
grid: {
top: 40,
left: 40,
left: 50,
right: 65,
bottom: 75
},

View File

@ -95,7 +95,7 @@ export default {
},
grid: {
top: 40,
left: 40,
left: 50,
right: 65,
bottom: 60
},

View File

@ -98,7 +98,7 @@ export default {
},
grid: {
top: 40,
left: 40,
left: 50,
right: 65,
bottom: 60
},