Merge remote-tracking branch 'origin/master'

# Conflicts:
#	web/src/router/route.ts
This commit is contained in:
H0nGzA1
2023-02-20 16:04:38 +08:00
12 changed files with 1650 additions and 100 deletions

View File

@@ -5,6 +5,7 @@ from datetime import datetime, timedelta
from captcha.views import CaptchaStore, captcha_image from captcha.views import CaptchaStore, captcha_image
from django.contrib import auth from django.contrib import auth
from django.contrib.auth import login from django.contrib.auth import login
from django.contrib.auth.hashers import make_password, check_password
from django.shortcuts import redirect from django.shortcuts import redirect
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from drf_yasg import openapi from drf_yasg import openapi
@@ -54,25 +55,40 @@ class LoginSerializer(TokenObtainPairSerializer):
登录的序列化器: 登录的序列化器:
重写djangorestframework-simplejwt的序列化器 重写djangorestframework-simplejwt的序列化器
""" """
captcha = serializers.CharField( captcha = serializers.CharField(
max_length=6, required=False, allow_null=True, allow_blank=True max_length=6, required=False, allow_null=True, allow_blank=True
) )
class Meta: class Meta:
model = Users model = Users
fields = "__all__" fields = "__all__"
read_only_fields = ["id"] read_only_fields = ["id"]
default_error_messages = {"no_active_account": _("账号/密码错误")} class LoginView(TokenObtainPairView):
"""
登录接口
"""
serializer_class = LoginSerializer
permission_classes = []
def validate(self, attrs): def post(self, request, *args, **kwargs):
captcha = self.initial_data.get("captcha", None) # username可能携带的不止是用户名可能还是用户的其它唯一标识 手机号 邮箱
username = request.data.get('username',None)
if username is None:
return ErrorResponse(msg="参数错误")
password = request.data.get('password',None)
if password is None:
return ErrorResponse(msg="参数错误")
captcha = request.data.get('captcha',None)
if captcha is None:
return ErrorResponse(msg="参数错误")
captchaKey = request.data.get('captchaKey',None)
if captchaKey is None:
return ErrorResponse(msg="参数错误")
if dispatch.get_system_config_values("base.captcha_state"): if dispatch.get_system_config_values("base.captcha_state"):
if captcha is None: if captcha is None:
raise CustomValidationError("验证码不能为空") raise CustomValidationError("验证码不能为空")
self.image_code = CaptchaStore.objects.filter( self.image_code = CaptchaStore.objects.filter(
id=self.initial_data["captchaKey"] id=captchaKey
).first() ).first()
five_minute_ago = datetime.now() - timedelta(hours=0, minutes=5, seconds=0) five_minute_ago = datetime.now() - timedelta(hours=0, minutes=5, seconds=0)
if self.image_code and five_minute_ago > self.image_code.expiration: if self.image_code and five_minute_ago > self.image_code.expiration:
@@ -87,34 +103,36 @@ class LoginSerializer(TokenObtainPairSerializer):
else: else:
self.image_code and self.image_code.delete() self.image_code and self.image_code.delete()
raise CustomValidationError("图片验证码错误") raise CustomValidationError("图片验证码错误")
data = super().validate(attrs) try:
data["name"] = self.user.name # 手动通过 user 签发 jwt-token
data["userId"] = self.user.id user = Users.objects.get(username=username)
data["avatar"] = self.user.avatar except:
dept = getattr(self.user, 'dept', None) return DetailResponse(msg='该账号未注册')
# 获得用户后校验密码并签发token
if check_password(password,user.password):
return DetailResponse(msg='密码错误')
result = {
"name":user.name,
"userId":user.id,
"avatar":user.avatar,
}
dept = getattr(user, 'dept', None)
if dept: if dept:
data['dept_info'] = { result['dept_info'] = {
'dept_id': dept.id, 'dept_id': dept.id,
'dept_name': dept.name, 'dept_name': dept.name,
'dept_key': dept.key 'dept_key': dept.key
} }
role = getattr(self.user, 'role', None) role = getattr(user, 'role', None)
if role: if role:
data['role_info'] = role.values('id', 'name', 'key') result['role_info'] = role.values('id', 'name', 'key')
request = self.context.get("request") refresh = LoginSerializer.get_token(user)
request.user = self.user result["refresh"] = str(refresh)
result["access"] = str(refresh.access_token)
# 记录登录日志 # 记录登录日志
request.user = user
save_login_log(request=request) save_login_log(request=request)
return {"code": 2000, "msg": "请求成功", "data": data} return DetailResponse(data=result,msg="获取成功")
class LoginView(TokenObtainPairView):
"""
登录接口
"""
serializer_class = LoginSerializer
permission_classes = []
class LoginTokenSerializer(TokenObtainPairSerializer): class LoginTokenSerializer(TokenObtainPairSerializer):

View File

@@ -37,7 +37,7 @@ module.exports = {
'@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-redeclare': 'error', '@typescript-eslint/no-redeclare': 'error',
'@typescript-eslint/no-non-null-asserted-optional-chain': 'off', '@typescript-eslint/no-non-null-asserted-optional-chain': 'off',
'@typescript-eslint/no-unused-vars': [2], '@typescript-eslint/no-unused-vars': ['off'],
'vue/custom-event-name-casing': 'off', 'vue/custom-event-name-casing': 'off',
'vue/attributes-order': 'off', 'vue/attributes-order': 'off',
'vue/one-component-per-file': 'off', 'vue/one-component-per-file': 'off',
@@ -67,7 +67,8 @@ module.exports = {
'generator-star-spacing': 'off', 'generator-star-spacing': 'off',
'no-unreachable': 'off', 'no-unreachable': 'off',
'no-multiple-template-root': 'off', 'no-multiple-template-root': 'off',
'no-unused-vars': 'error', 'no-unused-vars': 'warn',
'vue/no-unused-vars': "off",
'no-v-model-argument': 'off', 'no-v-model-argument': 'off',
'no-case-declarations': 'off', 'no-case-declarations': 'off',
'no-console': 'error', 'no-console': 'error',

View File

@@ -18,11 +18,11 @@
<body> <body>
<div id="app"></div> <div id="app"></div>
<script type="text/javascript"> <script type="text/javascript">
var _hmt = _hmt || []; // let _hmt = _hmt || [];
(function () { (function () {
var hm = document.createElement('script'); let hm = document.createElement('script');
hm.src = 'https://hm.baidu.com/hm.js?d9c8b87d10717013641458b300c552e4'; hm.src = 'https://hm.baidu.com/hm.js?d9c8b87d10717013641458b300c552e4';
var s = document.getElementsByTagName('script')[0]; let s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(hm, s); s.parentNode.insertBefore(hm, s);
})(); })();
</script> </script>

View File

@@ -11,6 +11,7 @@
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "^2.0.10", "@element-plus/icons-vue": "^2.0.10",
"@fast-crud/fast-crud": "^1.9.0", "@fast-crud/fast-crud": "^1.9.0",
"@fast-crud/fast-extends": "^1.9.0",
"@fast-crud/ui-element": "^1.9.0", "@fast-crud/ui-element": "^1.9.0",
"@vitejs/plugin-vue-jsx": "^3.0.0", "@vitejs/plugin-vue-jsx": "^3.0.0",
"@wangeditor/editor": "^5.1.23", "@wangeditor/editor": "^5.1.23",

View File

@@ -0,0 +1,208 @@
<template>
<div>{{props.modelValue}}</div>
<el-select popper-class="popperClass" class="tableSelector" :multiple="props.tableConfig.isMultiple"
@remove-tag="removeTag" v-model="data" placeholder="请选择" @visible-change="visibleChange">
<template #empty>
<div class="option">
<el-input style="margin-bottom: 10px" v-model="search" clearable placeholder="请输入关键词" @change="getDict"
@clear="getDict">
<template #append>
<el-button type="primary" icon="Search"/>
</template>
</el-input>
<el-table
ref="tableRef"
:data="tableData"
size="mini"
border
row-key="id"
style="width: 400px"
max-height="200"
height="200"
:highlight-current-row="!props.tableConfig.isMultiple"
@selection-change="handleSelectionChange"
@current-change="handleCurrentChange"
>
<el-table-column v-if="props.tableConfig.isMultiple" fixed type="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 props.tableConfig.columns"/>
</el-table>
<el-pagination style="margin-top: 10px" background
v-model:current-page="pageConfig.page"
v-model:page-size="pageConfig.limit"
layout="prev, pager, next"
:total="pageConfig.total"
@current-change="handlePageChange"
/>
</div>
</template>
</el-select>
</template>
<script setup lang="ts">
import {defineProps, onMounted, reactive, ref, toRaw, watch} from 'vue'
import {dict} from '@fast-crud/fast-crud'
import XEUtils from "xe-utils";
import qs from 'qs'
const props = defineProps({
modelValue: {},
tableConfig: {
url: null,
label: null, //显示值
value: null, //数据值
isTree:false,
data: [],//默认数据
isMultiple: false, //是否多选
columns: [], //每一项对应的列表项
}
} as any)
const emit = defineEmits(['update:modelValue'])
// tableRef
const tableRef = ref()
// template上使用data
const data = ref()
// 多选值
const multipleSelection = ref()
watch(multipleSelection, // 监听multipleSelection的变化
(value) => {
const {tableConfig} = props
if (!tableConfig.isMultiple) {
data.value = value ? value[tableConfig.label] : null
} else {
const result = value ? value.map((item: any) => {
return item[tableConfig.label]
}) : null
data.value = result
}
}, // 当multipleSelection值触发后同步修改data.value的值
{immediate: true} // 立即触发一次给data赋值初始值
)
// 搜索值
const search = ref(undefined)
//表格数据
const tableData = ref()
// 分页的配置
const pageConfig = reactive({
page: 1,
limit: 10,
total: 0
})
/**
* 表格多选
* @param val:Array
*/
const handleSelectionChange = (val: any) => {
multipleSelection.value = val
const {tableConfig} = props
const result = val.map((item: any) => {
return item[tableConfig.value]
})
emit('update:modelValue', result)
}
/**
* 表格单选
* @param val:Object
*/
const handleCurrentChange = (val: any) => {
multipleSelection.value = val
const {tableConfig} = props
emit('update:modelValue', val[tableConfig.value])
}
/**
* 获取字典值
*/
const getDict = async () => {
const url = props.tableConfig.url
const params = {
page: pageConfig.page,
limit: pageConfig.limit,
search: search
}
const dicts = dict({url: url, params: params})
await dicts.reloadDict()
const dictData = dicts.data
const {data, page, limit, total} = dictData
pageConfig.page = page
pageConfig.limit = limit
pageConfig.total = total
if (props.tableConfig.data === undefined || props.tableConfig.data.length === 0) {
if(props.tableConfig.isTree){
tableData.value = XEUtils.toArrayTree(data,{parentKey: 'parent', key: 'id', children: 'children'})
}else{
tableData.value = data
}
} else {
tableData.value = props.tableConfig.data
}
}
/**
* 下拉框展开/关闭
* @param bool
*/
const visibleChange = (bool: any) => {
if (bool) {
getDict()
}
}
/**
* 分页
* @param page
*/
const handlePageChange = (page: any) => {
pageConfig.page = page
getDict()
}
const getDictByValue = async ()=>{
const url = props.tableConfig.url
const dicts = dict({url: url, params: {}})
await dicts.reloadDict()
const dictData = dicts.data
console.log(dictData)
return dictData
}
// 监听modelValue的变化更新数据
watch(()=>{
return props.modelValue
},(value)=>{
},{immediate: true})
</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;
}
}
</style>

View File

@@ -6,7 +6,9 @@ import { setLogger } from '@fast-crud/fast-crud';
// element // element
import ui from '@fast-crud/ui-element'; import ui from '@fast-crud/ui-element';
import { request } from '/@/utils/service'; import { request } from '/@/utils/service';
//扩展包
import {FsExtendsEditor} from "@fast-crud/fast-extends";
import "@fast-crud/fast-extends/dist/style.css";
export default { export default {
async install(app: any, options: any) { async install(app: any, options: any) {
// 先安装ui // 先安装ui
@@ -16,7 +18,7 @@ export default {
//i18n, //i18n配置可选默认使用中文具体用法请看demo里的 src/i18n/index.js 文件 //i18n, //i18n配置可选默认使用中文具体用法请看demo里的 src/i18n/index.js 文件
// 此处配置公共的dictRequest字典请求 // 此处配置公共的dictRequest字典请求
async dictRequest({ dict }: any) { async dictRequest({ dict }: any) {
return await request({ url: dict.url }); //根据dict的url异步返回一个字典数组 return await request({ url: dict.url,params:dict.params || {} }); //根据dict的url异步返回一个字典数组
}, },
//公共crud配置 //公共crud配置
commonOptions() { commonOptions() {
@@ -52,6 +54,12 @@ export default {
}; };
}, },
}); });
//富文本
app.use(FsExtendsEditor,{
wangEditor:{
width:300,
}
})
setLogger({ level: 'error' }); setLogger({ level: 'error' });
// 设置自动染色 // 设置自动染色
const dictComponentList = ['dict-cascader', 'dict-checkbox', 'dict-radio', 'dict-select', 'dict-switch', 'dict-tree']; const dictComponentList = ['dict-cascader', 'dict-checkbox', 'dict-radio', 'dict-select', 'dict-switch', 'dict-tree'];

View File

@@ -54,12 +54,6 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
align: 'center', align: 'center',
width: '70px', width: '70px',
columnSetDisabled: true, //禁止在列设置中选择 columnSetDisabled: true, //禁止在列设置中选择
formatter: (context) => {
//计算序号,你可以自定义计算规则,此处为翻页累加
let index = context.index ?? 1;
let pagination = crudExpose.crudBinding.value.pagination;
return ((pagination.currentPage ?? 1) - 1) * pagination.pageSize + index + 1;
},
}, },
}, },
search: { search: {

View File

@@ -204,6 +204,9 @@ export const createCrudOptions = function ({ crudExpose,menuButtonRef }: { crudE
}, },
placeholder: '请输入菜单名称' placeholder: '请输入菜单名称'
}, },
},
column:{
width:180,
} }
}, },
icon: { icon: {

View File

@@ -1,14 +1,16 @@
import { request } from '/@/utils/service'; import {request} from '/@/utils/service';
import { PageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud'; import {PageQuery, AddReq, DelReq, EditReq, InfoReq} from '@fast-crud/fast-crud';
export const apiPrefix = '/api/system/message_center/';
export const apiPrefix = '/api/system/dept/';
export function GetList(query: PageQuery) { export function GetList(query: PageQuery) {
return request({ return request({
url: apiPrefix, url: apiPrefix,
method: 'get', method: 'get',
data: query, params: query,
}); });
} }
export function GetObj(id: InfoReq) { export function GetObj(id: InfoReq) {
return request({ return request({
url: apiPrefix + id, url: apiPrefix + id,
@@ -36,6 +38,6 @@ export function DelObj(id: DelReq) {
return request({ return request({
url: apiPrefix + id + '/', url: apiPrefix + id + '/',
method: 'delete', method: 'delete',
data: { id }, data: {id},
}); });
} }

View File

@@ -1,23 +1,26 @@
import * as api from "./api"; import * as api from "./api";
import { dict, PageQuery, AddReq, DelReq, EditReq, CrudExpose, CrudOptions, } from "@fast-crud/fast-crud"; import {dict, compute, PageQuery, AddReq, DelReq, EditReq, CrudExpose, CrudOptions,} from "@fast-crud/fast-crud";
import { request } from "/@/utils/service"; import {request} from "/@/utils/service";
import { dictionary } from "/@/utils/dictionary"; import {dictionary} from "/@/utils/dictionary";
import tableSelector from "/@/components/tableSelector/index.vue"
import {shallowRef} from "vue";
interface CreateCrudOptionsTypes { interface CreateCrudOptionsTypes {
crudOptions: CrudOptions; crudOptions: CrudOptions;
} }
export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExpose }): CreateCrudOptionsTypes { export const createCrudOptions = function ({crudExpose}: { crudExpose: CrudExpose }): CreateCrudOptionsTypes {
const pageRequest = async (query: PageQuery) => { const pageRequest = async (query: PageQuery) => {
return await api.GetList(query); return await api.GetList(query);
}; };
const editRequest = async ({ form, row }: EditReq) => { const editRequest = async ({form, row}: EditReq) => {
form.id = row.id; form.id = row.id;
return await api.UpdateObj(form); return await api.UpdateObj(form);
}; };
const delRequest = async ({ row }: DelReq) => { const delRequest = async ({row}: DelReq) => {
return await api.DelObj(row.id); return await api.DelObj(row.id);
}; };
const addRequest = async ({ form }: AddReq) => { const addRequest = async ({form}: AddReq) => {
return await api.AddObj(form); return await api.AddObj(form);
}; };
return { return {
@@ -29,23 +32,230 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
delRequest delRequest
}, },
columns: { columns: {
_index: {
title: '序号', id: {
form: { show: false }, title: 'id',
column: { form: {
//type: 'index', show: false
align: 'center', }
width: '70px',
columnSetDisabled: true, //禁止在列设置中选择
formatter: (context) => {
//计算序号,你可以自定义计算规则,此处为翻页累加
let index = context.index ?? 1;
let pagination = crudExpose.crudBinding.value.pagination;
return ((pagination.currentPage ?? 1) - 1) * pagination.pageSize + index + 1;
},
}, },
title: {
title: '标题',
search: {
disabled: false
}, },
type: ["text", "colspan"],
form: {
rules: [ // 表单校验规则
{
required: true,
message: '必填项'
}
],
component: {span: 24, placeholder: '请输入标题'}
} }
}, },
is_read: {
title: '是否已读',
type: 'dict-select',
column: {
show: false
},
dict: dict({
data: [
{label: '已读', value: true, color: 'success'},
{label: '未读', value: false, color: 'danger'}
]
}),
form: {
show: false
}
},
target_type: {
title: '目标类型',
type: ['dict-radio', 'colspan'],
width: 120,
// show() {
// return vm.tabActivted === 'send'
// },
dict: dict({
data: [{value: 0, label: '按用户'}, {value: 1, label: '按角色'}, {
value: 2,
label: '按部门'
}, {value: 3, label: '通知公告'}]
}),
form: {
component: {
optionName: "el-radio-button"
},
rules: [
{
required: true,
message: '必选项',
trigger: ['blur', 'change']
}
]
}
},
target_user: {
title: '目标用户',
search: {
disabled: true
},
width: 130,
disabled: true,
form: {
component: {
name: shallowRef(tableSelector),
vModel: "modelValue",
tableConfig: {
url: '/api/system/user/',
label: 'name',
value: 'id',
isMultiple: true,
columns: [{
prop: 'name',
label: '用户名称',
width: 120
}, {
prop: 'phone',
label: '用户电话',
width: 120
}]
}
},
show: compute(({form}) => {
return form.target_type === 0
}),
rules: [ // 表单校验规则
{
required: true,
message: '必填项'
}
],
},
component: {
name: 'manyToMany',
valueBinding: 'user_info',
children: 'name'
}
},
target_role: {
title: '目标角色',
search: {
disabled: true
},
disabled: true,
width: 130,
form: {
component: {
name: shallowRef(tableSelector),
vModel: "modelValue",
tableConfig: {
url: '/api/system/role/',
label: 'name',
value: 'id',
isMultiple: true,
columns: [{
prop: 'name',
label: '角色名称'
},
{
prop: 'key',
label: '权限标识'
}]
}
},
show: compute(({form}) => {
return form.target_type === 1
}),
rules: [ // 表单校验规则
{
required: true,
message: '必填项'
}
]
},
component: {
name: 'manyToMany',
valueBinding: 'role_info',
children: 'name'
}
},
target_dept: {
title: '目标部门',
search: {
disabled: true
},
width: 130,
type: 'table-selector',
form: {
component: {
name: shallowRef(tableSelector),
vModel: "modelValue",
tableConfig: {
url: '/api/system/dept/all_dept/',
label: 'name',
value: 'id',
isTree: true,
isMultiple: true,
columns: [{
prop: 'name',
label: '部门名称'
},
{
prop: 'status_label',
label: '状态'
},
{
prop: 'parent_name',
label: '父级部门'
}]
}
},
show: compute(({form}) => {
return form.target_type === 2
}),
rules: [ // 表单校验规则
{
required: true,
message: '必填项'
}
]
}
},
content: {
title: '内容',
column: {
width: 300,
show: false
},
type: ["editor-wang5", "colspan"],
form: {
rules: [ // 表单校验规则
{
required: true,
message: '必填项'
}
],
component: {
// disabled: compute(({form}) => {
// return form.disabled;
// }),
id: "1", // 当同一个页面有多个editor时需要配置不同的id
config: {},
uploader: {
type: "form",
buildUrl(res: any) {
return res.url;
}
}
}
}
}
}
}
} }
} }

View File

@@ -1,23 +1,23 @@
<template> <template>
<fs-page> <fs-page>
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud> <fs-crud ref="crudRef" v-bind="crudBinding"></fs-crud>
</fs-page> </fs-page>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, onMounted } from 'vue'; import {ref, onMounted} from 'vue';
import { useExpose, useCrud } from '@fast-crud/fast-crud'; import {useExpose, useCrud} from '@fast-crud/fast-crud';
import { createCrudOptions } from './crud'; import {createCrudOptions} from './crud';
// crud组件的ref // crud组件的ref
const crudRef = ref(); const crudRef = ref();
// crud 配置的ref // crud 配置的ref
const crudBinding = ref(); const crudBinding = ref();
// 暴露的方法 // 暴露的方法
const { crudExpose } = useExpose({ crudRef, crudBinding }); const {crudExpose} = useExpose({crudRef, crudBinding});
// 你的crud配置 // 你的crud配置
const { crudOptions } = createCrudOptions({ crudExpose }); const {crudOptions} = createCrudOptions({crudExpose});
// 初始化crud配置 // 初始化crud配置
const { resetCrudOptions } = useCrud({ crudExpose, crudOptions }); const {resetCrudOptions} = useCrud({crudExpose, crudOptions});
// 页面打开后获取列表数据 // 页面打开后获取列表数据
onMounted(() => { onMounted(() => {

File diff suppressed because it is too large Load Diff