工單系統之使用者模組整體實現
阿新 • • 發佈:2020-11-25
1 使用者模組管理分析
1.1 管理模組部分功能展示
1.2 前端總體業務邏輯分析
- Vue元件思想
vue本身就是一個元件,所以我們在使用的過程中,一定要有元件思想。本專案就是利用父子元件傳值,呼叫方法來進行編寫的。
1.2.1 Home頁面
- 其中巢狀元件Header和LeftMenu
總頁面,主要用於佈局。用於顯示左側選單和頭部資訊。其中巢狀子路由用於不同模組之間的跳轉。
- Header用於頭部資訊展示
- LeftMenu用於左側選單展示
- 模組跳轉部分巢狀子路由
1.2.2 Index頁面
- 巢狀四個元件
BreadCrumb,EditForm,Search,TableList,用於模組之間的跳轉變化。
- BreadCrumb用於頂部路徑展示
- EditForm用於控制輸入框資訊,修改和新增時有所不同
- 修改(在原有資料基礎上進行修改,並且不支援密碼和使用者名稱的修改)
-
- 新增
- Search控制查詢
- TableList控制表格展示,會隨著Search輸入的查詢資料變化
1.3 後端總體業務邏輯分析
1.3.1 ModelViewSet
- 用於使用者資訊管理的增刪改查
- 注意事項:
* 查詢介面 http://192.168.56.100:1594/user/user/?username='cat' * 新增介面 http://192.168.56.100:1594/user/user/ * 刪除介面(指定使用者id刪除) http://192.168.56.100:1594/user/user/id/ * 修改介面(指定使用者id修改) http://192.168.56.100:1594/user/user/id/
1.3.2 APIView
- 用於註冊使用者的業務邏輯
2 Django端
2.1 settings.py
""" Django settings for opwf project. Generated by 'django-admin startproject' using Django 2.0.13. For more information on this file, see https://docs.djangoproject.com/en/2.0/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/2.0/ref/settings/ """ import datetime import os, sys # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, os.path.join(BASE_DIR, 'apps')) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = 'uorj1ni^mnut@wo@c%)iv)%5=8dxlml4-j0!f3b%4#f*8a5)3t' # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True ALLOWED_HOSTS = ['*'] # Application definition INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'rest_framework', 'corsheaders', 'user.apps.UserConfig', 'workflow.apps.WorkflowConfig', 'workerorder.apps.WorkerorderConfig', # 'jwt', # 'rest_framework_jwt', # 'rest_framework.authentication' ] MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'corsheaders.middleware.CorsMiddleware', # 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] ROOT_URLCONF = 'opwf.urls' CORS_ORIGIN_ALLOW_ALL = True TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ] WSGI_APPLICATION = 'opwf.wsgi.application' # Database # https://docs.djangoproject.com/en/2.0/ref/settings/#databases DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'opwf_db', 'USER': 'root', 'PASSWORD': '1', 'HOST': '127.0.0.1', 'PORT': '3306' } } # Password validation # https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators REST_FRAMEWORK = { # 文件報錯: AttributeError: ‘AutoSchema’ object has no attribute ‘get_link’ # 用下面的設定可以解決 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.AutoSchema', # 預設設定是: # 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.openapi.AutoSchema', # 異常處理器 # 'EXCEPTION_HANDLER': 'user.utils.exception_handler', # Base API policies 預設渲染器類 'DEFAULT_RENDERER_CLASSES': [ 'rest_framework.renderers.JSONRenderer', 'rest_framework.renderers.BrowsableAPIRenderer', ], # 預設解析器類 'DEFAULT_PARSER_CLASSES': [ 'rest_framework.parsers.JSONParser', 'rest_framework.parsers.FormParser', 'rest_framework.parsers.MultiPartParser' ], # 1.認證器(全域性) 'DEFAULT_AUTHENTICATION_CLASSES': [ 'rest_framework_jwt.authentication.JSONWebTokenAuthentication', # 在 DRF中配置JWT認證 # 'rest_framework.authentication.SessionAuthentication', # 使用session時的認證器 # 'rest_framework.authentication.BasicAuthentication' # 提交表單時的認證器 ], # 2.許可權配置(全域性): 順序靠上的嚴格 'DEFAULT_PERMISSION_CLASSES': [ # 'rest_framework.permissions.IsAdminUser', # 管理員可以訪問 # 'rest_framework.permissions.IsAuthenticated', # 認證使用者可以訪問 # 'rest_framework.permissions.IsAuthenticatedOrReadOnly', # 認證使用者可以訪問, 否則只能讀取 'rest_framework.permissions.AllowAny', # 所有使用者都可以訪問 ], # 3.限流(防爬蟲) 'DEFAULT_THROTTLE_CLASSES': [ 'rest_framework.throttling.AnonRateThrottle', 'rest_framework.throttling.UserRateThrottle', ], # 3.1限流策略 # 'DEFAULT_THROTTLE_RATES': { # 'user': '100/hour', # 認證使用者每小時100次 # 'anon': '300/day', # 未認證使用者每天能訪問3次 # }, 'DEFAULT_CONTENT_NEGOTIATION_CLASS': 'rest_framework.negotiation.DefaultContentNegotiation', 'DEFAULT_METADATA_CLASS': 'rest_framework.metadata.SimpleMetadata', 'DEFAULT_VERSIONING_CLASS': None, # 4.分頁(全域性):全域性分頁器, 例如 省市區的資料自定義分頁器, 不需要分頁 # 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', # 每頁返回數量 # 'PAGE_SIZE': 1 # 5.過濾器後端 'DEFAULT_FILTER_BACKENDS': [ 'django_filters.rest_framework.DjangoFilterBackend', # 'django_filters.rest_framework.backends.DjangoFilterBackend', 包路徑有變化 ], # 5.1過濾排序(全域性):Filtering 過濾排序 'SEARCH_PARAM': 'search', 'ORDERING_PARAM': 'ordering', 'NUM_PROXIES': None, # 6.版本控制:Versioning 介面版本控制 'DEFAULT_VERSION': None, 'ALLOWED_VERSIONS': None, 'VERSION_PARAM': 'version', # Authentication 認證 # 未認證使用者使用的使用者型別 'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser', # 未認證使用者使用的Token值 'UNAUTHENTICATED_TOKEN': None, # View configuration 'VIEW_NAME_FUNCTION': 'rest_framework.views.get_view_name', 'VIEW_DESCRIPTION_FUNCTION': 'rest_framework.views.get_view_description', 'NON_FIELD_ERRORS_KEY': 'non_field_errors', # Testing 'TEST_REQUEST_RENDERER_CLASSES': [ 'rest_framework.renderers.MultiPartRenderer', 'rest_framework.renderers.JSONRenderer' ], 'TEST_REQUEST_DEFAULT_FORMAT': 'multipart', # Hyperlink settings 'URL_FORMAT_OVERRIDE': 'format', 'FORMAT_SUFFIX_KWARG': 'format', 'URL_FIELD_NAME': 'url', # Encoding 'UNICODE_JSON': True, 'COMPACT_JSON': True, 'STRICT_JSON': True, 'COERCE_DECIMAL_TO_STRING': True, 'UPLOADED_FILES_USE_URL': True, # Browseable API 'HTML_SELECT_CUTOFF': 1000, 'HTML_SELECT_CUTOFF_TEXT': "More than {count} items...", # Schemas 'SCHEMA_COERCE_PATH_PK': True, 'SCHEMA_COERCE_METHOD_NAMES': { 'retrieve': 'read', 'destroy': 'delete' }, # 'Access-Control-Allow-Origin':'http://localhost:8080', # 'Access-Control-Allow-Credentials': True } AUTH_PASSWORD_VALIDATORS = [ { 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', }, { 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', }, { 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', }, { 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', }, ] # Internationalization # https://docs.djangoproject.com/en/2.0/topics/i18n/ LANGUAGE_CODE = 'zh-hans' TIME_ZONE = 'Asia/Shanghai' USE_I18N = True USE_L10N = True USE_TZ = False # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/2.0/howto/static-files/ STATIC_URL = '/static/' AUTH_USER_MODEL = 'user.User' # jwt載荷中的有效期設定 JWT_AUTH = { # 1.token字首:headers中 Authorization 值的字首 'JWT_AUTH_HEADER_PREFIX': 'JWT', # 2.token有效期:一天有效 'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1), # 3.重新整理token:允許使用舊的token換新token 'JWT_ALLOW_REFRESH': True, # 4.token有效期:token在24小時內過期, 可續期token 'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(hours=24), # 5.自定義JWT載荷資訊:自定義返回格式,需要手工建立 'JWT_RESPONSE_PAYLOAD_HANDLER': 'user.utils.jwt_response_payload_handler', }
2.2 user/utils.py
# -*- coding: utf-8 -*-
def jwt_response_payload_handler(token, user=None, request=None, role=None):
"""
自定義jwt認證成功返回資料
:token 返回的jwt
:user 當前登入的使用者資訊[物件]
:request 當前本次客戶端提交過來的資料
:role 角色
"""
if user.first_name:
name = user.first_name
else:
name = user.username
return {
'authenticated': 'true',
'id': user.id,
"role": role,
'name': name,
'username': user.username,
'email': user.email,
'token': token,
}
2.3 user/views.py
import datetime
import random
from django.contrib.auth.hashers import make_password
from django.shortcuts import render
# Create your views here.
from rest_framework import viewsets
from rest_framework.response import Response
from rest_framework.views import APIView
from user.models import User
from user.serializers import UserSerializer, UserModelSerializer
class UserViewSet(viewsets.ModelViewSet):
# 負責使用者資訊的增刪改查
queryset = User.objects.all()
serializer_class = UserModelSerializer
filter_fields = {"username"}
class RegisterView(APIView):
# 註冊有一定業務邏輯,所以用APIView
def post(self, request):
username = request.data.get('username')
password = request.data.get('password')
password_new = request.data.get('password_new')
if not all([username, password, password_new]):
return Response(
{'msg': '資訊不全', 'code': 400}
)
if password != password_new:
return Response(
{'code': 400, 'msg':'兩次登入密碼不一致'}
)
user_serializer = UserSerializer(data=request.data)
if user_serializer.is_valid():
user_serializer.save()
user_info = User.objects.filter(username=username).first()
return Response(
{'msg': '註冊成功', 'code': 200, 'data':user_serializer.data}
)
return Response(
{'msg': '註冊失敗', 'error': user_serializer.errors }
)
2.4 user/serializers.py
# -*- coding: utf-8 -*-
from rest_framework import serializers
from rest_framework_jwt.serializers import jwt_payload_handler
from rest_framework_jwt.settings import api_settings
from user.models import User
class UserModelSerializer(serializers.ModelSerializer):
# 用於ModelViewSet的增刪改查
class Meta:
model = User
fields = '__all__'
def create(self, data):
username = data.get('username', '')
password = data.get('password', '')
mobile = data.get('mobile', '')
email = data.get('email', '')
user = User(username=username, email=email, mobile=mobile)
user.set_password(password)
user.save()
return user
# 只用於登入,註冊
class UserSerializer(serializers.Serializer):
# 用serialzier原方法進行方法重寫
username = serializers.CharField()
password = serializers.CharField()
mobile = serializers.CharField()
email = serializers.EmailField()
token = serializers.CharField(read_only=True)
def create(self, data):
username = data.get('username', '')
password = data.get('password', '')
mobile = data.get('mobile', '')
email = data.get('email', '')
user = User(username=username, email=email, mobile=mobile)
user.set_password(password)
user.save()
# 補充生成記錄登入狀態的token
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)
user.token = token
return user
# 重寫
# def update(self, instance, validated_data):
# instance.mobile = validated_data.get('mobile')
# instance.email = validated_data.get('email')
# instance.save()
# return instance
2.5 user/urls.py
# -*- coding: utf-8 -*-
from django.urls import path
from rest_framework.routers import DefaultRouter
from rest_framework_jwt.views import obtain_jwt_token
from user import views
router = DefaultRouter()
router.register(r'user', views.UserViewSet)
urlpatterns = [
path('login/', obtain_jwt_token),
path('register/', views.RegisterView.as_view()),
]
urlpatterns += router.urls
3 Vue端
- 目錄結構
3.1 main.js
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import router from './router'
// 使用ant-design-vue
import Antd from 'ant-design-vue';
import 'ant-design-vue/dist/antd.css';
Vue.config.productionTip = false
Vue.use(Antd);
// router.beforeEach((to, from, next) => {
// if (to.path =='/login' || localStorage.getItem("token")) {
// next()
// } else {
// alert("尚未登入,請先登入")
// return next("/login")
// }
// })
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
components: { App },
template: '<App/>'
})
3.2 http/api.js
//將我們http.js中封裝好的 get,post.put,delete,patch 導過來
import { axios_get, axios_post, axios_delete, axios_put, axios_patch } from './index.js'
//按照格式確定方法名
export const user_login = P => axios_post("/user/login/", P) // 使用者登入
// 使用者模組
export const get_userlist = P => axios_get('/user/user/', P) // 獲取使用者列表
export const add_user = P => axios_post('/user/register/', P) // 註冊新使用者
export const search_for = P => axios_get('user/user/?username=' + P.username) // 根據使用者名稱查詢指定使用者資訊並展示
export const delete_user = P => axios_delete('/user/user/' + P + '/') // 根據獲取到的使用者id刪除使用者資訊
export const update_user = P => axios_put('/user/user/'+ P.id +'/', P) // 根絕使用者id和提交來的資料修改使用者資訊
// export const get_dept_list = p => axios_get("/account/deptManage/", p) //
3.3 http/index.js
import axios from 'axios'
// 第一步:設定axios
axios.defaults.baseURL = "http://192.168.56.100:1594/"
//全域性設定網路超時
axios.defaults.timeout = 10000;
//設定請求頭資訊
axios.defaults.headers.post['Content-Type'] = 'application/json';
axios.defaults.headers.put['Content-Type'] = 'application/json';
// 第二:設定攔截器
/**
* 請求攔截器(當前端傳送請求給後端前進行攔截)
* 例1:請求攔截器獲取token設定到axios請求頭中,所有請求介面都具有這個功能
* 例2:到使用者訪問某一個頁面,但是使用者沒有登入,前端頁面自動跳轉 /login/ 頁面
*/
axios.interceptors.request.use(
config => {
// 每次傳送請求之前判斷是否存在token,如果存在,則統一在http請求的header都加上token,不用每次請求都手動添加了
const token = localStorage.getItem("token")
// console.log(token)
if (token) {
config.headers.Authorization = 'JWT ' + token
}
return config;
},
error => {
return Promise.error(error);
})
axios.interceptors.response.use(
// 請求成功,因為 API返回的狀態碼有多個,所以一定要在這裡寫上,不然會無法訪問頁面
res => res.status === 200 || 201 || 204 ? Promise.resolve(res) : Promise.reject(res),
// 請求失敗
error => {
if (error.response) {
// 判斷一下返回結果的status == 401? ==401跳轉登入頁面。 !=401passs
// console.log(error.response)
if (error.response.status === 401) {
// 跳轉不可以使用this.$router.push方法、
// this.$router.push({path:'/login'})
window.location.href = "http://127.0.0.1:8080/"
} else {
// errorHandle(response.status, response.data.message);
return Promise.reject(error.response);
}
// 請求已發出,但是不在2xx的範圍
} else {
// 處理斷網的情況
// eg:請求超時或斷網時,更新state的network狀態
// network狀態在app.vue中控制著一個全域性的斷網提示元件的顯示隱藏
// 關於斷網元件中的重新整理重新獲取資料,會在斷網元件中說明
// store.commit('changeNetwork', false);
return Promise.reject(error.response);
}
});
// 第三:封裝axios請求
// 3.1 封裝get請求
export function axios_get(url, params) {
return new Promise(
(resolve, reject) => {
axios.get(url, {params:params})
.then(res => {
// console.log("封裝資訊的的res", res)
resolve(res.data)
}).catch(err => {
reject(err.data)
})
}
)
}
// 3.2 封裝post請求
export function axios_post(url, data) {
return new Promise(
(resolve, reject) => {
// console.log(data)
axios.post(url, JSON.stringify(data))
.then(res => {
// console.log("封裝資訊的的res", res)
resolve(res.data)
}).catch(err => {
reject(err.data)
})
}
)
}
// 3.3 封裝put請求
export function axios_put(url, data) {
return new Promise(
(resolve, reject) => {
// console.log(data)
axios.put(url, JSON.stringify(data))
.then(res => {
// console.log("封裝資訊的的 res", res)
resolve(res.data)
}).catch(err => {
reject(err.data)
})
}
)
}
// 3.4 封裝patch請求(可用於區域性修改)
export function axios_patch(url, data) {
return new Promise(
(resolve, reject) => {
// console.log(data)
axios.patch(url, JSON.stringify(data))
.then(res => {
// console.log("封裝資訊的的res", res)
resolve(res.data)
}).catch(err => {
reject(err.data)
})
}
)
}
// 3.5 封裝delete請求
export function axios_delete(url, data) {
return new Promise(
(resolve, reject) => {
// console.log(data)
axios.delete(url, { params: data })
.then(res => {
// console.log("封裝資訊的的res", res)
resolve(res.data)
}).catch(err => {
// reject(err.data)
})
}
)
}
3.4 router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import Home from '@/components/layout/Home'
const page = name => () => import('@/views/' + name)
Vue.use(Router)
export default new Router({
mode: 'history',
routes: [
{ path: '/login',component: page('Login'),name: '登入'},
// 2.使用者管理模組
{ path: '/',component: Home,name: 'home',
children: [
{ path: 'usermanage', component: page('user-manage/index'), name: '使用者管理' },
{ path: 'flowconf', component: page('workflow/WorkFlowConf'), name: '模板管理' },
{ path: 'flowtype', component: page('workflow/WorkFlowType'), name: '模板管理' },
{ path: 'baidu', component: page('BaiDu'), name: '跳轉百度' },
]
},
// 3.配置工單模板模組
// { path: '/',component: Home,name: 'home',
// children: [
// { path: '/rolemanage', component: page('role-manage/RoleManage'), name: '角色管理' },
// { path: '/flowconf', component: page('flow-conf/FlowConf'), name: '模板管理' },
// ]
// },
]
})
3.5 Home.vue
<template>
<div id="components-layout-demo-basic">
<a-layout>
<a-layout-sider>
<LeftMenu style="margin-left:-55px"></LeftMenu>
<!-- 以元件發昂視匯入左側選單 -->
</a-layout-sider>
<a-layout>
<a-layout-header>
<Header/>
<!-- 匯入頭部 -->
</a-layout-header>
<div>
<!-- 這裡的 router-view 是繫結的路由 -->
<router-view></router-view>
</div>
</a-layout>
</a-layout>
</div>
</template>
<script>
// 匯入元件
import LeftMenu from '@/components/layout/LeftMenu'
import Header from '@/components/layout/Header'
export default {
// 註冊元件
components:{
LeftMenu,
Header
},
data() {
return {
}
},
methods: {
},
created() {
}
}
</script>
<style scoped>
#components-layout-demo-basic {
text-align: center;
}
#components-layout-demo-basic .ant-layout-header,
#components-layout-demo-basic .ant-layout-footer {
background: white;
color: white;
}
#components-layout-demo-basic .ant-layout-footer {
line-height: 1.5;
}
#components-layout-demo-basic .ant-layout-sider {
background: white;
color: white;
line-height: 120px;
}
#components-layout-demo-basic .ant-layout-content {
background: white;
color: white;
min-height: 120px;
line-height: 120px;
}
</style>
3.6 Header.vue
<template>
<div>
<h3 style="margin-top:20px"><a-icon type="smile" theme="outlined" style="font-size:30px"/> 歡迎你:{{username}}</h3>
</div>
</template>
<script>
export default{
data(){
return{
username:localStorage.getItem('username')
}
}
}
</script>
3.7 LeftMenu.vue
<template>
<div>
<a-icon type="smile"/>
<a-switch :default-checked="false" @change="changeMode"/>
<span className="ant-divider"/>
<a-icon type="bulb"/>
<a-switch :default-checked="false" @change="changeTheme"/>
<a-menu
style="width: 256px"
:default-selected-keys="['1']"
:default-open-keys="['sub1']"
:mode="mode"
:theme="theme"
@click="handleClick"
>
<!-- 必須定義click方法,handleClick是用來跳轉路由的,內建毀掉引數e,包括傳遞上去的key路由地址 -->
<a-menu-item key="usermanage">
<!-- key是路由地址 -->
<a-icon type="user" />
使用者管理模組
</a-menu-item>
<a-menu-item key="baidu">
<a-icon type="calendar" />
百度翻譯
</a-menu-item>
<a-sub-menu key="workflow">
<span slot="title">
<a-icon type="appstore" />
<span>工單管理系統</span>
</span>
<a-menu-item key="flowtype" title="工單分類">
工單分類
</a-menu-item>
<a-menu-item key="flowconf" title="工單模板">
工單模板
</a-menu-item>
<a-menu-item key="newflowuserroleactionconf" title="配置審批流">
配置審批流
</a-menu-item>
</a-sub-menu>
<a-sub-menu key="workorder">
<span slot="title"><a-icon type="setting" /><span>申請工單</span></span>
<a-menu-item key="workorder">
例項化工單
</a-menu-item>
<a-menu-item key="suborder">
例項化子工單
</a-menu-item>
</a-sub-menu>
</a-menu>
</div>
</template>
<script>
export default {
data() {
return {
mode: 'inline',
theme: 'light',
current: '1'
};
},
methods: {
changeMode(checked) {
this.mode = checked ? 'vertical' : 'inline';
},
changeTheme(checked) {
this.theme = checked ? 'dark' : 'light';
},
handleClick(e) {
this.current = e.key;
this.$router.push({path:this.current});
// click方法預設回撥引數中的 key,所以 e.key就是傳遞來的路由
},
},
};
</script>
3.8 index.vue
<template>
<div>
<div id="components-layout-demo-basic">
<a-layout>
<a-layout-header>
<BreadCrumb style="float:left"></BreadCrumb>
</a-layout-header>
<a-layout>
<a-layout-content>
<div style="margin-bottom:80px">
<a-button type="primary" ghost style="float:left;margin-left:20px" @click="addNew">
AddUser
</a-button>
<EditForm
:visible.sync="visible"
:userList='userList'
@add="add"
>
<!-- .sync控制組件是否顯示 -->
</EditForm>
<Search
style="margin-bottom:-20px;margin-top:10px"
:searchList="searchList"
@find="find"
@getUser="getUser"
>
</Search>
</div>
<TableList
:userListGet="userListGet"
:userList="userList"
@getUser="getUser"
@add="add"
>
</TableList>
<Pagination
@getPage="getPage"
:count="count"
></Pagination>
</a-layout-content>
</a-layout>
</a-layout>
</div>
</div>
</template>
<script>
import BreadCrumb from "./components/BreadCrumb";
import TableList from "./components/TableList";
import Search from "./components/Search";
import EditForm from "./components/EditForm";
import Pagination from "./components/Pagination"
import { add_user, search_for, get_userlist, update_user } from '@/http/apis';
import { delete_user } from '../../http/apis';
export default {
components:{
BreadCrumb,
TableList,
Search,
EditForm,
Pagination
},
data() {
return {
visible:false,
userList: {
'id':'',
'username': '',
'passowrd': '',
'password_new': '',
'email':'',
'mobile':'',
'weixin':''
},
userListGet:[],
searchList:{
'username':'',
'page':1,
'page_size':4
},
updateUserList:[],
// 當前頁碼
current:1,
// 總共的資料多少條
count:0
}
},
methods: {
addNew(){
this.visible = true
this.userList = {
'id':'',
'username': '',
'passowrd': '',
'password_new': '',
'email':'',
'mobile':'',
'weixin':''
}
// 用於控制組件顯示
},
add(){
if(this.userList.id){
this.visible = true
update_user(this.userList).then(res=>{
// alert('修改成功')
this.getUser()
})
}else{
// 新增使用者,子元件中編輯的值實際上是寫在父元件上面的
add_user(this.userList).then(res=>{
console.log(res)
alert('新增新使用者成功')
this.getUser()
})
this.visible=false
}
},
find(){
// 根據使用者名稱查詢使用者資訊
search_for(this.searchList).then(res=>{
// 闊落的辦法可以解決bug,但是不支援查詢出多條資料,因為沒辦法分頁
// if(this.searchList.username){
// // 修復如果沒有搜尋資料,回車就只能顯示一個頁面的bug
// console.log(res)
// this.userListGet = res.results
// this.count = res.results.length
// }else{
// this.getUser()
// }
this.getUser()
})
},
getUser(){
this.searchList.page = this.current
// 獲取使用者資訊列表,父元件傳遞給子元件
get_userlist(this.searchList).then(res=>{
this.userListGet = res.results
this.count = res.count
console.log(this.count)
console.log(this.userListGet)
})
},
// 獲取頁碼
getPage(currentChild){
// 獲取到的currentChild是子元件傳遞過來是第幾頁
this.current = currentChild
console.log(this.current)
this.getUser()
}
},
created() {
}
}
</script>
<style scoped>
#components-layout-demo-basic {
text-align: center;
}
#components-layout-demo-basic .ant-layout-header,
#components-layout-demo-basic .ant-layout-footer {
background: white;
color: #fff;
}
#components-layout-demo-basic .ant-layout-footer {
line-height: 1.5;
}
#components-layout-demo-basic .ant-layout-content {
background: white;
color: #fff;
min-height: 120px;
line-height: 120px;
}
#components-layout-demo-basic > .ant-layout {
margin-bottom: 48px;
}
#components-layout-demo-basic > .ant-layout:last-child {
margin: 0;
}
</style>
3.9 BreadCrumb.vue
<template>
<div>
<a-breadcrumb>
<a-breadcrumb-item href="">
<a-icon type="home" />
</a-breadcrumb-item>
<a-breadcrumb-item href="">
<a-icon type="user" />
<span>首頁</span>
</a-breadcrumb-item>
<a-breadcrumb-item>
使用者模組
</a-breadcrumb-item>
<a-breadcrumb-item>
資訊管理頁面
</a-breadcrumb-item>
</a-breadcrumb>
</div>
</template>
<script>
export default {
name:"BreadCrumb",
data() {
return {
}
},
methods: {
},
created() {
}
}
</script>
<style scoped>
</style>
3.10 EditForm.vue
<template>
<div>
<a-modal
title="Please write now."
:visible="visible"
@ok="handleOk"
@cancel="handleCancel"
>
<!-- @ok控制按鈕ok -->
<!-- @cancel控制按鈕cancel -->
<p v-if="userList.id">UserName:
<a-input
style="width:380px;float:right"
placeholder="username"
v-model="userList.username"
disabled="disabled"
></a-input>
</p>
<p v-else>UserName:
<a-input
style="width:380px;float:right"
placeholder="username"
v-model="userList.username"
></a-input>
</p>
<br>
<div v-if="userList.id">
</div>
<div v-else>
<p>PassWord:
<a-input
v-decorator="[
'password',
{ rules: [{ required: true, message: 'Please input your Password!' }] },
]"
type="password"
placeholder="Password"
style="width:380px;float:right"
v-model="userList.password"
:disabled = 'false'
>
<a-icon slot="prefix" type="lock" style="color:rgba(0,0,0,.25)" />
</a-input>
</p>
<br>
<p>PassWord Again:
<a-input
v-decorator="[
'password',
{ rules: [{ required: true, message: 'Please input your Password!' }] },
]"
type="password"
placeholder="Password"
style="width:340px;float:right"
v-model="userList.password_new"
>
<a-icon slot="prefix" type="lock" style="color:rgba(0,0,0,.25)" />
</a-input>
</p>
</div>
<br>
<p>Email:
<a-input style="width:410px;float:right" placeholder="Email" v-model="userList.email"></a-input>
</p>
<br>
<p>Mobile:
<a-input style="width:410px" placeholder="Mobile" v-model="userList.mobile"></a-input>
</p>
<br>
<p>Weixin:
<a-input style="width:410px" placeholder="WeiXin" v-model="userList.weixin"></a-input>
</p>
</a-modal>
</div>
</template>
<script>
export default {
props:['visible', 'userList'],
data() {
return {
}
},
methods: {
// 新增資料顯示username的input框,修改的時候input框內資料不能顯示
choose(user_id){
},
handleOk(e) {
this.$emit('add')
// add方法的呼叫一定要在關閉彈窗上面,否則方法不執行完畢沒有辦法關閉彈窗
// 呼叫父元件中 add 方法
this.$emit('update:visible', false)
// 把 visible 的值更新為 false,控制組件不顯示
},
handleCancel(e) {
this.$emit('update:visible', false)
// 把 visible 的值更新為 false,控制組件不顯示
},
},
created() {
}
}
</script>
<style scoped>
</style>
3.11 Search.vue
<template>
<div>
<a-input-search placeholder="Input the username that you want to search for..." enter-button @search="onSearch" style="float:right;width:400px" v-model="searchList.username"/>
</div>
</template>
<script>
export default {
props:['searchList'],
data() {
return {
}
},
methods: {
onSearch(){
this.$emit('find')
// 呼叫父元件中的find方法
}
},
created() {
}
}
</script>
<style scoped>
</style>
3.12 TableList.vue
<template>
<a-table
:columns="columns"
:data-source="userListGet"
:rowKey="(record,index)=>{return index}"
:pagination= 'false'
>
<!-- 帶:的都是屬性繫結,不可以更換名字,帶 : 就是 js 環境 -->
<!-- 不寫:rowKey="(record,index)=>{return index}"瀏覽器會發出警告 -->
<p slot="tags" slot-scope="text,tags,i">
<!-- 加入操作的按鈕! -->
<a-button @click="delUser(text,tags,i)">刪除</a-button>
<a-button @click="updateUser(text,tags,i)">修改</a-button>
</p>
</a-table>
</template>
<script>
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
// ellipsis: true,
width: 50,
},
{
title: 'UserName',
dataIndex: 'username',
key: 'username',
scopedSlots: { customRender: 'username' },
width: 80,
},
{
title: 'Email',
dataIndex: 'email',
key: 'email',
width: 100,
},
{
title: 'Mobile',
dataIndex: 'mobile',
key: 'mobile',
ellipsis: true,
width: 100,
},
{
title: 'WeiXin',
dataIndex: 'weixin',
key: 'weixin',
ellipsis: true,
width: 100,
},
{
title: 'Date Joined',
dataIndex: 'date_joined',
key: 'date_joined',
ellipsis: true,
width: 100,
},
{
title:'操作',
dataIndex: 'tags',
key : 'tags',
width: 100,
scopedSlots : { customRender: 'tags'}
// scopedSlots: { customRender: 'tags' },一定不能少不然渲染不了html標籤
}
]
import { delete_user } from '@/http/apis'
export default {
props:[ 'userListGet', 'userList'],
data() {
return {
columns,
}
},
methods:{
get(){
this.$emit('getUser')
// 呼叫父元件中的獲取使用者列表的方法
},
delUser(text,tags,i){
// 定義變數 isDel來控制 confirm,isDel==true執行的就是對話方塊的 ok,isDel==false執行的就是對話方塊的 false
const isDel = confirm('你確定要刪除' + tags.id)
if(isDel==true){
delete_user(tags.id).then(
res=>{
// 刪除回撥地址是 http://192.168.56.100:1594/id/
this.get()
alert('刪除成功啦~')
})
}else{
alert('有需要再叫我哈~')
}
},
updateUser(text,tags,i){
this.userList.id = tags.id
this.userList.username = tags.username
this.userList.password = tags.password,
this.userList.password_new = tags.password
this.userList.email = tags.email
this.userList.mobile = tags.mobile
this.userList.weixin = tags.weixin
this.$emit('add')
}
},
created(){
this.get()
}
};
</script>
3.13 BaiDu.vue
<template>
<div>
</div>
</template>
<script>
export default {
data() {
return {
}
},
methods: {
goBaiDu(){
window.location.href = 'https://fanyi.baidu.com/?aldtype=16047#auto/zh'
}
},
created() {
this.goBaiDu()
}
}
</script>
<style scoped>
</style>
3.14 Login.vue
<template>
<div width=300>
<center><h1>登入</h1></center>
<a-form-item label="使用者名稱" v-bind="formlayout">
<a-input v-model="username"/>
</a-form-item>
<a-form-item label="密碼" v-bind="formlayout">
<a-input-password v-model="password" placeholder="input password" />
</a-form-item>
<a-form-item v-bind="buttonlayout">
<a-button type="primary" @click="submit">登入</a-button>
</a-form-item>
</div>
</template>
<script type="text/javascript">
import { user_login } from '../http/apis';
export default{
data(){
return{
username:"",
password:'',
//表單樣式
formlayout:{
//標籤
labelCol:{
xs:{span:24},
sm:{span:8}
},
//文字框
wrapperCol:{
xs:{span:14},
sm:{span:6}
}
},
//按鈕樣式
buttonlayout:{
//按鈕
wrapperCol:{
xs:{
span:24,
offset:0
},
sm:{span:16,offset:8}
}
}
}
},
//自定義方法
methods:{
submit:function(){
var data={'username':this.username,'password':this.password}
user_login(data).then(resp => {
console.log(resp)
if(resp.token){
// 如果返還token值,就儲存 token username uid
localStorage.setItem('token',resp.token)
localStorage.setItem('username',resp.username)
localStorage.setItem('uid',resp.id)
this.$router.push('/')
}
}).catch(err=>{
console.log(err)
alert('登入失敗')
})
}
}
};
</script>
<style type="text/css">
</style>
3.15 Pagitation.vue
<template>
<div>
<a-pagination
show-quick-jumper
:default-current="2"
:pageSize = '4'
:total="count"
show-less-items
@change="onChange"
v-model="current"
/>
</div>
</template>
<script>
export default {
props:[ 'count' ],
data() {
return {
current:1
}
},
methods: {
onChange() {
this.$emit('getPage', this.current)
},
},
created() {
}
}
</script>
<style scoped>
</style>
4 總結點
4.1 serializer重寫
class UserSerializer(serializers.Serializer):
# 用serializer重寫,不能用ModelViewSet來新增,寫上id會報錯username不能設為key,因為User表繼承的是AbstractUser,其中的username欄位必須有unique=True屬性設定唯一約束
username = serializers.CharField()
password = serializers.CharField()
mobile = serializers.CharField()
email = serializers.EmailField()
token = serializers.CharField(read_only=True)
def create(self, data):
username = data.get('username', '')
password = data.get('password', '')
mobile = data.get('mobile', '')
email = data.get('email', '')
user = User(username=username, email=email, mobile=mobile)
user.set_password(password)
user.save()
# 補充生成記錄登入狀態的token
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)
user.token = token
return user
def update(self, instance, validated_data):
# instance 是當前物件obj
# validated_data 是獲取的變數
instance.mobile = validated_data.get('mobile')
instance.email = validated_data.get('email')
instance.save()
return instance
4.2 要注意ModelViewSet的介面路由
- 後端介面
* 查詢介面
http://192.168.56.100:1594/user/user/
http://192.168.56.100:1594/user/user/?username='cat'
* 新增介面
http://192.168.56.100:1594/user/user/
* 刪除介面(指定使用者id刪除)
http://192.168.56.100:1594/user/user/id/
* 修改介面(指定使用者id修改)
http://192.168.56.100:1594/user/user/id/
- 前端訪問路徑配置
export const get_userlist = P => axios_get('/user/user/', P)
// 查詢所有:獲取使用者列表
export const search_for = P => axios_get('user/user/?username=' + P.username)
// 查詢指定資訊:根據使用者名稱查詢指定使用者資訊並展示
export const add_user = P => axios_post('/user/user/', P)
// 新增使用者資訊
export const delete_user = P => axios_delete('/user/user/' + P + '/')
// 刪除指定資訊:根據獲取到的使用者id刪除使用者資訊
export const update_user = P => axios_put('/user/user/'+ P.id +'/', P)
// 修改指定資訊:根絕使用者id和提交來的資料修改使用者資訊
4.3 使用 ant-design-vue
// 使用ant-design-vue
import Antd from 'ant-design-vue';
import 'ant-design-vue/dist/antd.css';
Vue.config.productionTip = false
Vue.use(Antd);
4.4 ModelViewSet修改問題
- put請求預設是全部修改
- patch請求可以實現區域性修改(後端已經封裝好了方法)
4.5 Vue巢狀路由
- index.js
import Vue from 'vue'
import Router from 'vue-router'
import Home from '@/components/layout/Home'
// 用於導包路徑的配置
const page = name => () => import('@/views/' + name)
Vue.use(Router)
export default new Router({
mode: 'history',
routes: [
// 使用者管理模組
{ path: '/',component: Home,name: 'home',
children: [
{ path: 'usermanage', component: page('user-manage/Index'), name: '使用者管理' },
{ path: 'flowconf', component: page('workflow/WorkFlowConf'), name: '模板管理' },
{ path: 'flowtype', component: page('workflow/WorkFlowType'), name: '模板型別管理' },
{ path: 'baidu', component: page('BaiDu'), name: '跳轉百度' },
]
},
]
})
- Home頁面中寫入
<template>
<div id="components-layout-demo-basic">
<a-layout>
</a-layout-header>
<div>
<!-- 這裡的 router-view 是繫結的路由 -->
<router-view></router-view>
</div>
</a-layout-header>
</a-layout>
</div>
</template>
<script>
// 匯入元件
import LeftMenu from '@/components/layout/LeftMenu'
import Header from '@/components/layout/Header'
export default {
// 註冊元件
components:{
LeftMenu,
Header
},
}
- 繫結左側選單LeftMenu
<template>
<div>
<a-menu
style="width: 256px"
:default-selected-keys="['1']"
:default-open-keys="['sub1']"
:mode="mode"
:theme="theme"
@click="handleClick"
>
<!-- 必須定義click方法,handleClick是用來跳轉路由的,內建毀掉引數e,包括傳遞上去的key路由地址 -->
<a-menu-item key="usermanage">
<!-- key是路由地址 -->
使用者管理模組
</a-menu-item>
<a-menu-item key="baidu">
百度翻譯
</a-menu-item>
</a-menu>
</div>
</template>
<script>
export default {
data() {
return {
};
},
methods: {
handleClick(e) {
this.current = e.key;
this.$router.push({path:this.current});
// click方法預設回撥引數中的 key,所以 e.key就是傳遞來的路由
},
},
};
</script>
4.6 ant-design-vue 圖示大小
- font-size 可以調節圖示大小
4.7 v-if v-else控制input框disabled屬性
<template>
<div>
<a-modal
title="Please write now."
:visible="visible"
@ok="handleOk"
@cancel="handleCancel"
>
<!-- @ok控制按鈕ok -->
<!-- @cancel控制按鈕cancel -->
<p v-if="userList.id">UserName:
<a-input
style="width:380px;float:right"
placeholder="username"
v-model="userList.username"
disabled="disabled"
></a-input>
<!-- disabled可以用來控制input是否能輸入文字 -->
</p>
<p v-else>UserName:
<a-input
style="width:380px;float:right"
placeholder="username"
v-model="userList.username"
></a-input>
</p>
<br>
<div v-if="userList.id">
</div>
<div v-else>
<p>PassWord:
<a-input
v-decorator="[
'password',
{ rules: [{ required: true, message: 'Please input your Password!' }] },
]"
type="password"
placeholder="Password"
style="width:380px;float:right"
v-model="userList.password"
:disabled = 'false'
>
</a-input>
</p>
</a-modal>
</div>
</template>
<script>
export default {
props:['visible', 'userList'],
methods: {
handleOk(e) {
this.$emit('add')
// add方法的呼叫一定要在關閉彈窗上面,否則方法不執行完畢沒有辦法關閉彈窗
this.$emit('update:visible', false)
},
handleCancel(e) {
this.$emit('update:visible', false)
},
},
}
</script>
4.8 a-table中的分頁器&按鈕
<a-table
:columns="要展示的列名稱(可以自定義列表)"
:data-source="要展示的動態資料表名"
:pagination="pagination"
:rowKey="(record,index)=>{return index}"
>
<!-- 帶:的都是屬性繫結,不可以更換名字,帶 : 就是 js 環境 -->
<!-- 不寫:rowKey="(record,index)=>{return index}"瀏覽器會發出警告 -->
<p slot="tags" slot-scope="text,tags,i">
<!-- 加入操作的按鈕!其中tags是某列資料,i是索引 -->
<a-button @click="delUser(text,tags,i)">刪除</a-button>
</p>
</a-table>
4.9 a-table中獲取某一行值的方法
<template>
<a-table
:columns="columns"
:data-source="userListGet"
:pagination="pagination"
:rowKey="(record,index)=>{return index}"
>
<!-- 帶:的都是屬性繫結,不可以更換名字,帶 : 就是 js 環境 -->
<!-- 不寫:rowKey="(record,index)=>{return index}"瀏覽器會發出警告 -->
<p slot="tags" slot-scope="text,tags,i">
<!-- 加入操作的按鈕! -->
<a-button @click="delUser(text,tags,i)">刪除</a-button>
</p>
</a-table>
</template>
<script>
const columns = [
{
title:'操作',
dataIndex: 'tags',
key : 'tags',
width: 100,
scopedSlots : { customRender: 'tags'}
// scopedSlots: { customRender: 'tags' },一定不能少不然渲染不了html標籤
}
]
import { delete_user } from '@/http/apis'
export default {
data() {
return {
columns,//這裡要返回值哦
pagination:{
pageSize: 4,
}
};
},
methods:{
delUser(text,tags,i){
}
},
}
</script>
4.10 控制對話方塊
// 定義變數 isDel來控制 confirm,isDel==true執行的就是對話方塊的 ok,isDel==false執行的就是對話方塊的 false
const isDel = confirm('你確定要刪除' + tags.id)
if(isDel==true){
delete_user(tags.id).then(
res=>{
// 刪除回撥地址是 http://192.168.56.100:1594/id/
this.get()
alert('刪除成功啦~')
})
}else{
alert('有需要再叫我哈~')
}
4.11 自動跳轉到某一網址
window.location.href = 'https://www.baidu.com/'
4.12 前後端聯調分頁
-
為了解決前端+後端能同時進行分頁的問題,只定義前端分頁,後端如果分頁就會影響前端資料,如果後端不定義分頁器,就會造成後端admin難以管理。
-
後端程式碼
- 實現區域性分頁
from django.shortcuts import render from rest_framework import viewsets from rest_framework.pagination import PageNumberPagination from rest_framework.response import Response from rest_framework.views import APIView from user.models import User from user.serializers import UserSerializer, UserModelSerializer # 分頁(區域性):自定義分頁器 區域性 class PageNum(PageNumberPagination): page_size = 4 # 每頁顯示多少條 page_size_query_param = 'page_size' # 查詢字串中代表每頁返回資料數量的引數名, 預設值: None page_query_param = 'page' # 查詢字串中代表頁碼的引數名, 有預設值: page max_page_size = None # 最大頁碼數限制 class UserViewSet(viewsets.ModelViewSet): queryset = User.objects.all() serializer_class = UserModelSerializer filter_fields = {"username"} pagination_class = PageNum # 注意不是列表(只能有一個分頁模式)
-
前端程式碼
- Pagination.vue
<template> <div> <a-pagination show-quick-jumper :default-current="2" :pageSize = '4' :total="count" show-less-items @change="onChange" v-model="current" /> <!-- 一定是change方法,不然不能跳轉 --> </div> </template> <script> export default { props:[ 'count' ], data() { return { current:1 } }, methods: { onChange() { this.$emit('getPage', this.current) }, }, created() { } } </script>
- Index.vue
<template> <div> <div id="components-layout-demo-basic"> <a-layout-content> <TableList :userListGet="userListGet" :userList="userList" @getUser="getUser" @add="add" > </TableList> <Pagination @getPage="getPage" :count="count" > </Pagination> </a-layout-content> </div> </div> </template> <script> import Pagination from "./components/Pagination" import { get_userlist } from '@/http/apis'; export default { components:{ BreadCrumb, TableList, Search, EditForm, Pagination }, data() { return { userListGet:[], updateUserList:[], // 當前頁碼 current:1, // 總共的資料多少條 count:0 } }, methods: { getUser(){ // 獲取使用者資訊列表,父元件傳遞給子元件 get_userlist(this.current).then(res=>{ this.userListGet = res.results this.count = res.count console.log(this.count) console.log(this.userListGet) }) }, // 獲取頁碼 getPage(currentChild){ // 獲取到的currentChild是子元件傳遞過來是第幾頁 this.current = currentChild console.log(this.current) this.getUser() } }, created() { } } </script> <style scoped> #components-layout-demo-basic { text-align: center; } #components-layout-demo-basic .ant-layout-header, #components-layout-demo-basic .ant-layout-footer { background: white; color: #fff; } #components-layout-demo-basic .ant-layout-footer { line-height: 1.5; } #components-layout-demo-basic .ant-layout-content { background: white; color: #fff; min-height: 120px; line-height: 120px; } #components-layout-demo-basic > .ant-layout { margin-bottom: 48px; } #components-layout-demo-basic > .ant-layout:last-child { margin: 0; } </style>
4.13 利用資料解耦性完善查詢介面
export default {
components:{
BreadCrumb,
TableList,
Search,
EditForm,
Pagination
},
data() {
return {
roleListGet:[],
searchList:{
'zh_name':'',
'page':1,
'page_size':4,
},
},
methods: {
find(){
// 根據使用者名稱查詢使用者資訊
search_for_role(this.searchList).then(res=>{
this.getRole()
// 資料解耦性!!!查詢和查詢某個其實可以呼叫同一個介面!
// 查詢所有:http://192.168.56.100:1594/?page=1&zh_name=
// 查詢某個:http://192.168.56.100:1594/?page=1&zh_name=many
})
},
getRole(){
this.searchList.page = this.current
// 獲取使用者資訊列表,父元件傳遞給子元件
get_rolelist(this.searchList).then(res=>{
this.roleListGet = res.results
this.count = res.count
console.log(this.count)
console.log(this.roleListGet)
})
},
}