魔方APP项目-09-用户中心,退出登录、更新头像、本地更新头像信息

用户中心

一、退出登录

APP项目中对于用户的退出登录,一般都在设置中进行。
客户端新增配置页面setting.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 setting" id="app">
		<div class="bg">
      <img src="../static/images/form_bg.png">
    </div>
		<img class="back" @click="goto_home" src="../static/images/user_back.png" alt="">
    <div class="form">
      <div class="item avatar">
        <span class="title">头像</span>
        <span class="goto">&gt;</span>
        <span class="value">
          <img src="../static/images/avatar.png" alt="">
        </span>
      </div>
      <div class="item">
        <span class="title">昵称</span>
        <span class="goto">&gt;</span>
        <span class="value">清蒸小帅锅</span>
      </div>
      <div class="item">
        <span class="title">手机号</span>
        <span class="goto">&gt;</span>
        <span class="value">139****5901</span>
      </div>
      <div class="item">
        <span class="title">登陆密码</span>
        <span class="value"></span>
        <span class="goto">&gt;</span>
      </div>
      <div class="item">
        <span class="title">交易密码</span>
        <span class="value"></span>
        <span class="goto">&gt;</span>
      </div>
      <div class="item">
        <span class="title">地址管理</span>
        <span class="value"></span>
        <span class="goto">&gt;</span>
      </div>
      <div class="item">
        <span class="title">设备管理</span>
        <span class="value"></span>
        <span class="goto">&gt;</span>
      </div>
      <div class="item logout">
        <img @click="change_account" src="../static/images/change_account.png" alt="">
        <p @click="logout">退出账号</p>
      </div>
    </div>
	</div>
	<script>
	apiready = function(){
		init();
		new Vue({
			el:"#app",
			data(){
				return {
					prev:{name:"",url:"",params:{}},
					current:{name:"setting",url:"setting.html",params:{}},
				}
			},
			methods:{
        goto_home(){
          // this.game.outFrame("setting");
          this.game.goFrame("user","user.html",this.current);
        },
        change_account(){
          // 切换账号
          this.game.goFrame("login","login.html", this.current);
        },
        logout(){
          // 退出账号
          api.actionSheet({
              title: '您确认要退出当前登录吗?',
              cancelTitle: '取消',
              destructiveTitle: '退出登录'
          }, (ret, err)=>{
              if( ret ){
                  this.game.print(ret);
                   if(ret.buttonIndex==1){
                     this.game.save({"access_token":"","refresh_token":""});
                     this.game.fremove(["access_token","refresh_token"]);
                     this.game.outWin("user");
                   }
              }
          });

        }
			}
		});
	}
	</script>
</body>
</html>

static/css/main.css,样式代码:


.setting .bg img{
  animation: normal;
}
.setting .back{
  top: 4rem;
}
.setting .form {
  top: 9rem;
}
.setting .form .item{
  height: 3.9rem;
  line-height: 3.9rem;
  font-size: 1.25rem;
  text-indent: 0.6rem;
  border-bottom: 1px solid rgba(204,153,102,0.2);
}
.setting .form .avatar{
  height: 6.11rem;
  line-height: 6.11rem;
}
.setting .form .avatar img{
  width: 4.56rem;
  height: 4.56rem;
  vertical-align: middle;
}
.setting .form .item .value,
.setting .form .item .goto{
  float: right;
}
.setting .form .logout{
  margin-top: 3rem;
  text-align: center;
  height: 4rem;
  line-height: 2rem;
}
.setting .form .logout img{
  width: 11rem;
}

在用户中心实现页面跳转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" @click='goto_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");
          },
					goto_setting(){
						this.game.goFrame('setting', 'setting.html', this.current);
					}
			}
		});
	}
	</script>
</body>
</html>

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,animation=null){
		// 创建帧页面
		if(rect === null){
				rect = {
					// 方式1,设置矩形大小宽高
					x: 0,             // 左上角x轴坐标
					y: 0,             // 左上角y轴坐标
					w: 'auto',        // 当前帧页面的宽度, auto表示满屏
					h: 'auto'         // 当前帧页面的高度, auto表示满屏
				}
		}
		if (animation === null) {
			animation = {
				type: 'push',
				subType:'from_right',
				duration:300
			}
		}

		api.openFrame({
		    name: name,        // 帧页面的名称
		    url: url,          // 帧页面打开的url地址
		    bounces:false,     // 页面是否可以下拉拖动
		    reload: true,      // 帧页面如果已经存在,是否重新刷新加载
		    useWKWebView: true,
		    historyGestureEnabled:true,
		    animation:animation,
		    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);
  }
}

二、更新头像

点击头像进入更新头像页面,setting.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 setting" id="app">
		<div class="bg">
      <img src="../static/images/form_bg.png">
    </div>
		<img class="back" @click="goto_home" src="../static/images/user_back.png" alt="">
    <div class="form">
      <div class="item avatar">
        <span class="title">头像</span>
        <span class="goto">&gt;</span>
        <span class="value">
          <img @click="update_avatar_frame" src="../static/images/avatar.png" alt="">
        </span>
      </div>
      <div class="item">
        <span class="title">昵称</span>
        <span class="goto">&gt;</span>
        <span class="value">清蒸小帅锅</span>
      </div>
      <div class="item">
        <span class="title">手机号</span>
        <span class="goto">&gt;</span>
        <span class="value">139****5901</span>
      </div>
      <div class="item">
        <span class="title">登陆密码</span>
        <span class="value"></span>
        <span class="goto">&gt;</span>
      </div>
      <div class="item">
        <span class="title">交易密码</span>
        <span class="value"></span>
        <span class="goto">&gt;</span>
      </div>
      <div class="item">
        <span class="title">地址管理</span>
        <span class="value"></span>
        <span class="goto">&gt;</span>
      </div>
      <div class="item">
        <span class="title">设备管理</span>
        <span class="value"></span>
        <span class="goto">&gt;</span>
      </div>
      <div class="item logout">
        <img @click="change_account" src="../static/images/change_account.png" alt="">
        <p @click="logout">退出账号</p>
      </div>
    </div>
	</div>
	<script>
	apiready = function(){
		init();
		new Vue({
			el:"#app",
			data(){
				return {
					prev:{name:"",url:"",params:{}},
					current:{name:"setting",url:"setting.html",params:{}},
				}
			},
			methods:{
        goto_home(){
          // this.game.outFrame("setting");
          this.game.goFrame("user","user.html",this.current);
        },
        change_account(){
          // 切换账号
          this.game.goFrame("login","login.html", this.current);
        },
        logout(){
          // 退出账号
          api.actionSheet({
              title: '您确认要退出当前登录吗?',
              cancelTitle: '取消',
              destructiveTitle: '退出登录'
          }, (ret, err)=>{
              if( ret ){
                  this.game.print(ret);
                   if(ret.buttonIndex==1){
                     this.game.save({"access_token":"","refresh_token":""});
                     this.game.fremove(["access_token","refresh_token"]);
                     this.game.outWin("user");
                   }
              }
          });
        },
        update_avatar_frame(){
          // 更换头像
          this.game.goFrame('avatar','avatar.html', this.current, null,{
            type:'push',  // 动画类型(详见动画类型常量)
            subType:'from_top',  // 动画子类型(详见动画子类型常量)
            duration:300  // 动画过渡时间,默认300毫秒
          })
        }

        }
		});
	}
	</script>
</body>
</html>

avatar.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 frame avatar" id="app">
    <div class="box">
      <p class="title">上传头像</p>
      <img class="close" src="../static/images/close_btn1.png" alt="">
      <div class="content">
        <p class="header">!注意事项</p>
        <p class="text">禁止使用有诱导性的内容,二维码,联系方式等违规违法违约的图片,一经发现,永久封号,
          并保留追究法律责任的权利。</p>
      </div>
      <img @click="update_avatar_confirm" class="btn" src="../static/images/yes.png" alt="">
    </div>
	</div>
	<script>
	apiready = function(){
		init();
		new Vue({
			el:"#app",
			data(){
				return {
					prev:{name:"",url:"",params:{}},
					current:{name:"avatar",url:"avatar.html",params:{}},
				}
			},
			methods:{
        update_avatar_confirm(){
          // 确认上传头像

        },
        upload_avatar(ret){
          // 头像上传处理
          
        }
			}
		});
	}
	</script>
</body>
</html>

main.css,页面样式,代码:

.avatar.frame{
  background-color: rgba(0,0,0,0.6);
}
.avatar{
  overflow: hidden;
}
.avatar .box{
  width: 28.89rem;
  height: 34.44rem;
  background: url("../images/board_bg1.png") no-repeat 0 0;
  background-size: 100%;
  position: absolute;
  top: 11rem;
  margin: 0 auto;
  left: 0;
  right: 0;
}

.avatar .box .title{
  color: #fff;
  font-size: 2rem;
  text-align: center;
  margin-top: 2.8rem;
}
.avatar .box .close{
  width: 5.22rem;
  height: 5.78rem;
  position: absolute;
  right: 0;
  top: 8rem;
}
.avatar .box .header{
  margin-top: 3.6rem;
  font-size: 1.8rem;
  text-align: center;
  color: #ff3333;
  font-weight: bold;
}
.avatar .box .text{
  width: 16.67rem;
  margin: 1.4rem auto 0;
  font-size: 1.22rem;
  color: #ffffcc;
}
.avatar .box .btn{
  display: block;
  width: 12.22rem;
  height: 4.55rem;
  margin: 1.4rem auto 0;
}

头像上传来源选择,avatar.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 frame avatar" id="app">
    <div class="box">
      <p class="title">上传头像</p>
      <img class="close" @click="close_frame" src="../static/images/close_btn1.png" alt="">
      <div class="content">
        <p class="header">!注意事项</p>
        <p class="text">禁止使用有诱导性的内容,二维码,联系方式等违规违法违约的图片,一经发现,永久封号,
          并保留追究法律责任的权利。</p>
      </div>
      <img @click="update_avatar_confirm" class="btn" src="../static/images/yes.png" alt="">
    </div>
	</div>
	<script>
	apiready = function(){
		init();
		new Vue({
			el:"#app",
			data(){
				return {
					prev:{name:"",url:"",params:{}},
					current:{name:"avatar",url:"avatar.html",params:{}},
				}
			},
			methods:{
        close_frame(){
          this.game.outFrame("avatar");
        },
        update_avatar_confirm(){
          // 确认上传头像的方式
          api.actionSheet({
              title: '请选择上传头像的来源',
              cancelTitle: '取消',
              buttons: ['相册','相机']
          }, function(ret, err){
              if( ret ){
                   alert( JSON.stringify( ret ) );
              }else{
                   this.game.print( err );
              }
          });
          

        },
        upload_avatar(ret){
          // 头像上传处理

        }
			}
		});
	}
	</script>
</body>
</html>

接下来,调用apicloud提供的本地接口从相册或者相机中提取图片.

注意: 如果使用模拟器开发APP的话, 就不能使用相机!相机只能用于真机测试!

avatar.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 frame avatar" id="app">
    <div class="box">
      <p class="title">上传头像</p>
      <img class="close" @click="close_frame" src="../static/images/close_btn1.png" alt="">
      <div class="content">
        <p class="header">!注意事项</p>
        <p class="text">禁止使用有诱导性的内容,二维码,联系方式等违规违法违约的图片,一经发现,永久封号,
          并保留追究法律责任的权利。</p>
      </div>
      <img @click="update_avatar_confirm" class="btn" src="../static/images/yes.png" alt="">
    </div>
	</div>
	<script>
	apiready = function(){
		init();
		new Vue({
			el:"#app",
			data(){
				return {
					prev:{name:"",url:"",params:{}},
					current:{name:"avatar",url:"avatar.html",params:{}},
				}
			},
			methods:{
        close_frame(){
          this.game.outFrame("avatar");
        },
        update_avatar_confirm(){
          // 确认上传头像的方式
          api.actionSheet({
              title: '请选择上传头像的来源',
              cancelTitle: '取消',
              buttons: ['相册','相机']
          }, (ret, err)=>{
              if( ret ){
                  // 本地获取相册/相机的图片
                  var sourceType = ['album', 'camera'];
                  if (ret.buttonIndex > sourceType.length) {
                    // 如果用户选择了取消,则关闭当前修改头像的页面
                    this.game.outFrame('avatar');
                    return;
                  }
                   api.getPicture({
                       sourceType: sourceType[ret.buttonIndex - 1],
                       mediaValue: 'pic',
                       destinationType: 'base64',
                       allowEdit: true,
                       preview: true,
                       quality: 50,
                       targetWidth: 100,
                       targetHeight: 100,
                       saveToPhotoAlbum: true,
                   }, (ret, err)=>{
                       if(ret){
                            this.game.print(ret));
                       }else{
                            this.game.print(err));
                       }
                   });

                   alert( JSON.stringify( ret ) );
              }else{
                   this.game.print( err );
              }
          });


        },
        upload_avatar(ret){
          // 头像上传处理

        }
			}
		});
	}
	</script>
</body>
</html>

服务端提供头像更新接口,application/apps/users/views.py,代码:

import base64, uuid, os
@jsonrpc.method('User.avatar.update')
@jwt_required  # 验证jwt
def update_avatar(avatar):
    """更新用户头像"""
    # 1.接收客户端上传的头像信息
    ext = avatar[avatar.find('/') + 1: avatar.find(';')]  # 资源格式
    b64_avatar = avatar[avatar.find(',') + 1:]
    b64_image = base64.b64decode(b64_avatar)
    filename = uuid.uuid4()
    static_path = os.path.join(current_app.BASE_DIR, current_app.config['STATIC_DIR'])
    with open('%s/%s.%s' % (static_path, filename, ext), 'wb') as f:
        f.write(b64_image)
        
    return 'ok'

application/settings/dev.py,代码:

from . import InitConfig
class Config(InitConfig):
    """项目开发环境下的配置"""
    DEBUG = True
    # 数据库
    SQLALCHEMY_DATABASE_URI = "mysql://mofang_user:mofang@127.0.0.1:3306/mofang?charset=utf8mb4"
    SQLALCHEMY_ECHO = True

    # redis
    REDIS_URL = "redis://@127.0.0.1:6379/0"

    # session存储配置
    SESSION_REDIS_HOST = "127.0.0.1"
    SESSION_REDIS_PORT = 6379
    SESSION_REDIS_DB = 1

    # 日志配置
    LOG_LEVEL = "INFO"  # 日志输出到文件中的最低等级
    LOG_DIR = "/logs/mofang.log"  # 日志存储目录
    LOG_MAX_BYTES = 300 * 1024 * 1024   # 单个日志文件的存储上限[单位: b]
    LOG_BACKPU_COUNT = 20   # 日志文件的最大备份数量
    LOG_NAME = "mofang"  # 日志器名称

    # 注册蓝图
    INSTALLED_APPS = [
        "application.apps.home",
        "application.apps.users",
        "application.apps.marsh",
    ]

    # 短信相关配置
    SMS_ACCOUNT_ID = "8aaf0708754a3ef2017563ddb22d0773"  # 接口主账号
    SMS_ACCOUNT_TOKEN = "0b41612bc8a8429d84b5d37f29178743"  # 认证token令牌
    SMS_APP_ID = "8aaf0708754a3ef2017563ddb3110779"  # 应用ID
    SMS_TEMPLATE_ID = 1  # 短信模板ID
    SMS_EXPIRE_TIME = 60 * 5  # 短信有效时间,单位:秒/s
    SMS_INTERVAL_TIME = 60   # 短信发送冷却时间,单位:秒/s

    # jwt 相关配置
    # 加密算法,默认: HS256
    JWT_ALGORITHM = "HS256"
    # 秘钥,默认是flask配置中的SECRET_KEY
    JWT_SECRET_KEY = "y58Rsqzmts6VCBRHes1Sf2DHdGJaGqPMi6GYpBS4CKyCdi42KLSs9TQVTauZMLMw"
    # token令牌有效期,单位: 秒/s,默认: datetime.timedelta(minutes=15) 或者 15 * 60
    JWT_ACCESS_TOKEN_EXPIRES = 60 * 60
    # refresh刷新令牌有效期,单位: 秒/s,默认:datetime.timedelta(days=30) 或者 30*24*60*60
    JWT_REFRESH_TOKEN_EXPIRES = 30*24*60*60
    # 设置通过哪种方式传递jwt,默认是http请求头,也可以是query_string,json,cookies
    JWT_TOKEN_LOCATION = ["headers", "query_string"]
    # 当通过http请求头传递jwt时,请求头参数名称设置,默认值: Authorization
    JWT_HEADER_NAME = "Authorization"
    # 当通过查询字符串传递jwt时,查询字符串的参数名称设置,默认:jwt
    JWT_QUERY_STRING_NAME = 'token'
    # 当通过http请求头传递jwt时,令牌的前缀。
    # 默认值为 "Bearer",例如:Authorization: Bearer <JWT>
    JWT_HEADER_TYPE = "jwt"

    # 防水墙验证码
    CAPTCHA_GATEWAY = "https://ssl.captcha.qq.com/ticket/verify"
    CAPTCHA_APP_ID = "2041284967"
    CAPTCHA_APP_SECRET_KEY = "0FrDthTnnU8vG-jSwz7DOAA**"

application/settings/__init__.py,代码:

    # 静态文件目录存储路径
    STATIC_DIR = "application/static"

客户端基于axios上传图片数据,代码:
avatar.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 frame avatar" id="app">
    <div class="box">
      <p class="title">上传头像</p>
      <img class="close" @click="close_frame" src="../static/images/close_btn1.png" alt="">
      <div class="content">
        <p class="header">!注意事项</p>
        <p class="text">禁止使用有诱导性的内容,二维码,联系方式等违规违法违约的图片,一经发现,永久封号,
          并保留追究法律责任的权利。</p>
      </div>
      <img @click="update_avatar_confirm" class="btn" src="../static/images/yes.png" alt="">
    </div>
	</div>
	<script>
	apiready = function(){
		init();
		new Vue({
			el:"#app",
			data(){
				return {
					prev:{name:"",url:"",params:{}},
					current:{name:"avatar",url:"avatar.html",params:{}},
				}
			},
			methods:{
        close_frame(){
          this.game.outFrame("avatar");
        },
        update_avatar_confirm(){
          // 确认上传头像的方式
          api.actionSheet({
              title: '请选择上传头像的来源',
              cancelTitle: '取消',
              buttons: ['相册','相机'],
          }, (ret, err)=>{
              if( ret ){
                  // 本地获取相册/相机的图片
                  var sourceType = ['album', 'camera'];
                  if (ret.buttonIndex > sourceType.length) {
                    // 如果用户选择了取消,则关闭当前修改头像的页面
                    this.game.outFrame('avatar');
                    return;
                  }
                   api.getPicture({
                       sourceType: sourceType[ret.buttonIndex - 1],
                       mediaValue: 'pic',
                       destinationType: 'base64',
                       allowEdit: true,
                       preview: true,
                       quality: 100,
                       targetWidth: 100,
                       targetHeight: 100,
                       saveToPhotoAlbum: true,
                   }, (ret, err)=>{
                       if(ret){
                            this.upload_avatar(ret));
                       }else{
                            this.game.print(err));
                       }
                   });

                   alert( JSON.stringify( ret ) );
              }else{
                   this.game.print( err );
              }
          });


        },
        upload_avatar(ret){
          // 头像上传处理
          var token = this.game.get('access_token') || this.game.fget('access_token');
          if(!token){
            this.game.goFrame('login', 'login.html', this.current);
            return;
          }
          this.axios.post('', {
            'jsonrpc': '2.0',
            'id': this.uuid(),
            'method': 'User.avatar.update',
            'params': {
              'avatar': ret.base64Data,
            }
          },{
            headers:{
              Authorization: 'jwt ' + token,
            }
          }).then(response=>{
            if (parseInt(response.data.result.errno) === 1000) {
              this.game.print(response);
            }else {
              this.game.print(response.data.result);
            }
          }).catch(error=>{
            // 网络异常
            this.game.print(error);
          })

        }
			}
		});
	}
	</script>
</body>
</html>

三、本地更新头像信息

关闭avatar.html页面,展示底下的setting.html,并更新头像.
客户端的avatar.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 frame avatar" id="app">
    <div class="box">
      <p class="title">上传头像</p>
      <img class="close" @click="close_frame" src="../static/images/close_btn1.png" alt="">
      <div class="content">
        <p class="header">!注意事项</p>
        <p class="text">禁止使用有诱导性的内容,二维码,联系方式等违规违法违约的图片,一经发现,永久封号,
          并保留追究法律责任的权利。</p>
      </div>
      <img @click="update_avatar_confirm" class="btn" src="../static/images/yes.png" alt="">
    </div>
	</div>
	<script>
	apiready = function(){
		init();
		new Vue({
			el:"#app",
			data(){
				return {
					prev:{name:"",url:"",params:{}},
					current:{name:"avatar",url:"avatar.html",params:{}},
				}
			},
			methods:{
        close_frame(){
          this.game.outFrame("avatar");
        },
        update_avatar_confirm(){
          // 确认上传头像的方式
          api.actionSheet({
              title: '请选择上传头像的来源',
              cancelTitle: '取消',
              buttons: ['相册','相机'],
          }, (ret, err)=>{
              if( ret ){
                  // 本地获取相册/相机的图片
                  var sourceType = ['album', 'camera'];
                  if (ret.buttonIndex > sourceType.length) {
                    // 如果用户选择了取消,则关闭当前修改头像的页面
                    this.game.outFrame('avatar');
                    return;
                  }
                   api.getPicture({
                       sourceType: sourceType[ret.buttonIndex - 1],
                       mediaValue: 'pic',
                       destinationType: 'base64',
                       allowEdit: true,
                       preview: true,
                       quality: 100,
                       targetWidth: 100,
                       targetHeight: 100,
                       saveToPhotoAlbum: true,
                   }, (ret, err)=>{
                       if(ret){
                            this.upload_avatar(ret);
                       }else{
                            this.game.print(err);
                       }
                   });

              }else{
                   this.game.print( err );
              }
          });


        },
        upload_avatar(ret){
          // 头像上传处理
          var token = this.game.get('access_token') || this.game.fget('access_token');
          if(!token){
            this.game.goFrame('login', 'login.html', this.current);
            return;
          }
          this.axios.post('', {
            'jsonrpc': '2.0',
            'id': this.uuid(),
            'method': 'User.avatar.update',
            'params': {
              'avatar': ret.base64Data,
            }
          },{
            headers:{
              Authorization: 'jwt ' + token,
            }
          }).then(response=>{
            if (parseInt(response.data.result.errno) === 1000) {
              this.game.fsave({'avatar': response.data.result.avatar});
              // this.game.outFrame('avatar');
              this.game.goFrame('setting', 'setting.html', this.current);
            }else {
              // this.game.print('>>> fail');
              this.game.print(response.data);
            }
          }).catch(error=>{
            // 网络等异常
            this.game.print(error);
          })

        }
			}
		});
	}
	</script>
</body>
</html>

setting.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 setting" id="app">
		<div class="bg">
      <img src="../static/images/form_bg.png">
    </div>
		<img class="back" @click="goto_home" src="../static/images/user_back.png" alt="">
    <div class="form">
      <div class="item avatar">
        <span class="title">头像</span>
        <span class="goto">&gt;</span>
        <span class="value">
          <img @click="update_avatar_frame" :src="avatar" alt="">
        </span>
      </div>
      <div class="item">
        <span class="title">昵称</span>
        <span class="goto">&gt;</span>
        <span class="value">清蒸小帅锅</span>
      </div>
      <div class="item">
        <span class="title">手机号</span>
        <span class="goto">&gt;</span>
        <span class="value">139****5901</span>
      </div>
      <div class="item">
        <span class="title">登陆密码</span>
        <span class="value"></span>
        <span class="goto">&gt;</span>
      </div>
      <div class="item">
        <span class="title">交易密码</span>
        <span class="value"></span>
        <span class="goto">&gt;</span>
      </div>
      <div class="item">
        <span class="title">地址管理</span>
        <span class="value"></span>
        <span class="goto">&gt;</span>
      </div>
      <div class="item">
        <span class="title">设备管理</span>
        <span class="value"></span>
        <span class="goto">&gt;</span>
      </div>
      <div class="item logout">
        <img @click="change_account" src="../static/images/change_account.png" alt="">
        <p @click="logout">退出账号</p>
      </div>
    </div>
	</div>
	<script>
	apiready = function(){
		init();
		new Vue({
			el:"#app",
			data(){
				return {
          avatar:"",
					prev:{name:"",url:"",params:{}},
					current:{name:"setting",url:"setting.html",params:{}},
				}
			},
      created(){
        var token = this.game.get('access_token') || this.game.fget('access_token');
        this.avatar = `{this.settings.avatar_url}?sign=${this.game.fget('avatar')}&token{token}`
      },
			methods:{
        goto_home(){
          // this.game.outFrame("setting");
          this.game.goFrame("user","user.html",this.current);
        },
        change_account(){
          // 切换账号
          this.game.goFrame("login","login.html", this.current);
        },
        logout(){
          // 退出账号
          api.actionSheet({
              title: '您确认要退出当前登录吗?',
              cancelTitle: '取消',
              destructiveTitle: '退出登录'
          }, (ret, err)=>{
              if( ret ){
                  this.game.print(ret);  // 取消为2
                   if(ret.buttonIndex==1){
                     this.game.save({"access_token":"","refresh_token":""});
                     this.game.fremove(["access_token","refresh_token"]);
                     this.game.outWin("user");
                   }
              }
          });
        },
        update_avatar_frame(){
          // 显示修改头像的页面frame
          this.game.goFrame('avatar','avatar.html', this.current, null,{
            type:'push',  // 动画类型(详见动画类型常量)
            subType:'from_top',  // 动画子类型(详见动画子类型常量)
            duration:300  // 动画过渡时间,默认300毫秒
          })
        }

        }
		});
	}
	</script>
</body>
</html>

客户端的配置文件static/js/settings.js新增配置项avatar_url提供头像访问地址,代码:

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
    avatar_url: "http://192.168.20.158:5000/users/avatar",
  }
}

服务端提供展示图片的视图方法地址
application/apps/users/urls.py,代码:

from . import views
from application.utils import path
urlpatterns = [
    path('/avatar', views.avatar),
]

确认总路由设置了访问蓝图的url前缀,application/urls.py,代码:

from application.utils import include
urlpatterns = [
    include("", "home.urls"),
    include("/users", "users.urls"),
    include("/marsh", "marsh.urls"),
]

视图中, 显示图片时验证用户身份,application/apps/users/views.py代码:

import base64, uuid, os
@jsonrpc.method('User.avatar.update')
@jwt_required  # 验证jwt
def update_avatar(avatar):
    """更新用户头像"""
    # 1.接收客户端上传的头像信息
    ext = avatar[avatar.find('/') + 1: avatar.find(';')]  # 资源格式
    b64_avatar = avatar[avatar.find(',') + 1:]
    b64_image = base64.b64decode(b64_avatar)
    filename = uuid.uuid4()
    static_path = os.path.join(current_app.BASE_DIR, current_app.config['STATIC_DIR'])
    with open('%s/%s.%s' % (static_path, filename, ext), 'wb') as f:
        f.write(b64_image)
        
    current_user_ip = get_jwt_identity()
    user = User.query.get(current_user_ip)
    if user is None:
        return {
            'errno': status.CODE_NO_USER,
            'errmsg': message.user_not_exists,
        }
    user.avatar = '%s.%s' % (filename, ext)
    db.session.commit()
    return {
        'errno': status.CODE_OK,
        'errmsg': message.avatar_save_success,
        'avatar': '%s.%s' % (filename, ext)
    }

from flask import make_response, request

@jwt_required  # 验证jwt
def avatar():
    """获取头像信息"""
    avatar = request.args.get('sign')
    ext = avatar[avatar.find('.') + 1:]
    filename = avatar[:avatar.find('.')]
    static_path = os.path.join(current_app.BASE_DIR, current_app.config['STATIC_DIR'])
    with open('%s/%s.%s' % (static_path, filename, ext), 'rb') as f:
        content = f.read()
    response = make_response(content)
    response.headers['Content-Type'] = 'image/%s' % ext
    return response

application/utils/language/message.py,代码:

    avatar_save_success = "用户头像保存成功!"

完成了上面步骤代码以后, 我们现在能在客户端修改用户头像了,但是在关闭avatar.html页面以后, setting.html页面中的头像并没有及时发生变化,.所以我们在avatar.html页面更新了头像以后, 我们借住apicloud提供的自定义事件和事件监听接口,来实现,通知setting.html页面,用户头衔已经发生改变了.
文档: https://docs.apicloud.com/Client-API/api#72

当前功能,我们需要根据文档了解关于sendEventaddEventListener的使用.

# a页面可以通过sendEvent发起一个自定义事件
api.sendEvent({
    name: 'myEvent', # 自定义事件名称
    extra: {
        key1: 'value1',  # 事件传参
        key2: 'value2'
    }
});

# b页面可以通过addEventListener进行监听是否有对应名称的自定义事件进行发送了,一旦监听到,则自动执行回调函数:
api.addEventListener({
    name: 'myEvent' # 监听指定名称的事件
}, function(ret, err) {   # ret接收指定名称的事件参数
    alert(JSON.stringify(ret.value));
});

# c页面也可以通过addEventListener进行监听是否有对应名称的自定义事件进行发送了,一旦监听到,则自动执行回调函数:
api.addEventListener({
    name: 'myEvent'
}, function(ret, err) {
    alert(JSON.stringify(ret.value));
});

# b.c 页面都将收到 myEvent 事件

接下来,我们就可以在avatar.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 frame avatar" id="app">
    <div class="box">
      <p class="title">上传头像</p>
      <img class="close" @click="close_frame" src="../static/images/close_btn1.png" alt="">
      <div class="content">
        <p class="header">!注意事项</p>
        <p class="text">禁止使用有诱导性的内容,二维码,联系方式等违规违法违约的图片,一经发现,永久封号,
          并保留追究法律责任的权利。</p>
      </div>
      <img @click="update_avatar_confirm" class="btn" src="../static/images/yes.png" alt="">
    </div>
	</div>
	<script>
	apiready = function(){
		init();
		new Vue({
			el:"#app",
			data(){
				return {
					prev:{name:"",url:"",params:{}},
					current:{name:"avatar",url:"avatar.html",params:{}},
				}
			},
			methods:{
        close_frame(){
          this.game.outFrame("avatar");
        },
        update_avatar_confirm(){
          // 确认上传头像的方式
          api.actionSheet({
              title: '请选择上传头像的来源',
              cancelTitle: '取消',
              buttons: ['相册','相机'],
          }, (ret, err)=>{
              if( ret ){
                  // 本地获取相册/相机的图片
                  var sourceType = ['album', 'camera'];
                  if (ret.buttonIndex > sourceType.length) {
                    // 如果用户选择了取消,则关闭当前修改头像的页面
                    this.game.outFrame('avatar');
                    return;
                  }
                   api.getPicture({
                       sourceType: sourceType[ret.buttonIndex - 1],
                       mediaValue: 'pic',
                       destinationType: 'base64',
                       allowEdit: true,
                       preview: true,
                       quality: 100,
                       targetWidth: 100,
                       targetHeight: 100,
                       saveToPhotoAlbum: true,
                   }, (ret, err)=>{
                       if(ret){
                            this.upload_avatar(ret);
                       }else{
                            this.game.print(err);
                       }
                   });

              }else{
                   this.game.print( err );
              }
          });


        },
        upload_avatar(ret){
          // 头像上传处理
          var token = this.game.get('access_token') || this.game.fget('access_token');
          if(!token){
            this.game.goFrame('login', 'login.html', this.current);
            return;
          }
          this.axios.post('', {
            'jsonrpc': '2.0',
            'id': this.uuid(),
            'method': 'User.avatar.update',
            'params': {
              'avatar': ret.base64Data,
            }
          },{
            headers:{
              Authorization: 'jwt ' + token,
            }
          }).then(response=>{
            if (parseInt(response.data.result.errno) == 1000) {
              this.game.fsave({'avatar': response.data.result.avatar});
              // 发送自定义时间
              api.sendEvent({
                  name: 'change_avatar',
                  extra: {
                      'avatar': response.data.result.avatar
                  }
              });
              this.game.outFrame('avatar');
            }else {
              // this.game.print('>>> fail');
              this.game.print(response.data);
            }
          }).catch(error=>{
            // 网络等异常
            this.game.print(error);
          })

        }
			}
		});
	}
	</script>
</body>
</html>

setting.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 setting" id="app">
		<div class="bg">
      <img src="../static/images/form_bg.png">
    </div>
		<img class="back" @click="goto_home" src="../static/images/user_back.png" alt="">
    <div class="form">
      <div class="item avatar">
        <span class="title">头像</span>
        <span class="goto">&gt;</span>
        <span class="value">
          <img @click="update_avatar_frame" :src="avatar" alt="">
        </span>
      </div>
      <div class="item">
        <span class="title">昵称</span>
        <span class="goto">&gt;</span>
        <span class="value">清蒸小帅锅</span>
      </div>
      <div class="item">
        <span class="title">手机号</span>
        <span class="goto">&gt;</span>
        <span class="value">139****5901</span>
      </div>
      <div class="item">
        <span class="title">登陆密码</span>
        <span class="value"></span>
        <span class="goto">&gt;</span>
      </div>
      <div class="item">
        <span class="title">交易密码</span>
        <span class="value"></span>
        <span class="goto">&gt;</span>
      </div>
      <div class="item">
        <span class="title">地址管理</span>
        <span class="value"></span>
        <span class="goto">&gt;</span>
      </div>
      <div class="item">
        <span class="title">设备管理</span>
        <span class="value"></span>
        <span class="goto">&gt;</span>
      </div>
      <div class="item logout">
        <img @click="change_account" src="../static/images/change_account.png" alt="">
        <p @click="logout">退出账号</p>
      </div>
    </div>
	</div>
	<script>
	apiready = function(){
		init();
		new Vue({
			el:"#app",
			data(){
				return {
          avatar:"../static/images/avatar.png",
					prev:{name:"",url:"",params:{}},
					current:{name:"setting",url:"setting.html",params:{}},
				}
			},
      created(){
        this.change_avatar();
      },
			methods:{
        change_avatar(){
          api.addEventListener({
              name: 'change_avatar'
          }, (ret, err)=>{
              if( ret ){
                var token = this.game.get('access_token') || this.game.fget('access_token');
                this.avatar = `{this.settings.avatar_url}?sign=${ret.value.avatar}&token${token}`;
              }
          });

        },
        goto_home(){
          // this.game.outFrame("setting");
          this.game.goFrame("user","user.html",this.current);
        },
        change_account(){
          // 切换账号
          this.game.goFrame("login","login.html", this.current);
        },
        logout(){
          // 退出账号
          api.actionSheet({
              title: '您确认要退出当前登录吗?',
              cancelTitle: '取消',
              destructiveTitle: '退出登录'
          }, (ret, err)=>{
              if( ret ){
                  this.game.print(ret);  // 取消为2
                   if(ret.buttonIndex==1){
                     this.game.save({"access_token":"","refresh_token":""});
                     this.game.fremove(["access_token","refresh_token"]);
                     this.game.outWin("user");
                   }
              }
          });
        },
        update_avatar_frame(){
          // 显示修改头像的页面frame
          this.game.goFrame('avatar','avatar.html', this.current, null,{
            type:'push',  // 动画类型(详见动画类型常量)
            subType:'from_top',  // 动画子类型(详见动画子类型常量)
            duration:300  // 动画过渡时间,默认300毫秒
          })
        }

        }
		});
	}
	</script>
</body>
</html>

setting.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 setting" id="app">
		<div class="bg">
      <img src="../static/images/form_bg.png">
    </div>
		<img class="back" @click="goto_home" src="../static/images/user_back.png" alt="">
    <div class="form">
      <div class="item avatar">
        <span class="title">头像</span>
        <span class="goto">&gt;</span>
        <span class="value">
          <img @click="update_avatar_frame" :src="avatar" alt="">
        </span>
      </div>
      <div class="item">
        <span class="title">昵称</span>
        <span class="goto">&gt;</span>
        <span class="value">{{nickname}}</span>
      </div>
      <div class="item">
        <span class="title">手机号</span>
        <span class="goto">&gt;</span>
        <span class="value">{{mobile}}</span>
      </div>
      <div class="item">
        <span class="title">登陆密码</span>
        <span class="value"></span>
        <span class="goto">&gt;</span>
      </div>
      <div class="item">
        <span class="title">交易密码</span>
        <span class="value"></span>
        <span class="goto">&gt;</span>
      </div>
      <div class="item">
        <span class="title">地址管理</span>
        <span class="value"></span>
        <span class="goto">&gt;</span>
      </div>
      <div class="item">
        <span class="title">设备管理</span>
        <span class="value"></span>
        <span class="goto">&gt;</span>
      </div>
      <div class="item logout">
        <img @click="change_account" src="../static/images/change_account.png" alt="">
        <p @click="logout">退出账号</p>
      </div>
    </div>
	</div>
	<script>
	apiready = function(){
		init();
		new Vue({
			el:"#app",
			data(){
				return {
          nickname: '',
          mobile: '',
          avatar:"../static/images/avatar.png",
					prev:{name:"",url:"",params:{}},
					current:{name:"setting",url:"setting.html",params:{}},
				}
			},
      created(){
        this.get_user_info();
        this.change_avatar();
      },
			methods:{
        get_user_info(){
          var token = this.game.get('access_token') || this.game.fget('access_token');
          // 获取当前登录用户基本信息
          this.axios.post('',{
            'jsonrpc': '2.0',
            'id': this.uuid(),
            'method': 'User.info',
            'params': {}
          },{
            headers:{
              Authorization: 'jwt' + token,
            }
          }).then(response=>{
            var res = response.data.result;
            if (parseInt(res.errno) === 1000) {
              this.nickname = res.nickname;
              this.avatar = `${this.settings.avatar_url}?sign=${res.avatar}&token=${token}`;
              this.mobile = res.mobile;
            }
          })
        },
        change_avatar(){
          api.addEventListener({
              name: 'change_avatar'
          }, (ret, err)=>{
              if( ret ){
                var token = this.game.get('access_token') || this.game.fget('access_token');
                this.avatar = `${this.settings.avatar_url}?sign=${ret.value.avatar}&token=${token}`;
              }
          });

        },
        goto_home(){
          // this.game.outFrame("setting");
          this.game.goFrame("user","user.html",this.current);
        },
        change_account(){
          // 切换账号
          this.game.goFrame("login","login.html", this.current);
        },
        logout(){
          // 退出账号
          api.actionSheet({
              title: '您确认要退出当前登录吗?',
              cancelTitle: '取消',
              destructiveTitle: '退出登录'
          }, (ret, err)=>{
              if( ret ){
                  this.game.print(ret);  // 取消为2
                   if(ret.buttonIndex==1){
                     this.game.save({"access_token":"","refresh_token":""});
                     this.game.fremove(["access_token","refresh_token"]);
                     this.game.outWin("user");
                   }
              }
          });
        },
        update_avatar_frame(){
          // 显示修改头像的页面frame
          this.game.goFrame('avatar','avatar.html', this.current, null,{
            type:'push',  // 动画类型(详见动画类型常量)
            subType:'from_top',  // 动画子类型(详见动画子类型常量)
            duration:300  // 动画过渡时间,默认300毫秒
          })
        }

        }
		});
	}
	</script>
</body>
</html>

服务端提供用户基本信息APi接口,application/apps/users/views.py,代码:


from application import jsonrpc,db
from .marshmallow import MobileSchema,UserSchema
from marshmallow import ValidationError
from message import ErrorMessage as Message
from status import APIStatus as status

@jsonrpc.method("User.mobile")
def mobile(mobile):
    """验证手机号码是否已经注册"""
    ms = MobileSchema()
    try:
        ms.load({"mobile":mobile})
        ret = {"errno":status.CODE_OK, "errmsg":Message.ok}
    except ValidationError as e:
        ret = {"errno":status.CODE_VALIDATE_ERROR, "errmsg": e.messages["mobile"][0]}
    return ret

@jsonrpc.method("User.register")
def register(mobile,password,password2, sms_code):
    """用户信息注册"""

    try:
        ms = MobileSchema()
        ms.load({"mobile": mobile})

        us = UserSchema()
        user = us.load({
            "mobile":mobile,
            "password":password,
            "password2":password2,
            "sms_code": sms_code
        })
        data = {"errno": status.CODE_OK,"errmsg":us.dump(user)}
    except ValidationError as e:
        data = {"errno": status.CODE_VALIDATE_ERROR,"errmsg":e.messages}
    return data

from flask_jwt_extended import create_access_token,create_refresh_token,jwt_required,get_jwt_identity,jwt_refresh_token_required
from flask import jsonify,json
from sqlalchemy import or_
from .models import User
from message import ErrorMessage as message
from status import APIStatus as status
from flask import current_app, request
from urllib.parse import urlencode
from urllib.request import urlopen

@jsonrpc.method("User.login")
def login(ticket, randstr, account, password):
    """根据用户登录信息生成token"""
    # 校验防水墙验证码
    params = {
        'aid': current_app.config.get('CAPTCHA_APP_ID'),
        'AppSecretKey': current_app.config.get('CAPTCHA_APP_SECRET_KEY'),
        'Ticket': ticket,
        'Randstr': randstr,
        'UserIP': request.remote_addr
    }
    # 把字典数据转换成地址栏的查询字符串格式
    # aid=xxx&AppSecretKey=xxx&xxxxx
    params = urlencode(params)
    url = current_app.config.get('CAPTCHA_GATEWAY')
    # 发送http的get请求
    f = urlopen('%s?%s' % (url, params))
    # https://ssl.captcha.qq.com/ticket/verify?aid=xxx&AppSecretKey=xxx&xxxxx

    content = f.read()
    res = json.loads(content)
    print(res)

    if int(res.get('response')) != 1:
        # 验证失败
        return {'errno': status.CODE_CAPTCHA_ERROR, 'errmsg': message.captcaht_no_match}

    # 1. 根据账户信息和密码获取用户
    if len(account) < 1:
        return {"errno":status.CODE_NO_ACCOUNT,"errmsg":message.account_no_data}
    user = User.query.filter(or_(
        User.mobile == account,
        User.email == account,
        User.name == account
    )).first()

    if user is None:
        return {"errno": status.CODE_NO_USER,"errmsg":message.user_not_exists}

    # 验证密码
    if not user.check_password(password):
        return {"errno": status.CODE_PASSWORD_ERROR, "errmsg":message.password_error}

    # 2. 生成jwt token
    access_token = create_access_token(identity=user.id)
    refresh_token = create_refresh_token(identity=user.id)

    return {
        'errno': status.CODE_OK,
        'errmsg': message.ok,
        'id': user.id,
        'nickname': user.nickname if user.nickname else account,
        "access_token": access_token,
        "refresh_token": refresh_token
    }

from .marshmallow import UserInfoSchema
@jsonrpc.method("User.info")
@jwt_required # 验证jwt
def info():
    """获取用户信息"""
    current_user_id = get_jwt_identity()  # get_jwt_identity 用于获取载荷中的数据
    user = User.query.get(current_user_id)
    if user is None:
        return {
            'errno': status.CODE_NO_USER,
            'errmsg': message.user_not_exists,
        }
    uis = UserInfoSchema()
    data = uis.dump(user)

    return {
        'errno': status.CODE_OK,
        'errmsg': message.ok,
        **data
    }

@jsonrpc.method("User.refresh")
@jwt_refresh_token_required
def refresh():
    """重新获取新的认证令牌token"""
    current_user = get_jwt_identity()
    # 重新生成token
    access_token = create_access_token(identity=current_user)
    return access_token

import base64, uuid, os
@jsonrpc.method('User.avatar.update')
@jwt_required  # 验证jwt
def update_avatar(avatar):
    """更新用户头像"""
    # 1.接收客户端上传的头像信息
    ext = avatar[avatar.find('/') + 1: avatar.find(';')]  # 资源格式
    b64_avatar = avatar[avatar.find(',') + 1:]
    b64_image = base64.b64decode(b64_avatar)
    filename = uuid.uuid4()
    static_path = os.path.join(current_app.BASE_DIR, current_app.config['STATIC_DIR'])
    with open('%s/%s.%s' % (static_path, filename, ext), 'wb') as f:
        f.write(b64_image)
        
    current_user_id = get_jwt_identity()
    user = User.query.get(current_user_id)
    if user is None:
        return {
            'errno': status.CODE_NO_USER,
            'errmsg': message.user_not_exists,
        }
    user.avatar = '%s.%s' % (filename, ext)
    db.session.commit()
    return {
        'errno': status.CODE_OK,
        'errmsg': message.avatar_save_success,
        'avatar': '%s.%s' % (filename, ext)
    }

from flask import make_response, request

@jwt_required  # 验证jwt
def avatar():
    """获取头像信息"""
    avatar = request.args.get('sign')
    ext = avatar[avatar.find('.') + 1:]
    filename = avatar[:avatar.find('.')]
    static_path = os.path.join(current_app.BASE_DIR, current_app.config['STATIC_DIR'])
    with open('%s/%s.%s' % (static_path, filename, ext), 'rb') as f:
        content = f.read()
    response = make_response(content)
    response.headers['Content-Type'] = 'image/%s' % ext
    return response

marshmallow.py,代码:

...
from marshmallow import post_dump

class UserInfoSchema(SQLAlchemyAutoSchema):
    id = auto_field()
    mobile = auto_field()
    nickname = auto_field()
    avatar = auto_field()
    
    class Meta:
        model = User
        include_fk = True
        include_relationships = True
        fields = ['id', 'mobile', 'nickname', 'avatar']
        sql_session = db.session
        
    @post_dump()
    def mobile_format(self, data, **kwargs):
        data['mobile'] = data['mobile'][:3] + '****' + data['mobile'][-4:]
        return data

执行数据库迁移

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值