魔坊APP项目-16-种植园、websocket协议、服务端基于socket提供服务(基于房间管理分发信息)、种植园页面展示

种植园

我们需要完成的种植园,是一个互动频繁,并且要求有一定即时性的模块,所以如果继续基于http协议开发,那么需要通过ajax发送大量http请求,同时因为http本身属于单向通讯,所以服务端无法主动发送信息提供给客户端。所以对于客户端使用来说,非常不友好,所以我们需要基于socket通讯来完成这个模块的开发。当然,如果我们服务端基于socket实现tcp/ip通讯的同时,那么客户端必须也要使用websocket来实现tcp/ip通讯才能正常运作。

一、websocket协议

文档:https://tools.ietf.org/html/rfc6455

一直以来,HTTP是无状态、单向通信的网络协议,即客户端请求一次,服务器回复一次,默认情况下,只允许浏览器向服务器发出请求后,服务器才能返回相应的数据。如果想让服务器消息及时下发到客户端,需要采用类似于轮询的机制,大部分情况就是客户端通过定时器使用ajax频繁地向服务器发出请求。这样的做法效率很低,而且HTTP数据包头本身的字节量较大,浪费了大量带宽和服务器资源。

为了提高效率,HTML5推出了WebSocket技术。

WebScoket是一种让客户端和服务器之间能进行全双工通信(full-duplex)的技术。它是HTML最新标准HTML5的一个协议规范,本质上是个基于TCP的协议,它通过HTTP/HTTPS协议发送一条特殊的请求进行握手后创建了一个TCP连接,此后浏览器/客户端和服务器之间便可随时随地以通过此连接来进行双向实时通信,且交换的数据包头信息量很小。

同时为了方便使用,HTML5提供了非常简单的操作就可以让前端开发者直接实现socket通讯,开发者只需要在支持WebSocket的浏览器中,创建Socket之后,通过onopen、onmessage、onclose、onerror四个事件的实现即可处理Socket的响应。

注意:websocket是HTML5技术的一部分,但是websocket并非只能在浏览器或者HTML文档中才能使用,事实上在python或者C++等语言中只要能实现websocket协议报文,均可使用。

客户端报文:

GET /mofang/websocket HTTP/1.1
Host: 127.0.0.1
Origin: http://127.0.0.1:5000
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==      # Sec-WebSocket-Key 是随机生成的
Sec-WebSocket-Version: 13

服务端报文:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk= # 结合客户端提供的Sec-WebSocket-Key基于固定算法计算出来的
Sec-WebSocket-Protocol: chat

WebSocket与Socket的关系

他们两的关系就像Java和JavaScript,并非完全没有关系,只能说有点渊源。
 
Socket严格来说,其实并不是一个协议,而是为了方便开发者使用TCP或UDP协议而对TCP/IP协议进行封装出来的一组接口,是位于应用层和传输控制层之间的接口。通过Socket接口,我们可以更简单,更方便的使用TCP/IP协议。
 
WebSocket是实现了浏览器与服务器的全双工通信协议,一个模拟Socket的应用层协议。

二、服务端基于socket提供服务

在python中实现socket服务端的方式有非常多,一种最常用的有python-socketio,而我们现在使用的flask框架也有一个基于python-socket模块进行了封装的flask-socketio模块.

官方文档:https://flask-socketio.readthedocs.io/en/latest/

注意:
 
因为目前还有会存在一小部分的设备或者应用是不支持websocket的.所以为了保证功能的可用性,我们使用socektio,但是由此带来了2个问题,必须要注意的:
 

  1. python服务端使用基于socketio进行通信服务,则另一端必须也是基于socetio来进行对接通信,否则无法进行通信
  2. socketio还有一个版本对应的问题, 版本不对应则无法通信.回报版本错误.
    如果使用了javascript io 1.x或者2.x版本,则python-socketio或者flask-socketio的版本必须是4.x
    如果使用了javascriptio 3.x版本,则python-socketio或者flask-socketio的版本必须是5.x.

我们当前使用的flask-socketio版本是5.x,所以javasctipt的socketio版本就必须是3.x.

终端下执行命令,安装:

pip install flask-socketio
pip install gevent-websocket

模块初始化,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 faker import Faker
from flask_pymongo import PyMongo
from flask_qrcode import QRcode
from flask_socketio import SocketIO

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()

# mongoDB
mongo = PyMongo()

# qrcode
QRCode = QRcode()

# socketio
socketio = SocketIO()

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)
    mongo.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'))

    # qrcode初始化配置
    QRCode.init_app(app)
    
    # socketio
    socketio.init_app(app, cors_allowed_origins=app.config['CORS_ALLOWED_ORIGINS'], async_mode=app.config['ASYNC_MODE'], debug=app.config['DEBUG'])
    # 改写runserver命令
    if sys.argv[1] == 'runserver':
        manager.add_command('run', socketio.run(app, host=app.config['HOST'], port=app.config['PORT']))
    
    # 注册自定义命令
    load_command(manager)

    return manager

配置文件,application/settings/dev.py,代码:

    # socketio
    CORS_ALLOWED_ORIGINS = '*'
    ASYNC_MODE = None
    HOST = '0.0.0.0'
    PORT = 5000

application/utils/__init__.py,在加载蓝图的过程中,自动加载socket服务端的api,代码:

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
        
        # 加载蓝图内部的socket接口
        try:
            import_module(blueprint_path + '.socket')
        except:
            pass

        # 注册蓝图对象到app应用对象中,  url_prefix 蓝图的路由前缀
        app.register_blueprint(blueprint, url_prefix=url_prefix)

因为我们是基于python-socketio模块提供的服务端,所以客户端必须基于socketIO.js才能与其进行通信,所以客户端引入socketio.js。
socket.io.js的官方文档: https://socket.io/docs/v3
socket.io.js的github: https://github.com/socketio/socket.io/releases
我们可以新建一个orchard.html作为将来种植园模块的主页面,并在这个页面中使用socketio和服务端的flask-socketIO进行通信。
html/orchard代码:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title></title>
    <script type="text/javascript" src="../static/js/socket.io.js"></script>
</head>
<body>

<script>
  // 命名空间
  namespace = '/mofang';
  var socket = io.connect('ws://192.168.20.180:5000' + namespace, {transports:['websocket']});
  socket.on('connect', function(){
    console.log('客户端连接socket服务端');
  });
  
</script>
</body>
</html>

修改html/login.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/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>
    <ul>
      <li><img class="module1" @click='go_orchard' src="../static/images/image1.png"></li>
      <li><img class="module2" @click="go_home" 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(){
    init();
		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(){
        this.app_listener();
        this.check_user_login();
      },
			methods:{
        app_listener(){
          // 使用appintenr监听并使用appParam接收URLScheme的参数
          // 收集操作保存起来,并跳转到注册页面.
          // 注册frame中, 用户注册成功以后,记录邀请信息.
          api.addEventListener({
              name: 'appintent'  // 当前事件监听必须是唯一的,整个APP中只能编写一次,否则冲突导致监听无效
          }, (ret, err)=>{
              var appParam = ret.appParam;
              this.game.print(typeof appParam);  // {"uid":"15"}
              // 保存URLScheme参数到本地
              this.game.fsave(appParam);
              // 跳转到注册页面
              this.game.goWin('user', 'register.html', this.current);

          });

        },
        check_user_login(){
          let token = this.game.get('access_token') || this.game.fget('access_token');
          this.game.checkout(this, token, (new_access_token)=>{
            if(new_access_token.errno == 1005){
              this.game.save({'access_token': ''});
              this.game.fremove('access_token');
            }
          });
        },
        go_home(){
          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);
          }
        },
        go_orchard(){
          if(this.game.get('access_token') || this.game.fget('access_token')){
            this.game.goWin('orchard','orchard.html', this.current);
          }else {
            this.game.goWin('user','login.html', this.current);
          }
        }
			}
		})
	}
	</script>
</body>
</html>

服务端创建并注册蓝图目录orchard,终端命令如下:

cd application/apps/
python ../../manage.py blue -n=orchard

application/urls.py,代码:

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

applicaion/settings/dev.py,代码:

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

1.创建socket连接

在蓝图下面创建socket.py文件,并提供连接接口, orchard/socket.py

from application import socketio
from flask import request

@socketio.on('connect', namespace='/mofang')
def user_connect():
    # request.sid socketIO基于客户端生成的唯一会话ID
    print('用户%s连接过来了!' % request.sid)

@socketio.on('disconnect', namespace='/mofang')
def user_disconnect():
    print('用户%s退出了种植园' % request.sid)

2.客户端vue结合socketio

html/orchard.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/socket.io.js"></script>
</head>
<body>
	<div class="app orchard" id="app">
    <img class="music" :class="music_play?'music2':''" @click="music_play=!music_play" src="../static/images/player.png">
    <div class="orchard-bg">
			<img src="../static/images/bg2.png">
			<img class="board_bg2" src="../static/images/board_bg2.png">
		</div>
    <img class="back" @click="go_index" src="../static/images/user_back.png" alt="">

	</div>
	<script>
	apiready = function(){
		init();
		new Vue({
			el:"#app",
			data(){
				return {
          music_play:true,
          namespace: '/mofang_orchard',
          token:"",
          socket: null,
          timeout: 0,
					prev:{name:"",url:"",params:{}},
					current:{name:"orchard",url:"orchard.html",params:{}},
				}
			},
      created(){
        this.checkout();

      },
			methods:{
        checkout(){
          var token = this.game.get("access_token") || this.game.fget("access_token");
          this.game.checkout(this,token,(new_access_token)=>{
            this.connect();
          });
        },
        connect(){
          // socket连接
          this.socket = io.connect(this.settings.socket_server + this.namespace, {transports: ['websocket']});
          this.socket.on('connect', ()=>{
              this.game.print("开始连接服务端");
          });
        },
        go_index(){
          this.game.outWin("orchard");
        },
			}
		});
	}
	</script>
</body>
</html>

js/settings.js代码:

function init(){
  if (Game) {
    var game = new Game("../mp3/bg1.mp3");
    Vue.prototype.game = game;
  }
  server_url = 'http://192.168.20.180:5000';
  if(axios){
    // 初始化axios
    axios.defaults.baseURL = server_url+"/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: server_url+"/users/avatar",
    code_url: server_url,
    socket_server: 'ws://192.168.20.180:5000',
    socket_namespace: '/mofang',
  }
}

css样式,main.css,代码:

.app .orchard-bg{
	margin: 0 auto;
	width: 100%;
	max-width: 100rem;
	position: absolute;;
	z-index: -1;
  top: -6rem;
}
.app .orchard-bg .board_bg2{
  position: absolute;
  top: 1rem;
}
.orchard .back{
	position: absolute;
	width: 3.83rem;
	height: 3.89rem;
  z-index: 1;
  top: 2rem;
  left: 2rem;
}
.orchard .music{
  right: 2rem;
}
.orchard .header{
  position: absolute;
  top: 0rem;
  left: 0;
  right: 0;
  margin: auto;
  width: 32rem;
  height: 19.28rem;
}

.orchard .info{
  position: absolute;
  z-index: 1;
  top: 0rem;
  left: 4.4rem;
  width: 8rem;
  height: 9.17rem;
}
.orchard .info .avata{
  width: 8rem;
  height: 8rem;
  position: relative;
}
.orchard .info .avatar_bf{
  position: absolute;
  z-index: 1;
  margin: auto;
  width: 6rem;
  height: 6rem;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
}
.orchard .info .user_avatar{
  position: absolute;
  z-index: 1;
  width: 6rem;
  height: 6rem;
  margin: auto;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  border-radius: 1rem;
}
.orchard .info .avatar_border{
  position: absolute;
  z-index: 1;
  margin: auto;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  width: 7.2rem;
  height: 7.2rem;
}
.orchard .info .user_name{
  position: absolute;
  left: 8rem;
  top: 1rem;
  width: 11rem;
  height: 3rem;
  line-height: 3rem;
  font-size: 1.5rem;
  text-shadow: 1px 1px 1px #aaa;
  border-radius: 3rem;
  background: #ff9900;
  text-align: center;
}

.orchard .wallet{
  position: absolute;
  top: 3.4rem;
  right: 4rem;
  width: 16rem;
  height: 10rem;
}
.orchard .wallet .balance{
  margin-top: 1.4rem;
  float: left;
  margin-right: 1rem;
}
.orchard .wallet .title{
  color: #fff;
  font-size: 1.2rem;
  width: 6.4rem;
  text-align: center;
}
.orchard .wallet .title img{
  width: 1.4rem;
  margin-right: 0.2rem;
  vertical-align: sub;
  height: 1.4rem;
}
.orchard .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;
}
.orchard .header .menu-list{
  position: absolute;
  top: 9rem;
  left: 2rem;
}
.orchard .header .menu-list .menu{
  color: #fff;
  font-size: 1rem;
  float: left;
  width: 4rem;
  height: 4rem;
  text-align: center;
  margin-right: 2rem;
}
.orchard .header .menu-list .menu img{
  width: 3.33rem;
  height: 3.61rem;
  display: block;
  margin: auto;
  margin-bottom: 0.4rem;
}
.orchard .footer{
  position: absolute;
  width: 100%;
  height: 6rem;
  bottom: -2rem;
  background: url("../images/board_bg3.png") no-repeat -1rem 0;
  background-size: 110%;
}
.orchard .footer .menu-list{
  width: 100%;
  height: 4rem;
  display: flex;
  position: absolute;
  top: -1rem;
}
.orchard .footer .menu-list .menu,
.orchard .footer .menu-list .menu-center{
  float: left;
  width: 4.44rem;
  height: 5.2rem;
  font-size: 1.5rem;
  color: #fff;
  line-height: 4.44rem;
  text-align: center;
  background: url("../images/btn5.png") no-repeat 0 0;
  background-size: 100%;
  flex: 1;
  margin-left: 4px;
  margin-right: 4px;
}
.orchard .footer .menu-list .menu-center{
  background: url("../images/btn6.png") no-repeat 0 0;
  background-size: 100%;
  flex: 2;
}

3.基于事件接受信息

①基于未定义事件进行通信

html/orchard.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/socket.io.js"></script>
</head>
<body>
	<div class="app orchard" id="app">
    <img class="music" :class="music_play?'music2':''" @click="music_play=!music_play" src="../static/images/player.png">
    <div class="orchard-bg">
			<img src="../static/images/bg2.png">
			<img class="board_bg2" src="../static/images/board_bg2.png">
		</div>
    <img class="back" @click="go_index" src="../static/images/user_back.png" alt="">

	</div>
	<script>
	apiready = function(){
		init();
		new Vue({
			el:"#app",
			data(){
				return {
          music_play:true,  
          token:"",
          socket: null,
          timeout: 0,
					prev:{name:"",url:"",params:{}},
					current:{name:"orchard",url:"orchard.html",params:{}},
				}
			},
      created(){
        this.checkout();

      },
			methods:{
        checkout(){
          var token = this.game.get("access_token") || this.game.fget("access_token");
          this.game.checkout(this,token,(new_access_token)=>{
            this.connect();
          });
        },
        connect(){
          // socket连接
          this.socket = io.connect(this.settings.socket_server + this.settings.socket_namespace, {transports: ['websocket']});
          this.socket.on('connect', ()=>{
              this.game.print("开始连接服务端");
              this.login();
          });
        },
        login(){
          var id = this.game.fget('id');
          // 通过send方法可以直接发送数据,不需要自定义事件,数据格式是json格式
          this.socket.send({'uid': id});
        },
        go_index(){
          this.game.outWin("orchard");
        },
			}
		});
	}
	</script>
</body>
</html>

application/apps/orchard/socket.py服务端代码:


from application import socketio
from flask import request

# 建立socket通信
@socketio.on('connect', namespace='/mofang')
def user_connect():
    # request.sid socketIO基于客户端生成的唯一会话ID
    print('用户%s连接过来了!' % request.sid)

# 断开socket通信
@socketio.on('disconnect', namespace='/mofang')
def user_disconnect():
    print('用户%s退出了种植园' % request.sid)


# 未定义事件通信,客户端没有指定事件名称
@socketio.on('message', namespace='/mofang')
def user_message(data):
    print('接收到来自%s发送的数据:' % request.sid)
    print(data)
    print(data['uid'])
    """
    用户eVUPnaT_AqIAPlyJAAAB连接过来了!
    接收到来自eVUPnaT_AqIAPlyJAAAB发送的数据:
    {'uid': '51'}
    51
    """
②基于自定义事件进行通信

html/orchard.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/socket.io.js"></script>
</head>
<body>
	<div class="app orchard" id="app">
    <img class="music" :class="music_play?'music2':''" @click="music_play=!music_play" src="../static/images/player.png">
    <div class="orchard-bg">
			<img src="../static/images/bg2.png">
			<img class="board_bg2" src="../static/images/board_bg2.png">
		</div>
    <img class="back" @click="go_index" src="../static/images/user_back.png" alt="">

	</div>
	<script>
	apiready = function(){
		init();
		new Vue({
			el:"#app",
			data(){
				return {
          music_play:true,
          token:"",
          socket: null,
          timeout: 0,
					prev:{name:"",url:"",params:{}},
					current:{name:"orchard",url:"orchard.html",params:{}},
				}
			},
      created(){
        this.checkout();

      },
			methods:{
        checkout(){
          var token = this.game.get("access_token") || this.game.fget("access_token");
          this.game.checkout(this,token,(new_access_token)=>{
            this.connect();
          });
        },
        connect(){
          // socket连接
          this.socket = io.connect(this.settings.socket_server + this.settings.socket_namespace, {transports: ['websocket']});
          this.socket.on('connect', ()=>{
              this.game.print("开始连接服务端");
              this.login();
          });
        },
        login(){
          var id = this.game.fget('id');
          // 通过send方法可以直接发送数据,不需要自定义事件,数据格式是json格式
          // this.socket.send({'uid': id});
          this.socket.emit('login', {'uid': id});
        },
        go_index(){
          this.game.outWin("orchard");
        },
			}
		});
	}
	</script>
</body>
</html>

application/apps/orchard/socket.py服务端代码:


from application import socketio
from flask import request

# 建立socket通信
@socketio.on('connect', namespace='/mofang')
def user_connect():
    # request.sid socketIO基于客户端生成的唯一会话ID
    print('用户%s连接过来了!' % request.sid)

# 断开socket通信
@socketio.on('disconnect', namespace='/mofang')
def user_disconnect():
    print('用户%s退出了种植园' % request.sid)


# 未定义事件通信,客户端没有指定事件名称
@socketio.on('message', namespace='/mofang')
def user_message(data):
    print('接收到来自%s发送的数据:' % request.sid)
    print(data)
    print(data['uid'])
    """
    用户eVUPnaT_AqIAPlyJAAAB连接过来了!
    接收到来自eVUPnaT_AqIAPlyJAAAB发送的数据:
    {'uid': '51'}
    51
    """

# 自定义事件通信
@socketio.on('login', namespace='/mofang')
def user_login(data):
    print('接收到来自客户端%s发送的数据:' % request.sid)
    print(data)
    print(data['uid'])
    """
    用户tdpnmCy-44Bg1CJpAAAD连接过来了!
    接收到来自客户端tdpnmCy-44Bg1CJpAAAD发送的数据:
    {'uid': '51'}
    51
    """

4.服务端响应信息

application/apps/orchard/socket.py


from application import socketio
from flask import request
from application.apps.users.models import User

# 建立socket通信
@socketio.on('connect', namespace='/mofang')
def user_connect():
    # request.sid socketIO基于客户端生成的唯一会话ID
    print('用户%s连接过来了!' % request.sid)
    
    # 主动响应数据给客户端
    length = User.query.count()
    socketio.emit('server_response', {'count': length}, namespace='/mofang')

# 断开socket通信
@socketio.on('disconnect', namespace='/mofang')
def user_disconnect():
    print('用户%s退出了种植园' % request.sid)


# 未定义事件通信,客户端没有指定事件名称
@socketio.on('message', namespace='/mofang')
def user_message(data):
    print('接收到来自%s发送的数据:' % request.sid)
    print(data)
    print(data['uid'])
    """
    用户eVUPnaT_AqIAPlyJAAAB连接过来了!
    接收到来自eVUPnaT_AqIAPlyJAAAB发送的数据:
    {'uid': '51'}
    51
    """

# 自定义事件通信
@socketio.on('login', namespace='/mofang')
def user_login(data):
    print('接收来自客户端%s发送的数据:' % request.sid)
    print(data)
    print(data['uid'])
    """
    用户tdpnmCy-44Bg1CJpAAAD连接过来了!
    接收到来自客户端tdpnmCy-44Bg1CJpAAAD发送的数据:
    {'uid': '51'}
    51
    """

html/orchard.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/socket.io.js"></script>
</head>
<body>
	<div class="app orchard" id="app">
    <img class="music" :class="music_play?'music2':''" @click="music_play=!music_play" src="../static/images/player.png">
    <div class="orchard-bg">
			<img src="../static/images/bg2.png">
			<img class="board_bg2" src="../static/images/board_bg2.png">
		</div>
    <img class="back" @click="go_index" src="../static/images/user_back.png" alt="">

	</div>
	<script>
	apiready = function(){
		init();
		new Vue({
			el:"#app",
			data(){
				return {
          music_play:true,
          token:"",
          socket: null,
          timeout: 0,
					prev:{name:"",url:"",params:{}},
					current:{name:"orchard",url:"orchard.html",params:{}},
				}
			},
      created(){
        this.checkout();

      },
			methods:{
        checkout(){
          var token = this.game.get("access_token") || this.game.fget("access_token");
          this.game.checkout(this,token,(new_access_token)=>{
            this.connect();
          });
        },
        connect(){
          // socket连接
          this.socket = io.connect(this.settings.socket_server + this.settings.socket_namespace, {transports: ['websocket']});
          this.socket.on('connect', ()=>{
              this.game.print("开始连接服务端");
              this.login();
              this.get_count();
          });
        },
        get_count(){
          this.socket.on('server_response', (res)=>{
            this.game.print(res.count);
            alert(`欢迎来到种植园,当前有${res.count}人在忙碌着~`)
          });
        },
        login(){
          var id = this.game.fget('id');
          // 通过send方法可以直接发送数据,不需要自定义事件,数据格式是json格式
          // this.socket.send({'uid': id});
          this.socket.emit('login', {'uid': id});
        },
        go_index(){
          this.game.outWin("orchard");
        },
			}
		});
	}
	</script>
</body>
</html>

5.基于房间管理分发信息

application/apps/orchard/socket.py服务端代码:


from application import socketio
from flask import request
from application.apps.users.models import User
from flask_socketio import join_room, leave_room

# 建立socket通信
@socketio.on('connect', namespace='/mofang')
def user_connect():
    # request.sid socketIO基于客户端生成的唯一会话ID
    print('用户%s连接过来了!' % request.sid)

    # 主动响应数据给客户端
    length = User.query.count()
    socketio.emit('server_response', {'count': length, 'sid': '%S' % request.sid}, namespace='/mofang')

# 断开socket通信
@socketio.on('disconnect', namespace='/mofang')
def user_disconnect():
    print('用户%s退出了种植园' % request.sid)


# 未定义事件通信,客户端没有指定事件名称
@socketio.on('message', namespace='/mofang')
def user_message(data):
    print('接收到来自%s发送的数据:' % request.sid)
    print(data)
    print(data['uid'])
    """
    用户eVUPnaT_AqIAPlyJAAAB连接过来了!
    接收到来自eVUPnaT_AqIAPlyJAAAB发送的数据:
    {'uid': '51'}
    51
    """

# 自定义事件通信
@socketio.on('login', namespace='/mofang')
def user_login(data):
    print('接收来自客户端%s发送的数据:' % request.sid)
    print(data)
    print(data['uid'])
    """
    用户tdpnmCy-44Bg1CJpAAAD连接过来了!
    接收到来自客户端tdpnmCy-44Bg1CJpAAAD发送的数据:
    {'uid': '51'}
    51
    """
    
    # 一般基于用户id分配不同的房间
    room = data['uid']
    join_room(room)
    socketio.emit('login_response', {'data': '登录成功'}, namespace='/mofang', room=room)

html/orchard.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/socket.io.js"></script>
</head>
<body>
	<div class="app orchard" id="app">
    <img class="music" :class="music_play?'music2':''" @click="music_play=!music_play" src="../static/images/player.png">
    <div class="orchard-bg">
			<img src="../static/images/bg2.png">
			<img class="board_bg2" src="../static/images/board_bg2.png">
		</div>
    <img class="back" @click="go_index" src="../static/images/user_back.png" alt="">

	</div>
	<script>
	apiready = function(){
		init();
		new Vue({
			el:"#app",
			data(){
				return {
          music_play:true,
          token:"",
          socket: null,
          timeout: 0,
					prev:{name:"",url:"",params:{}},
					current:{name:"orchard",url:"orchard.html",params:{}},
				}
			},
      created(){
        this.checkout();

      },
			methods:{
        checkout(){
          var token = this.game.get("access_token") || this.game.fget("access_token");
          this.game.checkout(this,token,(new_access_token)=>{
            this.connect();
          });
        },
        connect(){
          // socket连接
          this.socket = io.connect(this.settings.socket_server + this.settings.socket_namespace, {transports: ['websocket']});
          this.socket.on('connect', ()=>{
              this.game.print("开始连接服务端");
              this.login();
              this.get_count();
              this.login_response();
          });
        },
        login_response(){
          this.socket.on('login_response', (res)=>{
            alert(res.data);
          });
        },
        get_count(){
          this.socket.on('server_response', (res)=>{
            this.game.print(res.count);
            alert(`欢迎${res.sid}来到种植园,当前有${res.count}人在忙碌着~`);
          });
        },
        login(){
          var id = this.game.fget('id');
          // 通过send方法可以直接发送数据,不需要自定义事件,数据格式是json格式
          // this.socket.send({'uid': id});
          this.socket.emit('login', {'uid': id});
        },
        go_index(){
          this.game.outWin("orchard");
        },
			}
		});
	}
	</script>
</body>
</html>

6.服务端定时推送数据

application/apps/orchard/socket.py服务端代码:


from application import socketio
from flask import request
from application.apps.users.models import User
from flask_socketio import join_room, leave_room

# 建立socket通信
@socketio.on('connect', namespace='/mofang')
def user_connect():
    # request.sid socketIO基于客户端生成的唯一会话ID
    print('用户%s连接过来了!' % request.sid)

    # 主动响应数据给客户端
    length = User.query.count()
    socketio.emit('server_response', {'count': length, 'sid': '%s' % request.sid}, namespace='/mofang')

# 断开socket通信
@socketio.on('disconnect', namespace='/mofang')
def user_disconnect():
    print('用户%s退出了种植园' % request.sid)


# 未定义事件通信,客户端没有指定事件名称
@socketio.on('message', namespace='/mofang')
def user_message(data):
    print('接收到来自%s发送的数据:' % request.sid)
    print(data)
    print(data['uid'])
    """
    用户eVUPnaT_AqIAPlyJAAAB连接过来了!
    接收到来自eVUPnaT_AqIAPlyJAAAB发送的数据:
    {'uid': '51'}
    51
    """

# 自定义事件通信
@socketio.on('login', namespace='/mofang')
def user_login(data):
    print('接收来自客户端%s发送的数据:' % request.sid)
    print(data)
    print(data['uid'])
    """
    用户tdpnmCy-44Bg1CJpAAAD连接过来了!
    接收到来自客户端tdpnmCy-44Bg1CJpAAAD发送的数据:
    {'uid': '51'}
    51
    """

    # 一般基于用户id分配不同的房间
    room = data['uid']
    join_room(room)
    socketio.emit('login_response', {'data': '登录成功'}, namespace='/mofang', room=room)

# 定时推送数据
from threading import Lock
import random
thread = None
thread_lock = Lock()

@socketio.on('chat', namespace='/mofang')
def chat(data):
    global thread
    with thread_lock:
        if thread is None:
            thread = socketio.start_background_task(target=background_thread)
            
def background_thread(uid):
    while True:
        socketio.sleep(1)
        t = random.randint(1, 100)
        socketio.emit('server_response', {'count': t}, namespace='/mofang')

html/orchard.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/socket.io.js"></script>
</head>
<body>
	<div class="app orchard" id="app">
    <img class="music" :class="music_play?'music2':''" @click="music_play=!music_play" src="../static/images/player.png">
    <div class="orchard-bg">
			<img src="../static/images/bg2.png">
			<img class="board_bg2" src="../static/images/board_bg2.png">
		</div>
    <img class="back" @click="go_index" src="../static/images/user_back.png" alt="">
    <h1 style="position: absolute; top: 20rem;">{{num}}</h1>
	</div>
	<script>
	apiready = function(){
		init();
		new Vue({
			el:"#app",
			data(){
				return {
          music_play:true,
          token:"",
          num: '',
          socket: null,
          timeout: 0,
					prev:{name:"",url:"",params:{}},
					current:{name:"orchard",url:"orchard.html",params:{}},
				}
			},
      created(){
        this.checkout();

      },
			methods:{
        checkout(){
          var token = this.game.get("access_token") || this.game.fget("access_token");
          this.game.checkout(this,token,(new_access_token)=>{
            this.connect();
          });
        },
        connect(){
          // socket连接
          this.socket = io.connect(this.settings.socket_server + this.settings.socket_namespace, {transports: ['websocket']});
          this.socket.on('connect', ()=>{
              this.game.print("开始连接服务端");
              this.login();
              this.get_count();
              this.login_response();
          });
        },
        login_response(){
          this.socket.on('login_response', (res)=>{
            alert(res.data);
          });
        },
        get_count(){
          this.socket.on('server_response', (res)=>{
            this.num = res.count;
            // alert(`欢迎${res.sid}来到种植园,当前有${res.count}人在忙碌着~`);
          });
        },
        login(){
          var id = this.game.fget('id');
          // 通过send方法可以直接发送数据,不需要自定义事件,数据格式是json格式
          // this.socket.send({'uid': id});
          // this.socket.emit('login', {'uid': id});
          this.socket.emit('chat', {'uid': id})
        },
        go_index(){
          this.game.outWin("orchard");
        },
			}
		});
	}
	</script>
</body>
</html>

7.服务端推送广播信息

# 推送广播信息
from flask_socketio import emit
@socketio.on('my_broadcast', namespace='mofang')
def my_broadcast(data):
    emit('broadcast_response', data, broadcast=True)
    socketio.emit('some event', {'data': 42})
    # 只要不声明房间ID,则默认返回给整个命名空间下所有的用户都可以接收

三、种植园页面展示

主框架,html/orchard.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/socket.io.js"></script>
</head>
<body>
	<div class="app orchard" id="app">
    <img class="music" :class="music_play?'music2':''" @click="music_play=!music_play" src="../static/images/player.png">
    <div class="orchard-bg">
			<img src="../static/images/bg2.png">
			<img class="board_bg2" src="../static/images/board_bg2.png">
		</div>
    <img class="back" @click="go_index" src="../static/images/user_back.png" alt="">
    <div class="header">
			<div class="info" @click='go_home'>
				<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="menu-list">
        <div class="menu">
          <img src="../static/images/menu1.png" alt="">
          排行榜
        </div>
        <div class="menu">
          <img src="../static/images/menu2.png" alt="">
          签到有礼
        </div>
        <div class="menu">
          <img src="../static/images/menu3.png" alt="">
          道具商城
        </div>
        <div class="menu">
          <img src="../static/images/menu4.png" alt="">
          邮件中心
        </div>
      </div>
		</div>
    <div class="footer">
      <ul class="menu-list">
        <li class="menu">新手</li>
        <li class="menu">背包</li>
        <li class="menu-center">商店</li>
        <li class="menu">消息</li>
        <li class="menu">好友</li>
      </ul>
    </div>
	</div>
	<script>
	apiready = function(){
		init();
		new Vue({
			el:"#app",
			data(){
				return {
          music_play:true,
          namespace: '/mofang_orchard',
          token:"",
          socket: null,
          timeout: 0,
					prev:{name:"",url:"",params:{}},
					current:{name:"orchard",url:"orchard.html",params:{}},
				}
			},
      created(){
        this.game.goFrame('orchard', 'my_orchard.html', this.current, {
          x: 0,
          y: 180,
          w: 'auto',
          h: 'auto'
        }, null);
        this.checkout();
      },
			methods:{
        checkout(){
          var token = this.game.get("access_token") || this.game.fget("access_token");
          this.game.checkout(this,token,(new_access_token)=>{
            this.connect();
          });
        },
        connect(){
          // socket连接
          this.socket = io.connect(this.settings.socket_server + this.namespace, {transports: ['websocket']});
          this.socket.on('connect', ()=>{
              this.game.print("开始连接服务端");
          });
        },
        go_index(){
          this.game.outWin("orchard");
        },
        go_friends(){
          this.game.goFrame('friends', 'friends.html', this.current);
          this.game.goFrame('friend_list', 'friend_list.html', this.current, {
            x: 0,
            y: 190,
            w: 'auto',
            h: 'auto'
          }, null, true);
        },
        go_home(){
          this.game.goWin('user', 'user.html', this.current);
        }
			}
		});
	}
	</script>
</body>
</html>

我的果园,html/my_orchard.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/socket.io.js"></script>
</head>
<body>
	<div class="app orchard orchard-frame" id="app">
    <div class="background">
      <img class="grassland2" src="../static/images/grassland2.png" alt="">
      <img class="mushroom1" src="../static/images/mushroom1.png" alt="">
      <img class="stake1" src="../static/images/stake1.png" alt="">
      <img class="stake2" src="../static/images/stake2.png" alt="">
    </div>
    <div class="pet-box">
      <div class="pet">
        <img class="pet-item" src="../static/images/pet3.png" alt="">
      </div>
      <div class="pet turned_off">
        <img class="turned_image" src="../static/images/turned_off.png" alt="">
        <p>请购买宠物</p>
      </div>
    </div>
    <div class="tree-list">
      <div class="tree-box">
        <div class="tree">
          <img src="../static/images/tree4.png" alt="">
        </div>
        <div class="tree">
          <img src="../static/images/tree3.png" alt="">
        </div>
        <div class="tree">
          <img src="../static/images/tree4.png" alt="">
        </div>
      </div>
      <div class="tree-box">
        <div class="tree">
          <img src="../static/images/tree3.png" alt="">
        </div>
        <div class="tree">
          <img src="../static/images/tree2.png" alt="">
        </div>
        <div class="tree">
          <img src="../static/images/tree2.png" alt="">
        </div>
      </div>
      <div class="tree-box">
        <div class="tree">
          <img src="../static/images/tree1.png" alt="">
        </div>
        <div class="tree">
          <img src="../static/images/tree0.png" alt="">
        </div>
        <div class="tree">
          <img src="../static/images/tree0.png" alt="">
        </div>
      </div>
    </div>
    <div class="prop-list">
      <div class="prop">
        <img src="../static/images/prop1.png" alt="">
        <span>1</span>
        <p>化肥</p>
      </div>
      <div class="prop">
        <img src="../static/images/prop2.png" alt="">
        <span>0</span>
        <p>修剪</p>
      </div>
      <div class="prop">
        <img src="../static/images/prop3.png" alt="">
        <span>1</span>
        <p>浇水</p>
      </div>
      <div class="prop">
        <img src="../static/images/prop4.png" alt="">
        <span>1</span>
        <p>宠物粮</p>
      </div>
    </div>
    <div class="pet-hp-list">
      <div class="pet-hp">
        <p>宠物1 饱食度</p>
        <div class="hp">
          <div style="width: 85%;" class="process">85%</div>
        </div>
      </div>
      <div class="pet-hp">
        <p>宠物2 饱食度</p>
        <div class="hp">
          <div style="width: 0;" class="process">0%</div>
        </div>
      </div>
    </div>
	</div>
	<script>
	apiready = function(){
		init();
		new Vue({
			el:"#app",
			data(){
				return {
          namespace: '/mofang',
          token:"",
          socket: null,
          timeout: 0,
					prev:{name:"",url:"",params:{}},
					current:{name:"orchard",url:"orchard.html",params:{}},
				}
			},
      created(){

      },
			methods:{

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

css样式,main.css代码:

.orchard-frame .background{
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100rem;
}
.orchard-frame .background .grassland1{
  width: 31.22rem;
  height: 13.53rem;
  position: absolute;
  top: 4rem;
}
.orchard-frame .background .grassland2{
  width: 31.22rem;
  height: 13.53rem;
  position: absolute;
  top: 5rem;
}
.orchard-frame .background .mushroom1{
  width: 4.56rem;
  height: 4.83rem;
  position: absolute;
  right: 1rem;
  top: 11rem;
}
.orchard-frame .background .stake1{
  width: 4.56rem;
  height: 4.83rem;
  position: absolute;
  top: 3rem;
  left: 0rem;
}
.orchard-frame .background .stake2{
  width: 6.31rem;
  height: 4.83rem;
  position: absolute;
  top: 3rem;
  left: 13rem;
}
.orchard-frame .pet-box{
  position: absolute;
  top: -2rem;
  left: 0;
  display: flex;
}
.orchard-frame .pet-box .pet{
  position: relative;
  width: 14.16rem;
  height: 15rem;
  flex: 1;
  margin-left: 1rem;
  margin-right: 1rem;
  background: url("../images/tree1.png") no-repeat 0 -0.5rem;
  background-size: 100%;
}
.orchard-frame .pet-box .turned_off .turned_image{
  width: 5.14rem;
  height: 6.83rem;
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  margin: auto;
}
.orchard-frame .pet-box .turned_off p{
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  margin: auto;
  border: 1px solid #fff;
  border-radius: 1rem;
  width: 8rem;
  height: 3rem;
  line-height: 3rem;
  font-size: 1.5rem;
  word-wrap: break-word;
  padding: 1rem;
  color: #000;
  text-align: center;
  background: rgba(255,255,255,.6);
}
.orchard-frame .pet-box .pet-item{
  width: 10rem;
  height: 10rem;
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  margin: auto;
}
.orchard-frame .tree-list{
  position: absolute;
  top: 9rem;
  width: 100%;
}
.orchard-frame .tree-box{
  margin-left: 3rem;
  margin-right: 3rem;
}
.orchard-frame .tree-box .tree{
  width: 9rem;
  height: 4rem;
  margin-bottom: 2rem;
  float: left;
}
.orchard-frame .tree-box .tree img{
  width: 9rem;
  height: 8rem;
  max-height: 8rem;
}
.orchard-frame .prop-list{
  position: absolute;
  bottom: 6rem;
  width: 100%;
}
.orchard-frame .prop-list .prop{
  float: left;
  margin-left: 1rem;
  width: 3rem;
  position: relative;
}
.orchard-frame .prop-list .prop img{
  width: 2.5rem;
  height: 2.5rem;
  margin: auto;
  display: block;
}
.orchard-frame .prop-list .prop span{
  position: absolute;
  top: -4px;
  right: -4px;
  border-radius: 50%;
  width: 1rem;
  height; 1rem;
  font-size: .8rem;
  color: #fff;
  background-color: #cc0000;
  text-align: center;
  line-height: 1rem;
  padding: 2px;
}
.orchard-frame .prop-list .prop p{
  text-align: center;
  color: #fff;
}
.orchard-frame .pet-hp-list{
  position: absolute;
  right: 0;
  bottom: 8rem;
  width: 11rem;
  height: 4rem;
}
.orchard-frame .pet-hp-list .pet-hp{
  margin-bottom: 5px;
}
.orchard-frame .pet-hp-list .pet-hp p{

}
.orchard-frame .pet-hp-list .pet-hp .hp{
  border: 1px solid #fff;
  border-radius: 5rem;
  width: 10rem;
  padding: 1px;
}
.orchard-frame .pet-hp-list .pet-hp .process{
  font-size: 0.5rem;
  background-color: red;
  color: #fff;
  border-radius: 5rem;
  border-top-right-radius: 0;
  border-bottom-right-radius: 0;
  text-align: center;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值