新功能(快速搭建CRUD) & v1.1.1版本发布

新功能(一键创建app命令): 一键创建app,并注册到settings和urls中
修复BUG(用户信息):修复无法管理员更新用户信息BUG
功能变化(框架-分页器): 可全局自定义配置分页
新功能(前端框架):前端封装快速搭建CRUD
pull/31/MERGE v1.1.1
李强 2021-05-17 00:59:20 +08:00 committed by Gitee
commit dc58bad7de
27 changed files with 1183 additions and 40 deletions

View File

@ -285,7 +285,7 @@ REST_FRAMEWORK = {
),
'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.AutoSchema',
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'DEFAULT_PAGINATION_CLASS': 'vadmin.op_drf.pagination.Pagination',
'DEFAULT_FILTER_BACKENDS': ('django_filters.rest_framework.DjangoFilterBackend',),
'EXCEPTION_HANDLER': 'apps.vadmin.utils.exceptions.op_exception_handler',
}

View File

@ -1,31 +0,0 @@
from logging import StreamHandler, getLevelName
from logging.handlers import RotatingFileHandler
from typing import Optional, IO
class MyStreamHandler(StreamHandler):
def __init__(self, stream: Optional[IO[str]] = ...) -> None:
print(222)
super().__init__(stream)
def __repr__(self):
level = getLevelName(self.level)
name = getattr(self.stream, 'name', '')
# bpo-36015: name can be an int
name = str(name)
if name:
name += ' '
print(111)
return '<%s %s(%s)>' % (self.__class__.__name__, name, level)
class MyRotatingFileHandler(RotatingFileHandler):
def __init__(self, filename: str, mode: str = ..., maxBytes: int = ..., backupCount: int = ...,
encoding: Optional[str] = ..., delay: bool = ...) -> None:
print(4444)
super().__init__(filename, mode, maxBytes, backupCount, encoding, delay)
def __repr__(self):
level = getLevelName(self.level)
print(22)
return '<%s %s (%s)>' % (self.__class__.__name__, self.baseFilename, level)

View File

@ -45,7 +45,7 @@ class GenericAPIView(CustomAPIView):
filter_backends = api_settings.DEFAULT_FILTER_BACKENDS
# The style to use for queryset pagination.
pagination_class = Pagination
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
def get_queryset(self):
"""

View File

@ -0,0 +1,75 @@
import logging
import os
import shutil
from django.core.management.base import BaseCommand
logger = logging.getLogger(__name__)
from application.settings import BASE_DIR
class Command(BaseCommand):
"""
创建App命令:
python manage.py createapp app名
python manage.py createapp app01 app02 ...
"""
def add_arguments(self, parser):
parser.add_argument('app_name', nargs='*', type=str, )
def handle(self, *args, **options):
app_name = options.get('app_name')
for name in app_name:
app_path = os.path.join(BASE_DIR, "apps", name)
# 判断app是否存在
if os.path.exists(app_path):
print(f"创建失败App {name} 已存在!")
break
source_path = os.path.join(BASE_DIR, "apps", "vadmin", "template")
target_path = app_path
if not os.path.exists(target_path):
# 如果目标路径不存在原文件夹的话就创建
os.makedirs(target_path)
if os.path.exists(source_path):
# 如果目标路径存在原文件夹的话就先删除
shutil.rmtree(target_path)
shutil.copytree(source_path, target_path)
# 修改app中的apps 内容
content = f"""from django.apps import AppConfig
class {name.capitalize()}Config(AppConfig):
name = '{name}'
verbose_name = "{name}App"
"""
with open(os.path.join(app_path, "apps.py"), 'w', encoding='UTF-8') as f:
f.write(content)
f.close()
# 注册app到 settings.py 中
injection(os.path.join(BASE_DIR, "application", "settings.py"), f" 'apps.{name}',\n", "INSTALLED_APPS",
"]")
# 注册app到 urls.py 中
injection(os.path.join(BASE_DIR, "application", "urls.py"),
f" re_path(r'^{name}/', include('apps.{name}.urls')),\n", "urlpatterns = [",
"]")
print(f"创建 {name} App成功")
def injection(file_path, insert_content, startswith, endswith):
with open(file_path, "r+", encoding="utf-8") as f:
data = f.readlines()
with open(file_path, 'w', encoding='UTF-8') as f1:
is_INSTALLED_APPS = False
is_insert = False
for content in data:
# 判断文件是否 INSTALLED_APPS 开头
if not is_insert and content.startswith(startswith):
is_INSTALLED_APPS = True
if not is_insert and content.startswith(endswith) and is_INSTALLED_APPS:
# 给前一行插入数据
content = insert_content + content
is_insert = True
f1.writelines(content)

View File

@ -140,7 +140,7 @@ class PermissionModeMiddleware(MiddlewareMixin):
def process_view(self, request, view_func, view_args, view_kwargs):
# 判断环境变量中,是否为演示模式(正常可忽略此判断)
white_list = ['/admin/logout/', '/admin/login/']
white_list = ['/admin/logout/', '/admin/login/', '/admin/api-auth/login/']
if os.getenv('DEMO_ENV') and not request.method in ['GET', 'OPTIONS'] and request.path not in white_list:
return ErrorJsonResponse(data={}, msg=f'演示模式,不允许操作!')

View File

@ -327,7 +327,7 @@ class ImportSerializerMixin:
for ele in data:
# 获取 unique 字段
filter_dic = {i: ele.get(i) for i in list(set(self.import_field_data.keys()) & set(unique_list))}
instance = queryset.filter(**filter_dic).first()
instance = filter_dic and queryset.filter(**filter_dic).first()
if instance and not updateSupport:
continue
if not filter_dic:

View File

@ -29,7 +29,7 @@ def get_object_or_404(queryset, *filter_args, **filter_kwargs):
class GenericViewSet(ViewSetMixin, GenericAPIView):
extra_filter_backends = []
pagination_class = Pagination
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
filter_backends = [DjangoFilterBackend, OrderingFilter, SearchFilter, AdvancedSearchFilter]
view_logger_classes = (CustomerModelViewLogger,)

View File

@ -12,7 +12,7 @@ logger = logging.getLogger(__name__)
class Command(BaseCommand):
"""
项目初始化命令: python manage.py initialization
项目初始化命令: python manage.py init
"""
def customSql(self, sql_list, model_name, table_name, is_yes):

View File

@ -235,7 +235,7 @@ class UserProfileSerializer(CustomModelSerializer):
class Meta:
model = UserProfile
depth = 1
exclude = ('password', 'secret', 'user_permissions', 'groups', 'is_superuser', 'date_joined')
exclude = ('password', 'secret', 'user_permissions', 'groups', 'is_superuser', 'date_joined', 'creator')
class ExportUserProfileSerializer(CustomModelSerializer):

View File

@ -1,6 +1,6 @@
from django.apps import AppConfig
class PermissionConfig(AppConfig):
class SystemConfig(AppConfig):
name = 'vadmin.system'
verbose_name = "权限管理"
verbose_name = "系统管理"

View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class TemplateConfig(AppConfig):
name = 'vadmin.template'
verbose_name = "模板App"

View File

@ -0,0 +1,2 @@
# from ..models.xxx import Xxx

View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@ -0,0 +1 @@
urlpatterns = []

View File

@ -0,0 +1,63 @@
<!-- 部门选择器 -->
<template>
<div>
<treeselect v-model="dept_value" :options="deptTree" :multiple="multiple" :show-count="true"
:placeholder="placeholder" :disable-branch-nodes="disable_branch_nodes"/>
</div>
</template>
<script>
import Treeselect from '@riophae/vue-treeselect'
import "@riophae/vue-treeselect/dist/vue-treeselect.css";
import {treeselect} from '@/api/vadmin/permission/dept'
export default {
name: "DeptTree",
props: {
/* 选择器的内容 */
value: {type: Number | Array,},
/* 用于显示选项 */
placeholder: {type: String, default: "请选择归属部门",},
/* 是否多选 */
multiple: {type: Boolean, default: false,},
/* 是否只能选末级 */
disable_branch_nodes: {type: Boolean, default: false,},
},
components: {Treeselect},
data() {
return {
deptOptions: [],
deptTree: [],
dept_value: ''
}
},
watch: {
dept_value(newValue) {
this.$emit('update:value', newValue)
},
value: {
handler: function (newValue) {
this.dept_value = newValue
},
immediate: true
}
},
created() {
this.getTreeselect()
},
methods: {
/** 查询部门下拉树结构 */
getTreeselect() {
treeselect().then(response => {
this.deptOptions = response.data
this.deptTree = this.handleTree(response.data, 'id')
})
},
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,956 @@
<!--
@description: 强大的CRUD组件封装
-->
<template>
<div style="padding-left: 10px;padding-top: 10px;">
<div class="grid-content bg-purple">
<el-form :model="queryParams" ref="queryForm" :inline="true" label-width="90px">
<el-row>
<el-form-item v-if="value.search" :label="value.label" :prop="value.prop" v-for="(value,index) in fields"
:key="index">
<!-- date/option/bool/users/depts -->
<el-switch
v-if="value.type==='boolean'"
v-model="queryParams[value.prop]"
active-color="#13ce66"
inactive-color="#ff4949">
</el-switch>
<dept-tree ref="dept_tree" v-else-if="value.type==='depts'" :value.sync="queryParams[value.prop]"
style="width: 150px;"></dept-tree>
<users-tree ref="users_tree" v-else-if="value.type==='users'" :value.sync="queryParams[value.prop]"
style="width: 150px;"></users-tree>
<el-date-picker
v-else-if="value.type==='date'"
v-model="dateRange"
size="small"
style="width: 240px"
value-format="yyyy-MM-dd HH:mm:ss"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="['00:00:00', '23:59:59']"
></el-date-picker>
<el-select
v-else-if="value.type==='option' && value.option_key"
v-model="queryParams[value.prop]"
:placeholder="value.label"
clearable
size="small"
style="width: 240px"
>
<el-option
v-for="dict in DictsOptions[value.option_key]"
:key="dict.dictValue"
:label="dict.dictLabel"
:value="dict.dictValue"
/>
</el-select>
<el-input
v-else
v-model="queryParams[value.prop]"
:placeholder="value.label"
clearable
size="small"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
</el-row>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleSearchFormSubmit"></el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery"></el-button>
</el-form-item>
</el-form>
</div>
<el-row v-if="topLayout" style="margin-bottom: 20px">
<el-col v-if="topLayoutLeft" :span="18">
<el-row :gutter="10" class="mb8">
<el-col :span="1.5" v-for="(func,index) in funcs" :key="index">
<el-button
v-if="func.type==='add'"
type="primary"
plain
:icon="func.icon||'el-icon-plus'"
size="mini"
@click="handleAdd(func.api)"
v-hasPermi="func.permis"
>{{func.label}}
</el-button>
<el-button
v-else-if="func.type==='update'"
type="success"
plain
:disabled="multipleSelection.length!==1"
:icon="func.icon||'el-icon-edit'"
size="mini"
@click="handleUpdate(func.api,{})"
v-hasPermi="func.permis"
>{{func.label}}
</el-button>
<el-button
v-else-if="func.type==='delete'"
type="danger"
plain
:disabled="multipleSelection.length===0"
:icon="func.icon||'el-icon-delete'"
size="mini"
@click="handleDelete(func.api,{})"
v-hasPermi="func.permis"
>{{func.label}}
</el-button>
<el-button
v-else-if="func.type==='export'"
type="warning"
plain
:icon="func.icon||'el-icon-download'"
size="mini"
@click="handleExport(func.api)"
v-hasPermi="func.permis"
>{{func.label}}
</el-button>
<el-button
v-else-if="func.type==='import'"
type="info"
plain
:icon="func.icon||'el-icon-upload2'"
size="mini"
@click="handleImport(func.api)"
v-hasPermi="func.permis"
>{{func.label}}
</el-button>
</el-col>
</el-row>
</el-col>
<el-col v-if="topLayoutRight" :span="6">
<div class="grid-content bg-purple-light" style="text-align: right">
<slot name="tools"/>
<el-popover
placement="bottom"
width="200"
trigger="click">
<div style="width: 50px;">
<el-checkbox-group v-model="showFields">
<el-checkbox v-for="(field, index) in fields" :key="index" :label="field" :checked="field.show"
style="width: 100%" @change="handleSelectField($event, field)">{{ field.label }}
</el-checkbox>
</el-checkbox-group>
</div>
<el-button
slot="reference"
:size="$ELEMENT.size"
name="refresh"
type="info"
icon="el-icon-s-fold"
title="设置显示的字段"/>
</el-popover>
</div>
</el-col>
</el-row>
<el-table
v-loading="tableLoading"
ref="tableData"
:span-method="spanMethod"
:data="tableData"
:max-height="maxHeight"
:row-key="getRowKeys"
:stripe="stripe"
:fit="fit"
:border="border"
:empty-text="emptyText"
:highlight-current-row="highlightCurrentRow"
:show-overflow-tooltip="showOverflowTooltip"
@sort-change="handleSortChange"
@cell-click="handleCellClick"
@cell-dblclick="handleCellDbClick"
@header-click="handleHeaderClick"
@row-click="handleRowClick"
@row-dblclick="handleRowDblClick"
@selection-change="handleSelectionChange">
<el-table-column v-if="selection" :reserve-selection="true" type="selection" width="50"/>
<slot name="prependColumn"/>
<!--<el-table-column v-if="false" :index="getRowIndex" label="序号" type="index" width="50"/>-->
<template v-for="field in fields">
<el-table-column
v-if="field.show"
:key="field.prop"
:prop="field.prop"
:label="field.label"
:sortable="field.sortable"
:width="field.width || ''"
:sort-method="sortMethod"
show-overflow-tooltip>
<template slot-scope="scope">
<slot :name="field.prop" :values="scope.row" :prop="field.prop" :field="field">
<span v-html="formatColumnData(scope.row, field)"/>
</slot>
</template>
</el-table-column>
</template>
<el-table-column
label="操作"
align="center"
class-name="small-padding fixed-width"
v-if="hasPermi(getOperationPermis())"
>
<template slot-scope="scope">
<span v-for="(func,index) in funcs" :key="index">
<el-button
v-if="func.type==='select'"
size="mini"
type="text"
:icon="func.icon||'el-icon-view'"
@click="handleSelect(func.api,scope.row)"
v-hasPermi="func.permis"
>{{func.label}}</el-button>
&nbsp;
<el-button
v-if="func.type==='update'"
size="mini"
type="text"
:icon="func.icon||'el-icon-edit'"
@click="handleUpdate(func.api,scope.row)"
v-hasPermi="func.permis"
>{{func.label}}</el-button>
&nbsp;
<el-button
v-else-if="func.type==='delete'"
size="mini"
type="text"
:icon="func.icon||'el-icon-delete'"
@click="handleDelete(func.api,scope.row)"
v-hasPermi="func.permis"
>{{func.label}}</el-button>
</span>
</template>
</el-table-column>
<slot name="appendColumn"/>
<slot name="column"/>
</el-table>
<el-row>
<el-col v-if="selection" :span="6" style="margin-top: 20px">
<span>已选择:<span style="color: #ff00ff;font-weight: bold;">{{ multipleSelection.length }}</span></span>
<el-button v-show="multipleSelection.length" type="info" size="mini" title="清空多选"
@click="clearMultipleSelection">清空
</el-button>
</el-col>
<el-pagination
:current-page="pagination.page"
:page-size="pagination.page_size"
:total="pagination.total"
:page-sizes="paginationStyle.pageSizes || [10, 20, 50, 100]"
:disabled="tableLoading"
:small="paginationStyle.small || false"
:layout="paginationStyle.layout || 'total, sizes, prev, pager, next, jumper'"
background
class="right_pagination"
@size-change="handleChangePageSize"
@current-change="handleChangeCurrentPage"/>
</el-row>
<!-- 添加或修改参数配置对话框 -->
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
<el-form-item v-if="value.form" :label="value.label" :prop="value.prop" v-for="(value,index) in fields"
:key="index">
<!-- date/option/bool/users/depts -->
<el-switch
v-if="value.type==='boolean'"
v-model="form[value.prop]"
active-color="#13ce66"
inactive-color="#ff4949">
</el-switch>
<dept-tree ref="dept_tree" v-else-if="value.type==='depts'" :value.sync="form[value.prop]"
style="width: 200px;"></dept-tree>
<users-tree ref="users_tree" v-else-if="value.type==='users'" :value.sync="form[value.prop]"
style="width: 200px;"></users-tree>
<el-date-picker
v-else-if="value.type==='date'"
v-model="form[value.prop]"
type="date"
size="small"
style="width: 240px"
value-format="yyyy-MM-dd HH:mm:ss"
placeholder="选择日期">
</el-date-picker>
<el-select
v-else-if="value.type==='option' && value.option_key"
v-model="form[value.prop]"
:placeholder="value.label"
clearable
size="small"
style="width: 240px"
>
<el-option
v-for="dict in DictsOptions[value.option_key]"
:key="dict.dictValue"
:label="dict.dictLabel"
:value="dict.dictValue"
/>
</el-select>
<el-input
v-else
v-model="form[value.prop]"
:placeholder="value.label"
clearable
size="small"
/>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitForm" v-if="this.title!=='详情'"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</el-dialog>
<!-- 导入对话框 -->
<el-dialog :title="upload.title" :visible.sync="upload.open" width="400px" append-to-body>
<el-upload
ref="upload"
:limit="1"
accept=".xlsx, .xls"
:headers="upload.headers"
:action="upload.url + '?updateSupport=' + upload.updateSupport"
:disabled="upload.isUploading"
:on-progress="handleFileUploadProgress"
:on-success="handleFileSuccess"
:auto-upload="false"
drag
>
<i class="el-icon-upload"></i>
<div class="el-upload__text">
将文件拖到此处
<em>点击上传</em>
</div>
<div class="el-upload__tip" slot="tip">
<el-checkbox v-model="upload.updateSupport"/>
是否更新已经存在的数据
<el-link type="info" style="font-size:12px" @click="importTemplate"></el-link>
</div>
<div class="el-upload__tip" style="color:red" slot="tip">提示仅允许导入xlsxlsx格式文件</div>
</el-upload>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitFileForm"> </el-button>
<el-button @click="upload.open = false"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import moment from 'moment';
import * as Utils from '@/utils';
import {getToken} from '@/utils/auth'
export default {
name: 'ModelDisplay',
props: {
value: {
// tableData
type: Array,
default: () => []
},
spanMethod: {
type: Function,
default: null
},
// eslint-disable-next-line vue/require-prop-types
maxHeight: {
default: 700
},
stripe: {
type: Boolean,
default: true
},
fit: {
type: Boolean,
default: true
},
highlightCurrentRow: {
type: Boolean,
default: true
},
showOverflowTooltip: {
type: Boolean,
default: true
},
border: {
type: Boolean,
default: false
},
emptyText: {
type: String,
default: '暂无数据'
},
paginationParams: {
// , 使, 使
// :{ page: 'page', pageSize: 'pageSize', count: 'total',results: 'list' }
type: Object,
default: () => {
return {
page: 'page',
pageSize: 'page_size',
count: 'count',
results: 'results'
};
}
},
listApi: {
// method + url
type: Function,
default: null
},
topLayout: {
//
type: Array,
default: () => {
return ['left', 'right'];
}
},
fields: {
//
type: Array,
default: () => {
return [];
}
},
funcs: {
//
type: Array,
default: () => {
return [];
}
},
selection: {
// (, true)
type: Boolean,
default: true
},
params: {
// ,=+++
type: Object,
default: () => {
return {};
}
},
//
paginationStyle: {
type: Object,
default: () => {
return {};
}
},
//
pageSizes: {
type: Array,
default: () => {
return [10, 20, 50, 100, 500];
}
}
},
data() {
return {
showFields: [], //
searchForm: {
search: '',
ordering: ''
},
queryParams: {},
tableLoading: false,
tableData: [],
rowKey: '',
dateRange: [],
multipleSelection: [],
pagination: {
page: 1,
page_size: 10,
total: 0
},
//
form: {},
rules: this.getFormRules(),
open: false,
// api
submitFormApi: '',
// api
selectApi: '',
// api
importApi: '',
DictsOptions: {},
getRowKeys: row => {
if (this.rowKey) {
return row[this.rowKey];
}
return row.id || row.uuid;
},
title: '',
//
upload: {
//
open: false,
//
title: '',
//
isUploading: false,
//
updateSupport: 0,
//
headers: {Authorization: 'Bearer ' + getToken()},
//
url: process.env.VUE_APP_BASE_API + '/admin/system/savefile/'
},
};
},
computed: {
topLayoutLeft() {
return this.topLayout.indexOf('left') >= 0;
},
topLayoutRight() {
return this.topLayout.indexOf('right') >= 0;
},
},
watch: {
params: {
deep: true,
handler: function (newValue, oldValue) {
this.getTableData();
}
},
},
mounted() {
},
created() {
this.initComponentData();
this.initDictsOptions();
this.getTableData();
this.funcs.map(value => {
if (value.type === 'select') {
this.selectApi = value.api
}
})
},
methods: {
initComponentData() {
this.pagination.page_size = this.pageSizes[0];
this.fields.forEach(field => {
field.show = (!!field.show);
field.type = (field.type || 'string').toLocaleLowerCase();
if (field.type.startsWith('bool')) {
field.type = 'boolean';
}
field.label = field.label || field.prop;
field.search = (!!field.search);
field.sortable = (!!field.sortable);
if (field.ordering && field.ordering.startsWith('desc')) {
this.searchForm.ordering = `-${field.prop}`;
} else if (field.ordering && field.ordering.startsWith('asc')) {
this.searchForm.ordering = `${field.prop}`;
}
field.width = field.width || '';
if (field.type === 'choices') {
if (Utils.isArray(field.choices) && field.choices.length > 0) {
if (!Utils.isObj(field.choices[0])) {
field.choices = field.choices.map(value => {
return {
label: value,
value: value
};
});
}
}
}
field.unique = (!!field.unique);
if (field.unique) {
this.rowKey = field.prop;
}
});
},
formatColumnData(row, field) {
const type = field.type || 'string';
const prop = field.prop;
if (field.formatter && typeof field.formatter === 'function') {
return field.formatter(row, prop, type);
}
if (type === 'string') {
return row[prop];
} else if (type === 'datetime') {
return this.formatDatetime(row[prop]);
} else if (type === 'date') {
return this.formatDate(row[prop]);
} else if (type === 'time') {
return this.formatTime(row[prop]);
} else if (type === 'option') {
return this.formatOptions(field.option_key, row[prop]);
} else if (type === 'users') {
return this.formatUsers(row[prop]);
} else if (type === 'depts') {
return this.formatDepts(row[prop]);
} else if (type.startsWith('bool')) {
return row[prop] ? '是' : '否';
} else if (type === 'choices') {
const choices = field.choices;
return this.formatChoices(choices, row[prop]);
} else {
return row[prop];
}
},
formatChoices(choices, value) {
for (const choice of choices) {
if (choice.value === value) {
return choice.label;
}
}
return value;
},
formatDatetime(datetime) {
return moment(datetime).format('YYYY-MM-DD HH:mm:ss');
},
formatDate(date) {
return moment(date).format('YYYY-MM-DD');
},
formatTime(time) {
return moment(time).format('HH:mm:ss');
},
formatOptions(option_key, id) {
var data = this.DictsOptions[option_key]
if (!id || !data) return ""
for (var i = 0; i < data.length; i++) {
if (data[i].dictValue === id) {
return data[i].dictLabel
}
}
return ""
},
formatUsers(id) {
if (!id) return ""
var data = this.$refs.users_tree[0].usersOptions
for (var i = 0; i < data.length; i++) {
if (data[i].id === id) {
return data[i].label
}
}
return ""
},
formatDepts(id) {
if (!id) return ""
var data = this.$refs.dept_tree[0].deptOptions
for (var i = 0; i < data.length; i++) {
if (data[i].id === id) {
return data[i].label
}
}
return ""
},
sortMethod(a, b) {
return -1;
},
getTableData() {
this.listInterfaceData(this.getRequestParams());
return this.tableData;
},
getFormattedPaginationParams() {
const pageParamName = this.paginationParams.page;
const pageSizeParamName = this.paginationParams.pageSize;
const params = {};
params[pageParamName] = this.pagination.page;
params[pageSizeParamName] = this.pagination.page_size;
return params;
},
// ,
getRequestParams() {
//
const tmpParams = {...this.params, ...this.getFormattedPaginationParams(), ...this.queryParams};
const params = {};
for (const prop of Object.keys(tmpParams)) {
if (tmpParams[prop]) {
params[prop] = tmpParams[prop];
}
}
//
if (this.searchForm.search) {
params['search'] = this.searchForm.search;
}
//
if (this.searchForm.ordering) {
params['ordering'] = this.searchForm.ordering;
}
// console.dir(params);
return this.addDateRange(params, this.dateRange);
},
// ,
listInterfaceData(params) {
this.tableLoading = true;
this.listApi(params).then(response => {
this.tableLoading = false;
if (response.status === 'success') {
const resultsParamName = this.paginationParams.results;
const countParamName = this.paginationParams.count;
this.tableData = response.data[resultsParamName] || [];
this.pagination.total = response.data[countParamName] || 0;
} else {
this.$message.warning(response.msg || '获取接口信息失败!');
}
}).catch(error => {
this.tableLoading = false;
console.error(error);
});
},
/** 清空已选择 */
clearMultipleSelection() {
this.clearSelection();
},
/** 清空已选择 */
clearSelection() {
this.$refs.tableData.clearSelection();
},
handleSelectField(e, field) {
field.show = e;
},
// ,
handleSearchFormSubmit() {
this.pagination.page = 1;
this.getTableData();
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1;
this.getTableData();
},
/** 重置按钮操作 */
resetQuery() {
this.dateRange = [];
this.queryParams = {}
this.resetForm("queryForm");
this.handleQuery();
},
/** 获取Dicts值 */
initDictsOptions() {
this.fields.map(value => {
if (value.option_key) {
this.getDicts(value.option_key).then(response => {
this.DictsOptions[value.option_key] = response.data
})
}
})
},
//
handleSelectionChange(val) {
this.$emit('selection-change', val);
this.multipleSelection = val;
},
// ()
handleChangePageSize(val) {
this.pagination.page_size = val;
this.getTableData();
},
// ()
handleChangeCurrentPage(val) {
this.pagination.page = val;
this.getTableData();
},
handleSortChange(info) {
const {prop, order} = info;
if (!order) {
this.searchForm.ordering = '';
} else if (order.startsWith('desc')) {
this.searchForm.ordering = `-${prop}`;
} else {
this.searchForm.ordering = `${prop}`;
}
this.getTableData();
},
handleCellClick(row, column, cell, event) {
this.$emit('cell-click', row, column, cell, event);
},
handleCellDbClick(row, column, cell, event) {
this.$emit('cell-dblclick', row, column, cell, event);
},
handleRowClick(row, column, event) {
this.$emit('row-click', row, column, event);
},
handleRowDblClick(row, column, event) {
this.$emit('row-dblclick', row, column, event);
},
handleHeaderClick(column, event) {
this.$emit('header-click', column, event);
},
/**新增按钮*/
handleAdd(api) {
this.dateRange = [];
this.queryParams = {}
this.resetForm("queryForm");
this.open = true;
this.title = "新增";
this.submitFormApi = api
},
/**修改按钮*/
handleUpdate(api, row) {
this.dateRange = [];
this.queryParams = {}
this.resetForm("queryForm");
this.submitFormApi = api
const id = row.id || this.multipleSelection.map(item => item.id);
if (this.selectApi) {
this.selectApi(id).then(response => {
this.form = response.data;
this.open = true;
this.title = "修改";
});
} else {
this.open = true;
this.title = "修改";
}
},
/**修改按钮*/
handleSelect(api, row) {
this.dateRange = [];
this.queryParams = {}
this.resetForm("queryForm");
this.submitFormApi = api
const id = row.id || this.multipleSelection.map(item => item.id);
if (this.selectApi) {
this.selectApi(id).then(response => {
this.form = response.data;
this.open = true;
this.title = "详情";
});
} else {
this.open = true;
this.title = "详情";
}
},
/** 删除按钮操作 */
handleDelete(api, row) {
const ids = row.id || this.multipleSelection.map(item => item.id);
this.$confirm('是否确认删除编号为"' + ids + '"的数据项?', "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(function () {
return api(ids);
}).then(() => {
this.getTableData();
this.msgSuccess("删除成功");
})
},
/** 导出按钮操作 */
handleExport(api) {
const queryParams = this.queryParams;
this.$confirm('是否确认导出所有符合条件的数据项?', "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(function () {
return api(queryParams);
}).then(response => {
this.download(response.data.file_url, response.data.name);
})
},
/** 导入按钮操作 */
handleImport() {
this.upload.title = '导入'
this.upload.open = true
}, /** 获取表单校验 */
getFormRules() {
let dict = {}
this.fields.map(value => {
if (value.form) {
dict[value.prop] = [{
required: value.required,
message: value.rules_message || value.label + "不能为空",
trigger: value.trigger || "blur"
}]
}
})
console.log(2, dict)
return dict
},
/** 提交按钮 */
submitForm() {
this.$refs["form"].validate(valid => {
if (valid) {
if (this.form.id !== undefined) {
this.submitFormApi(this.form).then(response => {
this.msgSuccess("修改成功");
this.open = false;
this.getTableData();
});
} else {
this.submitFormApi(this.form).then(response => {
this.msgSuccess("新增成功");
this.open = false;
this.getTableData();
});
}
}
})
},
//
cancel() {
this.open = false;
},
//
getOperationPermis() {
let Permis = []
this.funcs.map(value => {
if (['update', 'delete', 'select'].indexOf(value.type) !== 0) {
Permis.push(value.permis)
}
})
return Permis
},
/** 下载模板操作 */
importTemplate() {
this.funcs.map(value => {
if (value.type === 'import') {
this.importApi = value
}
})
this.importApi.template_api().then(response => {
this.download(response.data.file_url, response.data.name)
})
},
//
handleFileUploadProgress(event, file, fileList) {
this.upload.isUploading = true
},
//
handleFileSuccess(response, file, fileList) {
this.funcs.map(value => {
if (value.type === 'import') {
this.importApi = value
}
})
this.upload.open = false
this.upload.isUploading = false
this.$refs.upload.clearFiles()
//
this.importApi.api({
file_url: response.data.file_url,
updateSupport: this.upload.updateSupport
}).then(response => {
this.$alert('导入成功!', '导入结果', {dangerouslyUseHTMLString: true})
this.getTableData()
})
},
//
submitFileForm() {
this.$refs.upload.submit()
}
}
};
</script>
<style scoped>
.picker {
width: 240px;
}
.el-pagination {
padding: 5px;
}
.right_pagination {
text-align: right;
padding-top: 20px;
}
</style>

View File

@ -0,0 +1,62 @@
<!-- 用户选择器 -->
<template>
<div>
<treeselect v-model="users_value" :options="usersOptions" :multiple="multiple" :show-count="true"
:placeholder="placeholder" :disable-branch-nodes="disable_branch_nodes"/>
</div>
</template>
<script>
import Treeselect from '@riophae/vue-treeselect'
import "@riophae/vue-treeselect/dist/vue-treeselect.css";
import {listUser} from '@/api/vadmin/permission/user'
export default {
name: "DeptTree",
props: {
/* 选择器的内容 */
value: {type: Number | Array,},
/* 用于显示选项 */
placeholder: {type: String, default: "请选择用户",},
/* 是否多选 */
multiple: {type: Boolean, default: false,},
/* 是否只能选末级 */
disable_branch_nodes: {type: Boolean, default: false,},
},
components: {Treeselect},
data() {
return {
usersOptions: [],
users_value: ''
}
},
watch: {
users_value(newValue) {
this.$emit('update:value', newValue)
},
value: {
handler: function(newValue) {
this.users_value = newValue
},
immediate: true
}
},
created() {
this.getTreeselect()
},
methods: {
/** 查询所有用户信息 **/
getTreeselect() {
listUser({pageNum: "all", _fields: "id,name"}).then(response => {
response.data.map(val => { val["label"] = val['name'] })
this.usersOptions = this.handleTree(response.data, 'id')
})
},
}
}
</script>
<style scoped>
</style>

View File

@ -30,6 +30,9 @@ import Pagination from "@/components/Pagination";
// 自定义表格工具扩展
import RightToolbar from "@/components/RightToolbar"
import SmallDialog from '@/components/SmallDialog';
import DeptTree from '@/components/DeptTree';
import UsersTree from '@/components/UsersTree';
import ModelDisplay from '@/components/ModelDisplay';
import CommonIcon from '@/components/CommonIcon';
import CommonStaticTable from '@/components/CommonStaticTable';
import {getCrontabData, getIntervalData} from "./utils/validate"; // 通用图标组件
@ -67,6 +70,9 @@ Vue.prototype.msgInfo = function (msg) {
}
// 自定义组件
Vue.component('small-dialog', SmallDialog);
Vue.component('dept-tree', DeptTree);
Vue.component('users-tree', UsersTree);
Vue.component('model-display', ModelDisplay);
// 全局组件挂载
Vue.component('Pagination', Pagination)
Vue.component('RightToolbar', RightToolbar)