From e36eada496937ec86d32e3c0bf44474e4c2ab58a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=9D=8E=E5=BC=BA?= <1206709430@qq.com>
Date: Fri, 8 Apr 2022 10:03:34 +0800
Subject: [PATCH 01/25] =?UTF-8?q?=E4=BC=98=E5=8C=96=EF=BC=9A=E8=8E=B7?=
=?UTF-8?q?=E5=8F=96=E7=9C=9F=E5=AE=9Eip?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docker_env/nginx/my.conf | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/docker_env/nginx/my.conf b/docker_env/nginx/my.conf
index c6a4fb4..8ab724f 100644
--- a/docker_env/nginx/my.conf
+++ b/docker_env/nginx/my.conf
@@ -7,6 +7,8 @@ server {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
+ set_real_ip_from 177.7.0.0/16;
+ real_ip_header X-Forwarded-For;
root /usr/share/nginx/html;
index index.html index.php index.htm;
}
@@ -16,6 +18,8 @@ server {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
+ set_real_ip_from 177.7.0.0/16;
+ real_ip_header X-Forwarded-For;
rewrite ^/api/(.*)$ /$1 break; #重写
proxy_pass http://177.7.0.12:8000/; # 设置代理服务器的协议和地址
}
From 909efce9e39a032eb701486fa96dfca6e24a3532 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=9D=8E=E5=BC=BA?= <1206709430@qq.com>
Date: Sat, 9 Apr 2022 13:40:59 +0800
Subject: [PATCH 02/25] =?UTF-8?q?=E4=BC=98=E5=8C=96=EF=BC=9A=E9=85=8D?=
=?UTF-8?q?=E7=BD=AE=E6=96=87=E6=A1=A3=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 6 +++---
backend/conf/env.example.py | 5 +++++
web/src/store/modules/d2admin/modules/releases.js | 1 +
3 files changed, 9 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index a11b659..4640cc7 100644
--- a/README.md
+++ b/README.md
@@ -31,9 +31,9 @@
👩👧👦演示地址:[http://demo.django-vue-admin.com](http://demo.django-vue-admin.com)
- 账号:superadmin
+- 账号:superadmin
- 密码:superadmin123456
+- 密码:admin123456
👩👦👦文档地址:[https://django-vue-admin.com](https://django-vue-admin.com)
@@ -130,7 +130,7 @@ npm run dev
8. 启动项目
python3 manage.py runserver 0.0.0.0:8000
或使用 daphne :
- daphne -b 0.0.0.0 -8000 application.asgi:application
+ daphne -b 0.0.0.0 -p 8000 application.asgi:application
~~~
### 访问项目
diff --git a/backend/conf/env.example.py b/backend/conf/env.example.py
index c19f567..2d43149 100644
--- a/backend/conf/env.example.py
+++ b/backend/conf/env.example.py
@@ -10,6 +10,11 @@ from application.settings import BASE_DIR
DATABASE_ENGINE = "django.db.backends.sqlite3"
# 数据库名
DATABASE_NAME = os.path.join(BASE_DIR, 'db.sqlite3')
+
+# 使用mysql时,改为此配置
+# DATABASE_ENGINE = "django.db.backends.mysql"
+# DATABASE_NAME = 'django-vue-admin' # mysql 时使用
+
# 数据库地址 改为自己数据库地址
DATABASE_HOST = "127.0.0.1"
# 数据库端口
diff --git a/web/src/store/modules/d2admin/modules/releases.js b/web/src/store/modules/d2admin/modules/releases.js
index d4eb17d..6013da8 100644
--- a/web/src/store/modules/d2admin/modules/releases.js
+++ b/web/src/store/modules/d2admin/modules/releases.js
@@ -21,6 +21,7 @@ export default {
console.log('演示地址:https://demo.django-vue-admin.com')
console.log('社区地址:https://bbs.django-vue-admin.com')
console.log('文档地址:https://www.django-vue-admin.com')
+ console.log('前端配置文档地址:https://d2.pub/zh/doc/d2-crud-v2')
console.log('请不要吝啬您的 star,谢谢 ~')
}
}
From 71198542333ca681b369cffa927bc16d00ba9275 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=9D=8E=E5=BC=BA?= <1206709430@qq.com>
Date: Sun, 10 Apr 2022 00:06:57 +0800
Subject: [PATCH 03/25] =?UTF-8?q?=E4=BC=98=E5=8C=96=EF=BC=9A=E5=88=9D?=
=?UTF-8?q?=E5=A7=8B=E5=8C=96plugins=E6=8F=92=E4=BB=B6=E8=B7=AF=E5=BE=84?=
=?UTF-8?q?=E5=88=B0=E7=8E=AF=E5=A2=83=E5=8F=98=E9=87=8F=E4=B8=AD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
backend/application/settings.py | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/backend/application/settings.py b/backend/application/settings.py
index f668410..8c60b21 100644
--- a/backend/application/settings.py
+++ b/backend/application/settings.py
@@ -28,7 +28,10 @@ from conf.env import *
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure--z8%exyzt7e_%i@1+#1mm=%lb5=^fx_57=1@a+_y7bg5-w%)sm'
-sys.path.insert(0, os.path.join(BASE_DIR, 'plugins'))
+# 初始化plugins插件路径到环境变量中
+PLUGINS_PATH = os.path.join(BASE_DIR, 'plugins')
+[sys.path.insert(0, os.path.join(PLUGINS_PATH, ele)) for ele in os.listdir(PLUGINS_PATH) if
+ os.path.isdir(os.path.join(PLUGINS_PATH, ele)) and not ele.startswith('__')]
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = locals().get('DEBUG', True)
From 8340ef2d5657d45a8f2df2e19decc4acf55e3711 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=9D=8E=E5=BC=BA?= <1206709430@qq.com>
Date: Sun, 10 Apr 2022 00:37:56 +0800
Subject: [PATCH 04/25] =?UTF-8?q?=E9=87=8D=E6=9E=84:=20=E5=89=8D=E7=AB=AF?=
=?UTF-8?q?=E4=BC=98=E5=8C=96=E6=8F=92=E4=BB=B6=E5=8F=8A=E7=AC=AC=E4=B8=89?=
=?UTF-8?q?=E6=96=B9=E7=99=BB=E5=BD=95=E6=8F=92=E4=BB=B6=E5=85=BC=E5=AE=B9?=
=?UTF-8?q?=E6=80=A7=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
web/src/router/index.js | 2 +-
web/src/router/routes.js | 13 ++++++--
.../store/modules/d2admin/modules/account.js | 30 +++++++++----------
web/src/views/plugins/index.js | 10 +++++++
4 files changed, 36 insertions(+), 19 deletions(-)
diff --git a/web/src/router/index.js b/web/src/router/index.js
index ff3c84c..7ba6992 100644
--- a/web/src/router/index.js
+++ b/web/src/router/index.js
@@ -41,7 +41,7 @@ const router = new VueRouter({
*/
router.beforeEach(async (to, from, next) => {
// 白名单
- const whiteList = ['/login', '/auth-redirect', '/bind', '/register']
+ const whiteList = ['/login', '/auth-redirect', '/bind', '/register', '/thirdPartyLogin']
// 确认已经加载多标签页数据 https://github.com/d2-projects/d2-admin/issues/201
await store.dispatch('d2admin/page/isLoaded')
// 确认已经加载组件尺寸设置 https://github.com/d2-projects/d2-admin/issues/198
diff --git a/web/src/router/routes.js b/web/src/router/routes.js
index 78dfcb2..76a47c2 100644
--- a/web/src/router/routes.js
+++ b/web/src/router/routes.js
@@ -1,5 +1,5 @@
import layoutHeaderAside from '@/layout/header-aside'
-
+import { checkPlugins } from '@/views/plugins/index.js'
// 由于懒加载页面太多的话会造成webpack热更新太慢,所以开发环境不使用懒加载,只有生产环境使用懒加载
const _import = require('@/libs/util.import.' + process.env.NODE_ENV)
@@ -186,7 +186,16 @@ const frameOut = [
component: _import('system/login')
}
]
-
+/**
+ * 第三方登录
+ */
+if (checkPlugins('third-party-login')) {
+ frameOut.push({
+ path: '/thirdPartyLogin',
+ name: 'login',
+ component: _import('plugins/third-party-login/src/login/index')
+ })
+}
/**
* 错误页面
*/
diff --git a/web/src/store/modules/d2admin/modules/account.js b/web/src/store/modules/d2admin/modules/account.js
index 2a24872..e01badb 100644
--- a/web/src/store/modules/d2admin/modules/account.js
+++ b/web/src/store/modules/d2admin/modules/account.js
@@ -18,22 +18,20 @@ export default {
/**
* @description 登录
* @param {Object} context
- * @param {Object} payload username {String} 用户账号
- * @param {Object} payload password {String} 密码
- * @param {Object} payload route {Object} 登录成功后定向的路由对象 任何 vue-router 支持的格式
- */
- async login ({ dispatch }, {
- username = '',
- password = '',
- captcha = '',
- captchaKey = ''
- } = {}) {
- let res = await SYS_USER_LOGIN({
- username,
- password,
- captcha,
- captchaKey
- })
+ * @param {Object} data
+ * @param {Object} data username {String} 用户账号
+ * @param {Object} data password {String} 密码
+ * @param {Object} data route {Object} 登录成功后定向的路由对象 任何 vue-router 支持的格式
+ * @param {Object} data request function 请求方法
+ */
+ async login ({ dispatch }, data) {
+ let request = data.request
+ if (request) {
+ delete data.request
+ } else {
+ request = SYS_USER_LOGIN
+ }
+ let res = await request(data)
// 设置 cookie 一定要存 uuid 和 token 两个 cookie
// 整个系统依赖这两个数据进行校验和存储
// uuid 是用户身份唯一标识 用户注册的时候确定 并且不可改变 不可重复
diff --git a/web/src/views/plugins/index.js b/web/src/views/plugins/index.js
index ee67b16..e6e56d6 100644
--- a/web/src/views/plugins/index.js
+++ b/web/src/views/plugins/index.js
@@ -1,3 +1,5 @@
+import Vue from 'vue'
+
function importAll (r) {
const __modules = []
r.keys().forEach(key => {
@@ -8,10 +10,18 @@ function importAll (r) {
return __modules
}
+export const checkPlugins = async function install (pluginName) {
+ if (!window.pluginsAll) {
+ plugins(Vue)
+ }
+ return (window.pluginsAll && window.pluginsAll.indexOf(pluginName) !== -1)
+}
+
export const plugins = async function install (Vue, options) {
// 查找 src/views/plugins 目录所有插件,插件目录下需有 index.js 文件
// 再查找 node_modules/@great-dream/ 目录下所有插件
// 进行去重并vue注册导入
+ if (window.pluginsAll) return
let components = []
components = components.concat(importAll(require.context('./', true, /index\.js$/)))
components = components.concat(importAll(require.context('@great-dream/', true, /index\.js$/)))
From 7bbb33344fc6cb17524bc5440a370af9b31c2500 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=9D=8E=E5=BC=BA?= <1206709430@qq.com>
Date: Sun, 10 Apr 2022 18:28:05 +0800
Subject: [PATCH 05/25] =?UTF-8?q?=E4=BC=98=E5=8C=96:=20=E5=89=8D=E7=AB=AF?=
=?UTF-8?q?=E6=8F=92=E4=BB=B6=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
web/src/router/routes.js | 7 ++++---
web/src/views/plugins/index.js | 17 ++++++++++++-----
2 files changed, 16 insertions(+), 8 deletions(-)
diff --git a/web/src/router/routes.js b/web/src/router/routes.js
index 76a47c2..5456741 100644
--- a/web/src/router/routes.js
+++ b/web/src/router/routes.js
@@ -2,7 +2,7 @@ import layoutHeaderAside from '@/layout/header-aside'
import { checkPlugins } from '@/views/plugins/index.js'
// 由于懒加载页面太多的话会造成webpack热更新太慢,所以开发环境不使用懒加载,只有生产环境使用懒加载
const _import = require('@/libs/util.import.' + process.env.NODE_ENV)
-
+const pluginImport = require('@/libs/util.import.plugin')
/**
* 在主框架内显示
*/
@@ -189,11 +189,12 @@ const frameOut = [
/**
* 第三方登录
*/
-if (checkPlugins('third-party-login')) {
+const pluginsType = checkPlugins('third-party-login')
+if (pluginsType) {
frameOut.push({
path: '/thirdPartyLogin',
name: 'login',
- component: _import('plugins/third-party-login/src/login/index')
+ component: pluginsType === 'local' ? _import('plugins/third-party-login/src/login/index') : pluginImport('third-party-login/src/login/index')
})
}
/**
diff --git a/web/src/views/plugins/index.js b/web/src/views/plugins/index.js
index e6e56d6..91b1d41 100644
--- a/web/src/views/plugins/index.js
+++ b/web/src/views/plugins/index.js
@@ -1,5 +1,3 @@
-import Vue from 'vue'
-
function importAll (r) {
const __modules = []
r.keys().forEach(key => {
@@ -11,10 +9,19 @@ function importAll (r) {
}
export const checkPlugins = async function install (pluginName) {
- if (!window.pluginsAll) {
- plugins(Vue)
+ let pluginsList
+ pluginsList = importAll(require.context('./', true, /index\.js$/))
+ if (pluginsList && pluginsList.indexOf(pluginName) !== -1) {
+ // 本地插件
+ return 'local'
}
- return (window.pluginsAll && window.pluginsAll.indexOf(pluginName) !== -1)
+ pluginsList = importAll(require.context('@great-dream/', true, /index\.js$/))
+ if (pluginsList && pluginsList.indexOf(pluginName) !== -1) {
+ // node_modules 封装插件
+ return 'plugins'
+ }
+ // 未找到插件
+ return undefined
}
export const plugins = async function install (Vue, options) {
From 44fc7e934a085a158cdd8a0b7ed2a6d0ab01695c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=9D=8E=E5=BC=BA?= <1206709430@qq.com>
Date: Sun, 10 Apr 2022 19:13:19 +0800
Subject: [PATCH 06/25] =?UTF-8?q?=E4=BC=98=E5=8C=96:=20=E5=89=8D=E7=AB=AF?=
=?UTF-8?q?=E6=8F=92=E4=BB=B6=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
web/src/router/index.js | 2 +-
web/src/router/routes.js | 6 +++---
web/src/views/plugins/index.js | 25 +++++++++++++++++++++----
3 files changed, 25 insertions(+), 8 deletions(-)
diff --git a/web/src/router/index.js b/web/src/router/index.js
index 7ba6992..ba1761a 100644
--- a/web/src/router/index.js
+++ b/web/src/router/index.js
@@ -41,7 +41,7 @@ const router = new VueRouter({
*/
router.beforeEach(async (to, from, next) => {
// 白名单
- const whiteList = ['/login', '/auth-redirect', '/bind', '/register', '/thirdPartyLogin']
+ const whiteList = ['/login', '/auth-redirect', '/bind', '/register', '/oauth2']
// 确认已经加载多标签页数据 https://github.com/d2-projects/d2-admin/issues/201
await store.dispatch('d2admin/page/isLoaded')
// 确认已经加载组件尺寸设置 https://github.com/d2-projects/d2-admin/issues/198
diff --git a/web/src/router/routes.js b/web/src/router/routes.js
index 5456741..51916e0 100644
--- a/web/src/router/routes.js
+++ b/web/src/router/routes.js
@@ -189,12 +189,12 @@ const frameOut = [
/**
* 第三方登录
*/
-const pluginsType = checkPlugins('third-party-login')
+const pluginsType = checkPlugins('dvadmin-oauth2-web')
if (pluginsType) {
frameOut.push({
- path: '/thirdPartyLogin',
+ path: '/oauth2',
name: 'login',
- component: pluginsType === 'local' ? _import('plugins/third-party-login/src/login/index') : pluginImport('third-party-login/src/login/index')
+ component: pluginsType === 'local' ? _import('plugins/dvadmin-oauth2-web/src/login/index') : pluginImport('dvadmin-oauth2-web/src/login/index')
})
}
/**
diff --git a/web/src/views/plugins/index.js b/web/src/views/plugins/index.js
index 91b1d41..48e5b28 100644
--- a/web/src/views/plugins/index.js
+++ b/web/src/views/plugins/index.js
@@ -1,3 +1,5 @@
+import Vue from 'vue'
+
function importAll (r) {
const __modules = []
r.keys().forEach(key => {
@@ -8,17 +10,32 @@ function importAll (r) {
return __modules
}
-export const checkPlugins = async function install (pluginName) {
+export const checkPlugins = function install (pluginName) {
let pluginsList
pluginsList = importAll(require.context('./', true, /index\.js$/))
if (pluginsList && pluginsList.indexOf(pluginName) !== -1) {
- // 本地插件
- return 'local'
+ try {
+ const Module = import('@/views/plugins/' + pluginName + '/src/index')
+ // 注册组件
+ if (Module.default) {
+ Vue.use(Module.default)
+ }
+ // 本地插件
+ return 'local'
+ } catch (exception) {}
}
pluginsList = importAll(require.context('@great-dream/', true, /index\.js$/))
if (pluginsList && pluginsList.indexOf(pluginName) !== -1) {
// node_modules 封装插件
- return 'plugins'
+ try {
+ const Module = import('@great-dream/' + pluginName + '/src/index')
+ // 注册组件
+ if (Module.default) {
+ Vue.use(Module.default)
+ }
+ // 本地插件
+ return 'plugins'
+ } catch (exception) {}
}
// 未找到插件
return undefined
From 0f5ca3f3ab497ef83b5925138a0acf614db6963a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=9D=8E=E5=BC=BA?= <1206709430@qq.com>
Date: Tue, 12 Apr 2022 17:29:41 +0800
Subject: [PATCH 07/25] =?UTF-8?q?=E4=BC=98=E5=8C=96=EF=BC=9A=E6=95=B0?=
=?UTF-8?q?=E6=8D=AE=E6=9B=B4=E6=96=B0=E4=BA=BA=E4=B8=BA=E5=BD=93=E5=89=8D?=
=?UTF-8?q?=E6=9B=B4=E7=94=A8=E6=88=B7id?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
backend/dvadmin/utils/serializers.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/backend/dvadmin/utils/serializers.py b/backend/dvadmin/utils/serializers.py
index 8ab54ff..5ee5ed2 100644
--- a/backend/dvadmin/utils/serializers.py
+++ b/backend/dvadmin/utils/serializers.py
@@ -68,7 +68,7 @@ class CustomModelSerializer(DynamicFieldsMixin,ModelSerializer):
def update(self, instance, validated_data):
if self.request:
if hasattr(self.instance, self.modifier_field_id):
- self.instance.modifier = self.get_request_username()
+ self.instance[self.modifier_field_id] = self.get_request_user_id()
return super().update(instance, validated_data)
def get_request_username(self):
From 58491d0fef592d7e54c430906d6e578c402e7963 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=8C=BF=E5=B0=8F=E5=A4=A9?= <1638245306@qq.com>
Date: Fri, 15 Apr 2022 17:07:59 +0800
Subject: [PATCH 08/25] =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=8F=98=E5=8C=96:?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
部门信息进行优化
---
backend/dvadmin/system/views/dept.py | 19 +--
web/src/install.js | 56 +++----
web/src/views/system/user/crud.js | 215 +++++++++++++++++----------
3 files changed, 179 insertions(+), 111 deletions(-)
diff --git a/backend/dvadmin/system/views/dept.py b/backend/dvadmin/system/views/dept.py
index 050d478..63d83a3 100644
--- a/backend/dvadmin/system/views/dept.py
+++ b/backend/dvadmin/system/views/dept.py
@@ -6,6 +6,7 @@
@Created on: 2021/6/3 003 0:30
@Remark: 角色管理
"""
+from rest_framework import serializers
from dvadmin.system.models import Dept
from dvadmin.utils.json_response import SuccessResponse
@@ -17,7 +18,7 @@ class DeptSerializer(CustomModelSerializer):
"""
部门-序列化器
"""
-
+ parent_name = serializers.CharField(read_only=True,source='parent.name')
class Meta:
model = Dept
fields = "__all__"
@@ -55,11 +56,11 @@ class DeptViewSet(CustomModelViewSet):
update_serializer_class = DeptCreateUpdateSerializer
# extra_filter_backends = []
- def list(self, request, *args, **kwargs):
- queryset = self.filter_queryset(self.get_queryset())
- page = self.paginate_queryset(queryset)
- if page is not None:
- serializer = self.get_serializer(page, many=True, request=request)
- return self.get_paginated_response(serializer.data)
- serializer = self.get_serializer(queryset, many=True, request=request)
- return SuccessResponse(data=serializer.data, msg="获取成功")
+ # def list(self, request, *args, **kwargs):
+ # queryset = self.filter_queryset(self.get_queryset())
+ # page = self.paginate_queryset(queryset)
+ # if page is not None:
+ # serializer = self.get_serializer(page, many=True, request=request)
+ # return self.get_paginated_response(serializer.data)
+ # serializer = self.get_serializer(queryset, many=True, request=request)
+ # return SuccessResponse(data=serializer.data, msg="获取成功")
diff --git a/web/src/install.js b/web/src/install.js
index 47642e3..b3e7c80 100644
--- a/web/src/install.js
+++ b/web/src/install.js
@@ -254,7 +254,7 @@ Vue.prototype.commonEndColumns = function (param = {}) {
search: {
disabled: true
},
- type: 'cascader',
+ type: 'table-selector',
dict: {
cache: true,
url: '/api/system/dept/?limit=999&status=1',
@@ -262,9 +262,14 @@ Vue.prototype.commonEndColumns = function (param = {}) {
value: 'id', // 数据字典中value字段的属性名
label: 'name', // 数据字典中label字段的属性名
children: 'children', // 数据字典中children字段的属性名
- getData: (url, dict) => { // 配置此参数会覆盖全局的getRemoteDictFunc
- return request({ url: url }).then(ret => {
- return [{ id: null, name: '根节点', children: XEUtils.toArrayTree(ret.data.data, { parentKey: 'parent', strict: true }) }]
+ getData: (url, dict, {
+ _,
+ component
+ }) => {
+ return request({
+ url: url,
+ }).then(ret => {
+ return ret.data.data
})
}
},
@@ -273,13 +278,27 @@ Vue.prototype.commonEndColumns = function (param = {}) {
component: {
props: {
elProps: {
- clearable: true,
- showAllLevels: false, // 仅显示最后一级
- props: {
- checkStrictly: true, // 可以不需要选到最后一级
- emitPath: false,
- clearable: true
- }
+ treeConfig: {
+ transform: true,
+ rowField: 'id',
+ parentField: 'parent',
+ expandAll: true
+ },
+ columns: [
+ {
+ field: 'name',
+ title: '部门名称',
+ treeNode: true
+ },
+ {
+ field: 'status',
+ title: '状态'
+ },
+ {
+ field: 'parent_name',
+ title: '父级部门'
+ }
+ ]
}
}
},
@@ -289,21 +308,6 @@ Vue.prototype.commonEndColumns = function (param = {}) {
)
}
}
- },
- component: {
- dict: {
- cache: true,
- url: deptPrefix + '?limit=999&status=1',
- isTree: true,
- value: 'id', // 数据字典中value字段的属性名
- label: 'name', // 数据字典中label字段的属性名
- children: 'children', // 数据字典中children字段的属性名
- getData: (url, dict) => { // 配置此参数会覆盖全局的getRemoteDictFunc
- return request({ url: url }).then(ret => {
- return [{ id: null, name: '根节点', children: XEUtils.toArrayTree(ret.data.data, { parentKey: 'parent', strict: true }) }]
- })
- }
- }
}
},
{
diff --git a/web/src/views/system/user/crud.js b/web/src/views/system/user/crud.js
index 84a9a16..f28d3ed 100644
--- a/web/src/views/system/user/crud.js
+++ b/web/src/views/system/user/crud.js
@@ -2,6 +2,8 @@ import { request } from '@/api/service'
import { BUTTON_STATUS_BOOL } from '@/config/button'
import { urlPrefix as deptPrefix } from '../dept/api'
import util from '@/libs/util'
+import XEUtils from 'xe-utils'
+
const uploadUrl = util.baseURL() + 'api/system/img/'
export const crudOptions = (vm) => {
return {
@@ -85,7 +87,10 @@ export const crudOptions = (vm) => {
type: 'input',
form: {
rules: [ // 表单校验规则
- { required: true, message: '账号必填项' }
+ {
+ required: true,
+ message: '账号必填项'
+ }
],
component: {
placeholder: '请输入账号'
@@ -95,7 +100,7 @@ export const crudOptions = (vm) => {
},
helper: {
render (h) {
- return (< el-alert title="密码默认为:admin123456" type="warning" />
+ return (< el-alert title="密码默认为:admin123456" type="warning"/>
)
}
}
@@ -111,7 +116,10 @@ export const crudOptions = (vm) => {
type: 'input',
form: {
rules: [ // 表单校验规则
- { required: true, message: '姓名必填项' }
+ {
+ required: true,
+ message: '姓名必填项'
+ }
],
component: {
span: 12,
@@ -135,18 +143,29 @@ export const crudOptions = (vm) => {
url: deptPrefix,
value: 'id', // 数据字典中value字段的属性名
label: 'name', // 数据字典中label字段的属性名
- getData: (url, dict, { _, component }) => {
- return request({ url: url, params: { page: 1, limit: 10, status: 1 } }).then(ret => {
- component._elProps.page = ret.data.page
- component._elProps.limit = ret.data.limit
- component._elProps.total = ret.data.total
+ isTree: true,
+ getData: (url, dict, {
+ _,
+ component
+ }) => {
+ return request({
+ url: url,
+ params: {
+ page: 1,
+ limit: 999,
+ status: 1
+ }
+ }).then(ret => {
return ret.data.data
})
}
},
form: {
rules: [ // 表单校验规则
- { required: true, message: '必填项' }
+ {
+ required: true,
+ message: '必填项'
+ }
],
itemProps: {
class: { yxtInput: true }
@@ -155,14 +174,20 @@ export const crudOptions = (vm) => {
span: 12,
props: { multiple: false },
elProps: {
- pagination: true,
+ treeConfig: {
+ transform: true,
+ rowField: 'id',
+ parentField: 'parent',
+ expandAll: true
+ },
columns: [
{
field: 'name',
- title: '部门名称'
+ title: '部门名称',
+ treeNode: true
},
{
- field: 'status_label',
+ field: 'status',
title: '状态'
},
{
@@ -173,7 +198,72 @@ export const crudOptions = (vm) => {
}
}
}
- }, {
+ },
+ {
+ title: '角色',
+ key: 'role',
+ width: 160,
+ search: {
+ disabled: true
+ },
+ type: 'table-selector',
+ dict: {
+ cache: false,
+ url: '/api/system/role/',
+ value: 'id', // 数据字典中value字段的属性名
+ label: 'name', // 数据字典中label字段的属性名
+ getData: (url, dict, {
+ form,
+ component
+ }) => {
+ return request({
+ url: url,
+ params: {
+ page: 1,
+ limit: 10
+ }
+ }).then(ret => {
+ component._elProps.page = ret.data.page
+ component._elProps.limit = ret.data.limit
+ component._elProps.total = ret.data.total
+ return ret.data.data
+ })
+ }
+ },
+ form: {
+ rules: [ // 表单校验规则
+ {
+ required: true,
+ message: '必填项'
+ }
+ ],
+ itemProps: {
+ class: { yxtInput: true }
+ },
+ component: {
+ span: 12,
+ props: { multiple: true },
+ elProps: {
+ pagination: true,
+ columns: [
+ {
+ field: 'name',
+ title: '角色名称'
+ },
+ {
+ field: 'key',
+ title: '权限标识'
+ },
+ {
+ field: 'status',
+ title: '状态'
+ }
+ ]
+ }
+ }
+ }
+ },
+ {
title: '手机号码',
key: 'mobile',
width: 120,
@@ -183,12 +273,16 @@ export const crudOptions = (vm) => {
type: 'input',
form: {
rules: [
- { max: 20, message: '请输入正确的手机号码', trigger: 'blur' },
- { pattern: /^1[3|4|5|7|8]\d{9}$/, message: '请输入正确的手机号码' }
+ {
+ max: 20,
+ message: '请输入正确的手机号码',
+ trigger: 'blur'
+ },
+ {
+ pattern: /^1[3|4|5|7|8]\d{9}$/,
+ message: '请输入正确的手机号码'
+ }
],
- itemProps: {
- class: { yxtInput: true }
- },
component: {
placeholder: '请输入手机号码'
}
@@ -199,7 +293,11 @@ export const crudOptions = (vm) => {
width: 120,
form: {
rules: [
- { type: 'email', message: '请输入正确的邮箱地址', trigger: ['blur', 'change'] }
+ {
+ type: 'email',
+ message: '请输入正确的邮箱地址',
+ trigger: ['blur', 'change']
+ }
],
component: {
placeholder: '请输入邮箱'
@@ -211,12 +309,21 @@ export const crudOptions = (vm) => {
key: 'gender',
type: 'select',
dict: {
- data: [{ label: '男', value: 1 }, { label: '女', value: 0 }]
+ data: [{
+ label: '男',
+ value: 1
+ }, {
+ label: '女',
+ value: 0
+ }]
},
form: {
value: 1,
rules: [
- { required: true, message: '性别必填项' }
+ {
+ required: true,
+ message: '性别必填项'
+ }
],
component: {
span: 12
@@ -238,7 +345,13 @@ export const crudOptions = (vm) => {
disabled: false
},
dict: {
- data: [{ label: '前台用户', value: 1 }, { label: '后台用户', value: 0 }]
+ data: [{
+ label: '前台用户',
+ value: 1
+ }, {
+ label: '后台用户',
+ value: 0
+ }]
},
form: {
disabled: true
@@ -283,7 +396,10 @@ export const crudOptions = (vm) => {
if (ret.data == null || ret.data === '') {
throw new Error('上传失败')
}
- return { url: ret.data.data.url, key: option.data.key }
+ return {
+ url: ret.data.data.url,
+ key: option.data.key
+ }
}
},
elProps: { // 与el-uploader 配置一致
@@ -309,7 +425,6 @@ export const crudOptions = (vm) => {
component: {
props: {
buildUrl (value, item) {
- console.log(11, value)
if (value && value.indexOf('http') !== 0) {
return '/api/upload/form/download?key=' + value
}
@@ -317,58 +432,6 @@ export const crudOptions = (vm) => {
}
}
}
- },
- {
- title: '角色',
- key: 'role',
- width: 160,
- search: {
- disabled: true
- },
- type: 'table-selector',
- dict: {
- cache: false,
- url: '/api/system/role/',
- value: 'id', // 数据字典中value字段的属性名
- label: 'name', // 数据字典中label字段的属性名
- getData: (url, dict, { form, component }) => {
- return request({ url: url, params: { page: 1, limit: 10 } }).then(ret => {
- component._elProps.page = ret.data.page
- component._elProps.limit = ret.data.limit
- component._elProps.total = ret.data.total
- return ret.data.data
- })
- }
- },
- form: {
- rules: [ // 表单校验规则
- { required: true, message: '必填项' }
- ],
- itemProps: {
- class: { yxtInput: true }
- },
- component: {
- span: 12,
- props: { multiple: true },
- elProps: {
- pagination: true,
- columns: [
- {
- field: 'name',
- title: '角色名称'
- },
- {
- field: 'key',
- title: '权限标识'
- },
- {
- field: 'status_label',
- title: '状态'
- }
- ]
- }
- }
- }
}
].concat(vm.commonEndColumns({ update_datetime: { showTable: false } }))
}
From 20f88bda87c8900c50bc6dab3081e987fd70a7bd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=8C=BF=E5=B0=8F=E5=A4=A9?= <1638245306@qq.com>
Date: Fri, 15 Apr 2022 17:24:03 +0800
Subject: [PATCH 09/25] =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=8F=98=E5=8C=96:=20?=
=?UTF-8?q?=E5=90=8E=E7=AB=AF=E5=8F=AF=E5=85=B3=E9=97=AD=E9=AA=8C=E8=AF=81?=
=?UTF-8?q?=E7=A0=81=E7=9A=84=E9=AA=8C=E8=AF=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
backend/dvadmin/system/views/login.py | 30 +++++++++++++--------------
1 file changed, 15 insertions(+), 15 deletions(-)
diff --git a/backend/dvadmin/system/views/login.py b/backend/dvadmin/system/views/login.py
index 41f0a88..2aee9a1 100644
--- a/backend/dvadmin/system/views/login.py
+++ b/backend/dvadmin/system/views/login.py
@@ -56,7 +56,7 @@ class LoginSerializer(TokenObtainPairSerializer):
登录的序列化器:
重写djangorestframework-simplejwt的序列化器
"""
- captcha = serializers.CharField(max_length=6)
+ captcha = serializers.CharField(max_length=6, required=False, allow_null=True)
class Meta:
model = Users
@@ -67,21 +67,21 @@ class LoginSerializer(TokenObtainPairSerializer):
'no_active_account': _('账号/密码不正确')
}
- def validate_captcha(self, captcha):
- self.image_code = CaptchaStore.objects.filter(
- id=self.initial_data['captchaKey']).first()
- five_minute_ago = datetime.now() - timedelta(hours=0, minutes=5, seconds=0)
- if self.image_code and five_minute_ago > self.image_code.expiration:
- self.image_code and self.image_code.delete()
- raise CustomValidationError('验证码过期')
- else:
- if self.image_code and (self.image_code.response == captcha or self.image_code.challenge == captcha):
- self.image_code and self.image_code.delete()
- else:
- self.image_code and self.image_code.delete()
- raise CustomValidationError("图片验证码错误")
-
def validate(self, attrs):
+ if settings.CAPTCHA_STATE:
+ self.image_code = CaptchaStore.objects.filter(
+ id=self.initial_data['captchaKey']).first()
+ five_minute_ago = datetime.now() - timedelta(hours=0, minutes=5, seconds=0)
+ if self.image_code and five_minute_ago > self.image_code.expiration:
+ self.image_code and self.image_code.delete()
+ raise CustomValidationError('验证码过期')
+ else:
+ captcha = attrs['captcha']
+ if self.image_code and (self.image_code.response == captcha or self.image_code.challenge == captcha):
+ self.image_code and self.image_code.delete()
+ else:
+ self.image_code and self.image_code.delete()
+ raise CustomValidationError("图片验证码错误")
data = super().validate(attrs)
data['name'] = self.user.name
data['userId'] = self.user.id
From 9f52d633c16f27c76b54d579fbaf06466071356b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=8C=BF=E5=B0=8F=E5=A4=A9?= <1638245306@qq.com>
Date: Fri, 15 Apr 2022 17:26:40 +0800
Subject: [PATCH 10/25] =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=8F=98=E5=8C=96:=20?=
=?UTF-8?q?=E4=BF=AE=E6=94=B9mysql=E6=95=B0=E6=8D=AE=E5=BA=93=E9=85=8D?=
=?UTF-8?q?=E7=BD=AE=E7=9A=84=E6=B3=A8=E9=87=8A?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
backend/conf/env.example.py | 19 ++++++++++---------
1 file changed, 10 insertions(+), 9 deletions(-)
diff --git a/backend/conf/env.example.py b/backend/conf/env.example.py
index 2d43149..ec6fe37 100644
--- a/backend/conf/env.example.py
+++ b/backend/conf/env.example.py
@@ -5,24 +5,25 @@ from application.settings import BASE_DIR
# ================================================= #
# ************** 数据库 配置 ************** #
# ================================================= #
-#
+
# 数据库 ENGINE ,默认演示使用 sqlite3 数据库,正式环境建议使用 mysql 数据库
DATABASE_ENGINE = "django.db.backends.sqlite3"
# 数据库名
DATABASE_NAME = os.path.join(BASE_DIR, 'db.sqlite3')
+#************mysql配置*******************#
# 使用mysql时,改为此配置
# DATABASE_ENGINE = "django.db.backends.mysql"
# DATABASE_NAME = 'django-vue-admin' # mysql 时使用
-
# 数据库地址 改为自己数据库地址
-DATABASE_HOST = "127.0.0.1"
-# 数据库端口
-DATABASE_PORT = 3306
-# 数据库用户名
-DATABASE_USER = "root"
-# 数据库密码
-DATABASE_PASSWORD = "123456"
+# DATABASE_HOST = "127.0.0.1"
+# # 数据库端口
+# DATABASE_PORT = 3306
+# # 数据库用户名
+# DATABASE_USER = "root"
+# # 数据库密码
+# DATABASE_PASSWORD = "123456"
+
# ================================================= #
# ************** redis配置,无redis 可不进行配置 ************** #
# ================================================= #
From f0b41d28e64683a680d145fbe03faaea11682433 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=8C=BF=E5=B0=8F=E5=A4=A9?= <1638245306@qq.com>
Date: Fri, 15 Apr 2022 17:54:24 +0800
Subject: [PATCH 11/25] =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=8F=98=E5=8C=96:=20?=
=?UTF-8?q?=E4=BC=98=E5=8C=96=E7=99=BB=E5=BD=95=E9=AA=8C=E8=AF=81=E7=A0=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
backend/dvadmin/system/views/login.py | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/backend/dvadmin/system/views/login.py b/backend/dvadmin/system/views/login.py
index 2aee9a1..a7d567d 100644
--- a/backend/dvadmin/system/views/login.py
+++ b/backend/dvadmin/system/views/login.py
@@ -51,6 +51,7 @@ class CaptchaView(APIView):
return SuccessResponse(data=json_data)
+
class LoginSerializer(TokenObtainPairSerializer):
"""
登录的序列化器:
@@ -68,7 +69,10 @@ class LoginSerializer(TokenObtainPairSerializer):
}
def validate(self, attrs):
+ captcha = getattr(attrs,'captcha',None)
if settings.CAPTCHA_STATE:
+ if captcha is None:
+ raise CustomValidationError("验证码不能为空")
self.image_code = CaptchaStore.objects.filter(
id=self.initial_data['captchaKey']).first()
five_minute_ago = datetime.now() - timedelta(hours=0, minutes=5, seconds=0)
@@ -76,7 +80,6 @@ class LoginSerializer(TokenObtainPairSerializer):
self.image_code and self.image_code.delete()
raise CustomValidationError('验证码过期')
else:
- captcha = attrs['captcha']
if self.image_code and (self.image_code.response == captcha or self.image_code.challenge == captcha):
self.image_code and self.image_code.delete()
else:
From 1a85f55c92cc7eeb606b5ca6949b4d30bdc3e5fd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=9D=8E=E5=BC=BA?= <1206709430@qq.com>
Date: Fri, 15 Apr 2022 17:55:06 +0800
Subject: [PATCH 12/25] =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=8F=98=E5=8C=96:=20?=
=?UTF-8?q?=E4=BC=98=E5=8C=96=E9=85=8D=E7=BD=AE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
backend/conf/env.example.py | 11 +++++------
backend/dvadmin/utils/serializers.py | 2 +-
2 files changed, 6 insertions(+), 7 deletions(-)
diff --git a/backend/conf/env.example.py b/backend/conf/env.example.py
index ec6fe37..4b1b16d 100644
--- a/backend/conf/env.example.py
+++ b/backend/conf/env.example.py
@@ -7,22 +7,21 @@ from application.settings import BASE_DIR
# ================================================= #
# 数据库 ENGINE ,默认演示使用 sqlite3 数据库,正式环境建议使用 mysql 数据库
+# sqlite3 设置
DATABASE_ENGINE = "django.db.backends.sqlite3"
-# 数据库名
DATABASE_NAME = os.path.join(BASE_DIR, 'db.sqlite3')
-#************mysql配置*******************#
# 使用mysql时,改为此配置
# DATABASE_ENGINE = "django.db.backends.mysql"
# DATABASE_NAME = 'django-vue-admin' # mysql 时使用
# 数据库地址 改为自己数据库地址
-# DATABASE_HOST = "127.0.0.1"
+DATABASE_HOST = "127.0.0.1"
# # 数据库端口
-# DATABASE_PORT = 3306
+DATABASE_PORT = 3306
# # 数据库用户名
-# DATABASE_USER = "root"
+DATABASE_USER = "root"
# # 数据库密码
-# DATABASE_PASSWORD = "123456"
+DATABASE_PASSWORD = "123456"
# ================================================= #
# ************** redis配置,无redis 可不进行配置 ************** #
diff --git a/backend/dvadmin/utils/serializers.py b/backend/dvadmin/utils/serializers.py
index 5ee5ed2..c6af51c 100644
--- a/backend/dvadmin/utils/serializers.py
+++ b/backend/dvadmin/utils/serializers.py
@@ -68,7 +68,7 @@ class CustomModelSerializer(DynamicFieldsMixin,ModelSerializer):
def update(self, instance, validated_data):
if self.request:
if hasattr(self.instance, self.modifier_field_id):
- self.instance[self.modifier_field_id] = self.get_request_user_id()
+ setattr(self.instance, self.modifier_field_id, self.get_request_user_id())
return super().update(instance, validated_data)
def get_request_username(self):
From c8602c59c5edfadff693003fdadb22a6a94295d9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=8C=BF=E5=B0=8F=E5=A4=A9?= <1638245306@qq.com>
Date: Fri, 15 Apr 2022 19:07:55 +0800
Subject: [PATCH 13/25] =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=8F=98=E5=8C=96:=20?=
=?UTF-8?q?=E4=BC=98=E5=8C=96=E7=99=BB=E5=BD=95=E9=AA=8C=E8=AF=81=E7=A0=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
backend/dvadmin/system/views/login.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/backend/dvadmin/system/views/login.py b/backend/dvadmin/system/views/login.py
index a7d567d..2c3e291 100644
--- a/backend/dvadmin/system/views/login.py
+++ b/backend/dvadmin/system/views/login.py
@@ -69,7 +69,7 @@ class LoginSerializer(TokenObtainPairSerializer):
}
def validate(self, attrs):
- captcha = getattr(attrs,'captcha',None)
+ captcha = self.initial_data.get('captcha',None)
if settings.CAPTCHA_STATE:
if captcha is None:
raise CustomValidationError("验证码不能为空")
From 6aff934963b62d53c909121712b3c9d8da776723 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=8C=BF=E5=B0=8F=E5=A4=A9?= <1638245306@qq.com>
Date: Fri, 15 Apr 2022 19:47:27 +0800
Subject: [PATCH 14/25] =?UTF-8?q?=E4=BF=AE=E5=A4=8DBUG:?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
孙子级部门以下数据不正确问题
---
backend/dvadmin/utils/filters.py | 11 ++++++++---
1 file changed, 8 insertions(+), 3 deletions(-)
diff --git a/backend/dvadmin/utils/filters.py b/backend/dvadmin/utils/filters.py
index 1f5a43b..67c179b 100644
--- a/backend/dvadmin/utils/filters.py
+++ b/backend/dvadmin/utils/filters.py
@@ -36,7 +36,7 @@ def get_dept(dept_id: int, dept_all_list=None, dept_list=None):
if dept_list is None:
dept_list = [dept_id]
for ele in dept_all_list:
- if ele.get('parentId') == dept_id:
+ if ele.get('parent') == dept_id:
dept_list.append(ele.get('id'))
get_dept(ele.get('id'), dept_all_list, dept_list)
return list(set(dept_list))
@@ -94,10 +94,15 @@ class DataLevelPermissionsFilter(BaseFilterBackend):
return queryset.filter(dept_belong_id=user_dept_id)
# 3. 根据所有角色 获取所有权限范围
+ # (0, "仅本人数据权限"),
+ # (1, "本部门及以下数据权限"),
+ # (2, "本部门数据权限"),
+ # (3, "全部数据权限"),
+ # (4, "自定数据权限")
role_list = request.user.role.filter(status=1).values('admin', 'data_range')
- dataScope_list = []
+ dataScope_list = [] # 权限范围列表
for ele in role_list:
- # 3.1 判断用户是否为超级管理员角色/如果有1(所有数据) 则返回所有数据
+ # 判断用户是否为超级管理员角色/如果拥有[全部数据权限]则返回所有数据
if 3 == ele.get('data_range') or ele.get('admin') == True:
return queryset
dataScope_list.append(ele.get('data_range'))
From 48f18d7f1cc2508e05e0a768c29757432e950e4e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=8C=BF=E5=B0=8F=E5=A4=A9?= <1638245306@qq.com>
Date: Fri, 15 Apr 2022 19:51:56 +0800
Subject: [PATCH 15/25] =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=8F=98=E5=8C=96:?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
角色管理默认值优化
---
web/src/views/system/role/crud.js | 4 ++--
web/src/views/system/role/index.vue | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/web/src/views/system/role/crud.js b/web/src/views/system/role/crud.js
index a025a51..bbf1912 100644
--- a/web/src/views/system/role/crud.js
+++ b/web/src/views/system/role/crud.js
@@ -164,7 +164,7 @@ export const crudOptions = (vm) => {
data: BUTTON_WHETHER_BOOL
},
form: {
- value: 0,
+ value: false,
component: {
placeholder: '请选择是否管理员'
}
@@ -183,7 +183,7 @@ export const crudOptions = (vm) => {
data: BUTTON_STATUS_BOOL
},
form: {
- value: 1,
+ value: true,
component: {
placeholder: '请选择状态'
}
diff --git a/web/src/views/system/role/index.vue b/web/src/views/system/role/index.vue
index 56f6e6a..8ed34d7 100644
--- a/web/src/views/system/role/index.vue
+++ b/web/src/views/system/role/index.vue
@@ -48,7 +48,7 @@
>
- 当前角色管理员
+ 当前角色{{roleObj?roleObj.name:'无'}}
From 7706d6db84c4db31253f4be205f8439b6531e790 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=8C=BF=E5=B0=8F=E5=A4=A9?= <1638245306@qq.com>
Date: Fri, 15 Apr 2022 19:56:28 +0800
Subject: [PATCH 16/25] =?UTF-8?q?=E6=96=B0=E5=8A=9F=E8=83=BD:=20=E5=AF=86?=
=?UTF-8?q?=E7=A0=81=E9=87=8D=E7=BD=AE=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
backend/dvadmin/system/urls.py | 1 +
backend/dvadmin/system/views/user.py | 26 ++-
web/src/views/system/user/api.js | 18 +-
web/src/views/system/user/crud.js | 260 ++++++++++-----------------
web/src/views/system/user/index.vue | 108 +++++++----
5 files changed, 202 insertions(+), 211 deletions(-)
diff --git a/backend/dvadmin/system/urls.py b/backend/dvadmin/system/urls.py
index 2cacc87..8481e1d 100644
--- a/backend/dvadmin/system/urls.py
+++ b/backend/dvadmin/system/urls.py
@@ -41,6 +41,7 @@ urlpatterns = [
path('menu/web_router/', MenuViewSet.as_view({'get': 'web_router'})),
path('user/user_info/', UserViewSet.as_view({'get': 'user_info', 'put': 'update_user_info'})),
path('user/change_password/
/', UserViewSet.as_view({'put': 'change_password'})),
+ path('user/reset_password//', UserViewSet.as_view({'put': 'reset_password'})),
path('user/export/', UserViewSet.as_view({'post': 'export_data', })),
path('user/import/',UserViewSet.as_view({'get': 'import_data', 'post': 'import_data'})),
path('system_config/save_content/', SystemConfigViewSet.as_view({'put': 'save_content'})),
diff --git a/backend/dvadmin/system/views/user.py b/backend/dvadmin/system/views/user.py
index ae13ea0..5144952 100644
--- a/backend/dvadmin/system/views/user.py
+++ b/backend/dvadmin/system/views/user.py
@@ -11,6 +11,7 @@ import hashlib
from django.contrib.auth.hashers import make_password
from rest_framework import serializers
from rest_framework.decorators import action
+from rest_framework.permissions import IsAuthenticated
from dvadmin.system.models import Users
from dvadmin.utils.json_response import ErrorResponse, DetailResponse
@@ -147,7 +148,7 @@ class UserViewSet(CustomModelViewSet):
'gender': '用户性别(男/女/未知)',
'is_active': '帐号状态(启用/禁用)', 'password': '登录密码', 'dept': '部门ID', 'role': '角色ID'}
- @action(methods=['GET'], detail=True, permission_classes=[])
+ @action(methods=['GET'], detail=True, permission_classes=[IsAuthenticated])
def user_info(self, request):
"""获取当前用户信息"""
user = request.user
@@ -159,14 +160,14 @@ class UserViewSet(CustomModelViewSet):
}
return DetailResponse(data=result, msg="获取成功")
- @action(methods=['PUT'], detail=True, permission_classes=[])
+ @action(methods=['PUT'], detail=True, permission_classes=[IsAuthenticated])
def update_user_info(self, request):
"""修改当前用户信息"""
user = request.user
Users.objects.filter(id=user.id).update(**request.data)
return DetailResponse(data=None, msg="修改成功")
- @action(methods=['PUT'], detail=True, permission_classes=[])
+ @action(methods=['PUT'], detail=True, permission_classes=[IsAuthenticated])
def change_password(self, request, *args, **kwargs):
"""密码修改"""
instance = Users.objects.filter(id=kwargs.get('pk')).first()
@@ -185,3 +186,22 @@ class UserViewSet(CustomModelViewSet):
return ErrorResponse(msg="旧密码不正确")
else:
return ErrorResponse(msg="未获取到用户")
+
+ @action(methods=['PUT'], detail=True)
+ def reset_password(self, request, pk):
+ """
+ 密码重置
+ """
+ instance = Users.objects.filter(id=pk).first()
+ data = request.data
+ new_pwd = data.get('newPassword')
+ new_pwd2 = data.get('newPassword2')
+ if instance:
+ if new_pwd != new_pwd2:
+ return ErrorResponse(msg="两次密码不匹配")
+ else:
+ instance.password = make_password(new_pwd)
+ instance.save()
+ return DetailResponse(data=None, msg="修改成功")
+ else:
+ return ErrorResponse(msg="未获取到用户")
diff --git a/web/src/views/system/user/api.js b/web/src/views/system/user/api.js
index 4034b53..9b89613 100644
--- a/web/src/views/system/user/api.js
+++ b/web/src/views/system/user/api.js
@@ -6,7 +6,7 @@
* 联系Qq:1638245306
* @文件介绍: 用户接口
*/
-import { request, downloadFile } from '@/api/service'
+import { request } from '@/api/service'
export const urlPrefix = '/api/system/user/'
@@ -43,13 +43,15 @@ export function DelObj (id) {
}
/**
- * 导出
- * @param params
+ * 重置密码
+ * @param id
+ * @returns {*}
+ * @constructor
*/
-export function exportData (params) {
- return downloadFile({
- url: urlPrefix + 'export/',
- params: params,
- method: 'post'
+export function ResetPwd (obj) {
+ return request({
+ url: urlPrefix + 'reset_password/' + obj.id + '/',
+ method: 'put',
+ data: obj
})
}
diff --git a/web/src/views/system/user/crud.js b/web/src/views/system/user/crud.js
index f28d3ed..f0cd911 100644
--- a/web/src/views/system/user/crud.js
+++ b/web/src/views/system/user/crud.js
@@ -2,7 +2,6 @@ import { request } from '@/api/service'
import { BUTTON_STATUS_BOOL } from '@/config/button'
import { urlPrefix as deptPrefix } from '../dept/api'
import util from '@/libs/util'
-import XEUtils from 'xe-utils'
const uploadUrl = util.baseURL() + 'api/system/img/'
export const crudOptions = (vm) => {
@@ -14,8 +13,7 @@ export const crudOptions = (vm) => {
height: '100%'
},
rowHandle: {
- width: 140,
- fixed: 'right',
+ width: 180,
view: {
thin: true,
text: '',
@@ -36,7 +34,21 @@ export const crudOptions = (vm) => {
disabled () {
return !vm.hasPermissions('Delete')
}
- }
+ },
+ custom: [
+ {
+ thin: true,
+ text: '',
+ size: 'small',
+ type: 'warning',
+ icon: 'el-icon-refresh-left',
+ show () {
+ return vm.hasPermissions('ResetPwd')
+ },
+ emit: 'resetPwd'
+ }
+ ]
+
},
viewOptions: {
componentType: 'form'
@@ -47,7 +59,7 @@ export const crudOptions = (vm) => {
indexRow: { // 或者直接传true,不显示title,不居中
title: '序号',
align: 'center',
- width: 80
+ width: 100
},
columns: [
{
@@ -87,10 +99,7 @@ export const crudOptions = (vm) => {
type: 'input',
form: {
rules: [ // 表单校验规则
- {
- required: true,
- message: '账号必填项'
- }
+ { required: true, message: '账号必填项' }
],
component: {
placeholder: '请输入账号'
@@ -110,16 +119,12 @@ export const crudOptions = (vm) => {
title: '姓名',
key: 'name',
search: {
- key: 'name__icontains',
disabled: false
},
type: 'input',
form: {
rules: [ // 表单校验规则
- {
- required: true,
- message: '姓名必填项'
- }
+ { required: true, message: '姓名必填项' }
],
component: {
span: 12,
@@ -132,7 +137,6 @@ export const crudOptions = (vm) => {
},
{
title: '部门',
- width: 160,
key: 'dept',
search: {
disabled: true
@@ -143,29 +147,18 @@ export const crudOptions = (vm) => {
url: deptPrefix,
value: 'id', // 数据字典中value字段的属性名
label: 'name', // 数据字典中label字段的属性名
- isTree: true,
- getData: (url, dict, {
- _,
- component
- }) => {
- return request({
- url: url,
- params: {
- page: 1,
- limit: 999,
- status: 1
- }
- }).then(ret => {
+ getData: (url, dict, { form, component }) => {
+ return request({ url: url, params: { page: 1, limit: 10, status: 1 } }).then(ret => {
+ component._elProps.page = ret.data.page
+ component._elProps.limit = ret.data.limit
+ component._elProps.total = ret.data.total
return ret.data.data
})
}
},
form: {
rules: [ // 表单校验规则
- {
- required: true,
- message: '必填项'
- }
+ { required: true, message: '必填项' }
],
itemProps: {
class: { yxtInput: true }
@@ -174,20 +167,14 @@ export const crudOptions = (vm) => {
span: 12,
props: { multiple: false },
elProps: {
- treeConfig: {
- transform: true,
- rowField: 'id',
- parentField: 'parent',
- expandAll: true
- },
+ pagination: true,
columns: [
{
field: 'name',
- title: '部门名称',
- treeNode: true
+ title: '部门名称'
},
{
- field: 'status',
+ field: 'status_label',
title: '状态'
},
{
@@ -199,90 +186,21 @@ export const crudOptions = (vm) => {
}
}
},
- {
- title: '角色',
- key: 'role',
- width: 160,
- search: {
- disabled: true
- },
- type: 'table-selector',
- dict: {
- cache: false,
- url: '/api/system/role/',
- value: 'id', // 数据字典中value字段的属性名
- label: 'name', // 数据字典中label字段的属性名
- getData: (url, dict, {
- form,
- component
- }) => {
- return request({
- url: url,
- params: {
- page: 1,
- limit: 10
- }
- }).then(ret => {
- component._elProps.page = ret.data.page
- component._elProps.limit = ret.data.limit
- component._elProps.total = ret.data.total
- return ret.data.data
- })
- }
- },
- form: {
- rules: [ // 表单校验规则
- {
- required: true,
- message: '必填项'
- }
- ],
- itemProps: {
- class: { yxtInput: true }
- },
- component: {
- span: 12,
- props: { multiple: true },
- elProps: {
- pagination: true,
- columns: [
- {
- field: 'name',
- title: '角色名称'
- },
- {
- field: 'key',
- title: '权限标识'
- },
- {
- field: 'status',
- title: '状态'
- }
- ]
- }
- }
- }
- },
{
title: '手机号码',
key: 'mobile',
- width: 120,
search: {
disabled: true
},
type: 'input',
form: {
rules: [
- {
- max: 20,
- message: '请输入正确的手机号码',
- trigger: 'blur'
- },
- {
- pattern: /^1[3|4|5|7|8]\d{9}$/,
- message: '请输入正确的手机号码'
- }
+ { max: 20, message: '请输入正确的手机号码', trigger: 'blur' },
+ { pattern: /^1[3|4|5|7|8]\d{9}$/, message: '请输入正确的手机号码' }
],
+ itemProps: {
+ class: { yxtInput: true }
+ },
component: {
placeholder: '请输入手机号码'
}
@@ -290,14 +208,9 @@ export const crudOptions = (vm) => {
}, {
title: '邮箱',
key: 'email',
- width: 120,
form: {
rules: [
- {
- type: 'email',
- message: '请输入正确的邮箱地址',
- trigger: ['blur', 'change']
- }
+ { type: 'email', message: '请输入正确的邮箱地址', trigger: ['blur', 'change'] }
],
component: {
placeholder: '请输入邮箱'
@@ -307,57 +220,18 @@ export const crudOptions = (vm) => {
{
title: '性别',
key: 'gender',
- type: 'select',
+ type: 'radio',
dict: {
- data: [{
- label: '男',
- value: 1
- }, {
- label: '女',
- value: 0
- }]
+ data: [{ label: '男', value: 1 }, { label: '女', value: 0 }]
},
form: {
value: 1,
- rules: [
- {
- required: true,
- message: '性别必填项'
- }
- ],
component: {
span: 12
- },
- itemProps: {
- class: { yxtInput: true }
}
},
component: { props: { color: 'auto' } } // 自动染色
},
- {
- title: '用户类型',
- key: 'user_type',
- type: 'select',
- width: 120,
- search: {
- key: 'user_type',
- value: 0,
- disabled: false
- },
- dict: {
- data: [{
- label: '前台用户',
- value: 1
- }, {
- label: '后台用户',
- value: 0
- }]
- },
- form: {
- disabled: true
- },
- component: { props: { color: 'auto' } } // 自动染色
- },
{
title: '状态',
key: 'is_active',
@@ -380,7 +254,7 @@ export const crudOptions = (vm) => {
title: '头像',
key: 'avatar',
type: 'avatar-uploader',
- width: 80,
+ width: 100,
align: 'left',
form: {
component: {
@@ -393,13 +267,10 @@ export const crudOptions = (vm) => {
},
type: 'form',
successHandle (ret, option) {
- if (ret.data == null || ret.data === '') {
+ if (ret.data === null || ret.data === '') {
throw new Error('上传失败')
}
- return {
- url: ret.data.data.url,
- key: option.data.key
- }
+ return { url: ret.data.data.url, key: option.data.key }
}
},
elProps: { // 与el-uploader 配置一致
@@ -432,7 +303,58 @@ export const crudOptions = (vm) => {
}
}
}
+ },
+ {
+ title: '角色',
+ key: 'role',
+ search: {
+ disabled: true
+ },
+ type: 'table-selector',
+ dict: {
+ cache: false,
+ url: '/api/system/role/',
+ value: 'id', // 数据字典中value字段的属性名
+ label: 'name', // 数据字典中label字段的属性名
+ getData: (url, dict, { form, component }) => {
+ return request({ url: url, params: { page: 1, limit: 10 } }).then(ret => {
+ component._elProps.page = ret.data.page
+ component._elProps.limit = ret.data.limit
+ component._elProps.total = ret.data.total
+ return ret.data.data
+ })
+ }
+ },
+ form: {
+ rules: [ // 表单校验规则
+ { required: true, message: '必填项' }
+ ],
+ itemProps: {
+ class: { yxtInput: true }
+ },
+ component: {
+ span: 12,
+ props: { multiple: true },
+ elProps: {
+ pagination: true,
+ columns: [
+ {
+ field: 'name',
+ title: '角色名称'
+ },
+ {
+ field: 'key',
+ title: '权限标识'
+ },
+ {
+ field: 'status_label',
+ title: '状态'
+ }
+ ]
+ }
+ }
+ }
}
- ].concat(vm.commonEndColumns({ update_datetime: { showTable: false } }))
+ ].concat(vm.commonEndColumns({ show_create_datetime: false }))
}
}
diff --git a/web/src/views/system/user/index.vue b/web/src/views/system/user/index.vue
index ffc7d04..b8d787f 100644
--- a/web/src/views/system/user/index.vue
+++ b/web/src/views/system/user/index.vue
@@ -1,11 +1,3 @@
-
新增
- 新增
- 导出
-
- 导入
-
+
+
+
+
+
+
+
+
+
+
+
@@ -60,10 +55,42 @@ import { d2CrudPlus } from 'd2-crud-plus'
export default {
name: 'user',
-
mixins: [d2CrudPlus.crud],
data () {
- return {}
+ var validatePass = (rule, value, callback) => {
+ const pwdRegex = new RegExp('(?=.*[0-9])(?=.*[a-zA-Z]).{8,30}')
+ if (value === '') {
+ callback(new Error('请输入密码'))
+ } else if (!pwdRegex.test(value)) {
+ callback(new Error('您的密码复杂度太低(密码中必须包含字母、数字)'))
+ } else {
+ if (this.resetPwdForm.pwd2 !== '') {
+ this.$refs.resetPwdForm.validateField('pwd2')
+ }
+ callback()
+ }
+ }
+ var validatePass2 = (rule, value, callback) => {
+ if (value === '') {
+ callback(new Error('请再次输入密码'))
+ } else if (value !== this.resetPwdForm.pwd) {
+ callback(new Error('两次输入密码不一致!'))
+ } else {
+ callback()
+ }
+ }
+ return {
+ dialogFormVisible: false,
+ resetPwdForm: {
+ id: null,
+ pwd: null,
+ pwd2: null
+ },
+ passwordRules: {
+ pwd: [{ required: true, message: '必填项' }, { validator: validatePass, trigger: 'blur' }],
+ pwd2: [{ required: true, message: '必填项' }, { validator: validatePass2, trigger: 'blur' }]
+ }
+ }
},
methods: {
getCrudOptions () {
@@ -76,19 +103,38 @@ export default {
return api.AddObj(row)
},
updateRequest (row) {
- console.log('----', row)
return api.UpdateObj(row)
},
delRequest (row) {
return api.DelObj(row.id)
},
- onExport () {
- this.$confirm('是否确认导出所有数据项?', '警告', {
- confirmButtonText: '确定',
- cancelButtonText: '取消',
- type: 'warning'
- }).then(function () {
- return api.exportData()
+ // 重置密码弹框
+ resetPwd ({ row }) {
+ this.dialogFormVisible = true
+ this.resetPwdForm.id = row.id
+ },
+ // 重置密码确认
+ resetPwdSubmit () {
+ const that = this
+ that.$refs.resetPwdForm.validate((valid) => {
+ if (valid) {
+ const params = {
+ id: that.resetPwdForm.id,
+ newPassword: that.$md5(that.resetPwdForm.pwd),
+ newPassword2: that.$md5(that.resetPwdForm.pwd2)
+ }
+ api.ResetPwd(params).then(res => {
+ that.dialogFormVisible = false
+ that.resetPwdForm = {
+ id: null,
+ pwd: null,
+ pwd2: null
+ }
+ that.$message.success('修改成功')
+ })
+ } else {
+ that.$message.error('表单校验失败,请检查')
+ }
})
}
}
From 6bf257eb230547b177b58d508e1a97006c518cfd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=8C=BF=E5=B0=8F=E5=A4=A9?= <1638245306@qq.com>
Date: Fri, 15 Apr 2022 20:06:42 +0800
Subject: [PATCH 17/25] =?UTF-8?q?=E6=96=B0=E5=8A=9F=E8=83=BD:=20=E5=AF=86?=
=?UTF-8?q?=E7=A0=81=E9=87=8D=E7=BD=AE=E5=8A=9F=E8=83=BD=E5=88=9D=E5=A7=8B?=
=?UTF-8?q?=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
backend/dvadmin/system/initialize.py | 51 +++++++++++++++++++++-------
1 file changed, 38 insertions(+), 13 deletions(-)
diff --git a/backend/dvadmin/system/initialize.py b/backend/dvadmin/system/initialize.py
index 20fa3fa..2a2c463 100644
--- a/backend/dvadmin/system/initialize.py
+++ b/backend/dvadmin/system/initialize.py
@@ -163,6 +163,17 @@ class Initialize(CoreInitialize):
"name": "导出",
"value": "Export",
"creator_id": 1
+ },
+ {
+ "id": 9,
+ "description": None,
+ "modifier": "1",
+ "dept_belong_id": 1,
+ "update_datetime": datetime.datetime.now(),
+ "create_datetime": datetime.datetime.now(),
+ "name": "重置密码",
+ "value": "ResetPwd",
+ "creator_id": 1
}
]
self.save(Button, self.button_data, "权限表标识")
@@ -517,19 +528,19 @@ class Initialize(CoreInitialize):
"""
self.menu_button_data = [
{
- "id": 1,
- "description": None,
- "modifier": "1",
- "dept_belong_id": 1,
- "update_datetime": datetime.datetime.now(),
- "create_datetime": datetime.datetime.now(),
- "name": "查询",
- "value": "Search",
- "api": "/api/system/menu/",
- "method": 0,
- "creator_id": 1,
- "menu_id": 1
- },
+ "id": 1,
+ "description": None,
+ "modifier": "1",
+ "dept_belong_id": 1,
+ "update_datetime": datetime.datetime.now(),
+ "create_datetime": datetime.datetime.now(),
+ "name": "查询",
+ "value": "Search",
+ "api": "/api/system/menu/",
+ "method": 0,
+ "creator_id": 1,
+ "menu_id": 1
+ },
{
"id": 2,
"description": None,
@@ -1243,6 +1254,20 @@ class Initialize(CoreInitialize):
"method": 1,
"creator_id": 1,
"menu_id": 13
+ },
+ {
+ "id": 53,
+ "description": None,
+ "modifier": "1",
+ "dept_belong_id": 1,
+ "update_datetime": datetime.datetime.now(),
+ "create_datetime": datetime.datetime.now(),
+ "name": "重置密码",
+ "value": "ResetPwd",
+ "api": "/api/system/user/reset_password/{id}/",
+ "method": 2,
+ "creator_id": 1,
+ "menu_id": 3
}
]
self.save(MenuButton, self.menu_button_data, "菜单按钮表")
From ad8cf4f0fc8302f97da36bfce5fe7127d4ff5785 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=8C=BF=E5=B0=8F=E5=A4=A9?= <1638245306@qq.com>
Date: Fri, 15 Apr 2022 21:26:01 +0800
Subject: [PATCH 18/25] =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=8F=98=E5=8C=96:?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
优化用户头像
---
backend/dvadmin/system/models.py | 2 +-
backend/dvadmin/system/views/file_list.py | 3 +-
backend/dvadmin/system/views/login.py | 1 +
backend/dvadmin/system/views/user.py | 3 +-
.../components/header-user/index.vue | 1 +
.../components/header-user/userinfo.vue | 31 ++++++++++
web/src/router/routes.js | 56 +++++++++----------
.../store/modules/d2admin/modules/account.js | 2 +-
web/src/views/system/user/crud.js | 7 +--
9 files changed, 70 insertions(+), 36 deletions(-)
diff --git a/backend/dvadmin/system/models.py b/backend/dvadmin/system/models.py
index 6d0fcda..27dea49 100644
--- a/backend/dvadmin/system/models.py
+++ b/backend/dvadmin/system/models.py
@@ -206,7 +206,7 @@ class OperationLog(CoreModel):
def media_file_name(instance, filename):
h = instance.md5sum
basename, ext = os.path.splitext(filename)
- return os.path.join('media/files', h[0:1], h[1:2], h + ext.lower())
+ return os.path.join('files', h[0:1], h[1:2], h + ext.lower())
class FileList(CoreModel):
diff --git a/backend/dvadmin/system/views/file_list.py b/backend/dvadmin/system/views/file_list.py
index e9f8b07..87c4355 100644
--- a/backend/dvadmin/system/views/file_list.py
+++ b/backend/dvadmin/system/views/file_list.py
@@ -17,7 +17,7 @@ class FileSerializer(CustomModelSerializer):
url = serializers.SerializerMethodField(read_only=True)
def get_url(self, instance):
- return str(instance.url)
+ return 'media/'+str(instance.url)
class Meta:
model = FileList
@@ -41,3 +41,4 @@ class FileViewSet(CustomModelViewSet):
queryset = FileList.objects.all()
serializer_class = FileSerializer
filter_fields = ['name', ]
+ permission_classes = []
diff --git a/backend/dvadmin/system/views/login.py b/backend/dvadmin/system/views/login.py
index 2c3e291..3a85d10 100644
--- a/backend/dvadmin/system/views/login.py
+++ b/backend/dvadmin/system/views/login.py
@@ -88,6 +88,7 @@ class LoginSerializer(TokenObtainPairSerializer):
data = super().validate(attrs)
data['name'] = self.user.name
data['userId'] = self.user.id
+ data['avatar'] = self.user.avatar
return {
"code": 2000,
"msg": "请求成功",
diff --git a/backend/dvadmin/system/views/user.py b/backend/dvadmin/system/views/user.py
index 5144952..36cb938 100644
--- a/backend/dvadmin/system/views/user.py
+++ b/backend/dvadmin/system/views/user.py
@@ -156,7 +156,8 @@ class UserViewSet(CustomModelViewSet):
"name": user.name,
"mobile": user.mobile,
"gender": user.gender,
- "email": user.email
+ "email": user.email,
+ 'avatar':user.avatar
}
return DetailResponse(data=result, msg="获取成功")
diff --git a/web/src/layout/header-aside/components/header-user/index.vue b/web/src/layout/header-aside/components/header-user/index.vue
index fd7a566..d60058d 100644
--- a/web/src/layout/header-aside/components/header-user/index.vue
+++ b/web/src/layout/header-aside/components/header-user/index.vue
@@ -20,6 +20,7 @@
注销
+
diff --git a/web/src/layout/header-aside/components/header-user/userinfo.vue b/web/src/layout/header-aside/components/header-user/userinfo.vue
index e1cf799..67c8dff 100644
--- a/web/src/layout/header-aside/components/header-user/userinfo.vue
+++ b/web/src/layout/header-aside/components/header-user/userinfo.vue
@@ -20,6 +20,21 @@
:label-position="position"
center
>
+
+
+
+
+
+
+
@@ -130,6 +145,11 @@ export default {
return {
position: 'left',
activeName: 'userInfo',
+ action: util.baseURL() + 'api/system/file/',
+ headers: {
+ Authorization: 'JWT ' + util.cookies.get('token')
+ },
+ fileList:[],
userInfo: {
name: '',
gender: '',
@@ -177,6 +197,7 @@ export default {
params: {}
}).then((res) => {
_self.userInfo = res.data
+ _self.fileList = [{name:'avatar.png',url:res.data.avatar}]
})
},
/**
@@ -251,6 +272,16 @@ export default {
this.$message.error('表单校验失败,请检查')
}
})
+ },
+ /**
+ * 头像上传
+ * @param res
+ * @param file
+ */
+ handleAvatarSuccess(res, file) {
+ console.log(11,res)
+ this.fileList =[{ url: util.baseURL() + res.data.url, name:file.name }]
+ this.userInfo.avatar = util.baseURL() + res.data.url;
}
}
}
diff --git a/web/src/router/routes.js b/web/src/router/routes.js
index 51916e0..7d9b192 100644
--- a/web/src/router/routes.js
+++ b/web/src/router/routes.js
@@ -88,25 +88,25 @@ const frameIn = [{
// component: _import('system/user')
// },
// // 系统 按钮配置
- {
- path: 'button',
- name: 'button',
- meta: {
- title: '按钮',
- auth: true
- },
- component: _import('system/button')
- },
- // // 系统 菜单权限
- {
- path: 'menuButton/:id',
- name: 'menuButton',
- meta: {
- title: '菜单按钮',
- auth: true
- },
- component: _import('system/menuButton')
- },
+ // {
+ // path: 'button',
+ // name: 'button',
+ // meta: {
+ // title: '按钮',
+ // auth: true
+ // },
+ // component: _import('system/button')
+ // },
+ // // // 系统 菜单权限
+ // {
+ // path: 'menuButton/:id',
+ // name: 'menuButton',
+ // meta: {
+ // title: '菜单按钮',
+ // auth: true
+ // },
+ // component: _import('system/menuButton')
+ // },
// // 系统 角色管理
// {
// path: 'role',
@@ -149,15 +149,15 @@ const frameIn = [{
// component: _import('system/log/operationLog')
// },
// 系统 前端日志
- {
- path: 'frontendLog',
- name: 'frontendLog',
- meta: {
- title: '前端日志',
- auth: true
- },
- component: _import('system/log/frontendLog')
- },
+ // {
+ // path: 'frontendLog',
+ // name: 'frontendLog',
+ // meta: {
+ // title: '前端日志',
+ // auth: true
+ // },
+ // component: _import('system/log/frontendLog')
+ // },
// 刷新页面 必须保留
{
path: 'refresh',
diff --git a/web/src/store/modules/d2admin/modules/account.js b/web/src/store/modules/d2admin/modules/account.js
index e01badb..ce612c4 100644
--- a/web/src/store/modules/d2admin/modules/account.js
+++ b/web/src/store/modules/d2admin/modules/account.js
@@ -42,7 +42,7 @@ export default {
util.cookies.set('token', res.access)
util.cookies.set('refresh', res.refresh)
// 设置 vuex 用户信息
- await dispatch('d2admin/user/set', { name: res.name, user_id: res.userId }, { root: true })
+ await dispatch('d2admin/user/set', { name: res.name, user_id: res.userId,avatar:res.avatar }, { root: true })
// 用户登录后从持久化数据加载一系列的设置
await dispatch('load')
},
diff --git a/web/src/views/system/user/crud.js b/web/src/views/system/user/crud.js
index f0cd911..e151be8 100644
--- a/web/src/views/system/user/crud.js
+++ b/web/src/views/system/user/crud.js
@@ -3,7 +3,7 @@ import { BUTTON_STATUS_BOOL } from '@/config/button'
import { urlPrefix as deptPrefix } from '../dept/api'
import util from '@/libs/util'
-const uploadUrl = util.baseURL() + 'api/system/img/'
+const uploadUrl = util.baseURL() + 'api/system/file/'
export const crudOptions = (vm) => {
return {
pageOptions: {
@@ -261,7 +261,6 @@ export const crudOptions = (vm) => {
props: {
uploader: {
action: uploadUrl,
- name: 'url',
headers: {
Authorization: 'JWT ' + util.cookies.get('token')
},
@@ -270,7 +269,7 @@ export const crudOptions = (vm) => {
if (ret.data === null || ret.data === '') {
throw new Error('上传失败')
}
- return { url: ret.data.data.url, key: option.data.key }
+ return { url: util.baseURL() + ret.data.url, key: option.data.key }
}
},
elProps: { // 与el-uploader 配置一致
@@ -297,7 +296,7 @@ export const crudOptions = (vm) => {
props: {
buildUrl (value, item) {
if (value && value.indexOf('http') !== 0) {
- return '/api/upload/form/download?key=' + value
+ return util.baseURL() + value
}
return value
}
From cd50adb5565e5907e1f013faacbd7215c6227070 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=8C=BF=E5=B0=8F=E5=A4=A9?= <1638245306@qq.com>
Date: Fri, 15 Apr 2022 23:13:09 +0800
Subject: [PATCH 19/25] =?UTF-8?q?=E6=96=B0=E5=8A=9F=E8=83=BD:=20=E5=AF=86?=
=?UTF-8?q?=E7=A0=81=E9=87=8D=E7=BD=AE=E5=8A=9F=E8=83=BD=E5=88=9D=E5=A7=8B?=
=?UTF-8?q?=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
web/src/views/system/log/operationLog/index.vue | 2 --
1 file changed, 2 deletions(-)
diff --git a/web/src/views/system/log/operationLog/index.vue b/web/src/views/system/log/operationLog/index.vue
index 6d88eab..dac12e5 100644
--- a/web/src/views/system/log/operationLog/index.vue
+++ b/web/src/views/system/log/operationLog/index.vue
@@ -57,11 +57,9 @@ export default {
return api.GetList(query)
},
addRequest (row) {
- console.log('api', api)
return api.AddObj(row)
},
updateRequest (row) {
- console.log('----', row)
return api.UpdateObj(row)
},
delRequest (row) {
From 291c78c4113dee0eb37ea9d977888b8aaa803945 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=8C=BF=E5=B0=8F=E5=A4=A9?= <1638245306@qq.com>
Date: Sun, 17 Apr 2022 22:28:53 +0800
Subject: [PATCH 20/25] =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=8F=98=E5=8C=96:?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
注释CDN文件
---
web/dependencies-cdn.js | 40 ++++++++++++++++++++--------------------
1 file changed, 20 insertions(+), 20 deletions(-)
diff --git a/web/dependencies-cdn.js b/web/dependencies-cdn.js
index 1fc5eb1..427c252 100644
--- a/web/dependencies-cdn.js
+++ b/web/dependencies-cdn.js
@@ -1,22 +1,22 @@
module.exports = [
- { name: 'vue', library: 'Vue', js: 'https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.min.js', css: '' },
- { name: 'vue-i18n', library: 'VueI18n', js: 'https://cdn.jsdelivr.net/npm/vue-i18n@8.15.1/dist/vue-i18n.min.js', css: '' },
- { name: 'vue-router', library: 'VueRouter', js: 'https://cdn.jsdelivr.net/npm/vue-router@3.1.3/dist/vue-router.min.js', css: '' },
- { name: 'vuex', library: 'Vuex', js: 'https://cdn.jsdelivr.net/npm/vuex@3.1.2/dist/vuex.min.js', css: '' },
- { name: 'axios', library: 'axios', js: 'https://cdn.jsdelivr.net/npm/axios@0.19.0/dist/axios.min.js', css: '' },
- { name: 'better-scroll', library: 'BScroll', js: 'https://cdn.jsdelivr.net/npm/better-scroll@1.15.2/dist/bscroll.min.js', css: '' },
- { name: 'axios-mock-adapter', library: 'AxiosMockAdapter', js: 'https://cdn.jsdelivr.net/npm/axios-mock-adapter@1.18.1/dist/axios-mock-adapter.min.js', css: '' },
- { name: 'element-ui', library: 'ELEMENT', js: 'https://cdn.jsdelivr.net/npm/element-ui@2.15.5/lib/index.js', css: 'https://cdn.jsdelivr.net/npm/element-ui@2.15.5/lib/theme-chalk/index.css' },
- { name: 'lodash', library: '_', js: 'https://cdn.jsdelivr.net/npm/lodash@4.17.15/lodash.min.js', css: '' },
- { name: 'ua-parser-js', library: 'UAParser', js: 'https://cdn.jsdelivr.net/npm/ua-parser-js@0.7.20/dist/ua-parser.min.js', css: '' },
- { name: 'js-cookie', library: 'Cookies', js: 'https://cdn.jsdelivr.net/npm/js-cookie@2.2.1/src/js.cookie.min.js', css: '' },
- { name: 'nprogress', library: 'NProgress', js: 'https://cdn.jsdelivr.net/npm/nprogress@0.2.0/nprogress.min.js', css: 'https://cdn.jsdelivr.net/npm/nprogress@0.2.0/nprogress.css' },
- { name: 'dayjs', library: 'dayjs', js: 'https://cdn.jsdelivr.net/npm/dayjs@1.8.17/dayjs.min.js', css: '' },
- { name: 'fuse.js', library: 'Fuse', js: 'https://cdn.jsdelivr.net/npm/fuse.js@5.2.3/dist/fuse.min.js', css: '' },
- { name: 'hotkeys-js', library: 'hotkeys', js: 'https://cdn.jsdelivr.net/npm/hotkeys-js@3.7.3/dist/hotkeys.min.js', css: '' },
- { name: 'qs', library: 'Qs', js: 'https://cdn.jsdelivr.net/npm/qs@6.9.1/dist/qs.js', css: '' },
- { name: 'lowdb', library: 'low', js: 'https://cdn.jsdelivr.net/npm/lowdb@1.0.0/dist/low.min.js', css: '' },
- { name: 'lowdb/adapters/LocalStorage', library: 'LocalStorage', js: 'https://cdn.jsdelivr.net/npm/lowdb@1.0.0/dist/LocalStorage.min.js', css: '' },
- { name: 'screenfull', library: 'screenfull', js: 'https://cdn.jsdelivr.net/npm/screenfull@5.0.2/dist/screenfull.min.js', css: '' },
- { name: 'sortablejs', library: 'Sortable', js: 'https://cdn.jsdelivr.net/npm/sortablejs@1.10.1/Sortable.min.js', css: '' }
+ // { name: 'vue', library: 'Vue', js: 'https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.min.js', css: '' },
+ // { name: 'vue-i18n', library: 'VueI18n', js: 'https://cdn.jsdelivr.net/npm/vue-i18n@8.15.1/dist/vue-i18n.min.js', css: '' },
+ // { name: 'vue-router', library: 'VueRouter', js: 'https://cdn.jsdelivr.net/npm/vue-router@3.1.3/dist/vue-router.min.js', css: '' },
+ // { name: 'vuex', library: 'Vuex', js: 'https://cdn.jsdelivr.net/npm/vuex@3.1.2/dist/vuex.min.js', css: '' },
+ // { name: 'axios', library: 'axios', js: 'https://cdn.jsdelivr.net/npm/axios@0.19.0/dist/axios.min.js', css: '' },
+ // { name: 'better-scroll', library: 'BScroll', js: 'https://cdn.jsdelivr.net/npm/better-scroll@1.15.2/dist/bscroll.min.js', css: '' },
+ // { name: 'axios-mock-adapter', library: 'AxiosMockAdapter', js: 'https://cdn.jsdelivr.net/npm/axios-mock-adapter@1.18.1/dist/axios-mock-adapter.min.js', css: '' },
+ // { name: 'element-ui', library: 'ELEMENT', js: 'https://cdn.jsdelivr.net/npm/element-ui@2.15.5/lib/index.js', css: 'https://cdn.jsdelivr.net/npm/element-ui@2.15.5/lib/theme-chalk/index.css' },
+ // { name: 'lodash', library: '_', js: 'https://cdn.jsdelivr.net/npm/lodash@4.17.15/lodash.min.js', css: '' },
+ // { name: 'ua-parser-js', library: 'UAParser', js: 'https://cdn.jsdelivr.net/npm/ua-parser-js@0.7.20/dist/ua-parser.min.js', css: '' },
+ // { name: 'js-cookie', library: 'Cookies', js: 'https://cdn.jsdelivr.net/npm/js-cookie@2.2.1/src/js.cookie.min.js', css: '' },
+ // { name: 'nprogress', library: 'NProgress', js: 'https://cdn.jsdelivr.net/npm/nprogress@0.2.0/nprogress.min.js', css: 'https://cdn.jsdelivr.net/npm/nprogress@0.2.0/nprogress.css' },
+ // { name: 'dayjs', library: 'dayjs', js: 'https://cdn.jsdelivr.net/npm/dayjs@1.8.17/dayjs.min.js', css: '' },
+ // { name: 'fuse.js', library: 'Fuse', js: 'https://cdn.jsdelivr.net/npm/fuse.js@5.2.3/dist/fuse.min.js', css: '' },
+ // { name: 'hotkeys-js', library: 'hotkeys', js: 'https://cdn.jsdelivr.net/npm/hotkeys-js@3.7.3/dist/hotkeys.min.js', css: '' },
+ // { name: 'qs', library: 'Qs', js: 'https://cdn.jsdelivr.net/npm/qs@6.9.1/dist/qs.js', css: '' },
+ // { name: 'lowdb', library: 'low', js: 'https://cdn.jsdelivr.net/npm/lowdb@1.0.0/dist/low.min.js', css: '' },
+ // { name: 'lowdb/adapters/LocalStorage', library: 'LocalStorage', js: 'https://cdn.jsdelivr.net/npm/lowdb@1.0.0/dist/LocalStorage.min.js', css: '' },
+ // { name: 'screenfull', library: 'screenfull', js: 'https://cdn.jsdelivr.net/npm/screenfull@5.0.2/dist/screenfull.min.js', css: '' },
+ // { name: 'sortablejs', library: 'Sortable', js: 'https://cdn.jsdelivr.net/npm/sortablejs@1.10.1/Sortable.min.js', css: '' }
]
From eb412cfde53ff612845bf3e500805f6339a978c1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=9D=8E=E5=BC=BA?= <1206709430@qq.com>
Date: Mon, 18 Apr 2022 01:00:24 +0800
Subject: [PATCH 21/25] =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=8F=98=E5=8C=96:=20?=
=?UTF-8?q?=E5=90=8E=E7=AB=AF=E6=96=B0=E5=A2=9E=E7=99=BB=E5=BD=95=E6=97=A5?=
=?UTF-8?q?=E5=BF=97=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
backend/conf/env.example.py | 1 +
backend/dvadmin/system/models.py | 31 +++++++++++++++-
backend/dvadmin/system/views/login.py | 8 +++--
backend/dvadmin/utils/request_util.py | 51 +++++++++++++++++++++++++++
4 files changed, 88 insertions(+), 3 deletions(-)
diff --git a/backend/conf/env.example.py b/backend/conf/env.example.py
index 4b1b16d..9c8def9 100644
--- a/backend/conf/env.example.py
+++ b/backend/conf/env.example.py
@@ -35,3 +35,4 @@ DATABASE_PASSWORD = "123456"
DEBUG = True # 线上环境请设置为True
ALLOWED_HOSTS = ["*"]
LOGIN_NO_CAPTCHA_AUTH = True # 登录接口 /api/token/ 是否需要验证码认证,用于测试,正式环境建议取消
+ENABLE_LOGIN_ANALYSIS_LOG = True # 启动登录详细概略获取(通过调用api获取ip详细地址)
diff --git a/backend/dvadmin/system/models.py b/backend/dvadmin/system/models.py
index 27dea49..68ea579 100644
--- a/backend/dvadmin/system/models.py
+++ b/backend/dvadmin/system/models.py
@@ -29,7 +29,7 @@ class Users(AbstractUser, CoreModel):
(1, "前台用户"),
)
user_type = models.IntegerField(choices=USER_TYPE, default=0, verbose_name="用户类型", null=True, blank=True,
- help_text="用户类型")
+ help_text="用户类型")
post = models.ManyToManyField(to='Post', verbose_name='关联岗位', db_constraint=False, help_text="关联岗位")
role = models.ManyToManyField(to='Role', verbose_name='关联角色', db_constraint=False, help_text="关联角色")
dept = models.ForeignKey(to='Dept', verbose_name='所属部门', on_delete=models.PROTECT, db_constraint=False, null=True,
@@ -306,3 +306,32 @@ class SystemConfig(CoreModel):
def __str__(self):
return f"{self.title}"
+
+
+class LoginLog(CoreModel):
+ LOGIN_TYPE_CHOICES = (
+ (1, '普通登录'),
+ )
+ username = models.CharField(max_length=32, verbose_name="登录用户名", null=True, blank=True, help_text="登录ip")
+ ip = models.CharField(max_length=32, verbose_name="登录ip", null=True, blank=True, help_text="登录ip")
+ agent = models.TextField(verbose_name="agent信息", null=True, blank=True, help_text="agent信息")
+ browser = models.CharField(max_length=200, verbose_name="浏览器名", null=True, blank=True, help_text="浏览器名")
+ os = models.CharField(max_length=200, verbose_name="操作系统", null=True, blank=True, help_text="操作系统")
+ continent = models.CharField(max_length=50, verbose_name="州", null=True, blank=True, help_text="州")
+ country = models.CharField(max_length=50, verbose_name="国家", null=True, blank=True, help_text="国家")
+ province = models.CharField(max_length=50, verbose_name="省份", null=True, blank=True, help_text="省份")
+ city = models.CharField(max_length=50, verbose_name="城市", null=True, blank=True, help_text="城市")
+ district = models.CharField(max_length=50, verbose_name="县区", null=True, blank=True, help_text="县区")
+ isp = models.CharField(max_length=50, verbose_name="运营商", null=True, blank=True, help_text="运营商")
+ area_code = models.CharField(max_length=50, verbose_name="区域代码", null=True, blank=True, help_text="区域代码")
+ country_english = models.CharField(max_length=50, verbose_name="英文全称", null=True, blank=True, help_text="英文全称")
+ country_code = models.CharField(max_length=50, verbose_name="简称", null=True, blank=True, help_text="简称")
+ longitude = models.CharField(max_length=50, verbose_name="经度", null=True, blank=True, help_text="经度")
+ latitude = models.CharField(max_length=50, verbose_name="纬度", null=True, blank=True, help_text="纬度")
+ login_type = models.IntegerField(default=1, choices=LOGIN_TYPE_CHOICES, verbose_name="登录类型", help_text="登录类型")
+
+ class Meta:
+ db_table = table_prefix + 'system_login_log'
+ verbose_name = '登录日志'
+ verbose_name_plural = verbose_name
+ ordering = ('-create_datetime',)
diff --git a/backend/dvadmin/system/views/login.py b/backend/dvadmin/system/views/login.py
index 3a85d10..90bf163 100644
--- a/backend/dvadmin/system/views/login.py
+++ b/backend/dvadmin/system/views/login.py
@@ -25,6 +25,7 @@ from rest_framework_simplejwt.views import TokenObtainPairView
from application import settings
from dvadmin.system.models import Users
from dvadmin.utils.json_response import SuccessResponse, ErrorResponse, DetailResponse
+from dvadmin.utils.request_util import save_login_log
from dvadmin.utils.serializers import CustomModelSerializer
from dvadmin.utils.validator import CustomValidationError
@@ -51,7 +52,6 @@ class CaptchaView(APIView):
return SuccessResponse(data=json_data)
-
class LoginSerializer(TokenObtainPairSerializer):
"""
登录的序列化器:
@@ -69,7 +69,7 @@ class LoginSerializer(TokenObtainPairSerializer):
}
def validate(self, attrs):
- captcha = self.initial_data.get('captcha',None)
+ captcha = self.initial_data.get('captcha', None)
if settings.CAPTCHA_STATE:
if captcha is None:
raise CustomValidationError("验证码不能为空")
@@ -89,6 +89,10 @@ class LoginSerializer(TokenObtainPairSerializer):
data['name'] = self.user.name
data['userId'] = self.user.id
data['avatar'] = self.user.avatar
+ request = self.context.get('request')
+ request.user = self.user
+ # 记录登录日志
+ save_login_log(request=request)
return {
"code": 2000,
"msg": "请求成功",
diff --git a/backend/dvadmin/utils/request_util.py b/backend/dvadmin/utils/request_util.py
index 87fa3ff..14414fb 100644
--- a/backend/dvadmin/utils/request_util.py
+++ b/backend/dvadmin/utils/request_util.py
@@ -3,12 +3,16 @@ Request工具类
"""
import json
+import requests
+from django.conf import settings
from django.contrib.auth.models import AbstractBaseUser
from django.contrib.auth.models import AnonymousUser
from django.urls.resolvers import ResolverMatch
from rest_framework_simplejwt.authentication import JWTAuthentication
from user_agents import parse
+from dvadmin.system.models import LoginLog
+
def get_request_user(request):
"""
@@ -163,3 +167,50 @@ def get_verbose_name(queryset=None, view=None, model=None):
except Exception as e:
pass
return model if model else ""
+
+
+def get_ip_analysis(ip):
+ """
+ 获取ip详细概略
+ :param ip: ip地址
+ :return:
+ """
+ data = {
+ "continent": "",
+ "country": "",
+ "province": "",
+ "city": "",
+ "district": "",
+ "isp": "",
+ "area_code": "",
+ "country_english": "",
+ "country_code": "",
+ "longitude": "",
+ "latitude": ""
+ }
+ if ip != 'unknown' and ip:
+ if getattr(settings, 'ENABLE_LOGIN_ANALYSIS_LOG', True):
+ res = requests.post(url='https://ip.django-vue-admin.com/ip/analysis', data=json.dumps({"ip": ip}))
+ if res.status_code == 200:
+ res_data = res.json()
+ if res_data.get('code') == 0:
+ data = res_data.get('data')
+ return data
+ return data
+
+
+def save_login_log(request):
+ """
+ 保存登录日志
+ :return:
+ """
+ ip = get_request_ip(request=request)
+ analysis_data = get_ip_analysis(ip)
+ analysis_data['username'] = request.user.username
+ analysis_data['ip'] = ip
+ analysis_data['agent'] = str(parse(request.META['HTTP_USER_AGENT']))
+ analysis_data['browser'] = get_browser(request)
+ analysis_data['os'] = get_os(request)
+ analysis_data['creator_id'] = request.user.id
+ analysis_data['dept_belong_id'] = getattr(request.user, 'dept_id', '')
+ LoginLog.objects.create(**analysis_data)
From 005cb242c8109e9423d6ade8e01e03c429e1d02a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=9D=8E=E5=BC=BA?= <1206709430@qq.com>
Date: Tue, 19 Apr 2022 22:37:26 +0800
Subject: [PATCH 22/25] =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=8F=98=E5=8C=96:=20?=
=?UTF-8?q?=E5=90=8E=E7=AB=AF=E6=96=B0=E5=A2=9E=E7=99=BB=E5=BD=95=E6=97=A5?=
=?UTF-8?q?=E5=BF=97=E6=8E=A5=E5=8F=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
backend/dvadmin/system/urls.py | 5 +++-
backend/dvadmin/system/views/login_log.py | 36 +++++++++++++++++++++++
2 files changed, 40 insertions(+), 1 deletion(-)
create mode 100644 backend/dvadmin/system/views/login_log.py
diff --git a/backend/dvadmin/system/urls.py b/backend/dvadmin/system/urls.py
index 8481e1d..5ed359d 100644
--- a/backend/dvadmin/system/urls.py
+++ b/backend/dvadmin/system/urls.py
@@ -15,6 +15,7 @@ from dvadmin.system.views.button import ButtonViewSet
from dvadmin.system.views.dept import DeptViewSet
from dvadmin.system.views.dictionary import DictionaryViewSet
from dvadmin.system.views.file_list import FileViewSet
+from dvadmin.system.views.login_log import LoginLogViewSet
from dvadmin.system.views.menu import MenuViewSet
from dvadmin.system.views.menu_button import MenuButtonViewSet
from dvadmin.system.views.operation_log import OperationLogViewSet
@@ -43,10 +44,12 @@ urlpatterns = [
path('user/change_password//', UserViewSet.as_view({'put': 'change_password'})),
path('user/reset_password//', UserViewSet.as_view({'put': 'reset_password'})),
path('user/export/', UserViewSet.as_view({'post': 'export_data', })),
- path('user/import/',UserViewSet.as_view({'get': 'import_data', 'post': 'import_data'})),
+ path('user/import/', UserViewSet.as_view({'get': 'import_data', 'post': 'import_data'})),
path('system_config/save_content/', SystemConfigViewSet.as_view({'put': 'save_content'})),
path('system_config/get_association_table/', SystemConfigViewSet.as_view({'get': 'get_association_table'})),
path('system_config/get_table_data//', SystemConfigViewSet.as_view({'get': 'get_table_data'})),
path('system_config/get_relation_info/', SystemConfigViewSet.as_view({'get': 'get_relation_info'})),
+ path('login_log/', LoginLogViewSet.as_view({'get': 'list'})),
+ path('login_log//', LoginLogViewSet.as_view({'get': 'retrieve'})),
]
urlpatterns += system_url.urls
diff --git a/backend/dvadmin/system/views/login_log.py b/backend/dvadmin/system/views/login_log.py
new file mode 100644
index 0000000..4dc3617
--- /dev/null
+++ b/backend/dvadmin/system/views/login_log.py
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+
+"""
+@author: 猿小天
+@contact: QQ:1638245306
+@Created on: 2021/6/3 003 0:30
+@Remark: 按钮权限管理
+"""
+from dvadmin.system.models import LoginLog
+from dvadmin.utils.serializers import CustomModelSerializer
+from dvadmin.utils.viewset import CustomModelViewSet
+
+
+class LoginLogSerializer(CustomModelSerializer):
+ """
+ 登录日志权限-序列化器
+ """
+
+ class Meta:
+ model = LoginLog
+ fields = "__all__"
+ read_only_fields = ["id"]
+
+
+class LoginLogViewSet(CustomModelViewSet):
+ """
+ 登录日志接口
+ list:查询
+ create:新增
+ update:修改
+ retrieve:单例
+ destroy:删除
+ """
+ queryset = LoginLog.objects.all()
+ serializer_class = LoginLogSerializer
+ extra_filter_backends = []
From 4a3aaba0b58844815dc381e2cdc75b71574508d2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=9D=8E=E5=BC=BA?= <1206709430@qq.com>
Date: Wed, 20 Apr 2022 00:02:12 +0800
Subject: [PATCH 23/25] =?UTF-8?q?=E9=87=8D=E6=9E=84:=20=E7=99=BB=E5=BD=95?=
=?UTF-8?q?=E6=97=A5=E5=BF=97=E5=89=8D=E7=AB=AF=E5=AE=8C=E6=88=90?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
web/src/views/system/log/loginLog/api.js | 19 ++
web/src/views/system/log/loginLog/crud.js | 293 ++++++++++++++++++++
web/src/views/system/log/loginLog/index.vue | 48 ++++
3 files changed, 360 insertions(+)
create mode 100644 web/src/views/system/log/loginLog/api.js
create mode 100644 web/src/views/system/log/loginLog/crud.js
create mode 100644 web/src/views/system/log/loginLog/index.vue
diff --git a/web/src/views/system/log/loginLog/api.js b/web/src/views/system/log/loginLog/api.js
new file mode 100644
index 0000000..337a05f
--- /dev/null
+++ b/web/src/views/system/log/loginLog/api.js
@@ -0,0 +1,19 @@
+/*
+ * @创建文件时间: 2021-06-08 10:40:32
+ * @Auther: 猿小天
+ * @最后修改人: 猿小天
+ * @最后修改时间: 2021-06-09 10:36:20
+ * 联系Qq:1638245306
+ * @文件介绍: 操作日志
+ */
+import { request } from '@/api/service'
+
+export const urlPrefix = '/api/system/login_log/'
+
+export function GetList (query) {
+ return request({
+ url: urlPrefix,
+ method: 'get',
+ params: query
+ })
+}
diff --git a/web/src/views/system/log/loginLog/crud.js b/web/src/views/system/log/loginLog/crud.js
new file mode 100644
index 0000000..d7352c8
--- /dev/null
+++ b/web/src/views/system/log/loginLog/crud.js
@@ -0,0 +1,293 @@
+import { BUTTON_WHETHER_BOOL } from '@/config/button'
+
+export const crudOptions = (vm) => {
+ return {
+ pageOptions: {
+ compact: true
+ },
+ options: {
+ tableType: 'vxe-table',
+ rowKey: true, // 必须设置,true or false
+ rowId: 'id',
+ height: '100%', // 表格高度100%, 使用toolbar必须设置
+ highlightCurrentRow: false
+
+ },
+ rowHandle: {
+ fixed: 'right',
+ view: {
+ thin: true,
+ text: '',
+ disabled () {
+ return !vm.hasPermissions('Retrieve')
+ }
+ },
+ width: 70,
+ edit: {
+ thin: true,
+ text: '',
+ show: false,
+ disabled () {
+ return !vm.hasPermissions('Update')
+ }
+ },
+ remove: {
+ thin: true,
+ text: '删除',
+ show: false,
+ disabled () {
+ return !vm.hasPermissions('Delete')
+ }
+ }
+ },
+ viewOptions: {
+ componentType: 'form'
+ },
+ formOptions: {
+ disabled: true,
+ defaultSpan: 12 // 默认的表单 span
+ },
+ indexRow: { // 或者直接传true,不显示title,不居中
+ title: '序号',
+ align: 'center',
+ width: 70
+ },
+ columns: [
+ {
+ title: '关键词',
+ key: 'search',
+ show: false,
+ disabled: true,
+ search: {
+ disabled: false
+ },
+ form: {
+ show: false,
+ component: {
+ placeholder: '请输入关键词'
+ }
+ }
+ },
+ {
+ title: 'ID',
+ key: 'id',
+ width: 90,
+ disabled: true,
+ form: {
+ disabled: true
+ }
+ },
+ {
+ title: '登录用户名',
+ key: 'username',
+ search: {
+ disabled: false
+ },
+ width: 140,
+ type: 'input',
+ form: {
+ disabled: true,
+ component: {
+ placeholder: '请输入登录用户名'
+ }
+ }
+ },
+ {
+ title: '登录ip',
+ key: 'ip',
+ search: {
+ disabled: false
+ },
+ width: 130,
+ type: 'input',
+ form: {
+ disabled: true,
+ component: {
+ placeholder: '请输入登录ip'
+ }
+ }
+ }, {
+ title: '运营商',
+ key: 'isp',
+ search: {
+ disabled: true
+ },
+ disabled: true,
+ width: 180,
+ type: 'input',
+ form: {
+ component: {
+ placeholder: '请输入操作系统'
+ }
+ }
+ }, {
+ title: '大州',
+ key: 'continent',
+ width: 80,
+ type: 'input',
+ form: {
+ disabled: true,
+ component: {
+ placeholder: '请输入州'
+ }
+ },
+ component: { props: { color: 'auto' } } // 自动染色
+ }, {
+ title: '国家',
+ key: 'country',
+ width: 80,
+ type: 'input',
+ form: {
+ component: {
+ placeholder: '请输入国家'
+ }
+ },
+ component: { props: { color: 'auto' } } // 自动染色
+ }, {
+ title: '省份',
+ key: 'province',
+ width: 80,
+ type: 'input',
+ form: {
+ component: {
+ placeholder: '请输入省份'
+ }
+ },
+ component: { props: { color: 'auto' } } // 自动染色
+ }, {
+ title: '城市',
+ key: 'city',
+ width: 80,
+ type: 'input',
+ form: {
+ component: {
+ placeholder: '请输入城市'
+ }
+ },
+ component: { props: { color: 'auto' } } // 自动染色
+ }, {
+ title: '县区',
+ key: 'district',
+ width: 80,
+ type: 'input',
+ form: {
+ component: {
+ placeholder: '请输入县区'
+ }
+ },
+ component: { props: { color: 'auto' } } // 自动染色
+ }, {
+ title: '区域代码',
+ key: 'area_code',
+ width: 100,
+ type: 'input',
+ form: {
+ component: {
+ placeholder: '请输入区域代码'
+ }
+ },
+ component: { props: { color: 'auto' } } // 自动染色
+ }, {
+ title: '英文全称',
+ key: 'country_english',
+ width: 120,
+ type: 'input',
+ form: {
+ component: {
+ placeholder: '请输入英文全称'
+ }
+ },
+ component: { props: { color: 'auto' } } // 自动染色
+ }, {
+ title: '简称',
+ key: 'country_code',
+ width: 100,
+ type: 'input',
+ form: {
+ component: {
+ placeholder: '请输入简称'
+ }
+ },
+ component: { props: { color: 'auto' } } // 自动染色
+ }, {
+ title: '经度',
+ key: 'longitude',
+ width: 80,
+ type: 'input',
+ disabled: true,
+ form: {
+ component: {
+ placeholder: '请输入经度'
+ }
+ },
+ component: { props: { color: 'auto' } } // 自动染色
+ }, {
+ title: '纬度',
+ key: 'latitude',
+ width: 80,
+ type: 'input',
+ disabled: true,
+ form: {
+ component: {
+ placeholder: '请输入纬度'
+ }
+ },
+ component: { props: { color: 'auto' } } // 自动染色
+ }, {
+ title: '登录类型',
+ key: 'login_type',
+ width: 100,
+ type: 'select',
+ search: {
+ disabled: false
+ },
+ dict: {
+ data: [{ label: '普通登录', value: 1 }]
+ },
+ form: {
+ component: {
+ placeholder: '请选择登录类型'
+ }
+ },
+ component: { props: { color: 'auto' } } // 自动染色
+ }, {
+ title: '操作系统',
+ key: 'os',
+ width: 180,
+ type: 'input',
+ form: {
+ component: {
+ placeholder: '请输入操作系统'
+ }
+ }
+ }, {
+ title: '浏览器名',
+ key: 'browser',
+ width: 180,
+ type: 'input',
+ form: {
+ component: {
+ placeholder: '请输入操作系统'
+ }
+ }
+ }, {
+ title: 'agent信息',
+ key: 'agent',
+ disabled: true,
+ width: 180,
+ type: 'input',
+ form: {
+ component: {
+ placeholder: '请输入操作系统'
+ }
+ }
+ }, {
+ fixed: 'right',
+ title: '登录时间',
+ key: 'create_datetime',
+ width: 160,
+ type: 'datetime'
+ }
+ ]
+ }
+}
diff --git a/web/src/views/system/log/loginLog/index.vue b/web/src/views/system/log/loginLog/index.vue
new file mode 100644
index 0000000..cd6c6a3
--- /dev/null
+++ b/web/src/views/system/log/loginLog/index.vue
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
From 7a5d9e5fb0ddb587adbf0ee2be77ffd56b12c20d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=9D=8E=E5=BC=BA?= <1206709430@qq.com>
Date: Wed, 20 Apr 2022 00:10:30 +0800
Subject: [PATCH 24/25] =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=8F=98=E5=8C=96:=20?=
=?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=E7=99=BB=E5=BD=95=E6=97=A5=E5=BF=97?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
backend/dvadmin/system/initialize.py | 48 +++++++++++++++++++++
backend/dvadmin/system/models.py | 2 +-
web/src/views/system/log/loginLog/index.vue | 2 +-
3 files changed, 50 insertions(+), 2 deletions(-)
diff --git a/backend/dvadmin/system/initialize.py b/backend/dvadmin/system/initialize.py
index 2a2c463..a6d2198 100644
--- a/backend/dvadmin/system/initialize.py
+++ b/backend/dvadmin/system/initialize.py
@@ -518,6 +518,27 @@ class Initialize(CoreInitialize):
"creator_id": 1,
"parent_id": 15,
"is_catalog": 0
+ },
+ {
+ "id": 20,
+ "description": "",
+ "modifier": "1",
+ "dept_belong_id": 1,
+ "update_datetime": datetime.datetime.now(),
+ "create_datetime": datetime.datetime.now(),
+ "icon": "file-text",
+ "name": "登录日志",
+ "sort": 1,
+ "is_link": 0,
+ "web_path": "/loginLog",
+ "component": "system/log/loginLog/index",
+ "component_name": "loginLog",
+ "status": 1,
+ "cache": 0,
+ "visible": 1,
+ "creator_id": 1,
+ "parent_id": 15,
+ "is_catalog": 0
}
]
self.save(Menu, self.menu_data, "菜单表")
@@ -1268,6 +1289,33 @@ class Initialize(CoreInitialize):
"method": 2,
"creator_id": 1,
"menu_id": 3
+ },{
+ "id": 54,
+ "description": None,
+ "modifier": "1",
+ "dept_belong_id": "1",
+ "update_datetime": datetime.datetime.now(),
+ "create_datetime": datetime.datetime.now(),
+ "name": "查询",
+ "value": "Search",
+ "api": "/api/system/login_log/",
+ "method": 0,
+ "creator_id": 1,
+ "menu_id": 20
+ },
+ {
+ "id": 55,
+ "description": None,
+ "modifier": "1",
+ "dept_belong_id": "1",
+ "update_datetime": datetime.datetime.now(),
+ "create_datetime": datetime.datetime.now(),
+ "name": "详情",
+ "value": "Retrieve",
+ "api": "/api/system/login_log/{id}/",
+ "method": 0,
+ "creator_id": 1,
+ "menu_id": 20
}
]
self.save(MenuButton, self.menu_button_data, "菜单按钮表")
diff --git a/backend/dvadmin/system/models.py b/backend/dvadmin/system/models.py
index 68ea579..798612e 100644
--- a/backend/dvadmin/system/models.py
+++ b/backend/dvadmin/system/models.py
@@ -312,7 +312,7 @@ class LoginLog(CoreModel):
LOGIN_TYPE_CHOICES = (
(1, '普通登录'),
)
- username = models.CharField(max_length=32, verbose_name="登录用户名", null=True, blank=True, help_text="登录ip")
+ username = models.CharField(max_length=32, verbose_name="登录用户名", null=True, blank=True, help_text="登录用户名")
ip = models.CharField(max_length=32, verbose_name="登录ip", null=True, blank=True, help_text="登录ip")
agent = models.TextField(verbose_name="agent信息", null=True, blank=True, help_text="agent信息")
browser = models.CharField(max_length=200, verbose_name="浏览器名", null=True, blank=True, help_text="浏览器名")
diff --git a/web/src/views/system/log/loginLog/index.vue b/web/src/views/system/log/loginLog/index.vue
index cd6c6a3..6610bd2 100644
--- a/web/src/views/system/log/loginLog/index.vue
+++ b/web/src/views/system/log/loginLog/index.vue
@@ -30,7 +30,7 @@ import { crudOptions } from './crud'
import { d2CrudPlus } from 'd2-crud-plus'
export default {
- name: 'operationLog',
+ name: 'loginLog',
mixins: [d2CrudPlus.crud],
data () {
From c3dfb3bed4b901b20d5968052f401e81033109d7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=9D=8E=E5=BC=BA?= <1206709430@qq.com>
Date: Wed, 20 Apr 2022 00:36:26 +0800
Subject: [PATCH 25/25] =?UTF-8?q?=E9=87=8D=E6=9E=84:=20=E5=89=8D=E7=AB=AF?=
=?UTF-8?q?=E4=BC=98=E5=8C=96=E5=8F=8A=E4=B8=BB=E9=A2=98logo?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
web/public/image/theme/chester/logo/all.png | Bin 3421 -> 7787 bytes
.../image/theme/chester/logo/icon-only.png | Bin 2617 -> 5490 bytes
web/public/image/theme/chester/preview@2x.png | Bin 8710 -> 0 bytes
web/public/image/theme/element/logo/all.png | Bin 3392 -> 7836 bytes
.../image/theme/element/logo/icon-only.png | Bin 2288 -> 5509 bytes
web/public/image/theme/element/preview@2x.png | Bin 14388 -> 0 bytes
web/public/image/theme/line/logo/all.png | Bin 3413 -> 7840 bytes
.../image/theme/line/logo/icon-only.png | Bin 2260 -> 5505 bytes
web/public/image/theme/line/preview@2x.png | Bin 384595 -> 0 bytes
web/public/image/theme/star/logo/all.png | Bin 3290 -> 7501 bytes
.../image/theme/star/logo/icon-only.png | Bin 2225 -> 5036 bytes
web/public/image/theme/star/preview@2x.png | Bin 1978680 -> 0 bytes
.../theme/tomorrow-night-blue/logo/all.png | Bin 3696 -> 8048 bytes
.../tomorrow-night-blue/logo/icon-only.png | Bin 2525 -> 6304 bytes
.../theme/tomorrow-night-blue/preview@2x.png | Bin 10130 -> 0 bytes
web/public/image/theme/violet/logo/all.png | Bin 3290 -> 8030 bytes
.../image/theme/violet/logo/icon-only.png | Bin 2225 -> 5990 bytes
web/public/image/theme/violet/preview@2x.png | Bin 46362 -> 0 bytes
web/src/setting.js | 64 +++++++++---------
web/src/views/system/fileList/crud.js | 8 ++-
web/src/views/system/log/operationLog/crud.js | 27 ++++++--
web/src/views/system/user/crud.js | 14 ++--
web/src/views/system/whiteList/crud.js | 5 +-
23 files changed, 72 insertions(+), 46 deletions(-)
delete mode 100644 web/public/image/theme/chester/preview@2x.png
delete mode 100644 web/public/image/theme/element/preview@2x.png
delete mode 100644 web/public/image/theme/line/preview@2x.png
delete mode 100644 web/public/image/theme/star/preview@2x.png
delete mode 100644 web/public/image/theme/tomorrow-night-blue/preview@2x.png
delete mode 100644 web/public/image/theme/violet/preview@2x.png
diff --git a/web/public/image/theme/chester/logo/all.png b/web/public/image/theme/chester/logo/all.png
index bb858c3f75d5bbf65638b5c66bc81cbae9cb31b1..4d67b0c894ab68f7e7e089c9d8641b31d62d267d 100644
GIT binary patch
delta 7335
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
literal 3421
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
diff --git a/web/public/image/theme/chester/preview@2x.png b/web/public/image/theme/chester/preview@2x.png
deleted file mode 100644
index 7841526d4861cb45ca2e0d17d49afa334f9a8d65..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 8710
zcmeHNdpMM9*MFpqU7~jOmP47Sq#A>99L-SVkfabLhdpM-m@uO;Lr9d}wo49?Br&z4
zwyhzh19Ir(P$5$x5jh_da?0sjk5TWw-s^qeYyb28@m=5Bb#>)=o^`MFTWkH+y4QW{
z)Slhu%a+J5K@en_rG=?2f`}F&h{#KcMeyePkhS~Je|#+t_#w#BmFQm)VX=bN-2XNNynS-|@*75mu-`qFT=G(Q%XO`+}Zr!hGyUyV^I-`4ta+1~S>
zQYko^H_Zpc`~aBdH&eczEDp=hll5=>`1bjmH@*ZDHjT_-vF%wb@2`p4^HmGR!~{)@
z0cN8$nZjVAs?|_f3#(|RWDbp}tfir)iPg}JuP)I(nNm^a&aoLQ!iV1C`7n
z|2<(AmEjTicSNnN36@Mh4w*@zS(*}+0hBs}K_zHtX=%{hDLPmU4YCGSThD-ob=QPP
zJYK^?6R&}H*TU<}d-WG$l4SGGG6zqHXAkr%7^UzSFI>2Drhv&^w8HKYwBUCni_blwzfVMtFMjs
z!0Kr0llAq<1~d&liZX^uA$YLZK4c&Z!-q_#;e45NWz0X&n6SKAY-<)3VvhgodrK3O
zJ#3Z-!y7*M*_v<1Sne><)ilu6#cHW*qTB}<;I0M34-y@?P)&BU!wWxoGcfZMC6Fm-
zWe}AqC?#lA<%QRbe-Y-t=a5isfF})<{s)!_#r#+voB%SLwv7%c`Fm8v{W}l*$o~Hs
z|KFp@|2F=G08>24Ogar3E>0Qs2#2;NVR~@?oVodLe`#Z1#i2K%KYnY<@bImz)0m)|
z4Nd!6+FA(&QMh4gy3PK`rGchuuSR-m%g`-lD+kL>eGyjcE21f^+u?O7H`m_mXOQ29
zszf-)J>I_ml45^&f4F+h+B{S7!7#%J$DZ7qAh_{g5)fPB@k5}Dg;NKZgjDw8__eypO{)v?m
z$RWrH3p#>GD5r=ZnBO>JNEqh7FaI9~1anGEOl<1Qw2>O-z;wo9(9i;8p9gXRjXEQN!%PfCvc%-j>Dp%(b3U;*XZMSYluO)
zE62AYSW7z6nw&&@)!bY>j%%t#;w%ZG%Mau^%dJ|~IOq66Vo}Aq6p`N;)ZEw389B}I
zss($cM13`SB#pIIfBkhVM(kr?Zd(>U))cJif0>GL+;qEoj$5D;_9U(fF@1T|CzzF9
zddy?-@%iUH>NZAr`TJ#OFtPh#fvbWNZ;O<4ad&9nBeD56sq%8zfx^2@adO)*Ie@ni
zuc4uDaDddp`cxrjp#^4RwvIITTY