本文介绍pywebview+vue实现一个系统的登录页面,效果图如下:
在这里插入图片描述
在这里插入图片描述

一、python代码

创建test_pywebview_flask.py文件

import webview
from flask import Flask, render_template, jsonify, request
import json
from functools import wraps

STATIC_FOLDER = 'static-flask'
app = Flask(__name__, template_folder=STATIC_FOLDER, static_folder=STATIC_FOLDER)


def verify_token(function):
    @wraps(function)
    def wrapper(*args, **kwargs):
        data = json.loads(request.data)
        token = data.get('token')
        if token == webview.token:
            return function(*args, **kwargs)
        else:
            raise Exception('Authentication error')

    return wrapper


@app.route('/')
def index():
    return render_template('index.html', error='', token=webview.token)


@app.route('/login', methods=['POST'])
def login():
    print(request.data)
    data = json.loads(request.data)
    user = data.get('username')
    pwd = data.get('password')

    if user != 'test' or pwd != 'test':
        print({'code': '4013',  'msg': '用户名或密码错误'}, jsonify({'code': '4013',  'msg': '用户名或密码错误'}))
        return jsonify({'code': '4013',  'msg': '用户名或密码错误'})

    groups = {"首页": [], "业务菜单": ["3D模型", "画图展示", "业务3"], "系统设置": ["用户管理", "系统日志"]}
    roles = {"首页": ["读"], "3D模型": ["读", "写"], "业务2": ["读", "写"], "业务3": ["读", "写"],
             "用户管理": ["读", "写"], "系统日志": ["读", "写"]}

    return jsonify({'code': '0', 'data': {'groups': groups, 'roles': roles}, 'msg': 'ok'})


@app.route('/get_usr_info', methods=['GET'])
@verify_token
def get_usr_info():
    return jsonify({'code': '0', 'data': []})


def on_closed():
    print('pywebview window is closed')


def on_closing():
    print('pywebview window is closing')


def on_shown():
    print('pywebview window shown')


def on_loaded():
    print('DOM is ready')


if __name__ == '__main__':
    chinese = {
        'global.quitConfirmation': u'确定关闭?',
    }

    window = webview.create_window(
        title='pywebview+flask+vue实现系统登录',
        url=app,
        width=900,
        height=620,
        resizable=True,  # 固定窗口大小
        text_select=False,  # 禁止选择文字内容
        confirm_close=True,  # 关闭时提示
        min_size=(900, 620)
    )

    window.closed += on_closed
    window.closing += on_closing
    window.shown += on_shown
    window.loaded += on_loaded

    webview.start(localization=chinese, debug=True)

代码里面有两个注意的地方:

  • html编译的代码需要放到static-flask目录,test_pywebview.py与static-flask在同一级目录
    在这里插入图片描述

  • create_window指定url参数为Flask类对象,html和Flask通过ajax交互方式通讯

二、html代码

在main.js中定义doAjax函数与Flask交互

Vue.prototype.$getHttpRequestObject = function()
{
	// Define and initialize as false
	var xmlHttpRequst = false;

	// Mozilla/Safari/Non-IE
	if (window.XMLHttpRequest)
	{
		xmlHttpRequst = new XMLHttpRequest();
	}
	// IE
	else if (window.ActiveXObject)
	{
		xmlHttpRequst = new ActiveXObject("Microsoft.XMLHTTP");
	}
	return xmlHttpRequst;
}

Vue.prototype.$doAjax = function(url, method, responseHandler, data)
{
	// Set the variables
	url = url || "";
	method = method || "GET";
	let isAsync = true;
	data = data || {};
	data.token = window.token;

	if(url == "") {
		alert("URL can not be null/blank");
		return false;
	}
	var xmlHttpRequest = this.$getHttpRequestObject();

	// If AJAX supported
	if(xmlHttpRequest != false) {
		xmlHttpRequest.open(method, url, isAsync);
		// Set request header (optional if GET method is used)
		if(method == "POST")  {
			xmlHttpRequest.setRequestHeader("Content-Type", "application/json");
		}
		// Assign (or define) response-handler/callback when ReadyState is changed.
		xmlHttpRequest.onreadystatechange = responseHandler;
		// Send data
		xmlHttpRequest.send(JSON.stringify(data));
	}
	else
	{
		alert("Please use browser with Ajax support.!");
	}
}

创建login.vue文件,调用doAjax与Flask交互

<template>
	<div class="login-container">
		<el-col :span="8" :offset="8" class="login-panel">
			<p class="login-title">Vue前端页面</p>
			<el-form ref="form" :model="form" label-width="80px">
				<el-form-item label="账号">
					<el-input v-model="form.username"><i slot="prefix" class="icon_username"></i></el-input>
				</el-form-item>
				<el-form-item label="密码">
					<el-input type="password" v-model="form.password"><i slot="prefix" class="icon_password"></i></el-input>
				</el-form-item>
				<el-form-item>
					<el-checkbox v-model="form.record">记住密码</el-checkbox>
					<label class="" v-on:click="showMessage('请联系公司的管理帮忙重置密码!');">忘记密码?</label>
				</el-form-item>
				<el-form-item>
					<el-button class="btn-login" @click="login">登录</el-button>
				</el-form-item>
			</el-form>
		</el-col>
	</div>
</template>

<script>
	export default {
		name: 'login',
		props: {
			msg: String
		},
		components: {

		},
		data() {
			return {
				form: {
					username: '',
					password: '',
					record: false
				}
			}
		},
		mounted: function() {
			this.form.record = localStorage.getItem('record') == 'true' ? true : false;
			if (this.form.record) {
				this.form.username = localStorage.getItem('username');
			}
			this.autoLogin();
		},
		methods: {
			async login() {
				if (this.form.username == "" || this.form.password == "") {
					this.$message({
						message: '请输入用户名和密码',
						type: 'warning'
					});
				} else {
					let argc = {
						'username': this.form.username,
						'password': this.form.password
					};
					
					this.$doAjax("/login", "POST", (response)=>{
						if (response.currentTarget.readyState == 4) {// readyState: 2(HEADERS_RECEIVED); 3(LOADING); 4(DONE)
							console.log('-------------', response.currentTarget.readyState, response.currentTarget);
							let data = JSON.parse(response.currentTarget.responseText);	
							if (data.code == '0') {
								localStorage.setItem('record', this.form.record);
								localStorage.setItem('username', this.form.username);
								this.$store.commit('setData', {
									'access_token': this.form.username,
									'userInfo': this.form.username,
									'groups': data.data.groups,
									'roles': data.data.roles
								});
								this.$router.push('/home');
								this.form.password = '';
							} else {
								this.$message({
									message: data.msg,
									type: 'warning'
								});
							}
						}
					}, argc);
				}
			}
	}

</script>

<style lang="scss" scoped>
	$loginColor: #37637e;

	.login-container {
		position: fixed;
		width: 100%;
		height: 100%;
		top: 0;
		left: 0;
		//background: url("../assets/images/brd-img.png") no-repeat center;
		background-size: 100%;

		.el-form {
			padding-right: 40px;

			.el-form-item__content {
				.yzmcode {
					width: 235px;
				}

				.el-button {
					display: block;
					width: 100%;
				}
			}
		}

		.icon_username {
			//background:url("../assets/images/icon-yhm.png") no-repeat;
		}

		.icon_password {
			//background:url("../assets/images/icon-mm.png") no-repeat;
		}

		.icon_password,
		.icon_yzm,
		.icon_username {
			position: absolute;
			width: 13px;
			height: 13px;
			top: 14px;
		}

		.login-panel {
			position: absolute;
			width: 25%;
			max-width: 480px;
			min-width: 425px;
			height: 515px;
			background-color: #dbefff;
			border-radius: 10px;
			top: 0;
			bottom: 0;
			right: 37%;
			margin: auto;

			.login-title {
				font-family: MicrosoftYaHei;
				font-size: 24px;
				color: $loginColor;
				text-align: center;
				margin: 40px 0 30px;
			}

			.yzmContainer {
				position: absolute;
				height: 40px;
				width: 100px;
				right: 20px;
				top: 0;
				cursor: pointer;
			}

			.form-group {
				width: 350px;
				margin: 0 auto 10px;
				height: 38px;
				line-height: 38px;

				.form-control {
					border-radius: 5px;
					padding-left: 38px;
					height: 38px;
					line-height: 38px;
					font-size: 16px;
				}

				#username {
					//background: url("../assets/images/icon-yonghu.png") no-repeat 10px center;
				}

				#password {
					//background: url("../assets/images/icon-mima.png") no-repeat 10px center;
				}
			}

			.btn-login {
				width: 350px;
				height: 38px;
				line-height: 38px;
				text-align: center;
				border-radius: 20px;
				background-color: #b9ba5f;
				padding: 0;
				margin: 30px auto 15px;
				cursor: pointer;
			}

			.form-check {
				width: 350px;
				margin: 0 auto;

				label {
					cursor: pointer;

					.text {
						margin: 0 0 0 5px;
					}

					font-size: 16px;
					color: #999;

					&:hover {
						color: $loginColor;
					}
				}

				.form-check-input {
					width: 20px;
					height: 20px;
					border: solid 1px $loginColor;
					top: -1px;
				}
			}
		}
	}
</style>

通过this.$doAjax("/login", “POST”, (response)=>{}, argc);来调用Flask接口

this.$doAjax("/login", "POST", (response)=>{
	if (response.currentTarget.readyState == 4) {
		//处理Flask返回的数据
	}
}, argc);

三、程序打包

创建打包脚本和test_pywebview_flask.py放同一目录,打包脚本内容:

import os

def CreateExe(filename: str, source: str, console: bool=True):
    cmd = 'venv\Scripts\python.exe -m pip install -i https://pypi.tuna.tsinghua.edu.cn/simple/ pyinstaller'
    # print(cmd)
    print('{0}, result={1}'.format(cmd, os.system(cmd)))
    cmd = 'venv\Scripts\pyinstaller.exe -F -i {0} --add-data "static;{1}" {2} {3}'.\
        format('static/favicon.ico', source, filename, '--noconsole' if console else '')  # 去掉--noconsole就可以生成带dos窗口的exe,可以查看日志信息
    # print(cmd)
    print('{0}, result={1}'.format(cmd, os.system(cmd)))

if __name__ == '__main__':
	CreateExe('test_pywebview_flask.py', 'static-flask')

打包完成后会在当前目录生成dist目录,把dist目录下的exe文件和资源文件目录复制到一个release文件目录下就完成打包。

Logo

前往低代码交流专区

更多推荐