魔方APP專案-08-個人中心,登入跳轉並解決頁面卡頓現象、客戶端顯示個人中心頁面、flask-Admin構建和配置後臺運營站點管理使用者資訊、基於Faker生成模擬測試資料
個人中心
一、登入跳轉並解決頁面卡頓現象
html/login.html
,程式碼:
<!DOCTYPE html>
<html>
<head>
<title>登入</title>
<meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
<meta charset="utf-8" >
<link rel="stylesheet" href="../static/css/main.css">
<script src="../static/js/vue.js"></script>
<script src="../static/js/axios.js"></script>
<script src="../static/js/main.js"></script>
<script src="../static/js/uuid.js" ></script>
<script src="../static/js/settings.js"></script>
<script src="../static/js/TCaptcha.js"></script>
</head>
<body>
<div class="app" id="app">
<img class="music" :class="music_play?'music2' :''" @click="music_play=!music_play" src="../static/images/player.png">
<div class="bg">
<img src="../static/images/bg0.jpg">
</div>
<div class="form">
<div class="form-title">
<img src="../static/images/login.png">
<img class="back" @click="goto_index" src="../static/images/back.png">
</div>
<div class="form-data">
<div class="form-data-bg">
<img src="../static/images/bg1.png">
</div>
<div class="form-item">
<label class="text">賬戶</label>
<input type="text" v-model="account" placeholder="請輸入手機號/郵箱/使用者名稱">
</div>
<div class="form-item">
<label class="text">密碼</label>
<input type="password" v-model="password" placeholder="請輸入密碼">
</div>
<div class="form-item">
<input type="checkbox" class="agree remember" v-model="remember" checked>
<label><span class="agree_text ">記住密碼,下次免登入</span></label>
</div>
<div class="form-item">
<img class="commit" @click="loginHandle" src="../static/images/commit.png">
</div>
<div class="form-item">
<p class="toreg" @click="goto_register">立即註冊</p>
<p class="tofind">忘記密碼</p>
</div>
</div>
</div>
</div>
<script>
apiready = function(){
init();
new Vue({
el:"#app",
data(){
return {
account: "",
password: "",
remember: false, // 是否記住登陸
music_play:true,
prev:{name:"",url:"",params:{}},
current:{name:"login",url:"login.html",params:{}},
}
},
watch:{
music_play(){
if(this.music_play){
this.game.play_music("../static/mp3/bg1.mp3");
}else{
this.game.stop_music();
}
}
},
methods:{
loginHandle(){
// 登入處理
this.game.play_music('../static/mp3/btn1.mp3');
// 驗證密碼和賬戶是否填寫
if(this.account.length<1 || this.password.length<1){
api.alert({
title: '警告',
msg: '賬戶或密碼不能為空!',
});
return;
}
// 圖形驗證碼
var captcha1 = new TencentCaptcha(this.settings.captcha_app_id, res=>{
// 當用戶操作驗證碼成功以後的回撥處理,這裡需要把引數傳送到當前應用的服務端
/*
res的返回結果包含以下4個成員
"appid":"2041284967",
"ret":0,
"ticket":"t03WemDcWVY9tCdU0eiUtR41IlaU0Xk6Xop02s_COXMWuC0j4eA2UKHqdFnFIxk982gQD4sFSfsnrg8QPk6br4nDZd7dSQwAVl6vOPdApNr3wc*",
"randstr":"@SOF"
*/
if(res.ret == 0){
// 當用戶操作驗證通過以後, 傳送登入資訊和驗證校驗資訊
this.axios.post('', {
'jsonrpc': '2.0',
'id': this.uuid(),
'method': "User.login",
'params': {
'ticket': res.ticket, // 驗證通過以後的服務端驗證碼返回的臨時憑證,需要傳送給服務端,和騰訊伺服器進行校驗
'randstr': res.randstr, // 隨機數, 為了讓ticket更加隨機和安全
'account': this.account,
'password': this.password
}
}).then(response=>{
// 獲取服務端資料
if(response.data.result.errno == 1000){
if(this.remember){
// 記住登入
this.game.fsave({
'id': response.data.result.id,
'nickname': response.data.result.nickname,
'access_token': response.data.result.access_token,
'refresh_token': response.data.result.refresh_token
});
}else {
// 不記住登入
this.game.save({
'id': response.data.result.id,
'nickname': response.data.result.nickname,
'access_token': response.data.result.access_token,
'refresh_token': response.data.result.refresh_token,
});
}
// 頁面跳轉
api.confirm({
title: '磨方提示',
msg: '註冊成功',
buttons: ['返回首頁', '個人中心']
}, (ret, err)=>{
if( ret.buttonIndex == 1 ){
// 跳轉到首頁
this.game.outWin('user');
}else{
// 跳轉到個人中心
this.game.goFrame('user', 'user.html', this.current);
}
});
}
}).catch(error=>{
if(error.response){
// 服務端返回錯誤
this.game.print(error.response.data);
}else {
// 原生代碼出現錯誤
this.game.print(error);
}
});
}
});
captcha1.show(); // 顯示驗證碼
},
goto_register(){
// 去註冊
this.game.goFrame("user", 'register.html', this.current);
},
goto_index(){
// 返回首頁
this.game.outWin('user');
}
}
})
}
</script>
</body>
</html>
html/index.html
,程式碼:
<!DOCTYPE html>
<html lang="en">
<head>
<title>首頁</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
<meta name="format-detection" content="telephone=no,email=no,date=no,address=no">
<link rel="stylesheet" href="../static/css/main.css">
<script src="../static/js/vue.js"></script>
<script src="../static/js/main.js"></script>
</head>
<body>
<div class="app" id="app">
<img class="music" :class="music_play?'music2':''" @click="music_play=!music_play" src="../static/images/player.png">
<div class="bg">
<img src="../static/images/bg0.jpg">
</div>
<ul>
<li><img class="module1" src="../static/images/image1.png"></li>
<li><img class="module2" @click="gohome" src="../static/images/image2.png"></li>
<li><img class="module3" src="../static/images/image3.png"></li>
<li><img class="module4" src="../static/images/image4.png"></li>
</ul>
</div>
<script>
apiready = function(){
var game = new Game("../static/mp3/bg1.mp3");
// 允許ajax傳送請求時附帶cookie,設定為不允許
Vue.prototype.game = game;
new Vue({
el:"#app",
data(){
return {
music_play:true, // 預設播放背景音樂
prev:{name:"",url:"",params:{}}, // 上一頁狀態
current:{name:"index",url:"index.html","params":{}}, // 下一頁狀態
}
},
watch:{
music_play(){
if(this.music_play){
this.game.play_music("../static/mp3/bg1.mp3");
}else{
this.game.stop_music();
}
}
},
created(){
},
methods:{
gohome(){
if(this.game.get('access_token') || this.game.fget('access_token')){
this.game.goWin('user', 'user.html', this.current);
}else {
this.game.goWin('user', 'login.html', this.current);
}
}
}
})
}
</script>
</body>
</html>
html/register.html
,程式碼:
<!DOCTYPE html>
<html>
<head>
<title>註冊</title>
<meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
<meta charset="utf-8">
<link rel="stylesheet" href="../static/css/main.css">
<script src="../static/js/vue.js"></script>
<script src="../static/js/axios.js"></script>
<script src="../static/js/main.js"></script>
<script src="../static/js/uuid.js"></script>
<script src="../static/js/settings.js"></script>
</head>
<body>
<div class="app" id="app">
<img class="music" :class="music_play?'music2':''" @click="music_play=!music_play" src="../static/images/player.png">
<div class="bg">
<img src="../static/images/bg0.jpg">
</div>
<div class="form">
<div class="form-title">
<img src="../static/images/register.png">
<img class="back" @click="back" src="../static/images/back.png">
</div>
<div class="form-data">
<div class="form-data-bg">
<img src="../static/images/bg1.png">
</div>
<div class="form-item">
<label class="text">手機</label>
<input type="text" v-model="mobile" @change="check_mobile" placeholder="請輸入手機號">
</div>
<div class="form-item">
<label class="text">驗證碼</label>
<input type="text" class="code" v-model="sms_code" placeholder="請輸入驗證碼">
<img class="refresh" @click="send" src="../static/images/refresh.png">
</div>
<div class="form-item">
<label class="text">密碼</label>
<input type="password" v-model="password" placeholder="請輸入密碼">
</div>
<div class="form-item">
<label class="text">確認密碼</label>
<input type="password" v-model="password2" placeholder="請再次輸入密碼">
</div>
<div class="form-item">
<input type="checkbox" class="agree" v-model="agree" checked>
<label><span class="agree_text">同意磨方《使用者協議》和《隱私協議》</span></label>
</div>
<div class="form-item">
<img class="commit" @click="registerHandle" src="../static/images/commit.png"/>
</div>
</div>
</div>
</div>
<script>
apiready = function(){
init();
new Vue({
el:"#app",
data(){
return {
is_send: false,
send_interval: 60, // 簡訊傳送冷卻時間
mobile:"",
password: "",
password2: "",
sms_code:"",
agree:false,
music_play:true,
prev:{name:"",url:"",params:{}},
current:{name:"register",url:"register.html","params":{}},
}
},
watch:{
music_play(){
if(this.music_play){
this.game.play_music("../static/mp3/bg1.mp3");
}else{
this.game.stop_music();
}
}
},
methods:{
send(){
// 點擊發送簡訊
if (!/1[3-9]\d{9}/.test(this.mobile)){
api.alert({
title: "警告",
msg: "手機號碼格式不正確!",
});
return; // 阻止程式碼繼續往下執行
}
if(this.is_send){
api.alert({
title: "警告",
msg: `簡訊傳送冷卻中,請${this.send_interval}秒之後重新點擊發送!`,
});
return; // 阻止程式碼繼續往下執行
}
this.axios.post("",{
"jsonrpc": "2.0",
"id": this.uuid(),
"method": "Home.sms",
"params": {
"mobile": this.mobile,
}
}).then(response=>{
if(response.data.result.errno != 1000){
api.alert({
title: "錯誤提示",
msg: response.data.result.errmsg,
});
}else{
this.is_send=true; // 進入冷卻狀態
this.send_interval = 60;
var timer = setInterval(()=>{
this.send_interval--;
if(this.send_interval<1){
clearInterval(timer);
this.is_send=false; // 退出冷卻狀態
}
}, 1000);
}
}).catch(error=>{
this.game.print(error.response);
});
},
registerHandle(){
// 註冊處理
this.game.play_music('../static/mp3/btn1.mp3');
// 驗證資料[雙向驗證]
if (!/1[3-9]\d{9}/.test(this.mobile)){
api.alert({
title: "警告",
msg: "手機號碼格式不正確!",
});
return; // 阻止程式碼繼續往下執行
}
if(this.password.length<3 || this.password.length > 16){
api.alert({
title: "警告",
msg: "密碼長度必須在3-16個字元之間!",
});
return;
}
if(this.password != this.password2){
api.alert({
title: "警告",
msg: "密碼和確認密碼不匹配!",
});
return; // 阻止程式碼繼續往下執行
}
if(this.sms_code.length<1){
api.alert({
title: "警告",
msg: "驗證碼不能為空!",
});
return; // 阻止程式碼繼續往下執行
}
if(this.agree === false){
api.alert({
title: "警告",
msg: "對不起, 必須同意磨方的使用者協議和隱私協議才能繼續註冊!",
});
return; // 阻止程式碼繼續往下執行
}
this.axios.post("",{
"jsonrpc": "2.0",
"id": this.uuid(),
"method": "User.register",
"params": {
"mobile": this.mobile,
"sms_code":this.sms_code,
"password":this.password,
"password2":this.password2,
}
}).then(response=>{
this.game.print(response.data.result);
if(response.data.result.errno != 1000){
api.alert({
title: "錯誤提示",
msg: response.data.result.errmsg,
});
}else{
// 註冊成功!
api.confirm({
title: '磨方提示',
msg: '註冊成功',
buttons: ['返回首頁', '個人中心']
}, (ret, err)=>{
if(ret.buttonIndex == 1){
// 跳轉到首頁
this.game.outWin("user");
}else{
// 跳轉到個人中心
this.game.goFrame("user",'user.html', this.current);
}
});
}
}).catch(error=>{
this.game.print(error.response);
});
},
check_mobile(){
// 驗證手機號碼
this.axios.post("",{
"jsonrpc": "2.0",
"id": this.uuid(),
"method": "User.mobile",
"params": {"mobile": this.mobile}
}).then(response=>{
this.game.print(response.data.result);
if(response.data.result.errno != 1000){
api.alert({
title: "錯誤提示",
msg: response.data.result.errmsg,
});
}
}).catch(error=>{
this.game.print(error.response.data.error);
});
},
back(){
// this.game.outWin();
// this.game.outFrame();
this.game.goGroup("user",0);
}
}
})
}
</script>
</body>
</html>
解決部分頁面因為沒有引入對應js指令碼檔案,導致客戶端初始化報錯的問題。
static/js/settings.js
,程式碼:
function init(){
if (Game) {
var game = new Game("../mp3/bg1.mp3");
Vue.prototype.game = game;
}
if(axios){
// 初始化axios
axios.defaults.baseURL = "http://192.168.20.158:5000/api" // 服務端api介面閘道器地址
axios.defaults.timeout = 2500; // 請求超時時間
axios.defaults.withCredentials = false; // 跨域請求資源的情況下,忽略cookie的傳送
Vue.prototype.axios = axios;
Vue.prototype.uuid = UUID.generate;
}
// 介面相關的配置項
Vue.prototype.settings = {
captcha_app_id: "2041284967", // 騰訊防水牆驗證碼應用ID
}
}
解決關於獲取本地檔案或者從記憶體中提取單個數據返回陣列的問題。
static/js/main.js
,程式碼:
class Game{
constructor(bg_music){
// 建構函式,相當於python中類的__init__方法
this.init();
if(bg_music){
this.play_music(bg_music);
}
}
open(){
}
init(){
// 初始化
console.log("系統初始化");
this.rem();
// 幀頁面組的初始化
this.groupname = null;
this.groupindex = 0;
}
print(data){
// 列印資料
console.log(JSON.stringify(data));
}
back(prev){
// 返回上一頁
this.go(prev.name,prev.url,{...prev});
}
outWin(name){
// 關閉視窗
api.closeWin(name);
}
goWin(name,url,pageParam){
// 新建視窗
api.openWin({
name: name, // 自定義視窗名稱
bounces: false, // 視窗是否上下拉動
reload: true, // 如果頁面已經在之前被打開了,是否要重新載入當前視窗中的頁面
useWKWebView:true,
historyGestureEnabled:true,
url: url, // 視窗建立時展示的html頁面的本地路徑[相對於當前程式碼所在檔案的路徑]
animation:{ // 開啟新建視窗時的過渡動畫效果
type: "push", //動畫型別(詳見動畫型別常量)
subType: "from_right", //動畫子型別(詳見動畫子型別常量)
duration:300 //動畫過渡時間,預設300毫秒
},
pageParam: pageParam // 傳遞給下一個視窗使用的引數.將來可以在新視窗中通過 api.pageParam.name 獲取
});
}
goFrame(name,url,pageParam,rect=null){
// 建立幀頁面
if(rect === null){
rect = {
// 方式1,設定矩形大小寬高
x: 0, // 左上角x軸座標
y: 0, // 左上角y軸座標
w: 'auto', // 當前幀頁面的寬度, auto表示滿屏
h: 'auto' // 當前幀頁面的高度, auto表示滿屏
}
}
api.openFrame({
name: name, // 幀頁面的名稱
url: url, // 幀頁面開啟的url地址
bounces:false, // 頁面是否可以下拉拖動
reload: true, // 幀頁面如果已經存在,是否重新重新整理載入
useWKWebView: true,
historyGestureEnabled:true,
animation:{
type:"push", //動畫型別(詳見動畫型別常量)
subType:"from_right", //動畫子型別(詳見動畫子型別常量)
duration:300 //動畫過渡時間,預設300毫秒
},
rect: rect, // 當前幀的寬高範圍
pageParam: pageParam, // 要傳遞新建幀頁面的引數,在新頁面可通過 api.pageParam.name 獲取
});
}
outFrame(name){
// 關閉幀頁面
api.closeFrame({
name: name,
});
}
openGroup(name,frames,preload=1,rect=null){
// 建立frame組
if(rect === null){
rect = { // 幀頁面組的顯示矩形範圍
x:0, //左上角x座標,數字型別
y:0, //左上角y座標,數字型別
w:'auto', //寬度,若傳'auto',頁面從x位置開始自動充滿父頁面寬度,數字或固定值'auto'
h:'auto', //高度,若傳'auto',頁面從y位置開始自動充滿父頁面高度,數字或固定值'auto'
};
}
api.openFrameGroup({
name: name, // 組名
scrollEnabled: false, // 頁面組是否可以左右滾動
index: 0, // 預設顯示頁面的索引
rect: rect, // 頁面寬高範圍
preload: preload, // 預設預載入的頁面數量
frames: frames, // 幀頁面組的幀頁面成員
}, (ret, err)=>{
// 當前幀頁面發生頁面顯示變化時,當前幀的索引.
this.groupindex = ret.index;
});
}
outGroup(name){
// 關閉 frame組
api.closeFrameGroup({
name: name // 組名
});
}
goGroup(name,index){
// 切換顯示frame組下某一個幀頁面
api.setFrameGroupIndex({
name: name, // 組名
index: index // 索引,從0開始
});
}
rem(){
if(window.innerWidth<1200){
this.UIWidth = document.documentElement.clientWidth;
this.UIHeight = document.documentElement.clientHeight;
document.documentElement.style.fontSize = (0.01*this.UIWidth*3)+"px";
document.querySelector("#app").style.height=this.UIHeight+"px"
}
window.onresize = ()=>{
if(window.innerWidth<1200){
this.UIWidth = document.documentElement.clientWidth;
this.UIHeight = document.documentElement.clientHeight;
document.documentElement.style.fontSize = (0.01*this.UIWidth*3)+"px";
}
}
}
save(data){
// 儲存資料到記憶體中
for(var key in data){
api.setGlobalData({
key: key,
value: data[key]
})
}
}
get(data){
// 從記憶體中獲取資料
if(!Array.isArray(data)){
data = [data];
}
var result = {};
for(var key of data){
result[key] = api.getGlobalData({
'key': key
});
}
// 如果只是獲取一個數據的情況,直接返回值,不需要返回json物件
if(data.length == 1){
return result[key];
}
return result;
}
fsave(data){
// 儲存資料到檔案中
for(var key in data){
api.setPrefs({
'key': key,
value: data[key]
});
}
}
fremove(data){
// 從檔案中刪除資料
if(!Array.isArray(data)){
data = [data];
}
for(var key of data){
api.removePrefs({
'key': key
});
}
}
fget(data){
// 從檔案中獲取資料
if(!Array.isArray(data)){
data = [data];
}
var value;
var result = {}
for(var key of data){
result[key] = api.getPrefs({
sync: true,
key: key
});
}
// 如果只是獲取一個數據的情況,直接返回值,不需要返回json物件
if(data.length == 1){
return result[key];
}
return result;
}
stop_music(){
this.print("停止背景音樂");
document.body.removeChild(this.audio);
}
play_music(src){
this.print("播放背景音樂");
this.audio = document.createElement("audio");
this.source = document.createElement("source");
this.source.type = "audio/mp3";
this.audio.autoplay = "autoplay";
this.source.src=src;
this.audio.appendChild(this.source);
/*
<audio autoplay="autoplay">
<source type="audio/mp3" src="../static/mp3/bg1.mp3">
</audio>
*/
document.body.appendChild(this.audio);
// 自動暫停關閉背景音樂
var t = setInterval(()=>{
if(this.audio.readyState > 0){
if(this.audio.ended){
clearInterval(t);
try{
document.body.removeChild(this.audio);
}catch(error){
// 不處理
}
}
}
},100);
}
}
二、客戶端顯示個人中心頁面
html/user.html
,程式碼:
<!DOCTYPE html>
<html>
<head>
<title>使用者中心</title>
<meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
<meta charset="utf-8">
<link rel="stylesheet" href="../static/css/main.css">
<script src="../static/js/vue.js"></script>
<script src="../static/js/axios.js"></script>
<script src="../static/js/main.js"></script>
<script src="../static/js/uuid.js"></script>
<script src="../static/js/settings.js"></script>
</head>
<body>
<div class="app user" id="app">
<div class="bg">
<img src="../static/images/bg0.jpg">
</div>
<img class="back" @click="goto_index" src="../static/images/user_back.png" alt="">
<img class="setting" src="../static/images/setting.png" alt="">
<div class="header">
<div class="info">
<div class="avatar">
<img class="avatar_bf" src="../static/images/avatar_bf.png" alt="">
<img class="user_avatar" src="../static/images/avatar.png" alt="">
<img class="avatar_border" src="../static/images/avatar_border.png" alt="">
</div>
<p class="user_name">清蒸小帥鍋</p>
</div>
<div class="wallet">
<div class="balance">
<p class="title"><img src="../static/images/money.png" alt="">錢包</p>
<p class="num">99,999.00</p>
</div>
<div class="balance">
<p class="title"><img src="../static/images/integral.png" alt="">果子</p>
<p class="num">99,999.00</p>
</div>
</div>
<div class="invite">
<img class="invite_btn" src="../static/images/invite.png" alt="">
</div>
</div>
<div class="menu">
<div class="item">
<span class="title">我的主頁</span>
<span class="value">檢視</span>
</div>
<div class="item">
<span class="title">任務列表</span>
<span class="value">75%</span>
</div>
<div class="item">
<span class="title">收益明細</span>
<span class="value">檢視</span>
</div>
<div class="item">
<span class="title">實名認證</span>
<span class="value">未認證</span>
</div>
<div class="item">
<span class="title">問題反饋</span>
<span class="value">去反饋</span>
</div>
</ul>
</div>
</div>
<script>
apiready = function(){
init();
new Vue({
el:"#app",
data(){
return {
prev:{name:"",url:"",params:{}},
current:{name:"user",url:"user.html",params:{}},
}
},
methods:{
goto_index(){
// 返回首頁
this.game.outWin("user");
},
}
});
}
</script>
</body>
</html>
static/css/main.css
,程式碼:
.user .header{
position: absolute;
top: 1.22rem;
left: 0;
right: 0;
margin: auto;
width: 32rem;
height: 19.28rem;
background: url("../images/ucenter.png") no-repeat 0 0;
background-size: 100%;
}
.user .back{
position: absolute;
width: 3.83rem;
height: 3.89rem;
z-index: 1;
top: 2.72rem;
left: 3rem;
}
.user .setting{
position: absolute;
z-index: 1;
top: 4.2rem;
right: 3.8rem;
width: 3.06rem;
height: 3.06rem;
}
.user .info{
position: absolute;
z-index: 1;
top: 6.94rem;
left: 3.89rem;
width: 6.39rem;
height: 9.17rem;
}
.user .info .avatar{
width: 6.39rem;
height: 6.39rem;
position: relative;
}
.user .info .avatar_bf{
position: absolute;
z-index: 1;
margin: auto;
width: 4.56rem;
height: 4.56rem;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
.user .info .user_avatar{
position: absolute;
z-index: 1;
width: 4.56rem;
height: 4.56rem;
margin: auto;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
.user .info .avatar_border{
position: absolute;
z-index: 1;
margin: auto;
top: 0;
bottom: 0;
left: 0;
right: 0;
width: 6.2rem;
height: 6.2rem;
}
.user .info .user_name{
text-align: center;
}
.user .wallet{
position: absolute;
top: 5.56rem;
right: 1.94rem;
width: 16rem;
height: 10rem;
}
.user .wallet .balance{
margin-top: 1.4rem;
float: left;
margin-right: 1rem;
}
.user .wallet .title{
color: #300;
font-size: 1.2rem;
width: 6.4rem;
text-align: center;
}
.user .wallet .title img{
width: 1.4rem;
margin-right: 0.2rem;
vertical-align: sub;
height: 1.4rem;
}
.user .wallet .num{
background: url("../images/btn3.png") no-repeat 0 0;
background-size: 100%;
width: 6.4rem;
font-size: 0.8rem;
color: #fff;
height: 2rem;
line-height: 1.8rem;
text-indent: 1rem;
}
.user .invite{
position: absolute;
top: 11.6rem;
right: 6.4rem;
}
.user .invite .invite_btn{
width: 9.39rem;
height: 3.67rem;
}
.user .menu{
position: absolute;
top: 22rem;
padding-top: 4.6rem;
right: 0;
left: 0;
margin: auto;
width: 28.44rem;
height: 33.89rem;
background: url("../images/bg10.png") no-repeat 0 0;
background-size: 100%;
}
.user .menu .item{
margin-left: 2.6rem;
margin-bottom: 0.5rem;
width: 23.06rem;
height: 4.22rem;
line-height: 4.22rem;
overflow: hidden;
background: url("../images/title.png") no-repeat 0 0;
background-size: 100%;
text-indent: 2rem;
color: #fff;
font-size: 1.33rem;
}
.user .menu .item .title{
float: left;
}
.user .menu .item .value{
float: right;
padding-right: 2rem;
}
三、flask-Admin構建和配置後臺運營站點管理使用者資訊
Flask-Admin文件:https://flask-admin.readthedocs.io/en/latest/
1.安裝
pip install flask-admin
2.模組初始化
application/__init__.py
,程式碼:
import os,sys
from flask import Flask
from flask_script import Manager
from flask_sqlalchemy import SQLAlchemy
from flask_redis import FlaskRedis
from flask_session import Session
from flask_migrate import Migrate, MigrateCommand
from flask_jsonrpc import JSONRPC
from flask_marshmallow import Marshmallow
from flask_jwt_extended import JWTManager
from flask_admin import Admin
from application.utils import init_blueprint
from application.utils.config import load_config
from application.utils.session import init_session
from application.utils.logger import Log
from application.utils.commands import load_command
# 建立終端指令碼管理物件
manager = Manager()
# 建立資料庫連結物件
db = SQLAlchemy()
# redis連結物件
redis = FlaskRedis()
# Session儲存物件
session_store = Session()
# 資料遷移例項物件
migrate = Migrate()
# 日誌物件
log = Log()
# jsonrpc模組例項物件
jsonrpc = JSONRPC()
# 資料轉換器的物件建立
ma = Marshmallow()
# jwt認證模組例項化
jwt = JWTManager()
# flask_admin模組初始化
admin = Admin()
def init_app(config_path):
"""全域性初始化"""
# 建立app應用物件
app = Flask(__name__)
# 專案根目錄
app.BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# 載入導包路徑
sys.path.insert(0, os.path.join(app.BASE_DIR,"application/utils/language"))
# 載入配置
Config = load_config(config_path)
app.config.from_object(Config)
# 資料庫初始化
db.init_app(app)
redis.init_app(app)
# 資料轉換器的初始化
ma.init_app(app)
# session儲存初始化
init_session(app)
session_store.init_app(app)
# 資料遷移初始化
migrate.init_app(app,db)
# 新增資料遷移的命令到終端指令碼工具中
manager.add_command('db', MigrateCommand)
# 日誌初始化
app.log = log.init_app(app)
# 藍圖註冊
init_blueprint(app)
# jsonrpc初始化
jsonrpc.service_url = "/api" # api介面的url地址字首
jsonrpc.init_app(app)
# jwt初始化
jwt.init_app(app)
# 初始化終端指令碼工具
manager.app = app
# 註冊自定義命令
load_command(manager)
# admin站點
admin.init_app(app)
return manager
訪問站點地址http://127.0.0.1:5000/admin
,效果如下:
可以發現上面什麼都沒有,就一個Home首頁導航.
3.模組配置
因為後臺站點程式碼肯定頁面很多,所以我們把admin相關配置程式碼編寫在application/backend.py
中,並且在藍圖初始化操作的輔助函式中進行自動載入.
application/utils/__init__.py
,程式碼:
from flask import Blueprint
from importlib import import_module
def path(rule,func_view):
# 把藍圖下檢視和路由之間的對映關係處理成字典結構,方便後面註冊藍圖的時候,直接傳參
return {"rule": rule, "view_func": func_view}
def include(url_prefix, blueprint_path):
"""把路由字首和藍圖進行關係對映"""
return {"url_prefix":url_prefix,"blueprint_path":blueprint_path}
def init_blueprint(app):
"""自動註冊藍圖"""
blueprint_path_list = app.config.get("INSTALLED_APPS")
# 載入admin站點總配置檔案
try:
import_module(app.config.get('ADMIN_PATH'))
except:
pass
for blueprint_path in blueprint_path_list:
blueprint_name = blueprint_path.split(".")[-1]
# 自動建立藍圖物件
blueprint = Blueprint(blueprint_name,blueprint_path)
# 藍圖自動註冊和繫結檢視和子路由
url_module = import_module(blueprint_path+".urls") # 載入藍圖下的子路由檔案
for url in url_module.urlpatterns: # 遍歷子路由中的所有路由關係
blueprint.add_url_rule(**url) # 註冊到藍圖下
# 讀取總路由檔案
url_path = app.config.get("URL_PATH")
urlpatterns = import_module(url_path).urlpatterns # 載入藍圖下的子路由檔案
url_prefix = "" # 藍圖路由字首
for urlpattern in urlpatterns:
if urlpattern["blueprint_path"] == blueprint_name+".urls":
url_prefix = urlpattern["url_prefix"]
break
# 註冊模型
import_module(blueprint_path+".models")
# 載入藍圖內部的admin站點配置
try:
import_module(blueprint_path + '.admin')
except:
pass
# 註冊藍圖物件到app應用物件中, url_prefix 藍圖的路由字首
app.register_blueprint(blueprint,url_prefix=url_prefix)
application/settings/__init__.py
,程式碼:
class InitConfig():
"""專案預設初始化配置"""
# 除錯模式
DEBUG = True
# 資料庫相關配置
SQLALCHEMY_DATABASE_URI = ""
# 動態追蹤修改設定
SQLALCHEMY_TRACK_MODIFICATIONS = False
# 查詢時會顯示原始SQL語句
SQLALCHEMY_ECHO= True
# Redis
REDIS_URL = ""
# 設定金鑰,可以通過 base64.b64encode(os.urandom(48)) 來生成一個指定長度的隨機字串
SECRET_KEY = "y58Rsqzmts6VCBRHes1Sf2DHdGJaGqPMi6GYpBS4CKyCdi42KLSs9TQVTauZMLMw"
# session儲存配置
# session儲存方式配置
SESSION_TYPE = "redis"
# 如果設定session的生命週期是否是會話期, 為True,則關閉瀏覽器session就失效
SESSION_PERMANENT = False
# 設定session_id在瀏覽器中的cookie有效期
PERMANENT_SESSION_LIFETIME = 24 * 60 * 60 # session 的有效期,單位是秒
# 是否對傳送到瀏覽器上session的cookie值進行加密
SESSION_USE_SIGNER = True
# 儲存到redis的session數的名稱字首
SESSION_KEY_PREFIX = "session:"
# session儲存資料到redis時啟用的連結物件
SESSION_REDIS = None # 用於連線redis的配置
SESSION_REDIS_HOST = "127.0.0.1"
SESSION_REDIS_PORT = 6379
SESSION_REDIS_DB = 1
# 調整json資料轉換中文的配置
JSON_AS_ASCII = False
# 日誌相關配置
LOG_LEVEL = "INFO" # 日誌輸出到檔案中的最低等級
LOG_DIR = "logs/0.log" # 日誌儲存目錄
LOG_MAX_BYTES = 300 * 1024 * 1024 # 單個日誌檔案的儲存上限[單位: b]
LOG_BACKPU_COUNT = 20 # 日誌檔案的最大備份數量
LOG_NAME = "flask" # 日誌器的名字
# 藍圖註冊列表
INSTALLED_APPS = [
]
# 總路由
URL_PATH = "application.urls"
# admin站點配置
ADMIN_PATH = 'application.backend' # 預設admin總配置
FLASK_ADMIN_SWATCH = 'cerulean' # 站點主題
4.快速使用
①修改預設首頁
在application/backend
藍圖下,新增admin站點配置檔案,並重寫預設首頁的內容主體模板,
- 在application下新增模板目錄templates,並新增模板檔案目錄admin,並在admin目錄下建立首頁的模板檔案
home.html
,模板程式碼:
{% extends 'admin/master.html' %}
{% block body %}
<h1>admin站點預設首頁</h1>
{% endblock %}
- 在application目錄下新建admin 站點總配置檔案
backend.py
,程式碼:
from application import admin
from flask_admin import AdminIndexView
# 修改admin站點標題
admin.name = '魔方APP'
# admin站點的預設首頁資訊
admin._set_admin_index_view(index_view=AdminIndexView(
name='Home',
template='admin/home.html',
))
訪問效果:
②新增自定義導航
給admin站點新增一個使用者導航,在application/templates/admin
下新增user模板目錄,並建立user/index.html模板檔案,程式碼:
{% extends 'admin/master.html' %}
{% block body %}
<h1>使用者自定義導航頁面</h1>
<p>{{title}}</p>
{% endblock %}
在application/apps/users
藍圖下,新增admin.py
站點配置檔案,把上面模板載入到admin導航中,程式碼:
重新執行專案,效果如下:
③自動註冊模型生成功能頁面
application/apps/users/admin.py
,程式碼:
from flask_admin import BaseView, expose
from application import admin, db
# 自定義一個導航頁面
# class UserAdmin(BaseView):
# @expose('/')
# def index(self):
# title = 'admin站點使用者相關的內容'
#
# data = locals()
# data.pop('self')
# return self.render(template='admin/user/index.html', **data)
#
# admin.add_view(UserAdmin(name='使用者', url='user'))
# 根據模型自動生成頁面
from .models import User
from flask_admin.contrib.sqla import ModelView
class UserAdminModel(ModelView):
# 列表頁顯示欄位列表
column_list = ['id', 'name', 'nickname']
# 列表頁顯示排除欄位列表
column_exclude_list = ['is_delete']
# 列表頁可以直接編輯的欄位列表
column_editable_list = ['nickname']
# 是否允許檢視詳情
can_view_details = True
# 列表頁顯示直接可以搜尋資料的字典
column_searchable_list = ['nickname', 'name', 'email']
# 過濾器
column_filters = ['sex']
# 單頁顯示資料量
page_size = 10
# admin.add_view(UserAdminModel(User, db.session, name='使用者')) # 把當前頁面作為頂級導航進行顯示
admin.add_view(UserAdminModel(User, db.session, name='使用者', category='使用者管理')) # 把當前頁面新增到頂級導航下,category來設定,如果導航不存在,則自動建立
# 新增子導航還有種方式: 新增超連結作為導航
from flask_admin.menu import MenuLink
admin.add_link(MenuLink(name='百度', url='http://www.baidu.com', category='使用者管理')) # 把超連結作為子導航載入到頂級導航中
④國際化與本地化
flask-babelex
是Flask 的翻譯擴充套件工具,在專案安裝配置,可以實現中文顯示。方便我們管理admin後臺站點。
安裝模組
pip install flask-babelex
模組初始化application/__init__.py
,程式碼:
import os,sys
from flask import Flask
from flask_script import Manager
from flask_sqlalchemy import SQLAlchemy
from flask_redis import FlaskRedis
from flask_session import Session
from flask_migrate import Migrate, MigrateCommand
from flask_jsonrpc import JSONRPC
from flask_marshmallow import Marshmallow
from flask_jwt_extended import JWTManager
from flask_admin import Admin
from flask_babelex import Babel
from application.utils import init_blueprint
from application.utils.config import load_config
from application.utils.session import init_session
from application.utils.logger import Log
from application.utils.commands import load_command
# 建立終端指令碼管理物件
manager = Manager()
# 建立資料庫連結物件
db = SQLAlchemy()
# redis連結物件
redis = FlaskRedis()
# Session儲存物件
session_store = Session()
# 資料遷移例項物件
migrate = Migrate()
# 日誌物件
log = Log()
# jsonrpc模組例項物件
jsonrpc = JSONRPC()
# 資料轉換器的物件建立
ma = Marshmallow()
# jwt認證模組例項化
jwt = JWTManager()
# flask_admin模組初始化
admin = Admin()
babel = Babel()
def init_app(config_path):
"""全域性初始化"""
# 建立app應用物件
app = Flask(__name__)
# 專案根目錄
app.BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# 載入導包路徑
sys.path.insert(0, os.path.join(app.BASE_DIR,"application/utils/language"))
# 載入配置
Config = load_config(config_path)
app.config.from_object(Config)
# 資料庫初始化
db.init_app(app)
redis.init_app(app)
# 資料轉換器的初始化
ma.init_app(app)
# session儲存初始化
init_session(app)
session_store.init_app(app)
# 資料遷移初始化
migrate.init_app(app,db)
# 新增資料遷移的命令到終端指令碼工具中
manager.add_command('db', MigrateCommand)
# 日誌初始化
app.log = log.init_app(app)
# 藍圖註冊
init_blueprint(app)
# jsonrpc初始化
jsonrpc.service_url = "/api" # api介面的url地址字首
jsonrpc.init_app(app)
# jwt初始化
jwt.init_app(app)
# 初始化終端指令碼工具
manager.app = app
# 註冊自定義命令
load_command(manager)
# admin站點
admin.init_app(app)
# 專案語言
babel.init_app(app)
return manager
語言配置,application/settings/__init__.py
,程式碼:
...
# 國際化與本地化
LANGUAGE = 'zh_CN'
TIMEZONE = 'Asia/Shanghai'
# 針對babel模組的語言和設定
BABEL_DEFAULT_LOCALE = LANGUAGE
BABEL_DEFAULT_TIMEZONE = TIMEZONE
訪問效果:
基於Faker生成模擬測試資料
文件:https://faker.readthedocs.io/en/master/
安裝:
pip install Faker
初始化,application/__init__.py
, 因此接下來需要自定義終端命令,所以設定db物件和faker物件作為app應用物件的子物件存在,方便引入使用,程式碼:
import os,sys
from flask import Flask
from flask_script import Manager
from flask_sqlalchemy import SQLAlchemy
from flask_redis import FlaskRedis
from flask_session import Session
from flask_migrate import Migrate, MigrateCommand
from flask_jsonrpc import JSONRPC
from flask_marshmallow import Marshmallow
from flask_jwt_extended import JWTManager
from flask_admin import Admin
from flask_babelex import Babel
from faker import Faker
from application.utils import init_blueprint
from application.utils.config import load_config
from application.utils.session import init_session
from application.utils.logger import Log
from application.utils.commands import load_command
# 建立終端指令碼管理物件
manager = Manager()
# 建立資料庫連結物件
db = SQLAlchemy()
# redis連結物件
redis = FlaskRedis()
# Session儲存物件
session_store = Session()
# 資料遷移例項物件
migrate = Migrate()
# 日誌物件
log = Log()
# jsonrpc模組例項物件
jsonrpc = JSONRPC()
# 資料轉換器的物件建立
ma = Marshmallow()
# jwt認證模組例項化
jwt = JWTManager()
# flask_admin模組初始化
admin = Admin()
# flask_babelex模組例項化
babel = Babel()
def init_app(config_path):
"""全域性初始化"""
# 建立app應用物件
app = Flask(__name__)
# 專案根目錄
app.BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# 載入導包路徑
sys.path.insert(0, os.path.join(app.BASE_DIR,"application/utils/language"))
# 載入配置
Config = load_config(config_path)
app.config.from_object(Config)
# 資料庫初始化
db.init_app(app)
app.db = db
redis.init_app(app)
# 資料轉換器的初始化
ma.init_app(app)
# session儲存初始化
init_session(app)
session_store.init_app(app)
# 資料遷移初始化
migrate.init_app(app, db)
# 新增資料遷移的命令到終端指令碼工具中
manager.add_command('db', MigrateCommand)
# 日誌初始化
app.log = log.init_app(app)
# 藍圖註冊
init_blueprint(app)
# jsonrpc初始化
jsonrpc.service_url = "/api" # api介面的url地址字首
jsonrpc.init_app(app)
# jwt初始化
jwt.init_app(app)
# admin初始化
admin.init_app(app)
# 國際化本地化模組的初始化
babel.init_app(app)
# 初始化終端指令碼工具
manager.app = app
# 資料種子生成器[faker]
app.faker = Faker(app.config.get('LANGUAGE'))
# 註冊自定義命令
load_command(manager)
return manager
基本使用,程式碼:
# 隨機IP地址
from faker import Faker
from faker.providers import internet
fake = Faker()
fake.add_provider(internet)
print(fake.ipv4_private())
# 產生隨機手機號
print(fake.phone_number())
# 產生隨機姓名
print(fake.name())
# 產生隨機地址
print(fake.address())
# 隨機產生國家名
print(fake.country())
# 隨機產生國家程式碼
print(fake.country_code())
# 隨機產生城市名
print(fake.city_name())
# 隨機產生城市
print(fake.city())
# 隨機產生省份
print(fake.province())
# 產生隨機email
print(fake.email())
# 產生隨機IPV4地址
print(fake.ipv4())
# 產生長度在最大值與最小值之間的隨機字串
print(faker.pystr(min_chars=0, max_chars=8))
# 隨機產生車牌號
print(fake.license_plate())
# 隨機產生顏色
print(fake.rgb_color()) # rgb
print(fake.safe_hex_color()) # 16進位制
print(fake.color_name()) # 顏色名字
print(fake.hex_color()) # 16進位制
# 隨機產生公司名
print(fake.company())
# 隨機產生工作崗位
print(fake.job())
# 隨機生成密碼
print(fake.password(length=10, special_chars=True, digits=True, upper_case=True, lower_case=True))
# 隨機生成uuid
print(fake.uuid4())
# 隨機生成sha1
print(fake.sha1(raw_output=False))
# 隨機生成md5
print(fake.md5(raw_output=False))
# 隨機生成女性名字
print(fake.name_female())
# 男性名字
print(fake.name_male())
# 隨機生成名字
print(fake.name())
# 生成基本資訊
print(fake.profile(fields=None, sex=None))
print(fake.simple_profile(sex=None))
# 隨機生成瀏覽器頭user_agent
print(fake.user_agent())
# 隨機產生時間 月份
print(fake.month_name())
# 'May'
print(fake.date_time_this_century(before_now=True, after_now=False, tzinfo=None))
# 207-09-18 12:21:32
print(fake.time_object(end_datetime=None))
# 16:51:21
print(fake.date_time_between(start_date="-10y", end_date="now", tzinfo=None))
# 2015-11-15 21:07:38
print(fake.future_date(end_date="+30d", tzinfo=None))
# 2020-04-25
print(fake.date_time(tzinfo=None, end_datetime=None))
# 2002-09-01 18:27:45
print(fake.date(pattern="%Y-%m-%d", end_datetime=None))
# '1998-08-02'
print(fake.date_time_this_month(before_now=True, after_now=False, tzinfo=None))
# 2020-04-03 16:03:21
print(fake.date_time_this_decade(before_now=True, after_now=False, tzinfo=None))
# 2020-01-09 01:15:08
print(fake.month())
# '11'
print(fake.day_of_week())
# 'Sunday'
print(fake.date_object(end_datetime=None))
# 2017-06-26
print(fake.date_this_decade(before_today=True, after_today=False))
# 2020-03-30
fake.date_this_century(before_today=True, after_today=False)
# datetime.date(2000, 6, 1)
fake.date_this_month(before_today=True, after_today=False)
# datetime.date(2018, 6, 13)
fake.past_datetime(start_date="-30d", tzinfo=None)
# datetime.datetime(2018, 6, 25, 7, 41, 34)
fake.date_this_year(before_today=True, after_today=False)
# datetime.date(2018, 2, 24)
fake.date_time_between_dates(datetime_start=None, datetime_end=None, tzinfo=None)
# datetime.datetime(2018, 6, 26, 14, 40, 5)
fake.date_time_ad(tzinfo=None, end_datetime=None)
# datetime.datetime(673, 1, 28, 18, 17, 55)
fake.date_between_dates(date_start=None, date_end=None)
# datetime.date(2018, 6, 26)
fake.future_datetime(end_date="+30d", tzinfo=None)
# datetime.datetime(2018, 7, 4, 10, 53, 6)
fake.past_date(start_date="-30d", tzinfo=None)
# datetime.date(2018, 5, 30)
fake.time(pattern="%H:%M:%S", end_datetime=None)
# '01:32:14'
fake.day_of_month()
# '19'
fake.unix_time(end_datetime=None, start_datetime=None)
fake.date_time_this_year(before_now=True, after_now=False, tzinfo=None)
# datetime.datetime(2018, 5, 24, 11, 25, 25)
fake.date_between(start_date="-30y", end_date="today")
# datetime.date(2003, 1, 11)
fake.year()
# '1993'
自定義終端命令,生成指定數量的測試使用者,程式碼:
application/utils/commands.py
import os
from importlib import import_module
from flask_script import Command, Option
import inspect
def load_command(manager,command_path=None):
"""自動載入自定義終端命令"""
if command_path is None:
command_path = "application.utils.commands"
module = import_module(command_path)
class_list = inspect.getmembers(module,inspect.isclass)
for class_item in class_list:
if issubclass(class_item[1],Command) and class_item[0] != "Command":
manager.add_command(class_item[1].name,class_item[1])
class BlueprintCommand(Command):
"""藍圖生成命令"""
name = "blue"
option_list = [
Option('--name', '-n', dest='name'),
]
def run(self, name):
# 生成藍圖名稱物件的目錄
os.mkdir(name)
open("%s/__init__.py" % name, "w")
open("%s/views.py" % name, "w")
open("%s/models.py" % name, "w")
with open("%s/urls.py" % name, "w") as f:
content = """from . import views
from application.utils import path
urlpatterns = [
]"""
f.write(content)
print("藍圖%s建立完成...." % name)
from flask import current_app
import random
from faker.providers import internet
class CreateUserCommand(Command):
"""生成自定義數量的使用者資訊"""
name = 'faker'
option_list = [
Option('--num', '-n', dest='num')
]
def run(self, num):
with current_app.app_context():
from application.apps.users.models import User, UserProfile
current_app.faker.add_provider(internet)
faker = current_app.faker
try:
num = int(num)
except:
num = 1
user_list = []
password = '123456'
for _ in range(0, num):
sex = bool(random.randint(0, 2))
if sex == 0:
# 性別保密的使用者
nickname = faker.name()
elif sex == 1:
# 性別為男的使用者
nickname = faker.name_male()
else:
# 性別為女的使用者
nickname = faker.name_female()
name = faker.pystr(min_chars=6, max_chars=16)
# 生成指定範圍的時間物件
age = random.randint(13, 50)
birthday = faker.data_time_between(start_date='-%sy' % age, end_date='-12y', tzinfo=None)
hometown_province = faker.province()
hometown_city = faker.city()
hometown_area = faker.district()
living_province = faker.province()
living_city = faker.city()
living_area = faker.district()
user = User(
nickname=nickname,
sex=sex,
name=name,
password=name,
money=random.randint(100, 99999),
ip_address=faker.ipv4_public(),
email=faker.ascii_free_email(),
mobile=faker.phone_number(),
unique_id=faker.uuid4(),
province=faker.province(),
city=faker.city(),
area=faker.district(),
info=UserProfile(
birthday=birthday,
hometown_province=hometown_province,
hometown_city=hometown_city,
hometown_area=hometown_area,
hometown_address=hometown_province + hometown_city + hometown_area + faker.street_address(),
living_province=living_province,
living_city=living_city,
living_area=living_area,
living_address=living_province + living_city + living_area + faker.street_address()
)
)
user_list.append(user)
current_app.db.session.add_all(user_list)
current_app.db.session.commit()
python manage.py faker --num=3